01 - OLED Display Application
This chapter describes the OLED display application example on the Pico-G1 expansion board — oled_display. It demonstrates how to drive an SSD1306 OLED screen over the I2C interface to show a welcome screen and real-time system information. It is a classic example for learning I2C communication and display driver programming, covering the full technology stack from low-level I2C operations to high-level graphics drawing.
The application source code lives in the SDK directory source/app/01_oled_display/, providing a complete I2C character-device implementation and OLED driver — an important reference for learning display programming.
1 Application Overview
1.1 Features
- I2C communication: demonstrates read/write operations through the Linux I2C character-device interface
- OLED driver: complete SSD1306 driver implementation (initialization, drawing, refresh)
- Graphics display: basic graphics functions such as text rendering, pixel drawing, and screen clearing
- Real-time info: displays uptime, CPU load, memory usage, and other live system information
- Font support: built-in 8×16 ASCII bitmap font (0x20~0x7E)
- Multilingual support: the font-generation tool can build custom glyph libraries
1.2 Technical Specifications
| Parameter | Value |
|---|---|
OLED model | SSD1306, 128×64 pixels |
Communication interface | I2C (I2C3, /dev/i2c-3) |
Slave address | 0x3C (SA0 grounded; 0x3D when pulled high) |
I2C speed | 100kHz (standard mode) |
Display buffer | 128×64 monochrome bitmap (1KB framebuffer) |
Font size | 8×16 pixel ASCII font |
Refresh interval | 1 second (configurable) |
1.3 Test Case List
| index | Name | Test command | Expected result (success) | Possible causes of failure |
|---|---|---|---|---|
| 1 | Welcome screen | ./oled_display or ./oled_display --once | Shows the "ShimetaPi Pico-G1" welcome screen, then system info after 3 seconds | I2C connection failure, wrong OLED address |
| 2 | Continuous refresh | ./oled_display (no arguments) | After the welcome screen, system info refreshes every second (uptime/loadavg/memory) | Same as above |
| 3 | Argument test | ./oled_display -h | Shows help information | — |
1.4 Directory Structure
source/app/01_oled_display/
├── Makefile # Build script
├── main.c # Main program
├── i2c_hal.c # I2C HAL layer implementation
├── i2c_hal.h # I2C HAL layer header
├── ssd1306.c # SSD1306 driver implementation
├── ssd1306.h # SSD1306 driver header
├── font8x16.h # 8×16 ASCII bitmap font
├── gen_font8x16.py # Font generation tool (Python)
└── README.md # Documentation2 Hardware Connection
2.1 Pin Definition
| Signal | On-board GPIO | Controller | Device node |
|---|---|---|---|
| SCL | GPIO4_1 | I2C3 | /dev/i2c-3 |
| SDA | GPIO4_2 | I2C3 | /dev/i2c-3 |
| VCC | 3.3V | — | — |
| GND | GND | — | — |
2.2 Hardware Circuit
Standard I2C bus wiring (pull-up resistors are usually integrated on the OLED module):
Pico-G1 SSD1306 OLED
┌───────────┐ ┌──────────────┐
│ │ │ │
│ GPIO4_1 ──┼────── SCL ──┤ SCL │
│ │ │ │ │
│ GPIO4_2 ──┼────── SDA ──┤ SDA │
│ │ │ │ │
│ 3.3V ──┼─────────────┤ VCC │
│ │ │ │
│ GND ──┼─────────────┤ GND │
│ │ │ SA0 ─── GND│ (address 0x3C)
└───────────┘ └──────────────┘Address selection jumper
- SA0 tied to GND: I2C address = 0x3C (default)
- SA0 tied to VCC: I2C address = 0x3D
- Some modules have no SA0 pin and are fixed at 0x3C
2.3 Pin Multiplexing
The GPIO4_1/GPIO4_2 pin pair is physically multiplexed as I2C3:
- Register address:
iocfg_ctrl2 @ 0x112C0000 - Default function: I2C3 (configured by the BootROM/bootloader)
- Kernel status: the
i2c_bus3node hasstatus="ok" - Device node:
/dev/i2c-3is created automatically by the kernel
Pin-mux conclusion
This SoC has no standard pinctrl driver; pin functions are controlled by the IOCFG registers. GPIO4_1/GPIO4_2 are already in their I2C3 function out of reset via the SoC reset defaults / BootROM, so no manual configuration is needed.
3 Build and Deployment
3.1 Prerequisites
Before building this application, make sure the following preparations are complete:
- SDK environment ready: set up the cross-compilation toolchain and SDK by following Development Environment Setup
- Kernel configuration confirmed: make sure the kernel has
CONFIG_I2C_CHARDEV=yandCONFIG_I2C_LOTUS=yenabled - Hardware connected: the OLED module is correctly wired to GPIO4_1/GPIO4_2
3.2 Building the Application
# Set the toolchain path
export PATH=$PATH:<SDK>/tools/linux/toolchains/arm-gcc12.2.0-linux-uclibceabi/bin
# Enter the example directory
cd <SDK>/source/app/01_oled_display
# Build
make
# Clean
make cleanAfter a successful build, the executable oled_display is generated in the current directory.
3.3 Deploying to the Board
# Transfer to the development board with SCP
scp oled_display root@<board-IP>:/usr/bin/
# Or download via TFTP
tftp -g -r oled_display <board-IP>3.4 Running the Application
# Add execute permission
chmod +x /usr/bin/oled_display
# Run the OLED display example
/usr/bin/oled_display # Welcome screen, then system info refreshed every second
/usr/bin/oled_display --once # Welcome screen only, exits after 3 seconds
/usr/bin/oled_display -h # Show help informationOnce started, the OLED screen shows the ShimetaPi welcome screen; after 3 seconds it starts displaying real-time system information. Press Ctrl+C to exit.
3.5 Expected Output
Console output
/mnt # ./oled_display
[oled] init SSD1306 @ /dev/i2c-3 addr 0x3c ...
[oled] pad 0x100C0010 -> 0x00001002
[oled] pad 0x100C0014 -> 0x00001002
[oled] 初始化成功,开始显示。OLED screen display

Fixed and variable parts
- Fixed part: the display format and layout (matches the fixed code)
- Variable part: the system information values (updated on every refresh)
4 Internal Execution Logic
4.1 Application Architecture
This application uses a layered design consisting of a hardware abstraction layer, a driver layer, and an application layer:
// Application layer (main.c)
int main(int argc, char *argv[])
{
// 1. Initialize the I2C HAL layer
i2c_init(OLED_I2C_BUS, OLED_I2C_ADDR);
// 2. Initialize the OLED driver
ssd1306_init();
// 3. Show the welcome screen
ssd1306_display_welcome();
sleep(3);
// 4. Main loop refreshing system information
while (1) {
ssd1306_clear();
ssd1306_display_system_info();
ssd1306_refresh();
sleep(1);
}
return 0;
}4.2 I2C HAL Layer Implementation
The I2C HAL layer wraps the Linux I2C character-device operations:
// I2C initialization
int i2c_init(int bus, int addr)
{
char dev_path[32];
snprintf(dev_path, sizeof(dev_path), "/dev/i2c-%d", bus);
int fd = open(dev_path, O_RDWR);
if (fd < 0) {
perror("打开 I2C 设备失败");
return -1;
}
// Set the slave address
if (ioctl(fd, I2C_SLAVE, addr) < 0) {
perror("设置 I2C 地址失败");
close(fd);
return -1;
}
return fd;
}
// I2C write
int i2c_write(uint8_t *data, uint16_t len)
{
return write(i2c_fd, data, len);
}
// I2C read
int i2c_read(uint8_t *data, uint16_t len)
{
return read(i2c_fd, data, len);
}4.3 SSD1306 Driver Implementation
The SSD1306 driver implements full OLED control:
// OLED initialization sequence
void ssd1306_init(void)
{
// Turn the display off
ssd1306_command(SSD1306_DISPLAY_OFF);
// Set clock divider and frequency
ssd1306_command(SSD1306_SET_CLOCK_DIV);
ssd1306_command(0x80);
// Set the multiplex ratio
ssd1306_command(SSD1306_SET_MUX_RATIO);
ssd1306_command(0x3F); // 64MUX
// Set the display offset
ssd1306_command(SSD1306_SET_DISPLAY_OFFSET);
ssd1306_command(0x00);
// Set the start line
ssd1306_command(SSD1306_SET_START_LINE | 0x0);
// Enable the charge pump
ssd1306_command(SSD1306_CHARGE_PUMP);
ssd1306_command(0x14); // 0x10 disable, 0x14 enable
// Set the memory addressing mode
ssd1306_command(SSD1306_MEMORY_ADDR_MODE);
ssd1306_command(0x00); // horizontal addressing mode
// Set the column address range
ssd1306_command(SSD1306_SET_COLUMN_ADDR);
ssd1306_command(0x00); // start column
ssd1306_command(0x7F); // end column (127)
// Set the page address range
ssd1306_command(SSD1306_SET_PAGE_ADDR);
ssd1306_command(0x00); // start page
ssd1306_command(0x07); // end page (7)
// Configure segment remap and COM scan direction
ssd1306_command(SSD1306_SET_SEGMENT_REMAP | 0x1);
ssd1306_command(SSD1306_SET_COM_SCAN_DEC);
// Set the COM pins configuration
ssd1306_command(SSD1306_SET_COM_PINS);
ssd1306_command(0x12);
// Set the contrast
ssd1306_command(SSD1306_SET_CONTRAST);
ssd1306_command(0xCF);
// Set the pre-charge period
ssd1306_command(SSD1306_SET_PRECHARGE);
ssd1306_command(0xF1);
// Set the VCOMH deselect level
ssd1306_command(SSD1306_SET_VCOMH);
ssd1306_command(0x40);
// Turn the display on
ssd1306_command(SSD1306_DISPLAY_ON);
}
// Send a command
void ssd1306_command(uint8_t cmd)
{
uint8_t buf[2] = {0x00, cmd}; // Co=0, D/C#=0 means command
i2c_write(buf, 2);
}
// Send data
void ssd1306_data(uint8_t *data, uint16_t len)
{
uint8_t buf[len + 1];
buf[0] = 0x40; // Co=0, D/C#=1 means data
memcpy(&buf[1], data, len);
i2c_write(buf, len + 1);
}4.4 Drawing Functions
The driver layer provides basic drawing functions:
// Set a pixel
void ssd1306_set_pixel(int x, int y, int color)
{
if (x < 0 || x >= OLED_WIDTH || y < 0 || y >= OLED_HEIGHT)
return;
if (color)
buffer[x + (y / 8) * OLED_WIDTH] |= (1 << (y % 8));
else
buffer[x + (y / 8) * OLED_WIDTH] &= ~(1 << (y % 8));
}
// Clear the screen
void ssd1306_clear(void)
{
memset(buffer, 0, sizeof(buffer));
}
// Refresh the display
void ssd1306_refresh(void)
{
// Set the column address
ssd1306_command(SSD1306_SET_COLUMN_ADDR);
ssd1306_command(0x00);
ssd1306_command(0x7F);
// Set the page address
ssd1306_command(SSD1306_SET_PAGE_ADDR);
ssd1306_command(0x00);
ssd1306_command(0x07);
// Send the framebuffer data
ssd1306_data(buffer, sizeof(buffer));
}4.5 Fonts and Text Display
The built-in 8×16 ASCII bitmap font:
// Display a character
void ssd1306_putchar(int x, int y, char ch)
{
if (ch < 0x20 || ch > 0x7E)
ch = ' '; // replace non-printable characters with a space
const uint8_t *font = &font8x16[(ch - 0x20) * 16];
for (int row = 0; row < 16; row++) {
uint8_t line = font[row];
for (int col = 0; col < 8; col++) {
if (line & (0x80 >> col))
ssd1306_set_pixel(x + col, y + row, 1);
}
}
}
// Display a string
void ssd1306_puts(int x, int y, const char *str)
{
int orig_x = x;
while (*str) {
if (*str == '\n') {
x = orig_x;
y += 16;
} else {
ssd1306_putchar(x, y, *str);
x += 8;
if (x >= OLED_WIDTH) {
x = orig_x;
y += 16;
}
}
str++;
}
}5 Key Programming Points
5.1 I2C Character-Device Operations
I2C write timing:
// SSD1306 I2C write format
uint8_t buf[2] = {control_byte, data_byte};
// Control byte format
// bit7 = Co (Continuation): 0 = last byte, 1 = more bytes follow
// bit6 = D/C# (Data/Command): 0 = command, 1 = data
// Write a command
buf[0] = 0x00; // Co=0, D/C#=0
buf[1] = command_code;
i2c_write(buf, 2);
// Write data
buf[0] = 0x40; // Co=0, D/C#=1
buf[1] = data_byte;
i2c_write(buf, 2);5.2 Framebuffer Management
The SSD1306 uses the page addressing mode:
128×64 monochrome OLED memory layout:
- 8 pages (Page 0~7), each page covering 8 pixel rows
- 128 columns per page, 1 byte per column (8 bits map vertically to 8 pixels)
- Total framebuffer: 128 × 8 = 1024 bytes = 1KB5.3 Font Format
The 8×16 bitmap font format:
// Each character takes 16 bytes (16 rows; each row of 8 pixels is stored in 1 byte)
const uint8_t font8x16[] = {
// Character ' ' (0x20)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Character '!' (0x21)
0x00, 0x00, 0x18, 0x3C, 0x3C, 0x3C, 0x18, 0x18,
0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
// ... other characters
};5.4 Error Handling
Every I2C operation should check its return value:
int ret = i2c_write(buf, len);
if (ret < 0) {
perror("I2C 写入失败");
// Try re-initializing I2C
i2c_cleanup();
i2c_init(OLED_I2C_BUS, OLED_I2C_ADDR);
}6 Code Customization
6.1 Changing the I2C Bus or Address
Edit the macros at the top of ssd1306.h:
#define OLED_I2C_BUS 3 // I2C bus number (maps to /dev/i2c-3)
#define OLED_I2C_ADDR 0x3C // I2C slave address (0x3C or 0x3D)6.2 Changing the Resolution
Other resolutions (e.g. 128×32) are supported:
// Modify ssd1306.h
#define OLED_WIDTH 128
#define OLED_HEIGHT 32 // change to 32
// Adjust the corresponding parameters in the initialization sequence
ssd1306_command(SSD1306_SET_MUX_RATIO);
ssd1306_command(0x1F); // 32MUX (changed to 0x1F)
ssd1306_command(SSD1306_SET_COM_PINS);
ssd1306_command(0x02); // 128×32 configuration
ssd1306_command(SSD1306_SET_PAGE_ADDR);
ssd1306_command(0x00);
ssd1306_command(0x03); // 4 pages (changed to 0x03)6.3 Custom Fonts
Use gen_font8x16.py to generate a custom font:
# Run on the development machine (requires PIL/Pillow)
python3 gen_font8x16.pyThe generated font8x16.h contains the full ASCII font (0x20~0x7E).
6.4 Adding Chinese Support
Extend the font library to support Chinese text:
// 16×16 Chinese character bitmaps
const uint8_t font16x16[][32] = {
// Character "你"
{0x00,0x00,0x00,0x00,0x7F,0xFE,0x40,0x02,0x40,0x02,0x7F,0xFC,...},
// Character "好"
{0x00,0x40,0x00,0x20,0x00,0x1F,0xFF,0xF0,0x00,0x10,0x00,0x10,...},
// ... more characters
};
void ssd1306_putchar_chinese(int x, int y, uint16_t index)
{
const uint8_t *font = font16x16[index];
for (int row = 0; row < 16; row++) {
uint16_t line = (font[row*2] << 8) | font[row*2+1];
for (int col = 0; col < 16; col++) {
if (line & (0x8000 >> col))
ssd1306_set_pixel(x + col, y + row, 1);
}
}
}7 Troubleshooting
| Problem | Possible cause | Solution |
|---|---|---|
| OLED stays dark | I2C connection failure, wrong address, init failure | Check wiring, confirm the address, verify I2C communication |
| Garbled or corrupted screen | Wrong initialization sequence, wrong framebuffer format | Check init parameters, confirm the framebuffer layout |
| Black screen with backlight | Contrast set too low, display not turned on | Adjust contrast, confirm the DISPLAY_ON command |
| Partial display anomalies | Wrong page address, wrong column address range | Check the addressing mode configuration |
| I2C communication timeout | Bus speed mismatch, missing pull-up resistors | Lower the I2C speed, add pull-up resistors |
| Some characters missing | Incomplete font, wrong character range | Check font integrity, verify the character encoding |
8 Advanced Extensions
8.1 Graphics Drawing
Add more graphics functions:
// Draw a horizontal line
void ssd1306_draw_hline(int x1, int x2, int y, int color)
{
for (int x = x1; x <= x2; x++)
ssd1306_set_pixel(x, y, color);
}
// Draw a vertical line
void ssd1306_draw_vline(int x, int y1, int y2, int color)
{
for (int y = y1; y <= y2; y++)
ssd1306_set_pixel(x, y, color);
}
// Draw a rectangle
void ssd1306_draw_rect(int x1, int y1, int x2, int y2, int color)
{
ssd1306_draw_hline(x1, x2, y1, color);
ssd1306_draw_hline(x1, x2, y2, color);
ssd1306_draw_vline(x1, y1, y2, color);
ssd1306_draw_vline(x2, y1, y2, color);
}
// Draw a filled rectangle
void ssd1306_fill_rect(int x1, int y1, int x2, int y2, int color)
{
for (int y = y1; y <= y2; y++)
for (int x = x1; x <= x2; x++)
ssd1306_set_pixel(x, y, color);
}8.2 Scrolling Display
Implement a text scrolling effect:
void ssd1306_scroll_text(const char *text, int line, int delay_ms)
{
char buffer[32];
int len = strlen(text);
for (int offset = 0; offset < len; offset++) {
ssd1306_clear();
for (int i = 0; i < 16 && (offset + i) < len; i++) {
buffer[i] = text[offset + i];
}
buffer[16] = '\0';
ssd1306_puts(0, line, buffer);
ssd1306_refresh();
usleep(delay_ms * 1000);
}
}8.3 Image Display
Support bitmap-format images:
// Display an XBM image
void ssd1306_draw_xbm(int x, int y, int width, int height, const uint8_t *xbm)
{
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
int byte_idx = (row * ((width + 7) / 8) + (col / 8));
int bit_idx = col % 8;
int pixel = (xbm[byte_idx] >> bit_idx) & 0x01;
ssd1306_set_pixel(x + col, y + row, pixel);
}
}
}
// Usage example
const uint8_t logo_xbm[] = {
// XBM data of a 16×16 icon
};
ssd1306_draw_xbm(56, 24, 16, 16, logo_xbm);