Microcontroller vs Display
date
Apr 2, 2024
type
Post
year
slug
microcontroller-vs-display
status
Published
tags
Microcontroller
Arduino
Electronics
Research
summary
Everything you need to know to start attaching displays to your microcontrollers
Connecting Displays to Microcontrollers is … fun.
There’s quite a few different technologies to be aware of. Displays that use the same technology can have wildly different labeling. And the labeling on the pinout of the Microcontroller might also not be helpful in letting you know where to plug what into.
Look at these two 1.8 inch TFT LCD displays for example:
They
✅ look the same
✅ they both use
SPI
✅ both run with a ST7735
driver
But look at their pinout! It’s almost completely different!! 🙀Red Display:
LED
, SCK
, SDA
, A0
, Reset
, CS
, GND
, VCC
Blue Display: RST
, CS
, D/C
, DIN
, CLK
, VCC
, BL
, GND
Now look at the pinout of a recently released MicroController like the ESP32-S3 dev kit… Should the
CLK
pin on the display be connected to one of the many pins that have CLK in their name? Like FSPICLK
maybe? It’s got “SPI” and “CLK” in it, no idea what the “F” means though. 🤷♂️ Well… good luck.Order into Chaos
It might not look like it, but both displays actually have pretty much the same pins - just in different order and they use very different abbreviations for the same things. They both use
SPI
after all, but… what even is that? Let’s look at the different protocols that microcontrollers can use to communicate with displays…
Communication Protocols
TL;DR: ➡️ use SPI. It has a few more wires than I2C, but is much faster! (So if you’re building your own mini handheld gaming console, that’s probably the way to go), I2C is totally fine for most things, UART is fine if you just want to display some text. Parallel is only needed in extreme performance cases and requires a whole lot of pins.
UART - Universal Asynchronous Receiver/Transmitter)
4 Pins, probably SLOW
Not so much a protocol, but a circuit for asynchronous serial communication. It has one wire for transmitting (TX) and one wire for receiving (RX) with a configurable transmission speed.
The big downside of UART is that it’s really only made for one thing to talk to one other thing, so - unless your microcontroller has multiple sets of TX/RX pins - you can’t hook up multiple things via UART.
Possible Pin Labels | Display Pin Description |
VCC, 5V, 3V3 | Power In |
GND, G | Ground |
RX | Receive |
TX | Transmit |
I2C (Inter-Integrated Circuit)
4 Pins, Okay for small displays
A synchronous multi-master, multi-slave serial communication bus. Allows for complex communication scenarios. Only has a single data line that is used for sending and receiving (it uses packets to make sure everything gets where its supposed to go), and a clock line to synchronize data transmission. Slower than SPI, but totally fine if you’re not doing anything too crazy
Possible Pin Labels | Display Pin Description |
VCC, 5V, 3V3 | Power In |
GND, G | Ground - that one’s easy. |
SDL | Serial Data Line - the data line used for transmitting data packets |
SCL | Serial Clock Line - provides the timing signal for data transfer |
SPI - Serial Peripheral Interface
7 Pins, Best for small displays - Power, Ground, Backlight and four main lines: MISO (Master In Slave Out), MOSI (Master Out Slave In), SCK (Serial Clock) and SS (Slave Select).
SPI is a good way for a microcontroller to talk to multiple devices. The Controller (Master) device controls the communication and the Responder (Slave) responds.
SPI has good speed and efficiency (higher speeds than I2C). It doesn’t require a complex protocol and can transfer Data in a full-duplex manner (send and receive at the same time).
Most displays use a simplified version that doesn’t include a way for the display to talk back to the microcontroller. They basically only do MOSI (Master Out Slave In) and ignore MISO (Master In Slave Out) and instead use a Data/Command line that tells the display if it’s receiving pixel data or commands for the display controller.
Possible Pin Labels | Display Pin Description |
VCC, 5V, 3V3 | Power In (Notice how my blue display expects 5V but can be changed to 3V by soldering 2 blobs together on the back. The red one doesn’t say anything, but I know that it can handle 5V on the VCC, but is expecting only 3V on the data pins) |
GND, G | Ground - that one’s easy. |
LED, BL, BLK | Backlight - provide power to the backlight LED, you can adjust the brightness by sending more or less V |
CS, SS | Chip Select - Used to activate and deactivate the device on the SPI bus |
A0, DC, D/C | Address Select or Data/Command - Used to switch between command mode and data mode |
SDA, DIN | Serial Data - the data line used for sending data |
SCK, CLK | Serial Clock - provides the timing signal for data transfer |
Parallel
Many Pins, Super fast, but … did I mention so many pins?
Multiple Data Lines! (With 8bit, meaning 8 lines, it can send a whole byte at once) Plus some extra connections for other stuff.
Using Parallel might make sense for higher screen resolutions where you need to send a lot of data for each frame.
Haven’t tried this personally yet. Just know that this exists.
Hooking things up
Armed with this knowledge we can now begin to think about hooking things up.
We’ll need to:
- Connect all of this to relevant pins on our Microcontroller of choice
- Find a display Library that supports our display and configure it
But which are the relevant pins and what library should we use?
Well, as far as I can tell, microcontrollers are so configurable that it doesn’t matter all too much which pins you use as long as you then configure your library correctly.
For example: This is how I hooked up the red 1.8” TFT LCD display to an ESP32-S3-DevKitC-1:
TFT | ESP32-S3 | Description |
VCC | 3V3 | Power |
GND | G | Ground |
CS | 5 | Chip Select |
RESET | 7 | Reset |
AO | 6 | Address Select or Data/Command |
SDA | 15 | Serial Data (sending and receiving data) |
SCK | 16 | Serial Clock |
LED | 3v3 | Backlight |
Take note of how I’m not using any of the ports that have SPI in their name, or CLK or anything like that. But you do want to stick to 3v3 and GND of course… 😉
The important part is to take note of the GPIO pins and configure your display library accordingly!
🎵 Intermission - Framebuffer 🎵
The ⚠️ one thing you have to keep in mind with all display libraries is that you don’t want to draw directly to the screen!
If you clear the screen (or parts of it) and draw individual bits and pieces directly onto it, then you will get flickering. Why? Because on LCD and OLED displays every instruction gets executed immediately. So if you clear the screen, you will see a black screen before you get to the following instruction that draws something onto the blackness again.
The age-old solution to this issue is to draw everything into a buffer (”framebuffer”) and then plot the entire buffer onto the screen at once. Fixed!
Can your microcontroller even handle a color display?
The biggest limitation for using framebuffers with microcontrollers is RAM. You usually want to keep an entire screen’s worth of buffer in RAM. If you work with 1-bit color (meaning every pixel could be represented by 1 bit - black or white), then a 128x64 pixel display would take 8192 bit = 1024 byte = 1KB to keep in RAM. The same size buffer with 16-bit (65.000 possible colors, every pixel represented by 16 bit = 2 bytes) would require 131,072 bit = 16,384 byte = 16.4KB.
An Arduino UNO has 2KB of RAM, so no chance of fitting that. An ESP32-C6 on the other hand has 520KB of RAM (not all usable, but still), so has no problem at all with handling small color displays!
But even if your microcontroller can’t fit a whole screen’s worth of framebuffer into RAM, it’s not hopeless.
- You could reduce the color depth and use a 4-bit buffer (16 colors) or 8-bit (256 colors), significantly reducing your memory requirements.
- You could split the screen into multiple sections and update them by reusing the same smaller buffer. For example: split the screen into quarters, then update one after the other with every quarter reusing the same framebuffer.
- Or you can always draw directly to the screen, be as smart as you can about which parts of the display you redraw and simply live with some flickering.
Ok, now: Some display library recommendations:
Display Libraries
u8g2
⚫⚪ For monochrome displays → 🌐 https://github.com/olikraus/u8g2
Monochrome library that supports all sorts of small OLED and LCD displays.
The quickest way to get it working is to open a sample like
File > Samples > U8g2 > full_buffer > GraphicsTest
, then scroll down the long list of commented-out setups and find the option that’s closest to the display you have, copy that line and set up your wires accordingly.The easiest way to use this library is via the so-called “picture loop” (Example from docs):
#include <Arduino.h> #include <SPI.h> #include <U8g2lib.h> /* Constructor */ U8G2_UC1701_DOGS102_1_4W_SW_SPI u8g2(U8G2_R0, /* clock=*/ 13, /* data=*/ 11, /* cs=*/ 10, /* dc=*/ 9, /* reset=*/ 8); /* u8g2.begin() is required and will sent the setup/init sequence to the display */ void setup(void) { u8g2.begin(); } /* draw something on the display with the `firstPage()`/`nextPage()` loop*/ void loop(void) { u8g2.firstPage(); do { /* all graphics commands have to appear within the loop body. */ u8g2.setFont(u8g2_font_ncenB14_tr); u8g2.drawStr(0,20,"Hello World!"); } while ( u8g2.nextPage() ); delay(1000); }
You call
u8g2.firstPage();
, then start a do
/while
loop where you do all your actual drawing, then end it with u8g2.nextPage();
TFT_eSPI
🔴🟢🔵 For color displays → 🌐 https://github.com/Bodmer/TFT_eSPI
Great and fast library with optimizations for ESP32 (and others) and even support for Parallel
To configure this library you need to edit the
User_Setup.h
file in its library folder. There’s a lot of options, so again: find the closest match and go from there. (There’s a User_Setups
folder inside its library folder with a lot of configurations. Have a browse.)Once set up, grab the Graphics_Test example and build it to run a benchmark. Then start adjusting the
#define SPI_FREQUENCY
upwards and see how high you can go with your device/display. 4000000
is working well on my ESP32-S3!Framebuffer is handled via
Sprites
. You draw into a sprite, then push it to a specific position on the display via pushSprite(x, y)
. Here’s a condensed version of the Sprite_draw example:
// Width and height of sprite #define WIDTH 128 #define HEIGHT 160 #include <Arduino.h> #include <TFT_eSPI.h> // Include the graphics library (this includes the sprite functions) TFT_eSPI tft = TFT_eSPI(); // Declare object "tft" TFT_eSprite spr = TFT_eSprite(&tft); // Declare Sprite object "spr" with pointer to "tft" object void setup() { tft.init(); // Initialise the TFT registers spr.setColorDepth(8); // Optionally set colour depth to 8 or 16 bits, default is 16 if not specified spr.createSprite(WIDTH, HEIGHT); // Create a sprite of defined size tft.fillScreen(TFT_BLUE); // Clear the TFT screen to blue } void loop(void) { spr.fillSprite(TFT_BLACK); // Fill the whole sprite with black (Sprite is in memory so not visible yet) // Number of pixels to draw uint16_t n = 100; // Draw 100 random colour pixels at random positions in sprite while (n--) { uint16_t colour = random(0x10000); // Returns colour 0 - 0xFFFF int16_t x = random(WIDTH); // Random x coordinate int16_t y = random(HEIGHT); // Random y coordinate spr.drawPixel( x, y, colour); // Draw pixel in sprite } // Draw some text with Middle Centre datum spr.setTextDatum(MC_DATUM); spr.drawString("Sprite", WIDTH / 2, HEIGHT / 2, 4); spr.pushSprite(0, 0); // Now push the sprite to the TFT at position 0,0 on screen delay(100); }
Adafruit GFX + Display-Specific Library like Adafruit_ST7735
🔴🟢🔵 For color displays → 🌐 https://learn.adafruit.com/adafruit-gfx-graphics-library/overview
Adafruit has a lot of individual libraries out there for all sorts of display drivers. All of them depend on the Adafruit GFX library.
And again: Look through the examples and pick a setup that’s as close as possible to your display type.
With Adafruit GFX you can implement a Framebuffer using the
GFXcanvas1
(for 1-bit), GFXcanvas8
(for 8-bit) and GFXcanvas16
(for 16-bit) classes.Here’s a condensed combination of several examples, showing you how to draw to the screen and to a framebuffer:
#include <Adafruit_GFX.h> // Core graphics library #include <Adafruit_ST7789.h> // Hardware-specific library for ST7789 // Define Pins #define TFT_CS 4 #define TFT_RST 16 #define TFT_DC 5 Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST); void setup() { // Draw some stuff directly onto the screen tft.initR(INITR_BLACKTAB); // Init ST7735S chip, black tab tft.fillScreen(ST77XX_BLACK); tft.fillRoundRect(25, 10, 78, 60, 8, ST77XX_WHITE); tft.fillTriangle(42, 20, 42, 60, 90, 40, ST77XX_RED); delay(1000); } GFXcanvas1 canvas(128, 64); // 1-bit, 128x64 pixels void loop() { canvas.fillScreen(0); // Clear canvas (not display) canvas.setCursor(0, 24); // Pos. is BASE LINE when using fonts! canvas.print(millis()); // Print elapsed time in milliseconds // Copy canvas to screen at upper-left corner. As written here, assumes a color LCD, hence the color values of 0xFFFF (white) // for foreground, 0x0000 (black) for background. Mono OLED can use 1 and 0. BOTH colors must be specified to overwrite the // prior screen contents there. display.drawBitmap(0, 0, canvas.getBuffer(), canvas.width(), canvas.height(), 0xFFFF, 0x0000); }
GxEPD2
⚫⚪🔴 For e-Ink displays → 🌐 https://github.com/ZinggJM/GxEPD2
A library for e-Paper displays. Usage is very similar to TFT_eSPI.
Here’s a condensed example of how to get a 1.54” e-Paper Module from WeAct Studio to work with an ESP8266 LoLin NodeMcu v3 using GxEPD2:
#include <ESP8266WiFi.h> // base class GxEPD2_GFX can be used to pass references or pointers to the display instance as parameter, uses ~1.2k more code // enable or disable GxEPD2_GFX base class #define ENABLE_GxEPD2_GFX 0 #include <GxEPD2_BW.h> #include <GxEPD2_3C.h> #include <Fonts/FreeMonoBold9pt7b.h> // ESP8266 CS(SS)=15,SCL(SCK)=14,SDA(MOSI)=13,BUSY=16,RES(RST)=5,DC=4 // 1.54'' EPD Module GxEPD2_BW<GxEPD2_154_D67, GxEPD2_154_D67::HEIGHT> display(GxEPD2_154_D67(/*CS=5*/ 15, /*DC=*/ 4, /*RES=*/ 5, /*BUSY=*/ 16)); // GDEH0154D67 200x200, SSD1681 // 2.13'' EPD Module //GxEPD2_BW<GxEPD2_213_BN, GxEPD2_213_BN::HEIGHT> display(GxEPD2_213_BN(/*CS=5*/ 15, /*DC=*/4, /*RES=*/5, /*BUSY=*/16)); // DEPG0213BN 122x250, SSD1680 //GxEPD2_3C<GxEPD2_213_Z98c, GxEPD2_213_Z98c::HEIGHT> display(GxEPD2_213_Z98c(/*CS=5*/ 15, /*DC=*/ 4, /*RES=*/ 5, /*BUSY=*/ 16)); // GDEY0213Z98 122x250, SSD1680 // 2.9'' EPD Module //GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> display(GxEPD2_290_BS(/*CS=5*/ 15, /*DC=*/ 4, /*RES=*/ 5, /*BUSY=*/ 16)); // DEPG0290BS 128x296, SSD1680 //GxEPD2_3C<GxEPD2_290_C90c, GxEPD2_290_C90c::HEIGHT> display(GxEPD2_290_C90c(/*CS=5*/ 15, /*DC=*/ 4, /*RES=*/ 5, /*BUSY=*/ 16)); // GDEM029C90 128x296, SSD1680 void setup() { display.init(115200, true, 50, false); display.setFont(&FreeMonoBold9pt7b); display.setTextColor(GxEPD_BLACK); int16_t tbx, tby; uint16_t tbw, tbh; display.getTextBounds(connectionStr, 0, 0, &tbx, &tby, &tbw, &tbh); display.setFullWindow(); display.firstPage(); do { display.fillScreen(GxEPD_WHITE); display.setCursor(8, tbh + 8); display.print("Hello"); display.setCursor(display.width() - tbw - 8, tbh + 5); display.print("World"); } while (display.nextPage()); } void loop() {}
Closing Words / Other Considerations
This should be a good starting point for anyone who wants to attach a display to their microcontroller! Let me know if you’re working on anything interesting! I’ll post more about my handheld-gaming-via-microcontroller experiments in the future…