STM32 HAL库 硬件I2C 从机主机防BUG程序

2023-11-21 20:40

本文主要是介绍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被二次开启,导致BUGwhile (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 上,方便大家修改使用。

这篇关于STM32 HAL库 硬件I2C 从机主机防BUG程序的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/404971

相关文章

uniapp接入微信小程序原生代码配置方案(优化版)

uniapp项目需要把微信小程序原生语法的功能代码嵌套过来,无需把原生代码转换为uniapp,可以配置拷贝的方式集成过来 1、拷贝代码包到src目录 2、vue.config.js中配置原生代码包直接拷贝到编译目录中 3、pages.json中配置分包目录,原生入口组件的路径 4、manifest.json中配置分包,使用原生组件 5、需要把原生代码包里的页面修改成组件的方

Java面试八股之怎么通过Java程序判断JVM是32位还是64位

怎么通过Java程序判断JVM是32位还是64位 可以通过Java程序内部检查系统属性来判断当前运行的JVM是32位还是64位。以下是一个简单的方法: public class JvmBitCheck {public static void main(String[] args) {String arch = System.getProperty("os.arch");String dataM

零基础STM32单片机编程入门(一)初识STM32单片机

文章目录 一.概要二.单片机型号命名规则三.STM32F103系统架构四.STM32F103C8T6单片机启动流程五.STM32F103C8T6单片机主要外设资源六.编程过程中芯片数据手册的作用1.单片机外设资源情况2.STM32单片机内部框图3.STM32单片机管脚图4.STM32单片机每个管脚可配功能5.单片机功耗数据6.FALSH编程时间,擦写次数7.I/O高低电平电压表格8.外设接口

硬件基础知识——自学习梳理

计算机存储分为闪存和永久性存储。 硬盘(永久存储)主要分为机械磁盘和固态硬盘。 机械磁盘主要靠磁颗粒的正负极方向来存储0或1,且机械磁盘没有使用寿命。 固态硬盘就有使用寿命了,大概支持30w次的读写操作。 闪存使用的是电容进行存储,断电数据就没了。 器件之间传输bit数据在总线上是一个一个传输的,因为通过电压传输(电流不稳定),但是电压属于电势能,所以可以叠加互相干扰,这也就是硬盘,U盘

一道经典Python程序样例带你飞速掌握Python的字典和列表

Python中的列表(list)和字典(dict)是两种常用的数据结构,它们在数据组织和存储方面有很大的不同。 列表(List) 列表是Python中的一种有序集合,可以随时添加和删除其中的元素。列表中的元素可以是任何数据类型,包括数字、字符串、其他列表等。列表使用方括号[]表示,元素之间用逗号,分隔。 定义和使用 # 定义一个列表 fruits = ['apple', 'banana

美容美发店营销版微信小程序源码

打造线上生意新篇章 一、引言:微信小程序,开启美容美发行业新纪元 在数字化时代,微信小程序以其便捷、高效的特点,成为了美容美发行业营销的新宠。本文将带您深入了解美容美发营销微信小程序,探讨其独特优势及如何助力商家实现业务增长。 二、微信小程序:美容美发行业的得力助手 拓宽客源渠道:微信小程序基于微信社交平台,轻松实现线上线下融合,帮助商家快速吸引潜在客户,拓宽客源渠道。 提升用户体验:

程序人生--拔丝地瓜

一个会享受生活的人,难免会执迷于探索“三餐茶饭,四季衣裳”的朴素涵义。如今在这繁杂喧闹、竞争激烈的社会环境里,如何才能从周而复始的生活中挖掘出一点儿期待!这是一个仁者见仁智者见智的开放性话题。对于大部分的人来说,看电影、运动、旅游、美食、加班....是假日的备选安排。 春节临走之前,再次尝试“拔丝地瓜”,为何要强调“再次”二字?因为这道甜菜我已经尝试过很多次,失败与成功都经历过。十几年的烧饭经历

vscode python pip : 无法将“pip”项识别为 cmdlet、函数、脚本文件或可运行程序的名称

在vscode中控制台运行python文件出现:无法将"pip”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。 使用vscode开发python,需要安装python开发扩展: 本文已经安装,我们需要找的是python安装所在目录,本文实际路径如下: 如果在本文路径中没有此目录,请尝试在C盘中搜索 python,搜索到相关python目录后,点击Python 3.9进入目录,

2_为MFC程序添加菜单

在MFC中添加菜单栏 1,双击资源文件,显示资源视图,点击Menu插入Menu菜单,编辑菜单的ID,自己取名字。 2,点击“请在此处键入”添加菜单选项,输入&E,E的下面就会产生下划线;在产生的弹出菜单中继续编辑,并且可以添加事件处理函数; 在弹出菜单的任意位置,鼠标右键,弹出的菜单中选择“插入分隔符”,即可产生分隔符 3,在你设计的Dialog窗口的属性栏,选择Menu后面的

在Ubuntu 14.04上安装和配置SNMP守护程序和客户端的方法

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。 介绍 作为系统管理员的重要工作之一是收集关于服务器和基础设施的准确信息。有许多工具和选项可用于收集和处理这种类型的信息。其中许多工具都是建立在一种叫做 SNMP 的技术之上。 SNMP 代表简单网络管理协议。它是服务器可以共享关于其当前状态的信息的一种方式,也是管理员可以修改预定义值的通道。虽