各位好,從今天開(kāi)始,我的 BMS 電池保護(hù)板系列開(kāi)始聊一下軟件相關(guān)的話題。
首先要關(guān)注的,就是我們的主控芯片如何控制 AFE,如何從 AFE 中讀取到想要的信息,這就離不開(kāi) AFE 的通信接口。
AFE 的通信接口有很多種類,比如 Uart,IIC,SPI 等。其中 Uart 不多見(jiàn),以 IIC 和 SPI 最為常見(jiàn),因?yàn)檫@兩個(gè)通信協(xié)議是板級(jí)通信中最常用的,邏輯簡(jiǎn)單,線路少。SPI 有一種菊花鏈模式,這個(gè)模式在分布式 BMS 系統(tǒng)中使用普遍,基本各個(gè)AFE 廠家都設(shè)計(jì)了相應(yīng)的隔離芯片,有保障他們的AFE 可以被更好得使用。我的 Demo 中選擇的 AFE的通信是 IIC 接口,因此這一篇文章主要講述 IIC 的實(shí)現(xiàn)。
一、為什么要用模擬 IIC
在我設(shè)計(jì)的 Demo 中,我選擇了使用 IO 口來(lái)模擬 IIC 總線,這種選擇經(jīng)歷了很久的思考。首先,這個(gè)行業(yè)的伙伴都應(yīng)該了解,早期的 STM32F1 系列 MCU,在 IIC 的硬件設(shè)計(jì)上出現(xiàn)過(guò) bug,在中斷打斷 IIC 的時(shí)候會(huì)導(dǎo)致 IIC 總線無(wú)端掛起,或者有些標(biāo)志位無(wú)法置位,這是選擇模擬 IIC 的最初的原因。
隨后,經(jīng)過(guò)幾個(gè)項(xiàng)目的磨煉,這個(gè) IIC 使用模擬 IO 實(shí)現(xiàn)還是非選不可的,原因如下:
- 在 BMS 項(xiàng)目中,MCU 并不需要特別快速的運(yùn)行,因?yàn)榭焖夙憫?yīng)的過(guò)流保護(hù)和短路保護(hù)都有 AFE 的硬件直接操作,而讀取 AFE 采樣的數(shù)據(jù)也不需要很頻繁,想想,AFE 的 ADC 普遍的采樣頻率才 5Hz。從多陣列產(chǎn)品開(kāi)發(fā)的角度,我們經(jīng)常會(huì)遇到更換 MCU 的情況,原因不乏成本,缺貨,或者看原廠不順眼等。那么如果使用硬件的 IIC 模式,面對(duì)各家的 MCU 的外設(shè)驅(qū)動(dòng),還需要一定的學(xué)習(xí)成本和移植風(fēng)險(xiǎn),所以模擬的 IIC 總線直接使用兩個(gè) IO 口和一個(gè)簡(jiǎn)單的延時(shí)函數(shù)即可。
當(dāng)然,硬件 IIC 是有一定的好處的,除了通信的可靠性和容錯(cuò)性外,相對(duì)于模擬 IIC 最大的好處是,在單字節(jié)接收的過(guò)程中,我們可以利用中斷來(lái)讓 MCU 干些其他事情,也僅此而已。所以,如果你的系統(tǒng)運(yùn)行頻率很高,CPU 負(fù)荷比較高的情況下,肯定首選硬件 IIC。
二、實(shí)現(xiàn)模擬 IIC 的代碼封裝
要封裝一個(gè)代碼,首先要將模塊的功能抽象出來(lái),確定模塊的輸入輸出邏輯,從而確定如何封裝代碼成一個(gè)通用的庫(kù),或者說(shuō)利于移植的模塊。我個(gè)人在這一塊有一個(gè)整體的思路,就是按照 C++的面向?qū)ο缶幊趟枷雭?lái)規(guī)劃這個(gè)類,雖然 C 無(wú)法寫(xiě)成類的形式,但是大體的封裝思想是可以實(shí)現(xiàn)的。
首先我們確定,要模擬 IIC 通信總線,需要兩個(gè) IO,這兩個(gè) IO 的通信速率不必太高,因?yàn)?IIC 一般的通信速率才 400Khz,現(xiàn)在有一些 1Mhz 的。其次,我們需要一個(gè)延時(shí)函數(shù),來(lái)控制總線的時(shí)鐘延時(shí),這個(gè)延時(shí)最好使用定時(shí)器來(lái)實(shí)現(xiàn),這樣可以調(diào)整出比較好的 IIC 波形,但是這樣會(huì)引入一個(gè)復(fù)雜的 TIMER 模塊,因此我選擇了代碼延時(shí),只需要確定好主時(shí)鐘調(diào)試一次即可。有了上面的兩個(gè) IO 口和延時(shí)函數(shù),我們就可以通過(guò)控制兩個(gè)引腳的高低和時(shí)序來(lái)模擬 IIC 通信了。現(xiàn)在,我們已經(jīng)有了足夠的輸入來(lái)對(duì)模擬 IIC 這個(gè)類進(jìn)行構(gòu)造函數(shù)的編寫(xiě)。那么,進(jìn)一步的,我們確定 IIC 這個(gè)類的方法和屬性。我們可以把 IIC 總線通信的一些錯(cuò)誤和狀態(tài)作為屬性來(lái)定義,可以讓調(diào)用者通過(guò)調(diào)用類的屬性來(lái)獲取總線的狀態(tài),是空閑,還是忙狀態(tài)。也可以通過(guò)屬性來(lái)獲取上一次通信的結(jié)果。其次,對(duì)于方法就比較明確了,我們需要查詢忙狀態(tài)的方法,需要最基本的向設(shè)備-地址的讀寫(xiě)操作,然后再在上層實(shí)現(xiàn)多字節(jié)的讀寫(xiě)操作。
OK,看一下代碼吧
下面是硬件相關(guān)的定義,定義了兩個(gè)引腳和兩個(gè)延時(shí)函數(shù),因?yàn)樵?IIC 通信中,有控制時(shí)鐘的延時(shí)和控制時(shí)序的延時(shí)。
//===========硬件相關(guān)的定義==================================================
#include "cw32l031.h"
#include "cw32l031_gpio.h"
// I2C的引腳定義
#define I2C_SDA_PIN GPIO_PIN_0
#define I2C_SCL_PIN GPIO_PIN_1
//I2C 的軟件延時(shí),這個(gè)需要根據(jù)系統(tǒng)時(shí)鐘進(jìn)行調(diào)整
#define I2C_DELAY_INIT() u8 _counter = 0;
#define I2C_DELAY() for( _counter = 0; _counter < 20; ) {_counter++; } //100K 重新測(cè)試
#define I2C_DELAY_SHORT() for( _counter = 0; _counter < 10; ) {_counter++; } //
//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
然后,我們需要定義一些類的屬性,在這里其實(shí)就是一些關(guān)于通信模塊的設(shè)置,比如通信的重發(fā)嘗試次數(shù),比如 IIC 總線的狀態(tài)和錯(cuò)誤標(biāo)志等,這里我們直接使用宏定義來(lái)設(shè)定,沒(méi)有在提供變量給調(diào)用者進(jìn)行實(shí)例化的時(shí)候進(jìn)行構(gòu)建,因?yàn)檫@在 C語(yǔ)言中就相當(dāng)于脫褲子放屁。
//===============IIC 軟件層相關(guān)(2023.11.11整理)======================================
// I2C的一些錯(cuò)誤宏定義
#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 //嘗試次數(shù)
最后,我們需要給調(diào)用者提供一個(gè)可以調(diào)用的列表,從類的角度看,無(wú)非就是構(gòu)造函數(shù),析構(gòu)函數(shù)和幾個(gè)方法屬性。這里我們只有一個(gè)充當(dāng)構(gòu)造函數(shù)的初始化函數(shù)和兩個(gè)方法:讀方法和寫(xiě)方法。
// I2C對(duì)外接口的聲明
void i2c_init(void); //I2C的初始化函數(shù)
//多字節(jié)的讀寫(xiě)
u8 i2c_write(u8 addr, u8 reg_addr, u8* txBuff, int count);
u8 i2c_read( u8 addr, u8 reg_addr, u8* rxBuff, int count);
好啦,有了以上的一個(gè)頭文件,我們就可以使用這個(gè) IIC 模塊,使用的步驟很簡(jiǎn)單,先確定 IO 口,然后確定延時(shí)函數(shù),最后在我們的初始化過(guò)程中將 i2c_init()
調(diào)用一下,就可以在我們的系統(tǒng)中使用讀寫(xiě)方法了。我建了一個(gè)微信群,供大家來(lái)討論 BMS 相關(guān)技術(shù),為了保證討論質(zhì)量,請(qǐng)先加我的個(gè)人微信,備注 “BMS” ,我來(lái)拉大家入群。
三、實(shí)現(xiàn)模擬 IIC 的簡(jiǎn)要說(shuō)明
當(dāng)我們定義好模擬 IIC 模塊的外觀后,也就是對(duì)外接口后,我們就需要思考如何在這個(gè)封裝層下來(lái)實(shí)現(xiàn)邏輯,其實(shí)這是一種自頂向下的設(shè)計(jì)模式。咱們先把 IO 的拉高拉低變換成總線上的一些狀態(tài),對(duì)于 SCL 引線還好,他負(fù)責(zé)產(chǎn)生時(shí)鐘,可以直接拉高拉低,而對(duì)于 SDA 引線就稍微復(fù)雜一些,因?yàn)樗丝梢岳呃偷妮敵鐾?,還需要從總線上讀取電平。
GPIO_TypeDef* m_I2C_PORT = CW_GPIOA; //定義I2C的IO指針,默認(rèn)為GPIOB
//I2C的一些信號(hào)狀態(tài),不同的硬件需要重新定義
#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 的狀態(tài)轉(zhuǎn)換成了 IIC 總線上的多態(tài)端口。