RT-Thread 設備驅動SPI淺析及使用


OS版本:RT-Thread 4.0.0

測試BSP:STM32F407

SPI簡介

SPI總線框架其實和I2C差不多,可以說都是總線設備+從設備,但SPI設備的通信時序配置並不固定,也就是說控制特定設備的總線需要單獨配置;

SPI的特性是工作方式眾多,有標准SPI和QSPI

QSPI: QSPI 是 Queued SPI 的簡寫,是 Motorola 公司推出的 SPI 接口的擴展,比 SPI 應用更加廣泛。在 SPI 協議的基礎上,Motorola 公司對其功能進行了增強,增加了隊列傳輸機制,推出了隊列串行外圍接口協議(即 QSPI 協議)。使用該接口,用戶可以一次性傳輸包含多達 16 個 8 位或 16 位數據的傳輸隊列。一旦傳輸啟動,直到傳輸結束,都不需要 CPU 干預,極大的提高了傳輸效率。與 SPI 相比,QSPI 的最大結構特點是以 80 字節的 RAM 代替了 SPI 的發送和接收數據寄存器。

Dual SPI Flash: 對於 SPI Flash 而言全雙工並不常用,可以發送一個命令字節進入 Dual 模式,讓它工作在半雙工模式,用以加倍數據傳輸。這樣 MOSI 變成 SIO0(serial io 0),MISO 變成 SIO1(serial io 1),這樣一個時鍾周期內就能傳輸 2 個 bit 數據,加倍了數據傳輸。

Quad SPI Flash: 與 Dual SPI 類似,Quad SPI Flash增加了兩根 I/O 線(SIO2,SIO3),目的是一個時鍾內傳輸 4 個 bit 數據。

所以對於 SPI Flash,有標准 SPI Flash,Dual SPI Flash, Quad SPI Flash 三種類型。在相同時鍾下,線數越多傳輸速率越高。

SPI驅動分析

RT-Thread將驅動層抽象成設備,應用只需熟悉設備接口即可,驅動的分析我們從其 設備類的實現來剖析;

SPI的驅動里面主要包含兩種設備 rt_spi_device(掛載SPI總線並配置了使能引腳和通信時序之后的設備) 和 rt_spi_bus(SPI總線、類似Linux的SPI適配器);

rt_spi_bus 即 SPI 總線,rt_spi_device 是綁定 rt_spi_configuration 之后的設備

struct rt_spi_device
{
    struct rt_device parent;
    struct rt_spi_bus *bus;

    struct rt_spi_configuration config;
    void   *user_data;
};

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 操作具體設備之前,需要 rt_hw_spi_device_attach 對對應設備的SPI時序配置進行綁定,官方的說法是將設備掛載到SPI總線;

下面我們一步步來看 SPI 設備時怎么樣初始化和注冊設備的;

其中 SPI 總線bus 在drv_spi.c 中的 rt_hw_spi_init(), 系統啟動時進行了自動初始化

int rt_hw_spi_init(void)
{
    stm32_get_dma_info();
    return rt_hw_spi_bus_init();    //SPI-bus注冊
}
INIT_BOARD_EXPORT(rt_hw_spi_init);

而設備的掛載需要在用戶程序實現,可以使用前掛載,也可以使用自動初始化實現

// 自動初始化實現SPI設備掛載
int w25q_spi_device_init()
{
    __HAL_RCC_GPIOB_CLK_ENABLE();
    return rt_hw_spi_device_attach("spi1", "spi10", GPIOB, GPIO_PIN_14);  //設備掛載到SPI總線,抽象為 spi10 設備,同時使用時還需進行 rt_spi_configure
} 
INIT_DEVICE_EXPORT(w25q_spi_device_init);

注意設備驅動在使用之前,需要對掛載的設備進行 rt_spi_configure,當然也可以在自動初始化中就配置

    spi_dev_w25q = (struct rt_spi_device *)rt_device_find(name);
    if (!spi_dev_w25q)
    {
        rt_kprintf("spi sample run failed! can't find %s device!\n", name);
    }
    else
    {
        /* config spi */
        {
            struct rt_spi_configuration cfg;
            cfg.data_width = 8;
            cfg.mode = RT_SPI_MODE_0 | RT_SPI_MSB; /* SPI Compatible: Mode 0 and Mode 3 */
            cfg.max_hz = 50 * 1000 * 1000; /* 50M */
            rt_spi_configure(spi_dev_w25q, &cfg);
        }

以上是設備句柄的實現流程

SPI設備驅動

在 spi_dev.c 中可以看出,SPI設備的主要操作沒有主要使用 I/O 設備模型來操作;

其 spi_device_ops 沒有實現 contorl ,其讀寫則通過 rt_spi_transfer 實現;

但是官方給出的SPI驅動主要接口為 下面兩個,

rt_spi_configure

rt_spi_transfer_message

主要是 rt_spi_transfer_message 可以更加靈活的適應各種SPI設備的通信協議

當然還有其他數據傳輸接口,但都可以用 自定義傳輸 rt_spi_transfer_message 來實現,使用方式如下

        struct rt_spi_message msg1, msg2;

        msg1.send_buf   = &w25x_read_id;
        msg1.recv_buf   = RT_NULL;
        msg1.length     = 1;
        msg1.cs_take    = 1;
        msg1.cs_release = 0;
        msg1.next       = &msg2;

        msg2.send_buf   = RT_NULL;
        msg2.recv_buf   = id;
        msg2.length     = 5;
        msg2.cs_take    = 0;
        msg2.cs_release = 1;
        msg2.next       = RT_NULL;

        rt_spi_transfer_message(spi_dev_w25q, &msg1);
        rt_kprintf("use rt_spi_transfer_message() read w25q ID is:%x%x\n", id[3], id[4]);

    // 其等同於下面的操作
     rt_spi_send_then_recv(spi_dev_w25q, &w25x_read_id, 1, id, 5);
        rt_kprintf("use rt_spi_send_then_recv() read w25q ID is:%x%x\n", id[3], id[4]);

spi傳輸的核心實現在 drv_spi.c 中的 spixfer() 函數,實現spi數據的收發

先分析 spi 傳輸的消息體 

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;    /** 片選釋放 */
};

這樣第一包數據

msg1.cs_take = 1;
msg1.cs_release = 0;

中間包數據

msgx.cs_take = 0;
msgx.cs_release = 0;

最后一包的數據使用

msgn.cs_take = 0;
msgn.cs_release = 1;

同時應該指導 SPI 總線的工作原理,其在發送數據的同時也在接收數據,即發送數據時忽略了接收緩存,而接收數據也必須要發送數據來接收;

spixfer 則調用Hal 庫的 傳輸函數實現數據傳輸

HAL_SPI_TransmitReceive_DMA  /  HAL_SPI_TransmitReceive

HAL_SPI_Transmit_DMA  /  HAL_SPI_Transmit

HAL_SPI_Receive_DMA  /  HAL_SPI_Receive

這里我們注意 Hal 庫的SPI傳輸支持 輪詢、中斷即DMA 三種方式,其中輪詢支持超時檢錯,即數據傳輸完成、傳輸異常等可以較好發現,而DMA方式則需另外判斷標志位處理,當然有出錯回調處理;

SPI驅動的具體使用

修改 board 文件夾下的板級 Kconfig 文件,增加對 SPI 的支持

    menuconfig BSP_USING_SPI
        bool "Enable SPI BUS"
        default n
        select RT_USING_SPI
        if BSP_USING_SPI
            config BSP_USING_SPI1
                bool "Enable SPI1 BUS"
                default n

            config BSP_SPI1_TX_USING_DMA
                bool "Enable SPI1 TX DMA"
                depends on BSP_USING_SPI1
                default n
        
            config BSP_SPI1_RX_USING_DMA
                bool "Enable SPI1 RX DMA"
                depends on BSP_USING_SPI1
                select BSP_SPI1_TX_USING_DMA
                default n
            
            config BSP_USING_SPI2
                bool "Enable SPI2 BUS"
                default n
                
            config BSP_SPI2_TX_USING_DMA
                bool "Enable SPI2 TX DMA"
                depends on BSP_USING_SPI2
                default n
        
            config BSP_SPI2_RX_USING_DMA
                bool "Enable SPI2 RX DMA"
                depends on BSP_USING_SPI2
                select BSP_SPI2_TX_USING_DMA
                default n

進入env 使能 SPI, 另外 CubeMX 使能相應SPI外設 ,具體操作可參考上節

接下來即可使用SPI設備驅動了,當然對應的拓展有 SPI-flash  及其引申出的 塊設備文件系統,下次在單獨描述。

#if 1
/*
 * 程序清單:這是一個 SPI 設備使用例程
 * 例程導出了 spi_w25q_sample 命令到控制終端
 * 命令調用格式:spi_w25q_sample spi10
 * 命令解釋:命令第二個參數是要使用的SPI設備名稱,為空則使用默認的SPI設備
 * 程序功能:通過SPI設備讀取 w25q 的 ID 數據
*/
#include "drv_spi.h"

int w25q_spi_device_init()
{
    __HAL_RCC_GPIOB_CLK_ENABLE();
    return rt_hw_spi_device_attach("spi1", "spi10", GPIOB, GPIO_PIN_14);
}
INIT_DEVICE_EXPORT(w25q_spi_device_init);


#define W25Q_SPI_DEVICE_NAME     "spi10"

static void spi_w25q_sample(int argc, char *argv[])
{
    struct rt_spi_device *spi_dev_w25q;
    char name[RT_NAME_MAX];
    rt_uint8_t w25x_read_id = 0x90;
    rt_uint8_t id[5] = {0};

    if (argc == 2)
    {
        rt_strncpy(name, argv[1], RT_NAME_MAX);
    }
    else
    {
        rt_strncpy(name, W25Q_SPI_DEVICE_NAME, RT_NAME_MAX);
    }
    
//    rt_hw_spi_device_attach("spi1", "spi10", GPIOB, GPIO_PIN_14);
    
    /* 查找 spi 設備獲取設備句柄 */
    spi_dev_w25q = (struct rt_spi_device *)rt_device_find(name);
    if (!spi_dev_w25q)
    {
        rt_kprintf("spi sample run failed! can't find %s device!\n", name);
    }
    else
    {
        /* config spi */
        {
            struct rt_spi_configuration cfg;
            cfg.data_width = 8;
            cfg.mode = RT_SPI_MODE_0 | RT_SPI_MSB; /* SPI Compatible: Mode 0 and Mode 3 */
            cfg.max_hz = 50 * 1000 * 1000; /* 50M */
            rt_spi_configure(spi_dev_w25q, &cfg);
        }
        
        /* 方式1:使用 rt_spi_send_then_recv()發送命令讀取ID */
        rt_spi_send_then_recv(spi_dev_w25q, &w25x_read_id, 1, id, 5);
        rt_kprintf("use rt_spi_send_then_recv() read w25q ID is:%x%x\n", id[3], id[4]);

        /* 方式2:使用 rt_spi_transfer_message()發送命令讀取ID */
        struct rt_spi_message msg1, msg2;

        msg1.send_buf   = &w25x_read_id;
        msg1.recv_buf   = RT_NULL;
        msg1.length     = 1;
        msg1.cs_take    = 1;
        msg1.cs_release = 0;
        msg1.next       = &msg2;

        msg2.send_buf   = RT_NULL;
        msg2.recv_buf   = id;
        msg2.length     = 5;
        msg2.cs_take    = 0;
        msg2.cs_release = 1;
        msg2.next       = RT_NULL;

        rt_spi_transfer_message(spi_dev_w25q, &msg1);
        rt_kprintf("use rt_spi_transfer_message() read w25q ID is:%x%x\n", id[3], id[4]);

    }
}
/* 導出到 msh 命令列表中 */
MSH_CMD_EXPORT(spi_w25q_sample, spi en25q sample);
#endif

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM