news 2026/10/15 16:25:40

STM32F103C8T6模拟I2C驱动SSD1306 OLED:从原理到避坑实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103C8T6模拟I2C驱动SSD1306 OLED:从原理到避坑实战

简介:这套针对 STM32F103C8T6 的 OLED 模拟 I2C 显示例程,主要面向嵌入式初学者和单片机开发者,演示如何使用 HAL 库通过 GPIO 引脚模拟 I2C 时序,驱动 0.96 英寸 OLED 显示屏。相比占用硬件 I2C 外设的方案,模拟方式更灵活,也适合在硬件接口被其他设备占用时使用,可作为学习 I2C 协议与 HAL 库工程配置的完整样例。压缩包共 159 个文件,大小约 4.83MB,以 C 源码和头文件为主体,含 HAL 库底层驱动与 OLED 显示模块;同时保留了 Keil 工程配置、CubeMX 项目文件、编译中间文件以及可烧录的 hex 文件,工程层级清晰,便于对照学习。已有 3083 人学习下载。通过该例程可以掌握 GPIO 推挽输出配置、软件模拟 I2C 通信时序、OLED 初始化序列与显存刷新流程,也能借鉴模块化的代码组织和常见问题排错思路;参照该工程可快速搭建自己的显示项目,并迁移到温湿度显示、菜单交互界面等类似场景。

1. 交付前 OLED 蓝屏:0.96 寸屏 + 模拟 I2C 这个例程到底在解决什么

做设备联调的最后一天,OLED 屏幕突然只能显示上半屏,用示波器量 SCL/SDA 波形,ACK 位偶尔丢,最后查出来是硬件 I2C 在中断繁忙时被低频任务抢占,总线状态机卡在 BUSY 状态。这种情况下很多人会直接换方案,把手头的 0.96 寸 SSD1306 OLED 从硬件 I2C 改成模拟 I2C——用两根普通 GPIO 按位翻转去拼出 I2C 时序。标题里的「STM32F103C8T6 + HAL 库 + 模拟 I2C」就是这条最常见的落地路径:不需要外设复用配置,不依赖芯片内部 I2C 状态机,代码逻辑完全可控。这篇文章会把从 CubeMX 配置到 SSD1306 初始化和显示字符的完整例程讲清楚,顺带把频率、上拉电阻、ACK 检测这些参数说透,适合正在被显示驱动卡住的嵌入式开发者。

2. 为什么选模拟 I2C 而不是硬件 I2C:SSD1306 在 F103C8T6 上的选型逻辑

2.1 硬件 I2C 的“玄学”翻车:从 BUSY 卡死到 ACK 丢失

STM32F103C8T6 内部有完整的 I2C 外设,理论上用 HAL 库调用HAL_I2C_Mem_Write就能往 SSD1306 写数据,代码还简洁。但实际项目里,硬件 I2C 的表现经常让人摸不着头脑:初始化时一切正常,跑了几分钟后HAL_I2C_GetState返回 BUSY,后续所有写操作超时;或者总线上一有毛刺,状态机就停在错误位,必须复位外设才能恢复。这类问题和具体的库版本、编译优化等级、中断优先级都有关系,有时换了块板子又复现不了,属于典型的“黑匣子”行为。

我见过某开发者把硬件 I2C 的出错现场拿到示波器上看:SCL 波形正常,SDA 在第 9 个时钟的 ACK 窗口没有拉低,从机没应答,但主机已经认为传输完成。SSD1306 对时序要求并不苛刻,问题多半出在主机侧的开漏配置或者总线被其他中断打断。硬件 I2C 外设一旦进入异常状态,需要走完一整串错误恢复流程,而在产线环境和现场调试时,你没有那么多时间去分析外设状态寄存器。

2.2 模拟 I2C 的原理:用两个 GPIO 拼出完整时序

模拟 I2C 的本质是用 GPIO 输出寄存器手动控制 SCL 和 SDA 的电平变化,按 I2C 协议把起始、停止、数据位、应答位逐个拼出来。每条总线上只有主机和从机两个角色,OLED 这种只收不发的外设(读操作极少用)特别适合模拟,因为它不需要处理复杂的总线仲裁和时钟同步。常见做法是把 SCL 和 SDA 配置为推挽输出,SDA 在需要读 ACK 时临时切换为输入模式,读完再切回输出。推挽模式配合外部上拉电阻,电平翻转速度比开漏模式更快,这也是模拟 I2C 能跑到 400kHz 级别的原因之一。

起始条件定义为 SCL 高电平时 SDA 从高变低,停止条件定义为 SCL 高电平时 SDA 从低变高。数据位在 SCL 高电平期间必须保持稳定,SCL 低电平时才允许 SDA 变化。写一个字节就是从最高位开始逐位输出,每个位在 SCL 上升沿被从机采样。第 9 个时钟是 ACK 位,从机如果正常接收会把 SDA 拉低作为应答。这套时序完全由软件循环控制,不受芯片内部状态机影响,只要 GPIO 初始化没问题,代码写对了就一定能跑,这是它最大的价值。

2.3 什么时候该用模拟 I2C,什么时候该用硬件 I2C

我的判断标准是看总线负载和从机数量。如果总线上只有一个 SSD1306,传输频率不超过 400kHz,模拟 I2C 完全够用,而且代码可控、排错直观。如果总线上挂了多个从机,比如温度传感器、EEPROM、OLED 共存,或者需要 DMA 批量传输、需要多主机仲裁,那就老老实实用硬件 I2C,把超时重试和错误恢复写好。另外,如果项目里原本就有现成的硬件 I2C 驱动,且跑了很久没出过问题,不要为了“统一方案”强行改成模拟,改动的风险远大于收益。

下面是两种方案的对比,可以帮你快速做决定:

对比项模拟 I2C(GPIO 翻转)硬件 I2C(外设状态机)
引脚选择任意 GPIO,灵活换脚固定复用引脚,如 PB6/PB7
传输速率由软件延时可调,常见 100k~400k硬件 PCLK 分频,最高 400k
错误恢复代码重新初始化即可需处理 BUSY、AF、BERR 等状态
多从机支持可支持,但需自行处理仲裁原生支持,配合 DMA 更高效
排错难度每一拍波形都在代码里,直观状态机复杂,依赖寄存器

3. 用 HAL 库搭模拟 I2C OLED 显示例程:从 CubeMX 到屏幕点亮

3.1 接线与 CubeMX 引脚配置

先确定硬件连接。0.96 寸 OLED 模块通常引出 4 个引脚:VCC、GND、SCL、SDA。VCC 接 3.3V,GND 接地,SCL 和 SDA 分别接到 STM32F103C8T6 的两个 GPIO。我习惯选 PB6 和 PB7,理由有两个:一是这两个引脚在默认的调试下载器接口旁边,接线方便;二是即使以后想切回硬件 I2C1,引脚可以复用,不用重新改板。需要注意,板上如果已经接了 10k 或 4.7k 上拉电阻到 3.3V,那就不用再加,如果没有,建议在 SCL、SDA 上各加一个 4.7k 上拉。OLED 模块背面很多已经自带上拉,具体看模块丝印或原理图。

在 CubeMX 里配置时,把 PB6、PB7 都设置为 GPIO_Output,输出模式选 Push Pull,速度选 High,初始电平选 High。这里有两个细节容易被忽略:一是不需要开启 I2C 外设,所以不用在 Connectivity 里勾选 I2C1;二是不要开复用功能,一旦开了 AF,GPIO 输出寄存器就控制不了引脚电平了。配置好之后生成代码,得到MX_GPIO_Init()函数,里面会初始化这两个引脚。

GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6 | GPIO_PIN_7, GPIO_PIN_SET); GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

这段代码把 PB6、PB7 设为推挽输出并默认拉高。推挽输出的驱动能力比开漏强,翻转速度快;初始电平设为高是为了避免上电瞬间 SDA、SCL 处于低电平而被误认为起始条件。外部上拉电阻由GPIO_NOPULL承担,也就是说内部上下拉都关闭,避免和外部上拉分压产生不确定电平。

3.2 软件 I2C 底层:起始、停止与字节收发

软件 I2C 的核心是四个基础函数:起始、停止、写字节、读字节。起始和停止函数足够简单,直接操作宏翻转电平即可。写字节函数里最关键的是位输出顺序,一定要从高位开始,逐位移位,每输出一位后拉高 SCL 让从机采样,再拉低 SCL 准备下一位。延时函数放在 SCL 高和低之间,用空循环实现微秒级延时,不需要精确到纳秒级别,但也不能太短,否则高速模式下信号毛刺会增多。

#define OLED_I2C_PORT GPIOB #define OLED_SCL_PIN GPIO_PIN_6 #define OLED_SDA_PIN GPIO_PIN_7 #define OLED_SCL_H() HAL_GPIO_WritePin(OLED_I2C_PORT, OLED_SCL_PIN, GPIO_PIN_SET) #define OLED_SCL_L() HAL_GPIO_WritePin(OLED_I2C_PORT, OLED_SCL_PIN, GPIO_PIN_RESET) #define OLED_SDA_H() HAL_GPIO_WritePin(OLED_I2C_PORT, OLED_SDA_PIN, GPIO_PIN_SET) #define OLED_SDA_L() HAL_GPIO_WritePin(OLED_I2C_PORT, OLED_SDA_PIN, GPIO_PIN_RESET) static void oled_i2c_delay(void) { volatile uint16_t i = 50; while (i--) { __NOP(); } } static void oled_oled_i2c_start(void) { OLED_SDA_H(); OLED_SCL_H(); oled_i2c_delay(); OLED_SDA_L(); oled_i2c_delay(); OLED_SCL_L(); } static void oled_oled_i2c_stop(void) { OLED_SDA_L(); OLED_SCL_H(); oled_i2c_delay(); OLED_SDA_H(); oled_i2c_delay(); OLED_SCL_L(); } static void oled_i2c_write_byte(uint8_t dat) { for (uint8_t i = 0; i < 8; i++) { if (dat & 0x80) { OLED_SDA_H(); } else { OLED_SDA_L(); } dat <<= 1; OLED_SCL_H(); oled_i2c_delay(); OLED_SCL_L(); oled_i2c_delay(); } }

这段实现里用宏封装 GPIO 操作,是为了后续如果想换成其他引脚,只需要改宏定义,不用动逻辑层。oled_i2c_delay()用 volatile 局部变量做空循环,编译器不会因为优化把它删掉。延时数值 50 在 72MHz 主频下大约能产生几微秒的延时,对应 I2C 速率在 100kHz 到 200kHz 之间。实际项目里我不建议把延时拆到每一位上单独调,那样代码可读性差,直接调这个统一的延时值就够了。

3.3 SSD1306 初始化序列:命令字与电荷泵

SSD1306 是 OLED 模块里的驱动 IC,它通过命令字控制显示方式,数据字节则直接映射到显存。初始化序列是固定套路,顺序不能乱:先关显示,设置时钟分频和 multiplex 比率,打开电荷泵,设置寻址模式,最后开显示。其中电荷泵必须打开,否则屏幕不会亮,这是新手最容易踩的坑。下面代码里OLED_WrCmd负责发送单个命令字节,发送格式是:起始、写地址 0x78(即 0x3C 左移一位)、写控制字节 0x00(表示后面是命令)、再写命令本身。

#define OLED_ADDR_WRITE 0x78 static void OLED_WrCmd(uint8_t cmd) { oled_oled_i2c_start(); oled_i2c_write_byte(OLED_ADDR_WRITE); oled_i2c_write_byte(0x00); // 控制字节:命令 oled_i2c_write_byte(cmd); oled_oled_i2c_stop(); } static void OLED_Init(void) { HAL_Delay(50); OLED_WrCmd(0xAE); // 关闭显示 OLED_WrCmd(0xD5); OLED_WrCmd(0x80); // 时钟分频 OLED_WrCmd(0xA8); OLED_WrCmd(0x3F); // multiplex 64 OLED_WrCmd(0xD3); OLED_WrCmd(0x00); // 显示偏移 OLED_WrCmd(0x40); // 起始行 0 OLED_WrCmd(0x8D); OLED_WrCmd(0x14); // 打开电荷泵 OLED_WrCmd(0x20); OLED_WrCmd(0x00); // 水平寻址模式 OLED_WrCmd(0xA1); // 段重映射 OLED_WrCmd(0xC8); // COM 扫描方向 OLED_WrCmd(0xDA); OLED_WrCmd(0x12); // COM 引脚配置 OLED_WrCmd(0x81); OLED_WrCmd(0xCF); // 对比度 OLED_WrCmd(0xA4); // 显示内容跟随 RAM OLED_WrCmd(0xA6); // 正常反色模式 OLED_WrCmd(0xAF); // 打开显示 }

0x3C是常见的 7 位从机地址,左移一位变成0x78是因为 I2C 传输时最低位表示读写方向,0 为写,1 为读。0x8D, 0x14这两个命令字是电荷泵开关,这是供电电压低于 OLED 内部驱动电压时升压的关键,漏掉它屏幕只有微弱的暗亮。初始化完成后可以用一个简单函数测试屏幕是否正常点亮。

3.4 往显存写数据:全屏刷新与缓存设计

SSD1306 的显存是 128*64 位,分为 8 页,每页 8 像素高、128 像素宽,所以完整缓存是 1024 字节。常见做法是在内存里维护一个uint8_t oled_buffer[1024],所有绘图函数先修改这个缓存,最后统一刷新到屏幕上。这样做的最大好处是避免频繁调用 I2C 写单字节,把多次小传输合并成一次大量传输。全屏刷新时先发一个设置列地址和页地址的命令序列,然后连续写入 1024 字节数据。

static void OLED_SetPos(uint8_t page, uint8_t col) { OLED_WrCmd(0xB0 + page); // 设置页地址 OLED_WrCmd(col & 0x0F); // 列地址低 4 位 OLED_WrCmd(0x10 | (col >> 4)); // 列地址高 4 位 } static void OLED_Refresh(void) { uint8_t i; OLED_SetPos(0, 0); oled_oled_i2c_start(); oled_i2c_write_byte(OLED_ADDR_WRITE); oled_i2c_write_byte(0x40); // 控制字节:数据 for (i = 0; i < 1024; i++) { oled_i2c_write_byte(oled_buffer[i]); } oled_oled_i2c_stop(); }

这个函数只发一次起始条件,数据连续写入 1024 字节,中间不加停止条件,因为 SSD1306 在连续写模式下会自动递增列地址和页地址。OLED_SetPos也可以用来定位到指定页和列,比如局部刷新时只更新某个区域。控制字节0x40告诉 SSD1306 后续字节都是数据,和命令控制字节0x00区分开。

3.5 显示字符串:字模与取模方式

字符显示依赖字模,把 ASCII 字符用逐行取模或逐列取模的方式转换成 816 或者 68 的字节数组。常见做法是在工程里放一个oled_font.h,用数组下标对应字符 ASCII 码,每个字符占用 16 字节(8*16 字模)。取模工具有很多,网上能找到,选择逐列取模、低位在前的方式和 SSD1306 的显存布局最匹配。显示字符串时先算好字符总数,逐个把字模写入oled_buffer对应位置。

const uint8_t oled_font_8x16[][16] = { {0x00, 0x00, 0x00, 0x00, 0x00, ...}, // 空格 // 其他字符 }; void OLED_ShowString(uint8_t page, uint8_t col, const char *str) { uint16_t buf_idx; while (*str) { uint8_t char_idx = *str - ' '; // 字模数组从空格开始 for (uint8_t i = 0; i < 16; i++) { if ((*str - ' ') * 16 + i >= 0) { // 计算该字符在缓存中的写入位置 } } ... str++; } }

这段代码我只展示了取字模的核心思路,实际写时要注意缓冲区的边界判断,防止字符写到显存末尾时越界覆盖前面的内容。816 字模在 12864 屏幕上最多同时显示 8 行,每行 16 个字符,超出了就要换页。如果只是测试点亮,直接在oled_buffer里填满0xFF再刷新,整屏会变亮,这就是验证驱动是否正常的快速方法。

4. 参数怎么设:模拟 I2C 频率、上拉电阻与 OLED 时序边界

4.1 频率怎么算:从延时循环到 SCL 实际速率

模拟 I2C 的速率不是靠 PCLK 分频得到的,而是由软件延时循环决定的。每次 SCL 高电平持续时间和低电平持续时间相加,再乘 2 才是完整周期。以 72MHz 主频为例,oled_i2c_delay()里volatile uint16_t i = 50的循环大约耗时 3~5 微秒,加上宏翻转 GPIO 的耗时,一个 SCL 周期大约 10 微秒,换算下来就是 100kHz 左右。如果你需要跑 400kHz,把延时值改到 10 以内,但不能改成 0,否则 GPIO 翻转本身的时间会让 SCL 高电平宽度太窄,从机采样容易出错。

// 想提高速率时,把延时值调小 static void oled_i2c_delay(void) { volatile uint16_t i = 8; // 72MHz 下约 1us 左右 while (i--) { __NOP(); } }

调参时建议用示波器实测 SCL 引脚波形,看高电平和低电平的宽度,标准 I2C 要求 SCL 高电平最小宽度约 4 微秒(100kHz 模式),高速模式 400kHz 时要求 0.6 微秒。调试时从大往小调,先保证能通信,再追求速率。很多模块上的 SSD1306 内部有时候序检查,SCL 频率太高会偶发丢数据,所以模拟 I2C 一般建议跑在 200kHz 左右,兼顾速度和稳定。

4.2 上拉电阻怎么选:从 10k 换到 4.7k 的调整经验

0.96 寸 OLED 模块的 I2C 引脚通常需要外部上拉电阻,模块出厂时有些已经焊好,有些是预留焊盘。我遇到过一块裸屏,用户直接飞线到 GPIO,没加上拉,结果初始化偶尔成功偶尔失败,波形低电平正常,高电平被拉不到 VDD。用 10k 上拉时信号上升沿偏缓,SCL 高电平在高速下还没稳定就被拉低,从机采样误判。换成 4.7k 上拉后,上升沿明显变陡,通信稳定很多。

上拉电阻上升沿特点适用场景
2.2k最快,功耗稍高长线、高速 400kHz
4.7k均衡,推荐常规 100kHz~200kHz
10k慢,低功耗短线、低速,对时序要求不高

挑选上拉电阻时还要考虑总线上挂的设备数量,设备多了等效并联电容变大,上拉阻值就需要适当减小。如果模块背板已经自带上拉,外部再加一个会形成并联,阻值变小,但这不会导致损坏,只是上升沿更快,一般不需要特意处理。测量上拉电阻是否合适的方法是看 SCL 高电平是否完整到达 VDD 且维持至少 4 微秒。

4.3 时序边界:起始保持时间、SCL 高电平宽度的注意点

I2C 协议对起始和停止也有时序要求,虽然 SSD1306 的数据手册没有明确标注严格的最小保持时间,但实际调试时我发现:起始条件中 SDA 拉低后,必须等 SCL 拉低这个动作完成,从机才能识别起始条件。有些驱动代码写得太随意,SDA 刚拉低就马上进入数据位发送,SCL 低电平宽度不够,导致从机把第一个数据位误判为起始的一部分。一个稳妥做法是保证每个电平跳变后至少执行一次延时循环。

static void oled_oled_i2c_start(void) { OLED_SDA_H(); OLED_SCL_H(); oled_i2c_delay(); OLED_SDA_L(); // SDA 先拉低 oled_i2c_delay(); // 保持 OLED_SCL_L(); // SCL 再拉低 oled_i2c_delay(); // 保持 }

我一般会把 start 和 stop 函数里的延时做成可配置,和字节收发共用一个延时函数。实际踩坑时最典型的错误是删掉 stop 延时,导致连续多次初始化时总线还停在半空状态,后面的起始条件判断不出。这段边界问题在逻辑分析仪上看得最清楚:正常波形里 SDA 和 SCL 的下降沿之间有明显间隔,异常波形里两个沿几乎重叠。

4.4 性能误区:模拟 I2C 速率不等于画面刷新率

很多人以为模拟 I2C 速率慢,OLED 刷新就慢,实际不是一回事。OLED 刷新率的瓶颈在显存写入量和重绘逻辑,不在总线速率。全屏 1024 字节,在 400kHz 下单字节耗时约 22.5 微秒,整屏刷新大约 23 到 30 毫秒,也就是每秒能刷 30 帧以上,这对大部分菜单界面、传感器数值显示已经完全够用。真正影响体验的是绘图函数里频繁调用 I2C 写单字节,比如画一条直线循环 100 次每次都发起始和停止,那就会明显卡顿。

正确的做法是图像绘制全部写进oled_buffer,最后统一OLED_Refresh()。如果需要高帧率动画,可以只刷新变化区域,比如一个实时波形图只更新最新一列,而不是整屏重写。只要理解了「改缓存快、刷屏慢」这个原则,模拟 I2C 的速率完全不会成为性能瓶颈。

5. 避坑:模拟 I2C 驱动 OLED 的 5 个常见问题与排查

5.1 现象:上电后屏幕全黑,初始化时卡死

原因:起始条件或地址发送失败,SSD1306 没收到正确起始条件,从机没有应答。最常见的是 GPIO 配置成了复用功能,或者引脚被其他外设占用,导致宏翻转没生效。

解决:先用最简单的点灯程序测试这两个脚能正常输出高低电平,再检查 CubeMX 里是否误开了 I2C 外设。用示波器量 SCL 和 SDA,看起始条件有没有出现:SCL 高时 SDA 下降沿。如果没有波形,检查代码有没有真正调用OLED_Init()。

5.2 现象:初始化能过,但显示全屏乱码或者重复图案

原因:显存地址推进方式错误,或者字模取模方式与 SSD1306 页地址方向不匹配。常见的是把逐行取模的数组当成逐列取模用,导致字符被拆成碎片。

解决:确认字模工具输出的取模方式是逐列、低位在前,和 SSD1306 的数据位排列一致。全屏填0xFF测试是否整屏全亮,如果亮得均匀,说明寻址正确;如果出现竖条纹或横条纹,就要检查列地址设置和缓存索引page * 128 + col是否一致。

5.3 现象:换了一组 GPIO 后屏幕不亮

原因:宏定义里的引脚和实际接线不一致,或者新引脚没有在 CubeMX 里初始化。很多人把OLED_SCL_PIN改成GPIO_PIN_8后,忘了改端口宏,导致还是操作原来的端口。

解决:核对三处——引脚号宏、端口宏、CubeMX 的 GPIO 配置。建议把所有引脚相关定义集中放在同一个头文件里,换板时只改一处。初始化完成后读取GPIOB->ODR寄存器,确认输出状态和预期一致。

5.4 现象:屏幕能亮,但显示内容偏左或偏右镜像

原因:SSD1306 的段重映射和 COM 扫描方向配置错误,0xA1和0xA0选反,或者0xC8和0xC0选反,导致左右或上下镜像。

解决:初始化序列里确认0xA1是正常的段重映射,0xC8是正常的 COM 扫描方向。如果模块是异形屏或非标准封装,可能需要反着设置,这时直接试验两套组合即可,不用怀疑 I2C 时序。

5.5 现象:模拟 I2C 能稳定工作,硬件 I2C 偶尔失败

原因:F103C8T6 的硬件 I2C 外设对中断优先级和总线复用比较敏感,某些库版本下存在已知的异常行为。这不是玄学,是外设状态机和软件配合的问题。

解决:如果项目已经切到模拟 I2C 且稳定,就不必强行用硬件 I2C。如果必须用硬件,建议开启超时重试和总线释放逻辑,比如连续检测到 BUSY 时,把引脚配置成 GPIO 手动翻转九次时钟,让从机释放总线。最终实践里,单从机 OLED 场景我优先选模拟 I2C,省心。

6. 进阶:局部刷新与双缓冲,把模拟 I2C 的帧率用到极致

模拟 I2C 驱动 SSD1306 想要提升显示帧率,核心思路只有一个:不要全屏刷。SSD1306 支持水平寻址模式和页寻址模式,通过命令0x21设置列地址范围、0x22设置页地址范围,就能只更新屏幕上某一个矩形区域。比如实时温度显示只需要更新数字所在的两个字符区域,大约 16 列 2 页,只写几十字节,比全屏写 1024 字节快一个数量级。

static void OLED_RefreshArea(uint8_t page_start, uint8_t page_end, uint8_t col_start, uint8_t col_end) { uint8_t p, c; OLED_WrCmd(0x21); OLED_WrCmd(col_start); OLED_WrCmd(col_end); OLED_WrCmd(0x22); OLED_WrCmd(page_start); OLED_WrCmd(page_end); for (p = page_start; p <= page_end; p++) { for (c = col_start; c <= col_end; c++) { OLED_WrCmd(0xB0 + p); OLED_WrCmd(c & 0x0F); OLED_WrCmd(0x10 | (c >> 4)); oled_oled_i2c_start(); oled_i2c_write_byte(OLED_ADDR_WRITE); oled_i2c_write_byte(0x40); oled_i2c_write_byte(oled_buffer[p * 128 + c]); oled_oled_i2c_stop(); } } }

这段代码演示的是页寻址模式的局部刷新:每次先设置页和列,再写一个数据字节。实际项目里可以做一层脏区标记,绘图时把改动的页记录下来,刷新时只遍历脏页,这样动画的帧率能提高三四倍。另一个建议是学会用OLED_RefreshArea做滚动数字时钟这类界面,秒位变化只刷右下角两个字符区域,拨动感非常流畅。

我自己的使用习惯是始终保留一份oled_buffer作为唯一真源,任何绘图函数只改缓存,刷新函数只管按区域搬运缓存到屏幕,不用在刷新时重复计算字模位置。遇到难调的显示偏移问题时,优先怀疑列地址计算,而不是 I2C 时序——时序出错通常是完全不亮或花屏,而不是轻微偏移。这些经验是从好几个联调项目里踩出来的,希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 16:25:10

Pycopy:存储优先的Python方言,让嵌入式与云端部署更轻量

简介&#xff1a;Pycopy是一个极简、轻量且可高度扩展的Python方言实现&#xff0c;源自MicroPython&#xff0c;面向桌面端、云端与微控制器等各类受限环境。该源码压缩包适合嵌入式及物联网开发者&#xff0c;也适合想理解精简解释器设计的程序员。项目实现了完整的Python 3.…

作者头像 李华
网站建设 2026/10/15 16:25:10

RoboMaster硬件体系入门:供电、通信与执行器全链路解析

1. 从零理解RoboMaster硬件体系&#xff1a;这份讲义到底在讲什么如果你刚加入学校的机器人战队&#xff0c;或者正准备参加RoboMaster这类对抗性机器人竞赛&#xff0c;大概率会遇到一个很现实的问题&#xff1a;软件组的同学在群里催着要CAN通信协议&#xff0c;视觉组的同学…

作者头像 李华
网站建设 2026/10/15 16:24:41

金秋螃蟹上市季!看看国标GB28181视频平台EasyGBS是怎么管控塘口的

九月下旬到十一月&#xff0c;是蟹塘一年里最要紧的两个月&#xff1a;起捕、分拣、暂养、发货挤在一起&#xff0c;人手连轴转&#xff0c;塘口却比任何时候都更“空”——人都在包装车间&#xff0c;塘里没人看。本文按养殖现场的实际节奏&#xff0c;拆解国标GB28181视频平台…

作者头像 李华
网站建设 2026/10/15 16:23:58

ng-redux异步数据加载完全教程:redux-thunk与AngularJS $http实战指南

【免费下载链接】ng-redux Angular bindings for Redux 项目地址&#xff1a; https://gitcode.com/gh_mirrors/ng/ng-redux 点击查看 免费下载 在 AngularJS 项目里做 Redux 异步数据加载&#xff0c;ng-redux 是官方推荐的绑定方案。本教程基于仓库内置的 Reddit 新闻加载示…

作者头像 李华
网站建设 2026/10/15 16:23:41

CC2541 BLE开发避坑指南:GATT数据库Flash分区与协议栈硬约束解析

简介&#xff1a;本资源是面向单片机开发者与嵌入式初学者的CC2541蓝牙BLE开发全栈资料包&#xff0c;聚焦单片机与Android设备间的低功耗蓝牙通信实战。内容涵盖官方开发手册、完整Keil工程例程、Android端BLE调试APP&#xff08;含BLEDemo.apk及多个可运行的Debug版APK&#…

作者头像 李华