前言
上一章我们通过硬件I2C完成了OLED屏的驱动开发,实现了字符与图像的显示,但未深入拆解I2C协议的底层逻辑。I2C是工业嵌入式开发中最常用的低速串行总线,是传感器、存储芯片、显示外设的核心通信方式。对应51单片机开发,我们通常通过软件翻转IO口模拟I2C时序,存在时序精度差、CPU占用率100%、多从机兼容性差、无硬件错误处理的痛点;而STM32内置硬件I2C控制器,可自动生成标准时序、处理应答机制、支持DMA高速传输,完美适配工业场景多设备、高稳定性的通信需求。新手入门I2C普遍面临三大痛点:总线卡死无响应、从机无ACK应答、读写数据错乱、多从机通信冲突。本章将严格遵循「先寄存器原理拆解,再HAL库封装逻辑」的顺序,联动51单片机对应知识点,从底层时序到工业级实战全面吃透I2C通信,完成AT24C02 EEPROM、MPU6050六轴传感器两大核心场景开发。
本章目录
- 一、本章学习目标
- 二、核心知识点
- 2.1 I2C协议基础与51单片机驱动方案核心对比
- 2.2 I2C协议底层时序与主从机通信完整流程
- 2.3 I2C从机地址规则与多设备总线架构
- 2.4 STM32硬件I2C核心寄存器与C语言位操作联动
- 2.5 HAL库I2C封装逻辑与核心API深度解析
- 三、STM32CubeMX+Keil5保姆式实操:I2C工业级外设驱动实战
- 3.1 工程创建与基础配置
- 3.2 I2C外设与NVIC图形化配置
- 3.3 工程代码生成与驱动文件移植
- 3.4 AT24C02 EEPROM驱动与掉电存储实战
- 3.5 MPU6050六轴传感器驱动与数据采集实战
- 3.6 编译、烧录与效果验证
- 四、保姆式排错指南
- 五、我的踩坑记录
- 六、课后小练习(附完整标准答案)
- 6.1 基础巩固练习
- 6.2 进阶实战练习
- 七、核心知识点速记
- 八、本章小结
一、本章学习目标
- 掌握I2C串行通信协议的底层时序、主从机通信机制与应答规则,对比51单片机软件模拟方案的核心差异,理解硬件I2C的工程价值
- 吃透STM32硬件I2C的内核架构、寄存器级工作原理,联动C语言位操作、指针知识点,能独立实现寄存器级的I2C初始化与读写操作
- 掌握HAL库I2C的封装逻辑与核心API使用,能区分阻塞/中断/DMA三种传输模式的选型逻辑,适配不同工业开发场景
- 熟练完成AT24C02 EEPROM的驱动开发,实现字节/页读写、连续读写功能,完成掉电不丢失数据的存储实战
- 熟练完成MPU6050六轴传感器的驱动开发,实现加速度、陀螺仪、内置温度传感器的数据采集,能独立排查I2C总线卡死、无ACK、数据错乱等高频问题
二、核心知识点
2.1 I2C协议基础与51单片机驱动方案核心对比
术语通俗解释:I2C全称集成电路总线,是飞利浦公司推出的两线式半双工串行通信总线,仅需SCL(串行时钟线)、SDA(串行数据线)两根线,即可实现单主机与多从机之间的稳定通信。类比一条共享的快递通道:SCL是统一的运输节拍,SDA是运输的货物,主机是快递总站,每个从机有唯一的门牌号(从机地址),总站通过地址匹配对应从机,实现一对一的数据收发。
I2C协议核心特性:
- 两线制:仅需SCL、SDA两根线,大幅节省硬件IO资源;
- 主从架构:通信由主机发起,从机响应,支持单主机多从机架构,一条总线最多挂载127个7位地址的从机;
- 半双工通信:同一时间总线仅支持单向数据传输,收发分时进行;
- 硬件应答机制:每传输1字节数据,接收方必须反馈应答信号,保证数据传输的可靠性;
- 速率分级:标准模式100Kbps、快速模式400Kbps、高速模式3.4Mbps,STM32F103硬件I2C最高支持400Kbps快速模式;
- 开漏输出:总线引脚采用开漏输出模式,必须外接上拉电阻,保证总线空闲电平与多设备总线的兼容性。
我们从51单片机的软件模拟方案出发,无缝衔接理解STM32硬件I2C的核心优势:
| 驱动方案 | 51单片机软件模拟I2C | STM32硬件I2C | 对开发的核心影响 |
|---|---|---|---|
| 时序实现 | 手动翻转IO口,通过延时函数控制时序,精度差,受主频、中断干扰极大 | 硬件控制器自动生成标准时序,精度高,不受软件、中断干扰 | 软件模拟时序极易出现通信异常,不同主频芯片需重新调整延时;硬件时序兼容性极强,代码可跨芯片移植 |
| CPU占用 | 通信全程CPU需循环翻转IO、检测应答,占用率100%,无法同步执行其他任务 | 仅需配置初始参数,数据收发全程硬件自动完成,配合DMA可实现零CPU占用 | 软件模拟方案通信期间CPU完全卡死,无法同步执行采集、控制逻辑;硬件方案通信期间CPU可正常执行核心业务,系统实时性大幅提升 |
| 多从机支持 | 无总线仲裁机制,多从机通信极易出现冲突,兼容性差 | 原生支持总线仲裁、多从机寻址,硬件处理地址匹配与冲突 | 软件模拟方案一条总线最多挂载2-3个从机;硬件I2C一条总线可挂载数十个从机,完美适配工业多传感器场景 |
| 错误处理 | 无硬件错误检测,需手动编写超时、应答失败处理逻辑,极易出现总线死锁 | 硬件自动检测应答失败、总线冲突、超时等错误,提供错误标志位,可快速定位问题 | 软件模拟方案出现总线死锁后只能复位重启;硬件I2C可通过错误标志位快速恢复总线,保证工业设备运行稳定性 |
| 开发难度 | 需开发者完全掌握协议底层时序,手动实现起始/停止/应答/数据收发,代码量大,逻辑复杂 | HAL库已封装标准通信API,仅需调用读写函数即可完成通信,代码极简,稳定性强 | 软件模拟方案开发周期长,易出bug;硬件方案可快速实现外设驱动,聚焦业务逻辑开发,效率提升数十倍 |
2.2 I2C协议底层时序与主从机通信完整流程
I2C协议的核心是严格的时序规则,所有通信必须遵循标准时序,这是新手入门的核心重点,也是通信异常的头号根源。
1. 总线基础状态
- 空闲状态:SCL、SDA两条线均为高电平,由上拉电阻拉高,此时总线处于空闲状态,可发起新的通信。
- 总线忙状态:SCL为高电平时,SDA出现电平跳变,总线被占用,其他设备不能发起通信。
2. 核心时序信号
(1)起始信号(START)
时序规则:SCL为高电平时,SDA从高电平跳变到低电平,标志着一次通信的开始。
联动C语言知识点:51单片机软件模拟需先拉高SCL、SDA,再拉低SDA,加入延时保证时序;STM32硬件I2C仅需置位CR1寄存器的START位,硬件自动生成起始信号。
(2)停止信号(STOP)
时序规则:SCL为高电平时,SDA从低电平跳变到高电平,标志着一次通信的结束,总线回到空闲状态。
核心规则:每次完整的通信必须以起始信号开头,停止信号结尾,形成完整的通信闭环。
(3)数据传输时序
时序规则:
- SCL为低电平时,SDA才能改变数据电平,准备下一位数据;
- SCL为高电平时,SDA电平必须保持稳定,接收方在SCL高电平时采样SDA电平,获取1位数据;
- 每字节数据为8位,高位在前(MSB),低位在后(LSB),每传输1字节后跟随1位应答位,共9个时钟周期完成1字节传输。
(4)应答/非应答信号(ACK/NACK)
时序规则:每传输完8位数据后,第9个时钟周期由接收方控制SDA线:
- 应答信号ACK:接收方拉低SDA电平,表示成功接收1字节数据;
- 非应答信号NACK:接收方保持SDA高电平,表示接收失败,主机需终止通信或重新发送。
核心规则:主机发送数据时,每发1字节需等待从机反馈ACK;主机接收数据时,收到最后1字节后需发送NACK,告知从机停止发送,随后发送停止信号结束通信。
3. 完整的主从机通信时序
工业开发中99%的场景为单主机读写从机寄存器,核心分为写操作、读操作两大流程,适配AT24C02、MPU6050等绝大多数I2C从机设备。
(1)主机写从机寄存器(字节写)
完整时序流程:
- 主机发送起始信号START;
- 主机发送从机7位地址+1位写标志位(0),共1字节;
- 从机反馈应答信号ACK;
- 主机发送要访问的从机寄存器地址;
- 从机反馈应答信号ACK;
- 主机发送要写入寄存器的1字节数据;
- 从机反馈应答信号ACK;
- 主机发送停止信号STOP,结束通信。
(2)主机读从机寄存器(随机读)
完整时序流程:
- 主机发送起始信号START;
- 主机发送从机7位地址+1位写标志位(0),执行写操作,用于指定要读取的寄存器地址;
- 从机反馈应答信号ACK;
- 主机发送要读取的从机寄存器地址;
- 从机反馈应答信号ACK;
- 主机重新发送起始信号START(重启通信);
- 主机发送从机7位地址+1位读标志位(1),切换为读操作;
- 从机反馈应答信号ACK;
- 从机发送寄存器中的1字节数据;
- 主机反馈非应答信号NACK,告知从机停止发送;
- 主机发送停止信号STOP,结束通信。
2.3 I2C从机地址规则与多设备总线架构
1. 从机地址格式
I2C从机地址分为7位和10位两种,工业开发中99%的外设使用7位地址,格式如下:
| 7位地址位 | 读写位 |
|---|---|
| bit7~bit1 | bit0 |
- 高7位:从机的唯一硬件地址,由芯片厂商固定,部分芯片可通过硬件引脚修改地址;
- 最低位bit0:读写标志位,0=主机向从机写数据,1=主机从从机读数据。
新手易错点:7位地址与8位地址的换算,例如AT24C02的7位地址是0x50,8位写地址是0xA0,8位读地址是0xA1,HAL库中仅需传入7位地址,读写位由API自动处理。
2. 常用外设地址规则
| 外设型号 | 默认7位地址 | 地址修改方式 |
|---|---|---|
| AT24C02 | 0x50 | 通过A0/A1/A2引脚电平修改,最多支持8个同型号芯片挂载同一条总线 |
| MPU6050 | 0x68 | AD0引脚接高电平,地址变为0x69 |
| SSD1306 OLED | 0x3C | 部分屏硬件地址为0x3D |
| AT24C04/08/16 | 0x50 | 高位地址由芯片页地址位决定,A0/A1引脚功能变化 |
3. 多从机总线架构
一条I2C总线可挂载多个从机设备,核心规则:
- 所有从机的SCL、SDA引脚分别并联到主机的SCL、SDA引脚;
- 总线必须在SCL、SDA线上外接4.7KΩ上拉电阻到3.3V,一条总线仅需一组上拉电阻,无需每个设备都加;
- 所有从机的7位地址必须唯一,不能出现地址冲突,否则会导致通信完全异常;
- 所有设备必须共地,保证电平基准一致,否则会出现采样错误、无ACK等问题。
2.4 STM32硬件I2C核心寄存器与C语言位操作联动
STM32F103的I2C外设通过7个核心寄存器实现完整的协议控制,我们以I2C1为例,拆解核心寄存器的功能与C语言位操作实现,联动51单片机软件模拟逻辑,实现无缝衔接。
| 寄存器名称 | 结构体成员 | 读写属性 | 核心功能与位操作详解 |
|---|---|---|---|
| 控制寄存器1 | I2C1->CR1 | 读写 | I2C核心控制寄存器,关键位:- 位0(PE):外设使能位,写1开启I2C外设- 位8(START):起始信号生成位,写1硬件自动生成起始信号- 位9(STOP):停止信号生成位,写1硬件自动生成停止信号- 位10(ACK):应答使能位,写1开启自动应答- 位15(SWRST):软件复位位,总线死锁时写1复位I2C外设 |
| 控制寄存器2 | I2C1->CR2 | 读写 | 时钟与中断控制寄存器,关键位:- 位5~0(FREQ):外设时钟频率配置,72MHz主频下写36(APB1时钟36MHz)- 位8(ITEVTEN):事件中断使能位- 位9(ITBUFEN):缓冲区中断使能位- 位10(ITERREN):错误中断使能位 |
| 时钟控制寄存器 | I2C1->CCR | 读写 | 波特率配置寄存器,关键位:- 位15(F/S):模式选择位,0=标准模式100K,1=快速模式400K- 位11~0(CCR):时钟分频系数,标准模式下CCR=36MHz/(2×100KHz)=180 |
| 上升时间寄存器 | I2C1->TRISE | 读写 | 配置SCL上升时间,标准模式下最大值为1000ns,写36+1=37即可 |
| 数据寄存器 | I2C1->DR | 读写 | 低8位有效,存储要发送/已接收的数据,写入数据自动启动发送,读取数据获取接收结果 |
| 状态寄存器1 | I2C1->SR1 | 只读 | 事件与错误状态标志位,关键位:- 位0(SB):起始信号生成完成标志位- 位1(ADDR):地址发送完成标志位- 位2(BTF):字节传输完成标志位- 位6(TXE):发送数据寄存器空标志位- 位6(RXNE):接收数据寄存器非空标志位- 位10(AF):应答失败标志位,从机无ACK时置1 |
| 状态寄存器2 | I2C1->SR2 | 只读 | 总线状态标志位,读取SR1后读取SR2可清除ADDR标志位 |
C语言寄存器操作完整示例:I2C1初始化与AT24C02字节写、字节读功能,完全对应51单片机软件模拟逻辑
1#include "stm32f1xx.h" 2 3// I2C1初始化,100Kbps标准模式 4void I2C1_Init(void) 5{ 6 // 1. 开启GPIOB与I2C1时钟 7 RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; 8 RCC->APB1ENR |= RCC_APB1ENR_I2C1EN; 9 10 // 2. 配置PB6(SCL)、PB7(SDA)为复用开漏输出,50MHz 11 GPIOB->CRL &= ~(GPIO_CRL_MODE6 | GPIO_CRL_CNF6 | GPIO_CRL_MODE7 | GPIO_CRL_CNF7); 12 GPIOB->CRL |= GPIO_CRL_MODE6_1 | GPIO_CRL_CNF6_1 | GPIO_CRL_CNF6_0; 13 GPIOB->CRL |= GPIO_CRL_MODE7_1 | GPIO_CRL_CNF7_1 | GPIO_CRL_CNF7_0; 14 15 // 3. 软件复位I2C外设,恢复默认状态 16 I2C1->CR1 |= I2C_CR1_SWRST; 17 for(volatile uint32_t i=0; i<100; i++); 18 I2C1->CR1 &= ~I2C_CR1_SWRST; 19 20 // 4. 配置外设时钟频率36MHz 21 I2C1->CR2 |= 36; 22 // 5. 配置时钟控制寄存器,100Kbps标准模式 23 I2C1->CCR = 180; 24 // 6. 配置上升时间 25 I2C1->TRISE = 37; 26 // 7. 开启I2C外设,使能自动应答 27 I2C1->CR1 |= I2C_CR1_PE | I2C_CR1_ACK; 28} 29 30// I2C起始信号生成 31void I2C_Start(void) 32{ 33 I2C1->CR1 |= I2C_CR1_START; 34 while(!(I2C1->SR1 & I2C_SR1_SB)); // 等待起始信号生成完成 35} 36 37// I2C停止信号生成 38void I2C_Stop(void) 39{ 40 I2C1->CR1 |= I2C_CR1_STOP; 41 while(I2C1->CR1 & I2C_CR1_STOP); // 等待停止信号生成完成 42} 43 44// I2C发送1字节数据,等待ACK 45uint8_t I2C_Send_Byte(uint8_t data) 46{ 47 I2C1->DR = data; 48 while(!(I2C1->SR1 & I2C_SR1_TXE)); // 等待发送完成 49 if(I2C1->SR1 & I2C_SR1_AF) // 检测应答失败 50 { 51 I2C1->SR1 &= ~I2C_SR1_AF; 52 return 1; // 无ACK,返回错误 53 } 54 return 0; // 发送成功 55} 56 57// I2C读取1字节数据,ack=1发送ACK,ack=0发送NACK 58uint8_t I2C_Read_Byte(uint8_t ack) 59{ 60 if(ack) I2C1->CR1 |= I2C_CR1_ACK; // 开启应答 61 else I2C1->CR1 &= ~I2C_CR1_ACK; // 关闭应答,发送NACK 62 while(!(I2C1->SR1 & I2C_SR1_RXNE)); // 等待接收完成 63 uint8_t data = I2C1->DR; 64 return data; 65} 66 67// AT24C02字节写函数 68void AT24C02_Write_Byte(uint8_t addr, uint8_t data) 69{ 70 I2C_Start(); 71 I2C_Send_Byte(0xA0); // 7位地址0x50,写操作 72 I2C_Send_Byte(addr); // 寄存器地址 73 I2C_Send_Byte(data); // 写入数据 74 I2C_Stop(); 75 HAL_Delay(5); // 等待内部写入完成 76} 77 78// AT24C02字节读函数 79uint8_t AT24C02_Read_Byte(uint8_t addr) 80{ 81 uint8_t data; 82 I2C_Start(); 83 I2C_Send_Byte(0xA0); // 写操作,指定寄存器地址 84 I2C_Send_Byte(addr); 85 I2C_Start(); // 重启起始信号 86 I2C_Send_Byte(0xA1); // 读操作 87 data = I2C_Read_Byte(0); // 读取数据,发送NACK 88 I2C_Stop(); 89 return data; 90} 91
2.5 HAL库I2C封装逻辑与核心API深度解析
HAL库将I2C的底层寄存器操作封装为标准化的结构体与API函数,无需手动处理时序、标志位,仅需简单调用即可完成稳定的I2C通信,大幅提升开发效率。
1. I2C核心配置结构体
HAL库用I2C_HandleTypeDef结构体封装I2C外设的所有配置参数,与寄存器一一对应,联动C语言结构体知识点:
1typedef struct { 2 I2C_TypeDef *Instance; // I2C外设基地址,I2C1/I2C2 3 I2C_InitTypeDef Init; // I2C核心初始化参数 4 uint8_t *pBuffPtr; // 数据缓存指针 5 uint16_t XferSize; // 传输数据长度 6 __IO uint16_t XferCount; // 剩余传输长度 7 DMA_HandleTypeDef *hdmatx; // 发送DMA句柄 8 DMA_HandleTypeDef *hdmarx; // 接收DMA句柄 9 HAL_LockTypeDef Lock; // 锁保护 10 __IO HAL_I2C_StateTypeDef State; // I2C运行状态 11 __IO uint32_t ErrorCode; // 错误代码 12} I2C_HandleTypeDef; 13 14// I2C核心初始化参数结构体 15typedef struct { 16 uint32_t ClockSpeed; // 通信速率,100000=100K,400000=400K 17 uint32_t DutyCycle; // 快速模式时钟占空比,I2C_DUTYCYCLE_2 18 uint32_t OwnAddress1; // 主机自身地址,单主机模式随意设置 19 uint32_t AddressingMode; // 地址模式,I2C_ADDRESSINGMODE_7BIT 20 uint32_t DualAddressMode; // 双地址模式,单主机模式关闭 21 uint32_t OwnAddress2; // 第二地址,未使用 22 uint32_t GeneralCallMode; // 广播呼叫模式,关闭 23 uint32_t NoStretchMode; // 时钟拉伸模式,关闭 24} I2C_InitTypeDef; 25
2. HAL库I2C核心API与底层对应关系
工业开发中最常用的是寄存器读写API,适配AT24C02、MPU6050等带内部寄存器的从机设备,核心API如下:
| HAL库API函数 | 核心功能 | 传输模式 | 适用场景 |
|---|---|---|---|
| HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout) | 向从机的指定寄存器写入数据,自动处理起始/停止/应答时序 | 阻塞式轮询 | 绝大多数I2C外设的寄存器写入,如AT24C02数据写入、MPU6050初始化配置 |
| HAL_I2C_Mem_Read(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout) | 从从机的指定寄存器读取数据,自动处理重启、读写切换时序 | 阻塞式轮询 | 绝大多数I2C外设的寄存器读取,如AT24C02数据读取、MPU6050传感器数据采集 |
| HAL_I2C_Mem_Write_IT() | 中断模式写入数据,非阻塞式,传输完成触发回调 | 中断非阻塞 | 长数据写入,不希望阻塞主循环的场景 |
| HAL_I2C_Mem_Read_IT() | 中断模式读取数据,非阻塞式,传输完成触发回调 | 中断非阻塞 | 长数据读取,不希望阻塞主循环的场景 |
| HAL_I2C_Mem_Write_DMA() | DMA模式写入数据,全程零CPU占用,传输完成触发回调 | DMA非阻塞 | 高速大批量数据写入,如OLED屏全屏刷新 |
| HAL_I2C_Mem_Read_DMA() | DMA模式读取数据,全程零CPU占用,传输完成触发回调 | DMA非阻塞 | 高速大批量传感器数据连续采集 |
核心参数说明:
DevAddress:从机的7位地址,如AT24C02的0x50,HAL库会自动处理读写位,无需手动左移;MemAddress:从机的寄存器地址;MemAddSize:寄存器地址宽度,I2C_MEMADD_SIZE_8BIT(8位地址)或I2C_MEMADD_SIZE_16BIT(16位地址);pData:数据缓存数组指针;Size:传输数据的字节数;Timeout:超时时间,单位ms,避免程序卡死。
核心回调函数:
1// 内存写传输完成回调函数 2void HAL_I2C_MemTxCpltCallback(I2C_HandleTypeDef *hi2c); 3// 内存读传输完成回调函数 4void HAL_I2C_MemRxCpltCallback(I2C_HandleTypeDef *hi2c); 5// 传输错误回调函数 6void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c); 7
三、STM32CubeMX+Keil5保姆式实操:I2C工业级外设驱动实战
本次实操适配STM32F103C8T6核心板,实现两大工业级核心场景:① AT24C02 EEPROM掉电存储读写;② MPU6050六轴传感器数据采集,全程无跳步,零基础可零报错跟随完成。
硬件说明
| 外设型号 | 引脚分配 | 核心参数 | 接线说明 |
|---|---|---|---|
| AT24C02 | SCL→PB6(I2C1_SCL)、SDA→PB7(I2C1_SDA) | 2Kbit EEPROM,256字节存储,7位地址0x50 | VCC接3.3V,GND接核心板GND,A0/A1/A2接GND,WP接GND |
| MPU6050 | SCL→PB6(I2C1_SCL)、SDA→PB7(I2C1_SDA) | 3轴加速度+3轴陀螺仪,7位地址0x68 | VCC接3.3V,GND接核心板GND,AD0接GND,INT引脚悬空 |
| 总线配置 | SCL、SDA外接4.7K上拉电阻到3.3V | 快速模式400Kbps | 两个外设的SCL、SDA分别并联,共用一组上拉电阻 |
| 串口USART1 | PA9(TX)、PA10(RX) | 波特率115200,用于数据上报 | 接USB-TTL模块,TX-RX交叉接线,GND共地 |
3.1 工程创建与基础配置
- 打开STM32CubeMX,点击
ACCESS TO MCU SELECTOR,搜索选择STM32F103C8T6,点击Start Project创建工程。 - 调试接口配置:点击左侧
System Core -> SYS,Debug选项选择Serial Wire,开启SWD串行调试。 - 时钟配置:点击
RCC,HSE选项选择Crystal/Ceramic Resonator(外部8MHz晶振);进入Clock Configuration选项卡,配置PLL倍频为x9,系统时钟设置为72MHz,APB1总线时钟36MHz,无红色错误提示。
3.2 I2C外设与NVIC图形化配置
- I2C1配置:
- 点击左侧
Connectivity -> I2C1,Mode选择I2C; - 配置参数:
* I2C Speed Mode:Fast Mode(快速模式)
* I2C Clock Speed:400000 Hz(400Kbps)
* I2C Duty Cycle:Fast Mode duty cycle 2
* 其余参数保持默认,7位地址模式,关闭双地址、广播模式; - 引脚自动映射为PB6(I2C1_SCL)、PB7(I2C1_SDA),均为复用开漏输出模式。
- 点击左侧
- USART1配置:
- 点击左侧
Connectivity -> USART1,Mode选择Asynchronous; - 配置参数:Baud Rate=115200,Word Length=8 Bits,Parity=None,Stop Bits=1;
- 点击左侧
- NVIC中断配置:
- 点击左侧
System Core -> NVIC,优先级分组选择Priority Group 2; - 勾选
I2C1 event interrupt、I2C1 error interrupt、USART1 global interrupt,抢占优先级均设为1。
- 点击左侧
3.3 工程代码生成与驱动文件移植
- 工程生成配置:进入
Project Manager,设置全英文无空格的工程名与保存路径,Toolchain/IDE选择MDK-ARM V5;进入Code Generator,勾选Generate peripheral initialization as a pair of '.c/.h' files per peripheral和Keep User Code when re-generating,点击GENERATE CODE生成工程,完成后点击Open Project打开Keil5工程。 - 驱动文件移植:
- 在工程中新建
at24c02.c、at24c02.h、mpu6050.c、mpu6050.h四个文件,添加到工程中; - 所有驱动代码均适配STM32F103C8T6,可直接复制使用,核心代码见下文。
- 在工程中新建
3.4 AT24C02 EEPROM驱动与掉电存储实战
at24c02.h头文件
1#ifndef __AT24C02_H 2#define __AT24C02_H 3 4#include "stm32f1xx_hal.h" 5#include "i2c.h" 6 7#define AT24C02_ADDR 0x50 // 7位地址 8#define AT24C02_SIZE 256 // 总存储字节数 9#define AT24C02_PAGE_SIZE 8 // 每页字节数 10 11// 函数声明 12void AT24C02_Write_Byte(uint16_t addr, uint8_t data); 13uint8_t AT24C02_Read_Byte(uint16_t addr); 14void AT24C02_Write_Page(uint16_t addr, uint8_t *data, uint8_t len); 15void AT24C02_Read_Bytes(uint16_t addr, uint8_t *data, uint16_t len); 16void AT24C02_Erase_All(void); 17 18#endif 19
at24c02.c驱动文件
1#include "at24c02.h" 2 3// 字节写 4void AT24C02_Write_Byte(uint16_t addr, uint8_t data) 5{ 6 HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR<<1, addr, I2C_MEMADD_SIZE_8BIT, &data, 1, 100); 7 HAL_Delay(5); // 等待内部写入完成,必须加延时 8} 9 10// 字节读 11uint8_t AT24C02_Read_Byte(uint16_t addr) 12{ 13 uint8_t data; 14 HAL_I2C_Mem_Read(&hi2c1, AT24C02_ADDR<<1, addr, I2C_MEMADD_SIZE_8BIT, &data, 1, 100); 15 return data; 16} 17 18// 页写,最多8字节,不能跨页 19void AT24C02_Write_Page(uint16_t addr, uint8_t *data, uint8_t len) 20{ 21 if(len > AT24C02_PAGE_SIZE) len = AT24C02_PAGE_SIZE; 22 if(addr % AT24C02_PAGE_SIZE + len > AT24C02_PAGE_SIZE) len = AT24C02_PAGE_SIZE - (addr % AT24C02_PAGE_SIZE); 23 HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR<<1, addr, I2C_MEMADD_SIZE_8BIT, data, len, 100); 24 HAL_Delay(5); 25} 26 27// 连续读,无长度限制 28void AT24C02_Read_Bytes(uint16_t addr, uint8_t *data, uint16_t len) 29{ 30 HAL_I2C_Mem_Read(&hi2c1, AT24C02_ADDR<<1, addr, I2C_MEMADD_SIZE_8BIT, data, len, 100); 31} 32 33// 全片擦除,写入0xFF 34void AT24C02_Erase_All(void) 35{ 36 uint8_t buf[AT24C02_PAGE_SIZE] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF}; 37 for(uint16_t i=0; i<AT24C02_SIZE; i+=AT24C02_PAGE_SIZE) 38 { 39 AT24C02_Write_Page(i, buf, AT24C02_PAGE_SIZE); 40 } 41} 42
3.5 MPU6050六轴传感器驱动与数据采集实战
mpu6050.h头文件
1#ifndef __MPU6050_H 2#define __MPU6050_H 3 4#include "stm32f1xx_hal.h" 5#include "i2c.h" 6#include <math.h> 7 8#define MPU6050_ADDR 0x68 // 7位地址 9 10// 寄存器地址定义 11#define MPU6050_PWR_MGMT_1 0x6B 12#define MPU6050_SMPLRT_DIV 0x19 13#define MPU6050_GYRO_CONFIG 0x1B 14#define MPU6050_ACCEL_CONFIG 0x1C 15#define MPU6050_ACCEL_XOUT_H 0x3B 16#define MPU6050_TEMP_OUT_H 0x41 17#define MPU6050_GYRO_XOUT_H 0x43 18#define MPU6050_WHO_AM_I 0x75 19 20// 量程定义 21#define GYRO_RANGE_250 0 // ±250°/s 22#define GYRO_RANGE_500 1 // ±500°/s 23#define GYRO_RANGE_1000 2 // ±1000°/s 24#define GYRO_RANGE_2000 3 // ±2000°/s 25#define ACCEL_RANGE_2G 0 // ±2g 26#define ACCEL_RANGE_4G 1 // ±4g 27#define ACCEL_RANGE_8G 2 // ±8g 28#define ACCEL_RANGE_16G 3 // ±16g 29 30// 传感器数据结构体 31typedef struct { 32 int16_t accel_x_raw; 33 int16_t accel_y_raw; 34 int16_t accel_z_raw; 35 int16_t gyro_x_raw; 36 int16_t gyro_y_raw; 37 int16_t gyro_z_raw; 38 int16_t temp_raw; 39 float accel_x; // 单位g 40 float accel_y; 41 float accel_z; 42 float gyro_x; // 单位°/s 43 float gyro_y; 44 float gyro_z; 45 float temperature; // 单位℃ 46} MPU6050_Data_Typedef; 47 48// 函数声明 49uint8_t MPU6050_Init(uint8_t gyro_range, uint8_t accel_range); 50uint8_t MPU6050_Get_ID(void); 51void MPU6050_Get_Data(MPU6050_Data_Typedef *data); 52 53#endif 54
mpu6050.c驱动文件
1#include "mpu6050.h" 2 3// 量程灵敏度系数 4static float gyro_sensitivity; 5static float accel_sensitivity; 6 7// MPU6050初始化 8uint8_t MPU6050_Init(uint8_t gyro_range, uint8_t accel_range) 9{ 10 uint8_t id = MPU6050_Get_ID(); 11 if(id != 0x68) return 1; // 芯片ID错误,初始化失败 12 13 // 唤醒MPU6050,退出睡眠模式 14 uint8_t data = 0x00; 15 HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDR<<1, MPU6050_PWR_MGMT_1, I2C_MEMADD_SIZE_8BIT, &data, 1, 100); 16 HAL_Delay(10); 17 18 // 配置陀螺仪量程 19 data = gyro_range << 3; 20 HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDR<<1, MPU6050_GYRO_CONFIG, I2C_MEMADD_SIZE_8BIT, &data, 1, 100); 21 // 配置加速度量程 22 data = accel_range << 3; 23 HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDR<<1, MPU6050_ACCEL_CONFIG, I2C_MEMADD_SIZE_8BIT, &data, 1, 100); 24 // 配置采样率分频,1kHz采样率 25 data = 0x00; 26 HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDR<<1, MPU6050_SMPLRT_DIV, I2C_MEMADD_SIZE_8BIT, &data, 1, 100); 27 28 // 设置灵敏度系数 29 switch(gyro_range) 30 { 31 case GYRO_RANGE_250: gyro_sensitivity = 131.0f; break; 32 case GYRO_RANGE_500: gyro_sensitivity = 65.5f; break; 33 case GYRO_RANGE_1000: gyro_sensitivity = 32.8f; break; 34 case GYRO_RANGE_2000: gyro_sensitivity = 16.4f; break; 35 default: gyro_sensitivity = 131.0f; break; 36 } 37 switch(accel_range) 38 { 39 case ACCEL_RANGE_2G: accel_sensitivity = 16384.0f; break; 40 case ACCEL_RANGE_4G: accel_sensitivity = 8192.0f; break; 41 case ACCEL_RANGE_8G: accel_sensitivity = 4096.0f; break; 42 case ACCEL_RANGE_16G: accel_sensitivity = 2048.0f; break; 43 default: accel_sensitivity = 16384.0f; break; 44 } 45 return 0; 46} 47 48// 读取芯片ID 49uint8_t MPU6050_Get_ID(void) 50{ 51 uint8_t id; 52 HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR<<1, MPU6050_WHO_AM_I, I2C_MEMADD_SIZE_8BIT, &id, 1, 100); 53 return id; 54} 55 56// 读取传感器全部数据 57void MPU6050_Get_Data(MPU6050_Data_Typedef *data) 58{ 59 uint8_t buf[14]; 60 // 连续读取14个寄存器的数据 61 HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR<<1, MPU6050_ACCEL_XOUT_H, I2C_MEMADD_SIZE_8BIT, buf, 14, 100); 62 63 // 拼接原始数据,高位在前 64 data->accel_x_raw = (int16_t)(buf[0] << 8 | buf[1]); 65 data->accel_y_raw = (int16_t)(buf[2] << 8 | buf[3]); 66 data->accel_z_raw = (int16_t)(buf[4] << 8 | buf[5]); 67 data->temp_raw = (int16_t)(buf[6] << 8 | buf[7]); 68 data->gyro_x_raw = (int16_t)(buf[8] << 8 | buf[9]); 69 data->gyro_y_raw = (int16_t)(buf[10] << 8 | buf[11]); 70 data->gyro_z_raw = (int16_t)(buf[12] << 8 | buf[13]); 71 72 // 转换为实际物理量 73 data->accel_x = (float)data->accel_x_raw / accel_sensitivity; 74 data->accel_y = (float)data->accel_y_raw / accel_sensitivity; 75 data->accel_z = (float)data->accel_z_raw / accel_sensitivity; 76 data->gyro_x = (float)data->gyro_x_raw / gyro_sensitivity; 77 data->gyro_y = (float)data->gyro_y_raw / gyro_sensitivity; 78 data->gyro_z = (float)data->gyro_z_raw / gyro_sensitivity; 79 data->temperature = (float)data->temp_raw / 340.0f + 36.53f; 80} 81
main.c业务代码编写
所有代码必须写在用户代码区,避免重新生成时被覆盖。
1/* USER CODE BEGIN Includes */ 2#include "at24c02.h" 3#include "mpu6050.h" 4#include <stdio.h> 5/* USER CODE END Includes */ 6 7/* USER CODE BEGIN PV */ 8// 重定向printf到USART1 9int fputc(int ch, FILE *f) 10{ 11 HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); 12 return ch; 13} 14 15// AT24C02测试数据 16uint8_t write_buf[8] = {0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88}; 17uint8_t read_buf[8] = {0}; 18 19// MPU6050数据结构体 20MPU6050_Data_Typedef mpu6050_data; 21/* USER CODE END PV */ 22 23int main(void) 24{ 25 HAL_Init(); 26 SystemClock_Config(); 27 MX_GPIO_Init(); 28 MX_I2C1_Init(); 29 MX_USART1_UART_Init(); 30 31 /* USER CODE BEGIN 2 */ 32 printf("STM32 I2C Test Start\r\n"); 33 34 // ===================== AT24C02掉电存储测试 ===================== 35 printf("===== AT24C02 Test =====\r\n"); 36 // 写入数据 37 AT24C02_Write_Page(0x00, write_buf, 8); 38 printf("Write Data: 0x%02X 0x%02X 0x%02X 0x%02X 0x%02X 0x%02X 0x%02X 0x%02X\r\n", 39 write_buf[0],write_buf[1],write_buf[2],write_buf[3], 40 write_buf[4],write_buf[5],write_buf[6],write_buf[7]); 41 // 读取数据 42 AT24C02_Read_Bytes(0x00, read_buf, 8); 43 printf("Read Data: 0x%02X 0x%02X 0x%02X 0x%02X 0x%02X 0x%02X 0x%02X 0x%02X\r\n", 44 read_buf[0],read_buf[1],read_buf[2],read_buf[3], 45 read_buf[4],read_buf[5],read_buf[6],read_buf[7]); 46 // 数据校验 47 if(memcmp(write_buf, read_buf, 8) == 0) 48 printf("AT24C02 Test Success!\r\n"); 49 else 50 printf("AT24C02 Test Failed!\r\n"); 51 52 // ===================== MPU6050初始化 ===================== 53 printf("===== MPU6050 Test =====\r\n"); 54 uint8_t mpu_status = MPU6050_Init(GYRO_RANGE_250, ACCEL_RANGE_2G); 55 if(mpu_status == 0) 56 printf("MPU6050 Init Success, Chip ID: 0x%02X\r\n", MPU6050_Get_ID()); 57 else 58 printf("MPU6050 Init Failed!\r\n"); 59 /* USER CODE END 2 */ 60 61 while (1) 62 { 63 /* USER CODE END WHILE */ 64 65 /* USER CODE BEGIN 3 */ 66 // 读取MPU6050数据 67 MPU6050_Get_Data(&mpu6050_data); 68 // 串口上报数据 69 printf("Accel: X=%.2fg Y=%.2fg Z=%.2fg | Gyro: X=%.2f°/s Y=%.2f°/s Z=%.2f°/s | Temp=%.2f℃\r\n", 70 mpu6050_data.accel_x, mpu6050_data.accel_y, mpu6050_data.accel_z, 71 mpu6050_data.gyro_x, mpu6050_data.gyro_y, mpu6050_data.gyro_z, 72 mpu6050_data.temperature); 73 HAL_Delay(500); 74 } 75 /* USER CODE END 3 */ 76} 77
3.6 编译、烧录与效果验证
- 点击Keil顶部Build按钮(F7快捷键)编译工程,底部提示
0 Error(s), 0 Warning(s),说明编译成功。 - 仿真器配置:点击魔法棒图标(Alt+F7快捷键),Debug选项卡选择
ST-Link Debugger,Settings中确认Port为SW,能识别到芯片;Flash Download选项卡勾选Reset and Run,点击OK保存。 - 硬件接线核心注意事项:
- SCL、SDA必须外接4.7K上拉电阻到3.3V,否则会出现通信异常;
- 所有外设VCC接3.3V,严禁接5V,避免烧毁芯片;
- 所有外设GND必须与核心板GND可靠共地,保证电平基准一致;
- AT24C02的A0/A1/A2、MPU6050的AD0必须接GND,匹配驱动代码中的地址。
- 效果验证:
- 打开串口助手,配置115200波特率、8位数据、1位停止位、无校验,打开串口;
- 核心板上电后,串口打印AT24C02测试结果,写入与读取的数据一致,测试成功;
- MPU6050初始化成功,打印芯片ID 0x68,每500ms上报一次加速度、陀螺仪、温度数据;
- 晃动MPU6050模块,加速度、陀螺仪数值同步变化,温度数据正常,驱动完全正常;
- 断电重启核心板,重新读取AT24C02的数据,与写入的数据一致,实现掉电不丢失存储。
四、保姆式排错指南
| 异常现象/报错信息 | 核心根因 | 一步到位解决方法 |
|---|---|---|
| I2C通信一直超时,从机无ACK应答 | 1. SCL/SDA接反,接线错误;2. 未外接上拉电阻,总线电平异常;3. 从机地址错误,7位地址与8位地址混淆;4. 外设未供电、未共地,电平基准错误;5. I2C时钟配置错误,速率超过外设支持上限 | 1. 严格核对接线:SCL接SCL、SDA接SDA,严禁接反;2. SCL、SDA必须外接4.7K上拉电阻到3.3V;3. HAL库传入7位地址,无需手动左移,核对外设硬件地址;4. 确认外设供电正常,GND与核心板可靠共地;5. 降低I2C速率到100Kbps,匹配外设最大速率 |
| I2C总线卡死,程序进入死循环,无法通信 | 1. 总线死锁,从机拉低SDA线不释放;2. 通信超时时间设置过短,未完成传输就强制退出;3. 未开启I2C错误中断,错误发生后无法恢复;4. 软件模拟与硬件I2C混用,总线状态异常 | 1. 增加总线死锁恢复代码,模拟9个SCL时钟脉冲释放SDA线,软件复位I2C外设;2. 增加超时时间到100ms以上,避免程序卡死;3. 开启I2C错误中断,在错误回调中复位总线;4. 禁止同时使用软件模拟与硬件I2C,统一使用硬件I2C |
| AT24C02写入数据后,读取的数据全是0xFF,或只有前几个字节正确 | 1. 写入后未加5ms延时,内部写入未完成就发起读操作;2. 页写跨页,数据回卷覆盖;3. WP引脚接高电平,写保护开启,无法写入;4. 寄存器地址超出256字节范围 | 1. 每次写入操作后必须加5ms延时,等待EEPROM内部写入完成;2. 页写最多8字节,不能跨页,跨页需分多次写入;3. WP引脚接GND,关闭写保护;4. 限制寄存器地址在0~255范围内 |
| MPU6050初始化失败,读取ID为0x00或0xFF | 1. 模块供电错误,接了5V导致芯片烧毁;2. AD0引脚电平与地址不匹配,AD0接高电平时地址应为0x69;3. I2C通信速率过高,MPU6050最高支持400Kbps;4. 模块焊接不良,引脚虚焊 | 1. MPU6050必须接3.3V供电,严禁接5V,更换模块测试;2. AD0接高电平时,地址改为0x69;3. 降低I2C速率到100Kbps,提升稳定性;4. 检查模块引脚焊接,确保SCL、SDA、VCC、GND引脚连接可靠 |
| 多从机挂载同一条总线,只有一个设备能通信,另一个无响应 | 1. 两个从机地址冲突,地址相同导致总线冲突;2. 总线未接上拉电阻,或上拉电阻阻值错误;3. 总线走线过长,电容过大,信号畸变;4. 其中一个设备未上电,拉低总线电平 | 1. 修改其中一个设备的硬件地址,确保两个从机7位地址唯一;2. 更换4.7KΩ上拉电阻,一条总线仅保留一组上拉;3. 缩短总线走线长度,降低I2C通信速率;4. 确保所有从机供电正常,GND共地 |
| 读写数据错位,高8位与低8位颠倒,数据异常 | 1. 数据传输时大小端格式错误,MPU6050数据为高位在前;2. 连续读取时寄存器地址自增错误;3. 数据类型定义错误,用uint8_t接收16位数据 | 1. 读取16位数据时,先读高8位,再读低8位,左移8位后拼接;2. 确认从机支持地址自增,开启连续读取模式;3. 用int16_t类型存储16位原始数据,避免数据溢出 |
五、我的踩坑记录
- 踩坑现象:第一次调试硬件I2C,程序一直卡在HAL_I2C_Mem_Write函数里,返回超时错误,从机无ACK应答,换了软件模拟I2C就正常。
底层原因:我只在CubeMX里配置了I2C的复用开漏输出,没有在SCL、SDA线上外接4.7K上拉电阻,误以为STM32内部上拉就够用。但开漏输出模式下,内部上拉电阻阻值过大,总线电平上升沿缓慢,无法满足I2C时序要求,从机无法正确采样时钟与数据,自然不会反馈ACK。51单片机软件模拟时我加了外部上拉,换了硬件I2C反而忘了,踩了低级坑。
最终解决方案:在SCL、SDA线上各接一个4.7KΩ电阻到3.3V,重新烧录程序,I2C通信立即正常,超时问题彻底解决。 - 踩坑现象:AT24C02连续写入16字节数据,结果只有前8个字节正确,后8个字节覆盖了前8个,读取出来的数据完全错乱。
底层原因:AT24C02每页只有8字节,页写操作不能跨页,一旦跨页,地址会自动回卷到当前页的起始地址,导致后8个字节覆盖了前8个。我误以为连续写入可以跨页,完全忽略了页写的边界限制,51单片机里我是单字节写入,没遇到这个问题,换了页写就踩坑了。
最终解决方案:修改页写函数,增加跨页判断,超过页边界的内容分多次写入,每次写入后加5ms延时,修改后连续写入数据完全正常,无覆盖、无错乱。 - 踩坑现象:MPU6050初始化成功,能读到ID,但读取的加速度、陀螺仪数据全是0,或者数值完全不变化。
底层原因:我在初始化MPU6050时,只写了PWR_MGMT_1寄存器的最低位,没有把整个寄存器写0,导致MPU6050没有完全退出睡眠模式,传感器的测量单元没有启动,自然读不到正确的数据。同时我配置量程时,错误地把数值直接写入寄存器,没有左移3位,量程配置错误,传感器没有正常工作。
最终解决方案:初始化时向PWR_MGMT_1寄存器写入0x00,完全唤醒MPU6050,配置量程时将数值左移3位,匹配寄存器的位定义,重新烧录后,传感器数据完全正常,随晃动同步变化。 - 踩坑现象:设备上电后I2C通信正常,但运行一段时间后,总线突然卡死,程序完全无响应,只能复位重启。
底层原因:I2C总线出现了死锁,从机在接收数据时,主机意外复位,导致从机正在输出低电平,一直拉着SDA线不放,主机检测到SDA为低电平,无法生成起始信号,总线彻底死锁。我没有做总线死锁的恢复处理,一旦出现死锁,程序就会卡死在等待标志位的循环里。
最终解决方案:在I2C初始化函数中增加总线死锁恢复代码,将SCL、SDA配置为普通GPIO,模拟9个SCL时钟脉冲,让从机完成剩余的位传输,释放SDA总线,再重新初始化硬件I2C。同时在错误回调函数中增加总线复位逻辑,出现错误时自动恢复总线,彻底解决了卡死问题。
六、课后小练习(附完整标准答案)
6.1 基础巩固练习
练习1:实现I2C总线地址扫描函数,自动扫描总线上所有挂载的从机设备,串口打印有效设备地址。
标准答案:
1void I2C_Scan_Device(void) 2{ 3 printf("Start I2C Device Scan...\r\n"); 4 uint8_t device_cnt = 0; 5 for(uint8_t addr=0; addr<128; addr++) 6 { 7 // 检测地址是否有应答 8 if(HAL_I2C_IsDeviceReady(&hi2c1, addr<<1, 1, 10) == HAL_OK) 9 { 10 printf("Found I2C Device at 7-bit Address: 0x%02X\r\n", addr); 11 device_cnt++; 12 } 13 } 14 printf("Scan Complete, Found %d Devices\r\n", device_cnt); 15} 16 17// 主函数调用 18/* USER CODE BEGIN 2 */ 19I2C_Scan_Device(); 20/* USER CODE END 2 */ 21
练习2:基于AT24C02实现传感器校准参数的掉电存储,写入加速度、陀螺仪的零点校准值,断电重启后自动读取。
标准答案:
1/* USER CODE BEGIN PV */ 2// 校准参数结构体 3typedef struct { 4 float accel_x_offset; 5 float accel_y_offset; 6 float accel_z_offset; 7 float gyro_x_offset; 8 float gyro_y_offset; 9 float gyro_z_offset; 10} Calib_Data_Typedef; 11 12Calib_Data_Typedef calib_data = {0.02f, -0.01f, 1.02f, 0.5f, -0.3f, 0.2f}; 13Calib_Data_Typedef read_calib_data = {0}; 14/* USER CODE END PV */ 15 16// 主函数调用 17/* USER CODE BEGIN 2 */ 18// 写入校准参数 19AT24C02_Write_Page(0x10, (uint8_t *)&calib_data, sizeof(Calib_Data_Typedef)); 20// 读取校准参数 21AT24C02_Read_Bytes(0x10, (uint8_t *)&read_calib_data, sizeof(Calib_Data_Typedef)); 22// 串口打印 23printf("Calib Data Read: accel_x=%.3f, accel_y=%.3f, accel_z=%.3f\r\n", 24 read_calib_data.accel_x_offset, read_calib_data.accel_y_offset, read_calib_data.accel_z_offset); 25printf("gyro_x=%.3f, gyro_y=%.3f, gyro_z=%.3f\r\n", 26 read_calib_data.gyro_x_offset, read_calib_data.gyro_y_offset, read_calib_data.gyro_z_offset); 27/* USER CODE END 2 */ 28
练习3:基于MPU6050内置温度传感器,实现环境温度采集,串口上报,精度保留2位小数。
标准答案:
1/* USER CODE BEGIN WHILE */ 2while (1) 3{ 4 MPU6050_Get_Data(&mpu6050_data); 5 printf("Current Temperature: %.2f℃\r\n", mpu6050_data.temperature); 6 HAL_Delay(1000); 7} 8/* USER CODE END WHILE */ 9
6.2 进阶实战练习
练习1:实现I2C中断模式的AT24C02连续读写,非阻塞式传输,传输完成后在回调函数中进行数据校验。
标准答案:
1/* USER CODE BEGIN PV */ 2uint8_t it_write_buf[8] = {0xAA,0xBB,0xCC,0xDD,0xEE,0xFF,0x11,0x22}; 3uint8_t it_read_buf[8] = {0}; 4uint8_t tx_done = 0; 5uint8_t rx_done = 0; 6/* USER CODE END PV */ 7 8/* USER CODE BEGIN 0 */ 9// 写传输完成回调 10void HAL_I2C_MemTxCpltCallback(I2C_HandleTypeDef *hi2c) 11{ 12 if(hi2c->Instance == I2C1) 13 { 14 tx_done = 1; 15 } 16} 17 18// 读传输完成回调 19void HAL_I2C_MemRxCpltCallback(I2C_HandleTypeDef *hi2c) 20{ 21 if(hi2c->Instance == I2C1) 22 { 23 rx_done = 1; 24 } 25} 26/* USER CODE END 0 */ 27 28// 主函数调用 29/* USER CODE BEGIN 2 */ 30// 中断模式写入数据 31HAL_I2C_Mem_Write_IT(&hi2c1, AT24C02_ADDR<<1, 0x20, I2C_MEMADD_SIZE_8BIT, it_write_buf, 8); 32/* USER CODE END 2 */ 33 34/* USER CODE BEGIN WHILE */ 35while (1) 36{ 37 if(tx_done == 1) 38 { 39 printf("Write Done!\r\n"); 40 tx_done = 0; 41 HAL_Delay(5); 42 // 中断模式读取数据 43 HAL_I2C_Mem_Read_IT(&hi2c1, AT24C02_ADDR<<1, 0x20, I2C_MEMADD_SIZE_8BIT, it_read_buf, 8); 44 } 45 if(rx_done == 1) 46 { 47 printf("Read Done!\r\n"); 48 rx_done = 0; 49 // 数据校验 50 if(memcmp(it_write_buf, it_read_buf, 8) == 0) 51 printf("IT Mode Test Success!\r\n"); 52 else 53 printf("IT Mode Test Failed!\r\n"); 54 } 55 HAL_Delay(10); 56} 57/* USER CODE END WHILE */ 58
练习2:基于MPU6050加速度数据,实现倾斜角度检测,计算俯仰角和横滚角,串口实时上报。
标准答案:
1/* USER CODE BEGIN PV */ 2float pitch = 0.0f; // 俯仰角 3float roll = 0.0f; // 横滚角 4/* USER CODE END PV */ 5 6/* USER CODE BEGIN WHILE */ 7while (1) 8{ 9 MPU6050_Get_Data(&mpu6050_data); 10 11 // 计算俯仰角和横滚角,单位° 12 pitch = atan2(mpu6050_data.accel_y, mpu6050_data.accel_z) * 180.0f / 3.1415926f; 13 roll = atan2(-mpu6050_data.accel_x, sqrt(mpu6050_data.accel_y*mpu6050_data.accel_y + mpu6050_data.accel_z*mpu6050_data.accel_z)) * 180.0f / 3.1415926f; 14 15 // 串口上报 16 printf("Pitch: %.2f° | Roll: %.2f°\r\n", pitch, roll); 17 HAL_Delay(200); 18} 19/* USER CODE END WHILE */ 20
七、核心知识点速记
- I2C是两线半双工串行总线,SCL为串行时钟,SDA为串行数据,开漏输出必须外接4.7KΩ上拉电阻到3.3V,这是通信稳定的核心前提。
- I2C采用主从架构,单主机多从机,7位从机地址+1位读写位,写0读1,标准模式100Kbps,快速模式400Kbps,HAL库仅需传入7位地址,无需手动左移。
- I2C完整读写时序:起始信号→从机地址+读写位→应答→寄存器地址→应答→数据传输→应答/非应答→停止信号,每字节传输后必须等待应答。
- AT24C02是2Kbit EEPROM,256字节存储,每页8字节,页写不能跨页,每次写入后必须加5ms延时等待内部写入完成,支持掉电永久存储。
- MPU6050默认7位地址0x68,AD0接高电平为0x69,内置3轴加速度、3轴陀螺仪、温度传感器,初始化时必须写入0x00到PWR_MGMT_1寄存器,唤醒芯片退出睡眠模式。
- STM32硬件I2C必须处理总线死锁问题,通过模拟SCL时钟脉冲释放SDA总线,配合软件复位,避免程序卡死。
- HAL库I2C核心读写函数:
HAL_I2C_Mem_Write()写从机寄存器,HAL_I2C_Mem_Read()读从机寄存器,适配绝大多数带寄存器的I2C从机设备。 - 多从机挂载同一条总线,必须保证每个从机的7位地址唯一,共用一组上拉电阻,所有设备必须共地,避免地址冲突与电平异常。
- I2C无ACK应答的核心排查顺序:接线与共地→上拉电阻→从机地址→外设供电→时钟速率配置。
八、本章小结
本章我们深入拆解了I2C通信协议的底层时序与主从机通信机制,对比了51单片机软件模拟与STM32硬件I2C的核心差异,掌握了硬件I2C的寄存器级原理与HAL库封装逻辑,完成了AT24C02 EEPROM掉电存储、MPU6050六轴传感器数据采集两大工业级实战,解决了总线死锁、无ACK、数据错乱等高频问题。I2C是低速外设的首选通信总线,下一章我们将学习SPI串行外设协议,掌握高速同步串行通信的底层原理与W25Qxx Flash芯片的驱动开发,实现大容量数据的高速掉电存储。
《第12章 I2C通信协议全解:底层时序、主从机通信与AT24C02、MPU6050传感器实战》 是转载文章,点击查看原文。