跳转至

纯 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, &param);

提升 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 硬件安全

最重要的注意事项

  1. GPIO 电流限制:树莓派 GPIO 引脚最大输出 16 mA,绝不能直接驱动电机!必须接驱动模块(L298N / DRV8833 / TB6612)。
  2. 反电动势保护:电机制动时会产生反电动势,驱动模块必须有续流二极管(大多数集成驱动模块已内置)。
  3. 电源隔离:电机电源和树莓派/Jetson 的逻辑电源务必分开供电,避免电机噪声干扰系统。
  4. 共地:逻辑地和电机驱动地必须共地,否则信号无法正确传输。
  5. 过流保护:在电源回路中串联合适的保险丝。

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

推荐的开发流程

  1. 先用命令行工具验证硬件接口(gpiosetcansendcandump
  2. 再写 Python 原型快速验证逻辑
  3. 性能不足时迁移到 C/C++ 或使用 PREEMPT_RT 内核
  4. 遇到实时性瓶颈时,考虑将闭环控制下放到 MCU,Linux 只负责指令下发