STM32 IIC HAL库,LL库

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 更常见的做法。

相关推荐
辰哥单片机设计2 小时前
STM32智能门禁系统(语音版)
stm32·单片机·嵌入式硬件
映翰通朱工4 小时前
Windows 上位机 + EC3320:USB 转 CAN 通信实测(从接线到 SSH 收包)
windows·stm32·ssh
Industio_触觉智能4 小时前
瑞芯微ISP内核驱动深度解析|RKISP39从架构到Probe初始化全流程(上篇)
linux·嵌入式硬件·硬件架构·摄像头·isp·瑞芯微·rk3576
Funing75 小时前
系列一:STM32 外设初始化
stm32·单片机·嵌入式硬件
XiangrongZ5 小时前
debian中配置stm32的交叉编译环境
运维·stm32·debian
✎ ﹏梦醒͜ღ҉繁华落℘6 小时前
单片机基础知识--LVGL学习-lv_obj_add_flag()
c语言·单片机·学习
zcmodeltech6 小时前
钢铁冶金沙盘模型多工序协同控制系统设计:基于STM32与Modbus RTU的高炉-连铸-热轧全流程联动方案
stm32·单片机·嵌入式硬件·数据库架构·嵌入式实时数据库·多分类
西城微科方案开发7 小时前
充气泵PCBA方案:数字传感器VS模拟传感器
单片机
zcmodeltech7 小时前
供热系统模型多工况控制系统设计:基于STM32与Modbus RTU的热源-管网-末端联动方案
大数据·stm32·单片机·嵌入式硬件·制造·数据库系统