从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 成员配置值含义
ClockSpeed400000400 kHz 快速模式(AT24C02 手册:VCC≥2.7V 时最高 400 kHz)
DutyCycleI2C_DUTYCYCLE_2SCL 高低电平占空比 2:1(低速模式无占空比概念)
OwnAddress10本机作为从机时的地址
AddressingMode7BIT7 位地址模式
DualAddressModeDISABLE不使用双地址
GeneralCallModeDISABLE不响应广播呼叫(地址 0x00)
NoStretchModeDISABLE允许从机时钟拉伸

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          0x40000000UL

I2C_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.hI2C_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 —— 参数

参数含义
hi2cI2C 句柄结构体指针
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 → 选中 SR2
20x00100002 & 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 应答失败标志也没置位

  1. I2C_FLAG_ADDR (SR1 bit2) 主机发送从机地址 + 读写位之后:
    • 从机应答 ACK → 硬件置 ADDR=1(SET),代表地址发送完成
    • 还没收到 ACK → ADDR=0(RESET)
  2. 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(发送角度):

标志位含义说明
TXEDR 空数据从 DR 移到移位寄存器(还没发出去)
BTFDR 与移位寄存器都空该字节真正被发送出去了

这就是 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(硬核推演):

  1. 收到数据回 ACK 是 I2C 自动做的;
  2. 移位寄存器收满完整 1 字节(8 个时钟周期),第 9 个时钟周期自动回 ACK,同时可能移入 DR;
  3. 收 2 字节时无 POS:收倒数第二个 → 自动 ACK;收最后一个 → 默认只能继续 ACK。但我们需要最后一个回 NACK;
  4. 想在倒数第二个自动 ACK 后、最后一个到达移位寄存器前让 CPU 关 ACK——时序要求极高,这个窗口极短,尤其有中断时几乎不可能,不确定性太大;
  5. 所以用 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(接收角度):

标志位含义配合效果
RXNEDR 有数据可读通知 CPU"可以取走一个字节"
BTFDR 和移位寄存器都有数据两个都满:CPU 拷贝 DR 的同时,移位寄存器继续接收下一字节

接收也是双寄存器缓冲:

        CPU 取 DR 数据的同时,I2C 硬件在移位寄存器继续接收,提高吞吐量。

5. 本章总结(速查表)

① I2C 寄存器总览(基地址 0x40005800)

寄存器偏移本章涉及的关键位作用
CR10x00PE(0)、ENGC(6)、NOSTRETCH(7)、START(8)、STOP(9)、ACK(10)、POS(11)控制:使能、起停、应答
CR20x04FREQ[5:0]外设时钟频率范围
OAR10x08ADD0~ADD7、ADDMODE自身地址(从机用)
OAR20x0CENDUAL、ADD2双地址
DR0x10数据字节读写数据的"窗口"
SR10x14SB(0)、ADDR(2)、BTF(3)、RXNE(7)、TXE(8)、AF(11) 等状态标志(发送/接收过程)
SR20x18MSL(0)、BUSY(1)、TRA(2) 等状态标志(总线/主从)
CCR0x1CF/S、DUTY、CCR时钟速度与占空比
TRISE0x20TRISE上升时间

② 关键标志位速查

标志宏值所在寄存器·位含义(本章语境)
SB0x00010001SR1 bit0起始条件已发送
ADDR0x00010002SR1 bit2地址发送结束(收到 ACK)
BTF0x00010004SR1 bit3

发送:DR+移位寄存器都空(字节真发出);

接收:DR+移位都有数据

RXNE0x00010040SR1 bit7接收时 DR 有数据可读
TXE0x00010080SR1 bit8发送时 DR 空(可写下一字节)
AF0x00010400SR1 bit11应答失败(NACK)
BUSY0x00100002SR2 bit1总线忙
MSL0x00100001SR2 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=02 字节读取专用:当前字节强制 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 零参与,适合大数据量/高频率传输,但代码和资源管理最复杂。工程上按数据量和实时性要求取舍。


Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐