An Analog to Digital Converter (ADC) converts a continuous signal (usually a voltage) into a series of discrete values ??(sequences of bits). The main features are:
There are different types of ADCs, the most common are listed below (illustrating their operation is not the purpose of this article):
Generally, STM32 microcontrollers have at least one ADC (a SAR ADC) with the following characteristics:
Fc = ADCCLK / (12.5 + Number of cycles)
Each ADC has two conversion modes: “regular” and “injected”.
Furthermore, the "Dual Conversion Modes" can be active:
In the STM32 with almost two ADCs and it is, therefore, possible to perform different conversion modes: in these types of conversion the ADC2 acts as a slave while the ADC1 acts as a master allowing 8 different types of conversion.
We are now ready to write a first simple example using the ADC peripheral. The goal is to measure the voltage in a voltage divider composed of a fixed value resistor and a potentiometer (so that by moving the potentiometer cursor, the voltage to be read varies) we begin by configuring our peripheral with STCube Tool. For this project, we will use the NUCLEO STM32L053R8. This board has only one ADC with 16 channels and a resolution of up to 12bit.
Now we’ll see the configuration step by step:
Where To Buy? | ||||
---|---|---|---|---|
No. | Components | Distributor | Link To Buy | |
1 | STM32 Nucleo | Amazon | Buy Now |
We have to flag IN0 to activate Channel 0, then we can configure the peripheral. Channel 0 is on GPIO PA0 as we can see in the picture below:
We select the ADC_prescaler equal to 4, resolution to 12bit (maximum of a resolution, we can choice between 6, 8, 10 and 12 bits), “right data alignment” (we can choose between right and left alignment), and “forward” as scan direction (we can choose between forward and backward).
For this first example we’ll hold disabled Continuous, Discontinuous conversion and DMA mode. Furthermore, the ADC sets, at the end of single conversion, the EoC (End of Conversion) flag.
We select 12.5 Cycles as sampling time (in this way the sampling frequency is 320 kHz obtained from the formula described above), the start of conversion is triggered by software. Furthermore, for this application the watchdog is disabled.
After the generation of the initialization code with STCube, we can find in our project the ADC configuration. As for every peripheral, the HAL library defines the dedicated C structure, for the ADC defines “ADC_HandleTypeDef”.
In our case the “ADC1” is the instance that points to our ADC. The structure “ADC_InitTypeDef” is used to handle the configuration parameters. In our example is generated as follow:
static void MX_ADC_Init(void) { /* USER CODE BEGIN ADC_Init 0 */ /* USER CODE END ADC_Init 0 */ ADC_ChannelConfTypeDef sConfig = {0}; /* USER CODE BEGIN ADC_Init 1 */ /* USER CODE END ADC_Init 1 */ /** Configure the global features of the ADC (Clock, Resolution, Data Alignment and number of conversion) */ hadc.Instance = ADC1; hadc.Init.OversamplingMode = DISABLE; hadc.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; hadc.Init.Resolution = ADC_RESOLUTION_12B; hadc.Init.SamplingTime = ADC_SAMPLETIME_12CYCLES_5; hadc.Init.ScanConvMode = ADC_SCAN_DIRECTION_FORWARD; hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc.Init.ContinuousConvMode = DISABLE; hadc.Init.DiscontinuousConvMode = DISABLE; hadc.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; hadc.Init.ExternalTrigConv = ADC_SOFTWARE_START; hadc.Init.DMAContinuousRequests = DISABLE; hadc.Init.EOCSelection = ADC_EOC_SINGLE_CONV; hadc.Init.Overrun = ADC_OVR_DATA_PRESERVED; hadc.Init.LowPowerAutoWait = DISABLE; hadc.Init.LowPowerFrequencyMode = ENABLE; hadc.Init.LowPowerAutoPowerOff = DISABLE; if (HAL_ADC_Init(&hadc) != HAL_OK) { Error_Handler(); } /** Configure for the selected ADC regular channel to be converted. */ sConfig.Channel = ADC_CHANNEL_0; sConfig.Rank = ADC_RANK_CHANNEL_NUMBER; if (HAL_ADC_ConfigChannel(&hadc, &sConfig) != HAL_OK) { Error_Handler(); } /* USER CODE BEGIN ADC_Init 2 */ /* USER CODE END ADC_Init 2 */ }The function HAL_ADC_MspInit(ADC_HandleTypeDef* hadc) needs to initialize the peripheral and define the clock and the GPIO ( in our case PA0).
void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc) { GPIO_InitTypeDef GPIO_InitStruct = {0}; if(hadc->Instance==ADC1) { /* USER CODE BEGIN ADC1_MspInit 0 */ /* USER CODE END ADC1_MspInit 0 */ /* Peripheral clock enable */ __HAL_RCC_ADC1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); /**ADC GPIO Configuration PA0 ------> ADC_IN0 */ GPIO_InitStruct.Pin = GPIO_PIN_0; GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); /* USER CODE BEGIN ADC1_MspInit 1 */ /* USER CODE END ADC1_MspInit 1 */ } } /** * @brief ADC MSP De-Initialization * This function freeze the hardware resources used in this example * @param hadc: ADC handle pointer * @retval None */The function HAL_ADC_MspDeInit(ADC_HandleTypeDef* hadc) needs to de-initialize the peripheral.
void HAL_ADC_MspDeInit(ADC_HandleTypeDef* hadc) { if(hadc->Instance==ADC1) { /* USER CODE BEGIN ADC1_MspDeInit 0 */ /* USER CODE END ADC1_MspDeInit 0 */ /* Peripheral clock disable */ __HAL_RCC_ADC1_CLK_DISABLE(); /**ADC GPIO Configuration PA0 ------> ADC_IN0 */ HAL_GPIO_DeInit(GPIOA, GPIO_PIN_0); /* USER CODE BEGIN ADC1_MspDeInit 1 */ /* USER CODE END ADC1_MspDeInit 1 */ } }
Before describing the code let's see how to make the connections on the development board.
We need a 10kOhm potentiometer and a 2kOhm resistor. The potentiometer is connected between 3.3V and 2kOhm resistor, the common point is connected to PA0, and finally, the other end of the 2k Ohm resistor is connected to the ground pin.
Acting on the potentiometer we will see the read voltage vary from 3.3 Volt to about 0 Volt.
Now let's dive into the code: In the Includes section we add the header file of main./* USER CODE END Header */ /* Includes ------------------------------------------------------------------*/ #include "main.h" /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ /* USER CODE END Includes */
In “Private variables” section we find “ADC_HandleTypeDef hadc” as previous said is an instance to C structure to handle the ADC peripheral. Then, we add three variables:
/* Private variables -----------------------*/ ADC_HandleTypeDef hadc; /* USER CODE BEGIN PV */ const int Resolution = 4095; const float Vs =3.300; float volt; /* USER CODE END PV */
Then, we can find the protype of function to handle the peripherals and resources initialized (system timer, GPIO, and ADC).
/* Private function prototypes -----------------*/ void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_ADC_Init(void); /* USER CODE BEGIN PFP */ /* USER CODE END PFP */
Finally, the main starts.
In the first part we call functions to initialize the peripherals and resources used:
int main(void) { /* USER CODE BEGIN 1 */ /* USER CODE END 1 */ /* MCU Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_ADC_Init(); /* USER CODE BEGIN 2 */ /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */
In the second part, that is, inside an infinite loop (while (1)) there is the function to start the conversion of the ADC, read the data and save it in the variable volt and finally stop the conversion wait for a second and start with the conversion and so on.
while (1) { /* USER CODE END WHILE */ HAL_ADC_Start(&hadc); if(HAL_ADC_PollForConversion(&hadc,10)==HAL_OK) { volt=HAL_ADC_GetValue(&hadc)*Vs/Resolution; } HAL_Delay(1000); HAL_ADC_Stop(&hadc); /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ }
Now, once our code has been compiled, we can debug it in real-time, just press the "spider" icon (see figure below) and see how the volt variable varies by acting on the potentiometer.
Once we have clicked on the debug button, at the top right, we can select the "live expression" window and add (by writing the name in the table) the variable to be monitored.
Now we can start the debug by clicking on the “Resume” button (on the top right) or by pressing the F8 key (on our keyboard).
We are now ready to read our voltage value. We will see that by acting on the potentiometer we will read the voltages in the whole range considered.
Some measures are shown below: