WS281x, SK6812 driver for the Omega2? [Solved]



  • This weekend, I played a bit with the PWM unit in the MT7688 (using devmem from the command line), trying to make sense of the datasheet, which documents the registers and bits, but not the functionality behind.

    The good news is: the PWM as such is very much suited to generate WS281x timing accurately! It's not just a PWM where you can set frequencey/duty cycle, but it can also serialize 64bits into a succession of adjacent "high" and "low" times. That's exactly what is needed for non-standard serial data protocols like the WS281x!

    What I haven't found out yet is how to automatically feed more than 64bits of data into the PWM unit.

    Hours of googling revealed interesting bits: In Android, there is a "mediatek" kernel branch, which has full Linux PWM support for a "MediaTek PWM" with identical register layout as what we have in the MT7688. However, it seems the MT7688 PWM is a stripped down version of that. There's a gap of 4 longwords in the register block in the MT7688 (between PWMx_GDUR and PWMx_SEND_DATA0), where the full "MediatTek PWM" has registers for a so called memory mode. In memory mode, the PWM unit fetches data to be sent from memory. There are also bits 4-6 in PWMx_CON to select modes in the full PWM, which are read-only in the MT7688 version. So the memory mode that looked promising for generating a long stream of bits for WS281x is not available in the MT7688.

    However I find it very unlikely that the MT7688 cannot feed the PWM from memory at all. My current guess is that the memory mode is missing only because there's another mechanism in the MT7688 to feed data into the PWM, probably the generic DMA engine. The info I'm missing there is how the DMA engine would synchronize with the PWMs data demand. The MT7688 DMA has explicit channels for I2S, PCM and SPI, but not for PWM. I haven't actually tried connecting the DMA to the PWM to just look at what would happen, because that's beyond devmem from command line. But it's my plan to code something to test this soon.

    For those who want to play with the PWM in general:

    # make GPIO19 be PWM1 output
    omega2-ctrl gpiomux set pwm1 pwm
    # set PWM1 to output 64bit pattern (STOP_BITPOS=63), 100kHz base clock
    devmem 0x10005000 32 0x7E00
    # set duration for the 1-bits in the pattern (100 base clock cycles = 1mS)
    devmem 0x10005054 32 100
    # set duration for the 0-bits in the pattern (100 base clock cycles = 1mS)
    devmem 0x10005058 32 300
    # set duration for the pause (guard time) after sending all bits before starting again (10mS)
    devmem 0x1000505C 32 1000
    # now set the actual pattern
    devmem 0x10005070 32 0xFFFF00FF
    devmem 0x10005074 32 0x0F0F0000
    # set to repeat pattern forever (PWM1_WAVE_NUM=0)
    devmem 0x10005078 32 0
    # enable PWM 1
    devmem 0x10005000 32 0x2
    

    If you have a scope connected to GPIO19, you'll see a waveform like:
    ____-_-_--__----____-_-_--__----____-_-_--__----____-_-_--__----____
    (without a scope, connecting a speaker will at least allow making funny noise ;-))

    Note: for experimenting I used the slow 100kHz clock option. But the PWM can be run at 40Mhz, giving a resolution of 25nS, which is well precise enough to generate the 350nS and 700nS pulses needed for WS281x.



  • I too would be interested in a native Omega2 LED strip driver and tools. It might be possible to port the FastLED library and write some userland tools that link against it, but for my purposes I think I'm going to stick with using a Particle Photon to drive my LED strips, or get a FadeCandy board to handle the low level LED strip protocols and just control them by speaking Open Pixel Control protocol over USB.

    I have both APA102 and WS2812 lights, so I'm keen on solutions that have implementations for both.



  • @Roland-McIntosh once the DMA/PWM stuff works, I will certainly make a kernel module for WS281x and SK6812 (RGBW). APA102 on the other hand are not nearly as time critical, and should work fine using bitbanged SPI on any two GPIOs of the Omega2 (haven't tried yet though, because I have only a few APA102, but tons of WS281x I'd like to connect...)

    Eventually, my goal is also to have all types unified under a common library/toolset that abstracts the hardware details from the applications using the LEDs. But first the actual bit signal generator needs to be done 😉



  • @luz forgive a newbie question, where did you get devmem from? Ash say it is nit recognized and opkg list does not seem to have it available. Also, where did you get the memory addresses from as the datasheet I found for the MT7688 has very cryptic information.



  • @Jo-Kritzinger As I'm using my own LEDE images most of the time now, I'm not 100% sure the Omega2 standard image is configured the same way. But I think I remember testing stuff with devmem a while ago with the standard firmware. Unfortunately I can't easily test again because I don't have a free Omega2 with standard firmware available right now.

    There are two parts for devmem - first, the kernel support for /dev/mem (CONFIG_KERNEL_DEVMEM=y in LEDE .config), and second the busybox builtin devmem utility (CONFIG_BUSYBOX_CONFIG_DEVMEM=y).

    This thread suggests that at least in current firmware, /dev/mem should be there (check with ls /dev/mem), but maybe devmem utility might be still missing.

    About the addresses: These are 1:1 the addresses as shown in the MT7688 datasheet, see PWM register block on pages 233ff.

    BTW: with ls /sys/devices/platform you can see that palmbus (apparently the peripheral bus in the MT7688) is at 0x10000000, and going deeper with ls /sys/devices/platform/*.palmbus reveals the addresses of the different peripherals - there you find again 0x10005000 as base address of the PWM.



  • @luz thank you for the great info. Unfortunatly it seems devmem is not part of the standard firmware. Once I get to understand this harware a bit better and become more comfortable with Linux I will try to build a image that includes it.



  • @luz @José-Luis-Cánovas Quick question here, if you don't mind, related to accessing the registers directly. As I do not have access to devmem currently and I've just managed to get a cross compiler working (thanks José) I thought I'd use a C pointer to do the same thing as a way of starting to understand the environment/hardware. As I've mentioned previously, my background is in 8031's where you have program memory and data memory and you could implement memory accessed IO and the pointer casts would simply include a prefix to specify the relevant memory space. Pretty straight forward kind of stuff as there was no operating system in the way.
    The little test prog (see below) is how I'd imagine it would be implemented but the result I get from the program is

    1. the printf output, i.e. "GPIO 0-31 REG" - fine
    2. Segmentation fault - ?
      Is this caused by the same issue people are taking about fast_gpio or is this a lack of understanding how the Omega2 memory works? How would one setup a pointer into memory to access/alter the registers? I would have imagine that, since this is a C pointer, the OS is pretty much left out of the loop so why am I getting a fault?

    The code:

    #include <stdio.h>
    #include <stdint.h>
    #include <stdlib.h>

    //Def a pointer to REGISTER with a int cast to pointer
    //from the data sheet it seems the REG's are 32bits long
    #define GPIO0_31_DATA_REG (*(uint32_t*)0x10000620)
    #define GPIO0_31_DIR_REG (*(uint32_t*)0X10000600)

    int main(int argc, char *argv[]){
    printf("GPIO 0-31 REG\r\n");
    printf("GPIO0-31 Direction %x",GPIO0_31_DIR_REG);
    return EXIT_SUCCESS;
    }



  • @Jo-Kritzinger said in WS281x, SK6812 driver for the Omega2?:

    As I do not have access to devmem currently and I've just managed to get a cross compiler working (thanks José) I thought I'd use a C pointer to do the same thing as a way of starting to understand the environment/hardware.

    Unfortunately, that won't work. A system with a memory management unit that does virtual-to-physical mapping and enforces memory access permissions won't allow a user mode program to directly interact with hardware registers.

    Thus /dev/mem, an effectively giant hole blasted through the containment wall of operating system design, breaking all the rules. Opening and/or mapping /dev/mem is asking the kernel to set up a direct virtual-to-physical mapping to the hardware, complete with permission to actually do what otherwise there would be no way to request, nor any permission to accomplish.

    But without /dev/mem, or some other hole, or some more specifically tailored solution like a kernel driver that does particular narrow operations upon legitimate request - without that, all the usual restrictions on user mode apply, and you can't touch the hardware.



  • @Chris-Stratton Arrr, not what I was hoping to hear but sort of expected. Thank you Chris.

    (removed some text here - discovered what I did wrong)

    Where would one being to look for a kernel driver or info on how to write a kernel driver?

    Once again, thanks.



  • Short update for those waiting - my kernel driver for driving WS2812 using the Omega2 hardware PWM already works with a test LED chain of 24 WS2812 🙂
    It's not yet 100% reliable, and the code is a mess, caused by all the experiments I needed to find out how that sparsely documented PWM engine actually works. I need to clean up before publishing it, but I expect to get that done in the next few weeks.



  • Finally - the driver is ready!

    See p44-ledchain in my feed for LEDE on github.

    People testing it with various LED chains welcome, my current test object is the "pixelboard" (of which you find some parts in the same feed already) with 200 WS2813 LEDs.

    0_1489058389340_Pixelboard.jpg



  • Very cool! So I'm planning to order some SK6812 for a small project. But it would be first time for me building and installing a kernel driver on Omega2. Could you make a small howto guide?



  • @Anders-Öster
    If you want to build it yourself, you need to install a LEDE buildroot (see onion docs and maybe my post here).
    Once you have setup that, you can:

    • add the plan44 feed to feeds.conf.default by adding a line
      src-git plan44 https://github.com/plan44/plan44-feed.git;master
    • have LEDE fetch the latest stuff from all feeds with
      ./scripts/feeds update -a
    • "install" (meaning: make ready for being included in compilation) the p44ledchain package with
      ./scripts/feeds install kmod-p44-ledchain
    • enable the package for actually being built in the configuration
      (make menuconfig, then select it under kernel modules -> other modules -> kmod-p44-ledchain)
    • build the package with
      make package/p44-ledchain/compile

    Now you'll have the compiled package in bin/targets/ramips/mt7688/packages/kmod-p44-ledchain_....ipk

    For those that don't want to invest the time (1-2h build time for the initial LEDE make run) and disk space (~15GB), here's a built package for p44-ledchain.

    Sadly, because Onion still hasn't published their complete LEDE build environment (yes, @administrators, my ceterum censeo once again) it is not possible to build a kernel module that actually installs cleanly on the omega firmware. [Update: Luckily it is published by now]

    So, either way (self-built or downloaded from the link above), you need to override kernel version dependency checks when installing

    cd /tmp
    wget http://plan44.ch/downloads/experimental/kmod-p44-ledchain_4.4.61%2B0.9-2_mipsel_24kc.ipk
    opkg install --force-depends kmod-p44-ledchain*
    

    And because of the kernel version mismatch, insmod does not find the module automatically, so you need to specify the full path:

    insmod /lib/modules/4.4.61/p44-ledchain.ko ledchain0=0,200,2
    

    (this is for 200 WS2813 connected to PWM0)

    good luck!



  • @luz

    Thanks for the howto!



  • Hi
    I'm wondering if (and how) it is possible to dim the LED with the p44-ledchain module? (keyword: change of duty cycle)
    Regards
    Laurent



  • @Laurent-Nittler yes, you can dim (and color) every LED separately!

    In fact, the bytes you write to the /dev/ledchainX device correspond to the brightness (duty cycle) of the red, green and blue part of the LEDs (and a separate white LED for SK6812). With this, you can set any color and any brightness.

    Let's assume you have WS2813 connected to PWM0, then

    echo -en '\xFF\xFF\xFF' >/dev/ledchain0
    

    switches the first LED to full white brightness (255=0xFF = full on for all three R,G,B channels. Whereas:

    echo -en '\x7F\x7F\x7F' >/dev/ledchain0
    

    sets all three LEDs to half duty cycle (127=0x7F)

    A somewhat dimmed green (red=off, green=160=0xA0, blue=off) would be:

    echo -en '\x00\xA0\x00' >/dev/ledchain0
    

    and so forth.

    To control more LEDs, just write a longer string to /dev/ledchain0, 3 bytes for every LED in the chain (4 for RGBW LEDs like SK6812).

    Of course, you wouldn't do that with echo except for the first manual tests, but using some script/program that calculates the string to be sent to /dev/ledchain0.



  • Thanks Luz for this explanation.
    I forgot that HSL values could be transformed into the RGB values.

    I used python to control the 8 LED ring (WS2812b) using code like

    with open('/dev/ledchain0','w') as export:
    			export.write(col_string)
    

    where 'col_string' is a a text string containing all the RGB values for the hole ring.
    However sometimes (especially when I have to calculate the RGB values and then compose the string), I get an error 'ValueError: invalid \x escape'

    Maybe this isn't the best way to control the LED with python and you can advise a better one.

    Regards



  • @LaurentN Although I'm not fluent at all in Python, I'd say there's no reason why Python should not work fine controlling LED chains with p44-ledchain.

    @LaurentN said in WS281x, SK6812 driver for the Omega2? [Solved]:

    However sometimes (especially when I have to calculate the RGB values and then compose the string), I get an error 'ValueError: invalid \x escape'

    That sounds like you are trying to use the literal string syntax (the backslash escapes in \xhh form, with hh being a two-digit hex number) to construct your string, but maybe in a way that sometimes outputs only one digit instead of two, which could be the cause of the ValueError?

    Probably there's a more direct way to get numeric byte values into a string or byte array in Python.



  • Re: [WS281x](SK6812 driver for the Omega2? [Solved])

    Hi.
    In the last days I played a bit with Python to get the WS281x working wirth Luz' module.
    As I want to use HLS color schema (it allows to change the luminosity of the color by only one number), I programmed a function HLS to RGB color string:

    def hls2rgb(h,l,s):
    	r, g, b = colorsys.hls_to_rgb(h,l,s)
    	shr = str(hex(int(r * 255.0)))
    	shg = str(hex(int(g * 255.0)))
    	shb = str(hex(int(b * 255.0)))
    	# print ([int(255.0*r), int(255.0*g), int(255.0*b)])
    	if len(shr) == 3:
    		shr = shr[0:2] + str(0) + shr[2]
    	if len(shg) == 3:
    		shg = shg[0:2] + str(0) + shg[2]
    	if len(shb) == 3:
    		shb = shb[0:2] + str(0) + shb[2]
    	col_led_string = shr[2:4].decode('hex') +shg[2:4].decode('hex') + shb[2:4].decode('hex')
    

    By using the following additional function, it allows using a 2 dimensional color matrix which is converted in the corresponding hex color string needed for the module. (It might be useful and easier to use a matrix to handle LED chains instead of a string of hexadigitals)

    def hls2rgb_hexstring(hls_matrix):
    	dim1 = len(hls_matrix)
    	try:
    		dim2t = len(hls_matrix[0])
    	except:
    		dim2t = 0
    	if dim2t == 0:
    		dim2 = dim1
    		dim1 = 1
    	else: 
    		dim2 = dim2t
    	
    	if dim1 == 1:
    		h = hls_matrix[0]/360.0
    		l = hls_matrix[1]/100.0
    		s = hls_matrix[2]/100.0
    		col_led_string = hls2rgb(h,l,s)
    		return col_led_string
    	else:
    		col_led_string=""
    		for i in range(0,dim1):
    			h = hls_matrix[i][0]/360.0
    			l = hls_matrix[i][1]/100.0
    			s = hls_matrix[i][2]/100.0
    			col_led_string = col_led_string + hls2rgb(h,l,s)
    		return col_led_string
    

    To display a color gradient among several LED's (here 40) one has to run this code, which is using both function above:

    color_start = 0
    color_end = 359
    nr_led = 40
    lum = 2
    sat = 100
    colorMatrix = []
    for i in range(color_start,color_end, abs(color_end-color_start)/nr_led):
    	colorMatrix.append([i,lum,sat])
    	#print [i,lum, sat]
    print colorMatrix
    with open('/dev/ledchain0','w') as export:
    	export.write(hls2rgb_hexstring(colorMatrix))
    


  • @luz does it work for the omega 2+ version with kernel 4.4.6?



Looks like your connection to Community was lost, please wait while we try to reconnect.