GD32F103实战:PID控制算法从源码到稳定波形全解析
简介基于GD32F103的PID算法源代码zip包是面向嵌入式开发者的控制算法实战资源系统展示了比例-积分-微分控制器的完整实现适用于电机调速、温度控制、智能车循迹等需要闭环反馈的自动控制场景对学习MCU控制算法和工程化封装均有帮助。压缩包共216个文件除核心的31个C源文件与34个头文件外还包含Keil工程文件uvprojx、uvoptx可直接打开44个crf与45个o文件记录了编译中间过程配合hex、map、lst、axf等文件可核查构建与烧录细节整体体积仅7.38MB。源代码中还使用了GD32F10x标准外设库中的定时器、ADC等底层驱动方便衔接采样与控制输出。已有211人下载学习是嵌入式入门与进阶阶段较实用的参考。通过阅读代码中的初始化、误差计算、PID输出更新与限幅处理等关键流程可理解P、I、D三项的作用与调参逻辑并直接修改用于自身项目提升系统响应速度与稳定性配套的编译产物也为复现实验、对比输出提供了便利。 前阵子整理网盘又翻出了这个基于GD32F103的PID算法源代码包。说实话这种资源在各大代码站上一搜一大把但真正能把它用起来、调出稳定波形的人并不多。我见过太多人卡在同一个地方——不是看不懂pid.c里那几十行C代码而是不清楚整个闭环是怎么从ADC采样跑到PWM输出的更不懂拿到代码之后怎么把三个系数调到能用的状态。这篇博文就围绕这个源码包把硬件选型逻辑、位置式与增量式两种算法的取舍、工程实现细节、参数整定方法到实际调试踩过的坑完整梳理一遍。无论你是刚接触GD32的新手还是想快速给现有项目接入PID控制的老手应该都能从中拿到一些可以直接抄作业的东西。1. GD32F103做PID控制这套硬件选型的内在逻辑1.1 GD32F103的平台定位与核心参数GD32F103是兆易创新基于ARM Cortex-M3内核的32位通用MCU和STM32F103在引脚上基本兼容但主频可以跑到108MHz比同定位的STM32F103高了36MHz。这个差异在实际PID控制场景里不是跑分优势而是实打实的余量——意味着你在一个定时器中断里同时做编码器读取、AT指令解析、OLED刷新和PID运算时CPU负载依然可控。Flash最高到1MBSRAM最高96KB对于PID控制这种对存储要求极低的应用来说完全够用。更重要的是它的定时器资源非常丰富高级定时器通用定时器可以同时支撑多路PWM输出和编码器接口模式做双闭环或者多轴控制都不至于外设打架。1.2 PID控制场景对MCU外设的真实需求很多人选型时容易陷入主频焦虑觉得PID是高深算法必须上强芯。实际上对于温度控制、电机调速、平衡车这类经典PID场景MCU的真实需求并不在算力而在这三样外设定时器的PWM输出通道用来输出控制量。以电机调速为例12kHz到20kHz的PWM频率是主流区间既要避开人耳可听范围又要让H桥驱动有足够的开关效率。108MHz主频分频下来能得到非常细腻的占空比分辨率。ADC采样通道用来采集被控量的反馈。比如温度传感器经运放调理后的电压、电位器位置信号等。GD32F103的ADC是12位采样速度足够覆盖绝大多数慢变和中等速度被控对象。正交编码器接口或外部中断用来接速度传感器。GD32F103的通用定时器自带编码器模式直接读TIMERx的计数寄存器就能拿到电机转速连外部中断计数都省了。如果这三大件没有再牛的PID算法也只是数学游戏落不了地。这也是为什么GD32F103这个平台特别适合做PID学习和小型工控项目——它在20元以内的价位上把这些外设给全了。1.3 拿到源码后先确认的工程环境这个源代码包打开之后一般会看到KEIL的uvprojx工程文件或者IAR的项目文件。下手改代码之前建议先确认三件事库版本GD32官方旧标准库和新版固件库在接口命名上差异不小比如GPIO初始化在新库中直接用gpio_init统一搞定而旧库里还分gpio_init加gpio_mode_set。这篇文里的示例以GD32F10x固件库为准版本不同时对照官方GD32F10x_User_Guide调整即可。芯片具体型号GD32F103有C8/CB/RB/RC等后缀Flash大小和SRAM不同链接脚本和宏定义比如GD32F10C_START还是GD32F10X_CL不能选错。系统时钟配置默认工程多半是108MHz主频输出但如果手头板子用的是外部晶振且没起振代码会停在时钟切换的死循环里。现象就是下载后程序完全不跑。排查时先量晶振或直接改用内部RC振荡器IRC8M跑通。2. 算法选型与工程化改型位置式还是增量式2.1 两种离散PID的数学本质连续PID的表达式是u(t) Kp*e(t) Ki*∫e(t)dt Kd*de(t)/dt到了MCU里必须离散化。因为采样周期T固定积分可以退化成累加微分可以退化成差分就得到位置式PIDu(k) Kp*e(k) Ki*∑e(i) Kd*(e(k) - e(k-1))这里的∑e(i)是误差从启动到现在的累加和要求MCU长期维护这个变量。而增量式是从位置式推出来的——把u(k-1)的表达式写出来两边相减积分累加项就消掉了只剩下三次误差的线性组合Δu(k) Kp*(e(k) - e(k-1)) Ki*e(k) Kd*(e(k) - 2e(k-1) e(k-2))u(k) u(k-1) Δu(k)增量式的拉普拉斯本质是只输出控制量的变化值不输出绝对值。2.2 为什么嵌入式工程更偏爱增量式我之前做电机调速和舵机跟随用的几乎都是增量式。原因很现实无积分饱和问题增量式推导后没有单独的积分累加器误差项天然被Ki以比例形式限制在当前误差上。即使长时间堵转或者执行器饱和控制器输出也只会在限幅边界附近小幅摆动不会像位置式那样积分项越积越大等误差反向时半天退不出来导致超调。执行器带记忆如果控制对象是带锁存特性的设备——比如步进驱动器的脉冲数、伺服的位置给定、阀门的开度累加——增量式的输出天然适合这种每次只要给一个变化量的工作模式。手动/自动无扰切换从手动模式切回自动时位置式要先把积分项初始化成当前输出否则会有瞬间的跳变增量式只需要把上一个u(k-1)刷成当前手动输出切换过程平滑得多。但位置式也并非一无是处。对于温度控制这种执行器是0到100%占空比的纯比例设备位置式的输出语义更直观——直接就是此刻该给的加热功率。增量式此时反而要额外维护一个u(k-1)不然关掉PID再打开输出记忆还在加热棒会莫名其妙继续加热。实际做项目时两种算法我都不排斥看执行器类型定。2.3 积分限幅、输出限幅、微分滤波这些隐形代码不能省很多精简版PID源码只给一个裸算法跑起来效果一塌糊涂问题往往不出在公式而出在缺少工程化保护。这个源码包里的实现相对完整重点看这几处输出限幅不管你用位置式还是增量式最终输出必须夹在安全范围内。电机驱动器的PWM占空比超过100%没有意义加热棒输出为负更是灾难。增量式里是u(k)限幅位置式里是u(k)直接限幅实现上都是一句if判断的事但没有它系统第一次大扰动就能飞车。积分限幅/积分分离位置式PID必须有积分限幅我不止一次看到积分累计到几万后系统才反应迟钝的情况。更高级的做法是误差超过阈值就掐断积分即积分分离效果在电机启动阶段特别明显。微分滤波纯微分项Kd*(e(k)-e(k-1))对噪声非常敏感。ADC采样本身就带量化噪声再叠加工频干扰微分项能直接变成高频抖动源表现为PWM输出freq很高的小幅波动电机跟着嗡嗡响。工程上要么对反馈值做一阶低通要么用不完全微分结构。3. 核心源码拆解PID模块与硬件驱动的调用链路3.1 一个完整PID工程由哪几块组成直接看这个源码包的目录核心文件就四类pid驱动模块pid.h/pid.c算法本体与硬件完全解耦。结构体里放了目标值、反馈值、三次历史误差、三个系数、积分累加变量、输出限幅和积分限幅改系数不需要重新编译整个工程。外设驱动模块定时器PWM输出、ADC采样、编码器读取、按键和显示。这一层是平台相关的换MCU时主要改这里。控制调度层一般放在定时器中断或一个精确的时基任务里以固定周期调用PID计算函数。控制周期是PID调参的重要参数必须稳定不能出现中断里面干活太久导致周期抖动。应用层起停逻辑、异常保护、参数下发等。我拿到一个PID源码包第一件事就是顺着ADC采样 → PID计算 → PWM输出这条线把数据流读通而不是先抠算法公式。数据流通了代码就懂了一半。3.2 PID核心代码结构体与两种实现下面这部分是这个源码包的核心我用C语言做了一个符合标准GD32开发风格的实现示例。先看头文件里的结构体和接口/* pid.h */ #ifndef __PID_H #define __PID_H typedef struct { float target; /* 目标值 */ float feedback; /* 反馈值 */ float err; /* 当前误差 e(k) */ float err_last; /* 上一次误差 e(k-1) */ float err_prev; /* 上上次误差 e(k-2) */ float kp; /* 比例系数 */ float ki; /* 积分系数 */ float kd; /* 微分系数 */ float integral; /* 积分累加项 */ float integral_max; /* 积分限幅 */ float out; /* 输出 */ float out_max; /* 输出限幅 */ } pid_t; void pid_init(pid_t *pid, float kp, float ki, float kd, float out_max, float integral_max); float pid_positional(pid_t *pid, float target, float feedback); float pid_incremental(pid_t *pid, float target, float feedback); #endif位置式实现注意积分限幅和输出限幅的顺序——先夹积分再算输出最后夹输出/* pid.c */ #include pid.h void pid_init(pid_t *pid, float kp, float ki, float kd, float out_max, float integral_max) { pid-target 0.0f; pid-feedback 0.0f; pid-err pid-err_last pid-err_prev 0.0f; pid-kp kp; pid-ki ki; pid-kd kd; pid-integral 0.0f; pid-integral_max integral_max; pid-out 0.0f; pid-out_max out_max; } float pid_positional(pid_t *pid, float target, float feedback) { float p_out, i_out, d_out; pid-target target; pid-feedback feedback; pid-err_last pid-err; pid-err target - feedback; /* P */ p_out pid-kp * pid-err; /* 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; i_out pid-ki * pid-integral; /* D实际应用中Kd参数已经包含了1/T的因子按统一采样周期标定 */ d_out pid-kd * (pid-err - pid-err_last); pid-out p_out i_out d_out; /* 输出限幅 */ if (pid-out pid-out_max) pid-out pid-out_max; if (pid-out -pid-out_max) pid-out -pid-out_max; return pid-out; }增量式实现关键区别是函数返回的是控制量增量Δu你要在调用方维护一个u(k-1)并对u(k)做限幅。上面的结构体里的out就是用来做这个累计的float pid_incremental(pid_t *pid, float target, float feedback) { float delta, d_err; pid-target target; pid-feedback feedback; pid-err_prev pid-err_last; pid-err_last pid-err; pid-err target - feedback; /* 增量式三项 */ delta pid-kp * (pid-err - pid-err_last); delta pid-ki * pid-err; d_err pid-err - 2.0f * pid-err_last pid-err_prev; delta pid-kd * d_err; pid-out delta; if (pid-out pid-out_max) pid-out pid-out_max; if (pid-out -pid-out_max) pid-out -pid-out_max; return pid-out; }看过很多精简教程把增量式的Ki直接抄成位置式里的Ki*T结果调出来比例效果弱得离谱因为量纲对不上。增量式里Ki的物理含义是Kp*T/Ti你在仿真工具里整定的Ki值直接搬过来行为是不一样的要重新标定。3.3 PWM与ADC配置控制输出和反馈采集的底座PID算法再正确PWM配错了也是白搭。以GD32F103控制一个直流电机为例PWM输出用通用定时器TIMER1通道0对应引脚PA0。GD32固件库的写法是这样的void pwm_init(void) { timer_oc_parameter_struct timer_ocinitpara; timer_parameter_struct timer_initpara; rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_TIMER1); gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_0); timer_deinit(TIMER1); /* 定时器结构体初始化 */ timer_struct_para_init(timer_initpara); timer_initpara.prescaler 107; /* 108MHz/(1071)1MHz */ timer_initpara.period 999; /* 1MHz/(9991)1kHz */ timer_initpara.clockdivision TIMER_CKDIV_DIV1; timer_initpara.alignedmode TIMER_COUNTER_EDGE; timer_initpara.counterdirection TIMER_COUNTER_UP; timer_init(TIMER1, timer_initpara); /* 输出比较结构体初始化 */ timer_channel_output_struct_para_init(timer_ocinitpara); timer_ocinitpara.outputstate TIMER_CCX_ENABLE; timer_ocinitpara.ocpolarity TIMER_OC_POLARITY_HIGH; timer_channel_output_config(TIMER1, TIMER_CH_0, timer_ocinitpara); timer_channel_output_pulse_value_config(TIMER1, TIMER_CH_0, 500); timer_channel_output_mode_config(TIMER1, TIMER_CH_0, TIMER_OC_MODE_PWM0); timer_enable(TIMER1); }这里PWM频率是1kHz适合加热控制或对噪音不敏感的场合。如果是电机建议把prescaler改成119MHz、period改成899约10kHz。频率提高后占空比分辨率会降低period越小一格对应的占空比精度越粗这是硬件限制要自己做取舍。ADC采样配置同样有讲究。GD32F103的ADC时钟建议不要超过14MHz需要先分频再使能。下面这段配置了ADC0的通道0对应PA0引脚读取温度传感器电压void adc_init(void) { rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_ADC0); gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_0); adc_clock_config(ADC_ADCCK_PCLK2_DIV8); /* PCLK2通常为54MHz8分频后小于14MHz */ adc_channel_length_config(ADC0, ADC_ROUTINE_CHANNEL, 1); adc_routine_channel_config(ADC0, 0, ADC_CHANNEL_0, ADC_SAMPLETIME_55POINT5); adc_external_trigger_config(ADC0, ADC_EXT_TRIGGER_ROUTINE, ENABLE); adc_enable(ADC0); }采回来的12位原始值要换算成实际物理量。比如NTC温度采样先算电压再查分压电阻的换算表或直接套Steinhart-Hart方程。我建议PID反馈值统一用物理量纲比如摄氏度、RPM不要用ADC原始码值直接当反馈不然换一批硬件温度标定全乱。3.4 GD32库与STM32库的写法差异提醒如果你是从STM32F103的标准外设库转过来的有几个坑提前说GPIO初始化在STM32里往往要写上百行的GPIO_InitStructureGD32标准库精简成了gpio_init直接传GPIO_MODE_AF_PP这类枚举少写很多行。时钟使能函数名从RCC_APB2PeriphClockCmd变成了rcu_periph_clock_enable对应的外设RCU定义也不同比如TIMER1对应RCU_TIMER1而不是RCU_TIMER1命名规则整体趋同但首字母有差异。GD32定时器的通道枚举是TIMER_CH_0到TIMER_CH_3与STM32的TIM_Channel_1到TIM_Channel_4要区分。最稳妥的办法是下载源码包附带的手册或者去GD官网拉对应固件库的Examples目录对照着写。一个工程里混用两套库风格的代码编译能过但调试起来非常痛苦。4. 参数整定实操从Z-N公式到稳定控制4.1 先搞懂每个参数在管什么调参之前心里要对三个参数有直观感觉Kp比例当前误差的放大倍数。Kp越大同样的误差产生的输出越大系统响应越快但过大就会振荡甚至发散。只调Kp的系统稳态必然有残差——比如加热系统到目标温度附近输出和散热刚好平衡时误差停留在某个不为零的小值上。Ki积分负责吃掉稳态误差。它把历史误差累加起来哪怕误差很小累加久了也能推动输出变化。Ki过大有两个恶果一是积分项在启动阶段快速累积输出冲过头形成大超调二是容易诱发低频振荡。Kd微分根据误差的变化趋势提前动作相当于刹车。误差在快速减小时微分项反向拉输出抑制超调。但微分对噪声极敏感系数大了系统会高频抖动摸上去执行器都在细微震颤。4.2 临界比例法完整步骤我不太推荐一上来就试凑容易陷入调Kp → 振荡 → 降Kp → 响应慢 → 加Ki → 振荡 → ...的死循环。有理论依据的整定法里最适合作业的是齐格勒-尼科尔斯Ziegler-Nichols临界比例法。操作步骤是这样的先把Ki和Kd全部设为0只用纯比例控制。给系统一个阶跃给定逐步增大Kp观察被控量的响应。直到输出出现等幅振荡为止记录下这个临界增益Ku和振荡周期Tu。按下表计算各参数控制器类型KpKiKdP0.5 × Ku——PI0.45 × KuKp / (0.83 × Tu)—PID0.6 × KuKp / (0.5 × Tu)Kp × (0.125 × Tu)注意表中Ki是按时间量纲计算的换算成离散代码里的系数时要乘上你的采样周期T。比如采样周期10msPID表里Ki算出来是1.2那代码里写的Ki就是1.2×0.010.012。这个换算漏掉整定参数直接失控。这个方法给出的参数是保守起跳点不是终点。实际接入系统后我会按先Kp微调再Ki消除静差最后Kd抑制超调的顺序做二次精调每次只动一个参数观察两三个控制周期的波形再动下一个。4.3 调参过程中真实踩过的三个坑坑一微分项把你骗了噪声被当成快速变化我做温度控制时ADC采样值本身就带±1到±2码的跳动温度曲线上肉眼看着很平。加Kd后输出PWM波形开始密集抖动加热棒开关频率异常结果温度波动比不加Kd还大。后来在ADC原始值上做了一阶低通滤波抖动立刻消失。所以微分项前面加滤波不是可选项是必选项。滤波系数取多少看你的采样频率和反馈噪声带宽我一般先用移动平均取8到16点梯形或滑动窗口都行。坑二积分限幅设得太宽松系统恢复时间感人电机带载启动时误差大位置式PID的积分项在半秒内冲到几百。电机转到位后误差反向积分项要从几百慢慢退回来这个退积分的过程就是超调的来源。把积分限幅从500改成50之后超调量从30%降到10%以内。经验值是先设成输出限幅的1/4再看波形调整。坑三不同控制周期下同一组系数表现完全不同我把同样的PID参数从4ms控制周期挪到20ms系统直接不稳定。因为离散PID的系数与采样周期强相关换了周期就必须重新整定。工程上一个常见错误是这个参数在仿真里跑过没问题结果仿真步长1ms实际中断10ms两码事。固定控制周期并保持稳定是调参的前提。5. 实测与扩展这套源码的下限和上限5.1 两类典型场景的实测参考我用这套源码做过两个代表性项目数据可以作为你的起点参考。直流电机恒速控制增量式PID控制周期10msPWM频率10kHz编码器分辨率256线实际反馈rpm。整定后参数为Kp0.8、Ki0.012、Kd0.02注意这里的Ki已经包含了T因子。空载阶跃从0冲到1200rpm超调约5%稳定时间约0.4s。稳态误差能压到±3rpm以内。加热台恒温控制位置式PID控制周期500msPWM频率1Hz级用固态继电器通断NTC反馈。参数为Kp8、Ki0.05、Kd0纯PI控制效果已经够用。140℃阶跃超调约2℃稳定时间3分钟左右。温度波动±0.5℃以内。你会发现温度控制这种大惯性系统Kd几乎用不上微分项反而容易引入噪声。不要迷信三个参数都上对象特性决定哪些项该用。5.2 后续扩展方向这套源码只是一个基础框架往上扩展的空间非常大串级PID比如把电机调速改成电流环速度环结构内环电流环周期1ms外环速度环周期10ms。GD32F103的定时器和ADC资源完全能支撑双环内环输出作为电流给定控制性能会有质的提升。前馈补偿在PID输出上叠加一个前馈项比如温度控制中根据环境温度估算一个基础加热功率作为前馈PID只需要负责修正偏差响应速度和稳定性都能改善。模糊PID/自适应PID对Kp、Ki、Kd做在线模糊调整适合被控对象特性随工况变化明显的场景。GD32F103的108MHz主频跑一个简单模糊推理毫无压力。参数掉电保存把整定好的系数写进Flash或外挂EEPROM避免每次上电都手动输入。GD32内部Flash操作比较方便但注意擦写寿命和地址规划。有心的话还可以把控制周期做成可配置项运行时通过串口在上位机上动态调整采样周期和三个PID系数一边跑一边看波形。这个调试手段比改一次烧录一次效率高一倍不止很多商业电调也保留了这个接口。最后分享一个我个人的习惯每次拿到新的控制对象我会先在开环状态下手动给输出摸清执行器的输入范围和被控量的变化速度再决定PID的控制周期和输出方向。方向搞反了正反馈接成负反馈会让整定过程极其痛苦而这恰恰是很多人在代码里查了半天也发现不了的底层问题。先把这个理顺再看波形你会发现PID调参并没有那么玄。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →