PID算法详解与实现
一、什么是PID算法
-
PID算法简介
-
PID控制器的基本原理
- ⚙️ 比例控制(P):比例控制根据当前偏差值进行调整,调节器的输出与偏差成正比。控制力度过大可能导致系统不稳定,过小则响应过慢。
- 🧮 积分控制(I):积分控制根据历史误差累积进行调整,能消除长期存在的偏差,但可能导致系统超调或振荡。
- 📈 微分控制(D):微分控制根据偏差变化速率进行调整,能预测误差趋势,快速响应变化,但对噪声较敏感。🌐[博客网]
-
PID控制器的优缺点
- ✅ 优点:
- 结构简单,易于实现。
- 控制效果好,适应性强。
- ❌ 缺点:
- 参数整定复杂,需要经验和调试。
- 对环境变化敏感,可能需要定期调整。🌐[CSDN]
- ✅ 优点:
二、PID算法的C语言实现
-
PID算法的数学公式
- 📐 标准公式:
其中,( K_p ) 是比例系数,( K_i ) 是积分系数,( K_d ) 是微分系数,( e(t) ) 是当前误差。
- 📐 标准公式:
-
PID算法的伪代码
- 💻 伪代码:
previous_error = 0 // 初始化上一次误差为0 integral = 0 // 初始化积分项为0 loop: // 开始循环 error = setpoint - measured_value // 计算当前误差 integral = integral + error * dt // 计算误差的累积积分 derivative = (error - previous_error) / dt // 计算误差的变化率(微分) output = Kp * error + Ki * integral + Kd * derivative // 计算PID输出 previous_error = error // 更新上一次误差为当前误差 wait(dt) // 等待时间间隔dt goto loop // 回到循环开始
-
PID算法的C语言实现代码
-
💾 实现代码:
-
PID.h
#ifndef __PID_H #define __PID_H typedef struct { float target_val; // 目标值 float actual_val; // 实际值 float err; // 误差 float err_last; // 上次误差 float err_sum; // 累计误差 float Kp; // 比例 float Ki; // 积分 float Kd; // 微分系数 } tPID; // 定义一个结构体类型变量 extern tPID PID; //结构体名根据自己需要设置 // 给结构体类型变量赋初值 void PID_Init(void); // PID控制函数 float PID_Realize(tPID *pid, float actual_val); float Incremental_PID_Realize(tPID *PID, float position, float actual_val); #endif
- PID.c
#include "PID.h" // 定义一个结构体类型变量 tPID PID; // 给结构体类型变量赋初值 void PID_Init() { PID.target_val = 0.00; PID.actual_val = 0.00; PID.err = 0.00; PID.err_last = 0.00; PID.err_sum = 0.00; PID.Kp = 0.00; PID.Ki = 0.00; PID.Kd = 0.00; } /******************* * @brief 位置式PID控制函数 * @param *PID: 指向PID控制器结构体的指针 * actual_val: 当前实际值 * @return 调节后的输出值 *******************/ float Position_PID_Realize(tPID *PID, float actual_val) { PID->actual_val = actual_val; // 传递真实值 PID->err = PID->target_val - PID->actual_val; // 当前误差=目标值-真实值 PID->err_sum += PID->err; // 误差累计值 = 当前误差累计和 // 使用PID控制 输出 = Kp*当前误差 + Ki*误差累计值 + Kd*(当前误差-上次误差) PID->actual_val = PID->Kp * PID->err + PID->Ki * PID->err_sum + PID->Kd * (PID->err - PID->err_last); PID->err_last = PID->err; // 保存当前误差以备下次使用 return PID->actual_val; // 返回控制器输出的实际值 } /******************* * @brief 增量式PID控制函数 * @param *PID: 指向PID控制器结构体的指针 * actual_val: 当前实际值 * target_val: 目标值 * @return 调节后的输出位置 *******************/ float Incremental_PID_Realize(tPID *PID, float actual_val, float target_val) { float increment; // 增量变量 PID->err = actual_val - target_val; // 当前误差 PID->err_sum += PID->err; // 误差累计值 = 当前误差累计和 // 计算增量 = Kp*当前误差 + Ki*误差累计值 + Kd*(当前误差-上次误差) increment = PID->Kp * PID->err + PID->Ki * PID->err_sum + PID->Kd * (PID->err - PID->err_last); PID->err_last = PID->err; // 保存当前误差以备下次使用 return actual_val + increment; // 返回当前位置加上增量 }
-
-
代码注释与解析
- 结构体定义:
PID
结构体包含了PID参数和历史误差信息。 - 初始化函数:
PID_Init
函数初始化了PID控制器的参数。
- 结构体定义:
三、使用cJSON辅助调节PID
-
cJSON简介
cJSON是一个轻量级的JSON解析库,用于在C语言中处理JSON数据。它的主要功能包括生成和解析JSON字符串,使得在嵌入式系统中处理数据更加方便快捷。
-
cJSON的主要作用
- 📦 数据打包: 将PID算法的调试数据打包成JSON格式,以便传输。
- 🔍 数据解析: 在VOFA+中解析JSON数据并绘制波形。
-
cJSON的使用
- 安装与配置:
- 首先下载并安装cJSON库,将其添加到项目中。可以通过以下方式安装:🌐[github链接]
- 将cJSON.c和cJSON.h文件添加到工程中,并在代码中包含头文件。
- JSON数据的解析与调试:
#include "cJSON.h" #include <string.h> cJSON *cJsonData ,*cJsonVlaue; if (Usart_WaitReasFinish() == 0) // 是否接收完毕 { cJsonData = cJSON_Parse((const char *)Usart1_ReadBuf); // 解析接收缓冲区中的JSON数据 if (cJSON_GetObjectItem(cJsonData, "p") != NULL) // 检查是否存在键"p" { cJsonVlaue = cJSON_GetObjectItem(cJsonData, "p"); // 获取键"p"的值 p = cJsonVlaue->valuedouble; // 将值赋给变量p PID.Kp = p; // 设置PID的Kp参数 } if (cJSON_GetObjectItem(cJsonData, "i") != NULL) // 检查是否存在键"i" { cJsonVlaue = cJSON_GetObjectItem(cJsonData, "i"); // 获取键"i"的值 i = cJsonVlaue->valuedouble; // 将值赋给变量i PID.Ki = i; // 设置PID的Ki参数 } if (cJSON_GetObjectItem(cJsonData, "d") != NULL) // 检查是否存在键"d" { cJsonVlaue = cJSON_GetObjectItem(cJsonData, "d"); // 获取键"d"的值 d = cJsonVlaue->valuedouble; // 将值赋给变量d PID.Kd = d; // 设置PID的Kd参数 } if (cJSON_GetObjectItem(cJsonData, "target_val") != NULL) // 检查是否存在键"target_val1" { cJsonVlaue = cJSON_GetObjectItem(cJsonData, "target_val"); // 获取键"target_val"的值 target_val1 = cJsonVlaue->valuedouble; // 将值赋给变量target_val PID.target_val = target_val; // 设置PID的目标值 } if (cJsonData != NULL) { cJSON_Delete(cJsonData); // 释放cJSON对象内存 } memset(Usart1_ReadBuf, 0, 255); // 清空接收缓冲区 } printf("P:%.3f I:%.3f D:%.3f target_val:%.3f\n", p, i, d, target_val); // 打印当前PID参数和目标值
- 安装与配置:
四、使用VOFA+调节PID
-
VOFA+的简介
VOFA+是一款直观、灵活、强大的插件驱动高自由度的上位机,在与电气打交道的领域里,如自动化、嵌入式、物联网、机器人等,都能看到VOFA+的身影。VOFA+的名字来源于:Volt/伏特、Ohm/欧姆、Fala/法拉、Ampere/安培,是电气领域的基础单位,与他们的发明者——4位电子物理学领域的科学巨人,分别同名。他们的首字母共同构成了VOFA+的名字。 -
VOFA+的使用
-
🛠 基础设置:
-
🖥 界面设置:
-
-
实时调参
-
⚙️ 配置串口(以串口1为例)
- 调大堆栈
-
软件开启中断
-
开启接收中断
__HAL_UART_ENABLE_IT(&huart1,UART_IT_RXNE); //开启串口1接收中断
- 中断回调函数
uint8_t Usart1_ReadBuf[256]; //串口1 缓冲数组 uint8_t Usart1_ReadCount = 0; //串口1 接收字节计数 if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_RXNE))//判断huart1 是否读到字节 { if(Usart1_ReadCount >= 255) Usart1_ReadCount = 0; HAL_UART_Receive(&huart1,&Usart1_ReadBuf[Usart1_ReadCount++],1,1000); }
- 编写函数用于判断串口是否发送完一帧数据
extern uint8_t Usart1_ReadBuf[255]; //串口1 缓冲数组 extern uint8_t Usart1_ReadCount; //串口1 接收字节计数 //判断否接收完一帧数据 uint8_t Usart_WaitReasFinish(void) { static uint16_t Usart_LastReadCount = 0;//记录上次的计数值 if(Usart1_ReadCount == 0) { Usart_LastReadCount = 0; return 1;//表示没有在接收数据 } if(Usart1_ReadCount == Usart_LastReadCount)//如果这次计数值等于上次计数值 { Usart1_ReadCount = 0; Usart_LastReadCount = 0; return 0;//已经接收完成了 } Usart_LastReadCount = Usart1_ReadCount; return 2;//表示正在接受中 }
-
🔧 通过发送数据调参
- 重定向fputc
typedef struct __FILE FILE; int fputc(int ch, FILE *stream) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); return ch; }
- mian.c中添加此句形成波形。
printf("MotorSpeed:%f,\n", MotorSpeed); //根据自己的变量名进行修改
- 串口输出栏发送
{"p":0.00,"i":0.00,"d":0.00,"target_val":0.00}
改变Kp、Ki、Kd和target_val的值,实现调试效果
-
-
📊 验证效果
- 波形示例
- 波形示例