STM32 HAL库 硬件I2C 从机主机防BUG代码

前言

最近死磕了5天的STM32F1硬件I2C从机的程序,天天早上8点到凌晨,几乎全程心流状态。终于在结合各方资料及自己的思考后,做出了稳定的硬件I2C代码(这个文章中应该是目前为止能查到的最详述可用的硬件I2C代码),经过I2C主机发出的各种奇怪的信号蹂躏后,通讯都可以恢复正常,不会被卡死。证明该方案拥有极高稳定性。

需要注意我这次使用的是 STM32F103C8T6 的兼容型号 GD32F103C8T6 。要问他的兼容性有多强,连I2C bug都能做到一样,哈哈。我当初用GD想着硬件I2C应该能舒服用了,万万没想到,兆易连i2c 硬件BUG都复制了。

大家不要纠结于单片机的型号,我推测应该STM32FXXX 家族硬件I2C应该都是这个样子,具体情况我也无法一一测试,如果大家看了文章在自己的系统上测试成功后别忘了留个言,说下自己系统的配置,方便后来人~

STM32 硬件I2C BUG简述

相信很多接触STM32的用户在尝试使用I2C代码时都被警告过stm32 硬件I2C有bug,那么这个bug具体是什么,又是如何发生的呢,我们具体来分析一下,给我们之后代码解决这个问题来做个铺垫。

首先我们来看一下STM32 I2C的一个神奇的寄存器,SR2BUSY 位,具体他是做什么的参考手册中已经很清晰的描述,我就不多说了,我直接说它的问题。

当STM32 硬件I2C模块在通讯(无论做主机还是从机)中遇到总线被占用时,使得 STM32 在接管总线时发生总线仲裁失败,进而失去对总线的控制,导致BUSY位被置位,且无法通过使用官方驱动库自动清除。而后即表现为锁死状态。

该情况多会出现在I2C通讯错误,或者从机程序接收到了非预定的I2C指令时产生。
关于这个情况,在官方的一篇 《2C 接口进入 Busy 状态不能退出.pdf》 文档中也有描述,也正是这篇文档给了我思路,让我解决了这个问题。
在这里插入图片描述在这里插入图片描述

BUG实例

由于我使用的是硬件I2C从机,所以以下内容均以I2C从机为主要内容讲述,(如果你使用的是主机也请耐心看完,因为主机也可以按照该思路修改后解决)也可能之后我会补充主机内容(主要是I2C做主机时,从机大多时候都成熟器件,想要人为产生BUG情况也蛮困难的)

首先我们来看一段我用逻辑分析仪抓取的硬件系统的波形, 和造成该波形的测试代码(以下I2C写指令由主机发出,主机为其他公司的单片机承担,不建议使用两个STM32 硬件I2C对传测试,否则都出BUG找问题都找不到)
在这里插入图片描述

    while (1)
    {
        while(HAL_I2C_Slave_Receive_IT(&hi2c1, &i2c_buff, 1) != HAL_OK)
        {
        }
        while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY)
        {
        }
        delay_ms(10);
    }
  • 问题分析:

我们可以看到,由于我的代码中只有对一个I2C写指令接收的内容,而后就进入了延时等待状态,从而错过了主机连着发送的第二条写指令,我们的单片机甚至都没有对其做ACK响应。此时我们用调试器查看STM32寄存器,就会发现 BUSY进入了锁死状态。第二次进入的HAL_I2C_Slave_Receive_IT() 函数中会在等待 BUSY 清零的查询操作中超时退出,而后在之后的循环中再次循环这个过程。

而如果我们把循环中的延时去掉,我们会发现I2C的通讯就会变得正常了。即, 由于我们没能对主机发出的第二条的I2C指令及时处理,STM32 I2C就会出问题。而这种情况在使用中是不可能避免的,主机任何一次的误操作或误编程都可能会导致我们的I2C锁死。

解决方案

放上代码

void i2c_reset()
{
    /* 开漏输出,关闭I2C输入通道,并尝试将总线拉高 */
    GPIO_InitTypeDef GPIO_InitStruct;
    GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8 | GPIO_PIN_9, GPIO_PIN_SET);

    // SCL PB8 拉高
    for (uint8_t i = 0; i < 10; i++) {

        if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_8) == GPIO_PIN_SET)
        {
            rt_kprintf("retry %d\n", i);
            break;
        }
        /* 该延时循环的周期和时长,请根据你的实际主机对I2C通讯出错的处理来修改 */
        rt_thread_mdelay(10);
    }

    /* 归还总线控制权 */
    GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

    /* 复位I2C */
    hi2c1.Instance->CR1 |= I2C_CR1_SWRST;
    hi2c1.Instance->CR1 &= ~I2C_CR1_SWRST;

    /* 重新初始化I2C */
    MX_I2C1_Init();
}

/*
 * I2C最大的问题在于如果主机随意发送内容,而stm32作为从机时, 接收到程序意图之外的通讯内容后会卡死BUSY位。
 * 发生该情况后,可配置SCL SDA为开漏输出,关闭I2C输入通道防止错误再次被触发,而后尝试拉高SCL SDA线,或者
 * 等待总线被主机释放,这里的情况依据具体主机的不同可根据实际操作判断,最终我们需要等待I2C总线为空闲即SCL
 * SDA线为高电平保持时。将SCL SDA配回IIC,释放总线控制权 ,使用 SWRST复位I2C,清除全部I2C寄存器内容,
 * (BUSY位也会在该操作中被清除) 并重新初始化I2C总线。
 */
void i2c_salve_thread(void *parameter)
{
    while (1)
    {
        if(HAL_I2C_Slave_Receive_IT(&hi2c1, &i2c_buff, 1) != HAL_OK)
        {
        	// I2C设备出现故障无法开启接收
            i2c_reset();
        }

		// 检测标志位,防止I2C被二次开启,导致BUG
        while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY)
        {
            rt_thread_mdelay(1);
        }
        rt_thread_mdelay(10);
    }
}

void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode)
{
    direction = TransferDirection;
}


如代码中注释,我们在检测 HAL_I2C_Slave_Receive_IT() 返回超时或错误时,使用 i2c_reset() 函数来复位I2C硬件模块以重启I2C,使得STM32可以在主机恢复发送正常设定的I2C命令后可以恢复通讯。

i2c_reset() 函数核心操作思路如下:

  1. 配置 对应IO为开漏输出,以关闭I2C模块的硬件输入通道,防止后续的通讯继续触发BUG,并尝试将总线拉高。

  2. 尝试让从机释放总线,或等待主机释放总线(此步骤根据你的实际系统为准,最好查一下波形,看看具体故障表现是什么,如果STM32是从机,那么主机在通讯失败后,大部分会在一定时间后超时并释放总线。所以我的代码中只是在一定时间内检测总线是否被释放。如果你是主机的话则可以按照 《2C 接口进入 Busy 状态不能退出.pdf》 文档中的方法,在SCL线上发送脉冲来使得从机释放I2C总线)还是一句话:该步骤根据你的实际系统为准,我们在这步中核心需求就是让总线被释放。

  3. 将引脚配置回I2C的模式,将总线归还STM32的I2C模块

  4. 将CR1寄存器的SWRST置位后再清零,以复位I2C模块,在此过程中I2C除DR寄存器外所有寄存器都会被清空,包括BUSY位 (此处记忆有些模糊,不确定DR寄存器是否会被清空。DR为I2C收发数据缓存寄存器)

  5. 重新初始化I2C,原因是我们上一步的复位操作清空了I2C的配置

加入这些代码后,我们可以再次使用逻辑分析仪观察波形,或通过串口打印来检测程序运行。(由于逻辑分析仪波形太长,图片放不下就不做展示了)我们可以看到,不管主机如何发送诱发错误的信息,我们的代码都能让 stm32在i2c复位后的第一次接收时工作正常。而如果主机按照预定协议,间隔发送指令时,通讯就会完全恢复正常,不会触发i2c_reset() 函数。

小计

  1. 如果使用 HAL_I2C_Slave_Receive_IT() 函数接收了主机发送的读取指令,并不会触发BUG,主机会读取回 0x00的数据。

  2. 例子中我使用了 RT-thread 实时操作系统,所以延时不大一样。使用RTOS的延时时还可以让我们在等待时,将CPU调度到其他线程使用,防止一个I2C占用全部CPU周期。

  3. 有了故障处理程序后,我们就可以使用 HAL 库中自带的 I2C_TwoBoards_AdvComIT 例程来处理收发数据了。我使用的官方HAL库例程路径:

     D:\ST\STM32Cube\Repository\STM32Cube_FW_F1_V1.7.0\Projects\STM32F103RB-Nucleo\Examples\I2C\I2C_TwoBoards_AdvComIT
    
  4. i2c_reset() 核心操作思路的步骤顺序有严格要求,随意变更1~5操作的前后顺序会导致BUG再次触发

  5. IIC总线调试具有特殊性,我们最好还是准备一个逻辑分析仪来抓取波形,结合DEBUG时对寄存器的查看来分析解决故障

  6. 接上一条,在调试IIC前,我们应该确保自己对STM IIC的各个寄存器和位功能以及I2C波形时序的熟悉,以求在出现问题时能够找到问题所在,新版的HAL库IIC内容很清晰,只要了解寄存器,通过DEBUG+查看波形 的方法可以很快定位解决错误。

  7. 示例代码仅提供思路,具体使用需要修改为你的配置。

  8. 注意一定要使用新版本的HAL库,我已经被旧版的有明显错误的HAL I2C库坑过了(居然直接对只读的标志位赋值,来想要清除标志位)

总结

STM32的IIC模块确实存在BUG, 具体表现就是在我们代码没有处理预设之外的IIC指令或数据时会发生由于总线仲裁失败,导致BUSY位锁死的问题。出现这种问题,只要我们能够用代码监测到此情况的发生,并使用上述的 i2c_reset()核心操作思路就可以解决,从而让我们在实际工程应用中使用。毕竟软件也许可以方便的模拟100khz的IIC主机,但对400khz I2c通讯来说,无论是主机还是从机都很难实现(从机的软件模拟技巧性很强,且CPU消耗也大)。

最后

如果这个文章,解决了你的问题,请留个言交流下,因为我目前精力有限也只是在自己的硬件上测试了这个内容,让我知道你的问题被解决会让我很开心,也会让之后的朋友不在为这个问题困扰~

因为目前我的工程涉及到自己公司的产品,不方便直接发出,之后有空了,我会做一个示例工程挂到 githubgitee 上,方便大家修改使用。

猜你喜欢

转载自blog.csdn.net/zhengyangliu123/article/details/106308915