3、GPIO驱动开发:GPIO模块初始化、输入输出模式配置、轮询与中断方式读取轮速传感器信号
好,咱们进入第三章。这一章聊的是GPIO驱动开发,说白了就是怎么让芯片的引脚听话,去读取轮速传感器的信号。
轮速传感器,是ABS系统的眼睛。它告诉控制器,车轮现在转得有多快。如果GPIO配置不对,信号读进来就是错的,那ABS算法再牛也没用。我当年刚入行时,就吃过这个亏——轮速信号毛刺特别多,查了三天,最后发现是GPIO的上下拉电阻没配好。
3.1 GPIO模块初始化
初始化,是一切工作的起点。你想想看,芯片上电后,GPIO引脚默认是什么状态?通常是高阻输入。但轮速传感器信号进来,我们需要一个确定的电平状态。
我个人习惯,初始化分三步走:
- 使能GPIO时钟——没时钟,寄存器写不进去。
- 配置引脚功能——是普通IO,还是复用功能?
- 设置初始电平——输出的话,先拉高还是拉低?
这里给个示例代码,基于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);
}
3.2 输入输出模式配置
GPIO的模式,其实没那么复杂。输入就是读引脚电平,输出就是写引脚电平。但细节里藏着魔鬼。
输入模式:
- 浮空输入——引脚电平完全由外部决定。适合外部有强驱动的情况。
- 上拉输入——内部接个电阻到VCC。默认高电平,外部拉低才变低。
- 下拉输入——内部接个电阻到GND。默认低电平,外部拉高才变高。
输出模式:
- 推挽输出——能输出高,也能输出低。驱动能力强。
- 开漏输出——只能输出低,高电平要靠外部上拉。常用于I2C等总线。
我在项目中遇到过一个问题:轮速传感器是磁电式的,输出信号幅值随转速变化。低速时信号只有几百毫伏,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%。
3.4 中断方式读取轮速传感器信号
中断,才是正经做法。信号边沿一来,CPU立刻响应,不用傻等。
配置中断的步骤:
- 使能GPIO中断线
- 配置触发边沿(上升沿、下降沿、双边沿)
- 编写中断服务函数
- 在中断里读取引脚电平,并清除中断标志
示例代码:
// 配置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就能腾出手来处理其他任务。
3.6 实际项目中的一点心得
最后,分享几个我在实际项目中踩过的坑:
- 引脚复用冲突:有的芯片引脚默认是JTAG/SWD功能,如果没关闭,GPIO配置不生效。我遇到过PA15怎么都拉不高,查了半天才发现是JTAG占用了。
- 中断优先级:轮速中断优先级要设得高一些,但不能高于系统滴答定时器。否则时间戳会乱。
- 去抖动:轮速传感器信号在低速时可能有抖动。我习惯在中断里加一个20微秒的延时再读一次,确认电平稳定。但注意,延时不能太长,否则会丢脉冲。
嗯,这一章就到这里。GPIO看似简单,但它是整个ABS系统的地基。地基不稳,上层建筑再漂亮也没用。下一章,咱们聊聊定时器,怎么精确测量轮速脉冲的周期。