More Fun with Four-Wire SPI: Drawing to “E-Ink” Displays

In previous tutorials, I covered how to use the STM32 line of microcontrollers to draw to small displays using the SPI communication standard. First with software functions and small ‘SSD1331’ OLED displays, and then with the faster SPI hardware peripheral and slightly larger ‘ILI9341’ TFT LCD displays. Both of those displays are great for cheaply displaying data or multimedia content, because they can show 16 bits of color per pixel and have enough space to present a moderate amount of information. But if you want to design a very low-power application, you might want a display which does not need to constantly drain energy to maintain an image.

Enter ‘E-Ink’ displays, sometimes called “Electrophoretic Displays“. As the name implies, they use the same basic operating principle as techniques like Gel Electrophoresis, which separates polarized molecules such as DNA based on their electric charge. Each pixel in one of these displays is a tiny hollow sphere filled with oppositely-charged ink molecules, and they are separated between the top and bottom of their capsules to make the pixel light or dark. The ink remains in place even after power is removed; I think that they are suspended in a solid gel or something. Modern E-Ink modules sometimes have a third color such as red or yellow, but this post will only cover a humble monochrome display.

E-ink 😀

Specifically, we will go over the process of setting up and drawing to a 2.9″ E-Paper module from Waveshare. This display has a resolution of 128 x 296 pixels, so you’ll need a microcontroller with slightly more than 4KB of RAM to store a framebuffer for the display. That means that we can continue to target the affordable STM32L031K6 ‘Nucleo’ board with its 8KB of RAM, but the STM32F031K6 version which I’ve used in previous posts only has 4KB available, so this tutorial will use the STM32F042K6 (6KB) instead. Startup files like a linker script and vector table are provided in the example project on Github, but besides the fact that the project won’t link for an STM32F031K6, the code is identical.

Step 1: SPI Initialization

We’ll initialize the GPIO pins and SPI peripheral just like in the previous ILI9341 tutorial. I’ll be referring back to that tutorial a lot, since the E-Paper display uses a nearly identical protocol with the same peripheral settings. Give it a read if you’ve never used “4-wire” SPI communication before, or take a look at the simpler SSD1331 tutorial if you have never heard of “SPI communication” before. Just like those projects, our first step is to initialize the peripheral clocks and GPIO pins:

I won’t copy/paste all of the GPIO initialization code again, but you can find it in the example project on Github. The only difference between these GPIO settings and those for the ILI9341 is the addition of a BSY (‘Busy’) pin which the epaper display uses to tell us when it is busy refreshing the display and unable to listen to communications. It can take a lot of time to refresh an E-Ink display, especially with a ‘full refresh’ update, so there are points at which we will need to wait for the display to finish updating its pixels before we can send it more data.

The BSY pin should be configured as input, optionally with a pull-up resistor. The datasheet seems to say that the ‘Busy’ signal is held low while the device is busy, but this appears to be a misprint; most libraries and examples that I’ve seen delay while the pin is held high, and in my experience it does look like the display pulls the BSY pin low when it is idle.

Step 2: Initializing the display.

We will use the same hspi_w8 and hspi_cmd methods from the ILI9341 tutorial; the epaper display uses the same convention where a low D/C signal indicates a command, and a high D/C signal indicates data. Like the ILI9341, the ‘option’ bytes which follow most commands should be sent as data.

So, here is a basic initialization sequence mostly gleaned from Waveshare’s example project. You can also check the command descriptions in the module’s datasheet:

Step 3: Drawing to a Framebuffer

This is actually a pretty high-resolution display; 128 * 296 / 8 = 4736 Bytes, which is why the STM32F031K6 was not suitable for this tutorial; it only has 4KB of RAM. The display is monochrome, so each pixel only needs one bit and we’ll write 8 pixels to the display at once when we write a byte. I’ll store the framebuffer as an array of 8-bit integers (uint8_t), with dimensions based on the width and height of the display:

To test that the display works, we can define a few basic drawing methods, based on the epaper display’s settings; each byte is 8 horizontal pixels, and the vertical rows are offset by 128 / 8 = 16 bytes per row:

Step 4: Drawing to the Display

With some pixels in the framebuffer, drawing to the epaper display is very similar to drawing to the OLED or TFT display. The initialization method already set the display area to cover the whole screen, so all we have to do is send a ‘write to display RAM’ command, followed by the 4736 bytes of display data. But since epaper displays can take awhile to update, the display won’t actually change until we send another series of commands:

With the ‘full update’ LUT setting that we used in the initialization (it was a default used in the example code from Waveshare), the display will flash on and off a couple of times before settling on the image we sent to the display. It does this to make sure that there aren’t any colors left ‘burnt in’ from a previous update, but that is beyond the scope of this tutorial.

After running the initialization code, setting some starting values in your framebuffer, and drawing those values to the display, it should refresh with your image:

Rectangles drawn to the Eink display.

Conclusions

These displays are fun for all kinds of reasons. Apparently they are often used as displays on store shelves, and they are promising for low-power applications which don’t need to update their displayed information very often. And while I haven’t had a chance to play with the partial refresh modes yet, it sounds like you can get some cool effects by playing around with those. Here’s a link to an example project again, and I hope this has been helpful!

Related posts:

Like many of us, I’ve been stuck indoors without much to do for the past month or so. Unfortunately, I’m also in the process of moving, so I don’t know anyone in the local area and most of my ‘maker’ equipment is in storage. But there’s not much point in sulking for N months straight, so I’ve been looking at this as an opportunity to learn about designing and implementing FPGA circuits.

I tried getting into Verilog a little while ago, but that didn’t go too well. I did manage to write a simple WS2812B “NeoPixel” driver, but it was clunky and I got bored soon after. In my defense, Verilog and VHDL are not exactly user-friendly or easy to learn. They can do amazing things in the hands of people who know how to use them, but they also have a steep learning curve.

Luckily for us novices, open-source FPGA development tools have advanced in leaps and bounds over the past few years. The yosys and nextpnr projects have provided free and (mostly) vendor-agnostic tools to build designs for real hardware. And a handful of high-level code generators have also emerged to do the heavy lifting of generating Verilog or VHDL code from more user-friendly languages. Examples of those include the SpinalHDL Scala libraries, and the nMigen Python libraries which I’ll be talking about in this post.

I’ve been using nMigen to write a simple RISC-V microcontroller over the past couple of months, mostly as a learning exercise. But I also like the idea of using an open-source MCU for smaller projects where I would currently use something like an STM32 or MSP430. And most importantly, I really want some dedicated peripherals for driving cheap addressable “NeoPixel” LEDs; I’m tired of needing to mis-use a SPI peripheral or write carefully-timed assembly code which cannot run while interrupts are active.

But that will have to wait for a follow-up post; for now, I’m going to talk about some simpler tasks to introduce nMigen. In this post, we will learn how to read “program data” from the SPI Flash chip on an iCE40 FPGA board, and how to use that data to light up the on-board LEDs in programmable patterns.

The LEDs on these boards are very bright, because you’re supposed to use PWM to drive them.

The target hardware will be an iCE40UP5K-SG48 chip, but nMigen is cross-platform so it should be easy to adapt this code for other FPGAs. If you want to follow along, you can find a 48-pin iCE40UP5K on an $8-20 “Upduino” board or a $50 Lattice evaluation board. If you get an “Upduino”, be careful not to mis-configure the SPI Flash pins; theoretically, you could effectively brick the board if you made it impossible to communicate with the Flash chip. The Lattice evaluation board has jumpers which you could unplug to recover if that happens, but I don’t think that the code presented here should cause those sorts of problems. I haven’t managed to brick anything yet, knock on wood…

Be aware that the Upduino v1 board is cheaper because it does not include the FT2232 USB/SPI chip which the toolchain expects to communicate with, so if you decide to use that option, you’ll need to know how to manually write a binary file to SPI Flash in lieu of the iceprog commands listed later in this post.

I’ve written a little bit in the past about how to design a basic STM32 breakout board, and how to write simple software that runs on these kinds of microcontrollers. But let’s be honest: there’s still a bit of a gap between creating a small breakout board to blink an LED, and building hardware / software for a ‘real-world’ application. Personally, I would still want a couple of more experienced engineers to double-check any designs that I wanted to be reliable enough for other people to use, but building more complex applications is a great way to help yourself learn.

So in this post, I’m going to walk through the process of designing a small ‘gameboy’-style handheld with a GPS receiver and microSD card slot, for exploring the outdoors instead of video games. Don’t get me wrong, you could still write games to run on this if you wanted to, and that would be fun, but everyone and their dog has made a Cortex-M-based handheld game console by now; there are plenty of better guides for that, and many of those authors put a lot more time into their designs and firmware than I ever did.

Assembled GPS Doohicky. I left too much room between the ribbon connector footprint and the edge of the board on this first revision, so the display couldn’t fold over quite right. Oh well, you live and learn.

The board design isn’t too complicated, but there are several different parts and it gets easier to make small-but-important mistakes as a design gets larger. It mostly uses peripherals that I’ve talked about previously, but there are a couple of new ones too. The display will be driven over SPI, the speaker uses a DAC, the GPS receiver talks over UART, the battery and light levels will be read using an ADC, and the buttons will be listened to using interrupts. But I haven’t written about the USB or SD card (“MMC”) peripherals, and those will need to go in a future post since I haven’t actually worked them out myself yet. Note that SD cards can technically use either SPI or SD/MMC to communicate, but the microcontroller that I picked has a dedicated SD/MMC peripheral, and I wanted to learn about it.

Several years ago, a company called Future Technology Devices International (FTDI) sold what may have been the most popular USB / Serial converter on the market at the time, called the FT232R. But this post is not about the FT232R, because that chip is now known for its sordid history. Year after year, FTDI enjoyed their successful chip’s market position – some would say that they rested too long on their laurels without innovating or reducing prices. Eventually, small microcontrollers advanced to the point where it was possible to program a cheap MCU to identify itself as an FT232R chip and do the same work, so a number of manufacturers with questionable ethics did just that. FTDI took issue with the blatant counterfeiting, but they were unable to resolve their dispute through the legal system to their satisfaction, possibly because most of the counterfeiters were overseas and difficult to definitively trace down. Eventually, they had the bright idea of publishing a driver update which caused the counterfeit chips to stop working when they were plugged into a machine with the newest drivers.

FTDI may have technically been within their rights to do that, but it turned out to be a mistake as far as the market was concerned – as a business case study, this shows why you should not target your customers in retaliation for the actions of a 3rd party. Not many of FTDI’s customers were aware that they had counterfeit chips in their supply lines – many companies don’t even do their own purchasing of individual components – so companies around the world started to get unexpected angry calls from customers whose toy/media device/etc mysteriously stopped working after being plugged into a Windows machine. You might say that this (and the ensuing returns) left a bad taste in their mouths, so while FTDI has since recanted, a large vacuum opened up in the USB / Serial converter market almost overnight.

Okay, that might be a bit of a dramatized and biased take, but I don’t like it when companies abuse their market positions. Chips like the CH340 and CH330 were already entering the low end of the market with ultra-affordable and easy-to-assemble solutions, but I haven’t seen them much outside of Chinese boards, possibly due to a lack of multilingual documentation or availability from Western distributors. So at least in the US, the most popular successor to the FT232R seems to have been Silicon Labs’ CP2102N.

It’s nice to have a cheap-and-cheerful way to put a USB plug which speaks UART onto your microcontroller boards, so in this post, I’ll review how to make a simple USB / UART converter using the CP2102N. The chip comes in 20-, 24-, and 28-pin variants – I’ll use the 24-pin one because it’s smaller than the 28-pin one and the 20-pin one looks like it has some weird corner pads that might be hard to solder. We’ll end up with a simple, small board that you can plug into a USB port to talk UART:

Drivers for the CP2102N are included in most popular OSes these days, including Linux distributions, so it’s mostly plug-and-play.

It’s worth noting that you can buy minimal CP2102N boards from AliExpress or TaoBao for about $1, but where’s the fun in that?