# 04-OLED调试 ## 调试方式对比 | 方式 | 优点 | 缺点 | 适用场景 | |------|------|------|----------| | 串口(USART) | 只需 TX/RX 两根线 | 需 PC 端串口助手,不直观 | 日志输出,数据结构 | | OLED 屏幕 | 实时显示,无需 PC | 占 GPIO,I2C/SPI 总线 | 调试参数显示,仪表 | | Keil Debug | 单步/断点/变量观察 | 停止运行,实时性差 | 逻辑错误,内存检查 | | 逻辑分析仪 | 精确时序 | 需额外硬件 | 时序协议分析 | ## OLED 模块规格 (0.96寸) | 参数 | 值 | |------|-----| | 分辨率 | 128 × 64 | | 驱动 IC | SSD1306 | | 颜色 | 蓝/白/黄蓝双色 | | 接口 | I2C / SPI | | 供电 | 3.3V~5V | | 功耗 | ~20mA | | 可视角度 | >160° | ### 4针 I2C 版本引脚 | 引脚 | 功能 | 连接 | |------|------|------| | VCC | 电源 | 3.3V | | GND | 地 | GND | | SCL | I2C 时钟线 | PB8 (软件I2C) | | SDA | I2C 数据线 | PB9 (软件I2C) | ## SSD1306 与 I2C 协议 ```mermaid sequenceDiagram participant MCU as STM32 Master participant OLED as SSD1306 Slave MCU->>OLED: START Condition (SCL高, SDA下降沿) MCU->>OLED: 0x78 (设备地址 0x3C<<1 + W=0) OLED-->>MCU: ACK MCU->>OLED: 0x00 (Control Byte: Co=0, D/C#=0 → Command) OLED-->>MCU: ACK MCU->>OLED: Command Byte OLED-->>MCU: ACK MCU->>OLED: STOP Condition (SCL高, SDA上升沿) ``` ## 软件 I2C 模拟 ### 通信协议层 ```c // 起始信号 void I2C_Start(void) { GPIO_SetBits(GPIOB, GPIO_Pin_8 | GPIO_Pin_9); // SCL=H, SDA=H Delay_us(5); GPIO_ResetBits(GPIOB, GPIO_Pin_9); // SDA↓ Delay_us(5); GPIO_ResetBits(GPIOB, GPIO_Pin_8); // SCL↓ } // 停止信号 void I2C_Stop(void) { GPIO_ResetBits(GPIOB, GPIO_Pin_9); // SDA=L GPIO_SetBits(GPIOB, GPIO_Pin_8); // SCL=H Delay_us(5); GPIO_SetBits(GPIOB, GPIO_Pin_9); // SDA↑ Delay_us(5); } // 发送一个字节 (MSB first) void I2C_SendByte(uint8_t byte) { for (uint8_t i = 0; i < 8; i++) { if (byte & 0x80) GPIO_SetBits(GPIOB, GPIO_Pin_9); else GPIO_ResetBits(GPIOB, GPIO_Pin_9); byte <<= 1; GPIO_SetBits(GPIOB, GPIO_Pin_8); // SCL=H (从机采样) Delay_us(2); GPIO_ResetBits(GPIOB, GPIO_Pin_8); // SCL=L Delay_us(2); } // 接收 ACK GPIO_SetBits(GPIOB, GPIO_Pin_9); // SDA=H (释放总线) GPIO_SetBits(GPIOB, GPIO_Pin_8); Delay_us(2); // 等待从机拉低 SDA // 若 SDA 未被拉低 (NACK),可在此处理 GPIO_ResetBits(GPIOB, GPIO_Pin_8); GPIO_ResetBits(GPIOB, GPIO_Pin_9); // 恢复 } // 接收一个字节 uint8_t I2C_ReceiveByte(void) { uint8_t byte = 0; GPIO_SetBits(GPIOB, GPIO_Pin_9); // SDA=H (输入模式需设为浮空输入) for (uint8_t i = 0; i < 8; i++) { byte <<= 1; GPIO_SetBits(GPIOB, GPIO_Pin_8); Delay_us(2); if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_9)) byte |= 0x01; GPIO_ResetBits(GPIOB, GPIO_Pin_8); Delay_us(2); } // 发送 ACK (0) 或 NACK (1) GPIO_ResetBits(GPIOB, GPIO_Pin_9); // ACK GPIO_SetBits(GPIOB, GPIO_Pin_8); Delay_us(2); GPIO_ResetBits(GPIOB, GPIO_Pin_8); GPIO_SetBits(GPIOB, GPIO_Pin_9); return byte; } ``` ## OLED 驱动函数表 ### 底层命令函数 ```c void OLED_WriteCmd(uint8_t cmd); // 写入命令 void OLED_WriteData(uint8_t data); // 写入数据 void OLED_SetCursor(uint8_t x, uint8_t y); // 设置坐标 (x:0~127, y:0~7 页) ``` ### 用户接口函数 | 函数 | 原型 | 说明 | |------|------|------| | `OLED_Init` | `void OLED_Init(void)` | 初始化 SSD1306 | | `OLED_Clear` | `void OLED_Clear(void)` | 清屏 (全黑) | | `OLED_ShowChar` | `void OLED_ShowChar(uint8_t x, uint8_t y, char ch)` | 显示一个字符 (6x8) | | `OLED_ShowString` | `void OLED_ShowString(uint8_t x, uint8_t y, char* str)` | 显示字符串 | | `OLED_ShowNum` | `void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len)` | 显示十进制数字 | | `OLED_ShowSignedNum` | `void OLED_ShowSignedNum(uint8_t x, uint8_t y, int32_t num, uint8_t len)` | 显示有符号数 | | `OLED_ShowHexNum` | `void OLED_ShowHexNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len)` | 显示十六进制数 | | `OLED_ShowBinNum` | `void OLED_ShowBinNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len)` | 显示二进制数 | | `OLED_SetCursor` | `void OLED_SetCursor(uint8_t x, uint8_t y)` | 设置写位置 | | `OLED_DisplayOn` | `void OLED_DisplayOn(void)` | 开启显示 | | `OLED_DisplayOff` | `void OLED_DisplayOff(void)` | 关闭显示(省电) | ## SSD1306 初始化序列 ```c void OLED_Init(void) { Delay_ms(100); // 等待 SSD1306 上电稳定 // PB8(SCL), PB9(SDA) 推挽输出 GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏,需外部上拉 GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_SetBits(GPIOB, GPIO_Pin_8 | GPIO_Pin_9); // SSD1306 初始化命令序列 OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0xD5); // 设置振荡频率 OLED_WriteCmd(0x80); // 默认值 OLED_WriteCmd(0xA8); // 多路复用比 OLED_WriteCmd(0x3F); // 1/64 duty OLED_WriteCmd(0xD3); // 显示偏移 OLED_WriteCmd(0x00); OLED_WriteCmd(0x40); // 起始行 OLED_WriteCmd(0x8D); // 电荷泵 OLED_WriteCmd(0x14); // 开启 OLED_WriteCmd(0x20); // 内存寻址模式 OLED_WriteCmd(0x00); // 水平寻址 OLED_WriteCmd(0xA0); // 段重映射 OLED_WriteCmd(0xA1); // 列127→SEG0 (左右翻转) OLED_WriteCmd(0xC0); // COM扫描方向 OLED_WriteCmd(0xC8); // 从COM[N-1]到COM0 (上下翻转) 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); // 全局显示开启(跟随RAM) OLED_WriteCmd(0xA6); // 正常显示(不反色) OLED_WriteCmd(0x2E); // 停止滚动 OLED_WriteCmd(0xAF); // 开启显示 } ``` ## 完整代码示例 ```c #include "stm32f10x.h" #include "Delay.h" #include "OLED.h" int main(void) { Delay_Init(); OLED_Init(); uint32_t count = 0; OLED_ShowString(1, 1, "STM32 OLED Demo"); OLED_ShowString(2, 1, "Count:"); OLED_ShowString(3, 1, "Hex:0x"); OLED_ShowString(4, 1, "Bin:"); while (1) { OLED_ShowNum(2, 7, count, 5); OLED_ShowHexNum(3, 5, count, 8); OLED_ShowBinNum(4, 5, count, 16); count++; // 显示浮点数(间接) // OLED_ShowNum(5, 1, 1234, 4); // 整数部分 // OLED_ShowChar(5, 5, '.'); // OLED_ShowNum(5, 6, 56, 2); // 小数部分 Delay_ms(100); } } ``` ## OLED.H 参考实现 ```c #ifndef __OLED_H #define __OLED_H #include "stm32f10x.h" void OLED_Init(void); void OLED_Clear(void); void OLED_DisplayOn(void); void OLED_DisplayOff(void); void OLED_ShowChar(uint8_t x, uint8_t y, char ch); void OLED_ShowString(uint8_t x, uint8_t y, char* str); void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len); void OLED_ShowSignedNum(uint8_t x, uint8_t y, int32_t num, uint8_t len); void OLED_ShowHexNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len); void OLED_ShowBinNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len); void OLED_SetCursor(uint8_t x, uint8_t y); #endif ``` ## 常见坑点/注意事项 1. **I2C地址** — SSD1306 7位地址 `0x3C`,写入地址 `0x78` (`0x3C << 1`),读取地址 `0x79`。部分模块 SA0=1 时地址为 `0x7A` 2. **Control Byte** — 命令 `0x00` (Co=0, D/C=0),数据 `0x40` (Co=0, D/C=1) 3. **延时不宜过短** — 软件 I2C 时钟周期 `>2.5μs` 最稳定,`Delay_us(5)` 约 100kHz,太快会导致通信失败 4. **SCL/SDA 模式** — 推荐 `GPIO_Mode_Out_OD`(开漏),配合板载 4.7kΩ 上拉电阻;如果用推挽,SDA 双向时需注意切换方向 5. **OLED 上电慢** — 初始化前至少 `Delay_ms(100)` 等待 SSD1306 内部复位完成 6. **显示位置越界** — x: 0~127, y: 0~7(页模式,每页8像素),写超出会绕回 7. **字库** — 字符显示需要 6x8 或 8x16 点阵字库数组,使用标准库时需自己包含 `OLED_Font.c` 8. **OLED_SetCursor** — SSD1306 页地址模式默认水平寻址,SetCursor 需发送 `0xB0+y`、高4位列 `0x10+(x>>4)`、低4位列 `x&0x0F`