
从 0 到 1 让 ESP32-S3 跑 JavaScript:Moddable SDK 与 xsbug 调试初体验
从 0 到 1 让 ESP32-S3 跑 JavaScript:Moddable SDK 与 xsbug 调试初体验
这篇记录的是一个最小可运行的 Moddable 工程搭建过程:在 Windows + VS Code 环境下,使用 Moddable SDK 编写 JavaScript,编译并烧录到 ESP32-S3,最后通过 GPIO46 实现 LED 闪烁。
工程目标很明确:先把环境跑通,再从最基础的“点灯”开始验证整条链路。
Moddable 是什么
Moddable 是一个面向嵌入式设备的 JavaScript 开发平台。它的核心是 XS JavaScript 引擎,配套提供了构建工具、硬件访问模块、调试器和各类设备平台适配。
传统 ESP32 开发通常直接写 C/C++,使用 ESP-IDF 的 CMake/Ninja 工具链完成编译和烧录。Moddable 并不是替代 ESP-IDF 的底层能力,而是在 ESP-IDF 之上增加了一层 JavaScript 应用开发模型:
- 用 JavaScript 编写业务逻辑。
- 用
manifest.json描述应用依赖和入口模块。 - 用
mcconfig生成并驱动底层工程构建。 - 在 ESP32 平台上继续依赖 ESP-IDF、CMake、Ninja 和芯片工具链。
- 通过
xsbug支持 JavaScript 级别的调试。
所以,使用 Moddable 开发 ESP32-S3,本质上还是会走 ESP-IDF 的构建体系。只是我们日常操作的入口从 idf.py build/flash 变成了 mcconfig。
本工程目标
本工程是一个 ESP32-S3 的最小点灯示例:
- 目标芯片:ESP32-S3。
- LED 引脚:GPIO46。
- 开发语言:JavaScript。
- SDK:Moddable SDK。
- 底层工具链:ESP-IDF v6.0。
- 开发工具:VS Code。
- 烧录端口:优先自动检测,当前设备曾识别为
COM9。
当前工程已经开源,仓库地址:
https://github.com/Moyaoyyy/LED_ESP32_JS
如果要在本机复现,可以直接克隆工程:
git clone https://github.com/Moyaoyyy/LED_ESP32_JS
本次开发时,由于本机工程曾放在带空格的目录下(作者早年的傻逼设计),导致后面不得不专门加了一个路径映射脚本,避免部分构建工具在 Windows 下处理带空格路径时出问题。
目录结构
当前工程的核心文件如下:
LED_ESP32_JS
├─ main.js
├─ manifest.json
├─ README.md
├─ .vscode
│ ├─ settings.json
│ └─ tasks.json
├─ tools
│ ├─ env.cmd
│ ├─ check.cmd
│ ├─ build.cmd
│ ├─ flash.cmd
│ ├─ debug.cmd
│ ├─ clean.cmd
│ └─ run-mapped.cmd
└─ docs
└─ moddable-esp32s3-gpio46-build-blog.md
这里没有创建复杂的应用框架。点灯阶段只保留最小代码和必要的工具脚本,目的是让编译、烧录、调试这些基础链路先稳定下来。
安装 Moddable SDK
Moddable SDK 按要求放在 D 盘根目录:
D:\moddable
获取方式是从官方仓库克隆:
git clone https://github.com/Moddable-OpenSource/moddable D:\moddable
为了让命令行可以找到 Moddable 工具,需要设置 MODDABLE:
set MODDABLE=D:\moddable
实际工程里没有完全依赖系统全局环境变量,而是在 tools\env.cmd 里做了兜底:
if not defined MODDABLE set "MODDABLE=D:\moddable"
这样即使 VS Code 终端没有继承到全局变量,任务脚本也能找到 Moddable SDK。
ESP-IDF 环境
如果电脑上还没有 ESP-IDF,需要先安装 ESP-IDF,再回到这个工程执行编译和烧录。
原因是 Moddable 在 ESP32-S3 上并不是脱离 ESP-IDF 单独工作的。JavaScript 代码最终要被打包进 ESP32-S3 固件里,而固件构建、芯片工具链、CMake/Ninja、分区表、bootloader、烧录工具和串口/JTAG 调试能力都来自 ESP-IDF。简单说:
- Moddable 负责 JavaScript 运行时、模块系统和
mcconfig构建入口。 - ESP-IDF 负责 ESP32-S3 的底层 SDK、交叉编译器、CMake/Ninja、烧录工具和芯片支持。
- 没有 ESP-IDF,
mcconfig -p esp32/esp32s3_cdc就无法真正生成和烧录 ESP32-S3 固件。
没装 ESP-IDF 时怎么安装
ESP-IDF v6.0 及以上版本在 Windows 上推荐使用 ESP-IDF 安装管理器,也就是 EIM。最省事的方式是安装 GUI 版本:
winget install Espressif.EIM
如果更习惯命令行,也可以安装 CLI 版本:
winget install Espressif.EIM-CLI
安装好 EIM 后,推荐按下面步骤做:
- 打开
ESP-IDF 安装管理器。 - 选择
新安装。 - 如果只是普通 ESP-IDF 项目,可以用简易安装;本工程建议走自定义安装,选择 ESP-IDF
v6.0。 - 安装路径建议使用不带空格、不带中文的短路径,例如:
C:\esp\v6.0\esp-idf
- 安装完成后,打开 EIM 创建的 IDF 终端,或者在 VS Code ESP-IDF 插件里选择刚安装的 ESP-IDF。
如果使用 EIM CLI,可以先进入交互式安装向导:
eim wizard
如果版本列表里能看到 v6.0,也可以直接指定版本安装:
eim install -i v6.0
安装完成后,确认环境是否正常:
idf.py --version
where idf.py
如果已经手动克隆了 ESP-IDF,或者使用的是传统安装方式,也可以在 ESP-IDF 目录下安装 ESP32-S3 所需工具:
cd C:\esp\v6.0\esp-idf
install.bat esp32s3
export.bat
PowerShell 下对应命令是:
cd C:\esp\v6.0\esp-idf
.\install.ps1 esp32s3
.\export.ps1
这里的 install 会安装交叉编译工具链、CMake/Ninja、OpenOCD、esptool 以及 ESP-IDF 需要的 Python 虚拟环境;export 会把这些工具加入当前终端的环境变量。注意 export 只对当前终端会话生效,所以 VS Code 任务里才会专门调用 tools\env.cmd 自动激活 ESP-IDF。
本机已经安装 ESP-IDF,并且 VS Code ESP-IDF 插件也已经激活。当前配置记录在 .vscode\settings.json:
{
"idf.currentSetup": "C:\\esp\\v6.0\\esp-idf",
"idf.portWin": "COM9",
"idf.openOcdConfigs": [
"board/esp32s3-builtin.cfg"
]
}
ESP32-S3 对应的 ESP-IDF 路径是:
C:\esp\v6.0\esp-idf
需要注意的是,Moddable 仍然会调用 ESP-IDF 的 export.bat 和 idf.py。这里看到 Python 并不表示工程变成了 Python 项目,idf.py 只是 ESP-IDF 官方提供的构建入口,它底层仍然会驱动 CMake 和 Ninja。
工程脚本里通过 tools\env.cmd 自动完成 ESP-IDF 环境激活:
call "%IDF_PATH%\export.bat" >nul
如果 IDF_PATH 没有提前设置,脚本会优先读取 .vscode\settings.json 里的 idf.currentSetup,再回退到 C:\esp\v6.0\esp-idf。
Moddable 应用清单
Moddable 工程的入口不是 CMakeLists,而是 manifest.json。本工程的清单很短:
{
"include": [
"$(MODDABLE)/examples/manifest_base.json",
"$(MODULES)/pins/digital/manifest.json"
],
"modules": {
"*": "./main"
}
}
这里做了三件事:
- 引入 Moddable 的基础应用配置。
- 引入
pins/digital,用于操作 GPIO。 - 指定当前应用入口模块为
main.js。
这也是 Moddable 工程比较典型的组织方式:应用代码尽量简单,平台和模块依赖写在 manifest 里。
点灯代码
当前 main.js 直接操作 GPIO46:
import Timer from "timer";
import Digital from "pins/digital";
const LED_PIN = 46;
const BLINK_INTERVAL = 500;
const led = new Digital(LED_PIN, Digital.Output);
let value = 0;
debugger;
led.write(value);
Timer.repeat(() => {
value ^= 1;
led.write(value);
}, BLINK_INTERVAL);
代码逻辑很直接:
Digital用来把 GPIO46 配置为输出。Timer.repeat每 500ms 执行一次回调。value ^= 1在 0 和 1 之间翻转。led.write(value)把当前电平写到 GPIO46。
debugger; 是给 xsbug 调试用的断点入口。如果只想烧录运行,可以删除这一行;如果要验证 JavaScript 调试链路,可以保留。
VS Code 任务设计
为了日常使用方便,工程把常用操作都收敛到了 VS Code 任务里:
环境检查编译烧录调试清除
.vscode\tasks.json 只负责调用脚本,不把复杂逻辑直接写在 task 里:
{
"label": "编译",
"type": "shell",
"command": "tools\\build.cmd",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": []
}
这样做的好处是任务名保持简洁,真正的环境处理、路径处理和平台选择都放进 tools 脚本,后续维护更方便。
环境检查任务
tools\check.cmd 用来确认当前终端是否能找到关键工具:
@echo off
call "%~dp0env.cmd" || exit /b %ERRORLEVEL%
echo MODDABLE=%MODDABLE%
echo IDF_PATH=%IDF_PATH%
if defined UPLOAD_PORT (echo UPLOAD_PORT=%UPLOAD_PORT%) else (echo UPLOAD_PORT=auto)
where mcconfig
where idf.py
where nmake
exit /b %ERRORLEVEL%
这个任务重点检查:
MODDABLE是否正确。IDF_PATH是否正确。- 是否设置了
UPLOAD_PORT。 mcconfig、idf.py、nmake是否能被找到。
如果设备端口没有固定指定,脚本会显示:
UPLOAD_PORT=auto
也就是让 ESP-IDF 自动检测串口。
编译任务
编译脚本是 tools\build.cmd:
@echo off
call "%~dp0env.cmd" || exit /b %ERRORLEVEL%
call "%~dp0run-mapped.cmd" mcconfig -dn -m -p esp32/esp32s3_cdc -t build
exit /b %ERRORLEVEL%
关键命令是:
mcconfig -dn -m -p esp32/esp32s3_cdc -t build
参数含义:
-d:使用 debug 构建。-n:不启动调试器。-m:执行 make/build。-p esp32/esp32s3_cdc:目标平台选择 ESP32-S3 CDC。-t build:只执行构建,不烧录、不启动调试器。
这里选择 esp32/esp32s3_cdc,是因为当前 ESP32-S3 设备使用 USB CDC/JTAG 链路更合适。
烧录任务
烧录脚本是 tools\flash.cmd。它做了三层尝试:
@echo off
call "%~dp0env.cmd" || exit /b %ERRORLEVEL%
echo Trying ESP32-S3 CDC with automatic port detection...
call "%~dp0run-mapped.cmd" mcconfig -dn -m -p esp32/esp32s3_cdc -t build
if not errorlevel 1 call "%~dp0run-mapped.cmd" mcconfig -dn -m -p esp32/esp32s3_cdc -t deploy
if not errorlevel 1 exit /b 0
echo CDC failed. Trying ESP32-S3 UART...
call "%~dp0run-mapped.cmd" mcconfig -dn -m -p esp32/esp32s3 -t build
if not errorlevel 1 call "%~dp0run-mapped.cmd" mcconfig -dn -m -p esp32/esp32s3 -t deploy
if not errorlevel 1 exit /b 0
echo UART failed. Trying ESP32-S3 USB...
call "%~dp0run-mapped.cmd" mcconfig -dn -m -p esp32/esp32s3_usb -t build
if not errorlevel 1 call "%~dp0run-mapped.cmd" mcconfig -dn -m -p esp32/esp32s3_usb -t deploy
exit /b %ERRORLEVEL%
优先级是:
esp32/esp32s3_cdcesp32/esp32s3esp32/esp32s3_usb
烧录没有强制写死 COM9,而是优先让 ESP-IDF 自动检测。如果自动检测失败,再考虑设置:
set UPLOAD_PORT=COM9
这里还有一个细节:烧录任务没有直接使用 mcconfig 的默认目标,而是拆成了 -t build 和 -t deploy。这样可以避免默认流程在烧录后尝试启动 xsbug,导致没有调试器时烧录任务失败。
调试任务
调试脚本是 tools\debug.cmd:
@echo off
call "%~dp0env.cmd" || exit /b %ERRORLEVEL%
if not exist "%MODDABLE%\build\bin\win\release\xsbug.exe" (
echo xsbug.exe not found: %MODDABLE%\build\bin\win\release\xsbug.exe
echo Debug task is unavailable until the Moddable xsbug tool is built.
exit /b 1
)
call "%~dp0run-mapped.cmd" mcconfig -d -m -p esp32/esp32s3_cdc
exit /b %ERRORLEVEL%
调试任务依赖:
D:\moddable\build\bin\win\release\xsbug.exe
如果 xsbug.exe 不存在,脚本会直接提示,而不是让 Windows 弹出“找不到文件”的错误。
当 xsbug.exe 已经构建好时,执行:
mcconfig -d -m -p esp32/esp32s3_cdc
这会使用 debug 构建并启动 Moddable 的 JavaScript 调试器。main.js 里的 debugger; 可以作为调试入口断点。
清除任务
清除脚本是 tools\clean.cmd:
@echo off
call "%~dp0env.cmd" || exit /b %ERRORLEVEL%
if exist build (
echo Removing workspace build directory...
rmdir /s /q build
) else (
echo Workspace build directory not found.
)
call "%~dp0run-mapped.cmd" mcconfig -dn -m -p esp32/esp32s3_cdc -t clean
exit /b %ERRORLEVEL%
它只处理当前工程相关输出,不会删除 D:\moddable\build。
这个边界很重要:D:\moddable\build 里有 Moddable SDK 自己编译出来的工具链,例如 mcconfig.bat、xsbug.exe 等,不应该被工程清理任务误删。
处理带空格路径
本次开发时的本机工程路径是:
E:\Code Warehouse\esp32\led_js
其中 Code Warehouse 中间有空格。为了减少 Windows 批处理、makefile 或底层工具对空格路径的兼容风险,工程加了 tools\run-mapped.cmd。如果你把开源工程克隆到没有空格的路径下,这个脚本通常不会触发映射逻辑。
它的策略是:
- 如果当前路径包含空格,就临时把工程父目录映射到
M:。 - 在映射后的工程目录下执行实际命令。
- 命令结束后取消
M:映射。
核心逻辑如下:
echo "%RUN_DIR%" | find " " >nul
if not errorlevel 1 (
subst M: /d >nul 2>nul
subst M: "%PROJECT_PARENT%"
cd /d "M:\%PROJECT_NAME%"
set "USE_SUBST=1"
)
%*
这一步解决的是 Windows 嵌入式工具链里很常见的一类路径问题。
关键问题和处理过程
1. IDF_PATH 未设置
一开始执行任务时出现:
IDF_PATH is not set.
原因是普通 VS Code 任务终端不一定继承 ESP-IDF 插件激活后的环境变量。
解决办法是把环境激活放进 tools\env.cmd:
- 先读取
.vscode\settings.json的idf.currentSetup。 - 再回退到固定路径
C:\esp\v6.0\esp-idf。 - 最后调用
export.bat激活 ESP-IDF 环境。
2. Python 环境提示
ESP-IDF 激活时会检查 Python 版本和依赖:
Checking python version
Checking python dependencies
这不是说应用要用 Python 写,而是 ESP-IDF 的 idf.py 本身是 Python 工具。最终构建还是 CMake/Ninja 驱动的 ESP-IDF 工程。
3. ESP-IDF dirty 状态
ESP-IDF 目录曾经因为文件有改动,被识别成:
v6.0-dirty
Moddable 对 ESP-IDF 版本检查比较严格,ESP32-S3 要求匹配 ESP-IDF v6.0。这个问题通过恢复 ESP-IDF 目录里的非必要改动解决。
4. 烧录时错误启动 xsbug
烧录阶段曾经出现过类似:
xsbug.exe not found
问题不在烧录本身,而是默认目标链路会尝试进入调试流程。后面把烧录任务改成:
mcconfig -dn -m -p esp32/esp32s3_cdc -t build
mcconfig -dn -m -p esp32/esp32s3_cdc -t deploy
这样烧录只负责编译和下载固件,不依赖 xsbug.exe。
5. 调试器缺失
调试任务需要:
D:\moddable\build\bin\win\release\xsbug.exe
如果这个文件不存在,就需要先构建 Moddable 的 Windows 调试器工具。工程脚本里已经做了检查,缺失时会明确提示。
最终使用方式
在 VS Code 中打开当前工程后,进入:
Terminal > Run Task...
常用流程是:
- 执行
环境检查,确认MODDABLE、IDF_PATH、mcconfig、idf.py、nmake都正常。 - 执行
编译,确认 JavaScript 应用和 ESP-IDF 工程可以正常构建。 - 连接 ESP32-S3 开发板。
- 执行
烧录,脚本会优先自动检测端口。 - 如果要进入 JavaScript 调试,确认
xsbug.exe存在后执行调试。
小结
这次工程的重点不是点亮一个 LED 本身,而是把 Moddable + ESP-IDF + VS Code + ESP32-S3 的基础链路跑通。
当前工程已经完成了这些基础能力:
- Moddable SDK 固定放在
D:\moddable。 - ESP-IDF v6.0 通过工程脚本自动激活。
main.js使用pins/digital直接控制 GPIO46。- VS Code 提供中文任务入口:环境检查、编译、烧录、调试、清除。
- 烧录任务支持自动端口检测,并避免误触发调试器。
- 带空格工程路径通过临时盘符映射规避兼容问题。
后续可以在这个最小点灯工程上继续扩展按键输入、串口日志、Wi-Fi、传感器驱动和更完整的 JavaScript 调试流程。
参考资料
- Moddable SDK:https://github.com/Moddable-OpenSource/moddable
- ESP-IDF Windows 安装指南:https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32/get-started/windows-setup.html
- ESP32-S3 Windows 命令行构建指南:https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32s3/get-started/windows-start-project.html