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