树莓派4B与STM32的ROS小车:串口通信与运动控制实现详解
2026/9/16 13:07:10 网站建设 项目流程

简介:基于树莓派4B与STM32的ROS机器人项目资料包,面向嵌入式、单片机方向的毕业设计、课程设计、大作业、工程实训、大创项目及学科竞赛等场景,提供一套经过严格测试、可直接编译烧录运行的完整实现方案。压缩包内共一千一百零二个文件,以C语言源文件、头文件和汇编文件为主,另有Keil工程配置、ROS功能包(yaml/launch/xacro)、STM32CubeMX初始化文件、烧录hex文件与说明文档;内容覆盖底层驱动、传感器数据采集、电机控制、ROS节点通信、机器人建模与启动配置等关键模块,整体结构清晰,便于按模块检索和二次开发。项目代码运行稳定,若不会画电路板,可采用面包板加杜邦线连接外设模块的方式快速复现;资源包体积仅约28.29MB,已有293人学习参考,适合需要在有限时间内完成完整机器人项目的开发者借鉴使用,并可在此基础上扩展更多功能。

1. 树莓派4B与STM32的ROS小车:两级控制架构怎么分工才靠谱

树莓派4B跑ROS、STM32管电机闭环,这种上下位机拆分几乎是大赛小车和毕设作品的默认架构,但不少失败案例恰恰死在两片芯片之间的串口线上:协议随意、波特率对不上、里程计标定靠猜。这套基于树莓派4B与STM32的ROS机器人资源,提供完整源码、Keil工程与说明文档,STM32端含电机驱动、编码器读取和CMSIS-DSP信号处理,树莓派端负责ROS节点与运动控制。硬件不强制画板子,按引脚定义用面包板加杜邦线接好电机驱动和编码器模块,烧录源码即可复现。适合毕设、课程设计、大创和电子竞赛缺底盘的团队,也适合想搞懂上位机ROS与下位机实时控制怎么协作的嵌入式开发者。文章按通信协议、固件实现、ROS节点到整机标定这条线拆开。

2. 串口帧协议设计与STM32/树莓派通信实现

先把两片芯片的职责边界画清楚,再落串口帧格式和解析代码。很多人拿到现成工程直接烧录,改波特率只改树莓派一侧,结果交互全乱。协议先行,后续所有排查才有依据,这一步别省。

2.1 上下位机职责边界与选型依据

树莓派4B的Cortex-A72四核处理器跑Linux和ROS体量刚好,SLAM建图、路径规划、传感器融合这些重计算任务都放在这一层;缺点是Linux内核调度不可控,普通GPIO输出PWM抖动明显,做不了毫秒级电机闭环。STM32的Cortex-M内核配合定时器硬件PWM和编码器接口,速度环可以可靠跑在50到200Hz,中断响应是确定性的,这是树莓派替代不了的。

这套资源里出现了arm_rfft_init_f32.carm_dct4_init_q31.carm_common_tables.c这些CMSIS-DSP源文件,说明固件侧集成了ARM官方数学库,常见用途是传感器信号的频谱分析,第三章会展开。两片处理器的分工可以归纳为下表,后期加功能时先对号入座再动代码。

层次处理器职责实时性
决策层树莓派4BROS master、导航、建图、人机交互软实时
执行层STM32电机闭环、编码器计数、IO保护、底层传感器采集硬实时

2.2 串口帧格式与命令集设计

串口是上下位机之间唯一通信链路。默认115200波特率在50Hz上报频率下余量很大:每帧约13字节,按10bit折算一字节传输时间约87微秒,50Hz全双工也占不满2%带宽。所以初期调试用115200够用,等做高速循迹再把STM32和树莓派两侧同步提到460800,注意两侧必须一致。

帧格式采用帧头加命令加长度的结构,校验和覆盖命令字、长度和载荷,不包含帧头,这样解析时可以从任意字节开始同步。

偏移字段长度说明
0HEAD11固定0xAA
1HEAD21固定0x55
2CMD1命令字
3LEN1DATA字节数
4DATALEN载荷
末尾CHK1CMD+LEN+DATA取低8位

命令字按功能分组:0x01设置目标速度,载荷是两个float,线速度vx加角速度wz共8字节;0x02上报里程与轮速,载荷是vx、wz加左右轮转速共12字节;0x10是心跳查询,载荷为空。校验和计算规则是CHK = (CMD + LEN + sum(DATA)) & 0xFF,STM32和ROS节点必须用同一套算法,任何一侧算错都会整帧丢弃。

2.3 帧解析与校验实现

STM32端推荐用状态机解析串口字节流,而不是收到什么直接按位置取。串口是字节流,可能从帧中间开始接收,状态机在帧头同步之前会丢弃垃圾字节,收到完整帧后回到等待帧头状态,天然具备重新同步能力。

// STM32端串口帧解析状态机 typedef enum { ST_WAIT_H1, ST_WAIT_H2, ST_CMD, ST_LEN, ST_DATA, ST_CHK } FrameState; static FrameState state = ST_WAIT_H1; static uint8_t rx_cmd, rx_len, rx_sum, data_idx; static uint8_t rx_data[32]; void uart_byte_handler(uint8_t b) { switch (state) { case ST_WAIT_H1: if (b == 0xAA) state = ST_WAIT_H2; break; case ST_WAIT_H2: state = (b == 0x55) ? ST_CMD : ST_WAIT_H1; break; case ST_CMD: rx_cmd = b; rx_sum = b; state = ST_LEN; break; case ST_LEN: rx_len = b; rx_sum += b; data_idx = 0; state = (rx_len == 0) ? ST_CHK : ST_DATA; break; case ST_DATA: rx_sum += b; rx_data[data_idx++] = b; if (data_idx >= rx_len) state = ST_CHK; break; case ST_CHK: if (b == (rx_sum & 0xFF)) { process_frame(rx_cmd, rx_data, rx_len); } state = ST_WAIT_H1; break; } }

参数说明:rx_sum从CMD开始累加,变量命名对应帧格式的字段语义;process_frame按rx_cmd分发,0x01解析两个float给速度环,0x10回一个状态帧。注意LEN为0时直接跳到校验状态,避免空载荷帧卡死在数据阶段。

接收路径上,逐字节HAL_UART_Receive_IT回调在115200下每字节触发一次中断,对同时跑PID和编码器计数的MCU来说开销偏高。常见做法是串口DMA接收加空闲中断IDLE,CubeMX里配置UART的DMA循环接收,在HAL_UARTEx_RxEventCallback里拿到一包不定长数据后丢给状态机,CPU占用能降一半以上。

3. STM32固件实现:PWM、编码器与CMSIS-DSP滤波

固件侧的任务闭环是整车的底层保障:PWM输出驱动电机,编码器反馈测速,PID做速度闭环,CMSIS-DSP库负责传感器信号的频域分析。源码包的Keil工程文件结构比较典型,先梳理再改,避免误删编译依赖。

3.1 Keil工程文件梳理

工程根目录的contrl02.uvguix.62362是Keil uVision的GUI布局文件,记录窗口位置、断点和仿真器配置,换电脑后多半失效,删除后Keil会按默认重建,不影响编译。接下来的keilkilll.bat是清理编译中间产物的脚本,类似于下面这段行为:

@echo off del /s /q *.o *.d *.crf *.htm *.axf *.lnp *.sct rmdir /s /q Objects rmdir /s /q Listings

运行一次能把ObjectsListings目录下的.o、.d、.crf等文件清空。源码包在发布前跑过这类脚本,体积小很多;自己改代码后也建议跑一遍,避免旧目标文件造成改了代码不生效的假象。

CMSIS-DSP文件里,arm_common_tables.c是FFT/IFFT的位反转表和旋转因子表;arm_rfft_init_f32.c是实数FFT的初始化函数,float32版本;arm_dct4_init_q31.c是DCT4变换的q31定点初始化表;arm_linear_interp_data.c是插值查表数据。这些文件默认被编进工程,会增加编译时间。如果只用FFT,可以在编译选项里排除transform_tests_common_data.c等测试数据文件,Flash空间紧张时这一步能省出几十KB。

3.2 PWM与编码器接口配置

电机驱动接口按定时器通道划分,常见分配如下,具体引脚以原理图为准:

功能定时器通道参数
左轮PWMTIM1CH120kHz,PWM模式1
右轮PWMTIM1CH220kHz,PWM模式1
左轮编码器TIM2CH1/CH2编码器模式,x4倍频
右轮编码器TIM3CH1/CH2编码器模式,x4倍频

PWM频率由f_pwm = 72MHz / ((PSC+1)*(ARR+1))决定,20kHz处在人耳听觉范围之外,电机不会发出明显啸叫。如果所用驱动芯片开关频率上限偏低,降到10kHz再测。编码器模式在CubeMX里把TIM2配置为Encoder Mode,TIM2的CH1和CH2分别接编码器A相B相,__HAL_TIM_GET_COUNTER读到的值就是脉冲增量。换算线速度的公式:

// PPR为编码器每转脉冲数,ratio为减速比,wheel_d为轮径(米) float mm_per_pulse = (float)(PI * wheel_d) / (PPR * 4 * ratio); float left_speed = (float)encoder_cnt_delta * mm_per_pulse / dt;

参数说明:4倍频来自TI1和TI2两个输入的双沿计数。以减速比30:1、PPR=11的霍尔编码器为例,电机输出轴每转一圈产生11×4×30=1320个脉冲,dt取0.02s时测速分辨率约0.015m/s,闭环精度足够。encoder_cnt_delta要取两次读取之差而不是累计值,否则速度会随运行时间收敛到零。

3.3 速度环PID与增量式调节

速度环用增量式PID,输出的是PWM占空比增量,天然不需要保存绝对位置,掉电重启也不会跳变。

typedef struct { float kp, ki, kd; float integral, last_err, out; } Pid_t; // dt固定为0.02s,速度环50Hz float pid_step(Pid_t *p, float target, float current) { float err = target - current; p->integral += err * 0.02f; if (p->integral > 1000.0f) p->integral = 1000.0f; if (p->integral < -1000.0f) p->integral = -1000.0f; p->out = p->kp * err + p->ki * p->integral + p->kd * (err - p->last_err); p->last_err = err; return p->out; }

积分限幅是刹车的核心,不然堵转时积分会无限累加,恢复转动后产生巨大过冲。Kp从0.3起步,直线来回出现高频抖动就降Kp加Kd;Ki解决低速爬行静差,但过大时转弯回正会过冲。速度环周期必须固定,周期抖动会让PID参数整体失效。

差速模型从ROS速度指令换算左右轮目标速度:

float v_left = vx - wz * wheel_base / 2.0f; float v_right = vx + wz * wheel_base / 2.0f;

wheel_base是两个驱动轮中心距,单位米。这个参数标定不准会直接导致旋转半径错误,第五章会讲标定方法。

3.4 用CMSIS-DSP做共振频率扫描

整车装完,某个速度区间车体抖动,万用表量不出问题,常见做法是固定PWM占空比,采一段编码器速度或IMU加速度数据做FFT,找出频谱峰对应机械共振频率。

// 128点实数FFT,采样率500Hz,频率分辨率约3.9Hz #include "arm_math.h" #define FFT_N 128 #define FS 500.0f arm_rfft_fast_instance_f32 fft_inst; float32_t in[FFT_N], out[FFT_N]; void fft_analysis(const float32_t *sample) { arm_rfft_fast_init_f32(&fft_inst, FFT_N); memcpy(in, sample, FFT_N * sizeof(float32_t)); arm_rfft_fast_f32(&fft_inst, in, out, 0); // 0为正变换 uint16_t peak_bin = 1; float32_t peak_mag = 0; for (uint16_t i = 1; i < FFT_N / 2; i++) { float32_t mag = sqrtf(out[2*i]*out[2*i] + out[2*i+1]*out[2*i+1]); if (mag > peak_mag) { peak_mag = mag; peak_bin = i; } } // 峰值频率 = peak_bin * FS / FFT_N }

参数说明:arm_rfft_fast_f32的输出数组里DC和Nyquist分量的虚部为0,其余频点实部虚部交替存放,所以第i个频点的模取out[2*i]out[2*i+1]peak_bin乘FS/FFT_N得到实际频率。找到共振峰后,优先查机械固定和轮子动平衡,若改结构成本高,再用arm_biquad_cascade_df1_init_f32搭陷波滤波器滤掉该频段。FFT适合调试期做频谱扫描,不适合放进50Hz控制循环里逐帧跑。

提示:PID环路里只做简单低通的话,一阶IIR就够;FFT是找问题用的工具,不是控制链路的常驻算法。

4. 树莓派4B上的ROS节点:从环境搭建到cmd_vel闭环

树莓派4B这一侧重点在ROS节点编排和串口桥接。把系统装好、权限配好、桥接节点跑通,teleop_twist_keyboard一按方向键轮子就动,这条链路才算验证完成。

4.1 系统与串口权限准备

树莓派4B建议跑64位系统,Ubuntu Server 20.04配合ROS Noetic是常见组合;也有不少人用树莓派OS 64位加Docker跑ROS。社区里鱼香ROS一键安装脚本被大量教程采用,自动完成换源、依赖安装和ROS本体部署,适合不想在环境搭建上耗时间的场景,装完后再手动跑一遍rosdepcatkin_make即可。

ROS节点访问串口需要权限,默认用户不在dialout组里,打开/dev/ttyUSB0会报PermissionError:

sudo usermod -aG dialout $USER # 重新登录终端生效 ls -l /dev/ttyUSB0

USB转串口模块每次插入设备名可能漂移,用udev规则固定成稳定路径,以CH340芯片为例:

# /etc/udev/rules.d/99-robot-uart.rules SUBSYSTEM=="tty", KERNEL=="ttyUSB*", ATTRS{idVendor}=="1a86", \ MODE="0666", GROUP="dialout", SYMLINK+="robot_uart"

规则生效后执行sudo udevadm control --reload,重新插拔模块,/dev/robot_uart就会指向同一个串口。idVendor不确定时用lsusb查实际值。如果走树莓派GPIO 14/15的原生UART,还需要在config.txt加enable_uart=1并释放console占用,这个坑放在第五章排错部分。

4.2 串口桥接节点的实现

桥接节点订阅/cmd_vel,按第二章的帧格式打包下发给STM32,同时读取STM32的0x02上报帧,解包后发里程计。完整最小实现如下:

#!/usr/bin/env python3 # serial_bridge.py import rospy, serial, struct from geometry_msgs.msg import Twist from nav_msgs.msg import Odometry from tf import transformations from math import cos, sin class SerialBridge: def __init__(self): rospy.init_node('serial_bridge') self.ser = serial.Serial('/dev/robot_uart', 115200, timeout=0.05) rospy.Subscriber('/cmd_vel', Twist, self.on_twist) self.odom_pub = rospy.Publisher('/odom', Odometry, queue_size=10) self.x = self.y = self.th = 0.0 self.last_t = rospy.Time.now().to_time() def on_twist(self, msg): payload = struct.pack('<ff', msg.linear.x, msg.angular.z) head = bytes([0xAA, 0x55, 0x01, len(payload)]) chk = (0x01 + len(payload) + sum(payload)) & 0xFF self.ser.write(head + payload + bytes([chk])) def read_loop(self): rate = rospy.Rate(50) while not rospy.is_shutdown(): head = self.ser.read(4) if len(head) != 4 or head[0] != 0xAA or head[1] != 0x55: continue cmd, ln = head[2], head[3] data = self.ser.read(ln) if self.ser.read(1)[0] != (cmd + ln + sum(data)) & 0xFF: continue if cmd == 0x02: vx, wz, _ = struct.unpack('<fff', data) self.publish_odom(vx, wz) rate.sleep() def publish_odom(self, vx, wz): now = rospy.Time.now().to_time() dt = now - self.last_t self.last_t = now self.x += vx * cos(self.th) * dt self.y += vx * sin(self.th) * dt self.th += wz * dt odom = Odometry() odom.header.stamp = rospy.Time.now() odom.header.frame_id = 'odom' q = transformations.quaternion_from_euler(0, 0, self.th) odom.pose.pose.position.x = self.x odom.pose.pose.position.y = self.y odom.pose.pose.orientation.x = q[0] odom.pose.pose.orientation.y = q[1] odom.pose.pose.orientation.z = q[2] odom.pose.pose.orientation.w = q[3] odom.twist.twist.linear.x = vx odom.twist.twist.angular.z = wz self.odom_pub.publish(odom) if __name__ == '__main__': bridge = SerialBridge() bridge.read_loop()

参数说明:struct.pack('<ff'表示小端两个float,与STM32端float内存布局一致;校验和与第二章STM32解析算法完全对齐,任何一侧算错都会丢帧。read_loop的50Hz读取频率跟STM32上报频率对齐,漏帧会被校验和拦下。里程计用直线近似累积,dt取墙钟时间差而非固定假设,长时间运行不会因为计时漂移积累误差。

话题和TF的布局保持ROS标准命名,后续接navigation和map_server时不需要改节点名:

话题类型方向典型频率
/cmd_velgeometry_msgs/Twist树莓派→STM3210Hz
/odomnav_msgs/OdometrySTM32→树莓派50Hz
/tftf2_msgs/TFMessage桥接节点发布随odom

4.3 用teleop_twist_keyboard验证整条链路

环境里装好teleop,直接键盘控制验证通道完整性:

sudo apt install ros-noetic-teleop-twist-keyboard rosrun teleop_twist_keyboard teleop_twist_keyboard.py

按下方向键后,rostopic echo -n1 /cmd_vel能看到Twist消息;轮子不动时先查串口权限和校验和,再查STM32侧是否收到帧。在STM32的process_frame里加一个GPIO翻转的调试钩子,收到合法帧就翻转LED,能快速分辨是链路断了还是命令内容不对。rostopic hz /odom应稳定显示50Hz附近,频率掉一半先看串口拥堵和read超时。

5. 整机联调与里程计标定技巧

联调阶段按顺序来能省一半排查时间:先单独验证STM32侧开环PWM输出,再验证闭环测速,最后才把树莓派4B接入。上电顺序建议STM32先上电,树莓派4B后上电,电机电源独立供电。

树莓派4B对供电跌落很敏感,常见的红灯常亮绿灯不亮故障多半是5V被电机启动瞬间拉垮。用万用表量电机启动瞬间的5V电压,低于4.8V就务必把电机驱动电源和小板供电分开,共用电池也先过各自的稳压模块。

里程计标定按两步走。直线标定:发送固定vx直行3米,量横向偏差d。若偏右说明左轮实际轮径偏大,把左轮直径乘以修正系数(1 - d/L)再做一轮。旋转标定:原地旋转10圈,量实际角度θ_actual,里程计角度θ_odom,修正轮距:

L_cal = L_used * (θ_odom / θ_actual)

轮距偏小会让里程计算出的角速度偏大,表现在原地转圈时ROS说转了360度实际只转340度。修正后重复一次,偏差收敛到3度以内再继续导航。

如果走树莓派GPIO 14/15原生串口,Linux console默认占用这条线,ROS节点会收到乱码。在raspi-config里关闭serial login shell、保留hardware serial,等效于在config.txt写enable_uart=1,并清掉cmdline.txt里的console=serial0,115200。重启后执行cat /dev/ttyAMA0,不再弹登录提示而是静默等待,说明console已经把串口让给应用层了。

方向符号统一是最后一道坎:指令左转实际右转,先交换电机A/B相线;里程计显示倒退,把编码器AB相在CubeMX里对调或改计数方向。用rostopic echo -n1 /odomtwist.angular.z的正负号,再对照车体实际转向,两者相反就是符号约定不一致。调完符号再跑一遍直线和旋转标定,这组参数才能落到navigation里当初始值用。

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

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

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

立即咨询