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 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