基于自适应算法和增量式PID算法的模拟直升飞机控制系统

文章目录

  • 基于自适应算法和增量式PID算法的模拟直升飞机控制系统
    • 控制系统硬件
      • 单片机系统
      • 传感器系统介绍
      • 直升机模拟系统介绍
    • 系统模块介绍
      • ADC采样系统
      • STM32
        • CUBEMX配置
          • ADC1和ADC3的配置
            • 图形初始化
            • TIMEer定时器界面
          • ADC2的配置
            • 图形化初始化
            • Timer定时器界面
            • 中断初始化
        • 代码源码(不感兴趣的可以跳过)
          • ADC.c 与ADC.h
          • ADC3中断函数的说明
          • ADC1,2初始化时的说明
        • 测试结果
          • ADC1
          • ADC2
          • ADC3
        • 硬件说明(ADC1,2,3通用)
          • ADC硬件连接
      • 51ADC
        • DA 转换
        • AD 转换
        • 代码
      • 控制算法
        • 自适应控制算法
          • 自适应控制算法的原理
          • 自适应控制算法的代码
          • 自适应控制算法的不足
          • 控制算法的其他说明
            • 自适应控制PID结构体数组
            • 自适应控制PID 计算输出电压代码
        • 增量式 PID 控制算法
          • 增量式PID算法的原理
          • 增量式PID算法的代码
          • 增量式PID算法的不足
      • LED显示模块
      • 画点函数
    • 程序顺序分析
      • 初始化
      • while循环控制
    • 实验总结
      • 实验结果
      • 实验回顾

控制系统硬件

模拟直升机垂直升降控制系统主要由C8051F020单片机、按键和显示等模块以及直升机垂直升降模拟对象组成。

单片机系统

显示功能由液晶和数码管实现,液晶屏和数码管分别实现显示控制主菜单界面、当前霍尔电压的数值及变化曲线等功能,按键用于切换液晶屏显示内容、设置PID参数、增加和减少霍尔电压 设定值等功能。

传感器系统介绍

SS49E线性霍尔传感器具有体积小,用途广泛等特点。SS49E可由永磁体或电磁铁进行操作,电源电压 控制线性输出,可根据磁场强度的不同做成线性变化。SS49E内部集成了低噪声输出电路,省去了外部滤波 器的使用。器件包含了薄膜电阻,增加了温度的稳定性和精度。SS49E的工作电压为4.5V~6V。

直升机模拟系统介绍

直升机垂直升降模拟对象系统原理图如图2-2所示,实物如图2-3所示,接口说明如表2-1所示。

系统模块介绍

ADC采样系统

ADC采样系统因为是对整个项目生命最重要的部分,这里我们想要通过对于Stm32F103与C8051F020单片机的差距,来展现更高级系统对于控制的作用。

STM32

ADC实现思路:

我们首先需要了解STM32F103系列所拥有的三个高精度ADC,

显然我们可以看到这里fadc工作频率是14MHZ,而系统时钟是72MHZ,显然需要把分频因子设置为6。因为我们后续还希望CPU在处理ADC时执行更多操作,显然不应该使用轮询,由于我们希望由定时器中断触发ADC的采样,然后DMA源源不断的将代码从ADC的寄存器移动到指定数组。


CUBEMX配置

ADC1和ADC3的配置
图形初始化

以ADC3的配置举例,(ADC3的通道8,对应PF10)

  • 在ADC配置中需要修改 External Trigger Convision Source为定时器8触发,Sampling Time修改为7.5Cycles,Continuous Conversion Mode(连续转换)改为ENABLE。
  • 在DMA配置中,Data Width(数据宽)改为Half World. Mode 改为循环,这至关重要,这使得DMA传送不会停止,笔者一开始默认了NORMAL,DEBug发现每次只传送一次,就结束了 ,方向改为Peripheral to Memory
TIMEer定时器界面

要想实现每0.x秒的精准采样,这一点是必不可少的,因为我们如果只使用DMA技术,ADC将不断以高频率采样,笔者发现这种速度太快且不可控,尤其在实际生产中,我们还需要协调3个ADC的关系,如果3个ADC都在同一个时刻采样,显然是可以很好的解决我们的问题。

  • 分频系数改为1999,Trigger Event Selection 改为自动更新,每次中断结束后,自己重新开始计数。

ADC2的配置
图形化初始化

因为ADC2没有DMA系统,所以我们需要使用单独的Timer3来产生中断,这里我们用一个小技巧。我们如果直接设置ADC2由TIMER3触发,发生很奇怪的现象,始终没有中断写入。不如直接使用软件中断触发,然后写两个中断回调函数来判断,这么做本质原因就是调用HAL库之后,程序员部分失去了底层的控制。

  • Continuous Conversion Mode :改为Disabled
  • Samping Time (因为没有DMA,我们丧失了快速反应能力,不如直接增加Samping Time) :41.5 Cycles
Timer定时器界面

  • TIMER3的初始设置和TIMER8一样
中断初始化

  • 中断初始化只需要将值(0,0)赋值给(Preemption Priority, Sub Pority)

  • 确实没有必要给ADC太高的优先级,因为我们ADC采样是不断进行的,我们应该注意串口的优先级,串口再发送数据时,发送WIFI串口优先级>>发送Debug优先级>>ADC. 个人觉得打断串口的中断是一种疯狂的行为,曾经这个错误把我折腾了一下午,甚至无法靠调试得到结果


代码源码(不感兴趣的可以跳过)

ADC.c 与ADC.h

代码主体由CUBEMX组成,我们需要理解后,对指定部分进行更改,我们在后面进行了详细注释,尤其是修改部分

(ADC1 ADC3)( ADC2)分别有两种配置方式

/* USER CODE BEGIN Header */
/********************************************************************************* @file    adc.c* @brief   This file provides code for the configuration*          of the ADC instances.******************************************************************************* @attention** Copyright (c) 2022 STMicroelectronics.* All rights reserved.** This software is licensed under terms that can be found in the LICENSE file* in the root directory of this software component.* If no LICENSE file comes with this software, it is provided AS-IS.********************************************************************************/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "adc.h"/* USER CODE BEGIN 0 */
__IO uint16_t ADC_ConvertedValue1;
__IO uint16_t ADC_ConvertedValue2;
__IO uint16_t ADC_ConvertedValue3;
#include "tim.h"
/* USER CODE END 0 */ADC_HandleTypeDef hadc1;
ADC_HandleTypeDef hadc2;
ADC_HandleTypeDef hadc3;
DMA_HandleTypeDef hdma_adc1;
DMA_HandleTypeDef hdma_adc3;/* ADC1 init function */
void MX_ADC1_Init(void)
{/* USER CODE BEGIN ADC1_Init 0 */__HAL_RCC_DMA1_CLK_ENABLE();/* USER CODE END ADC1_Init 0 */ADC_ChannelConfTypeDef sConfig = {0};/* USER CODE BEGIN ADC1_Init 1 *//* USER CODE END ADC1_Init 1 *//** Common config*/hadc1.Instance = ADC1;hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;hadc1.Init.ContinuousConvMode = ENABLE;hadc1.Init.DiscontinuousConvMode = DISABLE;hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T8_TRGO;hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;hadc1.Init.NbrOfConversion = 1;if (HAL_ADC_Init(&hadc1) != HAL_OK){Error_Handler();}/** Enable or disable the remapping of ADC1_ETRGREG:* ADC1 External Event regular conversion is connected to TIM8 TRG0*/__HAL_AFIO_REMAP_ADC1_ETRGREG_ENABLE();/** Configure Regular Channel*/sConfig.Channel = ADC_CHANNEL_4;sConfig.Rank = ADC_REGULAR_RANK_1;sConfig.SamplingTime = ADC_SAMPLETIME_7CYCLES_5;if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK){Error_Handler();}/* USER CODE BEGIN ADC1_Init 2 *///HAL_ADC_Start_DMA(&hadc1, (uint32_t*)&ADC_ConvertedValue1, 1);/* USER CODE END ADC1_Init 2 */
}
/* ADC2 init function */
void MX_ADC2_Init(void)
{/* USER CODE BEGIN ADC2_Init 0 *//* USER CODE END ADC2_Init 0 */ADC_ChannelConfTypeDef sConfig = {0};/* USER CODE BEGIN ADC2_Init 1 *//* USER CODE END ADC2_Init 1 *//** Common config*/hadc2.Instance = ADC2;hadc2.Init.ScanConvMode = ADC_SCAN_DISABLE;hadc2.Init.ContinuousConvMode = DISABLE;hadc2.Init.DiscontinuousConvMode = DISABLE;hadc2.Init.ExternalTrigConv = ADC_SOFTWARE_START;hadc2.Init.DataAlign = ADC_DATAALIGN_RIGHT;hadc2.Init.NbrOfConversion = 1;if (HAL_ADC_Init(&hadc2) != HAL_OK){Error_Handler();}/** Configure Regular Channel*/sConfig.Channel = ADC_CHANNEL_2;sConfig.Rank = ADC_REGULAR_RANK_1;sConfig.SamplingTime = ADC_SAMPLETIME_41CYCLES_5;if (HAL_ADC_ConfigChannel(&hadc2, &sConfig) != HAL_OK){Error_Handler();}/* USER CODE BEGIN ADC2_Init 2 *//* USER CODE END ADC2_Init 2 */}
/* ADC3 init function */
void MX_ADC3_Init(void)
{/* USER CODE BEGIN ADC3_Init 0 */__HAL_RCC_DMA2_CLK_ENABLE();/* USER CODE END ADC3_Init 0 */ADC_ChannelConfTypeDef sConfig = {0};/* USER CODE BEGIN ADC3_Init 1 *//* USER CODE END ADC3_Init 1 *//** Common config*/hadc3.Instance = ADC3;hadc3.Init.ScanConvMode = ADC_SCAN_DISABLE;hadc3.Init.ContinuousConvMode = ENABLE;hadc3.Init.DiscontinuousConvMode = DISABLE;hadc3.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T8_TRGO;hadc3.Init.DataAlign = ADC_DATAALIGN_RIGHT;hadc3.Init.NbrOfConversion = 1;if (HAL_ADC_Init(&hadc3) != HAL_OK){Error_Handler();}/** Configure Regular Channel*/sConfig.Channel = ADC_CHANNEL_8;sConfig.Rank = ADC_REGULAR_RANK_1;sConfig.SamplingTime = ADC_SAMPLETIME_7CYCLES_5;if (HAL_ADC_ConfigChannel(&hadc3, &sConfig) != HAL_OK){Error_Handler();}/* USER CODE BEGIN ADC3_Init 2 *///HAL_ADC_Start_DMA(&hadc3, (uint32_t*)&ADC_ConvertedValue3, 1);/* USER CODE END ADC3_Init 2 */}void HAL_ADC_MspInit(ADC_HandleTypeDef* adcHandle)
{GPIO_InitTypeDef GPIO_InitStruct = {0};if(adcHandle->Instance==ADC1){/* USER CODE BEGIN ADC1_MspInit 0 *//* USER CODE END ADC1_MspInit 0 *//* ADC1 clock enable */__HAL_RCC_ADC1_CLK_ENABLE();__HAL_RCC_GPIOA_CLK_ENABLE();/**ADC1 GPIO ConfigurationPA4     ------> ADC1_IN4*/GPIO_InitStruct.Pin = GPIO_PIN_4;GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);/* ADC1 DMA Init *//* ADC1 Init */hdma_adc1.Instance = DMA1_Channel1;hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;hdma_adc1.Init.Mode = DMA_CIRCULAR;hdma_adc1.Init.Priority = DMA_PRIORITY_MEDIUM;if (HAL_DMA_Init(&hdma_adc1) != HAL_OK){Error_Handler();}__HAL_LINKDMA(adcHandle,DMA_Handle,hdma_adc1);/* ADC1 interrupt Init */HAL_NVIC_SetPriority(ADC1_2_IRQn, 0, 0);HAL_NVIC_EnableIRQ(ADC1_2_IRQn);/* USER CODE BEGIN ADC1_MspInit 1 *//* USER CODE END ADC1_MspInit 1 */}else if(adcHandle->Instance==ADC2){/* USER CODE BEGIN ADC2_MspInit 0 *//* USER CODE END ADC2_MspInit 0 *//* ADC2 clock enable */__HAL_RCC_ADC2_CLK_ENABLE();__HAL_RCC_GPIOC_CLK_ENABLE();__HAL_RCC_GPIOA_CLK_ENABLE();/**ADC2 GPIO ConfigurationPC2     ------> ADC2_IN12PA2     ------> ADC2_IN2*/GPIO_InitStruct.Pin = GPIO_PIN_2;GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);GPIO_InitStruct.Pin = GPIO_PIN_2;GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);/* ADC2 interrupt Init */HAL_NVIC_SetPriority(ADC1_2_IRQn, 0, 0);HAL_NVIC_EnableIRQ(ADC1_2_IRQn);/* USER CODE BEGIN ADC2_MspInit 1 *//* USER CODE END ADC2_MspInit 1 */}else if(adcHandle->Instance==ADC3){/* USER CODE BEGIN ADC3_MspInit 0 *//* USER CODE END ADC3_MspInit 0 *//* ADC3 clock enable */__HAL_RCC_ADC3_CLK_ENABLE();__HAL_RCC_GPIOF_CLK_ENABLE();/**ADC3 GPIO ConfigurationPF10     ------> ADC3_IN8*/GPIO_InitStruct.Pin = GPIO_PIN_10;GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);/* ADC3 DMA Init *//* ADC3 Init */hdma_adc3.Instance = DMA2_Channel5;hdma_adc3.Init.Direction = DMA_PERIPH_TO_MEMORY;hdma_adc3.Init.PeriphInc = DMA_PINC_DISABLE;hdma_adc3.Init.MemInc = DMA_MINC_ENABLE;hdma_adc3.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;hdma_adc3.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;hdma_adc3.Init.Mode = DMA_CIRCULAR;hdma_adc3.Init.Priority = DMA_PRIORITY_MEDIUM;if (HAL_DMA_Init(&hdma_adc3) != HAL_OK){Error_Handler();}__HAL_LINKDMA(adcHandle,DMA_Handle,hdma_adc3);/* USER CODE BEGIN ADC3_MspInit 1 *//* USER CODE END ADC3_MspInit 1 */}
}void HAL_ADC_MspDeInit(ADC_HandleTypeDef* adcHandle)
{if(adcHandle->Instance==ADC1){/* USER CODE BEGIN ADC1_MspDeInit 0 *//* USER CODE END ADC1_MspDeInit 0 *//* Peripheral clock disable */__HAL_RCC_ADC1_CLK_DISABLE();/**ADC1 GPIO ConfigurationPA4     ------> ADC1_IN4*/HAL_GPIO_DeInit(GPIOA, GPIO_PIN_4);/* ADC1 DMA DeInit */HAL_DMA_DeInit(adcHandle->DMA_Handle);/* ADC1 interrupt Deinit *//* USER CODE BEGIN ADC1:ADC1_2_IRQn disable *//*** Uncomment the line below to disable the "ADC1_2_IRQn" interrupt* Be aware, disabling shared interrupt may affect other IPs*//* HAL_NVIC_DisableIRQ(ADC1_2_IRQn); *//* USER CODE END ADC1:ADC1_2_IRQn disable *//* USER CODE BEGIN ADC1_MspDeInit 1 *//* USER CODE END ADC1_MspDeInit 1 */}else if(adcHandle->Instance==ADC2){/* USER CODE BEGIN ADC2_MspDeInit 0 *//* USER CODE END ADC2_MspDeInit 0 *//* Peripheral clock disable */__HAL_RCC_ADC2_CLK_DISABLE();/**ADC2 GPIO ConfigurationPC2     ------> ADC2_IN12PA2     ------> ADC2_IN2*/HAL_GPIO_DeInit(GPIOC, GPIO_PIN_2);HAL_GPIO_DeInit(GPIOA, GPIO_PIN_2);/* ADC2 interrupt Deinit *//* USER CODE BEGIN ADC2:ADC1_2_IRQn disable *//*** Uncomment the line below to disable the "ADC1_2_IRQn" interrupt* Be aware, disabling shared interrupt may affect other IPs*//* HAL_NVIC_DisableIRQ(ADC1_2_IRQn); *//* USER CODE END ADC2:ADC1_2_IRQn disable *//* USER CODE BEGIN ADC2_MspDeInit 1 *//* USER CODE END ADC2_MspDeInit 1 */}else if(adcHandle->Instance==ADC3){/* USER CODE BEGIN ADC3_MspDeInit 0 *//* USER CODE END ADC3_MspDeInit 0 *//* Peripheral clock disable */__HAL_RCC_ADC3_CLK_DISABLE();/**ADC3 GPIO ConfigurationPF10     ------> ADC3_IN8*/HAL_GPIO_DeInit(GPIOF, GPIO_PIN_10);/* ADC3 DMA DeInit */HAL_DMA_DeInit(adcHandle->DMA_Handle);/* USER CODE BEGIN ADC3_MspDeInit 1 *//* USER CODE END ADC3_MspDeInit 1 */}
}/* USER CODE BEGIN 1 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)    //定时器中断回调
{HAL_ADC_Start_IT(&hadc2);
}void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* AdcHandle){HAL_ADC_Stop_IT(&hadc2);    HAL_TIM_Base_Stop_IT(&htim3);ADC_ConvertedValue2=HAL_ADC_GetValue(&hadc2);HAL_TIM_Base_Start_IT(&htim3);
}
ADC3中断函数的说明

这里注意,一定要在中断里面关闭时钟,然后处理完再打开,这是一个好习惯,因为谁都不知道会在里面处理多久,如果不关闭,下一次中断来临,可能会造成程序异常。血的教训

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)    //定时器中断回调
{HAL_ADC_Start_IT(&hadc2); //定时器中断里面开启ADC中断转换,1ms开启一次采集
}void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* AdcHandle){HAL_ADC_Stop_IT(&hadc2);    //关闭adc2HAL_TIM_Base_Stop_IT(&htim3);  //关闭时钟Timer2ADC_ConvertedValue2=HAL_ADC_GetValue(&hadc2);//传递值HAL_TIM_Base_Start_IT(&htim3);  //打开时钟Timer2
}
ADC1,2初始化时的说明

为了使用DMA传输,我们需要在第一句使能DMA时钟,但是注意我们不用HAL库生成的DMA_init,其存在一定的时间逻辑混乱。

void MX_ADC1_Init(void)
{/* USER CODE BEGIN ADC1_Init 0 */__HAL_RCC_DMA1_CLK_ENABLE();/* USER CODE END ADC1_Init 0 */ADC_ChannelConfTypeDef sConfig = {0};...}void MX_ADC3_Init(void)
{/* USER CODE BEGIN ADC3_Init 0 */__HAL_RCC_DMA2_CLK_ENABLE();/* USER CODE END ADC3_Init 0 */ADC_ChannelConfTypeDef sConfig = {0};/* USER CODE BEGIN ADC3_Init 1 */...}

测试结果

ADC1

ADC2

ADC3

硬件说明(ADC1,2,3通用)

ADC硬件连接

由于ADC采样只能采集0-3.3v之间的电压,所以需要硬件设计中考虑对传感器的分压,建议采用2个贴片电阻将5V分压至3.3.v,请务必考虑ADC的输入电阻RAIN

本嵌入式,均采用Ts=55.5

51ADC

DA 转换

DAC 的输出方式有很多,如果选择了通过定时器 2 溢出来更新 DAC 的输出,由于 C8051F02 单片机的DAC0 为 12 位精度的数模转换器,当用 16 位来存储时,就产生了不同的数据对齐方式。设置 DAC0 时, 首先在初始化部分设置 DAC0 的更新方式和对齐模式,随后为 DAC0 选择参考电压(一般为内部参考电压), 随后将数据寄存器清 0 就完成了初始化。DAC0 的输出控制,在定时器 2 中断溢出时,把需要输出的电压所 对应的值送给高、低数据寄存器即可。

AD 转换

ADC0 是 12 位精度的模数转换器,和 DAC0 一样,也需要一个定时器来控制时序。不同的是 ADC0 不是靠定时器溢出的中断来开始转换,而是在定时器溢出的时候不触发中断,并且在转换结束的时候触发自 己的中断来让微处理器读取数据。定时器 3 的初始化与其它定时器的初始化略有不同,由于不需要定时器 溢出中断,在初始化时,应关闭定时器中断并立刻开始计时。
(1)ADC0 初始化

ADC0 的初始化设置相比于 DAC0 复杂了一些。除了设置采用定时器 3 溢出启动、数据对齐格式和参 考电压之外,还需要设置 ADC0 的采样通道、ADC0 的 SAR 时钟频率、ADC0 的增益,并且使能 ADC0 转 换结束的中断请求。这些参数可以根据需要自行修改。从 ADC0 中读取转换的结果时,在中断处理程序中,首先需要将中断触发为手动清零,随后利用缓冲 的思想,将 ADC0 的各个通道数值读入 buffer 中,不断变换通道,就可以从不同通道读取数值,这样可以 避免时序对于程序的影响。ADC0 的采样频率如果大于程序采用的频率,则 buffer 会不断被覆盖,这样可以 保证用到的数据都是最新的采样值,虽然浪费了 ADC0 的能力,但是不会导致程序出现问题。然而,当 ADC0 的采样频率低于系统利用采样值的频率时,就需要系统进行等待,以防止同一采样值的重复读取,可以采 用下述方法:如果 buffer 经过了读取,则为 buffer 赋一个不可能采到的值(如 0xffff),判断只要读取的值 出现异常,主程序就进行等待,直到获得正常的数值。

(2)ADC0 数据处理
ADC0 获取的数据处理时,从 ADC0 获取的 12 位数据需要转化为 10 进制数据。同时,由于物理环境的影响,ADC0 读取的数值并不会那么准确,甚至连线性都不能保证。计算时需要对 ADC0 的采样值进行 修正,修正的办法就是假设 ADC0 是线性的,ADC0 的输入端分别接入的是 1V、5V 电压,获取 ADC0对应的数值并做归一化处理,ADC0 就可以较为准确的读取输入电压了。例如在模拟直升机垂直升降控制系统中,需要根据外部放大电路,对 ADC0 的输入电压进行适当缩小,并转化为 12 位二进制数送给 ADC0 的数据寄存器。

代码

void Do(void) {if ((timer1_value & 0x0007) == 0x0001) {     if (channel == 1)     {// 从 ADC0 AIN1 取得 10 位 16 进制数 vadcvadc = ADC_ValueReturn(channel);// 将 vadc 转化为 10 进制数进行计算vadc_dec = (unsigned long int)vadc * (unsigned long int)vref / 4096;PID_contrl(&tmp_pid,vadc_dec);vdac_dec=vadc_dec+tmp_pid.result;// 将 10 进制数 vdac_dec 转化为 16 进制数vdac = (unsigned long int)vdac_dec * 4096 / (unsigned long int)vref;// 从 ADC0 输出 10 位 16 进制数 vdacDAC0_Output(vdac);//调节目标霍尔电压值if(KEY_FLAG==1){tmp_pid.setpoint-=100; //用于控制目标电压KEY_FLAG=0;}else{if(KEY_FLAG==2){tmp_pid.setpoint+=100; //用于控制目标电压KEY_FLAG=0;}}}}}

控制算法

自适应控制算法

自适应控制算法的原理

自适应过程是一个不断逼近目标的过程。它所遵循的途径以数学模型表示,称为自适应算法。通常采用基于梯度的算法,其中最小均方误差算法尤为常用。自适应算法可以用硬件(处理电路)或软件(程序控制)两种办法实现。前者依据算法的数学模型设计电路,后者则将算法的数学模型编制成程序并用计算机实现。算法有很多种,它的选择很重要,它决定处理系统的性能质量和可行性。

自适应控制算法的代码
void FuzzyPID(); //初始化PID参数
float FuzzyPIDcontroller(float e_max, float e_min, float ec_max, float ec_min, float kp_max, float kp_min, float error, float error_c, float ki_max, float ki_min,float kd_max, float kd_min,float error_pre, float error_ppre);  //模糊PID控制实现函数
float Quantization(float maximum, float minimum, float x);  //误差 error 和误差变化 error_c 映射到论域中的函数
void Get_grad_membership(float error, float error_c);   计算输入e与de/dt隶属度
void GetSumGrad();// 获取输出增量 △kp、△ki、△kd 的总隶属度
void GetOUT();  // 计算输出增量 △kp、△ki、△kd 对应论域值
float Inverse_quantization(float maximum, float minimum, float qvalues);  //去模糊化const int  num_area = 8; //划分区域个数
float e_membership_values[7] = {-3,-2,-1,0,1,2,3}; //输入e的隶属值
float ec_membership_values[7] = { -3,-2,-1,0,1,2,3 };//输入de/dt的隶属值
float kp_menbership_values[7] = { -3,-2,-1,0,1,2,3 };//输出增量kp的隶属值
float ki_menbership_values[7] = { -3,-2,-1,0,1,2,3 }; //输出增量ki的隶属值
float kd_menbership_values[7] = { -3,-2,-1,0,1,2,3 };  //输出增量kd的隶属值float kp;                       //PID参数kp
float ki;                       //PID参数ki
float kd;                       //PID参数kd
float qdetail_kp;               //增量kp对应论域中的值
float qdetail_ki;               //增量ki对应论域中的值
float qdetail_kd;               //增量kd对应论域中的值
float detail_kp;                //输出增量kp
float detail_ki;                //输出增量ki
float detail_kd;                //输出增量kd
float qerror;                    //输入e对应论域中的值
float qerror_c;                  //输入de/dt对应论域中的值
float e_gradmembership[2];      //输入e的隶属度
float ec_gradmembership[2];     //输入de/dt的隶属度
int e_grad_index[2];            //输入e隶属度在规则表的索引
int ec_grad_index[2];           //输入de/dt隶属度在规则表的索引
float KpgradSums[7] = { 0,0,0,0,0,0,0 };   //输出增量kp总的隶属度
float KigradSums[7] = { 0,0,0,0,0,0,0 };   //输出增量ki总的隶属度
float KdgradSums[7] = { 0,0,0,0,0,0,0 };   //输出增量kd总的隶属度float e_max = 150;      //误差最大值
float e_min = -150;    //误差最小值
float ec_max = 300;     //误差变化最大值
float ec_min = -300;    //误差变化最小值
float kp_max = 50;       //比例系数 kp 上限值
float kp_min = -50;    //比例系数 kp 下限值
float ki_max = 0.1;     //积分系数 ki 上限值
float ki_min = -0.1;    //积分系数 ki 下限值
float kd_max = 0.01;    //微分系数 kd 上限值
float kd_min = -0.01;   //微分系数 kd 下限值
float error;        //误差值
float error_c;      //误差变化值
float error_pre = 0;    //上一次误差值
float error_ppre = 0;   //上上次误差值int  Kp_rule_list[7][7] = { {PB,PB,PM,PM,PS,ZO,ZO},        //kp规则表{PB,PB,PM,PS,PS,ZO,NS},{PM,PM,PM,PS,ZO,NS,NS},{PM,PM,PS,ZO,NS,NM,NM},{PS,PS,ZO,NS,NS,NM,NM},{PS,ZO,NS,NM,NM,NM,NB},{ZO,ZO,NM,NM,NM,NB,NB} };int  Ki_rule_list[7][7] = { {NB,NB,NM,NM,NS,ZO,ZO},     //ki规则表{NB,NB,NM,NS,NS,ZO,ZO},{NB,NM,NS,NS,ZO,PS,PS},{NM,NM,NS,ZO,PS,PM,PM},{NM,NS,ZO,PS,PS,PM,PB},{ZO,ZO,PS,PS,PM,PB,PB},{ZO,ZO,PS,PM,PM,PB,PB} };int  Kd_rule_list[7][7] = { {PS,NS,NB,NB,NB,NM,PS},     //kd规则表{PS,NS,NB,NM,NM,NS,ZO},{ZO,NS,NM,NM,NS,NS,ZO},{ZO,NS,NS,NS,NS,NS,ZO},{ZO,ZO,ZO,ZO,ZO,ZO,ZO},{PB,NS,PS,PS,PS,PS,PB},{PB,PM,PM,PM,PS,PS,PB} };void FuzzyPID()  //参数初始化
{kp = 0;ki = 0;kd = 0;qdetail_kp = 0;qdetail_ki = 0;qdetail_kd = 0;
}//模糊PID控制实现函数
float FuzzyPIDcontroller(float e_max, float e_min, float ec_max, float ec_min, float kp_max, float kp_min, float error, float error_c,float ki_max,float ki_min,float kd_max,float kd_min,float error_pre,float error_ppre)
{float output;qerror = Quantization(e_max, e_min, error);     //将 误差 error 映射到论域中qerror_c = Quantization(ec_max, ec_min, error_c);      //将误差变化 error_c 映射到论域中Get_grad_membership(qerror, qerror_c);  //计算误差 error 和误差变化 error_c 的隶属度GetSumGrad();    //计算输出增量 △kp、△ki、△kd 的总隶属度GetOUT();     // 计算输出增量 △kp、△ki、△kd 对应论域值detail_kp = Inverse_quantization(kp_max, kp_min, qdetail_kp);    //去模糊化得到增量 △kpdetail_ki = Inverse_quantization(ki_max, ki_min, qdetail_ki);    //去模糊化得到增量 △kidetail_kd = Inverse_quantization(kd_max, kd_min, qdetail_kd);    //去模糊化得到增量 △kdqdetail_kd = 0;qdetail_ki = 0;qdetail_kp = 0;kp = kp + detail_kp;    //得到最终的 kp 值ki = ki + detail_ki;    //得到最终的 ki 值kd = kd + detail_kd;    //得到最终的 kd 值if (kp < 0){kp = 0;}if (ki < 0){ki = 0;}if (kd < 0){kd = 0;}detail_kp = 0;detail_ki = 0;detail_kd = 0;output = kp*(error - error_pre) + ki * error + kd * (error - 2 * error_pre + error_ppre);   //计算最终的输出return output;
}///区间映射函数
float Quantization(float maximum,float minimum,float x)
{float qvalues= 6.0 *(x-minimum)/(maximum - minimum)-3;return qvalues;
}//输入e与de/dt隶属度计算函数
void Get_grad_membership(float error,float error_c)
{int i;if (error > e_membership_values[0] && error < e_membership_values[6]){for ( i = 0; i < num_area - 2; i++){if (error >= e_membership_values[i] && error <= e_membership_values[i + 1]){e_gradmembership[0] = -(error - e_membership_values[i + 1]) / (e_membership_values[i + 1] - e_membership_values[i]);e_gradmembership[1] = 1+(error - e_membership_values[i + 1]) / (e_membership_values[i + 1] - e_membership_values[i]);e_grad_index[0] = i;e_grad_index[1] = i + 1;break;}}}else{if (error <= e_membership_values[0]){e_gradmembership[0] = 1;e_gradmembership[1] = 0;e_grad_index[0] = 0;e_grad_index[1] = -1;}else if (error >= e_membership_values[6]){e_gradmembership[0] = 1;e_gradmembership[1] = 0;e_grad_index[0] = 6;e_grad_index[1] = -1;}}if (error_c > ec_membership_values[0] && error_c < ec_membership_values[6]){for ( i = 0; i < num_area - 2; i++){if (error_c >= ec_membership_values[i] && error_c <= ec_membership_values[i + 1]){ec_gradmembership[0] = -(error_c - ec_membership_values[i + 1]) / (ec_membership_values[i + 1] - ec_membership_values[i]);ec_gradmembership[1] = 1 + (error_c - ec_membership_values[i + 1]) / (ec_membership_values[i + 1] - ec_membership_values[i]);ec_grad_index[0] = i;ec_grad_index[1] = i + 1;break;}}}else{if (error_c <= ec_membership_values[0]){ec_gradmembership[0] = 1;ec_gradmembership[1] = 0;ec_grad_index[0] = 0;ec_grad_index[1] = -1;}else if (error_c >= ec_membership_values[6]){ec_gradmembership[0] = 1;ec_gradmembership[1] = 0;ec_grad_index[0] = 6;ec_grad_index[1] = -1;}}}// 获取输出增量kp,ki,kd的总隶属度
void GetSumGrad()
{int i;int j;// 初始化 Kp、Ki、Kd 总的隶属度值为 0for ( i = 0; i <= num_area - 1; i++){KpgradSums[i] = 0;KigradSums[i] = 0;KdgradSums[i] = 0;}for ( i = 0; i < 2; i++){if (e_grad_index[i] == -1){continue;}for ( j = 0; j < 2; j++){if (ec_grad_index[j] != -1){int indexKp = Kp_rule_list[e_grad_index[i]][ec_grad_index[j]] + 3;int indexKi = Ki_rule_list[e_grad_index[i]][ec_grad_index[j]] + 3;int indexKd = Kd_rule_list[e_grad_index[i]][ec_grad_index[j]] + 3;KpgradSums[indexKp]= KpgradSums[indexKp] + (e_gradmembership[i] * ec_gradmembership[j]);KigradSums[indexKi] = KigradSums[indexKi] + (e_gradmembership[i] * ec_gradmembership[j]);KdgradSums[indexKd] = KdgradSums[indexKd] + (e_gradmembership[i] * ec_gradmembership[j]);}else{continue;}}}
}// 计算输出增量kp,kd,ki对应论域值
void GetOUT()
{int i;for ( i = 0; i < num_area - 1; i++){qdetail_kp += kp_menbership_values[i] * KpgradSums[i];qdetail_ki += ki_menbership_values[i] * KigradSums[i];qdetail_kd += kd_menbership_values[i] * KdgradSums[i];}
}//反区间映射函数
float Inverse_quantization(float maximum, float minimum, float qvalues)
{float x = (maximum - minimum) *(qvalues + 3)/6 + minimum;return x;
}
自适应控制算法的不足

​ 在实际代码中,由于系统已经进行了优化,但是仍然存在只能在相当狭窄的死区内活动,也就是说由于算力的问题造成了会忽然无法实现控制。

控制算法的其他说明
自适应控制PID结构体数组
typedef struct{int setpoint; //设定值long sumerror; //误差的总和     float P;            //pfloat I;         //Ifloat D; //Dint lasterror;//当前误差int preverror;//前一次误差int result;//本次PID结果
}PID;
自适应控制PID 计算输出电压代码
void PID_contrl(PID* ptr,int nowpoint){int tmp_error;int tmp_common;tmp_error = ptr->setpoint - nowpoint;    ptr->sumerror+=tmp_error;tmp_common=tmp_error-ptr->lasterror;ptr->result=FuzzyPIDcontroller(2000, -2000,500, -500, 50, -50, tmp_error, tmp_common, 0.1,-0.1,0.01, -0.01,ptr->lasterror, ptr->preverror);ptr->preverror=ptr->lasterror;ptr->lasterror = tmp_error;
}

增量式 PID 控制算法

*:该算法为验收时的代码,但是非发送的代码

增量式PID算法的原理

和位置式PID控制不同,增量式PID控制将当前时刻的控制量和上一时刻的控制量做差,以差值为新的控制量,是一种递推式的算法。

增量式PID算法的代码

u[n−1]=Kp{e[n−1]+TTi∑i=0n−1e[i]+TdT{e[n−1]−e[n−2]}}u[n-1]=K_{p}\left\{e[n-1]+\frac{T}{T_{i}} \sum_{i=0}^{n-1} e[i]+\frac{T_{d}}{T}\{e[n-1]-e[n-2]\}\right\} u[n−1]=Kp​{e[n−1]+Ti​T​i=0∑n−1​e[i]+TTd​​{e[n−1]−e[n−2]}}

extern float Kp
extern float T
extern float Ti
extern float Td
void PID_contrl(PID* ptr,int nowpoint){int tmp_error;int tmp_common;tmp_error = ptr->setpoint - nowpoint;   tmp_common=tmp_error-ptr->lasterror;ptr->result=Kp*(ptr->lasterror+(T/Ti)*ptr->sumerror+Td/T*(ptr->preverror-ptr->lasterror);ptr->sumerror+=tmp_error;ptr->preverror=ptr->lasterror;ptr->lasterror = tmp_error;
}
增量式PID算法的不足

增量式PID对于不同的器件效果完全不同,需要人工自己调节PID参数,相当的麻烦。

LED显示模块

这部分只需要修改实验1例程即可得到结果。
void my_LedDispNum(unsigned int num1,unsigned int num2,unsigned int num3)
{unsigned char temp1[4];unsigned char temp2[4];unsigned char temp3[4];temp1[0] = num1%10;temp1[1] = num1%100/10;temp1[2] = num1%1000/100; temp1[3] = num1/1000;temp2[0] = num2%10;temp2[1] = num2%100/10;temp2[2] = num2%1000/100; temp2[3] = num2/1000;temp3[0] = num3%10;temp3[1] = num3%100/10;temp3[2] = num3%1000/100; temp3[3] = num3/1000;select(4);display(temp1[0]); Delay1(500); P7 = 0xff;select(3);display(temp1[1]); Delay1(500); P7 = 0xff;select(2);display(temp1[2]); Delay1(500); P7 = 0xff;select(1);display(temp1[3]); P7 = P7 & ~0x80;if(temp1[3] == 0) P7 = 0xff; Delay1(500); P7 = 0xff; select(8);display(temp2[0]); Delay1(500); P7 = 0xff;select(7);display(temp2[1]); Delay1(500); P7 = 0xff;select(6);display(temp2[2]); Delay1(500); P7 = 0xff;select(5);display(temp2[3]); P7 = P7 & ~0x80;Delay1(500); P7 = 0xff; // Delay(500);select(12);display(temp3[0]); Delay1(500); P7 = 0xff;select(11);display(temp3[1]); Delay1(500); P7 = 0xff;select(10);display(temp3[2]); Delay1(500); P7 = 0xff;select(9) ;display(temp3[3]); P7 = P7 & ~0x80;Delay1(500); P7 = 0xff; // Delay(500);
}

画点函数

这是我在网上找到的一段规范代码,就是用于将X依次画出
void LcdShowPoint(unsigned char x)
{unsigned char i;unsigned char col=x/16;unsigned char off=x%16;unsigned char row1=wave[x]/128;unsigned char datah1=0;unsigned char datal1=0;for(i=0;i<8;i++){if(i<=off&&wave[col*16+i]/128==row1) datah1|=0x80>>i;if(i+8<=off&&wave[col*16+8+i]/128==row1) datal1|=0x80>>i;}WriteCommand(0x34);WriteCommand(0x80+31-row1);WriteCommand(0x80+col);WriteCommand(0x30);WriteData(datah1);WriteData(datal1);WriteCommand(0x32);WriteCommand(0x36);
}

程序顺序分析

由于逻辑完全是一条直线的方式,这里不再使用逻辑框图

初始化

需要初始化时钟和中断,这里额外提出,我们这段代码很多同学都不会。应当理解IF的意义。

int main(void) {int i=0,j=0;Device_Init();PID_init(&tmp_pid);
}
void INT_Init(void) {IT1 = 0;//enable INT1EX1 = 1;//enable all interruptEA = 1;  INT1_Type1;     // 设定 TCON 中断标志位 2,INT1 中断为边缘触发Enable_INT1;    // 设定 IE 标志位 2,允许 INT1 中断请求
}

while循环控制

while (1)
{       my_LedDispNum(vadc_dec,tmp_pid.setpoint,vdac_dec);//打印点PID_display();//PID屏幕初始化vdac=0;DAC0_Output(vdac);KEY_FLAG=0;LcdInit();if(KEY_FLAG==1){ //屏幕退出控制LcdClear(); ImageShow(blank);KEY_FLAG=0;}while(KEY_FLAG!=3){my_LedDispNum(vadc_dec,tmp_pid.setpoint,vdac_dec);//打印Do();//实现控制,函数在上面adc部分已介绍i+=1;if(i%5==0){      //每隔五个点打印一次wave[j]=vadc_dec;LcdShowPoint(j);i=0;j+=1;if(j==128)//屏幕到达边界{ImageShow(blank);j=0;}}}}

实验总结

实验结果

使用增量式PID时可以很快得到很好的结果,但是自适应算法明显可以发现由于算力的缺失,无法实现我们想要的功能。

实验回顾

之前在大创控制小车就有使用PID算法的经验,不难发现在这次实验中,第一次接触到直升机模拟器,感觉十分有意思了,在其中也遇到按键移植后无法使用的问题,但是最终都被我逐个克服,感谢老师在这个过程中的帮助,这片说明文档已经和项目一起发布在gitee以及我自己搭建的个人网站上

基于自适应算法和增量式PID算法的模拟直升飞机控制系统相关推荐

  1. LabVIEW增量式PID算法控制房间温度变化的简单例子的程序

    PID相关的基础知识可以查看我之前写的博客: PID算法的基础知识 基于PID算法的房间温度控制 增量式PID算法控制房间温度变化的简单例子 LabVIEW简单的PID控制程序 前面板

  2. 三菱PLC增量式PID算法FB(带死区设置和外部复位控制)

    关于PID废话不多说,各种位置式增量式资料和公式网上也非常多.PID从提出和发展目前已经一个世纪过去了,还在不断研究创新,足见它的重要性.本篇博文给出三菱FX系列增量型PID的源代码.(三菱系列的优化 ...

  3. 增量式PID算法控制房间温度变化的简单例子及python程序

    房间控制逻辑图 增量式PID控制器算法 T--采样周期 Ti--积分时间 TD--微分时间 加热器模型 房间模型

  4. 智能车增量式PID算法

    智能车电机增量式PID控制算法 Pid_Inc Right; Pid_Inc Left;int16 PulseRight=0; int16 PulseLeft=0;int16 P_SET=0; int ...

  5. 力控液位控制增量式PID算法

  6. 增量式速度pid调节策略_增量式PID是什么?不知道你就落伍了

    目录 1 什么是增量式PID? 2 举个例子 2.1 位置式PID 2.2 增量式PID 3 伪算法 4 C语言实现 5 总结 在之前一篇博客中( 简易PID算法的快速扫盲 )简单介绍了PID算法的基 ...

  7. 增量式PID到底是什么?

    0 前面的话 好久没有更新了,内心有种罪恶感,,至于原因,可能是因为菜吧,不知道该写什么,还有就是因为懒吧,虽然一部分在B乎上发了,被喷了一地,便没整理到公众号.后面打算整理一个PID算法系列,系统地 ...

  8. 位置式PID与增量式PID代码实现(python)

    位置式PID与增量式PID的python实现 一.PID控制器简介 二.一阶惯性环节 三.位置式PID 3.1 简介 3.2 程序 四.增量式PID 4.1简介 4.2 程序 五.几种控制效果对比 本 ...

  9. PID--位置型PID和增量式PID比较

    一. 位置型PID 位置型 PID 算法适用于不带积分元件的执行器. 执行器的动作位置与其输入信号呈一一对应的关系. 控制器根据第 n 次计算机采样结果与给定值之间的偏差 e 来计算出第 n 次采用后 ...

最新文章

  1. go channel 缓冲区最大限制_GO语言圣经学习笔记(八)Goroutines和Channels
  2. 前端学习(624):小结
  3. PAT乙级1011.A+B和C (15)(15 分)
  4. 百度网盘Linux版放出deb包客户端:新增支持Ubuntu 18.04 LTS
  5. 可临摹学习的精致的音乐播放器界面设计ui模板
  6. 数位板驱动压力测试_【又来甩锅了】数位板/数位屏延迟怎么办?
  7. 第三回 基类中的方法,应该根据实际情况,虚的虚,抽象的抽象!
  8. SSD的TRIM原理及实践
  9. windows10 无法设置屏幕保护程序
  10. Codeforces 863B Kayaking 暴力 水题
  11. 写了十几年代码,我为什么还没有被拿去“祭天”?
  12. Vulhub靶场搭建
  13. 我为何想参加2011年IT博客大赛?从心出发的选择
  14. 如何搜到专业数据和行业报告
  15. pyside2出现qt.qpa.plugin: Could not find the Qt platform plugin windows in 错误解决办法
  16. Auto.js实现自动解锁屏幕
  17. 区块链毕设源码开题论文-基于区块链的餐厅管理系统
  18. Best Cow Fences (前缀和 + 二分)
  19. 江西警方发布通缉令12小时 一涉黑犯罪在逃人员落网
  20. 短视频如何用标题吸引人?分享七种标题类型,引起兴趣很关键

热门文章

  1. Dual Graph Attention Networks for Deep Latent Representation of Multifaceted Social...》论文学习笔记
  2. 程序猿健身之腹肌~基本版本
  3. 量化机器人—马特炒币机器人
  4. IE8 使用 Oracle ERP
  5. HTTP和URL详细分析
  6. 怎么用j-link+j-flash烧写MM32
  7. ARM Cortex-M处理器详解
  8. docker 容器资源限制
  9. 王者荣耀转系统服务器繁忙,换手机党的福音,王者荣耀开启跨系统角色转移,但这些问题要注意...
  10. Android MTK系统编译与调试命令