在做MCU项目,跨芯片移植是家常便饭,硬件抽象层(HAL)要是搭不稳,代码分分钟炸窝。 今天就跟大伙唠唠,怎么利用厂家资源的同时,把HAL层做扎实,技术要求是啥,再吐槽几个踩过的坑。
厂家库是基础,但别完全依赖。现在MCU厂家确实给力,STM32、NXP这些大厂的库文件、设计案例一大堆,技术支持也到位。但直接用厂家库有个大问题依赖也会太多。比如STM32的HAL库,初始化函数里可能默认开了某些时钟,换到GD32上,时钟树结构不一样,直接用就可能出问题。所以咱们得把厂家库当原料,而不是成品,得自己再加工一层。
技术要求上,接口必须宽进严出。啥意思?就是HAL层的接口得能覆盖所有硬件功能,参数得全。比如PWM初始化,不能只写个PWM_Init(duty),得把频率、占空比、死区时间电机控制必备都塞进去。 这样上层想调死区时间,直接改结构体就行,不用改函数调用。厂家库可能没这些参数,咱就得在HAL层补上。 然后是资源管理的全局视角。厂家库可能只管自己模块的资源,比如定时器、DMA通道,但项目里多个模块可能抢资源。比如电机控制用定时器1,通信用定时器2,换芯片后定时器编号可能变了,这时候HAL层得有个全局资源表,记录哪个定时器被哪个模块占用了。
可以看一下这个代码,相信好多人都用过。 typedef struct {
uint8_t timer_id; // 定时器编号
uint8_t used_by; // 被哪个模块占用(电机、UART等)
} Resource_Table;
static Resource_Table resource_table[MAX_RESOURCES];
HAL_Status Resource_Alloc(uint8_t *resource_id, uint8_t used_by) {
for (int i = 0; i < MAX_RESOURCES; i++) {
if (resource_table[i].used_by == 0) { // 未占用
resource_table[i].used_by = used_by;
*resource_id = resource_table[i].timer_id;
return HAL_OK;
}
}
return HAL_ERROR;
}
要是忘了这个,换芯片后两个模块抢同一个定时器,直接死机。
错误处理必须认真的去检查 ,厂家库可能假设硬件操作肯定成功,但实际可能翻车。比如SPI传输可能因为总线冲突失败,这时候HAL层得返回错误码,比如HAL_TIMEOUT、HAL_ERROR,让上层能处理。参数校验更关键,比如PWM频率不能超过芯片最大值,要是上层传个1MHz,而芯片只支持500kHz,HAL层得拦住,返回HAL_PARAM_ERROR,而不是让硬件瞎跑,所以还是需要针对性的进行。 再说说算法与HAL的配合。比如电机控制的PID算法,核心是计算占空比,但具体怎么输出到PWM,得靠HAL层屏蔽硬件差异。假设算法层算出占空比是80%,HAL层得把80%转换成对应芯片的寄存器值(比如16位定时器是65535*0.8=52428),同时处理死区时间、通道互补输出这些细节。这样算法层不用管是STM32还是GD32,调HAL_PWM_SetDuty(80)就行,分享的都是以前在进行时的一些经验,相信也有人会遇到过。 厂家库可能测试过,但咱的HAL层改过,得自己测。用Unity或CMake搭测试框架,模拟硬件操作,比如测SPI_Transmit(),模拟传输成功、超时、参数错误的情况,确保HAL层能正确处理。特别是资源冲突、参数越界这些边界条件,必须测到。有次用STM32的库直接移植到GD32,SPI的时钟分频计算方式不同,STM32是APB2CLK / (divider+1),GD32是APB2CLK / divider,结果SPI不通信,排查两天才发现是分频系数没改。还有次PWM初始化没传死区时间,电机换相时短路,烧了功率管。更惨的是资源表没写全,换芯片后定时器编号变了,两个模块抢同一个定时器,直接死机。 HAL层要稳,得做到:接口覆盖全、资源管理严、错误处理真、测试覆盖广。厂家库是好帮手,但得自己再“加工”一层,把隐式依赖、资源冲突这些坑填平。这样跨芯片移植才能少掉坑,代码也能更干净。
|