资讯动态

基于STM32的麦克纳姆轮底盘轮速闭环与位置控制实战

发布时间:2026/9/17 10:29:03 来源:尧图企业网站定制
麦轮底盘这个玩意儿第一眼看上去有点反直觉——四个轮子斜着装车却能不转车头就横着平移出去。我最早接触它是在一个窄通道搬运小车的项目上过道只有七十厘米宽普通差速底盘掉头要来回蹭三次换成麦轮以后直接侧滑进位整个动作干净利落。后来我自己从零搓了一台主控用 STM32从建工程开始一路做到四个轮子的轮速闭环再往上叠位置控制能走直线、能横移、能原地旋转、也能走斜线。这篇东西适合谁看手上已经有一台麦轮小车、但代码跑起来只会原地打转或者画龙的朋友准备拿它做毕业设计、课程设计的学生还有想从差速底盘升级到全向移动的爱好者。整条链路我会按“需求拆解 → 建工程 → 测速 → 轮速闭环 → 位置控制 → 排查”的顺序讲STM32 用的是 F103 系列标准库和 HAL 两种思路都会点到代码片段可以直接抄去改。中间那些参数怎么算出来的、为什么这么选我都会把推导过程摆出来而不是甩一堆结论让你背。1. 先想清楚要做什么麦轮底盘的需求拆解与方案选型动手写第一行代码之前我建议先把需求写在纸上。麦轮底盘不是拿来炫技的它的价值集中在三个字全向性。搞清楚这个前提后面所有的硬件和算法选择都会顺理成章。1.1 麦轮底盘到底比差速底盘强在哪差速底盘靠左右两侧轮子的速度差转弯本质上一个非完整约束系统——它没法在保持车头朝向不变的前提下横向平移。你要它往右挪三十厘米它必须先右转、前进、再左转动作上至少要三步。麦轮底盘不一样四个轮子各自带一圈与地面成 45 度的辊子辊子的被动滚动会把轮子的线速度分解成一个沿车体纵向的分量和一個横向的分量四个轮子横向分量叠加、纵向分量抵消就能让车体整体平移。落到实际场景里这个差别非常具体。窄通道里取放货物麦轮可以贴边侧滑进去车头始终朝向通道口取完直接侧滑出来不需要任何掉头动作。精密对接工位上车体需要贴着装配台保持一个固定姿态微调位置差速底盘做这个动作要反复修正角度麦轮可以只调 x 和 y 不动航向角。再比如舞台上的道具车、比赛里的机器人底盘麦轮能做到“边走边转”轨迹是一个平滑的圆弧或者斜线视觉观感完全不一样。代价也得说清楚麦轮的辊子与地面是线接触抓地力比橡胶轮的差速底盘弱过坎、上坡、泥地这些工况会打滑辊子本身有机械间隙编码器算出来的里程计会有额外误差四个轮子要同时控制对主控的定时器和算力要求高一截。所以我的建议是场地平整、载荷在几十公斤以内、需要全向移动的场景选麦轮如果是户外越野或者重载老老实实上差速或者阿克曼。1.2 把运动学公式先拍在纸上我见过太多人代码写了半个月最后卡在“为什么我让它往左它往右”。根因就是运动学模型没对齐。麦轮的运动学模型不复杂但正负号特别容易错所以第一步一定是把公式定死。先定义几个量车体坐标系下前进方向为 vx向左横移为 vy逆时针旋转角速度为 ω四个轮子的线速度分别是前左 FL、前右 FR、后左 RL、后右 RR轮子半径 r前后轴距 L前轮中心到后轮中心的距离左右轮距 W左轮中心到右轮中心的距离。令 k (L W) / 2。逆运动学已知 vx、vy、ω求四个轮子该转多快v_FL vx - vy - k·ωv_FR vx vy k·ωv_RL vx vy - k·ωv_RR vx - vy k·ω这个式子里有个容易忽略的点k 用的是 (LW)/2 而不是 L/2 或者 W/2。原因是麦轮的速度矢量方向是沿辊子法线方向分解的转动力矩臂是轴距和轮距共同决定的两个方向各贡献一半。我一开始按 W/2 算结果原地旋转的实际角速度只有目标值的一半左右转十圈下来差了将近一倍修正系数才对上。正运动学已知四个轮子速度反推车体速度vx (v_FL v_FR v_RL v_RR) · r / 4vy (-v_FL v_FR v_RL - v_RR) · r / 4ω (-v_FL v_FR - v_RL v_RR) · r / (4k)正解主要用在里程计上也就是靠轮子转了多少来推算车走到哪了。验证方法很简单把上面两组公式里的 r 去掉因为都是线性关系代进 vx1、其他为 0你会得到四个轮子都是 1/4 的正数说明车往前走的时候四个轮子同向同速逻辑对得上。提示如果实车跑起来方向反了优先怀疑两件事——编码器计数方向和电机转向的对应关系、以及 vy 项的正负号。千万不要靠改 PID 参数去硬凑那是缘木求鱼。1.3 硬件选型每一分钱花在哪硬件这块我按“够用、留余量、好维护”三个原则选。下面这张表是我自己这台车的实际配置也标注了替代方案和取舍理由。部件我用的型号关键参数替代方案与取舍主控STM32F103RCT6256KB Flash、48KB RAM、TIM1~TIM8F103C8T6 便宜但只有 TIM1~TIM4定时器不够分电机直流减速电机12V、减速比 1:30、输出轴 300rpm带霍尔编码器版本必选不带编码器没法闭环编码器增量式霍尔13 线AB 两相4 倍频后 52 计数/电机转光电编码器精度更高但价格和体积都上去了驱动双路 H 桥模块单路持续 7A、峰值 20A选带散热片的堵转电流很容易冲到 10A 以上轮子麦克纳姆轮直径 60mm、辊子 9 个直径 80mm 通过性更好但整车速度和转矩要重算电源3S 锂电 11.1V容量 2200mAh 以上加一片 DC-DC 单独给主控供电别和电机共轨姿态六轴 IMU陀螺 加速度用于修正航向角漂移位置控制精度能提升一档选 F103RCT6 是有原因的。四个电机需要四路独立的 PWM还需要四路正交编码器接口做测速而 STM32 的编码器模式必须占用一个完整的定时器。F103C8T6 只有 TIM1 到 TIM4一旦 TIM1 拿来出 PWM就只剩三个定时器做编码器第四个轮子只能退而求其次用外部中断计数代码复杂度和 CPU 占用都上去了。换到 RCT6 之后有 TIM1 到 TIM8资源一下子就宽裕了多花十几块钱省下的调试时间非常值。电源部分单独说一句。电机启动和换向瞬间的电流冲击会在电源线上产生很大的纹波如果主控和电机共用一个 5V 或者 3.3V 稳压MCU 很容易复位或者编码器读数乱跳。我的做法是电池出来走两路一路直接给驱动板另一路经过 DC-DC 降压到 5V 再给主控和传感器两路只在电池负极共地。这个改动看起来很小但解决了我早期将近一半的“玄学复位”问题。2. STM32 工程从零建起来环境、时钟与定时器资源工程搭建这块很多人图快直接拿别人现成的工程改结果后面加了外设就冲突改起来比重写还痛苦。我的习惯是每个项目都自己从头搭一遍慢是慢一点但外设资源心里有数出问题也容易定位。2.1 开发环境与芯片包别在第一步就卡住工具链我用的是 Keil MDK5 加 STM32CubeMX 的组合。Keil 负责编译调试CubeMX 负责生成初始化代码分工明确。安装 Keil 之后必须先装芯片支持包F1 系列对应的是 STM32F1xx_DFP这个包不装的话新建工程时器件列表里根本找不到 F103RCT6很多人卡在这一步还以为是软件坏了。芯片包可以从官网下载离线包双击安装也可以直接在 Pack Installer 里在线装。如果你偏爱标准库我早期项目都用它那 CubeMX 就不参与了需要自己准备四样东西启动文件 startup_stm32f10x_hd.s大容量型号用 hd中容量用 md选错了直接跑飞、CMSIS 内核头文件、标准外设库源码 stm32f10x_stdperiph_driver、以及一个 stm32f10x_conf.h 配置文件。这里有个隐形的坑Keil 工程选项的 C/C 标签页里必须加上宏定义 USE_STDPERIPH_DRIVER 和 STM32F10X_HD头文件路径要包含 inc 目录少一个都会报一堆“undefined identifier”。我第一次建标准库工程报了六十多个错全是这两个宏没加导致的。HAL 库这条路就轻松很多CubeMX 里点几下时钟树和外设点生成代码Keil 工程直接就有了跨芯片移植也方便。缺点是多了一层函数封装中断里的执行效率比标准库低一些而且 HAL 的默认延时函数依赖 SysTick如果在中断里调用可能会死锁。我的取舍是外设初始化和低级驱动用 CubeMX 生成 HAL 打底控制算法部分完全手写不依赖 HAL 的任何延时和阻塞函数。2.2 工程目录怎么分层决定了后期好不好维护工程建好之后我第一件事是整理目录结构。一个能长期维护的 STM32 工程目录一定是有层次的不然改一个 PID 参数要在五个文件里翻。我用的结构大概是这样MecanumChassis/ ├── CMSIS/ 内核与启动文件 ├── FWlib/ 标准外设库或 HAL 库 ├── BSP/ 板级驱动 │ ├── bsp_pwm.c 四路 PWM 输出 │ ├── bsp_encoder.c 四路编码器读取 │ ├── bsp_uart.c 调试串口 │ └── bsp_timer.c 系统节拍与调度 ├── Module/ 功能模块 │ ├── motor.c 电机抽象目标速度、实际速度、输出 │ ├── pid.c 通用增量式 PID │ ├── kinematics.c 正逆运动学解算 │ ├── odometry.c 里程计积分 │ └── imu.c 姿态读取与融合 ├── App/ 应用逻辑 │ ├── chassis_task.c 速度环 位置环调度 │ └── cmd_parser.c 串口指令解析 └── main.c这个分层的好处是BSP 层只碰寄存器Module 层只关心数据流App 层只做逻辑编排。后面我把电机从直流有刷换成另一款只需要动 BSP 和 motor.c 两个文件PID 和运动学完全不用改。我见过把所有东西堆在 main.c 里的工程两千多行改一个参数靠 CtrlF 搜数字那种代码基本只能用一次。2.3 时钟树与定时器资源分配含 PWM 频率计算时钟配置是整台车的地基。F103 的最高主频是 72MHz走的是外部 8MHz 晶振经 PLL 九倍频的路线。这一步算错所有定时器的时间基准全错PWM 频率、测速周期、PID 采样周期都会跟着错而且错得很隐蔽——车能跑就是跑不准。定时器分配我先列成表一目了然定时器用途引脚关键配置TIM1四路电机 PWMPA8/PA9/PA10/PA11ARR3599不分频20kHzTIM2编码器接口 1左前PA15/PB3重映射编码器模式四倍频TIM3编码器接口 2右前PA6/PA7编码器模式四倍频TIM4编码器接口 3左后PB6/PB7编码器模式四倍频TIM5编码器接口 4右后PA0/PA1编码器模式四倍频TIM6系统节拍无5ms 定时中断跑速度环TIM7位置环节拍无20ms 定时中断跑位置环这里有个资源冲突要特别注意。STM32F103 的 TIM2 默认通道 1、2 就是 PA0 和 PA1而 TIM5 的通道 1、2 也是 PA0 和 PA1两个定时器抢同一组引脚。解决办法是把 TIM2 做部分重映射通道 1、2 挪到 PA15 和 PB3 上去PA15 和 PB3 默认是 JTAG 的引脚所以还要顺便关掉 JTAG 保留 SWD这也就是大家常说的“禁用 JTAG”。关 JTAG 的代码就三行RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); // 保留 SWD关闭 JTAG GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE); // TIM2 重映射到 PA15/PB3PWM 频率的计算也要交代清楚。直流有刷电机驱动常用的 PWM 频率在 15kHz 到 25kHz 之间低于 15kHz 电机会有明显的电流啸叫声耳朵很难受高于 25kHz 开关损耗上升普通 H 桥模块发热会加快。我选 20kHz正好在可听声频率边缘实际听感安静。计算过程是这样的定时器时钟 72MHz目标频率 20kHz预分频系数取 0那么自动重装载值 ARR 72 000 000 / 20 000 - 1 3599。所以 TIM_Period 填 3599TIM_Prescaler 填 0。占空比通过比较寄存器 CCR 设置CCR 的取值范围就是 0 到 3600。把 CCR 设成 1800占空比就是 50%。反过来如果想换频率直接套 ARR 72e6 / f - 1 就行别拍脑袋填数字。注意TIM1 和 TIM8 是高级定时器出了 PWM 之后必须额外调用 TIM_CtrlPWMOutputs(TIM1, ENABLE)否则引脚上一点波形都没有。这个函数忘了调现象是电机完全不转但示波器量引脚是低电平很容易误判成驱动板坏了。3. 电机驱动与编码器测速把轮子的状态读准速度环能不能调好八成取决于测速环节干不干净。我在测速上花的时间比调 PID 多得多事实证明这个投入是值得的。3.1 驱动板接线与方向控制逻辑常见的双路 H 桥模块单路的控制信号有两种模式一种是 PWM 加方向PWM 引脚管速度DIR 引脚管正反转另一种是双 PWM两个引脚分别控制正转和反转一个给高另一个必须给低。我用的是前者理由是省引脚而且方向逻辑清晰。接线的时候有个细节四个电机的正负极性不要凭感觉接。我的做法是先不装轮子代码里让四个电机都输出 30% 占空比正转看四个输出轴是不是都朝同一个旋转方向转从同一个视角看。如果有一个反了要么换两根电机线要么在代码里给这个电机单独加一个方向标志位取反。我倾向于代码里改因为线接错了以后拆装很麻烦。方向标志位我建议做成一个宏或者数组而不是在代码里写死正负号。原因很实际麦轮的辊子排布有左旋和右旋两种四个轮子装上去之后左右两侧的辊子方向是不同的如果代码里把所有轮子都当成同一个方向处理跑起来就会出现某个方向平移失效、斜着走的情况。用一个配置数组统一管理换轮子或者换装法只改一处。3.2 正交编码器四倍频与计数方向确认增量式霍尔编码器输出 A、B 两路方波相位差 90 度。STM32 的编码器接口模式会自动根据 A、B 的相位关系判断方向并且在 A、B 的上升沿和下降沿都计数所以一个电机转一圈产生的计数是编码器线数的四倍。我用的是 13 线编码器减速比 1:30那么一个轮子转一圈对应的计数值是13线数× 4倍频× 30减速比 1560 计数/圈已知轮子直径 60mm周长 π × 60 ≈ 188.5mm那么每个计数对应的位移是 188.5 / 1560 ≈ 0.1208mm。这个数很重要后面所有速度换算和里程计都建立在它上面。计数方向必须实测确认不能推理。方法是写一个测试函数手动把某个轮子往前转一圈读计数器的值如果是正的说明方向对负的就要在读取时取反或者改编码器的极性配置。判断“往前转”的标准要在四个轮子上统一不然正运动学解出来的 vx 会是乱的。这里还有个坑编码器计数器的值超过 ARR 会自动回绕比如 16 位计数器从 65535 变到 0。所以测速不能用当前值必须用两次采样的差值而且差值要做有符号处理。/* 读取编码器增量处理 16 位回绕 */ static int16_t encoder_read_delta(TIM_TypeDef *TIMx, int16_t *last) { int16_t now (int16_t)TIM_GetCounter(TIMx); /* 强转成有符号自动处理回绕 */ int16_t delta now - *last; *last now; return delta; }把 16 位无符号数强制转成 int16_t回绕问题就自然解决了这比写一堆 if-else 判断 65535 简洁得多实测很稳。3.3 三种测速方法的选择与滤波落地测速方法有 M 法固定时间数脉冲、T 法固定脉冲数时间、M/T 法两者结合。M 法适合高速T 法适合低速M/T 法全速段精度都好但实现复杂。对麦轮底盘来说我直接选 M 法原因是速度环周期固定5msM 法天然就是“固定周期读增量”跟控制周期对齐代码最简单而底盘实际工作转速大部分时间在中高速段M 法的量化误差在可接受范围内。低速段的精度问题我靠后面加一节一阶低通滤波来缓解没必要为了 1% 的工况上 M/T 法。具体实现是这样的每 5ms 定时中断里读取四个编码器的增量然后换算成线速度#define CNT_PER_REV 1560.0f /* 每圈计数 */ #define WHEEL_PERIM 0.1885f /* 轮子周长单位 m */ #define CTRL_DT 0.005f /* 控制周期 5ms */ /* delta 为本周期编码器增量单位计数 */ float mm_per_cnt WHEEL_PERIM * 1000.0f / CNT_PER_REV; /* 0.1208 mm */ float raw_speed delta * mm_per_cnt / CTRL_DT; /* 单位 mm/s */代入一组真实数据如果 5ms 里某个编码器增量为 10那么速度就是 10 × 0.1208 / 0.005 241.6 mm/s。四个轮子都往前车体前进速度就是 241.6 mm/s差不多每秒 24 厘米走一米的直线要四秒多这个速度对室内搬运场景比较合适。低速量化误差也要算一算。编码器增量最小单位是 1对应速度就是 0.1208 / 0.005 24.16 mm/s。也就是说当实际速度低于这个值时M 法读出来要么是 0 要么是 24精度很差。解决办法有两个一是把控制周期从 5ms 拉长到 10ms最小分辨速度降到 12 mm/s代价是控制带宽下降二是加低通滤波让速度值平滑过渡代价是引入相位滞后。我选的是保持 5ms 加一阶低通滤波系数取 0.3实测在 100mm/s 以上的速度段非常稳定低速段也勉强能用。/* 一阶低通滤波alpha 越小越平滑滞后越大 */ speed_filt speed_filt 0.3f * (raw_speed - speed_filt);这里再补一个容易被忽略的细节编码器信号线一定要用双绞或者屏蔽线并且远离电机动力线。我早期把编码器线和电机线捆在一起走线速度读数上叠加了很大的噪声PID 一调就抖后来把两组线分开走、编码器线加磁环问题直接消失。这属于硬件上的“隐形参数”纯靠调软件是调不出来的。4. 轮速闭环从开环乱跑到四个轮子听话轮速闭环是整个底盘的核心。它做好的标志是你给四个轮子各一个目标速度它们能稳稳地跟住不管地面上有没有阻力、电池电压是不是在掉。这一步做扎实了位置控制才有意义。4.1 为什么先做轮速闭环而不是直接上位置环新手最容易犯的错是直接给轮子一个 PWM 值就指望车走到指定位置。这是开环问题有三个一是电池电压从 12.6V 掉到 10.5V 的过程中同样的 PWM 值对应的转速会下降十几个百分点走同样距离用的时间越来越长二是四个电机特性有差异加上四个轮子的摩擦阻力不同同样的 PWM 会让车往一边偏三是麦轮的辊子会打滑一旦打滑开环完全没有反馈误差就一直累积。轮速闭环解决的就是这些。它的输入是目标速度mm/s输出是 PWM 占空比中间用编码器测出的实际速度做反馈。只要闭环做好了电池掉压、负载变化、单轮阻力差异控制器都会自动补偿。这也是为什么我坚持按“开环转起来 → 单轮速度闭环 → 四轮一起闭环 → 位置环”这个顺序推进每一步都能单独验证出了问题范围立刻缩小。4.2 增量式 PI 前馈的代码实现控制器我选增量式 PI不用完整 PID。原因有两个增量式输出的是增量天然带积分作用且抗积分饱和好处理微分项在速度环上噪声放大严重编码器测速本身就有量化噪声加微分等于给自己找麻烦。如果确实需要微分我宁可加在前馈或者位置环上。增量式 PI 的公式Δu Kp · (e[k] - e[k-1]) Ki · e[k] u[k] u[k-1] Δu再加上前馈项。因为电机转速和 PWM 占空比大致成线性关系我可以用一个简单的比例前馈让控制器不用从零开始积累积分u_ff target_speed / max_speed × PWM_MAX前馈系数的确定方法很朴素把车架空给某个轮子一系列固定占空比记录稳态速度画出占空比-速度曲线取斜率倒数即可。我这台车 100% 占空比对应空载约 900mm/s所以前馈系数约为 1/9 的 PWM 值每 mm/s。typedef struct { float kp, ki; float target; /* 目标速度 mm/s */ float actual; /* 实测速度 mm/s */ float err, err_last; float out; /* 输出 PWM带符号 */ float out_max; /* 输出限幅 */ float ff_gain; /* 前馈系数 */ } SpeedLoop; float speed_loop_update(SpeedLoop *s) { float delta; s-err s-target - s-actual; /* 前馈让响应更快、积分负担更轻 */ float ff s-target * s-ff_gain; /* 增量式 PI */ delta s-kp * (s-err - s-err_last) s-ki * s-err; s-out delta; s-err_last s-err; /* 输出限幅 积分回退防止饱和后回不来 */ float limit s-out_max; if (s-out limit) s-out limit; if (s-out -limit) s-out -limit; return s-out ff; }抗积分饱和用的是最直接的钳位法输出一旦撞到限幅就把它按回去同时不再累加同方向的增量。更好的做法是积分分离或者反计算抗饱和但对速度环这种场景钳位已经够用。我实测过不加抗饱和的话车从静止启动时会有一段明显的“冲过头再拉回来”加了之后就平顺很多。四个轮子各自维护一个 SpeedLoop 结构体定时中断里依次调用然后把输出写到四个 PWM 通道同时根据输出符号设置对应的 DIR 引脚。注意 PWM 的 CCR 只能是正数输出为负时要取绝对值并把方向引脚翻转。4.3 参数整定一步步把曲线调漂亮整定顺序我固定按这个流程走别跳步第一步架空空转确认编码器读数符号正确、方向逻辑正确。给正的目标速度轮子必须朝“前进”方向转否则先把符号问题解决掉不要带着符号错误去调参数。第二步纯比例环节Ki 和 Kp 的微分项先置零。Kp 从很小的值开始加比如 0.5然后 1.0、2.0观察阶跃响应。Kp 太小的时候响应很慢要到一两秒才接近目标Kp 太大就会在目标值附近持续振荡表现为轮子转速忽快忽慢。合适的 Kp 是响应快、有一点超调但能在三次振荡内收敛。第三步加 Ki 消除稳态误差。Ki 从 0.05 开始小步加每加一次做一次阶跃测试。Ki 的作用是让最终稳态速度精确等于目标值。加太多的表现是低频摆动转速会周期性大起大落这时候往回调一半。第四步加前馈把 Kp 和 Ki 适当调小。前馈承担了大部分“基础输出”反馈只需要处理扰动和误差整体响应会更干脆超调也更小。下面是我这台车最终的一组参数给个参考起点。注意这只是起点不同电机、不同轮径、不同控制周期数值差异会很大直接抄多半不好用。参数数值说明控制周期5ms200Hz速度环执行频率Kp1.8比例增益影响响应速度Ki0.12积分增益消除稳态误差前馈系数0.11每 mm/s 对应的 PWM 值输出限幅±3599对应 0~100% 占空比低通滤波系数0.3越小越平滑滞后越大调完之后我习惯做一次“打点测试”让车在瓷砖地面上走一条 2 米的直线用卷尺量实际偏移。轮速闭环调好的状态下2 米直线偏移能控制在 2 厘米以内。如果偏移超过 5 厘米说明还有轮子的速度跟踪不到位或者四个轮子的一致性有问题要回去查。提示调试期间用串口把四个轮子的目标速度和实际速度定时打印出来用上位机画成曲线看比盯着车看靠谱得多。串口推荐 115200 波特率每个周期打印一行简单直接。5. 运动学解算与位置控制让底盘走到指定坐标轮速闭环让四个轮子听话了但你还是得一个一个给四个轮子下速度指令这显然不实用。往上叠一层运动学解算你只需要给 vx、vy、ω 三个量再往上叠位置控制你只需要给一个目标坐标。5.1 逆解三轴速度指令到四轮目标逆解就是 1.2 节那四个公式的代码化。要注意的是单位统一公式里的速度单位可以是 m/s 也可以是 mm/s但 ω 是弧度每秒k 是米。我的建议是全部统一到毫米和秒避免混用。#define K_HALF 0.155f /* k (LW)/2单位米实测 L0.20m、W0.11m */ void kinematics_inverse(float vx, float vy, float omega, float *wheel) { float k K_HALF; wheel[0] vx - vy - k * omega; /* 前左 */ wheel[1] vx vy k * omega; /* 前右 */ wheel[2] vx vy - k * omega; /* 后左 */ wheel[3] vx - vy k * omega; /* 后右 */ }参数 k 的确定我用了实测法。理论值是把轴距 0.20m 和轮距 0.11m 代进去算k 0.155。但实车因为辊子有间隙、轮子有变形实际等效值会有偏差。我的标定方法是让车原地旋转指令 ω 1 rad/s跑 10 秒用 IMU 读出实际转过的角度理论上应该是 10 弧度约 573 度。如果实际只转了 500 度说明 k 偏小把 k 按比例乘 573/500 ≈ 1.15 修正。反复两三次就能标得很准。逆解算完之后每个轮子的目标速度是线速度mm/s要转成编码器能对上的量直接用就行因为速度环的输入就是线速度。注意如果某个轮子的目标速度超过电机极限整体要做等比缩放否则会出现“大速度轮子饱和、小速度轮子跟着失真”的情况车体运动方向就歪了。/* 等比限幅任一超限则整体缩放保证方向不失真 */ float max_v 0; for (int i 0; i 4; i) { float a fabsf(wheel[i]); if (a max_v) max_v a; } if (max_v WHEEL_MAX_SPEED) { float scale WHEEL_MAX_SPEED / max_v; for (int i 0; i 4; i) wheel[i] * scale; }这一步是很多人的盲区。不做等比缩放的话想让车快速斜着走的时候超限的那个轮子被单独限幅车体实际走出来的方向就跟你想要的方向不一样看起来像“走歪了”。5.2 正解与里程计车现在到底在哪要做位置控制就必须知道车现在在哪。里程计就是靠四个轮子的编码器数据反推车体位移。流程是每 5ms 读四个编码器的增量 → 换算成四个轮子的位移毫米→ 正解算出车体在自身坐标系下的位移和转角 → 根据当前航向角旋转到世界坐标系 → 累加。正解代码void kinematics_forward(float *wheel_disp, float *dx, float *dy, float *dtheta) { float k K_HALF; *dx ( wheel_disp[0] wheel_disp[1] wheel_disp[2] wheel_disp[3]) / 4.0f; *dy (-wheel_disp[0] wheel_disp[1] wheel_disp[2] - wheel_disp[3]) / 4.0f; *dtheta (-wheel_disp[0] wheel_disp[1] - wheel_disp[2] wheel_disp[3]) / (4.0f * k); }里程计积分需要注意航向角的问题。如果只用轮子推算的角度误差会累积得很快尤其是原地旋转打滑的时候可能转十圈下来角度就漂了几十度。我的做法是航向角主要信任六轴 IMU 的陀螺仪积分轮子算出的 dtheta 只作为辅助用互补滤波加权融合短时间信任陀螺长时间慢慢往轮速推算值上靠。这样既有陀螺的短期精度又不会有长期的零偏累积。世界坐标系的更新用旋转矩阵/* theta 当前航向角单位弧度 */ float cos_t cosf(theta), sin_t sinf(theta); x_world dx * cos_t - dy * sin_t; y_world dx * sin_t dy * cos_t; theta dtheta_fused;这里的符号要注意车体坐标系的 y 轴定义为向左旋转矩阵的写法跟常见的数学坐标系一致。如果发现车往前走的时候世界坐标里的 y 在变八成是把某个符号搞反了。5.3 位置环与速度规划别让底盘“冲过头”位置控制我用串级结构外层是位置环内层是速度环。位置环的周期比速度环慢我设成 20ms50Hz。外环的输入是当前坐标和目标坐标输出是期望速度这个期望速度再送给内层速度环。外环为什么用 P 控制而不是 PI因为位置环的输出是速度指令如果位置误差一直存在PI 会不断积分输出速度指令越来越大车就会冲过目标点再往回拉来回振荡。位置环加积分项还会让“位置超前”的问题变得严重也就是车停了但电机还在推。所以位置环只用比例剩下的稳态误差靠速度环的积分来兜底。/* 位置环P 控制 速度上限约束 */ float pos_kp 1.2f; /* 位置增益单位 1/s */ float max_vel 300.0f; /* 最大线速度 mm/s */ float ex target_x - x_world; float ey target_y - y_world; float dist sqrtf(ex * ex ey * ey); float vx_cmd pos_kp * ex; float vy_cmd pos_kp * ey; /* 限速越接近目标越慢形成减速段 */ float v_norm sqrtf(vx_cmd * vx_cmd vy_cmd * vy_cmd); if (v_norm max_vel) { vx_cmd vx_cmd / v_norm * max_vel; vy_cmd vy_cmd / v_norm * max_vel; }这样写出来的效果是距离目标远的时候位置误差大期望速度被限幅在 max_vel匀速前进距离近了之后期望速度自然下降形成平滑减速到达目标附近时速度趋近零。不需要显式写“加速段、匀速段、减速段”的状态机一个限幅就把梯形速度曲线近似出来了。如果对加减速平顺性要求更高可以再加一个加速度限幅让速度指令的变化率不超过某个值。航向角的控制也用同一个思路把目标航向角和当前航向角的差经过一个角度归一化保证误差在 -π 到 π 之间再乘一个增益输出 ω 指令。这里必须做角度归一化否则车从 179 度转到 -179 度的时候会绕远路转 358 度。运动模式vxvyω典型应用前进后退非零00直线运输左右横移0非零0窄通道侧向进位原地旋转00非零调整姿态、扫描斜向移动非零非零0抄近路、避障绕行边走边转非零非零非零圆弧轨迹跟踪这张表基本覆盖了麦轮底盘的全部运动模式。实际遥控的时候摇杆的左右轴映射到 vy前后轴映射到 vx旋转轴映射到 ω三个量一起给麦轮能同时响应这就是它比差速底盘灵活的地方。5.4 标定三件事轮径、轴距系数、陀螺零偏前面说了很多算法但最后决定精度的是标定。我每次装完车都会做三件事做成检查清单。第一件是轮径标定。让车在纸上走一条直线指令是前进 1000mm然后实测实际走了多少。如果实际走了 1050mm说明代码里用的轮径偏小把轮径乘以 1050/1000 修正。这个偏差通常来自轮子受压缩、地面材质和打滑一般要修正 2% 到 5%。第二件是 k 值标定就是 5.1 节说的原地旋转法。我会让车原地转 5 圈用 IMU 读实际角度对比理论值。这个系数对的准不准直接决定平移和旋转的耦合是否正确。如果 k 偏了现象是“让它斜着走 45 度实际走出来是 40 度”。第三件是陀螺零偏标定。陀螺仪在静止时会输出一个非零的小电压如果直接积分航向角会缓慢漂移。我的做法是上电后保持静止 2 秒采集 200 个样本取平均把这个平均值作为零偏后续所有陀螺读数都减去它。这个步骤必不可少不做的话车静止放着航向角一分钟能漂好几度。6. 常见问题与排查实录这部分是我踩坑最多的地方也是最有价值的部分。很多问题在文档里找不到只能靠实测摸索。6.1 上电到跑起来我踩过的九个坑坑一PWM 有配置但引脚无输出。九成是高级定时器忘了开主输出也就是 TIM_CtrlPWMOutputs 没调用。另外两个可能是 GPIO 模式没设成复用推挽或者引脚被 JTAG 占用了。坑二电机上电就飞车。多数是接线问题或者 DIR 引脚初始状态不对电机上电瞬间获得了满占空比。我的习惯是初始化完成后立刻把所有 PWM 通道的 CCR 清零再开主输出顺序不能反。坑三四个轮子有三个转一个不转。先量驱动板的输入信号有没有 PWM再量输出端电压。如果输入有信号输出没电压多半是那个通道的 H 桥坏了或者散热片过热保护了。我就遇到过一次堵转把通道烧掉的情况。坑四编码器计数只增不减。这是典型的 A、B 两相只接了一路或者其中一路虚焊。正常的两相正交信号反转时计数必须是递减的。用示波器同时看 A、B 两路确认相位差是 90 度。坑五车走直线越走越偏。分三种情况排查如果偏差是固定的方向是轮子装得不对称或者某个轮子阻力大如果偏差是随机左右摇摆是速度环参数太激进如果偏差随速度增加而变大是里程计标定不准导致的耦合误差。坑六原地旋转时半径很大。这通常是 k 值偏小。理论上原地旋转四个轮子应该形成一个纯旋转如果某个轮子速度分配不对车就会边走边转轨迹变成一个圆。坑七PID 高频抖动电机发出尖锐噪声。两个原因一是速度反馈噪声大编码器线没屏蔽好二是控制周期太短微分或者比例增益在高频段放大噪声。我一般把控制周期从 2ms 放宽到 5ms同时给速度加低通滤波抖动立刻减轻。坑八串口打印乱码。先确认波特率和时钟配置是否匹配。F1 系列用 8MHz 晶振九倍频到 72MHz串口波特率计算才会准。如果晶振实际是 12MHz 但代码按 8MHz 算波特率就会错打印出来全是乱码。坑九车跑一会儿就复位。电源问题占八成。电机启动瞬间拉低电压主控欠压复位。解决办法是主控单独一路 DC-DC 供电并在主控电源脚附近加 100μF 电解电容加 0.1μF 陶瓷电容去耦。6.2 问题速查表现象最可能的原因快速验证方法处理方式电机完全不转主输出未使能、GPIO 模式错示波器量 PWM 引脚调 TIM_CtrlPWMOutputs、改复用推挽上电飞车初始化顺序错、DIR 初值错断电手动测 DIR 电平先清零 CCR 再开输出单轮不转驱动通道损坏、接线松交换两路输出对比换通道、重新压线计数只增不减A/B 相只接一路双通道示波器看相位补焊、检查上拉走直线跑偏轮速闭环差、里程计标定错串口画四轮速度曲线重新整定、重标轮径原地转半径大k 值不准IMU 读实际转角按比例修正 k高频抖动反馈噪声、周期过短断电机只看速度曲线加滤波、放宽周期通讯乱码波特率与时钟不匹配换波特率测试核对晶振与 PLL 配置运行中复位电源纹波、共地干扰示波器量 3.3V 轨独立供电、加去耦电容这张表我打印出来贴在工位上出问题先对照着走一遍基本能覆盖八成以上的故障。6.3 一些不太写在文档里的调试技巧第一个技巧是分步验证永远不要一次把整个系统跑起来。我的顺序是先只开 PWM 验证电机转向再单独验证编码器计数方向和倍频系数再做单轮速度闭环然后四个轮子一起闭环做直线和旋转最后才上位置环。每一步都有明确的成功标准比如单轮闭环的标准是“给 200mm/s 的目标实测稳态在 195 到 205 之间阶跃响应三次内收敛”。第二个技巧是让数据可视化。串口打印四个轮子的目标速度和实际速度用上位机软件画成曲线比盯着车看有用得多。我调速度环的时候屏幕上四条曲线一目了然谁跟不上、谁超调、谁在有稳态误差全都看得见。纯靠肉眼观察车的行为很多细节会被掩盖。第三个技巧是给关键变量做“软示波器”。没有示波器的时候可以用一个空闲的定时器做微秒级计时把关键代码段的执行时间打印出来。比如速度环一次执行要多久、运动学解算要多久。如果总时间超过了控制周期说明该优化代码或者降低控制频率了。我最早在 F103 上把 float 运算写得很随意速度环执行时间到了 1.2ms接近 5ms 周期的四分之一改成定点运算之后降到 300μs。第四个技巧是低温环境要注意电池内阻。锂电在十几度的环境下内阻会上升大电流放电时压降更明显速度环的稳态误差会变大。如果冬天调试发现参数突然不好用了先怀疑电池而不是去改 PID。第五个技巧也是我个人体会最深的一点底盘机械装配的精度比软件参数的影响大得多。四个轮子的高度差如果超过 2mm车跑起来就会有轮子打滑编码器读得再准也没用。我后来专门做了一个找平工装装配时保证四个轮子在同一平面上装完之后直线精度直接提升了一个档次。所以别把所有问题都往代码上推有时候动手拧两颗螺丝比调半天参数管用。后续如果你想继续往上走可以在里程计里融合激光雷达或者超声波做闭环定位把累积误差彻底做掉也可以在位置环外面再套一层轨迹规划让底盘沿着贝塞尔曲线走运动会更顺滑。我目前这台车做到位置控制这一层室内定位精度能稳定在正负 2 厘米配合航向角闭环做定点对接和路径跟踪已经够用了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价