www.z-linear.com
D223固件大量使用HAL库回调函数——HAL_TIM_PeriodElapsedCallback、HAL_QSPI_RxCpltCallback、HAL_SPI_TxCpltCallback……这些回调是怎么被调用的?HAL库内部做了什么?本文追踪从硬件中断到用户回调的完整链路。
一、HAL库回调设计模式
1.1 为什么用回调?
HAL库采用回调设计模式,将硬件事件通知给用户代码:
传统方式(寄存器): 中断 → 用户ISR → 直接处理硬件 HAL方式(回调): 中断 → HAL_IRQHandler → HAL状态机 → 用户回调函数| 方式 | 优势 | 劣势 |
|---|---|---|
| 直接寄存器 | 快 | 代码复杂、移植性差 |
| HAL回调 | 统一接口、可移植 | 有额外开销 |
1.2 HAL回调的分类
| 回调类型 | 触发条件 | 典型函数 |
|---|---|---|
| 周期完成 | 定时器溢出 | HAL_TIM_PeriodElapsedCallback |
| 传输完成 | DMA/发送完成 | HAL_QSPI_RxCpltCallback |
| 半传输 | DMA半完成 | HAL_TIM_HalfCpltCallback |
| 错误 | 传输错误 | HAL_QSPI_ErrorCallback |
| 状态变化 | GPIO电平变化 | HAL_GPIO_EXTI_Callback |
二、HAL库中断处理链
2.1 完整调用链
以TIM13定时器中断为例:
① 硬件中断 TIM8_UP_TIM13_IRQHandler() ↓ ② HAL中断入口 HAL_TIM_IRQHandler(&htim13) ↓ ③ 检测中断标志 if(__HAL_TIM_GET_FLAG(htim, TIM_FLAG_UPDATE)) ↓ ④ 清除中断标志 __HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_UPDATE) ↓ ⑤ 调用HAL内部回调 HAL_TIM_PeriodElapsedCallback(htim) ↓ ⑥ 用户重写的回调 if(htim == &htim13) { qspiAdcTimTask(); // D223用户代码 }2.2 HAL_TIM_IRQHandler源码分析
// HAL库内部函数(简化)voidHAL_TIM_IRQHandler(TIM_HandleTypeDef*htim){// 检查更新中断标志if(__HAL_TIM_GET_FLAG(htim,TIM_FLAG_UPDATE)!=RESET){if(__HAL_TIM_GET_IT_SOURCE(htim,TIM_IT_UPDATE)!=RESET){__HAL_TIM_CLEAR_IT(htim,TIM_IT_UPDATE);// 清除标志HAL_TIM_PeriodElapsedCallback(htim);// 调用回调}}// 检查捕获/比较中断...if(__HAL_TIM_GET_FLAG(htim,TIM_FLAG_CC1)!=RESET){if(__HAL_TIM_GET_IT_SOURCE(htim,TIM_IT_CC1)!=RESET){__HAL_TIM_CLEAR_IT(htim,TIM_IT_CC1);HAL_TIM_OC_DelayElapsedCallback(htim);HAL_TIM_PWM_PulseFinishedCallback(htim);}}}HAL库在调用回调前自动清除中断标志,用户无需手动清除。
三、D223使用的HAL回调
3.1 定时器周期完成回调
// 用户重写的回调函数voidHAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef*htim){if(htim==&htim12){// TIM12: 1ms系统滴答dio_tim_task();// DIO刷新+PWM加减速}elseif(htim==&htim13){// TIM13: ADC采样定时qspiAdcTimTask();// QSPI DMA读取FPGA FIFO}elseif(htim==&htim14){// TIM14: DAC输出定时ddsDacTimTask();// DDS查表+DAC写入}}一个回调函数处理多个定时器,通过htim实例指针区分。
3.2 QSPI DMA接收完成回调
// QSPI DMA传输完成voidHAL_QSPI_RxCpltCallback(QSPI_HandleTypeDef*hqspi){// DMA已完成66字节FPGA FIFO数据读取// 数据已到达_qspiAdcBuf_adcFunctionParam.qspiDmaComplete=1;// 通知主线程// 在中断中不能做耗时操作// 仅设置标志,由主线程处理数据}3.3 QSPI错误回调
voidHAL_QSPI_ErrorCallback(QSPI_HandleTypeDef*hqspi){// QSPI通信错误(时序错误/超时等)// 记录错误并尝试恢复_adcFunctionParam.qspiErrorCount++;// 重新初始化QSPIHAL_QSPI_DeInit(&hqspi);HAL_QSPI_Init(&hqspi);}3.4 SPI传输完成回调
// SPI1(FPGA寄存器)传输完成voidHAL_SPI_TxCpltCallback(SPI_HandleTypeDef*hspi){if(hspi==&hspi1){fpga_spi_tx_busy=0;// 清除写忙标志HAL_GPIO_WritePin(FPGA_SPI_CS_GPIO_Port,FPGA_SPI_CS_Pin,1);// CS拉高}}// SPI1接收完成voidHAL_SPI_RxCpltCallback(SPI_HandleTypeDef*hspi){if(hspi==&hspi1){fpga_spi_rx_busy=0;// 清除读忙标志HAL_GPIO_WritePin(FPGA_SPI_CS_GPIO_Port,FPGA_SPI_CS_Pin,1);// CS拉高}}3.5 USB CDC接收回调
// USB CDC数据接收完成staticint8_tCDC_Receive_FS(uint8_t*Buf,uint32_t*Len){// 数据已到达Buf,长度为*Lenmemcpy(usbRxBuf,Buf,*Len);usbMsg_rxLen=*Len;usbMsg_rxComplete=1;// 通知消息处理线程// 重新启动接收USBD_CDC_SetRxBuffer(&hUsbDeviceFS,Buf);USBD_CDC_ReceivePacket(&hUsbDeviceFS);returnUSBD_OK;}四、__weak回调机制
4.1 weak关键字
HAL库使用__weak关键字定义回调函数。用户可以重写同名函数覆盖weak版本。
// HAL库中的weak定义(stm32h7xx_hal_tim.c)__weakvoidHAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef*htim){// 空实现,什么都不做UNUSED(htim);}// 用户代码中的重写(非weak)voidHAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef*htim){// 实际处理逻辑if(htim==&htim13)qspiAdcTimTask();}__weak函数的优先级低于非__weak函数。链接器优先使用用户的实现,如果用户没有实现,则使用HAL库的空实现。
4.2 回调注册
某些HAL外设支持动态注册回调:
// 方式1:重写__weak函数(D223使用)voidHAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef*htim){...}// 方式2:动态注册(HAL新版本支持)HAL_TIM_RegisterCallback(&htim13,HAL_TIM_PERIOD_ELAPSED_CB_ID,myCallback);D223使用方式1(重写__weak函数),因为更直观且兼容性好。
五、HAL状态机
5.1 外设状态枚举
typedefenum{HAL_STATE_RESET=0x00,// 未初始化HAL_STATE_READY=0x01,// 就绪HAL_STATE_BUSY=0x02,// 忙(传输中)HAL_STATE_TIMEOUT=0x03,// 超时HAL_STATE_ERROR=0x04// 错误}HAL_StatusTypeDef;5.2 QSPI状态转换
HAL_QSPI_Command() → hqspi.State = HAL_QSPI_STATE_BUSY → 配置命令寄存器 → hqspi.State = HAL_QSPI_STATE_READY HAL_QSPI_Receive_DMA() → hqspi.State = HAL_QSPI_STATE_BUSY → 启动DMA → DMA完成中断 → HAL_QSPI_RxCpltCallback() → hqspi.State = HAL_QSPI_STATE_READY5.3 状态检查
// 使用前检查状态if(hqspi.State==HAL_QSPI_STATE_READY){HAL_QSPI_Receive_DMA(&hqspi,buffer);// 可以启动新传输}else{// 上一次传输未完成,等待}六、回调执行上下文
6.1 在中断上下文执行
HAL回调在中断上下文中执行,不能调用阻塞函数。
// ❌ 错误:中断回调中调用阻塞函数voidHAL_QSPI_RxCpltCallback(QSPI_HandleTypeDef*hqspi){HAL_Delay(100);// ❌ 死循环(SysTick优先级低)rt_thread_delay(2);// ❌ 死锁(中断不能调度线程)}// ✅ 正确:只设标志位voidHAL_QSPI_RxCpltCallback(QSPI_HandleTypeDef*hqspi){_adcFunctionParam.qspiDmaComplete=1;// ✅ 设置标志}6.2 中断中可安全调用的函数
| 函数 | 可安全调用 | 说明 |
|---|---|---|
| 设置标志位 | ✅ | 最安全 |
| 读取寄存器 | ✅ | 快速操作 |
| HAL_GPIO_WritePin | ✅ | 直接寄存器操作 |
| HAL_Delay | ❌ | 依赖SysTick中断 |
| rt_thread_delay | ❌ | 需要调度器 |
| HAL_SPI_Transmit | ⚠️ | 阻塞但可短时使用 |
| memcpy | ✅ | 纯内存操作 |
七、D223回调总结
| 回调函数 | 触发源 | 优先级 | 用户处理 | 耗时 |
|---|---|---|---|---|
| HAL_TIM_PeriodElapsedCallback | TIM12 | 6 | dio_tim_task() | ~5μs |
| HAL_TIM_PeriodElapsedCallback | TIM13 | 5 | qspiAdcTimTask() | ~2μs |
| HAL_TIM_PeriodElapsedCallback | TIM14 | 5 | ddsDacTimTask() | ~3μs |
| HAL_QSPI_RxCpltCallback | DMA1 S5 | 4 | 设标志位 | <1μs |
| HAL_SPI_TxCpltCallback | SPI1 | 7 | CS拉高+清标志 | <1μs |
| HAL_SPI_RxCpltCallback | SPI1 | 7 | CS拉高+清标志 | <1μs |
| CDC_Receive_FS | USB | 3 | 设标志+重启接收 | <2μs |
| HAL_QSPI_ErrorCallback | QSPI | 4 | 重新初始化 | ~10μs |
所有回调执行时间都在10μs以内,确保不影响其他中断和主线程。
八、回调函数调试技巧
8.1 在回调中设置断点
voidHAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef*htim){if(htim==&htim13){__asmvolatile("NOP");// 设置断点在这里qspiAdcTimTask();}}8.2 使用ITM打印
voidHAL_QSPI_RxCpltCallback(QSPI_HandleTypeDef*hqspi){ITM_SendChar('D');// 通过ITM输出调试字符_adcFunctionParam.qspiDmaComplete=1;}8.3 使用GPIO翻转测量时间
voidHAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef*htim){if(htim==&htim13){HAL_GPIO_WritePin(DBG_GPIO_Port,DBG_Pin,1);// 拉高qspiAdcTimTask();HAL_GPIO_WritePin(DBG_GPIO_Port,DBG_Pin,0);// 拉低// 用示波器测量高电平时间 = 回调执行时间}}九、HAL回调 vs LL库
9.1 HAL vs LL对比
| 维度 | HAL库 | LL库 |
|---|---|---|
| 抽象层次 | 高 | 低 |
| 代码量 | 大 | 小 |
| 执行效率 | 中 | 高 |
| 回调机制 | ✅ | ❌ |
| 可移植性 | 好 | 一般 |
| D223使用 | ✅ | — |
9.2 D223中的HAL和直接寄存器混用
// 定时器中断中使用HALvoidHAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef*htim){if(htim==&htim13){qspiAdcTimTask();// 内部使用HAL_QSPI_Receive_DMA}}// PWM频率更新中使用直接寄存器voidfun_calcPwmParam(u8 ch,TIM_HandleTypeDef*_pTim){_pTim->Instance->PSC=_dioParam.psc[ch];// 直接写寄存器_pTim->Instance->ARR=_dioParam.arr[ch];_pTim->Instance->CCR1=_dioParam.duty[ch];}在中断中需要最高效率的地方(如PWM参数更新)使用直接寄存器操作,其他地方使用HAL函数。
十、总结
D223的HAL回调机制实现了:
| 设计要点 | 实现 |
|---|---|
| 回调机制 | __weak函数重写 |
| 定时器回调 | HAL_TIM_PeriodElapsedCallback(TIM12/13/14) |
| QSPI DMA回调 | HAL_QSPI_RxCpltCallback(设标志位) |
| SPI回调 | HAL_SPI_TxCpltCallback/RxCpltCallback(CS控制) |
| USB回调 | CDC_Receive_FS(设标志+重启接收) |
| 错误回调 | HAL_QSPI_ErrorCallback(重新初始化) |
| 回调上下文 | 中断上下文(不阻塞) |
| 回调耗时 | <10μs(所有回调) |
| HAL状态机 | BUSY→READY→BUSY循环 |
| 混用策略 | HAL为主+关键路径直接寄存器 |
从硬件中断触发到HAL状态机检测标志位,从清除中断标志到调用__weak回调函数,HAL库的回调机制为D223提供了统一的硬件事件处理框架。用户只需重写回调函数并判断htim实例,即可在统一的框架下处理所有外设事件。
下篇预告:《C#上位机帧解析引擎深度解析:从字节流到结构体》