STM32F103VE驱动0.96寸OLED:水平滚动与垂直水平滚动实现
简介本资源面向STM32嵌入式初学者与显示驱动开发者提供基于STM32F103VE的0.96寸OLEDSSD1306显示、水平滚动及垂直水平滚动示例程序帮助解决超长文字滚动显示与屏幕多方向滚动效果的实现问题。压缩包共283个文件约8.22MB包含42个c源文件、44个h头文件、58个o目标文件及57个crf编译中间文件另有uvprojx、uvoptx工程文件、hex烧录文件、map映射文件与sct分散加载文件覆盖STM32F10x标准外设库中TIM、FLASH、RCC、I2C、USART等模块便于直接编译与二次开发。目前已有1166人学习下载。读者可获取完整可运行的Keil工程掌握SSD1306初始化、显存写入、水平滚动与垂直水平滚动命令配置方法并借助工程结构与中间文件理解编译链接流程快速移植到同类OLED项目。1. 0.96寸OLED屏在STM32F103VE上的最小点亮路径0.96寸OLED屏幕在嵌入式圈子里几乎是“人手一块”的显示模块128×64分辨率、SSD1306驱动芯片、I2C或SPI接口成本低、资料多、点亮快。但真正让不少人卡住的不是“点亮”而是标题里那三个词显示、水平滚动、垂直水平滚动。前两个是基础第三个才是分水岭——很多人能显示字符却不知道怎么让画面动起来更不知道SSD1306的滚动命令有硬件限制。这篇笔记面向的是手上已经有STM32F103VE开发板、一块0.96寸OLED、想从“能显示”走到“能滚动”的嵌入式开发者。我会按“先跑通I2C通信 → 再封装显示驱动 → 再吃透滚动命令 → 最后避坑”的顺序讲每一步都给出可复现的代码和参数说明。滚动这块SSD1306的硬件滚动不是万能的它有明确的地址窗口限制搞不清楚就会遇到“滚一半花屏”“垂直滚动没反应”这类玄学问题。我用的方案是软件I2C驱动SSD1306不依赖HAL库的硬件I2C原因是F103VE的硬件I2C在部分板子上有已知的稳定性问题软件I2C时序可控、移植方便。如果你用的是硬件I2C通信层替换即可显示和滚动逻辑完全通用。2. 先把I2C通信和SSD1306初始化跑通2.1 为什么选软件I2C而不是硬件I2CSTM32F103VE的硬件I2C外设在实际项目里翻车概率不低尤其是多设备挂载或长线缆场景下容易出现总线锁死、ACK丢失。软件I2C用两个普通GPIO模拟时序速率虽然只能到100kHz~400kHz但驱动一块128×64 OLED刷一帧也就几毫秒完全够用。更重要的是软件I2C的时序你能用示波器逐位看出问题好排查。我一般选PB6做SCL、PB7做SDA这两个脚在F103VE上位置好接线也方便和别的外设错开。下面是最小可用的软件I2C底层。// 软件I2C底层PB6SCL, PB7SDA #include stm32f10x.h #define I2C_SCL_PIN GPIO_Pin_6 #define I2C_SDA_PIN GPIO_Pin_7 #define I2C_PORT GPIOB // 微秒级延时72MHz主频下粗略实现 static void i2c_delay(void) { volatile uint8_t i 10; while (i--); } static void scl_high(void) { GPIO_SetBits(I2C_PORT, I2C_SCL_PIN); } static void scl_low(void) { GPIO_ResetBits(I2C_PORT, I2C_SCL_PIN); } static void sda_high(void) { GPIO_SetBits(I2C_PORT, I2C_SDA_PIN); } static void sda_low(void) { GPIO_ResetBits(I2C_PORT, I2C_SDA_PIN); } // I2C起始条件SCL高时SDA由高变低 static void i2c_start(void) { sda_high(); scl_high(); i2c_delay(); sda_low(); i2c_delay(); scl_low(); i2c_delay(); } // I2C停止条件SCL高时SDA由低变高 static void i2c_stop(void) { sda_low(); scl_high(); i2c_delay(); sda_high(); i2c_delay(); } // 发送一个字节返回从机ACK状态 static uint8_t i2c_send_byte(uint8_t data) { uint8_t i, ack; for (i 0; i 8; i) { if (data 0x80) sda_high(); else sda_low(); data 1; i2c_delay(); scl_high(); i2c_delay(); scl_low(); i2c_delay(); } // 第9个时钟读ACK sda_high(); // 释放SDA i2c_delay(); scl_high(); i2c_delay(); ack GPIO_ReadInputDataBit(I2C_PORT, I2C_SDA_PIN); scl_low(); i2c_delay(); return ack; // 0表示ACK }这段代码的关键在时序延时。i2c_delay里的循环次数需要根据你的主频调整72MHz下10次空循环大约1~2微秒对应约200kHz的I2C速率。如果通信不稳定先把延时加大到20~30确认能通再逐步减小。SDA引脚在发送ACK阶段要切换为输入模式实际工程里我会在i2c_send_byte前后动态切换GPIO方向上面为了简洁省略了你移植时记得补上。2.2 SSD1306初始化命令序列与参数含义SSD1306上电后必须按固定序列写命令顺序错了屏幕不亮或者显示错位。I2C写命令的格式是起始条件 → 从机地址0x78写→ 控制字节0x00 → 命令字节 → 停止条件。控制字节0x00表示后面跟的是命令0x40表示后面跟的是数据。#define OLED_ADDR 0x78 // 默认从机地址SA0接地 // 写一个命令字节 void oled_write_cmd(uint8_t cmd) { i2c_start(); i2c_send_byte(OLED_ADDR); i2c_send_byte(0x00); // 控制字节命令 i2c_send_byte(cmd); i2c_stop(); } // 写一个数据字节 void oled_write_data(uint8_t data) { i2c_start(); i2c_send_byte(OLED_ADDR); i2c_send_byte(0x40); // 控制字节数据 i2c_send_byte(data); i2c_stop(); } // SSD1306初始化序列 void oled_init(void) { oled_write_cmd(0xAE); // 关闭显示 oled_write_cmd(0x20); // 设置内存寻址模式 oled_write_cmd(0x00); // 水平寻址模式 oled_write_cmd(0xB0); // 页地址起始 oled_write_cmd(0xC8); // COM输出扫描方向反向 oled_write_cmd(0x00); // 低列地址 oled_write_cmd(0x10); // 高列地址 oled_write_cmd(0x40); // 显示起始行0 oled_write_cmd(0x81); // 对比度设置 oled_write_cmd(0xCF); // 对比度值0xCF偏亮 oled_write_cmd(0xA1); // 段重映射列127映射到SEG0 oled_write_cmd(0xA6); // 正常显示非反色 oled_write_cmd(0xA8); // 多路复用比 oled_write_cmd(0x3F); // 1/64 duty oled_write_cmd(0xA4); // 输出跟随RAM内容 oled_write_cmd(0xD3); // 显示偏移 oled_write_cmd(0x00); // 偏移0 oled_write_cmd(0xD5); // 时钟分频 oled_write_cmd(0x80); // 默认值 oled_write_cmd(0xD9); // 预充电周期 oled_write_cmd(0xF1); oled_write_cmd(0xDA); // COM硬件配置 oled_write_cmd(0x12); oled_write_cmd(0xDB); // VCOMH电压 oled_write_cmd(0x40); oled_write_cmd(0x8D); // 电荷泵设置 oled_write_cmd(0x14); // 开启电荷泵 oled_write_cmd(0xAF); // 开启显示 }几个参数值得单独说。0x20后面跟的0x00是水平寻址模式这个模式决定了你写显存时列地址自动递增是后面滚动功能的基础。0xC8和0xA1这两个命令控制屏幕的物理方向如果你的屏幕显示上下颠倒或左右镜像就是这两个值需要改。0x81后面的0xCF是对比度0x00最暗、0xFF最亮0xCF在多数模块上观感舒适如果屏幕偏暗可以试0xEF。初始化完成后屏幕应该是全黑RAM内容随机的话可能有点但通常上电RAM是0。接下来封装一个清屏函数和写显存函数就能开始显示内容了。3. 显示驱动封装从显存到字符3.1 显存组织方式与页地址计算SSD1306的显存是128×64位按页组织每页8行共8页。水平寻址模式下写一个数据字节会填充当前页的当前列然后列地址自动加1写到第127列后自动跳到下一页第0列。这个特性让整屏刷新变得简单设置好起始页和列然后连续写1024个字节即可。// 全屏显存缓冲区1024字节 128列 × 8页 uint8_t oled_buffer[1024]; // 清空缓冲区 void oled_clear_buffer(void) { for (int i 0; i 1024; i) oled_buffer[i] 0x00; } // 把整个缓冲区刷到屏幕 void oled_refresh(void) { for (uint8_t page 0; page 8; page) { oled_write_cmd(0xB0 page); // 设置页地址 oled_write_cmd(0x00); // 列低地址0 oled_write_cmd(0x10); // 列高地址0 for (uint8_t col 0; col 128; col) { oled_write_data(oled_buffer[page * 128 col]); } } }oled_buffer的索引方式是page * 128 col这是水平寻址模式下的自然布局。刷屏时逐页设置地址再连续写128字节比每写一个字节都设一次地址快得多。如果你用硬件I2C可以把整页128字节用DMA一次性发出去速度还能再提。3.2 用取模软件生成字库并写入指定位置显示字符需要字库。常见做法是用PC端取模软件生成字模数组参数选“阴码逐列式顺向”对于8×16的ASCII字符每个字符16字节。下面是一个简化的字符写入函数假设你已经有了一张8×16的ASCII字库表。// 假设字库ASCII 32~127每个字符16字节 extern const uint8_t ascii_8x16[][16]; // 在指定页和列写入一个ASCII字符 // page: 0~7, col: 0~120留出8列宽度 void oled_draw_char(uint8_t page, uint8_t col, char ch) { if (ch 32 || ch 127) return; const uint8_t *font ascii_8x16[ch - 32]; for (uint8_t i 0; i 8; i) { // 上半页 oled_buffer[page * 128 col i] font[i]; // 下半页 oled_buffer[(page 1) * 128 col i] font[i 8]; } } // 写字符串 void oled_draw_string(uint8_t page, uint8_t col, const char *str) { while (*str) { oled_draw_char(page, col, *str); col 8; if (col 120) break; // 简单防溢出 } }这里有个细节8×16字符占两页所以page参数实际控制的是上半页下半页自动用page1。如果你要显示16×16的中文字符每个字符32字节占两页16列写入逻辑类似但列步进改成16。取模时注意“逐列式”和“逐行式”的区别SSD1306的显存是列优先的用逐列式取模才能直接对应。调用顺序是oled_clear_buffer()→oled_draw_string(0, 0, Hello)→oled_refresh()。如果屏幕没反应先检查oled_write_cmd里从机地址对不对再用逻辑分析仪抓一下I2C波形看ACK有没有正常拉低。4. 水平滚动与垂直水平滚动的硬件实现4.1 SSD1306滚动命令的寄存器格式SSD1306内置了硬件滚动功能不需要你手动搬显存发一条命令屏幕就自己滚。滚动命令分两种水平滚动0x26/0x27和垂直水平滚动0x29/0x2A。命令后面要跟6个参数字节格式固定。水平滚动命令格式以0x27右滚为例字节含义典型值0x27命令右水平滚动0x00dummy固定0x000x00起始页00x07滚动间隔0x00~0x070x07结束页70x00dummy固定0x000xFFdummy固定0xFF垂直水平滚动命令格式0x29字节含义典型值0x29命令垂直水平右滚0x00dummy0x000x00起始页00x07滚动间隔0x00~0x070x07结束页70x01垂直偏移0x01~0x3F0x00dummy0x000xFFdummy0xFF滚动间隔参数控制每帧滚动的速度值越小滚得越快。0x00最快0x07最慢。垂直偏移量只在垂直水平滚动里有效表示每水平滚动一步的同时垂直偏移多少行范围1~63。4.2 水平滚动示例代码与参数调节// 开启水平滚动 // dir: 0左滚(0x26), 1右滚(0x27) // start_page/end_page: 滚动区域页范围 0~7 // interval: 滚动速度 0~7越小越快 void oled_scroll_horizontal(uint8_t dir, uint8_t start_page, uint8_t end_page, uint8_t interval) { oled_write_cmd(dir ? 0x27 : 0x26); oled_write_cmd(0x00); // dummy oled_write_cmd(start_page); // 起始页 oled_write_cmd(interval); // 滚动间隔 oled_write_cmd(end_page); // 结束页 oled_write_cmd(0x00); // dummy oled_write_cmd(0xFF); // dummy oled_write_cmd(0x2F); // 开启滚动 } // 关闭滚动 void oled_scroll_stop(void) { oled_write_cmd(0x2E); }调用oled_scroll_horizontal(1, 0, 7, 0)就是全屏右滚速度最快。如果你只想让某几页滚动比如第2到第5页就把start_page设1、end_page设4。这里有个容易翻车的点滚动区域是按页划分的不是按像素行所以垂直方向的最小粒度是8行。想让屏幕上半部分滚、下半部分不动就设start_page0, end_page3。滚动开启后屏幕内容会持续滚动直到你发0x2E停止。注意滚动的是显存里的内容如果你在滚动过程中刷新显存画面会撕裂。正确做法是先停止滚动刷新完再重新开启。4.3 垂直水平滚动示例与偏移量控制// 开启垂直水平滚动 // dir: 0垂直左滚(0x2A), 1垂直右滚(0x29) // v_offset: 垂直偏移 1~63 void oled_scroll_vertical_horizontal(uint8_t dir, uint8_t start_page, uint8_t end_page, uint8_t interval, uint8_t v_offset) { oled_write_cmd(dir ? 0x29 : 0x2A); oled_write_cmd(0x00); oled_write_cmd(start_page); oled_write_cmd(interval); oled_write_cmd(end_page); oled_write_cmd(v_offset); // 垂直偏移量 oled_write_cmd(0x00); oled_write_cmd(0xFF); oled_write_cmd(0x2F); }垂直水平滚动的效果是画面一边水平移动一边上下漂移v_offset越大垂直方向漂移越快。实际用的时候v_offset设1~3比较自然设太大画面会像“掉帧”一样跳。这个功能适合做开机动画或者待机效果但要注意它同样受页范围限制垂直偏移是在页内偏移不是整屏平移。我一般会这样组合先显示一屏内容然后开垂直水平滚动跑几秒再停掉滚动、清屏、显示正常界面。滚动期间不要调用oled_refresh否则显存和滚动位置会打架。5. 滚动功能避坑与常见问题排查5.1 滚动开启后屏幕花屏或只滚一部分现象发完滚动命令屏幕不是整屏滚而是某几页乱滚或者出现残影。原因起始页和结束页设置超出了实际显示内容范围或者滚动区域和当前显存内容不对齐。SSD1306的滚动是按页窗口做的如果你设了start_page0, end_page7但显存里只有前4页有内容后4页是空的滚动时空白页也会参与看起来就像花屏。解决滚动前先把整个显存刷成你想要的内容确保滚动窗口内的每一页都有合理数据。如果只想让部分区域滚就把start_page和end_page严格限制在有内容的页范围内。另外滚动开启前先发一次0x2E停止之前的滚动避免命令叠加。5.2 垂直滚动没有垂直位移效果现象调用了0x29命令画面只有水平滚动垂直方向纹丝不动。原因v_offset参数设成了0或者设了但被后面的dummy字节覆盖。垂直水平滚动命令里第6个参数字节才是垂直偏移很多人照着水平滚动的格式写把第6个字节写成了0x00结果垂直偏移为0自然不垂直动。解决确认命令序列是8个字节命令7参数第6个参数是v_offset范围1~63。另外垂直偏移的效果在页边界处才明显如果滚动区域只有1页垂直偏移会被页边界截断看起来像没动。至少设2页以上的滚动区域。5.3 滚动速度调不了或者调了没变化现象改interval参数滚动速度感觉没区别或者改了之后屏幕闪烁。原因interval只控制帧间间隔实际滚动速度还受I2C刷新率和屏幕本身响应时间影响。如果你在滚动过程中还在频繁刷显存I2C带宽被占满滚动就会卡顿看起来像速度没变。另外interval设0x00时最快但有些模块在最快档会轻微闪烁这是正常的。解决滚动期间停止一切显存刷新操作。如果觉得最快档还是慢检查I2C速率是不是被软件延时拖低了把i2c_delay的循环次数减小。闪烁问题可以把interval调到0x01或0x02牺牲一点速度换稳定。5.4 滚动和显示刷新互相干扰导致画面撕裂现象一边滚动一边更新显示内容画面出现上下半屏不同步、撕裂。原因SSD1306的滚动是硬件在GDDRAM上操作的而你写显存是通过I2C改GDDRAM两者同时进行就会冲突。滚动指针和写指针不同步画面就撕裂。解决严格遵循“停滚动 → 刷显存 → 开滚动”的顺序。如果应用需要动态更新内容同时滚动那就别用硬件滚动改用软件滚动每次刷新时把显存内容整体移位一列再刷屏。软件滚动费CPU但可控硬件滚动省CPU但要求内容静态。5.5 不同批次模块滚动行为不一致现象同一份代码有的模块滚得正常有的模块滚起来方向反了或者不滚。原因0.96寸OLED模块的SSD1306芯片有不同版本部分兼容芯片如SSD1315在滚动命令上行为略有差异。另外模块的COM扫描方向配置0xC8/0xC0会影响滚动的视觉方向。解决先确认模块驱动芯片型号如果是SSD1315滚动命令基本兼容但初始化序列可能需要微调。方向反了就把0x26和0x27对调或者改0xC8为0xC0。最稳妥的办法是在初始化后加一个自检开一次滚动看方向对不对不对就切命令。6. 把滚动做成可复用的状态机与验证方法滚动功能单独调通不难难的是把它嵌到实际项目里还不影响其他逻辑。我的习惯是做一个轻量的显示状态机把“静态显示”“水平滚动”“垂直水平滚动”当成三个状态用定时器或主循环轮询切换。typedef enum { DISP_IDLE, // 静态显示 DISP_SCROLL_H, // 水平滚动 DISP_SCROLL_VH // 垂直水平滚动 } disp_state_t; disp_state_t disp_state DISP_IDLE; uint32_t disp_tick 0; // 每10ms调用一次 void disp_task(void) { disp_tick; switch (disp_state) { case DISP_IDLE: // 静态显示什么都不做 break; case DISP_SCROLL_H: if (disp_tick 1) { oled_scroll_horizontal(1, 0, 7, 0); } if (disp_tick 300) { // 滚3秒 oled_scroll_stop(); disp_state DISP_IDLE; disp_tick 0; } break; case DISP_SCROLL_VH: if (disp_tick 1) { oled_scroll_vertical_horizontal(1, 0, 7, 2, 1); } if (disp_tick 300) { oled_scroll_stop(); disp_state DISP_IDLE; disp_tick 0; } break; } }这个状态机的关键是进入滚动状态时只发一次滚动命令然后靠disp_tick计时到时间就停。不要在滚动状态里反复发命令否则滚动会不断重置看起来像卡住。disp_tick的计数周期取决于你的调用频率10ms一次的话300就是3秒。验证滚动是否正常我一般用两个方法。一是逻辑分析仪抓I2C波形确认滚动命令的8个字节完整发出没有丢字节。二是显示一屏有规律的图案比如棋盘格或者递增数字滚动时观察图案是否连续、有没有跳变。棋盘格特别适合验证垂直水平滚动因为垂直偏移会让格子错位一眼就能看出偏移量对不对。最后说一个我踩过的坑滚动命令发完后如果紧接着发其他命令比如设置对比度滚动可能会被意外终止。SSD1306的滚动状态对后续命令敏感所以滚动开启后除了0x2E停止命令不要穿插其他命令。如果确实需要调对比度先停滚动调完再开。这个细节在数据手册里写得很隐蔽但实际调试时经常遇到希望帮到你。本文还有配套的精品资源点击获取