Microcontrollers for everyone
Part 2: It’s all about the GPIO
In the last episode…
In the last episode, I have introduced the idea, the motivation, and the philosophy of this series. We also went thru the installation of the toolchain, which is STM32CubeIDE.
In this episode
In this episode, we will learn how to set up a pin as:
- GPIO output (blinking LED)
- GPIO input (reading the state of the button) thru two methods: polling and interrupt
Create a new project
After starting STM32CubeIDE, we are at the landing page:
In the landing page, there are default options:
- Start new STM32 project: it is obvious that this option let us create a new project from scratch.
- Start new project from STM32CubeMX .ioc file: this option allows us to open a configuration file from another project.
- Import SW4STM32 or TrueSTUDIO project: this option lets us import the project either from SW4STM32 or TrueSTUDIO, which is one of the supported IDEs.
We will start our project from scratch so, let’s click on the option of Start new STM32 project. You will be prompted to the next window to select the target (microcontroller).
Target Selection window allows us to choose the microcontroller by selecting different parameters. Since we are using the evaluation board, we move to the Board Selector tab and select as following:
You are prompted to a new window asking for Project Name:
Give your project a name, for my case, it is p3_BlinkyLED, and click Finish.
You will be prompted to a new window, this is where you will configure your MCU.
If you have a look at the toolbar which is marked red in the picture above, you will see, there are multiple tabs:
- Pinout and configuration: This tab lets you activate/deactivate the peripherals on the microcontroller and allocate the pins for those peripherals.
- Clock Configuration: let you set up the clock for the microcontroller and its peripherals.
- Project Manager: let you configure the general settings of the project.
- Tools: allows us to estimate the power consumption of the MCU with the given conditions.
- Additional Software: let you add Middlewares to your project.
Let’s switch back to the Pinout & Configuration tab. We will learn how to configure a peripheral and assign it to the pins.
Peripherals configuration and pins assignment
First, we clear the default pinout setup, so we will have a “clean” pinout. Under Pinout option on the top menu bar, choose Clear Pinouts.
After doing that, you will have a clear pinout as the picture below:
To briefly show you how to activate a peripheral (we will go into detail later when we use the peripherals), for example, USART1, let’s do the following things:
- On the left side of STM32CubeIDE, expand the Connectivity tab, and choose USART1. In the Mode option, choose Asynchronous from the drop-down menu.
- After choosing Asynchronous, the pins for USART1 are automatically activated at pin PA9, PA10 in the MCU pinout.
- If for some reason the pin PA10 and/or PA9 are blocked by other peripherals, there is still an option to move them to the other pins.
- Click Ctrl + Left-Click on the pin you want to move, the tool will highlight a new position for the pin in the blue color (see picture below). To move to the new position, we keep doing the Ctrl + Left-Click and dragging the pin to its new place.
- We don’t need USART1 for now, so we will deactivate it. To do that, go back to the peripheral configuration on the left side, under UASRT1, Mode, choose Disable.
If you still remember our holy goal is doing the infamous blinking LED, you will wonder why there is no peripheral called “LED”, “Pin on/off”, etc. in the configuration. Yes, you are correct, it is not there! To turn on and off an LED, i.e. providing some voltage to the controller’s pins, we need a peripheral called GPIO (General Purpose Input Output).
GPIO can be activated directly in the MCU pinout:
- On the evaluation kit, there is an LED called LD2. The only thing we need to find out is to which pin this LED is connected. If you still remember the board’s document, on page 23, it is described:
User LD2: the green LED is a user LED connected to Arduino signal D13 corresponding to STM32 I/O PA5 (pin 21) or PB13 (pin 34) depending on the STM32 target
- So, with simple 1-minute reading, we know the LED is connected to pin PA5 of the MCU. To activate that pin as a GPIO, we hover our mouse to pin PA5 in the MCU pinout, left-click → choose GPIO_Output. The pin will be activated as a GPIO and given the name GPIO_Output.
- If you are a picky guy/girl and you think that the name GPIO_Output is not intuitive enough, there is an option to change the name. Hovering the mouse to the pin, right-click → choose Enter User Lable and give it a name. I will call it LED2.
Great! We have activated and configured one pin as a GPIO. Next step, we will configure the clock configuration.
Clock Configuration
A microcontroller needs a clock to operate. It is applied the same for your Laptop or PC. Whenever you read a description like core i7 2.1 GHz blah blah, it means your processor is running at the speed of 2.1 GHz. Obviously, the higher the speed, the faster the processor (and physically, the more power it will take).
If you are a bit into PC stuff, you will know the term “Overclocking”, a.k.a increasing (and decreasing, but no one does that) your processor clock. It is applied to the microcontroller in same the manner. You can also adjust the clock of your microcontroller (even choose different clock sources). The major difference is, unlike overclocking your PC, you don’t have to worry about the temperature of the microcontroller and don’t even need a cooling system for it.
Back to STM32CubeIDE, let’s move to the next configuration tab-Clock Configuration. At the first look, you will ask yourself, what the heck you have gotten yourself into??
This is called a clock tree of a microcontroller. It describes all the clock sources available for the microcontroller and all kinds of possible adjustments in terms of clock speed. At the first look, it is quite scary, but let’s take a deep breath and use our engineering logic to find out what is going on there.
- By just focusing only on the area covered by the red square, we can guess/assume that there are some clock sources called LSE, LSI, MSI, HSI, HSE, and RC. With that information, we could have already deduced that LSE and HSE are external clock sources (surprise? E stands for External) and the rest should be internal clocks.
- Let’s shift our red square a bit to the right, we can see that the LCD and RTC peripheral can only receive clock source from HSE, LSE, or LSI. The IWDG can take the clock only from LSI. The PLL (Phase-locked loop) receives the clock only from HSE or HSI.
- Let’s shift our red window again to the right, we can observe that the system clock (mother of all) can take clock from sources: MSI, HSI, HSE and PLLCLK.
- Let’s do a final shift to the right. The system clock provides the clock to “other part of the system”, for example, FCLK, HCLK, APB1. We can also see that some peripherals — USART, I2C, etc. can take from other clock sources than the system clock. They are grey out because we didn’t activate them.
That’s was a lot for clock but this was only our assumption. To verify all the information above, please open the MCU reference manual (link), and read from page 174–185. It is only 12 pages (with some pictures) but all the knowledge is there. Just for your information, these are the names of the clock sources:
- HSI (high-speed internal) oscillator clock
- HSE (high-speed external) oscillator clock
- MSI (multispeed internal) oscillator clock
- LSI (low-speed internal) oscillator clock
- LSE (low-speed external) oscillator clock
- RC: RC oscillator circuit (normally dedicated for USB bus)
Theoretically, we can set up the clock manually, but we are only using a GPIO and no communication buses or timers so we can practically let the tool automatically set the clock for us.
- In the box HCLK (picture below), enter the number 32 ( 32 MHz, we will run the clock at maximum speed) and press enter. The tool will report that there is no solution with the current clock source and it is asking you for the permission to choose another one. Press OK.
- After a while, STM32CubeIDE comes up with a solution, let’s check what it proposes to us.
- Going from right to left, we see system clock derived from the PLLCLK clock with the Prescaler of 1. The PLLCLK clock reached 32MHz by adjusting the PLLMul and PLLDiv. Then, the source for the PLL is the HSI clock.
Clocking part is over but we do not finish yet. Let’s return to the Pinout & Configuration tab.
Peripheral configuration in detail
Let’s switch to Pinout & Configuration tab and navigate to the GPIO configuration as the picture below.
- GPIO output level: this means, after initialization, the pin will state in logic level high or low( output voltage 3.3V or 0V).
- GPIO mode: can be Output Push-pull or Output Open Drain
- GPIO Pull-up/Pull-down: the pin is internally pulled up to the VCC line (3.3V for example), pulled down (to the ground), or floating. This relates to the first point-GPIO output level. For example, if the pin is pulled-up, the logic level 0 will be 3.3V and logic level 1 will be 0V. It is applied the other way around for pulled-down.
- Maximum out speed: this will setup the rising/falling time of a pin, in other words, how long does it take for a pin to change the stage from low to high and high to low. For GPIO, it doesn’t have to be very fast but for any communication bus, it must be fast. But, this will be taken care of by STM32CubeIDE.
- User label: store the modified name of the pin.
So far so good, we have all we need, let’s generate the code from the configuration and add some code.
Project Generation
In STM32CubeIDE, clicking Project Manager → Advance Settings
Remember, we want to use the LL library, right? Change everything from HAL to LL.
Clicking the generate button to generate our code.
Let’s Code
Before looking at the code, let’s compile the program to be sure that the generation process causes no error.
To compile the program, click on the hammer button
After the compilation, there should be no error (If you run into any error, please describe it in the comment).
Now, let’s have a look at the generated code. Please open the main.c file, this is where your application code start. I emphasized the word application because the code start in the startup file (.s file for your information) but what we have generated is mostly located in the main.c .
Scrolling down to the main function approximately at line 65, we see three functions SystemClock_Config(), MX_GPIO_Init()
and not to mention the infamous while(1)
loop. We will have a look inside each function to see what does it do and to check if it acts according to what we have configured. To do that, highlight the function →right-click →choose Open Declaration.
We will start with SystemClock_Config()
void SystemClock_Config(void)
{
/*Setup latency for the flash*/
LL_FLASH_SetLatency(LL_FLASH_LATENCY_1);
/*
* Check if we get the correct configuration
* If it is not, jump to the Error_Handler() function
*/
if(LL_FLASH_GetLatency() != LL_FLASH_LATENCY_1)
{
Error_Handler();
}
/*
* Setup internal voltage regulator of the MCU
*/
LL_PWR_SetRegulVoltageScaling(LL_PWR_REGU_VOLTAGE_SCALE1);
/*
* Enable HSI clock, as what we did in CubeMX
*/
LL_RCC_HSI_Enable();
/* Wait till HSI is ready */
while(LL_RCC_HSI_IsReady() != 1)
{
}
/*
* Trimming the clock and set the multipler to 4, divider to 2
* The same as what the CubeMX configure
*/
LL_RCC_HSI_SetCalibTrimming(16);
LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_HSI,
LL_RCC_PLL_MUL_4, LL_RCC_PLL_DIV_2);
/*
* Enable PLL
*/
LL_RCC_PLL_Enable();
/* Wait till PLL is ready */
while(LL_RCC_PLL_IsReady() != 1)
{
}
/*
* Setup the prescalers and choose PLL as System clock source
* Again, the same as what CubeMX did
*/
LL_RCC_SetAHBPrescaler(LL_RCC_SYSCLK_DIV_1);
LL_RCC_SetAPB1Prescaler(LL_RCC_APB1_DIV_1);
LL_RCC_SetAPB2Prescaler(LL_RCC_APB2_DIV_1);
LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_PLL);
/* Wait till System clock is ready */
while(LL_RCC_GetSysClkSource() !=
LL_RCC_SYS_CLKSOURCE_STATUS_PLL)
{
}
/*
* This is new, we provide the information of the system clock
* to configure the 1ms system tick. This system tick provides
* 1 ms tick to any subsystem the required timing.
* e.g. software timer or real time operating system
*/
LL_Init1msTick(32000000);
LL_SYSTICK_SetClkSource(LL_SYSTICK_CLKSOURCE_HCLK);
LL_SetSystemCoreClock(32000000); /* SysTick_IRQn interrupt configuration */
NVIC_SetPriority(SysTick_IRQn, 0);
}
By skimming through the SystemClock_Config()
, we can conclude that STM32CubeIDE not only generates what we have configured but also generates a lot of hidden things to make the system running. Hopefully, this convinces you that STM32CubeIDE does what we told it to do so we don’t have to take a look at the MX_GPIO_Init()
function. (but you should on your own to make sure that I am not talking bullshit :P ).
So, at this point, you should ask me which function to use to set the pin on/off. The answer lies within the LL API documentation, but normally I am using another version which can be found in:
C:\Users\your_name\STM32Cube\Repository\STM32Cube_FW_L0_Vx.xx.x\Dr-ivers\STM32L0xx_HAL_Driver\STM32L073xx_User_Manual.chm (default path)
If you open that document and go exactly to the location I show in the picture below, you will find a function called LL_GPIO_TogglePin(params)
. This is what we need to turn the pin on/off. The parameters of the function are the port, which is GPIOA (If you give it a name it will become PIN_NAME_GPIO_Port) and the PinMask, which is LL_GPIO_PIN_x (again, if you give it a name, it will become PIN_NAME_Pin).
The only problem left is where to put this function. If you are experience with the embedded systems and the microcontrollers, you will immediately know that the function has to be in the while(1)
loop. If you don’t know that, don’t worry (be happy). It is located in an infinite loop because we want to execute the code forever.
But be cautious!!!! If you, again, remember that we are using a tool that automatically generates our code, how can it know that which is its code, and which is our code, where to delete and where to add code? If you pay attention when skimming through the main.c file (and other files if you encounter them in the future), you will see there are some areas with a pair comment/* USER CODE BEGIN…*/ and /* USER CODE END…*/. These two lines act as a place holder for user code and tell the STMCubeIDE that, the user’s code is located there, DO NOT DELETE IT.
So, the plan is, we will go to the while(1)
loop, at the toggle function between 2 place holders. This place is located at line 102 (if you have not touched anything in the main.c). My function will be:
LL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
//It becomes like that because my PIN_NAME is LED2
Is there anything missing? How can we see the blinking if the code is executed in a blink of the eye (approx. 32 million times/s)? We need a delay function to make it blink slowly. In the same document, where we found the toggle function, the delay function is located at:
Hence, our code will become
LL_GPIO_TogglePin(LD2_PIN_GPIO_Port, LD2_PIN_Pin);
LL_mDelay(1000); //Delay 1 sec, or LED will turn on/off every 1 sec
In the end, thewhile(1)
loop looks like this
/* USER CODE BEGIN WHILE */
while (1)
{
LL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
LL_mDelay(1000);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
Let’s compile the code. It should show no error. To debug the code, we have to connect the evaluation kit to the PC and press the bug button
The IDE will prompt you to a new window called Debug window. There, you press the play button to execute the code and you should see the green LED blinking on the evaluation board.
Now, you can play a bit with the blinking period by changing the delay time, compiling the code, and executing it. If you get lost, don’t worry, this is the github repo for the code in the whole tutorial.
One last point I want to mention is, if you are a paranoid guy and don’t trust the Code Generator to generate your code, you can write the peripheral configuration code on your own. Let’s do it!.
Go back to your main.c file, at line 93, we comment out the GPIO configuration function and write our code right after it, at line 95. If you still recall the configuration of a pin to work as an output pin, you will know that, you need some function to enable the clock for the pin, set output level, set pin mode, set pulled-up/pulled-down, and set speed. These functions are located at the same place where you find the toggle function.
This is what I have written for the pin configuration:
/* Initialize all configured peripherals */
//MX_GPIO_Init();
/* USER CODE BEGIN 2 *//*enable clock for GPIO PortA*/
LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA);/*set pin as output pin*/
LL_GPIO_SetPinMode(LED2_GPIO_Port, LED2_Pin,
LL_GPIO_MODE_OUTPUT);/*set output mode as push pull*/
LL_GPIO_SetPinOutputType(LED2_GPIO_Port, LED2_Pin,
LL_GPIO_OUTPUT_PUSHPULL);/*let our pin floating*/
LL_GPIO_SetPinPull(LED2_GPIO_Port, LED2_Pin, LL_GPIO_PULL_NO);/*toggling speed is low*/
LL_GPIO_SetPinSpeed(LED2_GPIO_Port, LED2_Pin,
LL_GPIO_SPEED_LOW);/* Infinite loop */
/* USER CODE BEGIN WHILE */
After compiling and executing the application, you will see the same result, i.e. it is working with our own configuration.
Reading input from the IO
Polling method
To give you an idea of setting a pin as input is just an opposite direction of setting a pin as an output, we will start from the same project, where we write our GPIO configuration. The final piece, which is missing, is which pin the button is connected to. To find that, we can open the schematic of the board on the board manual. At page 23 of the board manual, it is stated that:
B1 USER: the user button is connected to the I/O PC13 (pin 2) of the STM32 microcontroller.
Therefore, we have to configure pin PC13 as an input pin. We open file main.c and add the following snippet at line 113 (right after the GPIO output configuration).
/*PC13 as input*/
/*enable clock for GPIO PortC*/
LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOC);/*set pin as input pin*/
LL_GPIO_SetPinMode(GPIOC, LL_GPIO_PIN_13, LL_GPIO_MODE_INPUT);/*let our pin floating*/
LL_GPIO_SetPinPull(GPIOC, LL_GPIO_PIN_13, LL_GPIO_PULL_NO);
The code is more or less self-explained. It enabled the clock, configured pin PC13 as input pin with no pull-up or pull-down.
Next, in the while(1) loop, we replace the old code with the following snippet:
if(LL_GPIO_IsInputPinSet(GPIOC, LL_GPIO_PIN_13))
{
LL_GPIO_SetOutputPin(LED2_GPIO_Port, LED2_Pin);
}
else
{
LL_GPIO_ResetOutputPin(LED2_GPIO_Port, LED_Pin);
}
It did nothing else than checking the status of pin PC13. If the state of the pin is high or 1, we will turn on the LED and vice versa.
This method is called Polling because it involves constantly checking for the status of something and set the action according to that. You can also imagine, this method is like driving the car with an annoying kid to the theme park and the kid is constantly asking you “are we there yet? are we there yet?” and you have to answer every time, “yes” or “no”.
This method is pretty straight forward, easy to understand and implement. But for every quick and dirty trick, there are always drawbacks.
- Firstly, we will waste our CPU power just to poll for the status.
- Secondly, while checking for the desired state, we may miss something more important, for example, some message on the communication bus.
Let’s go back to out annoying kid, meanwhile answering to him “yes” or “no”, you could have missed a red light 😜.
You may ask yourself, wait, is there something to let the peripherals, kind of, checking the states itself and only inform you when it reaches the some defined states?. Yes, there is something like that, and it is called interrupt.
Interrupt method
As mentioned above, the savior of our problem is called interrupt. Shortly explain, interrupt is a signal that lets the CPU inside the microcontroller know that, something is happening and the CPU should stop what it is doing (normally code section in the while(1)) to handle the signal.
Whenever an interrupt happens (interrupt request), as soon as the CPU finishes its current instruction, it saves the address of the next instruction, status register, etc. and jumps to a section of code that handles the interrupt request (this section is called interrupt service routine or ISR).
The transition from the current code section to the interrupt code section is called interrupt latency. Of course, the shorter the latency, the better it is. After finishing the ISR, the CPU jumps back to where it left and continues from there. the rule of thumb for the ISR is, the code should be short and clean.
This is only a very brief explanation of how interrupt works. Each MCU architecture, each MCU vendor has a different way to handle the interrupt so please take a look at the datasheet to have a better understanding. I have found this article about interrupt pretty useful, please have a look if you are fancy about interrupting 😆 link (in fact, most of my explanation is based on it).
Back to our STM32 controller, most of the work is taking care of by STM32CubeIDE, so the thing we have to do is knowing how to configure interrupt, filling the right code at the right place, and that’s it.
Let’s create a new project with STM32CubeIDE, I hope you have already known the basic steps of how to do that.
For this application, we need two GPIOs, PA5 to drive the LED and PC13 for the button.
We set the PA5 pin as GPIO output and change its name to LED2 as the picture below:
On the PC13, we initialize it as GPIO_EXTI13. Besides, it is given a name BUTTON.
Although we have configured the PC13 as an external interrupt, we have to activate that function in the configuration tab. Therefore, go to System view → NVIC → Activate EXT line 4 to 15 interrupts
Why do I know that it is line 4 to 15 I have to activate? The answer is in the reference manual, page 289. As you can see in Table 54, the EXTI line 0 –15 are tied to GPIO.
Everything is set and done, let’s generate the project and add some code.
Before filling the code let’s go to MX_GPIO_Init
function to check what is the difference between using the polling method and interrupt method. The noticeable thing is that PC13 is configured as GPIO Input, exactly the same as what we have done in the previous section. Additionally, externally interrupt line 13 is activated and configured as rising edge triggering (code below).
/**/
LL_SYSCFG_SetEXTISource(LL_SYSCFG_EXTI_PORTC,
LL_SYSCFG_EXTI_LINE13);
EXTI_InitStruct.Line_0_31 = LL_EXTI_LINE_13;
EXTI_InitStruct.LineCommand = ENABLE;
EXTI_InitStruct.Mode = LL_EXTI_MODE_IT;
EXTI_InitStruct.Trigger = LL_EXTI_TRIGGER_RISING;
LL_EXTI_Init(&EXTI_InitStruct);/* EXTI interrupt init*/
NVIC_SetPriority(EXTI4_15_IRQn, 0);
NVIC_EnableIRQ(EXTI4_15_IRQn);
So, we have only to look for the ISR, fill in the code and the job is done.
The ISR is located in the file stm32l0xx_it.c. Open that file, look for the function void EXTI4_15_IRQHandler(void)
and fill in the code as below to toggle the LED:
if (LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_13) != RESET)
{
LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_13);
/* USER CODE BEGIN LL_EXTI_LINE_13 */
LL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
/* USER CODE END LL_EXTI_LINE_13 */
}
So…that’s it to toggle LED whenever we press the button, no more polling for the status of the pin connected to the button.
Application
We are able to control the GPIO and the question is, what is the usage of it? Some of the recommendations are:
- Control the relay instead of the LED
- Show the status of the system, e.g. the LED blinks fast - error; the LED blinks slow - system is running; the LED is constantly on -the System is in configuration phase, etc.
- Receive trigger from a sensor
Conclusion
We have learned how to set up GPIO in STM32CubeIDE and generate the project. We also learned how to control the GPIO using LL APIs.
In the next episode, we will learn about UART.
Stay safe and happy coding!