激光雷达驱动接入:从原理到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 扫描结束角度(弧度)
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层 话题发布/TF配置 串口配置 波特率/数据位/停止位 超时设置/缓冲区 数据解析 帧头校验/角度计算 距离转换/异常过滤 话题与TF LaserScan消息填充 静态TF发布 最终输出:/scan话题 + laser_frame坐标系 供Nav2导航栈使用

这张图把整个流程分成了三层。你想想看,从硬件到ROS2,每一层都有它的职责。驱动层是承上启下的关键。

五、实战中的避坑指南

最后,分享几个我实战中遇到的坑:

  1. 串口权限问题:Linux下访问串口需要权限。我习惯把用户加到dialout组里:sudo usermod -a -G dialout $USER
  2. 数据丢帧:如果雷达数据偶尔丢失,检查一下串口缓冲区大小。我一般设成4096字节以上
  3. 坐标系混乱:雷达的安装角度一定要和TF中的旋转量对应。我曾经因为装反了雷达,导致地图建出来是镜像的
  4. 话题名称冲突:多台机器人时,记得给话题加命名空间。比如/robot1/scan

总结一下:激光雷达驱动接入,说白了就是「读数据-解析-发话题」三步走。但每一步都有细节。把细节处理好,你的导航系统就成功了一半。

好了,这一章就到这里。记住,驱动写好了,记得用rviz2看一下点云。看到那一条条扫描线,你会很有成就感的。

可可精品资料库