PWM stands for Pulse-Width Modulation. Once the switching frequency (fsw) has been chosen, the ratio between the switch-on time (TON) and the switch-off time (TOFF) is varied. This is commonly called duty-cycle (D). The duty cycle can be between 0 and 1 and is generally expressed as a percentage (%).
D = TON / (TON + TOFF) = TON x fsw
The variation of the pulse width, made at a high frequency (kHz), is perceived as continuous and can be translated into a variation of the rotation speed of a motor, dimming a LED, driving an encoder, driving power conversion, and etc. The use of PWM is also widely used in the automotive sector in electronic control units (ECU - Electronic Control Unit) to manage the energy to be supplied to some actuators, both fixed and variable frequency. Among the various actuators used in a vehicle and controlled by PWM i.e. diesel pressure regulator, EGR valve actuator, voltage regulator in modern alternators, turbocharger variable geometry regulation actuator, electric fans, etc.
Where To Buy? | ||||
---|---|---|---|---|
No. | Components | Distributor | Link To Buy | |
1 | STM32 Nucleo | Amazon | Buy Now |
In this article, we will see how to configure and write an application to dim an LED connected to an output pin (GPIO) of the STM32F446RE Nucleo board. First of all, let's see how to configure the initialization code with the STCube tool.
It is necessary to identify a GPIO to associate a timer to generate the PWM. For example, we choose PB3 associated with Channel 2 of Timer 2 (TIM2_CH2). Note that the pin turns orange to remind us of that timer 2 still needs to be configured.
Now you need to configure timer two which is hooked to pin PB3.
We configure the clock tree so that the clock frequency (HCLK) of the microcontroller is 84 MHz, handling frequency divisor and multiplier of PLLCLK. The bus to which the timers are connected (APB1 timer and APB2 timer clocks) is also at 84 MHz.
We now know that timer 2 has a clock source of 84 MHz, so every time the clock switches the timer does a count. If we configure it to count to 84 million (it is a 32-bit timer, the Counter must be between 0 and 4294967295) it will take a second. We can set this number to exactly half so that the LED lights up for half a second and turns it off for another half-second. In practice, we will see at the output a square wave that will have a duty cycle of 50% and a switching frequency of 1Hz (too slow not to notice the switching on and off the LED !!). To do what has been said, the timer must be configured as follows:
Now we are ready to lunch the initialization code and write our application.
/* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_TIM2_Init(void);/* USER CODE BEGIN PFP */At the end of main() we find the initialization code of System Clock, Timer 2 and GPIO ( as previously selected in STCube):
void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; /** Configure the main internal regulator output voltage */ __HAL_RCC_PWR_CLK_ENABLE(); __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE3); /** Initializes the RCC Oscillators according to the specified parameters * in the RCC_OscInitTypeDef structure. */ RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI; RCC_OscInitStruct.HSIState = RCC_HSI_ON; RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI; RCC_OscInitStruct.PLL.PLLM = 16; RCC_OscInitStruct.PLL.PLLN = 336; RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV4; RCC_OscInitStruct.PLL.PLLQ = 2; RCC_OscInitStruct.PLL.PLLR = 2; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); } /** Initializes the CPU, AHB and APB buses clocks */ RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) { Error_Handler(); } } /** * @brief TIM2 Initialization Function * @param None * @retval None */ static void MX_TIM2_Init(void) { /* USER CODE BEGIN TIM2_Init 0 */ /* USER CODE END TIM2_Init 0 */ TIM_MasterConfigTypeDef sMasterConfig = {0}; TIM_OC_InitTypeDef sConfigOC = {0}; /* USER CODE BEGIN TIM2_Init 1 */ /* USER CODE END TIM2_Init 1 */ htim2.Instance = TIM2; htim2.Init.Prescaler = 0; htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 83999999; htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; if (HAL_TIM_PWM_Init(&htim2) != HAL_OK) { Error_Handler(); } sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK) { Error_Handler(); } sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 0; sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; if (HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_2) != HAL_OK) { Error_Handler(); } /* USER CODE BEGIN TIM2_Init 2 */ /* USER CODE END TIM2_Init 2 */ HAL_TIM_MspPostInit(&htim2); } /** * @brief GPIO Initialization Function * @param None * @retval None */ static void MX_GPIO_Init(void) { /* GPIO Ports Clock Enable */ __HAL_RCC_GPIOB_CLK_ENABLE(); } /* USER CODE BEGIN 4 */ /* USER CODE END 4 */ /** * @brief This function is executed in case of error occurrence. * @retval None */ void Error_Handler(void) { /* USER CODE BEGIN Error_Handler_Debug */ /* User can add his own implementation to report the HAL error return state */ __disable_irq(); while (1) { } /* USER CODE END Error_Handler_Debug */ }
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_TIM2_Init(); /* USER CODE BEGIN 2 */ HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, 41999999); /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ }
As mentioned earlier we have set the timer two to count up to 84 million, so we know that counting 84 million a second has elapsed. Now let's use the PWM function to make sure that for half a second the LED stays on and the other half a second off. In practice, to generate a square wave with a duty cycle of 50% and a frequency of one second.
We use the function “HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2)” to enable timer 2 to start in PWM mode and the macro “__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, 41999999)” tells the timer which is the value with which to compare the internal count (in this case 41999999) to decide whether the LED should be off or on.
Now we just must connect a led between pin PB3 and ground through a 220OHm resistor to limit the current (if we want to work with a duty cycle of 100%) and compile our application.
Once this is done, we see that the LED will remain on for 500ms second and off for another 500ms as can be seen from the waveform acquired by the PB3.
First, we declare the following define:
#define D_COUNTER 839
Now, we associate it with the field “htim2.Init.Period” of the structure *htim2:
htim2.Init.Period = D_COUNTER;
In this way, we can quickly the number of counts that the timer must do and therefore manage the frequency and duty cycle of our PWM.
This way our timer will count up to 839 in 10us. Consequently, the switching frequency will be 100kHz (clearly exceeding 1Hz !!). Note that as in the previous example we have subtracted 1 from the count value because the timer starts at zero.
Then, we define an unsigned int variable to set the duty cycle:
unsigned int D; //duty cycle In the main() we write; /* USER CODE BEGIN 2 */ D= 10; //it menas duty cycle of 10% HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, (D_COUNTER/100)*D); /* USER CODE END 2 */
Where (D_COUNTER/100)*D needs to re-proportion the value of the duty cycle to the count that the timer must perform.
Compiling we will see that now the LED will always be on but with low brightness, in fact, the duty cycle is only 10% as can be seen from the generated waveform and the rms value of the voltage given to the LED is about 1.1 Volt (as you can see in the bottom corner of the RMS figure for the green trace). Furthermore, the figure confirms that the duty cycle is 10% both intuitively by looking at the green trace and by reading the measurement at the bottom center (Dty+ = 10,48%).
If we set D=70, the LED will be brighter, in fact the RMS value is about 2.82 Volt (as you can see in the bottom corner of the RMS figure for the green trace). Furthermore, the figure confirms that the duty cycle is 70% both intuitively by looking at the green trace and by reading the measurement at the bottom center (Dty+ = 69,54%).
If we set D=100, the led will be illuminated with the maximum brightness imposed by the limitation of the 220 Ohm resistor. The rms value at the ends of the LED with the resistance in series will be 3.3 Volts (the maximum generated by the GPIO)
Now if you write the following code on while(), we will see that the LED will change brightness every 100ms (in practice it increases, every 100ms the duty by 1% starting from 1% until it reaches 100% and then starts all over again)
/* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ for(D=1; D<=100; D++){ HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, (D_COUNTER/100)*D); HAL_Delay(100); } /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */
To do this we include the PWM configuration function ( HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, (D_COUNTER/100)*Duty) in the following loop for(D=1; D<=100; D++) which increases the value of variable D by 1 every 100 ms through the function HAL_Delay(100).
Now we are ready to effectively manage the PWM to control the brightness of a led, but in a similar way, we can control the speed of a DC motor or various actuators.
One of the simplest and most classic DACs is the R-2R ladder, but today there are more complex objects with optimization in the signal reconstruction. Below is shown a 3bit R-2R ladder DAC.
Vout= -1/2*B1*Vref - (1/4)*B1*Vref- (1/8)*B1*Vref
If the 3bit string is D and Vref is equal to the logical voltage of 3.3V
Vout= (3.3*D)/2^3
The typical output characteristic is shown in the following figure.
Compared to the weighted resistor DAC, the R-2R scale DAC has the advantage of using only two resistive values. Therefore, it is more easily achievable with integrated circuit technology.
Where To Buy? | ||||
---|---|---|---|---|
No. | Components | Distributor | Link To Buy | |
1 | STM32 Nucleo | Amazon | Buy Now |
Many of the STM32 microcontrollers have on board at least one DAC (generally 12 bit) with the possibility of buffering the output signal (with internal operational amplifier OP-AMP). The use of the DAC finds various applications, for example, it can be used to reconstruct a sampled signal or to generate any waveform (sine wave, square wave, sawtooth, etc.), to generate a reference voltage (for example for a digital comparator).
The DAC peripheral can be controlled in two ways:
In this modality we can drive DAC to on/off a LED, to generate a reference voltage, etc. We will use a NUCLEO STM32L053R8 board to show as configure DAC with STCube. This NUCLEO has available a DAC with only one channel (in general every DAC has one or more channels) with resolution up to 12bit with a maximum bus speed of 32 MHz and a maximum sampling rate of 4 Msps. First, let's see how to initialize the peripherals using STCube Tool:
At this point, let's look at the generated code:
typedef struct { DAC_TypeDef *Instance; /*!< Register base address */ __IO HAL_DAC_StateTypeDef State; /*!< DAC communication state */ HAL_LockTypeDefLock; /*!< DAC locking object */ DMA_HandleTypeDef *DMA_Handle1; /*!< Pointer DMA handler for channel 1 */ #if defined (DAC_CHANNEL2_SUPPORT) DMA_HandleTypeDef *DMA_Handle2; /*!< Pointer DMA handler for channel 2 */ #endif __IO uint32_t ErrorCode; /*!< DAC Error code }DAC_HandleTypeDef;
/* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_DAC_Init(void); /* USER CODE BEGIN PFP */
Now, before writing our application, let's see what functions the HAL library makes available to handle the DAC.
The voltage output will be:
Vout,DAC = (Vref*data)/2^nb
where nb is a resolution (in our case 12bit), Vref is voltage reference (in our case 2 Volt) and the passed data.
So, to set DAC output to 1 Volt data must be 2047 (in Hexadecimal 0x7FF) so we call the function this way:
HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 0x7FF);
To set the output voltage to 3.3 Volt we call function in this way:
HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 0xFFF);
To verify that change the value in our main we write the following code and then we check the output connecting it to the oscilloscope probe.
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DAC_Init(); while (1) { /* USER CODE END WHILE */ HAL_DAC_Start(&hdac, DAC_CHANNEL_1); HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 0x7FF); HAL_Delay(1000); HAL_DAC_Stop(&hdac, DAC_CHANNEL_1); HAL_Delay(1000); HAL_DAC_Start(&hdac, DAC_CHANNEL_1); HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 0x7FF); HAL_Delay(1000); HAL_DAC_Stop(&hdac, DAC_CHANNEL_1); HAL_Delay(1000); /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ }
We expect the output voltage to change every second by repeating the following sequence: 1V - 0V - 2V – floating (as shown in the figure below)
Let's see mathematically how to reconstruct a sinewave starting from a given number of samples. The greater the number of samples, the more "faithful" the reconstructed signal will be. So, the sampling step is 2pi / ns where ns is the number of samples in this way, we have to save our samples in a vector of length ns. The values ??of every single element of the vector will be given by the following equation:
S[i] = (sin(i*(2p/ns))+1)
We know that the sinusoidal signal varies between 1 and -1 so it is necessary to shift it upwards to have a positive sinewave (therefore not with a null average value) therefore it must be moved to the middle of the reference voltage. To do this, it is necessary to retouch the previous equation with the following:
S[i] = (sin(i*(2p/ns))+1)*((0xFFF+1)/2)
Where 0xFFF is the maximum digital value of DAC (12bit) when the data format is 12 bits right aligned.
To set the frequency of the signal to be generated, it is necessary to handle the frequency of the timer trigger output (freq.TimerTRGO, in our case we use the TIM6) and the number of samples.
Fsinewave = freq.TimerTRGO/ns
In our case, we define Max_Sample = 1000 ( is uint32_t variable) and let's redefine some values of the timer 6 initialization.
static void MX_TIM6_Init(void) { /* USER CODE BEGIN TIM6_Init 0 */ /* USER CODE END TIM6_Init 0 */ TIM_MasterConfigTypeDef sMasterConfig = {0}; /* USER CODE BEGIN TIM6_Init 1 */ /* USER CODE END TIM6_Init 1 */ htim6.Instance = TIM6; htim6.Init.Prescaler = 1; htim6.Init.CounterMode = TIM_COUNTERMODE_UP; htim6.Init.Period = 100; htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; if (HAL_TIM_Base_Init(&htim6) != HAL_OK) { Error_Handler(); } sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; if (HAL_TIMEx_MasterConfigSynchronization(&htim6, &sMasterConfig) != HAL_OK) { Error_Handler(); } /* USER CODE BEGIN TIM6_Init 2 */ /* USER CODE END TIM6_Init 2 */ }
We have changed the following parameters:
htim6.Init.Prescaler = 1; htim6.Init.Period = 100;So with 1000 samples, the output sinewave will be a frequency of 10 Hz. We can change the number of samples (being careful not to use too few samples) or the “Init.Prescaler” and “Init.Period” values of timer 6.
Using the DAC in DMA mode the HAL library makes available to handle the DAC another function to set the DAC output.
HAL_DAC_Start_DMA(DAC_HandleTypeDef* hdac, uint32_t Channel, uint32_t* pData, uint32_t Length, uint32_t Alignment)
Compared to the manual function we find two more parameters:
As you can see from the following code we first need to include the "math.h" library, define the value of pigreco (pi 3.14155926), and write a function to save the sampled sinewave values in a array (we wrote a function declared as get_sineval () ).
#include "math.h" #define pi 3.14155926 DAC_HandleTypeDef hdac; DMA_HandleTypeDef hdma_dac_ch1; TIM_HandleTypeDef htim6; void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_DMA_Init(void); static void MX_DAC_Init(void); static void MX_TIM6_Init(void); uint32_t MAX_SAMPLES =1000; uint32_t sine_val[1000]; void get_sineval() { for (int i =0; i< MAX_SAMPLES; i++) { sine_val[i] = ((sin(i*2*pi/MAX_SAMPLES)+1))*4096/2; } } /* USER CODE END 0 */
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_DMA_Init(); MX_DAC_Init(); MX_TIM6_Init(); /* USER CODE BEGIN 2 */ get_sineval(); HAL_TIM_Base_Start(&htim6); HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, sine_val, MAX_SAMPLES, DAC_ALIGN_12B_R); /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ }
Now we just have to show the acquisition that the oscilloscope:
If we change the number of MAX_SAMPLE to 100 the sinewave will be a frequency of 100 Hz, but as can be seen from the acquisition, the number of samples, in this case, is a bit low.
We can optimize the waveform by acting on timer 6 in order to increase the number of samples. For example by modifying htim6.Init.Period = 400
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:
The SPI (Serial Peripheral Interface) protocol, or rather the SPI interface, was originally devised by Motorola (now Freescale) to support their microprocessors and microcontrollers. Unlike the I2C standard designed by Philips, the SPI interface has never been standardized; nevertheless, it has become a de-facto standard. National Semiconductor has developed a variant of the SPI under the name Microwire bus. The lack of official rules has led to the addition of many features and options that must be appropriately selected and set in order to allow proper communication between the various interconnected devices. The SPI interface describes a single Master single Slave communication and is of the synchronous and full-duplex type. The clock is transmitted with a dedicated line (not necessarily synchronous transmission that has a dedicated line for the clock) and it is possible to both transmit and receive data simultaneously. The figure below shows a basic connection diagram between two peripherals that make use of the SPI interface.
From the figure, it is immediately possible to notice what has just been said, namely that the communication generally takes place between a Master and a Slave. The interface presents 4 connection lines (excluding the ground however necessary), for which the standard SPI is also known as 4 Wire Interface. The Master starts the communication and provides the clock to the Slave. The nomenclature of the various lines in the SPI interface is normally as follows:
In addition to this standard nomenclature, there are other acronyms.
For example:The first advantage in SPI communication is faster communication, instead, the first disadvantage is the presence of the SS pin necessary to select the slave. It limits the number of slave devices to be connected and considerably increases the number of lines of the master dedicated to SPI communication as the connected slaves increase.
To overcome these problems, the devices in the daisy chain can be connected (output of a device connected to the input of the next device in the chain) as shown in the figure below where a single slave selection line is used.
The disadvantages, however, are the lower updating speed of the individual slaves and signal interruption due to the failure of an element.
We can use this communication to put in communication our micro-controller with different peripherals as Analog-Digital Converters (ADCs), Digital-Analog Converters (DACs), EEPROM memories, sensors, LCD screen, RF module, Real Time Clock, etc.
The STM32 micro-controllers provide up to 6 SPI interfaces based on the type of package that can be quickly configured with STCube Tool.
STCube Tool initializes the peripherals with HAL (Hardware Abstraction Layer) library. The HAL library creates for SPI (as all peripherals) an C structure:
As just said to initialize the SPI peripheral to be used, it is necessary to use the struct SPI_InitTypeDef. It is defined as follow:
When we use the STcubeMX to initialize the SPI peripheral we are modifying this structure
In details:If the slave supports, the full-duplex communication can be enabled.
By enabling the SPI and the chip select pin, the pins available on the microcontroller are automatically chosen to manage this interface (but they can be changed by looking for the alternative functions of the different pins of the microcontroller). For example, in our case the following pins are selected:
Now you can generate the initialization code. Before being able to write the first code to manage this communication interface, it is necessary to understand the functions that the libraries provide and the different communication modes.
As for other communication interfaces, the HAL library provides three modes to communicate: polling mode, interrupt mode, and DMA mode.
Where To Buy? | ||||
---|---|---|---|---|
No. | Components | Distributor | Link To Buy | |
1 | STM32 Nucleo | Amazon | Buy Now |
Using the SPI in Polling Mode is the easiest way, but it is the least efficient way as the CPU will remain in a waiting state for a long time. HAL library provides the following functions to transmit and receive in polling mode:
Master receives data packets in blocking mode (polling mode).
The parameters are:
Master transmits data packets in blocking mode (polling mode).
If the slave device supports, the full-duplex mode:
Master transmits and receives data packets in blocking mode (polling mode).
The parameters are:
Using the SPI in Interrupt Mode, also called non-blocking mode. In this way, the communication can be made more effective by enabling the interrupts of the SPI in order to receive, for example, signals when the data has been sent or received. This improves CPU time management. In applications where all the management must be deterministic and it is not known when an interrupt can arrive, these can potentially manage the time management of the CPU, especially when working with very fast buses such as SPI. We can enable the SPI interrupts directly during the initialization with STCube Mx.
HAL library provides the following functions to transmit and receive in interrupt mode:
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef * hspi) { // Message received .. Do Something ... }
Master transmits data packets in blocking mode (interrupt mode).
To handle the interrupt needs to write our code in the callback:
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef * hspi) { // Message transmitted.... Do Something ... }If the slave device supports, the full-duplex mode:
Master transmits and receives data packets in non-blocking mode (interrupt mode).
To handle the interrupt needs to write our code in the callback:
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef * hspi) { // Message transmitted or received.. .. Do Something ... }
Using the SPI in DMA Mode the SPI bus can be used at its maximum speed, in fact, since the SPI must store the received and transmitted data in the buffer to avoid overloading it is necessary to implement the DMA. In addition, use by DMA mode frees the CPU from performing "device-to-memory" data transfers. We can easily configure the DMA during the initialization using STCubeMx :
In this case, the DMA is enabled in normal (we can use it in circular mode) mode both in transmission and reception
HAL library provides the following functions to transmit and receive in DMA mode:
Master receives data packets in non-blocking mode (DMA mode).
The SPI device receives all bytes of data in the buffer one by one until the end in DMA mode. At this point, the callback function will be called and executed where something can be done.
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef * hspi) { // Message received .. Do Something ... }
Master transmits data packets in non-blocking mode (DMA mode).
The SPI device sends all bytes of data in the buffer one by one until the end in DMA mode. At this point, the callback function will be called and executed where something can be done.
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef * hspi) { // Message transmitted….. Do Something ... }
If the slave device supports, the full-duplex mode:
Master transmits and receives data packets in non-blocking mode (DMA mode).
The SPI device sends or receives all bytes of data in the buffer one by one until the end in DMA mode. At this point, the callback function will be called and executed where something can be done.
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef * hspi) { // Message transmitted or received.... Do Something ... }
We are now ready to handle an SPI communication with STM32.
EEPROMs (Electrically Erasable Programmable Read-Only Memories) allow the non-volatile storage of application data or the storage of small amounts of data in the event of a power failure. Using external memories that allow you to add storage capacity for all those applications that require data recording. We can choose many types of memories depending on the type of interface and their capacity.
EEPROMs are generally classified and identified based on the type of serial bus they use. The first two digits of the code identify the serial bus used:
Now we will see how to write or read data on an I2C EEPROM like 24C256C. This serial EEPROM is organized as 32,768 words of 8 bits each. The device’s cascading feature allows up to eight devices to share a common 2-wire bus. It is available in various 8-pin packages
The device can be used in applications consuming low power. The device is available in all standard 8-pin packages. The operating voltage is comprised of between 1.7V and 5.5V.
In our example, we connect A0, A1, A2 directly to VCC in this way the device address is 1010111 (in general A0, A1, A2 identify the last three significant bits of the device address 1 0 1 0 A2 A1 A0) is 0x57 in Hexadecimal. The 4 most significant bits are preset (Control Code), the A0, A1, A2 are Chip Select Bits.
Now we start with our project using STNucleoL053R8 and STCube to generate the initialization code. Below is shown the connection
So, we configure the I2C1 using STCube and leave all configuration as it is and we will operate in polling mode.
In GPIO setting select PA9 (SDA) and PA8 (SCL).
Our project has been initialized by STCubeMX. In the /Core/Src/main.c we will find our main where we will write the main body of our program.
Now let’s see what the code generator did:
First of all, we find the “Include” section we can add the library needed.
/* USER CODE END Header */ /* Includes ------------------------------------------------------------------*/ #include "main.h"In our case we can add also “stm32l0xx_hal.h” library to be able to use HAL library (I2C HAL library included)
#include "stm32l0xx_hal.h " #include "Var.h " #include "Funct.h "In “Private variables” has been defined two privates variable htim2 and hi2c1;
/* Private variables ---------------------------------------------------------*/ TIM_HandleTypeDef htim2; UART_HandleTypeDef hi2c1; unsigned short int address; // eeprom address unsigned char EEP_pag = 0x00 // EEPROM page unsigned char EEP_pos = 0x00 // EEPROM position unsigned char rdata = 0x00 // to store the data read from EEPROMIn “Private function prototypes” we find the protype of function to initialize the System Clock, GPIO, timer and peripheral:
/* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_TIM2_Init(void); static void MX_I2C1_Init(void);
This function has been generated automatically by STCubeMx with the parameter selected.
The main contains the initialization of the peripherals and variables, before the while loop the code call the function Write_EEPROM() and Read_EEPROM() to write and read a data in a specific address of the EEPROM. these functions were written in EEPROM.c, a C file added to our project in the src folder.
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_TIM2_Init(); MX_I2C1_Init(); /* USER CODE BEGIN 2 */ address = 0x00 << 8 | 0x00 // eeprom page 0 , position 0 // Now we want to store 10 in page 0x00 and position 0x00 of EEPROM Write_EEPROM(address, 10, 0) // Now we want store in rdata variable the content of cell memory 0x0000 rdata = Read_EEPROM(address, 0) /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ } /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ }Furthermore, we have added two header files and one c file:
/** Var.h * Created on: 27 ott 2021 * Author: utente */ #ifndef INC_VAR_H_ #define INC_VAR_H_ extern unsigned char buf[20]; extern int i; #endif /* INC_VAR_H_ */
/** Funct.h * Created on: 28 ott 2021 * Author: utente */ #ifndef INC_FUNCT_H_ #define INC_FUNCT_H_ extern unsigned char Read_EEPROM(unsigned int, unsigned char); extern void Write_EEPROM(unsigned int, unsigned char, unsigned char); #endif /* INC_FUNCT_H_ */
/** Serial.c * Created on: Oct 29, 2021 * Author: utente */ #include "stm32l0xx.h" // Device header #include "stm32l0xx_hal_conf.h" #include "stm32l0xx_hal.h " #include "Var.h " #include "Funct.h " extern I2C_HandleTypeDef hi2c1; unsigned char Read_EEPROM(unsigned int addr, unsigned char device) { unsigned char page; uint8_t dato; page=0xAF; // due to chip select bits setting HAL_I2C_Mem_Read(&hi2c1,page, addr, I2C_MEMADD_SIZE_16BIT, &dato,1,5); return dato; } void Write_EEPROM(unsigned int addr, unsigned char dato, unsigned char device) { unsigned char page; page=0xAF; //due to chip select bits setting HAL_I2C_Mem_Write(&hi2c1,page, addr, I2C_MEMADD_SIZE_16BIT, &dato,1,5 ); while(HAL_I2C_IsDeviceReady(&hi2c1, 0xA0, 1, HAL_MAX_DELAY) != HAL_OK); HAL_Delay(10); }Now we are ready to compile and run the project:
Hello friends, I hope you all are doing great. In today's lecture, we will have a look at the I2C Communication with STM32 Microcontroller board. I am going to use the Nucleo board for today's lecture. In the previous lecture, we have discussed STM32 Serial communication both in Interrupt Mode and polling Mode. Today, we will study another way of communication(i.e. I2C) with STM32. So, let's first have a look at what is I2C Communication:
Where To Buy? | ||||
---|---|---|---|---|
No. | Components | Distributor | Link To Buy | |
1 | STM32 Nucleo | Amazon | Buy Now |
I²C (Inter-Integrated Circuit) is a two-wire serial communication system used between integrated circuits. Like any serial protocol, one of its advantages is that of using only two lines that transmit or receive a sequence of bits, the limit is the communication speed which has been improved over the years.
The bus was conceived and developed by Philips (now NXP) It was designed to overcome the difficulties inherent in the use of parallel buses for communication between a control unit and various peripherals.
Serial transmission is a mode of communication between digital devices in which bits are sent one at a time and sequentially to the receiver in the same order in which they were transmitted by the sender. Although the communication modules are more complex than the parallel transmission, the serial mode is one of the most widespread especially in communications between chips that must communicate with each other over great distances, because:
SDA and SCL lines need to be pulled up with resistors. The value of these resistors depends on the bus length ( ie the bus capacitance) and the transmission speed. The common value is 4.7kO. In any case, there are many guides to size them and we refer their reading to the more attentive reader.
The transmission mode is Half-duplex ie the transmission between devices is alternated.
As shown by the previous image, we can use this communication to put in communication different peripherals as Analog-Digital Converters (ADCs), Digital-Analog Converters (DACs), EEPROM memories, sensors, LCD screen, RF module, Real-Time Clock, etc.
The Nucleo boards provide one or more I2C interfaces that can be quickly configured with STCube Tool.
There are four modes of operation:
The first two are used to operate in slave mode, while the last two are in master mode. By default, the interface is configurated in slave mode.
By default, the I2C interface operates in Slave mode, but it is possible to switch to Master mode to send a Start condition message. Furthermore, it needs to write in I2C_CR2 register the correct clock configuration to generate the expected timings. The Master sends a Stop condition when the last data byte is transferred, and the interface generates an interrupt.
In general, the packet message is as follow:
Furthermore, there are three ways to exchange data, named:
HAL library provides the following functions to transmit and receive in polling mode:
HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout)The parameters are:
HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout)
HAL_I2C_Slave_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout)
HAL_I2C_Slave_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout)
HAL_I2C_Mem_Read(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout)The additional parameters are:
HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout)
HAL_I2C_Master_Receive_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size)Master receives in master mode an amount of data in non-blocking mode with interrupt.
HAL_I2C_Master_Transmit_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size)Master transmits in master mode an amount of data in non-blocking mode with interrupt.
HAL_I2C_Slave_Receive_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size)Slave receives in master mode an amount of data in non-blocking mode with interrupt.
HAL_I2C_Slave_Transmit_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size)Slave transmits in master mode an amount of data in non-blocking mode with interrupt.
HAL_I2C_Master_Receive_DMA(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size)Master receives in master mode an amount of data in non-blocking mode with DMA.
HAL_I2C_Master_Transmit_DMA(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size)Master transmits in master mode an amount of data in non-blocking mode with DMA.
HAL_I2C_Slave_Receive_DMA(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size)Slave receives in master mode an amount of data in non-blocking mode with DMA.
HAL_I2C_Slave_Transmit_DMA(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size)Slave transmits in master mode an amount of data in non-blocking mode with DMA. In the STCube tool, the I2C can be configurated fastly and easily as follow.
In Pinout & Configuration, widow selects Connectivity and selects one of the available I2C (I2C1, I2C2, etc). In parameter settings, the master and slave features can be set. Master features are I2C speed mode (standard mode by default and fast mode) and the I2C clock speed (Hz). In standard mode, the device can send up to 400kbit/s while in fast mode up to 1Mbit/s. In general, like clock speed, the STM32 supports 100kHz, 400kHz and sometimes 1MHz.
The main feature of slaves is the primary address length that in general, as previously said, is 7-Bit. Furthermore, the slave can have a secondary address.
Then need to configure the GPIO, as follow:
Now the I2C configuration is terminated and can be possible to generate the code initialization and finally be ready to write our application.
USART is the acronym for Universal Synchronous-Asynchronous Receiver-Transmitter, and is the advancement of the old UART that was unable to handle synchronous communications; in computers, it deals with the management of communication via the RS-232 interface.
Generally, in the communication between devices, there is a transmitter and receiver that can exchange data bidirectionally, thus it happens for example in the communication between the microcontroller and third-party peripherals.
Where To Buy? | ||||
---|---|---|---|---|
No. | Components | Distributor | Link To Buy | |
1 | STM32 Nucleo | Amazon | Buy Now |
In serial communications, the transmitting device sends information (bitstream) through a single channel one bit at a time. Serial communications are distinguished from parallel communications where the bitstream is sent over several dedicated lines altogether. Over the years, serial communication has become the most used as it is more robust and economical. On the other hand, serial communication requires, compared to the parallel case, a more complex management and control architecture, think for example how a trivial loss of a bit during a communication could irreparably corrupt the content of the final message. To this end, various communication protocols have been introduced over the years in order to improve speed, reliability and synchronization. In detail, three different communication modes can be found: synchronous, asynchronous, and isochronous.
/* Private variables ---------------------------*/ TIM_HandleTypeDef htim2; UART_HandleTypeDef huart2;
/* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_TIM2_Init(void); static void MX_USART2_UART_Init(void);
We will use for our examples STM32CubeIDE released by ST and completely free. STM32CubeIDE is a development tool and supports multi operative system (SO), which is part of the STM32Cube software ecosystem. STM32CubeIDE allows using a single platform to configure peripherals, to generate/compile/debug the code for STM32 microcontrollers and microprocessors. The framework used is Eclipse®/CDT, as tool-chain for the development is used GCC toolchain and GDB for the debugging.
To start the project, you must first select the MCU and then initialize and configure the peripherals the user wants to use. At this point, the initialization code is generated. At any time, the user can go back and change initializations and configurations, without affecting the user code. We will dive into these steps with a simple example in the next paragraph.
Where To Buy? | ||||
---|---|---|---|---|
No. | Components | Distributor | Link To Buy | |
1 | STM32 Nucleo | Amazon | Buy Now |
We must connect in series to LED a resistor. What resistance value must be considered to limit the current to 20 mA (see the formula below)?
R > 5 - 1.7/0.02 , where R > 165 ?
A good value of R is 220 ?.
Now we must be able to turn off the led, to do it we need a switch to connect and disconnect the power supply as shown below.
In this case when the switch is closed the Led is ON, when the switch is open the Led is OFF. This can be easily done with a Nucleo board by configuring a GPIO (General Purpose Input Output) pin.
The steps required to configure a core board are explained below. Obviously, this is a very simple practical example, but peripherals, communication protocols and anything else can be configured in a similar way.
As already said, we will use for our examples a NUCLEO-L053R8 board. For our examples, so also a Windows PC is required. The ST-Link USB connector is used both for serial data communications, and firmware downloading and debugging on the MCU. A Type-A to mini-B USB cable must be connected between the board and the computer. The USART2 peripheral uses PA2 and PA3 pins, which are wired to the ST-Link connector. In addition, USART2 is selected to communicate with the PC via the ST-Link Virtual COM Port. A tool that emulates the serial communication terminal is necessary to view received messages and send them. You can download many open-source tools as Tera Term. In this way, you can display on PC the messages received from the board over the virtual communication Port and the other way around.
In this paragraph, we will initialize the peripherals using STM32CubeMX. The STM32CubeMX tool to create the necessary config. files to enable drivers of peripheral used (for more detail read “UM1718 - User manual STM32CubeMX for STM32 configuration and initialization C code generation.”).
The user code must be inserted between the "USER CODE BEGIN" and "USER CODE END" so that if you go to regenerate the initializations that part of the code is not deleted or overwritten. As shown in the code below, the simplest way is to use the function:
Note This function is declared as __weak to be overwritten in case of other implementations in a user file.
- Delay: specifies the delay time length, in milliseconds.
So, in our case GPIOx is GPIOA and the GPIO_PIN is GPIO_PIN_9 (see below).
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_TIM2_Init(); /* USER CODE BEGIN 2 */ /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_9); HAL_Delay(1000); /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ }
Now we are ready to compile and run the project:
Another simple way is to use the function
Note This function uses GPIOx_BSRR register to allow atomic read/modify accesses. In this way, there is no risk of an IRQ occurring between the read and the modified access.
Note that GPIOE is not available on all devices.
This parameter can be one of GPIO_PIN_x where x can be (0..15).
All port bits are not necessarily available on all GPIOs.
This parameter can be one of the GPIO_PinState enum values:
So, in our case GPIOx is GPIOA and the GPIO_PIN is GPIO_PIN_9 and the pin state changes between 0 (LOW or CLEAR) and 1 (HIGH or SET).
In this case, the code is the following:
while (1) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_9,0); HAL_Delay(1000); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_9,1); /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ }
There are many other ways to blink an Led such as PWM, Interrupts etc. and will discuss it in the upcoming lectures. Thanks for reading.
To become familiar with the world of microcontrollers it is necessary to have a development board (also known as a kit), which generally allows you to start working on it easily. Fortunately, the ST provides a wide portfolio of development boards. In this guide, we will describe and use the Nucleo board.
The Nucleo has been introduced a few years ago and its line is divided into three main groups:
Where To Buy? | ||||
---|---|---|---|---|
No. | Components | Distributor | Link To Buy | |
1 | STM32 Nucleo | Amazon | Buy Now |
The number of pins available, so the package, gives the name to the board: Nucleo-32 uses an LQFP-32 package; Nucleo-64 and LQFP-64; Nucleo-144 an LQFP-144. The Nucleo-64 was the first line introduced and counts 16 different boards.
The Nucleo boards have interesting advantages compared to the Discovery. First, is the cheaper cost, you can buy it for around 15-25 dollars. Now in 2021 due to the lack of processed semiconductors, it is very difficult to find them on the normal distribution channels and costs are rising. A return to normal is expected from 2023. Furthermore, Nucleo boards are designed to be pin-to-pin compatible with each other. It is a very important advantage, in fact, if I start to develop my firmware on generic Nucleo later then I can adapt my code to another one.
In the next paragraphs, we will see the main structure of STM32-64
The Nucleo-64 is composed of two parts:
The part with the mini-USB connector is an ST-LINK 2.1 integrated debugger. It needs to upload the firmware on the target MCU and run the debugging. Furthermore, the ST-LINK interface provides a Virtual COM Port (VCP), which can be used to exchange data and messages with the host PC. The ST-LINK interface can be used as a stand-alone ST-LINK programmer, in fac,t can be easily cuttable to reduce board size.
To program the STM32 on board, simply plug in the two jumpers on CN4, as shown in the figure below in pink, but do not use the CN11 connector as that may disturb communication with the STM32 microcontroller of the Nucleo.
However, the ST-LINK provides an optional SWD interface which can be used to program another board without detaching the ST-LINK interface from the Nucleo by removing the two jumpers labeled ST-LINK (CN4).
The rest of the board, MCU part, contains the target MCU (the microcontroller we will use to develop our applications), a RESET button, a user-programmable push button (switch), and an LED. It is possible to mount an external high-speed crystal (HSE) through X3 pads (see figure below). Generally, the Nucleo boards, especially the most recent ones, provide a low-speed crystal (LSE).
The STM32 Nucleo-64 board has 8 connectors:
The CN7 and CN10 ST morpho connectors are male pin headers (2x19, 2.54mm male pin headers) accessible on both sides of the STM32 Nucleo-64 board (see the figure below). All signals and power pins can be probed by an oscilloscope, logical analyzer, or voltmeter through the ST morpho connectors. They are two. They are called Morpho connectors and are a convenient way to access most of the MCU pins.
The first step in developing an application on the STM32 platform is to fully set up the tool-chain. A tool-chain is a set of programs, compilers, and tools that allows us:
To carry out these activities we basically need:
There are several complete tool-chain for the STM32 Cortex-M family, both free and commercial. The most used tools are: IAR for Cortex-M, ARM Keil, and AC6 SW4STM32.
They can integrate everything necessary for the development of applications on STM32 to simplify and accelerate their development. The first two are commercial solutions and therefore have a price to the public that may be too high for those approaching the first time.
So, that was all for today. I hope you have enjoyed today's lecture and have understood this Nucleo Development Board. In the next lecture, we will design our first project in STM32CubeIDE. Thanks for reading.The term, "STM32" refers to a family of 32-bit microcontroller integrated circuits based on the ARM® Cortex®M processor. The architecture of these CPUs (Central Processing Unit) is ARM (Advanced Risk Machine) which is a particular family of Reduced Instruction Set Computing (RISC). RISC architecture differs from Complex Instruction Set Computing (CISC) for its simplicity that allows you to create processors capable of executing instruction sets with shorter times. Why use STM32? The advantages are many, and now we will list a part of them:
In the next paragraph, it will be illustrated how the STM32 is divided to easily identify the one used for your purposes.
Where To Buy? | ||||
---|---|---|---|---|
No. | Components | Distributor | Link To Buy | |
1 | STM32 Nucleo | Amazon | Buy Now |
The STM32 Mainstream has been designed to offer solutions for a wide range of applications where costs, time to market, reliability, and availability are fundamental requirements. They are widely used in real-time control signal processing applications.
There are 5 series in this group:
The STM32 Ultra-Low-Power has been designed to meet the need to develop devices with low energy consumption (such as portable and wearable devices) but maintaining a good compromise with performance. There are 6 series in this group:
The STM32 High-Performance has been designed for data processing and data transfer. It also has a high level of memory integration. There are 5 series in this group:
With STM32 Wireless ST adds in the portfolio a platform for wireless connectivity to the portfolio. It has a broad spectrum of frequencies and is used in various industrial and consumer applications.
It has features compatible with multiple protocols which allows it to communicate with different devices in real-time. Now, only two series belong to this group:
So, that was all for today. I hope you have enjoyed today's lecture. In the next lecture, I am going to focus on the Nucleo Development board, as we are going to use that in our upcoming tutorials. Thanks for reading. Take care !!! :)