ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

ROS移动机器人底盘控制:L298N与MPU6050的运动学建模与闭环校准

ROS移动机器人底盘控制:L298N与MPU6050的运动学建模与闭环校准

简介:本资源是一套基于ROS的智能移动机器人底盘控制系统完整开发项目,面向机器人方向初学者、嵌入式开发者及高校课程设计/毕业设计实践者,聚焦电机驱动、姿态感知、闭环控制与数据融合等核心能力训练。压缩包共405个文件,含76个C源文件(如inv_mpu.c、tasks.c、stm32f10x_tim.c等)、74个头文件(h)、73个目标文件(o)及73个编译中间文件(crf),覆盖STM32F103C主控平台的底层驱动、MPU6050传感器数据采集、PID运动控制逻辑与卡尔曼滤波融合算法实现;另含docx设计说明、txt使用指南及完整Git工程目录,便于理解系统架构与快速部署。资源包大小为14.45MB,结构清晰、模块分层明确,含可直接烧录的.axf固件及调试配置文件,支持从硬件驱动到运动学建模的全链路学习。目前已有66人学习下载,适合希望掌握ROS底层控制集成、多传感器融合与实时闭环调参的实践者。

1. 为什么用 ROS 控制移动机器人底盘,光靠 L298N + MPU6050 还远远不够?——一个被低估的“运动控制黑匣子”问题

你手头有一块带 L298N 驱动的双轮差速底盘、一块 MPU6050 姿态传感器,也写了 PID 调速和串口读取加速度计数据的代码。但一上电,机器人不是原地打转就是突然加速撞墙;调好直行后,转角误差越来越大;加了卡尔曼滤波,姿态角反而更抖……这不是你代码写得差,而是你正在面对一个典型的「运动控制黑匣子」:底层硬件信号链(PWM→电机响应→轮径偏差→编码器缺失→IMU安装偏移→坐标系错位)与上层控制逻辑(ROS 的 tf 树、/cmd_vel 解析、wheel_odom 推算、control_msgs 接口)之间,存在至少 7 层未显式建模的耦合关系。本项目标题里藏的不是“功能清单”,而是一条从物理层到算法层的完整闭环验证路径:L298N 不是简单开关,它是非线性死区+温漂+占空比-转速非线性映射的源头;MPU6050 不是直接输出欧拉角,它提供的是原始陀螺仪+加速度计数据,必须经坐标对齐、零偏校准、温度补偿后才能喂给卡尔曼;PID 和卡尔曼不是并列模块,而是嵌套结构——卡尔曼输出的姿态角用于修正运动学模型中的 yaw 偏差,该偏差又反向影响 PID 的速度指令分解。适合正在 ROS 小车项目中卡在“能动但不稳、能走但不准、能转但不直”的开发者,尤其适合已跑通 turtlebot3 demo 却无法复现自研底盘稳定运动的实践者。


2. 搭建最小可运行 ROS 底盘控制框架:从硬件接线到 /cmd_vel 订阅闭环

2.1 硬件层真实接线约束与 ROS 节点映射关系

L298N 并非即插即用模块。其 IN1/IN2 控制左轮正反转,ENA 接 PWM 信号;右轮同理(IN3/IN4 + ENB)。关键陷阱在于:L298N 的使能引脚(ENA/ENB)必须接支持硬件 PWM 的 GPIO(如树莓派 GPIO12/GPIO13),且频率需设为 1–5 kHz——低于 1 kHz 易听见电机啸叫并引发扭矩波动,高于 20 kHz 则因驱动芯片响应延迟导致有效占空比失真。MPU6050 的 SDA/SCL 必须接 I²C 总线(树莓派默认 GPIO2/3),且必须外接 4.7kΩ 上拉电阻(仅靠模块板载电阻常不足,导致 I²C 通信丢帧)。接线后,通过i2cdetect -y 1应看到地址0x68,否则检查供电(VCC 必须为 3.3V,非 5V!MPU6050 5V 供电会烧毁)和上拉。

# 验证 I²C 设备在线(树莓派端) i2cdetect -y 1 # 正常输出应含: # 0 1 2 3 4 5 6 7 8 9 a b c d e f # 00: -- -- -- -- -- -- -- -- -- -- -- -- -- # 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # 60: -- -- -- -- -- -- -- -- 68 -- -- -- -- -- -- -- # 70: -- -- -- -- -- -- -- --

提示:若i2cdetect无输出,先执行sudo raspi-config→ Interface Options → I2C → Yes 启用 I²C 内核模块,并确认/boot/config.txt中含dtparam=i2c_arm=on。

2.2 编写底盘驱动节点:解析 /cmd_vel 并生成 PWM 输出

ROS 中底盘控制的核心是订阅/cmd_vel(geometry_msgs/Twist 类型),将其线速度vx和角速度vyaw按差速模型分解为左右轮目标转速。不能直接将vx除以轮半径得到 RPM——必须引入实际轮径(实测值,非标称值)、轴距(两轮中心距)、电机减速比(如 1:45)、编码器线数(若后续加装)等参数。本项目暂无编码器,故采用开环速度模型,但需预留闭环接口:

# chassis_driver_node.py (Python3, ROS Noetic) #!/usr/bin/env python3 import rospy from geometry_msgs.msg import Twist import RPi.GPIO as GPIO import time # 硬件引脚定义(树莓派 BCM 编号) LEFT_IN1 = 17 # GPIO17 → L298N IN1 LEFT_IN2 = 27 # GPIO27 → L298N IN2 LEFT_ENA = 12 # GPIO12 (PWM) → L298N ENA RIGHT_IN3 = 22 # GPIO22 → L298N IN3 RIGHT_IN4 = 23 # GPIO23 → L298N IN4 RIGHT_ENB = 13 # GPIO13 (PWM) → L298N ENB # 物理参数(必须实测!单位:米) WHEEL_RADIUS = 0.0325 # 实测轮半径 3.25cm(标称 3.3cm 误差达 1.5%) AXLE_TRACK = 0.148 # 实测轴距 14.8cm(标称 15cm 误差 1.3%) MOTOR_RPM_PER_VOLT = 120.0 # 电机额定转速 120 RPM/V(查电机规格书) class ChassisDriver: def __init__(self): rospy.init_node('chassis_driver', anonymous=True) self.cmd_sub = rospy.Subscriber('/cmd_vel', Twist, self.cmd_callback) GPIO.setmode(GPIO.BCM) GPIO.setup([LEFT_IN1, LEFT_IN2, RIGHT_IN3, RIGHT_IN4], GPIO.OUT) GPIO.setup([LEFT_ENA, RIGHT_ENB], GPIO.OUT) # 初始化 PWM(频率 2kHz,占空比 0%) self.pwm_left = GPIO.PWM(LEFT_ENA, 2000) self.pwm_right = GPIO.PWM(RIGHT_ENB, 2000) self.pwm_left.start(0) self.pwm_right.start(0) # 电机方向控制初始状态 GPIO.output(LEFT_IN1, GPIO.LOW) GPIO.output(LEFT_IN2, GPIO.LOW) GPIO.output(RIGHT_IN3, GPIO.LOW) GPIO.output(RIGHT_IN4, GPIO.LOW) rospy.loginfo("Chassis driver node started. Waiting for /cmd_vel...") def cmd_callback(self, msg): # 差速模型:v_left = vx - (vyaw * axle_track / 2), v_right = vx + (vyaw * axle_track / 2) vx = msg.linear.x vyaw = msg.angular.z v_left = vx - (vyaw * AXLE_TRACK / 2.0) v_right = vx + (vyaw * AXLE_TRACK / 2.0) # 转换为 PWM 占空比(0–100%),考虑电机死区电压(实测 2.1V) # 公式:duty = (|v| * 100) / (max_rpm * wheel_radius * 2 * pi / 60) * (1 / motor_rpm_per_volt) max_linear_speed = MOTOR_RPM_PER_VOLT * WHEEL_RADIUS * 2 * 3.1416 / 60.0 # m/s duty_left = min(100.0, abs(v_left) / max_linear_speed * 100.0) if abs(v_left) > 0.02 else 0.0 duty_right = min(100.0, abs(v_right) / max_linear_speed * 100.0) if abs(v_right) > 0.02 else 0.0 # 设置方向 if v_left >= 0: GPIO.output(LEFT_IN1, GPIO.HIGH) GPIO.output(LEFT_IN2, GPIO.LOW) else: GPIO.output(LEFT_IN1, GPIO.LOW) GPIO.output(LEFT_IN2, GPIO.HIGH) if v_right >= 0: GPIO.output(RIGHT_IN3, GPIO.HIGH) GPIO.output(RIGHT_IN4, GPIO.LOW) else: GPIO.output(RIGHT_IN3, GPIO.LOW) GPIO.output(RIGHT_IN4, GPIO.HIGH) # 更新 PWM(注意:GPIO.PWM.ChangeDutyCycle() 有最小更新间隔,避免高频调用) self.pwm_left.ChangeDutyCycle(duty_left) self.pwm_right.ChangeDutyCycle(duty_right) if __name__ == '__main__': try: driver = ChassisDriver() rospy.spin() except rospy.ROSInterruptException: pass finally: GPIO.cleanup()

关键参数说明:

  • WHEEL_RADIUS和AXLE_TRACK必须用游标卡尺实测,误差超 0.5mm 就会导致 3 米直线行走偏移 >15cm;
  • MOTOR_RPM_PER_VOLT来自电机数据手册,若无手册,需在空载下用万用表测输入电压、用激光转速计测 RPM 反推;
  • duty_left/duty_right中的0.02是速度死区阈值(m/s),低于此值强制停机,避免 L298N 在低占空比下电机“蠕动”;
  • PWM 频率固定为 2kHz,是经实测在噪声、效率、响应速度间取得平衡的值(1kHz 啸叫明显,5kHz 以上 L298N 驱动能力下降)。

3. MPU6050 数据采集与预处理:从原始寄存器到可用姿态角的三重校准

3.1 原始数据读取与坐标系对齐

MPU6050 默认输出加速度计(±2g)和陀螺仪(±250°/s)原始值(16-bit 补码),但模块 PCB 上的 X/Y/Z 轴标签与 ROS 的base_link坐标系(X 前、Y 左、Z 上)不一致。实测发现:当模块平放时,Z 轴加速度读数为 +16384(对应 +1g),但 X/Y 轴读数非零——这是由于模块贴片时存在微小旋转。必须先用mpu6050_dmp库或寄存器直读方式获取原始数据,再进行坐标变换。本项目采用寄存器直读(绕过 DMP 固件,确保可控性):

# mpu6050_raw_reader.py import smbus import time import numpy as np class MPU6050: def __init__(self, bus_num=1, address=0x68): self.bus = smbus.SMBus(bus_num) self.address = address self._init_device() def _init_device(self): # 复位设备 self.bus.write_byte_data(self.address, 0x6B, 0x80) time.sleep(0.1) # 关闭睡眠模式,设置陀螺仪量程 ±250°/s,加速度计量程 ±2g self.bus.write_byte_data(self.address, 0x6B, 0x00) self.bus.write_byte_data(self.address, 0x1B, 0x00) # GYRO_CONFIG self.bus.write_byte_data(self.address, 0x1C, 0x00) # ACCEL_CONFIG def read_raw_data(self): # 读取加速度计(0x3B–0x40)和陀螺仪(0x43–0x48)共 12 字节 data = self.bus.read_i2c_block_data(self.address, 0x3B, 12) ax = self._twos_complement(data[0] << 8 | data[1]) # 加速度 X ay = self._twos_complement(data[2] << 8 | data[3]) # 加速度 Y az = self._twos_complement(data[4] << 8 | data[5]) # 加速度 Z gx = self._twos_complement(data[6] << 8 | data[7]) # 陀螺仪 X gy = self._twos_complement(data[8] << 8 | data[9]) # 陀螺仪 Y gz = self._twos_complement(data[10] << 8 | data[11]) # 陀螺仪 Z return np.array([ax, ay, az, gx, gy, gz], dtype=np.int16) def _twos_complement(self, val): if val >= 0x8000: return val - 0x10000 return val # 使用示例 mpu = MPU6050() while True: raw = mpu.read_raw_data() print(f"Raw: AX={raw[0]}, AY={raw[1]}, AZ={raw[2]}, GX={raw[3]}, GY={raw[4]}, GZ={raw[5]}") time.sleep(0.02) # 50Hz 采样

注意:read_i2c_block_data一次读 12 字节,避免多次 I²C 事务引入时序抖动;_twos_complement将 16-bit 补码转为 Python int。

3.2 三步校准:零偏、比例因子、安装偏移矩阵

原始数据不能直接用于姿态解算。必须完成:

  1. 陀螺仪零偏校准:静止 10 秒,取 GX/GY/GZ 均值作为零偏bias_gx,bias_gy,bias_gz;
  2. 加速度计比例因子校准:将模块分别以 +X、-X、+Y、-Y、+Z、-Z 方向静置,记录各方向 AX/AY/AZ 极值,计算每轴灵敏度(LSB/g)和零偏;
  3. 安装偏移矩阵:用三维空间几何法求解模块坐标系到base_link的旋转矩阵R_mpu_to_base。

校准后数据转换公式:

acc_calibrated = R_mpu_to_base @ [ (ax - bias_ax)/scale_ax, (ay - bias_ay)/scale_ay, (az - bias_az)/scale_az ]^T gyro_calibrated = [ (gx - bias_gx)*0.00763, (gy - bias_gy)*0.00763, (gz - bias_gz)*0.00763 ]^T # 0.00763 deg/s per LSB

其中0.00763是 ±250°/s 量程下的陀螺仪 LSB-to-deg/s 换算系数(250*2/65536)。这一步不可跳过——未经校准的 MPU6050 在 10 秒内姿态角漂移可达 ±5°,足以让底盘转向失控。


4. PID 闭环控制与卡尔曼滤波融合:为什么把卡尔曼放在 PID 外层才是正确打开方式

4.1 底盘运动学模型的显式构建与误差来源

差速底盘的运动学模型本质是:

[x_dot, y_dot, theta_dot]^T = [cosθ, -sinθ, 0; sinθ, cosθ, 0; 0, 0, 1] @ [vx_body, vy_body, vyaw]^T

但 ROS 中/cmd_vel的vx是 body-frame X 方向速度,vyaw是绕 Z 轴角速度。问题在于:theta(朝向角)的精度直接决定vx_body在 world-frame 的投影是否准确。而theta来自 MPU6050,其原始陀螺仪积分存在漂移,加速度计静态倾角解算受振动干扰。因此,单纯对vx做 PID(如e = vx_target - vx_measured)是无效的——vx_measured本身因theta错误而错误。正确做法是:用卡尔曼滤波融合 IMU 数据,输出高置信度theta和theta_dot,再将theta用于修正运动学模型中的坐标变换,最后对修正后的vx_world,vy_world做 PID。

4.2 卡尔曼滤波状态向量设计与观测方程

本项目采用简化的一维卡尔曼(仅估计 yaw 角),因其计算轻量且满足底盘主要需求(航向稳定):

  • 状态向量:X = [theta, theta_dot]^T(朝向角 + 角速度)
  • 预测方程:X_k = F * X_{k-1} + B * u_k + w_k,其中F = [[1, dt], [0, 1]],u_k为陀螺仪角速度测量值,B = [[0.5*dt^2], [dt]]
  • 观测方程:Z_k = H * X_k + v_k,其中H = [[1, 0]],Z_k为加速度计解算的静态倾角(仅用atan2(ay, ax)得 yaw,忽略 pitch/roll)
# kalman_yaw_filter.py import numpy as np class KalmanYawFilter: def __init__(self, dt=0.02): self.dt = dt # 状态向量 [theta, theta_dot] self.X = np.array([0.0, 0.0]) # 状态协方差 self.P = np.eye(2) * 0.1 # 过程噪声协方差(陀螺仪噪声主导) self.Q = np.diag([0.01, 0.1]) # 观测噪声协方差(加速度计静态解算噪声大) self.R = np.array([[0.5]]) # 状态转移矩阵 self.F = np.array([[1, self.dt], [0, 1]]) # 控制输入矩阵(u = gyro_z) self.B = np.array([[0.5 * self.dt**2], [self.dt]]) # 观测矩阵 self.H = np.array([[1, 0]]) def predict(self, gyro_z): # 预测:X_k = F * X_{k-1} + B * u_k u = np.array([gyro_z]) self.X = self.F @ self.X + self.B @ u self.P = self.F @ self.P @ self.F.T + self.Q def update(self, acc_yaw): # 观测:Z = acc_yaw Z = np.array([acc_yaw]) # 卡尔曼增益 S = self.H @ self.P @ self.H.T + self.R K = self.P @ self.H.T @ np.linalg.inv(S) # 更新状态 y = Z - self.H @ self.X self.X = self.X + K @ y self.P = (np.eye(2) - K @ self.H) @ self.P # 使用流程(在主循环中) kf = KalmanYawFilter(dt=0.02) while True: raw = mpu.read_raw_data() # 校准后得到 gyro_z (deg/s) 和 acc_yaw (rad) gyro_z = (raw[5] - bias_gz) * 0.00763 # deg/s acc_yaw = np.arctan2(raw[1], raw[0]) # rad,粗略 yaw kf.predict(gyro_z) kf.update(acc_yaw) current_yaw = kf.X[0] # 卡尔曼输出的最优 yaw 角(rad) # 将 current_yaw 传入运动学模型,修正 /cmd_vel 分解

为什么卡尔曼必须在外层?

  • 若将卡尔曼用于滤波vx,则忽略了theta误差对vx投影的根本影响;
  • 卡尔曼输出theta后,可实时修正base_link到odom的 tf 变换,使整个导航栈(AMCL、move_base)获得准确位姿;
  • 本方案中卡尔曼仅 2 维,CPU 占用 < 1%,而全状态 6DOF 卡尔曼在树莓派上会卡顿。

5. 避坑指南:L298N + MPU6050 在 ROS 底盘中踩过的 5 个血泪坑

5.1 现象:机器人直行时缓慢向右偏移,调 PID 参数无效

原因:L298N 左右通道 ENA/ENB 引脚的 PWM 占空比响应非线性不一致。实测发现:当设定 40% 占空比时,左轮实际电压为 3.8V,右轮为 4.1V(因 PCB 走线阻抗差异)。更隐蔽的是,L298N 内部 H 桥 MOSFET 的导通电阻随温度升高而增大,导致运行 2 分钟后右轮扭矩下降 12%。
解决:在chassis_driver_node.py中为左右轮增加独立的 PWM 补偿系数。通过激光测距仪测量 1 米直线行走偏移量,反推补偿值。例如:duty_right_compensated = duty_right * 0.975(实测值),并将该系数写入 YAML 参数文件,由 ROS Parameter Server 加载。

5.2 现象:MPU6050 在机器人启动瞬间输出异常大加速度(AX > 50000)

原因:L298N 电机启停时产生强电磁干扰(EMI),通过共享电源地线耦合进 MPU6050 的模拟电路。MPU6050 的 VDDA(模拟供电)与 VDDD(数字供电)虽分离,但共地设计使其易受干扰。
解决:

  1. 为 MPU6050 单独铺设 3.3V 电源线(不与电机共用);
  2. 在 MPU6050 的 VDDA 引脚就近并联 10μF 钽电容 + 100nF 陶瓷电容;
  3. I²C 信号线(SDA/SCL)加 100Ω 串联电阻(靠近 MPU6050 端);
  4. ROS 节点中对加速度计数据做滑动窗口中值滤波(窗口大小 5),剔除脉冲干扰。

5.3 现象:卡尔曼滤波后 yaw 角在静止时仍缓慢漂移(>0.5°/min)

原因:未对陀螺仪零偏做温度补偿。MPU6050 的陀螺仪零偏随温度变化约 0.01°/s/°C。实验测得:室温 25°C 时零偏为bias_gz = 12.3,升温至 35°C 后变为bias_gz = 18.7。
解决:启用 MPU6050 的片上温度传感器(寄存器0x41–0x42),每 5 秒读取一次温度T,建立线性补偿模型:bias_gz_compensated = bias_gz_25C + 0.01 * (T - 25)。将补偿后零偏送入卡尔曼预测步骤。

5.4 现象:/cmd_vel发布 0.2 m/s 后,底盘实际速度仅 0.15 m/s,且随电池电压下降持续降低

原因:L298N 的输出电压 = 输入电压 × 占空比 × 效率,而效率随负载电流增大而下降。当电池从 7.4V(满电)降至 6.8V(欠压),相同占空比下电机端电压下降 8%,导致转速下降。
解决:在底盘驱动节点中加入电压反馈闭环。用 ADC 读取电池电压V_bat,动态调整MOTOR_RPM_PER_VOLT:MOTOR_RPM_PER_VOLT_ADJ = MOTOR_RPM_PER_VOLT * (V_bat / 7.4)。需外接分压电阻(如 10kΩ+10kΩ)将 0–8.4V 映射到 ADC 0–3.3V。

5.5 现象:ROS 中rosrun rqt_graph rqt_graph显示/cmd_vel订阅者正常,但底盘无响应

原因:树莓派的robot_state_publisher与tf树配置错误。若base_link到imu_link的静态变换未发布,或imu_link的frame_id在 IMU 驱动节点中硬编码为"imu"而非"imu_link",则robot_localization包无法订阅到 IMU 数据,进而导致ekf_localization_node不发布odom,最终move_base拒绝执行/cmd_vel(因无 odom 信息)。
解决:

  1. 确认imu_driver_node中Imu消息的header.frame_id = "imu_link";
  2. 在urdf文件中定义<joint name="imu_joint" type="fixed">并设置<origin xyz="0 0 0.05" rpy="0 0 0"/>(IMU 安装高度 5cm);
  3. 启动时运行rosrun robot_state_publisher robot_state_publisher并加载 URDF。

6. 运动学模型参数在线辨识:用 3 分钟实测数据自动拟合轮径与轴距

6.1 为什么出厂参数永远不准?一个真实案例

某开发者使用标称轮径 33mm、轴距 150mm 的底盘,在 ROS 中设置diff_drive_controller的wheel_separation为 0.15,wheel_radius为 0.033。结果:发布/cmd_vel线速度 0.3 m/s,实测 10 秒后里程计显示位移 2.85m,但激光雷达 SLAM 地图显示实际位移仅 2.62m,误差达 8.8%。根源在于:橡胶轮在负载下发生形变,有效滚动半径小于自由半径;铝制底盘加工公差导致轴距偏差;电机减速箱齿轮间隙引入运动滞后。

6.2 在线辨识方法:基于最小二乘的运动学参数估计

核心思想:让底盘沿已知长度直线(如 2 米卷尺)匀速行驶,同步记录/odom的pose.pose.position.x和实际位移s_true,构建方程:

s_true = k_r * s_odom_x + k_t * s_odom_theta

其中k_r为轮径缩放因子,k_t为轴距缩放因子。通过多组不同速度、不同距离的测试,收集(s_odom_x, s_odom_theta, s_true)数据,用最小二乘求解k_r,k_t。

# online_calibration.py import rospy from nav_msgs.msg import Odometry import numpy as np from scipy.optimize import least_squares class OnlineCalibrator: def __init__(self): self.odom_sub = rospy.Subscriber('/odom', Odometry, self.odom_callback) self.s_odom_list = [] self.s_true_list = [] self.theta_odom_list = [] self.is_collecting = False def start_collection(self, true_distance): """true_distance: 实际行走距离(米),如 2.0""" self.s_true_list.append(true_distance) self.is_collecting = True rospy.loginfo(f"Start collecting odom data for {true_distance}m...") def odom_callback(self, msg): if not self.is_collecting: return x = msg.pose.pose.position.x y = msg.pose.pose.position.y # 简化:只取 x 方向位移(要求直线行走) s_odom = np.sqrt(x**2 + y**2) self.s_odom_list.append(s_odom) # 记录最终位移 if len(self.s_odom_list) > 100: # 约 2 秒数据 self.s_odom_list = self.s_odom_list[-1:] # 只留最后一个 self.is_collecting = False def fit_parameters(self): """拟合 k_r, k_t""" if len(self.s_odom_list) < 2: rospy.logwarn("Not enough data!") return None # 构建设计矩阵 A 和观测向量 b # s_true = k_r * s_odom + k_t * theta_odom A = np.column_stack([self.s_odom_list, self.theta_odom_list]) b = np.array(self.s_true_list) # 最小二乘求解 x_sol = np.linalg.lstsq(A, b, rcond=None)[0] k_r, k_t = x_sol[0], x_sol[1] rospy.loginfo(f"Calibration result: k_r = {k_r:.4f}, k_t = {k_t:.4f}") return {"wheel_radius_scale": k_r, "wheel_separation_scale": k_t} # 使用流程: # 1. roslaunch chassis_driver.launch # 2. rosrun chassis_pkg online_calibration.py # 3. 在终端输入:rosservice call /calibrate_start 2.0 # 4. 手动推动底盘沿卷尺走 2 米,停止 # 5. rosservice call /calibrate_fit → 返回缩放因子

实操技巧:

  • 测试时关闭 PID 闭环,用rostopic pub /cmd_vel geometry_msgs/Twist "linear: {x: 0.2}"发送恒定速度;
  • 地面需平整(水泥地最佳),避免地毯导致轮子打滑;
  • 每组测试重复 3 次,取s_true平均值;
  • 辨识出的k_r通常为 0.97–0.99(轮径缩水),k_t为 0.985–0.995(轴距略短)。

我坚持每次新底盘组装后必做此辨识——它花不了 3 分钟,却能让后续所有导航、建图、路径跟踪的精度提升一个数量级。那些省掉这一步、靠“调参玄学”硬凑效果的项目,后期加装激光雷达或升级导航算法时,一定会为当初的偷懒付出十倍调试代价。希望帮到你。

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

返回列表