3-Wire SPI Explained

Intro

I'm working on a new PCB project with a SSD1306 128x64 OLED display and was curious to try using 3-wire SPI mode. It's not a new subject for me, but I wanted to try using it to gain speed rather than have it slow things down (e.g. bit banging the protocol). Some background information is required before I share the project details.

Small Displays

Small displays like the SSD1306 and color LCDs like the ST7789 from Sitronix support updating their internal display RAM over a serial (SPI) connection. The display controllers understand "commands" and pixel data. The commands serve to configure the controller and move the write pointer (where the pixel data gets written). In order to know if incoming serial data should be interpreted as commands or pixels, the controllers have a D/C (data/command) signal. This can either be a separate signal wire or encoded into the data stream. When it's a separate signal, this is referred to as "4-wire SPI". The 4 wires are: Clock, Data, D/C, and Chip Select. If the D/C signal is encoded in the serial data, this is referred to as "3-wire SPI" - Clock, Data and Chip Select. Here's a screen shot from the SSD1306 data sheet showing the insertion of a 9th bit into each byte for 3-wire SPI mode:


Why Support 3-Wire SPI?

The first time I saw 3-wire SPI mode mentioned in a display controller data sheet, I wondered to myself why it exists. It seemed at the time to be a bad idea with a major pitfall. Microcontroller SPI controllers usually only support 8 and 16-bit data lengths, so getting 9 bit data out of them would be problematic. I experimented with an LCD display that only supported 3-wire mode and my solution was to use bit banging (manually toggling GPIO pins to implement the protocol). The result of this was a relatively slow implementation that needed extra code to implement the protocol on GPIO pins. For 1-bit per pixel displays like the SSD1306 and small bitonal LCDs like the ST7567, it seems counter-productive to use the 3-wire SPI mode. The display buffer is tiny (e.g. 1K byte of RAM), so using a separate D/C pin or not to update pixels won't matter in the scheme of things. The displays support SPI clock speeds in the tens of megahertz range, so the entire display memory can be rewritten many times faster than the display can show those changes. It wasn't until I started using color SPI displays that I realized why 3-wire SPI mode might be advantageous. As the displays get larger, the internal memory size grows larger, and it takes more time to redraw every pixel (full screen update). A clever feature of all of these small displays is that you can change the position (aka pixel) of the write pointer so that non-sequential pixels can be changed. The mechanism to change the write position pointer is to send a command to set the column or row address. Well... sending a command means stopping the flow of data, switching to command mode, sending the command(s), stopping the flow of data again, and then switching to data mode and sending the pixel data. A simple example - drawing a diagonal (45 degree) line. Each successive pixel is on a different column and different row, so both 3-wire and 4-wire SPI modes will need to alternate between sending position commands followed by data for a single pixel. The difference between the two SPI modes is easier to see with this worst-case example. In the 3-wire SPI case, the commands and data can all be sent in a single transaction. In the 4-wire SPI case, there are significant pauses between each pixel as the SPI hardware is stopped and a separate GPIO line is toggled. This is the main advantage of 3-wire SPI - being able to mix commands and data into a continuous stream of data. You also save one GPIO pin; that may be important for MCUs with limited GPIO available (e.g. ESP32-C3).

Bit Banging 3-Wire SPI; isn't there a better way? :)

Implementing the 9-bit protocol by manually toggling GPIO pins is functional, but not necessarily optimal. If you're not familiar with my focus (see the title of this blog), I like things to go fast. Toggling the clock and data pins manually means you're missing out on using DMA and higher clock speeds. My first thought when presented with the "9-bit problem" is to find a common denominator between 8 and 9 (72) to make it work. The basic idea is that since each 8-bit byte needs to be sent down the wire as 9 bits, if you send 9 bytes, you've sent 8 bytes plus the extra bit for each. This leads you to wonder what happens in situations where you're not sending an even multiple of 8 bytes. Well, the display controller designers foresaw that issue and added a NOP (no operation) command. All you need to do is pad your data stream to fit in a multiple of 8 bytes and you're good to go. Here is how that looks in code. The following snippet implements this idea on the CH32V003 32-bit RISC-V MCU. It combines commands to set the write row+column position with a line of image data and sends them together in a single DMA transaction that has the 9th "D/C" bit inserted in each byte.

For the project I mentioned above, I wrote code to draw custom fonts in the Adafruit_GFX format. To save SRAM (there's only 2K on the MCU), I draw each Nx8 pixel row of the characters at a time. This means that the display update shown in the video below is doing 16 write transactions per change in the numbers. Fast, but it could be much faster if I used more RAM to buffer all of the display data that's changing.

Wrap Up

3-Wire SPI has its pluses and minuses:

  • + Streamlines display updates which move the write pointer
  • - Complicates SPI data transmission on systems without 9-bit support
  • + Saves one GPIO line
  • - Increases the pixel data size by 1 extra bit per byte


Comments

  1. Great blog! Very detailed, gives a clarity of what 3-wire SPI is for display and how it works.

    I feel like I should try it asap :)

    ReplyDelete

Post a Comment

Popular posts from this blog

How much current do OLED displays use?

Fast SSD1306 OLED drawing with I2C bit banging

Building the Pocket CO2 Project