Python通过pyserial串口通信控制Arduino驱动电机启停
详细讲解如何使用Python的pyserial库通过USB虚拟串口与Arduino单片机建立通信,并控制外部驱动模块实现直流电机的启动与停止,包含电气接线原理、下位机固件编写、上位机脚本与关键避坑指南。
要实现从计算机端通过 Python 控制直流电机的运转与停止,核心的解决路径是构建一套“上位机指令分发、虚拟串口透明传输、微控制器逻辑解析、驱动模块功率放大”的闭环链路。计算机运行 Python 脚本并通过 pyserial 库向 USB 虚拟串口写入约定的控制字符;挂载在串口总线上的 Arduino 实时监听并解析这些字符,依据指令切换指定数字引脚的高低电平;该引脚电平进一步送入 L298N 或 MOSFET 电机驱动模块,最终完成对外部供电动力电机的启停切换。
一、系统硬件架构与电气接线要点
在动手连接硬件前,必须先确立一个关键认知:Arduino 的 I/O 引脚绝对不能直接驱动直流电机。Arduino 单个引脚的最大连续输出电流通常仅为 20mA 至 40mA,而普通微型直流减速电机在启动或带载状态下的瞬态工作电流往往高达数百毫安至数安培。直接将电机接在单片机引脚上会直接烧毁微控制器内部端口。此外,电机作为感性负载,在断电瞬间会产生很高的反向感应电动势,必须通过外部驱动模块(如带有续流保护的 L298N、TB6612FNG 或继电器模块)进行功率隔离与控制。
整个硬件拓扑包含三个电气实体:计算机主机、Arduino 开发板、电机及功率驱动模块。它们的连接方式遵循以下规范:
- 上位机至下位机:使用标准 USB 数据线连接电脑与 Arduino。该线缆同时承担为 Arduino 逻辑芯片供电以及全双工虚拟串口数据通信两项任务。
- Arduino 至驱动模块:将 Arduino 的数字引脚(例如 D9 引脚)连接到电机驱动模块的通道控制端(如 L298N 的 IN1 引脚);若需要控制旋转方向,可再引入 D8 引脚连接 IN2。
- 动力电源与“共地(Common GND)”:电机必须由专用的外部独立直流电源(如 12V 动力电池或适配器)供电。驱动模块的供电正极接外部电源正极;最为关键的是,外部电源的负极、驱动模块的 GND 与 Arduino 的 GND 引脚必须牢固连接在一起。如果缺少这一公共参考地,控制引脚的电平高低将无法被驱动模块正确识别,往往表现为电机不受控乱转或完全无响应。
二、Arduino 下位机控制程序设计
在下位机侧,Arduino 的主要任务是轮询硬件串口缓冲区。一旦检测到新传入的字节,即将其读出并与预设命令比对,执行对应的 I/O 翻转,并向串口回送状态确认信息(ACK),以便上位机了解下位机状态。
为保证通信的高效与鲁棒性,推荐采用轻量级的单字节 ASCII 指令。例如规定:字符 '1' 表示启动电机,字符 '0' 表示停止电机,字符 'S' 或 's' 表示查询当前电机运行状态。完整的 Arduino 固件程序如下:
// Arduino 端电机启停控制程序
const int MOTOR_PIN = 9; // 连接至电机驱动模块的控制端(如 L298N 的 IN1)
void setup() {
// 设置控制引脚为输出模式
pinMode(MOTOR_PIN, OUTPUT);
// 初始化为低电平,确保上电初期电机处于停止状态
digitalWrite(MOTOR_PIN, LOW);
// 初始化硬件串口,设置通信波特率为 9600 bps
Serial.begin(9600);
}
void loop() {
// 检查串口缓冲区是否有待读取的数据
if (Serial.available() > 0) {
char command = Serial.read(); // 读取单个字节字符
if (command == '1') {
// 收到 '1':输出高电平,驱动电机旋转
digitalWrite(MOTOR_PIN, HIGH);
Serial.println("ACK:RUN");
}
else if (command == '0') {
// 收到 '0':输出低电平,切断电机驱动
digitalWrite(MOTOR_PIN, LOW);
Serial.println("ACK:STOP");
}
else if (command == 'S' || command == 's') {
// 收到 'S':读取当前引脚电平并反馈运行状态
int currentState = digitalRead(MOTOR_PIN);
if (currentState == HIGH) {
Serial.println("STATUS:RUNNING");
} else {
Serial.println("STATUS:STOPPED");
}
}
}
}
将上述代码烧录至 Arduino 开发板后,可以在 Arduino IDE 中打开“串口监视器”,将右下角波特率调整为 9600,手动输入 1 发送,观察驱动模块指示灯是否点亮、电机是否启动;输入 0 观察是否停机。验证下位机无误后,必须关闭 Arduino IDE 的串口监视器窗口,释放系统对串口的独占占用,否则后续 Python 脚本将无法打开该端口。
三、Python 环境准备与端到端操作流程
在计算机端,我们使用 Python 官方生态中广泛采用的 pyserial 库进行底层串口访问。在终端或命令行中执行以下命令进行安装:
pip install pyserial
整个系统从代码部署到动作执行的完整生命周期可分为四个阶段:准备运行环境、烧录下位机逻辑、建立串口连接并处理复位时延、发送指令与捕获响应。
四、Python 串口通信控制脚本实现
在编写 Python 脚本时,初学者最容易遇到的困惑是:“为什么明明调用了 ser.write(b'1'),下位机却毫无反应?”
这涉及 Arduino 标准板卡(如 Uno、Nano)的硬件特性:DTR(Data Terminal Ready)自动复位机制。在默认情况下,当宿主机程序通过 USB 打开串口时,USB 转串口芯片会将 DTR 引脚拉低,触发 ATmega328P 微控制器执行硬件重置,单片机将从引导加载程序(Bootloader)重新启动,这一过程大约耗时 1.5 到 2 秒。如果 Python 脚本在打开串口后立刻向缓冲区塞入指令,这些数据将在单片机重启初期被丢弃。因此,在打开串口之后、发送第一条业务指令之前,必须强制加入 time.sleep(2),等待单片机初始化完毕。
以下是具备完整异常捕获、连接延时等待及双向确认的 Python 控制脚本:
import serial
import time
def run_motor_controller():
# 串口参数设置
# Windows 环境通常形如 'COM3', 'COM4'
# macOS/Linux 环境通常形如 '/dev/ttyACM0', '/dev/cu.usbmodem14101'
serial_port = 'COM3'
baud_rate = 9600
print(f"正在尝试打开串口连接: {serial_port} (波特率: {baud_rate})...")
try:
# 打开串口,超时时间设为 1 秒
ser = serial.Serial(serial_port, baud_rate, timeout=1)
except serial.SerialException as err:
print(f"无法打开串口 {serial_port},请检查端口是否被占用或连接已断开: {err}")
return
# 关键防坑:等待 Arduino 自检并跳过 Bootloader
print("串口已连接,等待下位机重启就绪 (2秒)...")
time.sleep(2)
try:
# 1. 发送启动电机命令
print(">> 发送指令: 启动电机 (1)")
ser.write(b'1')
time.sleep(0.1) # 预留极短的硬件响应窗口
ack = ser.readline().decode('utf-8').strip()
print(f"<< 下位机响应: {ack}")
# 保持电机运行 3 秒
print("电机运转中,持续 3 秒...")
time.sleep(3)
# 2. 发送查询状态命令
print(">> 发送指令: 查询运行状态 (S)")
ser.write(b'S')
time.sleep(0.1)
status = ser.readline().decode('utf-8').strip()
print(f"<< 当前运行状态: {status}")
# 3. 发送停止电机命令
print(">> 发送指令: 停止电机 (0)")
ser.write(b'0')
time.sleep(0.1)
ack_stop = ser.readline().decode('utf-8').strip()
print(f"<< 下位机响应: {ack_stop}")
except Exception as ex:
print(f"控制过程中发生异常: {ex}")
finally:
# 确保无论发生何种情况,都关闭串口连接
if ser.is_open:
ser.close()
print("串口资源已正常释放,通信结束。")
if __name__ == '__main__':
run_motor_controller()
五、串口时序与状态转换分析
为了直观掌握上位机与下位机之间的数据包流动与状态机切换,我们可以观察二者在时间轴上的交互时序。整个系统本质上是一个典型的请求-应答(Request-Response)状态机模型。
六、联调测试与常见问题排查
在实际搭建与联调时,软硬件交叉往往会导致问题定位不够直观。以下梳理了四个最常见的故障现象、根本成因及排查解决措施:
- 串口占用报错(PermissionError: [Errno 13] Permission denied):这是新手最高频出现的失误。检查是否在 Arduino IDE 中打开了“串口监视器”,或后台有其他 Python 调试实例仍在运行。每个串口同一时刻只能被单个进程独占,必须关闭所有监视器后再启动脚本。
- 指令发出但电机无反应(Arduino板载TX/RX灯闪烁,电机不转):先观察驱动模块的输入指示灯是否随指令点亮。如果灯亮但电机不转,说明串口与逻辑代码正常,问题出在动力回路:检查外部电源电压是否充足、驱动板接线端子是否接触不良。如果指示灯完全不亮,检查 Python 脚本是否遗漏了
time.sleep(2)导致复位丢包。 - 电机乱转或单片机异常掉线重启:通常由两类原因导致:其一是未共地,驱动端与单片机无共同参考地导致引脚悬空拾取环境电磁杂波;其二是将电机的供电直接接在了 Arduino 的 5V 引脚上,电机启动瞬间拉低了板载供电电压导致微控制器欠压复位(Brown-out Reset)。
- 收到乱码或无法触发条件分支:核对双方波特率是否一致(本文示例均为
9600)。此外,Python 3 中ser.write()必须传入字节序列(如b'1'或'1'.encode('ascii')),传入未编码的 str 将抛出类型异常。
七、总结
通过 pyserial 串口通信控制 Arduino 驱动电机启停,是自动化测控、机器人原型开发以及软硬件协同项目中最基础且关键的一环。总结其核心要领,主要在于三个方面的协同:在硬件上,坚守“功率隔离与公共参考地(Common GND)”原则;在下位机上,建立简洁确定的指令解析与状态响应机制;在上位机 Python 侧,严守“等待 DTR 复位完成、字节编码传输、安全释放端口”的代码规范。掌握了这套标准的串口通信控制逻辑后,后续还可以轻松扩展引入 PWM 模拟写入(analogWrite)实现电机平滑无级调速,或引入光电编码器实现带位置闭环的步进或伺服精准控制。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









