各位好,从今天开始,我的 BMS 电池保护板系列开始聊一下软件相关的话题。
首先要关注的,就是我们的主控芯片如何控制 AFE,如何从 AFE 中读取到想要的信息,这就离不开 AFE 的通信接口。
AFE 的通信接口有很多种类,比如 Uart,IIC,SPI 等。其中 Uart 不多见,以 IIC 和 SPI 最为常见,因为这两个通信协议是板级通信中最常用的,逻辑简单,线路少。SPI 有一种菊花链模式,这个模式在分布式 BMS 系统中使用普遍,基本各个AFE 厂家都设计了相应的隔离芯片,有保障他们的AFE 可以被更好得使用。我的 Demo 中选择的 AFE的通信是 IIC 接口,因此这一篇文章主要讲述 IIC 的实现。
一、为什么要用模拟 IIC
在我设计的 Demo 中,我选择了使用 IO 口来模拟 IIC 总线,这种选择经历了很久的思考。首先,这个行业的伙伴都应该了解,早期的 STM32F1 系列 MCU,在 IIC 的硬件设计上出现过 bug,在中断打断 IIC 的时候会导致 IIC 总线无端挂起,或者有些标志位无法置位,这是选择模拟 IIC 的最初的原因。
随后,经过几个项目的磨炼,这个 IIC 使用模拟 IO 实现还是非选不可的,原因如下:
- 在 BMS 项目中,MCU 并不需要特别快速的运行,因为快速响应的过流保护和短路保护都有 AFE 的硬件直接操作,而读取 AFE 采样的数据也不需要很频繁,想想,AFE 的 ADC 普遍的采样频率才 5Hz。从多阵列产品开发的角度,我们经常会遇到更换 MCU 的情况,原因不乏成本,缺货,或者看原厂不顺眼等。那么如果使用硬件的 IIC 模式,面对各家的 MCU 的外设驱动,还需要一定的学习成本和移植风险,所以模拟的 IIC 总线直接使用两个 IO 口和一个简单的延时函数即可。
当然,硬件 IIC 是有一定的好处的,除了通信的可靠性和容错性外,相对于模拟 IIC 最大的好处是,在单字节接收的过程中,我们可以利用中断来让 MCU 干些其他事情,也仅此而已。所以,如果你的系统运行频率很高,CPU 负荷比较高的情况下,肯定首选硬件 IIC。
二、实现模拟 IIC 的代码封装
要封装一个代码,首先要将模块的功能抽象出来,确定模块的输入输出逻辑,从而确定如何封装代码成一个通用的库,或者说利于移植的模块。我个人在这一块有一个整体的思路,就是按照 C++的面向对象编程思想来规划这个类,虽然 C 无法写成类的形式,但是大体的封装思想是可以实现的。
首先我们确定,要模拟 IIC 通信总线,需要两个 IO,这两个 IO 的通信速率不必太高,因为 IIC 一般的通信速率才 400Khz,现在有一些 1Mhz 的。其次,我们需要一个延时函数,来控制总线的时钟延时,这个延时最好使用定时器来实现,这样可以调整出比较好的 IIC 波形,但是这样会引入一个复杂的 TIMER 模块,因此我选择了代码延时,只需要确定好主时钟调试一次即可。有了上面的两个 IO 口和延时函数,我们就可以通过控制两个引脚的高低和时序来模拟 IIC 通信了。现在,我们已经有了足够的输入来对模拟 IIC 这个类进行构造函数的编写。那么,进一步的,我们确定 IIC 这个类的方法和属性。我们可以把 IIC 总线通信的一些错误和状态作为属性来定义,可以让调用者通过调用类的属性来获取总线的状态,是空闲,还是忙状态。也可以通过属性来获取上一次通信的结果。其次,对于方法就比较明确了,我们需要查询忙状态的方法,需要最基本的向设备-地址的读写操作,然后再在上层实现多字节的读写操作。
OK,看一下代码吧
下面是硬件相关的定义,定义了两个引脚和两个延时函数,因为在 IIC 通信中,有控制时钟的延时和控制时序的延时。
//===========硬件相关的定义==================================================
#include "cw32l031.h"
#include "cw32l031_gpio.h"
// I2C的引脚定义
#define I2C_SDA_PIN GPIO_PIN_0
#define I2C_SCL_PIN GPIO_PIN_1
//I2C 的软件延时,这个需要根据系统时钟进行调整
#define I2C_DELAY_INIT() u8 _counter = 0;
#define I2C_DELAY() for( _counter = 0; _counter < 20; ) {_counter++; } //100K 重新测试
#define I2C_DELAY_SHORT() for( _counter = 0; _counter < 10; ) {_counter++; } //
//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
然后,我们需要定义一些类的属性,在这里其实就是一些关于通信模块的设置,比如通信的重发尝试次数,比如 IIC 总线的状态和错误标志等,这里我们直接使用宏定义来设定,没有在提供变量给调用者进行实例化的时候进行构建,因为这在 C语言中就相当于脱裤子放屁。
//===============IIC 软件层相关(2023.11.11整理)======================================
// I2C的一些错误宏定义
#define I2C_SUCCESS 0
#define I2C_ARBITRATION_LOST 0x11
#define I2C_NACK 0x12
#define I2C_TIMEOUT 0x13
#define I2C_WRITEFAIL 0x14
#define I2C_CRC 0x15
#define I2C_OTHER 0x16
#define I2C_MAX_ATTEMPTS 1000 //尝试次数
最后,我们需要给调用者提供一个可以调用的列表,从类的角度看,无非就是构造函数,析构函数和几个方法属性。这里我们只有一个充当构造函数的初始化函数和两个方法:读方法和写方法。
// I2C对外接口的声明
void i2c_init(void); //I2C的初始化函数
//多字节的读写
u8 i2c_write(u8 addr, u8 reg_addr, u8* txBuff, int count);
u8 i2c_read( u8 addr, u8 reg_addr, u8* rxBuff, int count);
好啦,有了以上的一个头文件,我们就可以使用这个 IIC 模块,使用的步骤很简单,先确定 IO 口,然后确定延时函数,最后在我们的初始化过程中将 i2c_init()
调用一下,就可以在我们的系统中使用读写方法了。我建了一个微信群,供大家来讨论 BMS 相关技术,为了保证讨论质量,请先加我的个人微信,备注 “BMS” ,我来拉大家入群。
三、实现模拟 IIC 的简要说明
当我们定义好模拟 IIC 模块的外观后,也就是对外接口后,我们就需要思考如何在这个封装层下来实现逻辑,其实这是一种自顶向下的设计模式。咱们先把 IO 的拉高拉低变换成总线上的一些状态,对于 SCL 引线还好,他负责产生时钟,可以直接拉高拉低,而对于 SDA 引线就稍微复杂一些,因为他除了可以拉高拉低的输出外,还需要从总线上读取电平。
GPIO_TypeDef* m_I2C_PORT = CW_GPIOA; //定义I2C的IO指针,默认为GPIOB
//I2C的一些信号状态,不同的硬件需要重新定义
#define i2cSetSDA_highZ() (m_I2C_PORT->ODR |= I2C_SDA_PIN)
#define i2cGetSDA() ((m_I2C_PORT->IDR & I2C_SDA_PIN) ? 1 : 0)
#define i2cSetSCL_highZ() (m_I2C_PORT->ODR |= I2C_SCL_PIN)
#define i2cGetSCL() ((m_I2C_PORT->IDR & I2C_SCL_PIN) ? 1 : 0)
#define i2cClearSDA() (m_I2C_PORT->ODR &= (~I2C_SDA_PIN))
#define i2cClearSCL() (m_I2C_PORT->ODR &= (~I2C_SCL_PIN))
以上,我们将 IO 的状态转换成了 IIC 总线上的多态端口。