PID算法详解与实现

PID算法详解与实现

一、什么是PID算法

  1. PID算法简介

    • 📘 定义:PID控制(比例-积分-微分控制)是一种常见的反馈控制算法,用于将实际输出与目标值进行比较,并根据偏差进行调整。🌐[维基百科]
    • 🔧 应用:PID控制广泛应用于工业自动化系统,如温度控制、位置控制、速度控制等。🌐[知乎]
  2. PID控制器的基本原理

    • ⚙️ 比例控制(P):比例控制根据当前偏差值进行调整,调节器的输出与偏差成正比。控制力度过大可能导致系统不稳定,过小则响应过慢。
    • 🧮 积分控制(I):积分控制根据历史误差累积进行调整,能消除长期存在的偏差,但可能导致系统超调或振荡。
    • 📈 微分控制(D):微分控制根据偏差变化速率进行调整,能预测误差趋势,快速响应变化,但对噪声较敏感。🌐[博客网]
  3. PID控制器的优缺点

    • 优点
      • 结构简单,易于实现。
      • 控制效果好,适应性强。
    • 缺点
      • 参数整定复杂,需要经验和调试。
      • 对环境变化敏感,可能需要定期调整。🌐[CSDN]

二、PID算法的C语言实现

  1. PID算法的数学公式

    • 📐 标准公式
      PID公式

    其中,( K_p ) 是比例系数,( K_i ) 是积分系数,( K_d ) 是微分系数,( e(t) ) 是当前误差。

  2. 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                // 回到循环开始
    
  3. 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; // 返回当前位置加上增量
    }
    
    
  4. 代码注释与解析

    • 结构体定义:PID 结构体包含了PID参数和历史误差信息。
    • 初始化函数:PID_Init函数初始化了PID控制器的参数。

三、使用cJSON辅助调节PID

  1. cJSON简介

     cJSON是一个轻量级的JSON解析库,用于在C语言中处理JSON数据。它的主要功能包括生成和解析JSON字符串,使得在嵌入式系统中处理数据更加方便快捷

  2. cJSON的主要作用

    • 📦 数据打包: 将PID算法的调试数据打包成JSON格式,以便传输。
    • 🔍 数据解析: 在VOFA+中解析JSON数据并绘制波形。
  3. cJSON的使用

    • 安装与配置:
      • 首先下载并安装cJSON库,将其添加到项目中。可以通过以下方式安装:🌐[github链接]
      • cJSON.ccJSON.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

  1. VOFA+的简介
    VOFA+是一款直观、灵活、强大的插件驱动高自由度的上位机,在与电气打交道的领域里,如自动化、嵌入式、物联网、机器人等,都能看到VOFA+的身影。VOFA+的名字来源于:Volt/伏特、Ohm/欧姆、Fala/法拉、Ampere/安培,是电气领域的基础单位,与他们的发明者——4位电子物理学领域的科学巨人,分别同名。他们的首字母共同构成了VOFA+的名字。

  2. VOFA+的使用

    • 🛠 基础设置:
      基础设置

    • 🖥 界面设置:
      界面设置

  3. 实时调参

    • ⚙️ 配置串口(以串口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的值,实现调试效果

    串口调参

  4. 📊 验证效果

    • 波形示例
      波形示例
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值