☰
ROS+STM32超声波测距串口通信:从环境配置到避坑排查
2026/11/5 5:56:28 网站建设 项目流程

简介:面向学习 ROS 与 STM32 串口联调的机器人开发者,代码包围绕 HC-SR04 超声波测距与下位机控制展开,可用于搭建传感器采集、距离话题发布到串口指令下发的实验链路,适合具备一定嵌入式与 ROS 基础、制作避障或远程控制原型的读者。压缩包共 58 个文件,约 1.63MB,含 11 个 cc 源文件、若干头文件以及 package.xml、CMakeLists.txt、Makefile、yml 配置、README 与说明文档,覆盖 ROS 功能包、serial 串口模块、示例程序和构建配置,目录便于按节点与固件查阅。已有 396 人学习。读者可理解 ROS 节点如何订阅距离数据、向 STM32 发送控制命令并驱动 LED 等外设,也可参考示例与配置完成环境搭建、代码梳理和串口排错,形成可复用的软硬件联调方案。

1. 一包 supersonic.tar.gz 落地前:ROS 和 STM32 的串口链路,卡人的往往不是代码

上个月某个下午,我把一包 supersonic.tar.gz 丢进工作机,解压、catkin_make、插线,三步走完,rostopic echo出来的距离值却一动不动。这套东西看起来简单:STM32 打 Trig、收 Echo、算距离,通过串口把字节甩给上位机,ROS 节点再把字节翻译成sensor_msgs/Range。链路不长,环节却不少——串口权限、波特率、帧解析、单位换算,任何一处对不上,表现出来的症状都一样:没有数据。

刚接触 ROS 串口通信的人最容易在这里耗掉一整天,因为编译过程完全不报错,节点也正常启动,只是安静地不回应。这套环境适合手上有块 STM32 最小系统板、一个超声波模块、想跑通第一段硬件在环数据流的人,也适合固件现成、只想让话题出数的人。

2. 环境清单:Ubuntu 20.04 + ROS Noetic + arm-none-eabi-gcc 怎么配才不返工

这套组合的版本关系比想象中紧。ROS 的serial包、Python 的pyserial、交叉工具链的 newlib,三者版本错位时,表现出来的是「编译过了但运行时报符号缺失」,比直接编译失败更难查。所以动手前先把版本钉死,比装完再补要省事得多。

2.1 三套版本组合与选择依据

我把常见搭配列成一张表,按优先级排。第一行是这两年我默认用的组合,装机成本低,社区资料也最全。

组合系统ROS 版本工具链建议
AUbuntu 20.04 LTSNoeticgcc-arm-none-eabi 9-2020-q2 及以上首选,Python3 原生
BUbuntu 18.04 LTSMelodic同上老机器继续用,注意 Python2
CUbuntu 22.04 LTS无官方 ROS1 二进制包同上新手别碰,要自己编

组合 A 的核心优势是 Python3。Noetic 默认 Python3,ros-noetic-serial、python3-serial直接 apt 装完就能用,不用在python和python3之间来回切换。组合 B 的坑集中在脚本层:Melodic 时期大量示例代码写的还是#!/usr/bin/env python,在 18.04 上指向 Python2,pyserial的Serial()参数名和行为跟 Python3 有细微差别,抄来的调试脚本经常第一行就崩。

组合 C 不是不能跑,而是没有官方二进制包,得靠源码编译或者容器。如果只是想让超声波出数,没必要在这上面耗两天。

提示:Noetic 的官方支持周期已经走到尾段,新装机如果是纯学习用途,仍然建议 20.04 + Noetic,因为这份固件和节点代码对 ROS1 的 API 依赖很直接,迁到 ROS2 需要改ros::NodeHandle那一层,属于另一件事。

2.2 系统依赖与串口库安装

依赖分两类:ROS 侧的serial封装,和系统侧的串口工具。前者给节点用,后者给排查用。两条线都要装,缺一个都会在某个时刻卡住。

sudo apt update sudo apt install -y \ ros-noetic-serial \ ros-noetic-rviz \ ros-noetic-tf2-tools \ python3-serial \ python3-catkin-tools \ build-essential \ cmake \ git

ros-noetic-serial是 ROS 包装过的串口库,提供serial::Serial类,节点源码里#include <serial/serial.h>依赖它。很多人只装了python3-serial,用 Python 脚本能读串口,就以为环境齐了,结果catkin_make到一半报Could not find a package configuration file provided by "serial",就是这一条漏了。

python3-serial是给命令行调试用的。我习惯在启动 ROS 节点之前,先用一个十行脚本直接读原始字节,确认硬件侧在发东西。这一步能砍掉一半的排查分支——如果 Python 直读都没字节,问题一定在 STM32 或接线,跟 ROS 无关。

python3-catkin-tools提供catkin build,和catkin_make二选一即可。这份包用catkin_make就够,装 tools 是为了后面单独编译某个包时方便。

装完之后把当前用户加进dialout组,这一步不做,后面所有串口操作都会报Permission denied:

sudo usermod -aG dialout $USER

命令本身没有输出,容易被当成没生效。它修改的是/etc/group,要重新登录(或者newgrp dialout开个新 shell)才作用于当前会话。groups命令能看到dialout才算成功。

2.3 交叉编译工具链与烧录工具

STM32 侧的固件要用 ARM 工具链编,不能用系统自带的 x86 gcc。Debian 系直接 apt 装:

sudo apt install -y \ gcc-arm-none-eabi \ binutils-arm-none-eabi \ libnewlib-arm-none-eabi \ stlink-tools

libnewlib-arm-none-eabi是最容易被忽略的一个。缺了它,编译阶段可能过得去,链接阶段会报一堆undefined reference to _sbrk、_write、_close,因为 newlib 提供的系统调用桩没链进来。看到这类报错,先查这个包,别去改链接脚本。

stlink-tools提供st-flash和st-info,对应 ST-Link 下载器。装完验证两条命令:

arm-none-eabi-gcc --version | head -n 1 st-flash --version

arm-none-eabi-gcc输出 9-2020-q2 或更高都行,这份固件没用到需要更新版本的语言特性。st-flash建议 1.6 以上,低于这个版本对部分克隆版下载器识别不稳,表现为st-info --probe一直找不到设备。

如果手头是 STM32CubeProgrammer 而不是 ST-Link,烧录命令换成STM32_Programmer_CLI -c port=SWD -w xxx.bin 0x8000000,工具链部分完全一样。

注意:有些开发板板载了 ST-Link,但需要把板上的跳线帽拨到正确位置才能被识别。st-info --probe报Found 0 stlink programmers时,先查硬件跳线,再查 USB 线是不是只能充电的那种。

3. 解包与编译:supersonic.tar.gz 落进 catkin 工作空间的三步

压缩包解压位置有讲究。catkin_make的扫描规则是递归找src下含package.xml的目录,如果解压到src外面再手动挪,很容易把build、devel的路径搞乱,后面source devel/setup.bash找不到包。我的做法是先看结构,再决定解到哪。

3.1 先看包结构再决定解压位置

不要直接tar -xzf。先列一下第一层内容:

tar -tzf supersonic.tar.gz | head -n 40

这一步的作用是确认压缩包有没有顶层目录。有两种情况:一种是supersonic/xxx这样带一层壳,另一种是一堆文件直接铺在根上。带壳的解压到src下会变成src/supersonic/supersonic_ros,catkin 照样能扫到,但工作空间会多一层,后面写路径容易错;不带壳的直接解到src就行。

我一般按下面这个流程走,把壳去掉,让包名直接落在src下:

mkdir -p ~/supersonic_ws/src tar -xzf supersonic.tar.gz -C ~/supersonic_ws/src --strip-components=1 ls ~/supersonic_ws/src

--strip-components=1是去掉第一层目录。如果tar -tzf显示第一层就是package.xml和CMakeLists.txt,说明没有壳,这时候要把它去掉,直接-C ~/supersonic_ws/src解压。

解出来的内容大致分三块:ROS 功能包(含package.xml、CMakeLists.txt、节点源码)、STM32 固件工程(含Makefile、Core/、Drivers/)、以及若干调试脚本。三者的关系是:固件跑在板子上,负责测距和发帧;ROS 节点跑在上位机,负责收帧和发话题。

提示:解压后先find ~/supersonic_ws/src -name package.xml确认包的位置。有输出说明结构没问题,没输出说明--strip-components用错了,删掉重来比硬改快。

3.2 建立工作空间并编译 ROS 节点

catkin_make的编译顺序是固定的:先source /opt/ros/noetic/setup.bash,再进工作空间根目录,最后执行。这三步的顺序错了就会报找不到catkin。

source /opt/ros/noetic/setup.bash cd ~/supersonic_ws catkin_make -DCMAKE_BUILD_TYPE=Release source devel/setup.bash

-DCMAKE_BUILD_TYPE=Release是给 CMake 传编译类型,不传的话默认是空,优化等级低。这份节点代码里有串口读循环和字节解析,Release 下 CPU 占用会明显低一些,跑在工控机上差别不大,跑在树莓派这类板子上能看出来。

编译过程最常见的失败是找不到serial:

Could not find a package configuration file provided by "serial" with any of the following names: serialConfig.cmake serial-config.cmake

这不是CMakeLists.txt写错了,是 2.2 里的ros-noetic-serial没装。装完之后要重新source /opt/ros/noetic/setup.bash,因为包的搜索路径是 source 时注入的。

第二个常见失败是devel/setup.bash没 source 就rosrun,报[rospack] Error: package 'supersonic_ros' not found。source devel/setup.bash是每个新终端都要执行的,写进~/.bashrc也行,但要注意别把devel的路径写死成绝对路径,换机器就废了。

编译成功后会看到[100%] Built target supersonic_node,二进制落在devel/lib/supersonic_ros/下。这时候先别急着插串口,把rospack find supersonic_ros跑一下,能打出来路径才算环境通了。

3.3 固件编译与烧录

固件工程一般是 Makefile 组织的,不依赖 CubeMX 也能编。进目录直接make:

cd ~/supersonic_ws/src/supersonic_fw make clean make -j4 arm-none-eabi-size build/supersonic.elf

make clean别省。改了Makefile里的晶振频率或者串口波特率之后,增量编译有时候不会重编所有对象,改完参数行为不变,人会怀疑人生。

arm-none-eabi-size是看 flash 和 RAM 占用的,输出三列text、data、bss。text是 flash 占用,data + bss是 RAM 占用。STM32F103C8T6 这类最小系统板是 64KB flash、20KB RAM,text超过 60000 就要警惕,可能是某个库把浮点格式化带进来了。

烧录走 ST-Link:

st-flash --reset write build/supersonic.bin 0x8000000

0x8000000是 STM32 主 flash 的起始地址,这个地址不要改。--reset表示烧完自动复位,省去手动按复位键。命令跑完会打印Flash written and verified!,看到verified才算真的写进去了,只打印writing就断开的情况多半是下载器接触不良。

烧录前有个顺序问题:先把串口线(USB 转 TTL)从板子上拔下来,再烧。留着线的时候,ST-Link 的 SWD 和 UART 共用部分引脚,偶尔会互相干扰,表现为烧录到 99% 报verify failed。

烧完之后把 USB 转 TTL 接回去,接线只认三根:TXD 对 RX、RXD 对 TX、GND 对 GND。VCC 不要接,板子自己有供电,两个电源顶在一起是常见的烧板原因。

4. ROS 端串口节点配置:端口、帧协议与单位,三个参数决定数据有没有

到这一步,硬件侧应该在发字节了。上位机这边要做的,是把字节流稳定地读进来、按帧切开、换算成 ROS 认识的消息。这部分配错,表现出来同样是「没数据」,但原因完全不同。

4.1 固定串口名与权限

/dev/ttyUSB0这个设备名不稳定。插拔一次,或者同时插了别的 USB 串口设备,编号就可能从 0 变成 1。节点里写死ttyUSB0,跑一次就换一次设备名,属于玄学级的调试体验。

常见做法是用 udev 规则给它一个固定软链。先查设备的 VID 和 PID:

lsusb

USB 转 TTL 大多是 CH340(1a86:7523)或 CP2102(10c4:ea60)。确认之后写规则文件:

sudo tee /etc/udev/rules.d/99-supersonic.rules > /dev/null <<'EOF' SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", SYMLINK+="supersonic", MODE="0666", GROUP="dialout" EOF sudo udevadm control --reload-rules sudo udevadm trigger

SYMLINK+="supersonic"生成/dev/supersonic,节点里用这个名字,插拔多少次都不变。MODE="0666"让所有用户可读写,GROUP="dialout"是兜底,两者有一个生效就行,我一般两个都写上,省得以后换用户还要改。

规则写完必须reload-rules+trigger,只改文件不重载是不生效的,这个坑我踩过一次,改了规则、重插设备、跑节点还是Permission denied,最后发现是没执行这两条。

验证:ls -l /dev/supersonic能看到指向ttyUSB0的软链就对了。

注意:如果同时插了多个 CH340 设备,只靠 VID/PID 区分不开。这种情况要加ATTRS{serial}=="...",具体值用udevadm info -a -n /dev/ttyUSB0 | grep serial查。

4.2 launch 参数逐个说明

节点参数我一般放在 launch 文件里,比命令行传参好维护:

<launch> <node pkg="supersonic_ros" type="supersonic_node" name="supersonic_node" output="screen"> <param name="port" value="/dev/supersonic"/> <param name="baudrate" value="115200"/> <param name="frame_id" value="sonar_link"/> <param name="scale" value="0.001"/> <param name="min_range" value="0.02"/> <param name="max_range" value="4.0"/> <param name="fov" value="0.26"/> </node> </launch>

port用软链名,不用ttyUSB0。baudrate必须和固件里HAL_UART_Init的波特率完全一致,115200 对不上 9600 时的表现不是乱码,而是收到的字节数永远凑不满一帧,节点一直等,话题一直不发。

frame_id是给 TF 和 RViz 用的坐标系名,随便起,但要和后面建 TF 时一致。scale是单位换算系数,固件上报的是毫米,ROS 的sensor_msgs/Range要求米,所以是 0.001。这个值写错是「有数据但差 1000 倍」的根因,比没数据更难发现,因为节点看起来完全正常。

min_range和max_range是传感器量程,HC-SR04 这类模块标称 2cm 到 400cm,填 0.02 和 4.0。RViz 的 Range 显示会用这两个值决定扇形颜色的深浅,填错不影响数值,只影响可视化。

fov是视场角,单位弧度,超声波模块的波束角一般十几度,0.26 弧度约 15 度,属于常见取值。这个参数只影响可视化,不影响测距。

4.3 帧格式与解析逻辑对照

串口是字节流,没有消息边界。固件发什么、节点怎么切,两边必须对上,否则会出现「偶尔出一帧、大部分时候是 0」这种最难查的现象。

这份固件用的是固定长度小帧:帧头 2 字节0xAA 0x55,载荷 2 字节距离(小端,毫米),校验 1 字节累加和。C 侧发送:

/* sonar_send: 把测距结果按 5 字节固定帧发出 */ static void sonar_send(uint16_t dist_mm) { uint8_t buf[5]; buf[0] = 0xAA; /* 帧头 1,固定值 */ buf[1] = 0x55; /* 帧头 2,固定值 */ buf[2] = (uint8_t)(dist_mm & 0xFF); /* 距离低字节 */ buf[3] = (uint8_t)(dist_mm >> 8); /* 距离高字节,小端序 */ buf[4] = (uint8_t)(buf[0] + buf[1] + buf[2] + buf[3]); /* 累加和校验 */ HAL_UART_Transmit(&huart1, buf, sizeof(buf), 10); }

dist_mm是定时器捕获到 Echo 高电平宽度换算出来的毫米值。buf[4]的累加和是四个字节相加后取低 8 位,简单但够用,能挡住大部分因波特率抖动导致的错位。

提示:HAL_UART_Transmit的最后一个参数是超时毫秒数。填太小(比如 1)在高波特率下可能没发完就返回,表现为帧被截断。10ms 是安全值。

上位机侧,节点内部的解析逻辑等价于下面这段 Python。调试阶段我建议先用它直读串口,不经过 ROS,能快速判断问题在硬件还是节点:

import serial, struct def read_one(ser): """从串口读一帧,返回距离(毫米);超时返回 None""" buf = bytearray() while True: b = ser.read(1) if not b: return None buf += b if len(buf) >= 2 and buf[-2] == 0xAA and buf[-1] == 0x55: tail = ser.read(3) # 读满剩余 3 字节:距离2 + 校验1 if len(tail) < 3: return None frame = buf[-2:] + tail if (sum(frame[:4]) & 0xFF) != frame[4]: buf = bytearray() # 校验失败,丢弃重同步 continue dist_mm = struct.unpack('<H', frame[2:4])[0] return dist_mm

这段的关键是「边读边找帧头」。不能一次read(5),因为流里可能从半帧开始,一次读满反而永远对不齐。找到0xAA 0x55之后再读后面 3 字节,校验不过就清空重来。<H是小端无符号短整型,和 C 侧的dist_mm & 0xFF/dist_mm >> 8一一对应。

sum(frame[:4]) & 0xFF取低 8 位,和 C 侧的累加和算法一致。两边只要有一边写成了异或校验,就会出现「十帧里过一帧」的现象,因为大部分帧的累加和与异或值恰好相同。

确认 Python 脚本能稳定打出距离值之后,再启动 ROS 节点,问题范围就缩小到参数配置上了。

5. 避坑排查:五个把「没数据」拖成两天的坑

这一节记录的是我在这条链路上真实卡过的点。它们的共同特征是:不报错、不崩溃、日志干净,只是数据不对。按出现频率排序。

5.1 串口打不开与设备名漂移

现象:节点启动后日志打印open /dev/ttyUSB0 failed: Permission denied,或者上一次跑得好好的,这次报No such file or directory。

原因:前者是当前用户不在dialout组,或者 udev 规则没重载;后者是设备枚举顺序变了,ttyUSB0变成了ttyUSB1,节点里写死的路径失效。两种情况都不会影响编译,只在上电那一刻暴露。

解决:先用ls -l /dev/supersonic确认软链存在,再用groups | grep dialout确认权限。两个都正常还打不开,说明有别的进程占着串口,用sudo lsof /dev/supersonic查一下,常见的是上一次的rosrun没退干净,或者minicom之类终端没关。fuser -k /dev/supersonic可以强杀占用进程。

5.2 话题有数但值不对

现象:rostopic echo /sonar/range能看到消息,但range字段要么恒为 0,要么是 2.5 这种明显不对的数,或者拿尺子量 30cm、读出来是 300。

原因:恒为 0 通常是 Echo 引脚的电平问题。超声波模块的 Echo 输出是 5V,STM32 的 IO 是 3.3V,直连时高电平可能被判成无效,捕获不到脉宽。差 1000 倍则是单位没换算,sensor_msgs/Range的range字段单位是米,固件给的是毫米,scale参数漏配。

解决:电平问题用分压电阻(1k + 2k 串联取中间点)或者电平转换模块,别指望 STM32 的 IO 都能扛 5V。单位问题检查 launch 里的scale,毫米转米填 0.001。还有一种情况是定时器捕获分辨率不够,主频 72MHz 下 1us 对应 72 个计数,理论精度够,但如果预分频系数设大了,短距离会全部读成同一个值,这时候要回去看固件里的TIM_ICInitTypeDef。

5.3 编译、烧录与运行期的三个坑

现象一:catkin_make报Could not find "serial"。

原因:ros-noetic-serial没装,或者装了但当前终端没重新 source。

解决:apt install ros-noetic-serial之后,新开一个终端,source /opt/ros/noetic/setup.bash再编译。别在旧终端里试,环境变量是 source 那一刻的快照。

现象二:st-flash报verify failed,但writing阶段看起来正常。

原因:烧录时 UART 线还插在板子上,SWD 和 UART 引脚互相干扰;或者下载器的排线太长、接触不良。

解决:拔掉 USB 转 TTL 再烧。烧完接回去。还是失败就换根短一点的杜邦线,st-flash的时钟速率可以用--freq=1000k降到 1MHz 试试。

现象三:节点跑起来了,rostopic hz /sonar/range显示频率正常,但数值每隔几秒跳一次 0。

原因:超声波模块本身有最小测量周期,发得太快会出现余波干扰,读回 0。固件里如果 20ms 就触发一次,模块还没准备好。

解决:把触发间隔拉到 50ms 以上,10Hz 左右比较稳。这份固件默认的定时器周期如果偏短,改Makefile里的宏或者在main.c里调HAL_Delay都行,改完记得make clean再编。

6. 验证跑通:从上电到 RViz 出扇形,按这个顺序走一遍

前面所有配置做完,还需要一次完整的端到端验证。我的习惯是严格按顺序走,每一步都有明确的「通过标志」,不通过就不往下走。这样出问题时能立刻定位到是哪一段。

第一步,硬件上电。先插 USB 转 TTL,等两秒让系统枚举设备,再给 STM32 上电。顺序反了偶尔会出现串口能打开但收不到数据的情况,原因是板子启动时串口还没准备好。

第二步,确认设备和权限:

ls -l /dev/supersonic groups | grep dialout

能看到软链、能看到dialout,这两条过了再继续。

第三步,绕过 ROS 直读串口。用 4.3 那段 Python 脚本,或者更简单:

python3 -c " import serial s = serial.Serial('/dev/supersonic', 115200, timeout=2) print(s.read(20).hex()) "

期望输出是一串十六进制,里面能反复看到aa55。如果两秒超时、打出空字符串,问题在硬件侧:查 TX/RX 有没有接反、固件有没有真的跑起来(板子上电时 LED 有没有闪)、波特率对不对。

第四步,启动节点:

source ~/supersonic_ws/devel/setup.bash roslaunch supersonic_ros supersonic.launch

正常会打印串口打开成功的日志,然后开始循环解析。

第五步,看频率和内容:

rostopic hz /sonar/range rostopic echo -n 1 /sonar/range

hz期望在 10Hz 上下,波动不超过 ±2Hz。echo出来的一条消息里,range应该是米为单位的小数,radiation_type是 0(超声波),field_of_view跟你配的fov一致。拿尺子量一下模块到手掌的距离,比如 0.35m,range读数在 0.33 到 0.37 之间都算正常,误差主要来自声速随温度变化和模块本身的波束角。

第六步,RViz 看可视化:

rosrun rviz rviz

Fixed Frame 设成sonar_link,Add 一个 Range 显示,Topic 选/sonar/range。正常能看到一个随距离伸缩的扇形。扇形不出现但话题有数据,八成是frame_id对不上,RViz 会报Fixed Frame [sonar_link] does not exist。

检查点期望结果不通过时先查
/dev/supersonic软链存在udev 规则、lsusb
原始字节含aa55帧头接线、固件、波特率
话题频率约 10Hz固件触发周期
range数值与实测差 < 2cmscale、Echo 电平
RViz 扇形随距离伸缩frame_id、Fixed Frame

这套顺序的价值在于:任何一步失败,前面的步骤都已经排除了。不用在「是串口没开还是单位错了」之间反复猜。

从那以后我每次碰这种「ROS + 单片机 + 串口」的组合,都强制先跑一遍直读字节的脚本,确认硬件在说话,再去动 ROS 那一层。这个习惯帮我省掉的调试时间,比任何调试工具都多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询