Now I’ll implement the plant model discussed in the previous post. The objective is to use the simulated plant to tune the PID, measure its performance, stress it and test special cases.

Implementation strategy

The input voltage is sampled with an ADC at one Hertz. The output voltage is generated with a DAC at one sample per second. The sampling and writing periods are controlled by a timer that rises an interruption every one second. The interruption sets a flag, which is served in the main loop. The flag triggers the input reading, the calculation of the object’s current temperature and the output update.

To avoid overhead, the constants m and c are pre-multiplied and stored as mc. The compiler enables the hardware floating point unit (FPU).

System clock

I’m using a STM32G491RET6 MCU, which operates at maximum clock frequency of 170 MHz. The clock signal is produced from an internal oscillator at 16 MHz and an PLL that scale the frequency to the desired value. The selection of the system clock impacts all the computations, so it has to compromise to meet all the application requirements. For this exercise I will keep the clock at maximum speed.

Peripherals

Timer

The timer controls the sampling period of the complete system. I’m using the Timer 3 counting up and triggering an interruption when it rolls down. I want an interruption every one second.

Two values set the timer period. One is the clock prescaler (P), the other is the target count (C).

\[\begin{aligned} T_i &= C \cdot \frac{1}{f_{clk}} \cdot P \\ C\cdot P &= T_i \cdot f_{clk} \end{aligned}\]

Where Ti is the interruption period and fclk is the system clock frequency. Take into account that C and P are 16 bit integers, so they should be smaller that 65535.

Then, fixing Ti=1 s, and fclk=170 MHz, C*P = 170*106. Then taking P=8000, C=21250. Now, these numbers represent the total counts in the prescaler counter and the timer, which starts at 0, then I need to rest 1 to them when configuring the timer.

The configuration looks like this:

  • Use internal clock (170MHz)
  • Prescaler = 7999
  • Counter mode = Up
  • Counter period = 21249
  • Global interrupt enabled

STM32CubeMX created the initialisation function MX_TIM3_Init, which assumes a global variable named htim3 that is defined in the preamble of the main.c file: TIM_HandleTypeDef htim3;.

/**
  * @brief TIM3 Initialization Function
  * @param None
  * @retval None
  */
static void MX_TIM3_Init(void)
{
  TIM_ClockConfigTypeDef sClockSourceConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};

  htim3.Instance = TIM3;
  htim3.Init.Prescaler = 6399;
  htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim3.Init.Period = 9999;
  htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  if (HAL_TIM_Base_Init(&htim3) != HAL_OK)
  {
    Error_Handler();
  }
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig) != HAL_OK)
  {
    Error_Handler();
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
}

Here, the interruption is not explicitly enabled. A different function defines the interruption (and it enable it, I guess) in the file stm32g4xx_it.c. We can write here the action we want to run after the interrupt is triggered.

/**
  * @brief This function handles TIM3 global interrupt.
  */
void TIM3_IRQHandler(void)
{
  HAL_TIM_IRQHandler(&htim3);
  /* USER CODE BEGIN TIM3_IRQn 1 */

  /* USER CODE END TIM3_IRQn 1 */
}

ADC

The ADC samples the control signal and make it available for calculations in the CPU.

I use the IN6 of the ACD1 without pull up or down resistor. In the chip, the input pin is PC0, pin 8 in the chip, which in the board is connected to the pin 6 of the pin header CN8. The ADC has 12 bits resolution and a maximum input voltage of 4 V.

STM32CubeMX creates the initialisation function ‘MX_ADC1_Init()’.

/**
  * @brief ADC1 Initialization Function
  * @param None
  * @retval None
  */
static void MX_ADC1_Init(void)
{
  ADC_MultiModeTypeDef multimode = {0};
  ADC_ChannelConfTypeDef sConfig = {0};

  /** Common config
  */
  hadc1.Instance = ADC1;
  hadc1.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV256;
  hadc1.Init.Resolution = ADC_RESOLUTION_12B;
  hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
  hadc1.Init.GainCompensation = 0;
  hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;
  hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
  hadc1.Init.LowPowerAutoWait = DISABLE;
  hadc1.Init.ContinuousConvMode = ENABLE;
  hadc1.Init.NbrOfConversion = 1;
  hadc1.Init.DiscontinuousConvMode = DISABLE;
  hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
  hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
  hadc1.Init.DMAContinuousRequests = DISABLE;
  hadc1.Init.Overrun = ADC_OVR_DATA_PRESERVED;
  hadc1.Init.OversamplingMode = DISABLE;
  if (HAL_ADC_Init(&hadc1) != HAL_OK)
  {
    Error_Handler();
  }

  /** Configure the ADC multi-mode
  */
  multimode.Mode = ADC_MODE_INDEPENDENT;
  if (HAL_ADCEx_MultiModeConfigChannel(&hadc1, &multimode) != HAL_OK)
  {
    Error_Handler();
  }

  /** Configure Regular Channel
  */
  sConfig.Channel = ADC_CHANNEL_6;
  sConfig.Rank = ADC_REGULAR_RANK_1;
  sConfig.SamplingTime = ADC_SAMPLETIME_92CYCLES_5;
  sConfig.SingleDiff = ADC_SINGLE_ENDED;
  sConfig.OffsetNumber = ADC_OFFSET_NONE;
  sConfig.Offset = 0;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }
}
MX_ADC1_Init();
HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED);

ADC_HandleTypeDef hadc1;

DAC

/**
  * @brief DAC1 Initialization Function
  * @param None
  * @retval None
  */
static void MX_DAC1_Init(void)
{
  DAC_ChannelConfTypeDef sConfig = {0};

  /** DAC Initialization
  */
  hdac1.Instance = DAC1;
  if (HAL_DAC_Init(&hdac1) != HAL_OK)
  {
    Error_Handler();
  }

  /** DAC channel OUT2 config
  */
  sConfig.DAC_HighFrequency = DAC_HIGH_FREQUENCY_INTERFACE_MODE_AUTOMATIC;
  sConfig.DAC_DMADoubleDataMode = DISABLE;
  sConfig.DAC_SignedFormat = DISABLE;
  sConfig.DAC_SampleAndHold = DAC_SAMPLEANDHOLD_DISABLE;
  sConfig.DAC_Trigger = DAC_TRIGGER_NONE;
  sConfig.DAC_Trigger2 = DAC_TRIGGER_NONE;
  sConfig.DAC_OutputBuffer = DAC_OUTPUTBUFFER_ENABLE;
  sConfig.DAC_ConnectOnChipPeripheral = DAC_CHIPCONNECT_EXTERNAL;
  sConfig.DAC_UserTrimming = DAC_TRIMMING_FACTORY;
  if (HAL_DAC_ConfigChannel(&hdac1, &sConfig, DAC_CHANNEL_2) != HAL_OK)
  {
    Error_Handler();
  }

}
DAC_HandleTypeDef hdac1;
MX_DAC1_Init()
HAL_DAC_SetValue(&hdac1, DAC_CHANNEL_2, DAC_ALIGN_12B_R, value_dac);

Floating Point Unit (FPU)

The FPU is enabled by default. This is an option controlled by the compiler, so it is set in SMT32CubeIDE. To check the state or use the software floating point library go to Project properties > C/C++ Build > Settings > MCU/MPU Settings, there we can enable the hardware FPU and chose the Floating-point Application Binary Interface (ABI) used by the compiler between software and hardware.

For this example, we want to have access to hardware FPU, It makes the calculation of the current temperature faster.

Implementation