拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32F103快速驱动I2C OLED:从CubeMX到完整代码

在实际的物联网小项目或商单验收里OLED 显示模块经常是“时间紧、要求直观”的那种外设需要显示温度、湿度、电压、电量、当前状态给用户一个不依赖串口就能看懂的界面。很多开发者拿到一块 0.96 寸 OLED 屏第一反应是去找现成代码但网上的代码往往缺文件、缺说明、改了几处还是黑屏最后只能靠猜。这篇文章会围绕“用 STM32F103C8T6 快速驱动 I2C 接口 OLED”这条主线展开基于 STM32CubeMX 生成 HAL 库工程目标是跑通一块常见的 SSD1306 控制芯片 128x64 OLED 屏并把手写驱动、显存刷新、显示汉字、排查黑屏花屏这些问题一次性讲清楚。1. OLED 驱动先理清概念控制芯片、I2C 接口和固件职责1.1 “驱动 OLED”驱动的是控制芯片不是 OLED 面板本身OLED 屏的核心并不是一块自带标准接口的显示器。它通常由两块物理部分组成像素面板和控制芯片。面板负责发光控制芯片负责接收 MCU 发来的命令和显示数据然后把数据写入内部的显示 RAMGRAM再根据 GRAM 内容驱动面板上的像素点。市面上常见的 0.96 寸 OLED 模块多数使用 SSD1306分辨率是 128x64。还有部分模块使用 SH1106分辨率实际上是 132x64虽然外形相似但 page 起始地址不同代码不能盲目通用。所以“快速实现 OLED 驱动”对 MCU 固件开发而言本质上是三件事根据屏幕信息选择正确的控制芯片型号。通过 I2C、SPI 或并行接口发送控制命令。将需要显示的像素数据按控制芯片要求的格式写入显存。把这三步拆开以后问题就从“我不会调屏”变成“我有没有跑通芯片的数据链路”。1.2 这里说的“驱动”和 STLink、CH340 那些驱动不是一回事搜索 OLED 相关资料时经常看到 “STLink 驱动安装”“JLink 驱动”“CH340 串口驱动”这些词混在一起容易让人产生误解。STLink、JLink、CH340 是开发工具或 USB 转串口芯片它们在电脑上安装的驱动解决的是“电脑能否识别调试器或串口模块”的问题。没有这些驱动Keil 找不到下载器串口工具打不开端口。而 OLED 驱动的对象是屏幕控制芯片 SSD1306它是烧录在 MCU 固件里的一段代码。开发板插上电脑后电脑不会给 OLED “安装驱动”屏幕能否显示取决于 MCU 程序是否发送了正确的初始化序列和显示数据。这两个层次不要混在一起。如果 STLink 在 Keil 里识别不到先解决电脑端驱动问题如果 STLink 正常但 OLED 黑屏则要从 MCU 固件、接线和 OLED 控制芯片本身去查。1.3 先理解 SSD1306 的“页”和“像素点”关系SSD1306 的 GRAM 是 128x64也就是屏幕上每个像素点对应 GRAM 中的一位数据。但控制芯片和 MCU 之间的通信不可能按“一个像素点一次操作”来做那会让 I2C 总线忙不过来。SSD1306 将 64 行像素分成 8 个 page每个 page 高度是 8 个像素。屏幕上的 128 列不变因此每个 page 里有 128 个字节每个字节的 8 个 bit 对应竖直方向上的 8 个像素点。显存映射关系通常可以表述为显存位置 x (y / 8) * 128 字节内的 bit y % 8这样的“页”结构决定了写显存时最好的做法是在 MCU 里维护一块 1024 字节的缓冲区即 128 * 64 / 8 1024 字节。程序先在缓冲区中画点、画线、画字符再把整块缓冲区刷到 OLED 上而不是每画一个点就立即通过 I2C 写屏幕。理解这一点后面写驱动代码就不会觉得流程繁琐了。2. 硬件接线先确认屏幕型号、I2C 地址和供电电压不能靠猜2.1 直接把 0.96 寸 128x64 I2C SSD1306 作为标准模板做商单或快速验证时最稳妥的模板是 0.96 寸、128x64、I2C 接口、SSD1306 控制芯片的 OLED 模块。它引脚少、体积小、代码资料多适合小仪表盘、温湿度计、物联网节点状态显示。正式接线前先确认模块背面是否有 4 个引脚标注引脚作用常见接法VCC电源正接 3.3V需先确认模块是否带稳压器和电平转换GND电源地与 STM32 共地SCLI2C 时钟线接到 MCU 的 I2C_SCL 引脚SDAI2C 数据线接到 MCU 的 I2C_SDA 引脚有的模块还会引出 RES、DC、CS 等引脚如果背面标注写的是 GND、VDD、SCLK、SDIN那可能是 SPI 接口版本引脚顺序和 I2C 版本不同不能直接套用下面的 I2C 接线说明。2.2 STM32F103C8T6 与 I2C OLED 的接线STM32F103C8T6 有多个 I2C 外设。常用 I2C1 的默认引脚是PB6I2C1_SCLPB7I2C1_SDA对应连接关系如下OLED 引脚STM32F103C8T6 引脚VCC3.3VGNDGNDSCLPB6SDAPB7需要注意PB6 和 PB7 在 MCU 内部可以配置为 I2C 的功能脚这与普通 GPIO 模拟 I2C 不同。使用 STM32CubeMX 配置硬件 I2C 后代码里直接操作hi2c1即可不需要手动拉高拉低 SCL、SDA。I2C 总线本身需要上拉电阻。很多 OLED 模块上已经带有 4.7k 或 10k 的上拉电阻所以直接用杜邦线连接就可以工作。如果屏幕是自己画的板子或者模块上没有看到上拉电阻可以在 SCL、SDA 上分别对 3.3V 接一个 4.7k 电阻。2.3 供电电压和逻辑电平问题要提前看原理图很多 OLED 模块上丝印标注为 3.3V 到 5V但这不代表可以直接用 5V 供电。有的模块板上带有 3.3V 稳压器和逻辑电平转换电路5V 供电没问题有的模块只是外部留了兼容空间核心屏幕和控制芯片实际都是 3.3V 逻辑。在项目里推荐统一按 3.3V 供电。STM32F103C8T6 的 GPIO 也是 3.3V 逻辑直接接 I2C OLED 更安全。如果一定要接 5V先看模块原理图确认模块上有没有稳压芯片。没有稳压芯片的模块接 5V 可能烧掉屏幕或控制芯片。这个风险不属于“软件调试能解决”的问题应该在上电前排除掉。2.4 拿到新模块后要做三个硬件检查在写代码前先做快速检查能省下大量排查时间用万用表测 VCC 和 GND 之间电压确认供电正常。观察模块丝印确认控制 IC 是 SSD1306 还是其他型号。看模块上 I2C 地址跳线确认 7 位地址是 0x3C 还是 0x3D。大多数模块默认 I2C 地址是 0x3C。少数模块把 SA0 引脚接高地址变成 0x3D。如果后面 I2C 扫描不到设备不要急着怀疑代码先检查地址引脚。3. 用 STM32CubeMX 生成 HAL I2C 工程3.1 创建项目和配置时钟在 STM32CubeMX 中创建新工程时选择芯片型号 STM32F103C8T6然后按下述配置RCC 的 HSE 选择 Crystal/Ceramic Resonator外部晶振作为时钟源。SYS 的 Debug 选择 Serial Wire否则下载程序后容易占用 SWDIO 导致第二次无法连接。Clock Configuration 中把系统时钟配置到 72MHzI2C1 挂在 APB1 总线上。如果要调试输出扫描信息可以顺带开启 USART1用于串口打印。在只验证 OLED 时也可以通过断点观察变量判断 I2C 设备是否扫描到不一定需要串口。3.2 配置 I2C1 外设参数在 Pinout 页面点击 I2C1选择 I2C 模式然后在 Configuration 中设置参数。首次验证建议不要直接调到最高速度先使用标准模式。配置项推荐值说明I2C Speed ModeStandard Mode标准模式 100kHz稳定性高I2C Clock Speed100000对应 100kHz 时钟Addressing Mode7-bit常见 SSD1306 是 7 位地址模式Clock StretchingEnable允许从设备拉低时钟延长时间Own Address可保持默认主机模式下一般不依赖自身地址配置完成后Project Manager 中可以选择生成独立的外设.c/.h文件勾选 “Generate peripheral initialization as a pair of .c/.h files per peripheral” 会让工程结构更清晰。生成代码后main.c中会自动调用MX_I2C1_Init()。3.3 上电后先用 I2C 地址扫描确认屏幕在线屏幕初始化代码写了一大堆但 I2C 总线根本没通再正确的初始化命令也发不出去。所以第一个验证点不是“亮没亮”而是“MCU 能不能扫描到 OLED 地址”。在main()函数中MX_I2C1_Init()之后先执行这段扫描uint8_t found 0; uint8_t addr 0; for (addr 0x08; addr 0x77; addr) { if (HAL_I2C_IsDeviceReady(hi2c1, (uint16_t)addr, 3, 10) HAL_OK) { found 1; break; } } if (found) { // 地址存储在 addr 中正常应得到 0x3C 或 0x3D } else { // I2C 总线上没有找到设备先检查接线和供电 Error_Handler(); }HAL_I2C_IsDeviceReady会向地址发送一个读探测如果屏幕正常应答说明 MCU 与屏幕之间的物理链路已经打通。这里要注意地址表示方式HAL 库在 7 位地址模式下直接传 0x3C不需要左移一位。很多旧例程喜欢写(0x3C 1)如果机械照搬到 HAL 的HAL_I2C_IsDeviceReady中反而会扫描到错误地址。扫描不到设备时的检查顺序OLED VCC 和 GND 是否接反是否共地。SCL 和 SDA 是否接反。PB6、PB7 是否被 CubeMX 配置成了普通 GPIO。I2C 总线上是否存在上拉电阻。模块地址是不是 0x3D而不是默认的 0x3C。4. SSD1306 最小驱动命令、初始化序列、显存刷新4.1 I2C 读写 OLED 时为什么要区分“命令字节”和“数据字节”SSD1306 的 I2C 传输格式中MCU 向屏幕发送每一段数据时都要在屏幕地址之后跟一个控制字节。控制字节用于告诉 SSD1306接下来是命令还是写显存的数据。常见的控制字节值控制字节含义0x00后续字节是命令0x40后续字节是显存数据在 HAL 库中可以利用HAL_I2C_Mem_Write的这个特性用寄存器地址来充当控制字节#define OLED_I2C_ADDR 0x3C #define OLED_I2C_TIMEOUT 100 static void OLED_WriteCmd(uint8_t cmd) { HAL_I2C_Mem_Write(hi2c1, OLED_I2C_ADDR, 0x00, I2C_MEMADD_SIZE_8BIT, cmd, 1, OLED_I2C_TIMEOUT); } static void OLED_WriteData(uint8_t dat) { HAL_I2C_Mem_Write(hi2c1, OLED_I2C_ADDR, 0x40, I2C_MEMADD_SIZE_8BIT, dat, 1, OLED_I2C_TIMEOUT); }把“控制字节”理解成“通道号”会更直观。0x00 是命令通道0x40 是数据通道。后续发送的字节都进入对应通道。4.2 初始化序列是屏幕能点亮的关键SSD1306 必须按一定顺序发送初始化命令常见初始化序列如下static const uint8_t OLED_Init_CMD[] { 0xAE, // 关闭显示 0x20, 0x10, // 内存寻址模式page addressing 0xB0, // page 起始地址 0xC8, // COM 扫描方向重映射 0x00, 0x10, // 列地址低 4 位和高 4 位 0x40, // 显示起始行 0x81, 0xCF, // 对比度 0xA1, // SEG 重映射 0xA6, // 正常显示不反色 0xA8, 0x3F, // 多路复用率 0xD3, 0x00, // 显示偏移 0xD5, 0x80, // 内部时钟频率 0xD9, 0xF1, // 预充电周期 0xDB, 0x30, // VCOMH 电压 0x8D, 0x14, // 电荷泵使能 0xAF // 打开显示 }; void OLED_Init(void) { for (uint16_t i 0; i sizeof(OLED_Init_CMD); i) { OLED_WriteCmd(OLED_Init_CMD[i]); } }这组命令里最容易忽略的是0x8D, 0x14。SSD1306 内部有一个电荷泵用于生成 OLED 面板需要的驱动电压。没有使能电荷泵很多模块即使 I2C 通信正常屏幕也只会保持黑色或者只有少数像素点有微弱变化。0xA1和0xC8控制的是屏幕显示方向和行列扫描方向。不同厂家使用不同面板绑定方式如果花屏出现镜像效果可以尝试把0xA1改成0xA0把0xC8改成0xC0也就是调整 SEG 重映射和 COM 扫描方向。4.3 使用 1024 字节显存缓冲区在oled.c中定义显示宽高和缓冲区#define OLED_WIDTH 128 #define OLED_HEIGHT 64 #define OLED_PAGE_NUM 8 static uint8_t oled_buffer[OLED_WIDTH * OLED_HEIGHT / 8];设置像素点的函数void OLED_SetPixel(uint8_t x, uint8_t y, uint8_t color) { if (x OLED_WIDTH || y OLED_HEIGHT) { return; } uint16_t index x (y / 8) * OLED_WIDTH; if (color) { oled_buffer[index] | (uint8_t)(1u (y % 8)); } else { oled_buffer[index] (uint8_t)~(1u (y % 8)); } }这个函数只是把像素画进 MCU 的缓冲区屏幕上并不会立即显示。要看到结果必须调用刷新函数把缓冲区数据通过 I2C 写到 OLED 的 GRAM。4.4 整屏刷新函数SSD1306 的 page 寻址模式下刷新逻辑是先选择 page 0然后设置列地址连续写 128 个数据字节再选择 page 1。void OLED_Refresh(void) { for (uint8_t page 0; page OLED_PAGE_NUM; page) { // 选择 page 地址 OLED_WriteCmd(0xB0 | page); // 列地址低字节 OLED_WriteCmd(0x00); // 列地址高字节 OLED_WriteCmd(0x10); uint8_t *pageBuf oled_buffer[page * OLED_WIDTH]; for (uint8_t x 0; x OLED_WIDTH; x) { OLED_WriteData(pageBuf[x]); } } }整屏需要写入 1024 个数据字节。在 100kHz 的 I2C 标准模式下一次刷新大约需要几十毫秒对静态参数显示完全够用。如果后续需要制作动画或高频刷新再把 I2C 时钟提高到 400kHz并评估波形质量和总线稳定性。清屏函数也不复杂void OLED_Clear(void) { memset(oled_buffer, 0x00, sizeof(oled_buffer)); }先调用OLED_Clear()再调用OLED_Refresh()屏幕上所有像素都会熄灭。整屏常量显示的示例void OLED_Fill(uint8_t color) { memset(oled_buffer, color ? 0xFF : 0x00, sizeof(oled_buffer)); }如果OLED_Fill(1)后屏幕不能变成全白问题基本可以定位在初始化序列、数据写入或刷新函数上。5. 文本、数值和汉字把显存做成可复用的“画笔”5.1 ASCII 字符显示使用内置字模SSD1306 内部没有字符字库。点阵字库必须由 MCU 提供。英文和数字比较常用 6x8 或 8x16 的字模。以一个 6x8 ASCII 字模为例字体数据本质上是一张常量表每个字符占 6 个字节每个字节表示一列像素。显示一个字符的思路static const uint8_t font6x8[95][6]; // 字模表实际工程可以引入常见字库 void OLED_DrawChar6x8(uint8_t x, uint8_t y, char ch) { const uint8_t *glyph font6x8[ch - ]; for (uint8_t col 0; col 6; col) { uint8_t bits glyph[col]; for (uint8_t row 0; row 8; row) { uint8_t color (bits row) 0x01; OLED_SetPixel(x col, y row, color); } } }在主循环中先画字符再调用OLED_Refresh()屏幕上才能
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门