How to display a logo on a 128x32 COG LCD display?
Understanding the 128x32 COG LCD Display Architecture
The 128x32 COG (chip-on-glass) LCD is a graphic monochrome display with a resolution of 128 columns by 32 rows, typically driven by the SSD1306 or ST7565 controller. The COG design mounts the driver IC directly onto the glass, reducing space and cost, but it means the SPI interface is the only way to communicate. The display’s memory is organized into 4 pages (Page 0 to Page 3), each page covering 8 rows of pixels. For a 32-row display, Page 0 covers rows 0-7, Page 1 covers rows 8-15, Page 2 covers rows 16-23, and Page 3 covers rows 24-31. Each page has 128 bytes, one byte per column, where each bit in the byte represents a pixel in that column for the 8 rows. For example, to turn on the pixel at column 10, row 5, you’d set bit 5 (since row 0 is the LSB) in the byte at column 10 of Page 0. This page-based layout is critical for logo display because you must map your logo’s rows to these pages. If your logo is 32 pixels tall, it fits exactly into 4 pages. If it’s smaller, like 16 pixels tall, you’ll only use Pages 0 and 1, leaving the rest blank. The SPI clock speed typically runs at 4 MHz to 10 MHz, with the SSD1306 supporting up to 10 MHz, so you can update the entire frame buffer in about 4.1 ms (128 bytes per page × 4 pages × 8 bits per byte / 10 MHz). This speed is fine for static logos but might cause flicker if you’re updating the display more than 60 Hz.
For initialization, you need to send a sequence of commands over SPI. The SSD1306 datasheet specifies a 25-byte initialization sequence, including commands like 0xAE (display off), 0xD5 (set display clock divide ratio), 0xA8 (set multiplex ratio to 0x1F for 32 rows), 0xD3 (set display offset to 0x00), 0x40 (set display start line to 0), 0x8D (enable charge pump), 0x20 (set memory addressing mode to horizontal), 0xA1 (set segment remap to column 127 as SEG0), 0xC8 (COM output scan direction from COM31 to COM0), 0xDA (set COM pins hardware configuration to 0x02 for 32 rows), 0x81 (set contrast to 0x7F), 0xD9 (set pre-charge period to 0xF1), 0xDB (set VCOMH deselect level to 0x40), 0xA4 (entire display on, resume to RAM content), 0xA6 (set normal display, not inverted), 0x2E (deactivate scroll), 0xAF (display on). Each command is sent with the D/C pin low (command mode), while data is sent with D/C high. For the ST7565, the sequence is different—it uses 0xE2 (reset), 0xA1 (segment direction), 0xC8 (common output mode), 0xA2 (bias set to 1/9), 0x2C (power control), 0x2E (regulator resistor set), 0x2F (follow mode), 0x81 (contrast), 0xAF (display on). A common mistake is using the wrong initialization for the driver, which can cause the logo to appear shifted or inverted. For example, the SSD1306’s segment remap (0xA1) flips the column order, so if you don’t set it, your logo’s leftmost column might appear on the right side of the display.
Converting Your Logo to Bitmap Data
To display a logo, you need to convert it into a byte array that matches the 128x32 memory layout. Tools like LCD Assistant, GIMP (with the LCD plugin), or online converters can generate the array from a monochrome BMP file. The image must be 128 pixels wide and 32 pixels tall, or you can pad it with zeros. For a 64x32 logo centered on the display, you’d have 32 bytes of zeros on the left, 64 bytes of logo data, and 32 bytes of zeros on the right for each page. The data format is vertical byte packing: for each column, the byte’s bits 0-7 represent rows 0-7 (Page 0), bits 8-15 represent rows 8-15 (Page 1), and so on. This means if your logo has a horizontal line from column 0 to 127 at row 0, you’d set bit 0 in every byte of Page 0. If you have a vertical line at column 0 from row 0 to 31, you’d set bit 0 in Page 0, bit 0 in Page 1, bit 0 in Page 2, and bit 0 in Page 3 for column 0. This vertical packing is different from the horizontal packing used in some OLED displays, so you must ensure your converter uses the correct format. For example, the Adafruit GFX library uses horizontal packing, but the SSD1306’s native format is vertical. If you use horizontal-packed data, the logo will appear rotated 90 degrees or scrambled.
Let’s say you have a 128x32 logo of a company symbol. You’d generate a 512-byte array (128 bytes per page × 4 pages). For a 64x32 logo, you’d generate a 256-byte array and pad it. The array is sent to the display using the SPI data write command. For the SSD1306, you set the column address range with 0x21 (set column address) followed by start and end columns (0 to 127), and the page address range with 0x22 (set page address) followed by start and end pages (0 to 3). Then you send the data bytes sequentially. For the ST7565, you set the column address with 0x10 (upper column) and 0x00 (lower column), and the page address with 0xB0 through 0xB3. The ST7565 also requires a 0xE0 (read-modify-write) entry for continuous data writes. A common issue is that the ST7565’s page address is 0xB0 + page number, so for Page 0, you send 0xB0, for Page 1, 0xB1, etc. If you send 0xB0 for all pages, you’ll overwrite the same page.
SPI Communication Timing and Voltage Levels
The SPI interface for this display uses four pins: SCK (clock), MOSI (data), CS (chip select), and D/C (data/command). The display operates at 3.3V logic, but many microcontrollers like Arduino Uno run at 5V. You must use a level shifter or voltage divider on the SPI lines to avoid damaging the display. The SSD1306 has a maximum input voltage of 3.3V, and exceeding it can cause permanent damage. For example, a 5V Arduino’s MOSI pin outputs 5V, which is 1.7V over the limit. You can use a 1kΩ and 2kΩ resistor divider to drop it to 3.3V. The CS pin is active low, so you pull it low before sending commands or data and high after. The D/C pin determines whether the byte is a command (low) or data (high). Timing is critical: the SPI clock must be idle low (CPOL=0) and data sampled on the rising edge (CPHA=0). The minimum clock period for the SSD1306 is 100 ns (10 MHz), but for the ST7565, it’s 250 ns (4 MHz). If you run the clock too fast, you’ll get corrupted data. For example, if you set the SPI clock to 8 MHz on an ST7565, the display might show random pixels because the data setup time isn’t met. The datasheet specifies a data setup time of 50 ns and a hold time of 20 ns for the SSD1306, so you should keep the clock below 10 MHz to be safe.
Power supply is another factor. The display requires a 3.3V supply for logic and a separate 12V to 15V supply for the LCD drive (generated internally by the charge pump). The SSD1306’s charge pump draws about 10 mA to 20 mA, so the total current consumption is around 15 mA to 30 mA depending on the number of pixels lit. If you’re powering it from a microcontroller’s 3.3V regulator, ensure the regulator can supply at least 50 mA to avoid brownouts. For example, the Arduino Uno’s 3.3V regulator can only output 150 mA, but if you’re also powering sensors, you might exceed it. Use a separate 3.3V regulator like the AMS1117-3.3 for reliability. The contrast voltage is set by the 0x81 command for the SSD1306, with values from 0x00 (lowest) to 0xFF (highest). A typical value is 0x7F (127), which gives a clear logo without ghosting. If the contrast is too high, you’ll see double images due to crosstalk. For the ST7565, the contrast is set by the 0x81 command followed by a resistor value (0x00 to 0x3F), with 0x20 being a good starting point.
Practical Code Example for Arduino
Here’s a concrete example using an Arduino Nano and the SSD1306 driver. You’ll need the SPI library. Define the pins: CS to pin 10, D/C to pin 9, MOSI to pin 11, SCK to pin 13. Initialize the display with the sequence above. Then, to display a logo, create a const byte array called logo[] with 512 bytes. For a 128x32 logo, it’s exactly 512 bytes. For a 64x32 logo, pad it with zeros. The code snippet for sending the logo:
void displayLogo() {
digitalWrite(csPin, LOW);
// Set column address range (0 to 127)
sendCommand(0x21);
sendCommand(0);
sendCommand(127);
// Set page address range (0 to 3)
sendCommand(0x22);
sendCommand(0);
sendCommand(3);
// Send data
digitalWrite(dcPin, HIGH);
for (int i = 0; i < 512; i++) {
SPI.transfer(logo[i]);
}
digitalWrite(csPin, HIGH);
}
The sendCommand function sets D/C low, CS low, then sends the byte via SPI.transfer, then sets CS high. This writes the entire frame buffer in one burst. If you want to update only part of the logo, like a 32x32 icon, you set the column range to 32 to 63 and page range to 0 to 3, then send 128 bytes (32 columns × 4 pages). This reduces update time to about 0.1 ms, which is useful for animations. The logo array must be stored in PROGMEM (flash memory) to save RAM, especially on AVR microcontrollers with only 2 KB of RAM. Use the pgmspace.h library and read bytes with pgm_read_byte(). For example:
#include
const byte logo[] PROGMEM = { 0x00, 0x7E, ... };
for (int i = 0; i < 512; i++) {
SPI.transfer(pgm_read_byte(&logo[i]));
}
This ensures the logo doesn’t eat up your RAM. If you’re using an ESP32, you don’t need PROGMEM because it has plenty of RAM, but you can still use it for efficiency. The ESP32’s SPI clock can go up to 40 MHz, but limit it to 10 MHz for the display. Also, the ESP32 uses 3.3V logic natively, so no level shifting is needed.
Common Logo Display Issues and Fixes
One frequent problem is the logo appearing inverted. This happens if you set the display to inverted mode (0xA7) instead of normal (0xA6). Another issue is the logo shifted horizontally. This is due to incorrect column address setting. For the SSD1306, the column address is set with 0x21, and the start column must be 0 for the leftmost column. If you set it to 10, the logo starts 10 columns to the right. For the ST7565, the column address is set with 0x10 (upper nibble) and 0x00 (lower nibble), so if you send 0x10 and 0x01, the column starts at 16 (0x10) + 1 = 17. This is a common source of confusion because the ST7565 uses a 6-bit column address. The maximum column is 127, so the upper nibble can be 0 to 7 (since 7 × 16 = 112, plus lower nibble 15 = 127). If you send 0x10 and 0x00, the column is 16, not 0. To start at column 0, send 0x10 and 0x00, but that’s actually column 0 because the ST7565’s column address is 0 to 127, and the upper nibble is the high 4 bits. So 0x10 means column 16, not 0. The correct way is to send 0x10 (for column 0) and 0x00, but that’s wrong because 0x10 is the command for setting the upper column, not the value. The value is sent in the next byte. For example, to set column 0, you send 0x10 (upper column command) followed by 0x00 (upper 4 bits = 0), then 0x00 (lower column command) followed by 0x00 (lower 4 bits = 0). This is a common pitfall. Use the datasheet’s example: for column 100, send 0x10 (upper column), then 0x06 (since 100 / 16 = 6.25, so upper 4 bits = 6), then 0x00 (lower column), then 0x04 (lower 4 bits = 4, because 6 × 16 + 4 = 100).
Another issue is the logo appearing vertically compressed or stretched. This happens if the multiplex ratio is set incorrectly. For a 32-row display, the multiplex ratio must be 31 (0x1F) for the SSD1306. If you set it to 63 (for 64 rows), the display will only show the top half of the logo. For the ST7565, the bias setting (0xA2) determines the duty cycle. A 1/9 bias is for 32 rows, while 1/7 bias is for 64 rows. If you use 1/7 bias, the contrast will be low and the logo will be faint. The display’s contrast also affects visibility. If the logo is too dim, increase the contrast value from 0x7F to 0xFF for the SSD1306, but watch for ghosting. If it’s too bright, decrease it. The charge pump must be enabled (0x8D with 0x14 for the SSD1306) to generate the high voltage. If you skip this, the display will be blank. For the ST7565, you need to set the power control registers (0x2C, 0x2E, 0x2F) to turn on the booster, regulator, and follower. A common mistake is using the SSD1306 initialization for an ST7565 display, which will result in no display at all.
Advanced Techniques for Logo Display
If you want to display a logo with multiple shades (like a grayscale effect), you can use pixel density modulation. Since the display is monochrome, you can simulate grayscale by varying the number of lit pixels in a 2x2 block. For example, a 50% gray can be achieved by lighting two pixels out of four in a checkerboard pattern. This requires a higher-resolution logo (e.g., 256x64) and downscaling, but it’s possible with careful data mapping