最近折腾了一阵子 ESP32-S3,在这个芯片上用 Arduino 写了不少小程序,从最简单的点灯到带着摄像头的图像识别,踩了不少坑,也算摸清楚它的脾气。这篇文章是我从“把板子拿到手”到“能稳定跑起业务逻辑”这一路的详细记录,包括芯片本身的好处、开发环境里那些容易绊人的细节、几段可以直接抄走的代码,以及我自己在处理过程中反复遇到的典型问题。如果你是刚接触这款板子,或者正犹豫要不要从老 ESP32 升级到 S3,这篇东西应该能帮你省下不少时间。
ESP32-S3 这块板子在 Arduino 生态里属于那种“看起来只是升级、实际拉开差距”的存在。相比前代,它双核 240MHz 的算力本就可观,关键是加入了向量指令和 PSRAM 大内存的支持,这意味着一些轻量级 AI 推理、图像识别这类原本得靠树莓派才能跑的活儿,现在一块几十块的开发板就能应付。加上板载的 USB 口可以直连烧录和串口调试,不用再到处找 USB 转 TTL 模块,我个人的体会是:这东西几乎是为“Arduino 玩家想再往上走一步”量身定做的。如果之前你被 ESP8266 的内存限制逼疯过,被 ESP32 老版的 USB 调试麻烦折腾过,那 S3 应该会让你舒服很多。
1. 为什么选择 S3:芯片底子与板卡设计的双重补全
很多刚开始接触 S3 的人会把它理解成一个“频率更高的 ESP32”,这么说不能算错,但偏差不小。先从芯片架构说起。ESP32-S3 的 CPU 是两个 Xtensa LX7 核,主频能到 240MHz,正常跑 Arduino 代码时,哪怕其中一个核在忙 WiFi 收发,另外一个核还能从容处理传感器采样、屏幕刷新这些任务,这一点在实际项目里非常实用,不会像单核板卡那样出现“网络一忙、业务逻辑就卡顿”的尴尬。
更重要的是它带向量指令集。用不太学术的话讲,这有点像给处理器加了一个“小号加速器”,在做 FFT、矩阵乘法、卷积这类重复性高的计算时,能比普通指令快不少。说实话,在 Arduino 这种开发方式里,你可能不会直接手写汇编去调这些指令,但后面跑音频分析、图像分类模型时,底子确实厚实,这是老 ESP32 不具备的。
S3 的另一个大杀器是 PSRAM。普遍的开发板型号是 N16R8,也就是板载 16 MB Flash 和 8 MB 的 PSRAM。如果你在网上查,很容易被这些参数绕晕,用大白话讲就是:Flash 是放程序和数据文件的“硬盘”,PSRAM 是程序运行时的“内存条”。之前 ESP32 老版一般只有 4MB Flash 和 512KB 左右的 SRAM,运行稍微复杂一点的项目就开始爆内存。而 S3 配上 PSRAM 后,前端在跑一个带图片的网页、后端同时处理传感器数据,基本没什么压力。特别是用 Arduino 的WebServer库时,网页里塞几张图片,字符串操作频繁,PSRAM 就是救命的。
板卡设计层面,“ESP32S3 Dev Module”是一个非常标准的开发板形态。正面引出两列排针,几乎把芯片所有的 GPIO 都拉出来了,方便接各种外设。比较关键的是它自带一个 USB 接口,这个 USB 是直接连到芯片原生的 USB-OTG 外设上的,走的是 CDC 协议。简单说,插上 Type-C 线,电脑就会识别出一个串口,既能烧录又能打印日志,不需要额外接刷机电路的模块。部分版本还板载了一颗单独的 USB 转串口芯片作为保守方案,于是你会看到板子上有两个 USB 口,一个是直连芯片的,一个是走转换芯片的,这点新手很容易搞混。
1.1 N16R8 这套常见配置代表什么
选型时,我看到“N16R8”这几个字母通常代表哪家模块厂出的方案,不同厂商略有差异,但基本逻辑一致:N 后面的数字是 Flash 容量,R 后面的数字是 PSRAM 容量。16MB Flash 其实对 Arduino 项目来说绰绰有余,毕竟一块板子的 Flash 还要留存储空间给文件系统(SPIFFS/LittleFS)、OTA 升级包等,4MB 的老配置经常会有“代码编译出来 1.2MB,剩余空间不够放网页资源”的尴尬。S3 默认的分区表和更大的 Flash 配合,让你在产品原型阶段不用太纠结空间优化。
8MB PSRAM 则是量变引起质变的另一层关键。Arduino 环境里只要在构建选项里正确开启 PSRAM,ps_malloc()就能直接分配大块内存,配合heap_caps_malloc这类底层调用,玩起图形界面和图像采集时会随意很多。很多人一开始不开启 PSRAM,然后跑个 OpenCV 风格的库就崩了,其实不是性能问题,是内存管理没打通。
1.2 板载 USB 带来的调试便利
我以前用老 ESP32 的时候,最烦的就是烧录时得按住 BOOT 键,然后手动按一下 EN 复位键进下载模式,时间没卡好还会失败。S3 的 USB-CDC 方案最大的好处是:只要烧录配置选对(后面我会详细说),插线之后,点上传按钮,它会自动复位进入下载状态,基本不用碰按键。同时串口监视器里面看到的不再是简单复位信息,芯片的 USB 枚举、日志输出都在同一个口,对排查问题方便很多。
当然,这个便利也带来一个小问题:如果你的代码里在初始化 USB CDC 时出了问题,或者说传了有问题的代码导致芯片运行异常,系统可能会陷入“反复复位”,电脑上的串口会不停地断开重连,这时候反而得手动进下载模式去救,不能完全依赖自动化。这也是我后面会专门提的排查要点之一。
2. Arduino 扩展环境配置:细节决定成败
我用的开发环境是 Arduino IDE 2.x,目前版本对 S3 的支持已经相当成熟了。安装过程比较简单,我不打算花太多篇幅复述如何安装软件,但有三个点需要特别留心:内核安装、开发板选项、烧录模式。如果你在配置阶段反复出问题,几乎都是这三个地方没兜住。
首先安装 ESP32 内核,打开 Arduino IDE 的“开发板管理器”,搜索“esp32”,会出现 Espressif Systems 提供的官方包,安装即可。这里有个提醒,国内网络环境下载这个包有时候会很慢,甚至中途失败。如果你遇到这种情况,可以手动下载离线包放到指定目录,或者尝试更换网络环境后再试,过程憋屈但好歹能解决。
装完内核后,在“开发板”菜单中选择“ESP32 Arduino”,子菜单里找到“ESP32S3 Dev Module”这一项。这一步容易踩坑:有很多命名相似的选项,比如带“Mini”“Generic”字样的。选择最通用的“ESP32S3 Dev Module”就好,一般不会错。选中后,注意菜单栏下方多出来的几个配置项。
2.1 烧录方式:USB CDC 还是 UART
这是一个新手极易踩的坑。在“工具”菜单里会看到“USB Mode”选项,常见的是“Hardware CDC and JTAG”和“UART0”。如果你插的是板子侧面的原生 USB 口(通常标记为 USB,或者 Type-C 靠近板边),选 Hardware CDC and JTAG 模式并选择对应串口,上传速度一般能拉比较高,而且不用手动按键进入下载模式。如果你插的是 USB 转串口芯片的那个口,选 UART0,走传统方式。有的板子上只有一个串口芯片方案,那就只能选 UART0,具体看你的板型。
如果不确定,一个实用做法是:插上线,在电脑的设备管理器里看串口号。如果只有一个串口出现,通常就是当前 USB 接口对应的通道,配合板子上的丝印推断即可。别小看这一步,我见过很多人上传时选错端口,显示连接失败,然后开始怀疑驱动问题,清驱动装驱动忙活半天,最后发现只是端口选错。
分区方案也需要稍微注意。默认的“Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS)”在绝大多数情况下够用,但如果你的项目涉及 OTA 升级,尽量选带 OTA 的分区表;如果是在跑摄像头项目,要把存储空间留给图像缓冲,那选“16MB Flash (3MB APP/9.9MB FATFS)”这类大容量分区会舒服一些。分区表别经常改,因为每次切换分区表,Flash 里面的文件系统内容就会被格式化,板子上的数据会清空,相当于重装系统。
2.2 串口驱动与操作系统兼容性
Windows 系统下,如果用的是原生 USB CDC 模式,通常不需要装额外的驱动,系统会识别一个串口设备;如果是使用板载转换芯片(常见的有 CH340 类),一般是需要装对应芯片厂商的驱动。从实际体验来看,老生常谈的“识别不到串口”大多发生在换了一根不能传数据的 USB 线时,这里要特别提醒:并不是所有 Type-C 线都支持数据通信,有些只能充电。遇到串口死活不出来,先换一根确认支持数据传输的线再排查其他环节,这一条能帮你节省很多时间。
在配置完这些后,就可以愉快地进入写代码阶段了。
3. 标准流程实操:从点亮一个 LED 到多任务并行
我觉得学一块新板子,不需要一开始就上特别复杂的项目,最好从标准动作展开,把开发闭环跑通。这里我选一个比较典型且信息量足够的路线:点灯、串口打印、按键中断、多任务。每一步既能验证板子是否正常,又为后面的联网和进阶内容打基础。
3.1 点灯与主板自检
先把最简单的基础代码放出来。几乎所有开发板都有板载 LED,通常接在一个固定 GPIO 上,S3 的 Dev Module 卡上比较多见的是 GPIO 48,有些板子可能在 GPIO 2 或者其他角,最好先查一下你的板子原理图。最稳妥的办法是翻卖家资料,或者直接看板子背面的丝印,这一项因板而异,不要照搬网络教程的引脚数字。
那段最经典的代码其实很简短:
#define LED_BUILTIN 48 void setup() { pinMode(LED_BUILTIN, OUTPUT); } void loop() { digitalWrite(LED_BUILTIN, HIGH); delay(500); digitalWrite(LED_BUILTIN, LOW); delay(500); }编译上传成功之后,板载 LED 应该以 0.5 秒间隔闪烁。如果你发现代码上传成功但灯不亮,那大概率是 LED 引脚号和你的板子对不上。Arduino 2.x 在编译时会根据所选开发板自动定义LED_BUILTIN,但有些第三方板卡对LED_BUILTIN的支持并不可靠,所以自己明确写数字更安心。
3.2 串口输出和 delay 的陷阱
紧接着第二步,在初始化中启用串口,并在循环中打印一些信息:
void setup() { Serial.begin(115200); while (!Serial) { delay(10); } Serial.println("ESP32-S3 Ready"); } void loop() { Serial.printf("Free heap: %u bytes\n", ESP.getFreeHeap()); delay(1000); }注意while (!Serial)这行,只有在原生 USB CDC 模式下才需要它,作用是把串口做好了连接,防止前面打印的信息“飞掉”。如果使用 UART0 模式,不写也无妨,但一致性起见,保留它问题不大。
提一下 delay 的使用误区。在简单例程里用 delay 无伤大雅,但只要你开始接入 WiFi、传感器读取等在后台执行的任务,长时间 delay 会阻塞主循环,导致查询网络事件、读取传感器超时、看门狗超时等情况出现。一个相对简单的改进是使用“非阻塞定时”写法,用millis()记录上一次操作时间,再对比当前时间决定是否执行下一次操作。
比如 LED 闪烁写成:
unsigned long prevMillis = 0; const unsigned long interval = 500; void loop() { if (millis() - prevMillis >= interval) { prevMillis = millis(); digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); } }这段代码在整个loop()执行期间不会阻塞,后续加入传感器采样或网络处理时,主循环始终可以快速轮询多个任务,这是 Arduino 世界里一个非常核心的编程习惯。
3.3 借助 FreeRTOS 实现真正的多任务并行
S3 毕竟是双核工程,如果我只会单线程写法,总觉得浪费了它 240MHz 双核的能力。Arduino 环境默认集成了 FreeRTOS 系统,只是平时不直接暴露。如果你想让 LED 闪烁和串口打印两个任务独立运行,完全可以使用它的 API。值得注意的是,Arduino 环境下的loop()本身也是被系统作为一个任务调度的,编码方式大体一致。
下面给出一段可以直接编译运行的演示代码,用两个任务分别控制 LED 和串口打印:
#include <freertos/FreeRTOS.h> #include <freertos/task.h> void blinkTask(void *parameter) { pinMode(48, OUTPUT); while (true) { digitalWrite(48, HIGH); vTaskDelay(500 / portTICK_PERIOD_MS); digitalWrite(48, LOW); vTaskDelay(500 / portTICK_PERIOD_MS); } } void printTask(void *parameter) { while (true) { Serial.printf("Task running on core %d\n", xPortGetCoreID()); vTaskDelay(2000 / portTICK_PERIOD_MS); } } void setup() { Serial.begin(115200); xTaskCreate(blinkTask, "blink", 2048, NULL, 1, NULL); xTaskCreate(printTask, "print", 4096, NULL, 1, NULL); } void loop() { vTaskDelay(10); }运行这个程序,你会从串口监视器里看到打印任务跑在核心 0 或核心 1 上。如果你的应用场景需要把时间要求高的传感器读取任务放在一个核心、把网络任务放在另一个核心,就可以用xTaskCreatePinnedToCore把它绑定到指定核心上。S3 的 CPU 调度能力比看起来强大很多,合理利用任务优先级和核心绑定,能跑出远超同价位开发板的效果。
4. 发挥核心价值:Wi-Fi 与内建 Web 控制
讲实话,如果只拿 ESP32-S3 当普通单片机用,确实有点浪费。这块板子非常吸引人的地方在于它自带完整 2.4GHz Wi-Fi,可以轻松联网。而在 Arduino 生态下,把这块板子变成一个能直接用浏览器控制的智能硬件,难度低到难以置信。我经常把这个过程拆成三步走:扫描、连接、建服务。
4.1 先扫描周围 Wi-Fi:验证射频模块
刚开始接触 Wi-Fi 功能时,建议先运行一个扫描程序,确认射频部分工作正常。ESP32 的库直接把扫描封装好了:
#include <WiFi.h> void setup() { Serial.begin(115200); WiFi.mode(WIFI_STA); WiFi.disconnect(); delay(100); Serial.println("Scan start"); int n = WiFi.scanNetworks(); Serial.printf("Found %d networks\n", n); for (int i = 0; i < n; i++) { Serial.printf("%d: %s (%d dBm)\n", i + 1, WiFi.SSID(i).c_str(), WiFi.RSSI(i)); } Serial.println("Scan done"); } void loop() { delay(1000); }扫描到网络后基本可以认定天线和射频无硬件问题。如果扫描结果为空,而你确认路由器正常工作,第一个要检查的是代码里是否调用了WiFi.mode(WIFI_STA),第二个是板子的天线区域有没有被遮挡。尽量不要把板子贴着金属板运行,那样信号会差得离谱。
4.2 建立迷你网页:用 HTTP 控制设备
Wi-Fi 连接成功之后,最常用的模式就是让板子自己开一个 HTTP 服务,手机或者电脑通过浏览器直接访问板子的 IP,往板子发送指令。
代码逻辑分三层:连接 Wi-Fi、启动 TCP 服务、处理 HTTP 请求。Arduino 提供的 WebServer 库会自动帮你解析 HTTP 协议,你只需要写请求路径的处理逻辑。这里我分享一个控制 LED 开关的例子,顺带把页面也嵌在一个响应里返回:
#include <WiFi.h> #include <WebServer.h> const char* ssid = "your_ssid"; const char* password = "your_password"; WebServer server(80); const int ledPin = 48; bool ledState = false; void handleRoot() { String html = "<!DOCTYPE html><html><head>"; html += "<meta charset='utf-8'>"; html += "<meta name='viewport' content='width=device-width, initial-scale=1'>"; html += "<title>ESP32-S3 控制台</title></head><body>"; html += "<h2>LED 控制</h2>"; html += "<p>当前状态: "; if (ledState) { html += "<span style='color:green'>点亮</span>"; } else { html += "<span style='color:red'>熄灭</span>"; } html += "</p>"; html += "<a href='/on'><button>开灯</button></a> "; html += "<a href='/off'><button>关灯</button></a><br><br>"; html += "<a href='/'><button>刷新</button></a>"; html += "</body></html>"; server.send(200, "text/html", html); } void handleOn() { ledState = true; digitalWrite(ledPin, HIGH); server.sendHeader("Location", "/", true); server.send(302, "text/plain", ""); } void handleOff() { ledState = false; digitalWrite(ledPin, LOW); server.sendHeader("Location", "/", true); server.send(302, "text/plain", ""); } void setup() { pinMode(ledPin, OUTPUT); digitalWrite(ledPin, LOW); Serial.begin(115200); WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); Serial.print("Connecting"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.print("IP: "); Serial.println(WiFi.localIP()); server.on("/", handleRoot); server.on("/on", handleOn); server.on("/off", handleOff); server.begin(); } void loop() { server.handleClient(); }替换好 Wi-Fi 名称和密码后,编译上传,打开串口监视器,看到打印出来的 IP 地址后,直接用同一局域网内的手机或电脑浏览器访问这个 IP 即可。整个过程不需要安装 App,不需要复杂的协议组包,对新手很友好。
4.3 从开关到状态机:网页控制的进阶思路
上面这个例子重在理解基础闭环。如果你想控制的不只是 LED,而是想让网页变成一个完整控制面板,例如支持 PWM 调光、显示传感器温度曲线,那么建议把页面设计思想和后端数据处理分开。你可以继续用嵌入式 HTML 字符串,也可以把静态资源放到 LittleFS 文件系统里,然后通过server.serveStatic()来提供页面。后者对于稍微大一点的页面非常友好,尤其当你要引入前端的 CSS、JavaScript 时,建议走文件系统,而不是把一长串字符串塞进 C++ 代码,那会带来转义问题和内存碎片。
另外,HTTP 服务端同时连接的客户端数量有限,所以不要在handleRoot里做耗时操作(比如读取一个需要 2 秒才能稳定的传感器),否则会卡住整个请求队列。比较合理的做法是:传感器数据在 loop 里异步刷新到全局变量,HTTP 回调只管读变量、拼 JSON 或 HTML,实时性完全够用。
5. 再进一步:聊聊 S3 上的轻量 AI 与视觉应用
如果你只是用它做个网页控制开关,其实不需要 S3 这么强的芯片,老一代的 ESP32 就够了。S3 的差异化在于它可以在边缘端跑一些轻量级的人工智能推理。这个方向对很多人来说听着新奇,但要冷静看待,这是一把双刃剑:能力真实存在,天花板也很明确。
5.1 轻量 AI 的算力底座与内存底座
S3 能跑 AI 模型,主要靠的是两样东西。第一是前面提过的向量指令,它有助提升卷积类计算的效率;第二是充足的 PSRAM,因为模型参数、中间缓存、图像帧缓冲都需要内存承载。举个例子,一个老式的 20KB 左右的语音唤醒模型,S3 运行起来绰绰有余;一个 MobileNet 级别的图像分类模型,如果做 INT8 量化,尺寸可能压缩到几百 KB,S3 也能勉强处理。但如果你希望在上面流畅运行 YOLOv5 做实时目标检测,那基本还是别抱期待,出帧率会低到令人绝望。
5.2 图像分类 Demo 的实操链路
想快速体验的话,比较成熟的路线是:先用平台的图形化训练工具做一个手势识别或物品分类模型,导出为 TensorFlow Lite 格式,再用工具将其转换成适合 S3 的项目格式,最后把模型文件放进板子的文件系统,借助第三方库在 Arduino 环境里加载模型,对摄像头采集的画面做推理。
整个过程涉及三个外部服务,但逻辑链条很清晰:数据采集与标注、模型训练与量化、部署与运行。以我的经验,最耗时也最容易出问题的反而不是模型推理本身,而是在数据采集环节,光照一致性、背景复杂度、类别均衡都极大影响模型最终效果。很多初学者在自己电脑上训练时准确率高得吓人,一到板子上跑就拉胯,大概率是训练数据和实际应用场景差距太大。所以如果你的目标是做一个手势识别,请在目标环境里采集数据,最好稍稍做一点数据增强,这对板端推理的效果会有明显改善。
5.3 实际落地的性能边界与取舍
S3 的性能虽然可观,但它定位仍是微控制器。运行一个图像分类模型时,单次推理大概在几百毫秒到一两秒之间,取决于模型大小和输入分辨率。你可以通过降低输入图片分辨率、用灰度图、尽量使用 INT8 量化模型来提速。实时视频流 + 检测框这种效果,更适合在树莓派或手机上去做。换言之,S3 适合做的是一些实时性要求不苛刻且功能相对单一的识别任务,比如“识别到猫就拍照”“识别到人就开始录音”“检测到关键词就亮灯”,而不是一台通用视觉处理单元。
这里也提一个省资源的思路:结合低功耗定时唤醒,每隔几秒采集一次图像做推理,只在有目标出现时触发动作,既能控制功耗又能延长设备寿命,电池供电场景尤其适用。S3 的休眠唤醒深度配置,加上 WiFi 连接管理,让这类小设备可以长期离线巡检。
6. 现场排查实录:高频问题与解决路径
到这一节,我想把真正在实际调试过程中遇到的高发问题整理成速查表。这些问题的共性是:代码看起来没问题,但硬件行为就是不对,排查起来特别费劲。
6.1 上传失败或串口消失:先查线再查驱动
最经常遇到的就是点击上传后,进度条停在“Connecting...”,接着报各种错误。此时第一反应不要是重装驱动,而是做三件事:换一根确认能传输数据的 USB 线,确认电脑识别到的是不是正确的串口号,确认开发板菜单里的 USB Mode 选项是否和实际接口匹配。在 USB CDC 模式下,如果你的代码里改了USB_VID或者快速交替复位,会导致串口反复消失。解决方式是:在上传前按住板上的 BOOT 键,插入 USB 线,之后再点上传,待进度条开始运行后再松开 BOOT。这种手动强制进入下载模式的做法虽然老派,但非常管用。
6.2 PSRAM 显示启用,运行却崩溃
很多开发板引脚在 Arduino 内核适配里不一定完全覆盖,特别是在ESP32S3 Dev Module这种通用板卡定义下,有个别引脚默认状态不确定。当你开启 PSRAM 后跑大型程序,如果出现随机崩溃或重启,先检查电源供电是否足够。S3 在跑 Wi-Fi 和摄像头时的电流峰值不小,如果用电脑 USB 口供电,或者用的充电线质量较差,很容易电压跌落导致反复重启。建议改用质量好的 USB 电源,或者用外部 5V 供电,确保有足够的电流余量。另外,注意排查你的电路里有没有把某些关键引脚外部拉低或拉高,例如 GPIO0 和 GPIO46 会影响启动模式,如果这些引脚被外部设备占用导致无法进入正常启动状态,也会表现为“上电重启、工作不稳定”。
6.3 内存碎片与堆溢出:一种隐蔽的故障源
S3 的 PSRAM 虽然很大,但 Arduino 库在默认堆栈分配上不一定都走 PSRAM。许多网络库和字符串操作默认使用内部 SRAM,而内部 SRAM 相对较小,因此长时间运行后可能出现堆碎片化,导致 WiFi 连接失败、DNS 解析异常等问题。如果你发现板子刚开始一切正常,连续运行几小时后变得卡顿或断连,可以考虑在setup()里调用ESP.getHeapSize()和ESP.getMaxAllocHeap()观察内部 RAM 的使用趋势。必要时,把大的缓冲区改为ps_malloc()从 PSRAM 分配,而关键的实时性操作仍留在内部 SRAM 中,以保持稳定。
下面是一段简单检查内存的示例:
void printMem() { Serial.printf("Total heap: %u\n", ESP.getHeapSize()); Serial.printf("Free heap: %u\n", ESP.getFreeHeap()); Serial.printf("Max alloc heap: %u\n", ESP.getMaxAllocHeap()); Serial.printf("Total PSRAM: %u\n", ESP.getPsramSize()); Serial.printf("Free PSRAM: %u\n", ESP.getFreePsram()); }当你渐渐对 S3 上手的实际情况形成一个整体判断后,如果让我用一句话总结对这块板子的印象,那就是:它让微控制器领域的“入门玩具”和“能干活的原型机”之间的边界模糊了一大截。单从开发效率来看,S3 配 Arduino,起到的作用可能比一些需要额外折腾 Linux 环境的卡片式电脑还高,尤其对于小体积、低功耗、需要无线连接的设备原型验证,称得上非常合适。
最后说个我实际反复确认过的小技巧:在项目的早期阶段就应该用一个 GPIO 分配表整理好引脚用途,特别是那些有特殊功能的引脚,比如连接 PSRAM 的引脚、启动模式引脚、ADC 输入引脚。S3 的引脚复用很强,但并不意味着你可以随意安排所有外设位置。某些外设(比如 LCD 接口、摄像头接口)如果占用了启动关键引脚,会导致后续调试时各种奇怪问题;提前规划、在 GPIO 分配上留出余地,能省下日后大量排查时间。如果你刚开始调试 S3,还卡在某个奇怪问题上,建议从供电、USB 线和引脚占用这三个最基本的方向查起,百分之八十的问题都出在这几个环节。