第三章 硬件抽象层(HAL)设计:外设驱动的标准化封装
好,咱们进入正题。这一章聊的是硬件抽象层,也就是HAL。说实话,很多点钞机开发团队在这一块栽过跟头。我见过不少项目,代码写得飞起,结果换个MCU平台,整个软件架构就得重写。为什么会这样?说白了,就是硬件依赖没剥离干净。
我个人习惯,在项目一开始就把HAL层搭好。哪怕后面不改芯片,这个习惯也能让代码清晰很多。你想想看,点钞机里用到的外设其实就那几样:GPIO、定时器、PWM、ADC、UART、SPI、I2C。咱们一个一个来拆解。
3.1 GPIO的抽象封装
GPIO看似简单,但坑最多。我记得有一次,一个同事直接操作寄存器来控制电机使能脚,结果换了个引脚号,满世界找代码改。嗯,这就是典型的反面教材。
我的做法是,定义一个结构体,把引脚号、模式、上下拉这些都封装进去:
/* gpio_hal.h */
typedef struct {
uint8_t port; /* GPIO端口号 */
uint8_t pin; /* 引脚号 */
uint8_t mode; /* 输入/输出/复用 */
uint8_t pull; /* 上拉/下拉/浮空 */
uint8_t speed; /* 输出速度 */
} gpio_cfg_t;
/* 标准接口 */
void gpio_hal_init(gpio_cfg_t *cfg);
void gpio_hal_set(gpio_cfg_t *cfg, uint8_t level);
uint8_t gpio_hal_get(gpio_cfg_t *cfg);
void gpio_hal_toggle(gpio_cfg_t *cfg);
3.2 定时器的抽象
点钞机里定时器用得特别多。电机控制、传感器采样、通信超时,哪个都离不开它。我建议把定时器抽象成两类:周期定时器和单次定时器。
/* timer_hal.h */
typedef void (*timer_callback_t)(void *arg);
typedef struct {
uint8_t timer_id; /* 定时器编号 */
uint32_t period_us; /* 周期,单位微秒 */
uint8_t mode; /* 周期模式或单次模式 */
timer_callback_t callback; /* 回调函数 */
void *arg; /* 回调参数 */
} timer_cfg_t;
int timer_hal_init(timer_cfg_t *cfg);
int timer_hal_start(timer_cfg_t *cfg);
int timer_hal_stop(timer_cfg_t *cfg);
int timer_hal_reload(timer_cfg_t *cfg, uint32_t period_us);
3.3 PWM的标准化
PWM在点钞机里主要控制电机速度和蜂鸣器。抽象起来其实不复杂,关键是频率和占空比要能独立调节。
/* pwm_hal.h */
typedef struct {
uint8_t pwm_id; /* PWM通道号 */
uint32_t freq_hz; /* 频率 */
uint16_t duty_cycle; /* 占空比,0-1000对应0.0%-100.0% */
} pwm_cfg_t;
int pwm_hal_init(pwm_cfg_t *cfg);
int pwm_hal_set_duty(pwm_cfg_t *cfg, uint16_t duty);
int pwm_hal_set_freq(pwm_cfg_t *cfg, uint32_t freq);
int pwm_hal_start(pwm_cfg_t *cfg);
int pwm_hal_stop(pwm_cfg_t *cfg);
这里有个细节。占空比我用的是千分比,不是百分比。为什么?因为千分比精度更高,调电机转速时更细腻。我在项目中试过百分比,电机低速时抖动得厉害,换成千分比就好多了。
3.4 ADC的封装
点钞机用ADC来检测传感器信号,比如红外对管、磁头信号。我一般提供两种接口:单次采集和连续采集。
/* adc_hal.h */
typedef struct {
uint8_t adc_id; /* ADC编号 */
uint8_t channel; /* 通道号 */
uint8_t resolution; /* 分辨率,如12位 */
uint32_t sample_time; /* 采样时间 */
} adc_cfg_t;
int adc_hal_init(adc_cfg_t *cfg);
uint16_t adc_hal_read(adc_cfg_t *cfg); /* 单次读取 */
int adc_hal_start_continuous(adc_cfg_t *cfg,
uint16_t *buffer,
uint32_t length); /* 连续采集 */
int adc_hal_stop_continuous(adc_cfg_t *cfg);
3.5 UART的抽象
UART在点钞机里用来和上位机通信,或者和传感器模块交互。我习惯把它抽象成收发缓冲区+回调的方式。
/* uart_hal.h */
typedef void (*uart_rx_callback_t)(uint8_t data);
typedef struct {
uint8_t uart_id; /* UART编号 */
uint32_t baudrate; /* 波特率 */
uint8_t data_bits; /* 数据位 */
uint8_t stop_bits; /* 停止位 */
uint8_t parity; /* 校验位 */
uart_rx_callback_t rx_cb; /* 接收回调 */
} uart_cfg_t;
int uart_hal_init(uart_cfg_t *cfg);
int uart_hal_send_byte(uart_cfg_t *cfg, uint8_t data);
int uart_hal_send_buffer(uart_cfg_t *cfg, uint8_t *data, uint32_t len);
int uart_hal_receive_byte(uart_cfg_t *cfg, uint8_t *data);
我个人建议,发送用阻塞方式,接收用中断+回调。为什么?因为点钞机对实时性要求高,发送时CPU等一会儿没关系,但接收时不能错过任何一个字节。
3.6 SPI和I2C的封装
这两个接口在点钞机里用得相对少一些,主要接一些传感器或者存储芯片。它们的抽象思路类似,都是主从模式。
/* spi_hal.h */
typedef struct {
uint8_t spi_id; /* SPI编号 */
uint32_t freq; /* 时钟频率 */
uint8_t mode; /* 模式0-3 */
uint8_t bit_order; /* MSB或LSB优先 */
} spi_cfg_t;
int spi_hal_init(spi_cfg_t *cfg);
int spi_hal_transfer(spi_cfg_t *cfg, uint8_t *tx, uint8_t *rx, uint32_t len);
/* i2c_hal.h */
typedef struct {
uint8_t i2c_id; /* I2C编号 */
uint32_t freq; /* 时钟频率,标准100k或快速400k */
} i2c_cfg_t;
int i2c_hal_init(i2c_cfg_t *cfg);
int i2c_hal_write(i2c_cfg_t *cfg, uint8_t dev_addr, uint8_t *data, uint32_t len);
int i2c_hal_read(i2c_cfg_t *cfg, uint8_t dev_addr, uint8_t *data, uint32_t len);
3.7 驱动接口标准化总结
好了,上面这些接口,其实遵循的都是同一个套路:
- 初始化函数:传入配置结构体,返回状态码
- 控制函数:启动、停止、设置参数
- 数据交换函数:读写操作
- 回调机制:异步事件通知上层
这样做的好处很明显。你想想看,如果有一天要换MCU,从STM32换成GD32,或者从NXP换成国产芯片,你只需要重写HAL层的这几个文件。上层的业务逻辑代码,一行都不用动。
我在一个点钞机项目里验证过这个思路。当时客户要求从STM32F103换到AT32F403,我花了三天时间重写HAL层,然后整个应用层代码直接编译通过。嗯,那种感觉,真的很爽。
| 外设 | 核心接口数 | 关键点 | 常见坑 |
|---|---|---|---|
| GPIO | 4 | 配置结构体封装 | 引脚号硬编码 |
| 定时器 | 4 | 回调函数不要耗时 | 中断中调用阻塞函数 |
| PWM | 4 | 千分比精度更高 | 频率突变导致电机抖动 |
| ADC | 4 | 检查转换完成标志 | 读取未完成的数据 |
| UART | 4 | 发送阻塞,接收中断 | 接收缓冲区溢出 |
| SPI | 2 | 片选交给上层 | 片选时序不匹配 |
| I2C | 3 | 注意应答信号 | 总线锁死 |
最后说一句。HAL层不是越薄越好,也不是越厚越好。我的原则是:刚好够用,不多不少。太薄了,换平台时还得改上层代码;太厚了,性能损耗大,调试也麻烦。这个度,得靠项目经验去把握。
下一章,咱们聊聊操作系统抽象层。点钞机要不要上RTOS?上了之后怎么封装?这些都是很有意思的话题。