实时控制系统设计:STM32下的确定性时延与PID实现
发布时间:2026/9/24 22:20:18来源:尧图网络
有次评审一个项目对方指着屏幕上的PID曲线说“算法没问题就是这个系统不够实时。”我问他“那你的采样点到输出控制量中间到底花了多少时间”他沉默了几秒答不上来。这个场景在我这些年看过的代码和方案里出现过太多次了——大家都在纠结PID参数怎么调、卡尔曼滤波怎么滤却很少有人先回答一个最基础的问题你的系统从“感知”到“执行”之间的时延是不是一个确定的值。这篇内容我就聚焦实时控制系统设计这件事。我会以一套基于STM32F103的直流电机转速控制系统为例把软硬件架构、定时器中断、编码器采样、PID算法到上位机实测这条链路完整拆一遍重点讲清楚“实时”两个字是怎么在每一个环节落地的。这套方法同样适用于温度、压力、液位等常见的闭环控制对象。如果你正在做课程设计、参加电子竞赛或者刚接触嵌入式控制但总觉得自己做出来的系统不稳定这篇文章应该能帮你找到问题的根子。1. 先界定清楚实时控制系统到底在控什么很多新手一听“实时控制”第一反应是“处理器要快、主频要高”。这个理解只对了一半。实时控制的本质不是快而是确定。一个控制任务在最坏情况下需要100毫秒才能完成但只要它的时延始终是100毫秒系统依然可以设计成稳定的反过来一个任务平均只要10微秒但偶尔被某个中断打断到1毫秒这个系统反而是不可控的。控制领域中我们把这种“每次执行时间都是确定的、可预测的”特性叫做时间确定性这才是实时控制系统的地基。1.1 实时的本质是可确定性而不是速度快我打个比方。你每天上班通勤如果路上稳定的30分钟你可以提前规划出门时间但如果这条路有时20分钟、有时堵到1个半小时哪怕平均只要40分钟你也会因为不确定性而迟到。实时系统就是这个道理控制器每秒执行1000次控制运算每一次都必须在规定的1毫秒内完成如果某个周期的计算被延迟到1.5毫秒那么这一拍的控制输出就是陈旧的执行器用的是过时信息系统就会产生相位滞后严重时直接发散震荡。在工程上实时性还分硬实时和软实时。电机转速控制、飞行器姿态控制、伺服位置控制这些都是硬实时一旦控制周期超时轻则转速波动重则机械损坏甚至安全事故。而温度控制、液位控制这类惯性极大的对象属于软实时偶尔一个周期慢了几个毫秒热惯性会帮你把波动吸收掉影响不大。所以设计前第一件事就是判断这个对象对超时的容忍程度这决定了后续所有设计策略。1.2 四个影响实时性的时延环节整个控制回路从物理量变化到输出更新通常要经过四个时延环节采样时延从传感器信号出现到控制器拿到这个数值的时间。比如编码器输出的脉冲是CPU轮询检测到的还是硬件计数器自动完成的时延差异非常大。计算时延控制算法运行的时间。PID这类简单算法通常在微秒级但如果用了复杂的状态观测器、滤波算法或者在中断里做浮点运算、调用耗时的库函数这个时间会显著拉长。输出时延从PWM更新寄存器的瞬间到执行器真正响应的时间。实际占空比的物理生效和寄存器的更新时刻有关也和驱动电路的响应速度有关。通信时延如果控制器和上位机之间有数据交互串口、CAN、以太网的传输时间也会进入整个回路尤其当通信中断抢占控制中断时影响会放大。设计实时控制系统第一步不是写代码而是先做一张时延预算表。比如我的控制周期是1毫秒那么编码器读取最好小于1微秒PID计算控制在5微秒以内PWM更新小于1微秒这三项加起来最坏情况也不会超过10微秒只占控制周期的1%剩下99%的时间都可以认为系统是空闲的。这张表一旦做出来你就知道哪些地方是安全的哪些地方可能成为隐患。2. 硬件平台选型与搭建为什么我用STM32做控制核心实时控制系统的硬件选型必须围绕“能否满足时间确定性”这个核心去展开。我见过有人用Arduino做电机调速库函数一行delay就把控制时序废掉也见过在树莓派上跑Linux做电机控制因为任务调度和系统中断的不确定性始终无法得到平滑的转速曲线。明确一点实时控制的软件再厉害也救不了硬件设计上的先天不足。2.1 处理器选择的三个硬性条件我选择STM32F103系列是有原因的。这类芯片其实不算快72MHz主频在今天看来很普通但它有三个为实时控制而生的硬件条件定时器资源丰富多个可独立配置的定时器一个用来产生精确的控制周期中断另一个可以用硬件编码器接口直接读取光电编码器的A/B相不需要CPU介入。中断响应快速且可配置优先级STM32的NVIC支持抢占优先级和子优先级你可以把控制周期中断设为最高优先级确保它不会被其他程序打断。外设DMA支持在需要采集电流、电压等模拟量时ADC可以配合DMA自动搬运数据不占用任何中断时间。相比之下Arduino Uno虽然上手快但定时器少、主频低、CPU中断负载能力弱而且它的标准库函数写起来随意很容易养成阻塞式编程的坏习惯。如果一定要用树莓派做那也要搭配实时补丁或者专门的外部实时控制器复杂度成倍上升不适合大多数中小型控制项目。2.2 电机、编码器、驱动器的搭配我用的这套硬件组合如下控制板STM32F103C8T6最小系统板便宜、资料多、引脚够用。电机直流减速电机额定电压12V带霍尔编码器。这类电机常见于小车底盘、云台、智能门锁等设备。驱动TB6612FNG电机驱动芯片比L298N效率高体积小逻辑电压兼容3.3V。编码器霍尔式正交编码器常见13PPR每圈13个脉冲经过STM32定时器编码器模式的四倍频后等效51个计数/圈。如果电机减速比是30轮的输出轴每转一圈编码器可产生约1560个计数对于大多数转速控制场景够用。这里要提醒一下编码器分辨率不是越高越好。分辨率太低速度反馈的量化误差大表现为转速在目标值附近持续小幅波动控制器会把这个量化噪声当成真实误差反复修正形成极限环分辨率太高每个计数对应的角度变小采样周期内可能读不到几个脉冲低速时会丢失信息。选型时要结合最低工作转速和控制周期来算比如你最低要稳定在60RPM控制周期1ms那么每个周期编码器只转0.36度如果一圈只有13个脉冲一个周期可能一个脉冲都没有这时候就必须用M法/T法结合的方式或者换更高分辨率的编码器。2.3 电源与接地的几个关键细节电源是整个系统里最容易出问题、也最容易被忽略的部分。电机启动或堵转时电流可以轻松达到1-3安培如果控制板和电机共用一路电源电机的冲击电流会导致电压瞬间跌落轻则单片机复位重启重则Flash写入错误、参数丢失。我的做法是分成两路供电一路12V直接给电机驱动供电另一路通过AMS1117-3.3或者DC-DC降压模块给MCU和传感器供电。两路电源的地线在电机驱动板端单点相连保证共地但又尽量避免大电流在地线上来回串扰。另外电机供电输入端加一个大容量电解电容比如1000uF/16V并并联一个0.1uF的高频瓷片电容用来吸收电机换向产生的尖峰。逻辑信号线比如PWM、编码器A/B尽量远离电机电源线如果不得不交叉可以用屏蔽线或者串一个小电阻来抑制振铃。这些细节看似不起眼但能帮你省掉很大一部分调试时间。3. 软件架构上的实时性落地定时器中断、编码器模式与NVIC硬件平台搭好后软件架构就是实时性落地的关键。我见过太多人把控制代码写进主循环然后再用延时函数去凑周期或者把所有外设中断的优先级平铺谁来了谁执行。这两种做法都会导致控制周期被拉长或者抖动。实时控制系统的软件核心是把“采样-控制-输出”这条实时链路上的一切都放进一个周期的中断服务函数里并且让这个中断拥有绝对优先的话语权。3.1 1ms控制周期是怎么定出来的采样周期不是拍脑袋定的。对电机转速环来说系统带宽通常在2Hz到10Hz之间控制频率至少要是带宽的10到20倍才能保证离散化后的控制器性能和连续系统接近。我选择1ms控制周期也就是1kHz的采样频率对转速环来说已经非常充裕。再短到0.1msCPU会被频繁打断而且编码器在一个周期内可能只有个位数脉冲量化误差变大再长到10ms转速响应的延迟会变得明显系统动态性能下降。1ms的中断我用基本定时器TIM7来产生因为基本定时器没有复杂的外部引脚绑定最适合单纯的定时中断。初始化代码大致是这样void TIM7_1ms_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM7, ENABLE); TIM_TimeBaseInitTypeDef timBase; timBase.TIM_Prescaler 72 - 1; // 72MHz / 72 1MHz也就是1us计数一次 timBase.TIM_CounterMode TIM_CounterMode_Up; timBase.TIM_Period 1000 - 1; // 1ms timBase.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM7, timBase); TIM_ITConfig(TIM7, TIM_IT_Update, ENABLE); NVIC_InitTypeDef nvic; nvic.NVIC_IRQChannel TIM7_IRQn; nvic.NVIC_IRQChannelPreemptionPriority 0; // 最高抢占优先级 nvic.NVIC_IRQChannelSubPriority 0; nvic.NVIC_IRQChannelCmd ENABLE; NVIC_Init(nvic); TIM_Cmd(TIM7, ENABLE); }中断函数里只做最核心的三件事读编码器、算PID、更新PWM。参考代码如下void TIM7_IRQHandler(void) { if (TIM_GetITStatus(TIM7, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM7, TIM_IT_Update); speed_raw TIM_GetCounter(TIM2); // 读取编码器计数 TIM_SetCounter(TIM2, 0); // 立即清零 pwm_out PID_Calc(target_speed, speed_raw); TIM_SetCompare1(TIM3, pwm_out); // 更新PWM占空比 } }注意编码器计数器读取之后要立刻清零。如果放到主循环里去做控制周期内就包含了不确定的等待时间采样时延就不可控了。把这三件事放在同一个中断里采样、计算、输出的时延是固定的这就是时间确定性的来源。3.2 编码器计数三种实现方式的对比测量电机转速的第一步是获取编码器脉冲。有三种常见做法它们的实时性和CPU开销差别非常大轮询GPIO主循环不停地读A、B相引脚电平统计上升沿。这种方式最简单但CPU空转严重而且只要主循环有其他任务就会漏掉脉冲低速时尤其明显。外部中断A相接外部中断每个上升沿触发一次中断在中断里累加计数。这种方式在低转速下还行但转速高的时候中断频率可能达到几万赫兹CPU几乎全耗在进出中断上系统实时性被彻底拖垮。定时器编码器模式STM32的定时器内部有硬件编码器接口A、B相信号接进去后硬件自动根据相位关系做四倍频计数CPU完全不需要管。控制器只在每个控制周期去读一次计数值然后清零。我肯定选第三种。初始化代码大致如下void TIM2_Encoder_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef gpio; gpio.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; gpio.GPIO_Mode GPIO_Mode_IN_FLOATING; // 编码器输出通常是推挽/开漏 gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); TIM_TimeBaseInitTypeDef tim; tim.TIM_Prescaler 0; tim.TIM_CounterMode TIM_CounterMode_Up; tim.TIM_Period 0xFFFF; // 16位计数最大值 tim.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, tim); TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_SetCounter(TIM2, 0); TIM_Cmd(TIM2, ENABLE); }从这里也能看出硬件定时器接口的本质就是把“采样时延”压缩到几乎为零而且完全不消耗CPU中断资源这是轮询和外部中断无法比拟的。3.3 中断优先级配置不当会直接毁掉实时性NVIC优先级配置是实时系统中最后一根救命稻草但恰恰是很多人栽跟头的地方。STM32的中断优先级分为抢占优先级和子优先级抢占优先级高的中断可以打断优先级低的中断。如果控制周期中断的优先级比串口中断还低那么当串口以高速率持续接收数据时控制中断会被不断延后系统的控制周期就变成了“平均值1ms最大不确定时间几毫秒”震荡和失稳几乎是必然的。我的配置原则是控制周期中断TIM7抢占优先级0最高任何其他通信、采集任务都不能打断它。ADC DMA中断如果需要电流环抢占优先级1。串口发送、接收中断抢占优先级2用于和上位机通信。主循环里的非实时任务显示、按键、数据记录不占用中断优先级只在剩余时间执行。这里特别提醒不要在控制中断里做任何耗时操作。比如HAL_Delay、printf、sprintf这类函数全部禁止出现在中断服务函数里。我自己的做法是中断里只更新一个全局变量的值然后由主循环或者串口DMA中断去把数据取走。如果非要发送数据可以用一个环形缓冲区中断里只往缓冲区写真正通过串口发送数据的操作交给DMA完成。4. PID控制在实时系统中的代码实现与整定实时架构做完了算法本身才有意义。接下来是PID控制在STM32上的落地。很多教程会贴一大段公式但真正到了单片机上你会发现算法的具体形态、整定步骤和边界条件才是决定控制效果的关键。4.1 位置式还是增量式电机调速我选增量式PID控制有两种最常见的离散实现形式位置式和增量式。位置式的输出是控制量的绝对值公式里需要对误差进行累加输出就是PWM的绝对占空比。它的优点是直观但缺点也很明显一旦误差持续存在积分项会越积越大输出进入饱和等到误差反向时系统要花很长时间来“消化”这个累积量表现就是大幅超调和震荡。增量式的输出是控制量的增量公式只和最近三次误差有关Δu(k) Kp * [e(k) - e(k-1)] Ki * e(k) Kd * [e(k) - 2*e(k-1) e(k-2)]然后把每次计算出来的Δu累加到上一次的输出值上。对于直流电机这种自带惯性保持特性的对象增量式天然比较温和即使控制器输出异常产生大幅跳变的可能性也小很多。我通常用增量式配合输出限幅安全性更高。参考代码typedef struct { float Kp; float Ki; float Kd; int32_t target; // 目标转速 int32_t actual; // 实际转速 int32_t e[3]; // 误差历史 int32_t u_out; // 当前输出 int32_t u_max; // 输出上限 int32_t u_min; // 输出下限 } PID_Inc_t; int32_t PID_Inc_Calc(PID_Inc_t *pid, int32_t setpoint, int32_t feedback) { pid-e[2] pid-e[1]; pid-e[1] pid-e[0]; pid-e[0] setpoint - feedback; float du pid-Kp * (pid-e[0] - pid-e[1]) pid-Ki * pid-e[0] pid-Kd * (pid-e[0] - 2*pid-e[1] pid-e[2]); // 对增量进行限幅防止单次跳变过大 if (du pid-u_max) du pid-u_max; if (du pid-u_min) du pid-u_min; pid-u_out (int32_t)du; // 对输出绝对值限幅 if (pid-u_out 5000) pid-u_out 5000; if (pid-u_out 0) pid-u_out 0; return pid-u_out; }这里我把PWM比较值范围设成0到5000对应占空比0%到100%。在实际项目中这个值还要考虑驱动芯片的最小有效占空比如果驱动芯片在极低占空比下无法导通可以设置一个输出死区。4.2 输出限幅与抗积分饱和不能省增量式PID虽然没有传统意义上的积分累加项但输出累积量依然可能饱和。比如目标转速是3000RPM实际转速卡在500RPM上不去控制器会不断累加增量输出直到PWM达到100%。此时如果负载突然释放转速开始上升控制器也要先把累积的输出降下来转速才能回落这个“迟滞”就会表现为不必要的超调。解决方案有两种一是限幅抗饱和也就是上面代码里做的对增量进行限幅对输出绝对值进行限幅。这样可以限制积分增长的速率但并不能完全消除掉累积的“惯性”。二是积分分离。思路很简单当偏差大于某个阈值时暂时禁用积分项当偏差缩小到阈值以内再恢复积分作用。这样既能快速消除静差又不会在启动阶段让积分疯狂累积。参考实现if (abs(e) 200) // 偏差大于200RPM禁用积分 { du pid-Kp * (pid-e[0] - pid-e[1]) pid-Kd * (pid-e[0] - 2*pid-e[1] pid-e[2]); } else { du pid-Kp * (pid-e[0] - pid-e[1]) pid-Ki * pid-e[0] pid-Kd * (pid-e[0] - 2*pid-e[1] pid-e[2]); }这个阈值要根据你的系统量纲去选不是固定值。对转速控制来说我一般选额定转速的10%作为阈值。4.3 一组能上手的整定路径PID参数整定是新手最容易卡住的地方。我给出一套实际可执行的步骤不需要复杂的数学推导。第一步先把Ki和Kd设为0只保留Kp给一个较小的目标转速比如500RPM观察实际转速曲线的响应。如果Kp太小响应慢稳态误差大如果Kp过大会出现持续等幅震荡。逐步增大Kp直到转速曲线出现持续等幅震荡记录此时的临界增益Kp_crit和震荡周期T_crit。第二步根据齐格勒-尼科尔斯经验公式确认初始参数控制器KpKiKdP0.5 * Kp_crit00PI0.45 * Kp_critKp / (T_crit / 1.2)0PID0.6 * Kp_critKp / (T_crit / 2)Kp * (T_crit / 8)有了初始参数后再在实际系统上微调。我自己的经验是先调Kp让系统响应速度基本合适再调Ki消除稳态误差最后调Kd抑制超调和震荡。注意增量式PID里的Ki数值通常会比较小因为它作用在误差的当前值上而不是累加误差值单位感和位置式不一样不要拿着位置式的参数直接套。整定过程中每一次改变参数至少等系统稳定运行几十秒再看曲线不要频繁调整参数。很多人越调越乱就是因为设定值和参数同时变根本无法判断是哪个因素引起的响应变化。5. 用上位机把控制效果“看”出来控制效果好不好不需要靠嘴说也不需要靠猜。把转速、目标值、PWM输出用上位机曲线画出来一切一目了然。但怎么把数据从实时控制系统中“偷”出来又不影响控制回路本身就是一门手艺。5.1 串口观测通道怎么搭才不干扰控制最简单的做法是串口输出。但我绝不建议在1ms控制中断里直接调用printf或者HAL_UART_Transmit因为115200波特率下发送一个字符就要接近87微秒发送10个字符就是870微秒几乎把一个控制周期占满了这种“观测行为”会直接毁掉控制实时性。正确的做法是控制中断里只更新一个全局结构体主循环里周期性把数据发送出去。比如volatile int32_t g_speed 0; volatile int32_t g_target 0; volatile int32_t g_pwm 0; // 控制中断里 void TIM7_IRQHandler(void) { // ... 读取编码器、PID计算、更新PWM ... g_speed speed_raw; g_target target_speed; g_pwm pwm_out; } // 主循环里 int main(void) { // 初始化... while (1) { // 大约每20ms发送一次观测数据 HAL_Delay(20); printf(%d,%d,%d\r\n, g_target, g_speed, g_pwm); } }这种方式下串口发送只占用主循环的时间即使偶尔被控制中断打断也不会反过来影响控制周期。如果发送的数据量很大还可以用DMA配合环形缓冲区把数据搬运彻底从CPU中解放出来。上位机端我常用两种工具一种是可以直接绘制串口曲线的桌面软件比如VOFA、Serial Studio简单配置就能把三个数字画成三条曲线另一种是自己写Python脚本用pyserial读串口再用matplotlib画图适合需要自定义分析逻辑的场景。5.2 实测阶跃响应与参数调整我用一组实际记录的数据来展示参数调整的效果。设定转速从0阶跃到1000RPM记录了三组PID参数下的响应指标KpKiKd超调量调节时间±5%稳态误差2.00002.1s约60RPM2.00.1015%1.3s约0RPM2.00.10.014%0.6s约0RPM从数据中可以看得很清楚纯比例控制没有超调但存在静差这是因为直流电机需要一定的PWM才能克服摩擦阻力加入积分项后静差被消除但超调明显上升再加入微分项超调被抑制调节时间显著缩短。我在调参时还会额外观察一种现象如果转速曲线在稳态时出现高频小幅度波动通常不是参数的问题而是编码器量化噪声被微分项放大了。这时候适当降低Kd或者对速度反馈值做一阶低通滤波效果比继续调参数更明显。总之上位机曲线不仅是调参工具更是定位问题的“X光片”。6. 调试中遇到的三类典型问题与完整排查链路这一节我把自己在调试这类系统时真正踩过、也帮别人排查过的三类高频问题按排查链路写出来。这些问题有个共同点表面上看都像是“PID没调好”实际上根子全在实时架构或者硬件细节上。6.1 测速数据偶发跳变中断与主循环的数据竞争现象是转速曲线偶尔冒出一个尖峰比如设定1200RPM曲线几乎一直平稳但每隔几秒会突然跳一个点显示3200RPM然后又掉回来。我当时的排查链路是这样走的。第一步检查编码器机械连接插头重新插紧问题依旧。第二步用示波器观察编码器A、B相波形信号无毛刺边沿也很干净。第三步怀疑是主循环读编码器计数和中断清零发生了竞争。回到代码一看问题确实在这里我在主循环里也读了一次TIM2的计数值用来做液晶显示而控制中断每1ms读完后立刻清零。当主循环读到刚被清零、或者正在被清零的瞬间时读到的值会是一个荒谬的大数或者截断值甚至比真实值大几十倍。解决方法很简单所有共享的编码器计数、PID输出等变量统一放到控制中断里更新主循环只读取这些变量的副本。如果必须由主循环读取硬件计数器那就先在主循环里关闭中断前清理逻辑__disable_irq(); speed_for_display TIM_GetCounter(TIM2); __enable_irq();这个问题的本质是数据竞争。实时控制系统中一个最稳妥的原则就是能所有IO操作都做成单写者模式采样-控制-输出这条实时链路只在同一个定时器中断里完成其他任务一律只读数据。6.2 电机不转或声音异常从PWM硬件开始排查第二种典型问题是程序下载后电机完全不转或者转动时发出刺耳的啸叫。排查链路必须从物理层开始不能一上来就怀疑PID。第一步用示波器或者逻辑分析仪看PWM引脚有没有波形。如果引脚始终为高电平或低电平检查GPIO复用功能是否配置正确。STM32的PWM输出引脚必须配置为复用推挽输出模式也就是GPIO_Mode_AF_PP而不是普通的GPIO_Mode_Out_PP很多人就是在这里栽跟头。第二步检查定时器通道和引脚映射是否匹配。比如TIM3的通道1默认是PA6如果代码里配置了PA6但实际接线接到了PA7那无论怎么配置都不会有输出。这种问题靠读代码很难发现最好的办法是对照数据手册的复用功能表逐个引脚核对。第三步检查PWM频率。我的电机驱动在1kHz PWM下会出现明显的电流啸叫转速越低声音越刺耳因为人耳对这个频段敏感而且转矩脉动大。把PWM频率提高到20kHz后电机明显安静了很多运行也更平顺。STM32F103在72MHz主频下20kHz PWM可以这样算72MHz / 20kHz 3600所以预分频器可设为0自动重装载值设为3599。第四步断开PID手动给定一个固定占空比50%测试。如果电机能稳定转起来说明PWM硬件正常问题大概率在PID输出通道如果固定占空比都不转那就是硬件接线、驱动芯片使能引脚或者电源问题。先分层确认再联合调试能省下大量时间。6.3 转速震荡不止不只在Kp上找原因第三种问题更迷惑人设定1200RPM实际转速在1100到1300之间持续震荡震荡周期约几十毫秒减不下Kp减了Kp又变成稳态误差大非常难受。我的排查链路从软件到硬件一层层拉开。先看PID参数没有问题再看PWM频率已经提到20kHz排除转矩脉动然后用示波器看驱动输出电压波形没有明显异常。最后怀疑到编码器分辨率上。我用的电机是13PPR霍尔编码器经过4倍频后只有52计数/圈也就是每个控制周期1ms内编码器计数可能只有0到1个脉冲。这个量化误差换算成转速误差可以达到几十RPM控制器会把这种量化噪声当成真实的速度变化反复调整输出极限环就出来了表现就是持续震荡。解决办法分三层第一层用更好的测速方法低速时不要每周期都计算转速而是累计多个周期的脉冲数后再算比如每10ms计算一次平均速度第二层对速度反馈做滑动平均滤波把量化噪声平滑掉第三层有条件就换更高分辨率的编码器比如用256线光电编码器效果立竿见影。这个案例让我最深的感受是控制系统性能是机械、电气、算法三者综合的结果。编码器分辨率、减速箱间隙、电机转矩波动这些硬件因素在闭环控制里都会转化为控制误差。软件只能在一定范围内补偿补不了硬件的短板。以后遇到震荡我的排查顺序永远是测量环节、驱动环节、机械环节最后才是PID参数。我最后再说一个自己用了很多年的小习惯。每次开始写实时控制代码前先在白纸上画一张时延预算表把采样、计算、输出、通信四个环节的预估时间和最坏时间都写出来然后把它们和确定好的控制周期放在一起对比确认总时延远小于控制周期再动手写第一行代码。这个习惯看起来有点费事但它真的能替你挡掉一半以上的稳定性问题——实时的烂账最好在代码之前就算清楚而不是等系统震荡了再回头翻代码。
网站建设高端定制企业官网