Embedded Systems Development


I've been developing software on embedded systems since 2009. I enjoy the challenge of solving problems on platforms with tight constraints. Embedded software development has unique challenges, and I have written about some of them here.


Tannhauser Lamp

Lamp firmware, Arduino IDE Finished Lamp

The Tannhauser Lamp is a 3D-printed lamp, suitable for indoor use. A ring of 24 individually-addressable RGB LEDs projects multi-colored light upward, where it diffuses through a lampshade and paints the surrounding environment. Each LED can have its own color and intensity, allowing for a wide variety of effects. This is driven by an ARM Cortex M0 processor running at 72MHz. The firmware, written in C++ in the Arduino IDE, combines a physics simulation with pixel shaders to generate smooth, organic, randomly-changing color effects.

I began writing the firmware for this lamp in 2015. It had been years since I'd done anything in C++, and it was good to get back into that language. Almost every modern programming language descends from C++, so it wasn't difficult. In fact, it was a really good exercise. C++ requires the use of interfaces in situations where other languages, like PHP, will let you get away with not using them. The Tannhauser Lamp code requires a lot of dependency injection, and because the compiler is single-pass, interfaces are by far the most elegant solution. They certainly make SOLID compliance much easier.

The biggest challenge was working with the Arduino IDE, which uses a version of C++ that does things in non-standard ways. It's a hassle to put code into sub-directories because it wants you to use its version of libraries instead, and program entry isn't the same as it is with console or GUI apps. However, anyone who's fluent in C++ can figure these things out. Other than that, it acts the same as C++ on other platforms. The biggest constraint is using classic 8-bit Arduino controllers, which have only a tiny amount of space for data and stack. It's better to allocate all RAM that will be needed up front, so you don't run into issues later. (This is part of what motivated me to use the ARM platform instead. 64K of RAM is far easier to program for than 2-4K.)

Aside from the electronics, the lamp is made entirely out of 3D printed parts that are designed to fit together.


Heuristic Calibration for Delta Printers

My first Z-probe mount, designed in SketchUp and using a fancy Hall Effect sensor Parallel Simulated Annealing

Linear delta printers like this one can be very difficult to calibrate. After searching for many months for a solution to this problem, and finding none, I decided to solve the problem for once and for all myself. I wrote an extension to the Smoothie 3D printer firmware. It helps users to calibrate their Z probes, and then uses those Z probes to capture information about errors in the printer's positional accuracy. This information is then run through an algorithm to determine the set of physical misalignments most likely to produce the observed errors.

To "brute-force" the problem — that is, to try every possible combination of misalignments, down to some specified granularity — would require so many calculations that it would take days for the search to run. This is because the firmware is capable of independently adjusting 13 variables, thereby making the problem space 13-dimensional. That's quite a few combinations! In order to solve this problem, I implemented a parallel simulated annealing algorithm that allows all 13 variables to slowly move towards the most likely optimum values. A perfect solution could cost a quadrillion or more iterations, but my code can get pretty close to perfect in only fifty iterations. It typically improves positional accuracy by tens of microns, which is significant.

The firmware modification also has code to take out any remaining error in the Z dimension, thereby ensuring perfect first-layer adhesion. This is done by running a depth map of the print surface through a bilinear interpolation algorithm, which is called by the firmware as the effector is moved in real time. This causes the effector's nozzle to be raised or lowered as necessary to keep it the correct distance from the print surface.

The code that does this is not SOLID compliant. It could be refactored for this somewhat. However, there are several reasons not to do that:


Raspberry Pi Device Driver for NeoPixel RGB LEDs

The first time I got the PWM generator to send color information to the NeoPixels Raspberry Pi running NeoPixel LEDs with my driver. The ring is wedged into a "gear vase" I printed in a material called T-Glase The driver building a waveform

NeoPixels are small RGB LEDs with integrated drivers. They can be connected in series very simply, requiring only power, ground, and data lines. Each NeoPixel is capable of receiving color information on its data line, as well as forwarding color information to the next NeoPixel in the series. The NeoPixels I use (WS2812 type) require a self-clocking signal on their data lines, meaning that the signal has to be divided into pulses that are each the same length, which is 400 nanoseconds. Two high pulses followed by a low pulse signify 1, and one high pulse followed by two low pulses signify 0.

In the Spring of 2014, I wanted to use a Raspberry Pi to drive NeoPixels. To my astonishment, I discovered that there was no Raspberry Pi driver. In fact, the chief hardware vendor suggested that the Pi could not drive NeoPixels because it lacked a real-time OS. After all, the pulses are only 400 nanoseconds wide. Since another task could interrupt signal generation at any time, the signal itself could have its timing thrown off, ruining it.

When I saw that, I knew I could make it work. The Pi has a reasonably powerful desktop-class processor with a DMA controller, which is a clever piece of hardware that can transfer information from one place to another independent of the main CPU. DMA controllers have their own dedicated execution resources, so even if the CPU is completely saturated, they can still transfer data with perfect timing. I reasoned that if I generated a waveform with the required high and low pulses, I could use the DMA controller to send that waveform to the Pi's PWM generator, which would then send the waveform down the NeoPixel's data line.

I bought an oscilloscope, and began writing code. A month later, I had a device driver that could run well over a hundred NeoPixels at the same time. This code was quite difficult to write at times. The DMA controller and PWM generators have to be controlled by writing values to hardware registers, but the CPU's documentation doesn't tell you all the registers, nor any "tricks" you need to get them to work properly. I had to find out about those things the hard way, through trial and error. I persevered, and at the end of the process, I had the first NeoPixel device driver for the Raspberry Pi. It was later incorporated into an open-source media center program.


Embedded Software Development Principles

Code for debouncing a button press

Embedded development requires different strategies from desktop, mobile, and server apps. This is especially true of memory management. Embedded platforms typically have a very different operating environment:

Methodologies like SOLID can be useful to some extent, as long as they are not implemented in a naïve way. For example, if you apply the Single Responsibility Principle, you may have to create many more objects. That means more CPU cycles and memory use. You may wind up allocating and freeing heap memory over and over, potentially thousands of times while the device is running. This can eventually cause problems:

There are strategies that can help in situations like these:

Whoops, looks like something went wrong.

(1/1) ErrorException

file_put_contents(): Only 0 of 185 bytes written, possibly out of free disk space

in Filesystem.php (line 122)
at HandleExceptions->handleError(2, 'file_put_contents(): Only 0 of 185 bytes written, possibly out of free disk space', '/home/solidox/solidox.xyz/vendor/laravel/framework/src/Illuminate/Filesystem/Filesystem.php', 122, array('path' => '/home/solidox/solidox.xyz/storage/framework/sessions/lJiOHR432nlLYRFZvAn2RmtwCQhPe80JjAI1DV4Z', 'contents' => 'a:3:{s:6:"_token";s:40:"GkiarXfrYub4jLQR6qU7uHNGgEG9ST8oOlakxIpc";s:9:"_previous";a:1:{s:3:"url";s:27:"http://solidox.xyz/embedded";}s:6:"_flash";a:2:{s:3:"old";a:0:{}s:3:"new";a:0:{}}}', 'lock' => true))
at file_put_contents('/home/solidox/solidox.xyz/storage/framework/sessions/lJiOHR432nlLYRFZvAn2RmtwCQhPe80JjAI1DV4Z', 'a:3:{s:6:"_token";s:40:"GkiarXfrYub4jLQR6qU7uHNGgEG9ST8oOlakxIpc";s:9:"_previous";a:1:{s:3:"url";s:27:"http://solidox.xyz/embedded";}s:6:"_flash";a:2:{s:3:"old";a:0:{}s:3:"new";a:0:{}}}', 2)in Filesystem.php (line 122)
at Filesystem->put('/home/solidox/solidox.xyz/storage/framework/sessions/lJiOHR432nlLYRFZvAn2RmtwCQhPe80JjAI1DV4Z', 'a:3:{s:6:"_token";s:40:"GkiarXfrYub4jLQR6qU7uHNGgEG9ST8oOlakxIpc";s:9:"_previous";a:1:{s:3:"url";s:27:"http://solidox.xyz/embedded";}s:6:"_flash";a:2:{s:3:"old";a:0:{}s:3:"new";a:0:{}}}', true)in FileSessionHandler.php (line 83)
at FileSessionHandler->write('lJiOHR432nlLYRFZvAn2RmtwCQhPe80JjAI1DV4Z', 'a:3:{s:6:"_token";s:40:"GkiarXfrYub4jLQR6qU7uHNGgEG9ST8oOlakxIpc";s:9:"_previous";a:1:{s:3:"url";s:27:"http://solidox.xyz/embedded";}s:6:"_flash";a:2:{s:3:"old";a:0:{}s:3:"new";a:0:{}}}')in Store.php (line 128)
at Store->save()in StartSession.php (line 88)
at StartSession->terminate(object(Request), object(Response))in Kernel.php (line 218)
at Kernel->terminateMiddleware(object(Request), object(Response))in Kernel.php (line 189)
at Kernel->terminate(object(Request), object(Response))in index.php (line 58)