尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32驱动0.96寸OLED实战:SSD1306水平滚动与垂直滚动详解

STM32驱动0.96寸OLED实战:SSD1306水平滚动与垂直滚动详解 简介一份基于STM32F103VE的0.96寸OLEDSSD1306显示示例工程围绕超长文字水平滚动与屏幕垂直水平滚动两个典型应用场景提供可直接编译运行的完整程序源码。面向正在学习I2C驱动OLED、需要实现滚动显示效果的嵌入式开发者尤其适合在无操作系统环境下快速移植验证。压缩包共283个文件以C源码、H头文件、编译中间文件o、d、crf及Keil工程配置文件uvprojx、uvoptx为主另含map、hex、axf等构建产物整体8.22MB目录结构完整便于直接打开工程查看或烧录测试。资源包含两篇配套博文的全部示例代码展示了滚动命令配置、显示缓冲区操作、延时处理等关键细节并附有工程配置说明可帮助读者理解SSD1306滚动机制快速复现和改编到自己项目中。目前已有1164人学习适合具备基础STM32开发经验的初学者进阶参考。 做嵌入式项目的朋友应该都有过这种经历核心逻辑调通了就差一个能直观显示状态的屏幕。0.96寸OLED屏体积小、功耗低、接口简单几乎是调试信息和状态展示的首选而它配套最常用的两个功能就是水平滚动和垂直水平滚动。这篇文章围绕STM32F103VE开发板把0.96寸OLEDSSD1306控制器的驱动、两种滚动模式以及实测中踩过的坑完整梳理一遍给准备在项目里直接抄作业的人一份能跑通的示例程序也顺带解释清楚为什么滚动显示这种功能用硬件实现而不是靠软件刷屏幕。我在多个项目里用过不同厂家的0.96寸OLED模块说实话这类屏幕的驱动芯片90%以上都是SSD1306或者兼容芯片区别只在于模块的接口形式是I2C还是SPI、有没有带RES引脚。只要把底层驱动写对网上随便找的初始化代码基本都能点亮。真正让人头疼的往往是滚动参数配置很多资料只贴了一串命令完全不解释每一字节的含义导致换个厂家屏幕就翻车。这篇文章直接把命令拆开讲透。1. 接线和驱动方式I2C还是SPI怎么选STM32F103VE是100脚的增强型芯片外设资源非常充裕I2C1、I2C2、SPI1、SPI2都有。驱动0.96寸OLED最常见的有I2C和SPI两种方式各有各的适用场景。我的建议是如果是做显示状态、菜单、滚动字幕这类对刷新率要求不高的功能无脑选I2C两根线就搞定如果要跑动态波形、动画或者频繁整屏刷新就选SPI速度能到10MHz以上显示更顺畅。我自己的习惯是用GPIO模拟I2C而不是用STM32的硬件I2C外设。原因很朴素0.96寸OLED的I2C时钟只要100kHz到400kHz就够用模拟I2C按400kHz跑完全没压力模拟I2C代码从STM32F103搬到F407、G030几乎不用改只要换一下引脚宏定义。硬件I2C虽然省CPU但不同型号的库实现有差异调试起来反而浪费时间。模块的I2C接线很简单四针模块只有VCC、GND、SCL、SDA。我常用的接法如下OLED模块引脚STM32F103VE引脚说明VCC3.3V注意看模块丝印部分模块支持5V供电GNDGND共地必须接SCLPB8模拟I2C时钟线可以随意换引脚SDAPB9模拟I2C数据线RESPB10可选复位引脚建议接上如果买到的是七针模块还会有DC和CS引脚。I2C模式下DC固定接地CS接高电平或者直接接VCC这两个脚用来选择从机地址和禁用SPI片选。RES引脚很多模块内部有RC复位电路悬空也能工作但我建议还是接到GPIO上因为初始化时做一次硬复位能规避不少上电时序问题。I2C从机地址也要留意。SSD1306的7位地址默认是0x3C转换成8位写地址就是0x78如果模块背面的地址选择电阻被短接到另一侧7位地址变成0x3D写地址变成0x7A。很多例程里面写0x78或者0x7A并不是随便写的对应的是模块上的地址跳线。初始化之后如果I2C总线波形正常但屏幕没反应第一个要查的就是这个地址。2. 初始化序列把SSD1306配置到能显示的状态SSD1306的初始化看起来很复杂其实拆开看就是一组固定配置命令。我整理了一份在STM32F103VE上实测可用的初始化序列每一步的作用注释在代码里。先搞定最底层的I2C写命令和写数据函数#define OLED_ADDR_W 0x78 // 7位地址0x3C左移一位写方向 void OLED_WriteCmd(uint8_t cmd) { I2C_Start(); I2C_SendByte(OLED_ADDR_W); I2C_SendByte(0x00); // Co0, D/C0表示后续字节是命令 I2C_SendByte(cmd); I2C_Stop(); } void OLED_WriteData(uint8_t dat) { I2C_Start(); I2C_SendByte(OLED_ADDR_W); I2C_SendByte(0x40); // Co0, D/C1表示后续字节是显示数据 I2C_SendByte(dat); I2C_Stop(); }I2C模式下访问SSD1306每次传输的第一个字节是从机地址第二个字节是控制字节。控制字节里的D/C位决定了后面跟着的是命令还是数据0x00表示命令0x40表示数据。不理解这一层后面所有代码都容易写偏。然后是初始化序列void OLED_Init(void) { // 先做一次硬复位RES引脚拉低至少10us再拉高 GPIO_ResetBits(GPIOB, GPIO_Pin_10); delay_us(100); GPIO_SetBits(GPIOB, GPIO_Pin_10); delay_ms(100); OLED_WriteCmd(0xAE); // 关闭显示初始化期间先关屏 OLED_WriteCmd(0xD5); OLED_WriteCmd(0x80); // 时钟分频因子 OLED_WriteCmd(0xA8); OLED_WriteCmd(0x3F); // 多路复用比1/64 OLED_WriteCmd(0xD3); OLED_WriteCmd(0x00); // 显示偏移0 OLED_WriteCmd(0x40); // 显示起始行0 OLED_WriteCmd(0x8D); OLED_WriteCmd(0x14); // 开启内部电荷泵 OLED_WriteCmd(0x20); OLED_WriteCmd(0x02); // 页地址模式 OLED_WriteCmd(0xA1); // 段重映射解决左右镜像 OLED_WriteCmd(0xC8); // COM扫描方向解决上下镜像 OLED_WriteCmd(0xDA); OLED_WriteCmd(0x12); // COM引脚硬件配置 OLED_WriteCmd(0x81); OLED_WriteCmd(0xCF); // 对比度0xCF偏亮 OLED_WriteCmd(0xD9); OLED_WriteCmd(0xF1); // 预充电周期 OLED_WriteCmd(0xDB); OLED_WriteCmd(0x40); // VCOMH电平 OLED_WriteCmd(0xA4); // 从RAM内容显示 OLED_WriteCmd(0xA6); // 正常显示0xA7是反色 OLED_WriteCmd(0x2E); // 停止滚动让屏幕处于已知状态 OLED_WriteCmd(0xAF); // 开启显示 OLED_Clear(); }这里有几个关键点必须重点说。第一个是0x8D 0x14开启内部电荷泵。0.96寸OLED工作电压虽然只有3.3V但驱动OLED面板需要更高的电压这个电压由模块上的电荷泵产生。如果这一条漏了屏幕不是完全不亮而是亮度非常低肉眼几乎看不到内容。新手排查OLED不显示问题我第一个建议就是检查这条命令有没有发出去。第二个是0xA1和0xC8。如果不设置段重映射和COM扫描方向显存里的内容会左右和上下镜像表现就是字符是反的、上下颠倒。这个不是屏幕坏了是显存寻址方向没对上。第三个是0x20 0x02选择页地址模式。SSD1306把128x64像素分成8页每页8行像素页地址模式方便按页连续写入数据和后面显示的缓冲区分页结构天然对应。初始化之后要定义显存缓冲区。128x64像素的显存是1024字节定义为8行128列的二维数组uint8_t oled_buf[8][128];这个缓冲区非常关键。SSD1306的GDRAM不能直接读回来修改屏幕内容前先在本地缓冲区里改然后整帧写入屏幕这是最稳妥的做法。在刷显存前先设置列地址和页地址void OLED_SetPos(uint8_t page, uint8_t col) { OLED_WriteCmd(0xB0 page); // 页地址0~7 OLED_WriteCmd(col 0x0F); // 列地址低4位 OLED_WriteCmd(0x10 | (col 4)); // 列地址高4位 }写一页数据的时候先把列地址定位到该页的第0列然后连续发送128个字节void OLED_Refresh(void) { for (uint8_t page 0; page 8; page) { OLED_SetPos(page, 0); for (uint8_t col 0; col 128; col) { I2C_Start(); I2C_SendByte(OLED_ADDR_W); I2C_SendByte(0x40); I2C_SendByte(oled_buf[page][col]); I2C_Stop(); } } }每次刷整屏要发送1024字节的数据按400kHz I2C速度算理论上一帧大概20多毫秒实际测试刷新率能到30fps左右。对状态显示和滚动字幕这个量级完全够用。这个本地缓冲整帧刷新的结构也为后面的滚动功能提供了基础。3. 滚动不是CPU在滚硬件引擎与两种滚动示例滚动显示这个功能很多人第一反应是写一个定时器每次都把显存里的像素往左移一位再刷新整屏。这么做当然能实现效果但非常浪费CPU刷一屏1024字节移动一次像素还要循环嵌套主循环稍复杂一点就会出现肉眼可见的卡顿。SSD1306自带硬件滚动引擎这才是正经做法。滚动原理其实不复杂控制器内部有一个滚动状态机配置好滚动方向、滚动页范围和帧间隔后它会自动把GDRAM里的内容按设定间隔移动一个像素或一行整个过程完全由屏幕控制器硬件完成不占用STM32的CPU时间。更妙的是滚动搬运的是GDRAM里的内容RAM本身并没有被改变所以在滚动期间STM32照样可以往显存缓冲区里写新内容下一轮滚动就会把新内容带出来。先看纯水平滚动。SSD1306把滚动区域按页划分一页对应屏幕的8个像素行。整屏滚动就是从页0到页7如果只想让屏幕下面一行字幕滚动可以把起始页设成7、结束页设成7。帧间隔参数控制滚动速度具体映射关系是固定的参数值帧间隔0x005帧0x0164帧0x02128帧0x03256帧0x043帧0x054帧0x0625帧0x072帧帧间隔越小滚动速度越快。参数0x07对应2帧滚一次视觉上会有一点跳跃感0x00对应5帧是很多人默认使用的速度看起来比较适中0x01到0x03就是慢速滚动了。水平滚动示例程序void OLED_Scroll_Horizontal(uint8_t direction, uint8_t interval) { OLED_WriteCmd(0x2E); // 先停止滚动参数修改前必须停止 if (direction 0) { OLED_WriteCmd(0x26); // 水平向右滚动 } else { OLED_WriteCmd(0x27); // 水平向左滚动 } OLED_WriteCmd(0x00); // dummy byte OLED_WriteCmd(0x00); // 起始页0表示从页0开始 OLED_WriteCmd(interval 0x07); // 帧间隔 OLED_WriteCmd(0x07); // 结束页7表示到页7结束 OLED_WriteCmd(0x00); // dummy byte OLED_WriteCmd(0xFF); // dummy byte OLED_WriteCmd(0x2F); // 开始滚动 }调用方法很简单OLED_Scroll_Horizontal(1, 0x00); // 整屏向左滚动默认速度这里必须提醒一个容易踩的坑想要改变滚动参数一定要先发0x2E停止滚动参数配置完再发0x2F启动。如果直接在新参数后面发0x2F有些屏会继续用旧参数滚动有些屏直接卡住不动。再看垂直水平滚动也就是标题里说的对角线滚动。这种方式屏幕内容会沿着斜向循环移动视觉效果比纯水平滚动更醒目适合做广告屏或者告警提示。配置方式和水平滚动类似但要额外设置垂直滚动区域和垂直偏移量void OLED_Scroll_VerticalHorizontal(uint8_t direction, uint8_t interval, uint8_t voffset) { OLED_WriteCmd(0x2E); // 停止滚动 // 设置垂直滚动区域全屏64行都参与滚动 OLED_WriteCmd(0xA3); OLED_WriteCmd(0x00); // 顶部固定行数 OLED_WriteCmd(0x40); // 滚动区域行数64 // 垂直水平滚动 if (direction 0) { OLED_WriteCmd(0x2A); // 垂直水平向右滚动 } else { OLED_WriteCmd(0x29); // 垂直水平向左滚动 } OLED_WriteCmd(0x00); // dummy byte OLED_WriteCmd(0x00); // 起始页 OLED_WriteCmd(interval 0x07); // 帧间隔 OLED_WriteCmd(0x07); // 结束页 OLED_WriteCmd(voffset 0x3F); // 垂直偏移量范围0~63 OLED_WriteCmd(0x2F); // 开始滚动 }垂直偏移量voffset决定内容在垂直方向上的移动幅度范围0到63。设成0时只有水平移动设成20左右内容会明显地斜向移动设成64附近滚动效果接近垂直回绕。实际测试下来我比较喜欢voffset8配合interval0x00滚动节奏不会太晃眼。滚动效果测试时可以先在显存里写几行文字再看效果OLED_ShowString(16, 0, STM32F103VE OLED); OLED_ShowString(32, 2, Horizontal Scroll Demo); OLED_Refresh(); OLED_Scroll_Horizontal(1, 0x00); // 启动水平滚动OLED_ShowString这类函数本质是把字模点阵按页写入oled_buf数组不同字库实现的取模方式不一样接口可能不同。我在项目里惯用的做法是中文字库用16x16点阵ASCII字符用8x16点阵全部放在STM32F103VE的512KB Flash里容量完全不用担心。4. 移植、实测及踩坑记录这套代码我在STM32F103VET6最小系统板上跑过也在其他几个不同厂家模块上验证过整体稳定。但调试过程中踩过的坑蛮多的挑几个最有代表性的写在这里。第一个坑是屏幕亮度极低几乎看不见内容。这种情况十有八九是初始化序列里漏了0x8D 0x14。我用逻辑分析仪看过命令发出去了但屏幕只有非常淡的底纹加回开启电荷泵的命令后亮度立刻正常。注意部分模块的电荷泵命令可能是0x8D 0x10这是关闭电荷泵区别在于模块是否板载负压电路买模块时看原理图或者直接试两种配置。第二个坑是I2C总线卡死。GPIO模拟I2C时如果SCL或者SDA其中一个引脚忘了配置成开漏输出加上拉通信一开始就卡在等待应答那里。STM32的GPIO默认可能是推挽输出直接驱动I2C总线会导致电平冲突。我自己的标准配置是SCL和SDA都配置成开漏输出外部接4.7k上拉电阻到3.3V如果开发板上的I2C上拉电阻已经集成外部可以不接。第三个坑是显示内容镜像。第一批字显示出来是反的当时差点以为是屏幕坏了。后来看了SSD1306的数据手册才反应过来段重映射和COM扫描方向这两个命令影响显存和屏幕的映射关系0xA0和0xA1镜像0xC0和0xC8镜像。初始化里最好统一用0xA1加0xC8这是多数例程的标准组合。第四个坑是滚动不生效。现象是代码里明明调用了滚动函数屏幕纹丝不动。排查过程很有代表性先确认0x2F有没有发出去发现发了再确认滚动命令的参数发现起始页和结束页都正常最后才注意到是在初始化序列还没执行完毕时调用了滚动函数。OLED_Init最后虽然有0xAF开显示命令但紧接着调滚动函数时有些屏需要等几个毫秒让内部状态稳定。在OLED_Init末尾加一个50ms延时问题解决。第五个坑是滚动时闪烁。这个和帧间隔有关把interval设成0x07虽然滚动看起来很快但屏幕闪烁明显。0x04和0x05对应3帧和4帧间隔闪烁依然能察觉到。最终我大多数场景固定用0x005帧间隔平衡了速度和稳定性。另外如果显存刷新和硬件滚动同时进行I2C总线上数据量会翻倍这时候SPI接口的优势就体现出来了。第六个坑是滚动方向反了。不同厂家固件对0x26和0x27的左右定义不完全一致我手里的模块按照数据手册是右滚0x26、左滚0x27但也有模块是反的。遇到方向反了不需要改代码逻辑把direction参数取反就行。如果做了产品化设计最好在配置头文件里加一个宏定义#define SCROLL_DIRECTION_REVERSE 0 // 1表示方向反了再分享一个很多人忽略的细节滚动区域是按页划分的如果在水平滚动时把起始页设成0、结束页设成2屏幕上只有顶部24行像素在滚动下面40行保持静止。这种局部滚动非常适合做标题固定、内容滚动的界面。比如顶部显示菜单标题下面几页滚动态数据比整屏滚动更有实用性。垂直水平滚动还有一个特殊之处我在实测中确认过滚动过程中往显存缓冲区写新内容新内容会从滚动的边缘逐渐显现出来而不是立即出现在当前位置。这是因为硬件滚动只是在搬运GDRAM里已有的像素新的RAM内容要等滚动循环到对应位置才会显示。所以如果要做跑马灯式的文字更新更稳妥的做法是先把下一屏内容写进缓冲区再启动滚动显示效果最自然。代码的整体框架就是四部分模拟I2C底层、OLED初始化、显存缓冲区操作、滚动控制函数。从STM32F103VE这个平台迁移到其他型号只需要改I2C引脚的宏定义和延时函数其他部分可以原封不动搬走。我在实际项目里把这套驱动从F103迁移到G030系列改动量没超过20行。本文还有配套的精品资源点击获取
返回列表