自定义小车底盘通过串口接入ROS1分享(附完整代码)
本文详细介绍了如何将自定义串口通信协议的机器人底盘接入ROS1导航系统。主要内容包括:1) 分析底盘通信协议帧结构,重点解析速度控制指令格式;2) 使用SSCOM和cutecom工具进行串口测试;3) 实现核心代码,包括串口库使用、ROS节点类开发及协议解析关键技术;4) 工程配置与编译说明;5) 测试验证方法。文章提供了完整的解决方案,涵盖协议解析、单位转换、接口封装和异常处理等关键环节,适用于
本文以实际自主研发的机器人底盘项目为例,讲解如何将自定义串口通信协议的底盘接入ROS1导航系统。
一、底盘通信协议分析
1.1 协议帧结构
底盘采用自定义串口协议,通用帧格式如下:
| 字节序号 | 字段名称 | 固定值 / 说明 | 字节数 | 核心作用 |
|---|---|---|---|---|
| 0 | 帧头 1 | 0x3A |
1 | 数据帧起始标识,固定不变,用于帧同步 |
| 1 | 帧头 2 | 0xAA |
1 | 数据帧起始标识,固定不变,双帧头提升抗干扰性 |
| 2 | 长度 (LEN) | 整帧总字节数(1 字节,范围 0x00~0xFF) | 1 | 标识当前帧的总长度,数据负载长度 = LEN - 6 |
| 3 | 命令 (CMD) | 功能指令码(1 字节,如0x01为速度控制) |
1 | 标识当前帧的功能类型,下位机根据 CMD 执行对应操作 |
| 4 ~ (LEN-3) | 数据负载 | (LEN-6) 字节,可变长度 | 可变 | 存储具体指令参数(如速度、角度、传感器数据等) |
| LEN-2 | CRC 校验 | 1 字节校验值 | 1 | 数据完整性校验,防止传输错误 |
| LEN-1 | 帧尾 | 0xBB |
1 | 数据帧结束标识,固定不变 |
关键特征:
帧头固定:0x3A 0xAA
帧尾固定:0xBB
第3字节为总长度(包含帧头帧尾)
第4字节为命令字
多字节数据采用大端格式
校验方式:0xFF - sum(LEN到payload末尾)
下面以速度控制为第四字节为0x01为例(速度控制)通过ROS下发指令给底盘控制器从而实现键盘控制小车运动进一步封装成ROS Navigation的控制包(本文只做了键盘控制),也可以按照这个格式发送底盘数据给ROS如电量信息、其他控制指令等
1.2 速度控制帧
帧格式:
| 字节序号 | 字段名称 | 固定值 / 说明 | 字节数 | 详细说明 |
|---|---|---|---|---|
| 0 | 帧头 1 | 0x3A |
1 | 固定起始 |
| 1 | 帧头 2 | 0xAA |
1 | 固定起始 |
| 2 | 长度 (LEN) | 0x0C(十进制 12) |
1 | 整帧总字节数固定为 12,因此数据负载长度 = 12-6=6 字节 |
| 3 | 命令 (CMD) | 0x01 |
1 | 速度控制指令专属命令码 |
| 4-5 | Vx(前后速度) | int16_t 大端格式,正数前进、负数后退 | 2 | 单位为最大速度的千分比(0~1000 对应 0%~100%) |
| 6-7 | Vy(左右速度) | 固定为0x00 0x00 |
2 | 多数差速 / 阿克曼底盘不支持 Y 方向平移,固定为 0 |
| 8-9 | Vw(角速度) | int16_t 大端格式,正数左转、负数右转 | 2 | 控制底盘旋转,0 为直行,数值大小对应旋转速度 |
| 10 | CRC 校验 | 按算法计算的 1 字节校验值 | 1 | 校验规则:CRC = 0xFF - (LEN + CMD + 数据负载所有字节) & 0xFF |
| 11 | 帧尾 | 0xBB |
1 | 固定结束 |
单位换算:
ROS: linear.x (m/s) → 底盘:x * 1000(mm/s)
ROS: angular.z(rad/s) → 底盘: z * 100 (0.01rad/s)
示例:
ROS发送: linear.x=0.5, angular.z=0.0(50%的最大速度)
串口帧: 3A AA 0C 01 01 F4 00 00 00 00 FD BB
二、串口助手测试(SSCOM和cutecom)
2.1 SSCOM测试
打开SSCOM 多字符串,填前进后退指令(按照通信协议),关循环发送,点对应按钮发串口指令,若可以正常执行前进后退等动作则说明通信成功,关闭循环发送是为了避免参数一下给太大容易受伤或受损。

2.2 cutecom测试
在Linux系统下,用户可以通过安装cutecom来进行机器人底盘的串口收发数据。
首先打开终端(ctrl+alt+T 键),输入安装指令:
sudo apt-get insatll cutecom
接下来我们可以通过终端来打开cutecom,输入以下指令:
sduo cutecom
一定要sudo,不然没有权限

这样通过ubuntu串口也可以做控制。
三、代码实现
需创建main.cpp,下载串口库 serialib。具体看4.1 目录结构
3.1 串口库(serialib.h)
使用开源串口库 serialib,提供跨平台串口操作:
class serialib {
public:
char openDevice(const char *Device, const unsigned int Bauds);
bool isDeviceOpen();
void closeDevice();
int writeBytes(const void *Buffer, const unsigned int NbBytes);
int readBytes(void *buffer, unsigned int maxNbBytes,
unsigned int timeOut_ms, unsigned int sleepDuration_us);
int available();
};
3.2 主节点类(main.cpp 完整部分)
下面是main.cpp,其核心功能是接收 ROS 导航栈的速度指令,按照底盘通信协议,通过串口发送运动指令,最后接收底盘回传的里程计/IMU 数据并发布。
#include <ros/ros.h>
#include <geometry_msgs/Twist.h>
#include <geometry_msgs/TransformStamped.h>
#include <nav_msgs/Odometry.h>
#include <sensor_msgs/Imu.h>
#include <tf/transform_broadcaster.h>
#include <tf/transform_datatypes.h>
#include <thread>
#include <chrono>
#include <cmath>
#include <cstring>
#include <string>
#include <cstdio>
#include <cstdint>
#include "serialib.h"
#define PKG_MIN_LEN 6
class AMRRcu {
enum CmdID { CMD_VEL = 0x01, CMD_ODOM = 0x81, CMD_IMU = 0x82, CMD_GPS = 0x83 };
enum { PKG_LEN_IDX = 2, PKG_CMD_IDX = 3 };
public:
AMRRcu() : pnh_("~"), rx_pkg_len_(PKG_MIN_LEN), prx_(rdata_), last_rev_ts_(0.0) {
pnh_.param<std::string>("tty", port_, "/dev/ttyUSB0");
pnh_.param<int>("baudrate", baud_, 115200);
pnh_.param<bool>("publish_tf", publish_tf_, true);
pnh_.param<std::string>("cmd_vel_topic", cmdvel_topic_, "cmd_vel");
pnh_.param<std::string>("odom_topic", odom_topic_, "odom");
pnh_.param<std::string>("imu_topic", imu_topic_, "imu/data");
cmdvel_sub_ = nh_.subscribe(cmdvel_topic_, 5, &AMRRcu::cmdvelCallback, this);
odom_pub_ = nh_.advertise<nav_msgs::Odometry>(odom_topic_, 50);
imu_pub_ = nh_.advertise<sensor_msgs::Imu>(imu_topic_, 50);
ROS_INFO("串口:%s, 波特率:%d, 接收主题:%s, 里程发布:%s, TF发布:%s", port_.c_str(), baud_, cmdvel_topic_.c_str(), odom_topic_.c_str(), publish_tf_ ? "是" : "否");
}
void spinOnce() {
if (!serial_.isDeviceOpen()) {
if (!connect()) { std::this_thread::sleep_for(std::chrono::milliseconds(500)); return; }
last_rev_ts_ = ros::Time::now().toSec();
}
readUartOnce();
if (ros::Time::now().toSec() - last_rev_ts_ > 5.0) {
serial_.closeDevice();
ROS_WARN("数据丢失超过5秒,尝试重新开启串口:%s", port_.c_str());
}
}
private:
ros::NodeHandle nh_, pnh_;
ros::Subscriber cmdvel_sub_;
ros::Publisher odom_pub_, imu_pub_;
tf::TransformBroadcaster tf_br_;
bool publish_tf_;
std::string port_, cmdvel_topic_, odom_topic_, imu_topic_;
int baud_;
serialib serial_;
uint8_t rdata_[256], rx_pkg_len_, *prx_;
nav_msgs::Odometry odo_;
double last_rev_ts_;
static int32_t be32(const uint8_t *p) { return (int32_t)((uint32_t(p[0]) << 24) | (uint32_t(p[1]) << 16) | (uint32_t(p[2]) << 8) | uint32_t(p[3])); }
static int16_t be16(const uint8_t *p) { return (int16_t)((uint16_t(p[0]) << 8) | uint16_t(p[1])); }
static std::string buf2str(const uint8_t *buf, int len) { std::string s; char t[5] = {0}; for (int i = 0; i < len; ++i) { std::snprintf(t, sizeof(t), " %02X", buf[i]); s += t; } return s; }
bool connect() {
char ret = serial_.openDevice(port_.c_str(), baud_);
if (ret != 1) { ROS_WARN("打开串口%s失败:%d", port_.c_str(), ret); return false; }
ROS_INFO("打开串口:%s @ %d", port_.c_str(), baud_); return true;
}
void cmdvelCallback(const geometry_msgs::Twist::ConstPtr &msg) {
sendSpdCmd(int(msg->linear.x * 1000.0), int(msg->linear.y * 1000.0), int(msg->angular.z * 100.0));
}
void pubOdom(double x, double y, double w, double vx, double vy, double vw) {
ros::Time now = ros::Time::now();
odo_.header.stamp = now; odo_.header.frame_id = "odom"; odo_.child_frame_id = "base_link";
odo_.pose.pose.position.x = x; odo_.pose.pose.position.y = y; odo_.pose.pose.position.z = 0.0;
odo_.pose.pose.orientation = tf::createQuaternionMsgFromYaw(w);
odo_.twist.twist.linear.x = vx; odo_.twist.twist.linear.y = vy; odo_.twist.twist.linear.z = 0.0; odo_.twist.twist.angular.z = vw;
bool still = std::fabs(vx) < 1e-5 && std::fabs(vy) < 1e-5 && std::fabs(vw) < 1e-5;
for (int i = 0; i < 36; ++i) { odo_.pose.covariance[i] = 0.0; odo_.twist.covariance[i] = 0.0; }
for (int i = 0; i < 6; ++i) { odo_.pose.covariance[i * 6 + i] = 0.01; odo_.twist.covariance[i * 6 + i] = still ? 1e-6 : 0.01; }
odom_pub_.publish(odo_);
if (publish_tf_) {
geometry_msgs::TransformStamped tfm;
tfm.header.stamp = now; tfm.header.frame_id = "odom"; tfm.child_frame_id = "base_link";
tfm.transform.translation.x = x; tfm.transform.translation.y = y; tfm.transform.translation.z = 0.0; tfm.transform.rotation = odo_.pose.pose.orientation;
tf_br_.sendTransform(tfm);
}
}
void pubImu(float ax, float ay, float az, float gx, float gy, float gz, float qw, float qx, float qy, float qz) {
sensor_msgs::Imu imu; imu.header.stamp = ros::Time::now(); imu.header.frame_id = "imu";
imu.orientation.w = qw; imu.orientation.x = qx; imu.orientation.y = qy; imu.orientation.z = qz;
imu.angular_velocity.x = gx; imu.angular_velocity.y = gy; imu.angular_velocity.z = gz;
imu.linear_acceleration.x = ax; imu.linear_acceleration.y = ay; imu.linear_acceleration.z = az;
for (int i = 0; i < 3; ++i) { imu.orientation_covariance[3 * i + i] = 1.0; imu.linear_acceleration_covariance[3 * i + i] = 3.0; imu.angular_velocity_covariance[3 * i + i] = 0.9; }
imu_pub_.publish(imu);
}
void sendSpdCmd(int x, int y, int w) {
if (!serial_.isDeviceOpen()) return;
uint8_t buf[12] = {0x3A, 0xAA, 12, CMD_VEL, 0, 0, 0, 0, 0, 0, 0, 0xBB};
buf[4] = (x >> 8) & 0xFF; buf[5] = x & 0xFF; buf[6] = (y >> 8) & 0xFF; buf[7] = y & 0xFF; buf[8] = (w >> 8) & 0xFF; buf[9] = w & 0xFF;
uint8_t sum = 0; for (int i = 2; i < 10; ++i) sum += buf[i]; buf[10] = uint8_t(0xFF - sum); serial_.writeBytes(buf, 12);
}
void rxReset() { rx_pkg_len_ = PKG_MIN_LEN; prx_ = rdata_; }
void decode() {
if (rdata_[PKG_CMD_IDX] == CMD_ODOM && rx_pkg_len_ >= 25) {
pubOdom(be32(rdata_ + 4) / 1000.0, be32(rdata_ + 8) / 1000.0, be16(rdata_ + 12) / 100.0, be16(rdata_ + 14) / 1000.0, be16(rdata_ + 16) / 1000.0, be16(rdata_ + 18) / 100.0);
} else if (rdata_[PKG_CMD_IDX] == CMD_IMU && rx_pkg_len_ >= 46) {
float d[10]; std::memcpy(d, rdata_ + 4, sizeof(d)); pubImu(d[0], d[1], d[2], d[3], d[4], d[5], d[9], d[6], d[7], d[8]);
}
}
void readUartOnce() {
while (ros::ok()) {
int need = rx_pkg_len_ - int(prx_ - rdata_); if (serial_.available() < need) return;
last_rev_ts_ = ros::Time::now().toSec(); serial_.readBytes(prx_, need, 1); prx_ += need;
if (rx_pkg_len_ == PKG_MIN_LEN) {
if (rdata_[0] != 0x3A || rdata_[1] != 0xAA) {
int i = 1; for (; i < rx_pkg_len_; ++i) if (rdata_[i] == 0x3A) { ROS_WARN("数据包同步:%s", buf2str(rdata_, rx_pkg_len_).c_str()); std::memmove(rdata_, rdata_ + i, rx_pkg_len_ - i); prx_ = rdata_ + (rx_pkg_len_ - i); break; }
if (i >= rx_pkg_len_) { ROS_WARN("数据包头错误:%s", buf2str(rdata_, rx_pkg_len_).c_str()); rxReset(); }
} else { rx_pkg_len_ = rdata_[PKG_LEN_IDX]; if (rx_pkg_len_ < PKG_MIN_LEN || rx_pkg_len_ > sizeof(rdata_)) { ROS_WARN("包长错误:%d", rx_pkg_len_); rxReset(); } }
continue;
}
if (rdata_[0] != 0x3A || rdata_[1] != 0xAA || rdata_[rx_pkg_len_ - 1] != 0xBB) { ROS_WARN("数据包格式不正确:%s", buf2str(rdata_, rx_pkg_len_).c_str()); rxReset(); continue; }
uint8_t sum = 0; for (int i = 2; i < rx_pkg_len_ - 2; ++i) sum += rdata_[i]; if (uint8_t(0xFF - sum) != rdata_[rx_pkg_len_ - 2]) { ROS_WARN("数据校验错误:%s", buf2str(rdata_, rx_pkg_len_).c_str()); rxReset(); continue; }
decode(); rxReset();
}
}
};
int main(int argc, char **argv) {
ros::init(argc, argv, "amr_rcu_cmd");
AMRRcu node; ROS_INFO("amr_rcu启动"); ros::Rate rate(200);
while (ros::ok()) { node.spinOnce(); ros::spinOnce(); rate.sleep(); }
ROS_INFO("amr_rcu退出"); return 0;
}
四、工程配置
4.1 目录结构
将创建好的main.cpp以及serialib放到对应的文件位置(需先创建工作空间)
catkin_ws/src/fireman_robot/
├── CMakeLists.txt
├── package.xml
├── src/
│ ├── main.cpp
│ └── serialib.cpp
└── include/fireman_robot/
└── serialib.h
4.2 修改CMakeLists.txt
cmake_minimum_required(VERSION 3.0.2)
project(fireman_robot)
find_package(catkin REQUIRED COMPONENTS
roscpp
geometry_msgs
nav_msgs
sensor_msgs
tf
)
catkin_package()
include_directories(
include
${catkin_INCLUDE_DIRS}
)
add_executable(amr_rcu_cmd
src/main_ros1.cpp
src/serialib.cpp
)
target_link_libraries(amr_rcu_cmd
${catkin_LIBRARIES}
)
4.3 修改package.xml
<build_depend>roscpp</build_depend>
<build_depend>geometry_msgs</build_depend>
<build_depend>nav_msgs</build_depend>
<build_depend>sensor_msgs</build_depend>
<build_depend>tf</build_depend>
<exec_depend>roscpp</exec_depend>
<exec_depend>geometry_msgs</exec_depend>
<exec_depend>nav_msgs</exec_depend>
<exec_depend>sensor_msgs</exec_depend>
<exec_depend>tf</exec_depend>
4.4 编译与运行
用rosrun需先开roscore,_tty:=/dev/ttyUSB0需临时设置端口号
bash
cd ~/catkin_ws
catkin_make
source devel/setup.bash
rosrun fireman_robot amr_rcu_cmd _tty:=/dev/ttyUSB0
Linux下必须配置串口权限,临时方案:
bash
sudo chmod 666 /dev/ttyUSB0
五、测试与验证
5.1 查看话题
运行4.4编译与运行后
rostopic list
应看到:
/cmd_vel
/odom
/imu/data
5.2 查看里程计
rostopic echo /odom
5.3 键盘发送速度
新开一个窗口,可能需先安装
sudo apt update
sudo apt install ros-noetic-teleop-twist-keyboard
安装完直接用
rosrun teleop_twist_keyboard teleop_twist_keyboard.py
可实现键盘控制控制

六、总结
核心要点
- 协议解析:帧同步、字节序、校验和
- 单位换算:ROS标准单位 ↔ 底盘协议单位
- 接口封装:cmd_vel订阅、odom发布、TF广播
- 异常处理:半包粘包、校验失败、串口重连
- 工程配置:CMakeLists.txt、package.xml、串口权限
适用场景
适用于:
- 自定义串口协议底盘
- 差速/全向/麦克纳姆轮底盘
- ROS1 Melodic/Noetic
- 需要接入move_base导航栈
按照本文步骤配置后即可直接编译运行。
参考
轮趣科技的串口通信控制与反馈部分
更多推荐



所有评论(0)