最近做一个项目需要将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[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;
- }
- }
复制代码- // 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 */
- }
复制代码 来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |