IOT-OS之RT-Thread(十二)--- 驅動分層與主從分離思想

一、驅動分層思想

通過前面對RT-Thread設備模型框架,以及UART、IIC、SPI 等設備驅動實現過程的介紹,我們應該對驅動分層思想並不陌生了。操作系統爲什麼對設備驅動採用分層管理呢?驅動分層有什麼好處呢?

在介紹驅動分層的好處前,我們先看看著名的TCP/IP協議棧分層模型
TCP/IP協議棧分層模型
TCP/IP協議棧每層都有自己獨特的作用:

  • 網絡接口層負責對網卡硬件設備的訪問,在進行網卡驅動開發時,需要將網卡硬件的屬性、接口等信息註冊到網絡接口層;
  • 網絡層則負責網際尋址與路由,負責兩個主機之間的尋址與連接;
  • 傳輸層則負責兩個應用程序端口之間的連接與數據傳輸;
  • 應用層則負責網絡數據格式的定義,並向上層APP提供網絡訪問的接口等。

類比TCP/IP協議的分層思想,不難得知操作系統驅動分層的好處,再參照RT-Thread I/O 設備模型框架:
I/O設備模型框架
RT-Thread I/O設備模型框架位於硬件和應用程序之間,共分成三層,每一層也都有自己的作用:

  • 設備驅動層負責對各種硬件設備的訪問,在進行設備驅動開發時,需要將硬件設備的屬性配置、訪問接口等信息註冊到上面的設備驅動框架層;
  • 設備驅動框架層負責定義主機控制器(比如CPU、MCU)與外設之間傳輸數據的格式和規則(比如總線通信協議中與硬件無關的上層協議),並按照上面 I/O 設備管理層要求的接口形式,向上層註冊設備對象與訪問接口;
  • I/O 設備管理層則將不同總線設備的訪問接口封裝爲統一的標準接口,讓上面的應用程序可以通過該層提供的標準接口訪問底層設備,設備驅動程序的升級、更替不會對上層應用產生影響。

協議和驅動雖然分層管理,每一層專注完成自己的事情,但層與層之間想要協同工作,還需要留出各自的接口用來交互資源信息等,最常見的接口就是設備對象的註冊或初始化,當然也包括設備訪問接口的註冊(包含在設備對象的註冊過程中)。RT-Thread創建設備對象、向上面的 I/O 設備管理器註冊設備對象和訪問接口、應用程序通過設備對象和接口訪問設備的一般過程如下所示:
應用程序訪問設備的過程
創建設備實際上就是在設備驅動層創建一個設備對象,並將該硬件設備的屬性信息(比如基地址、中斷號、時鐘、DMA等)與訪問接口保存到該設備對象結構體或類中,將初始化後的設備對象向上註冊到 I/O 設備管理器中。

既然可以將設備對象向上註冊,又可以從上層訪問該設備對象,這就要求不同層級描述設備的結構體或類相互兼容,存在自上而下的繼承關係。上層保存同類設備的通用屬性和訪問接口,下層增加硬件設備的專有屬性並完成硬件訪問接口的實現,就像下面這樣:
設備描述結構體間的繼承關係
下面分別用前面介紹過的串口設備、I2C設備、SPI設備爲例,回顧下具體的設備驅動是如何將驅動分層思想實現在代碼中的。

1.1 UART設備驅動分層

每一層都有自己的設備描述結構和接口函數集合,每一層的接口函數集合由低一層實現並註冊。在當前層直接調用,用來實現更上層的接口函數集合。

1.1.1 串口設備驅動框架層

串口設備的驅動框架層提供的設備描述結構和接口函數集合如下:

// .\rt-thread-4.0.1\components\drivers\include\drivers\serial.h

struct rt_serial_device
{
    struct rt_device          parent;

    const struct rt_uart_ops *ops;
    struct serial_configure   config;

    void *serial_rx;
    void *serial_tx;
};
typedef struct rt_serial_device rt_serial_t;

struct rt_uart_ops
{
    rt_err_t (*configure)(struct rt_serial_device *serial, struct serial_configure *cfg);
    rt_err_t (*control)(struct rt_serial_device *serial, int cmd, void *arg);

    int (*putc)(struct rt_serial_device *serial, char c);
    int (*getc)(struct rt_serial_device *serial);

    rt_size_t (*dma_transmit)(struct rt_serial_device *serial, rt_uint8_t *buf, rt_size_t size, int direction);
};

串口設備框架層使用rt_uart_ops接口函數實現 I / O 設備管理層的接口函數集合rt_device_ops,並將實現的接口函數集合serial_ops註冊到上層的過程如下:

// .\rt-thread-4.0.1\components\drivers\serial\serial.c

#ifdef RT_USING_DEVICE_OPS
const static struct rt_device_ops serial_ops = 
{
    rt_serial_init,
    rt_serial_open,
    rt_serial_close,
    rt_serial_read,
    rt_serial_write,
    rt_serial_control
};
#endif

/*
 * serial register
 */
rt_err_t rt_hw_serial_register(struct rt_serial_device *serial,
                               const char              *name,
                               rt_uint32_t              flag,
                               void                    *data)
{
    rt_err_t ret;
    struct rt_device *device;
    RT_ASSERT(serial != RT_NULL);

    device = &(serial->parent);

    device->type        = RT_Device_Class_Char;
    device->rx_indicate = RT_NULL;
    device->tx_complete = RT_NULL;

#ifdef RT_USING_DEVICE_OPS
    device->ops         = &serial_ops;
#else
    ......
#endif
    device->user_data   = data;

    /* register a character device */
    ret = rt_device_register(device, name, flag);

#if defined(RT_USING_POSIX)
    /* set fops */
    device->fops        = &_serial_fops;
#endif

    return ret;
}

接口函數集合serial_ops中的函數實現最終都是通過調用rt_uart_ops接口函數實現的,rt_uart_ops接口函數則由更底層的UART設備驅動層來實現並註冊。

1.1.2 串口設備驅動層

串口設備驅動層提供的設備描述結構如下:

// .\libraries\HAL_Drivers\drv_usart.h

/* stm32 uart dirver class */
struct stm32_uart
{
    UART_HandleTypeDef handle;
    struct stm32_uart_config *config;
    
#ifdef RT_SERIAL_USING_DMA
    struct
    {
        DMA_HandleTypeDef handle;
        rt_size_t last_index;
    } dma;
#endif
    rt_uint8_t uart_dma_flag;
    struct rt_serial_device serial;
};

// .\libraries\HAL_Drivers\drv_usart.c

static struct stm32_uart uart_obj[sizeof(uart_config) / sizeof(uart_config[0])] = {0};

static struct stm32_uart_config uart_config[] =
{
#ifdef BSP_USING_UART1
    {                                                           
    	.name = "uart1",                                            
    	.Instance = USART1,                                        
    	.irq_type = USART1_IRQn,                                   
    }
#endif
......
};

stm32_uart結構中包含的UART_HandleTypeDef由芯片廠商ST提供的標準庫HAL提供,UART驅動層用以實現上層rt_uart_ops接口函數的基礎函數也由HAL庫提供,可能用到的HAL庫函數如下:

// .\libraries\STM32L4xx_HAL\STM32L4xx_HAL_Driver\Inc\stm32l4xx_hal_uart.h

HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_UART_DeInit(UART_HandleTypeDef *huart);

HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);

void HAL_UART_IRQHandler(UART_HandleTypeDef *huart);
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart);

HAL_UART_StateTypeDef HAL_UART_GetState(UART_HandleTypeDef *huart);
uint32_t              HAL_UART_GetError(UART_HandleTypeDef *huart);

串口設備驅動層使用HAL庫函數實現串口設備框架層的接口函數集合rt_uart_ops,並將實現的接口函數集合stm32_uart_ops註冊到上層的過程如下:

// .\libraries\HAL_Drivers\drv_usart.c

static const struct rt_uart_ops stm32_uart_ops =
{
    .configure = stm32_configure,
    .control = stm32_control,
    .putc = stm32_putc,
    .getc = stm32_getc,
};

int rt_hw_usart_init(void)
{
    rt_size_t obj_num = sizeof(uart_obj) / sizeof(struct stm32_uart);
    struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;
    rt_err_t result = 0;

    stm32_uart_get_dma_config();
    
    for (int i = 0; i < obj_num; i++)
    {
        uart_obj[i].config = &uart_config[i];
        uart_obj[i].serial.ops    = &stm32_uart_ops;
        uart_obj[i].serial.config = config;

#if defined(RT_SERIAL_USING_DMA)
        if(uart_obj[i].uart_dma_flag)
        {
            /* register UART device */
            result = rt_hw_serial_register(&uart_obj[i].serial,uart_obj[i].config->name,
                                           RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX| RT_DEVICE_FLAG_DMA_RX 
                                           ,&uart_obj[i]);
        }
        else
#endif
        {
            /* register UART device */
            result = rt_hw_serial_register(&uart_obj[i].serial,uart_obj[i].config->name,
                                           RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX
                                           ,&uart_obj[i]);
        }
        RT_ASSERT(result == RT_EOK);
    }

    return result;
}

函數rt_hw_usart_init內部有一個for循環,可以將所有啓用的串口設備全部初始化並註冊到 I/O 設備管理層。啓用串口設備也比較簡單,先使用CubeMX配置好要啓用的串口設備引腳,再在Kconfig和menuconfig中配置並使能相關的宏定義就可以了。rt_hw_usart_init函數已經在board.c文件中的rt_hw_board_init()函數內部被調用(需要定義宏RT_USING_SERIAL),在博客系統啓動與初始化過程中有介紹。

1.1.3 串口設備中斷回調支持

到這裏就可以通過 I/O 設備管理接口rt_device_ops來訪問uart串口設備了,還記得rt_device設備描述結構中有兩個中斷回調函數,I/O設備管理層也有兩個函數接口分別用來設置這兩個可由用戶自定義的中斷回調函數嗎?

// .\rt-thread-4.0.1\include\rtdef.h

/* Device structure */
struct rt_device
{
......
    /* device call back */
    rt_err_t (*rx_indicate)(rt_device_t dev, rt_size_t size);
    rt_err_t (*tx_complete)(rt_device_t dev, void *buffer);

#ifdef RT_USING_DEVICE_OPS
    const struct rt_device_ops *ops;
......
};

// .\rt-thread-4.0.1\include\rtthread.h

rt_err_t
rt_device_set_rx_indicate(rt_device_t dev,
                          rt_err_t (*rx_ind)(rt_device_t dev, rt_size_t size));
rt_err_t
rt_device_set_tx_complete(rt_device_t dev,
                          rt_err_t (*tx_done)(rt_device_t dev, void *buffer));

當我們調用這兩個接口函數設置自定義的中斷回調函數後,驅動層是如何實現中斷調用的呢?我們在驅動框架層源文件“serial.c”中查找關鍵字“rx_indicate”,發現其只在函數rt_hw_serial_isr()中被調用,部分代碼如下:

// .\rt-thread-4.0.1\components\drivers\serial\serial.c

/* ISR for serial interrupt */
void rt_hw_serial_isr(struct rt_serial_device *serial, int event)
{
    switch (event & 0xff)
    {
        case RT_SERIAL_EVENT_RX_IND:
        {
            ......
            while (1)
            {
                ......
            /* invoke callback */
            if (serial->parent.rx_indicate != RT_NULL)
            {
                ......
                if (rx_length)
                {
                    serial->parent.rx_indicate(&serial->parent, rx_length);
                }
            }
            break;
        }
        case RT_SERIAL_EVENT_TX_DONE:
        ......
#ifdef RT_SERIAL_USING_DMA
        case RT_SERIAL_EVENT_TX_DMADONE:
        {
            ......
            /* invoke callback */
            if (serial->parent.tx_complete != RT_NULL)
            {
                serial->parent.tx_complete(&serial->parent, (void*)last_data_ptr);
            }
            break;
        }
        case RT_SERIAL_EVENT_RX_DMADONE:
        {
            ......
            if (serial->config.bufsz == 0)
            {
                ......
                serial->parent.rx_indicate(&(serial->parent), length);
                rx_dma->activated = RT_FALSE;
            }
            else
            {
                ......
                /* invoke callback */
                if (serial->parent.rx_indicate != RT_NULL)
                {
                    serial->parent.rx_indicate(&(serial->parent), length);
                }
            }
            break;
        }
#endif /* RT_SERIAL_USING_DMA */
    }
}

要想實現中斷回調功能,函數rt_hw_serial_isr()也應該在底層被調用,我們在串口驅動層源文件“drv_usart.c”中查找關鍵詞“rt_hw_serial_isr”,發現其在兩個函數uart_isr()和HAL_UART_RxCpltCallback()中被調用,後面這個是HAL庫函數,在完成UART接收後會自動被調用,我們看前面這個函數uart_isr()被誰調用呢?

// .\libraries\HAL_Drivers\drv_usart.c

static void uart_isr(struct rt_serial_device *serial)
{
    ......
    /* UART in mode Receiver -------------------------------------------------*/
    if ((__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_RXNE) != RESET) &&
        (__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_RXNE) != RESET))
    {
        rt_hw_serial_isr(serial, RT_SERIAL_EVENT_RX_IND);
    }
#ifdef RT_SERIAL_USING_DMA
    else if ((uart->uart_dma_flag) && (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_IDLE) != RESET) &&
             (__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_IDLE) != RESET))
    {
        ......
        if (recv_len)
        {
            rt_hw_serial_isr(serial, RT_SERIAL_EVENT_RX_DMADONE | (recv_len << 8));
        }
        __HAL_UART_CLEAR_IDLEFLAG(&uart->handle);
    }
#endif
    ......
}

#if defined(BSP_USING_UART1)
void USART1_IRQHandler(void)
{
    /* enter interrupt */
    rt_interrupt_enter();

    uart_isr(&(uart_obj[UART1_INDEX].serial));
    
    /* leave interrupt */
    rt_interrupt_leave();
}
......

由上面的代碼可以看出,函數uart_isr()最終被UART中斷請求函數USARTx_IRQHandler()調用(每個啓用的UART中斷請求函數都會調用),函數USARTx_IRQHandler()也是HAL庫函數提供的,在UART設備觸發中斷後會被自動調用。

在串口設備中設置接收回調函數rx_indicate()非常常見,比如在finsh shell組件中,設置finsh shell的交互設備爲串口設備,就需要設置接收回調函數。當用戶輸入數據後會觸發UART中斷,自動調用用戶設置的接收回調函數,及時讀取用戶輸入的數據,而不用進行低效的輪詢檢查。finsh shell組件設置交互設備和接收回調函數的過程代碼如下:

// .\rt-thread-4.0.1\components\finsh\shell.c

void finsh_set_device(const char *device_name)
{
    rt_device_t dev = RT_NULL;

    RT_ASSERT(shell != RT_NULL);
    dev = rt_device_find(device_name);
    if (dev == RT_NULL)
    {
        rt_kprintf("finsh: can not find device: %s\n", device_name);
        return;
    }

    /* check whether it's a same device */
    if (dev == shell->device) return;
    /* open this device and set the new device in finsh shell */
    if (rt_device_open(dev, RT_DEVICE_OFLAG_RDWR | RT_DEVICE_FLAG_INT_RX | \
                       RT_DEVICE_FLAG_STREAM) == RT_EOK)
    {
        if (shell->device != RT_NULL)
        {
            /* close old finsh device */
            rt_device_close(shell->device);
            rt_device_set_rx_indicate(shell->device, RT_NULL);
        }

        /* clear line buffer before switch to new device */
        memset(shell->line, 0, sizeof(shell->line));
        shell->line_curpos = shell->line_position = 0;

        shell->device = dev;
        rt_device_set_rx_indicate(dev, finsh_rx_ind);
    }
}

static rt_err_t finsh_rx_ind(rt_device_t dev, rt_size_t size)
{
    RT_ASSERT(shell != RT_NULL);

    /* release semaphore to let finsh thread rx data */
    rt_sem_release(&shell->rx_sem);

    return RT_EOK;
}

static int finsh_getchar(void)
{
#ifdef RT_USING_POSIX
    return getchar();
#else
    char ch = 0;

    RT_ASSERT(shell != RT_NULL);
    while (rt_device_read(shell->device, -1, &ch, 1) != 1)
        rt_sem_take(&shell->rx_sem, RT_WAITING_FOREVER);

    return (int)ch;
#endif
}

UART是一種比較簡單的雙工對等總線,沒有主從之分,所以在串口設備描述結構中並沒有專門區分串口總線與串口設備。由於UART在收發數據時不需要同步時鐘線,是一種異步傳輸協議,雙方進行通信時需要約定一致的波特率,常見的波特率比如115200bps,通信速率並不高,由於協議簡單,常用於調試接口輸出log信息。

1.2 I2C設備驅動分層

STM32平臺由於軟件模擬IIC比較常用,且方便移植,RT-Thread的IIC設備驅動也是使用的軟件模擬方式實現的。使用軟件模擬I2C協議,雖然可以根據需要靈活配置模擬IIC通信的GPIO引腳,但協議層需要自己實現,而且軟件模擬IIC工作效率比硬件IIC低不少,一般常用400kbps傳輸速率。

1.2.1 I2C設備驅動框架層

I2C設備是一種單工非對等總線,所以在I2C設備描述結構中對I2C總線與I2C設備分別描述,I2C設備驅動框架只對I2C總線進行了描述,I2C設備在具體的設備描述結構中出現,比如AHT10溫溼度傳感器的設備描述結構“aht10_device”就是一個I2C設備,aht10_device結構體繼承自rt_i2c_bus_device。

I2C協議的設備驅動框架層比UART協議更復雜,我們可以將其再細分爲三層,分別爲bus_dev layer、core layer、bit-ops layer三層,這三層的順序從高到低:

  • bus_dev layer更接近 I/O 設備管理層,實現I/O管理接口,並將其註冊到 I/O 設備管理層;
  • core layer利用底層實現的接口函數rt_i2c_bus_device_ops實現對指定I2C設備的同步訪問;
  • bit-ops layer更接近設備驅動層,同時也是I2C協議的軟件模擬實現層,使用I2C驅動層提供的函數接口rt_i2c_bit_ops實現core layer需要的rt_i2c_bus_device_ops接口函數。

I2C總線設備的描述結構與接口函數集合如下:

// .\rt-thread-4.0.1\components\drivers\include\drivers\i2c.h

/*for i2c bus driver*/
struct rt_i2c_bus_device
{
    struct rt_device parent;
    const struct rt_i2c_bus_device_ops *ops;
    rt_uint16_t  flags;
    rt_uint16_t  addr;
    struct rt_mutex lock;
    rt_uint32_t  timeout;
    rt_uint32_t  retries;
    void *priv;
};

struct rt_i2c_bus_device_ops
{
    rt_size_t (*master_xfer)(struct rt_i2c_bus_device *bus,
                             struct rt_i2c_msg msgs[],
                             rt_uint32_t num);
    rt_size_t (*slave_xfer)(struct rt_i2c_bus_device *bus,
                            struct rt_i2c_msg msgs[],
                            rt_uint32_t num);
    rt_err_t (*i2c_bus_control)(struct rt_i2c_bus_device *bus,
                                rt_uint32_t,
                                rt_uint32_t);
};

由於I2C總線支持多主多從(同一時刻只允許有一個主設備)方式工作,通信時涉及到從設備尋址與讀寫標記等,需要一個專門的數據結構來描述I2C通信的消息幀,從上面的I2C總線接口函數參數類型可以看出,I2C消息幀結構名爲rt_i2c_msg,描述代碼如下:
I2C通信消息幀結構

// .\rt-thread-4.0.1\components\drivers\include\drivers\i2c.h

struct rt_i2c_msg
{
    rt_uint16_t addr;
    rt_uint16_t flags;
    rt_uint16_t len;
    rt_uint8_t  *buf;
};

// flags type
#define RT_I2C_WR                0x0000
#define RT_I2C_RD               (1u << 0)
#define RT_I2C_ADDR_10BIT       (1u << 2)  /* this is a ten bit chip address */
#define RT_I2C_NO_START         (1u << 4)
#define RT_I2C_IGNORE_NACK      (1u << 5)
#define RT_I2C_NO_READ_ACK      (1u << 6)  /* when I2C reading, we do not ACK */

從這裏可以看出,I2C設備訪問接口用來傳遞讀寫數據的參數結構一般爲rt_i2c_msg或者將消息幀結構體中的各個成員都作爲參數傳入,I2C core layer實現的I2C設備訪問接口如下:

// .\rt-thread-4.0.1\components\drivers\include\drivers\i2c.h

rt_size_t rt_i2c_transfer(struct rt_i2c_bus_device *bus,
                          struct rt_i2c_msg         msgs[],
                          rt_uint32_t               num);
rt_size_t rt_i2c_master_send(struct rt_i2c_bus_device *bus,
                             rt_uint16_t               addr,
                             rt_uint16_t               flags,
                             const rt_uint8_t         *buf,
                             rt_uint32_t               count);
rt_size_t rt_i2c_master_recv(struct rt_i2c_bus_device *bus,
                             rt_uint16_t               addr,
                             rt_uint16_t               flags,
                             rt_uint8_t               *buf,
                             rt_uint32_t               count);

當底層驅動都實現後,用戶可以直接調用上面的接口函數來訪問I2C設備,但該接口函數的參數與 I/O 設備管理接口並不兼容,因此RT-Thread增加了bus_dev layer用來實現兩種接口之間的適配。

  • I2C bus_dev layer

bus_dev layer跟前面介紹的串口驅動框架層類似,也是實現並向上層註冊 I/O管理接口,只不過用以實現 I/O 管理接口的函數並非是總線訪問接口函數rt_i2c_bus_device_ops,而是下面的I2C core layer利用rt_i2c_bus_device_ops實現的接口函數(接口函數原型見上面的代碼)。

該層實現並向上層註冊 I/O管理接口的過程如下:

// .\rt-thread-4.0.1\components\drivers\i2c\i2c_dev.c

#ifdef RT_USING_DEVICE_OPS
const static struct rt_device_ops i2c_ops = 
{
    RT_NULL, 
    RT_NULL,
    RT_NULL,
    i2c_bus_device_read,
    i2c_bus_device_write,
    i2c_bus_device_control
};
#endif

rt_err_t rt_i2c_bus_device_device_init(struct rt_i2c_bus_device *bus,
                                       const char               *name)
{
    struct rt_device *device;
    RT_ASSERT(bus != RT_NULL);

    device = &bus->parent;

    device->user_data = bus;

    /* set device type */
    device->type    = RT_Device_Class_I2CBUS;
    /* initialize device interface */
#ifdef RT_USING_DEVICE_OPS
    device->ops     = &i2c_ops;
#else
    ......
#endif
    /* register to device manager */
    rt_device_register(device, name, RT_DEVICE_FLAG_RDWR);

    return RT_EOK;
}

static rt_size_t i2c_bus_device_read(rt_device_t dev,
                                     rt_off_t    pos,
                                     void       *buffer,
                                     rt_size_t   count)
{
    ......
    addr = pos & 0xffff;
    flags = (pos >> 16) & 0xffff;

    return rt_i2c_master_recv(bus, addr, flags, buffer, count);
}

static rt_size_t i2c_bus_device_write(rt_device_t dev,
                                      rt_off_t    pos,
                                      const void *buffer,
                                      rt_size_t   count)
{
    ......
    addr = pos & 0xffff;
    flags = (pos >> 16) & 0xffff;

    return rt_i2c_master_send(bus, addr, flags, buffer, count);
}

從上面的代碼可以看出,當使用 I/O 設備管理接口訪問I2C設備時,參數pos就要特別設置了,32位的pos應包含 i2c設備的addr與flags信息(uart設備沒有使用pos參數)。

  • I2C core layer

I2C core layer理解起來比較簡單,就是利用底層提供的接口函數rt_i2c_bus_device_ops,實現bus_dev layer需要的接口函數,當然I2C core layer實現的接口函數也可以被用戶直接調用。該層接口函數實現並向上層註冊的過程如下:

// .\rt-thread-4.0.1\components\drivers\i2c\i2c_core.c

rt_err_t rt_i2c_bus_device_register(struct rt_i2c_bus_device *bus,
                                    const char               *bus_name)
{
	......
    res = rt_i2c_bus_device_device_init(bus, bus_name);
    return res;
}

rt_size_t rt_i2c_transfer(struct rt_i2c_bus_device *bus,
                          struct rt_i2c_msg         msgs[],
                          rt_uint32_t               num)
{
    rt_size_t ret;

    if (bus->ops->master_xfer)
    {
        for (ret = 0; ret < num; ret++)
        {
        rt_mutex_take(&bus->lock, RT_WAITING_FOREVER);
        ret = bus->ops->master_xfer(bus, msgs, num);
        rt_mutex_release(&bus->lock);

        return ret;
    }
    else
        return 0;
}

rt_size_t rt_i2c_master_send(struct rt_i2c_bus_device *bus,
                             rt_uint16_t               addr,
                             rt_uint16_t               flags,
                             const rt_uint8_t         *buf,
                             rt_uint32_t               count)
{
    ......
    ret = rt_i2c_transfer(bus, &msg, 1);

    return (ret > 0) ? count : ret;
}

rt_size_t rt_i2c_master_recv(struct rt_i2c_bus_device *bus,
                             rt_uint16_t               addr,
                             rt_uint16_t               flags,
                             rt_uint8_t               *buf,
                             rt_uint32_t               count)
{
    ......
    ret = rt_i2c_transfer(bus, &msg, 1);

    return (ret > 0) ? count : ret;
}
  • I2C bit-ops layer

bit-ops layer主要實現I2C協議的軟件模擬,使用 I2C 設備驅動層提供的接口函數rt_i2c_bit_ops實現 I2C 總線設備訪問接口rt_i2c_bus_device_ops,I2C總線協議通訊時序圖可以參考文件I2C總線規範,這裏主要介紹驅動分層思想,就不贅述協議實現了。bit-ops layer向core layer註冊接口函數rt_i2c_bus_device_ops的過程如下:

// .\rt-thread-4.0.1\components\drivers\i2c\i2c-bit-ops.c

static const struct rt_i2c_bus_device_ops i2c_bit_bus_ops =
{
    i2c_bit_xfer,
    RT_NULL,
    RT_NULL
};

rt_err_t rt_i2c_bit_add_bus(struct rt_i2c_bus_device *bus,
                            const char               *bus_name)
{
    bus->ops = &i2c_bit_bus_ops;

    return rt_i2c_bus_device_register(bus, bus_name);
}

// .\rt-thread-4.0.1\components\drivers\include\drivers\i2c-bit-ops.h

struct rt_i2c_bit_ops
{
    void *data;            /* private data for lowlevel routines */
    void (*set_sda)(void *data, rt_int32_t state);
    void (*set_scl)(void *data, rt_int32_t state);
    rt_int32_t (*get_sda)(void *data);
    rt_int32_t (*get_scl)(void *data);

    void (*udelay)(rt_uint32_t us);

    rt_uint32_t delay_us;  /* scl and sda line delay */
    rt_uint32_t timeout;   /* in tick */
};

bit-ops layer使用的接口函數rt_i2c_bit_ops就要靠底層的I2C驅動層提供了,rt_i2c_bit_ops接口函數主要是對SDA、SCL引腳狀態的獲取與設置(也即對GPIO引腳電平的讀取與設置),加上I2C協議時許圖中引腳狀態轉換需要的延時函數等。

1.2.2 I2C設備驅動層

I2C驅動層的設備描述結構如下:

// .\libraries\HAL_Drivers\drv_soft_i2c.h

/* stm32 i2c dirver class */
struct stm32_i2c
{
    struct rt_i2c_bit_ops ops;
    struct rt_i2c_bus_device i2c2_bus;
};

// .\libraries\HAL_Drivers\drv_soft_i2c.c

static struct stm32_i2c i2c_obj[sizeof(soft_i2c_config) / sizeof(soft_i2c_config[0])];

static const struct stm32_soft_i2c_config soft_i2c_config[] =
{
#ifdef BSP_USING_I2C1
    {                                                    
        .scl = BSP_I2C1_SCL_PIN,                         
        .sda = BSP_I2C1_SDA_PIN,                         
        .bus_name = "i2c1",                              
    }
#endif
......
};
......

既然使用了I2C軟件模擬協議,可靈活配置I2C通訊引腳SCL_PIN與SDA_PIN,我們在使用I2C協議前就需要配置這兩個引腳的GPIO編號。

I2C設備驅動層是配置軟件模擬I2C設備的GPIO引腳,對SDA與SCL引腳的管理是使用RT-Thread提供的pin設備驅動接口,相當於I2C軟件模擬協議是在pin設備上層的,pin設備的驅動分層可以參考博客PIN設備對象管理

I2C設備驅動層實現並向驅動框架層註冊接口函數rt_i2c_bit_ops的過程如下:

// .\libraries\HAL_Drivers\drv_soft_i2c.c

static const struct rt_i2c_bit_ops stm32_bit_ops_default =
{
    .data     = RT_NULL,
    .set_sda  = stm32_set_sda,
    .set_scl  = stm32_set_scl,
    .get_sda  = stm32_get_sda,
    .get_scl  = stm32_get_scl,
    .udelay   = stm32_udelay,
    .delay_us = 1,
    .timeout  = 100
};

/* I2C initialization function */
int rt_hw_i2c_init(void)
{
    rt_size_t obj_num = sizeof(i2c_obj) / sizeof(struct stm32_i2c);
    rt_err_t result;

    for (int i = 0; i < obj_num; i++)
    {
        i2c_obj[i].ops = stm32_bit_ops_default;
        i2c_obj[i].ops.data = (void*)&soft_i2c_config[i];
        i2c_obj[i].i2c2_bus.priv = &i2c_obj[i].ops;
        stm32_i2c_gpio_init(&i2c_obj[i]);
        result = rt_i2c_bit_add_bus(&i2c_obj[i].i2c2_bus, soft_i2c_config[i].bus_name);
        RT_ASSERT(result == RT_EOK);
        stm32_i2c_bus_unlock(&soft_i2c_config[i]);
    }

    return RT_EOK;
}
INIT_BOARD_EXPORT(rt_hw_i2c_init);

從上面的代碼可以看出,I2C設備支持自動初始化,用戶在Kconfig和menuconfig中配置好I2C軟件模擬設備的GPIO引腳與相應的宏定義後,就可以使用I2C總線設備訪問接口或 I/O 設備管理層統一接口來訪問I2C設備了。

RT-Thread爲I2C設備提供的驅動並不支持中斷響應,因此也不支持設置中斷回調函數,只能由I2C總線設備主動讀寫指定I2C從設備。當然也可以在應用中設置一個緩衝區,爲I2C設備輪詢訪問創建一個線程,將輪詢讀取的I2C設備數據保存到緩衝區。

二、主從分離思想

從前面 I/O 設備管理器分層模型可以看出,I2C驅動與SPI驅動都分爲總線類型和設備類型,UART驅動則沒有作此區分,這是爲何呢?

前面已經介紹過,UART串口屬於雙工對等總線,沒有主從之分,所以在串口設備描述結構中並沒有專門區分串口總線與串口設備。I2C與SPI都屬於非對等總線,支持一主多從方式工作,也即多個從設備可以共用一組I2C或SPI總線進行通信,共用同一組總線進行通信的多個從設備訪問接口一樣,也有很多共有的屬性配置信息。因此抽取出總線類型作爲主機控制器來管理多個從設備的通信,可以帶來跟驅動分層類似的好處,這就是主機控制器與外設的分離思想。

什麼是總線呢?總線實際上是處理器和一個或多個設備之間進行數據傳輸的通道,所有的設備都通過總線與CPU進行通信。假設有一個叫 GITCHAT 的網卡,它需要連接到 CPU 的內部總線上,需要地址總線、數據總線和控制總線,以及中斷 pin 腳等。外接的 GITCHAT 網卡硬件稱爲外設或從設備,連接到SOC上的一組 I/O 引腳,這組 I/O 引腳實際就是SOC引出的一組總線(比如SPI總線或SDIO總線),程序設計中可以把這組總線封裝爲一個主機控制器,作爲一個設備對象來管理連接在其上面的多個外設。SOC內CPU與GITCHAT主機控制器直接的總線連接示意圖如下(圖片取自博客:Linux 總線、設備、驅動模型的探究):
CPU與主機控制器關係
由此可見,總線類型或主機控制器是跟SOC或CPU芯片平臺相關的,外設或從設備是跟具體的硬件設備相關的。我們在項目開發過程中,不僅外接的硬件設備(比如各種sensor或flash chip)型號會經常更換,我們使用的CPU或SOC主芯片型號也常會更換,如果將主機控制器驅動與外設驅動分開描述管理,能在更換CPU/SOC和硬件外設時,更方便的進行驅動移植,極大減少驅動新增開發量。反之,如果將主機控制器驅動與外設驅動耦合到一塊開發,外設驅動將和SOC/CPU強相關,當更換CPU/SOC時外設驅動需要全部更改,對驅動開發管理很不友好。

隨着芯片與外設總類的增加,系統的拓撲結構也越來越複雜,對熱插拔、跨平臺移植性的要求也越來越高,爲適應這種形勢的需要,從Linux 2.6內核開始提供了全新的設備模型,其優勢在於採用了總線的模型對設備與驅動進行了管理,提高了程序的可移植性。RT-Thread在設計時,更多的借鑑了Linux的設計思想,當然也吸收了Linux的總線設備驅動模型,Linux的總線設備模型示意圖如下(圖片取自博客:驅動程序分層分離概念-總線設備驅動模型):
Linux總線設備驅動模型
Linux連接的外設通常比較複雜,有些外設並不像一般的sensor或flash chip那樣,只需要單一的總線協議(比如I2C或SPI總線)完成數據訪問即可。比如WiFi網卡外設,雖然與CPU/SOC之間通過一種總線協議(比如SDIO總線)進行通信,但在SDIO總線協議之上還需要專門的WiFi驅動才能使該無線網卡正常工作,該WiFi驅動也是跟外設硬件相關的,但常常一個WiFi驅動可以兼容同一系列多個型號的WiFi網卡,因此Linux把驅動與外設也分開管理,總線、驅動、外設共同構成了Linux的總線設備驅動模型,總線則負責外設與驅動之間的匹配。

RT-Thread比較複雜的協議(比如SDIO驅動、USB驅動)都使用Linux的總線設備驅動模型,比較簡單的協議(比如I2C驅動、SPI驅動)則對上面的總線設備驅動模型進行簡化,只保留了總線類型與設備類型,並沒有將驅動專門分離出來進行管理。

總線、設備、驅動三者都是跟硬件相關的,我們在進行驅動開發時,三者都需要完成對象創建、設備註冊等工作,下面先以簡單的SPI驅動爲例,回顧下具體的設備驅動是如何將驅動分層與主從分離思想實現在代碼中的。總線設備驅動模型相對複雜,後面在介紹SDIO驅動WiFi無線網卡時再詳細介紹總線設備驅動模型的實現。

2.1 SPI設備主從分離

SPI設備是一種雙工非對等總線協議,支持一主多從方式工作,因此跟I2C協議類似,對SPI總線設備與SPI從設備分別進行描述。I2C設備通過消息幀包含的地址信息尋找目標從設備,SPI設備則通過片選信號線NCS的電平狀態來選擇目標從設備。

2.1.1 SPI設備驅動框架層

SPI設備驅動框架層跟I2C設備類似,也可以將其細分爲兩層,也即bus_dev layer和core layer,SPI設備並不需要軟件模擬協議實現,所以不需要I2C設備中的bit-ops layer,這兩層的功能跟I2C設備類似:

  • bus_dev layer 實現I/O管理接口,並將其註冊到 I/O 設備管理層;
  • core layer 利用設備驅動層實現的接口函數rt_spi_ops實現對指定SPI設備的同步訪問;

SPI總線設備描述結構與總線接口函數集合如下:

// .\rt-thread-4.0.1\components\drivers\include\drivers\spi.h

struct rt_spi_bus
{
    struct rt_device parent;
    rt_uint8_t mode;
    const struct rt_spi_ops *ops;

    struct rt_mutex lock;
    struct rt_spi_device *owner;
};

/**
 * SPI operators
 */
struct rt_spi_ops
{
    rt_err_t (*configure)(struct rt_spi_device *device, struct rt_spi_configuration *configuration);
    rt_uint32_t (*xfer)(struct rt_spi_device *device, struct rt_spi_message *message);
};

SPI從設備或外設的描述結構如下:

// .\rt-thread-4.0.1\components\drivers\include\drivers\spi.h

/**
 * SPI Virtual BUS, one device must connected to a virtual BUS
 */
struct rt_spi_device
{
    struct rt_device parent;
    struct rt_spi_bus *bus;

    struct rt_spi_configuration config;
    void   *user_data;
};

SPI設備通過片選信號線選擇從設備,其支持全雙工,也即可以同時進行發送、接收數據操作,因此訪問SPI設備的接口參數也以封裝的消息幀格式傳遞,SPI設備消息幀數據結構如下:

// .\rt-thread-4.0.1\components\drivers\include\drivers\spi.h

/**
 * SPI message structure
 */
struct rt_spi_message
{
    const void *send_buf;
    void *recv_buf;
    rt_size_t length;
    struct rt_spi_message *next;

    unsigned cs_take    : 1;
    unsigned cs_release : 1;
};

SPI總線設備接口rt_spi_ops.xfer就使用了rt_spi_message作爲參數傳入,如果嫌每次構造rt_spi_message結構比較麻煩,SPI core layer也爲我們實現了幾個不需要傳入rt_spi_message結構體,只需要傳入數據緩衝區首地址與長度的接口函數原型如下:

// .\rt-thread-4.0.1\components\drivers\include\drivers\spi.h

/**
 * This function transfers a message list to the SPI device.
 */
struct rt_spi_message *rt_spi_transfer_message(struct rt_spi_device  *device,
                                               struct rt_spi_message *message);

/**
 * This function transmits data to SPI device.
 */
rt_size_t rt_spi_transfer(struct rt_spi_device *device,
                          const void           *send_buf,
                          void                 *recv_buf,
                          rt_size_t             length);

/* set configuration on SPI device */
rt_err_t rt_spi_configure(struct rt_spi_device        *device,
                          struct rt_spi_configuration *cfg);
                          
/* send data then receive data from SPI device */
rt_err_t rt_spi_send_then_recv(struct rt_spi_device *device,
                               const void           *send_buf,
                               rt_size_t             send_length,
                               void                 *recv_buf,
                               rt_size_t             recv_length);

rt_err_t rt_spi_send_then_send(struct rt_spi_device *device,
                               const void           *send_buf1,
                               rt_size_t             send_length1,
                               const void           *send_buf2,
                               rt_size_t             send_length2);

當底層驅動都實現後,用戶可以直接調用上面的接口函數來訪問SPI設備,但該接口函數的參數與 I/O 設備管理接口並不兼容,因此RT-Thread增加了bus_dev layer用來實現兩種接口之間的適配。

  • SPI bus_dev layer

bus_dev layer跟前面介紹的 I2C 驅動框架層類似,也是使用SPI core layer利用rt_spi_ops實現的接口函數(特別是rt_spi_transfer()),向 I/O 設備管理層註冊統一的接口。

既然SPI總線與設備分開描述,二者都是與硬件相關的(SPI總線與CPU/SOC相關,SPI設備與外設硬件相關),二者都需要向上註冊設備對象。因此,bus_dev layer包含SPI bus的註冊和SPI device的註冊兩部分,這兩種設備向上層註冊 I/O管理接口的過程如下:

  • SPI bus設備對象與訪問接口的註冊過程:
// .\rt-thread-4.0.1\components\drivers\spi\spi_dev.c

#ifdef RT_USING_DEVICE_OPS
const static struct rt_device_ops spi_bus_ops = 
{
    RT_NULL,
    RT_NULL,
    RT_NULL,
    _spi_bus_device_read,
    _spi_bus_device_write,
    _spi_bus_device_control
};
#endif

rt_err_t rt_spi_bus_device_init(struct rt_spi_bus *bus, const char *name)
{
    struct rt_device *device;
    RT_ASSERT(bus != RT_NULL);

    device = &bus->parent;

    /* set device type */
    device->type    = RT_Device_Class_SPIBUS;
    /* initialize device interface */
#ifdef RT_USING_DEVICE_OPS
    device->ops     = &spi_bus_ops;
#else
    ......
#endif
    /* register to device manager */
    return rt_device_register(device, name, RT_DEVICE_FLAG_RDWR);
}
  • SPI device設備對象與訪問接口的註冊過程:
// .\rt-thread-4.0.1\components\drivers\spi\spi_dev.c

#ifdef RT_USING_DEVICE_OPS
const static struct rt_device_ops spi_device_ops = 
{
    RT_NULL,
    RT_NULL,
    RT_NULL,
    _spidev_device_read,
    _spidev_device_write,
    _spidev_device_control
};
#endif

rt_err_t rt_spidev_device_init(struct rt_spi_device *dev, const char *name)
{
    struct rt_device *device;
    RT_ASSERT(dev != RT_NULL);

    device = &(dev->parent);

    /* set device type */
    device->type    = RT_Device_Class_SPIDevice;
#ifdef RT_USING_DEVICE_OPS
    device->ops     = &spi_device_ops;
#else
    ......
#endif
    /* register to device manager */
    return rt_device_register(device, name, RT_DEVICE_FLAG_RDWR);
}

接口函數spi_bus_ops與spi_device_ops的實現主要通過調用SPI core layer提供的rt_spi_transfer()函數實現的,且向上註冊的接口函數rt_device_ops中的參數pos也沒有使用。

  • SPI core layer

SPI core layer跟I2C也類似,也是利用設備驅動層提供的接口函數rt_spi_ops,實現bus_dev layer需要的接口函數,當然SPI core layer實現的接口函數也可以被用戶直接調用。該層實現的接口函數向上層註冊的過程也分爲SPI bus和SPI device兩部分,這兩種設備向上層註冊的過程如下:

  • SPI bus的註冊過程:
// .\rt-thread-4.0.1\components\drivers\spi\spi_core.c

rt_err_t rt_spi_bus_register(struct rt_spi_bus       *bus,
                             const char              *name,
                             const struct rt_spi_ops *ops)
{
    rt_err_t result;

    result = rt_spi_bus_device_init(bus, name);
    if (result != RT_EOK)
        return result;

    /* initialize mutex lock */
    rt_mutex_init(&(bus->lock), name, RT_IPC_FLAG_FIFO);
    /* set ops */
    bus->ops = ops;
    /* initialize owner */
    bus->owner = RT_NULL;
    /* set bus mode */
    bus->mode = RT_SPI_BUS_MODE_SPI;

    return RT_EOK;
}
  • SPI device與特定SPI bus的綁定過程:
// .\rt-thread-4.0.1\components\drivers\spi\spi_core.c

rt_err_t rt_spi_bus_attach_device(struct rt_spi_device *device,
                                  const char           *name,
                                  const char           *bus_name,
                                  void                 *user_data)
{
    rt_err_t result;
    rt_device_t bus;

    /* get physical spi bus */
    bus = rt_device_find(bus_name);
    if (bus != RT_NULL && bus->type == RT_Device_Class_SPIBUS)
    {
        device->bus = (struct rt_spi_bus *)bus;

        /* initialize spidev device */
        result = rt_spidev_device_init(device, name);
        if (result != RT_EOK)
            return result;

        rt_memset(&device->config, 0, sizeof(device->config));
        device->parent.user_data = user_data;

        return RT_EOK;
    }

    /* not found the host bus */
    return -RT_ERROR;
}

該層實現的接口函數原型在前面已經給出來了(比如rt_spi_transfer()),這些接口函數最終是通過調用SPI總線設備接口rt_spi_ops實現的,而rt_spi_ops則由SPI設備驅動層實現並向本層註冊。

2.1.2 SPI設備驅動層

SPI設備驅動層由於每個SPI從設備的片選引腳不一樣,因此需要在註冊/初始化SPI從設備時將片選引腳一起傳遞給上層,以便用戶據此訪問特定的SPI從設備。

SPI驅動層的設備描述結構和片選引腳CS描述結構如下:

// .\libraries\HAL_Drivers\drv_spi.h

/* stm32 spi dirver class */
struct stm32_spi
{
    SPI_HandleTypeDef handle;
    struct stm32_spi_config *config;
    struct rt_spi_configuration *cfg;

    struct
    {
        DMA_HandleTypeDef handle_rx;
        DMA_HandleTypeDef handle_tx;
    } dma;
    
    rt_uint8_t spi_dma_flag;
    struct rt_spi_bus spi_bus;
};

struct stm32_hw_spi_cs
{
    GPIO_TypeDef* GPIOx;
    uint16_t GPIO_Pin;
};

// .\libraries\HAL_Drivers\drv_spi.c

static struct stm32_spi spi_bus_obj[sizeof(spi_config) / sizeof(spi_config[0])] = {0};

static struct stm32_spi_config spi_config[] =
{
#ifdef BSP_USING_SPI1
    {                                                    
        .Instance = SPI1,                                
        .bus_name = "spi1",                              
    }
#endif
......
};

stm32_spi結構中包含的SPI_HandleTypeDef、DMA_HandleTypeDef等都由ST廠商的標準庫HAL提供(與UART設備驅動層類似),在SPI驅動層實現rt_spi_ops最終也是要調用HAL庫函數的,可能調用到的HAL庫函數如下:

// .\libraries\STM32L4xx_HAL\STM32L4xx_HAL_Driver\Inc\stm32l4xx_hal_spi.h

HAL_StatusTypeDef HAL_SPI_Init(SPI_HandleTypeDef *hspi);
HAL_StatusTypeDef HAL_SPI_DeInit(SPI_HandleTypeDef *hspi);

HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_SPI_Transmit_IT(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_SPI_Receive_IT(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_SPI_Transmit_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_SPI_Receive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);

void HAL_SPI_IRQHandler(SPI_HandleTypeDef *hspi);
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi);
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi);
void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi);

HAL_SPI_StateTypeDef HAL_SPI_GetState(SPI_HandleTypeDef *hspi);
uint32_t             HAL_SPI_GetError(SPI_HandleTypeDef *hspi);

SPI設備驅動層使用HAL庫函數實現SPI設備框架層的接口函數集合rt_spi_ops,並將實現的接口函數集合stm_spi_ops伴隨stm_spi_bus設備對象註冊到上層的過程如下:

// .\libraries\HAL_Drivers\drv_spi.c

static const struct rt_spi_ops stm_spi_ops =
{
    .configure = spi_configure,
    .xfer = spixfer,
};

static int rt_hw_spi_bus_init(void)
{
    rt_err_t result;
    for (int i = 0; i < sizeof(spi_config) / sizeof(spi_config[0]); i++)
    {
        spi_bus_obj[i].config = &spi_config[i];
        spi_bus_obj[i].spi_bus.parent.user_data = &spi_config[i];
        spi_bus_obj[i].handle.Instance = spi_config[i].Instance;

        if (spi_bus_obj[i].spi_dma_flag & SPI_USING_RX_DMA_FLAG)
        {
            /* Configure the DMA handler for Transmission process */
            ......
        }

        if (spi_bus_obj[i].spi_dma_flag & SPI_USING_TX_DMA_FLAG)
        {
            /* Configure the DMA handler for Transmission process */
            ......
        }

        result = rt_spi_bus_register(&spi_bus_obj[i].spi_bus, spi_config[i].bus_name, &stm_spi_ops);
        RT_ASSERT(result == RT_EOK);
    }

    return result;
}

int rt_hw_spi_init(void)
{
    stm32_get_dma_info();
    return rt_hw_spi_bus_init();
}
INIT_BOARD_EXPORT(rt_hw_spi_init);

SPI總線設備使用了自動初始化機制,當我們需要使用某個或某幾個SPI總線設備時,只需要使用CubeMX配置好想要的SPI外設,並在Kconfig和menuconfig中配置並定義相應的條件宏就可以了。

SPI從設備又如何向上層註冊呢?又如何將SPI從設備的片選引腳向上傳遞呢?該過程的實現代碼如下:

// .\libraries\HAL_Drivers\drv_spi.c

/**
  * Attach the spi device to SPI bus, this function must be used after initialization.
  */
rt_err_t rt_hw_spi_device_attach(const char *bus_name, const char *device_name, GPIO_TypeDef *cs_gpiox, uint16_t cs_gpio_pin)
{
    RT_ASSERT(bus_name != RT_NULL);
    RT_ASSERT(device_name != RT_NULL);

    rt_err_t result;
    struct rt_spi_device *spi_device;
    struct stm32_hw_spi_cs *cs_pin;

    /* initialize the cs pin && select the slave*/
    ......
    HAL_GPIO_Init(cs_gpiox, &GPIO_Initure);
    HAL_GPIO_WritePin(cs_gpiox, cs_gpio_pin, GPIO_PIN_SET);

    /* attach the device to spi bus*/
    spi_device = (struct rt_spi_device *)rt_malloc(sizeof(struct rt_spi_device));
    RT_ASSERT(spi_device != RT_NULL);
    cs_pin = (struct stm32_hw_spi_cs *)rt_malloc(sizeof(struct stm32_hw_spi_cs));
    RT_ASSERT(cs_pin != RT_NULL);
    cs_pin->GPIOx = cs_gpiox;
    cs_pin->GPIO_Pin = cs_gpio_pin;
    result = rt_spi_bus_attach_device(spi_device, device_name, bus_name, (void *)cs_pin);

    if (result != RT_EOK)
    {
        LOG_E("%s attach to %s faild, %d\n", device_name, bus_name, result);
    }

    RT_ASSERT(result == RT_EOK);
    LOG_D("%s attach to %s done", device_name, bus_name);

    return result;
}

SPI從設備的註冊或總線綁定接口並沒有使用自動初始化機制,因此當用戶想要將某個SPI從設備綁定到指定的SPI總線上時,需要自己主動調用接口rt_hw_spi_device_attach(),除了傳入SPI總線名和SPI設備名外,還需要傳入該SPI從設備使用的片選引腳,在配置片選引腳時別忘了使能對應的RCC時鐘。

以使用SPI接口的ENC28J60網卡設備的使用爲例(詳見博客LwIP協議棧移植),我們來看下是如何向特定SPI總線註冊SPI外設的:

// https://github.com/StreamAI/LwIP_Projects/blob/master/stm32l475-pandora-lwip/applications/enc28j60_port.c

int enc28j60_init(void)
{
    __HAL_RCC_GPIOD_CLK_ENABLE();
    rt_hw_spi_device_attach("spi2", "spi21", GPIOD, GPIO_PIN_5);

    /* attach enc28j60 to spi. spi21 cs - PD6 */
    enc28j60_attach("spi21");
    ......
    return 0;
}
INIT_COMPONENT_EXPORT(enc28j60_init);

配置好相應的SPI引腳和相應宏定義後,spi bus就會被自動初始化,如果要使用SPI外設,再配置好相應的CS片選引腳(注意使能該引腳對應的RCC時鐘),調用rt_hw_spi_device_attach()接口函數即可將SPI設備註冊/綁定到相應的SPI總線,然後通過SPI上層接口訪問該SPI外設。

SPI 驅動的主從分離並沒有體現出Linux的總線設備驅動模型結構,SDIO 驅動則體現了總線設備模型驅動的結構框架,限於篇幅,在後面的博客:SDIO設備對象管理 + AP6181(BCM43362) WiFi模塊中專門介紹SDIO驅動是如何體現Linux的總線設備驅動模型思想的。

更多文章:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章