如何使用HAL驱动
GR551x的HAL层驱动的调用流程如图 3所示。
- 图中
浅蓝框内的函数为HAL驱动实现的函数。 - 图中
深蓝框内的操作为开发者需在用户程序中实现的代码。 - 图中
橙色框内的函数为在用户程序中实现的msp(MCU Specific Package)函数。 - 图中
紫色框线内的函数为在中断处理函数中调用的函数。
- 开发者在用户程序文件(main.c或gr55xx_msp.c等)中重写外设PPP的msp函数hal_ppp_msp_init()及hal_ppp_msp_deinit()。
- 如果需要使用中断方式的API,则还需重写相应的回调函数,如hal_ppp_process_cplt_callback()。
- 开发者在用户程序文件中定义外设PPP的句柄(handle),并配置相关参数。
- 开发者调用hal_ppp_init()初始化外设PPP,其中hal_ppp_init()由PPP的驱动文件实现,初始化过程中调用开发者重写的hal_ppp_msp_init() 以初始化PPP使用的GPIO引脚、NVIC中断、DMA通道。
- 对于轮询、中断、DMA三种IO操作方式,各自的调用流程如下:
- 轮询方式下,开发者调用驱动文件中的hal_ppp_process()进行IO操作,操作完成后才会退出当前函数。
- 中断方式下,开发者调用驱动文件中的hal_ppp_process_it()进行IO操作,中断使能后退出当前函数,IO操作会在PPP的中断处理函数PPP_IRQHandler中进行,操作完成时调用开发者重写的回调函数,从而进行事件完成通知。
- DMA方式下,开发者调用驱动文件中的hal_ppp_process_dma()进行IO操作,传输开始后退出当前函数,IO操作由DMA外设实现,操作完成时DMA的中断处理函数DMA_IRQHandler()会调用开发者重写的回调函数,从而进行事件完成通知。
- 外设PPP使用完成时,开发者可调用PPP驱动文件中的hal_ppp_deinit()反初始化外设PPP,恢复相应的寄存器为默认值。反初始化过程中会调用开发者重写的hal_ppp_msp_deinit()以反初始化PPP使用的GPIO引脚、NVIC中断及DMA通道。
下文将对HAL驱动调用流程中的初始化、IO操作、超时检测及错误检查进行详细说明。
HAL驱动初始化
HAL全局初始化
gr55xx_hal.c中提供了若干API接口,可对HAL驱动所使用的SysTick进行初始化和反初始化,从而实现外设驱动轮询方式数据收发的超时检测。
- hal_init():系统启动后可调用该函数实现以下功能:
调用hal_msp_init()初始化时钟、GPIO、中断、DMA等。
- hal_deinit():调用hal_msp_deinit()反初始化时钟、GPIO、中断、DMA等。
外设MSP初始化
在初始化外设PPP的过程中,hal_ppp_init()会调用hal_ppp_msp_init()函数对该外设所使用的GPIO、中断、DMA进行初始化。在反初始化的过程中,则会调用hal_ppp_msp_deinit()。hal_ppp_msp_init()及hal_ppp_msp_deinit()为weak类型的空函数,在实际使用过程中,开发者需根据具体应用重写,这两个函数在驱动中的weak类型的空函数如下:
__weak void hal_ppp_msp_init(ppp_handle_t *p_ppp)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(p_ppp);
}
__weak void hal_ppp_msp_deinit(ppp_handle_t *p_ppp)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(p_ppp);
}
HAL驱动IO操作
HAL驱动对外设的IO读写操作包括三种读写方式:轮询、中断、DMA。
轮询方式
在轮询方式中,数据的读写操作是轮询式的,即读写API会在数据的读写操作完成时才退出。读写成功完成时,读写API返回HAL_OK状态,否则返回HAL_ERROR或HAL_TIMEOUT状态。通过hal_ppp_get_state()及hal_ppp_get_error()可获取具体的状态及错误码。在轮询方式的读写API中,为了避免程序陷入死循环,HAL驱动采用了超时检测机制,可使用API中的timeout参数指定超时时间。
轮询方式读写API的典型定义如下:
hal_status_t hal_ppp_transmit(ppp_handle_t *p_ppp, uint8_t *p_data, uint16_t size, uint32_t timeout)
{
if ((NULL == p_data) || (0U == size))
{
return HAL_ERROR;
}
(…)
while (data processing is running)
{
if (timeout reached)
{
return HAL_TIMEOUT;
}
}
(…)
return HAL_OK;
}
中断方式
在中断方式中,数据的读写操作是非轮询式的,即读写API会在使能读写中断后退出,读写操作将在外设中断处理函数中进行。读写完成时,程序将调用开发者实现的回调函数来通知APP。开发者可以通过hal_ppp_get_state()接口来获取当前的读写状态。
在中断方式中,驱动中主要定义了如下接口函数:
- hal_ppp_process_it():中断方式的读写API。
- hal_ppp_irq_handler():外设PPP的中断处理函数。
- _weak hal_ppp_process_cplt_callback():操作完成时的回调函数,由开发者实现。
- _weak hal_ppp_process_error_callback():操作出错时的回调函数,由开发者实现。
使用中断方式的API接口时,开发者首先需要在gr55xx_it.c中注册中断处理函数hal_ppp_irq_handler(),然后调用hal_ppp_process_it()进行数据读写。
回调函数在外设的HAL驱动中被定义为weak类型,即开发者需自己实现要使用的回调函数,从而在数据读写完成时进行缓冲区内存的释放等操作。
例如,通过中断方式进行UART0读写操作的接口使用示例代码如下:
main.c文件中:
uart_handle_t uart_handle;
int main(void)
{
uart_handle.p_instance = UART0;
uart_handle.init.baud_rate = 115200;
uart_handle.init.data_bits = UART_DATABITS_8;
uart_handle.init.stop_bits = UART_STOPBITS_1;
uart_handle.init.parity = UART_PARITY_NONE;
uart_handle.init.hw_flow_ctrl = UART_HWCONTROL_NONE;
hal_uart_init(&uart_handle);
char *p_tx_buff = “Hello World!\r\n”;
hal_uart_transmit_it(&uart_handle,p_tx_buff,strlen((char*)p_tx_buff));
while (hal_uart_get_state(&uart_handle) != HAL_UART_STATE_READY);
hal_uart_deinit(&uart_handle);
}
void hal_uart_tx_cplt_callback(uart_handle_t *p_uart)
{
(…)
}
void hul_uart_error_callback(uart_handle_t *p_uart)
{
(…)
}
gr55xx_it.c文件中:
void UART0_IRQHandler(void)
{
Hal_uart_irq_handler(&uart_handle);
}
UART0_IRQHandler()也可直接放在main.c中。
DMA方式
在DMA方式中,数据的读写操作也是非轮询式的。在数据读写完成时,中断处理函数调用相应的回调函数通知APP。开发者还可通过hal_ppp_get_state()接口来读取当前读写操作的状态。
在DMA方式中,驱动中主要定义了如下API接口:
- hal_ppp_process_dma():DMA方式的读写API。
- hal_ppp_irq_handler():外设PPP的中断处理函数。
- _weak hal_ppp_process_cplt_callback():操作完成时的回调函数,由开发者实现。
- _weak hal_ppp_process_error_callback():操作出错时的回调函数,由开发者实现。
使用DMA方式的API接口时,开发者首先需注册中断处理函数hal_dma_irq_handler(),对于部分外设还需要注册hal_ppp_irq_handler(),如I2C。然后在hal_ppp_msp_init()中初始化需要使用的DMA通道,最后再调用hal_ppp_process_dma()进行数据读写。
例如,通过DMA方式进行UART0的读写操作,接口使用的示例代码如下:
- main.c文件
uart_handle_t uart_handle; int main(void) { uart_handle.p_instance = UART0; uart_handle.init.baud_rate = 115200; uart_handle.init.data_bits = UART_DATABITS_8; uart_handle.init.stop_bits = UART_STOPBITS_1; uart_handle.init.parity = UART_PARITY_NONE; uart_handle.init.hw_flow_ctrl = UART_HWCONTROL_NONE; hal_uart_init(&uart_handle); char *p_tx_buff = “Hello World!\r\n”; hal_uart_transmit_dma(&uart_handle, p_tx_buff, strlen((char*)p_tx_buff)); while (hal_uart_get_state(&uart_handle) != HAL_UART_STATE_READY); hal_uart_deinit(&uart_handle); } void hal_uart_tx_cplt_callback(uart_handle_t *p_uart) { (…) } void hul_uart_error_callback(uart_handle_t *p_uart) { (…) } - gr55xx_hal_msp.c文件
void hal_uart_msp_init (uart_handle_t *p_uart) { static dma_handle_t hdma_tx; static dma_handle_t hdma_rx; (…) hal_dma_init(&hdma_tx); hal_dma_init(&hdma_rx); __HAL_LINKDMA(p_uart, p_dmatx, hdma_tx); __HAL_LINKDMA(p_uart, p_dmarx, hdma_rx); (…) } - gr55xx_it.c文件
void UART0_IRQHandler(void) { hal_uart_irq_handler(&uart_handle); } void DMA_IRQHandler(void) { Hal_uart_irq_handler(uart_handle.p_dmatx); }
hal_uart_msp_init()和UART0_IRQHandler()均可直接放在main.c中。
HAL驱动超时检测及错误检查
超时检测
在轮询方式的API接口中,HAL驱动通过超时检测来避免程序因为错误状态而进入死循环中。如下为SPI轮询方式发送数据的API接口:
hal_status_t hal_spi_transmit(spi_handle_t *p_spi, uint8_t *p_data, uint32_t length,
uint32_t timeout)
接口中的输入参数timeout为发送数据所需要的最大超时时间。一旦超过指定的超时时间,API接口将返回HAL_TIMEOUT。
HAL驱动中的超时时间在文件gr55xx_hal_def.h中定义,取值范围为0 ~ HAL_MAX_DELAY,其中HAL_MAX_DELAY = 0xFFFFFFFF。具体的超时时间取值及描述如表 10所示:
| 超时时间值 | 描述 |
|---|---|
| 0 |
无超时,不等待,若标志检测条件不满足则立即退出循环。 |
| 1 ~ (HAL_MAX_DELAY - 1) |
超时时间,单位ms。 |
| HAL_MAX_DELAY |
一直循环,直到操作成功完成才退出。 |
此外,在某些情况下,固定长度的超时时间会在某些外设的HAL驱动中使用,例如I2C的busy检测超时时间为25 ms。与API接口输入参数中的超时时间不同,固定长度的超时时间在API内部以宏的方式进行使用,并且不能被修改。
错误检查
为了提高驱动程序的健壮性,避免出现不可预期的错误,HAL驱动中实现了错误检查机制。
- 输入参数有效性检查
在给开发者提供的API函数中,作为输入参数的变量需确保是已定义的且有效的,否则会导致程序崩溃或者进入未定义的状态,因此在HAL驱动中实现了输入参数的有效性检查。
例如,在API函数hal_uart_transmit_it()中会检查输入参数的缓冲区指针和长度的有效性,代码如下:
hal_status_t hal_uart_transmit_it(uart_handle_t *p_uart, uint8_t *p_data, uint16_t size) { /* Check that a Tx process is not already ongoing */ if(HAL_UART_STATE_READY == p_uart->g_state) { if((NULL == p_data ) || (0U == size)) { return HAL_ERROR; } (…) } } - 句柄有效性检查
外设句柄是外设HAL驱动中最重要的参数,其存储着外设驱动的配置参数以及各种运行时变量等,因此在外设的初始化函数hal_ppp_init()中实现外设句柄有效性检查。
例如,在UART的初始化函数hal_uart_init中检查句柄的有效性,代码如下:
hal_status_t hal_uart_init(uart_handle_t *p_uart) { /* Check the UART handle allocation */ if(NULL == p_uart) { return HAL_ERROR; } (…) } - 超时错误检查
在轮询方式的API函数中,需要对操作时间进行检查。一旦超时,需返回超时状态。
例如,UART在轮询方式下接收数据时,检查是否超时,代码如下:
hal_status_t hal_uart_receive(uart_handle_t *p_uart, uint8_t *p_data, uint16_t size, uint32_t timeout) { (…) /* as long as data have to be received */ while(0U < p_uart->rx_xfer_count) { if(HAL_OK != uart_wait_fifo_flag_until_timeout(p_uart, UART_FLAG_FIFO_RFNE, RESET, tickstart, timeout) { return HAL_TIMEOUT; } (…) } (…) }在外设句柄中定义了error_code这一全局变量,用于存储API操作过程中的错误码。因此,在API函数返回HAL_ERROR后,开发者可调用hal_ppp_get_error()函数获取具体的错误类型,例如:
uint32_t hal_uart_get_error(uart_handle_t *p_uart) { return p_uart -> error_code; }
运行时参数检查
HAL驱动提供了输入参数的运行时检查,用于判断输入的参数是否在允许的取值范围内。运行时检查是通过gr_assert_param宏实现的,该宏定义在gr55xx_hal_conf.h头文件中,可通过USE_FULL_ASSERT宏来启用/禁用运行时检查功能。
例如,在I2C的初始化函数中对I2C实例、速度、本机设备地址、设备地址模式等参数进行判断,代码如下:
hal_status_t hal_i2c_init(i2c_handle_t *p_i2c)
{
(…)
/* Check the parameters */
gr_assert_param(IS_I2C_ALL_INSTANCE(p_i2c->instance));
gr_assert_param(IS_I2C_SPEED(p_i2c->init.speed));
gr_assert_param(IS_I2C_OWN_ADDRESS(p_i2c->init.own_address));
gr_assert_param(IS_I2C_ADDRESSING_MODE(p_i2c->init.addressing_mode));
gr_assert_param(IS_I2C_GENERAL_CALL(p_i2c->init.general_call_mode));
(…)
}
如果gr_assert_param宏的判断结果是false,则assert_failed()函数将会被调用,并返回判断出错的文件名及行号,其中的assert_failed()函数需要由开发者进行实现。gr_assert_param宏的定义及实现的代码如下:
#ifdef USE_FULL_ASSERT
/**
* @brief The gr_assert_param macro is used for function's parameterscheck.
* @param expr If expr is false, it calls assert_failed function
* which reports the name of the source file and the source
* line number of the call that failed.
* If expr is true, it returns no value.
* @retval None
*/
#define gr_assert_param(expr) ((expr) ? (void)0U : assert_failed((char *)__FILE__, __LINE__))
/* Exported functions ------------------------------------------------------- */
void assert_failed(char* file, uint32_t line);
#else
#define gr_assert_param(expr) ((void)0U)
#endif /* USE_FULL_ASSERT */