激光雷达驱动接入:从原理到ROS2实战
大家好,我是你们的老朋友。今天我们来聊聊激光雷达驱动接入这件事。
说实话,激光雷达是导航系统的「眼睛」。没有它,机器人就是个瞎子。我最早接触激光雷达时,踩过不少坑。比如驱动节点写好了,雷达死活不出数据。后来发现是串口波特率配错了。嗯,这种低级错误,谁还没犯过呢?
这一章,我会带你从原理到实战,把激光雷达驱动接入这件事彻底搞明白。
一、LiDAR驱动原理:它到底在干什么?
激光雷达的工作原理,说白了就是「发射-接收-计算」。它发射一束激光,打到物体上反射回来。通过计算时间差,就能知道距离。
但这里有个关键点:单线雷达只能扫一个平面,多线雷达能扫多个平面。我项目中用得最多的是单线雷达,比如RPLIDAR A1、思岚S2。它们结构简单,成本低,适合室内导航。
驱动要做的事情,其实就是三件:
- 读取原始数据:通过串口或以太网,拿到雷达发来的二进制数据包
- 解析数据帧:按照雷达厂商的协议,把二进制数据转成角度、距离、信号强度
- 发布话题:把解析后的数据封装成ROS2的LaserScan消息,发布出去
核心要点:驱动不负责「理解」数据,只负责「搬运」和「格式化」。真正的SLAM建图、避障,那是上层算法的事。
二、ROS2驱动节点编写:手把手教你写一个
好,理论说完了。我们直接上手写代码。
我个人习惯用C++写驱动节点,因为性能好,实时性高。但Python也行,看个人喜好。
先看一个最简单的驱动节点框架:
// lidar_driver_node.cpp
#include "rclcpp/rclcpp.hpp"
#include "sensor_msgs/msg/laser_scan.hpp"
class LidarDriver : public rclcpp::Node
{
public:
LidarDriver() : Node("lidar_driver")
{
// 声明参数
this->declare_parameter<std::string>("port", "/dev/ttyUSB0");
this->declare_parameter<int>("baudrate", 115200);
// 创建发布者
publisher_ = this->create_publisher<sensor_msgs::msg::LaserScan>("scan", 10);
// 打开串口
serial_port_ = openSerialPort();
// 启动读取线程
read_thread_ = std::thread(&LidarDriver::readLoop, this);
}
private:
void readLoop()
{
while (rclcpp::ok())
{
// 1. 读取原始数据
std::vector<uint8_t> raw_data = readFromSerial();
// 2. 解析数据帧
auto scan_msg = parseLidarData(raw_data);
// 3. 发布话题
if (scan_msg)
publisher_->publish(*scan_msg);
}
}
rclcpp::Publisher<sensor_msgs::msg::LaserScan>::SharedPtr publisher_;
std::thread read_thread_;
};
这段代码看起来简单,但有几个坑要注意。
我曾经踩过的坑:串口读取一定要用非阻塞模式。否则雷达数据一断,整个节点就卡死了。我当年调试时,机器人撞墙了才发现节点挂了。血的教训。
三、话题发布与坐标系配置
驱动写好了,数据也解析了。接下来就是发布话题和配置坐标系。
ROS2中,激光雷达的标准话题是/scan,消息类型是sensor_msgs/msg/LaserScan。这个消息包含以下关键字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| header.frame_id | string | 坐标系名称,比如"laser_frame" |
| angle_min | float32 | 扫描起始角度(弧度) |
| angle_max | float32 td> | 扫描结束角度(弧度) |
| angle_increment | float32 | 角度分辨率 |
| time_increment | float32 | 每帧时间间隔 |
| scan_time | float32 | 扫描周期 |
| range_min | float32 | 最小测量距离 |
| range_max | float32 | 最大测量距离 |
| ranges | float32[] | 距离数据数组 |
| intensities | float32[] | 信号强度数组(可选) |
这里有个容易忽略的点:frame_id一定要和TF树中的坐标系名称一致。否则导航系统找不到雷达的位置,会报TF错误。
我建议在启动文件中,用static_transform_publisher发布一个静态TF:
# 在launch文件中添加
<node pkg="tf2_ros" exec="static_transform_publisher"
args="0 0 0 0 0 0 base_link laser_frame" />
这样就把雷达坐标系laser_frame固定到了机器人基座base_link上。偏移量根据实际安装位置填写。
小技巧:如果你不确定雷达安装位置,可以用尺子量一下。我一般会在机器人底盘上画个十字线,方便定位。精度要求不高的话,目测也行。
四、知识体系总览
为了让你对整个流程有个直观印象,我画了一张图:
这张图把整个流程分成了三层。你想想看,从硬件到ROS2,每一层都有它的职责。驱动层是承上启下的关键。
五、实战中的避坑指南
最后,分享几个我实战中遇到的坑:
- 串口权限问题:Linux下访问串口需要权限。我习惯把用户加到dialout组里:
sudo usermod -a -G dialout $USER - 数据丢帧:如果雷达数据偶尔丢失,检查一下串口缓冲区大小。我一般设成4096字节以上
- 坐标系混乱:雷达的安装角度一定要和TF中的旋转量对应。我曾经因为装反了雷达,导致地图建出来是镜像的
- 话题名称冲突:多台机器人时,记得给话题加命名空间。比如
/robot1/scan
总结一下:激光雷达驱动接入,说白了就是「读数据-解析-发话题」三步走。但每一步都有细节。把细节处理好,你的导航系统就成功了一半。
好了,这一章就到这里。记住,驱动写好了,记得用rviz2看一下点云。看到那一条条扫描线,你会很有成就感的。