第三章 硬件抽象层设计:GPIO、定时器、ADC与PWM的封装
各位同学,咱们今天聊点实在的。硬件抽象层,简称HAL,说白了就是给底层硬件穿上一件「通用外衣」。你想想看,今天用STM32,明天换NXP,后天可能又用英飞凌——如果每次换芯片都要重写一遍ABS逻辑,那不得累死?
我做了十几年嵌入式,踩过最大的坑就是「代码和硬件绑得太死」。有一次项目赶工期,我把GPIO操作直接写在控制逻辑里,结果客户临时换了MCU型号...嗯,那个通宵改代码的夜晚,我现在还记得。
3.1 GPIO的抽象与封装
GPIO是嵌入式世界最基础的外设。ABS系统里,轮速传感器的信号输入、电磁阀的驱动输出,都离不开它。
我的封装思路是这样的:
- 定义统一的GPIO结构体,包含端口、引脚、模式等信息
- 提供初始化、读、写、翻转四个基础接口
- 把硬件寄存器操作藏在底层,上层只调用接口
来看代码:
/* gpio_hal.h */
typedef struct {
uint32_t port; /* GPIO端口基地址 */
uint16_t pin; /* 引脚号 */
uint8_t mode; /* 输入/输出/复用 */
uint8_t pull; /* 上拉/下拉/浮空 */
} GPIO_Config_t;
/* 基础操作接口 */
void GPIO_Init(GPIO_Config_t *config);
void GPIO_WritePin(GPIO_Config_t *config, uint8_t level);
uint8_t GPIO_ReadPin(GPIO_Config_t *config);
void GPIO_TogglePin(GPIO_Config_t *config);
实际项目中,我习惯把ABS的电磁阀驱动封装成这样:
/* 电磁阀驱动抽象 */
typedef struct {
GPIO_Config_t valve_gpio;
uint16_t pwm_channel; /* 如果支持PWM调速 */
uint8_t active_level; /* 高电平有效还是低电平有效 */
} Valve_Driver_t;
void Valve_Init(Valve_Driver_t *valve);
void Valve_Open(Valve_Driver_t *valve);
void Valve_Close(Valve_Driver_t *valve);
你看,上层逻辑根本不需要知道GPIO是怎么配置的,它只管调用Open和Close。这就是抽象的魅力。
3.2 定时器的抽象与封装
ABS系统对时间精度要求很高。轮速脉冲的周期测量、电磁阀的PWM控制、系统主循环的调度——全都依赖定时器。
我个人建议把定时器抽象成三个层次:
- 基础定时器:提供微秒级和毫秒级的延时
- 周期定时器:产生固定频率的中断,用于系统调度
- 捕获定时器:测量外部脉冲的宽度或周期
来看看我常用的封装方式:
/* timer_hal.h */
typedef void (*Timer_Callback_t)(void);
typedef struct {
uint32_t base_freq; /* 定时器基础频率,单位Hz */
uint32_t period; /* 周期计数值 */
Timer_Callback callback; /* 中断回调函数 */
uint8_t priority; /* 中断优先级 */
} Timer_Config_t;
/* 接口定义 */
void Timer_Init(Timer_Config_t *config);
void Timer_Start(Timer_Config_t *config);
void Timer_Stop(Timer_Config_t *config);
uint32_t Timer_GetTick(void); /* 获取系统滴答计数 */
对于ABS的轮速测量,我通常这样用:
/* 轮速脉冲捕获 */
Timer_Config_t wheel_speed_timer = {
.base_freq = 1000000, /* 1MHz,精度1微秒 */
.period = 0xFFFF, /* 最大计数值 */
.callback = WheelSpeed_ISR,
.priority = 2
};
void WheelSpeed_ISR(void) {
/* 记录捕获值,计算轮速 */
uint32_t capture_val = Timer_GetCaptureValue();
/* ... 计算逻辑 ... */
}
3.3 ADC的抽象与封装
ABS系统里,ADC主要用于采集制动主缸压力、轮缸压力等模拟信号。不同MCU的ADC差异很大——有的12位,有的16位;有的支持连续转换,有的只能单次转换。
我的抽象原则是:上层只关心「我要测哪个通道」和「结果是多少」。
/* adc_hal.h */
typedef struct {
uint8_t channel; /* ADC通道号 */
uint8_t resolution; /* 分辨率,如12位、16位 */
uint32_t sampling_time; /* 采样时间,单位ns */
float ref_voltage; /* 参考电压,单位V */
} ADC_Config_t;
/* 接口 */
void ADC_Init(ADC_Config_t *config);
uint16_t ADC_Read(ADC_Config_t *config);
float ADC_ReadVoltage(ADC_Config_t *config); /* 直接返回电压值 */
关键点:ADC_ReadVoltage这个函数是我特别加上的。很多工程师只返回原始ADC值,然后让上层自己去换算电压。但你想,如果换了MCU,参考电压变了,上层所有换算代码都得改。直接在HAL层完成换算,上层永远拿到的是物理量,移植时只需改底层。
实际项目中,我还会加一个「滤波」功能:
/* 带滤波的ADC读取 */
uint16_t ADC_ReadFiltered(ADC_Config_t *config, uint8_t samples) {
uint32_t sum = 0;
for(uint8_t i = 0; i < samples; i++) {
sum += ADC_Read(config);
}
return (uint16_t)(sum / samples);
}
嗯,这里要注意:采样次数不能太多,否则会影响实时性。ABS系统里,我一般取3-5次做中值滤波,效果不错。
3.4 PWM的抽象与封装
PWM在ABS里主要控制电磁阀的开启程度。传统的ABS只有开/关两种状态,但现在的博世ESP系统已经能做到线性调节了。
PWM的抽象,我重点关注三个参数:频率、占空比、极性。
/* pwm_hal.h */
typedef struct {
uint8_t timer_id; /* 使用的定时器ID */
uint8_t channel; /* PWM通道 */
uint32_t frequency; /* PWM频率,单位Hz */
uint8_t polarity; /* 极性:高电平有效/低电平有效 */
} PWM_Config_t;
/* 接口 */
void PWM_Init(PWM_Config_t *config);
void PWM_SetDuty(PWM_Config_t *config, float duty); /* 0.0 ~ 100.0 */
void PWM_Start(PWM_Config_t *config);
void PWM_Stop(PWM_Config_t *config);
来看一个电磁阀线性控制的例子:
/* ABS电磁阀PWM控制 */
PWM_Config_t abs_valve_pwm = {
.timer_id = 2,
.channel = 1,
.frequency = 2000, /* 2kHz,远高于电磁阀响应频率 */
.polarity = 1 /* 高电平有效 */
};
void ABS_ValveControl(float pressure_target) {
/* 根据目标压力计算占空比 */
float duty = PressureToDuty(pressure_target);
PWM_SetDuty(&abs_valve_pwm, duty);
}
你可能会问:为什么不用直接寄存器操作?因为一旦封装好,你可以在不改变上层逻辑的前提下,轻松切换PWM的实现方式。比如从硬件PWM切换到软件模拟PWM,或者从定时器1换到定时器2。
小结
硬件抽象层设计,说白了就是「把变化封装起来,把稳定暴露出去」。GPIO、定时器、ADC、PWM这四大件,是嵌入式系统最常用的外设,也是HAL设计的核心。
我见过太多工程师,一上来就写控制逻辑,结果硬件一换,代码全废。记住:花在HAL上的时间,会在项目后期十倍百倍地回报你。
下一章,咱们聊聊ABS的核心——轮速信号处理。那才是真正考验功底的地方。