ros::init(argc, argv, "damiao_hardware_node");
ros::NodeHandle nh;
ros::NodeHandle nh_private("~");
  • ros::init:你的程序来到 ROS 世界,第一件事就是去注册中心登记名字:“大家好,我是 damiao_hardware_node”。

  • nhnh_private("~"):你向系统申请了两部通讯工具。

    • nh(公共大喇叭):用来向全网广播你的关节状态(/joint_states)。

    • nh_private(私密对讲机):带波浪号 ~,专门用来悄悄读取 launch 文件里加载的属于你自己的 motor_config.yaml 配置文件,绝不会和其他节点弄混。

damiao::DmHW robot;
if (!robot.init(nh, nh_private))
{
    ROS_ERROR("Failed to initialize Damiao Hardware Interface!");
    return -1;
}

init:串口初始化,使能电机,开启读取电机数据线程

注册关节,位置接口:ros上层得知要控制电机,只需要王cmd_pos的地址写数据就行

controller_manager::ControllerManager cm(&robot, nh);

为什么必须要传 &robot?(解决内存共享问题)

  • robot 是什么:它是你自己写的 DmHW 类的实例。在这个实例的内存里,存着你之前注册的各种接口(jointStateInterface_positionJointInterface_),以及电机实际位置 (pos) 和目标位置 (cmd_pos) 的物理内存地址。

  • 传递 &robot 的底层逻辑ControllerManager (cm) 本身只是一个纯数学算法库,它生来就是没有数据的。当你使用取地址符 &robot 传给它时,cm 就直接获得了访问 robot 内部变量的权限

  • 使用结果:从此以后,cm 内部的控制算法在计算时,不需要调用任何函数去向 robot 索要数据,而是直接去读写 robot 内存里的 poscmd_pos 变量。这在 C++ 中叫做“零拷贝数据交互”,效率极高。

2. 为什么必须要传 nh?(解决 ROS 通信问题)

  • nh 是什么ros::NodeHandle 是 ROS 系统提供的通信接口。有了它,C++ 程序才能在 ROS 网络中发布话题(Topic)或创建服务(Service)。

  • 传递 nh 的底层逻辑cm 需要接收外部的指令(比如你想启动哪个控制器、停止哪个控制器)。通过接收你传进来的 nhcm 会在后台自动为你生成并注册一系列标准的 ROS 服务(Services)。

  • 使用结果:只要这行代码一执行,你在终端里输入 rosservice list,就会立刻看到多出了一大堆服务,比如:

    • /controller_manager/load_controller

    • /controller_manager/switch_controller

    • /controller_manager/list_controllers 此时,外部程序就可以通过调用这些服务,来操控你的 C++ 代码。
       

这行代码仅仅是完成了“初始化”。在真正的机器人运行中,它是按以下 3 个步骤被使用的:

第一步:代码层面的初始化(就是这行代码本身) 当 C++ 的 main 函数执行到这一行时,cm 对象被创建。它绑定了底层硬件内存(&robot),并在 ROS 网络中注册了控制服务(利用 nh)。此时,算法处于待机状态,没有任何运算发生。

第二步:外部唤醒(Launch 文件中的 spawner 节点) 在你的 .launch 文件中,有一行专门针对它的指令: <node name="controller_spawner" pkg="controller_manager" type="spawner" args="joint_state_controller arm_controller"/>

  • 这个叫 spawner 的脚本会去调用第一步里 cm 创建的 ROS 服务。

  • 它对 cm 下达指令:“请立刻加载 joint_state_controllerarm_controller 这两个控制算法,并让它们开始运行。”

  • 此时,cm 会根据指令,在内存中把这两个算法实例化。

第三步:循环调用运算(C++ 代码中的 cm.update 在 C++ 的 while(ros::ok()) 死循环中,你写了 cm.update(current_time, elapsed_time);

  • 每次执行到这句话,cm 就会触发刚才加载的那两个控制算法。

  • 算法会利用第一步拿到的 &robot 权限,读取电机的实际角度。

  • 算法(如 arm_controller)接收 MoveIt 传来的目标轨迹,利用 PID 公式计算出当前的控制量,并直接把计算结果覆盖写回到 &robotcmd_pos 内存地址中。

  • 随后,你的 robot.write() 函数就会把这个新的 cmd_pos 变成 CAN 数据发给达妙电机。

    ros::AsyncSpinner spinner(1);
    spinner.start();

ros::AsyncSpinner:这是 ROS 提供的一个异步多线程处理器(Spinner 意为“轮询器”)。

(1):括号里的 1 代表你想分配几个独立的子线程。写 1 就意味着你在主线程(跑 while 循环的那个)之外,额外开辟了 1 个平行的后台线程,专门用来处理 ROS 的消息回调。

在后面有while循环,需要有独立的子线程去处理movelt发过来的数据

spinner.start();

函数作用:启动这个后台线程。

    ros::Rate rate(200); 
    ros::Time last_time = ros::Time::now();

代表频率是 200Hz。意思是告诉程序:“接下来你要干的活,必须每秒钟执行 200 次

记录下程序运行到这一行的当前绝对时间

ROS_INFO("Damiao Hardware Node Started!");

打印出一句话:“达妙硬件节点已启动!”

while (ros::ok())
{

死循环

        ros::Time current_time = ros::Time::now();
        ros::Duration elapsed_time = current_time - last_time;
        last_time = current_time;

看一眼现在几点了,存到 current_time(当前时间)里。

用现在的时刻减去刚才记录的时刻,算出了一个时间段 elapsed_time(流逝的时间差)。

控制管家 cm 里的 PID 算法(比如算速度、算加速度)极度依赖物理时间。如果时间算不准,算出来的控制指令就是错的,机械臂就会疯狂发抖。

robot.read(current_time, elapsed_time); // 读电机

调用你写的底层硬件代码,通过串口去问达妙电机:“你现在实际上转到多少度了?” 然后把度数存进共享内存里。

cm.update(current_time, elapsed_time);  // 算控制

控制管家(ControllerManager)出场!它把刚才读上来的真实角度,和 MoveIt 下发的目标角度做对比。结合时间差(elapsed_time),它经过一顿复杂的数学运算,算出:“为了消除误差,下一刻电机应该转到某某位置。” 并把这个新位置写进内存。

robot.write(current_time, elapsed_time); // 写电机

再次调用底层代码,把刚才管家算出来的新目标位置,打包成 CAN 通信的十六进制指令,顺着数据线狠狠地发给物理电机。电机收到后,就会嗡嗡地转动一丝丝距离。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐