简介:本资源为基于轮式 MEMS IMU 的航位推算系统完整项目包,面向计算机、自动化、电子信息等专业的在校学生、教师及企业研发人员,也适合作为毕设、课设或项目立项的参考方案。项目仅使用一个安装在车轮上的 IMU 实现轮式机器人航位推算,无需车轮编码器即可直接获取车速,并借助车轮连续旋转的调制效应在一定程度上抑制惯性导航误差积累,涉及 C++ 工程实现、算法验证与数据集处理。压缩包共约 2000 个文件,整体 182.03MB,以 836 个 h 头文件、254 个 cc 与 53 个 cpp 源文件为核心,辅以 53 个 Python 脚本、34 个 Markdown 说明及若干配置与数据文件,覆盖算法模块、构建脚本与文档说明。已有 93 人学习下载。读者可获得可运行的源码、文档说明、数据集与论文材料,便于理解轮式 IMU 航位推算的实现思路、调试方法与误差补偿策略,并在此基础上修改扩展功能。
1. 轮式航位推算到底在算什么:从一次地下车库跑偏说起
把一台小车放进没有卫星信号的地下车库,让它沿直线走 30 米再原路返回,你会发现它回到起点的位置偏了将近 1.5 米。这不是电机控制的问题,而是轮式 MEMS IMU 航位推算系统最典型的误差累积现象。这套方案的核心思路很朴素:用轮速计提供纵向位移,用 MEMS IMU 提供航向角变化,两者做互补融合,推算出车辆在二维平面上的位置。它不需要激光雷达,不需要视觉,成本可以压到几百块,适合低速轮式机器人、AGV、扫地机这类场景。但它的天花板也很明显——航向角漂移是这套系统的命门,MEMS 陀螺的零偏不稳定性直接决定了你 1 分钟后还能不能信这个位置。这篇笔记会从传感器选型、标定、融合算法、数据集验证一路讲到实车调试的坑,目标是让你看完能自己搭一套跑起来,并且知道什么时候该放弃它换方案。
2. 轮速计与 MEMS IMU 的选型与标定:为什么你的航向角一开始就是错的
2.1 轮速计分辨率与安装偏心怎么量化
轮速计的本质是把轮子转过的角度转换成位移。常见做法是霍尔编码器或光电编码器,每圈输出 N 个脉冲。单脉冲对应的位移分辨率是:
分辨率 = π × 轮径 / (N × 减速比)假设轮径 65mm,编码器 13 线,减速比 30:1,那单脉冲位移约 0.52mm。听起来够用,但实际安装时轮子不可能完美同心,偏心会导致每圈出现一次周期性误差。量化方法很简单:把车架起来,匀速转轮子,记录每圈脉冲间隔,用最大间隔减最小间隔除以平均间隔,得到偏心误差百分比。超过 3% 就要考虑换安装方式或者做偏心补偿。
我一般会先做 10 圈正转反转测试,取平均每圈脉冲数作为标定值,而不是直接用理论值。因为轮胎压缩、打滑、地面摩擦都会让有效轮径变化。标定代码如下:
import numpy as np def calibrate_wheel(encoder_counts, distance): """ encoder_counts: 10圈总脉冲数 distance: 实际直线行驶距离(米) """ counts_per_meter = encoder_counts / distance wheel_diameter = distance / (10 * np.pi) # 10圈对应距离 return counts_per_meter, wheel_diameter # 实测:推车直线10米,编码器累计 19230 个脉冲 cpm, dia = calibrate_wheel(19230, 10.0) print(f"每米脉冲数: {cpm:.1f}, 等效轮径: {dia*1000:.2f}mm") # 输出:每米脉冲数: 1923.0, 等效轮径: 63.66mm注意这里算出的等效轮径 63.66mm 比实际 65mm 小,说明轮胎有压缩。用这个等效值做后续推算,比用卡尺量的值准得多。
2.2 MEMS IMU 零偏与标度因数:静态标定只能解决一半问题
MEMS IMU 的陀螺零偏分两部分:启动重复性和温度漂移。启动重复性可以通过上电后静止采集 2000 个样本取均值来扣除,但温度漂移必须在线估计。加速度计的标度因数误差影响倾角补偿,如果车体有俯仰,纵向加速度会耦合进重力分量。
静态标定步骤:
- 把 IMU 水平静止放置,采集 5 分钟数据,计算陀螺三轴均值作为零偏。
- 用六面法标定加速度计:每个面朝下静止 30 秒,记录输出,解算零偏和标度因数。
- 把零偏和标度因数写入配置文件,运行时先做零偏扣除。
def gyro_calibration(gyro_data, fs=100): """ gyro_data: Nx3 陀螺原始数据,单位 rad/s fs: 采样率 Hz 返回零偏和噪声密度 """ bias = np.mean(gyro_data, axis=0) # 去偏后计算 Allan 方差简化版:用标准差近似噪声密度 detrended = gyro_data - bias noise_density = np.std(detrended, axis=0) / np.sqrt(fs) return bias, noise_density # 实测某款 MEMS IMU,静止 5 分钟 bias, nd = gyro_calibration(gyro_static) print(f"零偏: {bias*180/np.pi:.4f} deg/s") print(f"噪声密度: {nd*180/np.pi:.4f} deg/s/√Hz") # 输出:零偏: 0.0231 deg/s, 噪声密度: 0.0087 deg/s/√Hz零偏 0.023 deg/s 意味着 1 分钟航向漂移 1.38 度,10 分钟漂移 13.8 度。这就是为什么纯陀螺积分航向在低速场景下撑不过几分钟。必须用轮速差或者磁力计做辅助,但磁力计在地下车库会被钢筋干扰,所以轮速差成了唯一可靠来源。
2.3 轮速差航向估计的几何模型与失效边界
轮速差航向估计基于差速驱动模型:左右轮速差除以轮距得到角速度。公式:
ω = (v_r - v_l) / L其中 L 是轮距。这个模型在低速、小滑移时成立,但遇到以下情况会失效:单轮打滑、地面摩擦系数突变、急转弯时轮胎侧滑。我一般会加一个滑移检测:如果左右轮速差对应的角速度与陀螺测量的角速度偏差超过阈值,就降低轮速差权重,提高陀螺权重。但陀螺本身有漂移,所以这是一个此消彼长的博弈。
实际调试时,我会先让车走一个标准 8 字,记录轮速差航向和陀螺积分航向的差值曲线。如果差值在 5 度以内,说明轮速差可信;如果超过 15 度,说明轮胎打滑严重或者轮距参数不对。轮距 L 的标定方法:让车原地转 10 圈,记录左右轮脉冲差,用脉冲差算出的弧长除以 10×2π 得到等效轮距。
3. 航位推算融合算法:从互补滤波到误差状态卡尔曼
3.1 互补滤波的截止频率怎么定
互补滤波是最简单的融合方式:高通陀螺,低通轮速差。公式:
θ = α × (θ + ω_gyro × dt) + (1-α) × θ_wheelα 是滤波系数,对应截止频率 f_c = (1-α) / (2π × dt)。截止频率选得太高,陀螺漂移滤不掉;选得太低,轮速差打滑时航向响应迟钝。我一般从 0.5Hz 开始试,如果车在瓷砖地面上走直线,轮速差很准,可以降到 0.2Hz;如果地面不平,升到 1Hz。
class ComplementaryFilter: def __init__(self, alpha=0.98, dt=0.01): self.alpha = alpha self.dt = dt self.theta = 0.0 def update(self, gyro_z, theta_wheel): # 陀螺积分 theta_gyro = self.theta + gyro_z * self.dt # 互补融合 self.theta = self.alpha * theta_gyro + (1 - self.alpha) * theta_wheel return self.theta这段代码里 alpha=0.98 对应截止频率约 0.32Hz。注意 theta_wheel 必须做角度解缠,否则在 ±π 附近会跳变。我一般用 numpy 的 unwrap 或者手动处理差值。
3.2 误差状态卡尔曼滤波的 5 维状态怎么设
互补滤波够用,但如果你想做更精细的估计,误差状态卡尔曼是更好的选择。状态向量取 5 维:位置 x、位置 y、航向 θ、陀螺零偏 b_g、轮速标度因数 s。预测方程用轮速和陀螺,观测方程用轮速差航向和轮速位移。
import numpy as np class ESKF: def __init__(self): self.x = np.zeros(5) # [x, y, theta, b_g, s] self.P = np.eye(5) * 0.1 self.Q = np.diag([0.01, 0.01, 0.001, 1e-6, 1e-5]) self.R = np.diag([0.05, 0.05]) def predict(self, v, omega, dt): theta = self.x[2] # 状态传播 self.x[0] += v * np.cos(theta) * dt self.x[1] += v * np.sin(theta) * dt self.x[2] += (omega - self.x[3]) * dt # 雅可比 F = np.eye(5) F[0, 2] = -v * np.sin(theta) * dt F[1, 2] = v * np.cos(theta) * dt F[2, 3] = -dt self.P = F @ self.P @ F.T + self.Q def update(self, z): # z = [x_wheel, y_wheel] 来自轮速积分 H = np.zeros((2, 5)) H[0, 0] = 1 H[1, 1] = 1 y = z - H @ self.x S = H @ self.P @ H.T + self.R K = self.P @ H.T @ np.linalg.inv(S) self.x += K @ y self.P = (np.eye(5) - K @ H) @ self.PQ 矩阵里陀螺零偏的过程噪声设 1e-6,意味着零偏变化很慢。R 矩阵是轮速位移观测噪声,瓷砖地面可以设 0.02,地毯地面设 0.1。调参时先看新息序列,如果新息均值不为零,说明观测有偏;如果新息方差太大,说明 R 设小了。
3.3 零速修正与运动约束:什么时候该相信轮速
零速修正(ZUPT)是航位推算里最有效的误差抑制手段。当检测到轮速为零且陀螺输出接近零偏时,强制把速度置零,并更新零偏估计。检测逻辑:
def zero_velocity_detection(gyro, accel, wheel_speed, threshold=0.02): gyro_norm = np.linalg.norm(gyro) accel_norm = np.linalg.norm(accel) if gyro_norm < threshold and abs(accel_norm - 9.81) < 0.2 and wheel_speed < 0.01: return True return False阈值 0.02 rad/s 对应约 1.15 deg/s,比零偏略大。如果阈值太小,静止时检测不到;太大,慢速运动时误判。我一般用 0.03 rad/s 作为起点,根据实测调整。运动约束方面,轮式车不能侧滑,所以横向速度为零,这个约束可以加到观测方程里,进一步抑制航向漂移。
4. 数据集验证与实车调试:从仿真到地面的 4 个必调参数
4.1 用开源数据集做算法回归测试
标题里提到数据集,我一般会先用公开的轮式航位推算数据集做回归。常见的是某高校实验室放出的室内机器人数据集,包含轮速、IMU、激光真值。用法是:把轮速和 IMU 喂给算法,输出轨迹和真值对比,算绝对轨迹误差(ATE)。ATE 的计算用 evo 工具:
evo_ape tum groundtruth.txt estimated.txt -va --plot --save_results results.zip如果 ATE 在 0.1 米以内,说明算法在仿真层面没问题。但仿真数据往往没有打滑和地面起伏,所以实车 ATE 通常是仿真的 3 到 5 倍。我一般把仿真 ATE 作为下限参考,实车调试时重点看相对误差而不是绝对值。
4.2 轮距与轮径的在线辨识
轮距和轮径的误差会直接导致航向和位移的系统性偏差。在线辨识方法:让车走已知轨迹(比如 10 米直线加 90 度转弯),用最小二乘拟合轮距和轮径修正系数。
from scipy.optimize import least_squares def residual(params, wheel_counts, gyro_theta, true_theta): L, scale = params # 用轮速差算航向 theta_est = (wheel_counts[:, 0] - wheel_counts[:, 1]) * scale / L return theta_est - true_theta # wheel_counts: Nx2 左右轮脉冲累计 # true_theta: 陀螺积分航向(短时间可信) result = least_squares(residual, x0=[0.3, 0.001], args=(wheel_counts, gyro_theta, true_theta)) print(f"轮距: {result.x[0]:.4f} m, 标度: {result.x[1]:.6f}")注意这里用陀螺积分航向作为真值只在短时间内成立,所以数据段不要超过 30 秒。我一般采集 10 组数据,每组 20 秒,取拟合结果的中位数。
4.3 地面材质对参数的影响与切换策略
瓷砖、水泥、地毯、环氧地坪,四种地面的滑移特性完全不同。瓷砖上轮速差很准,地毯上打滑严重。我一般会维护两套参数:高摩擦地面用轮速差权重 0.7,低摩擦地面用 0.3。切换策略可以用轮速差和陀螺的残差来自动判断:
def adaptive_weight(residual): # residual = |theta_wheel - theta_gyro| if residual < 0.05: return 0.8 elif residual < 0.15: return 0.5 else: return 0.2这个权重直接乘在互补滤波的 alpha 上。实测在地毯上,自适应权重比固定权重能把航向误差降低 40% 左右。
4.4 温度漂移的在线补偿
MEMS 陀螺的零偏随温度变化,典型值 0.01 deg/s/°C。如果车从 20°C 的室内开到 35°C 的室外,零偏会漂 0.15 deg/s,1 分钟就是 9 度。在线补偿方法:用陀螺温度传感器读数,拟合零偏-温度曲线。标定方法:把 IMU 放进温箱,从 0°C 升到 50°C,记录每个温度点的零偏。
def temp_compensation(temp, coeffs): # coeffs 是二次多项式系数 [a, b, c] return coeffs[0] * temp**2 + coeffs[1] * temp + coeffs[2] # 标定得到 coeffs = [1e-5, -0.002, 0.05] bias_comp = temp_compensation(35, [1e-5, -0.002, 0.05]) print(f"35°C 零偏补偿: {bias_comp:.4f} deg/s")如果没有温箱,可以用吹风机和冰袋做粗略标定,精度差一些但够用。
5. 避坑与排查:航位推算翻车的 5 个血泪教训
5.1 现象:静止时航向缓慢旋转,1 分钟转 5 度
原因:陀螺零偏没扣干净,或者零偏随温度漂移。解决:上电后先静止 30 秒做零偏估计,运行时用 ZUPT 持续更新零偏。如果温度变化大,加温度补偿。
5.2 现象:直线行驶时位置偏航,越走越偏
原因:轮距参数不对,或者左右轮径不一致。解决:重新标定轮距,检查轮胎气压和磨损。我遇到过左右轮径差 2mm,10 米就偏 0.3 米。
5.3 现象:转弯时航向跳变,回正后误差大
原因:轮速差航向在转弯时打滑,互补滤波权重没切换。解决:加滑移检测,转弯时降低轮速差权重。或者用陀螺积分航向做短时主导。
5.4 现象:过坎或颠簸后位置突变
原因:轮子离地导致轮速计多计脉冲,或者 IMU 加速度饱和。解决:加离地检测,用加速度计 z 轴判断;轮速计加脉冲滤波,剔除异常间隔。
5.5 现象:长时间运行后航向漂移超过 20 度
原因:MEMS 陀螺零偏不稳定性累积,没有外部绝对参考。解决:这是物理限制,只能加磁力计或视觉做绝对航向修正。如果场景不允许,就要接受这个误差,或者定期停车做 ZUPT。
6. 进阶技巧:用轮速脉冲做零偏在线估计的工程实现
前面讲的 ZUPT 需要停车才能触发,但很多场景车一直在动。我后来用了一个技巧:利用轮速脉冲的周期性做零偏在线估计。原理是:当车走直线时,左右轮脉冲差应该为零,如果陀螺输出不为零,那这个输出就是零偏。实现上用一个滑动窗口,检测窗口内轮速差小于阈值且持续时间超过 2 秒,就把陀螺均值作为零偏更新量。
class OnlineBiasEstimator: def __init__(self, window_size=200): self.window = [] self.window_size = window_size self.bias = 0.0 def update(self, gyro_z, wheel_diff): self.window.append((gyro_z, wheel_diff)) if len(self.window) > self.window_size: self.window.pop(0) # 检测直线段 diffs = [w[1] for w in self.window] if max(diffs) - min(diffs) < 0.01: # 轮速差波动小 gyros = [w[0] for w in self.window] self.bias = 0.9 * self.bias + 0.1 * np.mean(gyros) return self.bias这个方法的边界是:如果车一直转弯,窗口内轮速差波动大,就不会更新零偏。所以它适合直线段较多的场景,比如走廊巡检机器人。实测在 10 分钟的走廊运行中,这个方法能把航向误差从 15 度压到 5 度以内。
另一个技巧是轮速脉冲的边沿计数。有些编码器输出正交脉冲,可以用四倍频提高分辨率。但四倍频会引入边沿抖动,我一般只在低速时用四倍频,高速时用单倍频。切换阈值设在 0.3m/s 左右。
最后说一个验证方法:用卷尺量 5 米直线,让车走 10 次,记录终点位置偏差。如果偏差的标准差小于 0.1 米,说明系统一致性不错;如果均值偏差大,说明有系统性误差,回去查轮径和轮距。这个土办法比任何仿真都管用,我每次改完参数都会跑一遍。希望帮到你。
本文还有配套的精品资源,点击获取