Polling IO with STM32
What is polling IO?
Polling IO is a method of controlling input/output through periodic polling.
The MCU reads or writes to the IO ports at fixed intervals.
This method is commonly used in IO boards for industrial equipment.
Previously, I introduced how to do this with Arduino, but this time I'll explain how to implement it with STM32—after all, Arduino isn't typically used in actual products.
Control Flow
Here's a brief explanation of the control flow.
In embedded systems, it's common to create an infinite loop in the main function and perform tasks such as IO updates within that loop. The following components are required for polling IO:
- Software timer
- Timer timeout check function
- IO update function
- Timer set function
Within the infinite loop, the system repeatedly checks whether the timer has timed out. If it has, it performs the IO update and then resets the timer after the IO operation.
Timer Interrupt Hardware Configuration
First, we need to prepare a software timer.
To create a software timer, it's necessary to configure a timer interrupt on the STM32.
In this example, we will configure the timer to trigger an interrupt every 100 µs.
TIM2 uses the APB1 bus clock as its clock source.

The following settings are used to generate an interrupt every 100 µs from an 84 MHz clock source:


Software Timer
We will use this interrupt to drive a software timer.
Depending on how many seconds you want to count, the required variable size may vary—but in my case, I simply used a 32-bit variable without overthinking it.
We'll prepare two functions: one to initialize the timer, and another to decrement the timer variable.
#define USER_TIMER_NUM 1
uint32_t u_timer[USER_TIMER_NUM] = {0};
/**
* @brief Timer tick handler for polling-based user timers
* @param none
* @retval none
* @note Decrements all user timers and increments the cycle timer
* This function is called every time when TIM2 interrupt occurs
*/
void user_timer()
{
uint8_t i;
for(i = 0; i < USER_TIMER_NUM; i++){
if(u_timer[i] > 0){
u_timer[i] = u_timer[i] - 1;
}
}
}
We also need a function to check whether the timer has timed out, and another function to set the timer duration.
These functions should be defined as follows. To make them accessible from other files, declare them in tim.h as shown below.
Don't forget to also declare the previously mentioned functions—such as the one registered in the interrupt service routine—in tim.h.
#define TIME_UP 0
#define TIMER_NO0 0
#define TIME_1MS 10
extern void user_timer();
extern uint32_t user_timer_check(uint8_t Timer_No);
extern void user_timer_set(uint8_t Timer_No, uint32_t time);
The timer check and set functions are implemented as follows:
/**
* @brief Get remaining time of the specified user timer
* @param timer_no Index of the timer (0 to USER_TIMER_NUM-1)
* @retval uint32_t Remaining time in units of 100 µs
* @note Returns remaining time of specified timer
*/
uint32_t user_timer_check(uint8_t timer_no)
{
return u_timer[timer_no];
}
/**
* @brief Set a specific software timer
* @param timer_no Index of the timer (0 to USER_TIMER_NUM-1)
* @param time Countdown time to set (unit: 100 µs)
* @retval none
*/
void user_timer_set(uint8_t timer_no, uint32_t time)
{
u_timer[timer_no] = time;
}
Finally, register the timer decrement function in the TIM2 interrupt service routine.
Don't forget to clear the interrupt flag as well.
/**
* @brief This function handles TIM2 global interrupt.
*/
void TIM2_IRQHandler(void)
{
/* USER CODE BEGIN TIM2_IRQn 0 */
if(LL_TIM_IsActiveFlag_UPDATE(TIM2)){
LL_TIM_ClearFlag_UPDATE(TIM2);
user_timer();
}
/* USER CODE END TIM2_IRQn 0 */
/* USER CODE BEGIN TIM2_IRQn 1 */
/* USER CODE END TIM2_IRQn 1 */
}
IO Update Function
Here is the function used to update the IO.
To make software development easier, I design the hardware so that the bit arrangement of the MCU's GPIO port matches the bit order as seen from the higher-level interface.
For this reason, I use the LL (Low-Layer) library instead of HAL. This choice depends on personal preference.
The circuit is designed to be active-low, so bit inversion is also performed in the software.
When timer No.0 times out, the GPIOA port is read.
Make sure to prepare a suitable variable to store the obtained data.
#define DI_REG_NUM 1
uint16_t di_reg[DI_REG_NUM] = {0};
/**
* @brief IO update
* @param none
* @retval none
* @note Update IO data and calculate actual cycle time of this period.
* This function is executed by every 1ms
*/
void io_update()
{
if(user_timer_check(TIMER_NO0) == TIME_UP){
read_di();
user_timer_set(TIMER_NO0, TIME_1MS);
}
}
/**
* @brief Read DI from MCU
* @param none
* @retval none
* @note chattering eliminator period 1ms,
* If previous data and current data is different,
* it is defined as chattering.
* In that case, do not update DI data.
*/
void read_di()
{
uint8_t i;
// Read di port. All di ports are low-active, so bit inversion is necessary
di_reg[0] = ~(uint8_t)(LL_GPIO_ReadInputPort(GPIOA));
}
Since io_update() needs to be called within the infinite loop in main.c, don’t forget to declare it in gpio.h.
extern void io_update();
Main Function
Finally, complete the setup by adding io_update() to the infinite loop in the main function.
Processing in the main function:
while(1){
io_update();
}
With this, you now have a program that updates the IO status every 1 ms.
Practical Use of Polling IO
Polling IO is commonly used in applications like industrial IO boards, where millisecond-level update intervals are sufficient.
Here are some of the reasons:
- Low implementation complexity
- Works reliably even on low-cost, low-performance microcontrollers
- From the MCU's perspective, connected devices like sensors and valves operate much more slowly, so high-speed IO updates are unnecessary
Since polling IO does not benefit much from the low-latency advantage of interrupts, ease of implementation takes priority.
If you aim to become an embedded systems engineer, or if you’re a student involved in robotics competitions at a technical college or university, I highly recommend learning how to implement polling IO.
Discussion