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