资讯动态

N10激光雷达SLAM实战:串口解析、SQLite存储与cartographer建图

发布时间:2026/9/29 4:57:59 来源:尧图企业网站定制
1. 从串口到地图N10激光雷达数据链路的完整拆解N10这款激光雷达在入门级SLAM项目里出镜率很高价格亲民、串口协议简单、360度扫描一圈能拿到几百个点拿来练手ROS2和cartographer再合适不过。但真正动手做的时候很多人会卡在几个地方串口读出来的原始字节流怎么解析成角度和距离、数据怎么落库方便事后复盘、cartographer的配置参数怎么调才能让建图不飘。我这次把整条链路完整跑了一遍从串口编程到SQLite存储再到ROS2里用cartographer单雷达建图中间踩的坑不少这里把过程和经验都摊开讲。整条链路的核心思路是这样的N10通过串口以固定波特率往外吐数据包每个包包含起始标志、角度、距离、校验等字段我用Python写一个串口解析节点把原始字节流拆成结构化的角度-距离对解析出来的数据一方面通过ROS2的话题发布出去给cartographer消费另一方面同步写入SQLite数据库方便后续用DB Browser for SQLite或者DBeaver做离线分析。这样做的好处是建图效果不理想的时候你可以回放数据库里的原始数据反复调参而不用重新跑车。这篇文章适合谁看如果你刚接触ROS2想找一个完整的、从底层串口到上层建图的实战项目那这篇就是为你写的。如果你已经会跑cartographer的demo但想搞清楚数据从哪来、怎么存、怎么排查建图飘的问题这篇也能给你一些参考。我会尽量把每个环节的为什么讲清楚而不是只丢一堆配置让你抄。2. N10串口协议解析字节流里到底藏了什么2.1 先搞清楚N10的数据包结构N10的串口输出不是ASCII文本而是二进制数据包。不同批次的固件协议略有差异但常见的格式是这样的每个数据包以一个固定的起始字节开头比如0xAA 0x55然后是包类型、采样点数、起始角度、结束角度接着是一串距离数据最后是校验和。一个包通常包含一圈或者半圈的点具体取决于雷达的转速和串口波特率。我手上这颗N10的波特率是230400每包包含约16个采样点包与包之间的角度是连续的。这里有个容易忽略的点角度不是每个点单独给的而是给出起始角度和角度增量你需要自己算每个点对应的角度。比如起始角度是0度角度增量是1度那第n个点的角度就是起始角度加上n乘以增量。这个细节如果搞错建出来的图会扭曲成螺旋状。解析的时候我建议先用一个最简单的脚本把原始字节打印出来肉眼确认起始标志和包长度。不要一上来就写完整的解析逻辑因为不同固件的包长度可能不一样盲目按固定长度读会错位。我的做法是先把串口数据存成二进制文件然后用十六进制查看器找规律确认了包结构之后再写解析代码。2.2 串口读取的缓冲区陷阱串口编程里最容易踩的坑是缓冲区处理。Python的pyserial库在读取时如果read()的字节数设置不当很容易出现半包或者粘包。所谓半包就是一次读取只拿到了一个完整包的一部分粘包则是两次读取之间缓冲区里积累了多个包的数据。我的处理方式是维护一个字节缓冲区每次从串口读取一批数据追加到缓冲区末尾然后循环检查缓冲区开头是否是起始标志。如果是就判断缓冲区长度是否足够一个完整包如果足够就切出一个包来解析剩下的继续留在缓冲区如果不够就等下一次读取。这个逻辑听起来简单但实际写的时候要注意边界情况比如起始标志出现在数据中间怎么办。我的经验是如果校验失败就丢弃缓冲区第一个字节重新寻找起始标志这样能自动从错位中恢复。import serial import struct class N10Parser: def __init__(self, port, baudrate230400): self.ser serial.Serial(port, baudrate, timeout0.1) self.buffer bytearray() self.HEADER b\xaa\x55 def read_packet(self): while True: data self.ser.read(128) if data: self.buffer.extend(data) idx self.buffer.find(self.HEADER) if idx -1: self.buffer.clear() continue if idx 0: self.buffer self.buffer[idx:] if len(self.buffer) 32: return None packet self.buffer[:32] if self.check_sum(packet): self.buffer self.buffer[32:] return self.parse(packet) else: self.buffer self.buffer[1:]上面这段代码是简化版核心思想就是找头、验长、校验、切包。实际项目中包长度和校验方式要根据你的N10固件手册来定。我遇到过校验和是前面所有字节异或的情况也遇到过是累加和取低八位的情况这个必须确认清楚否则解析出来的数据全是乱的。2.3 角度和距离的物理量转换解析出原始数值之后还要做物理量转换。距离值通常是毫米或者厘米为单位的整数需要除以1000或者100转换成米。角度值可能是0到36000表示0到360度精度是0.01度也可能是直接以度为单位。这个转换系数如果搞错建图的比例尺就会出问题地图要么缩成一团要么大得离谱。我建议在解析节点里加一个参数配置把距离系数和角度系数做成可配置项这样换雷达或者换固件的时候不用改代码。另外N10这类三角测距雷达在近距离和远距离的精度不一样太近比如小于10厘米和太远比如大于8米的数据建议直接过滤掉这些点噪声很大留着反而影响建图效果。3. 数据落库SQLite为什么不用CSV而用数据库3.1 SQLite在雷达数据存储场景下的优势很多人第一反应是把雷达数据存成CSV文件一行一个点简单直接。我一开始也是这么干的但跑了十分钟之后就放弃了。原因是数据量太大N10每秒能出几千个点十分钟就是几十万行CSV文件打开都卡更别说查询和筛选了。而且CSV没有索引你想按时间范围或者角度范围查数据只能全文件扫描效率极低。SQLite的优势在这里就体现出来了。它是一个单文件的嵌入式数据库不需要单独安装服务端Python标准库就带sqlite3模块拿来就能用。单文件意味着你可以把数据库文件拷到U盘里带走用DB Browser for SQLite或者DBeaver打开就能看非常方便。而且SQLite支持索引你可以在时间戳或者角度字段上建索引查询速度比CSV快几个数量级。另一个考虑是数据完整性。串口数据在写入过程中如果程序崩溃CSV文件可能只写了一半格式就乱了。SQLite有事务机制你可以批量提交即使中途出错已经提交的数据也是完整的。我一般每1000个点提交一次事务这样既保证了写入效率又不会因为频繁提交拖慢速度。3.2 表结构设计与写入优化表结构设计上我建了两张表一张是scan_info记录每一帧的元信息比如帧ID、时间戳、起始角度、结束角度、点数另一张是scan_point记录每个点的具体数据包括帧ID、角度、距离、强度如果雷达支持。两张表通过帧ID关联。CREATE TABLE scan_info ( frame_id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp REAL NOT NULL, start_angle REAL, end_angle REAL, point_count INTEGER ); CREATE TABLE scan_point ( id INTEGER PRIMARY KEY AUTOINCREMENT, frame_id INTEGER NOT NULL, angle REAL NOT NULL, distance REAL NOT NULL, intensity INTEGER, FOREIGN KEY (frame_id) REFERENCES scan_info(frame_id) ); CREATE INDEX idx_point_frame ON scan_point(frame_id); CREATE INDEX idx_info_time ON scan_info(timestamp);写入的时候用executemany批量插入比一条一条insert快很多。我实测下来批量插入1000个点大概只要几毫秒完全跟得上雷达的出点速度。这里有个细节scan_info的frame_id是自增主键插入之后可以用cursor.lastrowid拿到刚插入的帧ID然后把这个ID赋给这一帧的所有点。如果你用SQLite的insert into之后获取自动序号lastrowid就是标准做法不需要再查一次。注意SQLite默认的日志模式是DELETE写入时会锁库。如果你的程序同时有多个线程读写建议开启WAL模式命令是PRAGMA journal_modeWAL;这样读写可以并发不会互相阻塞。3.3 用DB Browser和DBeaver做离线复盘数据存进去之后怎么用我常用的工具是DB Browser for SQLite免费开源界面简单打开数据库文件就能浏览表数据、执行SQL查询。比如你想看看某一帧的点分布可以执行SELECT angle, distance FROM scan_point WHERE frame_id 100 ORDER BY angle;然后把结果导出成CSV用Python画个极坐标图一眼就能看出这一帧有没有异常。DBeaver功能更强一些支持多种数据库适合你同时管理多个项目的数据。它有个好处是可以直接画ER图表之间的关系一目了然。如果你在Windows上做开发这两个工具都有安装包装完就能用不需要额外配置。离线复盘的价值在于当cartographer建图效果不好的时候你可以把数据库里的数据回放出来看看是不是某些帧的数据有问题。比如我发现过一次建图飘的情况回放数据后发现是雷达在某个角度区间一直输出最大距离值说明那个方向有强光干扰或者雷达被遮挡了。这种问题如果只看建图结果很难定位原因但有了原始数据就一目了然。4. ROS2节点搭建从串口数据到激光扫描话题4.1 ROS2环境准备与依赖安装我用的ROS2版本是Humble跑在Ubuntu 22.04上。如果你还没装ROS2推荐用鱼香ROS2的一键安装脚本省去很多配置麻烦。安装完之后需要额外装pyserial和sqlite3的Python包命令是pip install pyserialsqlite3是标准库自带的不用装。创建工作空间和功能包的命令如下mkdir -p ~/n10_ws/src cd ~/n10_ws/src ros2 pkg create --build-type ament_python n10_driver --dependencies rclpy sensor_msgs功能包建好之后在n10_driver目录下创建两个节点文件一个负责串口解析和发布一个负责数据存储。我一开始想把两个功能合在一个节点里后来发现分开更好因为存储节点可以独立控制启停调试的时候只跑解析节点不写数据库速度更快。4.2 发布LaserScan消息的关键字段cartographer订阅的是sensor_msgs/msg/LaserScan话题所以解析节点最终要发布这个类型的消息。LaserScan的字段不少但核心是这几个angle_min、angle_max、angle_increment、range_min、range_max、ranges、intensities。angle_min和angle_max是扫描的起始和结束角度单位是弧度angle_increment是每个点之间的角度增量ranges是一个浮点数组存每个点的距离值单位是米。这里有个容易出错的地方N10输出的角度可能是顺时针增加的而ROS2的LaserScan默认是逆时针增加的。如果方向搞反了建出来的图会是镜像的。解决办法是在解析的时候把角度取负或者调整angle_increment的符号。我建议在代码里加一个参数来控制方向这样换雷达的时候不用改代码。from sensor_msgs.msg import LaserScan import math def build_scan_msg(points, frame_idlaser): msg LaserScan() msg.header.frame_id frame_id msg.header.stamp self.get_clock().now().to_msg() msg.angle_min math.radians(min(p[0] for p in points)) msg.angle_max math.radians(max(p[0] for p in points)) msg.angle_increment math.radians(1.0) msg.range_min 0.1 msg.range_max 8.0 msg.ranges [p[1] for p in points] msg.intensities [p[2] for p in points] return msg发布频率方面N10一般每秒转10圈左右所以发布频率大概10Hz。如果你发现cartographer建图时地图更新很慢可以检查一下发布频率是不是太低了。但也不要盲目提高频率因为cartographer需要时间做扫描匹配频率太高反而会增加计算负担。4.3 节点间的消息传递与QoS配置ROS2的消息传递机制和ROS1有很大不同QoS服务质量配置是一个重点。LaserScan这种传感器数据通常用SensorDataQoS它的特点是best effort不保证每条消息都送达但延迟低。cartographer默认订阅的QoS也是SensorDataQoS所以两边要匹配否则收不到数据。我踩过一次坑解析节点用了默认的QoSreliablecartographer用SensorDataQoS结果话题能连上但收不到数据。排查了半天才发现是QoS不兼容。后来统一改成SensorDataQoS就好了。如果你不确定用哪种可以在命令行用ros2 topic info /scan --verbose查看话题的QoS配置。另外如果解析和存储分成两个节点它们之间可以通过话题传递数据也可以直接在解析节点里调用存储模块。我选择的是后者因为存储不需要跨节点通信直接函数调用更简单。但如果你想让存储节点独立运行那就需要定义一个自定义消息类型把一帧的点打包发过去。5. cartographer单雷达建图配置参数调优与避坑5.1 cartographer_ros的安装与launch文件编写cartographer_ros的安装方式有源码编译和二进制安装两种。我推荐用二进制安装命令是sudo apt install ros-humble-cartographer ros-humble-cartographer-ros省去编译时间。装完之后需要创建一个launch文件来启动建图节点。launch文件的核心是配置两个部分一个是cartographer_node负责建图算法一个是occupancy_grid_node负责把建好的地图以栅格形式发布出来方便在rviz2里查看。还需要一个urdf文件描述雷达的安装位置和坐标系关系虽然单雷达建图对urdf要求不高但base_link到laser的tf变换必须有否则cartographer不知道雷达在哪。from launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): return LaunchDescription([ Node( packagecartographer_ros, executablecartographer_node, parameters[{use_sim_time: False}], arguments[-configuration_directory, /path/to/config, -configuration_basename, n10_2d.lua], remappings[(scan, /scan)] ), Node( packagecartographer_ros, executableoccupancy_grid_node, parameters[{resolution: 0.05}] ) ])5.2 lua配置文件里最影响建图效果的几个参数cartographer的配置文件是lua格式参数很多但真正影响单雷达建图效果的没几个。我挑几个重点说。第一个是TRAJECTORY_BUILDER_2D.min_range和max_range这两个要和LaserScan里的range_min和range_max一致。如果lua里设的max_range是30米但雷达实际只能测8米那cartographer会以为8米之外还有数据导致地图边缘出现虚假障碍物。我一般设成和雷达实际量程一致N10的话min_range设0.1max_range设8.0。第二个是TRAJECTORY_BUILDER_2D.missing_data_ray_length这个参数控制没有数据的地方怎么处理。默认是5米意思是如果某个方向没有返回数据就认为5米之内是空的。这个值如果设得太大地图上会出现大片的空白区域设得太小又会导致地图不完整。我的经验是设成max_range的一半左右比较合适。第三个是TRAJECTORY_BUILDER_2D.motion_filter这个参数控制运动滤波包括最大时间间隔、最大距离间隔和最大角度间隔。如果设得太小cartographer会频繁插入新节点计算量大设得太大地图更新不及时转弯的时候容易飘。我一般设max_time_seconds为5max_distance_meters为0.2max_angle_radians为0.1。5.3 建图飘的常见原因与排查思路建图飘是单雷达SLAM最常见的问题表现是地图上出现重影、墙壁不直、回环失败。原因可能有很多我按排查顺序列一下。先看数据质量。用之前存的SQLite数据回放检查每一帧的点是否正常。如果发现某些帧的点数明显偏少或者距离值全是最大值说明雷达在那个时刻被遮挡或者受干扰了。这种情况要先解决硬件问题调参没用。再看时间戳。cartographer非常依赖时间戳的准确性如果LaserScan的时间戳和tf的时间戳对不上扫描匹配就会出错。检查方法是ros2 topic echo /scan --field header.stamp看看时间戳是不是在递增有没有跳变。如果用的是系统时间确保没有NTP同步导致的跳变。然后看tf变换。base_link到laser的变换必须是静态的而且方向要正确。如果雷达装反了建出来的图就是镜像的。可以用ros2 run tf2_tools view_frames生成tf树检查一下。最后才是调参。如果数据质量、时间戳、tf都没问题那可能是motion_filter或者扫描匹配的参数需要调整。我一般先把motion_filter调小一点让cartographer更频繁地更新地图看看效果有没有改善。如果改善了再慢慢调大找到计算量和精度的平衡点。6. 实操中积累的几个关键经验6.1 串口权限和热插拔的处理Linux下串口设备默认需要root权限才能访问普通用户跑节点会报Permission denied。解决办法是把用户加到dialout组命令是sudo usermod -a -G dialout $USER然后重新登录生效。这个操作一次就行后面插拔雷达都不用再管权限。热插拔的问题是如果雷达在节点运行过程中被拔掉串口读取会抛异常节点可能崩溃。我的做法是在读取循环里加try-except捕获SerialException然后尝试重新打开串口。这样即使雷达短暂断开节点也能自动恢复不用重启。6.2 数据库文件的管理和清理SQLite数据库文件会随着时间不断增大跑一个小时可能就有几百MB。如果长期运行需要定期清理或者分文件存储。我的做法是按日期分文件每天一个数据库文件名带上日期比如n10_20250101.db。这样既方便管理也避免单个文件过大。清理的时候不要直接删文件先用SQL命令删除旧数据然后执行VACUUM命令回收空间。VACUUM会重建数据库文件把碎片整理掉文件大小会明显减小。但VACUUM需要额外的磁盘空间执行前确保磁盘有足够余量。6.3 从建图结果反推数据问题的技巧有时候建图结果不理想但你又不知道是数据问题还是参数问题。我的技巧是在rviz2里同时显示LaserScan和地图然后慢慢移动雷达观察扫描点是否贴合地图上的墙壁。如果扫描点和墙壁之间有明显的偏移说明位姿估计有问题如果扫描点本身就很散乱说明数据质量有问题。另一个技巧是把建图过程中的位姿轨迹导出来和实际移动路径对比。cartographer会发布tf你可以用ros2 bag record /tf录下来然后用Python画轨迹图。如果轨迹和实际路径偏差很大那肯定是SLAM算法的问题需要调参如果轨迹基本吻合但地图有重影那可能是数据噪声导致的。6.4 关于ROS2和cartographer版本匹配的提醒ROS2的版本和cartographer的版本必须匹配否则会出现各种奇怪的编译错误或者运行时错误。我用的是Humble配cartographer_ros的Humble分支一直很稳定。如果你用的是Jazzy或者Foxy要确认cartographer_ros有对应的分支。有些版本可能需要源码编译编译的时候注意依赖包的版本特别是protobuf和abseil这两个库版本不匹配是最常见的编译失败原因。如果你在编译过程中遇到undefined reference之类的链接错误大概率是依赖库版本问题。我的建议是先用二进制安装如果二进制包不存在再考虑源码编译。源码编译的时候严格按照官方文档的依赖列表来装不要自己升级或者降级某个库很容易引发连锁反应。7. 数据回放与二次分析的扩展思路7.1 用Python做雷达数据的可视化复盘存进SQLite的数据不只是用来备份的还可以做很多二次分析。我写了一个小脚本从数据库里读出一帧数据用matplotlib画极坐标图可以直观地看到每个角度的距离分布。如果某个角度区间一直有异常值一眼就能看出来。更进一步可以把多帧数据叠加起来画成点云图看看整体环境的结构。这个点云图虽然精度不如专门的点云处理工具但胜在简单快速不需要额外的依赖。对于排查建图问题来说这种粗粒度的可视化已经足够定位问题了。7.2 基于历史数据调参的闭环流程有了数据库之后调参就变成了一个闭环跑一次建图把数据存下来然后离线回放数据调整cartographer参数重新建图对比不同参数下的建图效果找到最优配置。这个过程不需要重新跑车只需要在电脑上回放数据就行效率高很多。回放的时候可以用ros2 bag play播放录制的bag文件也可以用自己写的回放节点从SQLite读数据然后发布LaserScan话题。后者更灵活因为你可以控制回放速度也可以只回放特定时间段的数据。我一般用后者因为数据库里的数据已经结构化好了读起来很方便。7.3 多传感器融合的预留接口虽然这次只用了单雷达但代码结构上我预留了多传感器融合的接口。解析节点发布LaserScan话题存储节点订阅这个话题并写库cartographer也订阅这个话题。如果以后要加IMU或者轮式里程计只需要新增对应的解析节点发布对应的话题然后在cartographer的配置里加上输入即可。这种松耦合的设计让系统更容易扩展也更容易调试。IMU的加入对建图效果提升很明显特别是在快速转弯或者雷达被短暂遮挡的时候IMU可以提供位姿估计的补充。如果你手头有IMU建议试试加上去cartographer对IMU的支持很成熟配置也不复杂。轮式里程计也是类似如果有的话尽量用上能显著减少建图飘的情况。7.4 关于数据存储格式的再思考SQLite虽然方便但也不是万能的。如果数据量特别大比如连续跑几个小时SQLite的写入速度可能会成为瓶颈。这种情况下可以考虑用ROS2的bag文件格式它专门为机器人数据设计支持压缩和分块写入效率更高。但bag文件的分析不如SQLite方便需要专门的工具来读取。我的建议是短时间调试用SQLite长时间运行用bag。两者也可以结合平时用SQLite存关键帧需要完整数据的时候再开bag录制。这样既保证了调试的便利性又不会因为数据量过大影响系统性能。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑