聚怪闩 发表于 2025-6-5 14:40:14

stm32cubemx+freertos+中断实现IIC从机

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

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

[*]主机发送数据(从机接收数据)

[*]起始信号(START)
主机在检测到总线为“空闲状态”(即 SDA、SCL 线均为高电平)时,发送一个启动信号“S”,开始一次通信。
[*]发送从机地址和写命令
主机接着发送一个命令字节。该字节由 7 位的外围器件地址和 1 位读写控制位 R/W 组成(此时 R/W=0 表示写操作)。
[*]从机应答
相对应的从机收到命令字节后向主机回馈应答信号 ACK(ACK=0),表示地址匹配并准备好接收数据。
[*]发送数据字节
主机收到从机的应答信号后开始发送第一个字节的数据。从机收到数据后返回一个应答信号 ACK。主机收到应答信号后再发送下一个数据字节。
[*]结束通信
当主机发送最后一个数据字节并收到从机的 ACK 后,通过向从机发送一个停止信号 P 结束本次通信并释放总线。从机收到 P 信号后也退出与主机之间的通信。
流程示意图:
┌───────┐    ┌─────────────┐    ┌───────┐    ┌──────────┐    ┌───────┐   ┌────────┐
│ START │ →│ 地址+写命令 │ →│ ACK│ →│ 数据字节 │ →│ ACK│ → ... →│ STOP   │
└───────┘    └─────────────┘    └───────┘    └──────────┘    └───────┘   └────────┘

[*]主机读取数据(从机发送数据)

[*]起始信号(START)
主机拉低SDA线(在SCL高电平期间产生下降沿),表示通信开始。
[*]发送从机地址+写命令

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

[*]发送寄存器地址

[*]主机发送8位寄存器地址,指定需要读取的从机内部寄存器位置。
[*]从机再次返回ACK确认接收成功。

[*]重复起始信号(Repeated START)
主机在未发送停止信号的情况下,再次产生起始信号,切换到读模式。
[*]发送从机地址+读命令

[*]重新发送7位从机地址,方向控制位改为1(读模式)。
[*]从机返回ACK,准备发送数据。

[*]接收数据并应答

[*]从机发送数据:从机在SCL的每个上升沿将数据位输出到SDA线,高位优先。
[*]主机应答
:每接收完8位数据,主机在第9个时钟周期:

[*]若需继续读取:发送ACK(SDA拉低)。
[*]若结束读取:发送NACK(SDA保持高电平)。


[*]停止信号(STOP)
主机在SCL高电平期间拉高SDA线(产生上升沿),结束通信。
流程示意图:
START → 地址+写 → ACK → 寄存器地址 → ACK → 重复START → 地址+读 → ACK → 接收数据 → (ACK/NACK) → STOP
这种“先写后读”的设计源于IIC协议的寄存器寻址机制,主要原因如下:

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

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

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

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

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

freertos的配置更简单,我这里开的是CMSIS_V1,其他的配置默认即可
生成代码后打开,进行以下操作,如下所示:
首先启动监听:
HAL_I2C_EnableListen_IT(&hi2c1);// 使能I2C1的侦听中断

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

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

__enable_irq();重写以下几个函数,并在里面实现具体的通信逻辑:
// 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可以方便很多,编程难度也会下降非常多
具体实现逻辑下面给一份实测可以使用的示例代码,注意此处代码只能做参考,需要略微修改按照才能移植使用
// 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// 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;// 接收数据的缓存数组
static uint8_t data_reveive_temp;// 接收数据的暂存中心,具体处理都是用这个来处理的
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;// 数据发送缓存数组
static uint8_t data_send_temp;// 数据发送暂存中心
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 = float_bytes;
    }
}

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, 1, I2C_NEXT_FRAME);
      }
      // 主机读数据,从机发送数据
    } else {
      switch (data_receive_buff) {
            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, 1, I2C_LAST_FRAME);
      }else if(send_data_len <= 0){
            // 会进入到这里说明收到的数据有问题,程序会自动检测问题并解决!
            // 主机请求数据时,如果有如何问题,会从此处跳出去自动恢复
      }else{
            HAL_I2C_Slave_Seq_Transmit_IT(hi2c, &data_send_temp, 1, I2C_NEXT_FRAME);
      }
    }
}

// I2C数据接收回调函数(在I2C完成一次接收时会关闭中断并调用该函数,因此在处理完成后需要手动重新打开中断)
void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c)
{
    switch (protocol_state) {
      case STATE_WAIT_CMD:
      {
            while(!data_receive_buff);
            //此判断意味着为主机读数据,从机写数据
            if((data_receive_buff == CMD_CONTROL_LIGHT) || (data_receive_buff == CMD_CONTRIL_GIMBAL)){
                protocol_state = STATE_WAIT_LENGTH;
                // 接收数据长度
                HAL_I2C_Slave_Seq_Receive_IT(hi2c, &data_receive_buff, 1, I2C_NEXT_FRAME);
            }else if(data_receive_buff == 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, 1, I2C_NEXT_FRAME);
            break;
      }
      case STATE_WAIT_DATA:
      {
            data_counter++;
            if (data_counter >= data_receive_buff) {
                protocol_state = STATE_WAIT_CHECKSUM;
                // 接收校验字节
                HAL_I2C_Slave_Seq_Receive_IT(hi2c, &data_receive_buff+2], 1, I2C_LAST_FRAME);
            }else
            {
                HAL_I2C_Slave_Seq_Receive_IT(hi2c, &data_receive_buff, 1, I2C_NEXT_FRAME);
            }
            break;
      }
      case STATE_WAIT_CHECKSUM:
      {
            if(data_receive_buff+2] == do_crc_table(data_receive_buff, data_receive_buff+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, 1, I2C_NEXT_FRAME);
      }else if(send_data_count == send_data_len - 1){
            HAL_I2C_Slave_Seq_Transmit_IT(hi2c, &data_send_temp, 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) {
            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 > 100) data_reveive_temp = 100;
                if(data_reveive_temp > 100) data_reveive_temp = 100;
                if(data_reveive_temp > 100) data_reveive_temp = 100;
                if(data_reveive_temp > 100) data_reveive_temp = 100;
                FloodLightPWMSetDutyRatio((float)data_reveive_temp/100, &htim3, TIM_CHANNEL_3);
                FloodLightPWMSetDutyRatio((float)data_reveive_temp/100, &htim3, TIM_CHANNEL_4);
                FloodLightPWMSetDutyRatio((float)data_reveive_temp/100, &htim2, TIM_CHANNEL_1);
                FloodLightPWMSetDutyRatio((float)data_reveive_temp/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 << 8);
//                pitch_angle &= data_reveive_temp;
//                Set_Gimbal_Pitch(pitch_angle);
                break;
            }
            default:
            {
                beer_ring_mode = 2;// 嘀嘀嘀三声,表示没有此写指令
                break;
            }
      }
      finish_receive = 0;
    }

}// 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 */
}
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: stm32cubemx+freertos+中断实现IIC从机