在做高动态双足、四足或者机械臂的高频笛卡尔阻抗控制时,控制周期的抖动(Jitter)是导致电机异响、系统发散甚至飞车的重要隐患。很多朋友在 Gazebo 里仿真跑得好好的,一旦切到真机,发现哪怕只是 500 Hz 的循环,周期延时也能偶尔飙到几个毫秒。
今天老唐结合手头机械臂和底盘的调试经验,把 ROS 2(以 Humble 为例)下 ros2_control 硬件接口编写,以及底层的 PREEMPT_RT 实时调优理一套可落地的操作清单。
一、 搭建与验证 PREEMPT_RT 实时内核
普通的 Linux 内核是吞吐量优先,不是低延迟优先。要跑实时控制,第一步就是换上 PREEMPT_RT 补丁。
1. 内核安装(以 Ubuntu 22.04 LTS 为例)
最省事的方式是直接使用 Ubuntu Pro 提供的实时内核,或者手动拉源码打补丁编译。如果用官方实时内核:
sudo ubuntu-advantage attach <YOUR_PRO_TOKEN>
sudo pro enable realtime-kernel
sudo apt install linux-realtime
sudo reboot
重启后检查内核版本:
uname -a
# 输出中包含 PREEMPT_RT 标志即为成功
2. 使用 cyclictest 压测基准
装好内核不能盲目信任,必须打压测。安装 rt-tests:
sudo apt install rt-tests stress-ng
一边施加高负载,一边测延迟:
# 终端 1:跑满 CPU 和 IO
stress-ng --cpu 4 --io 2 --vm 1 --vm-bytes 1G --timeout 60s
# 终端 2:运行 cyclictest 测 10 分钟
sudo cyclictest -l100000 -m -n -a -t 4 -p 90 -h 100 -q
达标指标:在 1 kHz(1000 us 周期)控制下,Max Latency 应该稳定在 30 ~ 50 us 以内。如果偶发飙到数百微秒,说明硬件 BIOS 或内核参数有“暗坑”。
二、 消除抖动的系统级调优清单
即使打了 RT 补丁,现代 x86/ARM CPU 的省电和调度特性也会引入毫秒级抖动。以下几项必须排查。
1. BIOS 层面
- 关闭 CPU C-States(防止 CPU 进睡眠浅睡导致唤醒延迟)。
- 关闭 Intel SpeedStep / AMD Cool'n'Quiet。
- 如果对极端实时性要求极高,可关闭超线程(Hyper-Threading),避免共享 L1/L2 缓存竞争。
2. GRUB 参数隔离核心
把 CPU 核心划分界限,指定核心给 OS 和 ROS 2 节点,特定核心独占给控制器。
编辑 /etc/default/grub:
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3 intel_idle.max_cstate=0 processor.max_cstate=1"
更新 grub 并重启:
sudo update-grub
sudo reboot
isolcpus=2,3:不让 Linux 普通调度器把进程扔到 Core 2 和 Core 3。nohz_full=2,3:停止隔离核心上的系统时钟滴答中断(Tickless)。
3. 配置用户组实时权限
非 root 用户默认无法提升至 SCHED_FIFO 优先级。修改 /etc/security/limits.d/99-realtime.conf:
@realtime soft rtprio 99
@realtime hard rtprio 99
@realtime soft memlock unlimited
@realtime hard memlock unlimited
将当前用户加入用户组:
sudo groupadd realtime
sudo usermod -aG realtime $USER
三、 编写实时安全的 ros2_control Hardware Interface
在 ros2_control 架构中,hardware_interface::SystemInterface 是直连驱动的总线桥梁。编写 read() 和 write() 时,实时安全(Real-Time Safety)是第一铁律。
核心禁忌
- 严禁在 read/write 中动态分配内存:禁用
new、malloc,禁用std::vector::push_back、std::string拼接。 - 严禁在控制路径上调用非实时日志:千万不要写
RCLCPP_INFO,这是阻塞式 I/O。 - 避免使用常规锁:如果必须与非实时线程(如参数更新、状态监控)通信,使用 Lock-free 队列或无锁环形缓冲区。
接口骨架示例
#include "hardware_interface/system_interface.hpp"
#include "hardware_interface/types/hardware_interface_type_values.hpp"
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_lifecycle/state.hpp"
namespace my_robot_hardware
{
class RobotArmHardware : public hardware_interface::SystemInterface
{
public:
hardware_interface::CallbackReturn on_init(const hardware_interface::HardwareInfo & info) override
{
if (hardware_interface::SystemInterface::on_init(info) != hardware_interface::CallbackReturn::SUCCESS) {
return hardware_interface::CallbackReturn::ERROR;
}
// 初始化固定长度数组,避免运行时分配内存
hw_commands_positions_.resize(info_.joints.size(), 0.0);
hw_states_positions_.resize(info_.joints.size(), 0.0);
hw_states_velocities_.resize(info_.joints.size(), 0.0);
return hardware_interface::CallbackReturn::SUCCESS;
}
std::vector<hardware_interface::StateInterface> export_state_interfaces() override
{
std::vector<hardware_interface::StateInterface> state_interfaces;
for (size_t i = 0; i < info_.joints.size(); ++i) {
state_interfaces.emplace_back(
info_.joints[i].name, hardware_interface::HW_IF_POSITION, &hw_states_positions_[i]);
state_interfaces.emplace_back(
info_.joints[i].name, hardware_interface::HW_IF_VELOCITY, &hw_states_velocities_[i]);
}
return state_interfaces;
}
std::vector<hardware_interface::CommandInterface> export_command_interfaces() override
{
std::vector<hardware_interface::CommandInterface> command_interfaces;
for (size_t i = 0; i < info_.joints.size(); ++i) {
command_interfaces.emplace_back(
info_.joints[i].name, hardware_interface::HW_IF_POSITION, &hw_commands_positions_[i]);
}
return command_interfaces;
}
hardware_interface::return_type read(const rclcpp::Time & /*time*/, const rclcpp::Duration & /*period*/) override
{
// 关键路径:严禁分配内存、严禁 I/O 阻塞
// 通过 EtherCAT / CAN 总线驱动读取当前电机编码器数据
// 示例:bus_driver_->receive_processdata(hw_states_positions_.data(), hw_states_velocities_.data());
return hardware_interface::return_type::OK;
}
hardware_interface::return_type write(const rclcpp::Time & /*time*/, const rclcpp::Duration & /*period*/) override
{
// 关键路径:下发指令到总线
// 示例:bus_driver_->send_processdata(hw_commands_positions_.data());
return hardware_interface::return_type::OK;
}
private:
std::vector<double> hw_commands_positions_;
std::vector<double> hw_states_positions_;
std::vector<double> hw_states_velocities_;
};
} // namespace my_robot_hardware
#include "pluginlib/class_list_macros.hpp"
PLUGINLIB_EXPORT_CLASS(my_robot_hardware::RobotArmHardware, hardware_interface::SystemInterface)
四、 Controller Manager 线程绑定与调度设置
写好了硬件接口,如果 ROS 2 的管理节点依然在普通线程里漂移,那功夫就白费了。需要显式锁定内存并把主线程绑定到隔离核上。
在生产环境中,通常会写一个主入口(Wrapper)拉起 controller_manager,并在进入事件循环前配置线程属性:
#include <sched.h>
#include <sys/mman.h>
#include <pthread.h>
#include "rclcpp/rclcpp.hpp"
#include "controller_manager/controller_manager.hpp"
int main(int argc, char ** argv)
{
rclcpp::init(argc, argv);
// 1. 内存锁定,防止因为 Swap 或缺页异常导致延迟突增
if (mlockall(MCL_CURRENT | MCL_FUTURE) == -1) {
perror("mlockall failed");
}
// 2. 绑定 CPU 隔离核心(例如绑定到 Core 2)
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(2, &cpuset);
pthread_t current_thread = pthread_self();
if (pthread_setaffinity_np(current_thread, sizeof(cpu_set_t), &cpuset) != 0) {
perror("pthread_setaffinity_np failed");
}
// 3. 设置 SCHED_FIFO 实时调度策略与优先级
struct sched_param param;
param.sched_priority = 90; // 高优先级
if (pthread_setschedparam(current_thread, SCHED_FIFO, ¶m) != 0) {
perror("pthread_setschedparam failed");
}
auto executor = std::make_shared<rclcpp::executors::MultiThreadedExecutor>();
auto cm = std::make_shared<controller_manager::ControllerManager>(executor, "controller_manager");
// 独立起一个实时线程跑控制循环
std::thread cm_thread([cm]() {
rclcpp::Rate rate(1000.0); // 1 kHz 闭环
while (rclcpp::ok()) {
cm->read(rclcpp::Clock().now(), rate.period());
cm->update(rclcpp::Clock().now(), rate.period());
cm->write(rclcpp::Clock().now(), rate.period());
rate.sleep();
}
});
executor->add_node(cm);
executor->spin();
cm_thread.join();
rclcpp::shutdown();
return 0;
}
五、 老唐的真机避坑清单
- 总线驱动(EtherCAT / CAN)本身的实时性:如果用 EtherCAT(如 SOEM / IgH),SOEM 默认的
ec_send_processdata没问题,但不要在循环周期内做 Mailbox 读取(如 SDO 读取温度)。SDO 必须下放到非实时异步线程处理,周期线程只做 PDO 数据交换。 - 缺页中断(Page Fault)排查:即使做了
mlockall,栈空间(Stack)如果在运行时超限,依然会触发缺页。建议在初始化阶段预先调用memset触碰分配的最大栈空间。 - RMW 的选择:如果网络通信量大,默认的 Fast DDS 在高频调度下可能会有内存锁竞争。尝试切换为 Cyclone DDS,通常在局域网内抖动表现更加可控。
- 日志输出规范:开发期如果需要监控高频数据,建议写入共享内存(Shared Memory),或者通过无锁环形队列传递给另一个普通优先级的 ROS 2 Topic 发布线程,千万别直接在控制循环里
printf。
把这一套流程打通之后,再上 MoveIt 2 做轨迹插补,或者接上层的具身策略(如 50 Hz 推理输出到 1 kHz 关节控制器),底盘和机械臂的动作平滑度会有非常直观的提升。实操中有任何内核或者总线问题,随时在评论区交流。
许可协议:CC BY-NC 4.0
更新于 12 小时前
觉得文章有帮助?点个赞吧!
0 条评论


