我觉得Ubuntu上的Ros2与单片机就是这么通讯的

首先Ubuntu上面建立一个功能包pkg ,包里面写一个cpp文件,然后在这个cpp文件里面建立一个node,然后使用这个node去获取单片机从串口传过来的数据。

平时都是使用串口调试助手来收发数据的,好像ros2里面有一个专门搞这个事情的东西叫做

serial库。学一下这个库就可以让ros2跟单片机通信了。我也不知道这个东西叫不叫驱动,反正能通讯就可以了

为了开发过程的方便,最好是实现确定一个Ubuntu和stm32通讯的通讯协议----其实就是通讯规则,比如我给单片机给Ubuntu发个1表示什么意思,发个2表示什么意思,然后Ubuntu给stm32发个1表示什么意思,2表示什么意思。之所以要提前写出来这个协议,最大的好处是,如果这个过程是2个人一起开发的,一个人开发stm32,一个人开个Ubuntu,就可以同时进行-----依据这个事先写好的协议。对于一个人开发也是有好处的,因为写出来这个协议,接口确定了,就不用总是去推理这里该怎么样,那样可能会比较混乱。

如果ros2没有serial库,那么需要自己学习《Ubuntu系统的串口通信编程》

后面是一个人写的文章,我觉得很写得好

基于ROS的两轮应急巡检车系列(二)ROS与STM32的串口通信

古月

古月

机器人社区“古月居”创始人,《ROS机器人开发实践》作者

前言

上段时间学弟总是问我ROS如何与单片机进行通信,这块也是做车的一大难点之一,但是网上的教程却很少,这次正好趁着这个机会写一篇博客记录一下,不过这段时间是真的忙,各种比赛和各种事情。

正文

ROS与单片机通信通常是使用UART通信,即串口通信。

一、UART通信简介

在UART通信中,每个数据位被传输为一个帧(Frame),每个帧由一个起始位(Start Bit)、一个或多个数据位(Data Bits)、一个可选的校验位(Parity Bit)和一个或多个停止位(Stop Bit)组成。

起始位通常是逻辑0,停止位通常是逻辑1。数据位的数量取决于通信双方事先约定的协议,校验位通常用于检查数据传输的正确性。如果使用校验位,通信双方必须在发送和接收端都采用相同的校验方式。

UART通信的速度是由波特率(Baud Rate)来定义的,波特率表示每秒钟传输的比特数。波特率越高,数据传输速度越快,但同时也需要更高的传输带宽和更可靠的信号质量。

在实际应用中,波特率通常是由发送和接收方事先约定好的,并在通信开始前进行设置。

具体的通信过程就不再这里具体描述了。

由于这里我用的就是STM32单片机,所以下面我就以STM32系列单片机为例了,其他单片机也是同理。

二、硬件连接

STM32芯片先连接到一个TTL电平转换芯片,再由这个电平转换芯片通过usb线连接ROS主控。

我这里的连接情况如下:

这里因为该单片机含有电平转换芯片,所以直接使用USB线连接就可以了。

连接成功之后在终端输入ll /dev/ttyUSB*来查看设备,如果显示下面信息说明设备成功被识别了。

三、软件端的通信

这里我们使用ros中serial库方便我们的通信,首先安装一下serial库

sudo apt install ros-noetic-serial

然后新建一个名为uart_ws的工作空间,在工作空间下新建一个uart功能包,在该功能包下新建一个uart.c和uart.h,目录结构如下:

先在uart.h文件中引用serial库

#include <serial/serial.h>

然后新建一个uart类,在类的定义中,声明一个 serial 类的实例

serial::Serial Stm32_Serial;  //声明串口对象

再在类中声明两个私有结构体变量,用来存储接收和要发送的数据

RECEIVE_DATA Receive_Data; //串口接收数据结构体
SEND_DATA Send_Data;        //串口发送数据结构体

再在类中声明两个定时函数,用来定时发送和接受

void controlLoopCB_send(const ros::TimerEvent&);//定时发送
void controlLoopCB_receive(const ros::TimerEvent&);//定时接收

在构造函数中设置串口名、波特率以及收发频率。这里由于是测试,就直接拿/dev/ttyUSB0了,实际使用中是串口名是很容易变的,如何把串口重命名再固定下来我将会在下篇博客介绍一下。

private_nh.param<std::string>("usart_port_name",  usart_port_name,  "/dev/ttyUSB0"); //串口名
private_nh.param<int>        ("serial_baud_rate", serial_baud_rate, 115200); //和下位机通信波特率115200,与单片机一致
private_nh.param("controller_freq", controller_freq, 10); //设置收发频率

然后初始化串口配置,再打开串口

try
  { 
    //Attempts to initialize and open the serial port //尝试初始化与开启串口
    Stm32_Serial.setPort(usart_port_name);  //选择要开启的串口号
    Stm32_Serial.setBaudrate(serial_baud_rate);  //设置波特率
    serial::Timeout _time = serial::Timeout::simpleTimeout(1000);  //超时等待
    Stm32_Serial.setTimeout(_time);
    Stm32_Serial.open();  //开启串口
  }
  catch (serial::IOException& e)
  {
    ROS_ERROR_STREAM("car_robot can not open serial port!");  //如果开启串口失败,打印错误信息
  }

判断串口是否被打开

if(Stm32_Serial.isOpen())
  {
    ROS_INFO_STREAM("car_robot serial port opened"); //Serial port opened successfully //串口开启成功提示
  }

串口接受函数,第一个变量是要接受的数据存放的数组,第二个变量是要接受的长度

Stm32_Serial.read(Receive_Data,sizeof(Receive_Data));

执行下面串口发送函数进行数据发送,第一个变量是要发送的数组,第二个变量是每次要发送的长度

 try
  {
    Stm32_Serial.write(Send_Data.tx,sizeof (Send_Data.tx)); //通过串口向下位机发送数据 
  }
  catch (serial::IOException& e)   
  {
    ROS_ERROR_STREAM("Unable to send data through serial port"); //如果发送数据失败,打印错误信息
  }

四、通信报文格式

在ROS与STM32通信时,要事先约定一个通信的数据包格式,这样通信双方才可以把收到的信息提取出来。

我这里测试用的格式如下,最终实现单片机直接把收到的消息返回给上位机端进行输出:

串口发送

串口接收

以串口接受为例,首先定义一个标志位判断接收到的数据是否与上述定义相同,读七个字节的数据到数组中,然后判断前两位是否与事先定义的包头相同.

如果相同则视为接受正确,然后将标志位置1,数据位赋值给相应变量,如果不则再读取一字节数据扔掉,将标志位置0,然后等待下次接收再读取7字节数据进行判断,重复上述步骤直到判断相同。具体实现如下:

void Uart::controlLoopCB_receive(const ros::TimerEvent&)
{
    int Serial_RxFlag = 0;    //标志位
  
  uint8_t buffer[1];
    int len=Stm32_Serial.read(Receive_Data.receive,7);  //获取长度

    if(Receive_Data.receive[0]==0xFF && Receive_Data.receive[1]==0xFE)
    {
        Serial_RxFlag=1;
    }
    else
    {
        len=Stm32_Serial.read(buffer,1);
        Serial_RxFlag=0;
    }
    if(Serial_RxFlag==1 && len==7)
    {
        for(int i = 0; i<2 ;i++)
        {
            rd1.receive[i] = Receive_Data.receive[i+2];
            rd2.receive[i] = Receive_Data.receive[i+4];
        }
        ROS_INFO("%d,%d,%d",rd1.d,rd2.d,Receive_Data.receive[6]);
        Serial_RxFlag = 0;
    }
}

这里接受和发送都是7字节的数组,从图中可以看到电机的高电平数是一个两字节的数,而它存放在每个元素都是一字节的数组中时会被拆开存放,所以要将其取出时,需要把两个字节赋给一个变量。

常见的方法有移位、联合体和c语言中的memcpy函数,这里测试用的是联合体,所以我先简要介绍一下原理,下篇博客中将会说明memcpy函数用法,因为联合体在变量多时会变得十分不易读。

联合体

联合体与结构体类似都是不同类型元素的集合,只不过结构体的每一个成员都拥有自己独立的存储空间,而联合体的成员是共用同一块内存空间的。

也就是说修改一个成员,另一个成员相对应的值也会被覆盖。联合体占用的字节数是成员中最大的那个

union receive_data
{
    short d;
    unsigned char receive[2];
}rd1,rd2;

对于上面用于接受的联合体,占用两个字节,拿其中左电机rd1变量举例,将单片机发送的左电机数据对应的赋值给rd1,这样就相当于rd1.d的值作了相应的修改。

rd1.receive[0] = Receive_Data.receive[2];
 rd1.receive[1] = Receive_Data.receive[3];

对于发送的联合体也是同理,定义的数组长度为要发送的结构体所占字节数。

union Send_Cmd{
    struct cmd cmd0;
    unsigned char data[8];
};

和校验

校验位采用的比较简单的和校验,实现过程是把要发送的数据拆开,然后做和,然后单片机将收到的数据做对比是否相同。

unsigned char Uart::check_uint(uint16_t data)
{
    union num_trans_uint16 num;
    num.num_int16 = data;
    return num.num_char[0] + num.num_char[1];
}

最后配置一下CMakeLists.txt,我这里配置情况如下。

编译过后执行rosrun uart uart进行测试。

最终打印结果如下:

这里我成功的输出了我发送的数据,说明通信是成功的。

如果报错串口不能打开,就输入下面给予一次串口读写权限

sudo chmod 777 /dev/ttyUSB0

最后附本次测试全部代码:

uart.h文件

#ifndef _UART_H_
#define _UART_H_

#define SEND_DATA_Num 18    

const unsigned char header[2] = { 0xFF, 0xFE };

struct RECEIVE_DATA
{
    unsigned char receive[7];
};

struct SEND_DATA
{
    uint8_t tx[SEND_DATA_Num];
};

union receive_data
{
    short d;
    unsigned char receive[2];
}rd1,rd2;

struct cmd {
    unsigned char null;
    unsigned char H;
    uint16_t Servo_PWM1;
    uint16_t Servo_PWM2;
    unsigned char CS;
    unsigned char T;
};

union Send_Cmd{
    struct cmd cmd0;
    unsigned char data[8];
};

union num_trans_uint16{
    unsigned char num_char[2];
    uint16_t num_int16;
};

class Uart
{
private:
    ros::NodeHandle private_nh; //节点句柄
    std::string usart_port_name;
    int serial_baud_rate;
    int controller_freq;
    serial::Serial Stm32_Serial;
    RECEIVE_DATA Receive_Data;
    SEND_DATA Send_Data;
    ros::Timer timer1,timer2;   //定时器
    union Send_Cmd send_cmd;
    
public:
    Uart();
    unsigned char uart_send_cmd(uint16_t Servo_PWM1,uint16_t Servo_PWM2);
    unsigned char check_uint(uint16_t data);
    void controlLoopCB_send(const ros::TimerEvent&); //定时器1回调函数,向单片机发送数据
    void controlLoopCB_receive(const ros::TimerEvent&); //定时器2回调函数,接收单片机发送过来的数据
};

#endif
uart.c文件

#include <iostream>
#include <serial/serial.h>
#include "ros/ros.h"
#include "../include/uart.h"


Uart::Uart()
{
    ros::NodeHandle private_nh("~");
    
    private_nh.param<std::string>("usart_port_name",  usart_port_name,  "/dev/ttyUSB0"); //固定串口号
    private_nh.param<int>        ("serial_baud_rate", serial_baud_rate, 115200); //和下位机通信波特率115200
    private_nh.param("controller_freq", controller_freq, 10);
    timer1 = private_nh.createTimer(ros::Duration((1.0)/controller_freq), &Uart::controlLoopCB_send, this); // Duration(0.05) -> 20Hz
    timer2 = private_nh.createTimer(ros::Duration((1.0)/controller_freq), &Uart::controlLoopCB_receive, this); // Duration(0.05) -> 20Hz
    try
    { 
        //尝试初始化与开启串口
        Stm32_Serial.setPort(usart_port_name); //选择要开启的串口号
        Stm32_Serial.setBaudrate(serial_baud_rate); //设置波特率
        serial::Timeout _time = serial::Timeout::simpleTimeout(1000);  //超时等待
        Stm32_Serial.setTimeout(_time);
        Stm32_Serial.open();  //开启串口
        if(Stm32_Serial.isOpen())
        {
            ROS_INFO_STREAM("car_robot serial port opened");  //串口开启成功提示
        }
    }
    catch (serial::IOException& e)
    {
        ROS_ERROR_STREAM("car_robot can not open serial port!"); //如果开启串口失败,打印错误信息
    }
    
}

void Uart::controlLoopCB_send(const ros::TimerEvent&)
{
    // Send_Data.tx[0] = header[0];
    // Send_Data.tx[1] = header[1];
    // double t = 2;
    // leftVelSet.d = t;
    // rightVelSet.d = t+1.1;
    // for(int i=0;i<8;i++)
    // {
    //     Send_Data.tx[i+2] = leftVelSet.data[i];
    //     Send_Data.tx[i+10] = rightVelSet.data[i];
    // }
    uart_send_cmd(1500,1500);
    
}

unsigned char Uart::check_uint(uint16_t data)
{
    union num_trans_uint16 num;
    num.num_int16 = data;
    return num.num_char[0] + num.num_char[1];
}


unsigned char Uart::uart_send_cmd(uint16_t Servo_PWM1,uint16_t Servo_PWM2)
{

    send_cmd.cmd0.H = 0xFF;
    send_cmd.cmd0.Servo_PWM1 = Servo_PWM1;
    send_cmd.cmd0.Servo_PWM2 = Servo_PWM2;
    send_cmd.cmd0.CS = check_uint(Servo_PWM1) + check_uint(Servo_PWM2);
    send_cmd.cmd0.T = 0xFE;
    try
    {
        Stm32_Serial.write(send_cmd.data,sizeof(send_cmd.data)); //通过串口向下位机发送数据 
    }
    catch (serial::IOException& e)   
    {
        ROS_ERROR_STREAM("Unable to send data through serial port");  //如果发送数据失败,打印错误信息
    }
    return send_cmd.cmd0.CS;
}

void Uart::controlLoopCB_receive(const ros::TimerEvent&)
{
    uint8_t Serial_RxPacket[18];
    int Serial_RxFlag = 0;    
  
  uint8_t reading[5],buffer[1];
    int len=Stm32_Serial.read(Receive_Data.receive,7);  //获取长度

    if(Receive_Data.receive[0]==0xFF && Receive_Data.receive[1]==0xFE)
    {
        Serial_RxFlag=1;
    }
    else
    {
        len=Stm32_Serial.read(buffer,1);
        Serial_RxFlag=0;
    }
    if(Serial_RxFlag==1 && len==7)
    {
        for(int i = 0; i<2 ;i++)
        {
            rd1.receive[i] = Receive_Data.receive[i+2];
            rd2.receive[i] = Receive_Data.receive[i+4];
        }
        ROS_INFO("%d,%d,%d",rd1.d,rd2.d,Receive_Data.receive[6]);
        Serial_RxFlag = 0;
    }
}

int main(int argc, char *argv[])
{
    ros::init(argc,argv,"send");
    Uart Uart1;
    ros::AsyncSpinner spinner(0); 
    spinner.start();
    ros::waitForShutdown();

    return 0;
}

发布于 2023-05-19 15:13・IP 属地湖北

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/171625.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

文章解读与仿真程序复现思路——电力自动化设备EI\CSCD\北大核心《计及氢电混合动力车响应的多区域综合能源系统协调优化调度》

这个标题涉及到一个复杂的能源系统优化问题&#xff0c;其中考虑了氢电混合动力车的响应。下面是对标题中各个关键词的解读&#xff1a; 多区域综合能源系统&#xff1a; 涉及多个地理区域的综合能源系统&#xff0c;这可能包括电力网络、燃气网络、热能网络等&#xff0c;这些…

基于JAVA+SpringBoot+VUE+微信小程序的前后端分离咖啡小程序

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 随着社会的快速发展和…

用向量数据库Milvus Cloud搭建GPT大模型+私有知识库的定制商业文案助手

随着智能助手的不断普及和发展,商业文案的创作也变得更加智能化和定制化。在这个信息爆炸的时代,商业文案的撰写已经不再是简单的文字表达,而是需要结合大数据分析和人工智能技术,以更好地满足目标客群的需求。在本文中,我们将介绍如何利用向量数据库Milvus Cloud搭建GPT大…

tokenizers Tokenizer 类

Tokenizer 类 依赖安装 pip install tensorflow pip install tensorflow -i https://pypi.tuna.tsinghua.edu.cn/simple基类原型 tokenizers.Tokenizer(model)基类说明 Tokenizer 函数构造一个分词器对象。分词方式主要有word-level、subword-level、char-level三种&#x…

财报解读:第三季度营收净利双增,中通快递的进击根本停不下来?

快递业又变天了。 在极兔上市之前&#xff0c;快递行业的格局几乎已经稳定&#xff0c;“通达系们”占据了过半的市场份额。数据显示&#xff0c;2022年按包裹量计&#xff0c;中通、圆通、申通、韵达市占率分别为22.1%、15.81%、11.71%、15.92%&#xff0c;共计占比达65.54%。…

数据结构——散列表

参考书籍&#xff1a; 《数据结构与抽象&#xff1a;Java语言描述》 第四版 一、背景知识 散列&#xff08;hashing&#xff09;&#xff1a;是仅利用项的查找键&#xff0c;无需查找就可确定其下标的一项技术散列表&#xff08;hash table&#xff09;&#xff1a;数组散列索引…

P1 C++如何从源文件变为可执行文件

前言 欢迎来到 C 系列的新章节&#xff0c;今天我们要学习 C 是如何工作的。现阶段我们尽量简单点说&#xff0c;学习如何从源文件开始&#xff0c;也就是实际的文本文档到可执行的二进制代码的过程。 对于C源文件&#xff0c;从文本到可执行文件一般需要四个过程&#xff1a;…

jmeter接口自动化部署jenkins教程详解

首先&#xff0c;保证本地安装并部署了jenkins&#xff0c;jmeter&#xff0c;xslproc 我搭建的自动化测试框架是jmeterjenkinsxslproc 注意&#xff1a;原理是&#xff0c;jmeter自生成的报告jtl文件&#xff0c;通过xslproc工具&#xff0c;再结合jmeter自带的模板修改&…

基于单片机电梯液晶显示防超重,防气体报警、防夹报警控制系统及源程序

一、系统方案 1、本设计采用51单片机作为主控器。 2、液晶显示楼层。 3、防超重&#xff0c;防气体报警、防夹报警。 二、硬件设计 原理图如下&#xff1a; 三、单片机软件设计 1、首先是系统初始化 /lcd1602初始化设置*/ void init_1602() //lcd1602初始化设置 { write_co…

【C++上层应用】6. 信号 / 中断

文章目录 【 1. signal 函数 】【 2. raise函数 】 信号是由操作系统传给进程的 中断&#xff0c;会提早终止一个程序。在 UNIX、LINUX、Mac OS X 或 Windows 系统上&#xff0c;可以通过按 CtrlC 产生中断。有些信号不能被程序捕获&#xff0c;但是下表所列信号可以在程序中捕…

手机 IOS 软件 IPA 签名下载安装详情图文教程

由于某些应用由于某些原因无法上架到 appStore 或者经过修改过的软件游戏等无法通过 appStore 安装&#xff0c;我们就可以使用签名的方式对相应软件的IPA文件进行签名然后安装到你的手机上 这里我们使用爱思助手进行签名安装&#xff0c;爱思助手支持两种方式&#xff0c;一种…

spring boot加mybatis puls实现,在新增/修改时,对某些字段进行处理,使用的@TableField()或者AOP @Before

1.先说场景&#xff0c;在对mysql数据库表数据插入或者更新时都得记录时间和用户id 传统实现有点繁琐&#xff0c;这里还可以封装一下公共方法。 2.解决方法&#xff1a; 2.1&#xff1a;使用aop切面编程&#xff08;记录一下&#xff0c;有时间再攻克&#xff09;。 2.1.1&am…

[⑤ADRV902x]: TES (Transceiver Evaluation Software) 使用

前言 在ADI官网的ADRV902x系列的参考设计软件包&#xff08;地址&#xff1a;https://www.analog.com/cn/products/adrv9029.html#product-requirement &#xff09;中包含了GUI软件TES (Transceiver Evaluation Software)。软件实用的功能非常多&#xff0c;比如可以用界面的…

无服务器开发实例|微服务向无服务器架构演进的探索

在当今的技术环境中&#xff0c;许多组织已经从构建单一的应用程序转变为采用微服务架构。微服务架构是将服务分解成多个较小的应用程序&#xff0c;这些应用程序可以独立开发、设计和运行。这些被拆分的小的应用程序相互协作和通信&#xff0c;为用户提供全面的服务。在设计和…

排名全球前列!Flat Ads再入选AppsFlyer广告榜单

近期&#xff0c;移动归因与营销分析公司AppsFlyer《广告平台综合表现报告》第16版重磅发布&#xff01;盘点全球买量渠道表现&#xff0c;洞察移动营销行业最新格局。其中Flat Ads凭借实力和体量&#xff0c;入选AppsFlyer广告平台综合表现非游戏类Top级流量媒体4个全球榜单排…

电视家最新消息

11 月 20 日消息&#xff0c;电视家 App 出现了无法收看电视直播的情况。与此同时&#xff0c;“电视家 跑路”已经登上微博热搜榜单的第二位。 11月20日&#xff0c;知名电视直播类APP“电视家”一系列动作引起网友关注。 ​ 网友反映&#xff0c;平时用来看直播或者点播影…

广西柳州机械异形零部件三维扫描3D抄数全尺寸测绘建模-CASAIM中科广电

一、背景介绍 复杂机械异形零部件具有不规则的形状和复杂的结构&#xff0c;给生产制造带来了很大的检测难度。为了确保零部件的制造质量和精度&#xff0c;需要对零部件进行全面的尺寸检测和分析。 CASAIM三维扫描仪在机械异形零部件全尺寸检测应用可以实现对机械异形零部件…

Kamailio default-routing-logic-flowchart

不是我画的&#xff0c;是一个斯洛伐克的人画的 https://nil.uniza.sk/wp-content/uploads/files/image/SIP/kamailio/kamailio.png

电脑显示找不到mfc140.dll怎么办?哪个修复方法值得推荐

在电脑使用过程中&#xff0c;我们经常会遇到一些错误提示&#xff0c;例如"mfc140.dll文件缺失"这个问题可能会导致某些应用程序无法正常运行&#xff0c;可能给您带来困扰。本篇文章为您提供了六种有效解决此类问题的策略&#xff0c;使您能够迅速修复并恢复应用程…

使用Mate 40 Harmony OS 4.0版本运行 codelabs ArkUI demo运行非常卡顿,换Mate 60没事

服务类型 DevEco Studio 概述 使用Mate 40 Harmony OS 4.0版本运行 codelabs ArkUI demo运行非常卡顿&#xff0c;换Mate 60没事 官方回复添加链接描述 客户支持工程师 2023-11-21 14:37:19 GMT08:00 尊敬的开发者&#xff0c;您好&#xff0c; 该机型卡顿黑屏为内部已知问题…