STM32F103C8T6 + CubeMX + HAL库驱动OLED完整指南

📅 发布时间:2026/9/2 5:36:59
STM32F103C8T6 + CubeMX + HAL库驱动OLED完整指南
简介面向STM32初学者的OLED驱动入门资料基于STM32CubeMX完整演示STM32F103C8T6的I2C外设配置过程帮助解决OLED屏初始化与显示调试难题。压缩包共999个文件、24.66MB以560个C源文件、246个H头文件为主体另含Keil/IAR工程文件、启动汇编、链接脚本和CubeMX的.ioc配置以及AXF/HEX编译固件便于直接打开工程或烧录验证。已有2263人学习下载适合正在学习STM32CubeMX或I2C通信的开发者参考。内容附带OLED时钟工程示例从CubeMX时钟配置、I2C主机模式到GPIO引脚映射都有完整源码可查读者可获得一套可直接编译运行的工程省去从零搭建外设驱动的时间同时通过目录结构理解HAL库、启动文件与编译产物之间的关系对入门STM32外设开发很有帮助。 干嵌入式这些年要说被问得最多的入门问题STM32F103C8T6怎么点亮一块OLED绝对排得上号。很多人拿到蓝板第一件事就是把屏幕接上然后发现网上教程五花八门有的讲寄存器有的用标准库还有人直接贴一大段代码让你复制折腾一晚上屏幕还是白屏。这篇文章就围绕“基于STM32CubeMX配置STM32F103C8T6驱动OLED”的完整流程来聊把原理、配置步骤和踩过的坑一次说清楚适合刚接触HAL库、或者想从标准库切换过来的朋友作为参考。1. 为什么选“CubeMX HAL I2C OLED”这套组合1.1 从寄存器到标准库到HAL开发方式怎么选很多老教程还在用寄存器操作或者标准库开发不是说不行而是项目一复杂效率会被拖得很厉害。拿点亮OLED这个需求来说寄存器版本要对着参考手册翻I2C外设的寄存器位CR1、CR2、SR1、SR2挨个配置稍不留神就漏一个位。标准库虽然有封装但和芯片绑得太紧以后换型号基本等于重写。HAL库配合STM32CubeMX的思路完全不同。CubeMX负责生成初始化代码你只需要用图形界面勾选外设、配置时钟树生成工程之后直接写业务逻辑。HAL库对底层做了一层抽象函数接口统一很多还带超时机制实际用下来比寄存器版本稳不少。这套组合现在已经是ST的官方主流路线社区的教程、例程也基本都迁到HAL上了跟着主流走不容易踩坑。1.2 硬件选型最小系统板与0.96寸OLED项目用的主控是STM32F103C8T6Cortex-M3内核主频72MHz64KB Flash20KB SRAMLQFP48封装。某宝上二三十块钱的“蓝板”就是最典型的F103C8T6最小系统板板载8MHz晶振、USB口、AMS1117稳压到3.3V引出全部IO拿来当入门板非常合适。要注意一点市面上的“蓝板”板载晶振质量参差不齐有些甚至没焊晶振后面配置时钟树的时候要留意。OLED选的是最常用的0.96寸I2C接口模块分辨率128x64驱动芯片是SSD1306。之所以选I2C版本而不是SPI版本是因为它只需要4根线VCC、GND、SCL、SDA接线极其简单不占用太多IO。缺点就是刷新率比SPI慢对纯文本显示来说完全够用。如果后续要刷图片、做动画再考虑换成SPI接口的模块。1.3 环境准备STM32CubeMX安装与固件包问题STM32CubeMX本身是个Java应用下载安装后需要联网下载对应芯片的固件包。第一次打开新建工程时如果下载芯片固件库失败最常见的原因是网络连接不稳定或者下载源在国外。处理办法有两个一是换网络环境多试几次二是在CubeMX里找到固件包管理器把下载好的固件包手动导入。一般F1系列的固件包版本是STM32Cube_FW_F1_V1.8.x下载完解压放到CubeMX的Repository目录下刷新就能识别。还有个小技巧CubeMX的界面默认是英文如果看着费劲可以在菜单里切换到中文语言包虽然界面汉化后有些术语翻译比较生硬但对新手友好不少。我这边为了写代码方便还是保持英文界面因为很多资料、报错信息都是英文早适应早省事。2. 在STM32CubeMX里完成工程配置2.1 新建工程与芯片选型打开STM32CubeMX在“New Project”里直接搜索STM32F103C8Tx选中之后双击打开。这里有个细节F103C8T6的Flash只有64KB如果后续项目比较大可以直接看C8T6对应的同封装高配版本比如STM32F103CBT6128KB引脚完全兼容改一下芯片型号就行CubeMX再生成一次代码就完事。新建工程后第一步不是急着配置外设而是先确认Project Manager里的“Project Name”和“Location”路径不能有中文否则生成的工程在编译时可能出现各种奇怪错误。Toolchain/IDE我习惯选MDK-ARM版本选V5因为Keil MDK5是目前大家用得最多的环境。另外Min Version可以不用管保持默认。2.2 RCC时钟树配置外部晶振到72MHz主频进入RCC选项卡把“High Speed ClockHSE”设为“Crystal/Ceramic Resonator”表示使用外部晶振。然后进入“Clock Configuration”页面这里能看到整个时钟树。默认情况下SysClock是HSI的8MHz我们需要把PLL倍频开到9倍也就是8MHz x 9 72MHz这是F103的最高主频。有几点要特别注意如果最小系统板上没有焊外部晶振HSE起振会失败程序跑不起来这时候只能退回到HSI内部时钟源把PLL Source选为HSI但这样USB功能会受限因为HSI精度不够。另外APB1总线频率默认是36MHzAPB2是72MHzI2C1挂在APB1上所以I2C的时钟源是36MHz配置I2C速率时就是在这个基础上分频的。2.3 I2C外设与GPIO引脚的配置要点在左侧Categories列表中找到Connectivity打开I2C1。I2C1的默认引脚是PB6SCL和PB7SDA这两个脚是AF功能CubeMX会自动配置复用。速率方面Standard Mode是100kHzFast Mode是400kHz实测下来OLED模块用400kHz没问题但如果你用的是杜邦线连接线路过长或者接触不良400kHz容易通信异常这时候果断降回100kHz显示效果没区别只是刷新慢一点。还有一个容易忽略的地方I2C1的GPIO配置里务必确认Pin Out中PB6、PB7显示的是I2C1_SCL、I2C1_SDA并且GPIO模式是开漏输出Open Drain。OLED模块板上一般都自带上拉电阻所以不用额外配置内部上拉。如果是自己搭的电路记得外接4.7k左右的上拉电阻到3.3V否则I2C通信会不稳定。3. OLED显示原理与SSD1306驱动3.1 像素从哪来OLED屏的像素构成和显存模型很多人把OLED当成一个“能显示字的屏幕”来看待但搞懂它的像素结构对写驱动帮助很大。OLED屏的每一个像素点由阳极、有机发光层、阴极等几层结构组成通电后有机材料发光所以它是自发光器件不需要背光对比度很高。SSD1306芯片控制的是整个128x64点阵内部有一块显存大小是128x64/81024字节每个bit对应一个像素点1亮0灭。重点来了SSD1306的显存不是按“行”连续存放的而是按“页”划分。128x64的屏幕分成8页Page每页是8行像素宽度128列。也就是说你要显示一个点需要同时告诉控制器页地址0~7和列地址0~127。我建议在驱动里直接准备一个1KB的缓存数组所有绘图操作都先往这个数组里写最后一次性刷新到SSD1306这样能避免频繁I2C通信造成的闪烁也方便做局部更新。3.2 I2C时序与写命令/写数据流程SSD1306在I2C协议下7位设备地址通常是0x3C如果模块上SA0引脚被拉高地址就变成0x3D。实际很多模块背面只有一个I2C地址选择电阻默认是0x3C。还有一个易混点如果你在代码里看到0x78那是0x3C左移一位后的8位写地址本质是同一个地址。I2C写入数据时SSD1306规定第一个字节是控制字节0x00表示后面跟着的是命令0x40表示后面跟着的是数据。HAL库里只需要调用HAL_I2C_Mem_Write这个函数就能完成整个过程比如写命令HAL_I2C_Mem_Write(hi2c1, 0x78, 0x00, I2C_MEMADD_SIZE_8BIT, cmd, len, 100);第二个参数是8位写地址0x78第三参数0x00就是控制字节表示写命令。如果想写数据把第三参数改成0x40即可。这里的100是超时时间单位毫秒正常情况下几毫秒内就能完成一帧刷新。3.3 从初始化序列到点亮屏幕的最小驱动SSD1306上电后需要发送一串初始化命令才能正常显示这个序列在各家驱动里大差不差。我用的是中景园OLED例程里的那套初始化命令实测稳定。关键命令包括关闭显示0xAE、设置显示时钟分频0xD5、设置电荷泵开启0x8D, 0x14、设置显示偏移、设置起始行、开启显示0xAF等。static const uint8_t ssd1306_init_cmds[] { 0xAE, 0x20, 0x00, 0xB0, 0xC8, 0x00, 0x10, 0x40, 0x81, 0x7F, 0xA1, 0xA6, 0xA8, 0x3F, 0xD3, 0x00, 0xD5, 0x80, 0xD9, 0xF1, 0xDA, 0x12, 0xDB, 0x40, 0x8D, 0x14, 0xAF };这段序列逐条发送屏幕就会点亮。之后每次刷新显示内容要先把列地址设为0页地址设为0然后连续输出1024字节的显存数据SSD1306会自动递增列地址。用水平寻址模式的话写满一页后会自动跳到下一页代码实现起来很顺手也不用每次写一页就去设置一次页地址。4. 代码实现显示字符串、数字与汉字4.1 基于HAL库的I2C发送函数封装为了让上层代码更简洁我习惯把I2C写命令和写数据再封装一层。新建一个oled.c里面实现OLED_Write_Cmd和OLED_Write_Data两个函数分别调用HAL_I2C_Mem_Write。这样后面无论写初始化序列还是刷新显存都用这两个函数不会让业务代码里到处是HAL调用也方便调试。void OLED_Write_Cmd(uint8_t cmd) { HAL_I2C_Mem_Write(hi2c1, 0x78, 0x00, I2C_MEMADD_SIZE_8BIT, cmd, 1, 100); } void OLED_Write_Data(uint8_t data) { HAL_I2C_Mem_Write(hi2c1, 0x78, 0x40, I2C_MEMADD_SIZE_8BIT, data, 1, 100); }这里有个细节传参cmdHAL库内部会复用同一个buffer所以调用前不需要重新复制一份但要注意函数执行期间buffer不能被修改。因为是阻塞式调用线程安全方面暂时不用太担心。4.2 坐标定位与画点、清屏函数驱动里最底层的是OLED_DrawPoint根据(x, y)坐标计算对应的页和列然后对显存数组的某一位进行置1或清零。这里x范围是0~127y范围是0~63。计算方式是页地址 y / 8列地址 x对应的bit位 y % 8。因为显存是逐行排列这个映射关系一开始容易搞混我建议画个表格对照验证一下。清屏函数其实就是在内存里把所有字节置零然后整体刷新一次。注意如果只清内存不刷屏屏幕上内容是不会消失的。同样显示一帧画面的流程是先改内存里的数据再一次性刷到SSD1306。很多新手改了显存忘记刷屏然后一直以为屏幕坏了其实只是数据没送出去。void OLED_Clear(void) { memset(oled_gram, 0, sizeof(oled_gram)); OLED_Refresh(); } void OLED_Refresh(void) { uint8_t i; for (i 0; i 8; i) { OLED_Write_Cmd(0xB0 i); // 设置页地址 OLED_Write_Cmd(0x00); // 设置列地址低4位 OLED_Write_Cmd(0x10); // 设置列地址高4位 HAL_I2C_Mem_Write(hi2c1, 0x78, 0x40, I2C_MEMADD_SIZE_8BIT, oled_gram[i * 128], 128, 100); } }这里我一次发送128字节数据正好是一整页的内容速度和可靠性都不错。4.3 字符显示与汉字取模工具的使用显示字符的原理是查表。SSD1306内部没有字库所以需要自己维护一个像素字模数组。最常用的是8x16或者16x8的ASCII字模网上随便一搜就是全套。OLED_ShowChar函数的逻辑是根据字符的ASCII值从字模表里取出对应数据然后按位写入显存。显示字符串就是在底层循环调用OLED_ShowChar并让x坐标累加字符宽度。显示汉字稍微麻烦一点常见做法是用取模软件生成16x16的字模。我用的是PCtoLCD2002设置要点是阴码、列行式、逆向、每行显示两字节。取模方式选“列行式”是为了匹配SSD1306的页扫描顺序否则字体会出现错位或者镜像。生成的字模数组放到oled_font.h里调用方式和ASCII字符类似只是字宽从8变成了16。void OLED_ShowChinese(uint8_t x, uint8_t y, uint8_t index, uint8_t size) { uint8_t i; for (i 0; i size / 8; i) { OLED_ShowGram(x i * 8, y, chinese_font[index * 32 i * 2], chinese_font[index * 32 i * 2 1]); } }取模的时候不要选“逐行式”不然在页模式下会出现上下半截错位。这是很多新手卡壳的重灾区我一开始也栽过后来发现只要统一用列行式取模问题直接消失。5. 实测记录与常见问题排查5.1 OLED白屏、不亮先查供电和地址遇到过最多的情况就是屏幕白屏或者完全没反应。我的排查顺序是先看VCC是否接到3.3V很多模块虽然标了5V兼容但直接接5V可能烧掉稳压部分稳妥起见统一用3.3V。然后看SCL和SDA是不是接反了这是杜邦线接线最容易犯的错。接着确认I2C地址默认0x3C如果模块上SA0跳线被焊到了高电平就要改成0x3D。还有一个隐藏坑OLED模块的GND要和单片机共地。如果板子用USB供电模块又单独接了电源两边不共地的话I2C波形会乱屏幕大概率不亮。5.2 花屏、乱码时钟频率和显存操作要背锅屏幕能亮但显示乱码先不要怀疑屏幕坏了多半是数据没写对。最常见的原因是I2C频率太高400kHz模式下杜邦线过长或者接触不良发送数据出错。降频到100kHz基本能解决。另一个原因是显存字节顺序不对比如汉字取模选错了模式显示出来就是镜像或者碎块。还有一点容易忽视如果你在主循环里频繁调用OLED_Refresh同时又有中断服务程序在修改显存可能造成数据错乱。虽然概率不高但工程要长期稳定运行就在刷新前关闭中断或者用临界区保护一下。我实测下来裸机场景加不加影响不大但如果后续移植FreeRTOS这步必须加。5.3 CubeMX生成工程后的编译与下载问题CubeMX生成的MDK工程有两个高频报错点。第一个是路径里有中文编译器报“cannot open source file”之类解决办法就是工程路径全英文。第二个是芯片型号选错C8T6的Flash是64KB如果代码量超过这个值链接阶段会报Flash溢出这时候要么裁代码要么换CBT6。下载方面用ST-Link接SWDIO、SWCLK、GND连烧录器之前确认BOOT0跳线帽在低电平0否则芯片会进入ISP模式而不是从Flash启动下载后程序也不跑。如果下载时报“No target connected”优先检查接线和驱动是否安装ST-Link的驱动有时候会被系统更新冲掉重装一遍就好。最后再分享一个实测有用的习惯写完驱动后用逻辑分析仪抓一次I2C波形看看起始条件、停止条件、ACK位是否正常。有了波形图很多玄学问题都会变得很直观。OLED驱动这个项目虽然简单但其中涉及的I2C协议、显存映射、时序配合这些点后面无论是驱动其他传感器还是移植LVGL都能复用得上。本文还有配套的精品资源点击获取