【STM32】 从HAL库源码深度解析I2C(源码解析+面试题)
从HAL库源码深度解析I2C
1.初始化相关
1.1 MX_I2C1_Init —— 初始化入口源码
static void MX_I2C1_Init(void) { hi2c1.Instance = I2C2; // 选择 I2C 寄存器基地址:I2C2 hi2c1.Init.ClockSpeed = 400000; // 400kHz,快速模式 Fast Mode hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; // 快速模式下高低电平占空比 2:1 hi2c1.Init.OwnAddress1 = 0; // 本机从机地址(Master 用不到,设 0) hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; // 7 位地址模式 hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; // 禁用双地址 hi2c1.Init.OwnAddress2 = 0; // 第二个地址(已禁用,无效) hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; // 禁用广播呼叫 hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; // 允许从机时钟拉伸 if (HAL_I2C_Init(&hi2c1) != HAL_OK) { Error_Handler(); } }要点:
这些配置项就是 CubeMX 图形界面上勾选的参数。
CubeMX 先把配置填进结构体
hi2c1.Init(数据在内存),再由HAL_I2C_Init把结构体数据"搬运" 到硬件寄存器中。
Init 成员 配置值 含义 ClockSpeed 400000 400 kHz 快速模式(AT24C02 手册:VCC≥2.7V 时最高 400 kHz) DutyCycle I2C_DUTYCYCLE_2 SCL 高低电平占空比 2:1(低速模式无占空比概念) OwnAddress1 0 本机作为从机时的地址 AddressingMode 7BIT 7 位地址模式 DualAddressMode DISABLE 不使用双地址 GeneralCallMode DISABLE 不响应广播呼叫(地址 0x00) NoStretchMode DISABLE 允许从机时钟拉伸
1.2 I2C_HandleTypeDef 与 I2C_TypeDef —— 句柄与寄存器
关键代码:
hi2c1.Instance = I2C2;I2C2 是什么 —— 指向寄存器基地址的指针宏:
#define I2C2 ((I2C_TypeDef *)I2C2_BASE) #define I2C2_BASE (APB1PERIPH_BASE + 0x00005800UL) // 0x4000 5800 #define APB1PERIPH_BASE PERIPH_BASE #define PERIPH_BASE 0x40000000ULI2C_TypeDef —— I2C 外设内部连续排列的 9 个寄存器:
typedef struct { __IO uint32_t CR1; // 控制寄存器 1 __IO uint32_t CR2; // 控制寄存器 2 __IO uint32_t OAR1; // 自身地址寄存器 1 __IO uint32_t OAR2; // 自身地址寄存器 2 __IO uint32_t DR; // 数据寄存器 __IO uint32_t SR1; // 状态寄存器 1 __IO uint32_t SR2; // 状态寄存器 2 __IO uint32_t CCR; // 时钟控制寄存器 __IO uint32_t TRISE; // 上升时间寄存器 } I2C_TypeDef;Instance 成员 —— I2C 句柄结构体中的关键字段:
typedef struct { I2C_TypeDef *Instance; /*!< I2C 寄存器基地址 */ I2C_InitTypeDef Init; /*!< I2C 通信参数(用户配置)*/ uint8_t *pBuffPtr; /*!< 指向 I2C 传输缓冲区 */ uint16_t XferSize; /*!< I2C 传输大小 */ __IO uint16_t XferCount; /*!< I2C 传输计数器 */ __IO uint32_t XferOptions; /*!< I2C 传输选项 */ __IO uint32_t PreviousState; /*!< 上一次状态与模式 */ DMA_HandleTypeDef *hdmatx; /*!< I2C Tx DMA 句柄 */ DMA_HandleTypeDef *hdmarx; /*!< I2C Rx DMA 句柄 */ HAL_LockTypeDef Lock; /*!< I2C 锁对象 */ __IO HAL_I2C_StateTypeDef State; /*!< I2C 通信状态 */ __IO HAL_I2C_ModeTypeDef Mode; /*!< I2C 通信模式 */ __IO uint32_t ErrorCode; /*!< I2C 错误码 */ __IO uint32_t Devaddress; /*!< I2C 目标设备地址 */ __IO uint32_t Memaddress; /*!< I2C 目标内存地址 */ __IO uint32_t MemaddSize; /*!< I2C 目标内存地址宽度 */ __IO uint32_t EventCount; /*!< I2C 事件计数 */ ... } I2C_HandleTypeDef;结论(核心):
hi2c1.Instance = I2C2:让Instance指针直接指向片上外设 I2C2,基地址 0x4000 5800;- 从该地址开始是 I2C 连续 9 个寄存器(每个 32 位),寄存器内存顺序与
I2C_TypeDef成员顺序完全一致,自然形成偏移量(CR1=0x00,CR2=0x04,…,TRISE=0x20);- 于是 Cortex-M3 可以用操作内存的方式(指针)直接操作 I2C 全部寄存器,如
hi2c->Instance->DR = data;等价于向 0x4000 5800+0x10 写数据。
1.3 I2C 复用 GPIO —— HAL_I2C_MspInit
void HAL_I2C_MspInit(I2C_HandleTypeDef* hi2c) { GPIO_InitTypeDef GPIO_InitStruct = {0}; if(hi2c->Instance==I2C2) { __HAL_RCC_GPIOB_CLK_ENABLE(); /**I2C2 GPIO Configuration PB10 ------> I2C2_SCL PB11 ------> I2C2_SDA */ GPIO_InitStruct.Pin = GPIO_PIN_10|GPIO_PIN_11; GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; // 复用开漏输出 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); /* Peripheral clock enable */ __HAL_RCC_I2C2_CLK_ENABLE(); } }MSP = MCU Support Package(微控制器支持包):特定芯片硬件相关的支持配置。
同样是 I2C 协议,STM32F1 和 F4 逻辑一样,但引脚(GPIO)、时钟(RCC)、中断号(IRQ)完全不同。
概念 含义 Alternate Function(复用功能) 引脚不再作为普通 GPIO,交给片上外设(I2C/SPI/USART)控制 Alternate Function Open Drain(复用开漏) 引脚只能拉低、靠外部上拉电阻拉高——正是 I2C"线与"机制需要的
1.4 开启 I2C2 时钟 —— __HAL_RCC_I2C2_CLK_ENABLE
#define __HAL_RCC_I2C2_CLK_ENABLE() do { \ __IO uint32_t tmpreg; \ SET_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C2EN); \ tmpreg = READ_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C2EN); \ UNUSED(tmpreg); \ } while(0U) #define RCC_APB1ENR_I2C2EN_Pos (22U) #define RCC_APB1ENR_I2C2EN_Msk (0x1UL << RCC_APB1ENR_I2C2EN_Pos) /*!< 0x00400000 */ #define RCC_APB1ENR_I2C2EN RCC_APB1ENR_I2C2EN_Msk解析:
I2C2 挂在 APB1 总线上,操作
RCC->APB1ENR的第 22 位(0x00400000)。置 1 后 I2C2 接入时钟树,才能响应后续寄存器配置。
1.5 I2C 设备使能 —— __HAL_I2C_ENABLE / DISABLE
#define __HAL_I2C_ENABLE(__HANDLE__) SET_BIT((__HANDLE__)->Instance->CR1, I2C_CR1_PE) #define __HAL_I2C_DISABLE(__HANDLE__) CLEAR_BIT((__HANDLE__)->Instance->CR1, I2C_CR1_PE) #define SET_BIT(REG, BIT) ((REG) |= (BIT)) #define CLEAR_BIT(REG, BIT) ((REG) &= ~(BIT)) #define I2C_CR1_PE_Pos (0U) #define I2C_CR1_PE_Msk (0x1UL << I2C_CR1_PE_Pos) /*!< 0x00000001 */ #define I2C_CR1_PE I2C_CR1_PE_Msk /*!< Peripheral Enable */开启时钟 vs 设备使能(必须分清):
操作 作用 类比 开启时钟(RCC) 外设接入时钟树,可以配置了 给洗衣机插上电源 设备使能(PE) 外设本身开始工作,可以正常使用 按下"开始"键 流程:开启时钟 → 配置寄存器 → 设备使能 → 正常工作
1.6 MODIFY_REG —— 修改寄存器指定位的万能宏
#define MODIFY_REG(REG, CLEARMASK, SETMASK) WRITE_REG((REG), (((READ_REG(REG)) & (~(CLEARMASK))) | (SETMASK)))核心作用:
在不改变寄存器其他位的情况下,安全地修改指定某几位(含 1 位)的值。
执行五步拆解:
步骤 表达式 做什么 ① READ_REG(REG) 读出寄存器当前 32 位原始值 ② ~(CLEARMASK) 清除掩码取反:保留位置 1、清除位置 0 ③ REG & ~(CLEARMASK) 把指定位置清零,其他位不变 ④ | SETMASK 给清空位置或上新值 ⑤ WRITE_REG(REG, ...) 写回寄存器 本质:"先清空、再设置"。
Demo(8 位寄存器改 LED 模式):
假设第 [5:4] 位控制 LED(00关/01低速/10高速/11常亮),当前
REG = 1011 0100(=11 常亮),要改成 10(高速闪)且不影响其他位:REG 1011 0100 ~CLEARMASK & 1100 1111 (CLEARMASK = 0011 0000) ---------------------- 1000 0100 <- 第[5:4]位清空,其他位未变 SETMASK | 0010 0000 ---------------------- 1010 0100 <- 完成:10 10 0100若不先清空直接
|:1011 0100 | 0010 0000 = 1011 0100,结果不变——这就是必须先清空的原因。实战案例:配置 CR1 的 ENGC(bit6)与 NOSTRETCH(bit7):
MODIFY_REG(hi2c->Instance->CR1, (I2C_CR1_ENGC | I2C_CR1_NOSTRETCH), (hi2c->Init.GeneralCallMode | hi2c->Init.NoStretchMode));
- CLEARMASK =
(1<<6)|(1<<7):先把这两个位清空;- 之前配置 GeneralCallMode=0、NoStretchMode=0,即 SETMASK=0 → 等效
MODIFY_REG(CR1, (ENGC|NOSTRETCH), 0),把两位清成 0(关闭广播、允许时钟拉伸);- 想开启直接传
I2C_CR1_ENGC | I2C_CR1_NOSTRETCH作 SETMASK。结论:
I2C 各标志位在寄存器中位置彼此独立、不重复,可以穷举所有位位置充当 CLEARMASK/SETMASK——这就是
stm32f103xe.h里I2C_xxx_Pos/_Msk宏清单的由来。用 MODIFY_REG,HAL 库把结构体选项(软件层)设置进寄存器(硬件层)。
2. 设备就绪相关
应用层:
bool wait_write_complete(void) { if(!at24c02.is_init) return false; return HAL_I2C_IsDeviceReady(at24c02.i2c, at24c02.dev_address, 100, 10) == HAL_OK ? true : false; }2.1 HAL_I2C_IsDeviceReady —— 参数
参数 含义 hi2c I2C 句柄结构体指针 DevAddress 目标设备地址;7 位地址调用前必须左移 1 位(最低位留给读写标志) Trials 尝试次数,建议 1~10(上例 100) Timeout 单次检测超时时间(毫秒),0 无限制(上例 10ms) 源码宏观循环:
HAL_StatusTypeDef HAL_I2C_IsDeviceReady(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint32_t Trials, uint32_t Timeout) { uint32_t I2C_Trials = 0U; if (hi2c->State == HAL_I2C_STATE_READY) { do { SET_BIT(hi2c->Instance->CR1, I2C_CR1_START); // 发 START hi2c->Instance->DR = I2C_7BIT_ADD_WRITE(DevAddress);// 发从机地址(写) hi2c->State = HAL_I2C_STATE_READY; if (__HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_ADDR) == SET) { ... return HAL_OK; // 有一次成功就 OK } else { if (I2C_WaitOnFlagUntilTimeout(hi2c, I2C_FLAG_BUSY, SET, I2C_TIMEOUT_BUSY_FLAG, tickstart) != HAL_OK) return HAL_ERROR; } I2C_Trials++; } while (I2C_Trials < Trials); ... return HAL_ERROR; } else { return HAL_BUSY; } }
2.2 三种就绪条件 —— 核心
必须同时满足:
# 条件 含义 1 设备本身空闲且正常 没有被任何程序(含中断)占用访问 2 总线就绪(不忙) SCL/SDA 上无正在进行的传输 3 目标设备在线且地址匹配 Slave 在线且能正确应答 ACK 类比:
① 女朋友愿意去(和闺蜜约了图书馆=忙);
② 道路畅通(堵车去不了);
③ 电影院营业且导航正确。
2.3 条件1:hi2c->State
if (hi2c->State == HAL_I2C_STATE_READY)为什么需要 State?
因为有中断——你用着 I2C 时中断函数也可能访问 I2C,导致重复访问。
用 State 做"谁先设置谁先用,其他人被拦截"的互斥。
typedef enum { HAL_I2C_STATE_RESET = 0x00U, /*!< 外设尚未初始化 */ HAL_I2C_STATE_READY = 0x20U, /*!< 外设已初始化,可使用 */ HAL_I2C_STATE_BUSY = 0x24U, /*!< 内部过程进行中 */ HAL_I2C_STATE_BUSY_TX = 0x21U, /*!< 数据发送过程中 */ HAL_I2C_STATE_BUSY_RX = 0x22U, /*!< 数据接收过程中 */ HAL_I2C_STATE_LISTEN = 0x28U, /*!< 地址监听模式中 */ HAL_I2C_STATE_BUSY_TX_LISTEN = 0x29U, /*!< 监听 + 发送中 */ HAL_I2C_STATE_BUSY_RX_LISTEN = 0x2AU, /*!< 监听 + 接收中 */ HAL_I2C_STATE_ABORT = 0x60U, /*!< 中止用户请求中 */ HAL_I2C_STATE_TIMEOUT = 0xA0U, /*!< 超时状态 */ HAL_I2C_STATE_ERROR = 0xE0U /*!< 错误 */ } HAL_I2C_StateTypeDef;不用背,源码直接能翻。
若 State != READY,直接 return HAL_BUSY。
2.4 条件2:总线是否就绪 —— BUSY 标志位
总线为什么忙?
对方来不及接收时可能利用"线与"机制拉低 SCL(时钟拉伸),不让发送方继续发——此时总线 busy。要发数据,总线必须 OK。
if (I2C_WaitOnFlagUntilTimeout(hi2c, I2C_FLAG_BUSY, SET, I2C_TIMEOUT_BUSY_FLAG, tickstart) != HAL_OK) return HAL_BUSY;
等待函数本体:
static HAL_StatusTypeDef I2C_WaitOnFlagUntilTimeout(I2C_HandleTypeDef *hi2c, uint32_t Flag, FlagStatus Status, uint32_t Timeout, uint32_t Tickstart) { while (__HAL_I2C_GET_FLAG(hi2c, Flag) == Status) // 标志位一直是该状态就一直等 { if (Timeout != HAL_MAX_DELAY) { if (((HAL_GetTick() - Tickstart) > Timeout) || (Timeout == 0U)) { if ((__HAL_I2C_GET_FLAG(hi2c, Flag) == Status)) { hi2c->State = HAL_I2C_STATE_READY; hi2c->ErrorCode |= HAL_I2C_ERROR_TIMEOUT; __HAL_UNLOCK(hi2c); return HAL_ERROR; } } } } return HAL_OK; }✨ 阻塞式接口基本都有超时时间,主要防止卡死。
标志位清单(注意设计规律):
#define I2C_FLAG_OVR 0x00010800U // SR1 溢出 #define I2C_FLAG_AF 0x00010400U // SR1 应答失败 #define I2C_FLAG_ARLO 0x00010200U // SR1 仲裁丢失 #define I2C_FLAG_BERR 0x00010100U // SR1 总线错误 #define I2C_FLAG_TXE 0x00010080U // SR1 数据寄存器空 #define I2C_FLAG_RXNE 0x00010040U // SR1 数据寄存器非空 #define I2C_FLAG_STOPF 0x00010010U // SR1 停止条件 #define I2C_FLAG_ADD10 0x00010008U // SR1 10 位地址 #define I2C_FLAG_BTF 0x00010004U // SR1 字节发送结束 #define I2C_FLAG_ADDR 0x00010002U // SR1 地址发送结束 #define I2C_FLAG_SB 0x00010001U // SR1 起始条件已发送 #define I2C_FLAG_DUALF 0x00100080U // SR2 双地址标志 #define I2C_FLAG_GENCALL 0x00100010U // SR2 广播呼叫 #define I2C_FLAG_TRA 0x00100004U // SR2 传输方向 #define I2C_FLAG_BUSY 0x00100002U // SR2 总线忙 #define I2C_FLAG_MSL 0x00100001U // SR2 主从模式
🎯 重点:__HAL_I2C_GET_FLAG —— "地址+偏移"封装:
#define __HAL_I2C_GET_FLAG(__HANDLE__, __FLAG__) ((((uint8_t)((__FLAG__) >> 16U)) == 0x01U) ? \ (((((__HANDLE__)->Instance->SR1) & ((__FLAG__) & I2C_FLAG_MASK)) == ((__FLAG__) & I2C_FLAG_MASK)) ? SET : RESET) : \ (((((__HANDLE__)->Instance->SR2) & ((__FLAG__) & I2C_FLAG_MASK)) == ((__FLAG__) & I2C_FLAG_MASK)) ? SET : RESET)) #define I2C_FLAG_MASK 0x0000FFFFU设计思想:
每个标志位宏是 32 位数——高 16 位选寄存器(0x0001→SR1,0x0010→SR2);
低 16 位定位标志位位置。
以
I2C_FLAG_BUSY = 0x00100002U展开:
步骤 运算 结果 1 (uint8_t)(0x00100002 >> 16)0x10 ≠ 0x01→ 选中 SR22 0x00100002 & 0x0000FFFF0x00000002(低 16 位=2)3 ((SR2 & 0x2) == 0x2) ? SET : RESET检测 SR2 第 2 个比特位 4 翻参考手册 SR2.bit1 = BUSY:总线忙 为什么"自己比自己"(全比较):
寄存器的标志位可能多位连起来表示,只有全部匹配才成立。
类比取款机:
if (动作 & 规定)——只输密码(0x01)没插卡(0x02),0x01 & 0x03 = 0x01(真)→ 钱被吐出(错误);
if ((动作 & 规定) == 规定)——0x01 ≠ 0x03→ 必须密码+卡都有才放行(正确)。
2.5 条件3:目标设备在线 —— 六步探测
本质:
在 SDA 上发送一个地址探测数据,携带地址让对方应答,收到 ACK 说明在线。
第1步:发 START
SET_BIT(hi2c->Instance->CR1, I2C_CR1_START); // CR1 bit8,0x100置 START 位后,硬件在 SCL 高电平期间让 SDA 产生从高到低跳变,表示开始。
第2步:等 SB(起始完成)
if (I2C_WaitOnFlagUntilTimeout(hi2c, I2C_FLAG_SB, RESET, Timeout, tickstart) != HAL_OK) { if (READ_BIT(hi2c->Instance->CR1, I2C_CR1_START) == I2C_CR1_START) hi2c->ErrorCode = HAL_I2C_WRONG_START; return HAL_TIMEOUT; }
I2C_FLAG_SB = 0x00010001U→ SR1 最低位 bit0:起始条件已发送。
第3步:发从机地址(写方向)
hi2c->Instance->DR = I2C_7BIT_ADD_WRITE(DevAddress); #define I2C_7BIT_ADD_WRITE(__ADDRESS__) ((uint8_t)((__ADDRESS__) & (uint8_t)(~I2C_OAR1_ADD0)))只要把数据写入 DR,I2C 接口自动发送(单位是字节)。
该宏把 DevAddress 最低位清 0(LSB=0 表示写)。
✨ 所以调用时 DevAddress 必须先左移 1 位。
不同平台/工具逻辑可能不同,养成读源码的习惯,以不变应万变。
第4步:等 ADDR / 查 AF(地址发送可靠性)
tmp1 = __HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_ADDR); // 地址发送是否结束 tmp2 = __HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_AF); // 有无应答失败 while ((hi2c->State != HAL_I2C_STATE_TIMEOUT) && (tmp1 == RESET) && (tmp2 == RESET)) { if (((HAL_GetTick() - tickstart) > Timeout) || (Timeout == 0U)) hi2c->State = HAL_I2C_STATE_TIMEOUT; tmp1 = __HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_ADDR); tmp2 = __HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_AF); } #define I2C_FLAG_ADDR 0x00010002U // SR1 bit2 #define I2C_FLAG_AF 0x00010400U // SR1 bit11循环继续的条件:
没有超时 + ADDR 标志没置位 + AF 应答失败标志也没置位
- I2C_FLAG_ADDR (SR1 bit2) 主机发送从机地址 + 读写位之后:
- 从机应答 ACK → 硬件置
ADDR=1(SET),代表地址发送完成- 还没收到 ACK →
ADDR=0(RESET)- I2C_FLAG_AF (SR1 bit11) Acknowledge Failure 应答失败
- 从机无应答 NACK → AF 硬件置 1 (SET)
- 正常收到 ACK → AF=0 (RESET
结束条件三种:
异常1 超时;
异常2 应答失败(AF);
正常:ADDR=1(Master 收到 ACK 后硬件置 ADDR,它只是对地址收到 ACK 的标识)。
第5步:判断
if (__HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_ADDR) == SET) { ...; return HAL_OK; }正常情况只有一种,看 ADDR 是否置位即可。
第6步:发 STOP 收尾
SET_BIT(hi2c->Instance->CR1, I2C_CR1_STOP); // CR1 bit9,0x200 __HAL_I2C_CLEAR_ADDRFLAG(hi2c); if (I2C_WaitOnFlagUntilTimeout(hi2c, I2C_FLAG_BUSY, SET, I2C_TIMEOUT_BUSY_FLAG, tickstart) != HAL_OK) return HAL_ERROR; hi2c->State = HAL_I2C_STATE_READY; __HAL_UNLOCK(hi2c); return HAL_OK; // 终于成功,设备 ready!写 STOP 位后,Master 自动在 SCL 高电平期间让 SDA 产生从低到高跳变,表示停止。
✨ STOP 没有"完成标志位",也不需要 Master 确认 Slave 收到——发出即完成。
3. 写相关:HAL_I2C_Mem_Write
3.1 完整源码(带逐段注释)
HAL_StatusTypeDef 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) { uint32_t tickstart = HAL_GetTick(); // 用于超时计数 assert_param(IS_I2C_MEMADD_SIZE(MemAddSize)); if (hi2c->State == HAL_I2C_STATE_READY) // 设备必须 ready 才能发 { if (I2C_WaitOnFlagUntilTimeout(hi2c, I2C_FLAG_BUSY, SET, I2C_TIMEOUT_BUSY_FLAG, tickstart) != HAL_OK) return HAL_BUSY; // 总线忙超时 __HAL_LOCK(hi2c); // 加锁,防止其他任务/中断再进来 if ((hi2c->Instance->CR1 & I2C_CR1_PE) != I2C_CR1_PE) __HAL_I2C_ENABLE(hi2c); // 鲁棒性:确保 I2C 已使能 CLEAR_BIT(hi2c->Instance->CR1, I2C_CR1_POS); // 写不用 POS,清零 hi2c->State = HAL_I2C_STATE_BUSY_TX; // 状态置"发送中" hi2c->Mode = HAL_I2C_MODE_MEM; // 模式 MEM:第一个数据带内存地址 hi2c->ErrorCode = HAL_I2C_ERROR_NONE; hi2c->pBuffPtr = pData; // 待发送数据起始地址 hi2c->XferCount = Size; hi2c->XferSize = hi2c->XferCount; // 还剩多少字节 hi2c->XferOptions = I2C_NO_OPTION_FRAME; // ★ 核心:START + 设备地址 + 字地址 if (I2C_RequestMemoryWrite(hi2c, DevAddress, MemAddress, MemAddSize, Timeout, tickstart) != HAL_OK) return HAL_ERROR; while (hi2c->XferSize > 0U) // 发送实际数据 { if (I2C_WaitOnTXEFlagUntilTimeout(hi2c, Timeout, tickstart) != HAL_OK) // 等 DR 空 { if (hi2c->ErrorCode == HAL_I2C_ERROR_AF) SET_BIT(hi2c->Instance->CR1, I2C_CR1_STOP); // 应答失败 → 发 STOP 收尾 return HAL_ERROR; } hi2c->Instance->DR = *hi2c->pBuffPtr; // 写一个字节 hi2c->pBuffPtr++; hi2c->XferSize--; hi2c->XferCount--; // BTF:DR 和移位寄存器都空 → 立即补下一个(双缓冲提速) if ((__HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_BTF) == SET) && (hi2c->XferSize != 0U)) { hi2c->Instance->DR = *hi2c->pBuffPtr; hi2c->pBuffPtr++; hi2c->XferSize--; hi2c->XferCount--; } } /* Wait until BTF flag is set */ // 循环结束只是"用户数据全部写进 DR/移位寄存器",不一定被对方接受了! // CPU 执行和 I2C 发送是异步的,必须等 BTF:所有数据真正发出并被对方收到 if (I2C_WaitOnBTFFlagUntilTimeout(hi2c, Timeout, tickstart) != HAL_OK) { if (hi2c->ErrorCode == HAL_I2C_ERROR_AF) SET_BIT(hi2c->Instance->CR1, I2C_CR1_STOP); return HAL_ERROR; } SET_BIT(hi2c->Instance->CR1, I2C_CR1_STOP); // 发停止位 hi2c->State = HAL_I2C_STATE_READY; hi2c->Mode = HAL_I2C_MODE_NONE; __HAL_UNLOCK(hi2c); return HAL_OK; } else return HAL_BUSY; }调用样例:
HAL_StatusTypeDef state = HAL_I2C_Mem_Write( at24c02.i2c, at24c02.dev_address, at24c02.mem_address, at24c02.mem_address_size, data, len, HAL_MAX_DELAY); bool dev_init(I2C_HandleTypeDef *i2c) { if(!i2c) return false; at24c02.dev_address = (DEV_ADDRESS<<1); // 7位地址左移1位 at24c02.mem_address = MEM_ADDRESS; at24c02.mem_address_size = I2C_MEMADD_SIZE_8BIT; at24c02.page_size = PAGE_SIZE; at24c02.i2c = i2c; at24c02.is_init = true; return true; }
3.2 细节1:工作模式(Mode)
STM32F103 没有直接的 Master/Slave 配置项,
主从由调用哪类函数决定——谁发 START 谁是 Master。
typedef enum { HAL_I2C_MODE_NONE = 0x00U, /*!< 空闲,无任何通信 */ HAL_I2C_MODE_MASTER = 0x10U, /*!< Master_Transmit/Receive:地址+原始数据流,机械收发字节 */ HAL_I2C_MODE_SLAVE = 0x20U, /*!< Slave_Transmit/Receive:被动等主机呼叫,硬件不主动产生 START/STOP */ HAL_I2C_MODE_MEM = 0x40U /*!< Mem_Read/Write:地址+内存地址+数据,是 MASTER 的特化, 内部自动处理"先写内存偏移地址,再读写数据"的复合时序 */ } HAL_I2C_ModeTypeDef;这就是写 EEPROM 不用
HAL_I2C_Master_Transmit的原因:它只发"设备地址+数据流",没有提前选中 AT24C02 内部内存地址的过程。
3.3 核心函数:I2C_RequestMemoryWrite
static HAL_StatusTypeDef I2C_RequestMemoryWrite(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint32_t Timeout, uint32_t Tickstart) { SET_BIT(hi2c->Instance->CR1, I2C_CR1_START); // ① 发开始 if (I2C_WaitOnFlagUntilTimeout(hi2c, I2C_FLAG_SB, RESET, Timeout, Tickstart) != HAL_OK) return HAL_TIMEOUT; // ② 等开始完成(SB) hi2c->Instance->DR = I2C_7BIT_ADD_WRITE(DevAddress);// ③ 发设备地址(写方向) if (I2C_WaitOnMasterAddressFlagUntilTimeout(hi2c, I2C_FLAG_ADDR, Timeout, Tickstart) != HAL_OK) return HAL_ERROR; // ④ 等地址发送完毕(ADDR) __HAL_I2C_CLEAR_ADDRFLAG(hi2c); // ⑤ 清 ADDR if (I2C_WaitOnTXEFlagUntilTimeout(hi2c, Timeout, Tickstart) != HAL_OK) return HAL_ERROR; // ⑥ 等 DR 空(TXE) if (MemAddSize == I2C_MEMADD_SIZE_8BIT) // 8位字地址 { hi2c->Instance->DR = I2C_MEM_ADD_LSB(MemAddress);// ⑦a 直接发低字节 } else // 16位字地址 { hi2c->Instance->DR = I2C_MEM_ADD_MSB(MemAddress);// ⑦b 先高字节 if (I2C_WaitOnTXEFlagUntilTimeout(hi2c, Timeout, Tickstart) != HAL_OK) { ... } hi2c->Instance->DR = I2C_MEM_ADD_LSB(MemAddress);// 再低字节 } return HAL_OK; // 这里没检测 DR 空表示发送完成——在后续代码中检测 }
#define I2C_MEMADD_SIZE_8BIT 0x00000001U/I2C_MEMADD_SIZE_16BIT 0x00000010U。AT24C02 字地址 8 位足够(256 字节)。
3.4 细节2:TXE 与 BTF(发送侧核心)
// I2C_WaitOnTXEFlagUntilTimeout:等 TXE while (__HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_TXE) == RESET) { if (I2C_IsAcknowledgeFailed(hi2c) != HAL_OK) // 检测 NACK(发送完下一时钟周期 SDA 为高=没 ACK) return HAL_ERROR; ...超时处理... } #define I2C_FLAG_TXE 0x00010080U // SR1 第8位:DR 空✨ 结论:
I2C_WaitOnTXEFlagUntilTimeout返回 OK,说明 DR 的数据已移入移位寄存器、DR 空了——但不一定发出去了哦!// I2C_WaitOnBTFFlagUntilTimeout:等 BTF while (__HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_BTF) == RESET) { if (I2C_IsAcknowledgeFailed(hi2c) != HAL_OK) // 发送数据收到的 ACK 是软件处理的 return HAL_ERROR; ...超时处理... } #define I2C_FLAG_BTF 0x00010004U // SR1 第3位✨ TXE vs BTF(发送角度):
标志位 含义 说明 TXE DR 空 数据从 DR 移到移位寄存器(还没发出去) BTF DR 与移位寄存器都空 该字节真正被发送出去了 这就是
HAL_I2C_Mem_Write尾部必须再等一次 BTF 的原因:确认所有字节都从移位寄存器发出并被对方收到,才能发 STOP。
4 读相关:HAL_I2C_Mem_Read
4.1 读为什么麻烦:最后的 NACK
为什么第 m+1 步主机发 NACK 而不是 ACK?
之前 Master 写、Slave 读时 Slave 一直在发 ACK;
到读取方向,Master 收完最后一个字节后要回 NACK + STOP,通知从机"不要再发了"。
抛出结论:
- 若不做任何提前配置,收到最后一个字节后 Master 会主动发 ACK——这不是我们想要的;
- 我们要的是:收到最后一个字节后,Master 回复 NACK,然后发送 STOP;
- 这个工作由软件提前配置完成;
- 为了让 Master 正确产生 NACK 和 STOP,软件必须在收到倒数第 2 个字节时立即提前配置(寄存器配置生效有延后性)。
坑(源码演进史):
这段代码个人猜测是被意法工程师 debug 出来的——把读取分成 4 种情况:读 1 字节、读 2 字节、读 3 字节、读 N 字节。客户踩了不少坑反馈后,工程师花很长时间 debug,这段代码至今没人敢随便改(可参考意法论坛 2015 年 HAL bug 讨论)。
源码宏观轮廓:
if (hi2c->State == HAL_I2C_STATE_READY) { if (I2C_RequestMemoryRead(...) != HAL_OK) return HAL_ERROR; // 步骤1:写地址时序 if (hi2c->XferSize == 0U) { ... } // 步骤2:按情况提前配置 else if (hi2c->XferSize == 1U) { ... } else if (hi2c->XferSize == 2U) { ... } else { ... } while (hi2c->XferSize > 0U) // 步骤3:开始接收 { if (hi2c->XferSize <= 3U) { if (hi2c->XferSize == 1U) { ... } // 收 1 字节 else if (hi2c->XferSize == 2U) { ... } // 收 2 字节 else { ... } // 收 3 字节 } else { ... } // 收 >3 字节 } ... return HAL_OK; }
4.2 步骤1:I2C_RequestMemoryRead(写地址时序)
前半部分(状态检查、BUSY、LOCK、PE、State=BUSY_RX、Mode=MEM、参数准备)与写函数几乎一样。
static HAL_StatusTypeDef I2C_RequestMemoryRead(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint32_t Timeout, uint32_t Tickstart) { SET_BIT(hi2c->Instance->CR1, I2C_CR1_ACK); // ★ 读取要先开 ACK(自己收到数据要给对方应答) SET_BIT(hi2c->Instance->CR1, I2C_CR1_START); // ① 发开始 if (I2C_WaitOnFlagUntilTimeout(hi2c, I2C_FLAG_SB, RESET, Timeout, Tickstart) != HAL_OK) return HAL_TIMEOUT; // ② 等开始完成 hi2c->Instance->DR = I2C_7BIT_ADD_WRITE(DevAddress);// ③ 发地址+写位(要向设备写字地址,必须是写) if (I2C_WaitOnMasterAddressFlagUntilTimeout(hi2c, I2C_FLAG_ADDR, Timeout, Tickstart) != HAL_OK) return HAL_ERROR; // ④ 等地址发送完成 __HAL_I2C_CLEAR_ADDRFLAG(hi2c); // ⑤ 清 ADDR if (I2C_WaitOnTXEFlagUntilTimeout(hi2c, Timeout, Tickstart) != HAL_OK) return HAL_ERROR; // ⑥ 等 DR 空 if (MemAddSize == I2C_MEMADD_SIZE_8BIT) { hi2c->Instance->DR = I2C_MEM_ADD_LSB(MemAddress); // ⑦a 发字地址(8位) } else { hi2c->Instance->DR = I2C_MEM_ADD_MSB(MemAddress); // 16位:先高字节 if (I2C_WaitOnTXEFlagUntilTimeout(...) != HAL_OK) { ... } hi2c->Instance->DR = I2C_MEM_ADD_LSB(MemAddress); // 再低字节 } if (I2C_WaitOnTXEFlagUntilTimeout(hi2c, Timeout, Tickstart) != HAL_OK) return HAL_ERROR; // ⑧ 等字地址发送完成 SET_BIT(hi2c->Instance->CR1, I2C_CR1_START); // ⑨ ★ 重新启动 Restart! if (I2C_WaitOnFlagUntilTimeout(hi2c, I2C_FLAG_SB, RESET, Timeout, Tickstart) != HAL_OK) { ... } // ⑩ 等第二个开始 hi2c->Instance->DR = I2C_7BIT_ADD_READ(DevAddress); // ⑪ ★ 再次发从机地址,这次是读方向 if (I2C_WaitOnMasterAddressFlagUntilTimeout(hi2c, I2C_FLAG_ADDR, Timeout, Tickstart) != HAL_OK) return HAL_ERROR; // ⑫ 等地址被确认 return HAL_OK; // 时序完成:START + 地址(W) + 字地址 + RESTART + 地址(R) }细节1:
ACK 标志位(CR1 bit10 = 0x400):ACK 使能后,硬件每收到一个字节的第 9 个时钟周期自动发 ACK。
细节2:I2C_7BIT_ADD_READ:
#define I2C_7BIT_ADD_READ(__ADDRESS__) ((uint8_t)((__ADDRESS__) | I2C_OAR1_ADD0)) #define I2C_OAR1_ADD0 0x00000001U /* Bit 0 */把 DevAddress 最低位强制置 1(LSB=1 表示读)。
由于"或 1"结果必为 1,所以无论读写,传设备地址都要
<<1(让出最低位给读写标志)。
4.3 步骤2:提前配置(0/1/2/3+ 字节)
为什么"提前":
修改寄存器是 CPU 直接改比特位,但修改完后 I2C 硬件还要在时序控制下根据配置完成动作——I2C 配置生效有延后性。
这就是源码大量"等待完成"的原因:CPU 配置很快,I2C 生效较慢。
类比:地铁到站播报当前站,并提前播报下一站(提前配置"下一站要不要下车")。
if (hi2c->XferSize == 0U) // ① 没有数据要接收 { __HAL_I2C_CLEAR_ADDRFLAG(hi2c); SET_BIT(hi2c->Instance->CR1, I2C_CR1_STOP); // 直接结束 } else if (hi2c->XferSize == 1U) // ② 只有 1 字节 { CLEAR_BIT(hi2c->Instance->CR1, I2C_CR1_ACK); // 关自动应答 → 收完即回 NACK __disable_irq(); // 关中断(EV6_3 时序必须完整执行) __HAL_I2C_CLEAR_ADDRFLAG(hi2c); SET_BIT(hi2c->Instance->CR1, I2C_CR1_STOP); // ★ 提前设置 STOP(并不立即发!) __enable_irq(); } else if (hi2c->XferSize == 2U) // ③ 有 2 字节 { SET_BIT(hi2c->Instance->CR1, I2C_CR1_POS); // ★ 设置 POS 位(跨字节预约 NACK) __disable_irq(); __HAL_I2C_CLEAR_ADDRFLAG(hi2c); // 必须这么做 CLEAR_BIT(hi2c->Instance->CR1, I2C_CR1_ACK); // 必须这么做 __enable_irq(); } else // ④ >=3 个数据 { SET_BIT(hi2c->Instance->CR1, I2C_CR1_ACK); // 开启自动 ACK __HAL_I2C_CLEAR_ADDRFLAG(hi2c); }细节1:
STOP 提前设置——
SET_BIT(CR1, STOP)不会立即发送,而是等硬件收到最后一个字节、发完 NACK 后自动发出。细节2:
POS 位(Acknowledge/PEC Position,CR1 bit11):
- POS 专门处理 2 字节读取;
- 核心作用:决定 NACK 发给当前字节还是下一个字节;
- 使用配置:POS=1 且 ACK=0;
- 霸道之处:临时剥夺 ACK 位对当前字节的控制权——对当前正在收的字节强行无视 NACK 设置、必须回 ACK,把 NACK 留给后面那个字节。
为什么必须有 POS(硬核推演):
- 收到数据回 ACK 是 I2C 自动做的;
- 移位寄存器收满完整 1 字节(8 个时钟周期),第 9 个时钟周期自动回 ACK,同时可能移入 DR;
- 收 2 字节时无 POS:收倒数第二个 → 自动 ACK;收最后一个 → 默认只能继续 ACK。但我们需要最后一个回 NACK;
- 想在倒数第二个自动 ACK 后、最后一个到达移位寄存器前让 CPU 关 ACK——时序要求极高,这个窗口极短,尤其有中断时几乎不可能,不确定性太大;
- 所以用 POS 提前设置,给硬件下达"跨字节预约 NACK":第 1 个字节自动 ACK,第 2 个字节自动 NACK。
一句话:
POS 是"为了一顿醋,包了顿饺子"——解决 2 字节读取收尾时序的专用机制。
4.4 步骤3:开始接收(1/2/3/N 字节)
① 收 1 字节(提前配置已关 ACK,收到即回 NACK):
if (hi2c->XferSize == 1U) { if (I2C_WaitOnRXNEFlagUntilTimeout(hi2c, Timeout, tickstart) != HAL_OK) // 等 RXNE:DR 有数据 return HAL_ERROR; *hi2c->pBuffPtr = (uint8_t)hi2c->Instance->DR; // 读走 hi2c->pBuffPtr++; hi2c->XferSize--; hi2c->XferCount--; }② 收 2 字节(POS=1 + ACK=0:第 1 个自动 ACK,第 2 个自动 NACK):
else if (hi2c->XferSize == 2U) { if (I2C_WaitOnFlagUntilTimeout(hi2c, I2C_FLAG_BTF, RESET, Timeout, tickstart) != HAL_OK) // 等 BTF return HAL_ERROR; __disable_irq(); SET_BIT(hi2c->Instance->CR1, I2C_CR1_STOP); // 置 STOP(数据还没读走!) *hi2c->pBuffPtr = (uint8_t)hi2c->Instance->DR; // 读第 1 个 hi2c->pBuffPtr++; hi2c->XferSize--; hi2c->XferCount--; __enable_irq(); *hi2c->pBuffPtr = (uint8_t)hi2c->Instance->DR; // 读第 2 个(移位寄存器移入 DR) hi2c->pBuffPtr++; hi2c->XferSize--; hi2c->XferCount--; }③ 收 3 字节(默认 ACK 开,读走第 1 个前关 ACK,让第 3 个回 NACK):
else // XferSize == 3 { if (I2C_WaitOnFlagUntilTimeout(hi2c, I2C_FLAG_BTF, RESET, Timeout, tickstart) != HAL_OK) // 前 2 个收齐,都已自动 ACK return HAL_ERROR; CLEAR_BIT(hi2c->Instance->CR1, I2C_CR1_ACK); // ★ 此时还没读 DR,最后 1 个还没收。 // 关 ACK → 读走 DR 后移位寄存器数据移入 DR, // 第 3 个收满后第 9 时钟周期回 NACK,完美! __disable_irq(); *hi2c->pBuffPtr = (uint8_t)hi2c->Instance->DR; // 拿走第 1 个 hi2c->pBuffPtr++; hi2c->XferSize--; hi2c->XferCount--; count = I2C_TIMEOUT_FLAG * (SystemCoreClock / 25U / 1000U); // 再次等 BTF:最后一个完整接收并已 NACK do { count--; if (count == 0U) { ...超时...; return HAL_ERROR; } } while (__HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_BTF) == RESET); SET_BIT(hi2c->Instance->CR1, I2C_CR1_STOP); // 3 个应答都完成,发 STOP *hi2c->pBuffPtr = (uint8_t)hi2c->Instance->DR; // 读第 2 个 hi2c->pBuffPtr++; hi2c->XferSize--; hi2c->XferCount--; __enable_irq(); *hi2c->pBuffPtr = (uint8_t)hi2c->Instance->DR; // 读第 3 个 hi2c->pBuffPtr++; hi2c->XferSize--; hi2c->XferCount--; }④ 收 N 字节(>3)(RXNE 逐个读 + BTF 双缓冲 + 剩 3 个时关 ACK):
else // XferSize > 3 { if (I2C_WaitOnRXNEFlagUntilTimeout(hi2c, Timeout, tickstart) != HAL_OK) return HAL_ERROR; *hi2c->pBuffPtr = (uint8_t)hi2c->Instance->DR; hi2c->pBuffPtr++; hi2c->XferSize--; hi2c->XferCount--; if (__HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_BTF) == SET) // DR 和移位寄存器都有 → 再读一个(提速) { if (hi2c->XferSize == 3U) CLEAR_BIT(hi2c->Instance->CR1, I2C_CR1_ACK); // ★ 还剩 3 个时提前关 ACK,为最后 NACK 准备 *hi2c->pBuffPtr = (uint8_t)hi2c->Instance->DR; hi2c->pBuffPtr++; hi2c->XferSize--; hi2c->XferCount--; } }收尾统一:
State=READY; Mode=NONE; __HAL_UNLOCK; return HAL_OK;细节1:
RXNE 标志位(0x00010040U,SR1 第 7 位)——只在接收用:1 = DR 有数据可读。
细节2:
BTF(接收角度)——读取模式下,DR 有数据 && 移位寄存器有数据时 BTF=1。
✨ RxNE vs BTF(接收角度):
标志位 含义 配合效果 RXNE DR 有数据可读 通知 CPU"可以取走一个字节" BTF DR 和移位寄存器都有数据 两个都满:CPU 拷贝 DR 的同时,移位寄存器继续接收下一字节 接收也是双寄存器缓冲:
CPU 取 DR 数据的同时,I2C 硬件在移位寄存器继续接收,提高吞吐量。
5. 本章总结(速查表)
① I2C 寄存器总览(基地址 0x40005800)
寄存器 偏移 本章涉及的关键位 作用 CR1 0x00 PE(0)、ENGC(6)、NOSTRETCH(7)、START(8)、STOP(9)、ACK(10)、POS(11) 控制:使能、起停、应答 CR2 0x04 FREQ[5:0] 外设时钟频率范围 OAR1 0x08 ADD0~ADD7、ADDMODE 自身地址(从机用) OAR2 0x0C ENDUAL、ADD2 双地址 DR 0x10 数据字节 读写数据的"窗口" SR1 0x14 SB(0)、ADDR(2)、BTF(3)、RXNE(7)、TXE(8)、AF(11) 等 状态标志(发送/接收过程) SR2 0x18 MSL(0)、BUSY(1)、TRA(2) 等 状态标志(总线/主从) CCR 0x1C F/S、DUTY、CCR 时钟速度与占空比 TRISE 0x20 TRISE 上升时间 ② 关键标志位速查
标志 宏值 所在寄存器·位 含义(本章语境) SB 0x00010001 SR1 bit0 起始条件已发送 ADDR 0x00010002 SR1 bit2 地址发送结束(收到 ACK) BTF 0x00010004 SR1 bit3 发送:DR+移位寄存器都空(字节真发出);
接收:DR+移位都有数据
RXNE 0x00010040 SR1 bit7 接收时 DR 有数据可读 TXE 0x00010080 SR1 bit8 发送时 DR 空(可写下一字节) AF 0x00010400 SR1 bit11 应答失败(NACK) BUSY 0x00100002 SR2 bit1 总线忙 MSL 0x00100001 SR2 bit0 主/从模式 标志位宏规律:
32 位数 = 高 16 位选寄存器(0x0001→SR1,0x0010→SR2)+ 低 16 位定位标志位位置。
③ 核心概念对比
对比项 说明 开启时钟 vs 设备使能 时钟=RCC 使能(可配置);
使能=CR1.PE(可工作)。
插电 vs 按开始
Master vs Slave 无专门配置项,谁发 START 谁是 Master;
MODE 由调用函数决定
Master_Transmit Mem_Write 前者只发"地址+数据流"(无内存地址过程);
后者带"内存/寄存器地址"复合时序
TXE vs BTF(发送) TXE=DR 空(数据进移位寄存器);
BTF=DR+移位寄存器都空(数据真正发出)
RXNE vs BTF(接收) RXNE=DR 有数据;
BTF=DR+移位寄存器都有数据(双缓冲提速)
ACK vs NACK 收到字节后第 9 个时钟周期的应答:低=ACK 继续,高=NACK 结束 POS=1+ACK=0 2 字节读取专用:当前字节强制 ACK,NACK 预约给下一个字节 7 位地址左移 最低位留给读写位:写=0( &~1)、读=1(|1),故调用前 DevAddress<<1
面试题总结
一、基础概念
1. I2C 通信的基本信号有哪些?START 和 STOP 的时序特征是什么?
答案:
起始 START(SCL 高电平期间 SDA 高→低)、
停止 STOP(SCL 高电平期间 SDA 低→高)、
数据位(SCL 高电平期间 SDA 电平稳定有效,SCL 低电平期间允许变化)、
应答 ACK(第 9 个时钟周期 SDA 拉低)、
非应答 NACK(第 9 个时钟周期 SDA 保持高)。
2. AT24C02 的 7 位器件地址怎么构成?为什么调用 HAL 函数前要左移 1 位?
答案:
完整 7 位器件地址格式为
0101 0 A2 A1 A0(AT24C02 固定前 4 位 1010,A2/A1/A0 由硬件引脚电平决定)。I2C 发送时第 8 位是读写位(0=写,1=读),所以 7 位地址要
<<1腾出最低位;HAL 内部用I2C_7BIT_ADD_WRITE(最低位清 0)或I2C_7BIT_ADD_READ(最低位置 1)补齐。3. 为什么 I2C 的 SCL/SDA 要配置成开漏输出(AF_OD)并接上拉电阻?
答案:
开漏输出只能拉低、不能主动拉高,靠外部上拉电阻把总线拉高,实现"线与"(任一设备拉低则总线为低)。这允许:① 多主机仲裁;② 从机时钟拉伸(拉低 SCL 暂停主机);③ 应答位由接收方拉低 SDA 实现。
4. 时钟拉伸(Clock Stretching)是什么?课件里 NoStretchMode 配置成什么?
答案:
从机处理不过来时拉低 SCL,让主机暂停发送。
课件配置
NoStretchMode = I2C_NOSTRETCH_DISABLE,即允许从机时钟拉伸(禁用"禁止拉伸"),这是标准行为。
二、初始化与寄存器
5. "开启时钟"和"设备使能"有什么区别?
答案:
开启时钟(RCC 使能)是让外设接入时钟树、可以配置寄存器;
设备使能(CR1.PE 置 1)是外设本身开始工作。
类比洗衣机:插电源(开时钟)→ 设定参数(配置)→ 按开始(设备使能)→ 正常工作。两者缺一不可。
6. MODIFY_REG 宏的原理是什么?为什么"先清空再设置"?
答案:
WRITE_REG(REG, (READ_REG(REG) & ~CLEARMASK) | SETMASK)。先读出寄存器 → 清除掩码取反后与运算(把指定位置清零、其他位不变)→ 或上 SETMASK(写入新值)→ 写回。
若不先清空直接
|,原值中的 1 会残留,结果不变,无法正确设置。7.
hi2c1.Instance = I2C2这行代码做了什么?结构体为什么能直接映射到寄存器?答案:
I2C2是((I2C_TypeDef*)0x40005800),把句柄的 Instance 指针指向 I2C2 寄存器基地址。因为
I2C_TypeDef结构体成员顺序(CR1→CR2→…→TRISE)与 I2C 外设 9 个连续 32 位寄存器的偏移顺序完全一致,所以可以用指针像操作内存一样直接读写寄存器(如hi2c->Instance->DR等价于访问 0x40005810)。8. HAL_I2C_MspInit 是做什么的?为什么 I2C 的 GPIO 配置要放在这里而不是 HAL_I2C_Init?
答案:
MSP(MCU Support Package)是芯片硬件相关配置(GPIO 引脚、时钟使能、中断),因芯片而异。
HAL_I2C_Init是协议逻辑(寄存器配置),与具体芯片无关;两者分离,换芯片只改 MspInit,协议代码不用动。
三、设备就绪检测
9. HAL_I2C_IsDeviceReady 判断"设备就绪"要满足哪三个条件?
答案:
① 设备本身空闲(
hi2c->State == HAL_I2C_STATE_READY,未被中断/其他任务占用,否则返回 HAL_BUSY);② 总线就绪(SR2.BUSY 复位,无传输占用);
③ 目标设备在线且地址匹配(发出地址后收到 ACK)。
10.
__HAL_I2C_GET_FLAG宏如何定位标志位?以I2C_FLAG_BUSY = 0x00100002U为例。答案:
标志位宏是"地址+偏移"封装——高 16 位选寄存器(0x0001→SR1,0x0010→SR2),低 16 位定位标志位位置。
BUSY 高 16 位=0x10≠0x01 → 选 SR2;
低 16 位=0x0002 → 检测
(SR2 & 0x2) == 0x2,即 SR2 的 bit1(总线忙)。11. 为什么标志位判断要写成
(寄存器 & 掩码)== 掩码而不是(寄存器 & 掩码)非零即可?答案:
这是"全比较"。
标志位可能由多位组成,只有全部匹配才成立;
若只判非零,部分位为 1 也会误判通过(类似取款机:只输密码没插卡,
0x01 & 0x03非零就放行是错的,必须== 0x03)。12. 简述 HAL_I2C_IsDeviceReady 中地址探测的六步流程。
答案:
① 置 CR1.START 发起始条件;
② 等 SR1.SB(起始完成,超时返回 HAL_TIMEOUT);
③ 写 DR = I2C_7BIT_ADD_WRITE(DevAddress) 发从机地址(写方向);
④ 循环检测 ADDR/AF(等地址发送结束且无应答失败);
⑤ 判 ADDR==SET(收到 ACK,设备在线);
⑥ 发 STOP、清 ADDR、等 BUSY 复位、恢复 State=READY、返回 HAL_OK。
13. 为什么写完后要调用 HAL_I2C_IsDeviceReady 等待而不是直接 delay?
答案:
AT24C02 内部写入需要 5ms+ 完成,期间不响应总线操作。
用 delay 粗暴等待无法感知"实际写完了没",而 IsDeviceReady 通过地址探测确认设备恢复应答,时间更精准、更可靠(阻塞式接口也都有超时保护防卡死)。
四、读写机制(重点)
14. TXE 和 BTF 标志位的区别?为什么 HAL_I2C_Mem_Write 结尾还要再等一次 BTF?
答案:
TXE=DR 空(数据已从 DR 移入移位寄存器,不一定发出去了);
BTF=DR 和移位寄存器都空(该字节真正在总线上发完)。
CPU 写 DR 和 I2C 接口移位发送是异步的,while 循环结束只是"用户数据全部写入 DR/移位寄存器",必须等 BTF 确认所有字节都被对方收到,才能发 STOP。
15. 为什么写 EEPROM 用 HAL_I2C_Mem_Write 而不是 HAL_I2C_Master_Transmit?
答案:
Master_Transmit 只完成"设备地址+原始数据流"的机械发送,没有提前选中设备内部内存地址的过程;
而 EEPROM 必须先发字地址(要访问哪个字节)才能读写数据。
Mem_Write 是 MASTER 模式的特化,内部自动处理"设备地址+内存地址+数据"的复合时序(对应 Mode=HAL_I2C_MODE_MEM)。
16. 读取时为什么最后一个字节要回 NACK?什么时候配置?
答案:
NACK 告诉从机"收够了,别再发",配合 STOP 结束本次读取。
因为 I2C 配置生效有延后性(CPU 改寄存器很快、硬件按时序执行较慢),所以必须在收到倒数第 2 个字节时提前配置(关 ACK/置 STOP),硬件才会在最后一个字节后自动回 NACK。
17. POS 位是干什么的?为什么 2 字节读取必须靠它?
答案:
POS(CR1 bit11)决定 NACK 是发给当前字节还是下一个字节。
收 2 字节时硬件对每个字节自动回 ACK,若要最后回 NACK,理论上可在"第 1 个字节自动 ACK 后、第 2 个字节到达前"由 CPU 关 ACK——但该时序窗口极短、中断下几乎不可行。
POS=1 + ACK=0 时硬件"跨字节预约":当前字节强制回 ACK,NACK 预约给下一字节,彻底绕开时序竞争。
18. 读取 1/2/3/N 字节时,ACK/NACK 分别是如何处理的?
答案:
- 1 字节:提前关 ACK + 置 STOP → 数据移入 DR 时自动回 NACK;
- 2 字节:POS=1 + ACK=0 → 第 1 个自动 ACK、第 2 个自动 NACK,等 BTF 后读走两个;
- 3 字节:默认开 ACK,前 2 个自动 ACK;读走第 1 个前关 ACK,第 3 个收满回 NACK,再置 STOP;
- N 字节(>3):等 RXNE 逐字节读,BTF 满则双缓冲再读一个提速,剩 3 个时关 ACK,之后走 3 字节流程。
19. RxNE 和 BTF(接收角度)有什么区别?
答案:
RXNE=DR 有数据可读(CPU 可以取走一个字节);
BTF=DR 和移位寄存器都有数据。
两者配合实现双寄存器缓冲:CPU 拷贝 DR 数据的同时,移位寄存器继续接收下一字节,提升吞吐量。
五、深挖/开放
20. STM32F103 中没有 Master/Slave 配置项,主从模式是怎么确定的?
答案:
由调用哪类 HAL 函数决定——谁发 START 谁是 Master。
hi2c->Mode只是逻辑分类记录(MASTER=地址+数据流;MEM=地址+内存地址+数据的特化;SLAVE=被动等主机呼叫,硬件不主动产生 START/STOP)。21. 为什么 HAL 库中所有等待循环都带超时?HAL_MAX_DELAY 是什么含义?
答案:
阻塞式接口等待标志位时可能因总线异常、设备无响应而永远等不到,超时防止卡死(单片机开发铁律)。
HAL_MAX_DELAY 表示无限等待(不超时),由调用者自行承担风险。
22.(开放)如果 I2C 通信突然卡死(如 SDA 被从机拉低),你会如何排查?
答案要点:
① 示波器/逻辑分析仪看 SCL/SDA 电平是否被拉低,定位是主机还是从机导致;
② 检查从机是否处于忙状态(如 EEPROM 写入中未完成);
③ 检查总线是否有设备地址冲突/仲裁失败(AF/ARLO/BERR 标志位);
④ 软件恢复手段:连续发送 9 个 SCL 时钟脉冲让从机释放总线,或复位从机;
⑤ 检查上拉电阻是否虚焊、线长是否过长(影响上升沿,400kHz 快速模式更敏感)。
23.(开放)阻塞式、中断式、DMA 式 I2C 怎么选?
答案要点:
阻塞式最简单、代码直观,但 CPU 全程等待,适合低速小数据(如 EEPROM 读写);
中断式让 CPU 在等待时干别的,适合中等数据量;
DMA 式由硬件搬运数据,CPU 零参与,适合大数据量/高频率传输,但代码和资源管理最复杂。工程上按数据量和实时性要求取舍。
更多推荐




所有评论(0)