Projects Research & Design Hub

Shedding Light on Smart LED Design (Part 1)

Written by Jeff Bachiochi

Programming and Pixels

Creating a smart LED design is both challenging and fun. In this article, Jeff first looks at the history and technology of LEDs, and then shares the details of his smart LED project based on RGB LEDs. He introduces a circuit that programs a string of NeoPixel LED strips to specific colors, and is controlled by push buttons.

It’s hard to imagine how the Earth could be illuminated by just the stars and the planetary bodies in the night sky. This is like a concert hall or arena being lit just by cigarette lighters (for you older readers) or cell phones (for you younger readers). Two thousand years ago, our ancestors had no concept of the universe or what was producing the points of light seen in night sky. If the moon happened to appear, the amount of light cast upon the Earth increased dramatically, depending on its phase. Unaware that this was reflected light from the sun, our ancestors knew its light far exceeded that of the stars and gave the moon its magic quality. Even with a full moon, this low level of light causes a problem for our eyes.

Our eye’s retina is made up of rod and cone cells. Rod cells are sensitive to low light levels in the green/blue area of the spectrum. These cells are unable to differentiate colors, so objects seen in low light appear to be black or white (shades of gray). The sun brings forth a blinding light (compared to the night). It was easy to be in awe of this mighty illumination that divides the day from the night. No wonder our ancestors thought of these as gods, lording over the heavens.

With sufficient light from the sun, three types of cone cells in the retina take over. Each type of cone cell responds to a specific light frequency range. The combination of the light levels received by each of these cone cells determines the color of the light being received in that area. So, the colors we see are, in fact, made up of the combination of only three cell outputs—just like the pixels of an HDTV create the illusion of any color using only three different colored LEDs (red, green and blue or RGB). This month we’ll look at the smart LED, made using RGB LEDs.

LEDs
Electroluminescence, a material’s ability to emit light in response to the presence of an electric current, was observe in the early 20th century. Practical LEDs weren’t available until the 1960s, and even then, only red LEDs were available. The color produced by an LED is based on the materials used, and it would be another 10 years before other colors were produced with adequate output. The band gap requirements—energy required to cross a junction—of each material is different, so we have different voltage requirements for each type of LED (Figure 1) [1].

FIGURE 1 – Listed here are some semiconductor materials used to produce an LED’s P-N junction, along with the wavelengths (colors) emitted and the typical band-gap voltages required [1].

Today, we have tri-color LEDs available in both through-hole and surface-mount (SMT) configurations (Figure 2). If you look closely at the SMT device in Figure 2, you may suspect there are more than three discrete LEDs in this SMT package. While discrete LEDs are available in SMT, this picture is of a NeoPixel from Adafruit, the smart LED I’ll be using for this project. The NeoPixel is the combination of an addressable IC (WS2811 or similar) and RGB LEDs. This IC handles the intensity of each of the three LEDs, based on three 8-bit values shifted into the device. It has just four connections—power, ground, and serial in and out. Let’s take a closer look on how to work with it.

FIGURE 2 – Today we can purchase RGB LEDs in both through-hole and SMT packages.

Anyone who has worked with graphics of any kind is probably familiar with the 24-bit digital signature used for describing pixel color. The 24-bits are actually three 8-bit bytes, with one byte for each of the RGB colors. This means that you have 8 bits of control over the intensity of each of the three colors, where 0 is fully OFF and 255 is fully ON. This 24-bit value is sent in an RGB sequence, MSB first. Since there is no external clock necessary, the WS8211 requires the serial data to conform to a special format for each bit.

— ADVERTISMENT—

Advertise Here

For a bit=0 the TON time must be between 150-450ns, and the TOFF time between 750ns and 1,050ns. For a bit=1 the TON time must be between 450-750ns and the TOFF time between 450-750ns. The total time for a cycle (TON + TOFF) is between 650ns and 1,650ns. A pause in excess of 50µs ends a sequence. Refer to Figure 3 for this, and note that the NeoPixel is designed to allow multiple devices to be daisy-chained SIN to SOUT.

FIGURE 3 – On the left are the timing specs for communicating with the WS2811. The ON time determines the bit’s value. Data must be sent continuously, until the last daisy-chained device receives the first 24 bits sent. An extended OFF time latches the present data into all devices.

You must send out 24-bits for each NeoPixel connected in series. Note that data flow through each device is 1 bit out for each bit in. So, the first 24 bits you send out will end up in the last device in the chain. When a pause in serial data occurs (greater than 50µs), all devices latch onto the bits in its shift register. This latched data will be used to set three PWM outputs to drive the LEDs. Note here that there is a 500ns delay between a bit in and a bit out of each device. The latching of each device’s data is therefore not synchronized. This will be perceptible only for very long strings of LEDs, because these delays add up.

Undoubtedly, you have noticed that the timing is fairly fast. This is good, because it allows you to update an entire string of LEDs quickly. But, it’s also bad, because this shifting will most likely require blocking other execution while active. If a bit time is about 1µs, then it will take 24µs for each NeoPixel and 2.4ms for a string of 100. That’s a long time to block any other routine.

Now that I’ve stated the facts as in the WS2811 sheet, let the truth be told. If you plan to work with NeoPixels, you’ll want to read Josh Levine’s WordPress blog [2] on the subject of bit timing. It seems that as long as you use the proper ON times, OFF timing can be much more relaxed, and this greatly improves the ability of your code to work with other interrupting sources.

CODING
I decided to give these constraints a test try by coding only the ON times in an interrupt. The extra code in the interrupt assures a minimum OFF time, and additional interruptions in between bits can extend this OFF time. As long as this doesn’t exceed the reset time, 50µs, we should be good. This month’s project uses a PIC16F1847 from Microchip Technology, which is an 18-pin flash microcontroller (MCU), and this circuit will be part of a larger project. This circuit consists of eight (switch) inputs along, with one output for driving a string of nine NeoPixels and a few miscellaneous I/Os for communications. The schematics are shown in Figure 4 and Figure 5.

CLICK TO ENLARGE
FIGURE 4 – This schematic shows the eight input switches and nine serial NeoPixels for this month’s project PCB.
CLICK TO ENLARGE
FIGURE 5 – Schematic of the PIC16F1847 MCU and communication, programming and debugging connectors. All parts are SMT parts except the switches and pin headers.

This MCU will run at 32MHz with its internal oscillator, giving an execution speed of 8MHz or 125ns. Each bit or ON time is coded as a “bsf” (bit set instruction), some delay and a “bcf” (bit clear instruction). The delay is different for a “0” and a “1” bit, and consists of NOPs (no operation instruction). Note the timing in each of the two Timer1 routines in the NeoPixel interrupt.

These adhere to the max and min ON times for NeoPixel bit. While Timer 1 had a 30ns resolution, it was easier to code two fixed timing routines than to load the timer and let the timer count a more exact timing. This would end up being a totally blocking interrupt, if not for the relaxed OFF timing. So, I’m just using the TIMER1 interrupt without actually using its timing ability! The remaining code must fetch a byte of data to send, strip off the data bit in question (MSB to LSB), and set/delay/clear the output bit, before leaving the routine. A check after each bit is sent disables the TIMER1 interrupt once all bits have been transmitted.

The NeoPixel data—3 bytes for each of the nine NeoPixel LEDs—are stored in the proper sequence as required by the NeoPixel data format. This makes fetching each byte easy, using an indirect register move. Picking off the appropriate bit of each byte is done by rotating the byte through the carry. The ON time code is based on the carry, as shown in Listing 1.

btfss STATUS, C ; skip next if carry=1
goto TIMER1_0
;
TIMER1_1
bsf LATB, Sout
nop ; 125ns
nop ; 250ns
nop ; 370ns
nop ; 500ns
bcf LATB, Sout ; 625ns
goto TIMER1_Continue
;
TIMER1_0
bsf LATB, Sout
nop ; 125ns
nop ; 250ns
bcf LATB, Sout ; 375ns
goto TIMER1_Continue
;
TIMER1_Continue

— ADVERTISMENT—

Advertise Here

LISTING 1 – TIMER1 interrupt routine handles the timing of each bit to the NeoPixels. The ON time is controlled by hard coding NOPs for each one-instruction cycle delay. An instruction cycle is 32MHz /4 = 125ns.

A BIT (24 BITS) ABOUT COLOR
The website rgbcolorcode.com [3] offers a graphic example of how RGB values change as they are mixed to produce a required color. You’ll note that a color’s code is different, depending on whether you are adding light—as in this project (RGB)—or applying pigments, as with printing (CMYK). RGB is an additive process, from no color (black) to all colors (white), whereas CMYK is a subtractive process from white (reflecting all colors) to black (no light reflection).

It’s easy to understand how we can produce black, white, red, green and blue with LEDs. Just turn them all ON or, either individually or collectively. If they are turned ON in pairs, we can also get cyan, yellow and magenta. Any other colors require percentages of something other than zero or 100%. Luckily, the WS2811 LED driver, discussed previously, uses PWM outputs. The PWM values are 8-bit and correspond to color chart values. This allows each LED to be adjusted to some percentage of full.

PWM values other than 0 or 255 are required for intermediate colors, such as orange or brown. Having PWM control also allows any color to be faded to black, by lowering the values of the RGB LEDs to zero while retaining the same color proportions. NeoPixels want to receive the color values for each LED in a red, green, blue sequence, so the table’s 27 consecutive memory locations are defined as R, G and B for each of the LEDs 1-8.

SWITCH INPUTS
For now, I’ll be using each of the eight switches to set the nine LEDs to a different color. This will test out both the switches and the NeoPixels. So, let’s look at those switches. The MCU requires certain functions to be on specific pins, so it’s not unusual that the leftover pins, used as switch inputs, are not on the same port. The first order of business is to gather the state of each input into a single byte, where switches 1-8 correspond to bits 0-7 of the register, NewSwitch. A second register, LastSwitch, will retain the previous sample’s switch states (originally initialized to 0xFF).

A byte of 0xFF means that all switches are “not pushed.” When a switch is pushed, it pulls its input to ground, and samples as a “0.” By comparing these two registers (XORed), we can determine if any switch has changed state. By comparing this value with NewSwitch (ANDed) we can eliminate the push changes, keeping only those changes due to releasing a switch. The complement of this will be indicated with a “0” any key that has been released since the last sample. This is combined (ANDed) with COSSwitch, which keeps a running tally of any changes.

While we have both I’s and O’s to deal with, no actual work will be done here, other than sampling the status of the switches and setting the color of each LED, based on the color table. What to do with the switch status and what colors to set each LED will be handled by a second circuit. So, we’ll need to set up a communications interface.

The SPI port will be used as a slave device for communication with another circuit. The UART will serve as a debug port for messages. The UART could be connected to an LCD or a PC to display messages about the status of this device. Which switch was just released? What SPI data are being received from a master? What is the bitstream going to the NeoPixels? While none of this is necessary for the operation, my coding mistakes are easier to find when I have good feedback about what’s actually happening inside this black box.

I’ve started this project with the I/O slave device, and have not yet produced a master device. So how can I test this part of the project? I can use the trusty Arduino as a master device, and write a simple program to collect switch information and set the color table through the Arduino’s SPI port. This project will use multiple slave devices, so I’m going to make use of the slave select (SS) line to choose which slave device I want to communicate with. I could have chosen to use I2C for inter-board communication, in which case each device would require a separate address. While SPI requires more than the two signal lines of I2C, each slave device has its own slave select input, so the code can stay identical for each without having to assign a different address to each slave device.

TESTING 1, 2, 3, 4…
The master provides a clock for the SPI shift register in each device. SPI communication transfers a byte in both directions at the same time. How the data are used is entirely up to you. I found a resource from ST Microelectronics that explained a simple protocol they use for some of their products [4]. The first two MSBits (most significant bits) indicate one of four modes of operation: Read, Write, Read and Clear Status, and Read Device Information. The last 6 bits indicate a register number. With this protocol, I could individually change any of the 27 registers used in the LED1:8 color table. At this point, I think I only need to write data, but it won’t hurt to follow this suggested protocol.

Because data moves in both directions at the same time, you can see that a slave can’t possibly know what to send before a request is made (the slave receives a command), so there will be times when “dummy” data is sent just to fill the gap. After sending a “read” command, the master must send a dummy byte to allow the slave to respond with the data and clock it back to the master.

There is only the time between clock cycles to determine which register is requested and transfer it into the SSP1BUF register, before the master’s clock begins shifting data out. This is not an issue when a slave device like an EEPROM has the hardware to handle it. But when the slave is a software device, there is code to execute. These data might be unobtainable in the required time frame, because the master doesn’t idle between bytes. You may need to expand your protocol by 1 byte to allow for this. The master might need to send 3 bytes: request, dummy, dummy. The slave would send dummy (while waiting for the request), dummy (while it processes the request) and then the data.

In this case I can make use of the first exchange by always loading the switch status into the SPI buffer before the beginning of any communications—normally a wasted dummy byte from the slave. If we can receive switch status on every transfer, we only need to be able to write data to the slave. A command of “0” would be a write to address “0” (switch status register). One additional byte includes the data to write to the slave. This byte will be the complement of the switch status it just received, and will then be IORed with COSSwitch (switch status register) to reset (and acknowledge) the status. A command of “1” would be a write to address “1” (LED color table start). This would be followed by (27) bytes to fill the LED Color Table, and that table’s data would then be used to update the NeoPixels. Listing 2 shows the code to handle these two transfers.

int writeSwitchStatusCommand()
{
// take the chip select low to select the device:
digitalWrite(chipSelectPin, LOW);
// send the device the 6-bit address register you want to write to, receive switch status
int result = SPI.transfer(0x00);
// send the value you wish to write to the
ddressed register, receive dummy
SPI.transfer(~result);
// take the chip select high to de-select:
digitalWrite(chipSelectPin, HIGH);
// return the result:
Serial.println(“Received “ + String(result,HEX));
return (result);
}

void writeLEDColorTableCommand()
{
// take the chip select low to select the device:
digitalWrite(chipSelectPin, LOW);
// send the device the 6-bit address register you want to write to, receive switch status
SPI.transfer(0x01);
for(int i=0; i<9; i++)
{
Serial.print(“LED” + string(i+1));
for(int j=0; j<3; j++)
{
// send the value you wish to write to the addressed register, receive dummy
SPI.transfer(FaceArray[(i*3)+j]);
Serial.print(“,”);
Serial.print(FaceArray[(i*3)+j],HEX);
}
Serial.println();
}
// take the chip select high to de-select:
digitalWrite(chipSelectPin, HIGH);
}

LISTING 2 – Code listing showing the SPI transfers

— ADVERTISMENT—

Advertise Here

Our circuit samples the switches while its idle. Any button pushed and released is added to the switch status register COSSwitch as a zero in the appropriate bit position (switch 1-8 = bits 0-7). The Arduino’s loop function begins with a request of the switch status. It receives this by using the command 0x00 (write to address 0). Each bit position 0-7 (switches 1-8) has been assigned a color: black, red, orange, yellow, green, blue, violet and white. When a zero is found in a bit position, the fill routine is called with the bit’s associated color palette. Then the function ends with a short delay before looping back (Listing 3).

//**************************************
// request switch status
// if a switch bit = 0, then fill
// the array with the appropriate color
// pause
//**************************************

void loop()
{
switches = writeSwitchStatusCommand();
if (!(switches & 1))
{
if(debug&1)
{
Serial.println(“Switch 1”);
}
Fill(Black);
}
if (!(switches & 2))
{
if(debug&1)
{
Serial.println(“Switch 2”);

}
Fill(Red);
}
if (!(switches & 4))
{
if(debug&1)
{
Serial.println(“Switch 3”);
}
Fill(Orange);
}
if (!(switches & 8))
{
if(debug&1)

Serial.println(“Switch 4”);
}
Fill(Yellow);
}
if (!(switches & 16))
{
if(debug&1)
{
Serial.println(“Switch 5”);
}
Fill(Green);
}
if (!(switches & 32))
{
if(debug&1)
{
Serial.println(“Switch 6”);
}
Fill(Blue);
}
if (!(switches & 64))
{
if(debug&1)
{
Serial.println(“Switch 7”);
}
Fill(Violet);
}
if (!(switches & 128))
{
if(debug&1)
{
Serial.println(“Switch 8”);
}
Fill(White);
}
delay(100);
}

LISTING 3 – As shown here, each bit position 0-7 (switches 1-8) has been assigned a color: black, red, orange, yellow, green, blue, violet and white.

The Fill(long grb) function breaks the 32-bit value into 4 bytes (Listing 4). The MSByte (most significant byte) isn’t used, but the second byte becomes the red color value, the third the green and fourth the blue palette values for that particular color. An array that holds the 3 color bytes for each of the nine LEDs is filled with that color. There isn’t any reason each LED couldn’t have a different color.

//**************************************
// fill array of bytes with color grb
// then send array using SPI
//**************************************
void Fill(long grb)
{
for (int i=0; i<9; i++)
{
//Serial.println(grb,HEX);
int x = grb/0x10000;
FaceArray[i*3] = x;
//Serial.println(x,HEX);
int y = (grb - (x * 0x10000)) / 0x100;
FaceArray[(i*3)+1] = y;
//Serial.println(y,HEX);
int z = grb - (x * 0x10000) - (y * 0x100);
FaceArray[(i*3)+2] = z;
//Serial.println(z,HEX);
}
writeLEDColorTableCommand();
}

LISTING 4 – The Fill(long grb) function breaks the 32-bit value into 4 bytes.

This array is used by the writeLEDColorTableCommand() function to update our PCB via SPI, with data for its LED Color Table. When our board receives these 27 bytes, it stores them in the LED Color Table and produces NeoPixel serial data to update the nine LEDs with new color data.

It took longer to wire up a 6-pin connector to the Arduino than it did to write this simple test program. That’s what I like about the Arduino—it makes a great testing vehicle. Each switch will, in turn, change all the NeoPixels to their particular colors (Figure 6).

FIGURE 6 – Prototype of this month’s circuit in operation

THE BIGGER PICTURE?
This column introduced a circuit that will program a string of nine NeoPixels to specific colors, based on receiving SPI data from some master SPI device. In addition, the circuit has eight push buttons that are constantly scanned for user presses. Switch status is reported to the master SPI device when it is requested. In our test case, the master merely sent data to change all LEDs to a particular color, based on the switch status. Remember—this is just a small part of a larger conglomeration, which will replace an inexpensive and simple, yet perplexing, curio with a pricey technology-ridden one.

I’ve designed using the SK6812 or WS6812 in its IC form, however these are also available pre-mounted on flex circuit and sold by the meter (Figure 6). I don’t want to give too much away here, so I’ll leave you with something to think about. Put on your Sherlock Holmes “deerstalker,” and see if you can figure it out before next month (Part 2). Too much to learn, so little time. 

Read Part 2 here and learn the mystery!

References:
[1]  http://web2.ph.utexas.edu/~coker2/index.files/xrayslasers.htm
[2] Josh Levine’s WordPress blog– Follow Josh’s investigation of NeoPixels:
https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them
[3] www.rgbcolorcode.com
[4] www.st.com    TN0897Technical note, ST SPI protocol

RESOURCES
Adafruit | www.adafruit.com
Microchip Technology | www.microchip.com
ST Microelectronics | www.st.com

PUBLISHED IN CIRCUIT CELLAR MAGAZINE • JANUARY 2020 #354 – Get a PDF of the issue


Don't miss out on upcoming issues of Circuit Cellar. Subscribe today!

 
 
Note: We’ve made the October 2017 issue of Circuit Cellar available as a free sample issue. In it, you’ll find a rich variety of the kinds of articles and information that exemplify a typical issue of the current magazine.


Would you like to write for Circuit Cellar? We are always accepting articles/posts from the technical community. Get in touch with us and let's discuss your ideas.

Become a Sponsor
Website | + posts

Jeff Bachiochi (pronounced BAH-key-AH-key) has been writing for Circuit Cellar since 1988. His background includes product design and manufacturing. You can reach him at: [email protected] or at: www.imaginethatnow.com.