系列文章目錄
持續更新中…
文章目錄
- 系列文章目錄
- 前言
- 一、SPI概述
- 1.主要功能
- 2.SPI控制器架構
- 3.SPI通信模式
- 4.SPI數據幀與事務
- 5.DMA與傳輸性能
- 6.中斷與驅動事件
- 二、SPI類型定義及相關API
- 三、SPI示例程序
- 總結
前言
在嵌入式開發中,SPI(串行外設接口)是一種常用的高速同步串行通信接口。ESP32 作為一款高性能 MCU,集成了多組 SPI 控制器,支持多主從設備連接、雙全工/半雙工通信以及 DMA 數據傳輸等高級功能。本篇文章將深入解析 ESP32(以 ESP32-S3 為例)的 SPI 外設,從硬件架構到軟件使用,幫助開發者掌握其高級應用技巧。
一、SPI概述
SPI 協議是由摩托羅拉公司提出的通訊協議 (Serial Peripheral Interface),即串行外圍設備接口,是一種高速全雙工的通信總線。它被廣泛地使用在 ADC設備、LCD 等設備與 MCU 間,要求通訊速率較高的場合。
1.主要功能
ESP32 系列芯片集成了 4 個 SPI 控制器(SPI0/1/2/3)。其中 SPI0 和 SPI1 主要用于內部連接 Flash 和 PSRAM,不提供給用戶;SPI2 和 SPI3 則作為通用 SPI(GP-SPI)對外開放,可用于連接各類 SPI 從設備。
SPI 控制器支持主機/從機模式,默認作為主機使用,可獨立配置時鐘頻率(ESP32-S3 默認時鐘源為 80 MHz APB,可分頻得到常用頻率,部分模式下最高支持 80MHz,特定八線模式下可達 120MHz)、數據傳輸模式(SPI Mode0/1/2/3,對應時鐘極性CPOL和相位CPHA組合)、傳輸字節序(MSB或LSB優先)等。SPI 支持 全雙工 同步收發(默認 MOSI/MISO 雙線),也支持 半雙工 模式(如三線 SPI,共用單數據線收發)。
GP-SPI2 和 GP-SPI3 支持的數據模式
ESP32 的 SPI 控制器具備多線并行傳輸能力,可工作在 Dual SPI、Quad SPI、Octal SPI等模式以提升吞吐量(常用于高速閃存、顯示屏等外設)。在數據傳輸方面,SPI 硬件提供發送/接收 FIFO 緩沖,并且可以結合 DMA(直接存儲器訪問)實現大數據塊的高速搬運,減少CPU負擔。通過上述功能組合,ESP32 的 SPI 接口既適用于傳感器等中低速外設,也能勝任顯示屏、存儲等大數據量高速場景。
2.SPI控制器架構
ESP32-S3 內部的 4 個 SPI 控制器架構如圖所示
SPI0 和 SPI1 控制器共享一套外部總線信號(稱作 SPI 主存儲總線,包括 D/Q 數據線、CS0CS2 片選、CLK 時鐘、WP/HD 輔助線等),通過硬件仲裁器實現對外部 Flash/PSRAM 的訪問(SPI0 常用于緩存操作,SPI1 用于向 Flash 寫入等)。由于這兩路總線承載著程序存儲器,IDF 驅動并不支持用戶直接操作 SPI0/1 控制器。SPI2 和 SPI3 控制器則擁有各自獨立的通用信號總線(通常也稱 HSPI 和 VSPI),可自由映射到支持輸出的任意 GPIO。
在 ESP32-S3 中,SPI2 控制器提供6 條片選線(CS0-CS5),SPI3 提供3 條片選線(CS0-CS2),意味著單個 SPI 主機最多可掛載 6 個或 3 個從設備。硬件會根據片選自動控制總線占用,實現多設備的時分復用。此外,SPI1~SPI3 控制器共用兩個 DMA 通道資源:當啟用 DMA 傳輸時,硬件可在這兩個 DMA 信道上調度數據搬運,從而實現高吞吐的連續讀寫。
每個 SPI 控制器內部包含發送/接收 FIFO(深度為 64 字節)、時鐘分頻器、模式控制邏輯等模塊。在主模式下,ESP32-S3 的 SPI 控制器通過配置寄存器即可自動完成從 拉低 CS、發送指令/地址、讀寫數據,到釋放 CS 的整個事務序列,期間支持硬件插入空周期以及精確的時序控制。ESP32 的 SPI 硬件架構為多主多從、高速大數據傳輸提供了靈活且強大的支撐。
3.SPI通信模式
SPI 通信由主設備產生時鐘并發起傳輸。ESP32 的 SPI 主控模式下支持 4 種時序模式(Mode0/1/2/3),分別對應時鐘空閑電平和數據采樣時機的不同組合:(0,0)、(0,1)、(1,0)、(1,1)。這些模式可以通過設備配置的 mode 參數來設置,以匹配不同 SPI 從設備的時序要求。
GP-SPI 功能塊圖:
數據線方面,默認 SPI 使用 MISO 和 MOSI 兩根數據線實現全雙工通信——在一個時鐘周期內,主機從 MOSI 發送1比特的同時,也從 MISO 接收1比特。如果外設不需要同時發送數據,或硬件只有單數據引腳,可以將 SPI 配置為 半雙工 模式,此時主機可以使用同一引腳(連接在 SPI 的 MOSI 引腳上)分時發送和接收數據,即經典“三線 SPI”接口(CLK、DATA、CS)。
開啟半雙工模式的方法是在設備配置的標志位中設置 SPI_DEVICE_HALFDUPLEX(IDF 會自動管理數據線方向)。除了標準的單比特串行,ESP32 的 GP-SPI 控制器還支持 多路并行模式:例如 Dual SPI(雙線)和 Quad SPI(四線)模式。在這些模式下,主機將同時使用 2 個或 4 個數據引腳進行并行傳輸,大幅提高有效帶寬。這通常用于與支持多I/O模式的存儲芯片或顯示屏通信。要使用并行模式,需要硬件上將 SPI 的 WP/HD 等引腳連接到從設備,并在驅動中啟用相應的總線標志(如 SPICOMMON_BUSFLAG_DUAL/QUAD 等)以及在事務中設置 SPI_TRANS_MODE_DIO/QIO 標志。
并行模式通常意味著通信只能半雙工進行(因為數據線被復用為輸出),因此驅動要求在多線模式下設備標志需包含 SPI_DEVICE_HALFDUPLEX。在大多數應用中,標準 4 線 SPI 已能滿足需求,而當追求極致速度時,可考慮使用并行模式并合理調整時序以確保可靠性。
4.SPI數據幀與事務
**SPI 的主從通信以事務為單位完成。**一次完整的 SPI 主機事務通常包括以下階段:主機拉低 CS(片選)以選中目標從設備,然后依次發送命令碼(可選,016位)、地址(可選,064位)、插入若干空等待周期(Dummy,滿足從設備時序要求),接著進入數據傳輸階段,包括發送數據(寫階段)和/或接收數據(讀階段),最后主機釋放 CS 結束該事務。
這些階段是否存在及長度,取決于設備配置和每次事務配置。例如,對某些存儲器或顯示屏操作,可能需要在正式數據之前發出命令或地址信息;而對一般傳感器可能只需簡單的讀寫數據而無額外命令。ESP-IDF 提供的 spi_device_interface_config_t 結構體中有專門的字段用于配置默認的命令位數和地址位數,以及每次事務可以按需調整的數據長度。在執行事務時,驅動會根據這些配置自動完成前序命令/地址的發送,然后進行數據階段。
主機模式下數據流控制:
SPI 的讀寫可以同時發生:在全雙工模式下,當主機發送每一比特時,從設備的輸出比特會同步被采集,這樣讀階段和寫階段實際上重疊進行,事務總時長取決于兩者中較長的一個。如果不希望同時讀寫(例如從設備要求先發后收),可以使用半雙工模式,在事務配置中分別指定發送數據長度和接收數據長度,驅動將按先發送后接收的順序完成。對于不需要的讀或寫相位,可以將對應緩沖區指針設為 NULL,SPI 控制器將自動跳過該階段。
從機模式下數據流控制:
總的來說,ESP32 SPI 通過硬件支持靈活的事務分段和自動片選控制,使復雜協議的實現更加簡潔高效。
5.DMA與傳輸性能
當進行小數據量傳輸時(比如幾字節),SPI 主機驅動可以直接通過 CPU 向硬件 FIFO 寄存器寫入/讀取數據完成通信;這種方式開銷低、速度快。但是對于較大數據塊(幾十上百字節乃至數KB),頻繁的中斷和字節搬運會給 CPU 帶來較大負擔。
為此,ESP32-S3 的 SPI 控制器支持 DMA(直接內存訪問)傳輸:通過給 spi_bus_initialize 提供 DMA通道參數,驅動將在發送/接收超過 FIFO 深度的數據時,自動啟用 DMA 控制器將數據塊搬運到 SPI FIFO。這使得單次事務可以發送非常長的數據(IDF 默認在 DMA 模式下單次傳輸可達約4092字節,理論上可調整受內存限制),同時將 CPU 從逐字節搬運中解放出來。ESP32-S3 的 SPI2 和 SPI3 控制器各自配備 DMA請求接口(共享2個 DMA信道),通過配置可分別占用一個 DMA通道實現并行數據傳輸。需要注意:如果選擇使用 DMA,則發送/接收緩沖區必須放在可被DMA訪問的內存區域(例如內部 SRAM,并避免使用cache映射的PSRAM),并且最好滿足 4 字節對齊,以發揮DMA最大效率。
主機模式下 DAM 控制的分段配置傳輸:
IDF 中提供了 heap_caps_malloc等API用于分配DMA合規的內存,也可以使用 spi_bus_dma_memory_alloc 輔助分配函數。另外,為確保高速下數據穩定,驅動允許用戶設置從設備的信號采樣延遲或調整采樣點位置。通常在低于8MHz時無需調整,而更高速率下根據線長、電平翻轉等情況,適當的延時設置可以改善可靠性。引腳選擇方面,SPI 若使用GPIO矩陣映射引腳,由于引入了約 2ns 的延遲,穩定工作的最高全雙工頻率約為 26MHz,半雙工約 40MHz;若全部使用IO_MUX指定的原生引腳,則可支持全雙工 40MHz、半雙工 80MHz的速率,甚至更高(實際最大受限于時鐘源及從設備性能)。
因此在設計高速 SPI 總線時,盡量選用芯片的硬件接口管腳并降低連線電容,以獲取最佳信號質量和速度。總體而言,通過 DMA、高速引腳和合理的時序配置,ESP32的 SPI 主接口可在高吞吐與低CPU占用之間取得良好平衡,滿足苛刻的數據傳輸需求。
6.中斷與驅動事件
ESP-IDF 的 SPI 主機驅動對底層硬件中斷和狀態變化進行了封裝,用戶通常不需要直接處理 SPI 中斷。驅動在后臺利用中斷檢測事務完成、隊列調度等事件,并提供了回調機制供用戶在特定時機(傳輸前后)執行操作。
在主模式下,如果采用異步隊列接口(spi_device_queue_trans),驅動會在每個事務完成時通過中斷將結果放入內部隊列,用戶可以通過 spi_device_get_trans_result 等API等待或輪詢完成事件。在設備配置結構中還可以指定 pre_cb 和 post_cb 回調函數,分別會在每次事務開始前和結束后被ISR調用,可用于控制引腳、電源管理等(注意需放置于IRAM以滿足中斷上下文要求)。
當多個任務共享同一 SPI 設備時,由于 SPI 驅動線程非安全(訪問同一設備需串行化),一種方式是使用 spi_device_acquire_bus/release_bus 手動鎖定總線;更簡單的做法是保證每個 SPI設備僅由一個任務訪問,或在應用層對共享訪問加互斥鎖。合理利用驅動提供的中斷/隊列機制,可以實現非阻塞的 SPI 通信和多設備的高效調度,充分發揮 SPI 總線的并發能力和數據吞吐。
二、SPI類型定義及相關API
需包含的公共頭文件:#include “driver/spi_master.h”
SPI類型定義
// ==========================================================
// SPI 主機編號類型
// ==========================================================
typedef int spi_host_device_t; // SPI 主機控制器代號
#define SPI1_HOST (0) // SPI1(一般用于內部Flash,不開放)
#define SPI2_HOST (1) // SPI2(用戶可用,一般默認HSPI)
#define SPI3_HOST (2) // SPI3(用戶可用,一般默認VSPI)
#define SPI_HOST_MAX (3) // SPI主機別名(兼容舊稱呼)
#define HSPI_HOST SPI2_HOST
#define VSPI_HOST SPI3_HOST // ==========================================================
// SPI DMA通道選擇
// ==========================================================
typedef enum { SPI_DMA_DISABLED = 0, // 不啟用 DMA,將受限于 FIFO 長度 SPI_DMA_CH1 = 1, // 使用 DMA通道1 SPI_DMA_CH2 = 2, // 使用 DMA通道2 SPI_DMA_CH_AUTO = 3 // 自動分配可用 DMA通道
} spi_dma_chan_t; // ==========================================================
// SPI 總線初始化配置結構
// ==========================================================
typedef struct { int mosi_io_num; // MOSI 引腳編號(主出從入數據線),-1表示不使用 int miso_io_num; // MISO 引腳編號(主入從出數據線),-1表示不使用 int sclk_io_num; // SCLK 引腳編號(時鐘線),-1表示不使用 int quadwp_io_num; // WP 引腳編號(寫保護,用于Quad模式),-1表示不使用 int quadhd_io_num; // HD 引腳編號(保持,用于Quad模式),-1表示不使用 int data4_io_num; // 數據線4(Octal模式),-1表示不使用 int data5_io_num; // 數據線5(Octal模式),-1表示不使用 int data6_io_num; // 數據線6(Octal模式),-1表示不使用 int data7_io_num; // 數據線7(Octal模式),-1表示不使用 bool data_io_default_level; // 空閑時數據線默認電平(輸出使能時),一般為false int max_transfer_sz; // 單次最大傳輸字節數(DMA模式下默認4092字節,非DMA模式下默認為 SOC_SPI_MAXIMUM_BUFFER_SIZE) uint32_t flags; // 總線能力標志位(SPICOMMON_BUSFLAG_* 的組合,用于校驗硬件能力) esp_intr_cpu_affinity_t isr_cpu_id; // 中斷分配到的CPU核心(默認不指定) int intr_flags; // 中斷分配標志(ESP_INTR_FLAG_LEVELx/IRAM等,一般使用默認0或ESP_INTR_FLAG_IRAM)
} spi_bus_config_t; // ==========================================================
// SPI 從設備接口配置結構(設備初始化時提供)
// ==========================================================
typedef struct { uint8_t command_bits; // 命令階段默認位寬(0~16位) uint8_t address_bits; // 地址階段默認位寬(0~64位) uint8_t dummy_bits; // 地址階段后插入的空等待時鐘周期數 uint8_t mode; // SPI 模式(0~3,對應 (CPOL, CPHA)) // uint8_t 非預留字段 (占位,以4字節對齊) uint16_t duty_cycle_pos; // 時鐘正脈沖占空比(1~256,對應占空比百分比=該值/256,128=50%) uint16_t cs_ena_pretrans;// 傳輸開始前 CS 提前拉低的時鐘周期數(0~16),半雙工模式下有效 uint8_t cs_ena_posttrans;// 傳輸結束后 CS 保持低電平的時鐘周期數(0~16) int clock_speed_hz; // 時鐘頻率(Hz) int input_delay_ns; // 輸入信號延遲(ns)補償(從屬設備數據準備時間,0表示不延遲) // 新版clock_source和sample_point省略,使用默認APB時鐘源 int spics_io_num; // 該設備使用的CS引腳(GPIO編號),-1表示不由驅動控制 uint32_t flags; // 設備標志位(SPI_DEVICE_*,如 LSBFIRST/3WIRE/HALFDUPLEX 等) int queue_size; // 事務隊列長度(驅動可同時掛起的未完成事務數) transaction_cb_t pre_cb; // 每次傳輸開始前的回調(中斷內調用,需放IRAM) transaction_cb_t post_cb;// 每次傳輸結束后的回調(中斷內調用,需放IRAM)
} spi_device_interface_config_t; // SPI 設備標志位宏(部分常用列舉)
#define SPI_DEVICE_TXBIT_LSBFIRST (1<<0) // 發送數據使用LSB優先(默認MSB先)
#define SPI_DEVICE_RXBIT_LSBFIRST (1<<1) // 接收數據使用LSB優先
#define SPI_DEVICE_BIT_LSBFIRST (SPI_DEVICE_TXBIT_LSBFIRST | SPI_DEVICE_RXBIT_LSBFIRST) // 發送接收均LSB優先
#define SPI_DEVICE_3WIRE (1<<2) // 啟用3線模式(共用MOSI引腳收發,等效半雙工)
#define SPI_DEVICE_POSITIVE_CS (1<<3) // CS信號極性取反(默認低有效,設置此標志后為高有效)
#define SPI_DEVICE_HALFDUPLEX (1<<4) // 半雙工模式(發送完再接收,不同時進行)
// ...(其他標志如 SPI_DEVICE_NO_DUMMY/SPI_DEVICE_CLK_AS_CS 等,可根據需要使用) // ==========================================================
// SPI 事務描述結構(每次傳輸參數)
// ==========================================================
typedef struct { uint32_t flags; // 事務標志(SPI_TRANS_*,如 VARIABLE_ADDR/CMD, CS_KEEP_ACTIVE 等) uint16_t cmd; // 本次事務使用的命令值(實際發送的位寬由 device 的 command_bits 定義) uint64_t addr; // 本次事務使用的地址值(實際發送位寬由 address_bits 定義) size_t length; // 本次發送數據總長度(bit為單位) size_t rxlength; // 本次接收數據長度(bit為單位,不大于length,全雙工為0則默認為length) void *user; // 用戶自定義指針(可用來標識事務來源等) const void *tx_buffer; // 發送數據緩沖區指針(若無發送則可為 NULL) uint8_t tx_data[4]; // 短數據直接存放在此(設置 SPI_TRANS_USE_TXDATA 時啟用) void *rx_buffer; // 接收數據緩沖區指針(若不需要接收可為 NULL) uint8_t rx_data[4]; // 短數據直接接收至此(設置 SPI_TRANS_USE_RXDATA 時啟用)
} spi_transaction_t; // SPI 事務標志位(常用)
#define SPI_TRANS_VARIABLE_CMD (1<<0) // 本次事務使用非常規長度的命令階段(spi_transaction_ext_t 擴展)
#define SPI_TRANS_VARIABLE_ADDR (1<<1) // 使用非常規長度的地址階段
#define SPI_TRANS_USE_TXDATA (1<<2) // 使用 tx_data 中的數據而非緩沖區指針
#define SPI_TRANS_USE_RXDATA (1<<3) // 接收數據直接存入 rx_data
#define SPI_TRANS_CS_KEEP_ACTIVE (1<<4) // 事務結束后保持 CS 拉低(需配合后續事務或手動控制) // SPI 設備句柄類型
typedef struct spi_device_t *spi_device_handle_t;
SPI相關API
// ======================= SPI 總線控制 =======================
/*** @brief 初始化 SPI 總線 ** @param host_id SPI 主機端口號 (SPI2_HOST / SPI3_HOST) * @param bus_config 指向總線配置結構體的指針 * @param dma_chan DMA通道選擇:* - SPI_DMA_DISABLED 不使用DMA(限制傳輸長度) * - SPI_DMA_CHx 使用指定 DMA通道 (ESP32-S3 通常用 1 或 2) * - SPI_DMA_CH_AUTO 由驅動自動分配可用 DMA通道 ** @note SPI0/SPI1 為內部總線,驅動不支持初始化 (調用此函數會返回錯誤)。 * 調用成功后,即完成GPIO引腳矩陣配置、FIFO和中斷初始化等。 * 如果指定了 DMA 通道,需確保后續使用的傳輸緩沖區在 DMA 可訪問內存中。 * @return * - ESP_OK: 初始化成功 * - ESP_ERR_INVALID_ARG: 參數非法 * - ESP_ERR_NOT_FOUND: 無可用DMA通道(當請求AUTO時) * - ESP_ERR_INVALID_STATE: 指定主機已經初始化過 */
esp_err_t spi_bus_initialize(spi_host_device_t host_id, const spi_bus_config_t *bus_config, spi_dma_chan_t dma_chan); /*** @brief 釋放 SPI 總線 ** @param host_id SPI 主機端口號 * @note 調用此函數前,需確保該總線上的所有設備已被移除 (spi_bus_remove_device)。 * @return * - ESP_OK: 釋放成功 * - ESP_ERR_INVALID_ARG: 參數非法 * - ESP_ERR_INVALID_STATE: 總線未初始化 或 上面仍掛有未移除的設備 */
esp_err_t spi_bus_free(spi_host_device_t host_id); // ======================= SPI 設備控制 =======================
/*** @brief 向 SPI 總線掛載一個從屬設備 ** @param host_id SPI 主機端口號 (SPI2_HOST / SPI3_HOST) * @param dev_config 指向設備接口配置結構體的指針 * @param handle 輸出:返回的設備句柄地址 ** @note 此函數會根據 dev_config 分配并初始化一個 SPI 從設備,* 包括為該設備分配一個 CS (片選) 引腳并通過 GPIO Matrix 連接。 * ESP32-S3 的 SPI2 支持最多 6 個 CS,引腳編號通常為 CS0~CS5;SPI3 支持 3 個。 * 如果超出主機可用的 CS 插槽數量,將返回 ESP_ERR_NOT_FOUND 錯誤。 * 默認支持最高 40MHz (IO_MUX引腳) / 26MHz (GPIO矩陣) 的速度,全雙工模式下矩陣引腳建議不超過 26MHz:contentReference[oaicite:15]{index=15}。 * @return * - ESP_OK: 設備添加成功 * - ESP_ERR_INVALID_ARG: 參數非法(比如配置沖突) * - ESP_ERR_NOT_FOUND: 主機沒有空余 CS 插槽可用 * - ESP_ERR_NO_MEM: 內存分配失敗 */
esp_err_t spi_bus_add_device(spi_host_device_t host_id, const spi_device_interface_config_t *dev_config, spi_device_handle_t *handle); /*** @brief 從 SPI 總線上移除一個從屬設備 ** @param handle 要移除的設備句柄 * @return * - ESP_OK: 移除成功 * - ESP_ERR_INVALID_ARG: 參數非法 * - ESP_ERR_INVALID_STATE: 設備已被移除或未曾添加 ** @note 調用后該設備占用的CS引腳和資源將釋放,可用于添加新設備 */
esp_err_t spi_bus_remove_device(spi_device_handle_t handle); // ======================= SPI 數據傳輸 =======================
/*** @brief 阻塞方式發送一個 SPI 事務 ** @param handle 設備句柄(spi_bus_add_device取得) * @param trans_desc 指向事務描述結構體的指針(需提前填充好發送/接收緩沖等) * @return * - ESP_OK: 傳輸成功,數據已發送/接收完成 * - ESP_ERR_INVALID_ARG: 參數非法(如 trans_desc 內容不合法) ** @note 此函數相當于依次調用 spi_device_queue_trans() 和 spi_device_get_trans_result(), * 內部會等待傳輸完成再返回。因此不應在已有掛起事務未完成時再次調用本函數。 * 默認情況下,同一設備上的串行調用是線程安全的,但若多個任務并發訪問同一設備句柄,需要自行確保互斥。 */
esp_err_t spi_device_transmit(spi_device_handle_t handle, spi_transaction_t *trans_desc); /*** @brief 輪詢方式發送 SPI 事務 ** @param handle 設備句柄 * @param trans_desc 事務描述結構指針 * @return ESP_OK 表示傳輸完成且成功 ** @note 本函數與 spi_device_transmit 類似,也會等待傳輸完成,但采用 “忙輪詢” 方式驅動硬件而不使用中斷。 * 這種方式適用于短小事務且對實時性要求高的場景,可避免中斷調度的延遲。 * 調用前后無需 acquire_bus,但不同任務并發仍需注意互斥。 */
esp_err_t spi_device_polling_transmit(spi_device_handle_t handle, spi_transaction_t *trans_desc); /*** @brief 異步排隊一個 SPI 事務(中斷驅動) ** @param handle 設備句柄 * @param trans_desc 事務描述結構指針 * @param ticks_to_wait 等待可用隊列空間的超時時間(RTOS ticks,portMAX_DELAY 表示永不超時) * @return * - ESP_OK: 事務已成功加入隊列 * - ESP_ERR_TIMEOUT: 在指定時間內隊列無空閑,事務未加入 * - ESP_ERR_NO_MEM: 內部申請DMA臨時緩沖失敗(極少見) * - ESP_ERR_INVALID_ARG: 參數非法,或指定了不支持的標志組合等 * - ESP_ERR_INVALID_STATE: 前一個事務未完成(正常不會發生,因為隊列有容量控制) ** @note 將事務加入驅動隊列后即立即返回,SPI硬件會通過中斷在后臺執行傳輸。 * 可以通過 spi_device_get_trans_result 獲取完成的事務結果。 * 同一設備上的事務將按調用順序依次執行;多個設備間則由驅動自動仲裁總線。 */
esp_err_t spi_device_queue_trans(spi_device_handle_t handle, spi_transaction_t *trans_desc, TickType_t ticks_to_wait); /*** @brief 獲取一個已完成的 SPI 異步事務結果 ** @param handle 設備句柄 * @param trans_desc 輸出:指向完成的事務描述指針的存放地址 * @param ticks_to_wait 最長等待時間(RTOS ticks) * @return * - ESP_OK: 成功獲取到已完成的事務 * - ESP_ERR_INVALID_ARG: 參數非法 * - ESP_ERR_TIMEOUT: 在指定時間內沒有事務完成 ** @note 本函數用于與 spi_device_queue_trans 配合,實現異步傳輸的結果獲取。 * 如果在隊列中尚有未完成的事務,本函數會等待直至有事務完成或超時。 * 獲得結果后,可檢查 trans_desc->rx_buffer 中的數據或其他標志,并且可以重復利用或釋放該事務結構。 */
esp_err_t spi_device_get_trans_result(spi_device_handle_t handle, spi_transaction_t **trans_desc, TickType_t ticks_to_wait); /*** @brief 手動占用 SPI 總線以獨占訪問 ** @param handle 設備句柄 * @param ticks_to_wait 等待總線可用的時間 * @return ESP_OK 表示成功占用總線 ** @note 調用此函數后,其他設備的事務將被暫掛,直到調用 spi_device_release_bus 釋放總線。 * 適用于需連續執行一組事務且中間不插入其他設備通信的場景。 * 使用完畢后務必調用 release_bus 釋放,否則會阻塞低優先級任務的SPI通信。 */
esp_err_t spi_device_acquire_bus(spi_device_handle_t handle, TickType_t ticks_to_wait); /*** @brief 釋放通過 spi_device_acquire_bus 占用的 SPI 總線 ** @param handle 設備句柄 */
esp_err_t spi_device_release_bus(spi_device_handle_t handle);
三、SPI示例程序
在 ESP32S3 上通過SPI驅動 LCD 屏幕顯示圖片
main.c
#include <stdio.h>
#include "lcd.h"
#include "yingwu.h"void app_main(void)
{bsp_i2c_init();pca9557_init();bsp_lcd_init(); // 液晶屏初始化lcd_draw_pictrue(0, 0, 320, 240, gImage_yingwu); // 顯示3只鸚鵡圖片while(1){}
}
lcd.c
#include "lcd.h"static const char *TAG = "BSP";esp_err_t bsp_i2c_init(void)
{i2c_config_t i2c_conf = {.mode = I2C_MODE_MASTER,.sda_io_num = BSP_I2C_SDA,.sda_pullup_en = GPIO_PULLUP_ENABLE,.scl_io_num = BSP_I2C_SCL,.scl_pullup_en = GPIO_PULLUP_ENABLE,.master.clk_speed = BSP_I2C_FREQ_HZ};i2c_param_config(BSP_I2C_NUM, &i2c_conf);return i2c_driver_install(BSP_I2C_NUM, i2c_conf.mode, 0, 0, 0);
}// 讀取PCA9557寄存器的值
esp_err_t pca9557_register_read(uint8_t reg_addr, uint8_t *data, size_t len)
{return i2c_master_write_read_device(BSP_I2C_NUM, PCA9557_SENSOR_ADDR, ®_addr, 1, data, len, 1000 / portTICK_PERIOD_MS);
}// 給PCA9557的寄存器寫值
esp_err_t pca9557_register_write_byte(uint8_t reg_addr, uint8_t data)
{uint8_t write_buf[2] = {reg_addr, data};return i2c_master_write_to_device(BSP_I2C_NUM, PCA9557_SENSOR_ADDR, write_buf, sizeof(write_buf), 1000 / portTICK_PERIOD_MS);
}// 初始化PCA9557 IO擴展芯片
void pca9557_init(void)
{// 寫入控制引腳默認值 DVP_PWDN=1 PA_EN = 0 LCD_CS = 1pca9557_register_write_byte(PCA9557_OUTPUT_PORT, 0x05);// 把PCA9557芯片的IO1 IO1 IO2設置為輸出 其它引腳保持默認的輸入pca9557_register_write_byte(PCA9557_CONFIGURATION_PORT, 0xf8);
}// 設置PCA9557芯片的某個IO引腳輸出高低電平
esp_err_t pca9557_set_output_state(uint8_t gpio_bit, uint8_t level)
{uint8_t data;esp_err_t res = ESP_FAIL;pca9557_register_read(PCA9557_OUTPUT_PORT, &data, 1);res = pca9557_register_write_byte(PCA9557_OUTPUT_PORT, SET_BITS(data, gpio_bit, level));return res;
}// 控制 PCA9557_LCD_CS 引腳輸出高低電平 參數0輸出低電平 參數1輸出高電平
void lcd_cs(uint8_t level)
{pca9557_set_output_state(LCD_CS_GPIO, level);
}// 背光PWM初始化
esp_err_t bsp_display_brightness_init(void)
{// Setup LEDC peripheral for PWM backlight controlconst ledc_channel_config_t LCD_backlight_channel = {.gpio_num = BSP_LCD_BACKLIGHT,.speed_mode = LEDC_LOW_SPEED_MODE,.channel = LCD_LEDC_CH,.intr_type = LEDC_INTR_DISABLE,.timer_sel = 0,.duty = 0,.hpoint = 0,.flags.output_invert = true};const ledc_timer_config_t LCD_backlight_timer = {.speed_mode = LEDC_LOW_SPEED_MODE,.duty_resolution = LEDC_TIMER_10_BIT,.timer_num = 0,.freq_hz = 5000,.clk_cfg = LEDC_AUTO_CLK};ESP_ERROR_CHECK(ledc_timer_config(&LCD_backlight_timer));ESP_ERROR_CHECK(ledc_channel_config(&LCD_backlight_channel));return ESP_OK;
}// 定義液晶屏句柄
static esp_lcd_panel_handle_t panel_handle = NULL;
esp_lcd_panel_io_handle_t io_handle = NULL;// 設置液晶屏顏色
void lcd_set_color(uint16_t color)
{// 分配內存 這里分配了液晶屏一行數據需要的大小uint16_t *buffer = (uint16_t *)heap_caps_malloc(BSP_LCD_H_RES * sizeof(uint16_t), MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM);if (NULL == buffer){ESP_LOGE(TAG, "Memory for bitmap is not enough");}else{for (size_t i = 0; i < BSP_LCD_H_RES; i++) // 給緩存中放入顏色數據{buffer[i] = color;}for (int y = 0; y < 240; y++) // 顯示整屏顏色{esp_lcd_panel_draw_bitmap(panel_handle, 0, y, 320, y + 1, buffer);}free(buffer); // 釋放內存}
}// 背光亮度設置
esp_err_t bsp_display_brightness_set(int brightness_percent)
{if (brightness_percent > 100){brightness_percent = 100;}else if (brightness_percent < 0){brightness_percent = 0;}ESP_LOGI(TAG, "Setting LCD backlight: %d%%", brightness_percent);// LEDC resolution set to 10bits, thus: 100% = 1023uint32_t duty_cycle = (1023 * brightness_percent) / 100;ESP_ERROR_CHECK(ledc_set_duty(LEDC_LOW_SPEED_MODE, LCD_LEDC_CH, duty_cycle));ESP_ERROR_CHECK(ledc_update_duty(LEDC_LOW_SPEED_MODE, LCD_LEDC_CH));return ESP_OK;
}// 關閉背光
esp_err_t bsp_display_backlight_off(void)
{return bsp_display_brightness_set(0);
}// 打開背光 最亮
esp_err_t bsp_display_backlight_on(void)
{return bsp_display_brightness_set(100);
}// 液晶屏初始化
esp_err_t bsp_display_new(void)
{esp_err_t ret = ESP_OK;// 背光初始化ESP_RETURN_ON_ERROR(bsp_display_brightness_init(), TAG, "Brightness init failed");// 初始化SPI總線ESP_LOGD(TAG, "Initialize SPI bus");const spi_bus_config_t buscfg = {.sclk_io_num = BSP_LCD_SPI_CLK,.mosi_io_num = BSP_LCD_SPI_MOSI,.miso_io_num = GPIO_NUM_NC,.quadwp_io_num = GPIO_NUM_NC,.quadhd_io_num = GPIO_NUM_NC,.max_transfer_sz = BSP_LCD_H_RES * BSP_LCD_V_RES * sizeof(uint16_t),};ESP_RETURN_ON_ERROR(spi_bus_initialize(BSP_LCD_SPI_NUM, &buscfg, SPI_DMA_CH_AUTO), TAG, "SPI init failed");// 液晶屏控制IO初始化ESP_LOGD(TAG, "Install panel IO");const esp_lcd_panel_io_spi_config_t io_config = {.dc_gpio_num = BSP_LCD_DC,.cs_gpio_num = BSP_LCD_SPI_CS,.pclk_hz = BSP_LCD_PIXEL_CLOCK_HZ,.lcd_cmd_bits = LCD_CMD_BITS,.lcd_param_bits = LCD_PARAM_BITS,.spi_mode = 2,.trans_queue_depth = 10,};ESP_GOTO_ON_ERROR(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)BSP_LCD_SPI_NUM, &io_config, &io_handle), err, TAG, "New panel IO failed");// 初始化液晶屏驅動芯片ST7789ESP_LOGD(TAG, "Install LCD driver");const esp_lcd_panel_dev_config_t panel_config = {.reset_gpio_num = BSP_LCD_RST,.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,.bits_per_pixel = BSP_LCD_BITS_PER_PIXEL,};ESP_GOTO_ON_ERROR(esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle), err, TAG, "New panel failed");esp_lcd_panel_reset(panel_handle); // 液晶屏復位lcd_cs(0); // 拉低CS引腳esp_lcd_panel_init(panel_handle); // 初始化配置寄存器esp_lcd_panel_invert_color(panel_handle, true); // 顏色反轉esp_lcd_panel_swap_xy(panel_handle, true); // 顯示翻轉esp_lcd_panel_mirror(panel_handle, true, false); // 鏡像return ret;err:if (panel_handle){esp_lcd_panel_del(panel_handle);}if (io_handle){esp_lcd_panel_io_del(io_handle);}spi_bus_free(BSP_LCD_SPI_NUM);return ret;
}// LCD顯示初始化
esp_err_t bsp_lcd_init(void)
{esp_err_t ret = ESP_OK;ret = bsp_display_new(); // 液晶屏驅動初始化lcd_set_color(0x0000); // 設置整屏背景黑色ret = esp_lcd_panel_disp_on_off(panel_handle, true); // 打開液晶屏顯示ret = bsp_display_backlight_on(); // 打開背光顯示return ret;
}// 顯示圖片
void lcd_draw_pictrue(int x_start, int y_start, int x_end, int y_end, const unsigned char *gImage)
{// 分配內存 分配了需要的字節大小 且指定在外部SPIRAM中分配size_t pixels_byte_size = (x_end - x_start) * (y_end - y_start) * 2;uint16_t *pixels = (uint16_t *)heap_caps_malloc(pixels_byte_size, MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM);if (NULL == pixels){ESP_LOGE(TAG, "Memory for bitmap is not enough");return;}memcpy(pixels, gImage, pixels_byte_size); // 把圖片數據拷貝到內存esp_lcd_panel_draw_bitmap(panel_handle, x_start, y_start, x_end, y_end, (uint16_t *)pixels); // 顯示整張圖片數據heap_caps_free(pixels); // 釋放內存
}
lcd.h
#ifndef LCD_H
#define LCD_H#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_lcd_types.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_vendor.h"
#include "esp_lcd_panel_ops.h"
#include "driver/ledc.h"
#include "driver/spi_master.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_check.h"
#include "driver/i2c.h"
#include <string.h>#define SET_BITS(_m, _s, _v) ((_v) ? (_m) | ((_s)) : (_m) & ~((_s)))#define BSP_I2C_SDA (GPIO_NUM_1) // SDA引腳
#define BSP_I2C_SCL (GPIO_NUM_2) // SCL引腳
#define BSP_I2C_NUM (0) // I2C外設
#define BSP_I2C_FREQ_HZ 100000 // 100kHz#define PCA9557_INPUT_PORT 0x00
#define PCA9557_OUTPUT_PORT 0x01
#define PCA9557_POLARITY_INVERSION_PORT 0x02
#define PCA9557_CONFIGURATION_PORT 0x03
#define PCA9557_SENSOR_ADDR 0x19 /*!< Slave address of the MPU9250 sensor */
#define LCD_CS_GPIO BIT(0) // PCA9557_GPIO_NUM_1
#define PA_EN_GPIO BIT(1) // PCA9557_GPIO_NUM_2
#define DVP_PWDN_GPIO BIT(2) // PCA9557_GPIO_NUM_3#define BSP_LCD_PIXEL_CLOCK_HZ (80 * 1000 * 1000)
#define BSP_LCD_SPI_NUM (SPI3_HOST)
#define LCD_CMD_BITS (8)
#define LCD_PARAM_BITS (8)
#define BSP_LCD_BITS_PER_PIXEL (16)
#define LCD_LEDC_CH LEDC_CHANNEL_0#define BSP_LCD_H_RES (320)
#define BSP_LCD_V_RES (240)#define BSP_LCD_SPI_MOSI (GPIO_NUM_40)
#define BSP_LCD_SPI_CLK (GPIO_NUM_41)
#define BSP_LCD_SPI_CS (GPIO_NUM_NC)
#define BSP_LCD_DC (GPIO_NUM_39)
#define BSP_LCD_RST (GPIO_NUM_NC)
#define BSP_LCD_BACKLIGHT (GPIO_NUM_42)// 函數聲明
esp_err_t bsp_i2c_init(void);
esp_err_t pca9557_register_read(uint8_t reg_addr, uint8_t *data, size_t len);
esp_err_t pca9557_register_write_byte(uint8_t reg_addr, uint8_t data);
void pca9557_init(void);
esp_err_t bsp_lcd_init(void);
esp_err_t bsp_display_brightness_init(void);
esp_err_t bsp_display_new(void);
void lcd_draw_pictrue(int x_start, int y_start, int x_end, int y_end, const unsigned char *gImage);#endif // !LCD_H
總結
本文圍繞 ESP32 的 SPI 外設,從硬件資源、通信機制到軟件接口進行了全面的介紹。在實際應用中,開發者可根據需求選擇合適的傳輸方式:對于少量數據的即時通信,可直接使用阻塞發送;對于大量數據或需要并發處理的場景,可采用 DMA 加中斷隊列的異步方式以降低 CPU 占用。在后續的項目中,開發者可以依據本文提供的知識框架,快速上手 SPI 編程并針對性能瓶頸進行優化,從而充分發揮 ESP32 SPI 外設的強大能力。