1. 硬件准备与IIC协议基础
第一次用STM32F103驱动OLED屏幕时,我对着四根线发呆了半小时——SCL、SDA、VCC、GND,这么简单的连接真的能显示内容?后来才明白,越是简单的接口背后越藏着精妙的设计。先说说硬件准备:找一块STM32F103C8T6最小系统板(蓝色小板子性价比超高),0.96寸OLED屏(注意一定是IIC接口的四线版本),外加几根杜邦线。硬件连接简单到令人发指:
- OLED的VCC接3.3V(千万别接5V,我烧过两块屏才记住这个教训)
- GND对GND
- SCL接PB6(硬件IIC1时钟线)
- SDA接PB7(硬件IIC1数据线)
IIC协议就像父子对话:SCL是父亲敲桌子的节奏(时钟信号),SDA是传递的纸条(数据)。每次传输开始前,主设备(STM32)会先发出"起始信号"——SCL高电平时SDA从高拉低,就像咳嗽一声引起注意。传输结束时则是"停止信号"——SCL高电平时SDA从低拉高,相当于说"今天就聊到这"。
地址协商是另一个关键点。多数OLED默认地址是0x78(七位地址是0x3C),但有些厂家会标0x7A。遇到屏幕没反应时,我用逻辑分析仪抓包发现地址不对,改成0x7A立即就能通信了。建议在初始化代码里加个地址扫描函数:
void I2C_Scan() { for(uint8_t addr = 1; addr < 127; addr++) { if(HAL_I2C_IsDeviceReady(&hi2c1, addr << 1, 2, 10) == HAL_OK) { printf("Found device at 0x%X\n", addr); } } }2. 硬件IIC配置实战
CubeMX配置IIC就像搭积木,但有几个坑我踩过你们不用再踩。打开CubeMX选中I2C1,模式选Fast Mode(400kHz),时钟配置保持默认72MHz。关键是要把PB6、PB7的GPIO模式设为开漏输出(Open Drain),而不是推挽输出——这是IIC标准要求的,推挽输出会导致总线冲突。
时钟配置有个隐藏技巧:在Clock Configuration标签页,把APB1时钟设为36MHz(对应72MHz主频下二分频)。因为STM32F103的硬件IIC时钟源来自APB1,400kHz速率下分频值要满足:
SCL频率 = APB1时钟 / (SCLL + SCLH + 2)实测发现CubeMX自动计算的值可能不准,我手动调整SCLL=0x13、SCLH=0xF后,用示波器测量才得到精准的400kHz时钟。
初始化代码生成后,强烈建议添加超时检测。有次我的OLED突然不响应,程序卡死在HAL_I2C_Mem_Write()里,后来加了超时判断:
HAL_StatusTypeDef status = HAL_I2C_Mem_Write(&hi2c1, OLED_ADDR, reg, I2C_MEMADD_SIZE_8BIT, data, len, 100); if(status != HAL_OK) { printf("I2C write error: %d\n", status); // 这里可以添加复位IIC总线的代码 }3. OLED初始化与显存管理
SSD1306芯片的初始化就像给显示器调参数,厂家给的初始化序列看起来像天书:
const uint8_t init_cmds[] = { 0xAE, // 关闭显示 0xD5, 0x80, // 设置时钟分频 0xA8, 0x3F, // 设置复用率 0xD3, 0x00, // 设置显示偏移 0x40, // 设置起始行 // ...更多命令 };这些十六进制数字其实是SSD1306的数据手册里规定的寄存器配置。有个取巧方法:网上找份能用的初始化代码,然后对照手册理解每个参数。比如0xA8后面的0x3F表示1/64占空比(对应64行屏幕)。
显存管理是OLED编程的核心。SSD1306的128x64屏幕实际被分成8页(Page),每页8行128列。向显存写数据时要注意:
- 先设置页地址(0xB0~0xB7)
- 再设置列地址低4位(0x00~0x0F)和高4位(0x10~0x1F)
- 连续写入数据会自动列地址递增
我封装了个画点函数,包含坐标转换:
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_mask = 1 << (y % 8); if(color) { oled_buffer[x][page] |= bit_mask; } else { oled_buffer[x][page] &= ~bit_mask; } }4. 高级功能与性能优化
当基础显示搞定后,我开始折腾高级功能。水平滚动效果特别适合跑马灯提示,代码比想象中简单:
void OLED_ScrollHorizontal(uint8_t dir) { WriteCmd(0x2E); // 关闭滚动 WriteCmd(dir ? 0x26 : 0x27); // 26右滚,27左滚 WriteCmd(0x00); // 虚拟字节 WriteCmd(0x00); // 起始页 WriteCmd(0x07); // 滚动时间间隔 WriteCmd(0x07); // 结束页 WriteCmd(0x00); // 虚拟字节 WriteCmd(0xFF); // 虚拟字节 WriteCmd(0x2F); // 开启滚动 }性能优化方面,有三个实用技巧:
- 局部刷新:修改显存后只刷新变化区域,比如更新数字时只重绘对应字符区域
- 双缓冲:在内存维护两个显存副本,比较差异后只传输变化部分
- 指令优化:连续写命令时合并IIC传输,减少起始/停止信号开销
最后分享一个坑:有次屏幕显示出现乱码,查了半天发现是IIC上拉电阻问题。STM32内部上拉约40kΩ,而SSD1306要求总线电容不超过400pF。长导线连接时最好外接4.7kΩ上拉电阻,波形用示波器看SCL/SDA的上升沿要干净利落。