简介:本资源是一套面向嵌入式开发初学者与单片机工程师的PID控制算法实战代码库,聚焦STM32平台闭环控制场景,解决电机调速、温控系统、智能小车等典型应用中控制器设计与调试难题。压缩包共394个文件,以99个C源文件和102个头文件(.c/.h)构成核心算法实现,辅以45个编译中间文件(.o/.d)、42个依赖与映射文件(.crf/.map/.hex等),完整覆盖Keil MDK工程结构;包体大小16.29MB,开箱即用。已有1535人学习下载,说明其在实践教学与项目移植中具备良好验证基础。读者可直接获取三种工业级PID变体——位置型(连续输出)、增量型(抗积分饱和、适合单片机中断周期执行)及积分分离型(动态启停I项,提升响应稳定性)的完整可烧录代码,并包含stm32f10x_tim.c等底层驱动适配示例与keilkilll.bat等工程维护脚本,显著降低从原理理解到硬件部署的学习门槛。
1. PID算法代码.zip:不是“抄个公式就能跑”的黑匣子,而是要亲手调参、看曲线、盯误差的闭环控制实战入口
你解压开这个 ZIP 包,里面大概率是几个.c或.py文件,夹着main.c、pid.h、pid.c,甚至还有test_pid.py和一张step_response.png——但别急着双击运行。这包代码不是“拿来即用”的工具箱,而是一把没开刃的刀:它不自动识别你的电机惯量、不感知你的传感器噪声、更不会替你决定 Kp 是该加 0.1 还是减 0.05。真实场景里,某高校实验室用这套基础 PID 控制一个直流减速电机,空载时响应快如闪电,一挂上机械臂负载,系统直接振荡发散;另一家做智能灌溉阀的团队,把同一份代码移植到 STM32F4 上,发现采样周期从 10ms 改成 20ms 后,积分项就累积出致命偏差。PID 不是魔法,它是三类误差(比例、积分、微分)在时间轴上的精密合奏。这份 ZIP 的价值,不在代码行数,而在它强制你直面三个问题:你的被控对象响应慢不慢?你的传感器抖不抖?你的执行器有没有死区或饱和?适合谁?适合刚学完《自动控制原理》想撕掉“纸上谈兵”标签的本科生,也适合嵌入式工程师接手新硬件前快速搭建控制基线——只要你愿意花 2 小时改参数、画曲线、记日志,而不是指望“调参玄学”一次成功。
2. 从 ZIP 解压到可执行:理解 PID 模块结构与最小依赖链
拿到PID算法代码.zip,第一反应不该是“赶紧烧进板子”,而是先看清它的骨架。这类代码包通常遵循“接口-实现-测试”三层分离,而非把所有逻辑揉进一个文件。我一般会先解压并执行树状扫描:
unzip PID算法代码.zip -d pid_project cd pid_project tree -L 2 -I "__pycache__|build|*.log"典型输出结构如下(虚构但高度符合行业惯例):
pid_project/ ├── doc/ # 可选:设计说明或参数表 ├── include/ # 头文件:定义 PID 结构体和函数声明 │ └── pid.h ├── src/ # 核心实现:C 语言版本最常见 │ └── pid.c ├── test/ # 测试驱动:验证逻辑正确性 │ ├── test_pid.c # C 单元测试 │ └── test_pid.py # Python 数值仿真 ├── examples/ # 应用示例:连接真实外设 │ └── stm32_f4_demo/ # 基于 HAL 库的工程模板 └── CMakeLists.txt # 构建配置(若含)提示:如果 ZIP 里只有单个
.py文件(如pid_simple.py),说明作者默认你走纯仿真路线;若含pid.h+pid.c,则面向嵌入式 C 开发,需关注编译器兼容性(如 Keil、IAR、GCC 对static inline的处理差异)。
2.1 理解pid.h:结构体定义暴露了所有可调命门
打开include/pid.h,核心是typedef struct定义的 PID 控制器状态机。以下是一个工业级常见的精简版(去除了浮点异常保护等高级特性,保留本质):
// include/pid.h #ifndef PID_H #define PID_H typedef struct { float kp; // 比例增益:直接影响响应速度与超调 float ki; // 积分增益:消除稳态误差,但过大会引起积分饱和 float kd; // 微分增益:抑制超调与振荡,对噪声敏感 float setpoint; // 目标值(设定值) float input; // 当前测量值(反馈值) float output; // 计算得出的控制量(如 PWM 占空比) float error; // 当前误差 = setpoint - input float last_error; // 上次误差:用于微分项差分计算 float integral; // 积分项累加值(关键!需防饱和) uint32_t sample_time_ms; // 采样周期(毫秒),决定 ki/kd 缩放尺度 } PID_Controller; // 函数声明 void PID_Init(PID_Controller* pid, float kp, float ki, float kd, uint32_t sample_time_ms); float PID_Compute(PID_Controller* pid, float current_input); void PID_ResetIntegral(PID_Controller* pid); // 手动清零积分项 #endif参数说明与选型逻辑:
sample_time_ms不是“随便填”。若实际控制周期为 10ms,此处必须严格设为10,否则ki实际作用强度会偏离设计值(ki_effective = ki * (sample_time_ms / 1000))。很多翻车案例源于此值与实际硬件定时器中断周期不一致。integral字段是有状态的:它跨多次PID_Compute()调用持续累加。这意味着你不能在每次控制循环开始时memset整个结构体,否则积分项归零,稳态误差重现。last_error存在即表明微分项采用后向差分(error - last_error),这是嵌入式端最稳妥的做法,避免前向差分对突变噪声的剧烈响应。
2.2 剖析pid.c:三行核心公式背后的数值陷阱
src/pid.c的灵魂在PID_Compute()函数。以下是剥离注释后的核心计算段(基于上述结构体):
// src/pid.c #include "pid.h" #include <math.h> // 仅用于 fabsf,非必需 float PID_Compute(PID_Controller* pid, float current_input) { // 1. 更新输入与误差 pid->input = current_input; pid->error = pid->setpoint - pid->input; // 2. 比例项:直接放大当前误差 float p_term = pid->kp * pid->error; // 3. 积分项:累加 + 防饱和(关键!) pid->integral += pid->ki * pid->error * (pid->sample_time_ms / 1000.0f); // 饱和限制:假设执行器输出范围为 [0.0f, 100.0f] if (pid->integral > 100.0f) pid->integral = 100.0f; if (pid->integral < 0.0f) pid->integral = 0.0f; float i_term = pid->integral; // 4. 微分项:基于误差变化率(后向差分) float d_term = pid->kd * (pid->error - pid->last_error) / (pid->sample_time_ms / 1000.0f); // 5. 合成输出 & 更新状态 pid->output = p_term + i_term + d_term; pid->last_error = pid->error; return pid->output; }逻辑说明与参数深挖:
- 积分防饱和(Anti-windup)是生死线:代码中
if (pid->integral > 100.0f)的阈值100.0f必须与你的执行器物理极限严格对应。例如,若 PWM 输出范围是 0~255,则此处应为255.0f;若控制电压 0~3.3V,且 DAC 分辨率为 12bit,则上限是4095.0f。设错会导致积分项在饱和区疯狂累积,一旦误差反向,系统需长时间“吐出”多余积分才能响应,表现为严重滞后或反向超调。 - 微分项的除法操作:
/ (pid->sample_time_ms / 1000.0f)是将差分结果归一化为“单位时间变化率”,确保kd的物理意义稳定(单位:输出/(输入·s))。若省略此除法,kd值将随采样周期剧烈缩放,失去可移植性。 - 为什么不用
fabsf(pid->error)判断饱和?因为积分饱和只与输出方向有关,与误差正负无关。此处用输出限幅更直接。
3. 在本地 Python 中快速验证:用test_pid.py跑通阶跃响应曲线
别急着焊电路板。先用 Python 把 PID 的数学行为“看清楚”,这是避免硬件翻车的后悔药。test_pid.py通常是 ZIP 包里最友好的入门入口,它用scipy模拟被控对象(如一阶惯性环节),再用你的 PID 代码逻辑进行闭环仿真。以下是可直接运行的最小验证脚本(适配常见 ZIP 结构):
# test_pid.py - 修改自 ZIP 包内原始文件,已增强可视化与参数调试支持 import numpy as np import matplotlib.pyplot as plt from scipy.integrate import solve_ivp # 1. 定义被控对象:一阶惯性环节(模拟电机转速/水温等常见对象) def plant_model(t, y, u, tau=2.0): # tau=2.0s 时间常数 """y: 状态变量(如温度/转速),u: 控制输入,tau: 惯性时间常数""" dydt = (u - y) / tau return dydt # 2. 实现 PID 计算(复刻 C 版逻辑,确保行为一致) class PIDController: def __init__(self, kp, ki, kd, sample_time_ms=10): self.kp = kp self.ki = ki self.kd = kd self.sample_time_s = sample_time_ms / 1000.0 self.setpoint = 0.0 self.input = 0.0 self.output = 0.0 self.error = 0.0 self.last_error = 0.0 self.integral = 0.0 self.output_min = 0.0 self.output_max = 100.0 # 与 C 版防饱和阈值一致 def compute(self, current_input, setpoint): self.setpoint = setpoint self.input = current_input self.error = self.setpoint - self.input # P term p_term = self.kp * self.error # I term with anti-windup self.integral += self.ki * self.error * self.sample_time_s self.integral = np.clip(self.integral, self.output_min, self.output_max) i_term = self.integral # D term (backward difference) d_term = self.kd * (self.error - self.last_error) / self.sample_time_s self.output = p_term + i_term + d_term self.last_error = self.error return self.output # 3. 仿真主流程:施加阶跃指令,记录响应 def simulate_pid_response(kp, ki, kd, duration=20, dt=0.01): pid = PIDController(kp, ki, kd, sample_time_ms=int(dt*1000)) t_span = (0, duration) t_eval = np.arange(0, duration + dt, dt) # 初始状态:y(0)=0, u(0)=0 y0 = [0.0] u_history = [] y_history = [] sp_history = [] # 仿真循环(替代 solve_ivp 的手动积分,更贴近嵌入式实时性) y = y0[0] for t in t_eval: # 阶跃设定值:5秒后从0跳到50 sp = 50.0 if t >= 5.0 else 0.0 sp_history.append(sp) # PID 计算控制量 u u = pid.compute(y, sp) u_history.append(u) # 被控对象更新(欧拉法近似) dy = (u - y) / 2.0 * dt # tau=2.0s y = y + dy y_history.append(y) return t_eval, np.array(y_history), np.array(sp_history), np.array(u_history) # 4. 执行仿真并绘图 if __name__ == "__main__": # 测试三组参数:展示不同 Kp/Ki/Kd 的影响 params_list = [ (2.0, 0.5, 0.1), # 保守:慢但稳 (8.0, 2.0, 0.5), # 激进:快但超调大 (5.0, 1.2, 0.3), # 平衡:本文推荐起点 ] plt.figure(figsize=(12, 8)) for i, (kp, ki, kd) in enumerate(params_list): t, y, sp, u = simulate_pid_response(kp, ki, kd) plt.subplot(2, 1, 1) plt.plot(t, y, label=f'Kp={kp}, Ki={ki}, Kd={kd}') plt.plot(t, sp, 'k--', alpha=0.6, label='Setpoint' if i==0 else "") plt.ylabel('Output (e.g., Temp/Speed)') plt.legend() plt.grid(True) plt.subplot(2, 1, 2) plt.plot(t, u, label=f'Control Output (Kp={kp})') plt.ylabel('Control Signal (e.g., PWM %)') plt.xlabel('Time (s)') plt.legend() plt.grid(True) plt.suptitle('PID Step Response Comparison: Tune Kp, Ki, Kd to Balance Speed/Stability') plt.tight_layout() plt.show()运行效果与解读:
- 执行
python test_pid.py后,你会看到两张图:上图是系统输出(如温度)跟踪设定值的曲线,下图是 PID 输出的控制信号(如加热功率)。 - 关键观察点:
- 若上图曲线缓慢爬升、永远追不上设定值 →Ki 太小或为 0,积分项无法消除稳态误差;
- 若上图冲过设定值后大幅震荡、迟迟不收敛 →Kp 过大或 Kd 过小,比例作用太猛,缺乏阻尼;
- 若下图控制信号在初始阶段就顶到
output_max(如 100%),之后长时间维持高位 →积分饱和已发生,需降低 Ki 或增大 output_max; - 若下图信号在设定值附近高频抖动 →Kd 过大或传感器噪声未滤波,微分项在放大噪声。
注意:此 Python 仿真用欧拉法离散化被控对象,虽不如
solve_ivp精确,但更贴近嵌入式中PID_Compute()被周期性调用的真实场景,参数迁移性更强。
4. 移植到 STM32 硬件:HAL 库环境下 PID 的中断服务与资源绑定
当 Python 仿真通过,下一步是让 PID 在真实芯片上呼吸。PID算法代码.zip中的examples/stm32_f4_demo/文件夹,就是为你准备的硬件桥接模板。这里不讲如何新建 CubeMX 工程,而是聚焦三个移植必踩的硬核细节:定时器中断精度、ADC 采样同步、PWM 输出映射。以 STM32F407 为例(最常见教学平台),假设你要控制一个带编码器的直流电机。
4.1 定时器中断:PID 计算的节拍器,毫秒级误差即灾难
PID 必须在严格固定的周期内执行,否则ki、kd的物理意义崩塌。在stm32_f4_demo/Core/Src/tim.c中,你需要配置一个高级定时器(如 TIM1)产生精确周期中断:
// Core/Src/tim.c - 关键配置片段 TIM_HandleTypeDef htim1; void MX_TIM1_Init(void) { htim1.Instance = TIM1; htim1.Init.Prescaler = 16799; // APB2=168MHz, PSC+1=16800 → 10kHz 计数频率 htim1.Init.CounterMode = TIM_COUNTERMODE_UP; htim1.Init.Period = 99; // 10kHz / 100 = 100Hz → 10ms 周期(完美匹配 sample_time_ms=10) htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_Base_Init(&htim1); HAL_TIM_Base_Start_IT(&htim1); // 启动中断 } // 中断服务函数:PID 计算唯一入口 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM1) { // 1. 读取 ADC 采样值(确保在此刻读取最新数据) uint32_t adc_val; HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 10); // 超时10ms adc_val = HAL_ADC_GetValue(&hadc1); float feedback = adc_to_physical(adc_val); // 自定义:ADC值→物理量(如转速 rpm) // 2. 执行 PID 计算 float control_output = PID_Compute(&motor_pid, feedback); // 3. 输出到 PWM(TIM3_CH1) uint32_t pwm_duty = (uint32_t)(control_output * 100.0f); // 映射到 0-10000 __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pwm_duty); } }参数说明与血泪经验:
Prescaler=16799和Period=99的组合,是为达成10ms 精确周期。任何计算误差(如Period=100)会导致实际周期变为 10.001ms,长期运行下ki累积偏差不可忽视。HAL_ADC_PollForConversion()必须放在中断内,且不能用 DMA 或回调——因为 PID 计算要求“采样-计算-输出”原子性。DMA 传输完成中断可能与 TIM1 中断嵌套,导致时序错乱。control_output是浮点数,但 PWM 占空比需整型。* 100.0f是因output_max=100.0f,若你的 PWM 寄存器是 16bit(0-65535),此处应改为* 655.35f并强转uint16_t。
4.2 ADC 与编码器:多源反馈的融合时机与抗干扰
真实系统 rarely 只有一个传感器。ZIP 包中examples/可能只示范单一 ADC,但你很可能需要融合编码器脉冲计数(测速)和电流采样(过载保护)。关键原则:所有反馈必须在同一时刻“快照”。
// Core/Inc/main.h - 新增全局结构体 extern volatile struct { uint32_t encoder_count; // 来自 TIM2 编码器接口 uint16_t current_adc; // 来自 ADC1 uint16_t temp_adc; // 来自 ADC2 } sensor_snapshot; // Core/Src/tim.c - 在 TIM1 中断中统一采集 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM1) { // 原子性读取所有传感器(利用编译器 barrier) __DMB(); // 数据内存屏障,防止编译器重排 sensor_snapshot.encoder_count = __HAL_TIM_GET_COUNTER(&htim2); sensor_snapshot.current_adc = HAL_ADC_GetValue(&hadc1); sensor_snapshot.temp_adc = HAL_ADC_GetValue(&hadc2); __DMB(); // 基于快照计算物理量 float speed_rpm = encoder_to_rpm(sensor_snapshot.encoder_count); float current_a = adc_to_ampere(sensor_snapshot.current_adc); // 决策:若电流超限,强制 PID 输出为 0 if (current_a > 2.5f) { PID_ResetIntegral(&motor_pid); motor_pid.output = 0.0f; } else { motor_pid.setpoint = target_speed_rpm; // 设定值可动态修改 PID_Compute(&motor_pid, speed_rpm); } // 输出 PWM... } }避坑点:
__DMB()是 Cortex-M4 的数据内存屏障指令,确保encoder_count、current_adc的读取顺序不被编译器优化打乱,获得真正同步的快照。- 电流保护必须在
PID_Compute()之前判断。若在计算后截断输出,积分项已累积,下次正常时会突喷大功率。
5. PID 参数整定避坑指南:3 个让工程师凌晨三点还在示波器前抓狂的真问题
参数整定不是调参游戏,而是与物理世界对话。以下是我从多个模拟项目X中总结的、最常导致“明明公式对,就是跑不稳”的 3 个硬核坑,每个都附带现象、根因与可立即执行的解决动作。
5.1 现象:空载响应完美,一加负载就振荡发散
原因:忽略了被控对象的惯性变化。空载时电机转动惯量小,kd可设较高;加载机械臂后总惯量剧增,原kd值不足以提供足够阻尼,系统相位裕度跌破安全阈值。
解决:
- 动作1:用
test_pid.py重新建模,将tau从 2.0s 改为 5.0s(模拟大惯量),再用 Ziegler-Nichols 法重新估算kd(新kd ≈ 旧 kd × (5.0/2.0)); - 动作2:在硬件端实现自适应 Kd:
kd_adaptive = kd_base * (1.0f + 0.5f * fabsf(load_estimate)),其中load_estimate可由电流环估算; - 验证:用示波器抓取 PWM 输出波形,振荡频率应随负载增加而显著降低(惯量↑→自然频率↓),若频率不变,说明
kd未起效。
5.2 现象:稳态时输出缓慢漂移,最终撞到限幅值
原因:积分项未在设定值变更时重置。例如,系统在 SP=30 时达到稳态,此时integral已稳定在某值;当 SP 突跳至 50,PID 开始加大输出,但integral仍带着旧值继续累加,导致过冲后积分项“过头”,需长时间反向修正。
解决:
- 动作1:在每次
setpoint变更时,强制调用PID_ResetIntegral(&pid); - 动作2:启用积分分离(Integral Separation):仅当
|error| < threshold(如 5% SP)时才允许积分项累加,否则integral = 0; - 代码补丁(加在
PID_Compute()开头):const float INTEGRAL_THRESHOLD = 0.05f * fabsf(pid->setpoint); // 5% of SP if (fabsf(pid->error) < INTEGRAL_THRESHOLD) { pid->integral += pid->ki * pid->error * (pid->sample_time_ms / 1000.0f); pid->integral = clamp(pid->integral, pid->output_min, pid->output_max); } else { pid->integral = 0.0f; // 误差大时冻结积分 }
5.3 现象:示波器显示 PWM 信号高频“毛刺”,电机发出啸叫
原因:微分项在放大传感器噪声。ADC 采样值本身含 1-2 LSB 噪声,kd将其放大后直接驱动执行器,形成高频扰动。
解决:
- 动作1:在
PID_Compute()中,对current_input施加一阶低通滤波(非在传感器端!):static float input_filtered = 0.0f; const float ALPHA = 0.1f; // 滤波系数,0.1=较重滤波 input_filtered = ALPHA * current_input + (1.0f - ALPHA) * input_filtered; pid->input = input_filtered; - 动作2:改用带滤波的微分(Filtered Derivative),将
d_term计算改为:float error_derivative = (pid->error - pid->last_error) / (pid->sample_time_ms / 1000.0f); static float d_filtered = 0.0f; d_filtered = 0.2f * error_derivative + 0.8f * d_filtered; // 20% 新值 + 80% 旧值 float d_term = pid->kd * d_filtered; - 验证:用逻辑分析仪抓取 ADC 原始数据流,确认噪声峰峰值 < 3LSB;若仍超标,需检查硬件滤波电容或 ADC 采样时间。
6. 进阶技巧:用实时日志 + Python 动态调参,把 PID 调试从“玄学”变成“可视实验”
最后分享一个让我彻底告别“改一个参数、编译、下载、观察、重启”循环的技巧:在 STM32 上实现 UART 实时日志 + PC 端 Python 动态调参界面。这不需要额外硬件,仅用开发板自带的 USB 转串口(如 ST-Link 的 Virtual COM Port)。
6.1 STM32 端:轻量级日志协议,每毫秒发一帧
在Core/Src/usart.c中,添加一个非阻塞日志发送函数(避免阻塞 PID 中断):
// Core/Inc/usart.h #define LOG_BUF_SIZE 64 extern uint8_t log_tx_buffer[LOG_BUF_SIZE]; extern uint16_t log_tx_len; // Core/Src/usart.c uint8_t log_tx_buffer[LOG_BUF_SIZE]; uint16_t log_tx_len = 0; void LOG_SendFrame(float sp, float fb, float out, float p, float i, float d) { // 格式:"$PID,SP,FB,OUT,P,I,D*CS\r\n",CS=ASCII码和校验 int len = snprintf((char*)log_tx_buffer, LOG_BUF_SIZE, "$PID,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f*", sp, fb, out, p, i, d); if (len > 0 && len < LOG_BUF_SIZE-2) { // 计算校验和(异或所有字符,不含$和*) uint8_t cs = 0; for (int i = 1; i < len-1; i++) cs ^= log_tx_buffer[i]; len += snprintf((char*)log_tx_buffer+len, LOG_BUF_SIZE-len, "%02X\r\n", cs); log_tx_len = len; HAL_UART_Transmit_IT(&huart2, log_tx_buffer, log_tx_len); // 非阻塞发送 } } // 在 TIM1 中断中调用(每10ms一帧) void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM1) { // ... PID 计算 ... LOG_SendFrame(motor_pid.setpoint, motor_pid.input, motor_pid.output, p_term, i_term, d_term); } }6.2 PC 端:Python 实时绘图与滑块调参
用pyserial和matplotlib.animation实现零延迟监控:
# pc_monitor.py - 运行在电脑上 import serial import numpy as np import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation from matplotlib.widgets import Slider # 串口配置 ser = serial.Serial('COM7', 115200, timeout=0.1) # 替换为你的端口号 data_buffer = {'t': [], 'sp': [], 'fb': [], 'out': []} t_start = plt.datetime.now() # 创建图形 fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 6)) line_sp, = ax1.plot([], [], 'r-', label='Setpoint') line_fb, = ax1.plot([], [], 'b-', label='Feedback') line_out, = ax2.plot([], [], 'g-', label='Output') ax1.set_ylabel('Physical Value') ax2.set_ylabel('Control Output') ax2.set_xlabel('Time (s)') ax1.legend() ax2.legend() ax1.grid(True) ax2.grid(True) # 滑块:动态修改 PID 参数(需在 STM32 端实现接收解析) ax_kp = plt.axes([0.2, 0.02, 0.5, 0.03]) slider_kp = Slider(ax_kp, 'Kp', 0.1, 20.0, valinit=5.0) ax_ki = plt.axes([0.2, 0.0, 0.5, 0.03]) slider_ki = Slider(ax_ki, 'Ki', 0.0, 5.0, valinit=1.2) def parse_log_line(line): if line.startswith(b'$PID,'): try: parts = line.decode().strip().split(',') if len(parts) >= 7 and parts[6].endswith('\r\n'): cs_received = parts[6][-2:] # 校验和 # 此处省略校验逻辑,实际项目需实现 return [float(parts[1]), float(parts[2]), float(parts[3])] except: pass return None def update_plot(frame): while ser.in_waiting: line = ser.readline() data = parse_log_line(line) if data: t_now = (plt.datetime.now() - t_start).total_seconds() data_buffer['t'].append(t_now) data_buffer['sp'].append(data[0]) data_buffer['fb'].append(data[1]) data_buffer['out'].append(data[2]) # 限制缓冲区大小,防内存爆炸 if len(data_buffer['t']) > 500: for key in data_buffer: data_buffer[key] = data_buffer[key][-500:] # 更新绘图 if data_buffer['t']: ax1.set_xlim(max(0, data_buffer['t'][-1]-30), data_buffer['t'][-1]) line_sp.set_data(data_buffer['t'], data_buffer['sp']) line_fb.set_data(data_buffer['t'], data_buffer['fb']) line_out.set_data(data_buffer['t'], data_buffer['out']) # 启动动画 ani = FuncAnimation(fig, update_plot, interval=50, cache_frame_data=False) plt.tight_layout() plt.show()落地效果:
- 运行
pc_monitor.py,你将看到两条实时曲线:上图是设定值(红)与反馈值(蓝)的追逐战,下图是 PWM 输出(绿)。 - 拖动下方滑块,
slider_kp的值会通过串口发送到 STM32(需在usart.c中添加接收中断解析逻辑),无需重新编译下载,参数即时生效。 - 我的习惯是:先用滑块把
Kp拉到系统开始振荡的临界点,记下此时Kp_critical;再将Kp设为0.6*Kp_critical,然后微调Ki消除残差——整个过程像调音一样直观。
希望帮到你。
本文还有配套的精品资源,点击获取