Three small screens on an Arduino Uno
A 128x32 SSD1306, a 1.3 inch SH1106 and a Nokia 5110 on one Uno, each started by hand: the charge pump, column 2 and Vop. The whole build, an Arduino Uno and 3 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Advanced About 40 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $43. On Mokxi they're free.
Step by step, with the wiring and the common mistakes: Arduino Nokia 5110 LCD: Wiring, Contrast and Code
What you'll learn
- Starting a display with raw command bytes
- Why two I2C devices at the same address need separate buses
- How pages and columns organize screen memory
- Why the SH1106 text starts at column 2
- Sending data to the Nokia 5110 with shiftOut
- Difficulty: Advanced
- Time: About 40 minutes
- Board: Arduino Uno R3
- Parts: 5
How it works
One Uno drives three different screens, and the sketch starts each one by hand. The two OLEDs share the I2C address 0x3C, so each gets its own bus on its own pair of pins: A4 and A5 for the 128x32, A2 and A3 for the 1.3 inch screen. The Nokia 5110 uses five pins, 3 to 7, plus pin 8 for its backlight.
All three store pixels as bytes of eight stacked dots. The differences are in start-up. The 128x32 needs its charge pump switched on. The SH1106 has 132 columns of memory, so text starts at column 2. The Nokia shows nothing until its contrast, Vop, is set.
After the labels are drawn, loop writes a seconds counter to each screen every 200 ms.
Parts list
5 parts, plus the jumper wires. They retail for about $43; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Full-size breadboard
- 1 × OLED display, 128x32
- 1 × OLED display, 1.3 inch SH1106
- 1 × Nokia 5110 display, PCD8544
Wiring table
Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.
| Part and pin | Connects to |
|---|---|
| Nokia 5110 display, PCD8544 pin LIGHT | Arduino Uno R3 pin 8 |
| Nokia 5110 display, PCD8544 pin CLK | Arduino Uno R3 pin 7 |
| Nokia 5110 display, PCD8544 pin DIN | Arduino Uno R3 pin 6 |
| Nokia 5110 display, PCD8544 pin DC | Arduino Uno R3 pin 5 |
| Nokia 5110 display, PCD8544 pin CE | Arduino Uno R3 pin 4 |
| Nokia 5110 display, PCD8544 pin RST | Arduino Uno R3 pin 3 |
| OLED display, 128x32 pin VCC | Arduino Uno R3 pin 5V |
| OLED display, 1.3 inch SH1106 pin VCC | Arduino Uno R3 pin 5V |
| Nokia 5110 display, PCD8544 pin VCC | Arduino Uno R3 pin 5V |
| OLED display, 128x32 pin GND | Arduino Uno R3 pin GND |
| OLED display, 1.3 inch SH1106 pin GND | Arduino Uno R3 pin GND |
| Nokia 5110 display, PCD8544 pin GND | Arduino Uno R3 pin GND |
| OLED display, 1.3 inch SH1106 pin SDA | Arduino Uno R3 pin A2 |
| OLED display, 1.3 inch SH1106 pin SCL | Arduino Uno R3 pin A3 |
| OLED display, 128x32 pin SDA | Arduino Uno R3 pin A4 |
| OLED display, 128x32 pin SCL | Arduino Uno R3 pin A5 |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Three small screens: a 128x32 SSD1306, a 1.3 inch SH1106 and a Nokia 5110,
// each driven by hand so the differences between them are in plain sight.
//
// SSD1306 128x32 SCL -> A5 SDA -> A4 address 0x3C
// SH1106 1.3 inch SCL -> A3 SDA -> A2 address 0x3C
// Nokia 5110 RST 3 CE 4 DC 5 DIN 6 CLK 7 LIGHT 8
//
// All three keep their pixels as bytes of eight stacked dots, bit 0 at the
// top, in rows of bytes called pages or banks. What differs is the start-up
// and the addressing:
//
// - the SSD1306 needs its charge pump switched on (0x8D 0x14), and a 32-row
// strip needs its multiplex (0xA8 0x1F) and COM pins (0xDA 0x02) set;
// - the SH1106 starts with its DC-DC on and has 132 columns of RAM, so every
// page is written from column 2;
// - the Nokia's contrast is Vop, which starts at 0: nothing shows until the
// sketch sets it.
//
// The two OLEDs are both at 0x3C, so each has an I2C bus of its own, bit-banged
// on two pins by mokxi_i2c.h. (On one shared bus, one of them would need its
// address jumper moved to 0x3D.) No frame buffers: this board has 2 KB of RAM
// and three screens would want more than that, so each line of text goes
// straight to its screen.
#include "mokxi_font5x7.h"
#include "mokxi_i2c.h"
const uint8_t OLED = 0x3C;
SoftI2c strip(A5, A4);
SoftI2c big(A3, A2);
const uint8_t N_RST = 3, N_CE = 4, N_DC = 5, N_DIN = 6, N_CLK = 7, N_LIGHT = 8;
// ---- the two OLEDs, each on its own bus -------------------------------------
// One transfer: the address, a control byte (0x00 commands, 0x40 display
// data), then the bytes.
static void oledSend(const SoftI2c &bus, uint8_t control, const uint8_t *bytes, uint8_t count) {
if (bus.start(OLED)) {
bus.write(control);
bus.write(bytes, count);
}
bus.stop();
}
// Point an OLED at a page and a column, the page-addressing way both chips
// understand: 0xB0 + page, then the column in two nibbles.
static void oledAt(const SoftI2c &bus, uint8_t page, uint8_t column) {
const uint8_t where[] = {(uint8_t)(0xB0 | page), (uint8_t)(column & 0x0F),
(uint8_t)(0x10 | (column >> 4))};
oledSend(bus, 0x00, where, 3);
}
static void oledClear(const SoftI2c &bus, uint8_t pages, uint8_t first) {
static const uint8_t ZEROS[16] = {0};
for (uint8_t page = 0; page < pages; page++) {
oledAt(bus, page, first);
for (uint8_t chunk = 0; chunk < 128 / 16; chunk++) oledSend(bus, 0x40, ZEROS, 16);
}
}
static void oledText(const SoftI2c &bus, uint8_t page, uint8_t column, const char *text) {
oledAt(bus, page, column);
for (; *text != '\0'; text++) {
uint8_t cell[FONT_WIDTH + 1];
for (uint8_t col = 0; col < FONT_WIDTH; col++) cell[col] = mokxi_font_column(*text, col);
cell[FONT_WIDTH] = 0;
oledSend(bus, 0x40, cell, FONT_WIDTH + 1);
}
}
// ---- the Nokia, on its own five pins -----------------------------------------
static void nokiaSend(bool data, uint8_t value) {
digitalWrite(N_DC, data ? HIGH : LOW);
digitalWrite(N_CE, LOW);
shiftOut(N_DIN, N_CLK, MSBFIRST, value);
digitalWrite(N_CE, HIGH);
}
static void nokiaText(uint8_t bank, uint8_t x, const char *text) {
nokiaSend(false, (uint8_t)(0x40 | bank));
nokiaSend(false, (uint8_t)(0x80 | x));
for (; *text != '\0'; text++) {
for (uint8_t col = 0; col < FONT_WIDTH; col++) nokiaSend(true, mokxi_font_column(*text, col));
nokiaSend(true, 0x00);
}
}
void setup() {
strip.begin();
big.begin();
// The 128x32: 32 rows, sequential COM pins, the charge pump, page mode, the
// two remaps that turn the picture upright, and on.
static const uint8_t STRIP[] = {0xAE, 0xA8, 0x1F, 0xDA, 0x02, 0x8D, 0x14,
0x20, 0x02, 0xA1, 0xC8, 0xAF};
oledSend(strip, 0x00, STRIP, sizeof(STRIP));
oledClear(strip, 4, 0);
oledText(strip, 0, 0, "SSD1306 128X32");
oledText(strip, 2, 0, "PAGES 0 TO 3");
// The SH1106: 64 rows, DC-DC on (it already is), the remaps, and on. No
// 0x20 addressing mode, because it has only one.
static const uint8_t BIG[] = {0xAE, 0xA8, 0x3F, 0xAD, 0x8B, 0xA1, 0xC8, 0xAF};
oledSend(big, 0x00, BIG, sizeof(BIG));
oledClear(big, 8, 2);
oledText(big, 0, 2, "SH1106 1.3 INCH");
oledText(big, 2, 2, "FROM COLUMN 2");
oledText(big, 7, 2, "ONE PAGE AT A TIME");
// The Nokia: reset, then the extended set for Vop, temperature and bias,
// back to the basic set, normal display.
pinMode(N_RST, OUTPUT);
pinMode(N_CE, OUTPUT);
pinMode(N_DC, OUTPUT);
pinMode(N_DIN, OUTPUT);
pinMode(N_CLK, OUTPUT);
pinMode(N_LIGHT, OUTPUT);
digitalWrite(N_CE, HIGH);
digitalWrite(N_RST, LOW);
delay(1);
digitalWrite(N_RST, HIGH);
static const uint8_t NOKIA[] = {0x21, 0x80 | 50, 0x04, 0x14, 0x20, 0x0C};
for (uint8_t i = 0; i < sizeof(NOKIA); i++) nokiaSend(false, NOKIA[i]);
nokiaSend(false, 0x40);
nokiaSend(false, 0x80);
for (int i = 0; i < 504; i++) nokiaSend(true, 0x00);
nokiaText(0, 0, "NOKIA 5110");
nokiaText(2, 0, "VOP IS 50");
digitalWrite(N_LIGHT, HIGH);
}
void loop() {
// A seconds count on each, so all three are plainly alive.
char digits[8];
unsigned long s = millis() / 1000;
uint8_t n = 0;
do {
digits[n++] = (char)('0' + s % 10);
s /= 10;
} while (s != 0 && n < 7);
char shown[8];
for (uint8_t i = 0; i < n; i++) shown[i] = digits[n - 1 - i];
shown[n] = '\0';
oledText(strip, 3, 0, shown);
oledText(big, 4, 2, shown);
nokiaText(4, 0, shown);
delay(200);
}
Code walkthrough
The key functions of the sketch, and what each one does.
oledSend sketch.ino
Sends one I2C transfer to an OLED: the address, a control byte, then the data. A control byte of 0x00 means the bytes are commands. 0x40 means they are display data. Every OLED call in the sketch is built on this one function.
// One transfer: the address, a control byte (0x00 commands, 0x40 display
// data), then the bytes.
static void oledSend(const SoftI2c &bus, uint8_t control, const uint8_t *bytes, uint8_t count) {
if (bus.start(OLED)) {
bus.write(control);
bus.write(bytes, count);
}
bus.stop();
}oledAt sketch.ino
Moves the drawing position to a page (a row of eight pixels) and a column. It sends 0xB0 plus the page, then the column in two halves. Both OLED chips understand this style, so it works for both screens.
// Point an OLED at a page and a column, the page-addressing way both chips
// understand: 0xB0 + page, then the column in two nibbles.
static void oledAt(const SoftI2c &bus, uint8_t page, uint8_t column) {
const uint8_t where[] = {(uint8_t)(0xB0 | page), (uint8_t)(column & 0x0F),
(uint8_t)(0x10 | (column >> 4))};
oledSend(bus, 0x00, where, 3);
}setup sketch.ino
Starts each screen in its own way. The 128x32 gets 32-row settings and the charge pump. The SH1106 gets 64 rows, and text goes from column 2. The Nokia is reset, then Vop is set to 50, and its 504 bytes of memory are cleared before text is written.
void setup() {
strip.begin();
big.begin();
// The 128x32: 32 rows, sequential COM pins, the charge pump, page mode, the
// two remaps that turn the picture upright, and on.
static const uint8_t STRIP[] = {0xAE, 0xA8, 0x1F, 0xDA, 0x02, 0x8D, 0x14,
0x20, 0x02, 0xA1, 0xC8, 0xAF};
oledSend(strip, 0x00, STRIP, sizeof(STRIP));
oledClear(strip, 4, 0);
oledText(strip, 0, 0, "SSD1306 128X32");
oledText(strip, 2, 0, "PAGES 0 TO 3");
// The SH1106: 64 rows, DC-DC on (it already is), the remaps, and on. No
// 0x20 addressing mode, because it has only one.
static const uint8_t BIG[] = {0xAE, 0xA8, 0x3F, 0xAD, 0x8B, 0xA1, 0xC8, 0xAF};
oledSend(big, 0x00, BIG, sizeof(BIG));
oledClear(big, 8, 2);
oledText(big, 0, 2, "SH1106 1.3 INCH");
oledText(big, 2, 2, "FROM COLUMN 2");
oledText(big, 7, 2, "ONE PAGE AT A TIME");
// The Nokia: reset, then the extended set for Vop, temperature and bias,
// back to the basic set, normal display.
pinMode(N_RST, OUTPUT);
pinMode(N_CE, OUTPUT);
pinMode(N_DC, OUTPUT);
pinMode(N_DIN, OUTPUT);
// ... 15 more lines in the editorTroubleshooting
The usual mistakes with this circuit, and the fix for each.
- The Nokia screen stays blank. This project
Its contrast starts at zero. The sketch sets Vop to 50 insetup, so make sure that part runs and the reset on pin 3 and chip enable on pin 4 are wired. - Text on the 1.3 inch screen is cut off or shifted. This project
The SH1106 has 132 columns, so text must start at column 2, asoledText(big, 0, 2, ...)does. - Both OLEDs show the same thing or one stays dark. This project
They both use address 0x3C. Keep each on its own pair of pins, A4 and A5 for one and A2 and A3 for the other. - The screen is blank, or only half draws. OLED display
Create the display with a height of 32, not 64, and use the matching address (0x3C is common). Wrong height shows garbled or offset text. - Nothing shows after drawing. OLED display
Call display.display() after drawing. Check SDA and SCL (A4 and A5 on an Uno) and 3.3 V or 5 V power. - The image is shifted or has a strip of noise on the edge. OLED display
The SH1106 has 132 columns, but the display shows 128, so there is a 2 pixel offset. Use a library made for the SH1106 instead of an SSD1306 one. - The display is blank. OLED display
Check SDA, SCL, power, and the I2C address, usually 0x3C. Only page addressing works, so draw with a SH1106 library and send the buffer to the display. - The screen stays blank or shows nothing. Nokia 5110 display
The contrast voltage (Vop) must be set in code, for example setContrast around 50 to 60. Without it the pixels stay invisible. Check the reset, CE and DC pins.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.