<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Architecting Life</title>
	<atom:link href="https://xujiwei.com/blog/feed/" rel="self" type="application/rss+xml" />
	<link>https://xujiwei.com/blog</link>
	<description>Just do it</description>
	<lastBuildDate>Sat, 11 Apr 2026 10:44:34 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=5.9.8</generator>
	<item>
		<title>ESP32-S3 小技巧：单口实现同时支持 USB 设备 + 一键烧录</title>
		<link>https://xujiwei.com/blog/2026/04/esp32-auto-flash-with-usbhid/</link>
					<comments>https://xujiwei.com/blog/2026/04/esp32-auto-flash-with-usbhid/#respond</comments>
		
		<dc:creator><![CDATA[Xu Jiwei]]></dc:creator>
		<pubDate>Sat, 11 Apr 2026 10:44:34 +0000</pubDate>
				<category><![CDATA[Electronics]]></category>
		<category><![CDATA[Arduino]]></category>
		<category><![CDATA[ESP32]]></category>
		<category><![CDATA[USBHID]]></category>
		<guid isPermaLink="false">https://xujiwei.com/blog/?p=2156</guid>

					<description><![CDATA[前言 最近很喜欢用 ESP32-S3-Zero 这个开发板，尺寸跟 ESP32- &#8230;<p class="read-more"><a href="https://xujiwei.com/blog/2026/04/esp32-auto-flash-with-usbhid/">继续阅读 &#187;</a></p>]]></description>
										<content:encoded><![CDATA[<h2>前言</h2>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2026/03/esp32-s3-usdhid-with-flash-2.jpg" alt="esp32-s3-usdhid-with-flash-2" /></p>
<p>最近很喜欢用 ESP32-S3-Zero 这个开发板，尺寸跟 ESP32-S3 模块差不多，但是直接集成了 USB Type-C 接口以及相关外围电路，在 DIY 的时候就很方便了。</p>
<p>但是在做 USB HID 设备的时候就发现一个问题：<strong>一旦启用了 USB 设备功能，原来通过 USB/JTAG 直接一键烧录的方式就失效了。</strong></p>
<p>这在调试固件的时候就很麻烦了，每次想烧录固件都得手动按住板子上的 BOOT 按钮，再按 RESET 按钮，才能进下载模式。改一行代码试一下效果，就得按一次 BOOT，开发体验可以说是相当差了。</p>
<p>所以就得想办法：<strong>只有一个 USB 口的情况下，怎么让 USB Device 功能和一键烧录共存？</strong></p>
<hr />
<h2>问题分析</h2>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2026/03/esp32-s3-usdhid-with-flash-3.jpg" alt="esp32-s3-usdhid-with-flash-3" /></p>
<p>ESP32-S3 芯片内置了两个跟 USB 相关的模块：一个是 <strong>USB OTG 控制器</strong>（用于实现 USB Device/Host 功能），另一个是<strong>内置的 USB/JTAG 串行调试模块</strong>（用于烧录和调试）。但关键在于，它们共享同一组 USB D+/D- 物理引脚（GPIO19/GPIO20）。<strong>这两个功能是互斥的，同一时刻只能有一个占用 USB 总线。</strong></p>
<p>芯片默认把这组引脚映射给 USB/JTAG 调试模块，所以开箱状态下通过 USB 就能直接烧录和看日志。但一旦固件里启用了 USB OTG 控制器（比如做 USB HID 设备），引脚就被 OTG 接管了，内置的调试模块自然就没法工作了。PlatformIO 找不到 JTAG 端口，烧录直接失败。</p>
<p>如果板子上有额外的 UART 口，还可以走 UART 烧录。但像 ESP32-S3-Zero 这种只有一个 USB 口的板子，<strong>就只剩手动按 BOOT 按钮上电这一条路了——每次烧录都来一遍，实在太麻烦了。</strong></p>
<hr />
<h2>方案思路</h2>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2026/03/esp32-s3-usdhid-with-flash-4.jpg" alt="esp32-s3-usdhid-with-flash-4" /></p>
<p>在动手之前，我先用 AI 研究了一下现有的烧录触发机制。原生 USB/JTAG 模式下，esptool 走的是 JTAG 专有的 reset 协议，跟我们的场景不同。但在研究过程中发现了一个有意思的东西，esptool.py 日志中会有一个 1200bps 通信过程，搜索了一下发现，<strong>原来Arduino 生态里有一个 1200bps touch 的约定</strong>——以 1200bps 波特率打开串口再关闭，就能触发设备重启进入 bootloader。这个机制最早来自 Arduino Leonardo，后来很多支持 USB 的开发板都沿用了这个惯例。</p>
<p>关键是，Arduino ESP32 框架的 <code>USBCDC</code> 类正好支持这个机制。那思路就很清楚了：<strong>在开启 USB HID 设备的同时，再自己开一个 CDC 串口，复用 1200bps touch 来触发重启进 bootloader。</strong></p>
<p>固件端实现起来相当简单，只需要通过 USBCDC 类启用 USB CDC（虚拟串口）功能，然后调用 <code>enableReboot(true)</code> 来启用 1200bps touch 能力，这样当外部以 1200bps 波特率连接这个串口时，固件会自动重启进入 bootloader 模式。</p>
<p>不过还有一个问题：<strong>使用 USB HID 时，设备在电脑上注册的串口名称跟进入 bootloader 后的不一样。</strong>应用模式下是自定义 VID/PID 对应的 CDC 串口，重启进 bootloader 后变成了 Espressif 的下载端口，名字完全不同。PlatformIO 默认不知道该连哪个，所以还<strong>需要一个 pre-upload 脚本来自动处理这个切换</strong>：找到应用 CDC 端口 → 1200bps touch → 等待 bootloader 端口出现 → 把正确的端口告诉 esptool。</p>
<p>最终效果就是 <code>pio run -t upload</code> 一键完成，不用手动按任何按钮，跟没用 USB Device 之前一样丝滑。</p>
<hr />
<h2>固件代码实现</h2>
<p>固件端要做的事情不多，就是在原有 USB HID 的基础上，加一个 CDC 接口，组成 USB 复合设备：</p>
<pre><code class="language-cpp">#include &quot;USB.h&quot;
#include &quot;USBCDC.h&quot;

USBCDC cdc;

void setup() {
    cdc.enableReboot(true);
    cdc.begin(115200);
    // ... USB HID 初始化
    USB.begin();
}</code></pre>
<p>这里最关键的就是 <code>enableReboot(true)</code> 这一行。它的作用是：当检测到外部以 1200bps 波特率连接这个 CDC 串口时，自动调用 <code>esp_restart()</code> 重启进入 bootloader 模式。烧录工具正是利用这个机制来触发下载模式的。</p>
<p>另外，这个 CDC 串口也可以用来当作普通串口使用，我就用 AI 实现了一个 REPL 交互能力，可以调调参数什么的。</p>
<p>还有一点值得一提：CDC 和 HID 是作为 USB 复合设备共存的，系统会同时识别出一个串口设备和一个 HID 设备，互不影响。</p>
<hr />
<h2>PlatformIO 端配置 + 上传脚本</h2>
<p>固件端准备好了，接下来就是让 PlatformIO 在每次 upload 的时候自动完成&quot;找端口 → 触发重启 → 等 bootloader → 烧录&quot;这一整套流程。</p>
<h3>platformio.ini 配置</h3>
<p>先看 <code>platformio.ini</code> 里需要加什么：</p>
<pre><code class="language-ini">[env:esp32s3]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
extra_scripts = pre:scripts/native_usb_upload.py
build_flags =
    -DAPP_USB_VID=0x1234
    -DAPP_USB_PID=0x5678</code></pre>
<p>几个要点：</p>
<ul>
<li><code>extra_scripts = pre:scripts/native_usb_upload.py</code> —— <code>pre:</code> 前缀表示这个脚本在 upload 动作之前执行。脚本的职责就是找到正确的端口并把芯片切到 bootloader 模式，然后再交给 esptool 去烧录。</li>
<li><code>build_flags</code> 里的 <code>-DAPP_USB_VID</code> 和 <code>-DAPP_USB_PID</code> 要和固件里实际用的 VID/PID 一致。上面的 <code>0x1234</code> / <code>0x5678</code> 只是占位，换成实际固件中使用的值。上传脚本会从这里读取 VID/PID 来匹配串口设备。</li>
</ul>
<h3>上传脚本的工作流程</h3>
<p>脚本的逻辑其实就五步，对应上一节方案思路里讲的那个流程：</p>
<ol>
<li><strong>扫描串口，找应用 CDC 端口</strong> —— 遍历系统当前所有串口，通过 VID/PID 匹配找到固件暴露的那个 CDC 虚拟串口。</li>
<li><strong>检查是否已经有 bootloader 端口</strong> —— 如果系统里已经有一个 VID 为 <code>0x303A</code>（Espressif）的端口，说明芯片已经在 bootloader 模式了，直接跳过后面的步骤。这种情况通常是上次烧录失败后芯片停在了 bootloader 里。</li>
<li><strong>1200bps touch 触发重启</strong> —— 以 1200bps 波特率打开 CDC 端口，设置 DTR/RTS 为低，然后关闭连接。固件检测到这个信号后会自动调用 <code>esp_restart()</code> 重启进入 bootloader。这一步有重试机制，因为端口可能暂时被占用或者响应慢。</li>
<li><strong>轮询等待 bootloader 端口出现</strong> —— 芯片重启后，USB 设备会先消失再重新枚举，出来的就是 bootloader 的端口了。脚本会在超时时间内反复扫描，直到发现新端口。</li>
<li><strong>替换 esptool 参数，开始烧录</strong> —— 把 esptool 的 <code>--before</code> 改成 <code>no-reset</code>（因为我们已经手动触发了重启），<code>--after</code> 改成 <code>watchdog-reset</code>，上传端口指向 bootloader 端口。然后 PlatformIO 正常调用 esptool 完成烧录。</li>
</ol>
<p>整个流程里最容易出问题的就是第 3 步和第 4 步。USB 设备的消失和重新枚举有时候会比较慢，不同操作系统的行为也不太一样。</p>
<hr />
<h2>新的烧录过程日志</h2>
<p>加上前面的配置之后，再次使用 <code>pio run -t upload</code> 命令烧录固件时，可以从日志中看到查找串口、1200bps touch 的过程。</p>
<pre><code class="language-bash">......
CURRENT: upload_protocol = esptool
_before_upload([&quot;upload&quot;], [&quot;.pio/build/esp32s3/firmware.bin&quot;])
Native USB upload: application CDC port is /dev/cu.usbmodemDCB4D916D93C2
Native USB upload: touching /dev/cu.usbmodemDCB4D916D93C2 at 1200 bps
Native USB upload: bootloader port is /dev/cu.usbmodem11301
Looking for upload port...
Using manually specified: /dev/cu.usbmodem11301
Uploading .pio/build/esp32s3/firmware.bin
......</code></pre>
<hr />
<h2>总结</h2>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2026/03/esp32-s3-usdhid-with-flash-5.jpg" alt="esp32-s3-usdhid-with-flash-5" /></p>
<p>整个方案在 macOS + PlatformIO + Arduino 下测试成功了，其他平台可能需要根据实际碰到的问题进行修改。</p>
<p>要改的东西其实不多，固件端加几行 CDC 初始化代码，PlatformIO 端加一个 pre-upload 脚本和两行配置。换来的是 <code>pio run -t upload</code> 一键烧录，不用再反复按 BOOT 按钮了。</p>
<p>理论上这个脚本也是可以在项目间共用的，不过放在以后再慢慢折腾吧。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://xujiwei.com/blog/2026/04/esp32-auto-flash-with-usbhid/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>单片机也能养小龙虾：mimiclaw初体验</title>
		<link>https://xujiwei.com/blog/2026/04/mimiclaw-esp32s3-experience/</link>
					<comments>https://xujiwei.com/blog/2026/04/mimiclaw-esp32s3-experience/#comments</comments>
		
		<dc:creator><![CDATA[Xu Jiwei]]></dc:creator>
		<pubDate>Thu, 02 Apr 2026 12:50:46 +0000</pubDate>
				<category><![CDATA[Electronics]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[ClaudeCode]]></category>
		<category><![CDATA[ESP32]]></category>
		<category><![CDATA[mimiclaw]]></category>
		<category><![CDATA[OpenClaw]]></category>
		<guid isPermaLink="false">https://xujiwei.com/blog/?p=2138</guid>

					<description><![CDATA[单片机也能养小龙虾：mimiclaw初体验 之前 OpenClaw 爆火的时候就 &#8230;<p class="read-more"><a href="https://xujiwei.com/blog/2026/04/mimiclaw-esp32s3-experience/">继续阅读 &#187;</a></p>]]></description>
										<content:encoded><![CDATA[<h1>单片机也能养小龙虾：mimiclaw初体验</h1>
<p>之前 OpenClaw 爆火的时候就在网上看到了一个<strong>可以在 ESP32-S3 上跑的替代版本，也是一个开源项目，叫 mimiclaw</strong>，纯 C 编写，资源需求比 OpenClaw 低很多。</p>
<p>周末的时候稍微花了点时间体验了一下，分享一下感受～</p>
<p>PS. 一开始我一直以为这个项目叫 <strong>mini</strong>claw，后面在终端中一直进目录不能自动补全，才发现是 <strong>mimi</strong>claw <img src="https://s.w.org/images/core/emoji/13.1.0/72x72/1f602.png" alt="😂" class="wp-smiley" style="height: 1em; max-height: 1em;" />。</p>
<hr />
<h2><strong>基本介绍</strong></h2>
<p>项目地址： <a href="https://github.com/memovai/mimiclaw">https://github.com/memovai/mimiclaw</a></p>
<p>mimiclaw 是一个基于 ESP-IDF 开发，可以运行在 ESP32-S3 开发板上的个人 AI 助理项目，支持 OpenAI 以及 Anthropic 的接口，基于 ReAct 循环，内置了一些工具和 SKILL。</p>
<hr />
<h2><strong>使用体验</strong></h2>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2026/03/mimiclaw-esp32s3-experience-2.jpg" alt="mimiclaw-esp32s3-experience-2" /></p>
<h3><strong>基本体验</strong></h3>
<p>基本的体验和 OpenClaw 没有特别大的区别，毕竟本身能力依赖于后端的模型使用哪一个，如果和 OpenClaw 使用同样的模型，在纯对话场景，不会有太大差距。</p>
<p>另外 mimiclaw 每次在使用 AI 处理消息之前，会先回复一个 <code>&#x1f431;mimi is working...</code>，显得更生动的同时，也能知道 Agent 是不是还在线 <img src="https://s.w.org/images/core/emoji/13.1.0/72x72/1f648.png" alt="🙈" class="wp-smiley" style="height: 1em; max-height: 1em;" />。</p>
<h3><strong>配置简单</strong></h3>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2026/03/mimiclaw-esp32s3-experience-3.png" alt="mimiclaw-esp32s3-experience-3" /></p>
<p>相比 OpenClaw 那复杂的 JSON 配置以及完全不可用的 Dashboard，mimiclaw 的配置简单太多了，只需要把 <code>mimiclaw_secrets.h</code> 中的基本参数配置完成就可以了。</p>
<p>甚至于你可以完全不配置，将固件烧录完成后，再通过 mimiclaw 自带的 Web 配置界面或者串口进行各项参数的配置，这相比 OpenClaw 复杂的状态管理，以及动不动起不来的情况，体验实在好太多了。</p>
<h3><strong>功能简单</strong></h3>
<p>和 OpenClaw 那庞大的功能相比，mimiclaw 的功能就相对简单很多，比如只有能配置一个 Agent，只能配置一个 AI Model 等。</p>
<p>另外相比 OpenClaw 跑在电脑上，mimiclaw 跑在单片机上维护相关文件也麻烦不少，虽然也可以让 AI 自己去维护 <img src="https://s.w.org/images/core/emoji/13.1.0/72x72/1f603.png" alt="😃" class="wp-smiley" style="height: 1em; max-height: 1em;" />。</p>
<h3><strong>内置代理支持</strong></h3>
<p>不知道这个项目是不是国人开发的，或者有国人参与了，竟然直接加了代理设置，这样在配置了 OpenAI 或者 Telegram 之类的服务时，也可以无障碍连通。</p>
<h3><strong>内置飞书支持</strong></h3>
<p>跟上面一样，mimiclaw 默认支持飞书作为 Channel，这在不使用 Telegram 的场景下，就非常有用了。</p>
<h3><strong>默认不支持修改 API Endpoint</strong></h3>
<p>相比上面两个方便的地方，mimiclaw 不能修改 API Endpoint 这点就不太方便了，毕竟现在太多国产模型又便宜又好用。</p>
<p>不过这个问题也不大，要么把硬编码的 API 地址改掉，要么召唤 AI 来加个配置项就好了。</p>
<h3><strong>常见工具都有</strong></h3>
<p>mimiclaw 内置了常见的文件操作、定时任务、搜索工具，另外作为跑在单片机上的 Agent，还可以操作 GPIO。</p>
<p>刚好手上的 ESP32-S3 开发板自带了一个 WS2812 LED，顺便让 AI 给他加了个 WS2812 操作工具，可以指挥 AI 来展示彩灯了 <img src="https://s.w.org/images/core/emoji/13.1.0/72x72/1f973.png" alt="🥳" class="wp-smiley" style="height: 1em; max-height: 1em;" />。</p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2026/03/mimiclaw-esp32s3-experience-4.jpg" alt="mimiclaw-esp32s3-experience-4" /></p>
<h3><strong>新 Tool 需要重新编译烧录</strong></h3>
<p>因为固件是编译再烧录的，因此 mimiclaw 里面要加点新 Tool 什么的，就得重新编译再烧录了，这相比 OpenClaw 体验还是会差不少。</p>
<hr />
<h2><strong>架构介绍</strong></h2>
<p>mimiclaw 整体架构还是比较简单的，用来当成</p>
<h3><strong>事件循环</strong></h3>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2026/03/mimiclaw-esp32s3-experience-5-2.jpg" alt="mimiclaw-esp32s3-experience-5" /></p>
<p><strong>内置工具列表</strong></p>
<p>mimiclaw 内置了不少必备工具，像文件操作、目录列举、定时任务管理、GPIO 管理等，也算是满足一个 AI Agent 的基本需求了，另外这里也把我新加的 WS2812 工具列出来了。</p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2026/03/mimiclaw-esp32s3-experience-6.jpg" alt="mimiclaw-esp32s3-experience-6" /></p>
<p>另外有一点需要注意的是，<strong>mimiclaw 默认最大只允许注册 16 个工具，在修改源代码的时候需要看看是不是超过限制了。</strong></p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2026/03/mimiclaw-esp32s3-experience-7.png" alt="mimiclaw-esp32s3-experience-7" /></p>
<hr />
<h2><strong>总结</strong></h2>
<p>mimiclaw 作为一个 AI Agent 项目，我觉得对于想尝试 AI Agent，并且不想花一个 Mac mimi 的钱来养龙虾的人来说，还是不错的，特别是如果手里已经有 ESP32-S3 开发板的嵌入式爱好者，体验起来就更方便了。</p>
<p>但是毕竟是跑在单片机上的 Agent，能力受限于单片机的资源、能力，整体功能还是相当有限的，建议有条件还是玩一玩跑在电脑上的 OpenClaw。</p>
<p><strong>不过我觉得拿 mimiclaw 作为一个学习 AI Agent 开发模式的项目很合适～</strong></p>
]]></content:encoded>
					
					<wfw:commentRss>https://xujiwei.com/blog/2026/04/mimiclaw-esp32s3-experience/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>使用 ESP32-S3 模块时两个小坑别踩到</title>
		<link>https://xujiwei.com/blog/2026/04/esp32s3-two-pitfalls/</link>
					<comments>https://xujiwei.com/blog/2026/04/esp32s3-two-pitfalls/#respond</comments>
		
		<dc:creator><![CDATA[Xu Jiwei]]></dc:creator>
		<pubDate>Thu, 02 Apr 2026 12:49:25 +0000</pubDate>
				<category><![CDATA[Electronics]]></category>
		<category><![CDATA[ESP32]]></category>
		<category><![CDATA[SPI]]></category>
		<category><![CDATA[电子DIY]]></category>
		<guid isPermaLink="false">https://xujiwei.com/blog/?p=2134</guid>

					<description><![CDATA[前言 之前已经用 ESP32-S3 模块做了不少项目，会经常使用到 SPI 总线 &#8230;<p class="read-more"><a href="https://xujiwei.com/blog/2026/04/esp32s3-two-pitfalls/">继续阅读 &#187;</a></p>]]></description>
										<content:encoded><![CDATA[<h2>前言</h2>
<p>之前已经用 ESP32-S3 模块做了不少项目，会经常使用到 SPI 总线来驱动外设，想起来有两个坑需要注意，这里记录一下供大家参考～</p>
<p>两个坑都跟 GPIO 使用有关系：</p>
<ol>
<li><strong>N16R8 版本的 ESP32-S3 模块，GPIO 35/36/37 被内部 PSRAM 占用，绝对不能拿来做 SPI</strong></li>
<li><strong>SPI 只有走 IOMUX 固定引脚才能跑到 80MHz；一旦用 GPIO Matrix 重映射，最高只有 40MHz</strong></li>
</ol>
<hr />
<h2>坑一：N16R8 的 GPIO 35/36/37 不能碰</h2>
<h3>N16R8 是什么</h3>
<p>ESP32-S3 有很多封装版本，N16R8 的意思是：板载 <strong>16MB Flash + 8MB PSRAM</strong>。听起来很香，内存大、Flash 大，DIY 项目首选。</p>
<p>但它有个代价：片内的 Octal Flash 和 Octal PSRAM 需要占用更多 GPIO 引脚。普通版本（比如 N8）只占用 GPIO 27-32，而 <strong>N16R8 额外还要占用 GPIO 35、36、37</strong>。</p>
<p>文档中说明如下：</p>
<blockquote>
<p>SPI0/1：GPIO26-32 通常用于 SPI flash 和 PSRAM，不推荐用于其他用途。当使用八线 flash 或八线 PSRAM 或同时使用两者时，GPIO33~37 会连接到 SPIIO4 ~ SPIIO7 和 SPIDQS。因此，对于内嵌 ESP32-S3R8 或 ESP32-S3R8V 芯片的开发板，GPIO33~37 也不推荐用于其他用途。</p>
</blockquote>
<h3>EDA 中要注意</h3>
<p>在 LCEDA 中直接使用 N16R8 模块器件时，并不会额外标明这几个引脚不能使用，因此在设计原理图时需要特别注意，不要把这几个引脚用来当作普通 GPIO 或者 SPI 总线的引脚来使用。</p>
<h3>解决方案</h3>
<p><strong>绕开 35/36/37，改用其他 GPIO。</strong> </p>
<p>选 pin 时可以参考这个范围：<strong>GPIO 0-21</strong>、<strong>GPIO 38-48</strong>。</p>
<p><strong>部分引脚是 Strapping 管脚，同样需要注意</strong>，建议使用前先仔细阅读数据手册是否有影响。</p>
<hr />
<h2>坑二：GPIO Matrix 把速度砍了一半</h2>
<p>ESP32-S3 相比其他 MCU 最方便的就是外设的 IO 可以随意使用，不是固定外设与固定引脚配对，但是这里也就会有一个小坑。</p>
<h3>两种 SPI 信号路由方式</h3>
<p>ESP32-S3 的 SPI 信号有两种走法：</p>
<table>
<thead>
<tr>
<th>路由方式</th>
<th>最高速度</th>
<th>特点</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>IOMUX</strong>（固定引脚）</td>
<td><strong>80 MHz</strong></td>
<td>直连，延迟低，速度满</td>
</tr>
<tr>
<td><strong>GPIO Matrix</strong>（任意引脚）</td>
<td><strong>40 MHz</strong></td>
<td>灵活，但经过矩阵有额外延迟</td>
</tr>
</tbody>
</table>
<p>ESP32-S3 的 SPI2（FSPI）有一组固定的 IOMUX 引脚：</p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2026/04/esp32s3-two-pitfalls-1.png" alt="" /></p>
<p>只要你把 SPI 脚分配到<strong>这几个固定引脚</strong>，硬件自动走 IOMUX，可以跑满 <strong>80 MHz</strong>。</p>
<p>一旦你换了任何一个脚，哪怕只换了一个 CS，信号就全部绕道 GPIO Matrix，<strong>整组 SPI 速度上限跌到 40 MHz</strong>。</p>
<h3>踩坑现象</h3>
<p>代码里写的是 <code>SPI.setFrequency(80000000)</code>，但用逻辑分析仪一量，CLK 实际频率 <strong>40 MHz</strong>，少了整整一半。</p>
<p>在之前测试 JPEG 解码（<a href="https://mp.weixin.qq.com/s/W62uBb48EffFaF2C8o49mA">ESP32-S3 JPEG 实测：从解码库对比到 DMA 优化，最终到达 SPI 上限</a>）中也说到，<strong>因为用了 GPIO Matrix，SPI 刷屏速度最高也只能 43 FPS。</strong></p>
<h3>怎么确认走的是哪种路由</h3>
<p>方法一：<strong>看引脚分配</strong>，对照 SPI2 的 IOMUX 表，每个脚都在固定位置才是 IOMUX。</p>
<p>方法二：<strong>用逻辑分析仪实测</strong>，设 80MHz，实际量出来 40MHz 就说明走了 Matrix。</p>
<p>方法三：查 ESP-IDF 日志，驱动初始化时会打印使用的模式（需要开 debug 级别日志）。</p>
<hr />
<h2>怎么避开这两个坑</h2>
<p>总结一下选 pin 的原则：</p>
<ul>
<li><strong>用 N16R8 时，GPIO 35/36/37 列为禁区</strong>，原理图里直接加上非连接标记</li>
</ul>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2026/04/esp32s3-two-pitfalls-2.png" alt="" /></p>
<ul>
<li><strong>SPI 想跑 80MHz，必须全部用 IOMUX 固定引脚</strong>，一个都不能换；如果板子布线不允许，就接受 40MHz 的上限</li>
</ul>
<hr />
<h2>后续打算</h2>
<p>之前测试 JPEG 解码是用了以前做好的开发板，后面手动连接到固定引脚，看看在 80Mbps 下刷屏速度能提升到多少。</p>
<hr />
<h2>参考资料</h2>
<ul>
<li><a href="https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32s3/api-reference/peripherals/spi_master.html">https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32s3/api-reference/peripherals/spi_master.html</a></li>
<li><a href="https://docs.espressif.com/projects/esp-idf/zh_CN/v5.0/esp32s3/api-reference/peripherals/gpio.html">https://docs.espressif.com/projects/esp-idf/zh_CN/v5.0/esp32s3/api-reference/peripherals/gpio.html</a></li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://xujiwei.com/blog/2026/04/esp32s3-two-pitfalls/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>ESP32-S3 自制游戏机固件升级记：更新官方最新版 + 中文显示支持</title>
		<link>https://xujiwei.com/blog/2026/04/retro-go-cn-font-support/</link>
					<comments>https://xujiwei.com/blog/2026/04/retro-go-cn-font-support/#respond</comments>
		
		<dc:creator><![CDATA[Xu Jiwei]]></dc:creator>
		<pubDate>Thu, 02 Apr 2026 12:42:00 +0000</pubDate>
				<category><![CDATA[Electronics]]></category>
		<category><![CDATA[ESP32]]></category>
		<category><![CDATA[GBA]]></category>
		<category><![CDATA[RetroGo]]></category>
		<category><![CDATA[电子DIY]]></category>
		<guid isPermaLink="false">https://xujiwei.com/blog/?p=2131</guid>

					<description><![CDATA[前言 之前用 ESP32-S3 搞了个游戏机（见 成本60元，用ESP32-S3 &#8230;<p class="read-more"><a href="https://xujiwei.com/blog/2026/04/retro-go-cn-font-support/">继续阅读 &#187;</a></p>]]></description>
										<content:encoded><![CDATA[<h2>前言</h2>
<p>之前用 ESP32-S3 搞了个游戏机（见 <a href="https://mp.weixin.qq.com/s/Q-YlbGm2wp7Bk_csI5CLuA">成本60元，用ESP32-S3做个开源<em>游戏机</em>，能玩FC/NES、GameBoy，还有专属彩色PCB</a>，啊好久远，已经是前年了 <img src="https://s.w.org/images/core/emoji/13.1.0/72x72/1f648.png" alt="🙈" class="wp-smiley" style="height: 1em; max-height: 1em;" />），最近又拿出来把玩了一下。</p>
<p>然后想起来之前有人说能不能加中文支持，这样游戏 ROM 可以直接用中文名存在 TF 卡里，刚好现在 AI 这么厉害，完全就可以让 AI 来完成这个开发嘛 <img src="https://s.w.org/images/core/emoji/13.1.0/72x72/1f603.png" alt="😃" class="wp-smiley" style="height: 1em; max-height: 1em;" />，顺便把固件版本更新到 retro-go 的最新版本。</p>
<hr />
<h2>一、升级固件：rebase 到官方最新 dev 分支</h2>
<p>原来的分支是从官方某个较早版本 fork 出来的，本身也修改不多，直接 rebase 来应用到最新的分支上。</p>
<pre><code class="language-bash">git remote add github https://github.com/ducalex/retro-go.git
git fetch github
git checkout esp32s3-st7789v
git rebase github/dev # 当时执行的是 git rebase origin/dev</code></pre>
<p>我在 rebase 的时候还感觉异常顺利，没有碰到冲突，没想到为后面埋下了坑。</p>
<hr />
<h2>二、中文支持：先走了一段弯路</h2>
<p>这里的中文需求是指在 launcher 显示 TF 卡中游戏列表时，能正确渲染中文游戏名。</p>
<p>刚好现在 AI 已经足够强大了，直接让用 AI 自己完成了一套 UTF-8 解析逻辑，再加一个简单的汉字点阵渲染，塞进去跑了一下，调整了几次，最终效果还不错。</p>
<p>打了几次包之后才发现，打出来的镜像文件咋还是 1.1 版本，这才发现，rebase 是基于了自己仓库的 origin/dev，不是官方仓库的 dev 分支 <img src="https://s.w.org/images/core/emoji/13.1.0/72x72/1f602.png" alt="😂" class="wp-smiley" style="height: 1em; max-height: 1em;" />。</p>
<p>重新做了 rebase 之后，发现了一件事：<strong>官方 dev 分支其实早就加了 UTF-8 支持。</strong></p>
<hr />
<h2>三、中文支持：正路其实很短</h2>
<p>既然官方已经处理好了 UTF-8 解析，那缺的就只有两件事：</p>
<ol>
<li><strong>一个包含汉字的字体文件</strong></li>
<li><strong>渲染时的 fallback 逻辑</strong>：如果当前字体里找不到某个字符的 glyph，就去中文字体里找</li>
</ol>
<h3>3.1 准备中文字体</h3>
<p>字体使用了文泉驿正黑，让 AI 生成脚本，转成 retro-go 的字体格式，只保留常用汉字范围（约 6700 个），控制一下 Flash 占用。</p>
<h3>3.2 添加 fallback 渲染</h3>
<p>在 launcher 的字体渲染路径里加了一段逻辑：</p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2026/04/retro-go-cn-font-diff.png" alt="" /></p>
<p>做完这些之后，重新编译、烧录，launcher 就可以显示中文了。</p>
<hr />
<h2>四、最终效果 + 固件下载</h2>
<p>升级完成后，launcher 里的中文游戏名显示正常，在 TF 卡里所有 ROM 使用中文命名都可以正常显示，再也不用每次手动换成拼音了 <img src="https://s.w.org/images/core/emoji/13.1.0/72x72/1f973.png" alt="🥳" class="wp-smiley" style="height: 1em; max-height: 1em;" />。</p>
<p><strong>编译好的固件已经放在 GitHub Release，直接下载烧录即可：</strong></p>
<p><img src="https://s.w.org/images/core/emoji/13.1.0/72x72/1f449.png" alt="👉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://github.com/ohdarling/retro-go/releases/tag/esp32s3-st7789v-chn-1.4x">https://github.com/ohdarling/retro-go/releases/tag/esp32s3-st7789v-chn-1.4x</a></p>
<p><strong>立创开源平台也同步更新了：</strong> <a href="https://oshwhub.com/wandaeda/ji-yu-esp32-s3-de-retrogo-you-xi-ji">https://oshwhub.com/wandaeda/ji-yu-esp32-s3-de-retrogo-you-xi-ji</a></p>
<p>烧录方式和之前一样，用 esptool.py 写入 IMG 文件：</p>
<pre><code class="language-bash">esptool.py -b 460800 write_flash --flash_size detect 0x0 retro-go_1.4x-gba-102-gd6262_esp32s3-ohda</code></pre>
<p>如果想自己编译，分支在 <code>esp32s3-ohda</code>，使用以下命令编译即可：</p>
<pre><code>./rg_tool.py build-img --target=esp32s3-ohda</code></pre>
<hr />
<h2>参考资料</h2>
<ul>
<li>上一篇：<a href="https://mp.weixin.qq.com/s/Q-YlbGm2wp7Bk_csI5CLuA">成本60元，用ESP32-S3做个开源<em>游戏机</em>，能玩FC/NES、GameBoy，还有专属彩色PCB</a></li>
<li>retro-go 官方仓库：<a href="https://github.com/ducalex/retro-go">https://github.com/ducalex/retro-go</a></li>
<li>本文固件：<a href="https://github.com/ohdarling/retro-go/releases/tag/esp32s3-st7789v-chn-1.4x">https://github.com/ohdarling/retro-go/releases/tag/esp32s3-st7789v-chn-1.4x</a></li>
<li>文泉驿正黑： <a href="http://wenq.org/wqy2/index.cgi?ZenHei">http://wenq.org/wqy2/index.cgi?ZenHei</a></li>
<li>立创开源项目： <a href="https://oshwhub.com/wandaeda/ji-yu-esp32-s3-de-retrogo-you-xi-ji">https://oshwhub.com/wandaeda/ji-yu-esp32-s3-de-retrogo-you-xi-ji</a></li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://xujiwei.com/blog/2026/04/retro-go-cn-font-support/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>ESP32-S3 JPEG 实测：从解码库对比到 DMA 优化，最终撞上 SPI 上限</title>
		<link>https://xujiwei.com/blog/2026/03/esp32-jpeg-decode-optimize/</link>
					<comments>https://xujiwei.com/blog/2026/03/esp32-jpeg-decode-optimize/#respond</comments>
		
		<dc:creator><![CDATA[Xu Jiwei]]></dc:creator>
		<pubDate>Sun, 15 Mar 2026 12:28:36 +0000</pubDate>
				<category><![CDATA[Electronics]]></category>
		<category><![CDATA[Arduino]]></category>
		<category><![CDATA[Benchmark]]></category>
		<category><![CDATA[ESP32]]></category>
		<category><![CDATA[JPEG]]></category>
		<guid isPermaLink="false">https://xujiwei.com/blog/?p=2108</guid>

					<description><![CDATA[前言 之前做了 ESP32-S3 上常见 JPEG 解码库（Tjpg_Decod &#8230;<p class="read-more"><a href="https://xujiwei.com/blog/2026/03/esp32-jpeg-decode-optimize/">继续阅读 &#187;</a></p>]]></description>
										<content:encoded><![CDATA[<h2>前言</h2>
<p>之前做了 ESP32-S3 上常见 JPEG 解码库（Tjpg_Decoder、JPEGDEC、ESP_NEW_JPEG）的性能测试，最终结论是 ESP_NEW_JPEG 在此前测试中表现最强（见 <a href="https://mp.weixin.qq.com/s/cSaqb_5Q6BvhtLr2ifkcbw">ESP32-S3 + Arduino 各种 JPEG 解码库速度对比，到底哪个才是最快的？</a>、<a href="https://mp.weixin.qq.com/s/-qAOWthe8w_qdWJ88EZOeg">勘误：ESP_NEW_JPEG 更新到最新版后，所有分辨率都是最快的</a>）。</p>
<p>但是<strong>上次是用的静态图片</strong>，每次解码都是同样图片数据，与真实使用场景会有差异，毕竟如果是使用 ESP32-S3 播放视频，每一帧的内容都是不一样的，解码时间也会有波动。</p>
<p>这次直接从视频里抽帧，模拟一下真实视频播放场景，看看在这个条件下能把 FPS 推到多高。</p>
<p>另外上次测试时也没有优化解码与传输步骤，<strong>理论上可以使用 DMA 实现 CPU 时间更有效的利用</strong>，这次也优化一下代码，对比看看效果。</p>
<p><strong>那么这次测试在以下两个条件下，最终结果会如何？</strong></p>
<ol>
<li>图片数据使用真实视频抽帧，每帧内容不同，更接近播放场景；</li>
<li>加入 DMA 和 Block 合并策略，测试“解码”和“上屏”能否真正并行。</li>
</ol>
<p>先说结论：<strong>在 240×240 + RGB565 + SPI 40MHz 的配置下，最终逼近链路上限，实测约 40 FPS，再往上提升空间很小了 <img src="https://s.w.org/images/core/emoji/13.1.0/72x72/1f603.png" alt="😃" class="wp-smiley" style="height: 1em; max-height: 1em;" />。</strong></p>
<hr />
<h2>测试视频</h2>
<h3>最终性能测试视频</h3>
<div style="width: 620px;" class="wp-video"><!--[if lt IE 9]><script>document.createElement('video');</script><![endif]-->
<video class="wp-video-shortcode" id="video-2108-1" width="620" height="349" preload="metadata" controls="controls"><source type="video/mp4" src="https://xujiwei.com/blog/wp-content/uploads/2026/03/ESP32-JPEG-DMA-Demo.mp4?_=1" /><a href="https://xujiwei.com/blog/wp-content/uploads/2026/03/ESP32-JPEG-DMA-Demo.mp4">https://xujiwei.com/blog/wp-content/uploads/2026/03/ESP32-JPEG-DMA-Demo.mp4</a></video></div>
<h3>默认 Block 上屏流程演示</h3>
<div style="width: 620px;" class="wp-video"><video class="wp-video-shortcode" id="video-2108-2" width="620" height="349" preload="metadata" controls="controls"><source type="video/mp4" src="https://xujiwei.com/blog/wp-content/uploads/2026/03/ESP32-JPEG-MCU-Demo.mp4?_=2" /><a href="https://xujiwei.com/blog/wp-content/uploads/2026/03/ESP32-JPEG-MCU-Demo.mp4">https://xujiwei.com/blog/wp-content/uploads/2026/03/ESP32-JPEG-MCU-Demo.mp4</a></video></div>
<hr />
<h2>测试方案</h2>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2026/03/esp32-jpeg-decode-optimize-2.jpg" alt="esp32-jpeg-decode-optimize-2" /></p>
<p><strong>测试硬件：</strong></p>
<ul>
<li>ESP32-S3-Zero 开发板，双核 240MHz，开启 PSRAM</li>
<li>屏幕：ST7789 240×240，SPI 接口</li>
<li>PCB 设计时连接屏幕没有使用标准 SPI 引脚，使用了 GPIO Matrix，因此这里 SPI 总线时钟配置 40 MHz</li>
</ul>
<p><strong>测试数据：</strong></p>
<ul>
<li>视频抽帧：2 秒共 <strong>60 帧</strong>，每帧内容各不相同</li>
<li>导出格式：JPEG，质量 80</li>
<li>分辨率：240×240</li>
</ul>
<p><strong>测试模式：</strong></p>
<p><strong>① FULL IMAGE + Sync SPI</strong></p>
<ul>
<li>解码完成后，使用同步 SPI 将像素数据上屏；至于解码输出是分块还是整帧，取决于各解码库自身的默认行为。</li>
<li>单帧最大占用内存：<strong>240×240×2 = 115,200 字节</strong></li>
<li>SPI 阻塞传输，无 DMA</li>
</ul>
<p><strong>② Block + DMA</strong></p>
<ul>
<li>边解码边用 SPI + DMA 推屏</li>
<li>解码库每输出一个 Block，立刻 DMA 传输，解码与传输部分重叠</li>
<li>细分为两种子模式：
<ul>
<li><strong>不合并 Strip</strong>：直接按解码库输出的 Block 尺寸发 DMA</li>
<li><strong>合并 Strip</strong>：把多个 Block 合并成 240×16 的整行再统一发</li>
</ul>
</li>
</ul>
<hr />
<h2>先搞清楚：MCU 是什么</h2>
<p>在看数据之前，有个概念要说明一下——<strong>MCU（Minimum Coded Unit）</strong>，JPEG 编码的最小单元。</p>
<p>为了方便理解，本文讨论的图片在当前采样配置下，<strong>可把 MCU 理解为常见的 16×16 输出块。</strong></p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2026/03/esp32-jpeg-decode-optimize-3.jpg" alt="esp32-jpeg-decode-optimize-3" /></p>
<p>三个库处理 MCU 的方式差别挺大：</p>
<table>
<thead>
<tr>
<th>Decoder</th>
<th>MCU 行为</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Tjpg_Decoder</strong></td>
<td>固定 16×16，任何情况都不合并</td>
</tr>
<tr>
<td><strong>JPEGDEC</strong></td>
<td>根据模式和分辨率计算，可以合并输出更大的 Strip</td>
</tr>
<tr>
<td><strong>ESP_NEW_JPEG</strong></td>
<td>自动合并到 240 宽度（整行输出）</td>
</tr>
</tbody>
</table>
<p>需要注意的是：<strong>JPEGDEC 单次输出 Strip 的尺寸，依赖于内部的 <code>iPitch</code> 参数</strong>，而 iPitch 又受解码库里 <code>MAX_BUFFERED_PIXELS</code>（默认 <strong>2048</strong>）这个上限影响。</p>
<p>对于 240×240 图片，如果想单次输出整行（15 个 MCU），需要约 <strong>15 × 16 × 16 = 3840 像素</strong>的缓冲——如果有这个需求，记得同步修改库里的这个上限值。</p>
<hr />
<h2>测试结果</h2>
<p>整体结果对比如下：</p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2026/03/esp32-jpeg-decode-optimize-4.jpg" alt="esp32-jpeg-decode-optimize-4" /></p>
<h3>一、FULL IMAGE 模式</h3>
<table>
<thead>
<tr>
<th>Decoder</th>
<th>MCU</th>
<th>Blocks</th>
<th>Decode Avg(ms)</th>
<th>Draw Avg(ms)</th>
<th>FPS Avg</th>
</tr>
</thead>
<tbody>
<tr>
<td>Tjpg_Decoder</td>
<td>16×16</td>
<td>225</td>
<td>29.49</td>
<td>51.44</td>
<td>12.36</td>
</tr>
<tr>
<td>JPEGDEC</td>
<td>128×16</td>
<td>30</td>
<td>18.07</td>
<td><strong>28.53</strong></td>
<td>21.46</td>
</tr>
<tr>
<td><strong>ESP_NEW_JPEG</strong></td>
<td>240×240</td>
<td>1</td>
<td><strong>11.91</strong></td>
<td>30.56</td>
<td><strong>23.56</strong></td>
</tr>
</tbody>
</table>
<p>这个模式下，<strong>ESP_NEW_JPEG 解码速度最快（11.91ms）</strong>，JPEGDEC 和 ESP_NEW_JPEG 的上屏时间接近，最终 FPS 都不高，最好也只有 <strong>23.56 FPS</strong>。</p>
<p>原因很明显：FULL IMAGE 模式没有 DMA，SPI 是阻塞的，CPU 在等待 SPI 传输期间什么都做不了，解码再快也拉不开差距。<strong>上屏时间明显高于解码时间，已经成为主要耗时来源。</strong></p>
<p><strong>Tjpg_Decoder 的 Block 数高达 225 个（每帧要回调 225 次）</strong>，虽然解码时间看着正常，但大量小块的调度开销也是累积的。ESP_NEW_JPEG 只需要回调 1 次（整帧），调度开销几乎为零。</p>
<h4><strong>FULL IMAGE 模式小结</strong></h4>
<p>这说明在同步 SPI 模式下，系统瓶颈根本不在 JPEG 解码，而在上屏阻塞；解码器再快，也只能等待 SPI。</p>
<hr />
<h3>二、Block + DMA 不合并 Strip</h3>
<table>
<thead>
<tr>
<th>Decoder</th>
<th>MCU</th>
<th>DMA Send</th>
<th>Decode Avg(ms)</th>
<th>Draw Avg(ms)</th>
<th>FPS Avg</th>
</tr>
</thead>
<tbody>
<tr>
<td>Tjpg_Decoder</td>
<td>16×16</td>
<td>16×16</td>
<td>32.19</td>
<td>27.93</td>
<td>16.63</td>
</tr>
<tr>
<td>JPEGDEC</td>
<td>64×16</td>
<td>64×16</td>
<td>18.55</td>
<td><strong>14.73</strong></td>
<td>30.05</td>
</tr>
<tr>
<td><strong>ESP_NEW_JPEG</strong></td>
<td>240×16</td>
<td>240×16</td>
<td><strong>6.70</strong></td>
<td>17.83</td>
<td><strong>40.78</strong></td>
</tr>
</tbody>
</table>
<p><strong>加了 DMA 之后效果非常明显，ESP_NEW_JPEG 直接跑到了 40.78 FPS <img src="https://s.w.org/images/core/emoji/13.1.0/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /></strong></p>
<p><strong>ESP_NEW_JPEG 解码时间只有 6.70ms</strong>，比 Tjpg_Decoder 快了将近 5 倍。上屏时间 17.83ms 看着多，但这里其实已经是在等 DMA 空闲——SPI 传输能力到顶了。</p>
<p><strong>值得关注的是 JPEGDEC</strong>：解码时间 18.55ms 与 FULL IMAGE 模式几乎相同，但上屏时间从 28.53ms 降到了 <strong>14.73ms</strong>，DMA 传输和解码的并行效果立竿见影。</p>
<p><strong>Tjpg_Decoder 则因为每次只发 16×16 的小块</strong>，DMA 启动次数太多（每帧 225 次），开销反而比 FULL IMAGE 还略高，收益有限。</p>
<h4><strong>Block + DMA 不合并 Strip 小结</strong></h4>
<p>这说明 DMA 的价值不只是“更快传输”，而是让“解码”和“传输”两段流水线重叠起来，CPU 可以更早地进入下一帧的图像解码工作。</p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2026/03/esp32-jpeg-decode-optimize-5.jpg" alt="esp32-jpeg-decode-optimize-5" /></p>
<p>只要 DMA 传输时间不超过解码时间，FPS 就只会受限于解码速度了。</p>
<hr />
<h3>三、Block + DMA 合并 Strip</h3>
<table>
<thead>
<tr>
<th>Decoder</th>
<th>MCU</th>
<th>DMA Send</th>
<th>Decode Avg(ms)</th>
<th>Draw Avg(ms)</th>
<th>FPS Avg</th>
</tr>
</thead>
<tbody>
<tr>
<td>Tjpg_Decoder</td>
<td>16×16</td>
<td>240×16</td>
<td>30.33</td>
<td><strong>2.31</strong></td>
<td>30.65</td>
</tr>
<tr>
<td>JPEGDEC</td>
<td>64×16</td>
<td>240×16</td>
<td>18.27</td>
<td>7.44</td>
<td>38.90</td>
</tr>
<tr>
<td><strong>ESP_NEW_JPEG</strong></td>
<td>240×16</td>
<td>240×16</td>
<td><strong>6.72</strong></td>
<td>17.83</td>
<td><strong>40.72</strong></td>
</tr>
</tbody>
</table>
<p>① 合并 Strip 对 Tjpg_Decoder 的上屏时间影响巨大：从 27.93ms 直接降到 <strong>2.31ms</strong>！DMA 单次传输整行（240×16），减少了大量传输启动开销。</p>
<p>Tjpg_Decoder 合并 Strip 后，上屏时间几乎可以忽略不计，解码时间（30.33ms）才是瓶颈——<strong>整体 FPS 从 16.63 跳到 30.65</strong>，提升将近一倍，说明之前 DMA 频繁启动的开销有多大。</p>
<p><strong>注意：这里 Tjpg_Decoder 的 2.31ms 容易产生误解，实际传输时间并没有消失，而是被流水线重叠后“吸收”进了解码阶段。</strong></p>
<p>② <strong>JPEGDEC 从 14.73ms 降到 7.44ms，FPS 涨到 38.90</strong>，已经非常接近 40 FPS 了。</p>
<p>③ ESP_NEW_JPEG 本来就是整行输出，合不合并对它没影响，依然稳在 <strong>40.72 FPS</strong>。这里的 <strong>17.83ms 上屏时间本质上不是软件额外开销</strong>，而是除去解码时间外 SPI 链路本身的传输耗时。这意味着优化空间已经不在解码侧，而在总线带宽侧。</p>
<h4><strong>Block + DMA 合并 Strip 小结</strong></h4>
<p>这说明当 Block 太碎时，DMA 启动成本会反过来吞噬收益。<strong>合并 Strip 的本质，是减少事务次数，而不是减少数据量。</strong></p>
<hr />
<h2>当前配置下的理论 FPS 上限</h2>
<p>根据前面给出的测试条件：</p>
<ul>
<li>分辨率 240×240</li>
<li>RGB565，2 字节每像素</li>
<li>单帧 115,200 字节</li>
<li>SPI 40MHz</li>
</ul>
<p>可以计算出来理论 FPS 上限：</p>
<pre><code>240 × 240 × 2 = 115,200 Byte/frame
115,200 × 8 = 921,600 bit/frame
40,000,000 / 921,600 ≈ 43.4 frame/s</code></pre>
<p>也就是说，在不考虑命令开销、窗口设置、DMA 调度损耗的理想情况下，上限大约就是 43 FPS。</p>
<p>实测 ESP_NEW_JPEG 跑到 40.7 FPS，已经非常接近 SPI 链路极限。</p>
<hr />
<h2>测试结论</h2>
<p>经过这轮测试，几个结论比较清晰：</p>
<ol>
<li><strong>DMA 影响巨大</strong> — 有没有 DMA，FPS 差距可以超过一倍。不用 DMA 的 FULL IMAGE 模式，再快的解码库也被上屏拖累。</li>
<li><strong>合并 Strip 很重要</strong> — 特别是 Tjpg_Decoder，合并后上屏时间从 27.93ms 暴降到 2.31ms。减少 DMA 启动次数，开销差异极大。</li>
<li><strong>SPI 是最终瓶颈</strong> — ESP_NEW_JPEG 的上屏时间 17.83ms 不是在等解码，是在等 DMA 传输完成。240×240 RGB565 画面，每帧 115,200 字节，40MHz SPI 理论极限就是这个帧率了。再怎么优化解码，SPI 这关过不去。</li>
<li><strong>ESP_NEW_JPEG 综合最强</strong> — 解码时间最短（<strong>6.70ms</strong>），自动整行输出，在有其他任务占 CPU 的情况下优势更明显。</li>
<li><strong>JPEGDEC 有潜力但需要改库</strong> — 想让它输出更大的 Strip，需要手动修改 <code>MAX_BUFFERED_PIXELS</code> 上限，默认 2048 不一定符合实际工作场景。</li>
</ol>
<hr />
<h2>使用建议</h2>
<ul>
<li>DMA + Merge Strip 有一定编码成本，且竞态控制比较复杂，需要权衡利弊后再使用</li>
<li>只做静态图片显示，优先选解码更快、接入更简单的方案</li>
<li>Block 很碎时，一定要想办法<strong>合并 Strip</strong>，否则 DMA 启动成本会吃掉收益</li>
<li>如果已经接近 SPI 理论上限，还有更高 FPS 需求，可以更多从 SPI 带宽/换接口方向考虑</li>
</ul>
<hr />
<h2>后续打算</h2>
<p>目前在这套 SPI 40MHz 配置下，继续提升 FPS 的收益已经很有限了。不过还有几个方向可以研究：</p>
<ul>
<li><strong>双核并行</strong>：如果要做彩色串流小电视，就还有网络任务，看看是否还能达到 40 FPS。</li>
<li><strong>更高 SPI 时钟</strong>：使用 ESP32-S3 原生 SPI 引脚，总线速度理论上可以到 80MHz，FPS 上限可以再提高一些。</li>
<li><strong>视频解码测试</strong>：拿真实视频文件直接测试，播放时 FPS 可以到多少。</li>
</ul>
<p>先这样，下次再折腾 <img src="https://s.w.org/images/core/emoji/13.1.0/72x72/1f602.png" alt="😂" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<h2>参考资料</h2>
<ul>
<li><a href="https://mp.weixin.qq.com/s/cSaqb_5Q6BvhtLr2ifkcbw">ESP32-S3 + Arduino 各种 JPEG 解码库速度对比，到底哪个才是最快的？</a></li>
<li><a href="https://mp.weixin.qq.com/s/-qAOWthe8w_qdWJ88EZOeg">勘误：ESP_NEW_JPEG 更新到最新版后，所有分辨率都是最快的</a></li>
<li><a href="https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/peripherals/spi_master.html#gpio-io-mux">https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/peripherals/spi_master.html#gpio-io-mux</a></li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://xujiwei.com/blog/2026/03/esp32-jpeg-decode-optimize/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>勘误：ESP_NEW_JPEG 更新到最新版后，所有分辨率都是最快的</title>
		<link>https://xujiwei.com/blog/2026/03/esp32-jpeg-decoder-errata/</link>
					<comments>https://xujiwei.com/blog/2026/03/esp32-jpeg-decoder-errata/#respond</comments>
		
		<dc:creator><![CDATA[Xu Jiwei]]></dc:creator>
		<pubDate>Thu, 05 Mar 2026 14:37:04 +0000</pubDate>
				<category><![CDATA[Electronics]]></category>
		<category><![CDATA[Arduino]]></category>
		<category><![CDATA[ESP32]]></category>
		<category><![CDATA[JPEG]]></category>
		<category><![CDATA[电子DIY]]></category>
		<guid isPermaLink="false">https://xujiwei.com/blog/?p=2102</guid>

					<description><![CDATA[前言 上篇文章（见 ESP32-S3 + Arduino 各种 JPEG 解码库 &#8230;<p class="read-more"><a href="https://xujiwei.com/blog/2026/03/esp32-jpeg-decoder-errata/">继续阅读 &#187;</a></p>]]></description>
										<content:encoded><![CDATA[<h2>前言</h2>
<p>上篇文章（见 <a href="https://xujiwei.com/blog/2026/03/esp32-jpeg-decoder-test/">ESP32-S3 + Arduino 各种 JPEG 解码库速度对比，到底哪个才是最快的？</a>）测试了三个 JPEG 解码库的性能，最终结论是 ESP_NEW_JPEG 在低分辨率遥遥领先，但到了 240x240 和 320x240，被 JPEGDEC 追平。</p>
<p>结果最近想测试 ESP_NEW_JPEG 的 <strong>block 解码</strong>方式，发现怎么找都找不到对应的 API，这才意识到——我用的库版本根本不对 <img src="https://s.w.org/images/core/emoji/13.1.0/72x72/1f605.png" alt="😅" class="wp-smiley" style="height: 1em; max-height: 1em;" />。</p>
<h2>问题出在哪里</h2>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2026/03/esp32-jpeg-decoder-errata-2.jpg" alt="esp32-jpeg-decoder-errata-2" /></p>
<p>上篇文章用的是 Arduino 生态里的封装库 <a href="https://github.com/esp-arduino-libs/ESP32_JPEG">ESP32_JPEG</a>，这个库最后一次更新是<strong>三年前</strong>，是对 ESP_JPEG 早期版本的封装，早就停止维护了。</p>
<p>乐鑫官方真正在维护的最新版本是另一个仓库：<a href="https://github.com/espressif/esp-adf-libs/tree/master/esp_new_jpeg">esp-adf-libs/esp_new_jpeg</a>，五个月前还在更新，block 解码、最新的 SIMD 优化都在这里。</p>
<p>两个库同名，但版本差距已经是三年了，之前完全踩坑了 <img src="https://s.w.org/images/core/emoji/13.1.0/72x72/1f602.png" alt="😂" class="wp-smiley" style="height: 1em; max-height: 1em;" />。</p>
<h2>更新库，重新测试</h2>
<p>换成最新版 ESP_NEW_JPEG，测试环境与上次完全一致：</p>
<ul>
<li>ESP32-S3-Zero，240MHz 双核</li>
<li>测试图片：Lenna，160x80 / 240x240 / 320x240，质量 80</li>
<li>每个尺寸解码 10 次取平均</li>
</ul>
<h3>160x80 解码速度</h3>
<table>
<thead>
<tr>
<th>Library</th>
<th style="text-align: right;">Decode(ms)</th>
<th style="text-align: right;">Draw(ms)</th>
<th style="text-align: right;">Total(ms)</th>
<th style="text-align: right;">FPS</th>
</tr>
</thead>
<tbody>
<tr>
<td>Tjpg_Decoder</td>
<td style="text-align: right;">10.74</td>
<td style="text-align: right;">11.48</td>
<td style="text-align: right;">22.23</td>
<td style="text-align: right;">45.0</td>
</tr>
<tr>
<td>JPEGDEC</td>
<td style="text-align: right;">7.42</td>
<td style="text-align: right;">6.91</td>
<td style="text-align: right;">14.33</td>
<td style="text-align: right;">69.8</td>
</tr>
<tr>
<td><strong>ESP32_JPEG</strong></td>
<td style="text-align: right;"><strong>3.75</strong></td>
<td style="text-align: right;">6.17</td>
<td style="text-align: right;"><strong>9.93</strong></td>
<td style="text-align: right;"><strong>100.8</strong></td>
</tr>
</tbody>
</table>
<h3>240x240 解码速度</h3>
<table>
<thead>
<tr>
<th>Library</th>
<th style="text-align: right;">Decode(ms)</th>
<th style="text-align: right;">Draw(ms)</th>
<th style="text-align: right;">Total(ms)</th>
<th style="text-align: right;">FPS</th>
</tr>
</thead>
<tbody>
<tr>
<td>Tjpg_Decoder</td>
<td style="text-align: right;">43.85</td>
<td style="text-align: right;">51.13</td>
<td style="text-align: right;">94.98</td>
<td style="text-align: right;">10.5</td>
</tr>
<tr>
<td>JPEGDEC</td>
<td style="text-align: right;">28.91</td>
<td style="text-align: right;">28.10</td>
<td style="text-align: right;">57.01</td>
<td style="text-align: right;">17.5</td>
</tr>
<tr>
<td><strong>ESP32_JPEG</strong></td>
<td style="text-align: right;"><strong>17.44</strong></td>
<td style="text-align: right;">30.48</td>
<td style="text-align: right;"><strong>47.92</strong></td>
<td style="text-align: right;"><strong>20.9</strong></td>
</tr>
</tbody>
</table>
<h3>320x240 解码速度</h3>
<table>
<thead>
<tr>
<th>Library</th>
<th style="text-align: right;">Decode(ms)</th>
<th style="text-align: right;">Draw(ms)</th>
<th style="text-align: right;">Total(ms)</th>
<th style="text-align: right;">FPS</th>
</tr>
</thead>
<tbody>
<tr>
<td>Tjpg_Decoder</td>
<td style="text-align: right;">57.63</td>
<td style="text-align: right;">68.10</td>
<td style="text-align: right;">125.73</td>
<td style="text-align: right;">8.0</td>
</tr>
<tr>
<td>JPEGDEC</td>
<td style="text-align: right;">38.12</td>
<td style="text-align: right;">38.21</td>
<td style="text-align: right;">76.33</td>
<td style="text-align: right;">13.1</td>
</tr>
<tr>
<td><strong>ESP32_JPEG</strong></td>
<td style="text-align: right;"><strong>23.99</strong></td>
<td style="text-align: right;">39.60</td>
<td style="text-align: right;"><strong>63.59</strong></td>
<td style="text-align: right;"><strong>15.7</strong></td>
</tr>
</tbody>
</table>
<h2>勘误结论</h2>
<p>旧版测试中，240x240 和 320x240 分辨率下 JPEGDEC 几乎追平 ESP_NEW_JPEG，这个结论是错的。</p>
<p>更新到最新版后，<strong>ESP_NEW_JPEG 在所有分辨率下都是三个库里最快的</strong>，差距相当明显：</p>
<ul>
<li><strong>160x80</strong>：ESP_NEW_JPEG 9.93ms / 100.8 FPS，JPEGDEC 14.33ms，领先 <strong>30%</strong></li>
<li><strong>240x240</strong>：ESP_NEW_JPEG 47.92ms / 20.9 FPS，JPEGDEC 57.01ms，领先 <strong>16%</strong></li>
<li><strong>320x240</strong>：ESP_NEW_JPEG 63.59ms / 15.7 FPS，JPEGDEC 76.33ms，领先 <strong>17%</strong></li>
</ul>
<p><strong>更新后的选库建议：</strong></p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2026/03/esp32-jpeg-decoder-errata-3.jpg" alt="esp32-jpeg-decoder-errata-3" /></p>
<ul>
<li><strong>追求最快解码速度</strong> → 用 ESP_NEW_JPEG（最新版），硬件加速 + SIMD，全分辨率最强</li>
<li><strong>需要简单好用、兼容 TFT_eSPI</strong> → 用 JPEGDEC，SIMD 加速后性能也不差</li>
<li><strong>需要 Progressive JPEG 支持</strong> → 只能用 JPEGDEC 或 Tjpg_Decoder，ESP_NEW_JPEG 不支持</li>
</ul>
<p>上篇文章也已经同步更新了测试数据和结论。</p>
<h2>后记</h2>
<p>如果不是想测 block 解码，可能就这么一直用着三年前的旧版本了……以后用 Arduino 封装库之前，还是要确认一下源头仓库是不是真正在维护的那个 <img src="https://s.w.org/images/core/emoji/13.1.0/72x72/1f614.png" alt="😔" class="wp-smiley" style="height: 1em; max-height: 1em;" />。</p>
<p>block 解码的测试结果后面再单独写一篇。</p>
<h2>参考资料</h2>
<ul>
<li><a href="https://github.com/esp-arduino-libs/ESP32_JPEG">https://github.com/esp-arduino-libs/ESP32_JPEG</a></li>
<li><a href="https://github.com/espressif/esp-adf-libs/tree/master/esp_new_jpeg">https://github.com/espressif/esp-adf-libs/tree/master/esp_new_jpeg</a></li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://xujiwei.com/blog/2026/03/esp32-jpeg-decoder-errata/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>真实案例了解了什么是反向电动势</title>
		<link>https://xujiwei.com/blog/2026/03/magent-clock-back-emf/</link>
					<comments>https://xujiwei.com/blog/2026/03/magent-clock-back-emf/#respond</comments>
		
		<dc:creator><![CDATA[Xu Jiwei]]></dc:creator>
		<pubDate>Tue, 03 Mar 2026 16:27:42 +0000</pubDate>
				<category><![CDATA[Electronics]]></category>
		<category><![CDATA[ESP32]]></category>
		<category><![CDATA[电子DIY]]></category>
		<guid isPermaLink="false">https://xujiwei.com/blog/?p=2098</guid>

					<description><![CDATA[之前分享了使用 NMOS 驱动电磁铁通断来实现的段码时钟（见 电磁铁驱动的段码时 &#8230;<p class="read-more"><a href="https://xujiwei.com/blog/2026/03/magent-clock-back-emf/">继续阅读 &#187;</a></p>]]></description>
										<content:encoded><![CDATA[<p>之前分享了使用 NMOS 驱动电磁铁通断来实现的段码时钟（见 <a href="https://mp.weixin.qq.com/s/lRszQwfN-0F50oCY6hb79g">电磁铁驱动的段码时钟找到解法了，先来一位试试</a>），在评论区有朋友指出<strong>电路中电磁铁没有并联续流二极管</strong>，我之前虽然接触过这个概念，但是对于为什么要使用并没有很深刻的概念，并且似乎也没有出问题？</p>
<p>但是这肯定是有问题的，为了学习一下没有续流二极管有什么危害，还是来测试看看。</p>
<h2>原电路设计</h2>
<p>在原来的电路中，电磁铁连接在 12V 和 NMOS 的 Drain 上，通过 Gate 控制 NMOS 的导通，从而接通电磁铁，实现对段码上永磁铁的推动，实现段码的翻转。</p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2025/12/magent-clock-back-emf-1.png" alt="magent-clock-back-emf-1" /></p>
<h2>示波器观察</h2>
<p>在测试时使用 10V 对电磁铁进行供电，并且通过单片机对 NMOS 进行操作，在翻转段码时，对电磁铁进行接通 50ms 的操作。</p>
<p>然后示波器连接在 NMOS 的 Drain 上，观察电压变化。</p>
<p><strong>未接续流二极管电压变化</strong></p>
<p>可以看到在关闭 NMOS 的瞬间，有一个极高的尖刺出现，<strong>示波器显示电压为 37.6V</strong>，这理论上已经超过了 NMOS 的工作电压 <img src="https://s.w.org/images/core/emoji/13.1.0/72x72/1f602.png" alt="😂" class="wp-smiley" style="height: 1em; max-height: 1em;" />。</p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2025/12/magent-clock-back-emf-2.png" alt="magent-clock-back-emf-2" /></p>
<p>放大看可以看到这个电压持续了 300us 再逐渐降低到正常工作电压的 10V。</p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2025/12/magent-clock-back-emf-3.png" alt="magent-clock-back-emf-3" /></p>
<h2>反向电动势是如何产生的？</h2>
<p>顺便向 AI 学习了一下为何会有这个反向电动势：</p>
<blockquote>
<p>核心原理是 <strong>电感中的电流不能突变</strong>（楞次定律）。</p>
</blockquote>
<ul>
<li><strong>导通阶段：</strong> 当NMOS导通时，电流从电源正极，流过电磁铁线圈，再通过导通的NMOS到地，形成回路。线圈（电感）内部储存了磁能，其电流稳定上升至 <code>I = Vcc / R</code>（R为线圈直流电阻）。</li>
<li><strong>断开瞬间：</strong> 突然关闭（关断）NMOS时，回路的电流路径被强行切断。线圈为了维持电流<strong>不会瞬间变为零</strong>，它会“反抗”这种变化。</li>
<li><strong>电压飙升：</strong> 线圈维持电流的唯一方法，就是瞬间在自身两端产生一个非常高的电压。这个电压的<strong>极性</strong>是：<strong>试图保持电流按原方向流动</strong>。在低侧驱动电路中，这意味着线圈的<strong>下端（接MOS管漏极的一端）电压会急剧升高，甚至远高于电源电压Vcc</strong>，而线圈上端（接Vcc的一端）电压则可能被拉低或跟随变化。</li>
</ul>
<h2>优化电路</h2>
<p>优化起来也比较简单，添加一个续流二极管即可，电磁铁线圈电阻为 26R，在 10V 下工作电流为 380mA，SS14 应该足以应付这个情况。</p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2025/12/magent-clock-back-emf-4.png" alt="magent-clock-back-emf-4" /></p>
<h2>优化后示波器观察</h2>
<p>电磁铁的焊盘用了 2.54mm 间距，刚好够一个 SOD-123FL 封装的二极管焊接上去，手动补一个之后再用示波器观察一下。</p>
<p>可以看到在关断 NMOS 之后的波形就相当正常了，并且最高电压也只有 13.6V。</p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2025/12/magent-clock-back-emf-5.png" alt="magent-clock-back-emf-5" /></p>
<h2>小结</h2>
<p>平时 DIY 的电路中比较少会有感性负载，电动机、线圈什么的用的比较少，对这块了解不够深入。</p>
<p>虽然板子简单，但是还是会出错，要学习的东西还有好多 <img src="https://s.w.org/images/core/emoji/13.1.0/72x72/1f603.png" alt="😃" class="wp-smiley" style="height: 1em; max-height: 1em;" />。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://xujiwei.com/blog/2026/03/magent-clock-back-emf/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>电磁铁驱动的段码时钟找到解法了，先来一位试试</title>
		<link>https://xujiwei.com/blog/2026/03/magnet-clock-finished-one-digit/</link>
					<comments>https://xujiwei.com/blog/2026/03/magnet-clock-finished-one-digit/#respond</comments>
		
		<dc:creator><![CDATA[Xu Jiwei]]></dc:creator>
		<pubDate>Tue, 03 Mar 2026 16:24:45 +0000</pubDate>
				<category><![CDATA[Electronics]]></category>
		<category><![CDATA[Arduino]]></category>
		<category><![CDATA[ESP32]]></category>
		<category><![CDATA[电子DIY]]></category>
		<category><![CDATA[电磁铁]]></category>
		<guid isPermaLink="false">https://xujiwei.com/blog/?p=2095</guid>

					<description><![CDATA[电磁铁段码时钟这个项目还是年初的时候在 Instructables 看到的，中间 &#8230;<p class="read-more"><a href="https://xujiwei.com/blog/2026/03/magnet-clock-finished-one-digit/">继续阅读 &#187;</a></p>]]></description>
										<content:encoded><![CDATA[<p>电磁铁段码时钟这个项目还是年初的时候在 Instructables 看到的，中间尝试过复刻，但是有两点实在有点麻烦就一直没弄：</p>
<ol>
<li>原作者是自己绕电磁线圈，太费事了</li>
<li>原作者用了我不懂的达林顿管啥的来驱动电磁铁，太麻烦了</li>
</ol>
<p>然后最近逛淘宝发现了有现成的线圈卖，买了测试一下，还真可以推动 5x2mm 的圆形永磁铁了，这下可以开始搞一搞了。</p>
<h2>单个数字演示效果</h2>
<p>这里因为是放在桌子上拍的，段码翻转声音会比较大，实际竖着放的时候会好一些。</p>
<p>PS. 拍视频的时候左上角那个段有一个电磁铁坏了，所以最后可以看到它没有翻转成黑色 <img src="https://s.w.org/images/core/emoji/13.1.0/72x72/1f603.png" alt="😃" class="wp-smiley" style="height: 1em; max-height: 1em;" />。</p>
<div style="width: 320px;" class="wp-video"><video class="wp-video-shortcode" id="video-2095-3" width="320" height="640" preload="metadata" controls="controls"><source type="video/mp4" src="https://xujiwei.com/blog/wp-content/uploads/2026/03/电磁铁时钟演示.mp4?_=3" /><a href="https://xujiwei.com/blog/wp-content/uploads/2026/03/电磁铁时钟演示.mp4">https://xujiwei.com/blog/wp-content/uploads/2026/03/电磁铁时钟演示.mp4</a></video></div>
<h2>电磁铁选择</h2>
<p>在找 DeepSeek 学习了一大圈之后，终于<strong>对电磁铁明白一点点，需要足够多的匝数，才能会有大一点的磁性。</strong></p>
<p>然后就找到了这个商品，应该是顺着别人做磁悬浮的配件找到的。</p>
<p>买回来之后把铁芯敲掉就可以了。</p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2025/12/magnet-clock-finished-one-digit-2.jpg" alt="magnet-clock-finished-one-digit-2" /></p>
<p>之前还买过下面这两种，测试的时候完全推不动 5x2mm 的圆形永磁铁，因此从年初到现在都没有折腾过。</p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2025/12/magnet-clock-finished-one-digit-3.jpg" alt="magnet-clock-finished-one-digit-3" /></p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2025/12/magnet-clock-finished-one-digit-4.jpg" alt="magnet-clock-finished-one-digit-4" /></p>
<h2>驱动电路</h2>
<p>原项目使用了达林顿管还有移位寄存器什么的组合起来控制，我觉得比较麻烦，板子也太多了。</p>
<p>在拿到电磁铁线圈之后，就想着<strong>既然是控制通断，是不是可以直接用 NMOS 就可以了</strong>。直接用零件搭了个测试电路，完全可以把圆形磁铁推开，那就开始画块板子吧。</p>
<p>直接最省事的做法了，MCU 的 GPIO 直接控制 NMOS Gate，电磁铁线圈接在 Drain 上，通过短暂开启 GPIO，就可以让电磁铁产生磁力推开特定的段码。</p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2025/12/magnet-clock-finished-one-digit-5.png" alt="magnet-clock-finished-one-digit-5" /></p>
<p>这里使用 NMOS 而不是 PMOS 来控制电磁铁通断，是因为如果电磁铁需要更大推力，需要更高的电压，测试了在 10V 的时候效果比较好，因此如果需要 MCU 3.3V 的 GPIO 可以控制，用 NMOS 比较合适了。</p>
<h2>主控芯片</h2>
<p>本来想用成本低一点的 MCU 来控制，但是这个板子要求最少 14 个 GPIO，手头上刚好有闲置的 ESP32-S3 模块，就直接用 ESP32-S3 了。</p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2025/12/magnet-clock-finished-one-digit-6.png" alt="magnet-clock-finished-one-digit-6" /></p>
<p>完全就是朴实无华的 14 个 GPIO 连接到 14 个 NMOS。</p>
<h2>PCB 设计</h2>
<p>PCB 就是相当简单的一块板子了，不过大小超过了嘉立创的免费打样尺寸，换到捷配去打样了。</p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2025/12/magnet-clock-finished-one-digit-7.jpg" alt="magnet-clock-finished-one-digit-7" /></p>
<h2>小结</h2>
<p>现在一位数字调通了，后面就是看看怎么 4 位数字联动以及通信，在 PCB 两侧预留了 5P 的 2.54mm 排针，可以传递电源以及 UART TX RX 信号，可以只需要一位数字开启 Wi-Fi 通信，其他的只控制电磁铁的通断。</p>
<p>不过 ESP32-S3 模块的成本还是有点高，后面可以考虑把次级数字的 MCU 换成其他低成本 MCU。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://xujiwei.com/blog/2026/03/magnet-clock-finished-one-digit/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>让串流小电视更实用，通过抖动实现 4 阶灰度图像显示</title>
		<link>https://xujiwei.com/blog/2026/03/esp32-oled-streaming-dithering/</link>
					<comments>https://xujiwei.com/blog/2026/03/esp32-oled-streaming-dithering/#respond</comments>
		
		<dc:creator><![CDATA[Xu Jiwei]]></dc:creator>
		<pubDate>Tue, 03 Mar 2026 16:13:06 +0000</pubDate>
				<category><![CDATA[Electronics]]></category>
		<category><![CDATA[Arduino]]></category>
		<category><![CDATA[ESP32]]></category>
		<category><![CDATA[OLED]]></category>
		<category><![CDATA[电子DIY]]></category>
		<guid isPermaLink="false">https://xujiwei.com/blog/?p=2086</guid>

					<description><![CDATA[前言、先看效果对比 二值化黑白图像展示 灰度抖动图像展示 一、黑白世界的色彩想象 &#8230;<p class="read-more"><a href="https://xujiwei.com/blog/2026/03/esp32-oled-streaming-dithering/">继续阅读 &#187;</a></p>]]></description>
										<content:encoded><![CDATA[<h2>前言、先看效果对比</h2>
<p><strong>二值化黑白图像展示</strong></p>
<div style="width: 320px;" class="wp-video"><video class="wp-video-shortcode" id="video-2086-4" width="320" height="640" preload="metadata" controls="controls"><source type="video/mp4" src="https://xujiwei.com/blog/wp-content/uploads/2026/03/ESP32-OLED串流演示.mp4?_=4" /><a href="https://xujiwei.com/blog/wp-content/uploads/2026/03/ESP32-OLED串流演示.mp4">https://xujiwei.com/blog/wp-content/uploads/2026/03/ESP32-OLED串流演示.mp4</a></video></div>
<p><strong>灰度抖动图像展示</strong></p>
<div style="width: 480px;" class="wp-video"><video class="wp-video-shortcode" id="video-2086-5" width="480" height="480" preload="metadata" controls="controls"><source type="video/mp4" src="https://xujiwei.com/blog/wp-content/uploads/2026/03/串流灰度抖动演示-xhs.mp4?_=5" /><a href="https://xujiwei.com/blog/wp-content/uploads/2026/03/串流灰度抖动演示-xhs.mp4">https://xujiwei.com/blog/wp-content/uploads/2026/03/串流灰度抖动演示-xhs.mp4</a></video></div>
<h2>一、黑白世界的色彩想象</h2>
<p>之前使用 ESP32-S3 + 12864 OLED 做了个串流小电视（见 <a href="https://mp.weixin.qq.com/s/v-3UYNmZbrDF3_yrVfuJLg">桌面小电视新思路，ESP32-S3 + 12864 OLED 串流视频</a>），通过网络发送视频帧的方式来播放视频。</p>
<p>因为 12864 OLED 是一个单色显示屏，因此在播放视频时，整个画面的颜色通过<strong>“灰度化-&gt;二值化”</strong>的步骤来生成黑白纯色画面，在视频中可以看到画面没有层次和细节，不太容易看清画面内容是什么。</p>
<p>因此我们需要引入灰度来让整个画面更加丰富，但是在 12864 OLED 没有灰度的情况下如何显示灰度信息呢？</p>
<p>在这里我们就可以引入一种特别的显示技术——<strong>抖动 (Dithering)</strong>，它能让单色屏幕拥有显示4阶灰度的能力。</p>
<h2>二、什么是抖动 (Dithering)？</h2>
<p><strong>抖动</strong>，它的原理却非常直观，相当于通过大脑形成一种视觉错觉。</p>
<p>在单色屏幕上，只有纯黑和纯白的像素。抖动的核心思想就是：<strong>通过改变像素点的排布密度，来模拟出不同程度的灰度。</strong></p>
<p>举个最简单的例子，如果你仔细观察报纸上的照片，你会发现那些“灰色”的部分，其实是由许多大小不一或疏密不同的黑色墨点组成的。在远处看，这些墨点混合在一起，就形成了我们看到的灰度。旧式的点阵打印机也是同样的原理，通过打印点的疏密来表现深浅。</p>
<p><strong>抖动就是利用人眼的这种视觉混合效应。</strong>当我们观察一个由许多纯黑和纯白像素组成的区域时，如果这些像素非常小并且排列得足够紧密，我们的大脑就会将这些黑白像素的混合视为一个统一的灰色。像素点越密，看起来就越黑；像素点越稀疏，看起来就越白。</p>
<h2>三、4 阶灰度：从纯黑到纯白</h2>
<p>我们通常说的灰度，可以有 256 级，甚至更多。但是通过抖动模拟出的灰度，其实是<strong>牺牲了有效分辨率</strong>换来的。</p>
<p>对于 12864 OLED 来说，原生的黑白分辨率是 128x64，通过 2x2 的点阵抖动来实现灰度时，<strong>实际有效分辨率可以等同于 64x32</strong>。</p>
<p>因此，要实现更多级的灰度，会严重损失画面内容，而 4 阶灰度，却是一个在有限资源下，既能有效提升显示效果，又相对容易实现的折衷方案。</p>
<p>那么，这 4 阶灰度具体指什么呢？它们通常代表以下四种亮度等级：</p>
<ul>
<li><strong>0% 亮度：</strong> 纯黑色</li>
<li><strong>33% 亮度：</strong> 较深的灰色</li>
<li><strong>66% 亮度：</strong> 较浅的灰色</li>
<li><strong>100% 亮度：</strong> 纯白色</li>
</ul>
<p>纯黑和纯白是单色屏幕的本色，可以直接显示。但介于两者之间的“较深的灰色”和“较浅的灰色”要如何呈现呢？这就是抖动技术大显身手的地方。</p>
<h2>四、实现原理：点阵的巧妙组合</h2>
<p>现在，我们不再把一个像素看作一个独立的点，而是将几个像素组合成一个微小的“点阵”。最常用也最容易理解的是使用 <strong>2x2 的点阵</strong>，也就是由四个像素组成的一个小方块。</p>
<p>通过控制这四个像素中哪些是“亮”的（白色）和哪些是“暗”的（黑色），我们就可以模拟出四种不同的灰度等级。</p>
<p>这里是2x2点阵的四种组合方式，以及它们所代表的灰度等级：</p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2026/02/oled-streaming-dithering-2.jpg" alt="oled-streaming-dithering-2" /></p>
<p>通过这样的点阵组合，我们的眼睛在一定距离下观看时，就会将这些微小的黑白点阵“混合”成具有不同灰度感觉的区域。这就是单色屏幕上实现4阶灰度的基础。</p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2026/02/oled-streaming-dithering-3.jpg" alt="oled-streaming-dithering-3" /></p>
<h2>五、抖动算法：误差扩散抖动</h2>
<p>虽然 2x2 点阵的原理很简单，但如果仅仅是机械地将图像划分为 2x2 的小块，并根据平均灰度值来选择对应的点阵模式，图像看起来可能会有明显的块状感，细节丢失严重。</p>
<p>为了获得更自然、更平滑的灰度过渡，我们需要更高级的抖动算法，其中最常用且效果显著的一种是<strong>误差扩散抖动 (Error Diffusion Dithering)</strong>。</p>
<p>简单来说，误差扩散抖动不再是孤立地处理每个像素或小点阵，而是考虑了图像的整体性。当算法处理一个像素时，它会首先判断这个像素应该显示为黑还是白。但关键在于，如果这个像素的原始灰度值与最终选择的黑白值之间存在“误差”，这个误差并不会被简单地丢弃，而是会被<strong>“扩散”</strong>到它周围的、尚未处理的像素点上。</p>
<p><strong>下图是 floyd-steinberg 典型误差扩散权重设定：</strong></p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2026/02/oled-streaming-dithering-4.png" alt="oled-streaming-dithering-4" /></p>
<p>这样一来，被扩散的误差会影响到后续像素的黑白决策，使得整体灰度分布更加均匀，视觉效果也更接近原始图像的灰度。常见的误差扩散算法有 Floyd-Steinberg、Jarvis-Judice-Ninke 等，它们的主要区别在于误差扩散的权重和方向。</p>
<p><strong>不同抖动算法效果对比</strong></p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2026/02/oled-streaming-dithering-5.jpg" alt="oled-streaming-dithering-5" /></p>
<p>通过示例图可以看到，经过算法优化的抖动图像，可以显示更多原始图像的细节。</p>
<h2>六、实际应用场景</h2>
<p>虽然现在大多数带屏设备已经是使用了更高像素、更多色彩的 LCD，甚至是 OLED 显示屏，但是在相当多的领域，还是会有单色屏在使用。</p>
<p>在这些单色屏场景中，抖动技术就发挥着重要作用：</p>
<ul>
<li><strong>电子书阅读器：</strong> 很多早期的E-Ink屏幕就是利用抖动技术来显示文本和图片的，以在有限的灰度等级下提供更好的阅读体验。</li>
<li><strong>工业显示屏：</strong> 在一些对成本、功耗有严格要求的工业设备上，单色显示屏配合抖动技术可以以较低的成本实现更好的视觉效果。</li>
<li><strong>旧款打印机：</strong> 点阵打印机和早期喷墨打印机也利用抖动来模拟灰度或颜色。</li>
<li><strong>图像处理软件：</strong> 许多图像编辑软件都提供了抖动功能，用于在将图像转换为低色深格式时，减少色带（banding）现象，保持图像细节。</li>
</ul>
<h2>七、总结</h2>
<p>不得不说，人类的智慧是无限的，<strong>在有限的资源下，通过算法能创造出超越硬件限制的体验。</strong></p>
<p>计算机图形学还有相当多的东西可以学习，在以后的 DIY 过程中再慢慢学习吧～</p>
<h2>参考资料</h2>
<ul>
<li><a href="https://www.emoe.xyz/image-dithering-and-its-application/">https://www.emoe.xyz/image-dithering-and-its-application/</a></li>
<li><a href="https://en.wikipedia.org/wiki/Dither">https://en.wikipedia.org/wiki/Dither</a></li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://xujiwei.com/blog/2026/03/esp32-oled-streaming-dithering/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>桌面小电视新思路，ESP32-S3 + 12864 OLED 串流视频</title>
		<link>https://xujiwei.com/blog/2026/03/esp32-oled-streaming/</link>
					<comments>https://xujiwei.com/blog/2026/03/esp32-oled-streaming/#respond</comments>
		
		<dc:creator><![CDATA[Xu Jiwei]]></dc:creator>
		<pubDate>Tue, 03 Mar 2026 16:10:28 +0000</pubDate>
				<category><![CDATA[Electronics]]></category>
		<category><![CDATA[Arduino]]></category>
		<category><![CDATA[ESP32]]></category>
		<category><![CDATA[OLED]]></category>
		<guid isPermaLink="false">https://xujiwei.com/blog/?p=2090</guid>

					<description><![CDATA[之前搞了个小电脑使用 ESP32-S3 + OLED 显示视频（见 3 步从 n &#8230;<p class="read-more"><a href="https://xujiwei.com/blog/2026/03/esp32-oled-streaming/">继续阅读 &#187;</a></p>]]></description>
										<content:encoded><![CDATA[<p>之前搞了个小电脑使用 ESP32-S3 + OLED 显示视频（见 <a href="https://mp.weixin.qq.com/s/mu1q-CiXpZABE8_FYRR9NA">3 步从 nana banana AI 生成的复古小电脑到真实桌面小玩具</a>），但是受制于 Flash 空间有限，只放了 10 秒。</p>
<p>后来就想到是不是可以 <strong>ESP32-S3 只作为一个播放设备，视频数据从电脑上串流过去</strong>，而且在 ESP32-S3 支持这样串流的能力之后，理论上<strong>可以显示任意想要显示的信息，例如作为电脑的小副屏。</strong></p>
<p>说干就干～</p>
<h2>先看看效果</h2>
<div style="width: 360px;" class="wp-video"><video class="wp-video-shortcode" id="video-2090-6" width="360" height="640" preload="metadata" controls="controls"><source type="video/mp4" src="https://xujiwei.com/blog/wp-content/uploads/2026/03/ESP32-OLED串流演示.mp4?_=6" /><a href="https://xujiwei.com/blog/wp-content/uploads/2026/03/ESP32-OLED串流演示.mp4">https://xujiwei.com/blog/wp-content/uploads/2026/03/ESP32-OLED串流演示.mp4</a></video></div>
<h2>串流完整流程</h2>
<p>因为屏幕使用的是 OLED，分辨率是 128x64，颜色是单色， <strong>单帧数据只需要  128x64/8 =1,024 字节</strong>，即使串流 30 FPS 也只有 30 KB/s 的流量，因此在电脑端直接发送完整屏幕数据是完全可以接受的。</p>
<p>并且大部分图像处理工作都在电脑端处理完成了，ESP32-S3 的固件在实现渲染时压力就会小很多，只需要 <strong>接收数据-&gt;写入 OLED 显存</strong> 即可。</p>
<p>通信协议为了省事，<strong>直接使用 WebSocket 了，这样直接一个消息就是一帧数据</strong>，不用额外设计一个 Socket 通信协议。</p>
<p><strong>完整的串流流程如下图所示：</strong></p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2025/12/esp32-oled-streaming-2.jpg" alt="esp32-oled-streaming-2" /></p>
<h2>电脑端脚本</h2>
<p>在电脑端，主要是使用 ffmpeg 库，将视频解析为独立的帧，并且将每一帧处理为目标格式，在 OLED 的场景下完整的步骤为：</p>
<p><strong>缩放到目标尺寸（128x64）-&gt; 灰度化 -&gt; 二值化 -&gt; 编码为 OLED 数据格式</strong></p>
<p>代码直接用 Node.js + fluent-ffmpeg 库编写。</p>
<p><strong>ffmpeg 处理代码</strong></p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2025/12/esp32-oled-streaming-3.jpg" alt="esp32-oled-streaming-3" /></p>
<p><strong>执行推流脚本</strong></p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2025/12/esp32-oled-streaming-4.jpg" alt="esp32-oled-streaming-4" /></p>
<h2>ESP32-S3 固件实现</h2>
<p>在固件侧就比较简单了，直接使用 WebSockets 库创建一个 WebSocket Server，然后监听在 81 端口，接收到 Binary 类型的消息时，直接作为完整的一帧屏幕数据发送到渲染队列进行显示。</p>
<p>这里额外设置了一个渲染队列任务，充分利用一下 ESP32-S3 的双核能力 <img src="https://s.w.org/images/core/emoji/13.1.0/72x72/1f603.png" alt="😃" class="wp-smiley" style="height: 1em; max-height: 1em;" />。</p>
<p><strong>将 WS Binary 消息数据发送到渲染队列</strong></p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2025/12/esp32-oled-streaming-5.jpg" alt="esp32-oled-streaming-5" /></p>
<p><strong>渲染队列中直接将数据显示到 OLED</strong></p>
<p><img src="https://xujiwei.com/blog/wp-content/uploads/2025/12/esp32-oled-streaming-6.png" alt="esp32-oled-streaming-6" /></p>
<h2>小结</h2>
<p>在目标屏幕是 OLED 的情况下，从电脑直接推流到 ESP32-S3 + OLED 显示还是挺方便的，效果也还不错，后面试试看搞个彩色 LCD，看看推流 + 解码 + 渲染的情况下，ESP32-S3 能达到什么样的效果。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://xujiwei.com/blog/2026/03/esp32-oled-streaming/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
