Issues with converting RGB LED driver code (LPD8806)
DiverBob
Posts: 1,123
in Propeller 2
I'm learning P2 Spin using the Propeller Tool. I set out to update some code written by Don Pomplun back in 2014 from the P1 version to P2. This is specifically for the LPD8806 LED Driver from AdaFruit (https://www.adafruit.com/product/306). I can get the code to compile without errors but no response on my 8 LED string. The code is pretty straightforward but I can't figure out what I'm doing wrong. Any suggestions?
Original P1 code:
Original P1 code:
{
******** Propeller I/O assignments ***********
P0 RGB data
P1 RGB clock
}
CON
_clkmode = xtal1 + pll16x 'Standard clock mode * crystal frequency = 80 MHz
_xinfreq = 5_000_000
numLEDs = 8
LEDdata = 0
LEDclock = 1
PUB startup
dira[LEDdata]~~ ' LED strip data line
outa[LEDdata]~
dira[LEDclock]~~ ' LED strip clock line
outa[LEDclock]~
colorTest
PRI colorTest | i
streamByte(0)
repeat i from 1 to 3*numLEDs ' turn off all LEDs
streamByte( $80 )
'waitcnt( clkfreq/60 + cnt )
repeat ' forever
streamByte(0) ' primes LED strip to receive new data
repeat i from 1 to numLEDs ' magenta
streamByte( $FF ) ' Blue . . . LED addressing order is Blue Red Green (not RGB)
streamByte( $FF ) ' Red
streamByte( $80 ) ' Green
waitcnt( clkfreq/2 + cnt ) ' 1/2 second between color changes
streamByte(0)
repeat i from 1 to numLEDs ' red
streamByte( $80 )
streamByte( $FF )
streamByte( $80 )
waitcnt( clkfreq/2 + cnt )
streamByte(0)
repeat i from 1 to numLEDs ' yellow
streamByte( $80 )
streamByte( $FF )
streamByte( $FF )
waitcnt( clkfreq/2 + cnt )
streamByte(0)
repeat i from 1 to numLEDs ' green
streamByte( $80 )
streamByte( $80 )
streamByte( $FF )
waitcnt( clkfreq/2 + cnt )
streamByte(0)
repeat i from 1 to numLEDs ' cyan
streamByte( $FF )
streamByte( $80)
streamByte( $FF )
waitcnt( clkfreq/2 + cnt )
streamByte(0)
repeat i from 1 to numLEDs ' blue
streamByte( $FF )
streamByte( $80 )
streamByte( $80 ) ' keep green on
waitcnt( clkfreq/2 + cnt )
PRI streamByte(d)
d ><= 8 ' reverse bit order to shift out MSB first
repeat 8
if (d & 1) <> 0
outa[LEDdata]~~
outa[LEDclock]~~ ' high
outa[LEDclock]~ ' low
outa[LEDdata]~
d >>= 1
Attempt at conversion to P2 spin:
{
LPD8806 LED Driver
******** Propeller I/O assignments ***********
P54 RGB data
P52 RGB clock
}
CON
CLK_FREQ = 200_000_000 ' system freq as a constant
_clkfreq = CLK_FREQ ' set system clock
numLEDs = 8
ledData = 54
ledClock = 52
PUB startup()
dirb[ledData]~~ ' LED strip data line
outb[ledData]~
dirb[ledClock]~~ ' LED strip clock line
outb[ledClock]~
colorTest()
PRI colorTest() | i
streamByte(0)
repeat i from 1 to 3*numLEDs ' turn off all LEDs
streamByte( $80 )
waitms(500) ' 1/2 second between color changes
repeat ' forever
streamByte(0) ' primes LED strip to receive new data
repeat i from 1 to numLEDs ' magenta
streamByte( $FF ) ' Blue . . . LED addressing order is Blue Red Green (not RGB)
streamByte( $FF ) ' Red
streamByte( $80 ) ' Green
waitms(500) ' 1/2 second between color changes
streamByte(0)
repeat i from 1 to numLEDs ' red
streamByte( $80 )
streamByte( $FF )
streamByte( $80 )
waitms(500) ' 1/2 second between color changes
streamByte(0)
repeat i from 1 to numLEDs ' yellow
streamByte( $80 )
streamByte( $FF )
streamByte( $FF )
waitms(500) ' 1/2 second between color changes
streamByte(0)
repeat i from 1 to numLEDs ' green
streamByte( $80 )
streamByte( $80 )
streamByte( $FF )
waitms(500) ' 1/2 second between color changes
streamByte(0)
repeat i from 1 to numLEDs ' cyan
streamByte( $FF )
streamByte( $80)
streamByte( $FF )
waitms(500) ' 1/2 second between color changes
streamByte(0)
repeat i from 1 to numLEDs ' blue
streamByte( $FF )
streamByte( $80 )
streamByte( $80 ) ' keep green on
waitms(500) ' 1/2 second between color changes
PRI streamByte(d)
d REV= d ' reverse bit order to shift out MSB first
repeat 8
if (d & 1) <> 0
outb[ledData]~~
outb[ledClock]~~ ' high
outb[ledClock]~ ' low
outb[ledData]~
d >>= 1

Comments
For low-level IO like this, it's best not to do a direct translation. The P2 has built-in IO commands that are in fact faster than the way you're trying to do it.
Give this a go:
pri stream_byte(d) d <<= 24 repeat 8 pinwrite(LED_DATA, (d rol= 1)) pinhigh(LED_CLOCK) pinlow(LED_CLOCK) pinlow(LED_DATA)If you want even faster output, you can use inline PASM2. Try this, too.pri stream_byte(d) | tix tix := (clkfreq / 2_000_000) >> 1 org shl d, #24 rep #7, #8 rcl d, #1 wc drvc #LED_DATA nop drvh #LED_CLOCK waitx tix drvl #LED_CLOCK waitx tix drvl #LED_DATA endBTW... when using the P1, I find it best to use direct assignments which are faster versus the cryptic post-set and clear operators. For you P1 version:
pri stream_byte(d) d <<= 8 repeat 8 outa[LED_DATA] := d <-= 1 outa[LED_CLOCK] := 1 outa[LED_CLOCK] := 0 outa[LED_DATA] := 0This code is faster and easier to understand. Of course, LED_DATA and LED_CLOCK must be set to output mode before using this method.Thanks for the code Jon! I tried the P2 Spin code first along with changing the clock to 20MHz. I'm getting a response finally from the LEDs, it seems to be a rapid flashing of the green and red portions of the RGBs but at least I'm finally getting some output! Interesting that it takes about 8 seconds after downloading before the LEDs start to flash.
I'll try out the PASM2 next. I've been reading up on PASM but still getting my head wrapped around it, the last assembly I did regularly was on a 1802 CMOS processor many years ago, finally getting a need to think at that level again. I've been using a nice driver object in the P1 library to run the LPD8806. That driver is all done using pasm but I need to learn more before I attempt to update that driver. In the meantime I thought I'd find a spin-based driver to convert and learn on.
Good catch, didn't even notice that part. It runs at 20Mhz but not 200Mhz.
pri stream_byte(d) d <<= 24 repeat 8 pinwrite(LED_DATA, (d rol= 1)) pinhigh(LED_CLOCK) waitus(2) pinlow(LED_CLOCK) waitus(2) pinlow(LED_DATA)This should slow the shift clock to a bit under 250kHz (irrespective of your system clock speed). I thought that LED chip could handle 2MHz -- maybe not.This is my first attempt, got it to compile but I'm not getting any output to the LED strip. I know there are a couple of things in the assembly code (using dira for one) that compiles but should be another command instead. I think I got the code right for bringing in the parameters via the ptra register.
Suggestions?
{{ ####################################### # # # Copyright (c) 2012 Fredrik Safstrom # # # # See end of file for terms of use. # # # ####################################### ############################# DESCRIPTION ######################## Re-write of driver for P2 usage. This is a driver for the LED Strip from adafruit.com https://www.adafruit.com/products/306 I took the source code for the Arduino and reversed engineered it to figure out the timing and latching. My first try in Spin didn't go to that well, the performance of Spin was not even close to what was needed. So I took the 74HC595 driver from Dennis Ferron and modified it to shift out the data for the LPD8806. ########################### PIN ASSIGNMENTS ##################### Passed to driver... ########################### REVISIONS ########################### 1.0 Got to start somewhere... 2.0 11/23/2020 Re-write driver for operation under P2 commands }} VAR long led_data 'test data ' Block of parameters to pass to the Assembler program. long par1 ' Address to LED Array long par2 ' Clock pin long par3 ' Data pin long par4 ' Number of LEDs in string long cog pub main() : i '' test new driver code start(1, 3, 8, @led_data) waitms(250) repeat i from 0 to 7 led_data[i] := rgbColor(0, 127, 0) 'green waitms(50) 'clear display repeat i from 0 to 7 led_data[i] := rgbColor(0, 0, 0) waitms(50) PUB start(clock_pin, data_pin, num_LEDs, LED_Address) : okay {{ Start the LPD8806 driver in an available cog }} '' Assign parameters to block par1 := LED_Address par2 := clock_pin par3 := data_pin par4 := num_LEDs '' Stop just in case we have a cog running stop() '' Start new cog cog := coginit(COGEXEC_NEW,@lpd8806_asm, @par1) + 1 PUB stop() {{ Stop the LPD8806 driver and free cog }} if cog cogstop(cog~ - 1) PUB rgbColor(red, green, blue) : rgbValue {{ Convert rgb to LED values}} rgbValue := blue + red << 8 + green << 16 PUB fillColor(color) | i {{ Fill array with a specific color }} repeat i from 0 to par4-1 LONG[par1][i] := color PUB clear() {{ Clear array }} fillColor(0) DAT {{ The LPD8806 driver. Runs continously in a separate cog. }} org 0 lpd8806_asm rdlong temp1, ptra ' First parameter is address to LED values add temp1, #4 ' Move to next parameter mov temp2, temp1 ' Get clock pin mov clkpin, #1 ' Prepare mask shl clkpin, temp2 ' Shift bit into position add temp1, #4 ' Move to next parameter mov temp2, temp1 ' Get data pin mov datpin, #1 ' Prepare mask shl datpin, temp2 ' Shift bit into position add temp1, #4 ' Move to next parameter mov numled, temp1 ' Save number of LEDs on string or dira, clkpin ' Set the direction bits for the pins - P2 equivalent for dira?? or dira, datpin mov temp1, #0 wz ' Set Z flag. main_loop ' Main loop mov ledcnt, numled ' Load LED counter rdlong leddata, ptra ' Get address of LED array led_loop muxnz outa, clkpin ' Set Clock Pin low rdlong shiftout, leddata ' Get LED value shl shiftout, #8 ' Shift into position, we only use lower three bytes or shiftout, led_mask ' Add $80 to all three bytes add leddata, #4 ' Point leddata to next long in array mov bitcnt, #24 ' Shift 24 bits shift_data shl shiftout, #1 wc ' Shift bit from shiftout and store it in carry flag muxnz outa, clkpin ' Set Clock Pin low muxc outa, datpin ' Set Data Pin to the value of the carry flag call #small_del ' Delay in between clock pulses muxz outa, clkpin ' Set Clock Pin high call #small_del ' Delay in between clock pulses djnz bitcnt, #shift_data ' Continue shift the 24 bits djnz ledcnt, #led_loop ' Continue shift data for LEDs on string mov bitcnt, #8 ' Shift 8 bits, these are the 'latch byte' for the driver mov shiftout, #0 ' All zero. shift_latch shl shiftout, #1 wc ' Shift bit from shiftout and store it in carry flag muxnz outa, clkpin ' Set Clock Pin low muxc outa, datpin ' Set Data Pin to the value of the carry flag call #small_del ' Delay in between clock pulses muxz outa, clkpin ' Set Clock Pin high call #small_del ' Delay in between clock pulses djnz bitcnt, #shift_latch ' Do next latch bit mov delcnt, latchdel ' Delay for latch to take place getct delcnt waitx delcnt jmp #main_loop ' And continue on forever small_del mov delcnt, shortdel ' Delay in between clock pulses getct delcnt waitx delcnt small_del_ret ret '' Data... shortdel long 10 ' Delay in between shift pulses. latchdel long 1000 ' Delay in between sending data to strip. led_mask long $80808000 ' Since all values need to be between $80 and $FF, or color with $80. '' Parameters... shiftout res 1 ' Value to shift out. clkpin res 1 ' Clock pin datpin res 1 ' Data pin numled res 1 ' Number of LEDs in string leddata res 1 ' Address of LED data in the array temp1 res 1 ' Temp 1 temp2 res 1 ' Temp 2 bitcnt res 1 ' Bits to shift ledcnt res 1 ' LEDs on string delcnt res 1 ' Delay loop {{ ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ TERMS OF USE: MIT License │ ├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation │ │files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, │ │modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software│ │is furnished to do so, subject to the following conditions: │ │ │ │The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.│ │ │ │THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE │ │WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR │ │COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, │ │ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ }}Maybe should be "long led_data[10]" or so?
Also, in your assembly, you are reading in only the par1 value with the rdlong.
But, you need to read in the value of 3 more parameters with rdlongs...
Just change the assembly parameters from type "res" to type "long".
Then, set them in the Spin code before starting the cog...
While you *can* do a near direct translation of P1 to P2 PASM, the P2 instruction set is larger and has many instructions that simplify things. After looking at your code and the Adafruit driver, this is how I coded the P2 assembly section (compiles, but no hardware to test with). Notice that it's shorter due to the nice instructions in the P2.
dat org 0 lpd8806 setq #5-1 ' get 5 parameters from hub rdlong sclk, ptra drvl sclk ' make outputs low drvl sdo led_main mov t1, nleds ' prep for new colors add t1, #31 shr t1, #5 mov ledbits, #0 .loop call #shift_out ' send 0 for every 32 pixels djnz t1, #.loop mov count, nleds ' set count of leds mov hub, p_buf ' point to start of buffer next_led rdlong ledbits, hub ' read color value add hub, #4 ' point to next or ledbits, ##$80_80_80_00 ' add sync bits to color bytes call #shift_out ' green call #shift_out ' red call #shift_out ' blue djnz count, #next_led jmp #led_main shift_out rep #7, #8 ' shift color bits shl ledbits, #1 wc ' get msb in c drvc sdo ' c --> sdo nop ' let sdo settle drvh sclk ' clock high waitx ctix drvl sclk ' clock low waitx ctix ret ' ------------------------------------------------------------------------------------------------- nleds res 1 ' leds to update p_buf res 1 ' hub address of buffer sdo res 1 ' data pin sclk res 1 ' clock pin ctix res 1 ' ticks in 1/2 sclk period count res 1 ' # of leds to update hub res 1 ' hub address of buffer ledbits res 1 ' color to shift out t1 res 1 ' temp vars t2 res 1 fit 472I've attached my test file (object and test code in one place). If you can verify that this works, I'll break out the object code and get it posted. I'm guessing you already have some LPD8806 strips -- they don't seem particularly popular, and it's hard to find good info about them. The best info came from the comments of the Adafruit driver.
Okay... back to a P1 project that has been kicking my butt for far too long....
JonnyMac, I’ll try out your test file and see what happens. I want to go through each line to really understand what is happening there. I spent a lot of time reading and looking at P2 assembly code, is there any written in-depth descriptions for the P2 assembly code out there like what is available for the P1 assembly yet? So far my best resource has been searching the forum and reading applicable P2 threads.
There's a link at propeller.parallax.com
// This is how data is pushed to the strip. Unfortunately, the company // that makes the chip didnt release the protocol document or you need // to sign an NDA or something stupid like that, but we reverse engineered // this from a strip controller and it seems to work very nicely! void LPD8806::show(void) { uint16_t i, nl3 = numLEDs * 3; // 3 bytes per LED for (i=0; i<nl3; i++ ) { write(pixels[i]); } // Write latch at end of data; latch length varies with number of LEDs writezeros(3 * ((numLEDs + 32-1) / 32)); }The Adafruit driver sends one zero per 32 pixels; this code sends three per 32 pixels. It is odd that there is no protocol spec floating around anywhere.The only thing I know of is the OE object that I used in my P1 code. Not sure what you mean by a instructions spreadsheet? The data sheet from the manufacturer doesn’t give any usable info (most info I ever found was from the Adafruit website) on timing or protocols. The manufacturer was really trying to keep everything proprietary, probably that’s why it never became popular/wide spread.
-- https://docs.google.com/spreadsheets/d/1_vJk-Ad569UMwgXTKTdfJkHYHpc1rZwxB-DcIiAZNdk/edit#gid=0
I misunderstood what Ray was referring to! Yes, I have that spreadsheet up continuously on the screen while studying P2 assembly.
After verifying I had Clk and data on the right pins I ran the code. The results are that only the 8 red LEDs are active continuously. No sign of the green or blue LEDs being active. Interestingly P56-P59 on the P2 Eval board LEDs are pulsing between half and full brightness a little faster than once per second.
I'm going to put in some debugging code into this and see if I can figure out what is happening. I'm running this code using Propeller Tool version 2.3.00 Alpha in case that makes a difference.
lpd8806 setq #5-1 ' get 5 parameters from hub rdlong sclk, ptra drvl sclk ' make outputs low drvl sdoThe setq command followed by rdlong downloads the ptra register which contains the addresses for the 4 parameters being passed to the code. rdlong command specifically puts the first parameter in ptra into variable sclk which is driven low. It then drives sdo low but how is sdo assigned to the second parameter? I see the variable list at the end of the code, are the variables automatically loaded in the order that the variable is listed at the end of the code?The rdlong repeats, while both addresses internally are incremented (ie the addresses of clk and ptra get incremented)
So, reads 5 longs from hub pointed to by ptra, into cog registers starting at clk.
lpd8806 setq #5-1 ' get 5 parameters from hub rdlong nleds, ptra...since the first variable of the group is the number of LEDs. I am at a friend's house for Thanksgiving, but I have my P2 Eval and a logic analyzer; I'm going to have a look and will post an update when I get it working.pub lpd8806_pasm(dpin, cpin, count, p_buf) | tix, npix, work '' Test driver for LPD8806 LEDs '' -- color values packed long, organized as $GG_RR_BB_00 '' * constituent bytes limited $00..$7F tix := (clkfreq / 100_000) >> 2 ' slow for testing org drvl dpin drvl cpin waitx tix mov npix, count show_pix rdlong work, p_buf or work, ##$80_80_80_00 add p_buf, #4 rep #7, #24 shl work, #1 wc drvc dpin nop drvh cpin waitx tix drvl cpin waitx tix djnz npix, #show_pix latch_pix drvl dpin add count, #31 shr count, #2 ' count := count / 32 * 8 rep #4, count drvh cpin waitx tix drvl cpin waitx tix endI will re-visit that other program with the full driver -- we may have this solved!That is fantastic, this version worked right off the bat. Now I need to study the pasm code and understand what it is really doing and how. What logic analyzer are you using?
I really appreciate your help, this is my first real attempt at pasm, still have a lot to learn! Have a great Thanksgiving tomorrow!
Bob
When traveling as I am this week, I use this $13 logic analyzer:
-- https://www.amazon.com/gp/product/B077LSG5P2
It's not fancy, but for simple projects like this it works well.
The software that controls it is:
-- https://sigrok.org/wiki/PulseView
It even has built in protocol analyzers that I've used when developing P2 code for 1-Wire and my SPI object.
There is a set of color constants in this driver.
Thanks for trying this. I checked it with the logic analyzer and it appears to be working properly.
-- Jon