STM32 HAL I2C 库其实是一个状态机 + 中断/DMA驱动 + 超时保护 的封装。理解它以后,你之前说的"IIC 每一步等待状态位,超时复位总线"这个思路,基本就是 HAL 库内部干的事情。
我以 STM32F1/F4/G4 常见 HAL I2C 为例拆开讲。
1. HAL I2C 整体架构
HAL 把 I2C 外设封装成:
应用层
|
| HAL_I2C_Master_Transmit()
| HAL_I2C_Master_Receive()
|
↓
HAL I2C驱动层
|
| 状态机
| Flag检测
| Error处理
| Timeout
|
↓
STM32 I2C寄存器
|
| CR1
| CR2
| SR1
| SR2
| DR
比如:
c
HAL_I2C_Master_Transmit(
&hi2c1,
dev_addr,
data,
len,
timeout
);
内部不是一句话发送完成。
实际:
START
↓
等待SB标志
↓
发送地址
↓
等待ADDR
↓
清ADDR
↓
发送数据
↓
等待TXE
↓
等待BTF
↓
STOP
2. HAL I2C核心结构体
先看:
c
I2C_HandleTypeDef hi2c1;
里面:
c
typedef struct
{
I2C_TypeDef *Instance;
I2C_InitTypeDef Init;
uint8_t *pBuffPtr;
uint16_t XferSize;
uint16_t XferCount;
HAL_LockTypeDef Lock;
HAL_I2C_StateTypeDef State;
HAL_I2C_ModeTypeDef Mode;
uint32_t ErrorCode;
}I2C_HandleTypeDef;
关键几个:
State 状态机
例如:
c
HAL_I2C_STATE_READY
空闲
发送:
READY
|
↓
BUSY_TX
|
↓
READY
接收:
READY
|
↓
BUSY_RX
|
↓
READY
错误:
BUSY
|
↓
ERROR
|
↓
READY
3. HAL发送流程分析
看:
c
HAL_I2C_Master_Transmit()
简化:
c
HAL_StatusTypeDef HAL_I2C_Master_Transmit(...)
{
hi2c->State = BUSY_TX;
I2C_RequestMemoryWrite();
while(!TXE)
{
timeout--;
if(timeout==0)
{
Error_Handler();
}
}
hi2c->Instance->DR=data;
}
实际上分几个阶段。
第一阶段:产生START
代码类似:
c
SET_BIT(I2C_CR1_START);
硬件:
SDA
|
|____
\____
SCL ________
然后等待:
SR1.SB = 1
也就是:
Start Bit generated
HAL:
c
while(!(SR1 & I2C_SR1_SB))
{
timeout--;
}
如果这里死:
原因:
- 总线被拉低
- SDA短路
- SCL被拉低
- 从机拉死
第二阶段:发送设备地址
比如:
设备:
0x68
HAL发送:
c
DR = 0x68<<1 | WRITE
硬件:
START
1010000 + 0
ACK?
等待:
SR1.ADDR=1
如果:
ADDR没有出现
说明:
- 地址错误
- 从机没上电
- 没有ACK
第三阶段:清ADDR
STM32 I2C比较坑。
ADDR必须清:
读取:
c
SR1
SR2
否则:
I2C卡死。
HAL里面:
c
__HAL_I2C_CLEAR_ADDRFLAG();
实际上:
c
tmp = I2C->SR1;
tmp = I2C->SR2;
第四阶段:发送数据
例如:
写:
0x20
0x55
0xaa
流程:
DR写入
↓
TXE等待
↓
BTF等待
↓
下一个字节
对应:
TXE=1
发送寄存器空
BTF=1
字节传输完成
第五阶段 STOP
最后:
c
SET_BIT(CR1,STOP)
释放:
SCL ↑
SDA ↑
结束。
4. HAL的超时机制
这个非常重要。
HAL里面大量:
c
while(condition)
{
if(timeout == 0)
{
hi2c->ErrorCode |= HAL_I2C_ERROR_TIMEOUT;
return HAL_ERROR;
}
timeout--;
}
例如:
等待:
c
TXE
超过:
25ms
直接退出。
所以不会像裸机:
c
while(!(SR1&TXE));
永久死循环。
5. HAL错误处理
HAL定义:
c
#define HAL_I2C_ERROR_BERR
#define HAL_I2C_ERROR_ARLO
#define HAL_I2C_ERROR_AF
#define HAL_I2C_ERROR_TIMEOUT
含义:
BERR
Bus Error
总线错误。
比如:
SDA非法跳变。
ARLO
仲裁丢失。
多主机。
AF
Acknowledge Failure
最常见。
就是:
没有ACK
例如:
设备不存在。
TIMEOUT
等待超时。
6. HAL卡死恢复机制
这里和你之前的问题高度相关。
HAL本身:
不会主动复位IIC外设
它一般:
返回HAL_ERROR
然后用户处理。
比如:
c
if(HAL_I2C_Master_Transmit()!=HAL_OK)
{
I2C_Bus_Reset();
}
7. IIC总线恢复
工业代码一般这样:
检测:
SDA=0
说明从机挂死。
GPIO模拟:
把SCL拉9次:
SCL:
_|
|_|
_|
|_|
...
让slave释放SDA。
然后:
重新初始化:
I2C_DeInit()
I2C_Init()
8. HAL中断模式
另外一种:
c
HAL_I2C_Master_Transmit_IT()
流程:
启动:
START
然后退出。
靠:
I2C_EV_IRQHandler
继续推进。
状态:
READY
↓
BUSY_TX
↓
EV interrupt
↓
发送下一字节
↓
完成callback
↓
READY
这就是状态机。
9. DMA模式
高速:
c
HAL_I2C_Master_Transmit_DMA()
CPU:
只启动:
I2C + DMA
然后:
DMA搬数据。
完成:
HAL_I2C_MasterTxCpltCallback()
10. 对你之前提出的方案评价
你之前说:
第一步发起始信号等待状态标志位 设置超时
第二步发地址等待ACK 设置超时
第三步发数据等待ACK
超时复位IIC
这个思路:
✅ 完全正确
实际上就是:
HAL I2C状态机
+
错误恢复
工业级一般:
I2C任务
IDLE
|
START
|
ADDR
|
DATA
|
STOP
|
DONE
任何一步timeout
↓
ERROR
↓
GPIO恢复SCL
↓
I2C重新初始化
↓
重试
如果你自己写驱动,我反而建议不要完全照HAL阻塞式写法,而是做:
I2C状态机
+
非阻塞
+
超时计数
+
自动bus recovery
+
重试3次
尤其你现在 ESP32/STM32 做传感器(AS5600、CS1237等),这种方式比 HAL 阻塞轮询更可靠。你之前说的"卡死后复位IIC"就是工业产品常用方案。
STM32 LL(Low Layer)库可以理解成:
比 HAL 更接近寄存器,但又保留了一些官方封装函数的轻量驱动库。
如果说:
- HAL = 高级司机(帮你踩油门、换挡)
- LL = 半自动挡(你控制动作,库提供工具)
- 直接寄存器 = 手动挡(全部自己控制)
对于 I2C 来说,LL 和 HAL 最大区别就是:
HAL帮你实现状态机,LL只帮你操作硬件。
1. HAL vs LL I2C架构区别
HAL
结构:
用户代码
|
HAL_I2C_Master_Transmit()
|
HAL状态机
|
判断SB
判断ADDR
判断TXE
判断BTF
|
寄存器
你调用:
c
HAL_I2C_Master_Transmit();
它内部完成整个流程。
LL
结构:
用户代码
|
自己写状态机
|
LL_I2C_GenerateStartCondition()
|
LL_I2C_IsActiveFlag_SB()
|
LL_I2C_TransmitData8()
|
寄存器
例如:
c
LL_I2C_GenerateStartCondition(I2C1);
while(!LL_I2C_IsActiveFlag_SB(I2C1));
LL_I2C_TransmitData8(I2C1,addr);
while(!LL_I2C_IsActiveFlag_ADDR(I2C1));
流程你自己控制。
2. LL I2C发送过程
假设:
写:
设备地址
+
寄存器地址
+
数据
Step1 START
HAL:
c
SET_BIT(CR1,START)
LL:
c
LL_I2C_GenerateStartCondition(I2C1);
底层:
c
I2C1->CR1 |= I2C_CR1_START;
等待:
c
while(!LL_I2C_IsActiveFlag_SB(I2C1));
对应:
SR1.SB = 1
Step2 发送地址
LL:
c
LL_I2C_TransmitData8(
I2C1,
dev_addr<<1
);
等:
c
while(!LL_I2C_IsActiveFlag_ADDR(I2C1));
硬件:
发送地址
slave ACK
ADDR=1
Step3 清ADDR
STM32 I2C经典坑。
LL:
c
LL_I2C_ClearFlag_ADDR(I2C1);
实际上:
读:
SR1
SR2
Step4 发送数据
例如:
寄存器:
0x0E
LL:
c
LL_I2C_TransmitData8(
I2C1,
0x0E
);
等待:
c
while(!LL_I2C_IsActiveFlag_TXE(I2C1));
Step5 STOP
c
LL_I2C_GenerateStopCondition(I2C1);
3. LL没有什么?
这是重点。
LL没有:
❌ 完整发送函数
比如没有:
c
LL_I2C_Master_Transmit()
没有:
❌ 超时机制
比如:
c
while(!TXE);
卡死。
你必须自己:
c
timeout++;
if(timeout>1000)
{
error;
}
没有:
❌ 自动错误恢复
比如:
SDA拉低:
HAL:
返回ERROR
LL:
你自己处理
没有:
❌ 多状态封装
例如:
HAL:
c
hi2c.State
LL:
没有。
你自己:
c
typedef enum
{
I2C_IDLE,
I2C_START,
I2C_ADDR,
I2C_TX,
I2C_RX,
I2C_STOP
}i2c_state_t;
4. LL的性能优势
HAL
函数调用:
HAL_I2C_Master_Transmit
{
锁
状态检查
参数检查
错误判断
状态切换
寄存器操作
}
代码量大。
LL
基本:
函数宏
↓
寄存器
速度快。
例如:
STM32G4:
FOC 20kHz任务:
HAL:
可能不适合。
LL:
非常适合。
5. LL适合什么场景?
适合:
电机控制
比如你的:
- FOC
- PWM同步ADC
- 编码器读取
通常:
TIM
ADC
DMA
PWM
↓
LL
因为:
微秒级。
高速传感器
比如:
AS5600
400kHz I2C
LL可以做到:
固定周期读取。
产品级驱动
很多厂:
HAL初始化
+
LL关键外设
混用。
6. 工业项目常见组合
实际上很多STM32项目:
不是纯HAL。
而是:
系统
|
HAL
|
----------------
普通外设:
UART
GPIO
SPI
用HAL
高速外设:
TIM
ADC
DMA
I2C
用LL
----------------
例如:
FOC:
TIM1
|
LL
ADC
|
LL
DMA
|
LL
NVS
UART
GPIO
|
HAL
7. 对你之前IIC死锁问题
你的思路:
START等待 → 地址ACK等待 → 数据ACK等待 → timeout → reset
如果用HAL:
你只能:
c
HAL_I2C_Master_Transmit()
失败
↓
恢复
内部过程看不到。
如果用LL:
你可以:
c
switch(i2c_state)
{
case START:
发送START;
timeout++;
break;
case ADDR:
检查ADDR;
timeout++;
break;
case DATA:
检查TXE;
timeout++;
break;
}
完全符合你的设计。
8. 如果做产品,我建议
结合你的项目:
- STM32G431
- ESP32 FOC
- AS5600
- 电机控制
推荐:
FOC核心:
LL
TIM1
ADC
DMA
I2C:
LL状态机
通讯:
HAL
GPIO:
HAL/LL都可以
尤其 AS5600 这种周期读取:
不要:
c
HAL_I2C_Master_Transmit()
阻塞等待
而是:
1kHz任务
启动I2C
状态机运行
完成更新角度
这和你之前做 FOC 多任务架构的思想是一致的。
实际上 LL + 自己写状态机,是很多高可靠嵌入式产品比 HAL 更常见的做法。