框架-裝置與驅動的拆分及實現-I2C
阿新 • • 發佈:2020-10-18
[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 裝置,直接在**裝置列表**中新增一個列舉,再在裝置初始化程式碼段中註冊、繫結即可。
* 要 **多出去走走**
* 這裡也不是讓你經常去遊山玩水,而是多逛逛一些優秀的論壇、多看看牛人的部落格、多研究一下優秀的原始碼、多瞭解一下常用的演算法、框架等等
* **本人圈子小,有優秀的學習源,跪求推薦給我哈哈,好東西不怕多**
* **包括技術、理財、外語(英語、日語)、二次元**
* **本人圈子小,有好的學習源,跪求推薦給我哈哈,好東西不怕多**
* **包括技術、理財、外語(英語、日語)、二次元**
* **本人圈子小,有好的學習源,跪求推薦給我哈哈,好東西不怕多**
* **包括技術、理財、外語(英語、日語)、二