当前位置: 首页 > news >正文

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 显字符串时,流程如下:

  1. 应用程序层确定显示内容(字符串)和位置(x、y 坐标 ),调用 OLED_PrintString 函数。
  2. 库函数层的 OLED_PrintString 函数遍历字符串,对每个字符调 OLED_PutChar 函数,处理字符显示坐标逻辑。
  3. OLED_PutChar 函数进入 OLED 驱动程序层,计算显示位置、检查合法性,再调用底层函数设位置并发送字符点阵数据。
  4. 最终由 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 读写操作:完整通信流程

(一)写操作:主设备→从设备发数据

  1. 发开始信号:主设备发 S 信号,启动通信。
  2. 发设备地址 + 方向:7 位设备地址 + 1 位 “写”(0),告诉从设备 “我要给你发数据”。
  3. 等从设备 ACK:从设备收到地址,拉低 SDA 回应。
  4. 发数据字节:主设备逐个发数据,每次发 1 字节,等从设备 ACK。
  5. 发停止信号:数据发完,主设备发 P 信号,结束通信。

就像图片展示的流程,白色背景是主→从的信号,灰色是从→主的回应,一步步把数据传过去 。

(二)读操作:从设备→主设备发数据

  1. 发开始信号:主设备发 S 信号。
  2. 发设备地址 + 方向:7 位设备地址 + 1 位 “读”(1),告诉从设备 “把数据给我”。
  3. 等从设备 ACK:从设备回应 ACK。
  4. 收数据字节:从设备发数据,主设备收 1 字节后,发 ACK 告诉从设备 “收到了,继续发”(最后 1 字节可发 NACK 表示 “别发了” )。
  5. 发停止信号:数据收完,主设备发 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 传感器交互展开,具体包含:

  1. 完成 STM32 硬件初始化,涵盖 HAL 库、系统时钟、各类外设 。
  2. 操控 OLED 屏幕,实现基础信息显示 。
  3. 利用 UART 串口发送调试数据 。
  4. 重点:通过 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), &reg, 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
    这是 “分步读写” 流程,具体为:

    1. 主机(STM32 )发送 “从机地址 + 写标志”(0x68<<1,最后一位 0 表示写操作 ),告知 MPU6050:“准备接收数据” 。
    2. 发送待读取的寄存器地址(0x75 ),明确告知 MPU6050:“要读取该寄存器内容” 。
    3. 再次发送 “从机地址 + 读标志”(0x68<<1 | 1,最后一位 1 表示读操作 ),告知 MPU6050:“现在开始读取数据” 。
    4. 接收 MPU6050 返回的数据(存入 val ) 。
  • 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 手册里的 “单字节写入时序”,流程为:

  1. 主机发送 “从机地址 + 写标志”(0x68<<1 ) 。
  2. 发送待写入的寄存器地址(如 0x31 ) 。
  3. 发送待写入的数据(如 0x55 ) 。
  4. 从机(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 驱动芯片兼容,不同驱动芯片指令存在差异 。

四、知识总结与串联

  1. I2C 通信核心逻辑:主机(STM32 )通过 SDA、SCL 引脚,向从机(MPU6050 )发送 “地址 + 读写命令 + 数据”,从机返回 ACK 信号或数据,完成交互 。
  2. MPU6050 地址规则:7 位地址为 b110100XX 由 AD0 引脚电平决定,在 STM32 代码中需左移 1 位使用 。
  3. HAL 库函数运用HAL_I2C_Master_Transmit/Receive 实现分步读写,HAL_I2C_Mem_Read/Write 简化操作流程,底层均基于标准 I2C 时序 。
  4. 调试思路指引:先通过读取 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 后标记接收完成 。

(三)整体流程梳理

  1. 数据传输触发:在实际的 I2C 通信中,比如使用 HAL_I2C_Master_Transmit_IT(主设备中断模式发送 )、HAL_I2C_Master_Receive_IT(主设备中断模式接收 )、HAL_I2C_Mem_Read_IT(MEM 模式中断读 )、HAL_I2C_Mem_Write_IT(MEM 模式中断写 )等函数触发数据传输后,当传输完成(发送或接收 ),对应的回调函数就会被调用 。
  2. 状态标记与等待:回调函数会修改对应的全局变量(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),&reg,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);
  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 库中断模式的核心内容:

  1. 中断模式优势:利用中断,让 CPU 摆脱轮询等待 I2C 传输的方式,在传输过程中可以处理其他任务,提升系统效率和实时性 。
  2. 回调函数与标记变量:通过各种回调函数(主设备发送、接收完成回调,MEM 模式发送、接收完成回调 )来标记传输状态,配合等待函数,保障数据传输的有序性和程序逻辑的正确性 。
  3. 实际应用:在与传感器(如 MPU6050 )等从设备通信的实际场景中,灵活运用这些函数和模式,实现数据的读写操作,并且能清晰看到代码是如何协同工作来完成整个通信流程的 
http://www.dtcms.com/a/311526.html

相关文章:

  • 2023年ASOC SCI2区TOP,可修灰狼优化算法RGWO+燃料电池参数辨识,深度解析+性能实测
  • 【无标题】根据11维拓扑量子色动力学模型(11D-TQCD)与当代宇宙学理论的融合分析,宇宙轮回的终结机制及其最终状态可系统论述如下:
  • 商品中台数据库设计
  • WPFC#超市管理系统(4)入库管理
  • 音视频学习(四十八):PCM和WAV
  • 基于深度学习的医学图像分析:使用GAN实现医学图像增强
  • 进阶向:Python生成艺术图案(分形、数学曲线)
  • MySQL索引解析
  • vue3pinia
  • Corrosion2靶机
  • Cyber Weekly #63
  • 搜索引擎评估革命:用户行为模型如何颠覆传统指标?
  • Sklearn 机器学习 数据聚类 用Numpy自己实现聚类
  • 【C++】类和对象(2)
  • 使用keil点亮stc8核心板的灯
  • 逻辑回归 银行贷款资格判断案列优化 交叉验证,调整阈值,下采样与过采样方法
  • MQTT 入门教程:MQTT工具调式
  • 堆----2.前 K 个高频元素
  • VirtualBox 的 HOST 键(主机键)是 右Ctrl 键(即键盘右侧的 Ctrl 键)笔记250802
  • 学习笔记:无锁队列的原理以及c++实现
  • Linux 高级 I/O 系统调用详解
  • Vue 响应式基础全解析2
  • Node.js中path模块的使用指南
  • InfluxDB 与 Node.js 框架:Express 集成方案(二)
  • 如何在`<link type=“icon“ href=`的`href`中写SVG并使用path标签? 笔记250802
  • 嵌入式 C 语言入门:递归与变量作用域学习笔记 —— 从概念到内存特性
  • 深入 Go 底层原理(十三):interface 的内部表示与动态派发
  • Javaweb————Apache Tomcat服务器介绍及Windows,Linux,MAC三种系统搭建Apache Tomcat
  • 技术文章:覆铜板的阻燃性
  • UniappDay07