Way back in 2007, I developed a big PC-controlled LED interface operated via serial port. The basic principle was simple: a PIC micro with a serial receiver pushed bits out to some shift registers which turned LEDs on and off.
The implementation was a bit more complicated:
- A computer sends commands to the RS-232 port (nominally at 19200bps as I recall).
- The serial line runs to a level converter (MAX232) that changes RS-232 signal voltages into TTL levels.
- The converted signal runs into the serial port on a microcontroller (here the USART on a PIC16F88).
- Code on the micro converts the received data into clock and data lines.
- The signals from the micro are carried to the breakout board. (If the run is short enough, this part can be omitted entirely.)
- The clock and data lines each run into an input of a differential line driver (MC3487), doubling the number of lines but adding noise immunity for potentially long runs.
- The driven lines are received by a differential line receiver (MC3486), changing them back to clock and data at logic levels.
- The clock and data signals are run into a chain of (nominally 4) latching shift registers (74HC595), converting the serialized data into logical outputs.
- The outputs of the shift registers feed the inputs of a transistor array (ULN2803A) to increase the current capacity of the output.
- Each output of the array sinks a high-brightness LED via a current-limiting resistor.
This week, I’ve been considering how I might cut the number of signal lines from two (clock and data) to one. This could possibly simplify the hardware at the expense of some increased code complexity.
One approach might be to combine the data and clock signals into an isochronous self-clocking signal. This would allow the breakout to receive data over one line while still not having to have previously agreed upon a data rate, keeping things flexible. Unfortunately, from what I can tell, there aren’t any microcontrollers to speak of that facilitate this behavior, and separate chips designed to implement this are expensive enough to make me think that this sort of thing is reserved for faster circuits than I can muster in my garage.
A more straightforward approach would be to do more or less what a UART does, which is to agree on a data rate in advance, signal start and end of frame, and have the receiver sample frequently enough to be reasonably dependable. Even though this is less flexible than self-clocking, it’s very easy to implement. Many micros have dedicated serial hardware for this sort of thing, and it’s also pretty easy to implement in code on (cheaper) micros that don’t.
Here’s some of the math: Say we want to use the ubiquitous 8-N-1 scheme (for each frame, 8 data bits, no parity bits, and 1 stop bit, plus the implicit start bit = 10 bits). To update one of the chains, we’ll send out 4 frames (32 data bits, but 40 bits overall). We’d like smooth animation, so let’s try for a full refresh at least 60 times per second. Our minimum bitrate is thus 40 bits × 60 times per second = 2400 bits per second.
The sender’s job is easy: Every (1/2400)s ≈ 416.67μs, send out a bit, making the first bit out for each frame a start bit (traditionally 0), the last a stop bit (traditionally 1), and the ones in between the data. This would be the job of the master board, which will hopefully be a micro with a timer-based interrupt peripheral. The clock source for the chip would either be chosen to be helpfully precise (for a PIC, which usually takes 4 clocks per cycle, an oscillator that is some large multiple of 4 × 2400 = 9600Hz, e.g. 19.2MHz) or quick enough to be precise enough (a PIC with a 20MHz clock would have a 0.2μs precision, making for a bit width of 416.6μs [−0.016%] or 416.8μs [+0.032%]).
Alternatively, you could just pick a higher framerate that makes for more convenient component selection. For example, the internal oscillator on many PICs is tuned to 8MHz divided by any of several powers of two. Say, for the sake of implementing potentially slower slave devices, we go for the 125kHz clock, meaning instructions cycle at 8μs each. We could round our maximum bit width down to 416μs (the closest, around 2404bps), 408μs (around 2451bps and is divisible by three), or 400μs (2500bps and is divisible by 100).
The receiver’s job isn’t quite as easy since there’s more busywork. However, this is done on the slave board, where hopefully there isn’t too much else to do. A timer-based interrupt makes this job easier, but can also be done through polling if necessary.
Polling: In this case, what we want to do is sample the input pin at 3 times the expected data rate, repeating the following, where clock, data, and latch signals are output to a ‘595-style shift register, where D = 1/3 expected bit width:
- First idle check: Sample line. Wait D. If sample was high, continue. Otherwise, redo.
- Second idle check: Sample line. Wait D. If sample was high, continue. Otherwise, go to first idle check.
- First start check: Sample line. Wait D. If sample was low, continue. Otherwise, redo.
- Second start check: Sample line. Wait D. If sample was low, continue. Otherwise, go to first idle check.
- Begin frame: Set latch low. Set remaining = 8 (data bits left this frame).
- Skip: Set clock low. Wait D. Set data low (optional). If remaining = 0, go to end frame.
- Accept: Sample line. Set data to sample. Decrement remaining. Wait D. Set clock high. Wait D. Go to skip.
- End frame: Set latch to high. Go to first idle check.
This is a state machine that generally takes a sample every D (except at the “skip” part). First, it waits for two high samples in a row to indicate that the line is idle. Then, it awaits (but doesn’t immediately require) two low samples in a row to indicate that a frame is starting. Once two samples have been taken to mean a start bit, the third is skipped (since it’s assumed to be near a transition) and the next sample is read as the first data bit. A new bit is then read every 3D (skip waits D and accept waits 2D) until the end of the frame. During the reads, the clock and data pins are set to output the data being read, turning the micro into a sort of async-to-sync serial converter. At the end of the frame, the latch pin is raised to signify to the shift register that the readout is complete. (If we were using a chain of 4 shift registers, we’d modify the code to only raise the latch every fourth frame.)
With timer interrupts: The state machine is similar, but there are no explicit delays; we simply defer to the timer to call us back when it’s ticked.
- At the start of the code, let state = 0.
- For the ISR, if the timer crossed over, dispatch to one of the following as indicated by state:
- 0 or 1 (Idle checks): Sample line. If line is high, increment state. Else, state = 0. Exit ISR.
- 2 or 3 (Start checks): Sample line. If line is low, increment state. Else, state = 0. Exit ISR.
- 4 (Begin frame): Set latch, clock, data low. Set remaining = 8, state = 6. Exit ISR.
- 5 (Skip): Set data low (optional). If remaining = 0, state = 8. Else, state = 6. Exit ISR.
- 6 (Accept): Sample line. Set clock low. Set data to sample. Decrement remaining. Increment state. Exit ISR.
- 7 (Clock): Set clock high. Set state = 5. Exit ISR.
- 8 (End frame): Set clock low. Set latch high. Set state = 0. Exit ISR.
And of course there’s always the option of just using the micro’s own UART peripheral where available, but I’m thinking of implementing this with some extremely cheap leftover chips I already have. Not only that, but I’m considering blocking the whole thing up into 32-data-bit frames instead of the 8- explored here, and for that a built-in UART just won’t do.
With any luck, I’ll be able to hammer some of this out in the next couple of weeks.