第12章 I2C通信协议全解:底层时序、主从机通信与AT24C02、MPU6050传感器实战

作者:是翔仔呐日期:2026/3/29

前言

上一章我们通过硬件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 进阶实战练习
  • 七、核心知识点速记
  • 八、本章小结

一、本章学习目标

  1. 掌握I2C串行通信协议的底层时序、主从机通信机制与应答规则,对比51单片机软件模拟方案的核心差异,理解硬件I2C的工程价值
  2. 吃透STM32硬件I2C的内核架构、寄存器级工作原理,联动C语言位操作、指针知识点,能独立实现寄存器级的I2C初始化与读写操作
  3. 掌握HAL库I2C的封装逻辑与核心API使用,能区分阻塞/中断/DMA三种传输模式的选型逻辑,适配不同工业开发场景
  4. 熟练完成AT24C02 EEPROM的驱动开发,实现字节/页读写、连续读写功能,完成掉电不丢失数据的存储实战
  5. 熟练完成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单片机软件模拟I2CSTM32硬件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)主机写从机寄存器(字节写)

完整时序流程:

  1. 主机发送起始信号START;
  2. 主机发送从机7位地址+1位写标志位(0),共1字节;
  3. 从机反馈应答信号ACK;
  4. 主机发送要访问的从机寄存器地址;
  5. 从机反馈应答信号ACK;
  6. 主机发送要写入寄存器的1字节数据;
  7. 从机反馈应答信号ACK;
  8. 主机发送停止信号STOP,结束通信。
(2)主机读从机寄存器(随机读)

完整时序流程:

  1. 主机发送起始信号START;
  2. 主机发送从机7位地址+1位写标志位(0),执行写操作,用于指定要读取的寄存器地址;
  3. 从机反馈应答信号ACK;
  4. 主机发送要读取的从机寄存器地址;
  5. 从机反馈应答信号ACK;
  6. 主机重新发送起始信号START(重启通信);
  7. 主机发送从机7位地址+1位读标志位(1),切换为读操作;
  8. 从机反馈应答信号ACK;
  9. 从机发送寄存器中的1字节数据;
  10. 主机反馈非应答信号NACK,告知从机停止发送;
  11. 主机发送停止信号STOP,结束通信。

2.3 I2C从机地址规则与多设备总线架构

1. 从机地址格式

I2C从机地址分为7位和10位两种,工业开发中99%的外设使用7位地址,格式如下:

7位地址位读写位
bit7~bit1bit0
  • 高7位:从机的唯一硬件地址,由芯片厂商固定,部分芯片可通过硬件引脚修改地址;
  • 最低位bit0:读写标志位,0=主机向从机写数据,1=主机从从机读数据。
    新手易错点:7位地址与8位地址的换算,例如AT24C02的7位地址是0x50,8位写地址是0xA0,8位读地址是0xA1,HAL库中仅需传入7位地址,读写位由API自动处理。
2. 常用外设地址规则
外设型号默认7位地址地址修改方式
AT24C020x50通过A0/A1/A2引脚电平修改,最多支持8个同型号芯片挂载同一条总线
MPU60500x68AD0引脚接高电平,地址变为0x69
SSD1306 OLED0x3C部分屏硬件地址为0x3D
AT24C04/08/160x50高位地址由芯片页地址位决定,A0/A1引脚功能变化
3. 多从机总线架构

一条I2C总线可挂载多个从机设备,核心规则:

  1. 所有从机的SCL、SDA引脚分别并联到主机的SCL、SDA引脚;
  2. 总线必须在SCL、SDA线上外接4.7KΩ上拉电阻到3.3V,一条总线仅需一组上拉电阻,无需每个设备都加;
  3. 所有从机的7位地址必须唯一,不能出现地址冲突,否则会导致通信完全异常;
  4. 所有设备必须共地,保证电平基准一致,否则会出现采样错误、无ACK等问题。

2.4 STM32硬件I2C核心寄存器与C语言位操作联动

STM32F103的I2C外设通过7个核心寄存器实现完整的协议控制,我们以I2C1为例,拆解核心寄存器的功能与C语言位操作实现,联动51单片机软件模拟逻辑,实现无缝衔接。

寄存器名称结构体成员读写属性核心功能与位操作详解
控制寄存器1I2C1->CR1读写I2C核心控制寄存器,关键位:- 位0(PE):外设使能位,写1开启I2C外设- 位8(START):起始信号生成位,写1硬件自动生成起始信号- 位9(STOP):停止信号生成位,写1硬件自动生成停止信号- 位10(ACK):应答使能位,写1开启自动应答- 位15(SWRST):软件复位位,总线死锁时写1复位I2C外设
控制寄存器2I2C1->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位有效,存储要发送/已接收的数据,写入数据自动启动发送,读取数据获取接收结果
状态寄存器1I2C1->SR1只读事件与错误状态标志位,关键位:- 位0(SB):起始信号生成完成标志位- 位1(ADDR):地址发送完成标志位- 位2(BTF):字节传输完成标志位- 位6(TXE):发送数据寄存器空标志位- 位6(RXNE):接收数据寄存器非空标志位- 位10(AF):应答失败标志位,从机无ACK时置1
状态寄存器2I2C1->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六轴传感器数据采集,全程无跳步,零基础可零报错跟随完成。

硬件说明

外设型号引脚分配核心参数接线说明
AT24C02SCL→PB6(I2C1_SCL)、SDA→PB7(I2C1_SDA)2Kbit EEPROM,256字节存储,7位地址0x50VCC接3.3V,GND接核心板GND,A0/A1/A2接GND,WP接GND
MPU6050SCL→PB6(I2C1_SCL)、SDA→PB7(I2C1_SDA)3轴加速度+3轴陀螺仪,7位地址0x68VCC接3.3V,GND接核心板GND,AD0接GND,INT引脚悬空
总线配置SCL、SDA外接4.7K上拉电阻到3.3V快速模式400Kbps两个外设的SCL、SDA分别并联,共用一组上拉电阻
串口USART1PA9(TX)、PA10(RX)波特率115200,用于数据上报接USB-TTL模块,TX-RX交叉接线,GND共地

3.1 工程创建与基础配置

  1. 打开STM32CubeMX,点击ACCESS TO MCU SELECTOR,搜索选择STM32F103C8T6,点击Start Project创建工程。
  2. 调试接口配置:点击左侧System Core -> SYS,Debug选项选择Serial Wire,开启SWD串行调试。
  3. 时钟配置:点击RCC,HSE选项选择Crystal/Ceramic Resonator(外部8MHz晶振);进入Clock Configuration选项卡,配置PLL倍频为x9,系统时钟设置为72MHz,APB1总线时钟36MHz,无红色错误提示。

3.2 I2C外设与NVIC图形化配置

  1. 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),均为复用开漏输出模式。
  2. USART1配置:
    • 点击左侧Connectivity -> USART1,Mode选择Asynchronous
    • 配置参数:Baud Rate=115200,Word Length=8 Bits,Parity=None,Stop Bits=1;
  3. NVIC中断配置:
    • 点击左侧System Core -> NVIC,优先级分组选择Priority Group 2
    • 勾选I2C1 event interruptI2C1 error interruptUSART1 global interrupt,抢占优先级均设为1。

3.3 工程代码生成与驱动文件移植

  1. 工程生成配置:进入Project Manager,设置全英文无空格的工程名与保存路径,Toolchain/IDE选择MDK-ARM V5;进入Code Generator,勾选Generate peripheral initialization as a pair of '.c/.h' files per peripheralKeep User Code when re-generating,点击GENERATE CODE生成工程,完成后点击Open Project打开Keil5工程。
  2. 驱动文件移植:
    • 在工程中新建at24c02.cat24c02.hmpu6050.cmpu6050.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 编译、烧录与效果验证

  1. 点击Keil顶部Build按钮(F7快捷键)编译工程,底部提示0 Error(s), 0 Warning(s),说明编译成功。
  2. 仿真器配置:点击魔法棒图标(Alt+F7快捷键),Debug选项卡选择ST-Link Debugger,Settings中确认Port为SW,能识别到芯片;Flash Download选项卡勾选Reset and Run,点击OK保存。
  3. 硬件接线核心注意事项:
    • SCL、SDA必须外接4.7K上拉电阻到3.3V,否则会出现通信异常;
    • 所有外设VCC接3.3V,严禁接5V,避免烧毁芯片;
    • 所有外设GND必须与核心板GND可靠共地,保证电平基准一致;
    • AT24C02的A0/A1/A2、MPU6050的AD0必须接GND,匹配驱动代码中的地址。
  4. 效果验证:
    • 打开串口助手,配置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或0xFF1. 模块供电错误,接了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位原始数据,避免数据溢出

五、我的踩坑记录

  1. 踩坑现象:第一次调试硬件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通信立即正常,超时问题彻底解决。
  2. 踩坑现象:AT24C02连续写入16字节数据,结果只有前8个字节正确,后8个字节覆盖了前8个,读取出来的数据完全错乱。
    底层原因:AT24C02每页只有8字节,页写操作不能跨页,一旦跨页,地址会自动回卷到当前页的起始地址,导致后8个字节覆盖了前8个。我误以为连续写入可以跨页,完全忽略了页写的边界限制,51单片机里我是单字节写入,没遇到这个问题,换了页写就踩坑了。
    最终解决方案:修改页写函数,增加跨页判断,超过页边界的内容分多次写入,每次写入后加5ms延时,修改后连续写入数据完全正常,无覆盖、无错乱。
  3. 踩坑现象:MPU6050初始化成功,能读到ID,但读取的加速度、陀螺仪数据全是0,或者数值完全不变化。
    底层原因:我在初始化MPU6050时,只写了PWR_MGMT_1寄存器的最低位,没有把整个寄存器写0,导致MPU6050没有完全退出睡眠模式,传感器的测量单元没有启动,自然读不到正确的数据。同时我配置量程时,错误地把数值直接写入寄存器,没有左移3位,量程配置错误,传感器没有正常工作。
    最终解决方案:初始化时向PWR_MGMT_1寄存器写入0x00,完全唤醒MPU6050,配置量程时将数值左移3位,匹配寄存器的位定义,重新烧录后,传感器数据完全正常,随晃动同步变化。
  4. 踩坑现象:设备上电后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

七、核心知识点速记

  1. I2C是两线半双工串行总线,SCL为串行时钟,SDA为串行数据,开漏输出必须外接4.7KΩ上拉电阻到3.3V,这是通信稳定的核心前提。
  2. I2C采用主从架构,单主机多从机,7位从机地址+1位读写位,写0读1,标准模式100Kbps,快速模式400Kbps,HAL库仅需传入7位地址,无需手动左移。
  3. I2C完整读写时序:起始信号→从机地址+读写位→应答→寄存器地址→应答→数据传输→应答/非应答→停止信号,每字节传输后必须等待应答。
  4. AT24C02是2Kbit EEPROM,256字节存储,每页8字节,页写不能跨页,每次写入后必须加5ms延时等待内部写入完成,支持掉电永久存储。
  5. MPU6050默认7位地址0x68,AD0接高电平为0x69,内置3轴加速度、3轴陀螺仪、温度传感器,初始化时必须写入0x00到PWR_MGMT_1寄存器,唤醒芯片退出睡眠模式。
  6. STM32硬件I2C必须处理总线死锁问题,通过模拟SCL时钟脉冲释放SDA总线,配合软件复位,避免程序卡死。
  7. HAL库I2C核心读写函数:HAL_I2C_Mem_Write()写从机寄存器,HAL_I2C_Mem_Read()读从机寄存器,适配绝大多数带寄存器的I2C从机设备。
  8. 多从机挂载同一条总线,必须保证每个从机的7位地址唯一,共用一组上拉电阻,所有设备必须共地,避免地址冲突与电平异常。
  9. I2C无ACK应答的核心排查顺序:接线与共地→上拉电阻→从机地址→外设供电→时钟速率配置。

八、本章小结

本章我们深入拆解了I2C通信协议的底层时序与主从机通信机制,对比了51单片机软件模拟与STM32硬件I2C的核心差异,掌握了硬件I2C的寄存器级原理与HAL库封装逻辑,完成了AT24C02 EEPROM掉电存储、MPU6050六轴传感器数据采集两大工业级实战,解决了总线死锁、无ACK、数据错乱等高频问题。I2C是低速外设的首选通信总线,下一章我们将学习SPI串行外设协议,掌握高速同步串行通信的底层原理与W25Qxx Flash芯片的驱动开发,实现大容量数据的高速掉电存储。


第12章 I2C通信协议全解:底层时序、主从机通信与AT24C02、MPU6050传感器实战》 是转载文章,点击查看原文


相关推荐


【Kotlin】 数据流完全指南:冷流、热流与 Android 实战
idealzouhu2026/3/21

文章目录 一、数据流简介1.1 Kotlin 数据流概述1.2 核心特性1.3 Flow 的基本组件 二、数据流的使用方法2.1 正向流2.1.1 创建数据流2.1.2 修改数据流2.1.3 收集数据流 2.2 反向流2.2.1 创建数据流2.2.2 修改并收集数据流 2.3 数据流在 Jetpack 中的应用场景 三、数据流的行为模式3.1 冷流(Cold Flow)3.1.1 什么是冷流3.1.2 为什么 Kotlin 默认使用冷流 3.2 热流(Hot


特高压输变电工程全生命周期BIM+GIS数字化管理平台:重塑能源动脉的数字基石(WORD)
无忧智库2026/3/13

引言:能源互联网时代的数字化觉醒 在“双碳”目标的宏大叙事下,中国能源结构正经历着前所未有的深刻变革。作为能源配置的“大动脉”,特高压输变电工程以其输送容量大、距离远、损耗低的技术优势,成为了构建新型电力系统的核心骨架。然而,随着工程规模的指数级增长和地理环境的日益复杂,传统的管理模式正面临着严峻的挑战。设计阶段的各专业协同困难、施工阶段的进度与质量管控盲区、运维阶段的海量数据孤岛,如同一个个隐形的枷锁,制约着电网建设效率的提升和全生命周期价值的释放。 当物理世界的铁塔银线不断向天际延伸时,数字


windows下配置Qt arm32交叉编译环境
itas1092026/3/5

windows下配置Qt arm32交叉编译环境 环境: 系统:windows 11 Qt: 5.12.12(2021-11-25) 本地编译器:mingw73 64(2018-04-25) 交叉编译器: gcc-linaro-7.3.1-2018.05-i686-mingw32_arm-linux-gnueabi perl: strawberry perl 5.22.1(2016-01-07) python: 3.8.10(2021-05-03) 1. 安装Qt MinGW Perl Pyt


告别死板流程:OpenSpec OPSX 如何重塑 SDD 开发工作流
fundroid2026/2/25

引言:SDD 与 OpenSpec 规范驱动开发(SDD)是什么? 近两年,AI 编码助手已经能“听懂人话”,从一段自然语言描述里生成大段代码。但很多团队也发现:如果需求只是散落在聊天记录里、脑补在每个人的心里,AI 很容易“发挥过度”——代码写出来了,却不是你真正想要的系统行为。 规范驱动开发(Spec-Driven Development,SDD)试图解决的,就是这个问题。它把规范(spec)而不是代码当成系统的“单一事实来源”:先用结构化、机器可读的方式,把系统应该做什么、有哪些边界和不变


WebMCP 时代:在浏览器中释放 AI 的工作能力
CharlesYu012026/2/16

随着 AI Agent 的广泛应用,传统的 Web 自动化与 Web 交互模式正在迎来根本性变化。WebMCP 是一个未来派的技术提案,它不仅改变了 AI 访问 Web 的方式,还为 AI 与前端应用之间建立起了 协议级的交互通道。本文从WebMCP架构分层解析这项技术及其工程意义。 面对 GEO 与 Agent 应用逐步弱化浏览器入口价值的趋势,浏览器厂商必须主动跟进,通过技术升级与生态重构来守住自身核心阵地。 一、WebMCP 是什么? WebMCP(Web Model Context P


MCP (Model Context Protocol) 技术理解 - 第二篇
想用offer打牌2026/2/8

引言 我们第一篇讲了MCP的基础概念、MCP解决的问题以及MCP的架构,我相信大家已经对MCP有了一定的了解,那么接下来让我们深入MCP具体是如何实现的,这一篇我们的重点放在通信协议和数据传输上,让我们一起来看看吧 如果你对前面的内容感兴趣,可以点击这里跳转 MCP (Model Context Protocol) 技术理解 - 第一篇 MCP的层级 MCP由两层组成: 数据层:定义了基于 JSON-RPC 的客户端-服务器通信协议,包括生命周期管理和核心原语,如工具、资源、提示和通知。 传输


type-challenges(ts类型体操): 11 - 元组转换为对象
fxss2026/1/30

11 - 元组转换为对象 by sinoon (@sinoon) #简单 #object-keys 题目 将一个元组类型转换为对象类型,这个对象类型的键/值和元组中的元素对应。 例如: const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const type result = TupleToObject<typeof tuple> // expected { 'tesla': 'tesla', 'model 3': 'mode


Flutter艺术探索-Flutter国际化:多语言支持实现
kirk_wang2026/1/20

Flutter 国际化:从原理到实践的多语言支持方案 引言:为什么你的 Flutter 应用需要国际化? 如今,开发一款成功的应用就不得不考虑全球市场。国际化(i18n)和本地化(l10n)不再是可选项,而是连接不同文化用户的桥梁。对于使用 Flutter 的开发者来说,框架本身提供了强大的国际化支持,这不仅能显著提升用户体验,更是扩大应用市场份额的关键一步。想想看,当你的应用能够用用户的母语与其沟通时,下载量和用户留存率的提升是显而易见的。 Flutter 的国际化体系基于 Dart 的 in


mongodb的基本命令
豆浆粉牛奶2026/1/12

大家好我是小帅,今天学习mongodb的简单认识和基本命令。 本章内容: 理解MongoDB的业务场景、熟悉MongoDB的简介、特点和体系结构、数据类型等。能够在Windows和Linux下安装和启动MongoDB、图形化管理界面Compass的安装使用掌握MongoDB基本常用命令实现数据的CRUD 掌握MongoDB的索引类型、索引管理、执行计划。使用Spring DataMongoDB完成文章评论业务的开发 文章目录 1. MongoDB认识1.1 业务场景1.2 结构体系


AI 有你想不到,也它有做不到 | 2025 年深度使用 Cursor/Trae/CodeX 所得十条经验
Piper蛋窝2026/1/4

去年的今天,我还在奋笔疾书地写着 VS Code + Roo Cline 的评测心得:个人评测 | Cursor 免费平替:Roo Cline + DeepSeek-v3/Gemini-2.0 + RepoPrompt AI 辅助编程 。当时的我没有想过:在 2025 年, Roo Cline 会被我迅速淘汰,我也成为了 Cursor 这类 Vibe Coding 工具的稳定用户之一。 站在 2026 年伊始的节点上,审视自己的工作流,发现已经完全被锚定在了如下工具链上: 对话工具: Chat

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客