Arduino-ESP32 SPI 多总线编程指南:从 SPIClass 双总线示例到硬件底层实现

发布时间:2026/9/13 13:41:55
Arduino-ESP32 SPI 多总线编程指南:从 SPIClass 双总线示例到硬件底层实现 Arduino-ESP32 SPI 多总线编程指南从 SPIClass 双总线示例到硬件底层实现【免费下载链接】arduino-esp32Arduino core for the ESP32 family of SoCs项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32本指南以 ESP32 Arduino Core 官方 SPI 文档docs/en/api/spi.rst为主线系统讲解 SPI 库的核心 API、总线编号与默认引脚、事务机制并以仓库内置的 SPI_Multiple_Buses 示例 为实战骨架说明如何在同一颗 ESP32 上同时驱动两条 SPI 总线。读完本文你将掌握 SPIClass 的初始化、引脚重映射、事务Transaction协议以及底层驱动调用链可直接在工程中落地多 SPI 从机场景。1. SPI 库概览与 Arduino 标准 API 对齐ESP32 Arduino Core 的 SPI 库为所有支持 GPSPI 外设SOC_GPSPI_SUPPORTED的 ESP32 系列芯片提供串行外设接口支持其库描述library.properties明确指出该库 Enables the communication with devices that use the Serial Peripheral Interface (SPI) Bus适用于除 Arduino Due 外的所有 Arduino 板卡在 ESP32 上的架构标签为esp32。按照官方文档 docs/en/api/spi.rst 的说明SPI 的 API 引用与 Arduino Core 保持一致——begin()、transfer()、beginTransaction()等接口的调用方式与标准 Arduino 相同因此 Arduino 生态中已有的 SPI 设备驱动代码几乎可以直接复用。唯一需要特殊处理的是 ESP32 的多总线特性详见第 3 节。库的头文件 libraries/SPI/src/SPI.h 定义了两个核心类SPISettings封装一次传输的三个关键参数——clock时钟频率、bitOrder位序SPI_MSBFIRST/SPI_LSBFIRST、dataModeSPI 模式 0~3。其默认构造函数给出1 MHz / MSBFIRST / SPI_MODE0的默认值这与示例中spiClk 1000000的选择一致。SPIClassSPI 总线实例负责引脚管理、时钟分频、事务与数据传输。库末尾还声明了全局实例extern SPIClass SPI;可用NO_GLOBAL_SPI宏关闭方便沿用 Arduino 风格的SPI.begin()写法。2. SPIClass 核心 API 与数据收发从 libraries/SPI/src/SPI.cpp 的类定义SPI.h可以整理出以下常用接口类别方法说明生命周期begin(sck, miso, mosi, ss)启动总线并绑定引脚四个参数均传-1时使用默认引脚生命周期end()释放引脚并停止总线配置setFrequency(freq)/setClockDivider(div)/getClockDivider()设置或读取时钟频率/分频配置setBitOrder(order)/setDataMode(mode)设置位序与 SPI 模式配置setHwCs(bool)/setSSInvert(bool)启用/禁用硬件片选、反转片选有效电平事务beginTransaction(SPISettings)/endTransaction()进入/退出原子化传输区段收发transfer(uint8_t)/transfer16()/transfer32()全双工单字节/16 位/32 位收发收发transfer(void*, size)/transferBytes(in, out, size)/transferBits(data, out, bits)缓冲区级/按位级全双工收发收发write()/write16()/write32()/writeBytes()/writePixels()/writePattern()只写不读的高效发送辅助pinSS()返回当前 SS 引脚号供digitalWrite手动控制片选辅助bus()返回底层spi_t*句柄2.1 事务机制beginTransaction 与 endTransaction标准 Arduino SPI 通信必须成对调用beginTransaction()/endTransaction()ESP32 实现对此做了两件事见 SPI.cpp参数生效beginTransaction(settings)会比较目标频率与当前分频若变化则通过spiFrequencyToClockDiv()重新计算分频并应用数据模式与位序并发保护底层用xSemaphoreCreateMutex()创建的互斥锁paramLock保护整段传输保证同一时刻只有一个任务占用总线参数避免多任务环境下 SPI 配置被并发改写。值得注意的实现细节transfer()、write()等收发函数会判断当前是否处于事务内_inTransaction事务内调用带NLNo Lock后缀的快速路径事务外则调用带锁的完整路径并自动补一次spiSimpleTransaction()因此即使忘记包事务收发仍能工作但并发安全性由事务保证。2.2 频率设置与硬件分频setFrequency()和beginTransaction()内部都会调用spiFrequencyToClockDiv()将目标频率换算为硬件分频。查看底层实现 cores/esp32/esp32-hal-spi.c 可以发现该函数会同时基于 XTAL 时钟与 SPLL 时钟源各计算一档分频_spiFrequencyToClockDivWithSource再从中挑选误差最小者从而在不同频率需求下自动选择最优时钟源。因此示例中spiClk 10000001 MHz只是一个请求值实际总线时钟由分频器就近匹配得出。3. ESP32 多总线架构VSPI 与 HSPIESP32 原厂 SoC 共有四个 SPI 控制器但 Arduino Core 默认开放其中两个给用户使用HSPI与VSPI。示例 SPI_Multiple_Buses.ino 的注释明确指出直接使用 Arduino 示例风格的SPI全局实例时默认占用 VSPI而 HSPI 保持空闲只要为两条总线各创建一个SPIClass实例即可同时使用两者——当然同一时刻仍只有一个总线实际在输出数据。3.1 总线编号与默认引脚全局实例SPI的绑定逻辑位于 SPI.cpp#if CONFIG_IDF_TARGET_ESP32 SPIClass SPI(VSPI); #else SPIClass SPI(FSPI); #endif即在经典 ESP32 上默认使用 VSPI而在 ESP32-S2/S3/C3 等后续芯片上使用 FSPI示例中通过#if !defined(CONFIG_IDF_TARGET_ESP32) #define VSPI FSPI #endif做了兼容映射。begin()不传引脚全部为-1时的默认引脚分配见 SPI.cpp汇总如下总线SCKMISOMOSISSVSPI经典 ESP321819235HSPI经典 ESP3214121315FSPIESP32-S2/S3 等由 pins_arduino.h 中SCK18 / MISO19 / MOSI23 / SS5定义其余总线默认-1这一表格与示例第 62 行、第 71 行注释中的引脚描述完全一致。3.2 自定义引脚ALTERNATE_PINSbegin()的四个参数SCLK, MISO, MOSI, SS允许将任意 GPIO 重映射到 SPI 外设。示例用ALTERNATE_PINS编译宏演示了这一能力#ifdef ALTERNATE_PINS #define VSPI_MISO 2 #define VSPI_MOSI 4 #define VSPI_SCLK 0 #define VSPI_SS 33 #define HSPI_MISO 26 #define HSPI_MOSI 27 #define HSPI_SCLK 25 #define HSPI_SS 32 #endif启用该宏后vspi-begin(VSPI_SCLK, VSPI_MISO, VSPI_MOSI, VSPI_SS)会把 VSPI 路由到 GPIO0/2/4/33hspi-begin(HSPI_SCLK, ...)把 HSPI 路由到 GPIO25/26/27/32从而避开与板载外设的引脚冲突。在 SPI.cpp 中begin()会依次调用spiAttachSCK()、spiAttachMISO()、spiAttachMOSI()完成 GPIO 复用spiAttachSS()仅在使用硬件片选时调用任一引脚绑定失败都会走err:分支打印 Attaching pins to SPI failed. 并返回false。3.3 片选控制软件 CS 与硬件 CS示例的spiCommand()函数展示了 Arduino 风格的手动片选流程void spiCommand(SPIClass *spi, byte data) { spi-beginTransaction(SPISettings(spiClk, MSBFIRST, SPI_MODE0)); digitalWrite(spi-pinSS(), LOW); // 拉低 CS 开始传输 spi-transfer(data); digitalWrite(spi-pinSS(), HIGH); // 拉高 CS 结束传输 spi-endTransaction(); }注意begin()只负责把 SS 引脚记录为输出并不会自动拉低片选示例第 78-81 行通过pinMode(vspi-pinSS(), OUTPUT)显式声明因此需要在事务内手动控制pinSS()。如果希望由硬件自动生成片选时序可调用setHwCs(true)需在begin()之后其内部调用spiAttachSS()与spiSSEnable()完成硬件 CS 挂接见 SPI.cpp。4. 实战同时驱动 VSPI 与 HSPI 两条总线示例 SPI_Multiple_Buses.ino 的核心思路是一总线一实例SPIClass *vspi NULL; SPIClass *hspi NULL; void setup() { vspi new SPIClass(VSPI); hspi new SPIClass(HSPI); // 默认引脚SCLK18, MISO19, MOSI23, SS5 vspi-begin(); // 默认引脚SCLK14, MISO12, MOSI13, SS15 hspi-begin(); pinMode(vspi-pinSS(), OUTPUT); pinMode(hspi-pinSS(), OUTPUT); } void loop() { spiCommand(vspi, 0b01010101); // 向 VSPI 从机发 0x55 spiCommand(hspi, 0b11001100); // 向 HSPI 从机发 0xCC delay(100); }运行流程与要点创建实例用new SPIClass(VSPI)与new SPIClass(HSPI)分别绑定两条总线构造器默认spi_bus HSPI见 SPI.h初始化两条总线各自调用begin()走默认引脚或ALTERNATE_PINS自定义引脚片选就绪通过pinSS()获取总线 SS 引脚并设为输出循环收发loop()中交替向两条总线写入0b010101010x55与0b110011000xCC每条总线以beginTransaction → 拉低 CS → transfer → 拉高 CS → endTransaction的完整事务完成一次 8 位全双工交换。由于两条总线共享同一颗芯片且任意时刻只能有一条在物理上输出示例通过delay(100)让两条总线轮流工作互不干扰。示例目录中还附带了逻辑分析仪抓拍文件 multiple_bus_output.PNG可直观看到 VSPIPin 18/19/23/5与 HSPIPin 14/12/13/15两组独立的总线波形VSPI 以相对低速的时钟发送 0x55 并回读全 1MISO 拉高HSPI 以另一时钟发送 0xCC 并回读全 0两条总线的 CS 均在各自传输窗口内拉低印证了双总线可独立选通、并发工作的特性。5. 从库接口到底层硬件关键调用链将示例中的一次vspi-transfer(0x55)展开可梳理出完整的调用链spiCommand() // 应用层事务 手动 CS ├─ SPIClass::beginTransaction() // [SPI.cpp#L199] 计算分频并加互斥锁 │ └─ spiTransaction() // [esp32-hal-spi.c#L1273] 应用分频/模式/位序 ├─ SPIClass::transfer() // [SPI.cpp#L226] 事务内走快速路径 │ └─ spiTransferByteNL() // 无锁单字节全双工 └─ SPIClass::endTransaction() // [SPI.cpp#L211] 释放互斥锁关键节点begin()→spiStartBus()在 esp32-hal-spi.c 中完成外设使能、时钟初始化初始分频来自spiFrequencyToClockDiv(NULL, _freq)默认 1 MHz事务内收发_inTransaction true时调用spiTransferByteNL()等无锁版本避免重复加锁开销适合批量高速传输缓冲区级收发transferBytes()底层对应spiTransferBytes()/spiTransferBytesNL()支持data为NULL的只读操作与out为NULL的只写操作专用写接口writePixels()兼容 ILI9341 等屏驱、writePattern()利用 64 字节硬件 FIFO 重复输出图案单次图案超过 64 字节时会自动分段见 SPI.cpp为显示与信号生成场景做了优化。6. 常见问题与注意事项两条总线不能同时输出多总线示例的注释明确提示 only will actually be outputting at a time多总线主要用于连接多个 SPI 从机而非并行提速不同芯片默认总线不同经典 ESP32 用SPI(VSPI)其余芯片用SPI(FSPI)跨芯片移植时需注意VSPI/HSPI与FSPI的宏映射示例第 43-45 行已示范兼容写法手动 CS 必须自己拉低/拉高begin()只配置引脚方向不会自动产生片选脉冲若嫌麻烦可在begin()后调用setHwCs(true)交给硬件事务必须成对多任务环境下漏掉endTransaction()会导致互斥锁不释放阻塞其他任务的总线访问时钟频率是就近匹配SPISettings中的频率经spiFrequencyToClockDiv()换算成最接近的分频值最终实际频率以getClockDivider()换算结果为准。7. 延伸阅读官方 SPI 文档与示例入口docs/en/api/spi.rstSPI 库源码libraries/SPI/src/SPI.h、libraries/SPI/src/SPI.cpp多总线完整示例SPI_Multiple_Buses.ino底层 HAL 实现cores/esp32/esp32-hal-spi.c总线启动、事务、频率分频与片选管理默认引脚定义variants/esp32/pins_arduino.h不同板型可在variants/下对应目录查看各自的pins_arduino.h【免费下载链接】arduino-esp32Arduino core for the ESP32 family of SoCs项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考