)
1. STM32F103C8T6最小系统板与两轮平衡小车基础两轮平衡小车是学习嵌入式开发的经典项目而STM32F103C8T6最小系统板凭借其性价比和丰富的外设资源成为很多开发者的首选。这块板子虽然只有拇指大小但内置了72MHz主频的Cortex-M3内核、定时器、PWM输出和编码器接口等关键功能完全能满足平衡小车的控制需求。我第一次用这块板子做平衡车时最直观的感受就是它的GPIO配置非常灵活。比如PA8和PA11可以复用为TIM1的PWM输出通道PB12-PB15既能控制电机方向又能读取编码器信号。这种设计大大减少了外部扩展电路的需求让整个系统更加紧凑。不过要注意的是TIM1属于高级定时器和普通定时器相比多了重复计数器和刹车功能初始化时需要特别注意MOE主输出使能位的配置。2. 电机驱动模块的实战设计2.1 PWM调速原理与实现PWM调速就像用开关水龙头的方式控制水流大小。通过快速切换高低电平通常频率在1kHz-20kHz改变占空比高电平持续时间比例来等效调节电压。在STM32上配置PWM时有三个关键参数ARR自动重装载值决定PWM周期PSC预分频系数调整定时器时钟频率CCR捕获比较值直接控制占空比我常用的配置是72MHz主频下设置PSC0ARR7199这样得到的PWM频率正好是10kHz72000000/(71991)10kHz。实际调试中发现电机在5kHz以下会有明显啸叫而超过20kHz又会增加开关损耗10kHz是个不错的折中点。2.2 电机方向控制技巧控制直流电机正反转需要H桥电路用STM32的GPIO直接驱动MOSFET时要特别注意死区时间。有次调试时电机突然短路就是因为上下桥臂同时导通导致电源直通。后来我在代码里加入了软件死区控制void Set_Motor_Direction(int dir) { if(dir FORWARD) { GPIO_ResetBits(DIR_PIN1); delay_us(5); // 加入5μs死区 GPIO_SetBits(DIR_PIN2); } else { GPIO_ResetBits(DIR_PIN2); delay_us(5); GPIO_SetBits(DIR_PIN1); } }另外建议在电机电源端并联大容量电解电容我用的470μF/25V能有效抑制PWM切换时的电压波动。3. 编码器数据采集的优化方案3.1 正交编码器接口配置常见的增量式编码器输出A、B两相90度相位差的脉冲信号。STM32的定时器自带编码器接口模式可以自动识别转向和计数。配置时要注意输入引脚必须配置为浮空输入模式滤波器参数根据实际信号质量设置我一般用TIM_ICFilter10计数方向与电机实际转向要对应遇到过最头疼的问题是编码器计数溢出。有次小车跑着跑着突然失控查了半天发现是TIM_GetCounter()返回的值超过了int16_t范围。后来改用定期读取并清零的策略int32_t total_count 0; void TIM4_IRQHandler() { if(TIM_GetITStatus(TIM4, TIM_IT_Update)) { total_count 32767 * 2; // 补偿溢出量 TIM_ClearITPendingBit(TIM4, TIM_IT_Update); } }3.2 速度计算与滤波直接从编码器获取的是位置脉冲数需要转换成速度值。简单做法是用固定周期如10ms的脉冲差值但这样噪声很大。实测下来采用移动平均滤波效果不错#define FILTER_LEN 5 int speed_buf[FILTER_LEN]; int get_filtered_speed() { static int index 0; speed_buf[index] Read_Speed(TIM3); index (index 1) % FILTER_LEN; int sum 0; for(int i0; iFILTER_LEN; i) { sum speed_buf[i]; } return sum / FILTER_LEN; }如果想更精确可以加上卡尔曼滤波不过对STM32F103来说计算量有点大。4. PID控制算法的工程实践4.1 参数整定经验分享调PID就像炒菜火候参数不对味道效果就差很远。我的调试步骤一般是先设Ki0,Kd0慢慢增大Kp直到小车出现等幅振荡取此时Kp值的60%作为基准加入Ki消除静差从小值开始逐步增加最后加Kd抑制超调具体到平衡小车角度环和速度环的参数差异很大。通常角度环的Kp在20-50之间而速度环的Kp可能只有0.1-0.5。有个小技巧先用手机的水平仪测量小车机械中位确保MPU6050安装绝对水平这样能减少零点漂移带来的干扰。4.2 抗积分饱和处理积分项累积会导致windup现象表现为控制延迟和超调。我的解决办法是设定积分限幅只在误差较小时进行积分加入遇限削弱因子typedef struct { float Kp, Ki, Kd; float integral; float max_output; float max_integral; } PID_Controller; float PID_Update(PID_Controller* pid, float error, float dt) { // 比例项 float Pout pid-Kp * error; // 积分项带抗饱和 if(fabs(error) 0.5f) { // 只在误差较小时积分 pid-integral error * dt; if(pid-integral pid-max_integral) pid-integral pid-max_integral; else if(pid-integral -pid-max_integral) pid-integral -pid-max_integral; } float Iout pid-Ki * pid-integral; // 微分项 static float last_error; float Dout pid-Kd * (error - last_error) / dt; last_error error; // 综合输出 float output Pout Iout Dout; if(output pid-max_output) output pid-max_output; else if(output -pid-max_output) output -pid-max_output; return output; }5. 系统整合与调试技巧把各模块组合起来时定时器的中断优先级配置很关键。我的优先级安排是最高MPU6050的DMP中断姿态解算中级编码器计数中断最低PID计算和电机控制建议使用FreeRTOS或者简单的状态机来管理任务周期。比如姿态解算1kHzTIM2触发PID计算200HzTIM3触发速度测量100HzTIM4触发调试时一定要先确保硬件没问题。有次调了三天PID没效果最后发现是电机线虚焊。建议的检查顺序用示波器看PWM波形是否正常手动转动电机观察编码器计数变化通过串口打印实时角度和速度数据最后才调PID参数