stm32cubemx+freertos+中断实现IIC从机

最近做一个项目需要将stm32配置为iic的从机模式来响应总线的读写需求,看了网上的大部分资料讲解的都不是很全面,因此这里做一个小分享。

iic通信流程

要编写iic从机模式的代码,就得对iic得整个通信流程足够熟悉,下面是流程的介绍讲解

  • 主机发送数据(从机接收数据)
  1. 起始信号(START)
    主机在检测到总线为"空闲状态"(即 SDA、SCL 线均为高电平)时,发送一个启动信号"S",开始一次通信。

  2. 发送从机地址和写命令
    主机接着发送一个命令字节。该字节由 7 位的外围器件地址和 1 位读写控制位 R/W 组成(此时 R/W=0 表示写操作)。

  3. 从机应答
    相对应的从机收到命令字节后向主机回馈应答信号 ACK(ACK=0),表示地址匹配并准备好接收数据。

  4. 发送数据字节
    主机收到从机的应答信号后开始发送第一个字节的数据。从机收到数据后返回一个应答信号 ACK。主机收到应答信号后再发送下一个数据字节。

  5. 结束通信
    当主机发送最后一个数据字节并收到从机的 ACK 后,通过向从机发送一个停止信号 P 结束本次通信并释放总线。从机收到 P 信号后也退出与主机之间的通信。

    流程示意图:
    ┌───────┐ ┌─────────────┐ ┌───────┐ ┌──────────┐ ┌───────┐ ┌────────┐
    │ START │ → │ 地址+写命令 │ → │ ACK │ → │ 数据字节 │ → │ ACK │ → ... → │ STOP │
    └───────┘ └─────────────┘ └───────┘ └──────────┘ └───────┘ └────────┘

  • 主机读取数据(从机发送数据)
  1. 起始信号(START)

    主机拉低SDA线(在SCL高电平期间产生下降沿),表示通信开始。

  2. 发送从机地址+写命令

    • 主机发送7位从机地址 ,后跟1位方向控制位(R/W) ,此处为0(写模式)。
    • 从机返回应答信号(ACK) (SDA拉低)确认地址匹配。
  3. 发送寄存器地址

    • 主机发送8位寄存器地址,指定需要读取的从机内部寄存器位置。
    • 从机再次返回ACK确认接收成功。
  4. 重复起始信号(Repeated START)

    主机在未发送停止信号的情况下,再次产生起始信号,切换到读模式。

  5. 发送从机地址+读命令

    • 重新发送7位从机地址 ,方向控制位改为1(读模式)。
    • 从机返回ACK,准备发送数据。
  6. 接收数据并应答

    • 从机发送数据:从机在SCL的每个上升沿将数据位输出到SDA线,高位优先。

    • 主机应答

      :每接收完8位数据,主机在第9个时钟周期:

      • 若需继续读取:发送ACK(SDA拉低)。
      • 若结束读取:发送NACK(SDA保持高电平)。
  7. 停止信号(STOP)

    主机在SCL高电平期间拉高SDA线(产生上升沿),结束通信。

    流程示意图:
    START → 地址+写 → ACK → 寄存器地址 → ACK → 重复START → 地址+读 → ACK → 接收数据 → (ACK/NACK) → STOP

这种"先写后读 "的设计源于IIC协议的寄存器寻址机制,主要原因如下:

  1. 指定目标寄存器位置
    从机通常包含多个寄存器,直接读取时无法确定目标地址。通过先发送写命令+寄存器地址 ,明确告知从机后续需要读取的寄存器位置。例如,读取EEPROM的第2个存储单元时,需先写入地址0x02
  2. 避免数据冲突
    若直接发送读命令,从机可能默认从某个固定地址(如最近访问地址)返回数据,导致主机无法精确控制数据来源。先写后读确保操作原子性。
  3. 协议复合格式支持
    IIC支持重复起始信号(Repeated START) ,允许在不释放总线的情况下切换读写模式。这种机制减少了总线占用时间,提升效率。
  4. 从机状态初始化
    部分从机需要先接收控制命令(如传感器配置寄存器地址),才能切换到数据输出模式。例如,读取温度传感器数据前需先指定数据寄存器的地址。

实战

了解完iic通信的整个流程后,下面就了解一下具体是如何实现的

cubemx设置

常规的时钟和调试口(SWD)等的设置这里就不说了,就说IICfreertos的设置,如下:

此处只需要配置一下对应的时钟和地址即可,此处地址是7位的,即用主机对该设备进行寻址和读写操作时,需要用该地址加上(0/1)读写标志位再进行后续的操作。

IIC的中断也需要打开一下。

freertos的配置更简单,我这里开的是CMSIS_V1,其他的配置默认即可

生成代码后打开,进行以下操作,如下所示:

首先启动监听:

c 复制代码
HAL_I2C_EnableListen_IT(&hi2c1);  // 使能I2C1的侦听中断

注意:理论上随时都可以开启这个,但是一般在初始化的时候开启,同时得注意后面如果还有其他初始化的东西且比较耗时的话,先将中断关闭!

c 复制代码
__disable_irq();

// 中间放其他的初始化代码!

__enable_irq();

重写以下几个函数,并在里面实现具体的通信逻辑:

c 复制代码
// I2C设备地址回调函数(地址匹配上以后会进入该函数)
void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode);
// I2C数据接收回调函数(在I2C完成一次接收时会关闭中断并调用该函数,因此在处理完成后需要手动重新打开中断)
void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c);
// I2C数据发送回调函数(在I2C完成一次发送后会关闭中断并调用该函数,因此在处理完成后需要手动重新打开中断)
void HAL_I2C_SlaveTxCpltCallback(I2C_HandleTypeDef *hi2c);
// 错误回调函数
void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c);
// 侦听完成回调函数(完成一次完整的i2c通信以后会进入该函数)
void HAL_I2C_ListenCpltCallback(I2C_HandleTypeDef *hi2c);

注意,本教程是没有使用DMA来实现IIC从机的,如果使用了DMA可以方便很多,编程难度也会下降非常多

具体实现逻辑下面给一份实测可以使用的示例代码,注意此处代码只能做参考,需要略微修改按照才能移植使用

c 复制代码
// iic.h
#ifndef ORIN_IIC_H
#define ORIN_IIC_H

#include "stm32f1xx_hal.h"
#include "i2c.h"
#include "FreeRTOS.h"
#include "cmsis_os.h"
#include "task.h"
#include "crc8.h"
#include "power_adc.h"
#include "string.h"
#include "board_led.h"
#include "floodlight.h"
#include "gimbal.h"

#define SLAVE_ADDRESS 0x40  // 设置的从机地址为0x40
#define GPIO_PIN_SCL GPIO_PIN_6
#define GPIO_PIN_SDA GPIO_PIN_7
#define I2C_GPIO_PORT GPIOB
// 电源数据的位置和长度
#define SEND_POWER_OFFSET 0
#define SEND_POWEER_LEN 4

typedef enum {
    STATE_WAIT_CMD,      // 等待命令
    STATE_WAIT_LENGTH,   // 等待数据长度
    STATE_WAIT_DATA,     // 等待数据
    STATE_WAIT_CHECKSUM  // 等待校验
} ProtocolState;

void Orin_IIC_Init(void);  // orin_iic的初始化操作
void HAL_I2C_ListenCpltCallback(I2C_HandleTypeDef *hi2c);
void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode);
void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c);
void HAL_I2C_SlaveTxCpltCallback(I2C_HandleTypeDef *hi2c);
void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c);  // 错误回调函数
void Orin_Flash_Data(void);  // 刷新要发送的数据
void Orin_IIC_Data_Parse(void);  // 处理接收的数据

#endif // ORIN_IIC_H
c 复制代码
// iic.c
#include "orin_iic.h"

extern I2C_HandleTypeDef hi2c1;
extern TIM_HandleTypeDef htim2;
extern TIM_HandleTypeDef htim3;
static ProtocolState protocol_state = STATE_WAIT_CMD;  // 接收数据的各种状态
static uint8_t data_receive_buff[64];  // 接收数据的缓存数组
static uint8_t data_reveive_temp[64];  // 接收数据的暂存中心,具体处理都是用这个来处理的
const int data_receive_buff_len = sizeof(data_reveive_temp);  // 接收的数据的长度
static volatile uint8_t finish_receive = 0;  // 1代表完成接收,有数据要处理,0代表没数据
static uint8_t data_send_buff[64];  // 数据发送缓存数组
static uint8_t data_send_temp[64];  // 数据发送暂存中心
const int data_send_buff_len = sizeof(data_send_temp);  // 要发送的数据的长度
static uint8_t send_offset = 0;  // 要发送数据的偏移量
static uint8_t send_data_len = 0;  // 要发送数据的长度
static uint8_t send_data_count = 0;  // 发送的数据记录长度
static uint8_t data_counter = 0;  // 记录接收了多少个数据
const uint8_t CMD_READ_POWER = 0x01;  // 读电压值
const uint8_t CMD_CONTROL_LIGHT = 0x41;  // 控制4个灯
const uint8_t CMD_CONTRIL_GIMBAL = 0x42;  // 控制云台
static void float_to_uint8_array(uint8_t *array, float value);  // float转为uint8_t
static HAL_StatusTypeDef current_mode;
static uint8_t tiaoshi1 = 0;
extern volatile uint8_t beer_ring_mode;  // 控制蜂鸣器叫的函数

static void float_to_uint8_array(uint8_t *array, float value)
{
    // 使用指针访问 float 的字节表示
    uint8_t *float_bytes = (uint8_t *)&value;
    // 保证小端字节序
    for (int i = 0; i < sizeof(float); i++) {
        array[i] = float_bytes[i];
    }
}

void Orin_IIC_Init(void)
{
    HAL_I2C_EnableListen_IT(&hi2c1);  // 使能I2C1的侦听中断
}

// 侦听完成回调函数(完成一次完整的i2c通信以后会进入该函数)
void HAL_I2C_ListenCpltCallback(I2C_HandleTypeDef *hi2c)
{
    protocol_state = STATE_WAIT_CMD;
    send_offset = 0;
    send_data_len = 0;
    data_counter = 0;
    send_data_count = 0;
    HAL_I2C_EnableListen_IT(hi2c);
}

// I2C设备地址回调函数(地址匹配上以后会进入该函数)
void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode)
{
    // 主机写,接收数据
    if (TransferDirection == I2C_DIRECTION_TRANSMIT){
    // 接收第一个命令字,主机发送的情况下该函数只会进入一次
        // 接收命令的状态
        if (protocol_state == STATE_WAIT_CMD) {
            // 即当前的协议是啥样的
            HAL_I2C_Slave_Seq_Receive_IT(hi2c, &data_receive_buff[0], 1, I2C_NEXT_FRAME);
        }
        // 主机读数据,从机发送数据
    } else {
        switch (data_receive_buff[0]) {
            case CMD_READ_POWER:
            {
                // 复制一份副本数据,防止在发送数据的过程中数据被修改导致出错
                memcpy(data_send_temp, data_send_buff, data_send_buff_len);
                send_offset = SEND_POWER_OFFSET;
                send_data_len = SEND_POWEER_LEN;
                beer_ring_mode = 1;
                break;
            }
            default:
            {
                break;
            }
        }
        if(send_data_len == 1){
            HAL_I2C_Slave_Seq_Transmit_IT(hi2c, &data_send_temp[send_offset], 1, I2C_LAST_FRAME);
        }else if(send_data_len <= 0){
            // 会进入到这里说明收到的数据有问题,程序会自动检测问题并解决!
            // 主机请求数据时,如果有如何问题,会从此处跳出去自动恢复
        }else{
            HAL_I2C_Slave_Seq_Transmit_IT(hi2c, &data_send_temp[send_offset], 1, I2C_NEXT_FRAME);
        }
    }
}

// I2C数据接收回调函数(在I2C完成一次接收时会关闭中断并调用该函数,因此在处理完成后需要手动重新打开中断)
void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c)
{
    switch (protocol_state) {
        case STATE_WAIT_CMD:
        {
            while(!data_receive_buff[0]);
            //  此判断意味着为主机读数据,从机写数据
            if((data_receive_buff[0] == CMD_CONTROL_LIGHT) || (data_receive_buff[0] == CMD_CONTRIL_GIMBAL)){
                protocol_state = STATE_WAIT_LENGTH;
                // 接收数据长度
                HAL_I2C_Slave_Seq_Receive_IT(hi2c, &data_receive_buff[1], 1, I2C_NEXT_FRAME);
            }else if(data_receive_buff[0] == CMD_READ_POWER)
            {
                // 发送数据指令排除
            }else{
                // 其他没用的指令
            }
            break;
        }
        case STATE_WAIT_LENGTH:
        {
            protocol_state = STATE_WAIT_DATA;
            data_counter = 0;
            // 准备接收数据
            HAL_I2C_Slave_Seq_Receive_IT(hi2c, &data_receive_buff[2], 1, I2C_NEXT_FRAME);
            break;
        }
        case STATE_WAIT_DATA:
        {
            data_counter++;
            if (data_counter >= data_receive_buff[1]) {
                protocol_state = STATE_WAIT_CHECKSUM;
                // 接收校验字节
                HAL_I2C_Slave_Seq_Receive_IT(hi2c, &data_receive_buff[data_receive_buff[1]+2], 1, I2C_LAST_FRAME);
            }else
            {
                HAL_I2C_Slave_Seq_Receive_IT(hi2c, &data_receive_buff[2+data_counter], 1, I2C_NEXT_FRAME);
            }
            break;
        }
        case STATE_WAIT_CHECKSUM:
        {
            if(data_receive_buff[data_receive_buff[1]+2] == do_crc_table(data_receive_buff, data_receive_buff[1]+2))
            {
                memcpy(data_reveive_temp, data_receive_buff, data_receive_buff_len);
                finish_receive = 1;
            }
            else
            {
                // 校验有误,初始化为0,同时蜂鸣器响一下
                memset(data_receive_buff, 0, sizeof(data_receive_buff));
                // 蜂鸣器响
                beer_ring_mode = 2;
            }
            protocol_state = STATE_WAIT_CMD; // 复位状态
            break;
        }
    }
}

// I2C数据发送回调函数(在I2C完成一次发送后会关闭中断并调用该函数,因此在处理完成后需要手动重新打开中断)
void HAL_I2C_SlaveTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
    send_data_count ++;
    // 判断数据传输完了没有
    if(send_data_len != 0)
    {
        if(send_data_count < send_data_len - 1)
        { 
            HAL_I2C_Slave_Seq_Transmit_IT(hi2c, &data_send_temp[send_offset + send_data_count], 1, I2C_NEXT_FRAME);
        }else if(send_data_count == send_data_len - 1){
            HAL_I2C_Slave_Seq_Transmit_IT(hi2c, &data_send_temp[send_offset + send_data_count], 1, I2C_LAST_FRAME);
        }
    }else
    {
      beer_ring_mode = 2;
    }
}

// 错误回调函数
void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c)
{
    // 获取错误类型
    uint32_t errors = HAL_I2C_GetError(hi2c);
    if (errors & (HAL_I2C_ERROR_BERR | HAL_I2C_ERROR_ARLO | HAL_I2C_ERROR_AF)) {
        // 重置 I2C 外设
        HAL_I2C_DeInit(hi2c);
        MX_I2C1_Init();  // 重新初始化
        Orin_IIC_Init();
    }
}

// 刷新数据缓存区的数据
void Orin_Flash_Data(void)
{
    // 刷新电源电压数据
    float power_temp = Get_Power_ADC_Value();
    float_to_uint8_array(data_send_buff, power_temp);
}

// 处理接收的数据
void Orin_IIC_Data_Parse(void)
{
    if(finish_receive){
        switch(data_reveive_temp[0]) {
            case CMD_CONTROL_LIGHT:
            {
                //            1    2    3    4
                // 0x41 0x04 0x64 0x00 0x32 0x25 0xB8
                // TIM3 TIM_CHANNEL_3对应板子上的灯1
                // TIM3 TIM_CHANNEL_4对应板子上的灯2
                // TIM2 TIM_CHANNEL_1对应板子上的灯3
                // TIM2 TIM_CHANNEL_2对应板子上的灯4
                // ReverseLedState();
                beer_ring_mode = 1;
                if(data_reveive_temp[2] > 100) data_reveive_temp[2] = 100;
                if(data_reveive_temp[3] > 100) data_reveive_temp[3] = 100;
                if(data_reveive_temp[4] > 100) data_reveive_temp[4] = 100;
                if(data_reveive_temp[5] > 100) data_reveive_temp[5] = 100;
                FloodLightPWMSetDutyRatio((float)data_reveive_temp[2]/100, &htim3, TIM_CHANNEL_3);
                FloodLightPWMSetDutyRatio((float)data_reveive_temp[3]/100, &htim3, TIM_CHANNEL_4);
                FloodLightPWMSetDutyRatio((float)data_reveive_temp[4]/100, &htim2, TIM_CHANNEL_1);
                FloodLightPWMSetDutyRatio((float)data_reveive_temp[5]/100, &htim2, TIM_CHANNEL_2);
                break;

            }
            case CMD_CONTRIL_GIMBAL:
            {
//                ReverseLedState();
//                beer_ring_mode = 1;
//                // 0x42 0x02 0x00 0x00 0x07(大端模式)
//                int16_t pitch_angle = 0xFF;
//                pitch_angle &= (data_reveive_temp[2] << 8);
//                pitch_angle &= data_reveive_temp[3];
//                Set_Gimbal_Pitch(pitch_angle);
                break;
            }
            default:
            {
                beer_ring_mode = 2;  // 嘀嘀嘀三声,表示没有此写指令
                break;
            }
        }
        finish_receive = 0;
    }

}

iic出错后自愈代码

不使用DMA时,若代码接收到错误协议发过来的数据时有时候会遇到一些意想不到的情况导致总线一直被占用,此时可以在freertos中添加一个任务监视总线的情况,如果有问题可以进行总线的释放,来恢复iic通信,如下:

c 复制代码
// freertos_task.c
void ReleaseBus(void const * argument)
{
  /* USER CODE BEGIN ReleaseBus */
  /* Infinite loop */
  // 每20ms检测一次总线是否有问题,若连续检测出5次则重新初始化!
  for(;;)
  {
    if (HAL_GPIO_ReadPin(I2C_GPIO_PORT, GPIO_PIN_SCL) == GPIO_PIN_RESET || 
        HAL_GPIO_ReadPin(I2C_GPIO_PORT, GPIO_PIN_SDA) == GPIO_PIN_RESET) {
            count_iic_bus_error ++;
        }else {
            count_iic_bus_error = 0;
        }
    if (count_iic_bus_error >= 5)
    {
        I2C_BusRecover();
    }
  }
  /* USER CODE END ReleaseBus */
}
c 复制代码
// release.c
void I2C_BusRecover(void) {
    // 1. 临时配置 SCL/SDA 为开漏输出
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = GPIO_PIN_SCL | GPIO_PIN_SDA;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 开漏输出
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(I2C_GPIO_PORT, &GPIO_InitStruct);

    // 2. 尝试释放 SDA 线
    HAL_GPIO_WritePin(I2C_GPIO_PORT, GPIO_PIN_SCL, GPIO_PIN_SET); // SCL 高
    HAL_GPIO_WritePin(I2C_GPIO_PORT, GPIO_PIN_SDA, GPIO_PIN_SET); // SDA 高
    osDelay(1);

    // 3. 生成 9 个 SCL 脉冲(I2C 协议要求)
    for (int i = 0; i < 9; i++) {
        // SCL 拉低
        HAL_GPIO_WritePin(I2C_GPIO_PORT, GPIO_PIN_SCL, GPIO_PIN_RESET);
        osDelay(1);
        // SCL 拉高,等待 SDA 被释放
        HAL_GPIO_WritePin(I2C_GPIO_PORT, GPIO_PIN_SCL, GPIO_PIN_SET);
        osDelay(1);
        // 检查 SDA 是否变为高电平
        if (HAL_GPIO_ReadPin(I2C_GPIO_PORT, GPIO_PIN_SDA) == GPIO_PIN_SET) {
            break; // SDA 已释放,退出循环
        }
    }

    // 4. 发送 STOP 条件(可选)
    HAL_GPIO_WritePin(I2C_GPIO_PORT, GPIO_PIN_SDA, GPIO_PIN_RESET);
    osDelay(1);
    HAL_GPIO_WritePin(I2C_GPIO_PORT, GPIO_PIN_SDA, GPIO_PIN_SET);
    osDelay(1);

    // 5. 恢复 GPIO 为 I2C 复用功能
    GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; // 复用开漏
    HAL_GPIO_Init(I2C_GPIO_PORT, &GPIO_InitStruct);

    // 6. 重新初始化 I2C 外设
    HAL_I2C_DeInit(&hi2c1);
    MX_I2C1_Init(); // 重新调用初始化函数
    Orin_IIC_Init();  // 重新启动监听iic中断
}

void Orin_IIC_Init(void)
{
    HAL_I2C_EnableListen_IT(&hi2c1);  // 使能I2C1的侦听中断
}