G35 RGB LED light control in eLua
(Last modified 17 Jul 2013)

Many embedded projects require streams of accurate, short-duration pulses.  I'm not talking about PWM here, but about various communications protocols, such as X10, TV IR remote-control, and G35 RGB LED lights.  In particular, the G35 protocol uses a series of 10 usec pulses to send light-control information through the power cord; you can find an excellent explanation of this protocol here:

But interpreters, such as eLua, are often too slow to generate the needed pulse stream for such protocols.  These tools can support SPI and PWM, where the precise pulse generation is off-loaded to on-chip subsystems, but non-supported protocols, such as G35, are usually out of reach without mods to the code underlying the interpreter.


Mod that code
So, I modified the pio.c code in the eLua source tree to add a new GPIO functionality.  Just for grins, here is a screen-shot from my oscilloscope showing part of a G35 pulse stream:

Using eLua to generate 10 usec pulses
Using eLua to generate arbitrary 10-microsecond pulse streams.  Cool!


If you need a reference, you can find a description of the current (as in eLua version 0.9) GPIO functionality here.

My modification adds a new class of GPIO functions, of the form pio.pulse.xxx().  This consists of the following four functions:

pio.pulse.reset(chnl) resets, or clears, a pulse stream so you can add new pulse information.
pio.pulse.add(chnl, value, nbits) takes the number of bits specfied by nbits from the given value and adds them to the selected channel.
pio.pulse.start(chnl, pinid, nusecs) starts sending the pulse stream in the selected channel to the GPIO pin defined by pinid; each pulse in the stream will be nusecs microseconds wide.
pio.pulse.isdone(chnl) checks the state of the pulse stream and returns true if that stream has been completely sent, else it returns false.

These functions use an array of bits, called a channel, to hold the pulse information.  A 1-bit in the array means that the associated output pin will go high, while a 0-bit means the pin will go low.  The amount of time the pin stays in that state is defined by the nusecs value in the call to pio.pulse.start().  The call to pio.pulse.start() also defines the GPIO pin used for the pulse stream.

As implemented, my version of eLua supports up to four pulse channels, with each channel carrying up to 160 bits in the pulse stream.  There is nothing magical about these values.  They are set with #defines in the pio.c source file and can be adjusted to provide different capabilities.


Using the new features
The first step for using the new pulse functions is to choose a GPIO pin for the output and configure that pin.  For example, you could use pin PD11 on an STM32F4 Discovery board (S4D), and you would set up that pin with:

G35 = 1          -- channel for G35 control
G35_PIN = pio.PD_11
pio.pin.setdir(pio.OUTPUT, G35_PIN)

pio.pin.setlow(G35_PIN)

You define a pulse stream by first selecting a channel and reseting it.  For example, to start a pulse stream on channel 1 (since this is eLua, channels are numbered from 1 to 4), you would use:

pio.pulse.reset(G35)

Now you can begin adding pulse information to the stream.  For example, you would add the G35 start bit with this call:

pio.pulse.add(G35, 1, 1)      -- always begin with start bit

This uses a value of 1 (2nd argument) and adds the low one bit (3rd argument) to channel G35 (1st argument).  Note that bits are always added at the low end of the channel; any existing bits in the channel are always moved toward the high end to make room for the addition.  Perhaps an example will clarify this.

Assume an existing pulse stream of 0011010 and you want to add the bits 110 to the stream, you would use a function call of:

pio.pulse.add(G35, 6, 3)

which adds the low three bits of the value 6 to the stream.  Your stream would then contain 0011010 110 (space added for formatting).  Note that your pulse stream has gone from seven bits to ten bits in length.

You can continue using the pio.pulse.add() function to push new information into the pulse stream.  Note that calling this function only adds information to the stream; it does not actually trigger the sending of the pulse stream.

After you have loaded all of the information you want into the pulse stream, you use a call to pio.pulse.start() to begin the actual transfer of the pulse stream to the output pin.  This function lets you select the channel to send, the GPIO pin to use for output, and the duration of each pulse, in microseconds.

Note that generating the pulse stream (pio.pulse.add()) is completely separate from sending the pulse stream (pio.pulse.start()).  This comes in very handy in debugging.  For example, you can route your pulse stream to an LED, and even lengthen the pulse duration into seconds, so you can visually check your data.  This concept also allows you to route a pulse stream to any of several different I/O pins, should your design require it.

The function pio.pulse.isdone() can be used to test if the pulse stream has finished transmission.  I included this function for future use, when the pulse code uses an ISR for pulse generation.  Right now, the code that sends the pulse stream blocks until the entire stream has been sent, so there is no need for this check function.  I hope that future versions of this code will be updated to use a timer ISR.


Enough discussion, already
Here is an eLua program that sets up a string of G35 LED lights and changes the color of all lights every three seconds:



-- G35 controller

G35 = 1      -- pulse channel for G35 comms
G35_PIN = pio.PD_11

function g35_AddBits(val, num)
  for n = 1, num do
    if (bit.isclear(val, num-n)) then
      pio.pulse.add(G35, 3, 3)      -- add 011
    else
      pio.pulse.add(G35, 1, 3)      -- add 001
    end
  end
end


function g35_AddAddr(addr)
  pio.pulse.reset(G35)
  pio.pulse.add(G35, 1, 1)      -- always begin with start bit
  g35_AddBits(addr, 6)
end


function g35_AddBright(bright)
  g35_AddBits(bright, 8)
end


function g35_AddColor(color)
  g35_AddBits(color, 4)
end


function g35_Send()
  pio.pulse.start(G35, G35_PIN, 8)
  pio.pin.setlow(G35_PIN)
end


function g35_Update(addr, bright, red, green, blue)
  g35_AddAddr(addr)
  g35_AddBright(bright)
  g35_AddColor(red)
  g35_AddColor(green)
  g35_AddColor(blue)
  g35_Send()
end


function g35_Enumerate(bulbs)
  for n = 1, bulbs do
    g35_Update(n, 10, 10, 10, 10)
  end
end


pio.pin.setdir(pio.OUTPUT, G35_PIN)
pio.pin.setlow(G35_PIN)
g35_Enumerate(50)

red = 15
green = 15
blue = 15

while (term.getchar(term.NOWAIT) == -1) do
  for n = 1, 50 do
    g35_Update(n, 255, red, green, blue)
  end
  red = (red + 2) % 16
  green = (green + 3) % 16
  blue = (blue + 4) % 16
  tmr.delay(tmr.SYS_TIMER, 3*1000000)
end



Wiring
You need to make a few simple connections to use this code for controlling a string of G35 LED lights.  You first need a 5 VDC wall-wart rated at 2 amps or more, for powering your LED light string.  DO NOT try and power your light string from the S4D board!

Connect together the negative wire from the power supply, a wire to a GND terminal on your S4D board, and the negative lead on the G35 light string.  You can find info on the G35 cable in the link at the top of this page.

Connect together the positive wire from the power supply and the positive lead on the G35 light string.  You can optionally wire an on-off switch in this connection, should you want to switch the lights on and off independently of the S4D board.

Finally, connect the pin labeled PD11 on the S4D board to the data wire (center wire) on the G35 light string.


Details on use
When you first power-up the light string, it will remain dark.  You will need to run the above program so the code can enumerate all of the bulbs in the string.  At that point, the code can begin altering each of the bulbs by sending data down the G35 cable.  You can find more details on how the bulbs behave on power-up through the link at the top of this page.

I developed and ran the above program on an STMicro S4D running at 168 MHz.  Note that eLua does not officially support this board yet (as of eLua 0.9).  I was working from a "master" branch where the eLua developers are building up what (I hope) will become the official S4D release.

Note, however, that my mods are done to the pio.c source file, which is universal to all of the eLua targets.  This essentially means that all eLua targets, from the mbed to the supported Atmel devices, could now use my pulse functions.

If you check the call to pio.pulse.start() above, you will notice that I'm using a time value (3rd argument) of eight, rather than the nominal 10 usecs.  This is because there is a two-usecs overhead in the S4D timer functions used to generate the pulses.  This means that the shortest pulse I can generate with this code is three usecs.  It also means that anytime you start a pulse stream, you must subtract two from the desired pulse width to allow for this overhead.

Note that this timer overhead is target-dependent.  For example, the mbed timer functions generate three usecs of overhead delay, so the above function call on an mbed would use a timer value of seven, not eight.  If you rebuild my code for a different target, you will need to use an oscilloscope or similar device to measure the actual delay versus the requested delay and adjust your function calls accordingly.

There are two ways to gain access to these pulse functions.  The first is to add my modified pio.c source file below to an existing 0.9 eLua source tree and rebuild for your target.  The second is to use a binary image already created with my pulse functions added.

If you want to play with these pulse functions and you have an S4D board handy, you can install this binary image.

If you want to rebuild eLua and include my pulse functions, replace the existing pio.c file with this file and rebuild for your target.

I really hope that these pulse functions make their way into the next official eLua release.  Being able to generate high-precision, arbitray streams of pulses from an interpreter, such as eLua, dramatically simplifies code generation.  And did I mention it's a total hoot?  :-)



Home