I2C(韦东山HAL库)
(1)——I2C协议层次
一、前言
在嵌入式开发里,OLED 显示屏是常用输出设备,可显示字符、图片等。本文围绕 OLED 显示系统,从硬件连接、软件层次及 I2C 总线等方面全面解析,助大家复习知识,理解从应用程序到硬件驱动的流程 。
二、硬件基础
(一)OLED 与主控芯片的连接
OLED(以 SSD1306 为例)和主控芯片(如 STM32 )借 I2C 接口连接。从硬件原理图能看到,主控芯片的 PB6 引脚作 I2C1 的 SCL(时钟线),PB7 引脚作 I2C1 的 SDA(数据线),分别连到 OLED 模块对应引脚。电路里两个 10K 上拉电阻 R1、R2 接 3.3V 电源,确保 I2C 总线空闲时呈稳定高电平 。
(二)I2C 总线多设备连接
I2C 总线可挂多个设备,经 SDA(数据线)和 SCL(时钟线)通信。总线上设备分主机和从机,主机发起、停止数据传输并提供时钟信号,从机被主机寻址。多个主机想控制总线时,仲裁机制保证仅一个获控制权,数据不被破坏,多个设备还能借同步机制对齐时钟信号 。
三、软件层次剖析
要在 OLED 显示字符串,涉及多个软件层次,分工明确:
(一)应用程序层
应用程序决定 “在哪个位置、显示什么字符” 。像代码里 OLED_PrintString(0, 3, "www.100ask.net");
、OLED_PrintString(0, 5, "Hello, world!");
,就在指定 x、y 坐标位置显对应字符串,是显示流程的 “指挥者”,明确显示内容和位置需求。
(二)库函数层(字符 / 图片显示)
库函数收集字符点阵数据,清楚咋把字符点阵发出去实现显示。以 OLED_PrintString
函数为例,它遍历要显的字符串,逐个字符调 OLED_PutChar
函数。函数内借 while (str[i])
循环遍历,每次调 OLED_PutChar(x, y, str[i])
处理单个字符,还处理坐标更新(x++ ,x 超 15 时,x 重置为 0 ,y 加 2 ),是 “桥梁”,把应用程序显示需求转成字符点阵处理。
(三)OLED(SSD1306)驱动程序层
该层清楚发啥 I2C 数据(先设地址、再发数据 ),能把点阵数据写入显存。OLED_PutChar
函数里,先算页面(page = y )和列(col = x*8 ),检查坐标是否合法(y > 7 || x > 15 时返回 ),再借 OLED_SetPosition(page, col);
设显示位置,用 OLED_WriteNBytes((uint8_t*)&ascii_font[c][0], 8);
和 OLED_WriteNBytes((uint8_t*)&ascii_font[c][8], 8);
发字符点阵数据到 OLED 显存,实现字符显示控制。
(四)I2C 控制器驱动(HAL)层
最底层的 I2C 控制器驱动(HAL)负责实际 I2C 数据发送操作 。它为上层 OLED 驱动程序提供基础 I2C 通信功能,让上层能通过 I2C 总线和 OLED 设备交互,是软件层次的 “基石”,屏蔽硬件底层复杂通信细节。
四、完整流程梳理
当在应用程序调用 OLED_PrintString
显字符串时,流程如下:
- 应用程序层确定显示内容(字符串)和位置(x、y 坐标 ),调用
OLED_PrintString
函数。 - 库函数层的
OLED_PrintString
函数遍历字符串,对每个字符调OLED_PutChar
函数,处理字符显示坐标逻辑。 OLED_PutChar
函数进入 OLED 驱动程序层,计算显示位置、检查合法性,再调用底层函数设位置并发送字符点阵数据。- 最终由 I2C 控制器驱动(HAL)层实际发 I2C 数据到 OLED 设备,OLED 设备接收数据后,把字符点阵写入显存,在屏幕显示对应字符和字符串 。
五、总结
通过对 OLED 显示系统从硬件连接到软件层次的分析,我们清晰了解在 OLED 显字符串的完整流程。从应用程序简单调用,到层层向下的函数处理,再到底层硬件通信,各环节相互配合。掌握这些知识,助我们复习嵌入式开发中软硬件协同工作原理,也能在实际项目更好调试、优化 OLED 显示功能,为开发更复杂显示应用打基础 。
(2)I2C 协议保姆级解析
一、开篇:为啥要学 I2C 协议?
在嵌入式开发里,小到传感器读数,大到屏幕显示,很多设备都靠 I2C 协议 通信。简单说,它是设备之间 “对话” 的通用语言!掌握它,不管是调硬件、写驱动,还是排查通信问题,都能心里有数。这篇从硬件连线到数据传输,掰开揉碎了讲,新手也能轻松跟上~
二、I2C 硬件基础:两条线撑起的通信网
(一)硬件连接长这样
先看最基本的 I2C 硬件:主控芯片(比如 ARM)引出 SCL(时钟线) 和 SDA(数据线),外接两个上拉电阻(接 3.3V),然后挂一堆设备(像 AT24C02、加密 IC 、RTC MT411 等 )。
为啥要上拉电阻?
因为 I2C 设备的引脚一般用 开漏模式 。开漏模式自身没法稳定输出高电平,得靠上拉电阻把总线 “拉” 到高电平,空闲时保持稳定。
(二)开漏模式 + 上拉电阻,实现 “线与” 逻辑
每个 I2C 设备内部,输出端是个 NMOS 管(类似开关):
- 想输出低电平:NMOS 导通,把总线拉到地(0V);
- 想输出高电平:NMOS 断开,总线靠上拉电阻回到高电平(3.3V)。
这就实现了 “线与” 逻辑:只要有一个设备输出低电平,总线就是低电平;所有设备都 “断开”(高阻态),总线才是高电平。多设备共存时,靠这逻辑避免信号冲突!
我们结合下图看:当设备 A、B 输出不同电平,SDA 最终电平遵循 “有 0 则 0,全 1 才 1” ,这就是线与逻辑在 I2C 总线的体现。
三、I2C 核心时序:数据咋 “跑” 在总线上?
I2C 通信靠 SCL 时钟 同步,SDA 数据 跟着时钟节奏传输。关键时序有这些,一个一个拆:
(一)开始 / 停止信号:通信的 “开关”
- 开始信号(S):SCL 保持高电平时,SDA 从高电平跳变到低电平 → 告诉设备 “要通信啦!”
- 停止信号(P):SCL 保持高电平时,SDA 从低电平跳变到高电平 → 告诉设备 “通信结束!”
从图里能清晰看到,开始信号是 SCL 高电平期间,SDA 的下降沿;停止信号是 SCL 高电平期间,SDA 的上升沿,这两个信号是 I2C 通信的 “启停键” 。
(二)数据传输:8 位数据 + 1 位回应(ACK)
I2C 每次传 1 字节(8 位),但需要 9 个时钟周期(前 8 个传数据,第 9 个传 “回应”)。
1. 数据位咋读?(插入图片 6 :SDA、SCL 电平变化,SCL 高电平时读 SDA 数据位 )
SCL 为 高电平 时,SDA 必须保持稳定 → 此时从设备读取 SDA 电平,得到 1 位数据(0 或 1);
SCL 为 低电平 时,SDA 可以改变电平 → 准备下一位数据。
简单说:SCL 高电平 “锁存” 数据,低电平 “更新” 数据。
比如在传输过程中,SCL 每个高电平阶段,SDA 的电平就代表一个数据位,像图片 6 里展示的,SCL 高电平对应 SDA 稳定电平,设备就是在这个时候读取数据位的。
2. 回应信号(ACK)是啥?
传完 8 位数据后,第 9 个 SCL 时钟期间,接收方要拉低 SDA(发 ACK),告诉发送方 “我收到啦!” 。如果没收到 ACK,发送方就知道通信可能出错了。
看图片里的时序,第 9 个 clk 对应 ACK 部分,SDA 被拉低,这就是接收方给出的回应,确保数据传输的可靠性 。
四、I2C 读写操作:完整通信流程
(一)写操作:主设备→从设备发数据
- 发开始信号:主设备发 S 信号,启动通信。
- 发设备地址 + 方向:7 位设备地址 + 1 位 “写”(0),告诉从设备 “我要给你发数据”。
- 等从设备 ACK:从设备收到地址,拉低 SDA 回应。
- 发数据字节:主设备逐个发数据,每次发 1 字节,等从设备 ACK。
- 发停止信号:数据发完,主设备发 P 信号,结束通信。
就像图片展示的流程,白色背景是主→从的信号,灰色是从→主的回应,一步步把数据传过去 。
(二)读操作:从设备→主设备发数据
- 发开始信号:主设备发 S 信号。
- 发设备地址 + 方向:7 位设备地址 + 1 位 “读”(1),告诉从设备 “把数据给我”。
- 等从设备 ACK:从设备回应 ACK。
- 收数据字节:从设备发数据,主设备收 1 字节后,发 ACK 告诉从设备 “收到了,继续发”(最后 1 字节可发 NACK 表示 “别发了” )。
- 发停止信号:数据收完,主设备发 P 信号结束。
五、形象类比:把 I2C 通信变 “生活化”
把主设备当老师,从设备当学生:
- 写操作(发球):老师喊 “开始(S)”→ “给 A 同学发球(地址 + 写)”→ A 喊 “到(ACK)”→ 老师发球(传数据)→ A 喊 “收到(ACK)”→ 老师喊 “结束(P)”。
- 读操作(接球):老师喊 “开始(S)”→ “B 同学把球传过来(地址 + 读)”→ B 喊 “到(ACK)”→ B 传球(传数据)→ 老师喊 “收到(ACK)”→ 老师喊 “结束(P)”。
用生活场景类比,是不是瞬间好懂了?
六、总结:I2C 协议核心知识点
- 硬件:两条线(SCL、SDA)+ 上拉电阻,开漏模式实现线与逻辑。
- 时序:开始 / 停止信号定边界,8 位数据 + 1 位 ACK 保证传输,SCL 高电平锁存数据。
- 流程:读写操作分阶段,地址 + 方向选设备,ACK 确保数据可靠。
(3)stm32——i2c硬件结构
一、I2C 核心硬件组成
先看这张 I2C 整体架构图:
- SDA、SCL 引脚:I2C 通信的 “物理通道”,SDA 传数据,SCL 传时钟,外部信号先过噪声滤波器,滤掉干扰。
- 数据控制模块:像 “交通指挥官”,协调数据寄存器、移位寄存器的数据流转。数据要发送时,从数据寄存器搬到移位寄存器,再通过 SDA 发出去;接收则相反,SDA 收的信号经移位寄存器存到数据寄存器 。
- 比较器 + 地址寄存器:从机模式关键!比较器会把总线上收到的地址,和自身 “Own address register(自身地址寄存器)”“Dual address register(双地址寄存器)” 里存的地址对比。匹配上了,才会响应主机,实现 “主机找从机” 的寻址功能 。
- 控制逻辑 + 状态 / 控制寄存器:CR1、CR2 是控制寄存器,你写代码配置 I2C 模式、使能、中断等,就是操作它们;SR1、SR2 是状态寄存器,通信中各种状态(比如地址匹配、发送完成等)会 “反映” 在这些寄存器的对应位上,程序通过读这些位,判断 I2C 工作到哪一步了。
简单说,这堆硬件模块协同工作,让 STM32 能发 / 收 I2C 信号、处理寻址、响应状态,完成通信。
二、I2C 关键寄存器与位功能
(一)I2C_CR1 控制寄存器
重点看几个关键位:
- START 位:软件置 1 后,I2C 会发 “起始条件(S)”,如果当前 BUSY 位是 0(总线空闲),直接进入主机模式;要是已经在主机模式,发的是 “重复起始(Sr)”,用于不释放总线、连续通信的场景。
- ACK 位:控制是否发送应答。主机 / 从机接收数据后,发个 ACK 告诉对方 “我收到了”,NACK 则说 “没收到 / 不想收了” 。
- PE 位:I2C 外设使能位,置 1 才让 I2C 工作起来,相当于 “打开 I2C 开关”。
(二)状态寄存器 SR1 里的关键位
- ADDR 位:
- 从机模式:收到的地址和自身地址匹配,硬件自动置 1;软件得先读 SR1,再读 SR2 才能清掉它,这是 “标准操作流程”,不按流程清,可能影响后续通信。
- 主机模式:地址发送完成(7 位地址发完等从机 ACK、10 位地址发完第二字节 ACK 后),硬件置 1,同样要读 SR1 + SR2 清位。
- AF 位(ACKNOWLEDGE FAILURE):当没收到应答时,硬件置 1,说明通信可能出问题(比如从机没响应、总线干扰),软件写 0 或者关闭 I2C(PE=0)能清掉。
这些位就像 “信号灯”,程序通过读它们,知道 I2C 现在是 “地址匹配了”“没人应答出错了” 等状态,从而决定下一步咋处理。
三、I2C 通信流程(结合 EV5 等事件,重点讲传输时序图)
(一)先理解 “事件(EVx)” 概念
I2C 通信里,“事件” 是硬件状态变化的 “标志”,比如发完起始信号、地址匹配成功、数据发送完成等,都会对应一个 “EVx”。程序里,常通过 “等待这些事件发生”,来判断通信到哪一步,再执行下一步操作。
(二)以 “主机发送(Master Transmitter)” 为例,结合时序图
时序图分 7 位地址、10 位地址两种主机发送流程,核心逻辑类似,咱们拆成步骤看:
1. 起始条件(对应 EV5)
- 操作:软件置 I2C_CR1 的 START 位,I2C 外设会在总线发 “起始信号(S)”,此时硬件会置 SR1(Status Register 1,状态寄存器 1) 的 SB 位(SB=1),如果开了事件中断(ITEVFEN=1),还会触发中断。
- 清位:程序得先读 SR1(读 SB 状态),然后往数据寄存器(DR)写 “从机地址”,完成这两步,SB 位自动清 0,进入下一步。
- 作用:这是通信 “开场”,告诉总线 “我要开始传数据啦,从机准备好” 。
2. 地址发送完成(对应 EV6)
- 操作:写完从机地址后,I2C 会等从机应答(ACK),从机应答后,硬件置 SR1 的 ADDR 位(ADDR=1)。
- 清位:程序得先读 SR1,再读 SR2,ADDR 位才会清 0。这一步是 “确认从机收到地址、准备好收发数据” 。
3. 数据发送阶段(对应 EV8_1、EV8 等)
- EV8_1:当 “发送数据寄存器空(TxE=1)、移位寄存器空” 时触发,说明可以往 DR 写 “要发的数据(比如 Data1)” 了。
- EV8:数据从 DR 移到移位寄存器后,TxE 会再置 1(因为 DR 又空了),但移位寄存器非空(数据正在往总线发),这时候可以继续写 DR 存下一个要发的数据(Data2、DataN… ),保证数据 “无缝衔接” 发出去。
- EV8_2:当 “发送完成(BTF=1)”,且程序发了 “停止请求”,硬件会处理停止信号(P),同时清 TxE、BTF 位。这一步是 “收尾”,告诉总线 “数据发完啦,通信结束” 。
4. 10 位地址额外步骤(EV9)
如果是 10 位地址主机发送,发完 “Header” 后,会触发 EV9(ADDR10=1),处理方式类似:读 SR1,然后写 DR 发剩下的地址字节,完成 10 位地址的完整发送。
时序图里的这些 “EVx” 事件,就是把复杂的 I2C 通信,拆成一个一个 “节点” 。程序里,你可以用 “轮询” 或者 “中断” 的方式,等某个 EVx 发生了,就执行对应的操作(比如写地址、写数据、清位),让通信一步步走完。
四、I2C 基础信号波形
最后看最基础的 “开始、结束、应答波形”:
- 起始信号(S):SCL 高电平期间,SDA 从高变低,告诉总线 “通信要开始啦” 。
- 结束信号(P):SCL 高电平期间,SDA 从低变高,告诉总线 “通信结束啦” 。
- 应答信号(ACK):主机发完 8 位数据后,会释放 SDA,从机拉低 SDA 表示 “收到了”(ACK);要是从机没拉低(SDA 保持高),就是 “没收到”(NACK),主机可能会重发或者终止通信。
这些波形是 I2C 通信的 “语言基础”,硬件就是靠产生 / 识别这些波形,完成数据收发的。
(4)i2c_HAL_c查询方式
一、代码功能全景概览
老师提供的代码,核心围绕 STM32 借助 I2C 外设与 MPU6050 传感器交互展开,具体包含:
- 完成 STM32 硬件初始化,涵盖 HAL 库、系统时钟、各类外设 。
- 操控 OLED 屏幕,实现基础信息显示 。
- 利用 UART 串口发送调试数据 。
- 重点:通过 I2C 总线读写 MPU6050 寄存器,验证通信链路是否畅通 。
二、核心代码逐段剖析(关联硬件原理与手册)
(一)前期准备:变量定义与外设初始化
// 用于存储字符串长度,后续 OLED 显示功能会用到
int len;
// 定义串口发送字符串,\r\n 是串口通信里的换行符,让打印内容排版规整
char *str = "Please enter a char: \r\n";
char *str2 = "www.100ask.net\r\n";
// 存储串口接收的字符,当前代码未完整使用该功能
char c; // 初始化 HAL 库,完成外设重置、Flash 访问初始化、系统滴答定时器启动(为延时功能提供支持 )
HAL_Init();
// 配置系统时钟,设定 CPU 及外设(如 I2C、UART )的工作频率
SystemClock_Config(); // 初始化环形缓冲区(代码中函数名拼写有误,正确应为 circle_buf_init ),用于缓存串口接收数据(本节课重点是 I2C,简单了解即可 )
circld_buf_init(&g_key_bufs, 100, g_data_buf); // 初始化各类外设,包括 GPIO(控制引脚电平 )、DMA(可选,用于加速数据传输 )、I2C1(负责与 MPU6050 通信 )、UART1(用于串口调试 )
MX_GPIO_Init();
MX_DMA_Init();
MX_I2C1_Init();
MX_USART1_UART_Init(); // 初始化 OLED 屏幕并清空屏幕显示内容
OLED_Init();
OLED_Clear();
硬件知识关联:STM32 运行前,需完成基础初始化。HAL 库为外设操作提供统一接口;系统时钟是设备 “动力源”,决定 CPU 和外设的运行速率,保障外设稳定工作 。I2C、UART 等外设,如同电脑的 USB 口、网口,需配置引脚、通信速率等参数后才可使用 。
(二)OLED 基础显示配置
// 在 OLED 屏幕第 0 行(纵坐标 0 )、第 0 列(横坐标 0 )显示字符串 "cnt : "
OLED_PrintString(0, 0, "cnt : ");
// 在 OLED 屏幕第 2 行显示 "key val : ",并将字符串长度存入 len (虽未后续使用,是 OLED 函数的正常流程 )
len = OLED_PrintString(0, 2, "key val : ");
作用:提前在 OLED 屏幕固定位置显示文本,后续更新传感器数据时,直接替换对应位置内容,使显示界面清晰有序 。
(三)串口数据发送(辅助调试)
// 通过 UART1 发送字符串 str2(内容为 "www.100ask.net\r\n" )
// 参数依次为:UART 句柄(&huart1 )、数据起始地址、数据长度(strlen 函数计算字符串长度 )、超时时间(1000ms 内未发送完成则报错 )
HAL_UART_Transmit(&huart1, str2, strlen(str2), 1000); // 启动 UART1 接收中断,使 UART1 处于接收待命状态,收到数据自动触发中断处理(本节课重点是 I2C,暂作简单了解 )
startuart1recv();
调试价值:串口发送功能可在电脑端直观呈现程序运行状态,确认 UART 外设工作正常,也能在 I2C 通信异常时,辅助打印报错信息,便于问题排查 。
(四)关键环节:I2C 读写 MPU6050 寄存器
此部分是核心内容,需结合 MPU6050 手册 与 I2C 通信时序 深入理解 !
1. 读取 MPU6050 的 WHO_AM_I 寄存器(验证通信链路)
MPU6050 手册中,WHO_AM_I
寄存器(地址 0x75
)存储设备 ID(默认值为 0x68
),读取该寄存器可验证 STM32 与 MPU6050 能否正常通信 。
分步读写代码(便于初学者理解流程):
uint8_t reg = 0x75; // 待读取的寄存器地址,即 WHO_AM_I 寄存器
uint8_t val; // 用于存储读取到的寄存器值 #if 0 // 该段代码被注释,改用更简洁的 HAL_I2C_Mem_Read 函数
// 第一步:向 MPU6050 发送待读取的寄存器地址,告知传感器:即将读取 0x75 寄存器内容
HAL_I2C_Master_Transmit(&hi2c1, (0x68<<1), ®, 1, 10000);
// 第二步:从 MPU6050 读取 1 个字节数据,即 0x75 寄存器的值
HAL_I2C_Master_Receive(&hi2c1, (0x68<<1), &val, 1, 10000);
#else
// 简洁写法:利用 HAL_I2C_Mem_Read 函数,一步完成 “写入寄存器地址 + 读取数据” 操作
// 参数含义:I2C 句柄、设备地址(0x68<<1 含义后续讲解 )、寄存器地址、地址长度(8 位 )、数据缓存区、读取字节数(1 个 )、超时时间
HAL_I2C_Mem_Read(&hi2c1, (0x68<<1), reg, I2C_MEMADD_SIZE_8BIT, &val, 1, 1000);
#endif// 在 OLED 屏幕第 4 行显示提示文本,表明即将显示 MPU6050 0x75 寄存器的值
OLED_PrintString(0, 4, "mpu6050 reg 0x75 val");
// 将读取到的 val 值(十六进制形式 )显示在 OLED 屏幕第 6 行
OLED_PrintHex(0, 6, val, 1);
结合手册与 I2C 时序解读:
设备地址
0x68<<1
:
MPU6050 的 I2C 从机地址为b110100X
(7 位地址 ),其中X
由AD0
引脚电平决定(默认X = 0
,故地址为0x68
)。
在 STM32 的 I2C 函数中,7 位地址需左移 1 位(为R/W
读写位预留空间 ),左移后变为0x68<<1 = 0xD0
(二进制11010000
) 。HAL_I2C_Master_Transmit
+HAL_I2C_Master_Receive
:
这是 “分步读写” 流程,具体为:- 主机(STM32 )发送 “从机地址 + 写标志”(
0x68<<1
,最后一位0
表示写操作 ),告知 MPU6050:“准备接收数据” 。 - 发送待读取的寄存器地址(
0x75
),明确告知 MPU6050:“要读取该寄存器内容” 。 - 再次发送 “从机地址 + 读标志”(
0x68<<1 | 1
,最后一位1
表示读操作 ),告知 MPU6050:“现在开始读取数据” 。 - 接收 MPU6050 返回的数据(存入
val
) 。
- 主机(STM32 )发送 “从机地址 + 写标志”(
HAL_I2C_Mem_Read
:
这是 STM32 HAL 库提供的 “快捷函数”,将 “写入寄存器地址 + 读取数据” 操作合并,底层仍遵循标准 I2C 时序。熟练掌握后,使用该函数可简化代码编写 。
2. 写入 MPU6050 寄存器(配置传感器参数)
代码中还演示了 “写寄存器” 操作,以操作 0x31
寄存器为例(具体功能可查阅 MPU6050 手册,此处重点关注通信流程 ):
uint8_t buf[2]= {107,0}; // 数组存储 “寄存器地址(107 )、待写入值(0 )”(107 寄存器具体功能需查手册,此处为示例 )
// 发送 2 个字节数据,先写寄存器地址,再写对应值(对应 “单字节写入时序” )
HAL_I2C_Master_Transmit(&hi2c1, (0x68<<1), buf, 2, 10000); uint8_t val = 0x55;
// 利用 HAL_I2C_Mem_Write 函数写寄存器,地址为 0x31,写入值为 0x55
HAL_I2C_Mem_Write(&hi2c1, (0x68<<1), 0x31, I2C_MEMADD_SIZE_8BIT, &val, 1, 1000);
// 写入后再次读取,验证写入是否成功
val=0;
HAL_I2C_Mem_Read(&hi2c1, (0x68<<1), 0x31, I2C_MEMADD_SIZE_8BIT, &val, 1, 1000);
// 在 OLED 屏幕显示读取回来的值,查看是否为 0x55
OLED_PrintHex(8, 6, val, 1);
结合时序理解:
写寄存器操作对应 MPU6050 手册里的 “单字节写入时序”,流程为:
- 主机发送 “从机地址 + 写标志”(
0x68<<1
) 。 - 发送待写入的寄存器地址(如
0x31
) 。 - 发送待写入的数据(如
0x55
) 。 - 从机(MPU6050 )每接收一步,都会返回 ACK 信号,表示 “数据已收到” 。可对应 MPU6050 手册中的 “单字节写入时序” 图,标注发送地址、写数据、ACK 回应等流程 。
三、常见问题与调试技巧(初学者必备)
1. 读取 WHO_AM_I
寄存器值不是 0x68
- 硬件接线检查:确认 SDA、SCL 引脚是否接反,上拉电阻(建议 4.7K 左右 )是否正确连接 。
- 设备地址检查:查看
AD0
引脚电平,若接 VCC,设备地址应为0x69<<1
,需同步修改代码中的地址 。 - I2C 初始化检查:确认通信速率(如 400KHz )、引脚配置是否正确 。
2. OLED 显示乱码
- OLED 初始化检查:确认引脚连接是否正确(如 SCL、SDA 是否对应 I2C1 引脚 ) 。
- 显示函数检查:确认
OLED_PrintHex
等函数与当前 OLED 驱动芯片兼容,不同驱动芯片指令存在差异 。
四、知识总结与串联
- I2C 通信核心逻辑:主机(STM32 )通过 SDA、SCL 引脚,向从机(MPU6050 )发送 “地址 + 读写命令 + 数据”,从机返回 ACK 信号或数据,完成交互 。
- MPU6050 地址规则:7 位地址为
b110100X
,X
由AD0
引脚电平决定,在 STM32 代码中需左移 1 位使用 。 - HAL 库函数运用:
HAL_I2C_Master_Transmit/Receive
实现分步读写,HAL_I2C_Mem_Read/Write
简化操作流程,底层均基于标准 I2C 时序 。 - 调试思路指引:先通过读取
WHO_AM_I
寄存器验证通信,再尝试写寄存器并回读验证,结合串口打印、OLED 显示功能辅助排查问题 。
(5)I2C —— HAL 库中断模式
一、核心概念理解
中断模式
在中断模式下,当 I2C 数据传输完成(发送完成或接收完成 )或者出现错误时,会触发相应的中断,CPU 会暂停当前正在执行的任务,转而去执行中断服务函数(回调函数 )。这样一来,CPU 不需要一直等待 I2C 传输完成,在传输过程中可以去处理其他事务,提升了系统的实时性和整体效率 。
三、代码模块解析
(一)全局变量定义
static volatile int g_i2c1_tx_complete = 0;//全局变量,用于标记I2C1发送是否完成
static volatile int g_i2c1_rx_complete = 0;//全局变量,用于标记I2C1接收是否完成
这里定义了两个全局变量,g_i2c1_tx_complete
用来标记 I2C1 总线的发送操作是否完成,g_i2c1_rx_complete
用来标记 I2C1 总线的接收操作是否完成。volatile
关键字的作用是告诉编译器,这些变量可能会被意外地修改(比如在中断服务函数中被修改 ),防止编译器对这些变量进行过度优化,保证程序的正确性 。
(二)回调函数实现
1. 主设备发送完成回调函数
/* USER CODE BEGIN 1 */
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c)//主设备发送完成回调函数
{if(hi2c == &hi2c1)//判断是否是I2C1的中断{g_i2c1_tx_complete=1; //标记发送完成}
}
/* USER CODE END 1 */
当 I2C1 作为主设备完成数据发送操作时,这个回调函数会被调用。函数内部通过判断传入的 hi2c
句柄是否是 &hi2c1
(假设 hi2c1
是我们配置的 I2C1 外设的句柄 ),来确定是不是我们关注的 I2C1 总线的发送完成事件,如果是的话,就把 g_i2c1_tx_complete
标记为 1,表示发送完成 。
2. 等待发送完成函数
void Wait_i2c1Tx_Complete(void)
{while(g_i2c1_tx_complete==0); //等待发送完成标记置1g_i2c1_tx_complete=0; //完成后重置标记,为下一次传输做准备
}
这个函数的作用是等待 I2C1 的发送操作完成。它会不断检查 g_i2c1_tx_complete
的值,直到其变为 1(表示发送完成 ),然后再把这个标记重置为 0,以便下次传输时能正确判断状态 。
3. 主设备接收完成回调函数
void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c)//主设备接收完成回调函数
{if(hi2c == &hi2c1)//判断是否是I2C1的中断{g_i2c1_rx_complete=1; //标记接收完成}
}
和发送完成回调函数类似,当 I2C1 作为主设备完成数据接收操作时,该函数被调用,通过判断句柄确定是 I2C1 后,将 g_i2c1_rx_complete
标记为 1,标识接收完成 。
4. 等待接收完成函数
void Wait_i2c1Rx_Complete(void)
{while(g_i2c1_rx_complete==0); //等待接收完成标记置1g_i2c1_rx_complete=0; //完成后重置标记,为下一次传输做准备
}
此函数用于等待 I2C1 的接收操作完成,不断检查 g_i2c1_rx_complete
,直到变为 1,然后重置标记 。
5. MEM 模式发送完成回调函数
void HAL_I2C_MemTxCpltCallback(I2C_HandleTypeDef *hi2c)//主设备发送完成回调函数(MEM模式)
{if(hi2c == &hi2c1)//判断是否是I2C1的中断{g_i2c1_tx_complete=1; //标记发送完成}
}
MEM 模式是 I2C 操作的一种软件概念,本质还是主设备读写数据。当在 MEM 模式下 I2C1 发送完成时,这个回调函数会被调用,同样通过判断句柄,标记发送完成状态 。 。
6. MEM 模式接收完成回调函数
void HAL_I2C_MemRxCpltCallback(I2C_HandleTypeDef *hi2c)//主设备发送完成回调函数(MEM模式)
{if(hi2c == &hi2c1)//判断是否是I2C1的中断{g_i2c1_rx_complete=1; //标记接收完成}
}
当 I2C1 在 MEM 模式下完成接收操作时,该函数被调用,判断是 I2C1 后标记接收完成 。
(三)整体流程梳理
- 数据传输触发:在实际的 I2C 通信中,比如使用
HAL_I2C_Master_Transmit_IT
(主设备中断模式发送 )、HAL_I2C_Master_Receive_IT
(主设备中断模式接收 )、HAL_I2C_Mem_Read_IT
(MEM 模式中断读 )、HAL_I2C_Mem_Write_IT
(MEM 模式中断写 )等函数触发数据传输后,当传输完成(发送或接收 ),对应的回调函数就会被调用 。 - 状态标记与等待:回调函数会修改对应的全局变量(
g_i2c1_tx_complete
或g_i2c1_rx_complete
)来标记传输完成状态。而Wait_i2c1Tx_Complete
和Wait_i2c1Rx_Complete
函数则是用来在需要的地方等待传输完成,确保后续操作在数据传输完毕后再执行,保证程序逻辑的正确性 。
四、实际应用场景举例(结合代码片段)
以下是一段实际的代码片段,展示了 I2C 中断模式在实际读取传感器(比如 MPU6050 )数据等场景中的应用:
uint8_t reg = 0x75;
uint8_t val;
#if 0
HAL_I2C_Master_Transmit_IT(&hi2c1,(0x68<<1),®,1);
Wait_i2c1Tx_Complete();HAL_I2C_Master_Receive_IT(&hi2c1, (0x68<<1), &val,1);
Wait_i2c1Rx_Complete();
#else
HAL_I2C_Mem_Read_IT(&hi2c1,(0x68<<1),reg,I2C_MEMADD_SIZE_8BIT,&val,1);
Wait_i2c1Rx_Complete();
#endif
OLED_PrintString(0,4,"mpu6050 reg 0x75 val");
OLED_PrintHex(0,6,val,1);// 无限循环:程序的主要逻辑在这里执行
//外设使能控制
uint8_t buf[2]= {107,0};//数组的变量名直接是数组首地址 107寄存器地址;0是寄存器的值
HAL_I2C_Master_Transmit_IT(&hi2c1,(0x68<<1),buf,2);
Wait_i2c1Tx_Complete();
val = 0x55;
HAL_I2C_Mem_Write_IT(&hi2c1,(0x68<<1), 0x31,I2C_MEMADD_SIZE_8BIT,&val,1 );
Wait_i2c1Tx_Complete();
val=0;
HAL_I2C_Mem_Read_IT(&hi2c1,(0x68<<1),0x31,I2C_MEMADD_SIZE_8BIT,&val,1);
Wait_i2c1Rx_Complete();
OLED_PrintHex(8,6,val,1);
- 读取 MPU6050 寄存器示例:
- 首先定义了要读取的寄存器地址
reg = 0x75
和用来存储读取值的变量val
。 - 然后有两种方式读取数据,一种是分别使用
HAL_I2C_Master_Transmit_IT
发送寄存器地址,再用HAL_I2C_Master_Receive_IT
接收数据(被#if 0
注释掉 );另一种是使用HAL_I2C_Mem_Read_IT
(MEM 模式中断读 ),之后调用Wait_i2c1Rx_Complete
等待接收完成,最后通过 OLED 显示读取的值 。 - 接着在无限循环部分,展示了向 MPU6050 的 107 寄存器写值(使用
HAL_I2C_Master_Transmit_IT
)、向 0x31 寄存器写值(HAL_I2C_Mem_Write_IT
)和读值(HAL_I2C_Mem_Read_IT
)的操作,每次操作后都调用对应的等待函数,确保操作完成后再进行下一步,最后同样通过 OLED 显示读取的结果 。
- 首先定义了要读取的寄存器地址
五、总结
通过本次课程学习,我们深入了解了 I2C HAL 库中断模式的核心内容:
- 中断模式优势:利用中断,让 CPU 摆脱轮询等待 I2C 传输的方式,在传输过程中可以处理其他任务,提升系统效率和实时性 。
- 回调函数与标记变量:通过各种回调函数(主设备发送、接收完成回调,MEM 模式发送、接收完成回调 )来标记传输状态,配合等待函数,保障数据传输的有序性和程序逻辑的正确性 。
- 实际应用:在与传感器(如 MPU6050 )等从设备通信的实际场景中,灵活运用这些函数和模式,实现数据的读写操作,并且能清晰看到代码是如何协同工作来完成整个通信流程的