ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

树莓派4B+STM32构建ROS机器人:上下位机通信与里程计融合实战

树莓派4B+STM32构建ROS机器人:上下位机通信与里程计融合实战 简介基于树莓派4B与STM32协同设计的ROS机器人完整项目包面向嵌入式、单片机方向的毕设、课设、竞赛及工程实训人群。内含完整源码、工程文件与说明文档可帮助快速复现机器人系统并支持在既有框架上扩展功能。压缩包共1102个文件以C源码、头文件、汇编与链接脚本为主辅以yaml/launch等ROS配置、STM32CubeMX的ioc工程及hex固件便于从底层驱动到上层ROS节点进行整体学习。包体大小28.29MB已有293人学习下载。对于硬件基础较弱的初学者还可参考描述中的面包板加杜邦线替代方案降低复刻门槛适合项目开发、毕业设计、课程作业与大创等场景。1. 从一块板子到一个会跑的ROS机器人中间缺的到底是什么树莓派4B加STM32这几乎是国内高校做ROS机器人课设、毕设和竞赛最经典的一套组合。硬件成本压得下来社区资料足够多而且两层架构恰好踩在嵌入式开发和机器人操作系统学习的交汇点上。但大多数人在这个项目上卡住不是因为某个芯片不会用而是因为“上下位机的职责边界”一开始就没划清楚树莓派上装了Ubuntu和ROS却不知道哪些节点该跑在派上哪些逻辑该下沉到STM32STM32写好了电机驱动却不知道怎么跟ROS端的话题和服务对接。这个标题里的zip文件拆开来看本质上就是一套已经跑通的最小系统——树莓派跑ROS做感知和决策STM32做底层运动控制和传感器采集两者通过串口或CAN通信。这篇文章会按这条主线从通信协议设计讲到里程计融合再到供电和调试技巧给出一套可以照着复现的方案。适合正在做课程设计、备战电赛或ROS竞赛、以及第一次把树莓派和STM32拼成移动底盘的工程师。2. 先想清楚为什么是树莓派4B STM32而不是单板直接干2.1 树莓派跑ROS的边界在哪STM32负责什么才算合理树莓派4B的CPU是四核Cortex-A72跑Ubuntu 20.04加ROS Noetic日常跑激光雷达驱动、导航栈和rviz可视化没有问题。但它的GPIO是致命的短板没有硬件PWM输出没有正经的编码器接口实时性更是完全没保证。如果直接在树莓派的GPIO上驱动两个直流电机你会发现PID控制周期抖动得厉害跑起来底盘一顿一顿的。这就是为什么业界默认把底盘控制下放给STM32而不是硬塞给树莓派。STM32F103C8T6或者F407系列在电机控制上几乎是教科书级的搭档TIM的编码器模式直接接AB相PWM输出配死区刹车ADC采样电池电压中断里做电流环或速度环实时性以微秒计。而树莓派这边跑ROS的master节点、激光雷达驱动、move_base、AMCL定位这些CPU密集型的任务才真正吃满它的性能。上下位机的划分核心原则是凡是需要硬实时的逻辑放STM32凡是需要Linux生态和ROS通信栈的逻辑放树莓派。我见过有人非要把IMU数据在STM32里跑完卡尔曼滤波再发给树莓派结果发现树莓派端做传感器融合反而更灵活因为ROS里现成的robot_localization包就是干这个的。千万别重复造轮子上下位机的信任边界一旦模糊后面的调试时间会翻倍。2.2 通信链路选型串口是默认答案但波特率和帧格式要自己定树莓派和STM32之间最常见的连接方式是UART串口。树莓派4B的40pin排针上物理引脚8是TX物理引脚10是RX对应/dev/ttyAMA0。这一代树莓派的mini UART和PL011 UART已经做了优化不再像3B那样锁频不稳所以直接用串口通信完全够用。波特率我一般选115200或者460800。115200在长线传输时抗干扰好460800适合需要高频发送里程计数据的场景。如果跑的是两轮差速底盘里程计数据频率20Hz、一个数据包大概20个字节115200完全富裕。但如果你要在STM32上发编码器原始值、IMU九轴数据再加目标速度控制字那20Hz×50字节就有点紧了建议直接上460800。帧格式不要用现成的协议库自己定一个简单的带校验的格式就行。网上不少项目直接裸发结构体这在实验室环境跑得通但一旦电机启动串口线上全是电磁干扰裸结构体大概率出乱码。我一般用这种帧格式字段长度说明帧头2字节0xAA 0x55用于找帧同步数据长度1字节有效载荷字节数数据类型1字节0x01速度控制0x02里程计反馈0x03IMU数据有效载荷N字节按具体类型解析浮点数转4字节小端CRC162字节从数据类型到有效载荷末尾的校验这种帧格式的好处是树莓派端不管收到什么垃圾数据只要扫描到0xAA 0x55就能重新对齐。CRC校验在10米线缆和电机启停的恶劣环境下也就损失一点点CPU换来的是定位数据不会突然跳变。2.2.1 树莓派端串口权限和映射的坑树莓派跑Ubuntu Server时用户默认不在dialout组里直接开串口会报Permission denied。常见做法是把当前用户加进dialout组或者写一个udev规则把串口映射成固定名称。# 将用户加入dialout组重新登录生效 sudo usermod -aG dialout $USER # 查看串口设备 ls -l /dev/ttyAMA0如果多次插拔USB转串口模块比如CP2102或者CH340设备名会在ttyUSB0和ttyUSB1之间跳。写一个udev规则固定在/dev/ttyRobotsudo nano /etc/udev/rules.d/99-robot.rules内容写一行其中idVendor和idProduct通过lsusb查你的USB转串口芯片KERNELttyUSB*, ATTRS{idVendor}1a86, ATTRS{idProduct}7523, MODE:0666, SYMLINKttyRobot保存后执行sudo udevadm control --reload-rules重新插拔设备以后代码里就固定写/dev/ttyRobot。别小看这一步串口设备名漂移是竞赛现场最容易把你心态搞崩的问题之一。2.2.2 数据协议怎么定义才能让ROS端解析最省事定义数据载荷的时候尽量贴近ROS消息类型。速度控制字直接从geometry_msgs/Twist取线速度x和角速度z转成两个float32发下去里程计反馈则按odom消息的位姿和速度填充。这样树莓派端写解析节点时几乎就是一个memcpy加字节序转换的事。有一个细节需要提前约好float的字节序。x86和ARM都是小端STM32也是小端但如果你中间插了一个串口转以太网的模块或者以后要跟大疆RoboMaster的裁判系统通信就要统一处理。我在协议里干脆就写死小端谁不服谁转。3. 树上装ROS环境最小化安装跑通第一帧激光雷达3.1 不要用树莓派桌面版Ubuntu Server是更优解很多树莓派ROS教程推荐直接烧录官方Ubuntu Desktop镜像打开终端敲安装命令。这么做的代价是桌面环境吃掉CPU和内存编译一个navigation相关的包能跑到80度。我建议用Ubuntu Server 20.04.5 LTS装完ROS Noetic之后如果你想要可视化直接树莓派上跑roscore和激光雷达驱动在你自己电脑上跑rviz并设置ROS_MASTER_URI连过去。安装ROS Noetic不要用国内某些教程里的半残脚本直接按官方步骤配置好镜像源之后安装ros-noetic-desktop。这个版本包含rqt、rviz和常用可视化工具但不带gazebo省几个GB的磁盘空间# 换清华源后更新 sudo apt update sudo apt upgrade -y # 安装完整版ROS包含rviz/rqt/slam库 sudo apt install ros-noetic-desktop -y # 初始化rosdep sudo rosdep init rosdep updaterosdep init如果报错多半是网络问题可以用rosdepc替代这是小鱼的一键安装工具集里的一个组件功能一致pip install rosdepc sudo rosdepc init rosdepc update装完这些在.bashrc里加上source /opt/ros/noetic/setup.bash。接下来验证ROS能跑起来# 终端1 roscore # 终端2 显示节点列表 rosnode list能输出/rosout就说明环境OK。这一步走通之后再装自己需要的功能包。3.2 雷达驱动编译与串口权限问题如果你的底盘没用激光雷达而是纯视觉方案这节可以跳过但绝大多数课设和竞赛都选的是思岚A1或者A2激光雷达。它们走串口或USB树莓派上需要编译slamtec的驱动包。使用catkin_make还是catkin build取决于你是否装了catkin_tools这里用传统方式mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src git clone https://github.com/Slamtec/rplidar_ros.git cd ~/catkin_ws catkin_make source devel/setup.bash连接雷达后先排除权限问题# 查看雷达对应的串口 ls -l /dev/ttyUSB* # 临时授权 sudo chmod 666 /dev/ttyUSB0然后启动雷达roslaunch rplidar_ros view_rplidar_a1.launch如果你看到rviz里没有点云先检查雷达的绿灯是否常亮再确认串口权限。这里注意不扫描帧数据的代码只解决驱动能不能起来的问题。能起来之后再接线到下一章。3.3 STM32端串口收发代码的骨架STM32这边我用的是标准外设库加HAL库混合写。初始化USART2PA2/PA3波特率460800开启空闲中断和接收中断。接收用DMA加空闲中断这是嵌入式里解析不定长帧最舒服的方式。// 串口接收缓冲区 uint8_t rx_buf[128]; uint8_t frame_buf[64]; uint8_t frame_len 0; uint8_t frame_ready 0; // 配置USART2波特率460800 void MX_USART2_UART_Init(void) { huart2.Instance USART2; huart2.Init.BaudRate 460800; huart2.Init.WordLength UART_WORDLENGTH_8B; huart2.Init.Parity UART_PARITY_NONE; huart2.Init.StopBits UART_STOPBITS_1; HAL_UART_Init(huart2); __HAL_UART_ENABLE_IT(huart2, UART_IT_IDLE); HAL_UART_Receive_DMA(huart2, rx_buf, sizeof(rx_buf)); } // DMA空闲中断回调 void HAL_UART_IdleCpltCallback(UART_HandleTypeDef *huart) { if (huart huart2) { uint16_t len sizeof(rx_buf) - __HAL_DMA_GET_COUNTER(hdma_usart2_rx); if (len 5) { memcpy(frame_buf, rx_buf, len); frame_len len; frame_ready 1; } __HAL_UART_CLEAR_IDLEFLAG(huart2); HAL_UART_Receive_DMA(huart2, rx_buf, sizeof(rx_buf)); } }代码里有个细节__HAL_DMA_GET_COUNTER拿到的是DMA还剩下的空间用缓冲区总长减去它才是实际接收到的数据长度。DMA接收完成回调HAL_UART_RxCpltCallback在循环缓冲区模式下不会触发所以必须用空闲中断来判断一帧数据结束。STM32的USART空闲中断在帧间隔大于一个字节时间时触发正好适应我们这种不定长协议。4. 上下位机联调让ROS收到第一个整数4.1 写一个串口节点包别用rosserial现成的用rosserial_arduino或者rosserial_stm32确实能省事它把订阅、发布都映射成协议栈看起来很美。但物尽其用一个道理懒得动脑的方案将来调试的时候会加倍还回来。rosserial生成的代码里面带了它自己的帧协议和掉线重连逻辑一旦出问题你很难定位是它内部状态机的问题还是你业务逻辑的问题。自己写一个串口节点三五十行数据帧格式和协议完全掌控在自己手里。创建功能包cd ~/catkin_ws/src catkin_create_pkg robot_serial roscpp std_msgs geometry_msgs nav_msgs源码写在src/serial_node.cpp里核心逻辑是打开串口、读写数据、发布里程计话题、订阅速度话题。串口读写用的是Linux原生termios库不是boost.asio因为自带的更轻量没有回调地狱。打开串口的配置函数是这样#include fcntl.h #include termios.h #include unistd.h int open_serial(const char* port, speed_t baud) { int fd open(port, O_RDWR | O_NOCTTY | O_NDELAY); if (fd 0) { ROS_ERROR(无法打开串口 %s, port); return -1; } struct termios options; tcgetattr(fd, options); cfsetispeed(options, baud); cfsetospeed(options, baud); options.c_cflag | (CLOCAL | CREAD); options.c_cflag ~CSIZE; options.c_cflag | CS8; options.c_cflag ~PARENB; options.c_cflag ~CSTOPB; options.c_cflag ~CRTSCTS; options.c_iflag ~(IXON | IXOFF | IXANY); options.c_lflag ~(ICANON | ECHO | ECHOE | ISIG); options.c_oflag ~OPOST; tcsetattr(fd, TCSANOW, options); tcflush(fd, TCIOFLUSH); return fd; }注意几个关键点c_cflag里的CLOCAL和CREAD必须开否则串口会监视调制解调器状态线导致open阻塞CRTSCTS必须关否则树莓派的UART引脚上没有CTS/RTS信号收发会直接卡住。c_lflag关闭ICANON避免串口按行缓冲把数据吞掉。这些配置少一个联调的时候就会出现“树莓派能发不能收”或者“程序卡在open不往下走”的妖孽现象。4.2 发布里程计格式要按nav_msgs/Odometry的规范来里程计话题是底盘跟导航栈交互的接口。树莓派端收到STM32发来的编码器计数换算成线速度和角速度填充进nav_msgs/Odometry消息同时查tf树发布odom到base_footprint的变换。很多课设的项目这一步做得很潦草或者干脆不发tf导致后面跑AMCL或者move_base的时候直接报错。nav_msgs::Odometry odom; odom.header.stamp ros::Time::now(); odom.header.frame_id odom; odom.child_frame_id base_footprint; // 位置由编码器里程计推算 odom.pose.pose.position.x x_pos; odom.pose.pose.position.y y_pos; odom.pose.pose.orientation.z sin(yaw / 2.0); odom.pose.pose.orientation.w cos(yaw / 2.0); // 速度由底盘运动学换算 odom.twist.twist.linear.x vx; odom.twist.twist.angular.z vz; odom_pub.publish(odom); // 发布tf变换 geometry_msgs::TransformStamped odom_tf; odom_tf.header.stamp ros::Time::now(); odom_tf.header.frame_id odom; odom_tf.child_frame_id base_footprint; odom_tf.transform.translation.x x_pos; odom_tf.transform.translation.y y_pos; odom_tf.transform.rotation odom.pose.pose.orientation; tf_broadcaster.sendTransform(odom_tf);这里的x_pos、y_pos和yaw是STM32端对编码器脉冲进行累计积分的结果。STM32每20ms发一次编码器原始值树莓派端负责把它积分成位置这样一旦串口偶尔丢一帧位置只是短暂不更新不会累积错误。相反如果STM32端直接积分再发位置丢一帧就永远少一块整个导航彻底没法用。4.3 订阅/cmd_vel直接转换成底盘运动学下行链路是ROS端发布geometry_msgs/Twist到/cmd_vel话题serial_node订阅后解析线速度和角速度打包通过串口发给STM32。STM32收到后按差速运动学反解左右轮目标速度再进PID控制器闭环。void cmdVelCallback(const geometry_msgs::Twist::ConstPtr msg) { float linear_x msg-linear.x; float angular_z msg-angular.z; // 差速运动学解算wheel_base是两轮间距单位米 float v_left linear_x - angular_z * wheel_base / 2.0; float v_right linear_x angular_z * wheel_base / 2.0; // 打包成协议帧左轮速度右轮速度各4字节float // 帧头 0xAA 0x55 长度 0x09 类型 0x01 数据8字节 CRC16 2字节 uint8_t frame[15]; frame[0] 0xAA; frame[1] 0x55; frame[2] 9; frame[3] 0x01; memcpy(frame 4, v_left, sizeof(float)); memcpy(frame 8, v_right, sizeof(float)); uint16_t crc crc16(frame 3, 9); frame[13] crc 0xFF; frame[14] crc 8; write(serial_fd, frame, 15); }wheel_base这个参数要量准别拿尺子量两轮中心距就算完。实际运行时轮胎打滑、地盘形变都会让等效轮距和理论值有偏差后期用轨迹对比法校准让机器人原地旋转360度观察odom的yaw变化量如果大于或小于2π按比例修正wheel_base的数值。STM32端收到这个帧后在定时中断里做速度闭环控制。PID参数整定有个推荐做法先调P让电机不抖再加上I消除静态误差D一般用不上除非你的电机响应特别慢或者底盘轻到一加速就震荡。我习惯把速度环频率设在50Hz和STM32的PWM频率分开PWM是20kHz速度环是50Hz中断触发PID计算然后更新占空比。5. 底盘动起来但跑不直编码器数据融合与轮径校准5.1 编码器读数不等于真实距离你需要校准轮径新组装的底盘接上电往前跑大概率发现两米直线实际跑成了1.85米。原因是轮子的理论直径和实际有效直径不一样胎压、负载和地面材质都会改变每转对应的实际行进距离。校准方法是做直线测试让机器人以固定PWM或固定目标速度走一段已知距离比如3米记录编码器累计脉冲数。STM32端把编码器计数发给树莓派用ROS里的rostopic echo直接抓到数据rostopic echo /odom看position.x的增量和真实3米做比值把轮径修正系数写进参数服务器或者STM32的宏定义里。这样校准之后里程计的scale factor误差能压到1%以内。5.2 转向不灵光轮距和角速度积分怎么调只校准轮径还不够。原地旋转测试中机器人实际转过的角度和odom里报出来的角度往往有5%到10%的偏差。这个偏差来源基本集中在两个地方一是轮距设置不准确二是左右轮直径不一致导致的差速误差。我一般做两组实验第一组原地旋转记录IMU的角度真值跟odom的yaw对比按比例修正wheel_base第二组直线往返分别前进和后退对比x的正负距离是否一致如果后退更远说明左右轮有效直径的均值偏大或者有反向间隙。这两个参数调完之后用robot_localization做EKF融合时输入数据的噪声协方差可以设置得更自信。5.3 IMU参与航向修正但不要无脑融合如果你的底板上装了IMU比如MPU6050或ICM20602建议把IMU数据用DMP读取四元数再转成yaw角通过串口发给树莓派。在robot_localization里配置odom和imu两个输入源odom负责位置和线速度imu负责航向和角速度。配置dual_filter还是single_filter要根据你的发送频率设计建议直接参考官方示例写一个ekf_localization_node的launch文件。一个常见踩坑是IMU的yaw角在机器人上电瞬间没有归零导致EKF把初始朝向当成0度底盘实际朝向和地图坐标系差了90度甚至更多。解决方法是在陀螺仪初始化完成后连续采样100次取平均减去零偏再把yaw强制设为0。注意这个零偏必须在电机不转的时候采样电机一启动电磁干扰会让陀螺仪零偏漂移好几个量级。6. 竞赛现场最容易翻车的三个细节供电、时间戳和波特率6.1 电源拓扑树莓派和舵机、电机必须分开供电树莓派4B的官方建议是5V 3A USB-C供电。但如果你的机器人上同时有电机驱动模块、舵机和激光雷达共用一个电源会让树莓派在电机启动瞬间掉电压轻则WiFi断连重则SD卡文件系统损坏。正确做法是12V锂电池给电机驱动板驱动板自带5V/5A的BEC输出给树莓派舵机直接吃12V或者独立5VSTM32开发板从树莓派的5V引脚取电或者独立降压模块。所有地线在电源输入端单点汇接避免形成地环路。6.2 时间戳对不上cartographer建图锯齿状建图时如果发现地图边缘有锯齿、回环检测疯狂报错大概率是时间戳问题。ROS里所有传感器数据的header.stamp必须用传感器的实际采集时间不是节点收到数据那一刻的墙钟时间。树莓派没有硬件RTC模块时每次开机时间会重置成1970年如果不同节点的时间基准不一致tf和时间同步就会乱掉。在没有外部网络的环境下建议在启动脚本里用chrony做一次软件校时或者接受时间漂移但保证所有节点用的是同一个ros::Time::now()来源。实战中最高效的做法是给树莓派加一个DS3231 RTC模块几块钱i2c接口配置好之后时间永远准。6.3 串口通信波特率不一致程序假死的排查顺序如果树上节点正常启动、STM32也正常跑但rostopic echo /odom就是没有任何数据先用逻辑分析仪或者示波器量一下UART TX/RX引脚的活动状态。没有示波器就听树莓派TX引脚接一个无源蜂鸣器能听到嘶嘶的噪声说明有数据在发送那就是波特率或者电平不匹配。使用ch340或者CP2102这类USB转串口模块时有一个隐蔽的坑模块上的TXD/RXD是3.3V TTL电平树莓派的GPIO也是3.3V兼容。但如果你用的是某宝几块钱的MAX3232模块模块上做了电平转换输出是RS232电平±12V直接怼到树莓派GPIO上会烧毁引脚。排查方法很简单测模块供电电压是3.3V还是5VTTL模块贴片芯片一般是MAX3232或SP3232RS232模块则有大块头的DB9接口。6.4 终极验证用rqt_graph和plotjuggler看整条链路联调完成后用rqt_graph看所有节点的话题连接关系确认serial_node和move_base之间的连线没有断开。再用plotjuggler订阅/odom的twist.linear.x和/cmd_vel的linear.x绘制两条曲线对比正常情况应该是cmd_vel先变化odom延迟几十毫秒后跟随。如果odom响应滞后超过200毫秒检查STM32端速度环的PID周期是不是被其他中断阻塞了。这个验证方法特别适用于答辩演示前快速定位是底盘的锅还是算法的锅省得在评委面前反复重启节点。本文还有配套的精品资源点击获取
返回列表