ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SICK LMS111雷达:从串口协议到3D点云与SLAM建图

SICK LMS111雷达:从串口协议到3D点云与SLAM建图 如果你手头正好有一台SICK LMS111别急着把它当高级玩具或者直接扔给ROS跑现成驱动。这阵子我在做一个仓库搬运平台的改造把一台吃灰多年的LMS111重新翻出来从RS485差分线一路剥到三维点云整个过程踩了不少坑也把协议彻底搞明白了。这篇东西就是一次完整复盘从底层原始帧怎么拆、距离角度怎么换算到加一个转台把2D扫描变成3D点云再到后续接ROS2配合Cartographer建图时那些飘的问题到底出在哪一次说清楚。不管你只想要一份能跑的解析代码还是想弄明白LMS111为什么同一个点一会儿近一会儿远又或者只是听过激光雷达SLAM几个词想从零开始这篇都能给你一个完整路径。我尽量不端着讲术语该上代码上代码该晒坑晒坑。1. 摸清LMS111的脾气单线雷达到底能干什么1.1 这台被炒到天价的老将参数其实很朴素SICK LMS111属于LMS1xx系列里的经典款放在今天看参数并不惊艳单线扫描、270度范围、20米标称距离角度分辨率最高0.25度扫描频率一般用25Hz或50Hz。可它在工业自动化、AGV导航、港口防撞里地位一直很稳因为皮实、可靠、IP67防护红外905nm激光对环境光的抗性比很多消费级雷达强一截。我这里从SOPAS里读出来的配置是扫描频率25Hz、角分辨率0.25度这样一帧就是1081个点。如果把角分辨率放宽到0.5度一帧541个点扫描频率能上到50Hz。说白了就是一个固定平面的截面扫描雷达转动的是内部棱镜每条光束发出去测到距离就得到一个以雷达为原点的极坐标点。这个只扫一个平面的特性是理解后面所有东西的前提。你可能会问单线雷达没有俯仰信息怎么做三维点云答案是加外部自由度。雷达本身扫X-Y平面我们给它加一个绕Z轴转动的云台或者让它做俯仰摆动每一帧2D扫描线就对应三维空间里的一个截面积累起来就是点云。思路不复杂难的是把帧数据和云台角度对上时间。1.2 从2D到3D的技术路线与工具链选择把LMS111改造成三维扫描主流做法有三种第一种是水平扫描的雷达装在俯仰摆动的云台上像点头一样一层层扫第二种是雷达固定抬头或者低头让转台带着绕竖直轴转适合大范围环境重建第三种是多台雷达拼角度。我这次用的是第二种因为仓库里需要360度环视俯仰方向只需要把安装倾角固定好就够。工具链上我全程用Python串口读取用pyserial数值计算用numpy2D可视化用matplotlib3D点云用Open3D。后续要接ROS2时再做一层ROS2驱动节点把解析结果发布成sensor_msgs/LaserScan。前面数据解析拿到的是干净的帧数据后面接SLAM还是做目标检测都顺理成章。2. LMS111通信协议与原始帧解剖2.1 RS485差分信号和物理接线LMS111在硬件上保留了RS485接口工业现场很喜欢这种差分信号方案两根线A和B靠电压差传输抗共模干扰能力强布线距离可以到百米以上。我这次就是买了一个USB转RS485的调试棒A接A、B接B另外把GND也连上别小看这根地线不接的时候偶尔会出现整帧丢字节的情况。RS485是半双工同一时刻只能收或者发所以串口参数必须和传感器侧对齐。我习惯在SOPAS软件里先把波特率固定住测试时设为1152008位数据位无校验1位停止位。如果你收到的数据全是乱码先别怀疑协议先用串口助手看一眼十六进制只要是02 73 52 41...这样的固定头就说明物理链路通了。接线的坑主要在插针定义上不同批次的LMS111接口定义可能略有差异动手前先对着铭牌查一下针脚定义别盲插。有一次我就是因为手里的线序定义是旧版本的结果A和B反了串口上永远都是错码折腾了半天才发现是线序问题。2.2 一个完整数据帧的内部结构LMS111数据帧的主体是ASCII文本加二进制控制字符混排的格式帧头是STX十六进制0x02紧接着是ASCII字符sRA LMDscandata作为类型标识然后是空格分隔的十进制数字段帧尾是ETX0x03最后跟一个字节的校验和。为了直观我把一帧的十六进制开头部分列出来给你看02 73 52 41 20 4C 4D 44 73 63 61 6E 64 61 74 61 20 31 20 31 20 30 20 30 20 35 ...解析成ASCII就是sRA LMDscandata 1 1 0 0 5 ...sRA是SICK协议里扫描数据响应的含义LMDscandata则是数据块标识后面跟着的字段就是真正的内脏。版本号、设备号、序列号、设备状态、电报计数、扫描计数、上电时间毫秒数、传输状态、扫描频率、测量频率……这些是头部信息。我在实际解析时不太死记每个字段的绝对位置因为不同固件版本偶尔会差一个字段但核心原则一样先按空格切分再根据通道数量字段动态定位通道数据区。2.3 距离、角度和反射率到底藏在哪个字段LMS111典型输出是两个通道第一个通道是距离第二个通道是反射强度RSSI。每个通道在帧里都有几个描述参数通道类型、编码数量、scale factor、scale offset、起始角度、角步长、数据点数量最后才是真正的数据序列。距离值的换算公式非常关键实际距离(mm) 原始整数值 / scale_factor - scale_offset注意scale offset前面是减号还是加号不同协议版本文档写法不一样我在自己设备上实测是减法你在SOPAS里和看到的距离对比一下就知道符号对不对。角度值则是实际角度(度) 起始角(1/10000度) / 10000 索引编号 * 角步长(1/10000度) / 10000LMS111在0.25度分辨率下起始角常见值是-450000也就是-45度角步长2500也就是0.25度点数1081。从-45度开始一直扫到225度正好覆盖270度范围。有了距离和角度每个二维点的极坐标就齐了。3. 原始帧解析实战手写Python解析器3.1 串口读取与组帧同步串口读数据时不会正好一次收到一帧所以要自己组帧。我的做法是逐字节读遇到STX(0x02)开始缓存一直读到ETX(0x03)为止再把ETX后面的校验字节一起收进来。这里有个容易踩的坑串口缓冲区里可能混着半帧或者脏数据所以必须用STX重新同步不能按固定长度切。import serial ser serial.Serial( port/dev/ttyUSB0, baudrate115200, bytesizeserial.EIGHTBITS, parityserial.PARITY_NONE, stopbitsserial.STOPBITS_ONE, timeout1.0 ) def read_lms111_frame(ser): while True: head ser.read(1) if head ! b\x02: continue buf bytearray(b\x02) while True: c ser.read(1) if not c: break buf.append(c[0]) if c b\x03: # 再读一个校验字节 chk ser.read(1) if chk: buf.append(chk[0]) return bytes(buf)注意这个函数里用STX同步的方式是很实用的习惯哪怕前面有垃圾数据只要等到了0x02就能重新对齐。工程上我还会加一个超时保护防止长时间没有完整帧时死循环。3.2 解析帧数据的核心代码拿到完整帧后去掉首尾的STX、ETX和校验字节剩下的ASCII部分按空格切分。我的解析思路是动态定位通道而不是写死所有索引。第一步先确认帧头字段第二步找到通道数量字段第三步循环解析每个通道。def parse_lms111_frame(frame): # frame 是不含STX/ETX的字节序列 tokens frame.decode(ascii, errorsignore).split( ) # 前三个token: s, RA, LMDscandata assert tokens[0] s and tokens[1] RA and tokens[2] LMDscandata idx 3 version int(tokens[idx]); idx 1 # 版本号 device int(tokens[idx]); idx 1 # 设备号 serial_no int(tokens[idx]); idx 1 # 序列号 status int(tokens[idx]); idx 1 # 设备状态 telegram_count int(tokens[idx]); idx 1 scan_count int(tokens[idx]); idx 1 time_ms int(tokens[idx]); idx 1 transmission int(tokens[idx]); idx 1 scan_freq_raw int(tokens[idx]); idx 1 meas_freq_raw int(tokens[idx]); idx 1 num_channels int(tokens[idx]); idx 1 channels [] for _ in range(num_channels): ch_type int(tokens[idx]); idx 1 # 1距离, 0RSSI等 enc_num int(tokens[idx]); idx 1 # 编码数量 scale_factor int(tokens[idx]); idx 1 scale_offset int(tokens[idx]); idx 1 start_angle_raw int(tokens[idx]); idx 1 step_angle_raw int(tokens[idx]); idx 1 point_count int(tokens[idx]); idx 1 values [] for _ in range(point_count): values.append(int(tokens[idx])) idx 1 channels.append({ type: ch_type, scale_factor: scale_factor, scale_offset: scale_offset, start_angle_raw: start_angle_raw, step_angle_raw: step_angle_raw, values: values }) return { version: version, device: device, serial_no: serial_no, scan_count: scan_count, time_ms: time_ms, channels: channels }这段代码有一个地方需要你实际对一下不同固件的头部字段数量可能差一个如果你解析出来的通道数量不对多半是头部某个字段切错了。我的校验方法很简单看距离通道的点数是不是10810.25度分辨率或者5410.5度分辨率如果是说明索引对了。3.3 把极坐标转成二维像素看效果解析出距离和角度后先用matplotlib画一下二维扫描线能立刻确认数据链路是不是通的。这里我把距离通道换算成毫米再按角度展开到X-Y平面过滤掉无效点。LMS111对于测不到回波的场景会输出特定的大值或小值一般可以选择设定阈值过滤比如距离大于20000mm的直接丢弃。import numpy as np import matplotlib.pyplot as plt def scan_channel_to_xy(channel): raw np.array(channel[values], dtypenp.float64) dist_mm raw / channel[scale_factor] - channel[scale_offset] start_deg channel[start_angle_raw] / 10000.0 step_deg channel[step_angle_raw] / 10000.0 n len(raw) angles_deg start_deg np.arange(n) * step_deg angles np.deg2rad(angles_deg) x dist_mm * np.cos(angles) y dist_mm * np.sin(angles) return x, y frame read_lms111_frame(ser) parsed parse_lms111_frame(frame[1:-2]) # 去掉STX/ETX/checksum dist_ch parsed[channels][0] x, y scan_channel_to_xy(dist_ch) plt.figure(figsize(8, 8)) plt.scatter(x, y, s1) plt.axis(equal) plt.show()跑到这一步你基本已经拿到LMS111吐出来的2D截面图了。第一次看到数据正常出图的时候还挺有成就感的但真正的重头戏还在后面——让这条扫描线在空间里动起来攒成三维点云。4. 从2D扫描线到三维点云坐标变换与转台方案4.1 三个坐标系和外参到底在说什么谈到三维点云绕不开坐标系。我这边涉及三个坐标系雷达自身坐标系、云台/转台坐标系、世界坐标系。雷达自身坐标系的原点在雷达光学中心X-Y平面就是扫描平面。转台坐标系的原点在转台旋转轴上Z轴是旋转轴。世界坐标系是最终点云所在的参考系一般和转台底座固定。外参的本质就是雷达坐标系到转台坐标系的平移量和旋转量。比如雷达装在转台上面安装高度是0.5米那么平移向量就是[0, 0, 0.5]。如果雷达安装时还有俯仰角或者滚动角就需要一个旋转矩阵把雷达扫描平面扭正。很多点云歪掉的情况就是外参里那几度没标对。一个很常见的直觉类比你拿着一把激光笔站在旋转椅子上激光笔是雷达椅子是转台你的身高和坐姿就是外参。椅子转是角度同步你歪着坐扫出来的墙就是歪的。4.2 角度同步才是三维重建的灵魂单帧2D数据只能画一条弧线要变成3D必须知道这条弧线在转台转到哪个角度时采的。这里有两个层次的做法。最省事的是软同步转台匀速转通过USB/串口把每一帧的编码器角度和时间戳同时读回来再用时间戳给每帧打上角度标签。如果转台速度很稳线性插值就行速度不稳就要用编码器实时角度。更好的是硬同步转台每转到一个固定角度步进通过IO触发LMS111采集一帧。这样每帧和角度严格对应不依赖时间戳精度。我用的是中间方案转台控制器每个周期输出当前角度我用单片机把角度和时间打包成串口数据和LMS111的扫描帧放在同一台工控机上最后按scan_count和时间戳对齐。这里必须多说一句如果你发现点云在墙面上出现扭曲或重影十有八九不是雷达坏了而是角度同步没做好。转台转速抖动、串口延迟、时间戳没有统一时钟都会让同一面墙被拧成麻花。4.3 坐标变换代码和Open3D可视化假设现在我的雷达水平安装绕Z轴旋转的转台在某个时刻给出了偏航角yaw_deg。对当前这一帧内的任意一点先算它在雷达坐标系下的三维坐标再绕Z轴旋转yaw。def scan_line_to_3d_points(channel, yaw_deg, z_offset0.0): raw np.array(channel[values], dtypenp.float64) dist_mm raw / channel[scale_factor] - channel[scale_offset] dist_m dist_mm / 1000.0 start_deg channel[start_angle_raw] / 10000.0 step_deg channel[step_angle_raw] / 10000.0 n len(raw) angles_deg start_deg np.arange(n) * step_deg theta np.deg2rad(angles_deg) # 雷达扫描平面内的坐标Z0 x_l dist_m * np.cos(theta) y_l dist_m * np.sin(theta) z_l np.zeros_like(x_l) # 绕Z轴旋转yaw_deg alpha np.deg2rad(yaw_deg) cos_a np.cos(alpha) sin_a np.sin(alpha) x_w x_l * cos_a - y_l * sin_a y_w x_l * sin_a y_l * cos_a z_w z_l z_offset # 过滤无效距离 valid dist_m 0.1 return np.stack([x_w[valid], y_w[valid], z_w[valid]], axis1)把多帧累积起来用Open3D显示import open3d as o3d all_points [] # points_by_scan假设是每帧解析出的点集合 for i, p in enumerate(points_by_scan): all_points.append(p) cloud np.concatenate(all_points, axis0) pcd o3d.geometry.PointCloud() pcd.points o3d.utility.Vector3dVector(cloud) o3d.io.write_point_cloud(warehouse_scene.pcd, pcd) o3d.visualization.draw_geometries([pcd])如果一切正常你会看到原本一条条的扫描弧线在空间里铺开墙面、货架、柱子逐渐成型。我第一次拿这个流程扫了一个20米长的仓库通道效果比预想中好很多尤其是柱子和门洞的位置轮廓非常清楚。4.4 外参标定的土办法外参标定坦白讲是个脏活。最靠谱的办法是用一个已知尺寸的立方体标定架或者干脆用一面足够平的墙。先把转台归零让雷达正面朝向墙面扫一帧记录墙面点在雷达坐标系下的法向量再把雷达装到转台上的俯仰角误差通过误差角和墙角特征反算出来。我自己的土办法是先用卷尺量出雷达光学中心到转台旋转轴的水平距离和高度差作为初始外参然后扫一圈看三个不同距离的柱子在点云里是什么形状。如果柱子拉得很扁或者呈圆弧状说明外参的旋转部分有偏差如果柱子位置整体偏了说明平移部分有误。再用手动调整加细化微调一般半小时能收敛。你也可以扫完直接和已知尺度的CAD图对齐用ICP算法自动迭代但前提是初始值别差太离谱。5. 工程化避坑与常见问题速查5.1 解析阶段校验失败、丢帧、乱码我在解析阶段遇到的问题可以整理成一张速查表现象 可能原因 处理方式 串口全乱码 波特率不对 / A、B线接反 / GND没接 核对SOPAS配置交换A/B补接GND 一帧很长但解析出来点数不对 头部字段索引错误 / 固件版本有差异 打印完整字段列表人工数一遍关键索引 偶尔跳帧或半帧 串口缓冲区截断 / USB转485质量差 改用逐字节组帧加STX同步 校验和一直失败 RS485半双工方向切换冲突 / 采集程序占用串口 确保只有单一进程读串口 距离值整体偏大或偏小 scale_offset符号错误 和SOPAS读数对比修正换算公式这里特别说一下校验和LMS111帧末尾那个校验字节我自己实测下来就是对前面所有字节累加取低8位。有些文档描述为STX checksum实现时要注意有些固件会把STX排除在外所以我在代码里会同时算含STX和不含STX两个累加结果哪个对用哪个。这种偏移问题在工业协议里太常见了别迷信文档以实际抓包为准。5.2 建图飘、点云飘的常见套路激光雷达建图飘是高频问题我在这个项目上也碰到了。首先要排查的永远是外参和时间同步。雷达相对于转台或者车体差半度在10米外就是10厘米的偏差这在建图里足够让墙变成双层。时间同步没做好的话转台已经转走了雷达还在发上一帧的数据点云会拖着一条尾巴。再往下排就是机械振动。LMS111本身测量很稳但装在转台上转台每个步进动作带来的抖动都会反映在点云里。解决方法是调整转台的加减速曲线扫描时尽量匀速不要在急加速阶段采数。我后来把转台每步的停留时间拉长点云质量明显提升。如果做SLAM的时候还飘那就是里程计和雷达融合的问题。Cartographer里常见的几个调整方向1. use_online_correlative_scan_matching: true 2. 适当减小 submap 的 size降低累积漂移 3. 如果环境特征少打开或者调高 loop closure 相关参数 4. 加IMU或者轮式里程计约束别只靠雷达有一点心得是Cartographer不是参数越宽松越好约束太强会让建图对初始外参极度敏感外参稍微偏一点就整个地图扭曲约束太弱又会在长走廊里飘。我最后是把外参标到误差小于0.5度然后才去调Cartographer参数效果立刻不一样。5.3 反射率数据也是金矿很多人只盯着距离通道忽略了LMS111自带的反射强度RSSI。反射强度通道在帧里通常是第二通道解析方式跟距离通道一样。这个数据特别适合做目标检测。比如地面上的反光条、货架上的标签、路沿的白线在距离图像里可能看不出明显特征但强度值会有一个明显跳变。我做目标检测时最简单的办法就是对RSSI做阈值分割先找出高反射区域再用聚类把一个个反光条或者标靶分出来。配合距离信息可以计算出目标在笛卡尔系下的精确位置。这个方法在反光板导航的AGV项目里很常用比纯几何特征鲁棒得多。5.4 后续扩展ROS2驱动与Cartographer建图这部分算是顺水推舟。有了前面的解析代码封装ROS2节点很直接把雷达数据发布成sensor_msgs/LaserScan把转台角度和里程计信息整合进tf树Cartographer就能直接用。写驱动时要注意LaserScan的angle_min、angle_increment和range_max必须和帧里读出来的起始角、角步长、量程一致否则Cartographer会在前端就把数据理解错。另外scan_count和时间戳要处理好我这边直接把LMS111的扫描计数填到header.seq里用上电时间毫秒数填stamp统一在节点内部转成ROS时间后面做时间同步才不手忙脚乱。最后再说两句做这个项目最大的感受是LMS111这类工业雷达的协议说复杂也复杂说简单也简单难点全在细节。校验算法、字节顺序、字段偏移、scale符号任何一个地方不对数据就是天书。但只要把原始帧彻底拆明白了后面无论是转台三维扫描、ROS2建图还是目标检测都会顺畅很多。我个人推荐的做法是拿到雷达先别急着跑现成库亲手写一遍解析器哪怕只是打印出帧号、点数、第一个点的距离这种最原始的信息也比直接拿黑盒驱动让你心里有底。等你真的从一串十六进制里还原出一张能看的点云图那种掌控感是别人和你说多少遍这个库挺好用都换不来的。最后再分享一个小技巧验证解析器对不对可以拿一个反光板或者白纸板放在已知距离处对比LMS111读数和卷尺量的值距离对了再看角度。这个看似蠢的办法帮我避开了至少三次自以为协议没问题、实际换算方向的坑。
返回列表