Python 这东西,很多人一开始都以为它只能写写爬虫、跑跑数据处理。但你真玩起硬件就会发现,它才是控制 Arduino 和树莓派最顺手的语言,没有之一。我见过不少做嵌入式出身的人习惯了 C/C++,觉得 Python 性能不行,可实际上对于绝大多数传感器读取、电机控制、LED 闪烁这类场景,Python 的爆发力完全够用,而且开发效率比写底层代码高一截。
我自己的项目经历里,既有用 Python 通过串口控制 Arduino 做机械臂动作,也有直接在树莓派上用 GPIO 控制继电器、接温湿度传感器做智能家居。算下来踩过的坑不少,但把思路捋顺之后,这两条路线其实都不复杂。这篇就把我的实操经验完整写出来,不绕弯子,从连接方式到代码细节到常见问题,一次讲透。
1. 先搞清楚 Python 和硬件之间到底怎么“说话”
很多新手最容易懵的地方,不是不会写 Python,而是搞不明白 Python 里的变量怎么变成物理世界里的电压和动作。其实本质就一句话:Python 只是一个发号施令的指挥官,硬件那边需要一个“翻译”来理解指令并执行动作。
1.1 Arduino 和树莓派的本质区别
Arduino 是一块微控制器开发板,核心是一颗主频几十到几百 MHz 的 MCU,它上面的引脚可以直接输出高低电平、读取模拟量、产生 PWM 波。它自己不能跑操作系统,程序是烧录在 Flash 里的,上电后只能执行预置的程序。
树莓派则是一台微型电脑,它有完整的 Linux 系统(通常用官方 Raspberry Pi OS),有 CPU、内存、USB 口、HDMI 口,跟一台小主机没区别。它能跑 Python 解释器,能装各种库,甚至能跑 Web 服务、数据库、Docker。
所以问题就来了:
- 控制 Arduino 时,Python 通常跑在电脑或树莓派上,通过串口向 Arduino 发送指令,Arduino 里的固件收到指令后控制引脚。
- 控制树莓派时,Python 可以直接运行在树莓派上,通过 GPIO 库访问硬件引脚,不需要额外的“翻译”。
这两种模式我下面会分别细讲。先记住这个基本区分,后续所有操作思路都建立在它上面。
1.2 控制链路里的三个关键角色
无论你控制哪块板子,链路里都有三个角色:
第一个是“指令源”。在 Arduino 方案里,指令源是 PC 或者树莓派上运行的 Python 脚本;在树莓派方案里,指令源就是树莓派上跑的 Python 脚本本身。
第二个是“执行器”,也就是 Arduino 或树莓派的引脚。Arduino 的引脚由板载固件管理,树莓派的引脚由系统内的 GPIO 库直接操作。
第三个是“通信介质”。Arduino 方案用的是 USB 串口(虚拟串口),或者蓝牙、网络转串口;树莓派方案则是直接访问 /dev 下的内存映射区或者文件系统接口。
这三个角色想清楚了,后面写代码就不会乱。我自己最早就是没分清“Python 到底运行在哪”,导致写串口代码时在 Arduino 和 PC 两边来回纠结,后来一句话总结:Python 在哪跑,哪边就是大脑。
2. 用 Python 控制 Arduino:串口通信与 Firmata
Python 控制 Arduino 最成熟、最容易上手的方式是串口通信。你也可以安装 Firmata 固件让 Python 更高级地直接控制引脚,但底层依然是串口。
2.1 最基础的方式:Python 发送串口指令
这个方案需要 Arduino 端先烧录一个跟随指令的固件,比如让云台机接收串口发来的角度值并转舵机。Arduino 端不需要装 Python,只负责解析串口数据。
Arduino 端示例代码(C++):
#include <Servo.h> Servo myServo; int angle = 90; void setup() { Serial.begin(9600); myServo.attach(9); } void loop() { if (Serial.available() > 0) { angle = Serial.parseInt(); angle = constrain(angle, 0, 180); myServo.write(angle); Serial.print("Angle set to "); Serial.println(angle); } delay(50); }Python 端(PC 或树莓派上)使用 pyserial 发送角度值:
import serial import time ser = serial.Serial('/dev/ttyUSB0', 9600, timeout=1) time.sleep(2) # 等待 Arduino 重启完成 ser.write(b'90\n') response = ser.readline() print(response.decode().strip()) ser.write(b'135\n') response = ser.readline() print(response.decode().strip()) ser.close()这里有几个关键点:
- 串口波特率必须一致,Arduino 和 Python 都设成 9600。
- Arduino 上电后会复位,所以 Python 连接后最好延时 2 秒再发指令,否则第一条数据往往会被复位时序吃掉。
- 发送指令末尾的
\n很重要,Arduino 的Serial.parseInt()会跳过非数字字符,遇到换行符可以正确结束解析。
这种方案的优点是代码简单、可控性高,缺点是你必须在 Arduino 里维护一套自定义协议。如果以后要控制很多引脚、传感器,每个功能都写解析逻辑会比较烦。这时就该用 Firmata。
2.2 更高级的方式:Firmata 固件
Firmata 是一个通用的协议,Arduino 烧录好 StandardFirmata 固件后,Python 就可以通过 pyfirmata 库直接读写引脚、读取模拟值、输出 PWM。相当于把 Arduino 变成一块“USB 接口的 IO 板”,引脚级操作全交给 Python 控制。
Arduino 端只需要一步:在 Arduino IDE 里打开示例代码Firmata > StandardFirmata,选中板子型号和串口号,编译烧录进去。
Python 端安装:
pip install pyfirmata然后就可以这样控制:
from pyfirmata import Arduino, util import time board = Arduino('/dev/ttyUSB0') # 初始化迭代器,让模拟值持续读取 it = util.Iterator(board) it.start() board.analog[0].enable_reporting() # 控制数字引脚 13 输出高电平 board.digital[13].write(1) time.sleep(2) board.digital[13].write(0) # 读取模拟引脚 A0 的值 value = board.analog[0].read() print('Analog value:', value) board.exit()Firmata 方案最大好处是不用反复改写 Arduino 代码,Python 里直接控制引脚就行。但要注意两点:
- 同一个串口同时只能被一个程序占用,不能 Arduino IDE 的串口监视器开着又让 Python 连。
- Firmata 协议本身有少量延迟,对于毫秒级精确时序控制(比如步进电机高速脉冲)不适合,那种场景还是写原生的 Arduino C 代码更稳。
2.3 串口不稳定怎么办
使用 pyserial 控制 Arduino 时,经常会遇到的坑有乱码、卡死、连接失败。
乱码通常是因为波特率不一致。解决方法是固定一套常用参数,比如 9600 和 115200 二选一,不要混用。
连接失败常见原因是串口号变化。Linux 下插拔 USB 后设备号可能从/dev/ttyUSB0变成/dev/ttyUSB1,所以动态检测端口很有必要:
import glob def find_arduino(): for port in glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*'): try: ser = serial.Serial(port, 9600, timeout=1) ser.close() return port except: continue return None卡死则往往是 Arduino 端Serial.print的数据量太大,缓冲区溢出导致。这时候要么降低打印频率,要么加大 Python 读取的 timeout,不要盲目提高波特率。
我还遇到过一种情况:Arduino 串口重启后,Python 发送的是b'90',但 Arduino 还在启动初始化,数据就丢了。稳妥做法是加一个握手协议,Python 先发ready?,Arduino 收到后回复ok,两边对上了再开始正式指令。这个习惯能帮你省掉很多莫名其妙的临时 bug。
3. 用 Python 控制树莓派:GPIO 直控与万物互联
树莓派上 Python 直接操作 GPIO,最常用的是 RPi.GPIO 和 gpiozero 这两个库。前者更底层、直白,后者封装得更好,适合快速开发。
3.1 RPi.GPIO 基础控制
安装系统自带的 RPi.GPIO 通常已经有了,如果没有:
sudo apt update sudo apt install python3-rpi.gpio控制 LED 闪烁的完整例子:
import RPi.GPIO as GPIO import time LED_PIN = 17 GPIO.setmode(GPIO.BCM) GPIO.setup(LED_PIN, GPIO.OUT) try: while True: GPIO.output(LED_PIN, GPIO.HIGH) time.sleep(1) GPIO.output(LED_PIN, GPIO.LOW) time.sleep(1) except KeyboardInterrupt: pass finally: GPIO.cleanup()GPIO.CLEanup()这个函数非常重要。很多新手在 Ctrl+C 中断后程序退出,但引脚还是保持高电平,结果 LED 一直亮着。执行 cleanup 后会把引脚恢复为默认输入状态,避免可能损坏外设。
需要注意 RPi.GPIO 的 pin 编号有两种模式:GPIO.setmode(GPIO.BCM)是按芯片引脚编号,GPIO.setmode(GPIO.BOARD)是按物理引脚编号。我建议统一用 BCM,因为树莓派不同型号的物理引脚位置可能变化,但 BCM 编号相对稳定(当然也要注意不同型号的差异)。
3.2 gpiozero 更人性化
gpiozero 是树莓派官方推荐的库,封装了大量传感器和电机的接口,写起来更像面向对象编程:
from gpiozero import LED, Button from signal import pause led = LED(17) button = Button(2) button.when_pressed = led.on button.when_released = led.off pause()按钮按下 LED 亮,松开灭,三行代码搞定。gpiozero 里还有很多现成组件,比如Servo、Motor、DistanceSensor、DHT11等,很适合快速搭建原型。
我实际在做智能家居小项目时偏爱 gpiozero,因为它的线程模型已经处理好了回调机制,不用自己写事件循环。但如果你要做极高性能、纳秒级精度的波形输出,gpiozero 或者 RPi.GPIO 都不够,得直接操作/dev/mem或者用 C 写扩展库。
3.3 权限问题与系统配置
树莓派 GPIO 操作最大的坑是权限。默认用户名pi不一定在gpio组里,或者有些精简系统根本没有这个组。运行 Python 脚本时报No access to /dev/mem大概率是权限问题。
把当前用户加入 gpio 组:
sudo usermod -a -G gpio your_username改完之后需要重新登录或者重启才会生效。如果急着调试,可以先用sudo python3跑,但别养成习惯,后面做服务时用 sudo 会有各种环境变量问题。
另外注意$ sudo raspi-config里打开 I2C 和 SPI 接口:
sudo raspi-config在 Interface Options 里打开 I2C、SPI、Serial Port。如果要用 I2C 传感器,Python 端还需要安装:
sudo apt install python3-smbus很多新手装上 smbus 后import报错,多半是系统 i2c 设备没开启,检查/dev/i2c-1是否存在,不存在就去 raspi-config 里打开。
3.4 用树莓派作为 Web 控制中枢
树莓派最经典的玩法就是跑一个 Flask 服务,通过网页来控制 GPIO。这里给一个能直接跑的最小例子:
from flask import Flask, request import RPi.GPIO as GPIO app = Flask(__name__) LED = 17 GPIO.setmode(GPIO.BCM) GPIO.setup(LED, GPIO.OUT, initial=GPIO.LOW) @app.route('/led', methods=['POST']) def led_control(): state = request.json.get('state') if state == 'on': GPIO.output(LED, GPIO.HIGH) return {'status': 'ok', 'state': 'on'} elif state == 'off': GPIO.output(LED, GPIO.LOW) return {'status': 'ok', 'state': 'off'} else: return {'status': 'error'}, 400 if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)启动后局域网内其他设备就能通过POST http://树莓派IP:5000/led控制 LED 了。对应 Python 客户端:
import requests response = requests.post('http://192.168.1.50:5000/led', json={'state': 'on'}) print(response.json())这个模式非常适合做智能家电改造,把树莓派接到继电器上,再用手机网页发指令就能开关灯、开关风扇。需要注意的是 Flask 默认开发服务器对并发支持一般,真在家里常年运行建议用 gunicorn 或者在前面加 nginx 反向代理。
4. Arduino 还是树莓派:两种控制方式的选型心得
很多人纠结到底买 Arduino 还是树莓派,其实这两个不是竞争关系,而是配合关系。我做过几个项目后的经验是:实时性强、引脚要求高、成本敏感的选 Arduino;需要跑复杂逻辑、联网交互、图像处理的选树莓派。
4.1 场景一:纯传感器采集与控制
如果你只是想做一个温度采集装置,每隔一秒读一次温湿度并显示,Arduino 更合适。它上电就运行,不用等系统启动,功耗低,代码逻辑简单,一个 Uno 板子加传感器几十块钱就能搞定。
Python 作为上位机,通过串口读取 Arduino 发来的数据,再写入数据库或者做成图表,这个组合在科研实验室里很常见。
4.2 场景二:视觉识别与决策
如果你需要摄像头识别一个物体,然后根据识别结果控制电机转动,那就必须上树莓派。OpenCV、TensorFlow Lite 这些库只有树莓派能跑得动,Arduino 的 MCU 根本吃不消。
我之前做一个模拟垃圾分类 Demo,就是用树莓派跑一个图像分类模型,识别结果通过 GPIO 控制舵机把“垃圾”分到对应区域。识别逻辑跑在 Python 里,控制舵机也跑在 Python 里,整个流程非常顺。
4.3 场景三:两者协作
高级玩法是 Arduino 负责底层运动控制和传感器实时采集,树莓派负责上层决策和通信。例如一个机器人小车:树莓派跑目标检测,根据图像帧计算出左右轮速,然后通过串口把速度值发给 Arduino,Arduino 立刻输出 PWM 给电机驱动。
这种方式的好处是利用各自强项,同时避免 Arduino 端复杂数据处理和树莓派端的硬实时短板。通信协议可以用最简单的两个字节:第一个字节是左轮速,第二个字节是右轮速,再加上帧头和校验,基本不会出错。
我自己强烈推荐新手先从“Python + Arduino 串口”开始,等熟悉了基础 GPIO 再接树莓派做 Web 控制。一上来就用树莓派直控 GPIO,容易因为系统层面的权限、依赖、配置问题打击信心。
5. 实操中常踩的坑与排查技巧
这一部分全是我真实记录下来的问题,每一条都让我记忆深刻。
5.1 串口号漂移与 udev 规则
USB 串口设备每次插拔可能变化,比如连接时是 ttyUSB0,下次变成 ttyUSB1。解决方案就是通过串口设备的 idVendor 和 idProduct 写 udev 规则,固定设备别名。
先用lsusb查看设备 ID,例如:
lsusb输出里有Bus 001 Device 004: ID 1a86:7523,前面是 VID,后面是 PID。然后创建规则:
sudo nano /etc/udev/rules.d/99-arduino.rules内容写:
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", SYMLINK+="arduino"之后插上设备,系统中就会多出一个稳定的/dev/arduino链接,Python 代码里直接用这个路径,再也不用担心号漂移。
5.2 Arduino 不响应指令的排查流程
如果你发了指令 Arduino 没反应,按顺序排查:
- 确认 Arduino 端程序真的烧录成功了(用串口监视器手动发数据测试)。
- 确认 Python 连接的串口号正确。
- 确认 USB 线不是“只充电不传数据”的线。
- 确认波特率一致。
- 确认 Arduino 因复位导致首条数据丢失。
其中 USB 线的问题最隐蔽。有些劣质线材只有电源线没有数据线,根本连不上。我踩过一次,浪费了半天才发现线材不对。
5.3 树莓派 Python 脚本开机自启
想让 GPIO 控制程序随树莓派启动,我推荐 systemd service,而不是 rc.local。因为 systemd 可以管理依赖关系、日志和重启策略。
创建 service 文件:
sudo nano /etc/systemd/system/gpio.service内容:
[Unit] Description=GPIO Control Daemon After=network.target [Service] ExecStart=/usr/bin/python3 /home/user/led_server.py WorkingDirectory=/home/user Restart=always User=your_username [Install] WantedBy=multi-user.target然后:
sudo systemctl daemon-reload sudo systemctl enable gpio.service sudo systemctl start gpio.service这里有一个重要细节:如果 service 里用User=your_username,那么你对 GPIO 的权限必须在系统里已配置好,否则会有权限错误。用 root 跑虽然省事但不够干净,出现问题时排查也更费劲。
5.4 PWM 精度问题
树莓派的软件 PWM 由系统调度,精度受系统负载影响,在无负载情况下可以输出 1kHz 左右比较稳定的波形。如果需要更高频率或更高精度,优先用专用 PWM 芯片或者 Arduino 硬件 PWM,而不是树莓派软件硬抗。
Arduino 的analogWrite内置 PWM 频率大约 490Hz,8bit 精度,控制舵机时使用Servo库可以达到 50Hz 刷新率,这个组合基本能满足绝大多数舵机应用。
我自己试验时发现,树莓派上使用 gpiozero 的LED调光效果还挺平滑,但同一时刻 CPU 高负载时亮度会有轻微抖动。所以做调光灯,树莓派可以用在“节奏慢”的场景,如果是高速频闪,我绝对会选择 Arduino。
5.5 电气安全与逻辑电平匹配
最后提醒一个硬件层面的坑:Arduino 和树莓派的 GPIO 都是 3.3V 或 5V 逻辑。Arduino Uno 的 IO 是 5V,树莓派 GPIO 是 3.3V,两者直接互连可能会让树莓派引脚过压损坏。
所以树莓派和 Arduino 之间通信时,要么用 USB 串口,要么在 GPIO 之间加电平转换模块(常见的有 TXS0108E 或者分压电阻)。千万别拿树莓派 GPIO 的 3.3V 直接驱动需要 5V 电平的传感器,也不要让 5V 信号直接灌进树莓派引脚。
我以前为了省事直接拿杜邦线把树莓派和 Arduino 连起来,结果树莓派 GPIO 口烧了一个,后来都乖乖用串口或者加转换板。硬件这条路,有些侥幸心理不能有。
写在最后的实操体会
我自己折腾这俩板子好几年,最大的感受就是:Python 控制硬件并不神秘,说白了就是“发数据”和“读数据”两件事。Arduino 那边串口收发,树莓派这边 GPIO 读写,剩下的就是把这两套逻辑组合成你想要的自动化功能。
中间那些坑都不是编程难题,反而更像工程问题:检查 USB 线、检查权限、检查波特率、检查固件有没有烧对。只要你把基础链路打通,剩下的事情就是熟练度的累积。
如果想往深处走,我个人建议后续可以试试用 MicroPython 直接刷到一些支持它的开发板上,语法和 Python 一致但直接跑在 MCU 上,又是一种玩法。或者给树莓派加一个 MQTT 协议,让手机、PC、传感器之间通过消息队列通信,自动化能力会上升一个档次。这些都是在掌握了基础控制之后水到渠成的方向,慢慢玩,总会豁然开朗。