【ROS2机器人入门到实战】里程计计算-速度积分

2023-10-26

14.里程计计算-速度积分

写在前面

  1. 当前平台文章汇总地址:ROS2机器人从入门到实战
  2. 获取完整教程及配套资料代码,请关注公众号<鱼香ROS>获取
  3. 教程配套机器人开发平台:两驱版| 四驱版
  4. 为方便交流,搭建了机器人技术问答社区:地址 fishros.org.cn

你好,我是小鱼,前面两节中我们完成机器人底盘正逆解的计算,我们通过机器人的运动学逆解完成了机器人实时的角速度和线速度的测量,那我们能不能利用对线速度和角速度的积分,计算机器人当前的位置呢?答案肯定是可以的,那么本节我们就来编写代码实现机器人的里程计。

一、里程计计算原理

在某一个时间段 t t t中,机器人的线速度为 v t v_t vt ,角速度为 w t w_t wt ,机器人在初始时刻的位置为 x t , y t x_t,y_t xt,yt 朝向为 θ t \theta _t θt ,求经过 t t t时刻是机器人新的位置和朝向,这一过程中假设机器人仅在平面上运动。

在这一段时间内机器人前进的距离为 d d d
d = v t ∗ t d = v_t*t d=vtt
转过的角度为 θ \theta θ
θ = ω t ∗ t \theta =\omega _t*t θ=ωtt
则机器人新的角度为 θ t + 1 \theta _{t+1} θt+1
θ t + 1 = θ t + θ \theta _{t+1} = \theta _{t}+\theta θt+1=θt+θ
我们将机器人前进的距离根据其朝向分解为在x和y轴上的位移量,则可得出
x t + 1 = x t + d ∗ c o s ( θ t + 1 ) y t + 1 = y t + d ∗ s o s ( θ t + 1 ) x_{t+1} = x_t + d*cos(\theta_{t+1}) \\ y_{t+1} = y_t + d*sos(\theta_{t+1}) xt+1=xt+dcos(θt+1)yt+1=yt+dsos(θt+1)
有了公式,我们开始撸代码。

二、编写代码

先修改Kinematics.h头文件,增加角度范围限制,里程计更新和里程计结构体定义,完成后代码如下:

/**
 * @file Kinematics.h
 * @author fishros@foxmail.com
 * @brief 机器人模型设置,编码器轮速转换,ODOM推算,线速度角速度分解
 * @version V1.0.0
 * @date 2022-12-10
 *
 * @copyright Copyright www.fishros.com (c) 2022
 *
 */
#ifndef __KINEMATICS_H__
#define __KINEMATICS_H__
#include <Arduino.h>

typedef struct
{
    uint8_t id;                // 电机编号
    uint16_t reducation_ratio; // 减速器减速比,轮子转一圈,电机需要转的圈数
    uint16_t pulse_ration;     // 脉冲比,电机转一圈所产生的脉冲数
    float wheel_diameter;      // 轮子的外直径,单位mm

    float per_pulse_distance;  // 无需配置,单个脉冲轮子前进的距离,单位mm,设置时自动计算
                               // 单个脉冲距离=轮子转一圈所行进的距离/轮子转一圈所产生的脉冲数
                               // per_pulse_distance= (wheel_diameter*3.1415926)/(pulse_ration*reducation_ratio)
    uint32_t speed_factor;     // 无需配置,计算速度时使用的速度因子,设置时自动计算,speed_factor计算方式如下
                               // 设 dt(单位us,1s=1000ms=10^6us)时间内的脉冲数为dtick
                               // 速度speed = per_pulse_distance*dtick/(dt/1000/1000)=(per_pulse_distance*1000*1000)*dtic/dt
                               // 记 speed_factor = (per_pulse_distance*1000*1000)
    int16_t motor_speed;       // 无需配置,当前电机速度mm/s,计算时使用
    int64_t last_encoder_tick; // 无需配置,上次电机的编码器读数
    uint64_t last_update_time; // 无需配置,上次更新数据的时间,单位us
} motor_param_t;


/**
 * @brief 里程计相关信息,根据轮子速度信息和运动模型推算而来
 *
 */
typedef struct
{
    float x;                 // 坐标x
    float y;                 // 坐标y
    float yaw;               // yaw
    float linear_speed;      // 线速度
    float angular_speed;     // 角速度
} odom_t;



class Kinematics
{
private:
    motor_param_t motor_param_[2];
    float wheel_distance_; // 轮子间距
    odom_t odom_;          // 里程计数据
public:
    Kinematics(/* args */) = default;
    ~Kinematics() = default;

    static void TransAngleInPI(float angle,float& out_angle);

    /**
     * @brief 设置电机相关参数
     * 
     * @param id 
     * @param reducation_ratio 
     * @param pulse_ration 
     * @param wheel_diameter 
     */
    void set_motor_param(uint8_t id, uint16_t reducation_ratio, uint16_t pulse_ration, float wheel_diameter);
    /**
     * @brief 设置运动学相关参数
     * 
     * @param wheel_distance 
     */
    void set_kinematic_param(float wheel_distance);

    /**
     * @brief 运动学逆解,输入机器人当前线速度和角速度,输出左右轮子应该达到的目标速度
     * 
     * @param line_speed 
     * @param angle_speed 
     * @param out_wheel1_speed 
     * @param out_wheel2_speed 
     */
    void kinematic_inverse(float line_speed, float angle_speed, float &out_wheel1_speed, float &out_wheel2_speed);


    /**
     * @brief 运动学正解,输入左右轮子速度,输出机器人当前线速度和角速度
     * 
     * @param wheel1_speed 
     * @param wheel2_speed 
     * @param line_speed 
     * @param angle_speed 
     */
    void kinematic_forward(float wheel1_speed, float wheel2_speed, float &line_speed, float &angle_speed);

    /**
     * @brief 更新轮子的tick数据
     * 
     * @param current_time 
     * @param motor_tick1 
     * @param motor_tick2 
     */
    void update_motor_ticks(uint64_t current_time, int32_t motor_tick1, int32_t motor_tick2);

    /**
     * @brief 获取轮子当前速度
     * 
     * @param id 
     * @return float 
     */
    float motor_speed(uint8_t id);

    /**
     * @brief 更新机器人里程计信息
     * 
     * @param dt 间隔时间dt
     */
    void update_bot_odom(uint32_t dt);
    
    /**
     * @brief 获取里程计函数
     * 
     * @return odom_t& 
     */
    odom_t &odom();

};

#endif // __KINEMATICS_H__

接着在Kinematics.cpp中实现刚刚定义的函数,主要添加函数代码如下:

void Kinematics::update_bot_odom(uint32_t dt)
{
    static float linear_speed, angular_speed;
    float dt_s = (float)(dt / 1000) / 1000;

    this->kinematic_forward(motor_param_[0].motor_speed, motor_param_[1].motor_speed, linear_speed, angular_speed);

    odom_.angular_speed = angular_speed;
    odom_.linear_speed = linear_speed / 1000; // /1000(mm/s 转 m/s)

    odom_.yaw += odom_.angular_speed * dt_s;

    Kinematics::TransAngleInPI(odom_.yaw, odom_.yaw);
    

    /*更新x和y轴上移动的距离*/
    float delta_distance = odom_.linear_speed * dt_s; // 单位m
    odom_.x += delta_distance * std::cos(odom_.yaw);
    odom_.y += delta_distance * std::sin(odom_.yaw);

}

void Kinematics::TransAngleInPI(float angle, float &out_angle)
{
    if (angle > PI)
    {
        out_angle -= 2 * PI;
    }
    else if (angle < -PI)
    {
        out_angle += 2 * PI;
    }
}

odom_t &Kinematics::odom()
{
    return odom_;
}

同时修改update_motor_ticks函数,在其中添加update_bot_odom

void Kinematics::update_motor_ticks(uint64_t current_time, int32_t motor_tick1, int32_t motor_tick2)
{

    uint32_t dt = current_time - motor_param_[0].last_update_time;   // 计算时间差
    int32_t dtick1 = motor_tick1 - motor_param_[0].last_encoder_tick;   // 计算电机1脉冲差
    int32_t dtick2 = motor_tick2 - motor_param_[1].last_encoder_tick;   // 计算电机2脉冲差
    // 轮子速度计算
    motor_param_[0].motor_speed = dtick1 * (motor_param_[0].speed_factor / dt);   // 计算电机1轮子速度
    motor_param_[1].motor_speed = dtick2 * (motor_param_[1].speed_factor / dt);   // 计算电机2轮子速度

    motor_param_[0].last_encoder_tick = motor_tick1;   // 更新电机1上一次的脉冲计数
    motor_param_[1].last_encoder_tick = motor_tick2;   // 更新电机2上一次的脉冲计数
    motor_param_[0].last_update_time = current_time;   // 更新电机1上一次更新时间
    motor_param_[1].last_update_time = current_time;   // 更新电机2上一次更新时间

    // 更新机器人里程计
    this->update_bot_odom(dt);
}

修改main.cpp中加入打印里程计数据

void loop()
{
  static float out_motor_speed[2];
  static uint64_t last_update_info_time = millis();
  kinematics.update_motor_ticks(micros(), encoders[0].getTicks(), encoders[1].getTicks());
  out_motor_speed[0] = pid_controller[0].update(kinematics.motor_speed(0));
  out_motor_speed[1] = pid_controller[1].update(kinematics.motor_speed(1));
  motor.updateMotorSpeed(0, out_motor_speed[0]);
  motor.updateMotorSpeed(1, out_motor_speed[1]);

  unsigned long currentMillis = millis(); // 获取当前时间
  if (currentMillis - previousMillis >= interval)
  {                                 // 判断是否到达间隔时间
    previousMillis = currentMillis; // 记录上一次打印的时间
    float linear_speed, angle_speed;
    kinematics.kinematic_forward(kinematics.motor_speed(0), kinematics.motor_speed(1), linear_speed, angle_speed);
    Serial.printf("[%ld] linear:%f angle:%f\n", currentMillis, linear_speed, angle_speed);                       // 打印当前时间
    Serial.printf("[%ld] x:%f y:%f yaml:%f\n", currentMillis,kinematics.odom().x, kinematics.odom().y, kinematics.odom().yaw); // 打印当前时间
  }

  // 延迟10毫秒
  delay(10);
}

三、下载测试

下载代码,运行agent,点击RST按键。

sudo docker run -it --rm -v /dev:/dev -v /dev/shm:/dev/shm --privileged --net=host microros/micro-ros-agent:$ROS_DISTRO udp4 --port 8888 -v6

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

看到连接建立表示通信成功,接着用ros2 topic list

ros2 topic list

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

看到/cmd_vel表示正常,接着我们使用teleop_twist_keyboard进行键盘控制

ros2 run teleop_twist_keyboard teleop_twist_keyboard

先调整下速度,降低到0.05左右(50cm/s),然后使用i\j\j\k,测试。

可以先让机器人空转,点击i,让机器人前进用串口查看数据变化。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到每次大约增加0.5左右,数据正常。

四、总结

最后记得提交代码

git add .
git commit -m "feat(13.14):完成里程计计算-速度积分"
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【ROS2机器人入门到实战】里程计计算-速度积分 的相关文章

随机推荐

  • 组个最小数(Java)

    题目描述 给定数字0 9若干个 你可以以任意顺序排列这些数字 但必须全部使用 目标是使得最后得到的数字尽可能小 注意0不能做首位 例如 给定两个0 两个1 三个5 一个8 我们得到的最小的数就是10015558 现给定数字 请编写程序输出能
  • git点击pull后没有同步_github fork后的pull和保持同步

    前言 对github上的某个项目贡献自己的修改 但自己可能并没有那个仓库的权限 那要如何操作呢 git的机制和svn还是有些区别的 本文做些记录 思路1 clone项目到本地 有修改之后 直接提交到原作者仓库 思路2 forck项目到自己的
  • error C2041: illegal digit ‘9‘ for base ‘8‘

    错误日志 文本 八进制数值超过范围 1 gt E CProject test12 Source c 5 10 error C2041 illegal digit 8 for base 8 十六进制数值超过范围 1 gt E CProject
  • git解决报错“remote: Not Found fatal: XXX”以及“remote: Support for password authentication was removed...”

    目录 1 报错如下 remote Not Found fatal repository XXX not found 2 报错如下 remote Support for password authentication was removed
  • visio画虚框

    1 选中矩形 2 修改线条颜色 3 去掉填充色 4 虚线
  • 【Masked Autoencoders Are Scalable Vision Learners】

    原文链接 感谢原作者 论文阅读笔记 Masked Autoencoders Are Scalable Vision Learners 摘要 介绍 实现 MASKING MAE编码器 MAE解码器 简单的实现 在 ImageNet 上的简单测
  • tensorflow深度学习实战笔记(一):使用tensorflow slim自带的模型训练自己的数据

    目录 0 准备 1 数据处理 图片格式转成TFRecord格式 2 模型训练 3 验证训练后的效果 说明 此处可以模仿源码中inception v3的分类案例 slim预训练好的包含inception v1 inception v2 inc
  • mac免费的虚拟机怎么安装?VMware虚拟机免费获取教程

    大部分Mac用户大部分都是通过Parallels Desktop或者VMware Fusion Pro安装虚拟机 可两款虚拟机价值不菲 但是今天小编为大家带来VMware虚拟机免费获取教程 仅限个人或者非商业用途使用 VMware虚拟机免费
  • createBean源码--方便查看

    Class clz Class forName 类名 createBean resolveBeanClass mbd beanName Object o clz newInstance createBean doCreateBean cre
  • socket编程选项——setsockopt和getsockopt

    头文件 include
  • 深入理解Spring IOC和AOP

    文章目录 1 什么是 Spring 框架 1 1Spring简介 1 2Spring的好处 1 3Spring体系结构 2 理解Spring IoC 2 1IoC简介 2 2IoC的好处 2 3依赖注入和控制反转 依赖注入详解 依赖注入的三
  • NN编译栈之TVM研究报告

    前言 深度学习 神经网络应用日益广泛 多终端部署形成常态 从CPU ARM GPU到专用的神经网络加速器 深度学习处理器 不同的终端 不同的体系结构引起神经网络的碎片化 为每一款设备特别是专用的加速芯片部署深度学习是一件费力不讨好的事情 同
  • RNA-seq——上游分析练习(数据下载+hisat2+samtools+htseq-count)

    步骤 0 练习前准备 1 找到文章对应的数据集 2 下载数据集 3 与参考基因组进行比对 4 reads计数 5 踩过的一点小坑 写在前面 之前使用的数据是单端测序 但是现在的数据基本都是双端测序 所以又找了个双端测序的例子来练习 之前在单
  • 23种设计模式

    目录 创建型 1 Factory Method 工厂方法 2 Abstract Factory 抽象工厂 3 Builder 建造者 4 Prototype 原型 5 Singleton 单例 结构型 6 Adapter Class Obj
  • qq音乐服务器的位置,QQ音乐的歌曲真实地址解析

    打开该页面地址 是这样的 梦一场 那英 QQ音乐 点击页面中的播放按钮 会在新页面的播放器里播放该音乐 在新页面中打开开发者工具的网络探测 重新刷新页面 会发现相关请求如下 https c y qq com base fcgi bin fc
  • RISC-V Debug Introduction

    1 JTAG简介 目前RISC V官方支持的调试方式是JTAG Joint Test Action Group 而ARM支持的调试方式有JTAG和SWD Serial Wire Debug 这两种 JTAG是一种国际标准的调试方式 IEEE
  • 人工智能驱动的个性化新闻 APP |Artifact

    个性化新闻 指根据用户的兴趣 行为和需求 通过数据挖掘 协同过滤等技术 实现新闻内容的个性化推荐 这种推荐技术可以帮助用户在海量新闻中快速找到感兴趣的内容 提高阅读体验 同时也能缓解用户信息过载的问题 随着人工智能的发展 个性化新闻 APP
  • ubuntu14.04-root用户

    一 其实我个人认为这没有多大必要 因为当你需要 root 的权限时 使用 sudo 便可以了 如果你实在需要在 Ubuntu 中启用 root 帐号的话 那么不妨执行下面的操作 1 重新设置 root 的密码 sudo passwd roo
  • opencv手势识别(2_KNN算法识别)

    手势识别系列博文2 KNN算法识别手势 前言 原理介绍 代码实现 前言 书山有路勤为径 学海无涯苦做舟 琴某人辛辛苦苦码的报告 当然不能让你们这么容易复制过去 运行视频见链接 https www bilibili com video BV1
  • 【ROS2机器人入门到实战】里程计计算-速度积分

    14 里程计计算 速度积分 写在前面 当前平台文章汇总地址 ROS2机器人从入门到实战 获取完整教程及配套资料代码 请关注公众号 lt 鱼香ROS gt 获取 教程配套机器人开发平台 两驱版 四驱版 为方便交流 搭建了机器人技术问答社区 地