找回密码
 立即注册
首页 业界区 业界 stm32cubemx+freertos+中断实现IIC从机

stm32cubemx+freertos+中断实现IIC从机

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

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

  • 主机发送数据(从机接收数据)

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


  • 主机读取数据(从机发送数据)

  • 起始信号(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线(产生上升沿),结束通信。
  1. 流程示意图:
  2. START → 地址+写 → ACK → 寄存器地址 → ACK → 重复START → 地址+读 → ACK → 接收数据 → (ACK/NACK) → STOP
复制代码
2.png

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

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

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

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

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

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

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

7.png

注意:理论上随时都可以开启这个,但是一般在初始化的时候开启,同时得注意后面如果还有其他初始化的东西且比较耗时的话,先将中断关闭!
  1. __disable_irq();
  2. // 中间放其他的初始化代码!
  3. __enable_irq();
复制代码
重写以下几个函数,并在里面实现具体的通信逻辑:
  1. // I2C设备地址回调函数(地址匹配上以后会进入该函数)
  2. void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode);
  3. // I2C数据接收回调函数(在I2C完成一次接收时会关闭中断并调用该函数,因此在处理完成后需要手动重新打开中断)
  4. void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c);
  5. // I2C数据发送回调函数(在I2C完成一次发送后会关闭中断并调用该函数,因此在处理完成后需要手动重新打开中断)
  6. void HAL_I2C_SlaveTxCpltCallback(I2C_HandleTypeDef *hi2c);
  7. // 错误回调函数
  8. void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c);
  9. // 侦听完成回调函数(完成一次完整的i2c通信以后会进入该函数)
  10. void HAL_I2C_ListenCpltCallback(I2C_HandleTypeDef *hi2c);
复制代码
注意,本教程是没有使用DMA来实现IIC从机的,如果使用了DMA可以方便很多,编程难度也会下降非常多
具体实现逻辑下面给一份实测可以使用的示例代码,注意此处代码只能做参考,需要略微修改按照才能移植使用
  1. // iic.h
  2. #ifndef ORIN_IIC_H
  3. #define ORIN_IIC_H
  4. #include "stm32f1xx_hal.h"
  5. #include "i2c.h"
  6. #include "FreeRTOS.h"
  7. #include "cmsis_os.h"
  8. #include "task.h"
  9. #include "crc8.h"
  10. #include "power_adc.h"
  11. #include "string.h"
  12. #include "board_led.h"
  13. #include "floodlight.h"
  14. #include "gimbal.h"
  15. #define SLAVE_ADDRESS 0x40  // 设置的从机地址为0x40
  16. #define GPIO_PIN_SCL GPIO_PIN_6
  17. #define GPIO_PIN_SDA GPIO_PIN_7
  18. #define I2C_GPIO_PORT GPIOB
  19. // 电源数据的位置和长度
  20. #define SEND_POWER_OFFSET 0
  21. #define SEND_POWEER_LEN 4
  22. typedef enum {
  23.     STATE_WAIT_CMD,      // 等待命令
  24.     STATE_WAIT_LENGTH,   // 等待数据长度
  25.     STATE_WAIT_DATA,     // 等待数据
  26.     STATE_WAIT_CHECKSUM  // 等待校验
  27. } ProtocolState;
  28. void Orin_IIC_Init(void);  // orin_iic的初始化操作
  29. void HAL_I2C_ListenCpltCallback(I2C_HandleTypeDef *hi2c);
  30. void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode);
  31. void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c);
  32. void HAL_I2C_SlaveTxCpltCallback(I2C_HandleTypeDef *hi2c);
  33. void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c);  // 错误回调函数
  34. void Orin_Flash_Data(void);  // 刷新要发送的数据
  35. void Orin_IIC_Data_Parse(void);  // 处理接收的数据
  36. #endif // ORIN_IIC_H
复制代码
  1. // iic.c
  2. #include "orin_iic.h"
  3. extern I2C_HandleTypeDef hi2c1;
  4. extern TIM_HandleTypeDef htim2;
  5. extern TIM_HandleTypeDef htim3;
  6. static ProtocolState protocol_state = STATE_WAIT_CMD;  // 接收数据的各种状态
  7. static uint8_t data_receive_buff[64];  // 接收数据的缓存数组
  8. static uint8_t data_reveive_temp[64];  // 接收数据的暂存中心,具体处理都是用这个来处理的
  9. const int data_receive_buff_len = sizeof(data_reveive_temp);  // 接收的数据的长度
  10. static volatile uint8_t finish_receive = 0;  // 1代表完成接收,有数据要处理,0代表没数据
  11. static uint8_t data_send_buff[64];  // 数据发送缓存数组
  12. static uint8_t data_send_temp[64];  // 数据发送暂存中心
  13. const int data_send_buff_len = sizeof(data_send_temp);  // 要发送的数据的长度
  14. static uint8_t send_offset = 0;  // 要发送数据的偏移量
  15. static uint8_t send_data_len = 0;  // 要发送数据的长度
  16. static uint8_t send_data_count = 0;  // 发送的数据记录长度
  17. static uint8_t data_counter = 0;  // 记录接收了多少个数据
  18. const uint8_t CMD_READ_POWER = 0x01;  // 读电压值
  19. const uint8_t CMD_CONTROL_LIGHT = 0x41;  // 控制4个灯
  20. const uint8_t CMD_CONTRIL_GIMBAL = 0x42;  // 控制云台
  21. static void float_to_uint8_array(uint8_t *array, float value);  // float转为uint8_t
  22. static HAL_StatusTypeDef current_mode;
  23. static uint8_t tiaoshi1 = 0;
  24. extern volatile uint8_t beer_ring_mode;  // 控制蜂鸣器叫的函数
  25. static void float_to_uint8_array(uint8_t *array, float value)
  26. {
  27.     // 使用指针访问 float 的字节表示
  28.     uint8_t *float_bytes = (uint8_t *)&value;
  29.     // 保证小端字节序
  30.     for (int i = 0; i < sizeof(float); i++) {
  31.         array[i] = float_bytes[i];
  32.     }
  33. }
  34. void Orin_IIC_Init(void)
  35. {
  36.     HAL_I2C_EnableListen_IT(&hi2c1);  // 使能I2C1的侦听中断
  37. }
  38. // 侦听完成回调函数(完成一次完整的i2c通信以后会进入该函数)
  39. void HAL_I2C_ListenCpltCallback(I2C_HandleTypeDef *hi2c)
  40. {
  41.     protocol_state = STATE_WAIT_CMD;
  42.     send_offset = 0;
  43.     send_data_len = 0;
  44.     data_counter = 0;
  45.     send_data_count = 0;
  46.     HAL_I2C_EnableListen_IT(hi2c);
  47. }
  48. // I2C设备地址回调函数(地址匹配上以后会进入该函数)
  49. void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode)
  50. {
  51.     // 主机写,接收数据
  52.     if (TransferDirection == I2C_DIRECTION_TRANSMIT){
  53.     // 接收第一个命令字,主机发送的情况下该函数只会进入一次
  54.         // 接收命令的状态
  55.         if (protocol_state == STATE_WAIT_CMD) {
  56.             // 即当前的协议是啥样的
  57.             HAL_I2C_Slave_Seq_Receive_IT(hi2c, &data_receive_buff[0], 1, I2C_NEXT_FRAME);
  58.         }
  59.         // 主机读数据,从机发送数据
  60.     } else {
  61.         switch (data_receive_buff[0]) {
  62.             case CMD_READ_POWER:
  63.             {
  64.                 // 复制一份副本数据,防止在发送数据的过程中数据被修改导致出错
  65.                 memcpy(data_send_temp, data_send_buff, data_send_buff_len);
  66.                 send_offset = SEND_POWER_OFFSET;
  67.                 send_data_len = SEND_POWEER_LEN;
  68.                 beer_ring_mode = 1;
  69.                 break;
  70.             }
  71.             default:
  72.             {
  73.                 break;
  74.             }
  75.         }
  76.         if(send_data_len == 1){
  77.             HAL_I2C_Slave_Seq_Transmit_IT(hi2c, &data_send_temp[send_offset], 1, I2C_LAST_FRAME);
  78.         }else if(send_data_len <= 0){
  79.             // 会进入到这里说明收到的数据有问题,程序会自动检测问题并解决!
  80.             // 主机请求数据时,如果有如何问题,会从此处跳出去自动恢复
  81.         }else{
  82.             HAL_I2C_Slave_Seq_Transmit_IT(hi2c, &data_send_temp[send_offset], 1, I2C_NEXT_FRAME);
  83.         }
  84.     }
  85. }
  86. // I2C数据接收回调函数(在I2C完成一次接收时会关闭中断并调用该函数,因此在处理完成后需要手动重新打开中断)
  87. void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c)
  88. {
  89.     switch (protocol_state) {
  90.         case STATE_WAIT_CMD:
  91.         {
  92.             while(!data_receive_buff[0]);
  93.             //  此判断意味着为主机读数据,从机写数据
  94.             if((data_receive_buff[0] == CMD_CONTROL_LIGHT) || (data_receive_buff[0] == CMD_CONTRIL_GIMBAL)){
  95.                 protocol_state = STATE_WAIT_LENGTH;
  96.                 // 接收数据长度
  97.                 HAL_I2C_Slave_Seq_Receive_IT(hi2c, &data_receive_buff[1], 1, I2C_NEXT_FRAME);
  98.             }else if(data_receive_buff[0] == CMD_READ_POWER)
  99.             {
  100.                 // 发送数据指令排除
  101.             }else{
  102.                 // 其他没用的指令
  103.             }
  104.             break;
  105.         }
  106.         case STATE_WAIT_LENGTH:
  107.         {
  108.             protocol_state = STATE_WAIT_DATA;
  109.             data_counter = 0;
  110.             // 准备接收数据
  111.             HAL_I2C_Slave_Seq_Receive_IT(hi2c, &data_receive_buff[2], 1, I2C_NEXT_FRAME);
  112.             break;
  113.         }
  114.         case STATE_WAIT_DATA:
  115.         {
  116.             data_counter++;
  117.             if (data_counter >= data_receive_buff[1]) {
  118.                 protocol_state = STATE_WAIT_CHECKSUM;
  119.                 // 接收校验字节
  120.                 HAL_I2C_Slave_Seq_Receive_IT(hi2c, &data_receive_buff[data_receive_buff[1]+2], 1, I2C_LAST_FRAME);
  121.             }else
  122.             {
  123.                 HAL_I2C_Slave_Seq_Receive_IT(hi2c, &data_receive_buff[2+data_counter], 1, I2C_NEXT_FRAME);
  124.             }
  125.             break;
  126.         }
  127.         case STATE_WAIT_CHECKSUM:
  128.         {
  129.             if(data_receive_buff[data_receive_buff[1]+2] == do_crc_table(data_receive_buff, data_receive_buff[1]+2))
  130.             {
  131.                 memcpy(data_reveive_temp, data_receive_buff, data_receive_buff_len);
  132.                 finish_receive = 1;
  133.             }
  134.             else
  135.             {
  136.                 // 校验有误,初始化为0,同时蜂鸣器响一下
  137.                 memset(data_receive_buff, 0, sizeof(data_receive_buff));
  138.                 // 蜂鸣器响
  139.                 beer_ring_mode = 2;
  140.             }
  141.             protocol_state = STATE_WAIT_CMD; // 复位状态
  142.             break;
  143.         }
  144.     }
  145. }
  146. // I2C数据发送回调函数(在I2C完成一次发送后会关闭中断并调用该函数,因此在处理完成后需要手动重新打开中断)
  147. void HAL_I2C_SlaveTxCpltCallback(I2C_HandleTypeDef *hi2c)
  148. {
  149.     send_data_count ++;
  150.     // 判断数据传输完了没有
  151.     if(send_data_len != 0)
  152.     {
  153.         if(send_data_count < send_data_len - 1)
  154.         {
  155.             HAL_I2C_Slave_Seq_Transmit_IT(hi2c, &data_send_temp[send_offset + send_data_count], 1, I2C_NEXT_FRAME);
  156.         }else if(send_data_count == send_data_len - 1){
  157.             HAL_I2C_Slave_Seq_Transmit_IT(hi2c, &data_send_temp[send_offset + send_data_count], 1, I2C_LAST_FRAME);
  158.         }
  159.     }else
  160.     {
  161.       beer_ring_mode = 2;
  162.     }
  163. }
  164. // 错误回调函数
  165. void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c)
  166. {
  167.     // 获取错误类型
  168.     uint32_t errors = HAL_I2C_GetError(hi2c);
  169.     if (errors & (HAL_I2C_ERROR_BERR | HAL_I2C_ERROR_ARLO | HAL_I2C_ERROR_AF)) {
  170.         // 重置 I2C 外设
  171.         HAL_I2C_DeInit(hi2c);
  172.         MX_I2C1_Init();  // 重新初始化
  173.         Orin_IIC_Init();
  174.     }
  175. }
  176. // 刷新数据缓存区的数据
  177. void Orin_Flash_Data(void)
  178. {
  179.     // 刷新电源电压数据
  180.     float power_temp = Get_Power_ADC_Value();
  181.     float_to_uint8_array(data_send_buff, power_temp);
  182. }
  183. // 处理接收的数据
  184. void Orin_IIC_Data_Parse(void)
  185. {
  186.     if(finish_receive){
  187.         switch(data_reveive_temp[0]) {
  188.             case CMD_CONTROL_LIGHT:
  189.             {
  190.                 //            1    2    3    4
  191.                 // 0x41 0x04 0x64 0x00 0x32 0x25 0xB8
  192.                 // TIM3 TIM_CHANNEL_3对应板子上的灯1
  193.                 // TIM3 TIM_CHANNEL_4对应板子上的灯2
  194.                 // TIM2 TIM_CHANNEL_1对应板子上的灯3
  195.                 // TIM2 TIM_CHANNEL_2对应板子上的灯4
  196.                 // ReverseLedState();
  197.                 beer_ring_mode = 1;
  198.                 if(data_reveive_temp[2] > 100) data_reveive_temp[2] = 100;
  199.                 if(data_reveive_temp[3] > 100) data_reveive_temp[3] = 100;
  200.                 if(data_reveive_temp[4] > 100) data_reveive_temp[4] = 100;
  201.                 if(data_reveive_temp[5] > 100) data_reveive_temp[5] = 100;
  202.                 FloodLightPWMSetDutyRatio((float)data_reveive_temp[2]/100, &htim3, TIM_CHANNEL_3);
  203.                 FloodLightPWMSetDutyRatio((float)data_reveive_temp[3]/100, &htim3, TIM_CHANNEL_4);
  204.                 FloodLightPWMSetDutyRatio((float)data_reveive_temp[4]/100, &htim2, TIM_CHANNEL_1);
  205.                 FloodLightPWMSetDutyRatio((float)data_reveive_temp[5]/100, &htim2, TIM_CHANNEL_2);
  206.                 break;
  207.             }
  208.             case CMD_CONTRIL_GIMBAL:
  209.             {
  210. //                ReverseLedState();
  211. //                beer_ring_mode = 1;
  212. //                // 0x42 0x02 0x00 0x00 0x07(大端模式)
  213. //                int16_t pitch_angle = 0xFF;
  214. //                pitch_angle &= (data_reveive_temp[2] << 8);
  215. //                pitch_angle &= data_reveive_temp[3];
  216. //                Set_Gimbal_Pitch(pitch_angle);
  217.                 break;
  218.             }
  219.             default:
  220.             {
  221.                 beer_ring_mode = 2;  // 嘀嘀嘀三声,表示没有此写指令
  222.                 break;
  223.             }
  224.         }
  225.         finish_receive = 0;
  226.     }
  227. }
复制代码
  1. // freertos_task.c
  2. void ReleaseBus(void const * argument)
  3. {
  4.   /* USER CODE BEGIN ReleaseBus */
  5.   /* Infinite loop */
  6.   // 每20ms检测一次总线是否有问题,若连续检测出5次则重新初始化!
  7.   for(;;)
  8.   {
  9.     if (HAL_GPIO_ReadPin(I2C_GPIO_PORT, GPIO_PIN_SCL) == GPIO_PIN_RESET ||
  10.         HAL_GPIO_ReadPin(I2C_GPIO_PORT, GPIO_PIN_SDA) == GPIO_PIN_RESET) {
  11.             count_iic_bus_error ++;
  12.         }else {
  13.             count_iic_bus_error = 0;
  14.         }
  15.     if (count_iic_bus_error >= 5)
  16.     {
  17.         I2C_BusRecover();
  18.     }
  19.   }
  20.   /* USER CODE END ReleaseBus */
  21. }
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册