一句话,让 AI 把一块 ESP8266 从头调到能上网

  • 从烧固件到手机配网,一个下午踩了 7 个坑
  • 我全程没打开 IDE,也没查过一次数据手册
  • 板子不骗人:那些只有真硬件才能抓到的 bug

引子

抽屉里翻出一块 NodeMCU,就是这个小板,ESP8266 芯片,MicroPython 时代的老朋友,大概是10年前买的。插上电脑,设备管理器里冒出个 COM10,芯片是 CP210x。

我打开 WorkBuddy,敲了一句话:

我这有一块 ESP8266 的小板子,NodeMcu 的板子,现在连接到电脑上了,串口号是 COM10,你能不能直接帮我写个脚本程序下载进去运行+调试?

后面发生的事,比我预想的有意思得多。不是因为它多顺利,恰恰是因为它一路翻车,而每一次翻车都翻得很有教育意义。

这篇文章把整个过程摊开讲:烧固件、写脚本、改网页、加配网,以及那 7 个坑。如果你手边也有一块吃灰的 ESP8266,照着走一遍,大概能省掉一个下午。


一、先让它能说话

选型:为什么是 MicroPython 而不是 Arduino

理由很朴素:脚本改完直接传,不用编译。而且 MicroPython 自带 REPL,板子在跑的时候你还能连上去敲命令,看变量、读内存、临时改个引脚电平。

C 语言当然更省资源,但调试成本高一个量级。这次的目标是"快速验证一块板子到底好不好使",MicroPython 是对的选择。

烧固件:三个必须显式写出来的参数

先探芯片:

bash 复制代码
esptool --port COM10 --baud 115200 flash-id
复制代码
Chip is ESP8266EX
Flash size: 4MB
MAC: ec:fa:bc:12:df:bd

确认无误,擦除、烧写 MicroPython v1.29.0(643KB,镜像首字节 0xE9 是合法标志)。

这里有个必须注意的细节 :write-flash 一定要显式给 Flash 参数,不让 esptool 自己猜:

bash 复制代码
esptool --port COM10 --baud 460800 write-flash \
    --flash-mode dout --flash-freq 40m --flash-size 4MB \
    0x0 fw/ESP8266_GENERIC-v1.29.0.bin

烧完 Hash 校验通过,一切看起来完美。

然后我就撞上了第一个坑。


二、坑 1:3 秒重启 29 次

烧完固件,打开串口一看------全是乱码,而且在无限重启。

抓 4 秒,数出 29 次启动,每一次都是:

复制代码
ets Jan  8 2013,rst cause:2, boot mode:(3,7)
rf_cal[0] !=0x05,is 0x...

一开始我以为是固件坏了、Flash 参数不匹配,甚至怀疑整片擦除破坏了射频校准区。重刷了两遍,症状不变。

真正的原因跟固件一点关系都没有:复位脉冲的保持时间太短。

NodeMCU 这颗 CP210x 的串口芯片,DTR 接 GPIO0、RTS 接 EN(复位脚)。我最初的复位代码是这样的:

python 复制代码
s.setRTS(True)      # 拉低复位
time.sleep(0.15)    # 只保持 0.15 秒
s.setRTS(False)     # 放开

0.15 秒,芯片在"复位"和"释放"之间来回抖,既没真正复位干净,也没进入下载模式,卡在临界点上疯狂重启。rst cause:2 的意思是"外部复位引脚触发"------不是软件崩了,是硬件一直在按它。

改成 0.3 秒,一次就好。 这是 esptool 内部用的标准值,抄它准没错。

python 复制代码
s.setRTS(True)
time.sleep(0.3)
s.setRTS(False)

第一次跑通的时候,串口吐出:

复制代码
MicroPython v1.29.0 on 2026-08-24; ESP module with ESP8266
>>> 

经验:ESP8266 出现"无限重启 + 全是乱码",十次有八次是复位时序问题,不是固件问题。先怀疑 DTR/RTS,再怀疑 Flash。

顺带一个坑:这块板子有两个波特率

这是新手最容易懵的地方:

输出阶段 波特率 你看到什么
上电/复位后最初几十毫秒(ROM 层) 74880 rst cause、boot mode、rf_cal
固件接管之后(应用层) 115200 MicroPython v1.29.0 ...、你的 print

所以用 115200 打开串口,每次复位开头都会有一小段乱码------那是正常的,不是板子坏了。

我一开始就在这儿误判了,以为烧写失败。


三、第一版脚本:让它自己汇报状态

链路通了,先写个自检脚本,确认板子的各个部件都活着:

  • 扫一遍周边 WiFi,按信号强度排序打印前 10 个
  • 板载 LED(GPIO2)每轮闪 3 下
  • 每 5 秒打一行心跳:运行时长、空闲内存、CPU 频率

实测扫到 24~26 个 AP,心跳长这样:

复制代码
[WIFI] found 24 AP(s)
[    0s] tick=1  mem_free=33312  freq=80MHz
[    5s] tick=2  mem_free=33312  freq=80MHz

这里有个小毛病,属于"能跑但不好看":心跳周期跑成了 6 秒。因为我在循环里先闪 LED(耗时约 1 秒)再 sleep(5s),累计就多了。正确做法是按绝对时间点对齐,而不是每次睡固定时长:

python 复制代码
next_tick = time.ticks_add(time.ticks_ms(), 5000)
while True:
    # ...干活...
    now = time.ticks_ms()
    if time.ticks_diff(next_tick, now) > 0:
        time.sleep_ms(time.ticks_diff(next_tick, now))
    next_tick = time.ticks_add(next_tick, 5000)

改完,心跳稳稳 5 秒一格。

这一步的意义:它证明了"写脚本 → 传进去 → 跑起来 → 看日志 → 改 → 再传"这条闭环是通的。后面所有的事都建立在这条闭环上。


四、第二版:把它变成网页

自检太无聊了。真正想验证的是整条 TCP/IP 链路------能扫到 WiFi 只能说明射频好,不代表网络栈好用。

目标:在浏览器里控制板载 LED。

复制代码
GET /                          控制页面
GET /api/led?state=on|off|toggle   控灯
GET /api/status                JSON 状态

用 socket 手写了一个极简 HTTP 服务端。没有用框架------ESP8266 那点内存,框架进来就没地方放业务代码了。

一个聪明的做法:先在电脑上把它跑一遍

板子调试最痛苦的地方是每次改代码都要等上传 + 重启 + 连 WiFi,一轮 30 秒。如果逻辑有低级错误,这个成本会被放大十倍。

所以我先做了一件事:写桩模块,在电脑上把 HTTP 逻辑整个跑一遍。

用假的 machine、network、gc 顶替板子的硬件模块,然后直接把伪造的 HTTP 请求喂给处理函数:

python 复制代码
# 伪造一个 socket
class FakeSock:
    def recv(self, n): return self.rx[:n]   # 预置的请求字节
    def send(self, data): self.out += data

# 伪造一次请求
c = FakeSock(b"GET /api/led?state=on HTTP/1.1\r\nHost: x\r\n\r\n")
handle_sta(c, fake_sta, "1.2.3.4", "myssid")
assert c.out.startswith(b"HTTP/1.1 200 ")

34 项断言,覆盖状态码、Content-Length 一致性、JSON 字段、LED 电平跟随、404/204、空请求不崩。

然后它抓到了一个我绝对想不到的 bug。

坑 2:CSS 里的百分号,把整个页面干崩了

页面是用 Python 的 % 格式化的:

python 复制代码
PAGE = """<style>.dot{border-radius:50%%}</style>...""" % {"host": HOSTNAME}

看着没问题对吧?但真实代码里写的是 50%,而这个 % 后面跟着 ;background ------ Python 解析器认为这是一个格式占位符,然后告诉你:

复制代码
not enough arguments for format string

后果是什么?首页直接 500,而你在串口里只会看到一行看不懂的异常。 找原因的时间会远超你的想象,因为你的目光会一直在 socket、在 WiFi、在内存上,就是不会想到 CSS。

解法:改用占位符替换,彻底绕开 % 语法。

python 复制代码
PAGE = PAGE.replace("{{HOST}}", config.HOSTNAME)

经验 :往模板里塞 HTML/CSS/JS 的时候,永远不要用 % 或 .format()。str.replace() 笨,但它不会咬你。


五、坑 3:MemoryError,ESP8266 上最大的那道坎

功能加完,代码涨到 19.3KB。传上去,板子直接报:

复制代码
MemoryError:

我一开始以为是内存泄漏,查了半天 gc.mem_free()。不是。ESP8266 的堆只有约 35KB,而 MicroPython 每次 import 一个模块,都要把整份 .py 源码读进 RAM,然后在 RAM 里编译成字节码。 19KB 的源码,光是读进来 + 编译,就把堆吃干净了。

那就拆呗。拆成 4 个模块:board.py(LED)、pages.py(模板)、web.py(HTTP)、main.py(入口)。

再传。还是 MemoryError。

一看,pages.py 单独一个文件 6.8KB,import pages 的时候照样炸。

这时候才想明白:问题不是总量,是单个文件的大小。只要任何一个源文件超过 6~7KB,导入它就会炸。

正解:预编译成 .mpy

MicroPython 有个交叉编译器 mpy-cross,可以在电脑上把 .py 编译成 .mpy 字节码。板子加载 .mpy 时直接读字节码,不需要在 RAM 里编译,内存占用立刻降下来。

关键点:mpy-cross 的版本必须和固件版本严格一致。

bash 复制代码
pip install mpy-cross
mpy-cross --version
# mpy-cross v1.29.0  /  mpy v6.3     ← 和板子固件 v1.29.0 严丝合缝

部署脚本里加了这一步,自动化 --all:先编译全部 .mpy,再上传,同时删掉板上同名的 .py ------因为 Python 的导入优先级里 .py 高于 .mpy,不删等于白干。

效果:

指标 改之前 改之后
源码体积 19.3 KB ---
上传体积(.mpy) --- 12.2 KB
能否启动 MemoryError 正常,空闲 18.7KB

六、三个只有真板子才能抓到的 bug

前面那个 CSS 的 bug 是 PC 端测试抓的。但接下来这三个,PC 端测试一个都抓不到------因为它们是 MicroPython 和真实 socket 的差异。

坑 4:MicroPython 的 bytes 没有 partition()

解析 HTTP 头需要把请求切成"头 / 空行 / body"三部分。CPython 里天经地义:

python 复制代码
head, _, body = data.partition(b"\r\n\r\n")

在板子上:

复制代码
AttributeError: 'bytes' object has no attribute 'partition'

MicroPython 为了省空间,砍掉了不少 CPython 的便利方法。改成最原始的写法:

python 复制代码
idx = data.find(b"\r\n\r\n")
head = data[:idx] if idx >= 0 else data
body = data[idx + 4:] if idx >= 0 else b""

这个 bug 是怎么抓到的? 我写了个"板上冒烟测试"------把测试脚本直接放到板子上跑,用伪 socket 喂请求给真实的服务端代码:

bash 复制代码
mpremote connect COM10 run tools/onboard_smoke.py

这一步的价值极高:它跑的是真正的 MicroPython 解释器 + 真正的固件 API,只是不经过网络。PC 端桩测试负责逻辑,板上冒烟负责 API 兼容性,两者缺一不可。

坑 5:socket.send() 会只发一部分

这个最阴。板上冒烟全绿,接口测试全绿,浏览器一打开------页面渲染一半就断。

复制代码
curl: (56) Recv failure: Connection reset by peer
# 只有 947 字节,而页面实际 3779 字节

原因是 MicroPython 的 socket.send() 语义和你想的不一样:它只保证"发出去了一部分",返回实际发送的字节数 ,不保证整包发完。而我在发完之后立刻 close(),剩下的数据就丢了。

必须用 sendall()(如果固件支持),或者自己循环:

python 复制代码
def send_all(cl, data):
    try:
        cl.sendall(data)
        return
    except AttributeError:
        pass
    n = 0
    total = len(data)
    while n < total:
        n += cl.send(data[n:])

这个 bug 只有在真实网络上才会暴露。 因为桩测试里 send() 是"全部接收",永远成功。

坑 6:心跳漂移(已在前文解决)

顺手记一笔:这类"能跑但不对"的小问题,如果你放任不管,后面排查大问题时会被它误导。


七、第三版:让手机给它配网

到这一步功能是通的,但有个现实问题:WiFi 密码得写死在代码里。

这不行。换个环境就得改代码重刷。于是改成 AP 配网:板子自己开热点,手机连上去填密码。

完整流程

板子开机:

  1. 读板内 wifi.json
  2. 有配置 → 直接连,连不上重试 2 次
  3. 没有 或 连不上 → 开热点 nodemcu-setup,IP 192.168.4.1
  4. 手机连热点 → 浏览器打开 http://192.168.4.1
  5. 页面真的扫一遍周边 WiFi列表出来(点一下自动填 SSID)→ 填密码 → 保存
  6. 板子重启去连,热点消失
  7. 想重配?工作页底部「重新配网」,一键清配置回第 3 步

一个小细节,但很关键:密码里的特殊字符 。用户的密码是 test@#$214 这种,含 #、@、$。表单提交时用 URL 编码,服务端必须自己实现解码(MicroPython 没有 urllib.parse.unquote):

python 复制代码
def unquote(s):
    """解码 x-www-form-urlencoded。密码里的 # @ $ 空格全靠这个还原"""
    out = []
    i = 0
    n = len(s)
    while i < n:
        c = s[i]
        if c == "%" and i + 2 < n:
            try:
                out.append(chr(int(s[i+1:i+3], 16)))
                i += 3
                continue
            except ValueError:
                pass
        elif c == "+":
            out.append(" ")
            i += 1
            continue
        out.append(c)
        i += 1
    return "".join(out)

测试里专门断言了test%40%23%214 能还原成 test@#$214。这种地方一旦错,表现是"密码错误、连不上",你会怀疑路由器、怀疑信道、怀疑天线,就是不会怀疑编码。


八、那个"打不开网页"的乌龙

配网成功,板子连上路由器。用户手机连着板子热点,打开 192.168.4.1:

复制代码
无法访问此网站
192.168.4.1 拒绝了我们的连接请求。
ERR_CONNECTION_REFUSED

第一反应肯定是:板子的 HTTP 服务挂了。

但串口日志说的是另一回事:

复制代码
[MAIN] try 1/2  ssid=TEST
[WIFI] connected  ip=192.168.1.142  gw=192.168.1.1  rssi=-55 dBm  ch=1
[HTTP] ready ->  http://192.168.1.142/

板子明明活得好好的,而且已经在工作模式了。

原因很简单:一旦连上路由器,板子就关掉了配网热点,开始在自己的新 IP 上服务。这时候 192.168.4.1 自然没人应答------那不是故障,那是成功的表现。

判据只有一条:看串口有没有打出 [HTTP] ready -> http://<IP>/。 有,就说明它在正常服务,你去访问那个 IP 就行。

最终验证(从电脑 curl):

复制代码
首页        http=200  bytes=3779  耗时=0.15s
/api/status {"led":0,"uptime":75,"mem":19056,"rssi":-58,"ip":"192.168.1.142",...}
控灯         {"ok":true,"led":1}

顺带两个环境层面的坑

排查过程中还遇到两个跟板子无关、但会浪费你一小时的坑:

① 串口被占用 。想抓日志,报 PermissionError(13, '拒绝访问')。因为 串口调试助手正开着 COM10。串口是独占资源,同一时刻只能一个程序用。

② 电脑上的深信服 。任务列表里躺着 SangforPWEx.exe 和 SangforUDProtectEx.exe(SSL VPN + 终端防护)。这类客户端会接管网络栈、改写路由表,是内网设备访问被拒的常见干扰源。如果你电脑上板子打不开、手机却正常,先怀疑它,别怀疑板子。


九、方法论:三层测试,缺一不可

整个过程最有价值的不是代码,是这套测试结构。我把它固化下来了:

层级 工具 跑在哪 能抓什么 耗时
① 离线桩测试 tools/devtest_pc.py 电脑 业务逻辑、响应格式、边界条件 < 1 秒
② 板上冒烟测试 tools/onboard_smoke.py 板子 MicroPython API 差异、内存 ~ 5 秒
③ 真链路测试 curl / 浏览器 真实网络 socket 语义、大包传输、并发 手动

每一层都抓到了别人抓不到的东西:

  • ① 抓到的是 CSS 里的 % 撞了格式化------纯逻辑 bug
  • ② 抓到的是 bytes.partition() 不存在------API 兼容性 bug
  • ③ 抓到的是 send() 只发了一部分------真实网络语义 bug

如果跳过第 ② 层直接上板,你会以为代码没问题,然后花一整晚排查网络。

如果跳过第 ③ 层,你会在演示的时候当众翻车。


十、最后,它现在是这样

一块 2016 年的开发板,MicroPython v1.29.0,5 个 Python 文件(编译后 12.2KB 字节码),一个手写 HTTP 服务端,跑在 35KB 的堆上:

  • 手机连热点配网,配完自动重启入网
  • 浏览器控制板载 LED,页面每 5 秒自刷新
  • /api/status 输出 JSON,Zabbix / Home Assistant 直接能接
  • WiFi 断了 5 秒内自愈重连
  • 想重配,页面点一下

指标:

项目 数值
空闲内存(工作模式) 18.7 KB
首页传输 3779 字节,0.15 秒
连续 10 次 API 请求 全部 200
渲染配网页时最低内存 10.1 KB(红线)

最后那行是提醒:10.1KB 是余量红线。如果以后要加继电器多路控制、数据图表、OTA,就得继续压页面体积,别再无脑往上堆 CSS 了。


附:7 个坑速查表

# 现象 真正原因 解法
1 无限重启,3 秒 29 次,rst cause:2 复位脉冲只保持 0.15s 改成 ≥0.3s
2 串口开头一小段乱码 ROM 层固定 74880,应用层 115200 正常现象,别当故障
3 烧完校验失败 / 起不来 没给 Flash 参数 显式 dout/40m/4MB
4 首页 500,串口一行英文异常 CSS 50% 撞了 Python % 格式化 用 {``{HOST}} + replace
5 MemoryError 单文件 >6~7KB,import 时在 RAM 编译 mpy-cross 预编译 .mpy
6 'bytes' has no attribute 'partition' MicroPython 砍了 CPython 便利方法 find() + 切片
7 页面只传回一半就断,curl exit 56 send() 只发一部分 sendall() / 循环发

附上这个小模块的小网页:

本文所有数据、日志、报错均来自一次真实的硬件调试过程,硬件为 ESP8266EX / 4MB Flash / CP210x,固件 MicroPython v1.29.0。


相关推荐
codigger2 小时前
前端没有被杀死,被杀死的是"只会把设计稿变成页面"
前端·ai·ai编程·编程语言·全栈·#人工智能
游鹄君2 小时前
lazyLLM官网文档无法访问,本地运行文档
ai
TomEval2 小时前
【测AI】第20篇:大模型安全测试 —— 越狱攻击与防护
人工智能·python·功能测试·ai·aigc·测试用例·安全性测试
Maynor9963 小时前
AI 表情包/热梗视频工具怎么选:RemixMeme 对比通用 AI 视频|remixmeme.com
人工智能·ai·短视频·tiktok·ai视频·表情包·remixmeme
Dola_Zou3 小时前
从二进制加固到离线鉴权 工业边缘AI安全防御架构
人工智能·安全·ai·架构·软件加密
严同学正在努力3 小时前
PostgreSQL 高可用教程(流复制 + Patroni 生产级实战)
运维·数据库·ai·postgresql·架构
平头哥~3 小时前
数字人主题化实践(十):婚庆策划主题的拱门对称布局与多模态感知、端到端编排
ai·ai编程
snow@li3 小时前
全景深度解析:大模型的思考力与内置知识库,是否天然绑定?
ai
mjhcsp3 小时前
DeepSeek V4.1 Flash 快速上手与实战指南
ai·语言模型·ai编程