3、GPIO驱动开发:GPIO模块初始化、输入输出模式配置、轮询与中断方式读取轮速传感器信号

好,咱们进入第三章。这一章聊的是GPIO驱动开发,说白了就是怎么让芯片的引脚听话,去读取轮速传感器的信号。

轮速传感器,是ABS系统的眼睛。它告诉控制器,车轮现在转得有多快。如果GPIO配置不对,信号读进来就是错的,那ABS算法再牛也没用。我当年刚入行时,就吃过这个亏——轮速信号毛刺特别多,查了三天,最后发现是GPIO的上下拉电阻没配好。

3.1 GPIO模块初始化

初始化,是一切工作的起点。你想想看,芯片上电后,GPIO引脚默认是什么状态?通常是高阻输入。但轮速传感器信号进来,我们需要一个确定的电平状态。

我个人习惯,初始化分三步走:

  1. 使能GPIO时钟——没时钟,寄存器写不进去。
  2. 配置引脚功能——是普通IO,还是复用功能?
  3. 设置初始电平——输出的话,先拉高还是拉低?

这里给个示例代码,基于STM32的HAL库,但思路通用:

void GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    // 1. 使能GPIO时钟
    __HAL_RCC_GPIOA_CLK_ENABLE();

    // 2. 配置PA0为输入,用于轮速传感器
    GPIO_InitStruct.Pin = GPIO_PIN_0;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;  // 外部已有上拉
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    // 3. 配置PB1为输出,用于控制指示灯
    GPIO_InitStruct.Pin = GPIO_PIN_1;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

    // 初始状态:指示灯灭
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET);
}
小提示:初始化时,如果传感器信号是霍尔式,通常外部已经有上拉电阻,GPIO内部就别再开上拉了,否则电平会被拉得太高,影响信号边沿。

3.2 输入输出模式配置

GPIO的模式,其实没那么复杂。输入就是读引脚电平,输出就是写引脚电平。但细节里藏着魔鬼。

输入模式:

  • 浮空输入——引脚电平完全由外部决定。适合外部有强驱动的情况。
  • 上拉输入——内部接个电阻到VCC。默认高电平,外部拉低才变低。
  • 下拉输入——内部接个电阻到GND。默认低电平,外部拉高才变高。

输出模式:

  • 推挽输出——能输出高,也能输出低。驱动能力强。
  • 开漏输出——只能输出低,高电平要靠外部上拉。常用于I2C等总线。

我在项目中遇到过一个问题:轮速传感器是磁电式的,输出信号幅值随转速变化。低速时信号只有几百毫伏,GPIO根本识别不了。后来我改用比较器+施密特触发器整形,才把信号变成干净的方波。

注意:磁电式轮速传感器,低速时信号幅值很低。直接接GPIO输入,可能读不到脉冲。建议先经过信号调理电路。

3.3 轮询方式读取轮速传感器信号

轮询,就是CPU不停地去读引脚电平。简单粗暴,适合低速场景。

代码逻辑一般是:

uint8_t ReadWheelSpeed_Polling(void)
{
    // 读取PA0引脚电平
    return (uint8_t)HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
}

然后在主循环里不断调用:

while(1)
{
    uint8_t level = ReadWheelSpeed_Polling();
    if(level == 1)
    {
        // 高电平处理
    }
    else
    {
        // 低电平处理
    }
}

但轮询有个致命问题:浪费CPU。你想想看,如果CPU一直在那傻等电平变化,其他任务还做不做了?ABS系统里,除了轮速,还要处理制动压力、通信、故障诊断……CPU资源很宝贵。

我曾经在一个项目里,轮询频率设得太高,结果CPU占用率飙到80%,CAN通信都丢帧了。后来改成中断,CPU占用率直接降到15%。

核心观点:轮询适合低速、非实时场景。对于ABS这种安全关键系统,轮询只建议用在初始化自检阶段。

3.4 中断方式读取轮速传感器信号

中断,才是正经做法。信号边沿一来,CPU立刻响应,不用傻等。

配置中断的步骤:

  1. 使能GPIO中断线
  2. 配置触发边沿(上升沿、下降沿、双边沿)
  3. 编写中断服务函数
  4. 在中断里读取引脚电平,并清除中断标志

示例代码:

// 配置PA0为中断输入,双边沿触发
void GPIO_Interrupt_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_SYSCFG_CLK_ENABLE();

    GPIO_InitStruct.Pin = GPIO_PIN_0;
    GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING_FALLING;  // 双边沿触发
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    // 使能中断线
    HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}

// 中断服务函数
void EXTI0_IRQHandler(void)
{
    // 检查中断标志
    if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET)
    {
        // 读取当前电平
        uint8_t level = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);

        // 记录脉冲时间戳,用于计算转速
        // 这里可以调用一个函数,记录tick值
        RecordPulseTimestamp(level);

        // 清除中断标志
        __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
    }
}
经验之谈:中断服务函数里,千万别做复杂运算。记录时间戳、设置标志位就够了。真正的转速计算,放到主循环或任务里去做。否则中断嵌套一多,系统就崩了。

3.5 轮询 vs 中断:怎么选?

我整理了一个对比表,方便你参考:

对比项 轮询方式 中断方式
CPU占用率 高(持续占用) 低(事件触发)
实时性 取决于轮询频率 高(立即响应)
代码复杂度 中等
适用场景 低速信号、初始化自检 高速信号、实时控制
抗干扰能力 弱(易受毛刺影响) 强(可配合滤波)

说白了,ABS系统里轮速传感器信号频率可能高达几千赫兹,用轮询根本扛不住。我建议你直接上中断,配合一个环形缓冲区来记录脉冲时间戳,这样CPU就能腾出手来处理其他任务。

避坑指南:我曾经在中断里直接调用HAL_Delay(),结果系统卡死了。记住,中断里不能有阻塞函数!也不能做printf打印,那会拖慢系统。

3.6 实际项目中的一点心得

最后,分享几个我在实际项目中踩过的坑:

  • 引脚复用冲突:有的芯片引脚默认是JTAG/SWD功能,如果没关闭,GPIO配置不生效。我遇到过PA15怎么都拉不高,查了半天才发现是JTAG占用了。
  • 中断优先级:轮速中断优先级要设得高一些,但不能高于系统滴答定时器。否则时间戳会乱。
  • 去抖动:轮速传感器信号在低速时可能有抖动。我习惯在中断里加一个20微秒的延时再读一次,确认电平稳定。但注意,延时不能太长,否则会丢脉冲。

嗯,这一章就到这里。GPIO看似简单,但它是整个ABS系统的地基。地基不稳,上层建筑再漂亮也没用。下一章,咱们聊聊定时器,怎么精确测量轮速脉冲的周期。