1. 程式人生 > >框架-裝置與驅動的拆分及實現-I2C

框架-裝置與驅動的拆分及實現-I2C

[toc] --- ## 前言 * 本筆記主要傳達一種**裝置驅動拆分**的概念和實現。 * 使得寫好一個驅動框架後,隨意新增相應裝置,提高開發效率。 * 使用到以空間換時間的方法,即是陣列管理裝置,使得時間複雜度為 O(1)。(**陣列直接定位**)。 * 本筆記的框架支援 **N個裝置 繫結 X個驅動** ## 筆錄草稿 ## 概要 * 觸發想法 * 有時候,在寫驅動時,發現多個裝置使用同一個驅動邏輯,只是部分內容不一樣(如引腳),此時就可以想如何寫出一個驅動邏輯支援多個不同裝置。 * 例子:IIC * 一個 IIC 邏輯 * 多個裝置繫結 IIC * 目標效果: * 只需要執行以下步驟即可: **註冊 IIC 驅動 --> 註冊實際裝置A並繫結 IIC --> 初始化該 IIC** * 只需要執行以下步驟即可: **註冊 IIC 驅動 --> 註冊實際裝置B並繫結 IIC --> 初始化該 IIC** ## 原理及實現方法 * 以 **ID** 為陣列下標,可以根據 **ID** 獲得 驅動或裝置 控制代碼。(LiteOS 裡任務ID和任務控制代碼也類似噢) * 陣列為 驅動陣列或裝置陣列或其它需要統一管理的陣列等等。主要為實體開闢空間,直接定位使用。 * 使用**陣列**管理是明顯的 **空間換時間的方法,時間複雜度達到O(1)** * 當然也可以使用**連結串列**,但是時間複雜度可能達不到 O(1)。 * 實現 **驅動部分** 1. 建立兩個驅動檔案:**bsp_xx.c** 和 **bsp_x.h** 2. 建立 xx 驅動名字列表 * 名字列表也就是 ID,用於下標、校驗和操作 * 下標:陣列下標,用於直接定位,獲得驅動控制代碼 * 校驗:下標對應的驅動裡面也有儲存 驅動 ID 的,在使用時,通過對比操作帶來的ID與結構體裡面的ID是否相等即可檢查到是否獲得準確的驅動實體 * 操作:通過 ID 獲得驅動控制代碼,便可進行操作 3. 組建 xx 驅動結構體 * xx 驅動結構體裡面 1. 必須包含 `驅動 ID` 2. 其他業務成員 4. 編寫 **註冊 xx 驅動函式** * **註冊 xx 驅動函式** 其實就是一個初始化,初始化 驅動ID 對應驅動陣列下標的實體驅動 * 必須給對應實體驅動裡的 驅動ID 賦 當前 ID 值,這樣使用時便可以校驗 5. 建立 xx 驅動陣列 * **xx 驅動陣列** 就是所有驅動實體的空間,不同下標對應不同的實體驅動 * 使用到陣列,即是靜態申請空間。當然也可以自己實現動態申請,如用連結串列的方法或者動態申請記憶體空間。 6. 編寫驅動邏輯 * 一個驅動,支援多個裝置 * 驅動邏輯,多個裝置的驅動邏輯相似,不同點可以通過 驅動結構體 中的成員區別開來。 * 實現 **裝置部分** 1. 建立兩個裝置檔案:**lss_yy.c** 和 **lss_yy.h** 2. 建立 yy 裝置名字列表 * 名字列表也就是 ID,用於下標、校驗和操作 * 下標:陣列下標,用於直接定位,獲得驅動控制代碼 * 校驗:下標對應的裝置裡面也有儲存 裝置 ID 的,在使用時,通過對比操作帶來的ID與結構體裡面的ID是否相等即可檢查到是否獲得準確的裝置實體 * 操作:通過 ID 獲得裝置控制代碼,便可進行操作 3. 組建 xx 裝置結構體 * xx 裝置結構體裡面 1. 必須包含 `裝置 ID` : 用於標識本結構體為哪一個裝置 2. 必須包含 `驅動 ID` : 就是繫結的 驅動 ID 3. 其他業務成員 4. 編寫 **註冊 xx 裝置函式** * **註冊 xx 裝置函式** 其實就是一個初始化,初始化 裝置ID 對應驅動陣列下標的實體裝置 * 必須給對應實體驅動裡的 驅動ID 賦 當前 ID 值,這樣使用時便可以校驗 5. 建立 xx 裝置陣列 * **xx 裝置陣列** 就是所有裝置實體的空間,不同下標對應不同的實體裝置 * 使用到陣列,即是靜態申請空間。當然也可以自己實現動態申請,如用連結串列的方法或者動態申請記憶體空間。 6. 編寫裝置邏輯 * 在裝置邏輯中,通過 裝置ID和裝置陣列 獲得裝置實體,再在裝置實體中找到驅動ID,把裝置ID傳給驅動邏輯函式即可。 7. 實現裝置初始化函式 ** * 簡要步驟(***必須遵循前三個步驟的順序***) 1. 先註冊 xx 驅動 2. 註冊 yy 裝置,並繫結對應的 xx 驅動 3. 初始化 xx 引腳 4. 執行自己的驅動業務 ## IIC 例子實戰-驅動 * 通過實現一下步驟,我們便實現了 裝置驅動框架的驅動部分 * 簡要步驟 1. 建立兩個檔案:**bsp_i2c.c** 和 **bsp_i2c.h** 2. 建立 I2C 驅動名字列表 3. 組建 I2C 驅動結構體 4. 編寫 **註冊 I2C 驅動函式** 5. 建立 I2C 驅動陣列 6. 編寫驅動邏輯 1. **`static uint32_t selectClkByGpio(const uint32_t addr)`** 選擇時鐘訊號函式 2. **`void i2cGpioInit(eI2C_ID id)`** I2C 引腳初始化函式 3. **`void i2cStart(eI2C_ID id)`** I2C Start 函式 4. **`void i2cStop(eI2C_ID id)`** I2C Stop 函式 5. **`uint8_t i2cSendByte(eI2C_ID id, uint16_t TxData)`** I2C SendByte 函式 6. **`uint8_t i2cReceiveByte(eI2C_ID id)`** I2C ReceiveByte 函式 7. **`void i2cAck(eI2C_ID id, uint8_t Ack)`** I2C Ack 函式 8. **`uint8_t i2cWaitAck(eI2C_ID id)`** I2C WaitAck 函式 #### 1. 建立檔案 * 建立兩個檔案:**bsp_i2c.c** 和 **bsp_i2c.h** #### 2. 建立 I2C 驅動名字列表 * 本驅動列表需要根據實際裝置修改 * **驅動名字其實就是對應驅動陣列下標,用於直接定位** * 注意: * 第一個驅動名必須從 **0** 開始 * **`ei2cDEVICE_COUNT`** 是和 **`i2cI2C_DEVICE_COUNT`** 一樣的大小,在實際工程中,二選一即可。 * 原始碼例子如下,驅動名字按照自己的命名風格命名即可。 ```c /* ********************************************************************************************************* * CONFIG ********************************************************************************************************* */ // [注][I2C] 根據實際裝置修改 // i2c 驅動數量 #define i2cI2C_DRIVER_COUNT 3 /** * @brief i2c id * @author lzm */ typedef enum { ei2cEEPROM_1 = 0, // 第一個 EEPROM 裝置驅動 ei2cEEPROM_2, // 第二個 EEPROM 裝置驅動 ei2cMPU6050, // MPU6050裝置驅動 ei2cDEVICE_COUNT; // 驅動數量 }eI2C_ID; ``` #### 3. 組建 I2C 驅動結構體 * I2C 驅動結構體必須包含 1. **`I2C ID`** : 就是一個**實體 I2C 的 ID** 及 **驅動陣列下標**。 2. SCL 及 SDA 引腳資料。 * 結構體中的延時資料,主要是為了 IIC 速度可控。 ```c /* ********************************************************************************************************* * BASIC ********************************************************************************************************* */ /** * @brief i2c struct * @author lzm */ struct I2C_T{ /* id */ eI2C_ID ID; /* delay */ // cnt unsigned char delayUsCnt; // delay function void ( *delayUsFun )(int cnt); /* pin */ GPIO_TypeDef * sclGpiox; uint16_t sclPin; GPIO_TypeDef * sdaGpiox; uint16_t sdaPin; }; typedef struct I2C_T i2c_t; ``` #### 4. 編寫-註冊 I2C 驅動函式 * **註冊 I2C 驅動函式** 其實就是初始化對應驅動的引數,如繫結 SCL 和 SDA 引腳。 * 在開發中,實際裝置繫結及使用 I2C 之前必須先註冊對應 I2C 驅動。 * 一些引數解析 * @param delayuscnt : 延時多少個 微妙 * @param fun : 微妙延時函式 * @param sclgpio : SCL 引腳 port * @param sclpin : SCL 引腳 pin * @param sdagpio : SDA 引腳 port * @param sdapin : SDA 引腳 pin ```c /* ********************************************************************************************************* * DEFINE [API] FUNCTION ********************************************************************************************************* */ /** * @brief 註冊IIC裝置 * i2cDeviceElem[i2cID].id = i2cID; // 保持下標與ID相等,查詢時可以直接定位,實現時間複雜度為O(1); * @param * @retval none * @author lzm */ #define REGISTER_I2C_DRI(i2cID, delayuscnt, fun, sclgpio, sclpin, sdagpio, sdapin) \ { \ i2cDeviceElem[i2cID].id = i2cID; \ i2cDeviceElem[i2cID].delayUsCnt = delayuscnt; \ i2cDeviceElem[i2cID].delayUsFun = fun; \ i2cDeviceElem[i2cID].sclGpiox = sclgpio; \ i2cDeviceElem[i2cID].sclPin = sclpin; \ i2cDeviceElem[i2cID].sdaGpiox = sdagpio; \ i2cDeviceElem[i2cID].sdaPin = sdapin; \ } ``` #### 5. 建立 I2C 驅動陣列 * `i2cI2C_DRIVER_COUNT` 表示有 **i2cI2C_DRIVER_COUNT** 個 I2C 驅動 * 建立 I2C 驅動陣列是提前為可能需要用到 I2C 驅動的裝置提前申請空間(靜態),當然也可以動態申請。 ```c /* ********************************************************************************************************* * DEFINE ********************************************************************************************************* */ // i2c 驅動元素(裝置表) i2c_t i2cDriverElem[i2cI2C_DRIVER_COUNT]; ``` #### 6. 編寫驅動邏輯 ##### **`static uint32_t selectClkByGpio(const uint32_t addr)`** 選擇時鐘訊號函式 * 本函式主要用於根據引腳埠來選擇時鐘,當然也可以選擇把 **時鐘變數** 放到 I2C 驅動結構體裡面 * 形參: `const uint32_t addr` 需要初始化引腳對應的 **port** * 返回:返回時鐘值 或 NULL ```c /** * @brief 選出時鐘訊號線 * @param addr : 引腳對應 port * @retval 返回時鐘值 或 NULL * @author lzm */ static uint32_t selectClkByGpio(const uint32_t addr) { switch(addr) { case GPIOA_BASE: return RCC_APB2Periph_GPIOA; case GPIOB_BASE: return RCC_APB2Periph_GPIOB; case GPIOC_BASE: return RCC_APB2Periph_GPIOC; case GPIOD_BASE: return RCC_APB2Periph_GPIOD; case GPIOE_BASE: return RCC_APB2Periph_GPIOE; case GPIOF_BASE: return RCC_APB2Periph_GPIOF; case GPIOG_BASE: return RCC_APB2Periph_GPIOG; } return NULL; } ``` ##### **`void i2cGpioInit(eI2C_ID id)`** 初始化I2C引腳 * 本函式主要用於初始化 I2C 需要的引腳:SCL 和 SDA * 形參: `eI2C_ID id` 為 I2C 驅動 ID,可以理解為需要初始化哪一個 I2C 驅動,從 I2C 驅動命名錶中選出。 * 返回:無 * 分析 * 原理:I2C 驅動 ID 即是 I2C 驅動陣列下標,對應一個 I2C 驅動,通過 ID 可以獲取 I2C 資料,然後做出處理。 * 步驟: 1. 獲取需要初始化的時鐘值 `sclGpioClk` 和 `sdaGpioClk` 2. 初始化需要的時鐘 3. 配置初始化引腳結構體並初始化 4. 拉高 SCL 和 SDA引腳。 ```c /** * @brief 初始化I2C引腳 * @param id : I2C 驅動 ID * @retval none * @author lzm */ void i2cGpioInit(eI2C_ID id) { GPIO_InitTypeDef G_GPIO_IniStruct; //定義結構體 uint32_t sclGpioClk; uint32_t sdaGpioClk; const i2c_t * i2c = &i2cDeviceElem[id]; sclGpioClk = selectClkByGpio((uint32_t)(i2c->sclGpiox)); sdaGpioClk = selectClkByGpio((uint32_t)(i2c->sdaGpiox)); RCC_APB2PeriphClockCmd(sclGpioClk | sdaGpioClk, ENABLE); //開啟時鐘 G_GPIO_IniStruct.GPIO_Pin = i2c->sclPin; //配置埠及引腳(指定方向) G_GPIO_IniStruct.GPIO_Mode = GPIO_Mode_Out_OD; G_GPIO_IniStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(i2c->sclGpiox, &G_GPIO_IniStruct); //初始化埠(開往指定方向) G_GPIO_IniStruct.GPIO_Pin = i2c->sdaPin; //配置埠及引腳(指定方向) G_GPIO_IniStruct.GPIO_Mode = GPIO_Mode_Out_OD; G_GPIO_IniStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(i2c->sdaGpiox, &G_GPIO_IniStruct); //初始化埠(開往指定方向) // 初始化完以後先拉高 iicOutHi(i2c->sclGpiox, i2c->sclPin); iicOutHi(i2c->sdaGpiox, i2c->sdaPin); } ``` ##### **`void i2cStart(eI2C_ID id)`** I2C Start函式 * 本函式為 I2C 邏輯函式 Start 部分 * 形參: `eI2C_ID id` 為 I2C 驅動 ID,可以理解為需要初始化哪一個 I2C 驅動,從 I2C 驅動命名錶中選出。 * 返回:無 * 分析 * 原理:I2C 驅動 ID 即是 I2C 驅動陣列下標,對應一個 I2C 驅動,通過 ID 可以獲取 I2C 資料,然後做出處理。 * 步驟: 1. 從驅動表中獲取一個驅動的控制代碼進行操作,`i2c_t * i2c = &i2cDeviceElem[id];` 2. 通過控制代碼獲取該 I2C 驅動資料,實現邏輯 ```c /** * @brief IIC START * @param id : I2C 驅動 ID * @retval none * @author lzm */ void i2cStart(eI2C_ID id) { i2c_t * i2c = &i2cDeviceElem[id]; iicSdaOutHi(i2c); iicSclOutHi(i2c); i2c->delayUsFun(i2c->delayUsCnt); iicSdaOutLo(i2c); i2c->delayUsFun(i2c->delayUsCnt); } ``` ##### 其餘 I2C 邏輯函式 * 其餘 I2C 邏輯函式原理和 `void i2cStart(eI2C_ID id)` 函式原理一樣,只是實現的邏輯不一樣而已,完整原始碼可以參考我的gitee上的 LiteOS 原始碼工程。 ## IIC 例子實戰-裝置 * 本筆記選用 **eeprom** 裝置做例子 * 通過實現一下步驟,我們便實現了 裝置驅動框架的裝置部分 * 簡要步驟 1. 建立裝置檔案:**lss_eeprom.c** 和 **lss_eeprom.h** 2. 建立裝置名字列表 3. 組鍵裝置結構體 4. 編寫註冊裝置函式 5. 建立裝置陣列 6. 實現裝置驅動邏輯 7. 實現裝置初始化函式 #### 1. 建立裝置檔案 * 直接建立 **lss_eeprom.c** 和 **lss_eeprom.h** 檔案即可。 #### 2. 建立裝置名字列表 * 本裝置列表需要根據實際裝置修改 * **裝置名字其實就是對應驅動陣列下標,用於直接定位** * 注意: * 第一個裝置名必須從 **0** 開始 * **`ei2cDEVICE_COUNT`** 是和 **`i2cI2C_DEVICE_COUNT`** 一樣的大小,在實際工程中,二選一即可。 * 原始碼例子如下,驅動名字按照自己的命名風格命名即可。 ```c /* ********************************************************************************************************* * CONFIG API ********************************************************************************************************* */ /* [注][eeprom]實時修改 */ // eeprom 裝置數量 #define eeEEPROM_DEVICE_COUNT 2 /* delay API */ #define eeDelayMs(cnt) vTaskDelay(cnt) /* 排程式延時 */ #define eeEEPROM_WRITE_COUNT 5 /* 寫頁時等待時間 */ /* fpga id. */ typedef enum { eAT24C08_1 = 0, eAT24C08_2, eeeprom_COUNT, }eEEPROM_ID; ``` #### 3. 組鍵裝置結構體 * 裝置結構體必須包含 1. **`eEEPROM_ID ID`** : 就是一個**實體 EEPROM 的 ID** 及 **裝置陣列下標**。 2. **`eI2C ID`** : 就是一個**實體 I2C 的 ID** 及 **驅動陣列下標**。 * 除了以上兩個必須的成員外,其他成員可以根據業務自行新增。 * 以上兩個 ID 是 **`eEEPROM_ID ID`** 繫結 **`eI2C ID`** ,裝置結構體只需要知道它對應哪一個 I2C 實體即可,即是隻需要知道一個 I2C ID即可。 ```c /* ********************************************************************************************************* * BASIC ********************************************************************************************************* */ /* eeprom struct */ struct EEPROM_T{ /* id */ eEEPROM_ID ID; /* i2c id */ eI2C_ID i2cID; }; ``` #### 4. 編寫註冊裝置函式 * **註冊裝置函式** 其實就是初始化一些資料,如繫結 I2C,繫結 SPI,繫結一些資料等等。 * 在開發中,實際裝置繫結及使用 I2C 之前必須先註冊對應 I2C 驅動,然後註冊 I2C 裝置。 * 一些引數解析 * @param eeid : EEPROM ID,用於直接定位,也可以同時用於定位校驗。 * @param i2cid : 裝置繫結的 I2C 驅動 ID。 ```c /* ********************************************************************************************************* * DEFINE [API] FUNCTION ********************************************************************************************************* */ /** * @brief 註冊IIC裝置 * @param eeid : EEPROM ID,用於直接定位,也可以同時用於定位校驗。 * @param i2cid : 裝置繫結的 I2C 驅動 ID。 * @retval none * @author lzm */ #define REGISTER_EEPROM_DEV(eeid, i2cid) \ { \ eepromDeviceElem[eeid].ID = eeid; \ eepromDeviceElem[eeid].i2cID = i2cid; \ } ``` #### 5. 建立 EEPROM 裝置陣列 * `eeEEPROM_DEVICE_COUNT` 表示有 **eeEEPROM_DEVICE_COUNT** 個 EEPROM 裝置 * 建立 I2C 驅動陣列是提前為可能需要用到 I2C 驅動的裝置提前申請空間(靜態),當然也可以動態申請。 ```c /* ********************************************************************************************************* * DEFINE ********************************************************************************************************* */ // eeprom 裝置元素(裝置表) eeprom_t eepromDeviceElem[eeEEPROM_DEVICE_COUNT]; ``` #### 6. 實現裝置驅動邏輯 * 原理:通過 **`eI2C_ID i2cid = eepromDeviceElem[id].i2cID;`** 獲取對應的 I2C 驅動實體 * 例子如下,該函式只需要用裝置 ID **`eEEPROM_ID`** 管理即可,APP 使用者不需接觸到 I2C 驅動名字的操作,只需要自己操作的裝置的裝置名字即可。 ##### eeprom 其中一個邏輯函式 * 其餘邏輯函式自己可以實現,只需要定址問題即可。 ```c /** * @brief read [size] bytes from pReadBuf * @param pReadBuf : store data form addr * addr : start addr * size : the size of need read * @retval 1 : normal * 0 : abnormal * @author lzm */ uint8_t __eeReadBytes(eEEPROM_ID id, uint16_t addr, uint8_t *pReadBuf, uint16_t lenght) { uint16_t i; uint8_t active = 0x0A; eI2C_ID i2cid = eepromDeviceElem[id].i2cID; while( active-- ) { i2cStart(i2cid); if (i2cSendByte(i2cid, eeEEPROM_DEVICE_ADDR + ((addr>>8)<<1))) { i2cStop(i2cid); continue; /* EEPROM器件無應答 */ } #if 0 // [注][eeprom] AT24C32 及以上的 eeprom才啟用 /* High 8 bits address. */ if(LSS_I2C_SendByte(addr>
>8)) { LSS_I2C_Stop();continue; } #endif if (i2cSendByte(i2cid, (uint8_t)(addr))) { i2cStop(i2cid); continue; /* EEPROM器件無應答 */ } i2cStart(i2cid); if (i2cSendByte(i2cid, eeEEPROM_DEVICE_ADDR | eeEEPROM_I2C_RD)) { i2cStop(i2cid); continue; /* EEPROM器件無應答 */ } for (i = 0; i < lenght; i++) { pReadBuf[i] = i2cReceiveByte(i2cid); if(i == lenght-1) i2cAck(i2cid,1); //No ACK else i2cAck(i2cid,0); //ACK } i2cStop(i2cid); return 0; /* 執行成功 */ } return 1; } ``` #### 7. 實現裝置初始化函式 ** * 簡要步驟 1. 先註冊 I2C 驅動 2. 註冊 EEPROM 裝置,並繫結對應的 I2C 驅動 3. 初始化 I2C 引腳 4. 執行自己的驅動業務 ```c /** * @brief 所有EEPROM裝置初始化 * @param * @retval * @author lzm */ void eepromInit(void) { uint8_t eepromID; // 先註冊 I2C 驅動 REGISTER_I2C_DRI(ei2cEEPROM_1, 5, dwtDelayUs, EEP_SCL_PORT, EEP_SCL_PIN, EEP_SDA_PORT, EEP_SDA_PIN); REGISTER_I2C_DRI(ei2cEEPROM_2, 5, dwtDelayUs, EEP_SCL_PORT, EEP_SCL_PIN, EEP_SDA_PORT, EEP_SDA_PIN); // 註冊 EEPROM 裝置並繫結 i2c 驅動 REGISTER_EEPROM_DEV(eAT24C08_1, ei2cEEPROM_1); REGISTER_EEPROM_DEV(eAT24C08_2, ei2cEEPROM_2); for (eepromID = 0; eepromID < eeEEPROM_DEVICE_COUNT; eepromID++) { // 初始化 I2C i2cGpioInit( (eI2C_ID)(ei2cEEPROM + eepromID) ); // 業務 [待寫] } // 業務 [待寫] } ``` ## 重要後語(小小雞湯) * 自己寫 MCU 驅動時想出上述這種框架,感覺很清晰,很精簡,開發效率很高,後面才發現和 linux 的裝置驅動框架相識。 * 不過,想出這個框架還是收穫滿滿的。 * 要學會 **偷懶** * 這裡的 **偷懶** 是提高效率的意思,這不是一件簡單的事,還得學會思考。 * 搭建好一個優秀的框架,後期開發效率高。如上述中新增一個 I2C 裝置,直接在**裝置列表**中新增一個列舉,再在裝置初始化程式碼段中註冊、繫結即可。 * 要 **多出去走走** * 這裡也不是讓你經常去遊山玩水,而是多逛逛一些優秀的論壇、多看看牛人的部落格、多研究一下優秀的原始碼、多瞭解一下常用的演算法、框架等等 * **本人圈子小,有優秀的學習源,跪求推薦給我哈哈,好東西不怕多** * **包括技術、理財、外語(英語、日語)、二次元** * **本人圈子小,有好的學習源,跪求推薦給我哈哈,好東西不怕多** * **包括技術、理財、外語(英語、日語)、二次元** * **本人圈子小,有好的學習源,跪求推薦給我哈哈,好東西不怕多** * **包括技術、理財、外語(英語、日語)、二