用STM32实现轮询IO
轮询IO是什么?
轮询IO是一种通过定时轮询的方式来控制输入或输出的技术。
简单来说,就是按照固定的时间间隔,读取DI或者写入DO端口。MCU(微控制器)会持续不断地进行这些操作。这种方法在工业设备的IO板卡中非常常见。
之前我写过一篇关于如何用Arduino实现轮询IO的文章,这次就来聊聊用STM32该怎么做。毕竟,Arduino大多是用在开发板和学习阶段,真正的产品里还是STM32这种芯片更常见。
控制流程简介
我们先简单说说轮询IO的基本实现流程。
在嵌入式开发中,一般都会在主函数里写一个死循环(while(1)),然后在循环中定期执行一些任务,比如IO更新。
想要实现轮询IO,通常需要准备以下几个部分:
- 软件定时器
- 定时器超时检查函数
- IO更新函数
- 设置定时器的函数
在主循环中,我们会不断检查定时器是不是到时间了。
如果超时了,就执行一次IO更新,然后重新设置定时器,等待下一次轮询。
定时器中断配置
首先,我们需要准备一个用于驱动的软件定时器。
在STM32中,实现软件定时器的前提是配置定时器中断。
本示例中,我们将设置定时器每100微秒触发一次中断。
TIM2的时钟源为APB1总线的时钟。

以下是基于84 MHz时钟源配置定时器,使其每100 µs触发一次中断的设置截图:


软件定时器的实现
接下来我们用这个中断来驱动软件定时器。
至于变量的位数,根据你要计的最长时间决定就好。我这里图方便,直接用了一个32位变量。
我们需要两个函数:
- 一个用于初始化定时器变量
- 一个在中断中递减它的值
#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;
}
}
}
另外,我们还需要两个配套函数,一个用于检查定时器是否超时,一个是设置定时器倒计时:
#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);
/**
* @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;
}
最后,需要在TIM2的中断服务函数中调用 user_timer(),并别忘了清除中断标志位:
/**
* @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更新函数
接下来就是实际的IO更新函数了。
为了让软件更好写,我在电路设计时特意把MCU的GPIO位顺序,跟上位机看到的一致。
所以在软件上,我就用LL库来直接控制寄存器,没用HAL库(当然这看个人喜好啦)。
电路是低电平有效,所以读取的时候还要做位反转。
#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));
}
由于 io_update() 需要在主循环中调用,请务必在 gpio.h 中声明该函数:
extern void io_update();
主函数
在主函数的 while(1) 循环里加上 io_update(),整个IO轮询流程就算完成了:
while(1){
io_update();
}
这样就能做到每1ms更新一次IO状态了。
轮询IO的实际应用
轮询IO其实在工业控制场景中很常见,特别是一些对响应时间要求不是特别高的IO板。
它的优点也很明显:
- 实现简单,不用太多花里胡哨的逻辑
- 就算是便宜的、性能不高的MCU也能轻松胜任
- 像传感器、阀门这种设备对响应时间要求不高,没必要频繁更新
- 而且中断虽然延迟低,但实现稍微复杂一些,轮询在这方面就简单多了。
所以如果你是搞嵌入式开发的,或者是学生准备参加机器人比赛,建议你一定要掌握轮询IO这种最基本的实现方法。后面无论做什么平台,都会用得上!
Discussion