
简介面向嵌入式初学者的STM32微控制器驱动0.96寸OLED显示屏滚动显示文字的完整Keil工程覆盖屏幕初始化、字符点阵取模、显存刷新和滚动方向速度控制等关键环节适合入门实践与项目参考。压缩包共90个文件以C语言源文件和头文件为核心包含工程配置、启动文件、编译生成的依赖文件、目标文件以及可直接烧录的十六进制文件整体仅1.72兆字节目录层级清晰便于按模块学习。目前已有1688人学习下载适合对照练习与实践参考。工程中配套延时、定时器、串口等外设驱动模块并给出滚动显示的具体实现思路可帮助理解OLED屏的显存管理机制头文件与C源文件分别承担接口声明和驱动逻辑便于二次开发。基于该工程可方便地扩展动画、菜单或传感器数据展示等应用适合课程设计、电子竞赛及物联网终端开发参考。1. 拿到OLED滚动显示的工程先弄明白这行字是怎么滚起来的把“OLED屏滚动显示文字”这个工程解压后最典型的代码结构是一份SSD1306驱动外加一个主循环。滚动显示的本质不是每隔几十毫秒重画一屏而是让屏幕驱动芯片或MCU在显存里做位移屏幕只管跟着刷新。实现路径有两条一条是SSD1306内置的硬件滚动引擎只发几条命令字就能滚起来另一条是MCU内部维护一块显存Buffer把整幅画面按位左移或右移一像素再刷新全屏。两条路径在STM32 HAL库、ESP32-IDF里的代码写法不同但思路完全一致。这篇从驱动指令讲到Buffer位移算法最后落到滚动参数的换算和“加了OLED函数就卡死”的排查。2. OLED滚动显示的基础SSD1306的页结构、接口与Buffer模型2.1 SSD1306把显示区划分成页硬件滚动的单位就是页市面上常见的0.96寸OLED和0.91寸OLED驱动IC基本都是SSD1306分辨率分别是128x64和128x32。SSD1306内部有一块对应整屏的GRAMGRAM按“页”组织每页高度8像素宽度128列。128x64的屏一共8页编号0~7128x32的屏只有4页编号0~3。这个页结构和滚动直接相关。SSD1306做硬件滚动时上报参数填的是起始页和结束页每次移动的步进就是一页也就是8个像素。汉字字模常用16x16点阵正好占两页高滚动时跨页处的笔画会被拆开视觉上就是上半段和下半段错位一帧。这也是很多初始跑硬件滚动的人第一次看到“字裂成两半”的原因不是你代码写错了是硬件滚动的最小粒度就是页。屏型总页数硬件滚动可选页范围页内数据位宽0.96寸 128x6480~78bit x 128列0.91寸 128x3240~38bit x 128列1.3寸 128x64部分为SH110680~78bit x 128列SH1106指令集和SSD1306接近但GRAM内部偏移和列寻址不同硬件滚动指令虽然能认实际滚动效果经常对不齐用之前先用基础画点例程确认列地址偏移量。2.2 I2C与SPI接口的差异只在软件滚动时体现滚动显示有两种刷新路径接口对它们的影响完全不同。硬件滚动是SSD1306控制器内部的GRAM搬运MCU只需要把配置指令发完之后控制器自己执行滚动CPU可以去做矩阵按键扫描、传感器读取这些任务。启动后滚动过程不占用I2C或SPI总线所以接口选型对硬件滚动几乎没有影响。软件滚动则不然。软件滚动需要MCU先把显存里的数据移位再整屏刷新刷新一次要把1KB显存全部推到屏幕。I2C工作在400kHz时1字节传输包含起始位、地址位、数据和应答位实际吞吐大约每秒50KB整屏1KB刷新就要20ms滚动帧率上限撑死50fps。SPI接口的时钟一般是几MHz到几十MHz同样刷1KB显存只需要几毫秒瓶颈反而变成了MCU执行移位运算的耗时。工程里用I2C做软件滚动时我一般把滚动区域限制在一行或一页的局部范围只刷新变化的那一小条屏幕而不是整屏刷新。0.91寸的128x32只有4页局部刷新的优势比128x64更明显。HAL库的I2C底层命令写法是固定的void OLED_WriteCmd(uint8_t cmd) { HAL_I2C_Mem_Write(hi2c1, 0x78, 0x00, I2C_MEMADD_SIZE_8BIT, cmd, 1, 100); } void OLED_WriteData(uint8_t data) { HAL_I2C_Mem_Write(hi2c1, 0x78, 0x40, I2C_MEMADD_SIZE_8BIT, data, 1, 100); }这里0x78是7位地址0x3C左移一位后的写地址0x00是控制字节中的命令标识0x40是数据标识。每个屏幕模块的I2C地址可能不同0x3C是比较常见的配置也有部分模块是0x3D先量模块背面的电阻或查数据手册不要把地址写死。2.3 显存Buffer决定了滚动后能不能回退很多初学者的OLED例程里没有Buffer显示字符时直接把字模一列一列写进屏幕这种写法做静态显示没问题但一旦配合滚动就麻烦。硬件滚动启动后控制器内部的GRAM会偏移MCU侧却不知道GRAM当前偏到了哪里。停止滚动的那一瞬屏幕停在某个中间偏移位置再往屏幕上写新内容新老内容就会叠加错位。所以我现在维护OLED工程时无论如何都会保留一个全局显存缓冲uint8_t OLED_Buffer[8][128];所有画点、显示字符、显示图片的函数只修改Buffer最后调用一次OLED_Refresh()把Buffer整体刷到屏幕。硬件滚动启动前先刷新一次滚动停止后再刷新一次Buffer内容就能和屏幕恢复一致。软件滚动则把Buffer当作画面本身移位运算直接在Buffer里做。江协科技STM32例程、正点原子OLED例程都是这个结构只是函数名不同。先看工程里有没有这个二维数组没有的话补一个再往下调滚动参数。3. 硬件滚动用HAL库下发SSD1306指令实现水平与垂直滚动3.1 水平滚动命令的完整参数序列SSD1306的水平滚动由两条命令触发0x26表示向右滚0x27表示向左滚。它们后面必须跟6个参数命令和参数合起来是7字节。配置完成后用0x2F激活滚动用0x2E停止滚动。在HAL库工程里封装一个水平滚动函数核心代码是这样的#define OLED_SCROLL_RIGHT 0x26 #define OLED_SCROLL_LEFT 0x27 void OLED_HorizontalScroll(uint8_t direction, uint8_t startPage, uint8_t endPage, uint8_t frameInterval) { uint8_t cmd[7]; OLED_WriteCmd(0x2E); /* 先关闭滚动保证可重入 */ cmd[0] direction; /* 0x26 右移0x27 左移 */ cmd[1] 0x00; /* 虚拟字节固定0 */ cmd[2] startPage 0x07; /* 起始页 */ cmd[3] frameInterval 0x07; /* 帧间隔 */ cmd[4] endPage 0x07; /* 结束页 */ cmd[5] 0x00; /* 虚拟字节固定0 */ cmd[6] 0xFF; /* 固定值 */ for (uint8_t i 0; i 7; i) { OLED_WriteCmd(cmd[i]); } OLED_WriteCmd(0x2F); /* 激活滚动 */ }这段代码里的关键是cmd[2]和cmd[4]必须满足endPage大于等于startPage否则滚动区域不成立。cmd[3]的frameInterval并不是滚动速度的毫秒数而是“滚动一步需要等待的帧数”0x00对应2帧0x07对应9帧每帧时长由SSD1306内部的显示时钟决定。这个参数的具体换算在第5章展开。调用方式OLED_HorizontalScroll(OLED_SCROLL_LEFT, 0, 7, 0x03);上面这行让整屏8页从左往右滚间隔是5帧。如果在128x32的0.91寸屏上滚动结束页最多写到3。3.2 垂直滚动必须先设置滚动区域SSD1306还支持垂直和水平同时滚命令是0x29和0x2A。0x29是向右下滚动0x2A是向左下滚动。这个模式多了一个“垂直滚动偏移”参数而且必须在滚动配置前用0xA3设置垂直滚动区域。垂直滚动区域的设置比较特殊0xA3后面跟两个参数第一个是上方固定不滚动的行数第二个是滚动区域的总行数。void OLED_SetVerticalArea(uint8_t fixedTopRows, uint8_t scrollRows) { OLED_WriteCmd(0xA3); OLED_WriteCmd(fixedTopRows); OLED_WriteCmd(scrollRows); }比如想让第16到第55行参与垂直滚动上方16行固定不动滚动区域40行OLED_SetVerticalArea(16, 40);但要注意0xA3设置的是垂直滚动的可滚动行范围水平滚动的页参数仍然有效。真正下发垂直水平滚动命令时参数结构是void OLED_VerticalHorizontalScroll(uint8_t direction, uint8_t startPage, uint8_t endPage, uint8_t frameInterval, uint8_t verticalOffset) { uint8_t cmd[7]; OLED_WriteCmd(0x2E); /* 关滚动 */ cmd[0] direction; /* 0x29 向右下0x2A 向左下 */ cmd[1] 0x00; cmd[2] startPage 0x07; cmd[3] frameInterval 0x07; cmd[4] endPage 0x07; cmd[5] verticalOffset 0x3F; /* 垂直偏移0~63 */ cmd[6] 0xFF; for (uint8_t i 0; i 7; i) { OLED_WriteCmd(cmd[i]); } OLED_WriteCmd(0x2F); }verticalOffset这个参数决定的是滚动起点在垂直方向上的初始位置。效果上它让画面在一开始就有一个纵向位移而不是每次移动固定量。实际工程里一般设为0真正让垂直位移变化的还是前面0xA3设置的滚动区域和滚动行数。3.3 初始化顺序0x2E停滚、配置、0x2F启动一个都不能错硬件滚动最常见的问题不是命令不会写而是启动顺序不对。SSD1306的滚动状态是全局的。如果上次滚动没有停或者初始化过程中不小心触发了滚动屏幕会一直在内部偏移状态。所以OLED初始化函数里要先把滚动关掉OLED_WriteCmd(0x2E); OLED_WriteCmd(0xAF); /* 打开显示 */完整顺序应该是关滚动0x2E、初始化显存和显示模式、写入显示内容、配置滚动参数、最后0x2F启动。还有一点容易被忽略硬件滚动启动后再去写GRAM写入的坐标会被滚动偏移影响导致内容显示不到预期位置。需要边写内容边滚动的话只能先停滚、写显存、重启滚动或者改用软件滚动。这个限制是芯片硬件行为无解。提示如果发现滚动后文字越滚越乱不要急着改速度参数先执行一次0x2E然后调用OLED_Refresh()全屏刷新看屏幕能否恢复正常。能恢复就说明滚动配置没问题问题出在滚动期间又写了显存。4. 软件滚动显存Buffer移位算法与定时器调度4.1 硬件滚动满足不了三个场景就得换软件方案硬件滚动虽然省CPU但它的能力边界很固定。第一滚动步长是页单位也就是8像素做不到平滑的1像素位移。第二滚动区域由页参数框死你要想只滚动“第3行到第5行”这一条硬件滚动做不到因为它最细的粒度是一整页。第三硬件滚动是周期性的滚到边后又从另一边冒出来做不了“滚到底就停住”的公告屏效果。这三个需求叠加起来就是“文字滚动显示”标题里最常见但硬件滚动搞不定的部分。解决的办法是软件滚动MCU自己维护显存Buffer每次把整幅画面的位数据移动1像素再把新画面推给屏幕。4.2 向左滚动一个像素的位运算实现软件左移的核心是按位移动一页数据有128列每列是8bit。想整体左移1bit就从第0列开始把当前列左移一位同时把第1列的最高位借过来。这个借位处理是滚动算法的关键。void OLED_ScrollLeftOnePixel(void) { uint8_t page, col; uint8_t carry; for (page 0; page OLED_PAGE_NUM; page) { carry 0; for (col 0; col OLED_COL_NUM; col) { uint8_t current OLED_Buffer[page][col]; OLED_Buffer[page][col] (current 1) | carry; carry current 7; } } OLED_Refresh(); }循环里的carry就是上一列移出的最高位把它补到当前列的最低位整页数据就能连续左移。最右边列的carry最终是0所以画面右侧会空出一列黑点。向右滚动同理只需要把方向反过来从第127列往第0列处理void OLED_ScrollRightOnePixel(void) { uint8_t page, col; uint8_t carry 0; for (page 0; page OLED_PAGE_NUM; page) { carry 0; for (col 127; col 0; col--) { uint8_t current OLED_Buffer[page][col]; OLED_Buffer[page][col] (current 1) | (carry 7); carry current 0x01; } OLED_Buffer[page][0] (OLED_Buffer[page][0] 1) | (carry 7); } OLED_Refresh(); }这段代码一定要倒着遍历列否则后一列还没读就被前一列覆盖画面会糊成一团。把左移和右移封装好后面的定时调度就简单了。4.3 用HAL定时器把滚动速度变成可调参数软件滚动不能直接在主循环里加HAL_Delay否则整个系统卡死矩阵按键扫描、串口响应全会停摆。更好的做法是用一个基本定时器产生周期中断在中断里计数主循环里判断计数阈值再执行滚动。static volatile uint16_t scrollTick 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { scrollTick; } }假设定时器配置成1ms中断一次主循环里这样调度#define SCROLL_INTERVAL_MS 20 void OLED_ScrollTask(void) { if (scrollTick SCROLL_INTERVAL_MS) { scrollTick 0; OLED_ScrollLeftOnePixel(); } }这里SCROLL_INTERVAL_MS是20也就是每20ms滚动1像素速度约为每秒50像素。128列宽的屏幕滚完一整屏需要2.56秒。把这个宏定义放在头文件里调速度只需改一个数字不用翻代码。定时器中断里只做累加计数不做移位运算和I2C传输把耗时的显示刷新放到主循环避免在中断里长时间占用总线。4.4 滚动内容从哪里来先把长字符串画进超宽Buffer软件滚动只能滚OLED_Buffer里的内容OLED_Buffer横向只有128列要滚动显示一条超过128像素宽的长字符串得先把字符串画到一个更宽的临时Buffer里再从中截取128列复制到OLED_Buffer。我一般用一个横向扩展的画布数组#define MAX_SCROLL_CANVAS_WIDTH 512 static uint8_t scrollCanvas[OLED_PAGE_NUM][MAX_SCROLL_CANVAS_WIDTH];先把整条字符串用字模取模函数画进scrollCanvas然后维护一个canvasOffset变量每次滚动将canvasOffset加1把scrollCanvas在canvasOffset处的128列复制到OLED_Buffer再刷新屏幕void OLED_ScrollFromCanvas(uint16_t offset) { uint8_t page, col; for (page 0; page OLED_PAGE_NUM; page) { for (col 0; col 128; col) { uint16_t srcCol offset col; if (srcCol MAX_SCROLL_CANVAS_WIDTH) { OLED_Buffer[page][col] scrollCanvas[page][srcCol]; } else { OLED_Buffer[page][col] 0x00; /* 超出画布宽度补黑 */ } } } OLED_Refresh(); }调用时把offset每次加1效果就是从长画布中平滑扫过一段窗口。这个方案的优点是不需要反复移位重组Buffer每帧只是做一次memcpy级的搬运滚动速度只取决于offset变化频率速度和可读性都比分步移位好。5. 滚动参数换算、卡死排查与多行拼接滚动5.1 滚动速度参数的真实含义与换算SSD1306硬件滚动的frameInterval参数数据手册给出的对应关系是0x00表示2帧移动一次0x01表示3帧一直到0x07表示9帧。帧时长不是固定的1ms或10ms它由SSD1306内部显示时钟和分频设置共同决定。在一个默认初始化参数下内部帧率大约100Hz那么0x03对应5帧滚动一步就是50ms相当于每秒移动20像素。0x00的2帧间隔是20ms每秒50像素。这个速度很快肉眼看着会比较跳实际工程里滚动的文字大小比较小时0x03到0x05之间观感更好。frameInterval取值帧间隔帧100Hz帧率下的步进时间近似速度像素/秒0x00220ms500x03550ms200x05770ms140x07990ms11软件滚动的速度换算更直观SCROLL_INTERVAL_MS是每像素耗时速度就是1000除以这个值。想在128x64屏幕上让一条文字大约3秒扫过128像素除以3秒约等于42像素每秒取整就是24ms移动一像素宏定义填24。5.2 “加了OLED滚动函数就卡死”的排查顺序工程里经常出现的情况是静态显示正常一加滚动函数就卡死。这个卡死通常不是显示芯片坏了而是MCU侧执行卡住。第一优先级查I2C地址和寄存器地址。HAL_I2C_Mem_Write的DevAddress写0x78是7位地址0x3C左移的结果少数屏幕是0x7A或0x7E地址不对时HAL库会返回HAL_ERROR然后进入超时等待表现就是程序卡在写函数里。第二优先级查初始化顺序。0x2E必须在初始化最前面发一次如果初始化里直接配置滚动参数而不关滚动芯片可能处在未知滚动状态写入GRAM的列地址会偏移显示一片乱码但不一定卡死。第三优先级查中断优先级。HAL_I2C_Mem_Write如果配置成长时效模式在定时器中断回调里频繁调用中断优先级比I2C外设中断高时I2C传输会被打断主循环里的HAL_I2C_Mem_Write一直拿不到总线锁程序就“卡死”了。解决办法是把滚动刷新放主循环中断里只置标志位。第四优先级的典型情况是之前的任务场景矩阵按键在OLED没有反应。可以检查按键扫描是否被OLED滚动显示阻塞。如果滚动用HAL_Delay实现一次Delay期间按键完全失去响应换成定时器计数方式后按键扫描能和滚动并行。5.3 多行文本滚动把两行内容先拼到同一行画布里硬件滚动整屏滚软件滚动默认也是整幅Buffer滚但业务场景里常常要求两行文字粘连在一起同步滚动比如上行是设备名下行是实时数值。这种情况的处理方式是先把两行分别画到画布的同一行区域再以整个画布为单位滚动。画布宽度按两行文本中最长的那条决定拼字符串时在Hex格式和数值格式之间留一个空格间隔void OLED_ScrollWithTwoLines(char *line1, char *line2) { drawStringToCanvas(scrollCanvas, line1, 0, 0); drawStringToCanvas(scrollCanvas, line2, 0, 1); /* 两行画进同一个scrollCanvas滚动时同时被扫过 */ }两行的16x16字模高度占两页所以上面例子中第二行画在0页和1页则两个drawString的yPage参数都需要对应调整不是“一行占一页”。16x16汉字占页0和页1把line2直接画在页1会重叠需要画到页2和页3。128x32屏只有4页能同时显示两行16px高度文字但滚动高度就是整个屏。若用128x64屏两行汉字占4页剩下4页还可以切一个独立区域显示不同步的滚动内容区域划分的关键是保持每部分各自页地址连续滚动配置也不会跨到其他区域。本文还有配套的精品资源点击获取