阻塞IIC
152.7.1 阻塞式 I2C
/* 配套工程:视频配套/3、STM32单片机裸机部分/code/CubeIDE/13IIC */
/* Core/Src/i2c.c:I2C1,标准模式 100 kHz */
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 100000;
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 0;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
/* Core/Src/oled.c:配套工程实际采用阻塞发送 */
void Write_IIC_Command(unsigned char IIC_Command)
{
uint8_t IIC_Send_Cmd[] = {0x00, IIC_Command};
HAL_I2C_Master_Transmit(&hi2c1, 0x78, IIC_Send_Cmd, 2, 100);
}
void Write_IIC_Data(unsigned char IIC_Data)
{
uint8_t IIC_Send_Data[] = {0x40, IIC_Data};
HAL_I2C_Master_Transmit(&hi2c1, 0x78, IIC_Send_Data, 2, 100);
}
原笔记中的“ffc”“fnc”“IC”等是语音识别或 OCR 对
I2C的误识别;本稿统一写作 I2C。原有章节标题为便于同视频和原笔记对照而保留。配套工程目录为13IIC,其实际实现是 I2C1 + SSD1306 OLED + 阻塞式 HAL 发送,不是软件模拟 I2C。
一、STM32的ffc实验原理
I2C(Inter-Integrated Circuit)是两线、同步、串行总线:SCL 提供时钟,SDA 传输地址、读写位和数据。两根线都由总线上的上拉电阻拉到逻辑高电平;器件只负责把线路拉低或释放线路,因而多个器件可以共享同一对总线。空闲时 SCL、SDA 都应为高电平。
这不是普通的“推挽输出高低电平”通信。若把 I2C 引脚配置为推挽输出,主机与从机可能同时驱动相反电平,造成硬件争用;本工程生成的 GPIO_MODE_AF_OD 正是复用开漏模式。开发板或 OLED 模块常已带上拉电阻,是否还需外接、阻值多大,应以模块原理图、总线电压、线路长度和总线电容为准,不能不加判断地并联多个上拉电阻。
标准模式的最高速率是 100 kbit/s,不是“100 KB/s”;快速模式通常为 400 kbit/s。速率还会受上拉电阻、总线电容、连线长度和器件能力约束。本工程在 .ioc 中选择的是 I2C Standard,ClockSpeed = 100000,即 100 kbit/s。
一次典型写传输遵循下面的时序:
空闲(SCL=SDA=高)
-> START:SCL 为高时 SDA 从高变低
-> 7 位地址 + 写位(0) -> 从机 ACK
-> 1 个或多个数据字节,每字节后从机 ACK
-> STOP:SCL 为高时 SDA 从低变高
数据在 SCL 为高期间必须稳定,通常只在 SCL 为低期间改变;START 和 STOP 是例外。每个字节按最高位先行传输,并跟随第 9 个时钟的应答位。设备数不能简单等同于“128 个”:7 位地址空间中有保留地址,实际还受器件可选地址、总线电气负载和电容限制。
1.1 实验演示
本节配套工程在 MX_I2C1_Init() 后调用 OLED_Init()、OLED_Clear() 和八次 OLED_ShowCHinese(),显示的是字库中预置的汉字。下一节才把此前的定时器输入捕获结果移植到 OLED 显示;不要把两个工程的功能混为一谈。
接线时应先核对 OLED 模块丝印和手册。源码能够确定的信号连接只有:
| STM32F103ZET6 | I2C 信号 | OLED 模块常见标注 | 说明 |
|---|---|---|---|
| PB6 | I2C1_SCL | SCL / SCK | 时钟线 |
| PB7 | I2C1_SDA | SDA | 数据线 |
| GND | GND | GND | 必须共地 |
VCC 应接模块规定的供电电压,并确保 I2C 上拉后的高电平不超过 MCU 引脚允许范围。不要只因某模块有 “VCC” 引脚就默认它能接受 5 V,也不要把调试器供电、开发板供电和外部电源不加核对地同时相连。
1.2 STM32f103zet6手册细节
本工程使用 STM32F103ZET6 的 I2C1 默认引脚:PB6 为 I2C1_SCL,PB7 为 I2C1_SDA。CubeMX 生成的 HAL_I2C_MspInit() 做了三件关键事:打开 GPIOB 时钟、把 PB6/PB7 配置为高速复用开漏输出、打开 I2C1 外设时钟。I2C1 也可经重映射使用另一组引脚,但本工程没有启用重映射,排错和接线都应以 PB6/PB7 为准。
系统时钟由 HSE 经 PLL 配到 72 MHz,APB1 为 36 MHz;I2C1 位于 APB1。I2C 外设的时序寄存器由 HAL 依据外设时钟和 ClockSpeed 配置,手工改时钟树后必须重新生成或复核 I2C 时序,不能只保留原来的 100 kHz 数值就假定波形仍正确。
1.3 理论知识
I2C 常见有两种实现:
| 实现方式 | 特点 | 本节是否采用 |
|---|---|---|
| 软件模拟 I2C | 用普通 GPIO 手动产生 START、STOP、时钟和 ACK;引脚较灵活,但占用 CPU,时序由软件保证 | 否,旧 iic.c 调用被注释 |
| 硬件 I2C | 由 I2C 外设完成总线时序、地址和应答检测;应用只通过寄存器或 HAL 提交传输 | 是,使用 I2C1 |
I2C 从机地址必须区分三种写法。OLED 驱动中的 0x78 不是“未经处理的 7 位地址”:它对应 7 位地址 0x3C 左移一位后的 HAL 参数。
| 含义 | 数值 | 用途 |
|---|---|---|
| OLED 的 7 位从机地址 | 0x3C | 数据手册和扫描工具常这样显示 |
| 总线上的写地址字节 | `(0x3C << 1) | 0 = 0x78` |
| 总线上的读地址字节 | `(0x3C << 1) | 1 = 0x79` |
STM32F1 HAL 的 DevAddress 文档要求把 7 位地址左移一位后再传入。因此写成 0x3C 会导致地址位错误;写成 0x78 在本工程中是正确的。不同厂商库的 API 约定可能不同,移植前必须看所用库函数的 DevAddress 说明,不能用“HAL 会自动补读写位”一概而论。
1.3.1 建立工程
建立本节工程的可靠顺序如下:
- 选择 STM32F103ZET6,RCC 选择外部高速时钟 HSE,系统时钟配置为 72 MHz。
- 在 Pinout 中启用 I2C1,选择 PB6/PB7;在 System Core 中保留
Serial Wire,以便用 PA13(SWDIO)和 PA14(SWCLK)下载、调试。 - 在 I2C1 参数页选择标准模式、7 位寻址、100 kbit/s;本节阻塞发送不需要启用 I2C 事件和错误中断。
- 生成工程后,把 OLED 的
.c、.h和字库文件加入工程。应用源码应显式包含oled.h,再调用 OLED 接口。
配套 main.c 只包含了 main.h、i2c.h、gpio.h,却直接调用 OLED_Init() 等函数。这依赖编译器的宽松诊断,标准 C 中应补上 #include "oled.h",以获得正确的函数声明和参数检查。
1.3.2 配置参数
本工程的 I2C 初始化参数可解释为:
| 字段 | 工程值 | 含义 |
|---|---|---|
Instance | I2C1 | 选择硬件 I2C1 外设 |
ClockSpeed | 100000 | 标准模式 100 kbit/s |
DutyCycle | I2C_DUTYCYCLE_2 | 标准/快速模式下的时序参数 |
OwnAddress1 | 0 | 本工程只做主机,不作为被其他主机寻址的从机 |
AddressingMode | I2C_ADDRESSINGMODE_7BIT | 使用 7 位地址模式 |
DualAddressMode、GeneralCallMode | 关闭 | 不使用双地址和通用呼叫 |
NoStretchMode | 关闭 | 允许从机按协议需要拉低 SCL 延长时钟 |
阻塞模式没有在 .ioc 中启用 I2C1_EV_IRQn 和 I2C1_ER_IRQn,这与源码一致。以后若改为 _IT,只改函数名是不够的,必须同时启用对应 NVIC 中断并生成/实现中断服务函数。
1.3.3 应用案例
SSD1306 风格的 I2C 接口在从机地址后还需要一个控制字节:0x00 表示后续字节是命令,0x40 表示后续字节是显示数据。因此当前驱动的两字节有效载荷分别是:
写命令:START -> 0x78 -> ACK -> 0x00 -> ACK -> 命令字节 -> ACK -> STOP
写数据:START -> 0x78 -> ACK -> 0x40 -> ACK -> 数据字节 -> ACK -> STOP
每次 HAL_I2C_Master_Transmit() 返回前,当前地址和两个数据字节已经处理完毕,栈上的 IIC_Send_Cmd / IIC_Send_Data 仍然有效,因此这种局部数组在阻塞模式下安全。每次调用的返回值目前被忽略;工程化代码应检查 HAL_StatusTypeDef,在 HAL_TIMEOUT、HAL_BUSY 或 HAL_ERROR 时读取错误码并采取恢复措施。
二、STM32中文参考手册
2.1 I2C寄存器描述
理解 HAL 不要求逐位手写全部寄存器,但应知道 STM32F1 I2C 外设的职责:CR1/CR2 控制外设、START/STOP 和中断;OAR1/OAR2 保存自身从机地址;DR 是发送/接收数据寄存器;SR1/SR2 给出 START、地址应答、发送空、接收非空、仲裁丢失和应答失败等状态;CCR 与 TRISE 配置 SCL 时序。
本节使用 HAL 时,这些寄存器由 HAL_I2C_Init() 和 HAL_I2C_Master_Transmit() 操作。原笔记中把某些“清空/刷新”描述成 I2C 固有步骤并不准确:I2C 没有通用的“清屏寄存器”。OLED 清屏是向显示 RAM 写入 0,I2C 总线本身只负责传输字节。
2.2 例题1:I2C寄存器初始化配置
若从寄存器视角检查初始化,应确认以下对应关系,而不是抄写固定寄存器值:
- GPIOB 与 I2C1 时钟已经打开;PB6/PB7 是复用开漏而非推挽输出。
- I2C 外设时钟与目标 SCL 频率匹配;本工程目标是 100 kbit/s。
- 主机使用 7 位地址模式,并在发送 API 中传入左移一位的
DevAddress。 - 传输前总线应处于空闲;若 SCL 或 SDA 被持续拉低,应先排查接线、电压、上拉和从机复位,而不是反复调用发送函数。
HAL 的价值是把上述寄存器序列封装起来;学习寄存器是为了理解和定位问题,不应在已经使用 HAL 的工程里随意插入直接寄存器写操作,以免破坏 HAL 维护的状态机。
2.3 例题2:I2C在STM32CubeIDE中的移植
从 51 单片机的软件 I2C 移植到 CubeIDE 的核心是替换底层字节传输层,而不是把每一行旧代码原样复制:
旧驱动:IIC_Start / IIC_Write_Byte / IIC_Stop
↓
新驱动:HAL_I2C_Master_Transmit(&hi2c1, 地址, 缓冲区, 长度, 超时)
↓
保留:OLED_Init、OLED_Set_Pos、字库和“命令/数据”这类上层语义
oled.c 中保留了被注释的旧位操作调用,正好说明了替换位置。迁移后的 Write_IIC_Command() 和 Write_IIC_Data() 将控制字节与有效字节组成缓冲区,一次交给 I2C1 外设发送。
2.4 例题3: 移植五⺓单片机的I2C代码到STM32
这里的“五⺓单片机”应为“51 单片机”。移植时应逐项检查,而非根据能编译就判定完成:
| 检查项 | 正确做法 |
|---|---|
| 数据类型 | 优先使用 <stdint.h> 的 uint8_t、uint32_t;原驱动的 u8、u32 宏可暂时兼容,但应确保定义唯一且可见 |
| 头文件 | oled.c 包含 oled.h、oledfont.h、i2c.h;调用 OLED API 的 main.c 也应包含 oled.h |
| 句柄 | I2C 句柄 hi2c1 由 i2c.c 定义、在 i2c.h 中 extern 声明,不要在 OLED 文件中重新定义一个同名句柄 |
| 地址 | 确认模块实际 7 位地址,再按本 HAL 的规则左移一位传入 |
| 返回值 | 至少在调试阶段保留并检查 HAL_I2C_Master_Transmit() 的状态和 HAL_I2C_GetError() |
同一份 OLED 字库可以继续使用,但必须匹配显示函数索引和字模尺寸。地址不响应时,先用逻辑分析仪或 I2C 扫描程序确认 ACK,再检查 0x3C/0x3D 等实际地址,不能只凭模块外观认定地址。
2.5 例题4: 主函数移植与功能实现
主函数应在 HAL_Init()、时钟配置、MX_GPIO_Init() 和 MX_I2C1_Init() 之后再调用 OLED_Init()。这是因为 OLED 驱动第一次发送命令时,I2C 时钟、GPIO 复用和 hi2c1 都必须已就绪。
配套程序的顺序为:初始化 OLED、清屏、按坐标写入八个汉字、进入空循环。烧录后无显示时,可按“供电与共地 -> PB6/PB7 接线 -> 是否有上拉 -> I2C 地址是否 ACK -> 初始化是否成功 -> 字库和坐标”逐层排查;不要把显示问题直接归因于字体或延时。
三、阻塞IIC
3.1 问题提出
软件 I2C 驱动移植到 STM32 硬件 I2C 时,最容易遇到的不是 API 拼写问题,而是总线协议和库约定不一致:地址左移、开漏上拉、控制字节、初始化先后顺序、ACK 失败,以及在未完成前重复启动下一笔传输。
阻塞模式的含义是:调用 HAL_I2C_Master_Transmit() 后,CPU 会在该函数内轮询外设状态,直到本次传输完成、出错或超时才返回。它实现简单,特别适合本节这种上电后只初始化和显示少量内容的场景;代价是传输期间主循环不能处理其他实时任务。
3.2 IC初始化分析
本工程初始化链为:
HAL_Init / SystemClock_Config
-> MX_GPIO_Init
-> MX_I2C1_Init
-> HAL_I2C_MspInit:GPIOB、PB6/PB7 开漏复用、I2C1 时钟
-> HAL_I2C_Init:100 kbit/s、7 位寻址等外设参数
-> OLED_Init
OLED_Init() 中的每条配置命令最终都调用阻塞发送。若 MX_I2C1_Init() 失败,源码会进入 Error_Handler();若初始化成功但某次 OLED 发送无 ACK,则当前代码因忽略返回值难以定位,建议在精简示例之外增加错误处理。
3.3 IC接口功能分析
OLED_WR_Byte(dat, cmd) 是显示驱动与 I2C 传输层之间的接口:cmd == OLED_CMD 时调用命令发送,cmd == OLED_DATA 时调用数据发送。它不是直接操作 I2C 寄存器,而是把 OLED 的“命令/数据”语义翻译为控制字节 0x00 或 0x40。
OLED_Clear() 依次选择 8 个页地址,并对每页写入 128 个零字节。当前实现每写一个显示数据字节就发起一次独立的两字节 HAL 传输,即清屏至少发生 1024 次数据事务。以 100 kbit/s 粗略估算,仅地址和数据位就约需 0.28 s,未计 START/STOP 和软件开销。这是功能正确但效率不高的写法。
若后续要优化,应确认控制器确为 SSD1306 兼容、确认控制字节和连续写模式后,把同页多个数据字节放入一个 {0x40, data...} 缓冲区发送。不要在未核对控制器手册时机械改为批量传输。
3.4 IC接口头文件分析
驱动分层建议如下:
main.c :包含 oled.h,调用 OLED_Init / OLED_Show...
oled.h :对外声明 OLED 接口和必要类型
oled.c :包含 oled.h、oledfont.h、i2c.h,实现显示逻辑
i2c.h/.c :声明并定义 hi2c1,完成 CubeMX 生成的硬件初始化
头文件负责声明,.c 文件负责定义。不要在 oled.h 中定义 hi2c1 或完整字体数组,否则多个翻译单元会产生重复定义。字体数组若放在头文件,除非使用合适的 extern 声明和单独定义,否则更推荐迁移至一个 .c 文件。
3.5 IC接口参数分析
本节核心函数原型为:
HAL_StatusTypeDef HAL_I2C_Master_Transmit(
I2C_HandleTypeDef *hi2c,
uint16_t DevAddress,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout);
本工程调用中的参数含义:
| 参数 | 命令发送示例 | 说明 |
|---|---|---|
hi2c | &hi2c1 | 已完成初始化的 I2C1 句柄 |
DevAddress | 0x78 | 0x3C << 1,不是裸 7 位地址 |
pData | IIC_Send_Cmd | 待发送的 {0x00, 命令} 缓冲区 |
Size | 2 | 控制字节和有效字节共两个字节 |
Timeout | 100 | 最长等待 100 ms;单位是 HAL tick,通常为毫秒 |
超时不等于每次传输一定耗时 100 ms,它只是故障或总线异常时的上限。对于 100 kbit/s 的两字节有效载荷,正常耗时远小于该值;若频繁接近超时,应优先检查硬件和 ACK。
3.6 回顾过程
本节可按下面的逻辑回顾:
确认 OLED 电源、共地、上拉和 PB6/PB7
-> CubeMX 配置 I2C1 标准模式 100 kbit/s
-> 初始化 hi2c1
-> 按 HAL 规则传入左移后的从机地址
-> 用 0x00/0x40 区分 OLED 命令和数据
-> 阻塞等待每笔传输结束并检查状态
-> 再处理显示内容
这一过程的重点不是记住 0x78,而是知道它来自当前 OLED 的 7 位地址和当前 HAL 的参数约定。更换模块时地址、供电要求和控制器都可能变化。
3.7 编译问题
从笔记、网页或聊天记录复制 C 代码时,常把 Markdown 的 c、、行号或不可见字符一起复制进源文件,这些内容不是 C 语法,必须删除。遇到编译错误应先读取第一条实际报错和对应行,避免围绕后续连锁报错盲目修改。
本工程还存在一个应修正的包含关系:main.c 调用了 OLED 函数却没有 #include "oled.h"。有些旧编译选项会把它当作警告放过,但现代 C 编译器应将缺少函数声明视为错误或至少高优先级警告。
3.8 编码问题
3.8.1 编码错误实例分析
编码问题与字符编码问题要分开看。源文件若显示为乱码,先用编辑器确认其实际编码(常见为 UTF-8 或 GBK)并统一保存;注释乱码通常不改变编译结果,但标识符、字符串字面量或复制出的特殊符号可能影响编译。不要用删除任意代码的方式掩盖编码问题。
当出现 expected ... before ...、未声明标识符、重复定义等错误时,分别检查:是否混入 Markdown 标记,所需头文件是否已包含,类型宏是否已定义,是否有同名定义。把错误信息、文件名和行号保留下来,才能得到可复现的修复。
3.8.2 编码中const的使用与删除
原笔记中“移植到 STM32 必须删除 const”的说法不正确。配套工程的 oledfont.h 中 F6x8、F8X16 等字体数组本来就定义为 const unsigned char;只读字库应保持 const,这样编译器可以将其放入只读存储区,也能阻止意外写坏字模。
正确的判断标准是接口是否需要修改数据:只读参数用 const uint8_t *,需要写入的缓冲区才用 uint8_t *。若旧函数把只读字库传给了一个可写指针参数,应优先把该函数形参改为 const;只有数据确实需要修改时才建立可写副本,而不是为了“适配 STM32”删除 const。
3.8.3 编码中定义变量相关问题
u8、u32 是旧驱动常见别名,本工程在 oled.h 中把它们映射到 uint8_t、uint32_t。新代码优先直接使用标准定宽类型,并确保 <stdint.h> 通过头文件可见。局部发送缓冲区在阻塞模式下没有生命周期问题,因为函数返回时传输已完成;此结论不能直接套用到中断或 DMA 模式。
避免在多个 .c 文件中定义相同全局变量。hi2c1 只能在 i2c.c 定义一次,其余文件通过 i2c.h 的 extern 声明引用。需要由主循环和中断回调共同访问的状态标志应使用 volatile,并设计好原子访问和状态转换。
3.8.4 编码中头文件包含问题
推荐的包含原则是“使用什么声明,就包含声明它的头文件”。例如 oled.c 需要 OLED 函数声明、字体表和 I2C 句柄,故包含 oled.h、oledfont.h、i2c.h;main.c 使用 OLED API,则应包含 oled.h。不要仅依赖某个头文件间接包含了 stdint.h 或 HAL 头文件,这会使代码随包含顺序改变而失效。
3.9 编译通过
“编译通过”只说明语法、类型和链接关系在当前构建配置下成立,不证明 I2C 波形、模块电压和从机地址正确。完成构建后仍应观察是否有隐式函数声明、指针类型不匹配、未使用返回值等警告,并在实际板上验证 ACK 和显示效果。
3.10 烧录程序
使用 ST-LINK 通过 SWD 下载时,至少确认 SWDIO(PA13)、SWCLK(PA14)、GND 和目标板参考电压连接正确;NRST 可按调试需求连接。ST-LINK 的逻辑电平必须与目标板兼容。若开发板已经由 USB 或外部电源供电,不应未经确认再由调试器电源脚强行供电。
下载前先断开可能影响 PB6/PB7 的外部设备或确认其接线无短路。下载成功但 OLED 无显示时,优先检查 I2C ACK 和总线波形,不能把“程序烧入成功”误判为“外设已正常通信”。
3.11 移植过程
完整移植过程可归纳为:保留经过验证的 OLED 上层绘制函数和字库;在 CubeMX 配置硬件 I2C1;用阻塞 HAL 发送替代 IIC_Start、逐字节写和 IIC_Stop;补齐头文件与类型;确认地址左移和 0x00/0x40 控制字节;最后在硬件上验证每个阶段。
先让初始化、清屏和单个字符显示可靠,再引入输入捕获、传感器等业务数据。这样能将“通信问题”和“业务数据问题”分开,排错成本最低。
3.12 中断方式
3.12.1 中断方式的演示与介绍
HAL_I2C_Master_Transmit_IT() 启动的是非阻塞传输:函数很快返回,之后由 I2C 事件中断逐字节推进,结束时 HAL 调用 HAL_I2C_MasterTxCpltCallback()。它适合主循环还要做其他工作的场景,但不保证“更快显示”;总线物理速率仍由 SCL 决定,且每笔事务仍须按顺序完成。
配套工程没有实现 I2C 中断版本。视频中把阻塞调用直接替换为 _IT 后出现显示不完整,随后用延时掩盖现象。根因不应归结为“中断方式天然不好”:原阻塞函数中的两个局部数组会在函数返回后失效或被下一次调用覆盖,而异步 HAL 仍保留着该指针继续发送;同时,前一笔传输未完成就启动下一笔会得到 HAL_BUSY 或打乱命令、数据顺序。
3.12.2 中断方式的初始化与应用
改为 I2C 中断模式至少需要同时完成以下事项:
- 在 CubeMX/NVIC 启用 I2C1 事件中断和错误中断。
- 在对应中断服务函数中分别调用
HAL_I2C_EV_IRQHandler(&hi2c1)和HAL_I2C_ER_IRQHandler(&hi2c1)。 - 发送缓冲区在完成回调之前必须持续有效,可用静态缓冲区、由队列管理的缓冲区或明确所有权的对象;不能继续使用函数栈上的两字节数组。
- 只能在 I2C 处于空闲且上一笔已完成后启动下一笔。用完成回调推进一个小型状态机或发送队列,而不是连续调用
_IT后插入任意延时。 - 在
HAL_I2C_MasterTxCpltCallback()和错误回调中更新状态并安排后续工作;回调中不要调用会长时间阻塞的函数。
一个可靠的逻辑顺序是:空闲 -> 填充持久缓冲区 -> Start_IT 成功 -> 等待完成/错误回调 -> 释放当前缓冲区 -> 发送下一帧。主循环若只是在等待标志位而什么也不做,使用中断的收益很小;此时阻塞模式反而更直接。
3.12.3 例题1: 中断方式与普通方式对比
| 项目 | 阻塞 HAL_I2C_Master_Transmit | 中断 HAL_I2C_Master_Transmit_IT |
|---|---|---|
| 函数返回 | 整笔传输结束、失败或超时后 | 启动传输后立即返回 |
| CPU | 传输期间轮询等待 | 可在传输期间处理其他任务 |
| 缓冲区要求 | 返回前有效即可,局部数组可用 | 完成回调前始终有效,不能用即将失效的局部数组 |
| 下一笔传输 | 函数返回后可继续启动 | 必须等待完成回调后再启动 |
| 额外配置 | 本工程已具备 | I2C 事件/错误 NVIC、ISR、完成与错误回调、状态机 |
| 适合场景 | 初始化、低频少量显示、简单程序 | 有并行任务且能维护异步队列的程序 |
DMA 模式也不是“必然效果差”。它减少 CPU 对每个字节的参与,适合较长的连续数据块,但仍需要正确的 DMA 链接、完成/错误处理、缓冲区生命周期和事务串行化。对于本驱动每次只有两个字节的命令帧,DMA 配置与调度开销未必值得;若把整页显示数据批量化后,DMA 才更有实际价值。
3.13 回调函数交互
回调函数是异步传输的完成通知,不是“延时之后一定执行”的普通函数调用。HAL 在 I2C 事件中断中推进传输,成功结束才进入 HAL_I2C_MasterTxCpltCallback();NACK、总线错误、仲裁丢失或启动失败应由错误状态和错误回调处理。
对于 OLED,建议把“写命令”和“写数据”封装为待发送帧,再由单一发送器按顺序提交。发送器拥有缓冲区,完成回调只负责将其标为可复用并触发下一帧,主循环或任务层决定何时生成更多帧。这样命令、页地址和显示数据不会相互覆盖,也不需要用 HAL_Delay(1) 侥幸维持时序。
本节屏幕初始化和少量汉字显示并不需要强行改成异步。先把阻塞版本的接线、地址、ACK、错误处理和批量写策略验证好;只有当主循环确实因 I2C 等待影响其他任务时,再按完整的状态机方案切换到中断或 DMA。