框架-設備與驅動的拆分及實現-I2C


前言

  • 本筆記主要傳達一種設備驅動拆分的概念和實現。
  • 使得寫好一個驅動框架後,隨意添加相應設備,提高開發效率。
  • 使用到以空間換時間的方法,即是數組管理設備,使得時間複雜度為 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.cbsp_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.clss_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.cbsp_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.cbsp_i2c.h

2. 創建 I2C 驅動名字列表

  • 本驅動列表需要根據實際設備修改
  • 驅動名字其實就是對應驅動數組下標,用於直接定位
  • 注意:
    • 第一個驅動名必須從 0 開始
    • ei2cDEVICE_COUNT 是和 i2cI2C_DEVICE_COUNT 一樣的大小,在實際工程中,二選一即可。
  • 源碼例子如下,驅動名字按照自己的命名風格命名即可。
/*
*********************************************************************************************************
*                                                 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 速度可控。
/*
*********************************************************************************************************
*                                                 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
/*
*********************************************************************************************************
*                                                 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 驅動的設備提前申請空間(靜態),當然也可以動態申請。
/*
*********************************************************************************************************
*                                                 DEFINE
*********************************************************************************************************
*/
// i2c 驅動元素(設備表)
i2c_t i2cDriverElem[i2cI2C_DRIVER_COUNT];

6. 編寫驅動邏輯

static uint32_t selectClkByGpio(const uint32_t addr) 選擇時鐘信號函數
  • 本函數主要用於根據引腳端口來選擇時鐘,當然也可以選擇把 時鐘變量 放到 I2C 驅動結構體裏面
  • 形參: const uint32_t addr 需要初始化引腳對應的 port
  • 返回:返回時鐘值 或 NULL
/**
  * @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. 獲取需要初始化的時鐘值 sclGpioClksdaGpioClk
      2. 初始化需要的時鐘
      3. 配置初始化引腳結構體並初始化
      4. 拉高 SCL 和 SDA引腳。
/**
  * @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 驅動數據,實現邏輯
/**
  * @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.clss_eeprom.h
    2. 創建設備名字列表
    3. 組鍵設備結構體
    4. 編寫註冊設備函數
    5. 創建設備數組
    6. 實現設備驅動邏輯
    7. 實現設備初始化函數

1. 創建設備文件

  • 直接創建 lss_eeprom.clss_eeprom.h 文件即可。

2. 創建設備名字列表

  • 本設備列表需要根據實際設備修改
  • 設備名字其實就是對應驅動數組下標,用於直接定位
  • 注意:
    • 第一個設備名必須從 0 開始
    • ei2cDEVICE_COUNT 是和 i2cI2C_DEVICE_COUNT 一樣的大小,在實際工程中,二選一即可。
  • 源碼例子如下,驅動名字按照自己的命名風格命名即可。
/*
*********************************************************************************************************
*                                                 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即可。
/*
*********************************************************************************************************
*                                                 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。
/*
*********************************************************************************************************
*                                                 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 驅動的設備提前申請空間(靜態),當然也可以動態申請。
/*
*********************************************************************************************************
*                                                 DEFINE
*********************************************************************************************************
*/
// eeprom 設備元素(設備表)
eeprom_t eepromDeviceElem[eeEEPROM_DEVICE_COUNT];

6. 實現設備驅動邏輯

  • 原理:通過 eI2C_ID i2cid = eepromDeviceElem[id].i2cID; 獲取對應的 I2C 驅動實體
  • 例子如下,該函數只需要用設備 ID eEEPROM_ID 管理即可,APP 用戶不需接觸到 I2C 驅動名字的操作,只需要自己操作的設備的設備名字即可。
eeprom 其中一個邏輯函數
  • 其餘邏輯函數自己可以實現,只需要尋址問題即可。
/**
  * @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. 執行自己的驅動業務
/**
  * @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 設備,直接在設備列表中添加一個枚舉,再在設備初始化代碼段中註冊、綁定即可。
    • 多出去走走
      • 這裡也不是讓你經常去遊山玩水,而是多逛逛一些優秀的論壇、多看看牛人的博客、多研究一下優秀的源碼、多了解一下常用的算法、框架等等
        • 本人圈子小,有優秀的學習源,跪求推薦給我哈哈,好東西不怕多
          • 包括技術、理財、外語(英語、日語)、二次元
        • 本人圈子小,有好的學習源,跪求推薦給我哈哈,好東西不怕多
          • 包括技術、理財、外語(英語、日語)、二次元
        • 本人圈子小,有好的學習源,跪求推薦給我哈哈,好東西不怕多
          • 包括技術、理財、外語(英語、日語)、二次元