More displays, less frustration

Intro

I've been fascinated with pixels and digital displays as long as I can remember (a very long time). Over the last few years, I've written and published a few libraries for microcontrollers to talk to various types of low cost displays. I'm constantly adding support for more displays, but lately I've been focused on making my code easier to use. I've had my share of frustrations connecting and using all of these displays, so I can sympathize with people who are new to microcontrollers. The point of this blog post is to help you avoid many of these frustrations.

Start from the beginning

Choosing the right display for your project is a subject for another blog post, instead let's address a few of the common impediments to having things just work.  Here are a few sources of frustration:

  • Voltage / signal mismatch
  • Solderless breadboard flaky connections
  • I2C/SPI wiring problems
  • I2C/SPI bus initialization problems
Voltage / signal mismatch
For many years, 'Arduino' referred to a development board based on one of the Atmel AVR microcontrollers and had a Vcc/signal level of 5 volts. The industry has migrated away from 5V logic levels and today (August 2022), 3.3v is the standard for nearly all microcontrollers, sensors and displays. So what happens when you connect 5 Volt signal levels to electronics expecting 3.3 Volts? Let's just say that the phrase "magic smoke" was coined for situations like this. The solution is called "level translation". Some display board vendors like Adafruit add level translation logic to all (most?) of their display boards so that any MCU can drive them with logic levels between 3.3V and 5V without hurting the display or the MCU. If you're using a more modern MCU such as an ESP32, Arm Cortex-M or RISC-V, then you don't have to worry about this issue because those all run on 3.3V and all of the displays that have crossed my desk use 3.3V signals.

Solderless Breadboards
I'm totally on board with using solderless breadboards to quickly prototype circuits. There are high quality ones and low quality ones. Unfortunately most people end up getting the low quality ones because they're so inexpensive and often tossed into "IoT Kits". The difference in quality usually comes down to the type of metal used (cheap ones can oxidize), the age of the wires (may oxidize or have poor end contacts), and the robustness of the springs holding the wires in place. Flaky breadboard connections will send you down endless rabbit holes trying to find the source of trouble (bad component? wrong wiring? wrong software?). Another factor working against solderless breadboards is that the more connections there are, the higher the probability of it not working. If I'm working on something that I'll use for a few minutes/hours and then tear down, I try to get it working on a breadboard (see photo, left). If it's something I'm going to use in that configuration for a long time, then I usually make the extra effort of soldering a double-sided tinned protoboard version (see photo, right). This has saved me a lot of frustration. 




I2C Problems
A whole book can be written about this topic, but I'll try to narrow it down to the most helpful bits. The AVR MCUs had fixed function pins so that the pins used for I2C and SPI were always the same ones and there was no doubt about how to properly initialize and use them. Newer MCUs have more flexible pin functions. I'll use the ESP32-S2 in the photo above as an example. The Qwiic connector on the board is the obvious and logical way to easily connect I2C devices to it; that's what it was made for. However, the pins chosen for that connector (GPIO41/SDA & GPIO40/SCL) are defined as Wire device 1 whereas Wire device 0 is brought out to the 0.1" pins on the side of the PCB (GPIO7/SDA, GPIO6/SCL). This means that a lot of software won't work "out of the box" because nearly every example sketch and library which uses the Wire library is hard coded to use the first Wire device (device 0). I make this easy to manage with my OneBitDisplay library (explained below in the second example). Other common issues with I2C devices are whether or not pullup resistors are needed. Clarification - they're ALWAYS needed, but some sensors/displays provide them and others don't. Most I2C OLEDs sold on breakout boards come with pullup resistors, so you can relax about adding them.

SPI Problems
SPI normally makes use of 3 connections: data out (MOSI), data in (MISO) and the clock signal. There are two "flavors" of SPI used by displays: 3-wire and 4-wire. 4-wire refers to the use of a 4th wire for telling the display if you're sending it a command or pixel data (usually labeled the D/C or RS pin). The 3-wire mode means that the Data/Command info is sent as a 9th bit with each data byte. Most experimenter displays sold in retail channels are configured as 4-wire SPI due to the complexity of getting SPI hardware/software to work with 9-bit words. Once you get that sorted, the next point of frustration usually comes from matching the TX/RX pins from your MCU to the display. In SPI terminology, the outbound data from your MCU is labeled MOSI (master out / slave in) and the inbound data on the display may be labeled "SDI/DIN/SDA = serial data in".  Additional wires need to be connected for CS (chip select), and RST (reset). Leaving any of these extra signals disconnected or in the wrong state will usually cause the display to not work. Once you have the wires connected properly, the software setup is the next area of frustration. SPI has several configuration options and most displays are set up for:
- MSB first (the most significant bit is each byte sent first)
- SPI mode refers to the phase of the clock and data (0-3, 0 is the usual mode supported)
- Clock frequency - it's best to check the data sheet of your display. The slowest speed is usually down around 1Mhz and the fastest could support up to 60Mhz or even beyond.

Display Configurations - from simplest to most complicated

1) Simplest
I2C is the easiest to get going because it has the fewest connections (VCC, GND, SDA, SCL). If your MCU has default I2C pins that are easy to access, then this is the place to start. The most ubiquitous display devices with an I2C interface are the SSD1306 monochrome OLEDs. They can be bought from a variety of vendors in a variety of sizes and colors, but are generally the least expensive and easiest to get going.  Out of the various types of SSD1306's, the most common is the 128x64 (see photo). Here it is connected to an UnexpectedMaker FeatherS3 (ESP32-S3) through the Qwiic connector with a custom case I created.


I recently made changes to my OneBitDisplay library to make this work with minimum frustration. In the code below, the I2Cbegin() method has no parameters - it defaults to a SSD1306 128x64 OLED on the Wire 0 device. If you have a different display or it's connected differently you can provide additional info to configure it. With a simple starting point that works, you can focus on your project.



2) Slightly Challenging
You've connected a display with I2C, but it's not on the default pins. This is more likely to occur on ESP32 boards because the pin definitions are pretty flexible. For this example I'm going to use the Adafruit Qt Py-S2 (ESP32-S2). It also has a Qwiic connector, but the GPIO pins used for it are set up as Wire1 (second Wire device), not Wire0. This means that most software hardcoded to talk to Wire device 0 won't work through the Qwiic connector. I added code to make this situation easier too. For the specific case of the Qt Py-S2, add this line before the I2Cbegin():

obd.setI2CPins(SDA1, SCL1);

Those names are defined in the pins_arduino.h file of the Qt Py-S2 variant as SDA1 = 41 and SCL1 = 40. They're also on the pinout image provided by Adafruit (see below). You can use the numbers 41 and 40 in place of the defined names.



3) Moderately Challenging
Connecting an SPI display to your MCU brings a whole new level of complexity to your project. If things don't work on the first try, you've got a longer list of potential points of failure (wiring problem, SPI configuration problem, software problem). In this example I'll connect an SPI version of an OLED display to the same Qt Py-S2 we used in the previous example. With so few exposed pins on the board, it actually makes it easier to configure since there are limited ways to make it work. Some of the pins are already assigned for you (MOSI, SCK). The display has 8 pins exposed (GND, 3.3V, SDI, SCK, SDO, D/C, RST, CS). I'm only going to connect 7 of them (we don't need to read data from the display for our use case). Here are the connections (MCU on the left, display on the right)

GND --- GND
3.3V  --- 3.3V
MOSI --- SDI
SCK  --- SCK
A0    ---   CS
A1    ---   D/C
A2    ---   RST 

This display also has an 8-bit parallel option. LOTS OF CONNECTIONS to make that work!


The code to initialize the SPI display is slightly different; it's also necessary to explicitly specify the display type (OLED_64x48). The MOSI and SCK pins are set to -1 to use the system default values for this board.

** OLED Pro Tip **
There's no real advantage to connecting a small OLED display using SPI (or parallel) compared to I2C. The I2C interface can run at 1Mhz and the display can be updated faster than your eye can see it. By using SPI/parallel you will be connecting more wires and likely will encounter more frustrations.

4) Very Challenging (proceed at your own risk 😜)
The last level of challenging displays (8/16-bit parallel) goes beyond just the number of connections, but getting working software. Connecting low cost color LCDs with parallel interfaces is the most challenging topic I'm going to cover in this blog post. Why subject yourself to this (highest) level of frustration? Speed! Color LCDs with a large number of pixels (e.g. 320x480) normally have a SPI 3/4-wire and parallel interface option (switched with some strapping pins).  For this example, the number of bits required to redraw the entire display is 320x480x16 = 2,457,600. If the display is connected via SPI running at 20Mhz (a high speed for SPI), then the maximum possible full screen refresh rate will be 20,000,000 / 2,457,600 = 8.138 fps. Not a very impressive number. However, that same display can receive 16-bits @ 20Mhz too; this would allow a full screen refresh rate of > 120 FPS. Even sending 8 bits at a time would get us to > 60FPS.  8-bit parallel LCDs have been sold for use on AVR (Arduino UNO) boards for many years. Here's an example of a 320x480 color LCD with an 8-bit parallel interface:


This display needs nearly every GPIO available on the UNO to work. Since the UNO doesn't have a specific resource for driving an 8-bit parallel interface, the required software simply "bit bangs" the data to the GPIO pins as fast as it can. This involves writing the 8 data bits to a pair of 8-bit GPIO ports in parallel and then toggling the WR (write signal) bit with a different GPIO port. The most efficient way to do this on the UNO was with direct port manipulation like this code below:

#define WR_ACTIVE  *wrPort &=  wrPinUnset

#define WR_IDLE    *wrPort |=  wrPinSet

#define WR_STROBE { WR_ACTIVE; WR_IDLE; }

#define write8(d) { PORTD = (PORTD & ~DMASK) | ((d) & DMASK); PORTB = (PORTB & ~BMASK) | ((d) & BMASK); WR_STROBE; }


Without hardware to help it along, it needs quite a few instructions to send each byte of data. More modern MCUs have additional hardware to handle the same task without CPU intervention. Modern Cortex-M and ESP32 MCUs have DMA controllers, I2S hardware and Programmed I/O state machines to do this work without needing the CPU involved. I'm going to use the Pimoroni Tufty2040 as an example of the complexity of using this interface along with its benefits. This product is listed as a 'badge'; it's a Raspberry Pi Pico MCU connected with an 8-bit parallel interface to a Sitronix ST7789 IPS 240x320 LCD. The RPI Pico has an I/O state machine (PIO) with DMA that can create precisely timed output without help from the CPU. For this product, Pimoroni wrote a 2-line PIO program to output data as quickly as possible to the LCD. The fastest the PIO program can run successfully is CPU_CLOCK / 4. In this case that means 133Mhz / (4 * 2 PIO instructions) = 16.6Mhz. 8-bit data pushed to the display at 16.6Mhz gives a maximum full screen refresh rate of:

16,625,000 / (240x320x2) = 108 FPS. With DMA doing the 'pushing' you can potentially have the CPU do its work while the DMA is sending the previous frame. I used Pimoroni's 2-line PIO program (with full attribution) into my bb_spi_lcd library and was able to generate the following demo:


There's a lot of support code needed beyond the 2-line PIO program to make this work. My point in bringing this to your attention is that these little inexpensive LCDs are capable of impressive speed if you put in the extra effort. Now it wouldn't be very useful to just tease you with how frustrating it is to use parallel LCDs and then not provide a solution. I've been working with 3 platforms (ESP32-S2/S3, RP2040 and Teensy 4.1) to provide a simple way to use parallel (8 and 16-bit) displays with my bb_spi_lcd library. Here's how you initialize the Tufty2040 display with my parallel LCD API:


lcd.begin(DISPLAY_TUFTY2040);


Okay, that's kind of cheating - the details are hidden because I added it to my "named displays" list. Popular MCU/display combos get an easy way to initialize them. A better example is my Teensy 4.1 adapter for that Arduino UNO TFT shield listed above. Here's the setup instructions for that combo:


// Teensy 4.1 + Kuman 3.5" LCD Shield
#define BUS_WIDTH 8
#define LCD_RD 7
#define LCD_WR 8
#define LCD_CS 10
#define LCD_DC 9
#define LCD_RST 11
#define LCD_TYPE LCD_ILI9486
#define LCD_FLAGS (FLAGS_SWAP_RB | FLAGS_FLIPX)
const uint8_t u8Pins[BUS_WIDTH] = {19,18,14,15,40,41,17,16};

lcd.beginParallel(LCD_TYPE, LCD_FLAGS, LCD_RST, LCD_RD, LCD_WR, LCD_CS, LCD_DC, BUS_WIDTH, u8Pins);

A similar initialization sequence is how you use a ESP32-S2/S3 also. For the ESP32, Espressif has an LCD API which initializes their parallel I2S hardware, so I used their API to initialize the hardware and my LCD support software from that point forward. Here's the Teensy 4.1 displaying a 480x320 GIF animation on that display:

I haven't done an official release yet of this parallel LCD code because I'm still doing some cleanup on it. I'll notify everyone when it's ready to use.

Comments

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