Simulated helicopter control system based on adaptive algorithm and incremental PID algorithm


Simulated helicopter control system based on adaptive algorithm and incremental PID algorithm

Control system hardware

The simulated helicopter vertical lift control system is mainly composed of C8051F020 microcontroller, button and display modules, and helicopter vertical lift simulation objects.

Microcontroller system

The display function is realized by the LCD and the digital tube. The LCD screen and the digital tube realize the display control main menu interface, the current value of the Hall voltage and the change curve respectively. The buttons are used to switch the LCD display content, set the PID parameters, increase and decrease Hall voltage setting value and other functions.

Screenshot 2022-05-30 23.19.17

Introduction to sensor systems

The SS49E linear Hall sensor has the characteristics of small size and wide range of uses. SS49E can be operated by permanent magnets or electromagnets. The power supply voltage controls the linear output and can make linear changes according to the magnetic field strength. SS49E integrates a low-noise output circuit inside, eliminating the need for external filters. The device incorporates thin film resistors for increased temperature stability and accuracy. The operating voltage of SS49E is 4.5V~6V.

Introduction to Helicopter Simulation System

The schematic diagram of the helicopter vertical lift simulation object system is shown in Figure 2-2, the actual object is shown in Figure 2-3, and the interface description is shown in Table 2-1.

Screenshot 2022-05-30 23.22.03

System module introduction

ADC sampling system

Because the ADC sampling system is the most important part of the entire project, here we want to show the role of a more advanced system in control through the difference between the Stm32F103 and the C8051F020 microcontroller.

STM32

ADC implementation ideas:

We first need to understand the three high-precision ADCs owned by the STM32F103 series.image-20220119132911923

Obviously we can see that the f adc operating frequency here is 14MHZ, and the system clock is 72MHZ. Obviously the division factor needs to be set to 6. Because we later want the CPU to perform more operations when processing the ADC, polling obviously should not be used, because we hope that the timer interrupt will trigger the sampling of the ADC, and then the DMA will continuously move the code from the ADC register to the specified array.


CUBEMX configuration
Configuration of ADC1 and ADC3
Graphics initialization

image-20220119171906385

Take the configuration of ADC3 as an example (channel 8 of ADC3 corresponds to PF10)

  • In the ADC configuration, it is necessary to modify the External Trigger Convision Source to be triggered by timer 8, the Sampling Time to 7.5Cycles, and the Continuous Conversion Mode to ENABLE.
  • In the DMA configuration, the Data Width is changed to Half World. The Mode is changed to Loop. This is very important, so that the DMA transfer will not stop. The author defaulted to NORMAL at the beginning, but DEBug found that it was only transferred once each time. It's over , the direction is changed to Peripheral to Memory
TIMEr timer interface
image-20220119172725216

This is essential to achieve accurate sampling every 0.x seconds, because if we only use DMA technology, the ADC will continue to sample at a high frequency. The author found that this speed is too fast and uncontrollable, especially in practice. In production, we also need to coordinate the relationship between the three ADCs. If the three ADCs all sample at the same time, our problem can obviously be solved very well.

  • The frequency division coefficient is changed to 1999, and the Trigger Event Selection is changed to automatically update. After each interruption, it starts counting again.

ADC2 configuration
Graphical initialization
image-20220119191932844

Because ADC2 does not have a DMA system, we need to use a separate Timer3 to generate interrupts. Here we use a little trick. If we directly set ADC2 to be triggered by TIMER3, a very strange phenomenon will occur, and the writing will never be interrupted. It is better to directly use software interrupt triggering, and then write two interrupt callback functions to judge. The essential reason for this is that after calling the HAL library, the programmer partially loses the underlying control.

  • Continuous Conversion Mode: Change to Disabled
  • Samping Time (Because there is no DMA, we lose the ability to respond quickly, so it is better to increase the Samping Time directly): 41.5 Cycles
Timer timer interface

image-20220119192014751image-20220119192231707

  • The initial settings of TIMER3 are the same as TIMER8
Interrupt initialization

image-20220119204400931

  • Interrupt initialization only needs to assign the value (0, 0) to (Preemption Priority, Sub Portity)

  • There is really no need to give ADC too high a priority, because our ADC sampling is ongoing, we should pay attention to the priority of the serial port. When the serial port sends data again, send WIFI serial port priority >> send Debug priority >> ADC. Personal I feel that interrupting the serial port is a crazy behavior. This error once troubled me all afternoon and I couldn't even get the result by debugging.


Code source code (you can skip if you are not interested)
ADC.c 与ADC.h

The main body of the code is composed of CUBEMX. We need to understand and make changes to the specified parts. We have made detailed comments later, especially the modified parts.

(ADC1 ADC3) (ADC2) have two configuration methods respectively

/* 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 Configuration
    PA4     ------> 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 Configuration
    PC2     ------> ADC2_IN12
    PA2     ------> 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 Configuration
    PF10     ------> 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 Configuration
    PA4     ------> 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 Configuration
    PC2     ------> ADC2_IN12
    PA2     ------> 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 Configuration
    PF10     ------> 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); 
}

Description of ADC3 interrupt function

Note here that you must turn off the clock in the interrupt, and then turn it on after processing. This is a good habit, because no one knows how long it will be processed in it. If it is not turned off, the next interrupt may cause program exceptions. Bloody lesson

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);    //关闭adc2
    HAL_TIM_Base_Stop_IT(&htim3);	//关闭时钟Timer2
		ADC_ConvertedValue2=HAL_ADC_GetValue(&hadc2);//传递值
    HAL_TIM_Base_Start_IT(&htim3);  //打开时钟Timer2
}
Instructions for initialization of ADC1,2

In order to use DMA transfer, we need to enable the DMA clock in the first sentence, but note that we do not use the DMA_init generated by the HAL library, which has a certain timing logic confusion.

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 */
  ...}
  
Test Results
ADC1
image-20220119214943265
ADC2
image-20220119214959132
ADC3
image-20220119215016871
Hardware description (common to ADC1,2,3)
ADC hardware connection

Since ADC sampling can only collect voltages between 0-3.3v, the voltage division of the sensor needs to be considered in the hardware design. It is recommended to use 2 chip resistors to divide the 5V to 3.3.v. Please be sure to consider the input resistance of the ADC. RAIN _

image-20220119215036440

This embedded system adopts T s =55.5

51ADC

DA conversion

There are many DAC output methods. If you choose to update the DAC output through timer 2 overflow, since the DAC0 of the C8051F02 microcontroller is a 12-bit precision digital-to-analog converter, different data will be generated when 16 bits are used to store it. Alignment. When setting DAC0, first set the update mode and alignment mode of DAC0 in the initialization part, then select the reference voltage for DAC0 (usually the internal reference voltage), and then clear the data register to 0 to complete the initialization. For the output control of DAC0, when the timer 2 interrupt overflows, the value corresponding to the voltage to be output is sent to the high and low data registers.

AD conversion

ADC0 is a 12-bit precision analog-to-digital converter. Like DAC0, it also requires a timer to control the timing. The difference is that ADC0 does not rely on the interrupt of the timer overflow to start conversion, but does not trigger an interrupt when the timer overflows, and triggers its own interrupt when the conversion is completed to allow the microprocessor to read the data. The initialization of timer 3 is slightly different from the initialization of other timers. Since the timer overflow interrupt is not required, the timer interrupt should be turned off during initialization and start timing immediately.
(1)ADC0 initialization

The initial setting of ADC0 is a bit more complicated than that of DAC0. In addition to setting the timer 3 overflow start, data alignment format and reference voltage, you also need to set the sampling channel of ADC0, the SAR clock frequency of ADC0, the gain of ADC0, and enable the interrupt request for the end of ADC0 conversion. These parameters can be modified as needed. When reading the conversion result from ADC0, in the interrupt handler, you first need to trigger the interrupt to manual clearing, and then use the idea of ​​buffering to read the values ​​of each channel of ADC0 into the buffer, and continuously change the channels, you can start from Reading values ​​from different channels can avoid the impact of timing on the program. If the sampling frequency of ADC0 is greater than the frequency used by the program, the buffer will be continuously overwritten. This ensures that the data used are the latest sampling values. Although the ability of ADC0 is wasted, it will not cause problems in the program. However, when the sampling frequency of ADC0 is lower than the frequency of the system using sampled values, the system needs to wait to prevent repeated reading of the same sampled value. The following method can be used: If the buffer has been read, assign the buffer to An impossible value (such as 0xffff), it is judged that as long as the read value is abnormal, the main program will wait until a normal value is obtained.

(2) ADC0 data processing
When processing the data obtained by ADC0, the 12-bit data obtained from ADC0 needs to be converted into decimal data. At the same time, due to the influence of the physical environment, the value read by ADC0 will not be so accurate, and even linearity cannot be guaranteed. The sampling value of ADC0 needs to be corrected during calculation. The correction method is to assume that ADC0 is linear. The input terminals of ADC0 are connected to 1V and 5V voltages respectively. Obtain the corresponding value of ADC0 and perform normalization processing. ADC0 can Read the input voltage more accurately. For example, in the simulated helicopter vertical lift control system, the input voltage of ADC0 needs to be appropriately reduced according to the external amplifier circuit, and converted into a 12-bit binary number to be sent to the data register of ADC0.

code
void Do(void) {
    
    
    if ((timer1_value & 0x0007) == 0x0001) {
    
         
        if (channel == 1) 	 {
    
    
            // 从 ADC0 AIN1 取得 10 位 16 进制数 vadc
            vadc = 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 进制数 vdac
            DAC0_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;}
          }
      }
		}
	}

control algorithm

Adaptive control algorithm
Principle of adaptive control algorithm

The adaptive process is a process of constantly approaching the goal. The path it follows is represented by a mathematical model called an adaptive algorithm. Gradient-based algorithms are usually used, among which the minimum mean square error algorithm is particularly commonly used. Adaptive algorithms can be implemented in two ways: hardware (processing circuit) or software (program control). The former designs circuits based on the mathematical model of the algorithm, while the latter compiles the mathematical model of the algorithm into a program and uses a computer to implement it. There are many kinds of algorithms, and its selection is very important, as it determines the performance quality and feasibility of the processing system.

Code for adaptive control algorithm
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);    //去模糊化得到增量 △kp
	detail_ki = Inverse_quantization(ki_max, ki_min, qdetail_ki);    //去模糊化得到增量 △ki
	detail_kd = Inverse_quantization(kd_max, kd_min, qdetail_kd);    //去模糊化得到增量 △kd
	qdetail_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 总的隶属度值为 0
								
	for ( 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;
}
Disadvantages of adaptive control algorithms

​ In the actual code, since the system has been optimized, there is still a situation where it can only move within a fairly narrow dead zone, which means that control may suddenly become impossible due to computing power issues.

Additional notes on control algorithms
Adaptive control PID structure array
typedef struct{
    
    
	int setpoint;	//设定值
	long sumerror;	//误差的总和		
	float P;			//p
	float I;			//I
	float D;	//D
	int lasterror;//当前误差
	int preverror;//前一次误差
	int result;//本次PID结果
}PID;
Adaptive control PID calculates output voltage code
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;		
}
Incremental PID control algorithm

*: This algorithm is the code at the time of acceptance, but not the code sent

The principle of incremental PID algorithm

Different from positional PID control, incremental PID control makes a difference between the control quantity at the current moment and the control quantity at the previous moment, and uses the difference as the new control quantity. It is a recursive algorithm.

Code for incremental PID algorithm

u [ n − 1 ] = K p { e [ n − 1 ] + T T i ∑ i = 0 n − 1 e [ i ] + T d T { 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[n1]=Kp{ e[n1]+TiTi=0n1e [ i ]+TTd{ e[n1]e[n2]}}

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;		
}
Disadvantages of incremental PID algorithm

Incremental PID has completely different effects on different devices, and requires manual adjustment of PID parameters, which is quite troublesome.

LED display module

这部分只需要修改实验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);
}

draw point function

这是我在网上找到的一段规范代码,就是用于将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);
}

Program sequence analysis

Since the logic is completely in a straight line, the logic block diagram is no longer used here.

initialization

The clock and interrupt need to be initialized, which is mentioned here. Many students in our code do not know how to do this. The meaning of IF should be understood.

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

while loop control

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;
				}
			}
    	}}
    

Experiment summary

Experimental results

When using incremental PID, you can get good results quickly, but the adaptive algorithm can obviously find that due to the lack of computing power, it cannot achieve the functions we want.

Experiment review

I have experience using PID algorithms in Daiso control cars before. It is not difficult to find that in this experiment, it was the first time I came into contact with the helicopter simulator. It felt very interesting. I also encountered the problem that the buttons could not be used after transplantation. But in the end I overcame them one by one. Thanks to the teacher for his help in this process, this documentation has been published together with the project on gitee and my own personal website.

Guess you like

Origin blog.csdn.net/xrk00/article/details/125057168