4. 轮询与中断对比

任务定义

本案例在STM32F103最小系统板上,使用一个按键(PA0,外部下拉)和一个LED(PC13,低电平点亮),分别以轮询方式和外部中断方式实现按键检测:每次按下按键,LED状态翻转。最终通过代码结构对比两种方式的CPU占用差异,并通过示波器或逻辑分析仪测量按键按下到LED翻转的响应延迟。

硬件连接

STM32F103 Cortex-M3 @72MHz PA0 PC13 按键 SW-PB 10kΩ下拉 GND LED 330Ω 3.3V ← 按键信号 ← LED驱动 测量点: 示波器CH1→PA0 示波器CH2→PC13

实现代码

以下代码包含两个独立工程文件,分别对应轮询与中断方式。两者共用相同的GPIO初始化逻辑,差异仅在按键检测机制。

// ============ 方式一:轮询检测 (polling.c) ============
#include "stm32f1xx_hal.h"

void GPIO_Init_All(void) {
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_GPIOC_CLK_ENABLE();
    
    GPIO_InitTypeDef gpio = {0};
    // PA0 输入,下拉
    gpio.Pin = GPIO_PIN_0;
    gpio.Mode = GPIO_MODE_INPUT;
    gpio.Pull = GPIO_PULLDOWN;
    HAL_GPIO_Init(GPIOA, &gpio);
    
    // PC13 推挽输出
    gpio.Pin = GPIO_PIN_13;
    gpio.Mode = GPIO_MODE_OUTPUT_PP;
    gpio.Pull = GPIO_NOPULL;
    gpio.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOC, &gpio);
    
    HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); // LED灭
}

// 简单消抖:连续读取20次(约2ms@72MHz)
uint8_t Debounce_Read(void) {
    uint8_t stable = 0;
    for (int i = 0; i < 20; i++) {
        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET)
            stable++;
        // 空转延时约100us
        for (volatile int d = 0; d < 1000; d++);
    }
    return (stable > 15) ? 1 : 0;  // 多数一致则有效
}

int main_polling(void) {
    HAL_Init();
    GPIO_Init_All();
    
    uint8_t last_state = 0;
    uint8_t led_on = 0;
    
    while (1) {
        uint8_t cur = Debounce_Read();
        // 检测上升沿(按下瞬间)
        if (cur == 1 && last_state == 0) {
            led_on = !led_on;
            HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, 
                led_on ? GPIO_PIN_RESET : GPIO_PIN_SET);
        }
        last_state = cur;
        // 主循环空转,模拟其他任务占用CPU
        for (volatile int t = 0; t < 5000; t++);
    }
}

// ============ 方式二:外部中断 (exti.c) ============
#include "stm32f1xx_hal.h"

volatile uint8_t g_led_on = 0;

void GPIO_Init_All(void) {
    // 与轮询方式相同的GPIO初始化代码
    // ...(同上,PA0输入下拉,PC13输出)
}

void EXTI0_IRQHandler(void) {
    if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET) {
        __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
        // 消抖:中断触发后延时10ms再确认
        HAL_Delay(10);
        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET) {
            g_led_on = !g_led_on;
            HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13,
                g_led_on ? GPIO_PIN_RESET : GPIO_PIN_SET);
        }
    }
}

int main_exti(void) {
    HAL_Init();
    GPIO_Init_All();
    
    // 配置EXTI0中断
    HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 0);
    HAL_NVIC_EnableIRQ(EXTI0_IRQn);
    
    // 配置PA0为外部中断模式(上升沿触发)
    GPIO_InitTypeDef gpio = {0};
    gpio.Pin = GPIO_PIN_0;
    gpio.Mode = GPIO_MODE_IT_RISING;
    gpio.Pull = GPIO_PULLDOWN;
    HAL_GPIO_Init(GPIOA, &gpio);
    
    while (1) {
        // 主循环完全空闲,可执行其他任务
        __WFI();  // 等待中断,CPU进入休眠
    }
}

CPU占用对比方法

在两种方式的while(1)主循环中,各放置一个GPIO翻转指令(使用PB0作为调试引脚),用示波器测量PB0的波形占空比:

  • 轮询方式:主循环持续运行,PB0输出连续方波,占空比接近50%,表明CPU始终被占用。
  • 中断方式:主循环执行__WFI()后CPU休眠,PB0仅在中断唤醒瞬间翻转一次,波形为稀疏脉冲,占空比趋近0%。

响应速度测量

将示波器CH1接PA0(按键输入),CH2接PC13(LED输出)。按下按键,测量CH1上升沿到CH2电平翻转的时间差:

  • 轮询方式:延迟 = 消抖时间(约2ms) + 主循环空转时间(约0.5ms) ≈ 2.5ms,且随主循环负载变化而波动。
  • 中断方式:延迟 = 中断响应(约1μs) + HAL_Delay消抖(10ms) ≈ 10ms,但该延迟固定,不受主循环负载影响。

验证与判定标准

  1. 编译烧录两种固件,分别按下按键,LED均应每次翻转一次,无漏检、无抖动误触发。
  2. 用示波器观察PB0调试引脚:轮询方式为连续方波,中断方式为稀疏脉冲,证明CPU占用差异。
  3. 测量按键按下到LED翻转的延迟:轮询方式延迟随主循环代码量增加而增大;中断方式延迟恒定(约10ms),且主循环即使执行重负载任务(如大量浮点运算),响应延迟不变。
  4. 判定通过标准:两种方式均能可靠检测按键;示波器波形证明中断方式CPU空闲率显著高于轮询方式;中断方式响应延迟不随主循环负载变化。