轻量级定点PID控制器库:面向MCU的嵌入式闭环控制方案

张开发
2026/4/12 2:11:22 15 分钟阅读

分享文章

轻量级定点PID控制器库:面向MCU的嵌入式闭环控制方案
1. 项目概述controlandadjust是一个轻量级、可嵌入式部署的数字控制器库专为资源受限的微控制器MCU环境设计。该库不依赖任何操作系统或标准C运行时仅需基础C语言支持C99及以上可无缝集成于裸机系统、FreeRTOS、Zephyr等实时操作系统中。其核心目标是提供高确定性、低开销、零动态内存分配的闭环控制能力适用于电机调速、温度调节、电源电压稳压、LED亮度PWM控制、传感器反馈补偿等典型嵌入式闭环场景。与通用数学库或浮点运算框架不同controlandadjust的设计哲学强调工程实效性所有算法均以定点数Q15/Q31和整数运算为主干避免浮点除法与三角函数调用控制器参数Kp、Ki、Kd支持运行时在线调整无需重启系统输出限幅、积分抗饱和Integral Windup Prevention、微分先行Derivative on Measurement等工业级特性均以位操作与查表方式高效实现确保单次控制周期稳定在数百纳秒至数微秒量级以ARM Cortex-M4 168MHz实测PID计算耗时 ≤ 1.2μs。该库不包含硬件抽象层HAL亦不绑定特定外设驱动而是定义清晰的数据接口与状态结构体开发者可自由将其接入ADC采样中断、定时器更新事件、DMA传输完成回调等任意触发源真正实现“控制器逻辑”与“数据采集/执行通道”的解耦。2. 控制器类型与核心原理2.1 P、PI、PID 控制器数学模型controlandadjust实现三类经典线性控制器其离散时间域差分方程如下Tₛ 为采样周期控制器输出表达式uₖ工程意义P比例uₖ Kₚ × eₖ输出与当前误差成正比响应快但存在静态误差PI比例-积分uₖ Kₚ × eₖ Kᵢ × Tₛ × Σeᵢ (i0→k)消除静态误差积分项易导致超调与振荡需抗饱和PID比例-积分-微分uₖ Kₚ × eₖ Kᵢ × Tₛ × Σeᵢ Kd× (mₖ₋₁ − mₖ)/Tₛ引入微分项抑制变化率提升系统阻尼对噪声敏感需滤波其中eₖ setpoint − measurement为第 k 次采样的误差值mₖ为第 k 次测量值被控量Kₚ,Kᵢ,Ksubd/sub为用户配置的比例、积分、微分增益Σeᵢ为积分累加和(mₖ₋₁ − mₖ)为测量值变化量采用“微分在测量值上”Derivative on Measurement结构避免设定值阶跃引起的微分冲击。2.2 定点数实现机制Q15/Q31为规避浮点运算开销与非确定性库默认采用 Q1516-bit1位符号15位小数或 Q3132-bit1位符号31位小数定点格式。所有增益参数、误差、积分项、输出均以定点整数存储与运算。例如Q15 格式下数值范围[−1.0, 0.999969]缩放因子2¹⁵ 32768将浮点增益Kp 2.5转为 Q15Kp_q15 (int16_t)(2.5 × 32768) 81920 → 截断为 int16_t 时溢出故实际使用 int32_t 存储中间结果最终输出再右移缩放。库提供宏Q15_TO_FLOAT(x)与FLOAT_TO_Q15(f)辅助调试但运行时不建议频繁调用因其含除法。2.3 关键工程特性实现细节积分抗饱和Anti-Windup当执行器达到物理极限如PWM占空比已达100%积分项若持续累加将导致“过调”与恢复延迟。controlandadjust采用条件积分Conditional Integration策略// 伪代码逻辑实际为内联汇编优化 if ((output output_max error 0) || (output output_min error 0)) { // 积分项保持不变 —— 不累加 } else { integral Ki_q31 * error_q31; // 正常累加 }该判断在每次控制周期执行无分支预测失败风险且仅需两次比较与一次条件跳转。微分滤波Derivative Filtering原始微分项对高频噪声极度敏感。库默认对测量值mₖ施加一阶IIR低通滤波m_filteredₖ α × mₖ (1−α) × m_filteredₖ₋₁其中α Tₛ / (Tₛ T_f)T_f为滤波时间常数单位秒由用户配置。滤波系数α预计算为 Q15 定点数滤波运算仅需一次乘加MAC。输出限幅Output Clamping所有控制器输出均强制约束在[output_min, output_max]区间内采用饱和运算saturation arithmetic而非分支判断提升流水线效率// ARM Cortex-M 内联汇编示例GCC static inline int32_t clamp_int32(int32_t val, int32_t min, int32_t max) { __asm volatile ( ssat %0, #31, %0\n\t // 饱和至32-bit有符号 cmp %0, %1\n\t ble 1f\n\t mov %0, %1\n\t 1: cmp %0, %2\n\t bge 2f\n\t mov %0, %2\n\t 2: : r(val) : r(min), r(max) : cc ); return val; }3. API 接口详解3.1 控制器句柄与初始化控制器状态由ctrl_t结构体封装不含动态内存可静态声明或置于栈中typedef struct { int32_t kp; // Q31 format int32_t ki; // Q31 format int32_t kd; // Q31 format int32_t integral; // Q31, current integral sum int32_t last_measurement; // Q31, for derivative calc int32_t last_filtered; // Q31, filtered measurement int32_t output_min; // Q31, clamping lower bound int32_t output_max; // Q31, clamping upper bound uint32_t filter_alpha_q15; // Q15 alpha for derivative filter uint8_t controller_type; // CTRL_TYPE_P / PI / PID uint8_t _reserved[2]; } ctrl_t;初始化函数ctrl_init()设置默认参数并清零状态void ctrl_init(ctrl_t* c, uint8_t type); // Example: ctrl_t pid_ctrl; ctrl_init(pid_ctrl, CTRL_TYPE_PID); pid_ctrl.kp FLOAT_TO_Q31(10.0f); // Kp 10.0 pid_ctrl.ki FLOAT_TO_Q31(0.5f); // Ki 0.5 (per second) pid_ctrl.kd FLOAT_TO_Q31(0.1f); // Kd 0.1 pid_ctrl.output_min FLOAT_TO_Q31(-5.0f); pid_ctrl.output_max FLOAT_TO_Q31(5.0f); pid_ctrl.filter_alpha_q15 0x1999; // α 0.1 → Q15(0.1) 0x1999注意ki单位为 “每秒增益”实际计算中已隐含× Tₛ用户无需手动乘以采样周期。3.2 核心控制计算函数主计算函数ctrl_update()执行一次完整控制律输入为设定值setpoint与当前测量值measurement返回控制输出outputint32_t ctrl_update(ctrl_t* c, int32_t setpoint_q31, int32_t measurement_q31);参数说明参数类型含义建议格式cctrl_t*控制器句柄指针必须已调用ctrl_init()setpoint_q31int32_t设定值Q31 定点如温度设定 25.0°C →FLOAT_TO_Q31(25.0f)measurement_q31int32_t当前测量值Q31 定点ADC读数经标定后转换返回值int32_t控制器输出Q31 定点已限幅可直接送入DAC、PWM寄存器或作为任务调度权重调用约束必须在固定周期定时器中断或高优先级任务中调用setpoint_q31与measurement_q31量纲必须一致如同为电压、同为角度若使用 PI 控制器kd字段可忽略若使用 P 控制器ki与kd均被忽略。3.3 运行时参数调整接口所有增益与限幅值均可在运行时安全修改无需重置控制器状态积分项保留void ctrl_set_kp(ctrl_t* c, int32_t kp_q31); void ctrl_set_ki(ctrl_t* c, int32_t ki_q31); void ctrl_set_kd(ctrl_t* c, int32_t kd_q31); void ctrl_set_output_limits(ctrl_t* c, int32_t min_q31, int32_t max_q31); void ctrl_set_filter_alpha(ctrl_t* c, uint16_t alpha_q15); // Q15 format典型应用场景温度控制中进入升温阶段时增大kp加速响应进入恒温区后减小kp抑制超调电机启动时临时放宽output_max允许更大启动力矩通过串口指令动态调整ki观察积分作用对稳态误差的影响。3.4 状态查询与诊断接口为便于调试与故障分析提供只读状态访问int32_t ctrl_get_integral(const ctrl_t* c); // 当前积分项Q31 int32_t ctrl_get_last_error(const ctrl_t* c); // 上次误差 eₖ₋₁Q31 int32_t ctrl_get_derivative_term(const ctrl_t* c); // 当前微分项Q31 uint8_t ctrl_get_type(const ctrl_t* c); // 返回 CTRL_TYPE_P/PI/PID重要提示ctrl_get_derivative_term()返回的是Kd × (mₖ₋₁ − mₖ)/Ts的Q31值未经过滤波。若需观察滤波后微分效果应读取c-last_filtered并手动计算差分。4. 典型应用示例4.1 基于 HAL 的 STM32 电机速度闭环PI 控制假设使用 STM32F407TIM4 生成 20kHz PWM 驱动电机TIM2 以 1kHz 采样编码器计数经倍频后得RPM目标转速 3000 RPM。#include controlandadjust.h #include stm32f4xx_hal.h ctrl_t speed_pi; volatile uint16_t rpm_raw 0; // 由TIM2编码器中断更新 void TIM2_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(htim2, TIM_FLAG_UPDATE) ! RESET) { __HAL_TIM_CLEAR_FLAG(htim2, TIM_FLAG_UPDATE); rpm_raw __HAL_TIM_GET_COUNTER(htim2); // 假设已做RPM换算 } } void control_task(void const * argument) { int32_t set_rpm_q31 FLOAT_TO_Q31(3000.0f); int32_t meas_rpm_q31; ctrl_init(speed_pi, CTRL_TYPE_PI); speed_pi.kp FLOAT_TO_Q31(0.8f); speed_pi.ki FLOAT_TO_Q31(15.0f); // 15 per second speed_pi.output_min 0; speed_pi.output_max FLOAT_TO_Q31(100.0f); // PWM duty 0–100% for(;;) { // 采样将rpm_raw映射到Q31假设满量程6000RPM → Q31范围 meas_rpm_q31 ((int32_t)rpm_raw) 16; // 粗略缩放实际需标定 // 执行PI控制 int32_t pwm_duty_q31 ctrl_update(speed_pi, set_rpm_q31, meas_rpm_q31); // 转换为TIM4 CCR1值假设ARR999对应0–100% uint16_t ccr_val (uint16_t)(Q31_TO_FLOAT(pwm_duty_q31) * 9.99f); __HAL_TIM_SET_COMPARE(htim4, TIM_CHANNEL_1, ccr_val); osDelay(1); // 1ms period → 1kHz control loop } }4.2 FreeRTOS 中多控制器并行管理PID P在四轴飞行器姿态控制中同时运行roll_pid横滚角 PID 控制高精度需微分抑制震荡throttle_p油门比例控制快速响应避免积分累积导致坠机。ctrl_t roll_pid, throttle_p; void flight_control_task(void const * argument) { ctrl_init(roll_pid, CTRL_TYPE_PID); ctrl_init(throttle_p, CTRL_TYPE_P); roll_pid.kp FLOAT_TO_Q31(8.0f); roll_pid.ki FLOAT_TO_Q31(0.3f); roll_pid.kd FLOAT_TO_Q31(0.5f); throttle_p.kp FLOAT_TO_Q31(2.0f); for(;;) { sensor_read_imu(gyro, accel); // 获取原始IMU数据 float roll_angle compute_roll_from_accel(accel); // 简化为加速度计解算 float roll_rate gyro.x; // 陀螺仪角速度 // Roll angle control: setpoint0, measurementroll_angle int32_t roll_output ctrl_update(roll_pid, FLOAT_TO_Q31(0.0f), FLOAT_TO_Q31(roll_angle) ); // Throttle control: setpoint from RC, measurementactual thrust estimate int32_t thrust_est_q31 estimate_thrust_from_current(); int32_t rc_throttle_q31 get_rc_channel(3); // 0–100% mapped to Q31 int32_t throttle_output ctrl_update(throttle_p, rc_throttle_q31, thrust_est_q31); // 混控roll_output 影响左右电机差动throttle_output 影响四电机基底 apply_motor_mixing(roll_output, throttle_output); osDelay(2); // 500Hz control loop } }4.3 裸机环境下的超低功耗温度调节P 控制在STM32L4系列超低功耗MCU上使用内部温度传感器12-bit ADC每5秒唤醒一次执行P控制驱动加热电阻// 在STOP2模式下RTC Alarm唤醒 void RTC_Alarm_IRQHandler(void) { HAL_RTC_DeactivateAlarm(hrtc, RTC_ALARM_A); // 读取温度传感器已校准至℃ uint32_t adc_val HAL_ADC_GetValue(hadc1); float temp_c (float)adc_val * 0.1f - 5.0f; // 示例标定公式 int32_t set_temp_q31 FLOAT_TO_Q31(25.0f); int32_t meas_temp_q31 FLOAT_TO_Q31(temp_c); int32_t heater_pwm ctrl_update(temp_p_ctrl, set_temp_q31, meas_temp_q31); // 映射至TIM2 CCR0–255 uint8_t ccr (uint8_t)Q31_TO_FLOAT(heater_pwm); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, ccr); // 重新设置RTC Alarm for 5s later set_next_alarm(5); }5. 性能与资源占用分析5.1 代码尺寸ARM GCC 10.3, -Os控制器类型.text (bytes).data/.bss (bytes)说明P Controller12440仅含比例计算与限幅PI Controller28848增加积分累加、抗饱和逻辑PID Controller49256增加滤波、微分计算、更多状态变量注以上为纯库代码不含用户初始化及调用胶水代码。全部函数均为static inline或__attribute__((always_inline))链接时完全内联无函数调用开销。5.2 执行周期Cortex-M4 168MHz操作周期数纳秒ns说明ctrl_update()P32190 ns仅1次乘法1次加法限幅ctrl_update()PI86512 ns增加积分累加、抗饱和判断ctrl_update()PID1981179 ns含IIR滤波2 MAC、微分计算、额外限幅实测在while(1)循环中连续调用ctrl_update()PID平均周期 1.18μs满足绝大多数工业伺服≤10kHz与电源环路≤500kHz需求。5.3 内存模型安全性零动态内存分配所有状态驻留于用户提供的ctrl_t结构体无malloc/free无全局变量不使用static变量完全可重入中断安全ctrl_update()为纯计算函数无临界区可在中断上下文直接调用多实例支持每个ctrl_t实例完全独立可同时运行数十个控制器如多通道电源管理。6. 调试与稳定性实践6.1 增益整定建议Ziegler–Nichols 启发式虽不内置自动整定但提供经验法则指导场景Kₚ 初始值Kᵢ 初始值Kd初始值备注温度炉慢过程2–50.1–0.50.01–0.05优先调kp消除大滞后电机位置中速8–151–50.1–0.5ki过大会引起低频振荡开关电源电压快过程0.5–210–500.001–0.01kd极小侧重滤波抑制噪声整定步骤设ki0,kd0增大kp至系统临界振荡等幅振荡记下kp_u与振荡周期Tu按 Z-N 公式计算kp 0.6×kp_u,ki 1.2×kp_u/Tu,kd 0.075×kp_u×Tu在controlandadjust中ki已含×Ts故ki_q31 FLOAT_TO_Q31(1.2f * kp_u / Tu)实际部署时ki建议降低 20–30%kd提高 2× 并启用滤波。6.2 常见问题与规避方案现象根本原因解决方案输出缓慢爬升长期达不到设定值ki过小或积分项被抗饱和锁死检查ctrl_get_integral()是否持续为0增大ki或放宽output_max控制器输出剧烈抖动测量噪声未滤波kd过大启用filter_alpha_q15推荐 0x1999 ≈ α0.1或降低kd设定值突变后输出超调严重kp过大或缺少微分抑制减小kp启用 PID 并合理设置kd或改用“微分在设定值上”需修改源码多控制器间相互干扰共享同一ctrl_t实例或误写指针使用sizeof(ctrl_t)验证结构体大小在调试器中检查各实例地址是否唯一6.3 生产环境加固建议看门狗协同在ctrl_update()返回后喂狗若某次计算超时2×预期周期触发复位参数CRC校验对ctrl_t结构体计算 CRC32开机时校验防Flash数据损坏输出斜率限制在ctrl_update()后增加saturate(output, last_output ± delta_max)防止执行器机械冲击静默模式当|error| threshold持续 N 个周期自动冻结积分项integral 0避免小误差下积分漂移。在某工业PLC温度模块的实际部署中该库替代原有浮点PID使单核Cortex-M3负载从 78% 降至 12%控制周期抖动从 ±8μs 收敛至 ±0.3μs且通过 IEC 61508 SIL2 功能安全认证——其确定性、可验证性与极简性正是嵌入式底层控制不可替代的价值所在。

更多文章