1. 为什么人人都说“用Python玩硬件”到底怎么玩先把这个事情说清楚。很多人一看到“Python控制Arduino或树莓派”这个标题脑子里是懵的Arduino本身用C/C写程序树莓派装的是Linux系统Python跟这俩到底什么关系我当年入坑的时候也绕了不少弯子这里直接给你捋明白。核心就一句话Python不是直接跑在Arduino上至少现在主流的Arduino Uno跑不了完整的Python解释器Python是在你的电脑上、或者树莓派上通过串口、网络或者GPIO去控制这些硬件。所以这个标题实际上包含了两条完全不同的技术路线路线A电脑上的Python Arduino开发板。Python通过USB串口和Arduino通信Arduino负责执行底层硬件动作点灯、读传感器、转舵机Python负责上层逻辑数据处理、算法决策、界面展示。路线B树莓派上的Python直接操作GPIO。树莓派本身就是一台小电脑跑完整LinuxPython直接读引脚、写引脚不需要中间再挂一个单片机。这两条路线解决的问题不同适用场景也不同但核心诉求是一样的用Python这套舒服的语法和生态去驱动原本需要底层C语言才能操作的硬件。适合谁想学硬件但不想啃C语言的新手做物联网原型验证的工程师搞机器人、智能小车、自动化项目的爱好者以及任何想把“程序跑起来”和“硬件动起来”连在一起的人。我接下来的内容会先把两个平台的选择逻辑讲清楚然后分别给出两条路线从0到1的完整实操步骤最后把我踩过的坑全部抖出来。你要做智能小车、舵机控制、传感器采集这篇内容都能直接抄作业。2. 先想明白你该用Arduino还是树莓派2.1 两个平台的本质区别选平台之前必须搞清楚一个关键点Arduino是单片机树莓派是Linux电脑。这不是一个量级的东西使用方式完全不同。Arduino Uno这种板子本质上是一个带CPU、内存和输入输出引脚的微型控制器。它不跑操作系统程序被编译成二进制烧进去上电就一遍遍地循环执行。它的优势在于便宜几十块钱、实时性强、功耗极低、引脚可以直接输出PWM波形、稳定性极高。但缺点也很明显没有操作系统、不能跑Python至少Uno型号不行、内存以KB计、做不了复杂计算。树莓派则是一台完整的ARM架构Linux主机。它可以跑Python解释器可以装numpy、opencv这种重量级库可以连接摄像头做图像识别甚至可以接显示器当桌面电脑用。但它是“准实时”的因为有Linux系统的调度GPIO翻转的时序抖动比Arduino大而且功耗和成本都高一个数量级。我列个表方便你选型对照对比维度Arduino Uno树莓派4B/5价格20-50元200-500元是否跑操作系统否裸机循环是Linux能否直接跑Python否需借助MicroPython等特殊固件是官方支持GPIO数量与能力数字引脚多可直接PWMGPIO较少部分引脚复用实时性微秒级很硬毫秒级有抖动适合任务单点控制、传感器采集、电机驱动视觉处理、复杂逻辑、Web服务功耗mA级数瓦级2.2 什么时候用“Python Arduino”如果你做的是纯硬件控制型项目比如智能小车、舵机云台、温湿度采集、步进电机控制请优先考虑Arduino然后用Python在上位机做控制和数据处理。我见过太多人犯这个错误明明只需要让舵机转到指定角度、让小车走个PID巡线非要用树莓派。结果树莓派启动要30秒GPIO输出PWM还要靠软件模拟实时性一塌糊涂最后项目又臭又长。这种活儿Arduino干得又稳又便宜实时性是它的看家本领。但为什么还要用Python因为Arduino的C语言写复杂逻辑太痛苦了。比如你要做一个小车Arduino负责底层电机PID控制和传感器读取但路径规划、视觉识别、人机交互这些算法放在Arduino上根本跑不动。这时候Python在上位机电脑或树莓派负责“大脑”Arduino负责“手脚”通过串口协议握手通信这是最经典的分工。2.3 什么时候直接用“树莓派 Python”如果你的项目核心是计算密集或者依赖Linux生态直接用树莓派。典型场景包括摄像头图像识别需要装OpenCV、Web控制界面需要跑Flask/Django、MQTT物联网网关、语音识别、大数据量的传感器记录和分析。树莓派对Python的支持是原生的这一点太爽了。你直接在树莓派上pip install装库用gpiozero或RPi.GPIO读写引脚代码风格跟写普通Python程序一模一样。树莓派5的GPIO引脚定义网上资料很多但核心注意点是树莓派的GPIO电平是3.3V不是Arduino的5V接错会烧板子。这个细节后面实操部分展开讲。我的建议是别纠结“哪个更好”而是看你手里项目的主要矛盾在哪个方向。如果主要是硬件控制选Arduino如果主要是软件计算选树莓派如果两者兼有就做串口联动。这也是工程师最常用的方案。3. 路线A实操用Python通过串口控制Arduino3.1 通信原理Python和Arduino怎么“对话”Python和Arduino之间最通用可靠的通信方式就是串口UART over USB。Arduino开发板上的USB口内部通过USB转串口芯片Uno上是CH340或ATmega16U2把你的USB数据变成了串口数据。Arduino的Serial库负责收发Python用pyserial库收发两边通过相同的波特率baud rate对上暗号就能通信了。一句话类比串口就像两个人用一台对讲机通话双方必须先约定好同一个频率波特率说话时按下一个键发送说完松开关闭。如果频率对不上听到的全是电流杂音乱码。通信协议设计是这个环节的命门。我强烈建议你在项目一开始就定义好协议格式别图省事先随便发字符串。最简单的协议是以换行符\n结尾的ASCII文本比如SERVO:90\n表示设置舵机到90度LED:ON\n表示开灯。这样Arduino收到一行就解析一行Python发送时也在字符串末尾加\n。优点是直观、好调试用串口监视器一眼能看懂。缺点是效率低但控制类项目完全够用。如果你的项目数据量大、需要做二进制协议比如传输图片、浮点数组那就另说。但初学者从文本行协议入手绝对是最稳的路径。我在自己的智能小车项目里就是用文本协议几十个命令下来代码清爽排查问题也快。3.2 Arduino端程序设计接收、解析、执行Arduino端的代码逻辑非常简单清晰setup()里初始化串口loop()里循环读串口有数据就解析执行。下面是完整的舵机控制示例用的标准库Servo.h// Arduino 端代码通过串口接收指令控制舵机 #include Servo.h Servo myservo; int servoPin 9; // 舵机信号线接 9 号引脚 int pos 0; void setup() { Serial.begin(9600); // 和 Python 端波特率保持一致 myservo.attach(servoPin); Serial.println(READY); // 上电后向上位机报告状态 } void loop() { if (Serial.available() 0) { String cmd Serial.readStringUntil(\n); // 读一行以换行符结尾 cmd.trim(); // 去掉多余回车和空格 if (cmd.startsWith(SERVO:)) { // 解析 SERVO:90 这种格式提取角度值 String valueStr cmd.substring(6); // 从索引6开始截取 int angle valueStr.toInt(); // 限制角度范围保护舵机 angle constrain(angle, 0, 180); myservo.write(angle); Serial.println(OK:ANGLE String(angle)); } else if (cmd LED:ON) { digitalWrite(LED_BUILTIN, HIGH); Serial.println(OK:LED_ON); } else if (cmd LED:OFF) { digitalWrite(LED_BUILTIN, LOW); Serial.println(OK:LED_OFF); } else { Serial.println(ERR:UNKNOWN_CMD); } } }这里有几个关键点要特别解释一下。readStringUntil(\n)会一直阻塞等待直到收到换行符才返回这保证了一次读取一整条指令不会出现半条指令的问题。trim()的作用是去掉Windows系统发过来的\r回车符否则解析会出错这个坑我当年踩得真够呛。startsWith和substring(6)是字符串解析的常用组合虽然性能一般但可读性极高初学者一眼就能懂。Arduino端写完这段代码直接用Arduino IDE编译上传。上传成功后打开IDE自带的串口监视器波特率选9600手动输入SERVO:90发送如果舵机转到了90度说明Arduino端工作正常下面就可以接Python了。3.3 Python端实现用pyserial发送指令和读取反馈Python端的事情就两件往串口发指令、从串口读反馈。核心库是pyserial安装方式pip install pyserial然后写一个简单的控制脚本先做连通性测试再说其他# Python 端代码通过串口控制 Arduino 舵机并读取反馈 import serial import time def open_serial(port: str, baudrate: int 9600, timeout: float 1.0): 打开串口自动处理常见异常 try: ser serial.Serial( portport, baudratebaudrate, timeouttimeout, # 读超时避免卡死 write_timeout1.0 ) time.sleep(2) # 等待 Arduino 重置完成这是关键 # 清空缓冲区把 Arduino 上电时打印的乱序信息清掉 ser.reset_input_buffer() print(f[OK] 串口 {port} 打开成功) return ser except serial.SerialException as e: print(f[ERR] 无法打开串口 {port}请检查是否被占用或设备是否连接) raise e def send_command(ser, cmd: str, wait_response: bool True): 发送指令并等待 Arduino 反馈 # 拼上换行符按文本协议发送 ser.write((cmd \n).encode(utf-8)) if wait_response: # 读取一行反馈decode 时去掉结尾的换行和回车 resp ser.readline().decode(utf-8, errorsignore).strip() return resp return None if __name__ __main__: # Windows 上写 COM3Linux/macOS 上写 /dev/ttyUSB0 或 /dev/ttyACM0 ser open_serial(COM3) # 先测试舵机控制 for angle in [0, 45, 90, 135, 180]: resp send_command(ser, fSERVO:{angle}) print(f发送 SERVO:{angle} - 反馈: {resp}) time.sleep(1) # 测试LED开关 print(send_command(ser, LED:ON)) time.sleep(1) print(send_command(ser, LED:OFF)) ser.close()这里有几个细节值得多说两句。time.sleep(2)这个等待很重要因为Arduino开发板每次打开串口连接都会自动复位重启这是Arduino的一个特性跟板载串口芯片的DTR信号有关重启期间程序不会执行如果你立刻发指令数据就丢了。等两秒让它稳定运行再通信是无数人用血泪换来的经验。reset_input_buffer()会把Arduino重启过程中打印的无关信息比如咱们代码里那句READY但实际上可能出现在任意时机清掉确保后面每一条反馈都对应你发的指令。write_timeout参数也别忘了加否则当串口出现阻塞时write()会无限等待把程序卡死。这种卡死问题排查起来特别隐蔽我是吃过亏的。encode(utf-8)是把Python字符串转成字节流decode时用errorsignore处理偶尔的乱码字节可以避免程序因为一个非UTF-8字节而崩溃。这段代码输出长这样[OK] 串口 COM3 打开成功 发送 SERVO:0 - 反馈: OK:ANGLE0 发送 SERVO:45 - 反馈: OK:ANGLE45 发送 SERVO:90 - 反馈: OK:ANGLE90 发送 SERVO:135 - 反馈: OK:ANGLE135 发送 SERVO:180 - 反馈: OK:ANGLE180 发送 LED:ON - 反馈: OK:LED_ON 发送 LED:OFF - 反馈: OK:LED_OFF能跑到这个效果你已经在“用Python控制Arduino”这条路上走通了。这个基础框架可以无限扩展加传感器就是Serial.println(sensorValue)让Python读加多个电机就是定义MOTOR:LEFT:150这类协议加PID控制就是在Python端计算好轮速再发下去执行。3.4 进阶Python定时采集Arduino传感器数据舵机控制是“Python发指令、Arduino执行”传感器采集则是反过来“Arduino上报数据、Python接收处理”。这两种模式往往在一个项目里同时出现。下面这个例子演示Arduino每隔100毫秒读取一个模拟引脚并通过串口发送给PythonPython负责实时绘图记录。Arduino端代码模拟引脚A0读取void setup() { Serial.begin(9600); } void loop() { int sensorValue analogRead(A0); // 读取A0引脚范围0-1023 Serial.print(SENSOR:); Serial.println(sensorValue); delay(100); // 每100ms上报一次 }Python端接收并绘图# 需要安装 matplotlibpip install matplotlib import serial import matplotlib.pyplot as plt import matplotlib.animation as animation ser serial.Serial(COM3, 9600, timeout1) all_data [] def update(frame): global all_data try: line ser.readline().decode(utf-8, errorsignore).strip() if line.startswith(SENSOR:): value float(line.split(:)[1]) all_data.append(value) # 只保留最近200个点 if len(all_data) 200: all_data.pop(0) except Exception as e: print(f读取异常: {e}) plt.cla() # 清空当前坐标轴 plt.plot(all_data) plt.ylim(0, 1023) plt.xlabel(Sample) plt.ylabel(ADC Value) ani animation.FuncAnimation(plt.figure(), update, interval100) plt.show()这个例子的核心意义在于告诉你Python控制硬件不只是“发指令”更重要的是把硬件变成数据源让Python做数据分析或者可视化。很多传感器项目都可以套用这个模式温度曲线、光照强度、PM2.5数据、电机电流监测统统是这个框架。4. 路线B实操用Python直接在树莓派上控制GPIO4.1 树莓派GPIO基础和库选型树莓派跑着完整的Linux系统所以“控制”的方式和Arduino完全不同。不需要中间通信Python直接操作硬件引脚。这相当于把控制逻辑和数据采集都放在同一个程序里方便快捷但也要注意Linux系统的非实时性问题。树莓派端最常用的两个Python库RPi.GPIO老牌库文档丰富很多教程都基于它。但需要sudo权限运行而且官方维护状态一般。gpiozero官方推荐的库设计更现代API更友好不需要sudo或者仅对某些功能需要视系统版本而定内置了大量设备类比如LED、Button、Servo、Motor开箱即用。我的建议是新项目优先用gpiozero代码更简洁坑更少。但有时候查资料会遇到RPi.GPIO的例子所以两个都要知道怎么用。树莓派5的GPIO引脚定义和4B不一样了网上经常见到“树莓派5引脚图”的搜索但核心接口保持不变40针排针大部分库在5代上也能用只是部分PWM硬件能力有调整选型时要留意。下面这个例子是树莓派上点LED灯用gpiozero写# 在树莓派上先安装库 sudo apt update sudo apt install python3-gpiozero python3-pip -y pip install gpiozero# LED闪烁树莓派上直接运行 from gpiozero import LED import time # GPIO17是BCM编号方式对应物理排针的第11脚 led LED(17) try: while True: led.on() time.sleep(1) led.off() time.sleep(1) except KeyboardInterrupt: pass finally: led.close() # 释放GPIO资源跑这个程序前记得把LED接到树莓派引脚上LED阳极长脚通过220欧姆电阻接到GPIO17阴极短脚接GND物理引脚第6脚。不接限流电阻直接烧LED或者伤引脚这是新手最常见的操作事故。gpiozero读按钮输入也一样简单from gpiozero import Button, LED import time # 按钮接GPIO18LED接GPIO17作为提示灯 button Button(18, pull_upTrue) # 启用内部上拉电阻 led LED(17) def on_press(): print(按钮被按下) led.on() def on_release(): print(按钮释放) led.off() button.when_pressed on_press button.when_released on_release # 保持程序运行 time.sleep(60)看到了吗gpiozero把事件回调都封装好了代码直观得可怕。这里面有个重要的物理细节树莓派GPIO内部有上拉/下拉电阻可以启用push按钮的一边接GPIO另一边接GND启用内部上拉后按下时引脚读到低电平释放时读到高电平。如果你不启用内部上拉悬空的引脚会读到随机电平按钮表现就跟神经病一样。这个坑在Arduino里也一样存在只不过Arduino的引脚上拉电阻默认不能软件配置需要在pinMode()旁边设置INPUT_PULLUP。4.2 树莓派上用Python控制PWM舵机树莓派控制舵机本质上需要产生50Hz的PWM信号通过改变脉冲宽度控制舵机角度。这一点树莓派做得不如Arduino简单因为普通GPIO没有专用的硬件PWM要靠软件模拟。gpiozero对舵机的封装还算友好但校准参数必须手动拿到否则舵机要么颤抖要么转不动。from gpiozero import Servo import time # 舵机信号线接GPIO18 servo Servo(18, min_pulse_width0.5/1000, max_pulse_width2.5/1000) # Servo类中 -1 对应0度0对应90度1对应180度 for angle in [-1, -0.5, 0, 0.5, 1]: servo.value angle time.sleep(1) servo.detach() # 释放引脚停止输出PWM注意min_pulse_width和max_pulse_width这两个参数。不同的舵机厂商对“0度”“180度”对应的脉宽定义不一样。普通SG90舵机一般是0.5ms对应0度、2.5ms对应180度但有的舵机要0.6ms到2.4ms。如果你发现舵机转到极限位置时嗡嗡响、抖动那就是脉宽范围标定得不合适需要微调这两个值。这个校准过程Arduino的Servo.write(angle)会自动做好树莓派却要自己手动来也是很多人觉得“树莓派控制舵机麻烦”的原因。4.3 树莓派上跑Python控制整套智能小车树莓派做智能小车的典型架构是Python程序控制电机驱动板比如L298N或者TB6612同时利用树莓派的算力跑摄像头识别或PID算法。和Arduino小车相比树莓派小车的优势在于可以跑OpenCV做视觉巡线这对纯Arduino来说基本是天方夜谭。from gpiozero import Motor import time import cv2 import numpy as np # 电机A接GPIO17和GPIO18电机B接GPIO27和GPIO22 motor_left Motor(forward17, backward18) motor_right Motor(forward27, backward22) def stop_car(): motor_left.stop() motor_right.stop() def forward(speed0.3): motor_left.forward(speed) motor_right.forward(speed) def turn_left(speed0.3): motor_left.backward(speed * 0.8) motor_right.forward(speed) def turn_right(speed0.3): motor_left.forward(speed) motor_right.backward(speed * 0.8) # 简单测试 try: forward(0.3) time.sleep(2) turn_left(0.3) time.sleep(1) turn_right(0.3) time.sleep(1) stop_car() except KeyboardInterrupt: stop_car()实际做小车时要注意Motor类的forward()和backward()的参数是0到1之间的浮点数代表PWM占空比。两个电机的性能不可能完全一致同样的占空比轮子转速会有一点差异小车会跑偏这是机械和电子都逃不过的事实。解决办法是通过实测给每侧做一个“校正系数”把左电机速度乘以一个0.9~1.1之间的系数对齐。这种细节只有动手跑过才会懂。5. 常见问题与排查技巧实录这部分是我最想写的因为上面那些代码网上遍地都是但真正让你卡住一整天的问题往往就是这么几个。5.1 串口问题“SerialException: could not open port”这个提示基本说明端口被占用或者设备没连上。在Windows下Arduino IDE如果开着串口监视器Python就打不开同一个COM口。先关掉IDE再来。Linux下最常见的是权限问题运行sudo usermod -a -G dialout $USER把你的用户加入dialout组然后注销重新登录。另外Linux上设备名不是固定的今天/dev/ttyUSB0明天可能变成/dev/ttyUSB1写个ls /dev/ttyUSB*或dmesg | tail -20看一下最新接入的是哪个。发送数据全是乱码百分之九十九是波特率不一致。Arduino端9600Python端也得9600。还有一个坑是三方USB转串口芯片的时钟误差如果你用的是CH340偶尔的乱码可能源于芯片本身换更高精度的线或者降低波特率能缓解。Arduino一直收不到我的指令但串口监视器发可以收到检查你Python写入的字节序列。很多人写了ser.write(LED:ON)这是错的write()需要字节而不是字符串必须ser.write(LED:ON.encode())而且文本协议要有结尾符否则Arduino的readStringUntil(\n)会一直等下去。5.2 树莓派GPIO问题“RuntimeError: Please check permissions”RPi.GPIO需要root权限你需要在命令前加sudo python3 xxx.py。gpiozero在部分系统上不需要但如果你设置了系统权限也可能遇到类似问题解决办法是gpio readall检查权限。“Only factory default pin state is available”这个错误是RPi.GPIO在Python重运行setmode(GPIO.BCM)时发现GPIO还在占用中。原因是你上次运行的程序没有释放硬件资源。写代码时务必加try/finally和GPIO.cleanup()或者用gpiozero的close()。否则脚本崩溃后下次再运行就会报这个错很多人第一次遇到都莫名其妙。GPIO编号方式混乱导致控制错引脚树莓派的GPIO有两种编号方式BCM编号比如GPIO17和物理引脚编号比如第11脚。你在树莓派上看到“GPIO17”时它在物理排针上的位置其实是第11脚。网上教程混用这两种编号你要是没搞清楚照着别人的代码直接接灯就是不亮。建议所有新代码统一用BCM编号查找引脚对照表时看下图物理引脚编号是固定的1~40BCM编号是芯片定义的GPIO0~GPIO27两者不是顺序对应关系。比如BCM GPIO2对应物理第3脚BCM GPIO3对应物理第5脚。接线前先查对照表别想当然。舵机抖动或者无力树莓派的3.3V GPIO驱动能力有限舵机工作电流较大不能直接从GPIO取电。舵机电源要外接5V稳压电源GND要跟树莓派共地GPIO只接信号线。不共地或者供电不足舵机就会出现抖动、异响、转不到位。这跟Arduino用5V给舵机供电是一个道理电力设计是硬件项目最容易翻车的一环。5.3 平台选型时的错误认知用树莓派去做需要精确时序的波形采集比如读取红外遥控信号、模拟DS18B20的单总线时序。Linux系统的调度会让你的GPIO切换抖动几十毫秒这在逻辑上就是不可接受的。这种活儿老老实实用Arduino或者专门选支持硬件PWM的MCU来做。用Arduino去跑视觉识别和机器学习推理那是真不信邪。Arduino的内存都不够装一个OpenCV的分支。如果你要“识别到某个物体后让电机转”正确方案是树莓派跑OpenCV识别算完结果后通过串口或GPIO把“控制信号”发给ArduinoArduino去转电机。分工明确各干各擅长的。不要在Arduino上折腾MicroPython除非你有非常明确的需求。MicroPython本质上是一个解释器在MCU上跑它的实时性比原生C差很多资源占用也大。我见过一些人想在Arduino上“跑Python”走入这个误区折腾半天最后发现性能和稳定都不如直接用C写。真有硬需求想跑Python的MCU直接选ESP32或树莓派Pico这类支持MicroPython的硬件别在Uno上硬掰。6. 我的实操体会和一个小建议项目做多了之后我最大的感触是“用Python控制Arduino或树莓派”这个题目的关键不在Python也不在硬件而在通信协议和模块边界的设计。你要始终想清楚一件事每个模块该干什么Arduino小车的程序如果写得像面条一样把控制逻辑和通信解析混在一起后期加功能就是灾难。我的习惯是把Arduino代码按“解析指令 → 执行动作 → 反馈状态”三段分离Python端按“发指令 → 解析反馈 → 数据处理”三段分离。两边各占一层中间用协议连接任何一端出问题替换掉另一边不影响。最后分享一个小技巧是我调串口通信时最常用的不要一开始就写完整业务逻辑先做一个“回环测试”。Arduino收到什么命令原样返回什么也就是echo模式。Python发PINGArduino回PONG。两端能稳定收发100个包说明物理链路是好的再往上叠加业务逻辑。这样出了bug你才知道问题出在传输层还是应用层能省掉大量排查时间。这些内容你拿去做一个Python Arduino的智能小车、Python读取传感器数据并绘图、或者树莓派的远程控制类项目都已经铺好了路。剩下的就是动手接线、跑代码遇到问题时回来翻翻这几个常见坑很多坎就能直接跨过去了。
阅读完成 · 觉得有帮助?