3 分钟阅读

简介

如果机器人是一台运行 Linux、连接执行器和传感器的计算机”,下一个问题是:这台计算机上的软件应该怎样拆分、通信和管理故障。

Linux 通过串口驱动和设备接口提供串口配置与字节收发能力;用户态协议库在这些能力之上,实现舵机寻址、寄存器读写和控制命令。

  1. 板载计算机通过 UART、I²C 等接口连接舵机、IMU、传感器;一条总线上可以挂多个设备
  2. 内核驱动管理通信硬件,用户态程序通过 OS 接口收发数据;设备协议由用户态代码继续解释 OS 不需要理解“站起来”或“肩关节转到 0.9 rad”,这些语义由上层控制代码、协议库和舵机固件共同实现。

using serial ports in Linux

Everything Is A File: In typical UNIX style, serial ports are represented by files within the operating system. These files usually pop-up in /dev/, and begin with the name tty*. 比如 /dev/ttyUSB0 - 大多数 USB 转串口线都会以这样的文件名显示。

要写入串行端口,需对该文件执行写操作。要读取串行端口,需对该文件执行读操作。 但如何设置波特率、校验位等串行端口参数呢?这需要通过一个特殊的 tty 配置 struct 来完成。

网络:应用协议库 → socket 接口 → Linux 网络栈与驱动
串口:设备协议库 → 串口接口   → Linux 串口驱动

如何写程序让舵机动起来

网络:biz程序 → redis-py → socket → Linux TCP/IP 协议栈与网卡驱动
                                      ↓
                                 Redis 服务端

串口:biz程序 → dynamixel_sdk → pyserial → Linux 串口接口与驱动
                                             ↓
                                         舵机固件

从 “应用 → 用户态库 → OS 接口与驱动 → 硬件” 分层来看

层次 连接 Redis 控制 Dynamixel 舵机 读取 IMU(I²C) 读取 USB 摄像头
应用 业务程序:读写 key/value 控制程序:设置目标位置 感知程序:读取加速度、角速度 视觉程序:获取图像帧
用户态库 redis-py dynamixel_sdk → pyserial 芯片专用库 → smbus2 OpenCV(使用 V4L2 后端)
OS 接口 socket:connect/send/recv 串口设备:read/write、termios 配置 /dev/i2c-*:ioctl 等 V4L2:ioctl、mmap 等
内核子系统与驱动 TCP/IP 协议栈 → 网卡驱动 TTY/串口驱动,或 USB 转串口驱动 I²C 子系统 → I²C 控制器驱动 V4L2/UVC 驱动 → USB 子系统与控制器驱动
本机通信硬件 网卡 UART 控制器,或 USB 控制器 + USB 转串口适配器 I²C 控制器 USB 主控制器
链路另一端 网络 → Redis 所在服务器 串口总线 → 舵机 I²C 总线 → IMU 芯片 USB 线 → 摄像头

以 Dynamixel 舵机为例,让 1 号舵机在当前位置基础上再转动 10°。假设舵机已经启用位置控制和扭矩,目标在允许范围内。

# 第三方通用串口库:pip install pyserial,导入名是 serial
import serial
# 厂商设备协议库:封装 Dynamixel 指令和响应
from dynamixel_sdk import PortHandler, PacketHandler


# 用户态 SDK:内部通过 pyserial 调用 Linux 串口接口。
port = PortHandler("/dev/ttyUSB0")
port.openPort()
port.setBaudRate(57600)  # 与设备配置一致

# 用户态 SDK:负责 Dynamixel Protocol 2.0 的编码与解析。
protocol = PacketHandler(2.0)

# 以下寄存器地址、位置分辨率以 XL430-W250 为例。
SERVO_ID = 1
PRESENT_POSITION = 132
GOAL_POSITION = 116

# SDK 发送读取请求,再解析舵机返回的当前位置。
current, comm_result, device_error = protocol.read4ByteTxRx(
    port, SERVO_ID, PRESENT_POSITION
)

# 应用逻辑:把“再转 10°”转换为绝对目标位置。
# 一圈对应 4096 个位置计数。
target = current + round(10 / 360 * 4096)

# SDK 编码写入指令,经串口发送给 1 号舵机。
comm_result, device_error = protocol.write4ByteTxRx(
    port, SERVO_ID, GOAL_POSITION, target
)

port.closePort()

robotd:持续运行的身体控制进程

Linux 提供串口、网络、视频采集等设备访问能力,用户态协议库进一步封装具体设备。但要让多个关节配合运动,还需要软件管理身体状态、控制指令、执行时序和异常处理。这套软件栈在 robot application 与linux 之间,ROS 2 生态和 Microduck 的多 daemon 方案,以不同方式组织这些能力。robotd 是 Microduck 为这些工作实现的常驻身体控制服务。

软件职责 Microduck 的做法 ROS 2 系统中的常见做法
功能拆分 robotd、mediad、tofd 等独立进程 拆成多个 Node,部署到一个或多个进程
指令与查询 Unix socket + JSON-RPC Service、Topic,长任务可用 Action
状态与传感器数据 各服务提供订阅流或专门传输路径 Topic,以及按需要设计的数据传输路径
身体控制 robotd 的控制循环和硬件接口 控制 Node,可结合 ros2_control
启动与恢复 systemd 管理进程 ROS 2 launch 组织启动,也可以结合 systemd

能干啥

如同 MySQL 在 Linux 的文件与存储能力之上构建数据库服务,robotd 在设备通信能力之上构建身体控制服务。前者让应用通过 SQL 操作数据,后者让应用通过运动指令控制身体,而不必自行处理每个舵机的通信和执行时序。

业务应用
    ↓ SQL
MySQL Server 进程
    ├─ 连接管理、SQL 解析、优化与执行
    └─ InnoDB:索引、事务、缓存、数据页与日志读写
          ↓
Linux 文件接口 → 文件系统、块设备层与存储驱动
          ↓
存储控制器 → SSD / 磁盘
控制应用
    ↓ Command / Action Chunk
robotd 进程
    ├─ 请求处理、控制权、状态管理
    └─ 控制循环:读取反馈、计算/选择目标、检查与下发
          ↓
硬件访问与设备协议库
          ↓
Linux 串口接口与驱动
          ↓
通信控制器 → 多个舵机

把 robotd 理解成一个长期运行的后端服务比较容易(类似mysqld):它监听 Unix domain socket,接受 JSON-RPC 请求,即IPC(Inter-Process Communication)通信。假设1号舵机管的是鸭子头部的转动,若初始角度为 0°,让鸭子的头部偏航角设为 10° 的伪代码如下(没有现成的、类似PyMySQL 的robotclient)

import json
import math
import socket

# 假设 robotd 已启动,机器人已初始化且头部控制可用。
request = {
    "jsonrpc": "2.0",
    "method": "robot.head",
    "params": {
        "head_yaw": math.radians(10),
        "head_pitch": 0.0,
        "head_roll": 0.0,
        "neck_pitch": 0.0,
    },
}

# AF_UNIX:通过本机 Unix socket 连接 robotd。
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
    client.connect("/run/robotd.sock")

    # robotd 使用换行分隔 JSON 消息。
    message = json.dumps(request) + "\n"
    client.sendall(message.encode("utf-8"))
数据库 机器人
PyMySQL 等客户端库 假设封装的 RobotClient
MySQL 协议 JSON-RPC
TCP 或 Unix socket Unix socket
mysqld 服务进程 robotd 服务进程
SQL 请求 robot.head 等控制请求

如何实现

robotd在独立控制线程中,持续运行一个目标频率为 50 Hz 的循环,消费客户端提供的指令状态或动作序列(即使没有新指令,也可能需要继续读取反馈、维持姿态并处理异常)。50 Hz 即大约每 20 ms 一轮。这是调度目标,包含本轮计算和 I/O,并非每次处理完后额外休眠 20 ms,也不是普通 Linux 上的硬实时保证。

下面用 Python 风格伪代码展示正常运行路径,假设身体已初始化、运动控制已启用。

# robotd 内部,接口名称为示意。
controller = MotionController()
safety = Safety(RobotIo.open())
timer = PeriodicTimer(hz=50)

while True:
    timer.wait_next_tick()

    # 1. 读取实际身体状态
    # 包括各关节位置、速度,以及 IMU 提供的姿态等反馈。
    sensors = safety.read_body()

    # 2. 取得当前指令
    # 本例中包含 head_yaw = 0.1745 rad,即头部目标为 10°。
    command = intents.snapshot()

    # 3. 计算本周期各关节的目标
    # 原有控制路径中,运动 Policy 根据指令和身体状态进行推理,
    # 控制代码再将模型输出转换为关节目标角度。
    targets = controller.step(sensors, command)

    # 4. 检查并下发目标
    # Safety 检查数值及执行器范围,
    # 经 RobotIo 完成设备映射、单位转换和协议发送。
    safety.apply(targets)

    # 5. 更新供外部读取的状态
    publish_state(
        joints=sensors.positions,  # 本轮写入之前的实际位置
        targets=targets,           # 本轮刚下发的目标
    )

Action Chunk 是一段有时间含义的目标序列

假设用户说“把前面的球往前推”。机器人需要接近球、调整身体与头部姿态,再向前施力。模型看到当前画面和身体状态后,一次预测接下来一小段时间内各关节的动作,这就是 Action Chunk。执行期间,球可能滚动、接触位置可能偏离预期。机器人取得新观测后,再生成下一段动作,修正后续运动。

任务层面的描述:
接近球 → 调整姿态 → 接触球 → 推球

某次模型推理输出的 Action Chunk:
时间步       腿部各关节目标      颈部与头部关节目标……
t            一组角度           一组角度
t + Δt       一组角度           一组角度
t + 2Δt      一组角度           一组角度
……

为什么一次预测多个动作:腿部、身体和头部需要在时间上配合。

  1. 抓取、迈步、绕障碍都需要前后动作配合。预测一段动作,更容易表达持续的运动趋势,减少逐步独立预测带来的犹豫或抖动。
  2. 降低模型推理成本/延迟。假设机器人每 20 ms 需要一个目标,而模型推理一次需要 100 ms,就无法每轮都等它生成新动作。一次生成多个目标后,执行端可以按控制频率逐个使用;推理端同时准备下一段。异步推理。
  3. 利用示范数据中的连续动作片段。人类演示本来就是连续运动。预测一段动作,可以直接学习“观察到这种情况后,接下来怎样完成这一小段行为”。 但预测得越远,越可能因为环境变化而过时。 因此常见做法是“预测较长一段,只执行前面几步,再用新观测重新预测”,即 receding-horizon execution(滚动时域执行)。

留下评论