☰
ESP32-S3实战:双核任务下TFT彩屏显示与传感器采集记录
2026/10/14 17:04:51 网站建设 项目流程

前阵子帮朋友折腾一个小型桌面终端,要求能接一块TFT彩屏、能刷WiFi做数据展示、还希望留出跑轻量AI模型的余地。翻了一圈开发板,最后把目标锁定在了 ESP32S3 Dev Module 这块板子上。这类板子是目前 Arduino 生态里性价比非常高的选择,双核 240MHz、板载 8MB PSRAM、16MB Flash,WiFi 和 BLE 全都有,Arduino IDE 开箱即用,几分钟就能把屏点亮。这篇文章我就把自己从拿到板子到能稳定跑双核任务的过程完整写下来,包括环境配置、屏幕驱动、传感器读取、任务分配和一堆实测踩坑记录,给想入门或者已经在折腾的朋友做个参考。

我用惯了普通单片机,一开始对 ESP32-S3 还有点不放心,担心它和 Arduino 生态的配合不够顺。实际用了之后会发现,这块板子完全是“体力活选手”:算力足够、内存够大、外设多,而且社区里各种库已经非常成熟,几乎你要的传感器、屏幕、通讯模块都有现成方案。正文里我会把关键的原理讲清楚,也会给出可以直接抄的配置和代码,尽量让刚接触的人看完就能上手,少走我走过的弯路。

1. 开发板定位与核心价值

1.1 为什么选 ESP32-S3,而不是老款 ESP32 或单核板子

很多人的第一块板子可能是 Uno 或者 Nano,这两类板子跑个传感器、控制个舵机没问题,但一旦涉及屏幕刷新、网络请求、音频采样这些任务,8 位单片机的资源就非常吃紧了。ESP32-S3 不一样,它是一颗双核 Xtensa LX7 处理器,主频最高能到 240MHz,片上 SRAM 有 512KB,配合外置 PSRAM 后内存空间更是相当充裕,跑 LVGL、加载图片帧缓冲、做音频缓冲都完全不虚。

老款 ESP32 用的是 Xtensa LX6 内核,虽然也是双核,但 ESP32-S3 在架构上做了一些调整。更重要的是,S3 这颗芯片在指令集上增加了向量指令支持,做一些轻量级音频特征提取、关键词唤醒、屏幕UI渲染加速时有明显优势。如果你的项目有“屏幕显示+无线通信+简单边缘处理”这几项需求,ESP32-S3 比老款 ESP32 更值得选,它把不必要的蓝牙经典模式砍掉了一些复杂度,同时强化了低功耗、USB 和 GPIO 的灵活性。

对比时我还考虑过树莓派 Pico 系列。Pico 的优点是超低功耗和实时响应,但 WiFi 需要额外模块,生态和例程也比不上 ESP32 阵营。ESP32-S3 的吸引力在于“一颗芯片解决几乎所有联网外设需求”,不需要像 STM32+ESP8266 那样拼两块板子,也不用像树莓派那样跑完整操作系统,适合物联网终端、桌面仪表、小游戏机、语音助手这类产品原型。

1.2 Dev Module 板级资源盘点,越用越顺手

市面上最常见的 ESP32S3 Dev Module 形态,一般是基于 WROOM-1 模组做的开发板,板载一个 USB 口、一堆排母、复位和 Boot 按键。模组内部集成的是 16MB Flash 和 8MB PSRAM,这个组合对我来说很关键。8MB PSRAM 意味着你可以开高分辨率屏幕的帧缓冲、缓存大数组,在跑复杂 UI 时才不会动不动就内存溢出。

板子上的 USB 口比较特殊,它直接连接到芯片的 USB OTG 外设,不是普通单片机那种只用来下载程序的串口芯片方式。也就是说,这块板子既能被识别成串口设备做烧录和日志输出,也能被配置成 USB 键盘、鼠标、U 盘之类的设备。用 Arduino 开发时,通常把它当串口设备用,但如果你喜欢,后面的代码里也可以用 USB HID 功能做自定义键鼠,扩展性比传统串口方案高不少。

引脚方面,Dev Module 把大部分 GPIO 都引出来了,包括 SPI、I2C、UART、ADC、电容触摸等。需要注意的是,ESP32-S3 的 GPIO26 到 GPIO32、GPIO33 到 GPIO37 这些引脚在作为普通数字输入使用时会有限制,比如不能接外部上拉电阻或需要做特殊处理。实际项目里我会尽量避免用这些“有脾气”的引脚,把普通 IO 需求都用常见引脚解决。板载的 Neopixel RGB 灯通常接在 GPIO48 上,调试时很实用,能用它快速区分程序跑没跑起来。

这张表总结了我常用的板子资源分布:

资源数量/位置常用场景
CPU双核 Xtensa LX7,240MHz任务并行、事件处理
SRAM512KB 片上变量、任务栈、协议栈
PSRAM8MB 板载帧缓冲、大数组、UI缓存
Flash16MB固件、字库、资源文件
WiFi/BLE2.4GHz WiFi + BLE 5网络通信、蓝牙透传
USBUSB OTG 接口烧录、日志、HID 设备
常用 GPIO多数引到排母屏幕、按键、传感器
板载 LEDGPIO48状态指示

2. Arduino 环境配置与板级参数调整

2.1 开发板支持包安装与串口驱动排查

在 Arduino IDE 里使用 ESP32-S3,首先要安装 esp32 开发板支持包。我建议直接在“文件 -> 首选项 -> 附加开发板管理器网址”里填入官方维护的包地址,这样后面的板型列表里才会出现 ESP32S3 Dev Module。安装过程需要联网下载编译器工具链,时间长短取决于网络情况,装完以后在“工具 -> 开发板 -> esp32”菜单下就能看到这块板子了。

打开开发板管理器,搜索 esp32,安装 2.x 以上的版本比较稳。新版支持包已经统一了 S3 的编译环境,对 USB CDC、PSRAM、双核任务的支持都挺完善。我遇到过有人装完支持包后板子选不到,多半是 Arduino IDE 版本太老或者安装过程被中断,建议用较新的 IDE 版本,重新安装一遍支持包基本能解决。

然后是串口识别问题。ESP32S3 Dev Module 有好几种硬件版本,有的板子用板载 USB 转串口芯片,有的直接走芯片的 USB CDC。如果你插上板子后在“端口”菜单里看到一个带“USB JTAG/serial debug unit”字样的设备,那就说明用的是原生 USB 模式,不用额外装驱动;如果看到的是无法识别的设备或者用了 CH340、CP2102 之类的串口芯片,那就要安装对应驱动。用 Windows 的朋友最容易在这里卡住,我会先装一个驱动精灵类工具不行,还是建议直接去芯片厂商官网下驱动,稳定且干净。

一个非常常见的问题:数据线只供电不传输数据。USB 线内部只有两根电源线,插上后板子能亮灯,但系统里就是不出现串口。这种情况换一根确定支持数据传输的线就能解决,看起来像笑话,实际项目里我至少碰到过三次。

2.2 烧录参数这样设定,少踩一半坑

板型选择正确只是第一步,真正决定能不能顺利烧录和稳定运行的是工具菜单里那一堆参数。我把自己常用的配置组合整理在这里:

参数项我的推荐值说明
BoardESP32S3 Dev Module选错板型会烧不进固件
USB CDC On BootEnabled开启后 USB 口直接输出日志
Flash Size16MB必须与实际 Flash 一致
Partition SchemeHuge APP / No OTA大项目预留足够空间
CPU Frequency240MHz跑复杂 UI 时尽量拉满
Flash ModeQIO上传失败时改成 DIO 再试
Upload Speed460800比默认值更稳,适合新手
PSRAMOPI PSRAM必须开启,否则内存不够用

第一次插上板子后,我通常先做一件事:按住 BOOT 键不放,再按一下 RST 键,进入下载模式。这样做的原理是芯片内部有一个从 UART 下载固件的启动分支,如果应用固件本身有 bug 或者跑飞了,串口监听程序会一直等待设备响应,这时候手动拉低 BOOT 引脚重新进入下载模式是最可靠的。

上传代码时如果看到“Connecting...____”卡住不动,别着急,先把上传波特率从 921600 降到 460800。高速波特率在实际 USB 转串口链路里容易丢包,降低之后通常能正常烧录。还有一点经验:Arduino IDE 里上传前不要开着另一个串口监视器,两个程序同时占用同一个 COM 口会导致打开失败或上传冲突,我每次都先关监视器再上传。

3. 实操流程:双核任务下的显示与传感器采集

3.1 外设选型与接线,第一步先保证供电稳定

为了完整演示这块板子的能力,我搭了一个“桌面环境监测终端”的模拟项目:一块 SPI 接口的 240x320 彩色屏幕显示温度和湿度,一个 DHT22 传感器负责采集环境数据。这套组合非常适合入门,因为屏幕能直观地看到代码运行结果,传感器又能说明任务调度逻辑,两个外设加在一起还不算太复杂。

屏幕我选了常见的 ST7789 驱动芯片 SPI 屏,这类屏在 Arduino 生态里支持最好,资料多、驱动库成熟。接线按照 SPI 的方式连接:屏幕的 SCLK 接 GPIO12,MOSI 接 GPIO11,CS 接 GPIO10,DC 接 GPIO9,RST 接 GPIO8,背光控制接 GPIO14。这些引脚可以随便改,但为了后续代码一致,建议先按这个顺序接。电源方面,屏幕模块的 VCC 接 3.3V,有些大尺寸屏幕需要 5V 背光供电,最好查看模块丝印说明,不要盲目全接 3.3V。

DHT22 传感器则更简单,VCC 接 3.3V,GND 接 GND,DATA 接 GPIO4,同时需要在 DATA 和 VCC 之间加一个 10k 欧姆上拉电阻。DHT22 本身是单总线协议,上拉电阻是必须的,如果没有装,读取结果就会一直是 NaN 或者干脆读不到数据。很多市售模块已经把上拉电阻集成进去了,但如果你买的是裸传感器,记得自己加上。

关于供电我要特别多说一句。ESP32-S3 的 3.3V 引脚通常由板载 LDO 稳压提供,这颗 LDO 的电流余量很小,如果同时给屏幕模块、传感器、还有外接的 GPS 或音频模块全部从 3.3V 引脚取电,电流很容易超标,导致电压跌落、屏幕闪烁、WiFi 掉线。我的做法是:小功耗外设从板子 3.3V 取电,大功耗模块直接用外部 5V 稳压供电,并且共地。这个习惯帮我避免了很多奇怪的随机故障。

3.2 屏幕驱动的关键配置:TFT_eSPI 的 User_Setup.h

Arduino 下驱动 ST7789 屏幕,我最常用的是 TFT_eSPI 库。这个库功能强大,文字渲染、图形绘制、Sprite 帧缓冲都支持,而且底层做了不少性能优化。安装好库之后,真正要动的地方是库目录下的 User_Setup.h 配置文件,里面要告诉库“你到底用的什么驱动、引脚怎么接的”。这段配置是屏幕能不能点亮的关键,我每次都要仔细检查。

打开 User_Setup.h,把包含文件的部分改成下面这样(不同版本位置略有差异,但核心字段一致):

#define ST7789_DRIVER #define TFT_WIDTH 240 #define TFT_HEIGHT 320 #define TFT_MOSI 11 #define TFT_SCLK 12 #define TFT_CS 10 #define TFT_DC 9 #define TFT_RST 8 #define TFT_BL 14 #define SPI_FREQUENCY 40000000

把这些字段设置好之后,编译上传一个简单的画点测试程序,屏幕如果能显示纯色的填充画面,就说明驱动和接线没问题。如果屏幕没有任何反应,不要急着改代码,先用万用表确认每个引脚的对地电压和焊接情况,很多时候问题出在杜邦线接触不良。

关于刷新率,我建议 ST7789 的 SPI 频率先设成 40MHz。虽然这个库在部分屏幕上能跑到 80MHz,但长杜邦线、面包板的寄生电容会让高速信号变形,出现花屏、条纹。40MHz 是一个很可靠的折中点,显示流畅度已经足够日常使用。我实测用一个 240x320 的 Sprite 做全屏填充刷新,40MHz 下的帧率肉眼感觉完全畅快,只有在做视频播放那种场景才需要再压榨性能。

另外要注意,TFT_eSPI 建议用setRotation设置屏幕方向。我的布局是横屏显示,所以初始化后调用tft.setRotation(1),后面的坐标轴就会对应旋转后的宽高,写绘制代码时思路更清楚。

3.3 双核任务怎么写:采集与显示互不卡顿

ESP32-S3 是双核芯片,在 Arduino 环境里默认情况下setup()和loop()跑在 Core 1 上。如果我在loop()里既做 DHT22 的 2 秒延时读取、又做屏幕全屏刷新、还要处理网络请求,很容易出现这种现象:屏幕刷新到一半,传感器读取卡住了,或者网络连接超时了。解决方法是手动创建任务,把不同功能分配到两个核上。

DHT22 这种传感器读取很慢,时序要求又严格,读一次需要等待几十到上百毫秒,如果把它放在主循环里,整个显示流程都会被拖住。我在这个项目里把传感器采集独立成了一个任务,固定放在 Core 0 上,让它每 2 秒唤醒一次更新全局变量;显示刷新则继续走loop(),运行在 Core 1 上。两个核各干各的,互不阻塞,屏幕就能一直保持流畅刷新。

引脚方法如下:

#include <TFT_eSPI.h> #include <DHT.h> #define DHTPIN 4 #define DHTTYPE DHT22 DHT dht(DHTPIN, DHTTYPE); TFT_eSPI tft = TFT_eSPI(); volatile float tempC = 0.0f; volatile float humi = 0.0f; void sensorTask(void *param) { float t = 0.0f; float h = 0.0f; for (;;) { t = dht.readTemperature(); h = dht.readHumidity(); if (!isnan(t)) { tempC = t; } if (!isnan(h)) { humi = h; } vTaskDelay(pdMS_TO_TICKS(2000)); } } void setup() { Serial.begin(115200); delay(1000); dht.begin(); tft.init(); tft.setRotation(1); tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_WHITE, TFT_BLACK); // 点灯验证 pinMode(48, OUTPUT); digitalWrite(48, HIGH); xTaskCreatePinnedToCore( sensorTask, // 任务函数 "sensor", // 任务名称 4096, // 栈大小,单位字节 NULL, // 任务参数 1, // 任务优先级 NULL, // 任务句柄 0 // 核心编号,这里放到 Core 0 ); Serial.println("boot ok"); } void loop() { static uint32_t last = 0; if (millis() - last > 300) { last = millis(); // 全屏清屏后再绘制,简单直接 tft.fillScreen(TFT_BLACK); tft.setTextDatum(MC_DATUM); tft.drawString("Temp", tft.width() / 2, 80, 4); tft.drawString(String(tempC, 1) + " C", tft.width() / 2, 130, 6); tft.drawString("Humi", tft.width() / 2, 210, 4); tft.drawString(String(humi, 0) + " %", tft.width() / 2, 260, 4); } }

简单解释一下这个代码。sensorTask是一个 FreeRTOS 任务函数,内部是个死循环,vTaskDelay(pdMS_TO_TICKS(2000))让任务每 2 秒醒来一次。读取到的温湿度写入全局变量tempC和humi,因为它们被另一个核上的loop()读取,我特意声明成volatile,避免编译器缓存优化造成读不到最新值。

任务创建函数xTaskCreatePinnedToCore是 ESP32 Arduino 封装好的接口,最后两个参数分别是优先级和核心编号。优先级 1 和 Arduino 的loopTask优先级相同,在任务不是特别频繁触发的情况下能和谐共存;核心编号 0 表示放到 Core 0 上。如果以后还要跑 WiFi,WiFi 协议栈本身跑在 Core 0 上,任务调度器会合理安排时间,通常不会冲突。需要更快响应时,把优先级调成 2 到 3 即可。

loop()里我用了一个时间片判断,每 300ms 做一次全屏刷新。这里每次都是fillScreen清屏再重新画文字,优点是代码简单,缺点是全屏填充稍微浪费性能。实际项目里更优雅的做法是只在数据变化时更新局部区域,或者用 Sprite 先在内存里画好整帧再一次性推送到屏幕。对于演示项目来说,这个代码已经足够直观了。

启动时我把板载 LED 通过 GPIO48 点了一下,用来快速判断固件有没有跑起来。这样即使屏幕出问题,我也能从 LED 状态确认程序逻辑是否正常,排查问题的时候能省很多时间。

4. 高频问题与排查思路实录

4.1 八个最常遇到的坑,一张表说清楚

整个开发过程中,我把遇到的问题按频率排了个序,整理成速查表。这些是我和身边几个朋友实测都遇到过的,覆盖了从硬件到软件的常见坑。

现象可能原因解决办法
电脑不识别串口驱动未装 / 数据线只能供电安装对应驱动芯片驱动,换传输线
上传卡在 Connecting没有进入下载模式 / 波特率过高按住 BOOT 再上点,或把速率降到 460800
屏幕白屏无显示CS/DC 接错 / 引脚配置不对 / SPI 频率过高对照 User_Setup.h 逐项检查,降低频率到 40MHz
串口监视器无输出USB CDC 未开启 / 端口选错开启 USB CDC On Boot,重新拔插 USB
程序偶尔重启供电电流不足 / 复位引脚干扰使用外部稳压电源,检查 EN 引脚上拉
内存申请失败未开启 PSRAM / 分区太小工具菜单选择 OPI PSRAM,换 Huge APP 分区
DHT22 读数一直 NaN上拉电阻缺失 / 引脚不对加上拉电阻,确认 DHT 连接稳定
屏幕刷新卡顿明显SPI 频率太低 / 全屏填充过多调频率到 40 或 80MHz,改用局部刷新

这些坑里,最隐蔽的是“串口有日志但烧录失败”。这种情况通常是 USB CDC 模式已经启动,串口监视器占用着那个端口,IDE 没法弹掉重新下载。遇到后我会拔掉 USB 重新插,等 2 秒再点上传,几乎每次都成功。

另一个隐蔽问题是分区的坑。如果你在工具菜单里选择了 Huge APP,但板子的实际 Flash 是 16MB,下载时固件会被放到一个很大区域,看起来没问题。但如果 Flash 型号是 8MB 而你还按 16MB 设置,烧录会提示校验失败或者直接造成启动死循环。所以拿到板子第一件事就是确认模组具体型号,不同批次配置可能不一样。

4.2 排查思路比单个技巧更重要

我的习惯是遵循“从电源到串口、从串口到外设”的顺序排查。板子没反应,先看板载 LED 是否点亮,测量 3.3V 引脚电压是否正常;电压没问题再看串口能不能打印启动信息;串口通了再逐个接外设,每接一个就测试一次。全部正常后再写业务逻辑。

最怕的情况是“屏幕偶尔闪、WiFi 时不时断”,这种随机性问题九成和供电有关。我用过一块杂牌屏幕模块,峰值电流接近半安培,从板子 3.3V 引脚直接取电后,主芯片电压被拉低,导致 WiFi 连接都断。后来把屏幕供电改成外部 5V 经过单独稳压给 3.3V,只保留共地连接,问题立刻消失。这个经验后来被我用在了多个项目里,凡是带屏幕或者电动机的项目,我都会单独设计供电链路。

调 DHT22 时还有个细节容易被忽略:每次读取至少要间隔 2 秒,因为传感器本身测量周期就是 2 秒左右,如果你频繁读取,它可能还在忙于上一次测量,返回的数据要么是上一次的结果,要么直接超时。我在任务里用vTaskDelay(2000)就是出于这个原因。真需要更高频率的环境数据,建议换成数字 I2C 传感器,比如常见的 SHT30 系列,读取速度会快很多。

关于 TFT_eSPI 的使用,有另一个小技巧值得说。如果你在显示中同时用到中文字库,库内置的字体是不包含中文的,需要额外加载字库文件或者使用SPIFlash方式存储字库。这个项目我用的是英文字符串,所以没踩这个坑。但如果是做中文天气站、菜单界面,最好提前规划字库方案,否则后期文字显示出来全是方块会很烦躁。

使用 Arduino 环境做 ESP32-S3 开发并不复杂,门槛主要在环境配置和硬件细节上。我第一次拿到板子时,也经历了“连不上、烧不进、屏不亮”的三连坑,后来把上述内容逐个落实,整个流程变得非常顺滑。尤其是双核任务这个概念,看起来有点吓人,但当你真正体会到采集和显示互不阻塞的流畅感之后,就会觉得这块芯片的设计确实很有意思。

最后再分享一点我在实践中的体会:无论你打算做天气站、小游戏机还是 IoT 网关,先把“串口日志 + 板载 LED”这个最小调试链路打通,再接入其他外设。这样做的好处是每次引入新变量都有明确的判断依据,出了问题能快速定位是硬件、配置还是代码的锅。如果连串口日志都没有,就先别急着写复杂的业务逻辑,先解决供电、驱动和连接这三件小事。这套方法虽然朴素,但真的能帮你省下大量排查时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询