"用 Python 开发 Arduino"的准确意思是——用 Python 从外面指挥 Arduino。做法是给 Arduino 烧一份官方固件(StandardFirmata),把它变成一块"哑巴 I/O 卡":自己不做任何决定,Python 说拉高就拉高。
8-3 的思路是"写 C++ 进芯片",本节换了一条路:芯片里跑的代码固定不变,逻辑全搬到 Python 里。 本节踩到的三个老熟人:PEP 668(6-5 那个"pip 装不了库"的坑)、venv(6-5 学的)、电位器接模拟脚(7-5 接过一模一样的东西)。 本节与 8-5 的区别要先分清:本节用现成的标准协议(Firmata),8-5 是自己订协议(纯串口收发)。前者省事、后者更灵活也更快。
一、先破一个误解:Python 能"直接跑"在 Arduino 上吗
不能——至少 Uno 系列不能。
8-1 讲过:Uno 只有 2KB 内存,而一个 Python 解释器本体就要几百 KB。装不进去。
所以"用 Python 和 Arduino 打交道"这件事,实际上有三条路,先把它们分清楚:
| 方式 | Python 跑在哪 | 芯片里跑什么 | Uno 能用吗 |
|---|---|---|---|
| ① 串口通信(pyserial) | 树莓派 | 你自己写的 C++ | ✅ 能(8-5 讲) |
| ② Firmata 协议(本节) | 树莓派 | 官方固件 StandardFirmata | ✅ 能 |
| ③ MicroPython | 就在芯片上 | Python 解释器 | ❌ Uno 不行 |
方式 ③ 要用什么板子? 内存够大的那些:
- Arduino Nano ESP32(官方支持 MicroPython)
- Arduino Nano RP2040 Connect
- 树莓派 Pico(严格说不是 Arduino,但同理)
"使用 Python 开发 Arduino 程序"——不是"在 Arduino 上用 Python 写程序",而是"用 Python 去指挥 Arduino"。 这个区别很关键,因为它决定了后面所有限制的来源:Python 那台机器(树莓派)必须在场。
二、思路:把 Arduino 变成一块"哑巴 I/O 卡"
打个比方:
8-3 的方式 = 你给一个工人写了张施工图,他自己照着干(Arduino 自己有逻辑)。 本节的方式 = 你把工人变成一只听话的手,你让他抬手他就抬手(逻辑全在你这边)。
技术上怎么实现:给 Arduino 烧一份官方提供的标准固件 StandardFirmata。它做的事情只有一件——一直监听串口,收到指令就照做:
Python 发指令:"把 13 脚拉高" → Arduino 拉高 13 脚
Python 发指令:"A0 现在多少?" → Arduino 读 A0 并把结果发回来
Firmata 就是一个协议:双方约定好"发这个字节代表做那个动作"。
代价是什么(三条,都要接受)
| 代价 | 说明 |
|---|---|
| 延迟 | 每次操作都要走一趟 USB 串口,实测 10~25 毫秒。点灯、转慢舵机没问题;高频操作就不行 |
| 离不开树莓派 | Arduino 自己不记得任何逻辑。树莓派一断、脚本一停,它就冻在那儿 |
| 不省事在"实时"上 | 本来用 Arduino 就是为了微秒级实时(8-1 讲的),结果所有决定都绕到树莓派再绕回来 |
换来什么? 不用写一行 C++,改逻辑不用重新上传。 而且——Python 那一整套生态(摄像头、网络、数据库、画图)都能直接调用。
三、动手:三步
3.1 第一步:把 StandardFirmata 烧进 Arduino
在 Arduino IDE 里:文件 → 示例 → Firmata → StandardFirmata,然后点上传。
用 arduino-cli 的话(8-2 装的那个),先找到示例路径:
arduino-cli sketch new ~/Arduino/MyFirmata # 先建个空程序占位
# 示例文件在核心包里,路径类似:
find ~/.arduino15 -name "StandardFirmata.ino" 2>/dev/null | head -3
找到后把那个文件复制进你的程序目录、改名成同名,再编译上传(或者干脆用 IDE 做这一步,这一步用图形界面更省事)。
看到什么算成功:上传过程无报错。烧完之后板载灯不再闪了(因为 StandardFirmata 什么也不主动做,它只是等命令)。
烧进去的固件会留在芯片里(断电不丢)。之后你改 Python 脚本一百遍,都不用再碰 Arduino。 这也正是这条路最舒服的地方。
3.2 第二步:在树莓派上装 Python 库
⚠️ 先回忆 6-5 那个坑:从 Bookworm 起,系统不允许直接 pip install 往全局装包(PEP 668),会报 externally-managed-environment。
所以走 venv:
python3 -m venv ~/arduino-env # 建一个虚拟环境
source ~/arduino-env/bin/activate # 激活它
pip install pyfirmata2 pyserial # 装库
看到什么算成功:提示 Successfully installed pyfirmata2-... pyserial-...
① 每次新开终端都要重新激活:source ~/arduino-env/bin/activate。忘了激活就会报 ModuleNotFoundError: No module named 'pyfirmata2'——不是没装,是你没进那个环境。
② 用 pyfirmata2 而不是 pyfirmata:原版 pyfirmata 基本已经停止维护,pyfirmata2 是活跃的现代分支(支持 Python 3.10+,异步处理与内存管理都改好了)。老教程里写 pip install pyfirmata,那是旧写法。
③ 这个环境只管这一章:以后要装别的库,还是建议按项目各自建 venv(6-5 讲过的习惯)。
3.3 第三步:跑第一个脚本(用 Python 点灯)
新建 py_blink.py:
from pyfirmata2 import Arduino
import time
board = Arduino('/dev/ttyACM0') # ← 端口名按你实际的改
try:
led = board.get_pin('d:13:o') # 数字脚 13,输出
for i in range(5):
led.write(1) # 高 → 灯亮
time.sleep(0.5)
led.write(0) # 低 → 灯灭
time.sleep(0.5)
finally:
board.exit() # 一定要收尾,否则串口不释放
跑:
python3 py_blink.py
看到什么算成功:板载 L 灯闪 5 次(快闪,每半秒一换),脚本跑完正常退出。
读结果:
| 值得注意的 | 为什么 |
|---|---|
| 一行 C++ 都没写 | 逻辑全在 Python 这边 |
| 不用重新上传 | 改这里的 0.5 就能改速度,Arduino 那边的固件一动不动 |
最后一行 board.exit() |
必须调用。不调用的话,串口不释放,下次跑脚本会报"端口被占" |
四、'd:13:o' 这个字符串怎么读
这是 pyfirmata 的核心写法,三段用冒号隔开:
'd' : 13 : 'o'
│ │ └── 模式:o = 输出 / i = 输入 / p = PWM 输出 / s = 舵机
│ └──────── 脚号
└──────────────── 类型:d = 数字脚 / a = 模拟脚
完整对照表:
| 想做什么 | 写法 |
|---|---|
| 13 脚输出(数字) | 'd:13:o' |
| 5 脚 PWM 输出(调亮度) | 'd:5:p' |
| 9 脚接舵机 | 'd:9:s' |
| 读 7 脚的高低(按钮) | 'd:7:i' |
| 读 A0 的电压 | 'a:0:i' |
用法:
led = board.get_pin('d:13:o')
led.write(1) # 输出:写 1=高、0=低、0.5=半亮(PWM 脚)
led.write(0.5)
btn = board.get_pin('d:7:i')
btn.read() # 输入:返回 True / False
pot = board.get_pin('a:0:i')
pot.read() # 模拟输入:返回 0.0 ~ 1.0 的浮点数
① 用原版 pyfirmata 时,必须调用 pot.enable_reporting()——不调用就永远读到 None(Arduino 默认不主动上报模拟值,得先告诉它"开始报")。
pyfirmata2 会自动处理这一步,所以上面的代码能直接用。
② 必须起一个后台线程(Iterator)——见下一节,这是本节最大的坑。
五、⚠️ 本节最大的坑:必须起 Iterator 线程
现象:读模拟量读到一堆 None/读数一直不动/脚本跑几十秒后崩掉。
原因:Arduino 会源源不断地把模拟值往串口推。如果 Python 不持续去读,数据就会在缓冲区里越堆越多——最后溢出,通信全乱。
解决:开一个后台线程专门负责"收信":
from pyfirmata2 import Arduino, util
board = Arduino('/dev/ttyACM0')
it = util.Iterator(board) # 建这个"收信员"
it.start() # 让他开始工作
这两行是模拟输入的标配,忘了就等着被坑。
串口是一个很小的缓冲区(几十到几百字节)。发得比读得快,它一定会塞满——这是所有串口程序的通病,不管是 Firmata 还是你自己写协议。 记住这条:"有人在不停地说话,你就必须有人在不停地听"。
六、动手 Demo:Python 点灯 + 读电位器
硬件:把电位器按 7-5 学的接法接到 Arduino 上(三个脚都要接:两端分别接 5V 和 GND,中间脚接 A0)。
为什么是 5V 而不是 3.3V?因为 Arduino 是 5V 逻辑的板子(8-1 讲过)。这次是接到 Arduino 上,不是接树莓派——别接错了板子。
代码(py_pot.py):
from pyfirmata2 import Arduino, util
import time
board = Arduino('/dev/ttyACM0')
it = util.Iterator(board) # 起"收信员"
it.start()
led = board.get_pin('d:13:o')
pot = board.get_pin('a:0:i') # A0,模拟输入
try:
print("转一转电位器,看看读数(Ctrl+C 结束)")
while True:
v = pot.read()
if v is not None:
volt = v * 5.0 # Arduino 的满量程是 5V
print(f"读数 {v:.3f} 约 {volt:.2f} V")
# 顺便让灯跟着亮灭:电压超过一半就亮
led.write(1 if v > 0.5 else 0)
time.sleep(0.3)
except KeyboardInterrupt:
print("\n结束")
finally:
board.exit()
看到什么算成功:
读数 0.000 约 0.00 V
读数 0.253 约 1.27 V
读数 0.508 约 2.54 V
读数 0.997 约 4.99 V
而且转电位器时数字在变,板载灯大致在"一半以上"亮起。
读结果(逐条对):
| 观察 | 解释 |
|---|---|
| 读数在 0.0 ~ 1.0 之间 | pot.read() 返回的是归一化后的小数(不是 0~1023 的原始值) |
| 乘 5.0 得到电压 | Arduino 的 A0 满量程是 5V(对比 7-5:树莓派那边接了 3.3V 的 MCP3008,所以是乘 3.3) |
| 只有十几个不同的值 | 每次读要走一趟 USB,10~25ms 延迟——这就是"用 Python 指挥"的代价 |
| 转得很快时读数跟不上 | 同上,不是坏了,是这条路本来就这样 |
转电位器 → 读电压 → 决定灯亮不亮——这就是一条完整的感知 → 判断 → 动作链路(7-5 末尾那个"电位器控灯亮度"是同一件事,只是这次"要动的那只手"变成了 Arduino)。 本章真正的价值就在这里:树莓派负责"想"(读数据、判断、以后还能联网、存数据库、跑摄像头),Arduino 负责"动手"。
七、30 秒实验:亲手量一量这条 USB 的延迟
本节一直在说"延迟 10~25 毫秒"——这个数字其实可以自己量出来。量完,你对"该不该走这条路"就有判断力了。
在 8-4 那个 venv 里跑(记得先激活):
import time
from pyfirmata2 import Arduino, util
board = Arduino('/dev/ttyACM0')
it = util.Iterator(board)
it.start()
pot = board.get_pin('a:0:i')
time.sleep(1) # 等它开始上报数据
t0 = time.perf_counter()
n = 0
while n < 200:
v = pot.read()
if v is not None:
n += 1
time.sleep(0.005) # ⚠️ 别读太快(下面解释)
used = time.perf_counter() - t0
print(f"读到 200 个有效值,用了 {used:.2f} 秒")
print(f"平均每个值 {used / 200 * 1000:.1f} 毫秒")
board.exit()
你会看到类似:
读到 200 个有效值,用了 4.30 秒
平均每个值 21.5 毫秒
读结果:
| 观察 | 解释 |
|---|---|
| 每个值要 20 毫秒上下 | 这个数就是"数据绕一趟 USB 再回来"的代价 |
| 和树莓派本地 GPIO 比 | 7-3 里读一次按钮是微秒级的——差了一万倍 |
为什么加了 time.sleep(0.005) |
⚠️ 这一条最反直觉:pot.read() 读的是 Python 这边的缓存值。不加延时疯狂读,你会反复读到同一个旧数——读得快 ≠ 数据新 |
| 数据到底多久更新一次 | 由 Arduino 上报的频率决定(Firmata 默认大约每 20 毫秒报一次),跟你 Python 读多快没关系 |
- 做界面、记数据、跟摄像头联动 → 20 毫秒完全无所谓,走这条路很划算。
- 控制舵机、步进电机 → 20 毫秒是灾难(舵机靠脉冲宽度定位,抖动会让它嗡嗡响)。这类活应该在 Arduino 上写 C++ 本地干(8-3 那条路)。
- 一句话原则:需要"快"的部分留在 Arduino 上,需要"想"的部分放到树莓派上。
八、什么时候该走这条路,什么时候别走
| 情况 | 用 Firmata(本节)合适吗 |
|---|---|
| 做个桌面控制台/数据记录/跟摄像头或网络联动 | ✅ 非常合适(Python 生态全能用上) |
| 只是想让灯闪、按钮有反应 | ✅ 合适,比写 C++ 省事 |
| 控制舵机、步进电机(要精确定时) | ❌ 不合适,延迟和抖动会让它抖个不停 |
| 要脱机独立运行(不插树莓派) | ❌ 不行,逻辑不在板子上 |
| 每秒要读几千次 | ❌ 不行,USB 延迟撑不住 |
一句话总结这条路:省事的代价是"离不开"和"慢一点"。
九、常见误区
-
"Python 可以直接跑在 Arduino Uno 上" —— ❌ Uno 不行(2KB 内存装不下解释器)。要在板子上跑 Python,选 Nano ESP32/Nano RP2040 Connect 那类。本节讲的是"从外面指挥"。
-
"不用给 Arduino 烧固件" —— ❌ 必须先烧
StandardFirmata。烧完之后板子就变成"哑巴执行器"了,这也是为什么烧完之后板载灯不再闪。 -
"
pyfirmata和pyfirmata2装哪个都行" —— 用pyfirmata2。原版基本停止维护,老教程里的pip install pyfirmata是旧写法。 -
"直接
pip install就行" —— ❌ 从 Bookworm 起会被 PEP 668 拦下(6-5 讲过)。必须建 venv,而且每次新开终端要重新 activate。 -
"读模拟量直接读就行" —— ⚠️ 两个条件:起
util.Iterator线程(不起会缓冲溢出)、原版 pyfirmata 还要enable_reporting()。 -
"这个延迟和本地 GPIO 差不多" —— 差得远。每次操作 10~25 毫秒(要绕一趟 USB),而树莓派本地 GPIO 是微秒级。这是本章最需要权衡的一个数。
-
"Arduino 会记住我写的逻辑" —— ❌ 在 Firmata 模式下它什么逻辑都没有(这正是"哑巴卡"的意思)。树莓派一断,它就冻在那儿。
-
"本节和 8-5 是一件事" —— 不是。本节用的是现成的标准协议(Firmata,规则别人订好了);8-5 是自己订协议(发什么字符、怎么分隔字段,你自己设计)。前者省事,后者灵活也更快。
-
"电位器两端也接 3.3V 吧" —— ⚠️ 本节是接在 Arduino 上,5V 逻辑,两端接 5V 和 GND。别和 7-5 接在树莓派/ADC 上的接法混了。
自测三问(点开对答案)
1. "用 Python 开发 Arduino 程序"这句话准确的含义是什么?为什么 Uno 不能让 Python 直接跑在上面? **参考答案**:准确含义是 **"用 Python 从外面指挥 Arduino"**:给 Arduino 烧官方固件 `StandardFirmata`,把它变成一块**哑巴 I/O 卡**,逻辑全在 Python 这边。**Uno 跑不了 Python 的原因**:它只有 **2KB 内存**,而 Python 解释器本体就要几百 KB,**根本装不进去**。想在板子上跑 Python,要换成 Nano ESP32/Nano RP2040 Connect/Pico 这类。 2. 读模拟量的时候,"必须起 Iterator 线程"是为了解决什么问题? **参考答案**:Arduino 会**持续不断地把模拟值往串口推**。Python 如果不持续读,数据会在**串口缓冲区**里越积越多,最后**溢出、通信全乱**(表现为读到一堆 `None`、读数不动、脚本崩掉)。`util.Iterator(board)` + `it.start()` 就是开一个**专用后台线程不停收信**。 3. 走 Firmata 这条路,最大的三个代价是什么? **参考答案**:① **延迟**——每次操作要绕一趟 USB,实测 **10~25 毫秒**,做舵机/步进电机这类精确定时就不行;② **离不开树莓派**——Arduino 自己不存任何逻辑,树莓派一断它就不动了;③ **丢掉了 Arduino 最大的长处**——本来是冲着"微秒级实时"才用它的,结果所有决定都绕到树莓派再绕回来。换来的好处是**不用写 C++、改逻辑不用重新上传、Python 生态全能用**。- ☐ 下一节(8-5 在树莓派使用 Python 建立串口通信)
相关笔记
- 使用 Arduino IDE 建立 Arduino 程序 —— 上一节:那条"改完要重新上传"的路,本节换成了"只改 Python 一行"
- 在树莓派使用 Python 建立串口通信 —— 下一节:不用 Firmata,自己在串口上订协议,讲透"通话线路"
- Python 函数与模块 —— 本节三个坑的出处:PEP 668、venv 必须激活、装库的两条正路
- 模拟输入 —— 同一颗电位器、同一件"读电压"的事,那里接在树莓派+MCP3008 上,本节接在 Arduino 上
- 认识 Arduino Uno 开发板 —— 2KB 内存、"有 ADC"这两条,是本节能成立的前提
- 🍓 树莓派 · 索引 —— 全课路线图