Programmable LED Driver - Part 2: Get to Blinky and More
With my requirements set from Programmable LED Driver - Part 1, it was time to start actually building something that might look like an LED controller. I’m comfortable building projects where I write my own microcontroller firmware, but this would be my first time working on an ARM platform. To help me along the learning curve, I bought Mastering STM32 by Carmine Noviello. This book was a great tool that helped me get the development environment working quickly. It also has a lot of well-explained example code to aid development. The book is not currently at version 1.0 and Carmine’s native language isn’t English, so some of the wording requires extra thought. Regardless, the way he goes over the code line by line and explains the foibles of the ST Hardware Abstraction Layer (HAL) make the book worth way more than the price of purchase in my opinion.
There’s a wide range of STM32 Nucleo development boards out there and they all seem to have a similar price, so I chose the STM32F103 board from the middle of the lineup because it has tons of memory (for a microcontroller at least) and is a step up in performance from the chips designed for low power operation. It’s almost certainly way more than is required for this application, but it will be easier to optimize down to a cheaper chip with mostly finished firmware than to go up in performance mid-development. Timer 3 on the chip has four channels of PWM output that have independent duty cycles using the same base frequency, which is exactly what my requirements call for, so that’s perfect.
Electronics manufacturers have, more or less, agreed that a common cathode configuration is the standard for LED strips and other multi-channel devices. This made it an obvious choice to use an N-channel MOSFET to drive each channel. Going into this project I thought I would need a pair of MOSFETs in a Darlington-style configuration to get the performance needed at a reasonable size and price. I was particularly concerned about getting full control of the potential 24V drain-source voltage with only 0-3.3V available at the gate. I knew that I could get something out of a single MOSFET, but I wasn’t sure that it would be able to fully drive a load with a drain current as high as 5A. I grabbed a few jelly bean MOSFETs out of my parts bin and I was able to drive a 6” section of RGBW strip that drew around 150mA per channel with a single N-channel MOSFET, which points to the possibility that the Darlington pairs might be unnecessary. Breadboards aren’t exactly designed to handle the full load of this LED driver, so once I have a working prototype assembled on a PCB I will gradually up the current load to ensure that the MOSFETs are being driven completely into saturation throughout the entire load range, but for now I’m moving forward with the single MOSFET design.
As mentioned earlier, with the help of Mastering STM32 it wasn’t too bad to get an Eclipse & GCC environment for the Nucleo board up and running. The simplest and most logical dimming algorithms fall down pretty quickly for fades that have very small steps or long durations, so that will certainly require some optimizing, but it didn’t take long for me to have five sequences built and stored in Flash memory that cross-fade from full intensity of one color to another color at typical fade times. The user button on the Nucleo board is used to switch between sequences. I decided on a 64-bit data structure for each step of the sequence. In the comments of my source code I explain it like this:
\* A single pattern STEP requires two 32-bit registers. The have the form: * [ch1 10-bit value] [ch2 10-bit value] [ch3 10-bit value] [command 2-bit] * [ch4 10-bit value] [fade 10-bit value] [dwell 10-bit value] [command 2-bit] * * Command bits are 0x01 for first step register, 0x10 for second step register. * If the second step register is 0x11 that signifies the end of the pattern (EOF). *\ /* Fade and dwell times have 2 components. The two MSB bits define the time base: * 00 = 100ms range of 0:25.5 - 0:00.1 * 01 = seconds range of 4:15.0 - 0:01.0 * 10 = 5 seconds range of 21:15.0 - 0:05.0 * 11 = minutes range of 4:15:00.0 - 0:01:00.0 * * The eight LSB bits of the time values define the actual value in the form: * * [8-bit value] * timebase * * For patterns of greater timing resolution multiple pattern values should * be used with the same color values, a fade of 0 ms, and the necessary * dwell time to achieve the desired total dwell time. * * ie. for 5 minutes and 17.6 seconds * 10 * 3C //5 minutes * 00 * B0 //17.6 seconds */
Below is a capture of my oscilloscope probing two of the output channels. They have a nice crisp fall to 0V (which turns the LEDs on) and the different duty cycle of identical frequency is clear. Currently, I’m able to run at ~17.5kHz which is much faster than my requirements. This leaves some headroom for increased PWM resolution. I’m not sure it would benefit the user to increase the bit-depth of the color in the desktop software because LED strips are rarely going to be anywhere close to that accurate, but bumping up to 14 or even 16-bit PWM resolution might help smooth out the gradual fades that I’m currently having trouble displaying. At this point I’m happy with the critical loop of my firmware. There are improvements that I would like to make long-term, but I needed to move on to integrating the USB so that I can keep moving on this project.
Unlike everything I had encountered up to this point, I couldn’t find much information on using the USB peripheral with the Nucleo boards. I found some information about integrating it on STM32 Discovery units, but those come with a USB port wired up on the board, and many of the tutorials used some of the more advanced functionality in Visual Studio to program the MCU, so they were not that helpful. It took some time, a whole lot of Google searches, and some perusing of the HAL source code, but eventually I got the virtual COM port on my MCU to be recognized by my computer over USB. I believe most USB ports are pretty well protected these days, but I still had to pause for a minute to ask myself if I really wanted to connect an untested USB device into my computer. I double- and triple-checked all the wiring to make sure I wasn’t about to do something silly like send 24V down the data line, and in the end, it was a completely drama-free experience (phew!).
I modified my source code to send data through the COM port, and after some fiddling around with the connection properties I was able to send messages back and forth between the Nucleo and my computer. At this point I had to develop a protocol for communicating between the master device (the computer) and the slave (the Nucleo). This is still very much a work-in-progress as I work on the desktop application, but currently the first byte sent is a command from the master to the slave, and the rest of the frame and response depend on the command. The commands that I’ve implemented are: get pattern number, set pattern number, get pattern steps, and set pattern steps. This means that the length of the frame can range from a single byte to 252 and the response can range from 1 byte to 250 bytes. The virtual COM port over USB is surprisingly stable and has none of the finicky behavior that I’ve experienced with other serial connections. The biggest problem I ran into working on these commands was remembering to switch back and forth between HEX and ASCII display in the terminal window so I could understand what was being sent by the LED controller.
This seems like a decent place to stop this update. My next steps are to solidify the BOM, finish the schematic, and do the PCB layout. While the board is off getting fabricated, hopefully I can find some time to start working on the desktop application. The code for the LED controller is on my GitHub.