🔖

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