Early on in bare metal development it is advantageous to secure a channel for output. An environment that can communicate it is working (or at least still working) – be it via audio (a simple “beep” sound) or visual (a lit-up LED) – is not only highly desirable, but also rewarding.
Systems are, after all, nothing without input or output.
The ubiquitous GPIO (General-Purpose Input/Output) aspect of the Raspberry Pi offers immediate output potential, and many articles exist on how to program a single pin to light an LED. However, use of an interesting (and affordable) expansion kit like the Pimoroni Blinkt! can instead – with only a little extra effort – lead to a simple channel for output that is not only more rewarding, but comparatively more useful.
Introducing the Blinkt!
The Blinkt! is a device consisting of eight LEDs situated on a small expansion board that can be placed on the Raspberry Pi’s GPIO pins. Importantly, although it occupies the full-width 40-pin connector, it actually only uses two of the pins: one for data (BCM pin 23), and one for a clock signal (BCM pin 24).
This method of using just two pins – one data, one clock – is an example of SPI (“Serial Peripheral Interface”). SPI is a protocol that allows devices to communicate data between each other – in this case, the Raspberry Pi and the Blinkt! – one bit at a time, in a simple and synchronized manner. In the case of the Raspberry Pi, the SPI protocol can be used to communicate to the Blinkt! the required colour and brightness settings of each LED.
A closer examination of the Raspberry Pi’s GPIO pins shows that several have been specifically allocated for SPI already, but the creators of Blinkt! have decided not to “clutter” those, and instead have opted to use pins 23 and 24 (which are allocated as ‘general purpose’ pins, a clearly acceptable alternative).
Systems are, after all, nothing without input or output.
To send a single bit of data using SPI, the data pin needs to be set to either high or low (indicating a one or a zero), and then a clock cycle needs to be issued by first setting the clock from low to high, and then back to low again. When this occurs, the receiving SPI component (in this case, the Blinkt!) sees the clock cycle complete, and processes the bit of data seen on the data pin. (This can be repeated, however many times required, to send all the relevant bits of data to the device).
As is to be expected, there is a specific format to the data that must be sent in order to deliver the intended message. In the case of the Blinkt!, this is a set of LED brightness and colour settings within a “frame” of data which can be described as follows:
- Start of Frame
1 x 32-bit word 0x00000000, indicating the start of a “frame” of data
- LED settings
8 x 32-bit words, indicating the brightness and RGB settings for each of the eight LEDs
(one 32-bit word per LED)
- End of Frame
1 x 32-bit word 0x00000000, to indicate the end of the “frame” of data
4 x 1-bit values “0”, to flush the Blinkt!‘s data buffer (and so light the LEDs)
The format of the 32-bit word to describe an LED setting looks like this:
111 <5:brightness> <8:blue> <8:green> <8:red>
So with five bits available, the brightness setting can assume values in the range of 0-31, whilst the red, green and blue components can – with their eight bits available – assume values in the range of 0-255.
By way of example, to request that all eight LEDs be set to maximum saturation green (with a brightness of 5), the following frame of data would be required:
00000000 00000000 00000000 00000000 11100101 00000000 11111111 00000000 11100101 00000000 11111111 00000000 11100101 00000000 11111111 00000000 11100101 00000000 11111111 00000000 11100101 00000000 11111111 00000000 11100101 00000000 11111111 00000000 11100101 00000000 11111111 00000000 11100101 00000000 11111111 00000000 00000000 00000000 00000000 00000000 0000
In terms of bit order, data is sent with the MSB (most significant bit) first, and the LSB (least significant bit) last.
(Note: There is an optimisation to this based on the observed behaviour of the Blinkt!, which will be discussed toward the end).
With this information to hand, control of the Blinkt! can be achieved with two essential steps: configure the GPIO pins ready for output, and implement the protocol described allowing communication of a “frame” of LED configuration data.
The Raspberry Pi’s GPIO pins are memory-mapped into the ARM, and so can be easily addressed and configured as required. For talking to the Blinkt!, as has been discussed pins BCM23 (data) and BCM24 (clock) need to be configured as output pins.
The code required to do this, at least in a generic manner, may appear at first slightly obscure, but the ‘complexity’ is driven by the fact that the data for the GPIO pins are tightly packed into a small memory space for efficiency, resulting in the appearance of division/modulo expressions to calculate not only which word of memory to access but also which bits within the word of memory to access.
The example code can be found here: [https://github.com/abbeycatuk/ledoutput], and the GPIO management code can be found at src/gpio/gpio.c. The code should be somewhat self-explanatory, and for further detail on the GPIO memory mapping, page 89 onward of the following [Broadcom Manual] contains useful information on how the GPIO configuration is neatly (and tightly!) mapped into memory.
Sending the Data
With the pins configured for output, all that remains is a small amount of code that is capable of sending each bit of data, by setting the data pin high or low, and then “clocking” the data to the recipient. This in itself is very straightforward, and can then be abstracted to a higher level by code that utilises this to implement the data protocol required to communicate the LED configurations to the Blinkt!. Again, the example code provided (src/blinkt/blinkt.c) provides a simple reference implementation.
The official data-sheet for the APA102 and APA102C appears to be slightly misleading, in specifying that a 32-bit word 0xffffffff forms the “end of frame” marker. Certainly when working with the Blinkt! it appears that the way to end the frame and light up the LEDs is actually to send it 36 bits of zero.
After initial investigation, it appears the reality is that the Blinkt! is buffering the data that is sent to it, and only begins to flush the buffer (and thus light the LEDs) at the point in time when a new “start of frame” has been sent and the next set of LED configurations is being transmitted to it.
So in some sense, there is no actual “end of frame” marker. What is required, is to send the next “start of frame” marker. When this has been sent, by then continuing to send zero-bits, the device remains in its “start of frame” state but as each extra zero-bit is received, the current buffer is flushed, two LEDs at a time.
With these observations to hand, the code can be optimised by changing it to implement the following:
- At initialisation, send an initial SOF marker (32-bit word 0x00000000) and 4-bits of zero to effectively inform the Blinkt! to expect a new set of LED configurations, the extra 4-bits forcing the Blinkt! to flush its existing buffer (which upon start-up contains nothing, so no LEDs light up).
- Whenever a client requests the LEDs to light up, given that an SOF has already been sent at this point, simply send the LED configurations and complete it with another SOF marker and 4-bits of zero (as per step (1)), which – again – ensures that the Blinkt! is left in a waiting state, having had its buffer flushed.
A small optimisation perhaps, but an optimisation nevertheless. This optimisation can be seen in the example code.
Taking control of output is a vital early step in bare metal developments. Although controlling a single GPIO is straightforward enough, expansions such as the Blinkt! can offer more useful – and rewarding – diagnostic outputs for a little extra effort. A brief but useful exposure both to GPIO and SPI come together nicely to demonstrate just how quickly the Raspberry Pi can offer useful output.