卡尔曼滤波与PID控制C语言实现:从算法到嵌入式联调实战
2026/9/13 10:28:47 网站建设 项目流程

简介:面向嵌入式开发、自动化控制与信号处理学习者的卡尔曼滤波与PID算法综合示例包,重点解决在C语言环境下完成状态估计与闭环控制联合调试的需求。压缩包共15个文件,约17KB,以6个C源码和4个头文件为核心,分别实现卡尔曼滤波的状态预测与更新流程、PID控制的比例积分微分逻辑,同时附带目标文件、工程配置与txt说明文档,便于对照代码理解编译和移植方法。资源包目录按功能模块整理,源码、文档与调试配置分层存放,可快速定位滤波、控制与辅助工具。已有394人浏览学习。通过阅读示例、调试配置与使用说明,可掌握两类算法在资源受限平台上的编写思路,了解传感器数据融合、噪声抑制及反馈控制参数整定的实践技巧,适合正在学习嵌入式滤波控制或准备课程设计的开发者参考。

1. 卡尔曼滤波与PID在C语言工程中的定位

做嵌入式控制的人绕不开两个坎:传感器读数满身噪声,执行机构动作又慢又钝。卡尔曼滤波解决前者,PID解决后者,但两者单独用都不够——滤波后的数据要给控制器当反馈,控制器的输出又反过来影响系统状态,这条链路才是工程实战的完整闭环。这套压缩包里的C代码,意义不在于算法本身有多新,而在于它把 kalman.c/h 和 PID_Control.c/h 放在同一个 main.c 里跑通了,包括初始化、状态转移、协方差迭代、PID输出限幅这些嵌入式中最容易被忽略的细节。适合正在做无人机、平衡车、电机调速或传感器数据融合的开发者,也适合想搞懂滤波和控制如何联调的初学者照着移植。

2. 卡尔曼滤波的C实现:预测、更新与协方差迭代

2.1 滤波器在 C 语言中的数据结构设计

卡尔曼滤波的核心不是那两条公式,而是状态向量、协方差矩阵、噪声矩阵在内存里怎么组织。这套代码里 kalman.h 的结构体设计是标准的嵌入式写法,把滤波器实例封装成结构体,每个被控对象单独 new 一个实例,互不干扰:

typedef struct { float x[2]; // 状态向量:位置、速度 float P[2][2]; // 误差协方差矩阵 float A[2][2]; // 状态转移矩阵 float H[2][2]; // 观测矩阵 float Q[2][2]; // 过程噪声协方差 float R[2][2]; // 测量噪声协方差 } Kalman_t; void Kalman_Init(Kalman_t *kf, float dt); void Kalman_Predict(Kalman_t *kf); void Kalman_Update(Kalman_t *kf, float z[]);

这里用一维运动模型:状态向量x[0]是位置,x[1]是速度,dt是采样周期。P矩阵代表对当前状态估计的置信程度,Q是模型本身的误差(比如加速度突变),R是传感器测量噪声的方差。初学时最容易搞混的是QR的物理含义——Q越大代表你越信任测量值,R越大代表你越信任模型预测值,两者比值决定滤波器的响应速度和平滑程度。

2.2 预测阶段代码与协方差传播

void Kalman_Predict(Kalman_t *kf) { kf->x[0] += kf->x[1] * dt; // 位置 = 位置 + 速度 * 时间 kf->x[1] = kf->x[1]; // 匀速度模型,速度不变 // P = A * P * A^T + Q float P00 = kf->P[0][0] + dt * (kf->P[1][0] + kf->P[0][1]) + dt * dt * kf->P[1][1] + kf->Q[0][0]; float P01 = kf->P[0][1] + dt * kf->P[1][1] + kf->Q[0][1]; float P10 = kf->P[1][0] + dt * kf->P[1][1] + kf->Q[1][0]; float P11 = kf->P[1][1] + kf->Q[1][1]; kf->P[0][0] = P00; kf->P[0][1] = P01; kf->P[1][0] = P10; kf->P[1][1] = P11; }

代码里的P00P01P10P11是直接把A * P * A^T + Q展开后的四项结果,避免引入二维矩阵乘法函数,在单片机上是省RAM和CPU的标准做法。注意A矩阵在这里被隐式写入了,没有单独分配空间。如果你的系统状态不止二维,比如要扩展加速度估计,就得老老实实写通用矩阵乘法,或者引入 Eigen 这类库,但8位MCU上不建议。

2.3 更新阶段与卡尔曼增益计算

void Kalman_Update(Kalman_t *kf, float z[]) { // 计算残差 y = z - H*x float y0 = z[0] - kf->x[0]; float y1 = z[1] - kf->x[1]; // 计算 S = H * P * H^T + R float S00 = kf->P[0][0] + kf->R[0][0]; float S11 = kf->P[1][1] + kf->R[1][1]; float det = S00 * S11 - kf->R[0][1] * kf->R[1][0]; // 2x2求逆用行列式 // 卡尔曼增益 K = P * H^T * S^-1 float K00 = kf->P[0][0] / S00; float K11 = kf->P[1][1] / S11; // 状态修正 kf->x[0] += K00 * y0; kf->x[1] += K11 * y1; // 协方差更新 P = (I - K*H) * P kf->P[0][0] *= (1.0f - K00); kf->P[1][1] *= (1.0f - K11); }

这里做了简化处理:假设观测矩阵H是单位阵,且位置和速度的观测相互独立,所以增益矩阵K退化成对角形式。完整形式下K的计算需要对(H*P*H^T + R)做矩阵求逆,2x2 矩阵求逆可以像上面代码这样用伴随矩阵法直接算det,再逐项代入。工程中实际写代码时,要留意det趋近于零的情况——如果R给得太小且P初始值过大,S可能病态,导致K震荡。

2.4 Q 和 R 矩阵的参数标定经验

参数物理含义调参经验
R传感器测量噪声方差直接对静止传感器采样几百个点,计算方差填入
Q过程模型不确定性先给一个很小的量级(如 1e-4),看滤波输出是否太“迟钝”,再逐步增大
P(0)初始协方差给一个较大的初值,如 10,表示初始状态不置信

实际场景中,卡尔曼滤波最常被误解的是:它并不等于“更平滑的低通滤波”。低通滤波只对当前测量做时间域加权,卡尔曼则是利用状态转移模型对下一时刻做预测,再用观测数据修正。换句话说,如果你不知道系统的运动模型,盲目使用卡尔曼滤波,效果可能不如一阶低通。使用这套代码前,先确认你的系统状态方程能够用常速或常加速度模型描述,否则需要修改A矩阵。

3. PID控制的离散化实现与参数工程

3.1 位置式PID与增量式PID的代码差异

PID_Control.c 里提供的结构体和使用方法,最常见的是位置式PID写法。位置式的本质是“全量输出”,直接计算当前时刻应给执行机构的绝对控制量:

typedef struct { float Kp, Ki, Kd; // 比例、积分、微分系数 float target; // 目标值 float integral; // 积分累计项 float lastError; // 上一拍误差,用于微分项 float outMax, outMin; // 输出限幅 } PID_t; float Pid_Calc(PID_t *pid, float feedback) { float error = pid->target - feedback; pid->integral += error; // 积分项累加 float derivative = error - pid->lastError; pid->lastError = error; float output = pid->Kp * error + pid->Ki * pid->integral + pid->Kd * derivative; if (output > pid->outMax) output = pid->outMax; if (output < pid->outMin) output = pid->outMin; return output; }

增量式PID则只计算控制量的增量:

float Pid_Incremental(PID_t *pid, float feedback) { float error = pid->target - feedback; float dError = error - pid->lastError; float d2Error = error - 2.0f * pid->lastError + pid->lastLastError; pid->lastLastError = pid->lastError; pid->lastError = error; float delta = pid->Kp * dError + pid->Ki * error + pid->Kd * d2Error; return delta; // 执行机构自行累加 }

两段代码对比可以明显看出,位置式适合直接输出PWM占空比、加热功率这类“绝对量”被控对象;增量式适合步进电机、或带积分累积能力的执行机构,因为增量式不会在目标值突变时让输出瞬间拉满,天然具备抗积分饱和能力。压缩包里main.c用的是位置式,但代码里把两种注释都留了,移植时按执行机构类型切换即可。

3.2 积分饱和、微分噪声与输出限幅

嵌入式PID最容易出问题的不是 P、I、D 本身,而是三个隐性问题。

积分饱和——当执行机构已经输出到极限,系统误差一直存在,积分项持续累加,一旦误差方向反转,积分项需要很长一段时间的反向误差才能“消化”掉,系统出现明显超调。常见做法有两类:一是积分限幅,给integral单独设置阈值;二是积分分离,误差超过一定范围时暂停积分。代码里只做了输出限幅,这是最基础的保护,实际项目建议叠加积分限幅。

微分项的噪声放大问题更隐蔽。位置式PID的微分项直接对误差做差分,而卡尔曼滤波器的输出本身仍含有高频残差,差分运算相当于高通滤波器,会把噪声进一步放大。折中的方案是四点差分法:

float derivative = (error - pid->lastLastError + 3.0f * (error - pid->lastError)) / 2.0f;

用多组历史数据做加权平均,牺牲少许响应速度换取控制量平稳。这也是为什么工程上常把卡尔曼滤波和PID搭配使用——卡尔曼输出的平滑状态量,恰好可以让PID的微分项不再剧烈抖动。

3.3 位置式PID离散化差分方程的理解

《PID控制》教材上常给出连续域的传递函数形式:

u(t) = Kp * e(t) + Ki * ∫e(t)dt + Kd * de(t)/dt

C语言里没法直接做积分和微分,必须离散化。用矩形法近似积分,用一阶后向差分近似微分,得到差分方程:

u(k) = Kp * e(k) + Ki * T * Σe(i) + Kd * (e(k) - e(k-1)) / T

上面的代码里,Ki * pid->integral其实就是省略了采样周期T,把Ki直接当作Ki * T的合并系数。这样做的好处是调参直观,但坏处是——如果你修改了PID的调用频率(比如从100Hz改成1kHz),不做换算的话,积分和微分增益会发生显著变化。严谨的做法是在初始化时传入采样时间dt,并把代码改成:

pid->integral += error * dt; float output = pid->Kp * error + pid->Ki * pid->integral + pid->Kd * (error - pid->lastError) / dt;

压缩包里的代码为了简单没有这样做,这点在替换其他平台时容易踩坑。

4. main.c 联调:一个温度系统的滤波与控制实战

4.1 模拟对象与被控场景

打开main.c,整套代码的实现思路是:用一个一阶惯性环节模拟被控对象(比如加热器),叠加高斯噪声模拟传感器测量误差,然后让卡尔曼滤波估计真实温度,PID根据估计值和目标温度的误差计算PWM输出,再回灌到被控对象。模拟逻辑如下:

float plant_model(float temp, float pwm, float dt) { float k = 0.02f; // 加热增益 float tau = 5.0f; // 惯性时间常数 return temp + (k * pwm - temp) / tau * dt; } float fake_sensor(float true_temp) { return true_temp + ((float)rand() / RAND_MAX - 0.5f) * 2.0f; }

主循环的控制流程设计得清晰:先执行Kalman_Predict,再读取传感器并执行Kalman_Update,然后用滤波后的状态量作为PID反馈,最后把PID输出限幅到0~100的PWM范围。这个调用顺序不是随意的——预测必须先于更新,因为更新需要用到预测阶段产生的先验状态和协方差,这是卡尔曼标准流程。

4.2 完整调用链代码

int main(void) { Kalman_t kf; PID_t pid; float temp = 25.0f; float pwm = 0.0f, measured, filtered; Kalman_Init(&kf, 0.1f); Pid_Init(&pid, 12.0f, 0.05f, 0.5f, 0.0f, 100.0f); pid.target = 60.0f; // 目标温度 60 度 for (int i = 0; i < 2000; i++) { Kalman_Predict(&kf); measured = fake_sensor(temp); float z[2] = {measured, 0.0f}; // 只有位置观测 Kalman_Update(&kf, z); filtered = kf.x[0]; pwm = Pid_Calc(&pid, filtered); temp = plant_model(temp, pwm, 0.1f); // 每隔 100 拍打印一次数据,便于串口或终端画曲线 if (i % 100 == 0) { printf("%d %.2f %.2f %.2f\n", i, temp, measured, filtered); } } return 0; }

逻辑说明:Kalman_Update传入的z数组只有z[0]有效,z[1]置0,表示系统只有位置观测而没有速度观测。此时H矩阵其实应该写成[1, 0],但在上述简化实现中,让S11等于P[1][1] + R[1][1],即整个滤波器对速度的校正完全依赖于过程模型,这种方式在惯导和电机转速估计中很常见。PID的target是目标温度,filtered是滤波后的估计温度,两者相减得到误差。pwm的输出范围被限制在0~100之间,代表加热功率百分比。

4.3 编译运行与打印结果解读

Windows下用 MinGW 或 Linux/Mac 下直接执行:

gcc main.c kalman.c PID_Control.c -o demo -lm ./demo

-lm是链接数学库,因为kalman.c中可能会用到sqrtf开方运算(例如协方差求逆),如果没有用到可以去掉。程序跑完后会输出四列数据:循环拍数、真实温度、带噪声的测量值、卡尔曼估计值。把数据导成 CSV 后画曲线,你会看到测量值上下尖刺,卡尔曼估计值比较平滑且滞后很小,PID 拿平滑数据算出来的 PWM 不会骤变。这是验证整套代码是否跑通的最直观办法。

4.4 级联PID与多传感器融合的扩展方向

这套 demo 是最基础的单级控制结构。实际项目里,卡尔曼滤波和PID经常不只是串在一个回路里——比如四轴飞行器的姿态控制,内环角速度PID和外环角度PID构成级联PID,而卡尔曼滤波在更前一级融合陀螺仪和加速度计的数据,输出姿态角给外环。此时卡尔曼的QR矩阵要根据IMU手册里的噪声密度和零偏稳定性来标定,PID参数也需要分内外环整定,且内外环频率通常相差5~10倍,这些细节决定了系统能否稳定飞行。

参考压缩包里的使用方法.txt,作者提供的流程是先跑通单变量示例,把卡尔曼和PID的输入输出打印出来,确认曲线符合预期后,再逐步引入真实传感器量和被控对象。这套打法在工程上是比较稳的——先仿真验证算法逻辑,再硬件验证模型差异。

5. 参数整定与C语言实现中的边界问题

5.1 卡尔曼滤波的初始化顺序

用这套代码最常见的报错现象是前几百个采样点滤波值剧烈震荡,甚至输出NaN。多数原因是P矩阵初始化不当。P是误差协方差估计,它会在滤波过程中自动收敛,初值大小只会影响收敛速度,但如果给的过小(比如全0),滤波器会极度自信于初始状态,导致前几步增益K很小,实际测量被忽略,状态长时间拉不回来。工程上一般把P(0)给到远大于QR的量级,比如P[0][0] = 10.0f,让滤波器快速适应。另外注意QR的元素如果是0,矩阵运算中的除法会直接溢出,这是C实现里需要手动保证的约束。

5.2 PID三个参数的经验法则与快速整定

老工程师很少用临界比例度法去严格推 PID 参数,更多是靠经验初值+微调。对于温度这类大惯性系统,经验上Ki必须很小,否则会产生振荡——加热滞后性强,积分项过早积累会产生大幅超调。对于电机转速这类快系统,Kp主导响应速度,Kd可以抑制超调,但Kd太大会引入高频干扰。从这套代码入手,我的建议是先用纯P控制,把KiKd都设为0,逐步增大Kp到系统刚好持续振荡,再用Ziegler-Nichols经验表计算KiKd的基准值,最后微调Kd来控制超调量。哪怕在嵌入式平台上,这个流程也比盲目试凑快得多。

5.3 一套可复用的验证方法与常见坑位

验证卡尔曼滤波是否生效,不要只看图形是否平滑。一个可复现的测试手段是把传感器静置,采集静态数据,此时真实值不变,滤波器输出应该收敛到测量均值附近,且协方差P的对角元持续减小并趋于稳定。如果P发散到无穷大,说明Q给的过大或者数值运算精度不够——在这套代码里所有变量都是float单精度,如果你移植到STM32并且开启FPU,性能和精度都够用,但在某些低端M0芯片上用double会严重拖慢主循环。

在实际的控制项目中还有几个高频雷区:PID调用的时间间隔不固定导致微分项失真,需要在调用时传入真实dt;滤波与控制的频率不匹配导致信号混叠,一般滤波频率至少是控制频率的2倍以上;限幅只做了输出限幅没做积分限幅导致饱和恢复延迟。这些边界问题往往在仿真中不出现,在实物上才暴露。建议把这套代码跑通后,逐一加上这层保护,系统才具备台架测试的条件。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询