☰
STM32F103VE驱动0.96寸OLED:水平滚动与垂直水平滚动实现
2026/10/14 19:51:34 网站建设 项目流程

简介:本资源面向STM32嵌入式初学者与显示驱动开发者,提供基于STM32F103VE的0.96寸OLED(SSD1306)显示、水平滚动及垂直水平滚动示例程序,帮助解决超长文字滚动显示与屏幕多方向滚动效果的实现问题。压缩包共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而不是硬件I2C

STM32F103VE的硬件I2C外设在实际项目里翻车概率不低,尤其是多设备挂载或长线缆场景下,容易出现总线锁死、ACK丢失。软件I2C用两个普通GPIO模拟时序,速率虽然只能到100kHz~400kHz,但驱动一块128×64 OLED刷一帧也就几毫秒,完全够用。更重要的是,软件I2C的时序你能用示波器逐位看,出问题好排查。

我一般选PB6做SCL、PB7做SDA,这两个脚在F103VE上位置好接线,也方便和别的外设错开。下面是最小可用的软件I2C底层。

// 软件I2C底层,PB6=SCL, PB7=SDA #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参数实际控制的是上半页,下半页自动用page+1。如果你要显示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固定0x00
0x00起始页0
0x07滚动间隔0x00~0x07
0x07结束页7
0x00dummy固定0x00
0xFFdummy固定0xFF

垂直水平滚动命令格式(0x29):

字节含义典型值
0x29命令垂直+水平右滚
0x00dummy0x00
0x00起始页0
0x07滚动间隔0x00~0x07
0x07结束页7
0x01垂直偏移0x01~0x3F
0x00dummy0x00
0xFFdummy0xFF

滚动间隔参数控制每帧滚动的速度,值越小滚得越快。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_page=0, end_page=3。

滚动开启后,屏幕内容会持续滚动,直到你发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_page=0, end_page=7但显存里只有前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停止命令,不要穿插其他命令。如果确实需要调对比度,先停滚动,调完再开。这个细节在数据手册里写得很隐蔽,但实际调试时经常遇到,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询