ESP32-S3 多 SPI 设备并行在线:4 步让两条总线不打架
【免费下载链接】arduino-esp32Arduino core for the ESP32 family of SoCs项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32
屏幕刚亮起来画面就花了,SD 卡里的日志文件直接变砖,串口监视器吐出一屏 0xFF。ESP32-S3 上多 SPI(一条时钟线共享、一主对多从的总线协议)设备同时跑,常见姿势就是屏幕和卡抢同一条时钟线。今天用 arduino-esp32 里现成的双总线机制,把这事拆开做一遍——两条总线各带各的设备,互不打架。
引脚布局决定分线空间,先看你板子上哪些脚是真正空闲的。
30 秒自检:你板子是不是真在打架
如果你看到"单跑都正常,一起跑就出错",大概率不是器件问题,而是时序撞车:同一条总线被两段代码同时驱动,时钟沿被搅在一起。
如果你看到屏幕撕裂、卡上读回一片 0xFF,且恰好发生在一帧画面推送和文件写入重叠的时段,这是共享总线的典型症状。多 SPI 总线冲突,八成是引脚分配的问题,先别急着怀疑器件。
如果你看到两个模块用的引脚都指向 pins_arduino.h 里那组默认值(S3 devkit 上是 SCK=12、MOSI=11、MISO=13、SS=10),那代码再怎么调也救不回来,得先把线切开。换板子时也先来这里核对,引脚定义是排雷的第一现场。
SPI 信号不从固定引脚出来,而是经过 GPIO 矩阵——芯片里的一块"交换台"——接到任意 GPIO。所谓冲突,本质是两台控制器抢同一组 GPIO。
分步走:从接线到加速 🔧
这一步起按接线、初始化、调度、加速四件事来,每步先说做什么,再说为什么,最后给关键参数。
第一步 接线:把共用的线切开
做什么:给每个设备配一整组 SCK、MOSI、MISO、CS,任何两根线都不被两个设备共享。
为什么:S3 上全局SPI对象默认占着 FSPI,HSPI 完全空闲,两者是互相独立的硬件控制器,走线全靠 GPIO 矩阵,没有"必须走哪几个脚"的硬约束——约束只有一条:别重叠。
关键参数:SD 卡沿用 FSPI 默认引脚,屏幕在 HSPI 上另起一组新脚,避开 strap 脚(0、3、45~47)和 USB 脚(19、20)。
| 设备 | 总线 | SCK | MOSI | MISO | CS |
|---|---|---|---|---|---|
| SD 卡 | FSPI(默认) | 12 | 11 | 13 | 10 |
| TFT 屏 | HSPI | 33 | 35 | 37 | 34 |
这组 ESP32-S3 SPI 引脚分配可以直接抄:
// 屏幕引脚与 SD 卡完全独立,一条线都不共享 #define TFT_SCK 33 #define TFT_MOSI 35 #define TFT_MISO 37 #define TFT_CS 34第二步 初始化:把第二条总线建出来
做什么:new SPIClass(HSPI)建一个专用实例,begin()时显式传引脚。
为什么:全局SPI对象只占一条总线,S3 上是 FSPI;HSPI 没有默认引脚,不指定就挂不上去,这和初代 ESP32 差别最大。SPIClass的定义在 SPI.h,官方示例 SPI_Multiple_Buses.ino 骨架一样。
建立第二条总线,并把片选管好:
SPIClass *tft = new SPIClass(HSPI); // 第二条独立总线 tft->begin(TFT_SCK, TFT_MISO, TFT_MOSI, TFT_CS); // 参数顺序:SCK, MISO, MOSI, CS // CS 不会自动配置,得自己拉 pinMode(TFT_CS, OUTPUT); digitalWrite(TFT_CS, HIGH); // 空闲时保持高关键参数:begin()的参数顺序是 SCK、MISO、MOSI、CS,MISO/MOSI 传反了屏幕不亮,而且很难看出问题在哪。官方示例的注释也明说:Arduino 这套 API 不会自动把 CS 拉低。
第三步 调度:每帧用事务包起来
做什么:每次访问设备用beginTransaction/endTransaction包住一次完整操作,进事务后再拉低 CS。
为什么:事务(一对开始/结束,把这条总线的频率和模式临时锁住)里的锁是内核自带的互斥——SPI.cpp 的beginTransaction会拿 paramLock 锁,多任务用同一条总线时,频率和模式不会被对方改到一半。两条不同的总线各自拿各自的锁,天然并行,谁也不用等谁。
推一帧画面,整段包在事务里:
void pushFrame(uint8_t *buf, uint32_t len) { tft->beginTransaction(SPISettings(40e6, MSBFIRST, SPI_MODE0)); // 40MHz,屏幕模式 digitalWrite(TFT_CS, LOW); // 传输前拉低片选 tft->transfer(buf, len); digitalWrite(TFT_CS, HIGH); // 传输完释放片选 tft->endTransaction(); }想省手动拉 CS 的麻烦,可以setHwCs(true)让硬件自动接管;但多设备场景里手动切换更直观,示例用的也是手动。
第四步 加速:让 FIFO 干活
做什么:一帧画面用一次transfer/writePixels推完,别把一帧拆成很多小事务。
为什么:看 esp32-hal-spi.c 的传输路径,是"64 字节一批写进硬件 FIFO(先进先出的缓冲寄存器),再忙等它搬完"。开销集中在每次事务开头:拿锁、设分频、写控制寄存器。S3 上这条 HAL 路径是 CPU 陪着搬,加速空间就在"少停多跑"——少开几次事务,多走几段 64 字节。想要 DMA(让硬件自己搬数据,CPU 不用死等)级别的异步,是 IDF 层驱动的事,知道方向就行,这里不展开。
【免费下载链接】arduino-esp32Arduino core for the ESP32 family of SoCs项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考