01 - OLED 显示应用
本章节介绍 Pico-G1 扩展板上的 OLED 显示应用示例 —— oled_display。该应用演示了如何使用 I2C 接口驱动 SSD1306 OLED 屏幕,显示欢迎画面和实时系统信息。这是学习 I2C 通信和显示驱动编程的经典示例,展示了从底层 I2C 操作到高级图形绘图的完整技术栈。
应用源码位于 SDK 目录 source/app/01_oled_display/,提供了完整的 I2C 字符设备操作和 OLED 驱动实现,是学习显示编程的重要参考。
1 应用概述
1.1 功能特性
- I2C 通信:演示 Linux I2C 字符设备接口的读写操作
- OLED 驱动:完整的 SSD1306 驱动实现(初始化、绘图、刷新)
- 图形显示:支持文字显示、像素绘制、屏幕清除等基础图形功能
- 实时信息:显示系统运行时间、CPU 负载、内存使用等实时信息
- 字体支持:内置 8×16 ASCII 点阵字库(0x20~0x7E)
- 多语言支持:字模生成工具支持生成自定义字库
1.2 技术参数
| 参数 | 值 |
|---|---|
OLED 型号 | SSD1306 128×64 像素 |
通信接口 | I2C(I2C3,/dev/i2c-3) |
从设备地址 | 0x3C(SA0 接地,0x3D 接高) |
I2C 速率 | 100kHz(标准速度) |
显示缓冲 | 128×64 单色位图(1KB 显存) |
字体大小 | 8×16 像素 ASCII 字库 |
刷新周期 | 1 秒(可配置) |
1.3 测试用例列表
| index | 名称 | 测试指令 | 预期现象(成功) | 失败可能原因 |
|---|---|---|---|---|
| 1 | 欢迎画面 | ./oled_display 或 ./oled_display --once | 显示 "ShimetaPi Pico-G1" 欢迎画面,3 秒后显示系统信息 | I2C 连接失败、OLED 地址错误 |
| 2 | 持续刷新 | ./oled_display(无参数) | 欢迎画面后每秒刷新系统信息(uptime/loadavg/memory) | 同上 |
| 3 | 参数测试 | ./oled_display -h | 显示帮助信息 | — |
1.4 目录结构
source/app/01_oled_display/
├── Makefile # 构建脚本
├── main.c # 主程序
├── i2c_hal.c # I2C HAL 层实现
├── i2c_hal.h # I2C HAL 层头文件
├── ssd1306.c # SSD1306 驱动实现
├── ssd1306.h # SSD1306 驱动头文件
├── font8x16.h # 8×16 ASCII 点阵字库
├── gen_font8x16.py # 字模生成工具(Python)
└── README.md # 说明文档2 硬件连接说明
2.1 引脚定义
| 信号 | 板上 GPIO | 对应控制器 | 设备节点 |
|---|---|---|---|
| SCL | GPIO4_1 | I2C3 | /dev/i2c-3 |
| SDA | GPIO4_2 | I2C3 | /dev/i2c-3 |
| VCC | 3.3V | — | — |
| GND | GND | — | — |
2.2 硬件电路
标准 I2C 总线接线(上拉电阻通常已集成在 OLED 模组上):
Pico-G1 SSD1306 OLED
┌───────────┐ ┌──────────────┐
│ │ │ │
│ GPIO4_1 ──┼────── SCL ──┤ SCL │
│ │ │ │ │
│ GPIO4_2 ──┼────── SDA ──┤ SDA │
│ │ │ │ │
│ 3.3V ──┼─────────────┤ VCC │
│ │ │ │
│ GND ──┼─────────────┤ GND │
│ │ │ SA0 ─── GND│(地址0x3C)
└───────────┘ └──────────────┘地址选择跳线
- SA0 接 GND:I2C 地址 = 0x3C(默认)
- SA0 接 VCC:I2C 地址 = 0x3D
- 某些模组可能没有 SA0 引脚,固定为 0x3C
2.3 引脚复用说明
GPIO4_1/GPIO4_2 这对引脚物理上复用为 I2C3:
- 寄存器地址:
iocfg_ctrl2 @ 0x112C0000 - 默认功能:I2C3(由 BootROM/Bootloader 配置)
- 内核状态:
i2c_bus3节点status="ok" - 设备节点:
/dev/i2c-3由内核自动创建
引脚复用结论
本 SoC 没有标准 pinctrl 驱动,引脚功能由 IOCFG 寄存器控制。GPIO4_1/GPIO4_2 靠 SoC 复位默认值 / BootROM 已处于 I2C3 功能,无需手动配置。
3 编译与部署
3.1 前置条件
在编译本应用之前,请确保已完成以下准备工作:
- SDK 环境已搭建:参照 开发环境搭建 完成交叉编译工具链和 SDK 配置
- 内核配置确认:确认内核已启用
CONFIG_I2C_CHARDEV=y和CONFIG_I2C_LOTUS=y - 硬件已连接:OLED 模组正确连接到 GPIO4_1/GPIO4_2
3.2 编译应用
# 设置工具链路径
export PATH=$PATH:<SDK>/tools/linux/toolchains/arm-gcc12.2.0-linux-uclibceabi/bin
# 进入示例目录
cd <SDK>/source/app/01_oled_display
# 编译
make
# 清理
make clean编译成功后,在当前目录下生成可执行文件 oled_display。
3.3 部署到板端
# 使用 SCP 传输到开发板
scp oled_display root@<板端IP>:/usr/bin/
# 或通过 TFTP 下载
tftp -g -r oled_display <板端IP>3.4 运行应用
# 添加执行权限
chmod +x /usr/bin/oled_display
# 运行 OLED 显示示例
/usr/bin/oled_display # 欢迎画面,随后每秒刷新系统信息
/usr/bin/oled_display --once # 仅显示欢迎画面,3 秒后退出
/usr/bin/oled_display -h # 显示帮助信息应用启动后,OLED 屏幕将显示 ShimetaPi 欢迎画面,3 秒后开始显示实时系统信息,按 Ctrl+C 退出。
3.5 预期输出
控制台输出
/mnt # ./oled_display
[oled] init SSD1306 @ /dev/i2c-3 addr 0x3c ...
[oled] pad 0x100C0010 -> 0x00001002
[oled] pad 0x100C0014 -> 0x00001002
[oled] 初始化成功,开始显示。OLED 屏幕显示

固定与可变部分说明
- 固定部分:显示格式和布局(对于固定的代码)
- 可变部分:系统信息数值(每次刷新都会更新)
4 内部执行逻辑详解
4.1 应用架构
本应用采用分层设计,包括硬件抽象层、驱动层和应用层:
// 应用层(main.c)
int main(int argc, char *argv[])
{
// 1. 初始化 I2C HAL 层
i2c_init(OLED_I2C_BUS, OLED_I2C_ADDR);
// 2. 初始化 OLED 驱动
ssd1306_init();
// 3. 显示欢迎画面
ssd1306_display_welcome();
sleep(3);
// 4. 主循环刷新系统信息
while (1) {
ssd1306_clear();
ssd1306_display_system_info();
ssd1306_refresh();
sleep(1);
}
return 0;
}4.2 I2C HAL 层实现
I2C HAL 层封装了 Linux I2C 字符设备操作:
// I2C 初始化
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;
}
// 设置从设备地址
if (ioctl(fd, I2C_SLAVE, addr) < 0) {
perror("设置 I2C 地址失败");
close(fd);
return -1;
}
return fd;
}
// I2C 写入
int i2c_write(uint8_t *data, uint16_t len)
{
return write(i2c_fd, data, len);
}
// I2C 读取
int i2c_read(uint8_t *data, uint16_t len)
{
return read(i2c_fd, data, len);
}4.3 SSD1306 驱动实现
SSD1306 驱动实现了完整的 OLED 控制功能:
// OLED 初始化序列
void ssd1306_init(void)
{
// 关闭显示
ssd1306_command(SSD1306_DISPLAY_OFF);
// 设置时钟分频和频率
ssd1306_command(SSD1306_SET_CLOCK_DIV);
ssd1306_command(0x80);
// 设置复用比
ssd1306_command(SSD1306_SET_MUX_RATIO);
ssd1306_command(0x3F); // 64MUX
// 设置显示偏移
ssd1306_command(SSD1306_SET_DISPLAY_OFFSET);
ssd1306_command(0x00);
// 设置起始行
ssd1306_command(SSD1306_SET_START_LINE | 0x0);
// 启用电荷泵
ssd1306_command(SSD1306_CHARGE_PUMP);
ssd1306_command(0x14); // 0x10 禁用, 0x14 启用
// 设置内存寻址模式
ssd1306_command(SSD1306_MEMORY_ADDR_MODE);
ssd1306_command(0x00); // 水平寻址模式
// 设置列地址范围
ssd1306_command(SSD1306_SET_COLUMN_ADDR);
ssd1306_command(0x00); // 起始列
ssd1306_command(0x7F); // 结束列(127)
// 设置页地址范围
ssd1306_command(SSD1306_SET_PAGE_ADDR);
ssd1306_command(0x00); // 起始页
ssd1306_command(0x07); // 结束页(7)
// 配置重映射和段扫描
ssd1306_command(SSD1306_SET_SEGMENT_REMAP | 0x1);
ssd1306_command(SSD1306_SET_COM_SCAN_DEC);
// 设置 COM 引脚配置
ssd1306_command(SSD1306_SET_COM_PINS);
ssd1306_command(0x12);
// 设置对比度
ssd1306_command(SSD1306_SET_CONTRAST);
ssd1306_command(0xCF);
// 设置预充电周期
ssd1306_command(SSD1306_SET_PRECHARGE);
ssd1306_command(0xF1);
// 设置 VCOMH 退位级别
ssd1306_command(SSD1306_SET_VCOMH);
ssd1306_command(0x40);
// 开启显示
ssd1306_command(SSD1306_DISPLAY_ON);
}
// 发送命令
void ssd1306_command(uint8_t cmd)
{
uint8_t buf[2] = {0x00, cmd}; // Co=0, D/C#=0 表示命令
i2c_write(buf, 2);
}
// 发送数据
void ssd1306_data(uint8_t *data, uint16_t len)
{
uint8_t buf[len + 1];
buf[0] = 0x40; // Co=0, D/C#=1 表示数据
memcpy(&buf[1], data, len);
i2c_write(buf, len + 1);
}4.4 绘图功能实现
驱动层提供了基本的绘图功能:
// 设置像素
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));
}
// 清屏
void ssd1306_clear(void)
{
memset(buffer, 0, sizeof(buffer));
}
// 刷新显示
void ssd1306_refresh(void)
{
// 设置列地址
ssd1306_command(SSD1306_SET_COLUMN_ADDR);
ssd1306_command(0x00);
ssd1306_command(0x7F);
// 设置页地址
ssd1306_command(SSD1306_SET_PAGE_ADDR);
ssd1306_command(0x00);
ssd1306_command(0x07);
// 发送缓冲区数据
ssd1306_data(buffer, sizeof(buffer));
}4.5 字库和文字显示
内置 8×16 ASCII 点阵字库:
// 显示字符
void ssd1306_putchar(int x, int y, char ch)
{
if (ch < 0x20 || ch > 0x7E)
ch = ' '; // 替换不可显示字符为空格
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);
}
}
}
// 显示字符串
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 关键编程要点
5.1 I2C 字符设备操作
I2C 写入时序:
// SSD1306 I2C 写入格式
uint8_t buf[2] = {control_byte, data_byte};
// 控制字节格式
// bit7 = Co(Continuation): 0=最后一个字节, 1=后续还有字节
// bit6 = D/C#(Data/Command): 0=命令, 1=数据
// 写命令
buf[0] = 0x00; // Co=0, D/C#=0
buf[1] = command_code;
i2c_write(buf, 2);
// 写数据
buf[0] = 0x40; // Co=0, D/C#=1
buf[1] = data_byte;
i2c_write(buf, 2);5.2 显存管理
SSD1306 采用页寻址模式:
128×64 单色 OLED 内存布局:
- 8 页(Page 0~7),每页 8 个像素行
- 每页 128 列,每列 1 字节(8 位垂直对应 8 个像素)
- 总显存:128 × 8 = 1024 字节 = 1KB5.3 字库格式
8×16 点阵字库格式:
// 每个字符占 16 字节(16 行,每行 8 像素用 1 字节表示)
const uint8_t font8x16[] = {
// 字符 ' ' (0x20)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 字符 '!' (0x21)
0x00, 0x00, 0x18, 0x3C, 0x3C, 0x3C, 0x18, 0x18,
0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
// ... 其他字符
};5.4 错误处理
每个 I2C 操作都应检查返回值:
int ret = i2c_write(buf, len);
if (ret < 0) {
perror("I2C 写入失败");
// 尝试重新初始化 I2C
i2c_cleanup();
i2c_init(OLED_I2C_BUS, OLED_I2C_ADDR);
}6 代码定制说明
6.1 修改 I2C 总线或地址
编辑 ssd1306.h 顶部宏:
#define OLED_I2C_BUS 3 // I2C 总线编号(对应 /dev/i2c-3)
#define OLED_I2C_ADDR 0x3C // I2C 从地址(0x3C 或 0x3D)6.2 更改分辨率
支持其他分辨率(如 128×32):
// 修改 ssd1306.h
#define OLED_WIDTH 128
#define OLED_HEIGHT 32 // 改为 32
// 修改初始化序列中的相应参数
ssd1306_command(SSD1306_SET_MUX_RATIO);
ssd1306_command(0x1F); // 32MUX(改为 0x1F)
ssd1306_command(SSD1306_SET_COM_PINS);
ssd1306_command(0x02); // 128×32 配置
ssd1306_command(SSD1306_SET_PAGE_ADDR);
ssd1306_command(0x00);
ssd1306_command(0x03); // 4 页(改为 0x03)6.3 自定义字库
使用 gen_font8x16.py 生成自定义字库:
# 在开发机上运行(需要 PIL/Pillow)
python3 gen_font8x16.py生成的 font8x16.h 包含完整 ASCII 字库(0x20~0x7E)。
6.4 添加中文支持
扩展字库支持中文显示:
// 16×16 汉字点阵
const uint8_t font16x16[][32] = {
// 汉字 "你"
{0x00,0x00,0x00,0x00,0x7F,0xFE,0x40,0x02,0x40,0x02,0x7F,0xFC,...},
// 汉字 "好"
{0x00,0x40,0x00,0x20,0x00,0x1F,0xFF,0xF0,0x00,0x10,0x00,0x10,...},
// ... 更多汉字
};
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 常见问题排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| OLED 不亮 | I2C 连接失败、地址错误、初始化失败 | 检查接线、确认地址、验证 I2C 通信 |
| 花屏或乱码 | 初始化序列错误、显存格式错误 | 检查初始化参数、确认显存布局 |
| 黑屏但有背光 | 对比度设置过低、显示未开启 | 调整对比度参数、确认 DISPLAY_ON 命令 |
| 部分显示异常 | 页地址设置错误、列地址范围错误 | 检查寻址模式配置 |
| I2C 通信超时 | 总线速率不匹配、上拉电阻缺失 | 降低 I2C 速率、添加上拉电阻 |
| 部分字符不显示 | 字库缺失、字符范围错误 | 检查字库完整性、验证字符编码 |
8 进阶扩展
8.1 实现图形绘制
添加更多图形函数:
// 绘制水平线
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);
}
// 绘制垂直线
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);
}
// 绘制矩形
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);
}
// 绘制填充矩形
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 实现滚动显示
实现文本滚动效果:
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 实现图片显示
支持位图格式显示:
// 显示 XBM 格式图片
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);
}
}
}
// 使用示例
const uint8_t logo_xbm[] = {
// 16×16 图标的 XBM 数据
};
ssd1306_draw_xbm(56, 24, 16, 16, logo_xbm);