
树莓派 ROS2 控制 STM32 小车从串口通信到 cmd_vel 速度控制完整方案做机器人小车最常见的一套低成本组合就是“上位机树莓派 下位机 STM32”。树莓派负责跑 ROS2、处理摄像头、跑导航算法STM32 负责采集编码器数据、驱动电机、执行底层运动控制。两者之间通过串口通信一来一回就构成了一台标准的 ROS2 移动机器人。这次我们直接聊一个能落地的方案树莓派上安装 Ubuntu 22.04 ROS2 HumbleSTM32 端用 HAL 库写串口协议解析和电机 PID两边通过自定义通信协议对接。树莓派发布/cmd_vel话题STM32 收到线速度和角速度后换算成左右轮目标转速再把编码器里程计数据通过串口发回树莓派树莓派发布/odom话题供 Rviz2 显示或 Nav2 导航使用。这套方案写清楚之后你可以把它拆到任意一款 STM32 小车和任意树莓派型号上。下面按“能不能跑、怎么搭、怎么测、怎么排错”的顺序展开。1. 核心能力速览先看这张表确认这套方案解决什么问题、需要什么硬件。本文不绑定具体小车型号只讲通用架构和关键移植点。能力项说明系统架构树莓派为 ROS2 上位机STM32 为运动控制下位机通信方式串口 UART树莓派 GPIO 上的/dev/ttyAMA0或/dev/ttyUSB0与 STM32 USART 互联ROS2 版本Ubuntu 22.04 对应 ROS2 Humble这是当前资料最多、最稳的版本下位机主控STM32F103C8T6 / STM32F407VET6 均可HAL 库工程开发核心功能订阅/cmd_vel速度话题、发布/odom里程计话题、串口透传自定义协议帧里程计来源STM32 读取定时器编码器模式下的 AB 相脉冲换算轮速后累加是否支持批量任务不涉及批量任务属于实时控制链路重点在通信稳定与实时性是否提供接口 API串口本身即接口通过 ROS2 话题接入 Nav2、Rviz2、SLAM 工具适合场景本科毕设、ROS2 机器人入门、基于树莓派的小型移动底盘二次开发这套架构的好处是树莓派性能不够时Nav2 的代价地图计算、路径规划依旧可以跑STM32 硬件实时性好PWM 输出和编码器采集不会因为 Linux 调度抖动而失步。两边各干各擅长的活这也是工业机器人里常用的上下位机分工方式。2. 适用场景与使用边界先说明这不是“树莓派直接驱动电机”那种 GPIO 点灯级玩法也不是“STM32 单独跑 PID”的裸机玩法而是把两个平台串成一套完整 ROS2 机器人系统。这套方案适合以下读者正在做机器人毕设需要树莓派和 STM32 之间的数据交换想研究 ROS2 的cmd_vel如何真正驱动一台物理小车需要在 ROS2 中做 SLAM 建图、路径规划但手头只有一个 STM32 底盘希望从零搭一套可复用的小车通信协议而不只是复制别人家的整包代码。这套方案不擅长处理的事如果你只想让 STM32 小车原地转圈不需要 ROS2那直接写裸机代码更省事如果你想做自动驾驶级别的多传感器融合和复杂规划树莓派 4B 会显得吃力建议换成 Jetson Orin Nano 或工控机如果你完全没接触过 Linux 和 STM32 工程建议先把串口收发跑通再说 ROS2。使用边界与合规提醒涉及 STM32 端底层代码和自建通信协议时不要直接复制网上不明来源的驱动代码到生产环境尤其在电机驱动、电池供电的底盘上做测试时要确保舵机或电机驱动板有电流保护。树莓派与 STM32 之间用串口互联注意共地不要用 5V 电平直连 STM32 的 3.3V 引脚。如果后续接入摄像头、麦克风做语音交互或视觉识别涉及他人肖像、声音、隐私数据时必须获得相应授权并控制在本地测试环境内使用。3. 系统架构与通信协议设计整体框图如下树莓派 4B (Ubuntu 22.04 ROS2 Humble) | | 串口 UART | STM32 (HAL 库 编码器模式 PID 串口中断) | |--- 左轮电机 (PWM DIR) | |--- 右轮电机 (PWM DIR) | |--- 左右编码器 (定时器 AB 相)树莓派和 STM32 之间传输的两种关键消息树莓派 → STM32线速度v和角速度wSTM32 → 树莓派左右轮编码器累计值或当前里程计数据。3.1 ROS2 端数据结构ROS2 的/cmd_vel话题类型是geometry_msgs/msg/Twist包含线速度linear.x和角速度angular.z。我们需要在自己的 ROS2 节点中订阅它再通过串口把这两个 float 值打包发出去。3.2 STM32 端数据结构STM32 不需要解析 ROS2 消息它只认自己的串口协议帧。所以帧格式设计要足够简单、可靠。一般推荐下面这种帧结构帧头 0xAA 0x55 | 数据长度 | 数据域 | 校验和其中数据域的内容通常是左轮目标速度 float32单位 m/s 右轮目标速度 float32单位 m/s也可以直接传v和w然后在 STM32 里做差速换算。差速轮式底盘常见的换算公式左轮速度 v_left v - w * L / 2 右轮速度 v_right v w * L / 2其中L是左右轮距单位米。树莓派端负责发布cmd_vel把线速度和角速度消息留给 STM32 端由用户自己按底盘实际参数换算或者在上位机直接换算好两种做法都可以关键是协议里要注明单位。3.3 树莓派和 STM32 的串口连通方式树莓派 GPIO 上的 UART默认会被 Linux 的串口调试占用如果你使用树莓派 3B/4B/5、且直接使用 GPIO 14/15 对应的/dev/ttyAMA0通常需要在/boot/config.txt中加入enable_uart1 dtoverlaydisable-btdisable-bt不是必需的但如果你不用蓝牙可以关闭蓝牙串口映射让/dev/ttyAMA0是纯粹的 GPIO UART。如果接了 USB 转 TTL那设备名通常会是/dev/ttyUSB0。STM32 端用 USART1 或 USART2 均可配置为 115200 波特率8 数据位1 停止位无校验。如果直接用 USB 转 TTL 模块就需要把模块的 TX 连接到 STM32 的 RX 引脚模块的 RX 连接到 STM32 的 TX 引脚并且共地。4. 环境准备与前置条件以下环境先准备好后面实际操作时不用再中断。4.1 树莓派端环境树莓派型号选择上推荐树莓派 4B 或 5官方系统选择Ubuntu Server 22.04或树莓派官方 64 位系统两者均可。如果选择 Ubuntu Server 22.04ROS2 直接装 Humble# 设置编码 sudo apt update sudo apt install locales sudo locale-gen en_US en_US.UTF-8 sudo update-locale LC_ALLen_US.UTF-8 LANGen_US.UTF-8 export LANGen_US.UTF-8 # 添加 ROS2 源 sudo apt install software-properties-common sudo add-apt-repository universe sudo apt update sudo apt install curl -y sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg echo deb [arch$(dpkg --print-architecture) signed-by/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release echo $UBUNTU_CODENAME) main | sudo tee /etc/apt/sources.list.d/ros2.list /dev/null # 安装 ROS2 Humble 桌面版 sudo apt update sudo apt upgrade -y sudo apt install ros-humble-desktop python3-colcon-common-extensions桌面版会带 Rviz2这是调试时很有用的可视化工具。如果只想跑最小节点可以安装ros-humble-ros-base不过建议直接装桌面版。装完后激活环境echo source /opt/ros/humble/setup.bash ~/.bashrc echo source /usr/share/colcon_cd/function/colcon_cd.sh ~/.bashrc source ~/.bashrcROS2 的 Python 串口库也需要装sudo apt install python3-pip pip3 install pyserial4.2 STM32 端环境STM32 开发环境使用 Keil MDK 或 STM32CubeIDE二选一即可。对于 ROS2 小车项目推荐直接用 STM32CubeMX 生成 HAL 库工程省去手动配置寄存器的工作量。打开 STM32CubeMX需要配置以下外设外设配置项用途USART1 或 USART2异步模式波特率 115200开启中断与树莓派通信定时器 TIM2/TIM3/TIM4编码器模式AB 相读取左右轮编码器通用定时器例如 TIM1/TIM8PWM 输出频率 10kHz 至 20kHz驱动直流电机GPIO若干引脚配置为输出控制电机方向 DIR生成工程后在main.c中开启串口接收中断并实现协议解析。这里要注意 HAL 库串口中断接收有长度上限建议自己做一个环形缓冲区否则数据一长就会丢帧。4.3 硬件连接清单树莓派 4B 或 5带电源STM32 开发板常见如 STM32F103C8T6 蓝色药丸板USB 转 TTL 模块也可以用 TTL 电平直接接树莓派 UART直流减速电机 × 2带霍尔编码器或光电编码器电机驱动板例如 TB6612、L298N、DRV8833锂电池组或 DC 电源注意电机驱动和逻辑电压分开供电或做好共地接线示意树莓派 TXD (GPIO14) - USB转TTL RX 树莓派 RXD (GPIO15) - USB转TTL TX GND - GND USB转TTL TX - STM32 USART1 RX (PA10) USB转TTL RX - STM32 USART1 TX (PA9) GND - GND使用 USB 转 TTL 时/dev/ttyUSB0就能被识别也省去了树莓派自身 UART 被系统占用的麻烦。调试时更推荐这种方式确定功能正常后再换成 GPIO 直连。5. STM32 端代码实现下面给出一组 STM32 端的关键代码框架这里用的是 HAL 库加 STM32CubeMX 生成工程的方式。5.1 通信帧解析与处理STM32 的作用逻辑可以分成两部分接收树莓派发来的速度指令回传编码器脉冲数据和速度状态。定义一组简单的帧结构#define FRAME_HEAD1 0xAA #define FRAME_HEAD2 0x55 #define FRAME_DATA_LEN 8 // 两个 float // 数据域定义 typedef struct { float linear_x; // 线速度 m/s float angular_z; // 角速度 rad/s } TwistData; // 接收状态机 typedef enum { STATE_IDLE 0, STATE_HEAD1, STATE_HEAD2, STATE_LEN, STATE_DATA, STATE_CHECK } ReceiveState;在接收处理函数中把解析到的linear_x和angular_z换算成左右轮速度void Robot_SetSpeed(float linear, float angular) { float v_left linear - angular * ROBOT_WHEEL_BASE / 2.0f; float v_right linear angular * ROBOT_WHEEL_BASE / 2.0f; Motor_SetLeftSpeed(v_left); Motor_SetRightSpeed(v_right); }ROBOT_WHEEL_BASE是左右轮距单位米实际调试时用尺子量一下两个轮子接地点的距离。5.2 编码器读取与里程计计算编码器配置为定时器的编码器模式后每产生一个脉冲计数寄存器会自增或自减。MCU 的计数器通常会溢出所以要想办法处理这个溢出。常见做法是直接读取htim-Instance-CNT然后判断正负方向再累加到全局计数值中。STM32F1 系列只有 16 位定时器计数范围只有 -32768 到 32767所以不能等溢出后再处理要用高频定时中断不断读取。int16_t enc_left_raw __HAL_TIM_GET_COUNTER(htim3); int16_t enc_right_raw __HAL_TIM_GET_COUNTER(htim4); // 清空计数器 __HAL_TIM_SET_COUNTER(htim3, 0); __HAL_TIM_SET_COUNTER(htim4, 0); // 累加 g_left_encoder_total enc_left_raw; g_right_encoder_total enc_right_raw;轮速计算需要知道减速比和编码器线数假设电机减速比为 30:1编码器为 13 线4 倍频后每圈脉冲数为单圈脉冲数 减速比 x 编码器线数 x 4 30 x 13 x 4 1560 轮子直径 0.065m 轮子周长 pi x 0.065 0.2042m 每脉冲位移 0.2042 / 1560 0.0001309 m这些参数决定了里程计数据的准确度实测时可以根据跑动距离微调。编码器位置变化量和轮速差速模型放在一起就能计算出小车的位姿(x, y, yaw)。STM32 向树莓派回传数据时可以按固定周期发送最简单的是把左右轮编码器的累计值打包树莓派端再算里程也可以直接在 STM32 中算好里程计把x、y、yaw发给树莓派。推荐第二种因为 STM32 是实时系统计算频率更稳定。回传的位姿帧数据域结构typedef struct { float pos_x; // m float pos_y; // m float pos_yaw; // rad float vel_left; // m/s float vel_right; // m/s } OdometryData;5.3 控制周期与 PIDSTM32 端最好使用一个固定频率的控制定时器比如 100Hz 或 200Hz。定时器中断里执行读取编码器计数计算左右轮平均转速PID 计算输出到 PWM定期向树霹派发送里程计。PID 控制没必要一开始就写太复杂先用简单的增量式 PD 就能跑直。先弄通串口和电机方向让小车能前进后退再去调 PID。6. ROS2 树莓派端节点实现树莓派端的任务有两个订阅/cmd_vel把线速度和角速度通过串口发给 STM32读取 STM32 回传的里程计数据发布成/odom话题。6.1 创建 ROS2 功能包进入你的工作空间mkdir -p ~/ros2_ws/src cd ~/ros2_ws/src ros2 pkg create robot_bridge --build-type ament_python --dependencies rclpy geometry_msgs nav_msgs sensor_msgs这个功能包就是专门做“ROS2 话题和串口数据之间的翻译”的。6.2 串口发送端节点在robot_bridge/robot_bridge/cmd_vel_to_serial.py写入如下代码#!/usr/bin/env python3 import rclpy from rclpy.node import Node from geometry_msgs.msg import Twist import serial import struct class CmdVelToSerial(Node): def __init__(self): super().__init__(cmd_vel_to_serial) self.serial_port serial.Serial( port/dev/ttyUSB0, baudrate115200, timeout0.1 ) self.subscription self.create_subscription( Twist, /cmd_vel, self.cmd_vel_callback, 10 ) self.get_logger().info(cmd_vel_to_serial node started) def cmd_vel_callback(self, msg): linear_x msg.linear.x angular_z msg.angular.z self.send_speed(linear_x, angular_z) def send_speed(self, linear_x, angular_z): data struct.pack(ff, linear_x, angular_z) frame b\xAA\x55 bytes([len(data)]) data checksum sum(frame[2:]) 0xFF frame bytes([checksum]) self.serial_port.write(frame) self.get_logger().debug(fSend speed: v{linear_x:.2f}, w{angular_z:.2f}) def main(argsNone): rclpy.init(argsargs) node CmdVelToSerial() try: rclpy.spin(node) except KeyboardInterrupt: pass finally: node.destroy_node() rclpy.shutdown() if __name__ __main__: main()6.3 串口接收端节点在robot_bridge/robot_bridge/serial_to_odom.py中写入里程计发布节点#!/usr/bin/env python3 import rclpy from rclpy.node import Node from nav_msgs.msg import Odometry from geometry_msgs.msg import Quaternion import serial import struct import math class SerialToOdom(Node): def __init__(self): super().__init__(serial_to_odom) self.serial_port serial.Serial( port/dev/ttyUSB0, baudrate115200, timeout0.1 ) self.odom_pub self.create_publisher(Odometry, /odom, 10) self.timer self.create_timer(0.1, self.read_serial_data) self.get_logger().info(serial_to_odom node started) def read_serial_data(self): if self.serial_port.in_waiting 20: data self.serial_port.read(20) if data[0] 0xAA and data[1] 0x55: # 这里根据实际帧格式解析 # 假设数据域为 4 个 float共 16 字节 pos_x, pos_y, pos_yaw struct.unpack(fff, data[4:16]) self.publish_odom(pos_x, pos_y, pos_yaw) def publish_odom(self, x, y, yaw): odom_msg Odometry() odom_msg.header.stamp self.get_clock().now().to_msg() odom_msg.header.frame_id odom odom_msg.child_frame_id base_link odom_msg.pose.pose.position.x x odom_msg.pose.pose.position.y y odom_msg.pose.pose.orientation.z math.sin(yaw / 2.0) odom_msg.pose.pose.orientation.w math.cos(yaw / 2.0) self.odom_pub.publish(odom_msg) def main(argsNone): rclpy.init(argsargs) node SerialToOdom() try: rclpy.spin(node) except KeyboardInterrupt: pass finally: node.destroy_node() rclpy.shutdown() if __name__ __main__: main()上方代码是框架性示例帧长度的解析需要和你在 STM32 端定义的完全一致。可以先定义 STM32 向树莓派发送 18 或 20 字节的帧然后逐个字段解包。这个收发的两边一致性恰恰是最容易出 bug 的地方。7. 小车 URDF 模型与 Rviz2/ Nav2 衔接树莓派端如果需要做可视化调试需要建立一个小车 URDF 模型保证base_link、odom坐标系关系正确。在robot_bridge/urdf目录下新建一个简单的描述文件?xml version1.0? robot namestm32_car link namebase_link visual geometry box size0.2 0.15 0.05/ /geometry /visual /link link nameleft_wheel/ link nameright_wheel/ joint nameleft_wheel_joint typecontinuous parent linkbase_link/ child linkleft_wheel/ origin xyz0.1 0.08 0/ axis xyz0 1 0/ /joint joint nameright_wheel_joint typecontinuous parent linkbase_link/ child linkright_wheel/ origin xyz0.1 -0.08 0/ axis xyz0 1 0/ /joint /robot这个模型可以在 Rviz2 中显示小车形状方便观察。更完整的模型还包括laser_link、camera_link等传感器位置用于在 Rviz2 中查看传感器数据。如果打算接上激光雷达跑 SLAM 和 Nav2就需要引出laser话题到 Tree/Nav2 配置中。此时还需要用到robot_localization包来做轮式里程计和 IMU 的融合这里先不展。8. 功能测试与效果验证系统搭好后按以下顺序验证不要跳步。8.1 第一步串口通信直连测试STM32 上电后用一根 USB 转 TTL 接好树莓派。树莓派查看串口ls -l /dev/ttyUSB0设置权限sudo chmod 666 /dev/ttyUSB0先在树莓派上用echo直接发一帧测试数据# 帧结构0xAA 0x55 0x08 8字节数据 校验和 printf \xAA\x55\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xBE /dev/ttyUSB0如果 STM32 的串口中断和校验逻辑正常可以在调试串口或屏幕上看到树莓派发来的速度值。这一步成功了再往下走 ROS2 部分。8.2 第二步ROS2 话题测试先运行串口桥接节点cd ~/ros2_ws colcon build --packages-select robot_bridge source install/setup.bash ros2 run robot_bridge cmd_vel_to_serial然后再开一个终端用命令行发布速度ros2 topic pub /cmd_vel geometry_msgs/msg/Twist {linear: {x: 0.1}, angular: {z: 0.0}}预期结果STM32 端两个电机开始缓慢转动如果是差速底盘且w0两个轮子速度方向一致如果只发angular.z0.2那左右轮速度相反、小车原地旋转。8.3 第三步里程计话题测试启动串口接收节点ros2 run robot_bridge serial_to_odom之后再开一个终端用ros2 topic echo /odom查看里程数据ros2 topic echo /odom --once预期结果手动推着小车前进 1 米pose.position.x有增加把小车原地旋转 90 度orientation.z的绝对值接近 0.707。如果里程数值明显偏大或偏小多是因为轮径、减速比、编码器线数、轮距这四个参数设置有偏差。可以实测推车走 1 米对照里程数据反推一个修正系数来校准。8.4 第四步Rviz2 可视化验证分别启动robot_state_publisher和 Rviz2ros2 run robot_state_publisher robot_state_publisher --ros-args -p robot_description:$(cat ~/ros2_ws/src/robot_bridge/urdf/car.urdf) rviz2在 Rviz2 左侧的Fixed Frame设为odom然后添加RobotModel和Odometry显示项。如果坐标系正确小车模型会跟随cmd_vel指令移动并且模型姿态和真实小车一致。如果 Rviz2 里小车旋转方向相反说明 STM32 端回传的yaw符号反了将轮距符号取反或检查转向的正负约定即可。9. 接口 API 与多节点集成这套系统里没有传统意义上的 Web API但可以把它理解成一组 ROS2 接口可以通过话题订阅发布控制指令也可以将串口节点封装成 ROS2 service用来发送特殊指令比如“开启循迹”、“回零”等。在实际工程中键盘控制、手柄控制和 Nav2 路径规划都属于发布/cmd_vel的节点。可以利用 ROS2 的 QoS 和 Priority 机制把多个控制器节点接到同一个话题上但要注意同一时刻只能有一个活跃的cmd_vel发布者。一个可选的实用功能在 STM32 串口协议中增加控制字让树莓派可以切换“手动控制”和“自动控制”模式。STM32 默认不响应cmd_vel直到收到“进入自动模式”的指令后再启用速度控制。这能避免小车一开机就乱跑。批量任务层面这类小车并没有图像生成或批量文件处理的需求但在做路径点巡检时可以在树莓派上写一个循环发布目标点列表的程序等待小车到达后再发布下一个点。下面示例是用actionlib的方式来控制小车的路径点执行而不用每个点都手动发话题#!/usr/bin/env python3 import rclpy from rclpy.node import Node from geometry_msgs.msg import Twist import time class WaypointExecutor(Node): def __init__(self): super().__init__(waypoint_executor) self.cmd_pub self.create_publisher(Twist, /cmd_vel, 10) def exec_path(self): waypoints [(0.5, 0.0), (0.0, 0.5), (-0.5, 0.0)] for x, y in waypoints: self.move_to(x, y) time.sleep(1.0) def move_to(self, x, y): # 这里只做简单演示实际应用需要结合里程计闭环 msg Twist() if x 0: msg.linear.x 0.15 else: msg.linear.x -0.15 self.cmd_pub.publish(msg) time.sleep(x / 0.15 if x ! 0 else 0.5) msg.linear.x 0.0 self.cmd_pub.publish(msg)需要明确一点真正走直线不能只靠“定时开”。你至少要用里程计做闭环否则左右车轮差异会很快把路线带偏。需要快速落地上位机效果可以使用turtlesim或者gazebo但需要把机器人的 URDF 模型导入到 Gazebo 中。10. 资源占用与性能观察本方案涉及的资源占用主要来自树莓派上的 ROS2 进程。一般小车运行两个串口节点、一个robot_state_publisher、一个 Rviz2 时树莓派 4B 的内存占用大约为 1.5GB 到 2GB。如果只是运行节点不做可视化内存占用会小很多。显存树莓派不涉及 GPU 显存。但如果连接摄像头跑 YOLOv5 等视觉模型就要考虑 NPU 或 GPU 加速问题。树莓派 5 的 VideoCore VII GPU 在边缘推理中的表现有限如果要做目标检测建议先用轻量模型在 CPU 上做推理或考虑外接 Google Coral TPU/Hailo 加速器。CPU 占用ROS2 的 Python 节点不做复杂计算时CPU 占用很低。一旦跑 Rviz2、Nav2、SLAMCPU 会迅速上升。树莓派 4B 跑 Cartographer 已经很吃力但跑slam_toolbox2D LiDAR SLAM 还能接受。串口负载115200 波特率下理论传输速率为约 11.5KB/s。如果你用 20Hz 频率发送 20 字节的里程计帧流量大约为20 x 20 x 20 8000B/s不到串口带宽的 70%足够。但如果再把日志、调试信息、遥控数据都塞进同一条串口就会造成拥堵建议调试期用第二个串口输出日志发布版不要保留日志打印。降低资源占用建议1. 控制周期改为 10Hz 到 20Hz40Hz 的里程计在小车场景中通常足够 2. Rviz2 只在调试时开启平时运行不启动可视化 3. Nav2 中使用 2D 代价地图不要开 3D 占用栅格 4. ROS2 日志级别设为 WARN减少终端彩色 log 输出 5. ser2net 或 docker 等额外服务尽量不装到树莓派上 6. echo /odom 输出会明显拉高 CPU使用 ros2 topic hz 验证频率即可不要长期 echo。11. 常见问题与排查方法问题现象可能原因排查方式解决方案树莓派找不到/dev/ttyUSB0USB 转 TTL 驱动未加载lsusb看设备 iddmesg看内核日志重新插拔在 Ubuntu 上安装对应驱动如ch341、cp210x串口打开失败Permission denied当前用户无串口权限ls -l /dev/ttyUSB0执行sudo usermod -aG dialout $USER注销后再试STM32 接收到的数据乱码波特率不一致检查两端115200和时钟频率统一波特率如果用的外部晶振不是 8MHz检查 CubeMX 时钟配置STM32 能收不能发USART TX 引脚冲突或重映射未配置检查 CubeMX 引脚分配正确配置GPIO_AF和复用功能必要时用逻辑分析仪测引脚树莓派发速度 STM32 无反应帧头校验不对或 STM32 中断未开STM32 调试串口打印状态检查帧头0xAA 0x55是否被转义或串口工具自动加换行关闭串口工具的换行发送一个轮子转另一个不转电机驱动板的 PWM/方向引脚接错或 PID 目标符号反换输入测试用 GPIO 点灯方式逐个测试电机驱动引脚确认使能引脚初始化小车不走直线左右电机参数不一致空转测试左右轮速是否一致左右 PID 参数分开考虑使用两个独立 PID允许左右增益不同里程计数据乱跳编码器 A/B 相接反或共地问题用手拨动轮子、观察累计值是增是减调整编码器 A/B 相在定时器上的接线或软件取反cmd_vel话题有数据但小车不动节点没有启动或 QoS 不匹配用ros2 topic info /cmd_vel查看发布者和订阅者把发布端和订阅端的 QoS 都设为SensorDataQoS或SystemDefaultQoSSTM32 复位后树莓派还在发旧速度树莓派节点缓存了上次的速度值查看代码是否在启动时空发(0,0)发送节点启动时先向底盘发布零速度帧里程计和实际方向相反yaw符号约定不一致推车转向观察/odom的z值变化在所有下发速度上取反或调整差速轮速度公式补充一下 QoS 问题这在 ROS2 Foxy 之后的版本里是个常见坑。默认的cmd_vel话题是 reliable 传输如果 Nav2 发送端也是 reliable两者通常能匹配。但部分 SLAM 工具发布/odom或/scan时会用 sensor data 的 best effort 策略如果你的订阅端是 reliable两者就不匹配会一直收不到数据。解决办法是统一设置为from rclpy.qos import qos_profile_sensor_data self.subscription self.create_subscription( Odometry, /odom, self.callback, qos_profile_sensor_data )排查这个问题时可以用ros2 topic info /odom --verbose看发布端和订阅端的 QoS 是否 Match。另外如果小车使用的是树莓派 5默认串口和树莓派 4B 在引脚定义上保持一致但 Ubuntu 24.04 上默认可能启用的是ttyAMA10这类新串口名称做 GPIO 直连时可以先在/boot/firmware/config.txt中确认uart_2ndstage或dtoverlayuart0的配置然后使用ls /dev/ttyAMA*查看实际设备名。12. 针对常见热词的四类问题解答很多读者在搜索框里会输“树莓派 ros2 安装 stm32”这里把常见的情况直接归类回答一下。**第一类“树莓派装 Ubuntu 22.04 后 ROS2 怎么装更稳”最常见的问题出在源上。树莓派官方源在国内更新很慢先换清华源或阿里源再装 ROS2速度完全不同。清华源配置 Ubuntu 22.04 参考sudo sed -i s//.*archive.ubuntu.com//mirrors.tuna.tsinghua.edu.cng /etc/apt/sources.list sudo apt update如果在树莓派 5 上用 Ubuntu 24.04ROS2 Jazzy 的安装步骤和 Humble 类似。要装哪个 ROS2 版本先确认 Ubuntu 版本不要跨版本安装。**第二类“树莓派 Ubuntu Server 没有桌面Rviz2 跑不了怎么办”远程可视化方案在本地电脑上安装 Ubuntu Desktop ROS2 Humble将树莓派的 ROS2 节点通过 DDS 分布式通信连到本地。或者用ssh -X做 X11 转发但 Rviz2 在 X11 转发下会比较卡。如果在树莓派上装 Ubuntu Desktop 22.04直接跑 Rviz2 也可以但体验一般毕竟树莓派的 GPU 能力和内存容量有限开 Rviz2 后占用 CPU 较高。**第三类“STM32 和树莓派之间除了串口还能用什么通信”如果板子支持 USB OTG也可以用 USB CDC 虚拟串口通信速度更快且免驱。如果做无线遥控可以用两个 ESP32 透传模块树莓派端接一个 USB-TTL 到 ESP32STM32 端接另一个 ESP32延迟最低可以到几十毫秒级别但稳定性不如有线。需要注意的是一旦使用无线模式速度控制指令的延迟和丢包会影响 PID 调试建议先做好有线版本再改无线。**第四类“电机驱动的 PID 参数怎么调”这是一个看似简单、实则非常繁琐的过程。推荐步骤1. 只给左轮供电右轮悬空 2. 让左轮目标速度设为 0.1m/s 3. 先 P 由小到大加到出现轻微振荡再把 P 回退到 60%~70% 4. 加 I 消除稳态误差 5. 加极小 D 抑制超调但编码器噪声大时 D 不要太大 6. 左右轮重复上面步骤电机参数通常不完全对称允许 P/I 有一定差异。没有万能 PID 参数。换个电池电压、换个地面摩擦参数都会变。这是做小车项目最容易卡壳的地方。13. 最佳实践与使用建议把这段时间在树莓派、ROS2 和 STM32 三种环境下踩过的坑统一整理一份清单1. 先做串口回环测试再做 ROS2 话题测试再做空转测试最后才能上轮子落地 2. 通信协议帧结构要同时预留 2 字节帧头和 1 字节校验不要省校验字节 3. STM32 编码器计数使用定时器中断读取周期建议 5ms 或 10ms 4. 树莓派端的串口节点写成 systemd 服务开机自启避免断电后手动执行 5. 里程计话题发布频率要恒定建议 20Hz不要用循环里带 sleep 的方式 6. STM32 中默认在系统启动时禁止电机输出收到有效速度帧后再使能 7. 所有参数轮距、轮径、减速比、编码器线数集中在头文件中配置 8. 备用一个 USB 转 TTL 串口调试线这是排查 STM32 串口问题的利器 9. ROS2 日志可以使用 ros2 topic hz 检查话题频率确认节点是否正常 10. 使用树莓派 GPIO 直连串口时需要先在 Raspberry Pi 的 config.txt 中查找是否被蓝牙或别的模块占用 11. 把 STM32 的固件升级接口预留出来树莓派可以通过串口进入 bootloader方便远程升级 12. 整机测试时要注意电流防止电机堵转把板子烧掉有条件时加电流检测或保险丝。如果做 SLAM 导航树莓派 4B 建议只跑 SLAM 和导航不要在同一块板子上再跑目标检测模型和语音识别。一旦 CPU 占满里程计话题和激光雷达帧都会有明显延迟导航效果会立刻劣化。需要跑视觉模型时购买一个带 NPU 的协处理器模块或单独用一台 Jetson 做视觉节点把树莓派解放出来专做运动控制。13.1 关于使用场景的补充串口和引脚树莓派 5 相比树莓派 4B 在 GPIO 串口上有所调整新的 RP1 芯片让部分 UART 功能和引脚出现变化从 4B 迁移到 5 时看到/dev/ttyAMA0不存在或者设备名变化不要惊讶查看/boot/firmware/config.txt按树莓派 5 的设备树来配置即可。如果你还在用树莓派 3B/4B则直接沿用 GPIO14/15 的 UART 配置即可。STM32 端要注意接线电平。树莓派的 GPIO 是 3.3V TTL和大多数 STM32 开发板电平均为 3.3V可以直连。但如果用了 5V 的 Arduino 或 5V 单片机和 STM32 互联就一定要加电平转换模块否则 STM32 的引脚可能被烧。使用 L298N 驱动板时控制信号逻辑电平可能会被拉高到 5V最好加光耦隔离或使用 3.3V 逻辑兼容的驱动板。14. 总结与下一步“树莓派 ROS2 控制 STM32 小车”这类项目难的不是 ROS2 本身而是两块板子之间通信协议的一致性和差异的调试。先验证串口能不能收到帧再验证cmd_vel话题能不能驱动电机轮子最后验证里程计能不能闭环。先把小车调到“开机后让它原地转 90 度它能转到位”再考虑跑 SLAM 和 Nav2。下一步建议按这个顺序扩展在 STM32 端加一个循迹传感器灰度传感器树莓派端先不做处理直接让 STM32 输出避障标志即可在树莓派接入 2D 激光雷达使用slam_toolbox建一张室内地图接入 Nav2让/cmd_vel的发布源从键盘换成导航目标点继续加入视觉模型做红绿灯识别或标签跟随用树莓派摄像头或低成本 USB 摄像头都可以。如果第一次跑通你会发现“车能跑”和“车能不偏不差地跑”是两种难度。建议从一开始就注意轮径和轮距的实测标定这能省掉后期大量调整的时间。整个链路里最值得收藏的部分就是通信协议设计和里程计计算这套逻辑把它们抽象成通用模块换到其他小车上时基本可以直接复用。