Single Channel using Interrupt & DMA
This is the second tutorial in the STM32 ADC series. In this series will see how to use the ADC peripheral of the STM32 to read the data from the Analog devices. We will cover how to use the ADC in different modes, that includes polling mode, interrupt mode and the DMA mode. We will also see how to use the multiple channels to read multiple analog devices using the same ADC. Later in the series we will cover more advanced features like differential ADC, combined ADCs, ADC watchdogs, injected channels etc.
I am going to use the STM32H750 based development board for this series because it has advanced ADC features. But I will also cover the configuration changes required for some of the popular series that includes STM32F103C8 and STM32F446RE.
This tutorial will cover how to use the ADC single channel in interrupt & DMA mode. We have already covered How to configure the ADC and How to use the Polling mode in the previous tutorial. This tutorial is a continuation of the previous one, so the configuration remains the same.
Using Interrupt
Configuration
Below is the image showing the cubeMX configuration for the Single channel Interrupt Mode.
As you can see in the first image above, we need to enable the continuous conversion mode. This is to make sure that the second conversion should start as soon as the first one is finished. This way the conversion will keep taking place continuously, and the interrupt will trigger at a constant rate. In our case of single conversion, enabling the continuous conversion will make sure the the conversion for the same channel will take place continuously.
In the NVIC tab, make sure to enable the ADC global interrupt.
The Code
The code will remain the same for all the STM32 MCUs. If there are any changes, I will specify in this section itself.
Definitions
Let’s define some variables, which we will use in the main function later.
uint16_t ADC_VAL = 0;
int value = 0;
long map(long x, long in_min, long in_max, long out_min, long out_max)
{
return (x - in_min) * (out_max - out_min + 1) / (in_max - in_min + 1) + out_min;
}
ADC_VAL is a 16 bit variable, which will be used to store the converted ADC Value. Even if you are using 10bits, 12bits or 14bits resolution the ADC_VAL will still be defined as a 16 bit variable.
We will use the map function to map the converted ADC value to our desired range. The mapped value will be stored in the value variable, which I have defined as an integer here.
The main function
Below is the main function.
int main ()
{
....
....
HAL_ADC_Start_IT(&hadc1);
while (1)
{
}
}
Inside the main function, we will start the ADC in the interrupt mode. Once the ADC finishes the sampling and conversion of the data from the channel, an interrupt will trigger and the Conversion complete callback will be called. We will write the rest of the code inside this callback function.
Conversion Complete Callback
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
ADC_VAL = HAL_ADC_GetValue(&hadc1);
value = map(ADC_VAL, 1700, 65535, 0, 100);
}
Inside the callback we will read the converted value using the function HAL_ADC_GetValue. This function returns the ADC value based on the resolution set during the configuration.
We now have the converted data stored in the ADC_VAL variable. We will use the map function to map the converted value to 0 to 100 range. Since I have set the ADC resolution to 16bits, the maximum value of the ADC_VAL variable will be 216-1 = 65535. Similarly, the maximum value for the 12bits resolution will be 212-1 = 4095 and the same for the 10bits resolution will be 210-1 = 1023.
I have set the minimum input value to the map function as 1700. This is because it is the value we were getting when the slider was at the extreme left. Similarly the maximum value to the input of the map function will be 65535, as this is the highest value for the 16 bit resolution.
Result
Below is the gif showing the result of the above code.
As you can see in the image above, the ADC value is more responsive now. Also the value variable varies from 0 to 100 as we move the slider from left to right.
This means that the ADC is working pretty fine in the interrupt mode. One important thing you need to remember is not to set the ADC clock too high. If the conversion time is very low, the interrupts will trigger at an extremely high rate and this will make the while loop unusable.
Basically you have to tradeoff between the sampling rate and the while loop.
Using the DMA
Using DMA for a single channel is of no use. The interrupt will trigger at the same rate as that in the case of the interrupt mode, so basically we don’t have any noticeable advantage by using the DMA. But just for the sake of it, we will cover the DMA mode as well.
Configuration
We will see the available configuration for all three MCUs, F103C8, F446RE and H750VB.
F446RE
We will start with the F446RE. Below is the image showing the configuration for the it.
As you can see in the first image above, we need to enable the continuous conversion mode. This is to make sure that the second conversion should start as soon as the first one is finished. This way the conversion will keep taking place continuously, and the interrupt will trigger at a constant rate. In our case of single conversion, enabling the continuous conversion will make sure the the conversion for the same channel will take place continuously.
The F446RE has an option to enable the DMA Continuous Request. This mode ensures that the DMA can make a continuous data transfer request to the ADC and hence the we do not need to start the DMA again.
In the DMA settings, add the DMA request for the ADC. Make sure the DMA mode is circular, so that the DMA can re-trigger the request automatically after the required data has been transferred once. In Normal mode the DMA will stop after the required data has been transferred, and we need to start the transfer request again for a new transfer.
If the ADC resolution is higher than 8bits, set the Data Width as Half Word (16bits).
F103C8
Below is the image showing the configuration for the F103C8.
As you can see in the image above, we need to enable the continuous conversion mode. This is to make sure that the second conversion should start as soon as the first one is finished. This way the conversion will keep taking place continuously, and the interrupt will trigger at a constant rate. In our case of single conversion, enabling the continuous conversion will make sure the the conversion for the same channel will take place continuously.
Unlike F446RE, the F103C8 does not has the option to enable the DMA Continuous Request. Since the option is not available, we will simply set the DMA in the circular mode and this will automatically enable the DMA to make the continuous data requests.
In the DMA settings, add the DMA request for the ADC. Make sure the DMA mode is circular, so that the DMA can re-trigger the request automatically after the required data has been transferred once. In Normal mode the DMA will stop after the required data has been transferred, and we need to start the transfer request again for a new transfer.
If the ADC resolution is higher than 8bits, set the Data Width as Half Word (16bits).
H750VB
Below is the image showing the configuration available for the H750VB.
As you can see in the image above, we need to enable the continuous conversion mode. This is to make sure that the second conversion should start as soon as the first one is finished. This way the conversion will keep taking place continuously, and the interrupt will trigger at a constant rate. In our case of single conversion, enabling the continuous conversion will make sure the the conversion for the same channel will take place continuously.
Here we have anew option for Conversion Data Management Mode. It allows us to configure what to do with the converted data. We can leave the data in the ADC Data Register (DR), or we can use DMA to transfer the data. Here I am using the option DMA CIRCULAR MODE, which will allow the DMA to continuously transfer the converted data.
In the DMA settings, add the DMA request for the ADC. Make sure the DMA mode is circular, so that the DMA can re-trigger the request automatically after the required data has been transferred once. In Normal mode the DMA will stop after the required data has been transferred, and we need to start the transfer request again for a new transfer.
If the ADC resolution is higher than 8bits, set the Data Width as Half Word (16bits
The Code
The code will remain the same for all the STM32 MCUs. If there are any changes, I will specify in this section itself.
Definitions
Let’s define some variables, which we will use in the main function later.
uint16_t ADC_VAL[10];
int count = 0;
int value = 0;
long map(long x, long in_min, long in_max, long out_min, long out_max)
{
return (x - in_min) * (out_max - out_min + 1) / (in_max - in_min + 1) + out_min;
}
ADC_VAL is an array of 10 elements of 16 bit in size. We will use this array to store the converted ADC Value. Even if you are using 10bits, 12bits or 14bits resolution the ADC_VAL will still be defined as a 16 bit variable.
We will use the map function to map the converted ADC value to our desired range. The mapped value will be stored in the value variable, which I have defined as an integer here.
The main function
Below is the main function.
int main ()
{
....
....
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)ADC_VAL, 1);
while (1)
{
}
}
Inside the main function, we will start the ADC in the DMA mode. The parameter ADC_VAL is the array where we want to store the converted data, and 1 is the number of conversions we want the DMA to transfer.
Once the DMA finishes the transfer of the converted data from the channel, an interrupt will trigger and the Conversion complete callback will be called. We will write the rest of the code inside this callback function.
Conversion Complete Callback
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
value = map(ADC_VAL[0], 1700, 65535, 0, 100);
}
Unlike the interrupt mode, Inside the callback we do not need to use the function HAL_ADC_GetValue. The data is already stored in the ADC_VAL array, therefore we can simply process this data.
We now have the converted data stored in the ADC_VAL variable. We will use the map function to map the converted value to 0 to 100 range. Since I have set the ADC resolution to 16bits, the maximum value of the ADC_VAL variable will be 216-1 = 65535. Similarly, the maximum value for the 12bits resolution will be 212-1 = 4095 and the same for the 10bits resolution will be 210-1 = 1023.
I have set the minimum input value to the map function as 1700. This is because it is the value we were getting when the slider was at the extreme left. Similarly the maximum value to the input of the map function will be 65535, as this is the highest value for the 16 bit resolution.
Cortex M7 Related
In this section we will cover the configuration and code exclusively needed for the cortex M7 devices.
We know that as per ST’s recommendation, we should use the Instruction and Data cache to improve the system performance. But if the cache are enabled, the DMA will always have the cache coherency issue. This is why we will use the Memory Processing Unit (MPU) of the cortex M7 to deal with the cache coherency.
Below is the image showing the MPU configuration in the cortex M7 Devices.
As you can see in the image above, I have enabled both Instruction and Data Cache (ICache & DCache).
We also need to enable the MPU in the MPU_PRIVILEGED_DEFAULT mode. Here I am choosing the Base Address region 0x30000000, this is the region for the RAM_D2 and we will relocate our buffer in this region.
- Here I have selected the 32 Bytes region, since it’s the least size available. The ADC Buffer is going to be 20 bytes in size.
- The rest of the configuration is to set the region as non-cacheable region. This would prevent the cache coherency issue between the CPU and the DMA.
In the main file, we need to relocate the ADC_VAL array to the RAM_D2 Region, 0x30000000. We can do this by using the section attribute as shown below.
__attribute__((section(".adcarray"))) uint16_t ADC_VAL[10];
Here I am relocating the ADC_VAL array to the section “.adcarray“. The section (.adcarray) will be defined in the flash script file as shown below.
.mysection (NOLOAD):
{
. = ABSOLUTE(0x30000000);
*(.adcarray)
} >RAM_D2
As you can see above, the section (.adcarray) is linked to the region RAM_D2 (0x30000000).
The rest of the code remains the same as we used in the DMA section.
Result
Below is the gif showing the output of the DMA code.
As you can see in the gif above, the first element of the ADC_VAL array is updating continuously. The value variable also varies with the slider.
This means that the DMA is continuously transferring the data, and it is working fine even with the cache being enabled. There is no issue of the cache coherency, otherwise the data in the ADC_VAL array would not update at all.