> ## Content Index
> Fetch the complete content index at: https://www.hubwiz.com/blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# 10 美元微控制器上的语言模型
- URL: https://www.hubwiz.com/blog/a-language-model-on-a-10-dollar-microcontroller/
- Published: 2026-08-06T16:43:02.000Z
- Updated: 2026-08-06T16:43:02.000Z
- Description: 一个从头训练的自定义 transformer 在 ESP32-S3 上解析英文硬件命令——1.2 MB 的权重，无 WiFi、无云端、无 API 密钥。
- Author: admin
- Tags: MODEL-ZOO, #Import 2026-10-14 14:41

我训练了一个体积只有 **1.2 MB**、运行在 **10 美元微控制器** 上、完全不依赖任何网络的的语言模型。你在串口终端里输入 `blink pin 4 every 5 seconds 3 times`，一个 LED 就开始闪烁。没有任何数据离开这块板子——没有 WiFi、没有云端 API、甚至根本没有编译进任何网络协议栈。

## 1、系统

一个从头训练、把英文转换成 GPIO 命令的 transformer，完全运行在 ESP32-S3 微控制器上。没有 WiFi、没有云端、没有 API 密钥——没有编译进任何网络协议栈。你在串口终端里输入，引脚就会切换状态。

模型不输出 JSON。它输出一种符号文法：

```
turn on pin 4            ->  <set> <pin> 4 <high> <end>
blink pin 4 every 500ms  ->  <blink> <pin> 4 <int> 5 0 0 <cnt> 0 <end>
chase pins 4 5 6         ->  <seq> <pin> 4 <pin> 5 <pin> 6 <end>

```

一个 C 头文件把这些符号重新组装成结构体。没有会引错引号的字符串，没有会拼错的字段名，没有会漏掉的括号。格式错误的命令根本无法表示，而这部分从第一天起就完全按设计工作。

它是一个仅解码器（decoder-only）的 transformer，312,128 个参数：

```
vocab 1024   d_model 64   layers 6   heads 8   head_dim 8
ffn_hidden 128 (SwiGLU)   seq_len 96   RoPE theta 10000
tied input embedding and output head   fp32   no biases anywhere

```

每个块——一共六个——是 41,088 个参数：

![None](https://www.hubwiz.com/blog/content/images/2026/08/1-Lu7cPvL6KuBuoVXwnu6lhg.jpeg.png)

再加上与输出头共享的 (1024, 64) = 65,536 的 token 嵌入，以及最后的归一化层。这就是从专用 flash 分区内存映射出来的 1,219 KB fp32 权重。KV 缓存——288 KB——位于 PSRAM 中，启动时一次性分配。推理路径中没有动态分配。

这些组件是现代小型模型的标准配置：RMSNorm、作用于 query 和 key 的 RoPE、因果多头注意力、SwiGLU 前馈层、pre-norm 残差块。没有什么奇特之处。有两个细节对后续内容很重要。

RoPE 采用 split-half 而非 interleaved 形式：向量被切成两半，按 `[x1·cos − x2·sin, x2·cos + x1·sin]` 旋转。interleaved 公式更常见且数值上不同，而 C 运行时必须逐操作复现 PyTorch 的行为，所以两边都固定使用同一约定。

数字是独立的 token。分词器在 ByteLevel 之前应用 `Digits(individual_digits=True)`，因此任何合并都不可能跨越两个数字——`300ms` 变成 `3 0 0 ms`，绝不会是 `30 0`。十六个保留符号占据 id 0 到 15，所以模型发出的每个 token 要么是保留符号，要么是单个数字。

最后一个决定正是本文的全部主题，而第一个版本把它搞反了。

## 2、那个看起来显然正确的设计

第一版文法给每个在白名单内的引脚分配了自己的 token：`<p4>`、`<p18>`，一共二十五个。这块板子有二十五个可用的 GPIO 引脚，所以模型的词汇表恰好包含所有真实存在的引脚。

因此，非法引脚是无法表示的。模型说不出"pin 100"，就像它说不出词汇表之外的任何词一样。这感觉像是一个真正的安全属性，而非便利之举——不是可能有 bug 的校验，而是一种结构性保证。

然后我在保留数据上对它进行了测量。

```
switch off pin 100         ->  <set> <p10> <low>      switches pin 10
name pin 35 as back door   ->  <alias> <p39> ...      renames pin 39
blink pin 8 at 12000 ms    ->  <blink> <p8> 1200      runs at 1200 ms

```

一个说不出"pin 100"的模型不会拒绝。它会说出它能说的最接近的东西。

引脚 10 是板子上真实存在的引脚。命令顺利通过校验、通过硬件白名单，然后驱动了错误的硬件——悄无声息，日志里没有任何线索能把它和正确命令区分开。用户请求一个不存在的引脚，却得到了一个真实存在的引脚。

```
> An unrepresentable request does not produce a refusal. It produces a plausible substitute.

```

这种失败比错误更糟糕，因为错误会自我宣告。而它产生的是一个格式良好、合法、可执行的命令，做的是没有人要求的事。

## 3、为什么这具有普遍性

我不认为这是微控制器特有的问题，这也是我把它写出来而不是仅仅修掉它的原因。

受限解码（constrained decoding）、文法受限采样、JSON-schema 强制输出和函数调用 API 都共享同一种机制：它们限制模型被允许输出的内容，然后采样最有可能的允许 token。这种限制并不会教会模型某个请求是无效的。它只是把请求重定向到最近的合法输出。

如果你的 schema 有五个状态的枚举，而用户描述了第六种，模型会返回你五种之一。如果你的函数签名接受一个客户 ID，而用户说出一个不存在的客户，模型会返回一个存在的 ID。两种情况你都会收到语法完美的响应，却没有任何信号表明出错了。

一般形式是：如果你的输出空间无法表达"输入超出范围"，你的模型就会表达某个范围内的东西，而在下游你永远看不到差别。

## 4、修复：先转录，再裁决

重新定义范围的做法是：移除那个保证，用一个真正可能失败的检查来替代它。

引脚号现在逐位输出——`<pin> 1 0 0`——这意味着模型可以说出"pin 100"。硬件层本来就持有板子的白名单，由它来执行拒绝。模型负责转录；硬件负责裁决。

同样的输入现在产生：

```
> switch off pin 100
refused: pin 100 is not a GPIO on this board   (779ms)

```

数字之所以会出现在拒绝信息里，是因为模型能够说出它。这不是表面上的改进。这是"报告不匹配的系统"与"隐藏不匹配的系统"之间的区别。

同样的重构应用于每个数字槽位。间隔、重复次数和引脚列表都逐位转录，并由硬件层对照真实边界进行校验：

```
> blink pin 4 every 60 seconds
refused: interval 60000ms is outside 50-10000ms   (1101ms)

```

## 5、三个 LED 找到了评估套件找不到的问题

同样的失败形态又出现了四次，出现在我没想到要检查的槽位里：

![None](https://www.hubwiz.com/blog/content/images/2026/08/1-XHlVarr1_55dmA4HcAHtdQ.jpeg.png)

我是通过在引脚 4、5 和 6 上接上 LED，然后输入 `turn on pin 4 5 and 6` 发现第三个问题的。只有一个亮了。

它对项目中的每一个指标都不可见，而且必然如此。保留集是依据旧文法收集的，所以它不可能包含旧文法无法表达的语句。

```
> An evaluation derived from a grammar cannot find what that grammar cannot say.

```

这是一个具体且被低估的盲区。保留数据感觉像是一种独立的检查，但如果数据是用模型输出的同一套 schema 生成、采样或标注的，那么 schema 的局限也被烙进了评估本身。数据集和模型共享同一个假设，再多的保留数据也无法让它浮出水面。

在这里，硬件与评估集并非冗余。它覆盖了数据集结构性固有的盲区。三个 LED 和一块面包板测试了整条流水线在结构上根本无法测试的属性。

这四个 bug 各有不同的成因，却有相同的形态：`set` 接受一个目标，`read` 接受一个，`blink` 接受一个，`stop` 接受一个。四个独立的限制，四个独立的静默失败。修复是一条规则而非四个补丁——每个接受引脚的动作都接受一个引脚列表。

![None](https://www.hubwiz.com/blog/content/images/2026/08/1-Z7xxBcl5OHpadZ3Q8Bgbew.jpeg.png)

## 6、运行起来是什么样子

板子上一次真实的会话，包含计时：

```
> turn on pin 4
pin 4 high   (515ms)

> turn on pin 4 and 5
pin 4, 5 high   (947ms)

> blink pin 4 every 5 seconds 3 times
blinking 1 pin every 5000ms, 3 times   (1155ms)

> chase pins 4 and 5 every 1 second
chasing 2 pins every 1000ms   (1429ms)

> switch off pin 100
refused: pin 100 is not a GPIO on this board   (779ms)

> turn on the desk lamp
refused: I don't understand that   (359ms)

```

每条命令的延迟在 150 ms 到 1.5 s 之间，取决于需要生成多少个符号。那两条拒绝信息是设计在起作用，而不是模型在失败。

## 7、注意事项

模型没有达到自己的目标。精确匹配率是 91.7%，目标是 95%；误接受率是 4.4%，目标是 2%。这是一个可用的系统，而不是一个完成品，第 2 部分会讲剩余错误出现在哪里。

关于受限解码的普遍性推论是论证出来的，而不是测量出来的。我没有在带有 JSON schema 或 GBNF 文法的大型模型上做过等价实验。请把它当作一个有精心测量支持案例的假设。

命名和别名不在范围内。"Turn on the desk lamp" 按设计返回 `<unknown>`——目前还没有别名表，而构建一个别名表正是本文结论的直接复赛。

所有测量都在一块运行单一狭窄命令语言的板子上完成。

## 8、自己试试

仓库附带一个可烧录的单一镜像，包含引导加载程序、分区表、固件和模型，因此不需要工具链或训练运行：

```
pip install esptool
esptool.py --chip esp32s3 --port /dev/cu.usbmodemXXXX --baud 921600 \
  write_flash 0x0 espcontrol-esp32s3-v0.2.1.bin

```

以 115200 打开串口监视器并输入。你需要一块带 PSRAM 的 ESP32-S3、一个 LED，以及在 GPIO 4 和地之间串联一个 220Ω 到 1kΩ 的电阻。

## 9、我学到的东西

限制模型的输出空间并不会让它变得安全。它只会让它变得沉默。逐引脚 token 的文法是整个项目中最自信的设计决策，而它把"我做不到"变成了"给你另一个引脚"。每一个结构化输出系统都继承了这一属性。

校验需要有可以拒绝的东西。位于一个无法表达非法值的模型下游的检查永远不会触发，而这看起来和正在通过的检查一模一样。给模型犯错的能力，给校验器可捕获的东西。

测试属性本身，而不是它的代理。由文法生成的评估共享该文法的盲区。三个 LED 找到了整条流水线在结构上无法发现的 bug。

模型仍然没有达到四个目标中的三个。但它运行在一块十美元的芯片上，没有网络，而且当你让它关掉一个不存在的引脚时，它会把你说的那个数字告诉你。

---

原文链接: [A Language Model on a $10 Microcontroller: 312K Parameters Driving Real GPIO Pins, Fully Offline](https://medium.com/data-science-collective/a-language-model-on-a-10-microcontroller-312k-parameters-driving-real-gpio-pins-fully-offline-f06ad803805f?ref=hubwiz.com)

汇智网翻译整理，转载请标明出处