纯 Linux 控制电机¶
不依赖单片机,直接用 Linux 系统(树莓派、Jetson、工控机等)通过各种硬件接口驱动电机。适用于对实时性要求不极端、但需要强大算力或丰富生态的场景。
一、常识与基础认知¶
Linux 控制电机的适用场景¶
首先明确:Linux 不适合所有电机控制
Linux 是分时操作系统,调度延迟通常在 100 µs ~ 几 ms 级别,无法保证微秒级实时响应。
以下场景不推荐纯 Linux:
- FOC(磁场定向控制)—— 需要微秒级 PWM 更新
- 高速步进电机精密定位(脉冲频率 > 100 kHz)
- 多轴硬实时同步(EtherCAT 伺服网络)
适合纯 Linux 控制的典型场景:
| 场景 | 说明 |
|---|---|
| 舵机位置控制 | 50 Hz PWM,时序要求宽松 |
| 低速 DC 电机调速 | 软件 PWM 足够,或外接驱动模块 |
| 步进电机慢速定位 | 脉冲速率 < 10 kHz |
| 通过 CAN/串口驱动商业电调 | Linux 发指令,驱动器内部闭环 |
| 机器人关节位置/速度指令下发 | 上层算法在 Linux,底层闭环在驱动器 |
电机类型与接口对应¶
graph LR
subgraph "电机类型"
M1["直流有刷电机<br>DC Motor"]
M2["无刷电机 BLDC<br>(电调驱动)"]
M3["步进电机<br>Stepper"]
M4["舵机<br>Servo"]
M5["无刷伺服<br>(CAN/串口总线)"]
end
subgraph "Linux 接口"
I1["PWM + 方向 GPIO<br>H桥驱动模块"]
I2["PWM 信号<br>(ESC 电调接受)"]
I3["STEP/DIR 信号<br>(GPIO 模拟或硬件 PWM)"]
I4["50 Hz PWM<br>(1~2 ms 脉宽)"]
I5["CAN Bus / UART / RS485"]
end
M1 --> I1
M2 --> I2
M3 --> I3
M4 --> I4
M5 --> I5
Linux 硬件接口速查¶
| 接口 | 典型用途 | Linux 子系统 | 常用工具/库 |
|---|---|---|---|
| GPIO | 方向控制、使能信号、步进脉冲 | /sys/class/gpio(旧)/ libgpiod(新) |
gpioset, gpiomon, python-gpiod |
| PWM | 调速、舵机控制 | /sys/class/pwm (sysfs PWM) |
python-periphery, 直接写 sysfs |
| UART / RS232 / RS485 | 与驱动器通信(Modbus、自定义协议) | /dev/ttyS*, /dev/ttyUSB* |
pyserial, minicom |
| SPI | 读编码器(AS5048B 等)、驱动 DRV8xxx | /dev/spidev* |
spidev(Python) |
| I²C | 读 IMU、扩展 IO | /dev/i2c-* |
smbus2 |
| CAN Bus | 商业伺服驱动器(RoboMaster、Unitree) | can0 SocketCAN |
python-can, cantools |
| USB | USB 转串口、USB-CAN 适配器 | /dev/ttyUSB*, /dev/ttyACM* |
pyserial, python-can |
二、架构流程¶
整体软件架构¶
graph TB
subgraph "应用层"
A1["路径规划 / 状态机<br>ROS节点 / Python脚本"]
A2["PID控制器 / 速度规划"]
end
subgraph "接口层"
B1["MotorController 类<br>封装发送指令/读反馈"]
B2["CAN驱动 / 串口驱动<br>python-can / pyserial"]
end
subgraph "Linux 内核层"
C1["SocketCAN / tty 驱动"]
C2["PWM 子系统"]
C3["GPIO 子系统 (libgpiod)"]
end
subgraph "硬件层"
D1["CAN 收发器<br>MCP2515 / USB-CAN"]
D2["电机驱动模块<br>L298N / DRV8833 / TB6612"]
D3["商业伺服驱动器<br>RoboMaster M3508 / 宇树电机"]
end
A1 --> A2 --> B1 --> B2 --> C1
B1 --> C2
B1 --> C3
C1 --> D1 --> D3
C2 --> D2
C3 --> D2
典型控制循环流程¶
flowchart TD
Init["初始化<br>打开设备、配置PWM/CAN频率"]
--> ReadSensor["读传感器反馈<br>编码器位置/速度、电流"]
--> CalcError["计算误差<br>目标值 - 实际值"]
--> PID["PID / 控制算法<br>计算输出量"]
--> SendCmd["发送指令<br>更新PWM占空比 / CAN帧"]
--> Sleep["定时等待<br>time.sleep() 或 select()"]
--> ReadSensor
Emergency["急停信号<br>GPIO/软件标志"] --> |"中断循环"| Stop["关闭使能<br>安全停机"]
三、各接口详细实践¶
3.1 PWM 控制(sysfs 方式)¶
Linux PWM sysfs 路径为 /sys/class/pwm/pwmchipN/,适用于硬件 PWM(树莓派、Jetson 等)。
# 启用 PWM 通道 0(以 pwmchip0 为例)
echo 0 > /sys/class/pwm/pwmchip0/export
# 设置周期 20ms(50Hz,单位纳秒)
echo 20000000 > /sys/class/pwm/pwmchip0/pwm0/period
# 设置占空比 1.5ms(舵机中位,单位纳秒)
echo 1500000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle
# 使能
echo 1 > /sys/class/pwm/pwmchip0/pwm0/enable
Python 封装示例:
import time
class SysfsPWM:
def __init__(self, chip: int, channel: int, freq_hz: float):
self.base = f"/sys/class/pwm/pwmchip{chip}/pwm{channel}"
period_ns = int(1e9 / freq_hz)
# Export channel
with open(f"/sys/class/pwm/pwmchip{chip}/export", "w") as f:
f.write(str(channel))
with open(f"{self.base}/period", "w") as f:
f.write(str(period_ns))
self.period_ns = period_ns
with open(f"{self.base}/enable", "w") as f:
f.write("1")
def set_duty_cycle(self, duty: float):
"""duty: 0.0 ~ 1.0"""
duty = max(0.0, min(1.0, duty))
duty_ns = int(duty * self.period_ns)
with open(f"{self.base}/duty_cycle", "w") as f:
f.write(str(duty_ns))
def set_pulse_width_us(self, us: float):
"""直接设置脉宽(µs),常用于舵机"""
duty_ns = int(us * 1000)
with open(f"{self.base}/duty_cycle", "w") as f:
f.write(str(duty_ns))
def close(self):
with open(f"{self.base}/enable", "w") as f:
f.write("0")
3.2 GPIO 控制(libgpiod,新标准)¶
sysfs GPIO 已废弃
/sys/class/gpio/ 方式在内核 4.8+ 后被废弃,新代码应使用 libgpiod。
# 安装
sudo apt install libgpiod-dev gpiod python3-gpiod
# 命令行快速测试(查看芯片)
gpiodetect
gpioinfo gpiochip0
# 设置 GPIO 输出高电平(第 17 号引脚)
gpioset gpiochip0 17=1
# 监听 GPIO 输入变化
gpiomon gpiochip0 17
Python(gpiod 库)控制 H 桥举例:
import gpiod
import time
# 打开 GPIO 芯片
chip = gpiod.Chip("gpiochip0")
# 申请引脚(输出)
IN1 = chip.get_line(17)
IN2 = chip.get_line(27)
ENA = chip.get_line(22) # 配合硬件PWM
config = gpiod.LineRequest()
config.consumer = "motor_ctrl"
config.request_type = gpiod.LineRequest.DIRECTION_OUTPUT
IN1.request(config)
IN2.request(config)
ENA.request(config)
def motor_forward():
IN1.set_value(1)
IN2.set_value(0)
ENA.set_value(1)
def motor_backward():
IN1.set_value(0)
IN2.set_value(1)
ENA.set_value(1)
def motor_stop():
IN1.set_value(0)
IN2.set_value(0)
ENA.set_value(0)
try:
motor_forward()
time.sleep(2)
motor_stop()
finally:
IN1.release()
IN2.release()
ENA.release()
3.3 CAN Bus 控制(SocketCAN)¶
CAN 总线是工业和机器人领域最常见的电机驱动器接口(如大疆 M3508/M2006、宇树电机等)。
# 配置 CAN 接口(以 MCP2515 SPI-CAN 或 USB-CAN 为例)
sudo ip link set can0 type can bitrate 1000000 # 1 Mbit/s
sudo ip link set can0 up
# 查看状态
ip -details link show can0
# 抓包调试
candump can0
# 发送测试帧(ID=0x200,数据 8 字节)
cansend can0 200#0000000000000000
Python(python-can)控制大疆 M3508:
import can
import struct
import time
class RM3508Controller:
"""大疆 M3508 无刷电机 CAN 控制(电流指令模式)"""
CMD_ID_1_4 = 0x200 # 控制电机 ID 1~4
CMD_ID_5_8 = 0x1FF # 控制电机 ID 5~8
def __init__(self, channel="can0", bitrate=1000000):
self.bus = can.interface.Bus(
channel=channel,
bustype="socketcan",
bitrate=bitrate
)
def send_current(self, currents: list[int]):
"""
发送电流指令
currents: 4 个电机的电流值,范围 -16384 ~ 16384
对应电机 ID 1~4(发给 CMD_ID_1_4)
"""
assert len(currents) == 4
data = struct.pack(">4h", *currents) # 大端 4 × int16
msg = can.Message(
arbitration_id=self.CMD_ID_1_4,
data=data,
is_extended_id=False
)
self.bus.send(msg)
def read_feedback(self, timeout=0.01):
"""读取电机反馈(角度、转速、电流)"""
msg = self.bus.recv(timeout=timeout)
if msg is None:
return None
# 大疆 M3508 反馈帧格式
angle, rpm, current, temp = struct.unpack(">HhHB", msg.data[:7])
return {
"motor_id": msg.arbitration_id - 0x200,
"angle": angle, # 0 ~ 8191(对应 0~360°)
"rpm": rpm,
"current": current,
"temp": temp
}
def close(self):
self.bus.shutdown()
3.4 串口 / RS485 控制¶
许多步进驱动器(如雷赛 DM 系列)、舵机总线(如 Dynamixel、飞特 FE)通过串口/RS485 通信。
import serial
import time
# 打开串口
ser = serial.Serial(
port="/dev/ttyUSB0",
baudrate=115200,
timeout=0.1
)
def send_modbus_rtu(addr: int, func: int, reg: int, value: int):
"""简易 Modbus RTU 写单寄存器"""
import struct
frame = struct.pack(">BBHH", addr, func, reg, value)
# 计算 CRC16
crc = _crc16(frame)
frame += struct.pack("<H", crc)
ser.write(frame)
def _crc16(data: bytes) -> int:
crc = 0xFFFF
for byte in data:
crc ^= byte
for _ in range(8):
if crc & 0x0001:
crc = (crc >> 1) ^ 0xA001
else:
crc >>= 1
return crc
四、实时性问题与解决方案¶
为什么 Linux 实时性差?¶
graph TD
App["用户程序<br>time.sleep(1ms)"] --> Kernel["Linux 内核调度"]
Kernel --> |"调度延迟<br>100µs ~ 5ms"| HW["硬件 GPIO/PWM"]
subgraph "延迟来源"
L1["进程调度延迟"]
L2["中断延迟"]
L3["内存换页"]
L4["Python GIL"]
end
| 方案 | 延迟量级 | 难度 | 适用场景 |
|---|---|---|---|
| 普通 Linux + Python | 1 ~ 10 ms | 简单 | 舵机/低速电机 |
| 普通 Linux + C | 500 µs ~ 2 ms | 中等 | 一般速度环 |
| PREEMPT_RT 补丁内核 | 50 ~ 200 µs | 较难 | 软实时控制 |
| Xenomai / RTAI | < 50 µs | 困难 | 硬实时需求 |
| 专用 MCU(STM32等) | < 1 µs | 中等 | 需要硬实时时用 MCU |
PREEMPT_RT 实时内核(推荐中间方案)¶
# 查看当前内核是否有 RT 补丁
uname -a # 含 "PREEMPT_RT" 说明已是实时内核
# 树莓派安装实时内核(以 Ubuntu 为例)
sudo apt install linux-image-rt-raspi
# 设置进程为实时调度
sudo chrt -f -p 99 <PID>
# C 程序中设置实时优先级
#include <sched.h>
struct sched_param param = { .sched_priority = 80 };
sched_setscheduler(0, SCHED_FIFO, ¶m);
提升 Python 控制稳定性的技巧¶
import os
import time
import ctypes
# 技巧1:锁定内存,防止换页引起延迟
import ctypes
libc = ctypes.CDLL("libc.so.6")
MCL_CURRENT = 1
MCL_FUTURE = 2
libc.mlockall(MCL_CURRENT | MCL_FUTURE)
# 技巧2:使用 time.perf_counter() 做精确定时
def precise_sleep_until(target: float):
"""精确等待到目标时间(busy-wait 最后 1ms)"""
while time.perf_counter() < target - 0.001:
time.sleep(0.0005)
while time.perf_counter() < target:
pass
# 技巧3:控制循环结构
LOOP_PERIOD = 0.01 # 10ms 控制周期
def control_loop():
next_time = time.perf_counter()
while running:
# --- 控制逻辑 ---
feedback = read_sensor()
cmd = pid_update(feedback)
send_command(cmd)
# --- 定时 ---
next_time += LOOP_PERIOD
precise_sleep_until(next_time)
五、注意事项¶
5.1 硬件安全¶
最重要的注意事项
- GPIO 电流限制:树莓派 GPIO 引脚最大输出 16 mA,绝不能直接驱动电机!必须接驱动模块(L298N / DRV8833 / TB6612)。
- 反电动势保护:电机制动时会产生反电动势,驱动模块必须有续流二极管(大多数集成驱动模块已内置)。
- 电源隔离:电机电源和树莓派/Jetson 的逻辑电源务必分开供电,避免电机噪声干扰系统。
- 共地:逻辑地和电机驱动地必须共地,否则信号无法正确传输。
- 过流保护:在电源回路中串联合适的保险丝。
5.2 软件安全¶
软件层面必须处理
- 急停机制:注册
SIGINT/SIGTERM信号处理,确保程序退出时关闭电机使能 - 看门狗:若控制循环因异常卡住,需有超时机制(定时器线程)发送停止指令
- 限幅处理:所有控制输出必须做 clamp,防止积分饱和或异常值烧驱动
import signal
import sys
motor = None # 全局电机对象
def emergency_stop(sig, frame):
global motor
print("[ESTOP] 急停触发,关闭电机...")
if motor:
motor.set_duty_cycle(0)
motor.disable()
sys.exit(0)
signal.signal(signal.SIGINT, emergency_stop)
signal.signal(signal.SIGTERM, emergency_stop)
5.3 权限问题¶
# GPIO / PWM / CAN 操作通常需要 root 或特定组权限
# 将用户加入 gpio / dialout 组(免 sudo)
sudo usermod -aG gpio $USER
sudo usermod -aG dialout $USER # 串口权限
sudo usermod -aG plugdev $USER # USB 设备
# CAN 接口设置需要 root(或通过 udev 规则)
# 推荐写一个 systemd 服务在启动时配置 CAN 接口
# /etc/systemd/system/can-setup.service 示例:
# [Service]
# ExecStart=/sbin/ip link set can0 type can bitrate 1000000
# ExecStartPost=/sbin/ip link set can0 up
# Type=oneshot
# RemainAfterExit=yes
5.4 设备树与内核配置¶
- 树莓派:编辑
/boot/config.txt启用硬件 PWM、SPI-CAN(MCP2515) - Jetson:使用
Jetson-IO工具配置引脚复用 - 确认硬件 PWM 通道已在设备树中使能,软件 PWM 精度远不如硬件 PWM
# 树莓派 /boot/config.txt 常用配置
dtoverlay=pwm-2chan # 启用双通道硬件PWM(GPIO12/13)
dtoverlay=mcp2515-can0,oscillator=12000000,interrupt=25 # SPI-CAN
enable_uart=1 # 启用 UART
六、调试工具与技巧¶
常用调试命令¶
# 查看 CAN 总线流量(实时)
candump -td can0
# 统计 CAN 帧频率
canbusload can0@1000000
# 查看 PWM 状态
cat /sys/class/pwm/pwmchip0/pwm0/duty_cycle
cat /sys/class/pwm/pwmchip0/pwm0/period
# 查看串口数据
minicom -D /dev/ttyUSB0 -b 115200
# 用 logic analyzer(sigrok/PulseView)抓 PWM/GPIO 波形
性能监控¶
# 查看进程调度延迟(需 rt-tests 工具包)
sudo apt install rt-tests
sudo cyclictest -m -S -p 80 -i 1000 -l 10000 # 测量实时延迟
# 查看系统中断占用
watch -n 0.5 cat /proc/interrupts
# 查看 CPU 频率(防止节能降频影响控制性能)
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
软件示波器替代方案¶
# 在控制循环中记录时序,事后分析抖动
import time
import csv
timestamps = []
with open("timing_log.csv", "w") as f:
writer = csv.writer(f)
for i in range(1000):
t = time.perf_counter()
timestamps.append(t)
writer.writerow([t])
time.sleep(0.01)
# 分析抖动
import numpy as np
intervals = np.diff(timestamps) * 1000 # ms
print(f"平均: {intervals.mean():.3f} ms")
print(f"最大: {intervals.max():.3f} ms")
print(f"标准差: {intervals.std():.3f} ms")
七、完整示例:树莓派 PWM 控制直流电机¶
以下是一个包含方向控制、速度渐变、急停的完整示例:
"""
树莓派 + L298N 控制直流电机
硬件接线:
GPIO12 (PWM0) → L298N ENA
GPIO17 → L298N IN1
GPIO27 → L298N IN2
"""
import gpiod
import time
import signal
import sys
# ---- 配置 ----
PWM_CHIP = 0
PWM_CHANNEL = 0
PWM_FREQ_HZ = 10000 # 10 kHz PWM 频率
GPIO_CHIP = "gpiochip0"
PIN_IN1 = 17
PIN_IN2 = 27
# ---- PWM 封装 ----
class HardwarePWM:
def __init__(self, chip, channel, freq_hz):
base = f"/sys/class/pwm/pwmchip{chip}"
self.ch_base = f"{base}/pwm{channel}"
period_ns = int(1e9 / freq_hz)
try:
with open(f"{base}/export", "w") as f:
f.write(str(channel))
except OSError:
pass # 已经 export
time.sleep(0.1) # 等待 sysfs 节点创建
with open(f"{self.ch_base}/period", "w") as f:
f.write(str(period_ns))
self.period_ns = period_ns
with open(f"{self.ch_base}/enable", "w") as f:
f.write("1")
def set_duty(self, duty: float):
duty = max(0.0, min(1.0, duty))
with open(f"{self.ch_base}/duty_cycle", "w") as f:
f.write(str(int(duty * self.period_ns)))
def close(self):
self.set_duty(0)
with open(f"{self.ch_base}/enable", "w") as f:
f.write("0")
# ---- 电机控制类 ----
class DCMotor:
def __init__(self):
self.pwm = HardwarePWM(PWM_CHIP, PWM_CHANNEL, PWM_FREQ_HZ)
chip = gpiod.Chip(GPIO_CHIP)
self.in1 = chip.get_line(PIN_IN1)
self.in2 = chip.get_line(PIN_IN2)
cfg = gpiod.LineRequest()
cfg.consumer = "dc_motor"
cfg.request_type = gpiod.LineRequest.DIRECTION_OUTPUT
self.in1.request(cfg)
self.in2.request(cfg)
self._speed = 0.0
def forward(self, speed: float = 1.0):
"""speed: 0.0 ~ 1.0"""
self.in1.set_value(1)
self.in2.set_value(0)
self.pwm.set_duty(abs(speed))
self._speed = speed
def backward(self, speed: float = 1.0):
self.in1.set_value(0)
self.in2.set_value(1)
self.pwm.set_duty(abs(speed))
self._speed = -speed
def brake(self):
"""主动刹车"""
self.in1.set_value(1)
self.in2.set_value(1)
self.pwm.set_duty(0)
self._speed = 0.0
def coast(self):
"""自由滑行"""
self.in1.set_value(0)
self.in2.set_value(0)
self.pwm.set_duty(0)
self._speed = 0.0
def ramp_to(self, target: float, duration: float, steps: int = 50):
"""速度渐变(-1.0 ~ 1.0)"""
current = self._speed
for i in range(steps + 1):
t = i / steps
speed = current + (target - current) * t
if speed >= 0:
self.forward(speed)
else:
self.backward(-speed)
time.sleep(duration / steps)
def close(self):
self.coast()
self.pwm.close()
self.in1.release()
self.in2.release()
# ---- 主程序 ----
motor = DCMotor()
def on_exit(sig, frame):
print("\n急停并关闭...")
motor.close()
sys.exit(0)
signal.signal(signal.SIGINT, on_exit)
signal.signal(signal.SIGTERM, on_exit)
try:
print("渐加速到 80%...")
motor.ramp_to(0.8, duration=2.0)
time.sleep(3)
print("减速并反转...")
motor.ramp_to(-0.5, duration=2.0)
time.sleep(2)
print("刹车停止")
motor.brake()
finally:
motor.close()
八、推荐开发环境与库¶
| 库/工具 | 用途 | 安装 |
|---|---|---|
python-gpiod |
GPIO 控制(libgpiod Python 绑定) | pip install gpiod |
python-periphery |
GPIO/PWM/SPI/I²C 统一接口 | pip install python-periphery |
pyserial |
串口通信 | pip install pyserial |
python-can |
CAN 总线通信 | pip install python-can |
smbus2 |
I²C 通信 | pip install smbus2 |
spidev |
SPI 通信 | pip install spidev |
rt-tests |
实时性测试(cyclictest) | sudo apt install rt-tests |
can-utils |
CAN 总线命令行工具 | sudo apt install can-utils |
gpiod |
GPIO 命令行工具 | sudo apt install gpiod |
推荐的开发流程
- 先用命令行工具验证硬件接口(
gpioset、cansend、candump) - 再写 Python 原型快速验证逻辑
- 性能不足时迁移到 C/C++ 或使用 PREEMPT_RT 内核
- 遇到实时性瓶颈时,考虑将闭环控制下放到 MCU,Linux 只负责指令下发