简介:基于STM32F103ZET6的麦克纳姆轮小车驱动底板设计资料,是一套个人大四期间经导师指导并高分通过的毕业设计/课程设计项目。资源面向嵌入式、计算机相关专业正在做课设毕设的学生,也适合需要项目实战练习的入门到进阶学习者,可系统解决麦克纳姆轮小车驱动底板的硬件电路设计与STM32程序开发问题,覆盖从原理图设计、PCB布局到固件调试的关键环节。压缩包共396个文件,约59.93MB,其中以166个C源码和152个H头文件构成完整嵌入式程序框架,配合12个PCB封装与10个原理图文档,清晰展示驱动底板电路连接;还包含汇编启动文件、HEX固件、Keil工程配置及设备器件清单,便于直接烧录验证或按需修改。目前已有203人学习下载。资料附带项目说明与关键脚本,既可用于课程设计、期末大作业,也能作为学习借鉴和二次开发的基础,对想掌握STM32小车开发全流程的读者有较高参考价值。
1. 麦克纳姆轮小车为什么要把运动学解算跑在STM32F103ZET6驱动底板上
麦克纳姆轮底盘和普通差速底盘的根本差异不在轮子外形,而在控制自由度。四个轮子独立驱动后,底盘可以同时做前后移动、左右平移和原地旋转三个方向的运动。很多人拿到这类源码后直接在应用层发PWM值,结果负载一变化四个轮子互相别劲,车身摆、电流大、跑不成直线。这套驱动底板把方案定得很明确:STM32F103ZET6负责麦克纳姆轮运动学解算、编码器闭环和指令解析,板子把电源、电机驱动和主控集成在一起,整台STM32F103ZET6智能小车只需要外部供一路电池电、往串口发目标速度就能动起来。
选F103ZET6而不是小封装型号,理由是这颗512KB Flash和112引脚的主控能把四路编码器接口、四路PWM、两路串口、SWD调试口全部原生承载,毕设进行到第二阶段要加IMU或者WiFi模块时不用重新画板。下载包内是完整Keil工程加PCB源文件,可以直接作为课程设计、期末大作业的移动平台,也适合做驱动层的二次开发起点。
2. 驱动底板硬件:电机驱动选型、电源树与PCB走线规则
2.1 驱动芯片选型:在TB6612FNG与BTN7971之间做决定
麦克纳姆轮小车是四电机系统,选驱动芯片时对比的不是理论峰值电流,而是“四个电机同时堵转”时的母线电流。TB6612FNG和BTN7971是两套完全不同的设计路线,决定了PCB的铜箔面积和散热方式。
| 对比项 | TB6612FNG | BTN7971 |
|---|---|---|
| 单路连续电流 | 1.2A,峰值3.2A | 20A |
| 封装形态 | SSOP-24,手工焊接难度中等 | T022-7,焊盘带散热片 |
| PWM频率上限 | 100kHz | 25kHz,推荐10~15kHz |
| 外围电路 | 逻辑电源去耦电容 | 需要自举电容、RC滤波 |
| 典型应用 | 小型直流减速电机 | 大扭矩底盘、竞赛电机 |
如果毕设选的是JGA25-370这类额定电流1A以内的减速电机,TB6612FNG是够用的。但要注意一点:麦克纳姆轮四轮同时从静止加速时,启动瞬时电流很容易冲到1.5A以上。我的经验是驱动底板选TB6612时一定要检查铜箔——电源输入到VM引脚的走线宽度至少要2mm,最好加铺铜,否则大电流时线路压降会把VM电压拉低,导致驱动芯片进入欠压保护。
切换BTN7971则完全是另一套思路。每个电机需要两片BTN7971组成半桥对,光驱动通道就要8片,散热要求也高。所以驱动底板在设计时得把电机驱动区域固定成一种方案,不要想着上了一个板子再飞线兼容,飞线在大电流下会变成热源。
2.2 电源树设计:12V电机电与3.3V逻辑电必须做单点接地
这个驱动底板的电源树直接决定系统稳定性。12V电池输入后,先经过SS34肖特基二极管防反接,然后兵分两路:一路直接给驱动芯片VM引脚供电,另一路经MP1584降压到5V,再经AMS1117-3.3给STM32F103ZET6供电。
电池12V ──> SS34 ──> 驱动芯片VM(电机电源域) │ │ │ └──> MP1584(DC-DC 5V) ──> AMS1117-3.3 ──> MCU VCC │ └──> 分压电阻(100K/10K) ──> PA1(ADC电压监测)电机启动和堵转时,电池线上会有很大的电流尖峰。如果3.3V电源的地和VM的地直接大面积连通,这个尖峰就会通过地平面耦合进MCU,表现为小车一动串口就乱码、程序死机。正确的做法是分电源域,在PCB上用磁珠或0Ω电阻把模拟地、数字地、电机地在STM32的GND焊盘附近做单点汇合。DC-DC的反馈电阻取电压采样点要放在输出电容之后,不要远距离拉到接插件处再回来,否则输出纹波会明显偏大。
2.3 布局布线细节与AD20 DRC检查
麦克纳姆轮驱动底板的PCB面积通常不大,但上面既有大电流回路又有MCU最小系统,布局密度高。布线时我把电机驱动区域的规则单独设置:
- 电机输出A+/A-、B+/B-走线宽度2mm,铜厚2oz时连续电流余量充足;
- PWM信号线和编码器信号线与电机线保持至少3倍线宽的间距,避免磁场耦合;
- STM32的VDDA引脚单独用10Ω电阻和1μF电容做模拟电源滤波,VREF+接3.3V;
- 去耦电容放置顺序是100nF靠近电源脚,10μF钽电容在稍远外圈,不能只放一个大的。
用AD20做PCB DRC检查时,有一个设置很多人会忽略:默认规则里Power Net的间距往往小于Signal Net。如果给GND单独设置了0.3mm间距,但规则优先级里默认的0.254mm间距规则在上层,DRC不会报错,但实际板厂会按更松的规则做。检查规则优先级时把“Supersede Rules”选项打开,再重新跑一次DRC,重点看Un-Routed和Clearance约束的输出结果。
提示:投板前还要在AD20里执行Tools->Fabrication->Fabrication Features检查,确认丝印没有压到焊盘,否则回流焊时丝印会干扰上锡。
3. 麦克纳姆轮运动学解算:从四轮线速度到底盘速度的映射
3.1 逆运动学公式与坐标约定
麦克纳姆轮的辊子与轮体轴线呈45度角,轮子转动时地面对辊子施加的摩擦力可以分解为沿轮轴方向和垂直辊子方向两个分量。对四个轮子的力进行矢量合成,就得到了底盘的三自由度速度模型。设底盘中心速度为(vx, vy),旋转角速度为ω,轮子半径为R,底盘中心到前/后轮轴线的距离为a,到左/右轮中心线的距离为b,四个轮子的线速度如下:
轮1(前左):V1 = (Vx − Vy − (a + b)·ω) / R
轮2(前右):V2 = (Vx + Vy + (a + b)·ω) / R
轮3(后左):V3 = (Vx + Vy − (a + b)·ω) / R
轮4(后右):V4 = (Vx − Vy + (a + b)·ω) / R
这个公式隐含了一个前提:四个辊子安装方向必须是前左轮和后右轮为右旋45度,前右轮和后左轮为左旋45度。PCB板或装配图里辊子装反是麦克纳姆轮小车运动学解算最常见的物理错误,表现是前进时正常,左平移时车体却发生了明显的旋转。遇到这种情况不用改代码,先把四个轮子拆下来重新对一遍辊子朝向。
3.2 STM32F103ZET6上的解算函数实现
运动学解算是纯算术逻辑,适合放在主循环或定时器中断里周期性调用,不需要占用独立任务。下面这段代码在工程里对应mecanum.c,输入底盘目标速度,输出四轮线速度,单位统一为m/s。
// mecanum.c - 基于STM32F103ZET6的麦克纳姆轮逆运动学解算 #define WHEEL_RADIUS 0.045f // 轮子等效半径,单位:m #define ROBOT_HALF_WIDTH 0.090f // 底盘中心到前/后轮轴距离,单位:m #define ROBOT_HALF_LENGTH 0.095f // 底盘中心到左/右轮中心距离,单位:m void Mecanum_IK(float vx, float vy, float wz, float wheel_vel[4]) { // wz正值为顺时针旋转 float sum = ROBOT_HALF_WIDTH + ROBOT_HALF_LENGTH; wheel_vel[0] = ( vx - vy - sum * wz) / WHEEL_RADIUS; // 前左轮 FL wheel_vel[1] = ( vx + vy + sum * wz) / WHEEL_RADIUS; // 前右轮 FR wheel_vel[2] = ( vx + vy - sum * wz) / WHEEL_RADIUS; // 后左轮 RL wheel_vel[3] = ( vx - vy + sum * wz) / WHEEL_RADIUS; // 后右轮 RR }wheel_vel数组输出的是车轮线速度,后续PID闭环的控制目标就是这个值。注意这里除的是等效半径WHEEL_RADIUS,不是轮毂半径。四路结果同时包含正负号,负号代表反转,代码在进入PWM函数前还要做一个方向判断,把负速度映射到反方向占空比上。
3.3 关键参数标定:轮径不等于轮子直径
麦克纳姆轮的辊子外表面不是完整的圆形,直接用卡尺量轮子最外沿得到的直径是偏大的,因为辊子间隙导致等效接触圆变小。正确做法是标定前进距离的映射关系:在地面贴一条长胶带,标记轮子某一根辊子作为起始位,手动推着小车让轮子转20圈,量行走距离。等效半径 = 行走距离 / (20 × 2π)。
| 参数 | 错误做法 | 正确做法 |
|---|---|---|
| WHEEL_RADIUS | 直接量轮子直径再除以2 | 转20圈实测距离反算 |
| ROBOT_HALF_WIDTH | 用底盘最外沿宽度 | 左右轮子接地中线的距离/2 |
| ROBOT_HALF_LENGTH | 凭车架图纸估计 | 前轴与后轴轮子中心距/2 |
a和b的标定误差不会在空载时暴露,但跑边长1米的正方形轨迹时,旋转项(a+b)·ω会放大误差,轨迹会不闭合。用正运动学反推也可以验证参数正确性,即用四个编码器的实测速度反算底盘速度,对比期望值是否一致。
4. STM32程序实现:定时器PWM、增量式PID与编码器接口
4.1 定时器资源分配与PWM频率选择
F103ZET6的定时器资源在这个项目里分配得很紧凑。四路电机PWM使用TIM3和TIM4分别输出两组,编码器接口使用TIM2和TIM5的编码器模式,串口通信占用USART1。预留TIM1作为高级定时器,后续如果需要输出带死区的互补PWM驱动大功率MOS管,可以直接复用。
| 定时器 | 通道分配 | 工作模式 | 用途 |
|---|---|---|---|
| TIM3 | CH1~CH4 | PWM输出 | 四路电机PWM信号 |
| TIM4 | CH1~CH4 | PWM输出 | 备用(舵机或预留) |
| TIM2 | CH1~CH2/CH3~CH4 | 编码器模式 | FL、FR轮转速反馈 |
| TIM5 | CH1~CH2/CH3~CH4 | 编码器模式 | RL、RR轮转速反馈 |
| TIM1 | CH1~CH3 | PWM互补输出 | 预留大功率驱动 |
PWM频率我选10kHz。这个频率对减速电机来说足够低,不会产生明显的线圈啸叫;又比TB6612的上限低很多,留有换向死区的余量。PC6、PC7等实现方向控制的GPIO引脚输出电平变换必须在PWM更新之前完成,否则电机换向的瞬间会出现一个占空比冲突的毛刺,长期冲击驱动芯片。
4.2 PWM初始化与方向控制逻辑
下面这段是TIM3四路PWM通道配置的核心代码,其中关键点是使用PWM1模式,ARR设为3600-1,预分频器设为36-1。TIM3挂在APB1上,时钟36MHz,所以实际PWM频率=36MHz / (3600 × 36) ≈ 277Hz。如果要在10kHz工作,预分频值应设为0,自动重装载值设为3600-1。
// motor.c - 基于STM32F103ZET6的标准库初始化 static void TIM3_Config(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tim; TIM_OCInitTypeDef oc; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); // PA6/PA7 -> TIM3_CH1/CH2(前两轮PWM) GPIO_StructInit(&gpio); gpio.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); // PB0/PB1 -> TIM3_CH3/CH4(后两轮PWM) gpio.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_Init(GPIOB, &gpio); TIM_StructInit(&tim); tim.TIM_Period = 3600 - 1; // 10kHz,占空比分辨率0.027% tim.TIM_Prescaler = 0; tim.TIM_ClockDivision = TIM_CKD_DIV1; tim.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &tim); TIM_StructInit(&oc); // 需要重新初始化结构体 oc.TIM_OCMode = TIM_OCMode_PWM1; oc.TIM_OutputState = TIM_OutputState_Enable; oc.TIM_Pulse = 0; oc.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM3, &oc); TIM_OC2Init(TIM3, &oc); TIM_OC3Init(TIM3, &oc); TIM_OC4Init(TIM3, &oc); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC3PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC4PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_Cmd(TIM3, ENABLE); }方向控制由TB6612的IN1/IN2引脚决定。每个电机的方向占用两个GPIO,在PWM配置完成后单独初始化。占空比写入使用TIM_SetComparex函数,代码里需要先判断目标线速度的正负:正数则IN1=1、IN2=0,写入占空比;负数则IN1=0、IN2=1,写入占空比绝对值。这里要特别注意在切换方向前先把PWM设为0,否则会有一个极短时间的电制动,对齿轮损伤较大。
4.3 增量式PID速度环:让四个轮子真正同步
四个麦克纳姆轮如果只是开环PWM驱动,负载变化会导致转速漂移,底盘运动轨迹完全不可预期。所以程序里对每个轮子都做了增量式PID闭环,控制频率1kHz,由SysTick中断驱动。PID的目标量是3.2节解算出的线速度,反馈量来自编码器测速。
// pid.c - 增量式PID控制器 typedef struct { float kp; // 比例系数 float ki; // 积分系数 float kd; // 微分系数 float target; // 目标线速度 m/s float integral; // 积分累计值 float last_error; // 上次误差 int16_t pwm_out; // PWM增量输出 } MotorPID; void PID_Calc(MotorPID *pid, float current_speed) { float error = pid->target - current_speed; pid->integral += error; // 积分限幅,防止低速长时间堵转导致积分饱和 if (pid->integral > 50.0f) pid->integral = 50.0f; if (pid->integral < -50.0f) pid->integral = -50.0f; float derivative = error - pid->last_error; pid->last_error = error; // 增量式输出,只需要累加PID系数与误差的组合 pid->pwm_out += (int16_t)(pid->kp * error + pid->ki * pid->integral + pid->kd * derivative); if (pid->pwm_out > 3599) pid->pwm_out = 3599; if (pid->pwm_out < -3599) pid->pwm_out = -3599; }增量式PID输出的是PWM占空比增量而不是绝对值,好处是没有积分饱和的累积阶跃,转速变化时车体冲击小。实际调试时先给kp一个较小值如0.5,ki先从0开始,观察轮子是否来回振荡。如果轮子发出高频“嗡嗡”声,说明kd太大,把微分项放0.1再调。
5. 联调实战:单轮测试、全向验证与串口指令封装
5.1 单轮方向与编码器极性验证
焊接完驱动底板第一次上电,先断开所有电机线,用万用表量驱动芯片VM对地电压为12V、VCC对地3.3V后再接电机。下载程序后不要直接跑PID,而是写一段单轮开环测试:
static void Wheel_OpenLoop_Test(uint8_t idx, int16_t pwm_value) { // 以左前轮FL为例:PA6 PWM,PC0/PC1方向 if (idx == 0) { GPIO_WriteBit(GPIOC, GPIO_Pin_0, Bit_SET); GPIO_WriteBit(GPIOC, GPIO_Pin_1, Bit_RESET); TIM_SetCompare1(TIM3, (uint16_t)pwm_value); } Delay_Ms(3000); TIM_SetCompare1(TIM3, 0); }轮子正转时观察编码器计数值。如果计数为负,说明编码器A/B相接反了,此时可以选择在TIM编码器配置里交换CH1和CH2的极性,也可以直接调换电机编码器排线。务必在标签纸上标好每个轮子的PWM通道、方向引脚和编码器计数方向,四个轮子全部确认后再进入闭环调试。
5.2 全向运动验证矩阵
闭环调通后,用串口依次发送三种运动指令,验证底盘响应是否符合预期。下表是三种典型指令对应的输入参数和预期运动:
| 指令类型 | vx (m/s) | vy (m/s) | wz (rad/s) | 预期运动 |
|---|---|---|---|---|
| 前进 | 0.20 | 0 | 0 | 沿车头方向直线前进 |
| 左平移 | 0 | 0.20 | 0 | 整体向左平移,车头方向不变 |
| 原地右转 | 0 | 0 | 0.50 | 绕底盘中心顺时针旋转 |
如果前进时车体跑偏,优先检查左右两侧轮子的WHEEL_RADIUS标定是否一致。如果左平移伴随旋转,把前左轮和后右轮的辊子方向和公式再次比对。原地旋转必须严格不产生平移,如果旋转时底盘画圈,说明a+b参数偏大或偏小。
5.3 串口指令协议与中断同步封装
联调阶段最实用的是封装一个极简串口指令协议。我这里用$开头、逗号分隔三个浮点、\n结尾的格式,波特率115200:
// 串口中断里只做帧接收,不解析 void USART1_IRQHandler(void) { static uint8_t buf[32]; static uint8_t len = 0; if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { uint8_t ch = USART_ReceiveData(USART1); if (ch == '$') { len = 0; } else if (ch == '\n' && len > 0) { buf[len] = '\0'; sscanf((char *)buf, "%f,%f,%f", &cmd_vx, &cmd_vy, &cmd_wz); cmd_ready = 1; len = 0; } else { if (len < 31) buf[len++] = ch; } } }中断里只做收帧和标志位置位,运动学解算和PID计算放在主循环里读取cmd_ready后执行。这样串口数据不会打断PWM更新时序,也避免了在中断里调用耗时函数导致的定时器抖动。
本文还有配套的精品资源,点击获取