宇树四足机器狗代码详解[unitree_ros]中的unitree_legged_control功能包:代码框架整理3

#这一篇文章会讲讲[unitree_ros](https://github.com/unitreerobotics/unitree_ros)的代码框架,仅代表个人看法,写出来加深自己的印象,欢迎交流。

#附上宇树家的学习资料[《四足机器人控制算法--建模、控制与实践》](https://detail.tmall.com/item.htm?spm=a212k0.12153887.0.0.5487687dBgiovR&id=704510718152)#

下载了GitHub上的压缩包之后,有五个文件(第五个unitree_ros_to_real是空的),

robots装的是宇树家不同机器狗的描述文件

unittree_controller是一个控制器功能包

unitree_legged_control是底层电机控制功能包

前面两篇文章把unittree_controller控制器功能包的代码分析完了,这篇文章分析unitree_legged_control是底层电机控制功能包

如果对于CMakeLists和package.xml不知道是干嘛的,可以去学习一下ros的基本架构。

1.joint_controller.h

/************************************************************************
Copyright (c) 2018-2019, Unitree Robotics.Co.Ltd. All rights reserved.
Use of this source code is governed by the MPL-2.0 license, see LICENSE.
************************************************************************/
//定义了一个名为 UnitreeJointController 的类,该类继承自 controller_interface::Controller,用于实现ROS中的关节控制器,可能是用于控制Unitree Robotics公司制造的机器人腿部的电机
#ifndef _UNITREE_ROS_JOINT_CONTROLLER_H_//这是预处理器指令,用于防止头文件被多次包含,是C++中常见的头文件保护宏
#define _UNITREE_ROS_JOINT_CONTROLLER_H_

#include <ros/node_handle.h>//节点处理
#include <urdf/model.h>//urdf模型解析
#include <control_toolbox/pid.h>//PID控制
#include <boost/scoped_ptr.hpp>//包含Boost库中的智能指针
#include <boost/thread/condition.hpp>//包含Boost库中的线程同步机制的头文件
#include <realtime_tools/realtime_publisher.h>//实时发布
#include <hardware_interface/joint_command_interface.h>//包含硬件接口
#include <controller_interface/controller.h>//控制器接口的头文件,这些接口定义了与机器人硬件通信的标准方式。
#include <std_msgs/Float64.h>//用于发射和接收64位浮点数
#include <realtime_tools/realtime_buffer.h>//包含实时缓冲区的头文件,用于同步节点间的实时消息传递
#include <controller_interface/controller.h>
#include <hardware_interface/joint_command_interface.h>
#include "unitree_legged_msgs/MotorCmd.h"
#include "unitree_legged_msgs/MotorState.h"
#include <geometry_msgs/WrenchStamped.h>//
#include "unitree_joint_control_tool.h"//包含自定义的Unitree关节控制工具的头文件,可能包含一些用于控制机器人关节的辅助函数或类

#define PMSM      (0x0A) //一些宏定义值 16进制,代表10 ,应该是电机模式 mode=10 PMSM
#define BRAKE     (0x00)
#define PosStopF  (2.146E+9f) //位置停止
#define VelStopF  (16000.0f)  //速度停止

namespace unitree_legged_control
{
    class UnitreeJointController: public controller_interface::Controller<hardware_interface::EffortJointInterface>
    {
private:
        hardware_interface::JointHandle joint;//定义一个关节句柄,用于与机器人的硬件接口进行交互
        ros::Subscriber sub_cmd, sub_ft;//两个订阅者Subscriber,用于接收控制命令sub_cmd,和接收力矩反馈sub_ft
        // ros::Publisher pub_state;
        control_toolbox::Pid pid_controller_;//实例化一个PID控制对象pid_controller_
        boost::scoped_ptr<realtime_tools::RealtimePublisher<unitree_legged_msgs::MotorState> > controller_state_publisher_ ;//一个智能指针,指向实时发布器,用于发布电机状态

public:
        // bool start_up;
        std::string name_space;//定义变量,存储命名空间
        std::string joint_name;//存储关节名称
        float sensor_torque;//用于存储传感器测量的扭矩值
        bool isHip, isThigh, isCalf, rqtTune;//布尔变量,表示关节类型和是否使用rqt工具进行PID参数调整。
        urdf::JointConstSharedPtr joint_urdf;//指向urdf关节的指针
        realtime_tools::RealtimeBuffer<unitree_legged_msgs::MotorCmd> command;//实时缓冲区,用于存储接到的电机命令
        unitree_legged_msgs::MotorCmd lastCmd;//定义结构体变量 lastCmd
        unitree_legged_msgs::MotorState lastState;//
        ServoCmd servoCmd;//可能是一个用于控制伺服电机的自定义命令结构体,定义在unitree_joint_control_tool.h

        UnitreeJointController();//构造函数和析构函数
        ~UnitreeJointController();
        virtual bool init(hardware_interface::EffortJointInterface *robot, ros::NodeHandle &n);//虚函数,初始化函数,设置控制器的参数和订阅者
        virtual void starting(const ros::Time& time);//启动前调用,应该是开计时器?
        virtual void update(const ros::Time& time, const ros::Duration& period);//更新函数,根据实时循环调用,实现控制逻辑
        virtual void stopping();//停止时调用,开始清理
        void setTorqueCB(const geometry_msgs::WrenchStampedConstPtr& msg);//回调函数,用于处理订阅的力/扭矩和电机命令消息。
        void setCommandCB(const unitree_legged_msgs::MotorCmdConstPtr& msg);
        void positionLimits(double &position);//获取位置、速度、力的极限值)
        void velocityLimits(double &velocity);
        void effortLimits(double &effort);

        void setGains(const double &p, const double &i, const double &d, const double &i_max, const double &i_min, const bool &antiwindup = false);
        void getGains(double &p, double &i, double &d, double &i_max, double &i_min, bool &antiwindup);//用于设置和获取PID控制器的增益
        void getGains(double &p, double &i, double &d, double &i_max, double &i_min);

    };
}

#endif

2.joint_controller.cpp(具体实现文件)

/************************************************************************
Copyright (c) 2018-2019, Unitree Robotics.Co.Ltd. All rights reserved.
Use of this source code is governed by the MPL-2.0 license, see LICENSE.
************************************************************************/
                                                        //这个文件定义了unitree_legged_control命名空间,注册了一个ROS插件用于电机控制
// #include "unitree_legged_control/joint_controller.h"
#include "joint_controller.h"
#include <pluginlib/class_list_macros.h>//这个包含指令是用于引入插件库的宏定义。pluginlib是ROS中用于管理插件的一个库
//class_list_macros.h提供了宏,这些宏用于自动注册插件和创建类列表,使得插件能够在运行时被识别和加载
// #define rqtTune // use rqt or not

namespace unitree_legged_control //定义了unitree_legged_control命名空间,用于封装相关的代码以避免命名冲突
{

    UnitreeJointController::UnitreeJointController(){
        memset(&lastCmd, 0, sizeof(unitree_legged_msgs::MotorCmd));
        memset(&lastState, 0, sizeof(unitree_legged_msgs::MotorState));
        memset(&servoCmd, 0, sizeof(ServoCmd)); //构造函数 
    } //使用memset函数将这些变量的内存区域设置为零。这通常用于初始化结构体或类的实例,确保所有成员变量都有确定的初始值

    UnitreeJointController::~UnitreeJointController(){
        sub_ft.shutdown();//析构函数,调用了两个订阅者的shutdown方法,这将取消订阅话题,释放相关资源
        sub_cmd.shutdown();
    }
//回调函数,用于处理订阅到的力矩消息
    void UnitreeJointController::setTorqueCB(const geometry_msgs::WrenchStampedConstPtr& msg)
    {
        if(isHip) sensor_torque = msg->wrench.torque.x;
        else sensor_torque = msg->wrench.torque.y;
        // printf("sensor torque%f\n", sensor_torque);
    }//根据isHip变量的值,它从消息中提取x或y分量作为传感器扭矩,并将其存储在sensor_torque成员变量中。注释掉的printf语句可能是用于调试目的
//回调函数,用于处理订阅到的电机命令消息
    void UnitreeJointController::setCommandCB(const unitree_legged_msgs::MotorCmdConstPtr& msg)
    {
        lastCmd.mode = msg->mode;
        lastCmd.q = msg->q;
        lastCmd.Kp = msg->Kp;
        lastCmd.dq = msg->dq;
        lastCmd.Kd = msg->Kd;
        lastCmd.tau = msg->tau;
        // the writeFromNonRT can be used in RT, if you have the guarantee that
        //  * no non-rt thread is calling the same function (we're not subscribing to ros callbacks)
        //  * there is only one single rt thread
        command.writeFromNonRT(lastCmd);//使用writeFromNonRT方法将命令写入实时缓冲区,这个方法适用于实时线程,用于在实时和非实时线程之间同步数据
    }

    // Controller initialization in non-realtime ROS控制器接口的一部分,用于在非实时环境中初始化控制器
    bool UnitreeJointController::init(hardware_interface::EffortJointInterface *robot, ros::NodeHandle &n)
    {//接受两个参数:一个指向EffortJointInterface的指针,用于与机器人的硬件接口交互;一个ros::NodeHandle引用,用于处理ROS参数和话题
        isHip = false;//标志变量设为false
        isThigh = false;
        isCalf = false;
        // rqtTune = false; //用于判断是否用rqt工具调整了pid参数
        sensor_torque = 0;//传感器力矩初设为0
        name_space = n.getNamespace();
        if (!n.getParam("joint", joint_name)){//尝试从ROS参数服务器获取名为joint的参数,该参数应包含要控制的关节名称。
            ROS_ERROR("No joint given in namespace: '%s')", n.getNamespace().c_str());//不存在就打印报错信息
            return false;
        }
        
        // load pid param from ymal only if rqt need 
        // if(rqtTune) {
#ifdef rqtTune //使用ros::NodeHandle创建的子节点句柄初始化pid_controller_成员变量。这个子节点句柄指向参数服务器上的pid节点,其中应包含PID控制器的参数
            // Load PID Controller using gains set on parameter server
            if (!pid_controller_.init(ros::NodeHandle(n, "pid")))
                return false;
#endif
        // }
//解析urdf模型 设置关节属性、初始化ros发布者和订阅者
        urdf::Model urdf; // Get URDF info about joint
        if (!urdf.initParamWithNodeHandle("robot_description", n)){//使用initParamWithNodeHandle方法初始化URDF模型,该方法从ROS参数服务器获取名为robot_description的URDF描述。如果初始化失败,打印错误并返回false
            ROS_ERROR("Failed to parse urdf file");
            return false;
        }
        joint_urdf = urdf.getJoint(joint_name);//通过getJoint方法获取名为joint_name的关节的URDF表示
        if (!joint_urdf){
            ROS_ERROR("Could not find joint '%s' in urdf", joint_name.c_str());//没找到就打印出来
            return false;
        }
        if(joint_name == "FR_hip_joint" || joint_name == "FL_hip_joint" || joint_name == "RR_hip_joint" || joint_name == "RL_hip_joint"){
            isHip = true;//如果名称等于hip髋关节,就把hip标志变量设为真
        }
        if(joint_name == "FR_calf_joint" || joint_name == "FL_calf_joint" || joint_name == "RR_calf_joint" || joint_name == "RL_calf_joint"){
            isCalf = true;//如果名称等于Calf小腿关节,标志变量设为真
        }        
        joint = robot->getHandle(joint_name);//使用EffortJointInterface的getHandle方法获取名为joint_name的关节的硬件接口句柄。

        // Start command subscriber
        sub_ft = n.subscribe(name_space + "/" +"joint_wrench", 1, &UnitreeJointController::setTorqueCB, this);//订阅名为name_space + "/joint_wrench"的话题,回调函数为setTorqueCB,存到sensor_torque
        sub_cmd = n.subscribe("command", 20, &UnitreeJointController::setCommandCB, this);//订阅话题名称为“command”的topic,队列长度为20,消息在回调函数中存到lastCmd

        // pub_state = n.advertise<unitree_legged_msgs::MotorState>(name_space + "/state", 20); 
        // Start realtime state publisher 开始发布实时状态
        controller_state_publisher_.reset(
            new realtime_tools::RealtimePublisher<unitree_legged_msgs::MotorState>(n, name_space + "/state", 1));//使用realtime_tools::RealtimePublisher创建一个实时发布者,用于发布关节状态        

        return true;
    }
//调用pid_controller_.setGains方法设置pid参数p i d i最大值 i最小值 布尔值antiwindup,指示是否启用防积分饱和功能
    void UnitreeJointController::setGains(const double &p, const double &i, const double &d, const double &i_max, const double &i_min, const bool &antiwindup)
    {
        pid_controller_.setGains(p,i,d,i_max,i_min,antiwindup);
    }
//调用pid_controller_.setGains方法获得pid参数p i d i最大值 i最小值 布尔值antiwindup,指示是否启用防积分饱和功能
    void UnitreeJointController::getGains(double &p, double &i, double &d, double &i_max, double &i_min, bool &antiwindup)
    {
        pid_controller_.getGains(p,i,d,i_max,i_min,antiwindup);
    }
//一个重载,当调用者不需要返回防积分饱和状态时使用。它通过一个临时布尔变量dummy来忽略防积分饱和状态的返回值
    void UnitreeJointController::getGains(double &p, double &i, double &d, double &i_max, double &i_min)
    {
        bool dummy;
        pid_controller_.getGains(p,i,d,i_max,i_min,dummy);
    }

    // Controller startup in realtime
    void UnitreeJointController::starting(const ros::Time& time)
    {
        // lastCmd.Kp = 0;
        // lastCmd.Kd = 0;
        double init_pos = joint.getPosition();//设置初始位置为 获取当前位置
        lastCmd.q = init_pos;   //把值赋给lastCmd
        lastState.q = init_pos; //把值赋给lastState
        lastCmd.dq = 0;
        lastState.dq = 0;
        lastCmd.tau = 0;
        lastState.tauEst = 0;
        command.initRT(lastCmd); //初始化缓冲区

        pid_controller_.reset();//重置pid控制器
    }

    // Controller update loop in realtime 实时更新控制器,参数分别表示当前时间点和周期
    void UnitreeJointController::update(const ros::Time& time, const ros::Duration& period)
    {
        double currentPos, currentVel, calcTorque;//声明变量 当前位置 当前速度 计算得到的扭矩
        lastCmd = *(command.readFromRT());//从实时命令缓冲区读取最新的命令,并将其存储在lastCmd变量中

        // set command data
        if(lastCmd.mode == PMSM) { //mode=10 PMSM(永磁同步电机模式)
            servoCmd.pos = lastCmd.q;
            positionLimits(servoCmd.pos);//限制位置的范围
            servoCmd.posStiffness = lastCmd.Kp;//位置刚度设置
            if(fabs(lastCmd.q - PosStopF) < 0.00001){
                servoCmd.posStiffness = 0; //有一点抖动正常,不可能位置完全为0,而阈值就是PosStopF
            }
            servoCmd.vel = lastCmd.dq;  //速度值也做一次重复的工作
            velocityLimits(servoCmd.vel);
            servoCmd.velStiffness = lastCmd.Kd;
            if(fabs(lastCmd.dq - VelStopF) < 0.00001){
                servoCmd.velStiffness = 0;
            }
            servoCmd.torque = lastCmd.tau; //赋值力矩给servoCmd
            effortLimits(servoCmd.torque); //限制力矩的值
        }
        if(lastCmd.mode == BRAKE) { //如果退出永磁同步电机模式
            servoCmd.posStiffness = 0;
            servoCmd.vel = 0;
            servoCmd.velStiffness = 20; //速度刚度(即阻尼c)
            servoCmd.torque = 0;
            effortLimits(servoCmd.torque);
        }

        // } else {
        //     servoCmd.posStiffness = 0;
        //     servoCmd.velStiffness = 5;
        //     servoCmd.torque = 0;
        // }
        
        // rqt set P D gains
        // if(rqtTune) {
#ifdef rqtTune //如果定义了rqtTune,就编译下面
            double i, i_max, i_min;
            getGains(servoCmd.posStiffness,i,servoCmd.velStiffness,i_max,i_min);
#endif  //通过getGains函数获取当前的PID增益,包括比例增益、积分增益、积分最大值、积分最小值和速度刚度
        // } 

        currentPos = joint.getPosition();  //从硬件接口获取关节的当前位置,并赋值给currentPos
        currentVel = computeVel(currentPos, (double)lastState.q, (double)lastState.dq, period.toSec());//据当前位置、上次状态的位置和速度,以及周期(period.toSec()),计算当前速度currentVel
        calcTorque = computeTorque(currentPos, currentVel, servoCmd);   //调用computeTorque函数,根据当前位置、当前速度、电机当前状态 计算所需的扭矩
        effortLimits(calcTorque);//限值

        joint.setCommand(calcTorque);//将计算出的扭矩calcTorque作为命令发送到硬件接口

        lastState.q = currentPos; //将当前角度和角速度设为上一个状态
        lastState.dq = currentVel;
        // lastState.tauEst = calcTorque;
        // lastState.tauEst = sensor_torque;
        lastState.tauEst = joint.getEffort();//这个值按道理是硬件接口来的力矩,但是为什么不用sensor_torque呢,区别在哪呢?

        // pub_state.publish(lastState);
        // publish state
        if (controller_state_publisher_ && controller_state_publisher_->trylock()) { //如果实时发布器存在并且成功锁定,更新并发布关节状态
            controller_state_publisher_->msg_.q = lastState.q; //给发布器的消息赋值
            controller_state_publisher_->msg_.dq = lastState.dq;
            controller_state_publisher_->msg_.tauEst = lastState.tauEst;
            controller_state_publisher_->unlockAndPublish(); //解除锁定
        }

        // printf("sensor torque%f\n", sensor_torque);

        // if(joint_name == "wrist1_joint") printf("wrist1 setp:%f  getp:%f t:%f\n", servoCmd.pos, currentPos, calcTorque);
    }

    // Controller stopping in realtime 实时控制器的停止方法,当前为空实现
    void UnitreeJointController::stopping(){}
//根据URDF定义的限制,使用clamp函数限制位置值
    void UnitreeJointController::positionLimits(double &position)
    {
        if (joint_urdf->type == urdf::Joint::REVOLUTE || joint_urdf->type == urdf::Joint::PRISMATIC)
            clamp(position, joint_urdf->limits->lower, joint_urdf->limits->upper);
    }
//根据urdf定义限制,限值速度值
    void UnitreeJointController::velocityLimits(double &velocity)
    {
        if (joint_urdf->type == urdf::Joint::REVOLUTE || joint_urdf->type == urdf::Joint::PRISMATIC)
            clamp(velocity, -joint_urdf->limits->velocity, joint_urdf->limits->velocity);
    }
//根据URDF定义的限制,使用clamp函数限制扭矩值
    void UnitreeJointController::effortLimits(double &effort)
    {
        if (joint_urdf->type == urdf::Joint::REVOLUTE || joint_urdf->type == urdf::Joint::PRISMATIC)
            clamp(effort, -joint_urdf->limits->effort, joint_urdf->limits->effort);
    }

} // namespace 结束这个unitree_legged_control命名空间

// Register controller to pluginlib 使用PLUGINLIB_EXPORT_CLASS宏将UnitreeJointController类注册为ROS插件,使其能够在运行时被识别和加载
PLUGINLIB_EXPORT_CLASS(unitree_legged_control::UnitreeJointController, controller_interface::ControllerBase);

3.unitree_joint_control_tool.h

/************************************************************************
Copyright (c) 2018-2019, Unitree Robotics.Co.Ltd. All rights reserved.
Use of this source code is governed by the MPL-2.0 license, see LICENSE.
************************************************************************/
                                            //定义了一个名为 LaikagoControlTool 的工具集,用于机器人关节控制
// #ifndef _UNITREE_JOINT_CONTROL_TOOL_H_
// #define _UNITREE_JOINT_CONTROL_TOOL_H_
#ifndef _LAIKAGO_CONTROL_TOOL_H_ //和上面被注释的语句只有名称区别,没有功能区别
#define _LAIKAGO_CONTROL_TOOL_H_

#include <stdio.h>//标准输入输出库
#include <stdint.h>//标准整数
#include <algorithm>//算法
#include <math.h>//数学方法库

#define posStopF (2.146E+9f)  // stop position control mode
#define velStopF (16000.0f)   // stop velocity control mode

typedef struct //定义这个ServoCmd结构体
{
    uint8_t mode;//模式
    double pos;//位置
    double posStiffness;//位置刚度
    double vel;//速度
    double velStiffness;//速度刚度(阻尼)
    double torque;//力矩
} ServoCmd;

double clamp(double&, double, double);  //使用方法eg. clamp(1.5, -1, 1) = 1
double computeVel(double current_position, double last_position, double last_velocity, double duration);  // get current velocity
double computeTorque(double current_position, double current_velocity, ServoCmd&);  // get torque

#endif

4.unitree_joint_control_tool.cpp

#include "unitree_joint_control_tool.h"
//浮点数的限制值函数clamp
float clamp(float &val, float min_val, float max_val)
{
    val = std::min(std::max(val, min_val), max_val);
}
//双精度浮点数 限值在范围内
double clamp(double &val, double min_val, double max_val)
{
    val = std::min(std::max(val, min_val), max_val);
}
//据当前位置、上次位置、上次速度、周期来计算速度
double computeVel(double currentPos, double lastPos, double lastVel, double period)
{
    return lastVel*0.35f + 0.65f*(currentPos-lastPos)/period;//简单的滤波方法,上次速度占0.35权重,这次计算的速度占0.65
}
//计算力矩
double computeTorque(double currentPos, double currentVel, ServoCmd &cmd)
{
    double targetPos, targetVel, targetTorque, posStiffness, velStiffness, calcTorque;
    targetPos = cmd.pos; //目标位置
    targetVel = cmd.vel; //目标速度
    targetTorque = cmd.torque;//目标力矩
    posStiffness = cmd.posStiffness;//刚度
    velStiffness = cmd.velStiffness;//阻尼
    if(fabs(targetPos-posStopF) < 1e-6) posStiffness = 0;
    if(fabs(targetVel-velStopF) < 1e-6) velStiffness = 0;
    calcTorque = posStiffness*(targetPos-currentPos) + velStiffness*(targetVel-currentVel) + targetTorque;//计算力矩
    return calcTorque;
}

四足机器人控制是指对四条腿进行控制实现机器人的运动和稳定。四足机器人控制的主要目标是保持机器人的平衡、实现稳定的步态和精确的姿态控制。 在四足机器人的控制,有两种常见的方法:基于全局视觉或传感器反馈的开环控制和反馈控制。 在开环控制机器人会根据预先编程好的步态模式来移动。这种控制方法相对简单,但对于障碍物的感知和环境的变化不敏感。因此,在复杂环境下,开环控制的稳定性和适应性较差。 反馈控制是一种更高级的控制方法。它通过不断地从传感器获取信息,对机器人的状态进行实时调整和反馈。这种控制方法可以根据环境的变化实时调整步态和姿态,提高机器人的稳定性和适应性。 在四足机器人控制,还需要考虑到运动规划和路径规划。运动规划是确定机器人如何移动的过程,路径规划是确定机器人如何选择最佳路径的过程。这些规划过程需要考虑机器人的动力学特性和环境的限制,以尽可能地避免碰撞和保持平衡。 在现代四足机器人的控制,还应用了机器学习和人工智能的方法。通过不断地与环境交互学习和优化,机器人可以逐渐提高自己的控制能力和适应性。 总而言之,四足机器人控制是一个复杂而综合的任务,需要考虑到稳定性、适应性、路径规划和环境感知等方面。随着技术的不断进步,四足机器人的控制将变得更加精确和智能化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值