STM32 F4 ADC DMA Temperature Sensor
Goal: detecting temperature variations using a temperature sensor, ADC with DMA and TIM3 as a trigger (ADC sampling frequency = TIM3 trigger frequency).
Note: Using TIM3 as a trigger is suited for monitoring temperature variations over time. However, this is an overkill for checking the absolute temperature once in a while; a software trigger would be a better option for that.
Description:
The temperature sensor is connected to ADC1 input channel 16. TIM3 will trigger periodically (CK_CNT/(Autoreload value+1)) and ADC1 will sample the signal using the 3-cycle sample time. I will use the 12-bit resolution which results in the total of 15-cycle conversion time (including the 3-cycle sampling time). Now, the ADC clock frequency is 21MHz (driven by the prescaled APB2). The total conversion time is then 15/21Mhz = ~714 ns (~1.4Msps max). A DMA request is made after each conversion, which places the converted voltage value into a user-specified buffer. An end-of-conversion (EOC) flag generates an interrupt at the TIM3 trigger frequency (with a 15 cycle offset). TIM3 shouldn't trigger faster than the minimum conversion time (~714ns). In the case of measuring the temperature, sampling frequency is not an issue so I'll set up TIM3 to trigger, say, 2000 times per second.
ADC sampling frequency: 2kHz (max is 1.4MHz with a 21MHz ADC clock, 12-bit resolution and 3-cycle sampling).
ADC interrupt frequency: 2kHz (I will use ADC_IRQHandler() to toggle a pin to make sure my calculations are correct, the toggling frequency should be 1kHz).
Now, we end up with some value in the user-defied buffer (which the DMA writes into at the end of each conversion, 2000 times per second). The manual gives us a linear algebraic formula for calculating the temperature: Temperature (in °C) = {(VSENSE – V25) / Avg_Slope} + 25
VSENSE is the 12-bit value we get from ADC in our buffer (this is not the voltage value).
VSENSE should be multiplied by the ADC resolution step (Vref/4095) to get the actual voltage value.
V25 is 0.76V (voltage that sensor outputs at 25°C)
Avg_Slope is 0.0025V/°C (rate at which voltage changes when temperature changes by one degree Celsius)
According to the manual, the offset of the function can be up to 45°C due to process variation so it'll require some calibration if we plan to measure absolute temperature.
I will pass the ADC values through an averaging filter to improve the variation accuracy. The filter also removes highest and lowest samples (can add a more sophisticated mechanism here).
#include <stm32f4xx.h> #include "mcu_init.h" //==================================================================================== // Global variables for temperature measurements //==================================================================================== volatile uint16_t ADC_Raw[NS] = {0}; // Updated 2000 times per second by DMA uint16_t Sample_ADC_Raw[NS] = {0}; // Non-volatile copy of ADC_Raw[NS] uint32_t ADC_Average = 0; // Average of the samples float Temp = 0; // Temporary register float Temp_Celsius = 0; // Temperature in Celsius float Calibration_Value = 11.0; // For measuring absolute temperature //==================================================================================== // Functions used in this module //==================================================================================== void Sort_values(uint16_t [], uint8_t); float Get_Temperature(void); //==================================================================================== // main function //==================================================================================== int main() { //ADC_Interrupt_Config(); // Indirectly testing my calculations //GPIOD_Config(); // Indirectly testing my calculations TIM3_Config(); ADC_Config(); while (1) { Temp_Celsius = Get_Temperature(); // Monitoring Temp_Celsius in debug mode // Set a threshold value, light up LEDs if Temp_Celsius goes above or below it. // I put it in the freezer to test :) } } //==================================================================================== // Description: Averaging samples from ADC, calculating temperature in Celsius //==================================================================================== float Get_Temperature(void) { uint8_t i; for(i = 0; i < NS; i++) { Sample_ADC_Raw[i] = ADC_Raw[i]; } Sort_values(Sample_ADC_Raw, NS); ADC_Average = 0; for(i = SR/2; i < NS-SR/2; i++) { ADC_Average += Sample_ADC_Raw[i]; } ADC_Average /= (NS-SR); Temp += ADC_Average; Temp *= 3; Temp /= 4095; Temp -= (float)0.76; Temp /= (float)0.0025; Temp += (float)25.0; Temp -= Calibration_Value; return Temp; } //==================================================================================== // Description: Bubble sort min to max //==================================================================================== void Sort_values(uint16_t A[], uint8_t L) { uint8_t i = 0; uint8_t status = 1; while(status == 1) { status = 0; for(i = 0; i < L-1; i++) { if (A[i] > A[i+1]) { A[i]^=A[i+1]; A[i+1]^=A[i]; A[i]^=A[i+1]; status = 1; } } } } void ADC_IRQHandler(void) // (for testing) { GPIO_ToggleBits(GPIOD, GPIO_Pin_9); ADC_ClearITPendingBit(ADC1, ADC_IT_EOC); }
#ifndef __MCU_INIT_H #define __MCU_INIT_H #include <stm32f4xx.h> #define NS 10 // Number of samples to get from ADC #define SR 4 // Samples removed after sorting, 4=(2 highest & 2 lowest) #define ADC1_RDR 0x4001204C // ADC1 Regular Data Register (read only) extern volatile uint16_t ADC_Raw[NS]; // DMA writes ADC values into this buffer //==================================================================================== // Functions used for measuring temperature variations //==================================================================================== void GPIOD_Config(void); // Indirectly testing my calculations void ADC_Interrupt_Config(void); // Indirectly testing my calculations void TIM3_Config(void); void ADC_Config(void); #endif // __MCU_INIT_H
#include "mcu_init.h" #include <stm32f4xx.h> //==================================================================================== // Configuring TIM3 to trigger at 2kHz which is the ADC sampling rate //==================================================================================== void TIM3_Config(void) { TIM_TimeBaseInitTypeDef TIM3_TimeBase; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); TIM_TimeBaseStructInit(&TIM3_TimeBase); TIM3_TimeBase.TIM_Period = (uint16_t)49; // Trigger = CK_CNT/(49+1) = 2kHz TIM3_TimeBase.TIM_Prescaler = 420; // CK_CNT = 42MHz/420 = 100kHz TIM3_TimeBase.TIM_ClockDivision = 0; TIM3_TimeBase.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM3_TimeBase); TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update); TIM_Cmd(TIM3, ENABLE); } //==================================================================================== // Configuring GPIO PD9 (for testing) //==================================================================================== void GPIOD_Config(void) { GPIO_InitTypeDef gpio_D; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE); gpio_D.GPIO_Mode = GPIO_Mode_OUT; gpio_D.GPIO_OType = GPIO_OType_PP; gpio_D.GPIO_Pin = GPIO_Pin_9; gpio_D.GPIO_PuPd = GPIO_PuPd_NOPULL; gpio_D.GPIO_Speed = GPIO_Medium_Speed; GPIO_Init(GPIOD, &gpio_D); } //==================================================================================== // Configuring ADC global interrupt (for testing) //==================================================================================== void ADC_Interrupt_Config(void) { NVIC_InitTypeDef NVIC_ADC1; NVIC_ADC1.NVIC_IRQChannel = ADC_IRQn; NVIC_ADC1.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_ADC1); } //==================================================================================== // Configuring ADC with DMA //==================================================================================== void ADC_Config(void) { ADC_InitTypeDef ADC_INIT; ADC_CommonInitTypeDef ADC_COMMON; DMA_InitTypeDef DMA_INIT; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); DMA_INIT.DMA_Channel = DMA_Channel_0; DMA_INIT.DMA_PeripheralBaseAddr = (uint32_t)ADC1_RDR; DMA_INIT.DMA_Memory0BaseAddr = (uint32_t)&ADC_Raw[0]; DMA_INIT.DMA_DIR = DMA_DIR_PeripheralToMemory; DMA_INIT.DMA_BufferSize = NS; DMA_INIT.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_INIT.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_INIT.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_INIT.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_INIT.DMA_Mode = DMA_Mode_Circular; DMA_INIT.DMA_Priority = DMA_Priority_High; DMA_INIT.DMA_FIFOMode = DMA_FIFOMode_Disable; DMA_INIT.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull; DMA_INIT.DMA_MemoryBurst = DMA_MemoryBurst_Single; DMA_INIT.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; DMA_Init(DMA2_Stream4, &DMA_INIT); DMA_Cmd(DMA2_Stream4, ENABLE); ADC_COMMON.ADC_Mode = ADC_Mode_Independent; ADC_COMMON.ADC_Prescaler = ADC_Prescaler_Div2; ADC_COMMON.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled; ADC_COMMON.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles; ADC_CommonInit(&ADC_COMMON); ADC_INIT.ADC_Resolution = ADC_Resolution_12b; ADC_INIT.ADC_ScanConvMode = DISABLE; ADC_INIT.ADC_ContinuousConvMode = DISABLE; // ENABLE for max ADC sampling frequency ADC_INIT.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_Rising; ADC_INIT.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO; ADC_INIT.ADC_DataAlign = ADC_DataAlign_Right; ADC_INIT.ADC_NbrOfConversion = 1; ADC_Init(ADC1, &ADC_INIT); ADC_RegularChannelConfig(ADC1, ADC_Channel_16, 1, ADC_SampleTime_3Cycles); ADC_DMARequestAfterLastTransferCmd(ADC1, ENABLE); ADC_ITConfig(ADC1, ADC_IT_EOC, ENABLE); // (for testing) ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_TempSensorVrefintCmd(ENABLE); }
To work with other sensors, follow these steps:
- choose the ADC channel
- choose the sampling rate
- adjust the TIMx trigger frequency
- configure DMA
A software trigger is suited for reading sensor's values on demand (turn off ADC when not using).