前言很多新手在备战电赛时最容易犯的致命错误就是把单片机当成单线程的傻瓜机。代码里充斥着 delay_ms(1000)导致按下按键没反应、小车遇到障碍物直接撞穿因为单片机正在“死等”延时结束根本没空处理突发事件同时PID 控制作为电赛控制类B题、C题、小车、无人机的绝对核心很多同学调参全靠“随缘”调了三天三夜小车依然抖得像帕金森。本文将带你彻底告别 delay手把手教你搭建**“时间片轮询”软件架构**并深度扒开PID 和串级 PID 的底层逻辑与调参玄学TOC一、 软件架构革命告别 delay()拥抱时间片轮询在电赛的复杂系统中你需要同时干很多事读取传感器10ms一次、计算 PID5ms一次、刷新 OLED 屏幕50ms一次、处理视觉串口数据随时发生。如果你用 delay()整个系统就会卡死。如果上 FreeRTOS 操作系统很多新手又觉得太难、出 Bug 查不出来。 黄金方案基于 SysTick 的“时间片轮询”架构裸机天花板我们只需要利用 STM32 滴答定时器SysTick生成一个全局的时间戳类似 Arduino 的 millis()就能实现完美的非阻塞多任务核心代码实现直接抄作业首先在滴答定时器中断里或者开一个 1ms 的通用定时器维护一个全局变量codeCvolatile uint32_t sys_tick 0; // 全局系统时间(毫秒) // STM32 的 SysTick 中断回调函数每1ms触发一次 void SysTick_Handler(void) { HAL_IncTick(); // HAL库自带 sys_tick; // 我们的时间戳 }在主循环 main() 中利用时间差来执行任务绝对不阻塞主循环codeCuint32_t timer_PID 0; uint32_t timer_OLED 0; uint32_t timer_Sensor 0; int main(void) { // ... 初始化硬件 ... while(1) { // 1. PID 控制任务 (每 5ms 执行一次) if (sys_tick - timer_PID 5) { timer_PID sys_tick; Motor_PID_Control(); } // 2. 传感器读取任务 (每 20ms 执行一次) if (sys_tick - timer_Sensor 20) { timer_Sensor sys_tick; Read_MPU6050(); } // 3. OLED 刷新任务 (每 50ms 执行一次) if (sys_tick - timer_OLED 50) { timer_OLED sys_tick; OLED_Refresh(); } // 4. 串口解析任务 (无条件全速执行有数据就处理) Vision_Data_Parse(); } }优势CPU 利用率拉满OLED 刷新慢不会影响 PID 的计算传感器卡顿也不会让电机失控。这就是历年国奖队伍最常用的极简高鲁棒性架构。二、 控制之魂PID 算法底层解密与代码封装网上的 PID 公式一堆微积分看着就头疼。在单片机里微积分其实就是累加和作差。1. PID 三个兄弟到底是干嘛的假设你要控制小车速度达到 100P (比例/现在的误差)小车当前速度是 20距离 100 差了 80。P 就是一巴掌拍过去误差越大给电机的力度越大。但只有 P 会导致系统永远达不到目标稳态误差。I (积分/过去的误差)把每一次差的值累加起来。只要车速没到 100I 就会不断积攒力量强行把车速推到 100。专治各种不服消除稳态误差。D (微分/未来的误差)计算当前误差和上一次误差的差值。如果车速正在以极快的速度飙向 100D 就会敏锐地察觉到“快刹车要超头了”。专门用来抗击震荡、防止超调。2. 位置式 PID 核心 C 语言封装建立一个标准化的 PID 结构体比赛时直接拿去创建多个实例左轮、右轮、云台俯仰、云台偏航。codeCtypedef struct { float Kp; float Ki; float Kd; float target; // 目标值 float measured; // 测量值 float err; // 当前误差 float err_last; // 上次误差 float integral; // 误差积分 float output; // PID输出 float out_max; // 输出限幅 (必须有) float integral_max;// 积分限幅 (防积分饱和) } PID_TypeDef; /** * brief 位置式 PID 计算 * param pid: PID 结构体指针 * param target: 目标值 * param measured: 传感器实际测量值 * retval PID 输出控制量 (直接给 PWM) */ float PID_Calc(PID_TypeDef *pid, float target, float measured) { pid-target target; pid-measured measured; pid-err pid-target - pid-measured; // 1. 积分项累加并限幅 (防止积分抗饱和俗称I炸了) pid-integral pid-err; if(pid-integral pid-integral_max) pid-integral pid-integral_max; if(pid-integral -pid-integral_max) pid-integral -pid-integral_max; // 2. PID 核心算式 (P I D) pid-output (pid-Kp * pid-err) (pid-Ki * pid-integral) (pid-Kd * (pid-err - pid-err_last)); // 3. 更新上次误差 pid-err_last pid-err; // 4. 总输出限幅 (保护电机和硬件) if(pid-output pid-out_max) pid-output pid-out_max; if(pid-output -pid-out_max) pid-output -pid-out_max; return pid-output; }三、 高阶必杀技串级 PID (Cascade PID)在做倒立摆、平衡车、或者无人机时单环 PID 根本稳不住你必须使用串级 PID。什么是串级 PID串级就是两个 PID 套娃外环控制位置角度内环控制速度角速度。核心逻辑外环的输出作为内环的目标值。通俗例子平衡车外环角度环发现车往前倾斜了 10 度。外环 PID 计算后输出“我需要轮子以 50 转/秒的速度往前开才能把车身扶正”内环速度环接收到外环的指令目标速度50读取轮子编码器当前速度内环 PID 疯狂调节电机 PWM死死咬住 50 的速度。codeC// 串级 PID 伪代码示例 void Cascade_PID_Control(void) { // 1. 外环输入目标角度输出期望速度 float target_speed PID_Calc(PID_Angle, Target_Angle, Current_Angle); // 2. 内环输入期望速度输出电机 PWM float motor_pwm PID_Calc(PID_Speed, target_speed, Current_Speed); // 3. 执行机构 Set_Motor_PWM(motor_pwm); }避坑外环的计算频率可以低一点如 10ms内环的计算频率必须极高如 2ms 或 5ms因为内环需要极速响应。四、 拒绝玄学科学的 PID 调参指南多少人调 PID 是随便敲几个数字进去然后祈祷奇迹发生记住调参口诀先调内环再调外环先调 P再调 D最后调 I。调参利器推荐千万别用 printf 看数字用眼睛看数字是看不出震荡趋势的必须使用上位机串口示波器强烈推荐VOFA或者匿名上位机。将 目标值 和 实际值 通过串口发给电脑看波形图科学调参步骤以电机速度环为例归零将 Ki 和 Kd 全部设为 0。调 Kp加弹性从小到大慢慢加 Kp。看波形实际速度开始逼近目标速度但无法完全重合存在稳态误差。加到什么时候停止加到实际速度产生高频的小幅震荡然后把此时的 Kp乘以 0.6 ~ 0.8。调 Ki消误差加入 Ki从小慢慢加。看波形你会发现原本存在的“稳态误差”被慢慢消除了实际曲线和平稳地贴合在目标曲线上。注意Ki 太大曲线会缓慢地上下大波浪摇摆。调 Kd加阻尼速度环通常不需要 Kd因为编码器测速本身就有噪声Kd会把噪声无限放大导致电机抽搐。如果是位置环/角度环遇到小车到了目标位置还前后晃荡超调就需要加 Kd。Kd 能让小车在快到终点时“踩刹车”。五、 电赛常见控制类翻车现场 QAQ1电机在目标位置附近疯狂滋滋响小幅度抽搐原因你的 P 给大了或者 D 给太大了放大了传感器噪声还有可能是**齿轮间隙死区**导致的来回跳动。对策加入死区判断。当 abs(err) 2误差很小时直接强制 output 0让电机休息。Q2小车一遇到障碍物卡住稍微一推它就疯了一样以最大速度冲出去原因积分爆炸I 饱和。车被卡住时误差一直存在I 项一直在累加累加到了一个天际线。等障碍物一移开释放出的巨大力量直接让车失控。对策必须在代码里加上积分限幅参考上文代码的 integral_max或者使用抗积分饱和算法Anti-windup当误差极大时停止积分累加。Q3下坡或者外界给干扰时电机完全软弱无力原因你可能只写了位置环没写速度环。或者你没接入硬件编码器对策没有编码器闭环的电机是没有灵魂的务必使用带霍尔编码器的电机启用 STM32 定时器的 Encoder 模式读取脉冲形成真正的闭环控制。结语在电赛的赛场上代码的优雅与否直接决定了你能睡多少个小时。抛弃 delay 拥抱时间片架构你的系统将百毒不侵理解了 PID 的底层逻辑与上位机调参你将彻底告别“盲盒调参”的痛苦。预祝各位控制组、小车组、无人机组的同学波形完美贴合小车指哪打哪顺利拿下国一觉得有用的话别忘了点赞 ⭐收藏调参崩溃的时候翻出来看一遍你在调 PID 时遇到过什么奇葩现象或者调参调到深夜有什么感悟欢迎在评论区留言交流有问必答
【电赛保姆级教程】别再用 delay() 死等了!电赛软件架构与 PID 调参硬核避坑指南(附 STM32 源码)
前言很多新手在备战电赛时最容易犯的致命错误就是把单片机当成单线程的傻瓜机。代码里充斥着 delay_ms(1000)导致按下按键没反应、小车遇到障碍物直接撞穿因为单片机正在“死等”延时结束根本没空处理突发事件同时PID 控制作为电赛控制类B题、C题、小车、无人机的绝对核心很多同学调参全靠“随缘”调了三天三夜小车依然抖得像帕金森。本文将带你彻底告别 delay手把手教你搭建**“时间片轮询”软件架构**并深度扒开PID 和串级 PID 的底层逻辑与调参玄学TOC一、 软件架构革命告别 delay()拥抱时间片轮询在电赛的复杂系统中你需要同时干很多事读取传感器10ms一次、计算 PID5ms一次、刷新 OLED 屏幕50ms一次、处理视觉串口数据随时发生。如果你用 delay()整个系统就会卡死。如果上 FreeRTOS 操作系统很多新手又觉得太难、出 Bug 查不出来。 黄金方案基于 SysTick 的“时间片轮询”架构裸机天花板我们只需要利用 STM32 滴答定时器SysTick生成一个全局的时间戳类似 Arduino 的 millis()就能实现完美的非阻塞多任务核心代码实现直接抄作业首先在滴答定时器中断里或者开一个 1ms 的通用定时器维护一个全局变量codeCvolatile uint32_t sys_tick 0; // 全局系统时间(毫秒) // STM32 的 SysTick 中断回调函数每1ms触发一次 void SysTick_Handler(void) { HAL_IncTick(); // HAL库自带 sys_tick; // 我们的时间戳 }在主循环 main() 中利用时间差来执行任务绝对不阻塞主循环codeCuint32_t timer_PID 0; uint32_t timer_OLED 0; uint32_t timer_Sensor 0; int main(void) { // ... 初始化硬件 ... while(1) { // 1. PID 控制任务 (每 5ms 执行一次) if (sys_tick - timer_PID 5) { timer_PID sys_tick; Motor_PID_Control(); } // 2. 传感器读取任务 (每 20ms 执行一次) if (sys_tick - timer_Sensor 20) { timer_Sensor sys_tick; Read_MPU6050(); } // 3. OLED 刷新任务 (每 50ms 执行一次) if (sys_tick - timer_OLED 50) { timer_OLED sys_tick; OLED_Refresh(); } // 4. 串口解析任务 (无条件全速执行有数据就处理) Vision_Data_Parse(); } }优势CPU 利用率拉满OLED 刷新慢不会影响 PID 的计算传感器卡顿也不会让电机失控。这就是历年国奖队伍最常用的极简高鲁棒性架构。二、 控制之魂PID 算法底层解密与代码封装网上的 PID 公式一堆微积分看着就头疼。在单片机里微积分其实就是累加和作差。1. PID 三个兄弟到底是干嘛的假设你要控制小车速度达到 100P (比例/现在的误差)小车当前速度是 20距离 100 差了 80。P 就是一巴掌拍过去误差越大给电机的力度越大。但只有 P 会导致系统永远达不到目标稳态误差。I (积分/过去的误差)把每一次差的值累加起来。只要车速没到 100I 就会不断积攒力量强行把车速推到 100。专治各种不服消除稳态误差。D (微分/未来的误差)计算当前误差和上一次误差的差值。如果车速正在以极快的速度飙向 100D 就会敏锐地察觉到“快刹车要超头了”。专门用来抗击震荡、防止超调。2. 位置式 PID 核心 C 语言封装建立一个标准化的 PID 结构体比赛时直接拿去创建多个实例左轮、右轮、云台俯仰、云台偏航。codeCtypedef struct { float Kp; float Ki; float Kd; float target; // 目标值 float measured; // 测量值 float err; // 当前误差 float err_last; // 上次误差 float integral; // 误差积分 float output; // PID输出 float out_max; // 输出限幅 (必须有) float integral_max;// 积分限幅 (防积分饱和) } PID_TypeDef; /** * brief 位置式 PID 计算 * param pid: PID 结构体指针 * param target: 目标值 * param measured: 传感器实际测量值 * retval PID 输出控制量 (直接给 PWM) */ float PID_Calc(PID_TypeDef *pid, float target, float measured) { pid-target target; pid-measured measured; pid-err pid-target - pid-measured; // 1. 积分项累加并限幅 (防止积分抗饱和俗称I炸了) pid-integral pid-err; if(pid-integral pid-integral_max) pid-integral pid-integral_max; if(pid-integral -pid-integral_max) pid-integral -pid-integral_max; // 2. PID 核心算式 (P I D) pid-output (pid-Kp * pid-err) (pid-Ki * pid-integral) (pid-Kd * (pid-err - pid-err_last)); // 3. 更新上次误差 pid-err_last pid-err; // 4. 总输出限幅 (保护电机和硬件) if(pid-output pid-out_max) pid-output pid-out_max; if(pid-output -pid-out_max) pid-output -pid-out_max; return pid-output; }三、 高阶必杀技串级 PID (Cascade PID)在做倒立摆、平衡车、或者无人机时单环 PID 根本稳不住你必须使用串级 PID。什么是串级 PID串级就是两个 PID 套娃外环控制位置角度内环控制速度角速度。核心逻辑外环的输出作为内环的目标值。通俗例子平衡车外环角度环发现车往前倾斜了 10 度。外环 PID 计算后输出“我需要轮子以 50 转/秒的速度往前开才能把车身扶正”内环速度环接收到外环的指令目标速度50读取轮子编码器当前速度内环 PID 疯狂调节电机 PWM死死咬住 50 的速度。codeC// 串级 PID 伪代码示例 void Cascade_PID_Control(void) { // 1. 外环输入目标角度输出期望速度 float target_speed PID_Calc(PID_Angle, Target_Angle, Current_Angle); // 2. 内环输入期望速度输出电机 PWM float motor_pwm PID_Calc(PID_Speed, target_speed, Current_Speed); // 3. 执行机构 Set_Motor_PWM(motor_pwm); }避坑外环的计算频率可以低一点如 10ms内环的计算频率必须极高如 2ms 或 5ms因为内环需要极速响应。四、 拒绝玄学科学的 PID 调参指南多少人调 PID 是随便敲几个数字进去然后祈祷奇迹发生记住调参口诀先调内环再调外环先调 P再调 D最后调 I。调参利器推荐千万别用 printf 看数字用眼睛看数字是看不出震荡趋势的必须使用上位机串口示波器强烈推荐VOFA或者匿名上位机。将 目标值 和 实际值 通过串口发给电脑看波形图科学调参步骤以电机速度环为例归零将 Ki 和 Kd 全部设为 0。调 Kp加弹性从小到大慢慢加 Kp。看波形实际速度开始逼近目标速度但无法完全重合存在稳态误差。加到什么时候停止加到实际速度产生高频的小幅震荡然后把此时的 Kp乘以 0.6 ~ 0.8。调 Ki消误差加入 Ki从小慢慢加。看波形你会发现原本存在的“稳态误差”被慢慢消除了实际曲线和平稳地贴合在目标曲线上。注意Ki 太大曲线会缓慢地上下大波浪摇摆。调 Kd加阻尼速度环通常不需要 Kd因为编码器测速本身就有噪声Kd会把噪声无限放大导致电机抽搐。如果是位置环/角度环遇到小车到了目标位置还前后晃荡超调就需要加 Kd。Kd 能让小车在快到终点时“踩刹车”。五、 电赛常见控制类翻车现场 QAQ1电机在目标位置附近疯狂滋滋响小幅度抽搐原因你的 P 给大了或者 D 给太大了放大了传感器噪声还有可能是**齿轮间隙死区**导致的来回跳动。对策加入死区判断。当 abs(err) 2误差很小时直接强制 output 0让电机休息。Q2小车一遇到障碍物卡住稍微一推它就疯了一样以最大速度冲出去原因积分爆炸I 饱和。车被卡住时误差一直存在I 项一直在累加累加到了一个天际线。等障碍物一移开释放出的巨大力量直接让车失控。对策必须在代码里加上积分限幅参考上文代码的 integral_max或者使用抗积分饱和算法Anti-windup当误差极大时停止积分累加。Q3下坡或者外界给干扰时电机完全软弱无力原因你可能只写了位置环没写速度环。或者你没接入硬件编码器对策没有编码器闭环的电机是没有灵魂的务必使用带霍尔编码器的电机启用 STM32 定时器的 Encoder 模式读取脉冲形成真正的闭环控制。结语在电赛的赛场上代码的优雅与否直接决定了你能睡多少个小时。抛弃 delay 拥抱时间片架构你的系统将百毒不侵理解了 PID 的底层逻辑与上位机调参你将彻底告别“盲盒调参”的痛苦。预祝各位控制组、小车组、无人机组的同学波形完美贴合小车指哪打哪顺利拿下国一觉得有用的话别忘了点赞 ⭐收藏调参崩溃的时候翻出来看一遍你在调 PID 时遇到过什么奇葩现象或者调参调到深夜有什么感悟欢迎在评论区留言交流有问必答