15. OLED 显示
📝本节您将学习如何通过软件 I2C 驱动天巧星板载的 128×64 OLED 显示屏,掌握基本的图形绘制和文字显示方法。
🏆本章目标
1️⃣ 了解 SSD1306 OLED 驱动芯片的工作原理
2️⃣ 实现 OLED 初始化和基础绘图函数
3️⃣ 掌握 ASCII 字符和中文字符的显示方法
4️⃣ 实现简单的 UI 界面框架
15.1 OLED 显示屏介绍
天巧星板载一块 0.96 英寸 128×64 像素的 OLED 显示屏,驱动芯片为 SSD1306。
基本参数
| 参数 | 规格 |
|---|---|
| 分辨率 | 128 × 64 像素 |
| 驱动芯片 | SSD1306 |
| 通信接口 | I2C(7-bit 地址 0x3C) |
| 显示颜色 | 单色(白/蓝,取决于屏幕型号) |
| 工作电压 | 3.3V |
| 连接引脚 | PA0(SDA) / PA1(SCL) |
显存结构
SSD1306 内部有 1KB 显存(128×64 / 8 = 1024 字节),显存按页(Page)组织:
列 0 列 1 列 2 ... 列 127
页 0 [byte] [byte] [byte] ... [byte] ← 8 行像素
页 1 [byte] [byte] [byte] ... [byte] ← 8 行像素
...
页 7 [byte] [byte] [byte] ... [byte] ← 8 行像素1
2
3
4
5
2
3
4
5
- 共 8 页,每页覆盖 8 行像素
- 每个字节的 8 个 bit 对应垂直方向的 8 个像素(LSB 在上)
- 总共 8 页 × 128 列 = 1024 字节
15.2 OLED 驱动层
15.2.1 I2C 通信格式
SSD1306 的 I2C 通信格式:
Start → [0x3C << 1 | W] → ACK → [控制字节] → ACK → [数据字节] → ACK → ... → Stop1
控制字节决定后续数据的类型:
0x00:后续为命令(Command)0x40:后续为显示数据(Data)0x80:单字节命令(每个命令前都带此控制字节)
15.2.2 发送命令和数据
c
#include "ti_msp_dl_config.h"
#include "myiic.h"
#define OLED_ADDR 0x3C
// 发送命令
void OLED_WriteCmd(uint8_t cmd)
{
i2c_start();
i2c_write_byte(OLED_ADDR << 1);
i2c_write_byte(0x00); // 命令模式
i2c_write_byte(cmd);
i2c_stop();
}
// 发送数据(单字节)
void OLED_WriteData(uint8_t data)
{
i2c_start();
i2c_write_byte(OLED_ADDR << 1);
i2c_write_byte(0x40); // 数据模式
i2c_write_byte(data);
i2c_stop();
}
// 批量发送数据(提高刷屏效率)
void OLED_WriteDataBurst(uint8_t *data, uint16_t len)
{
i2c_start();
i2c_write_byte(OLED_ADDR << 1);
i2c_write_byte(0x40);
for (uint16_t i = 0; i < len; i++) {
i2c_write_byte(data[i]);
}
i2c_stop();
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
15.2.3 OLED 初始化
SSD1306 上电后需要发送一系列配置命令才能正常工作:
c
void OLED_Init(void)
{
delay_cycles(3200000); // 等待 100ms,确保 OLED 电源稳定
OLED_WriteCmd(0xAE); // 关闭显示
OLED_WriteCmd(0xD5); // 设置时钟分频
OLED_WriteCmd(0x80);
OLED_WriteCmd(0xA8); // 设置多路复用率
OLED_WriteCmd(0x3F); // 64 行
OLED_WriteCmd(0xD3); // 设置显示偏移
OLED_WriteCmd(0x00);
OLED_WriteCmd(0x40); // 设置起始行
OLED_WriteCmd(0x8D); // 使能电荷泵
OLED_WriteCmd(0x14);
OLED_WriteCmd(0x20); // 内存寻址模式
OLED_WriteCmd(0x02); // 页寻址模式
OLED_WriteCmd(0xA1); // 列地址重映射(左右翻转)
OLED_WriteCmd(0xC8); // 行扫描方向(上下翻转)
OLED_WriteCmd(0xDA); // COM 引脚配置
OLED_WriteCmd(0x12);
OLED_WriteCmd(0x81); // 对比度设置
OLED_WriteCmd(0xCF);
OLED_WriteCmd(0xD9); // 预充电周期
OLED_WriteCmd(0xF1);
OLED_WriteCmd(0xDB); // VCOMH 电压
OLED_WriteCmd(0x40);
OLED_WriteCmd(0xA4); // 正常显示(非全亮)
OLED_WriteCmd(0xA6); // 非反色显示
OLED_WriteCmd(0xAF); // 开启显示
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
15.3 显存缓冲区
为提高绘图效率,通常在 MCU 内存中维护一份完整的显存缓冲区。所有绘图操作先在缓冲区中完成,再一次性刷新到屏幕:
c
// 128×64 / 8 = 1024 字节显存缓冲区
static uint8_t OLED_Buffer[128 * 8];
// 清除缓冲区
void OLED_Clear(void)
{
memset(OLED_Buffer, 0x00, sizeof(OLED_Buffer));
}
// 将缓冲区刷新到屏幕
void OLED_Refresh(void)
{
for (uint8_t page = 0; page < 8; page++) {
OLED_WriteCmd(0xB0 + page); // 设置页地址
OLED_WriteCmd(0x00); // 设置列低地址
OLED_WriteCmd(0x10); // 设置列高地址
OLED_WriteDataBurst(&OLED_Buffer[page * 128], 128);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
15.4 基础绘图函数
15.4.1 画点
c
// 在 (x, y) 位置画一个像素点
// x: 0~127, y: 0~63
// color: 1=亮, 0=灭
void OLED_DrawPixel(uint8_t x, uint8_t y, uint8_t color)
{
if (x >= 128 || y >= 64) return;
uint8_t page = y / 8;
uint8_t bit = y % 8;
if (color)
OLED_Buffer[page * 128 + x] |= (1 << bit);
else
OLED_Buffer[page * 128 + x] &= ~(1 << bit);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
15.4.2 画线(Bresenham 算法)
c
void OLED_DrawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t color)
{
int16_t dx = abs(x1 - x0);
int16_t dy = -abs(y1 - y0);
int16_t sx = (x0 < x1) ? 1 : -1;
int16_t sy = (y0 < y1) ? 1 : -1;
int16_t err = dx + dy;
while (1) {
OLED_DrawPixel(x0, y0, color);
if (x0 == x1 && y0 == y1) break;
int16_t e2 = 2 * err;
if (e2 >= dy) { err += dy; x0 += sx; }
if (e2 <= dx) { err += dx; y0 += sy; }
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
15.4.3 画矩形
c
// 空心矩形
void OLED_DrawRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t color)
{
OLED_DrawLine(x, y, x + w - 1, y, color);
OLED_DrawLine(x, y + h - 1, x + w - 1, y + h - 1, color);
OLED_DrawLine(x, y, x, y + h - 1, color);
OLED_DrawLine(x + w - 1, y, x + w - 1, y + h - 1, color);
}
// 填充矩形
void OLED_FillRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t color)
{
for (uint8_t i = 0; i < w; i++) {
for (uint8_t j = 0; j < h; j++) {
OLED_DrawPixel(x + i, y + j, color);
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
15.5 ASCII 字符显示
使用内置字库显示英文字符和数字。常见字体大小:6×8、8×16。
15.5.1 字库数据格式
以 6×8 字体为例,每个字符占 6 字节,每字节 8 bit 表示一列的 8 个像素:
c
// 6x8 字库(ASCII 32~126)
const uint8_t Font_6x8[][6] = {
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 空格 (32)
{0x00, 0x00, 0x5F, 0x00, 0x00, 0x00}, // ! (33)
// ... 完整字库省略
};1
2
3
4
5
6
2
3
4
5
6
15.5.2 显示单个字符
c
// 在指定位置显示一个 6x8 ASCII 字符
void OLED_ShowChar6x8(uint8_t x, uint8_t y, char ch)
{
if (ch < 32 || ch > 126) return;
const uint8_t *font = Font_6x8[ch - 32];
for (uint8_t i = 0; i < 6; i++) {
for (uint8_t j = 0; j < 8; j++) {
OLED_DrawPixel(x + i, y + j, (font[i] >> j) & 0x01);
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
15.5.3 显示字符串
c
void OLED_ShowString(uint8_t x, uint8_t y, const char *str)
{
while (*str) {
if (x + 6 > 128) { // 自动换行
x = 0;
y += 8;
}
if (y + 8 > 64) break; // 超出屏幕
OLED_ShowChar6x8(x, y, *str++);
x += 6;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
15.5.4 格式化显示(类似 printf)
c
#include <stdio.h>
#include <stdarg.h>
void OLED_Printf(uint8_t x, uint8_t y, const char *fmt, ...)
{
char buf[64];
va_list args;
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
OLED_ShowString(x, y, buf);
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
使用示例:
c
OLED_Clear();
OLED_Printf(0, 0, "Tianqiaoxing");
OLED_Printf(0, 16, "Temp: %d.%d C", 25, 6);
OLED_Printf(0, 32, "ADC: %u", adc_val);
OLED_Refresh();1
2
3
4
5
2
3
4
5
15.6 中文字符显示
中文字库体积较大(几十 KB ~ 几百 KB),无法放在 MCU 内部 Flash 中。天巧星将中文字库存储在板载 SPI Flash(W25Q64)中,运行时按需从 Flash 读取对应汉字的点阵数据。
字库存储方案
| 字库 | 地址范围 | 大小 | 用途 |
|---|---|---|---|
| HZK16 | 0x000000 ~ 0x03FFFF | 256KB | 16×16 像素中文字体 |
| HZK12 | 0x050000 ~ 0x07FFFF | 192KB | 12×12 像素中文字体 |
| HZK20 | 0x080000 ~ 0x0F7FFF | 480KB | 20×20 像素中文字体 |
| Unicode 映射 | 0x040000 ~ 0x04A3FF | 42KB | UTF-8 → GB2312 转换表 |
中文显示流程
UTF-8 字符串 → Unicode 码 → 查映射表 → GB2312 编码 → 计算字库偏移 → 从 Flash 读点阵 → 绘制1
c
// 从 SPI Flash 读取一个 16×16 汉字的点阵数据(32 字节)
void OLED_ReadChineseFont16(uint16_t gb2312_code, uint8_t *font_buf)
{
uint8_t qh = (gb2312_code >> 8) - 0xA1; // 区码
uint8_t wh = (gb2312_code & 0xFF) - 0xA1; // 位码
uint32_t offset = (qh * 94 + wh) * 32; // HZK16 中每个字符 32 字节
W25Q_Read(0x000000 + offset, font_buf, 32);
}
// 显示一个 16×16 中文字符
void OLED_ShowChinese16(uint8_t x, uint8_t y, uint16_t gb_code)
{
uint8_t font[32];
OLED_ReadChineseFont16(gb_code, font);
for (uint8_t row = 0; row < 16; row++) {
uint8_t byte1 = font[row * 2];
uint8_t byte2 = font[row * 2 + 1];
for (uint8_t col = 0; col < 8; col++) {
OLED_DrawPixel(x + col, y + row, (byte1 >> (7 - col)) & 0x01);
OLED_DrawPixel(x + 8 + col, y + row, (byte2 >> (7 - col)) & 0x01);
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
字库依赖
中文显示功能依赖 SPI Flash 中预先烧录的字库数据。出厂固件已烧录字库。如果 Flash 被误擦除,需通过串口使用 mid_font_burner 模块重新烧录。
15.7 完整实验
c
#include "ti_msp_dl_config.h"
#include "myiic.h"
#include <string.h>
#include <stdio.h>
// 假设已实现上述所有 OLED 函数
int main(void)
{
SYSCFG_DL_init();
W25Q_Init();
OLED_Init();
// 显示开机画面
OLED_Clear();
OLED_ShowString(16, 0, "Tianqiaoxing");
OLED_ShowString(24, 16, "MSPM0G3519");
OLED_DrawRect(0, 0, 128, 64, 1); // 外框
OLED_Refresh();
delay_cycles(64000000); // 显示 2 秒
// 动态数据显示
uint32_t count = 0;
char buf[32];
while (1) {
OLED_Clear();
OLED_ShowString(0, 0, "Counter Demo:");
sprintf(buf, "Count: %lu", count++);
OLED_ShowString(0, 16, buf);
// 进度条
uint8_t progress = count % 100;
OLED_DrawRect(0, 40, 128, 10, 1);
OLED_FillRect(2, 42, progress * 124 / 100, 6, 1);
OLED_Refresh();
delay_cycles(3200000); // 100ms 刷新
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
15.8 性能优化
| 优化方法 | 效果 |
|---|---|
| 批量数据传输 | I2C 一次发送整页 128 字节,减少 Start/Stop 开销 |
| 局部刷新 | 只刷新变化的页,减少传输量 |
| 提高 I2C 速率 | 减小 i2c_delay(注意不超过设备支持的最高速率) |
| 使用硬件 I2C | 比软件 I2C 更高效,但引脚固定 |
| 双缓冲 | 绘制和刷新交替进行,避免画面撕裂 |
出厂固件中 OLED 刷新率约 30fps,足以满足 UI 动画和游戏需求。
15.9 知识总结
| 内容 | 说明 |
|---|---|
| 驱动芯片 | SSD1306 |
| 分辨率 | 128×64 像素 |
| 通信方式 | I2C,地址 0x3C |
| 显存大小 | 1024 字节(8 页 × 128 列) |
| 绘图方式 | MCU 缓冲区绘制 → 批量刷新到屏 |
| ASCII 字体 | 内置 6×8 / 8×16 字库 |
| 中文字体 | 存储在 SPI Flash,按需读取 |
下一章将学习 WS2812 RGB LED 的驱动方法,实现彩色灯效。