硬件平台:STM32H743 软件平台:Keil 5 采用HAL库+FreeRTOS系统
初始化UART配置
void DEBUG_USART_Config(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_PeriphCLKInitTypeDef RCC_PeriphClkInit;
/* 配置串口2时钟源*/
RCC_PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART2;
RCC_PeriphClkInit.Usart234578ClockSelection = RCC_USART234578CLKSOURCE_D2PCLK1;
HAL_RCCEx_PeriphCLKConfig(&RCC_PeriphClkInit);
/* 使能串口时钟 */
__USART2_CLK_ENABLE();
/* 使能GPIO口时钟 */
__HAL_RCC_GPIOD_CLK_ENABLE();
/* 配置USART2------D5、D6 */
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_6;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
/* 配置USART2 */
Uart2Handle.Instance = USART2;
Uart2Handle.Init.BaudRate = 115200;
Uart2Handle.Init.WordLength = UART_WORDLENGTH_8B;
Uart2Handle.Init.StopBits = UART_STOPBITS_1;
Uart2Handle.Init.Parity = UART_PARITY_NONE;
Uart2Handle.Init.Mode = UART_MODE_TX_RX;
Uart2Handle.Init.HwFlowCtl = UART_HWCONTROL_NONE;
Uart2Handle.Init.OverSampling = UART_OVERSAMPLING_16;
Uart2Handle.Init.OneBitSampling = UART_ONEBIT_SAMPLING_DISABLED;
Uart2Handle.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
HAL_UART_Init(&Uart2Handle);
/*串口2中断初始化--连接ROS */
HAL_NVIC_SetPriority(USART2_IRQn, 6, 0);
HAL_NVIC_EnableIRQ(USART2_IRQn);
/*配置串口接收中断 */
__HAL_UART_ENABLE_IT(&Uart2Handle,UART_IT_RXNE);
}
上述代码也可以用STM32CubeMX自动生成,或者野火例程里面也可以直接复制。如果发现串口HAL函数不可用,很可能并没有引入该库函数。需要在stm327xx_hal_conf.h把注释去掉。
新建任务
此次工程主要使用UART2与工控机的ROS系统进行通信,所以需要新建一个单独的任务完成通信任务。在main.c文件创建任务,在bsp_debug_usart.h函数实现任务。 在main.c先声明ROS通信任务句柄。 利用函数创建任务,任务栈设置为最小空间512字节,任务UartRos_Task的实现不在main.c,所以需要引用头文件#include "./usart/bsp_debug_usart.h" ,不然它找不到对应函数会报错。 在bsp_debug_usart.h需要声明任务函数,这也是为了方便main.c的引用。 在bsp_debug_usart.c实现任务函数的编写,目前还是空的,待会再填充代码。至此,任务创建完成。
中断程序原理剖析
前面我们已经开启了UART2的读取中断。在stm32h7xx_it.c保存着所有外设的中断入口函数,我们需要在这里添加UART2的重点函数。函数不需要声明,HAL库已经声明过了。所以当中断发生后,程序会通过USART2_IRQHandler函数进入到HAL_UART_IRQHandler中断处理函数。 而Uart2Handle不是在该文件声明的,所以需要在文件前面加上extern关键词声明。告诉编译器,这里没有,你去其他地方找找。 HAL_UART_IRQHandler函数位于stm32h7xx_hal_uart.c文件。进入到HAL_UART_IRQHandler函数后,看看内部是如何起作用的。实际上在真正应用中,是不需要理会HAL驱动函数是怎么写的,直接编写回调函数就行了。可是为了更好了解底层驱动是如何运作,还是需要多看看大佬写的东西。 进来之后之后使用READ_REG函数,这个函数看名字就知道是读取寄存器的值。ISR是中断和状态寄存器,也就是中断发生了没?哪个中断发生了?CR1和CR3都是控制寄存器,都是使能一些功能和中断的。 第一句代码就是通过逻辑与、或,看看ISR的PE位、FE位、ORE位、NE位有没有置1,置1代表发生错误了。所以这些位就叫状态标志位。 如果没有发生错误,首先看ISR的RXNE/EXFNE标志位,查看一下数据手册这个标志位干嘛用的。这个标志位主要是表示数据寄存器或RXFIFO里面是否有数据的,由硬件置1,读取操作直接清零,意味着不需要软件清零。 然后看CR1的RXNEIE/RXFNEIE标志位。就是看程序有没有使能接收中断,如果没有使能,那就跳过不处理了。 最后看CR3的RXFTIE位,这个位表示RXFIFO阈值中断使能。 综上,判断语句的内容是,1.使能读取中断或者阈值中断;2.中断标志位置1;3.两个条件同时满足。这次使用的是普通中断,没有涉及到RXFIFO,主要看接收数据寄存器。 判断成功后,利用RxISR函数进行处理。接下来探索一下这个函数来自何方? 这个RxISR看串口结构体声明,这是一个函数指针。 但是在前面设置很明显,并没有对RxISR进行赋值。看一下HAL_UART_Init函数源码也没有发现对RxISR进行赋值。 最后在HAL_UART_Receive_IT函数里面发现了以下代码。根据之前设置的数据长度8位,赋值的是UART_RxISR_8BIT函数。
/**
* @brief RX interrrupt handler for 7 or 8 bits data word length .
* @param huart UART handle.
* @retval None
*/
static void UART_RxISR_8BIT(UART_HandleTypeDef *huart)
{
uint16_t uhMask = huart->Mask;
uint16_t uhdata;
/* Check that a Rx process is ongoing */
if (huart->RxState == HAL_UART_STATE_BUSY_RX)
{
uhdata = (uint16_t) READ_REG(huart->Instance->RDR);
*huart->pRxBuffPtr = (uint8_t)(uhdata & (uint8_t)uhMask);
huart->pRxBuffPtr++;
huart->RxXferCount--;
if (huart->RxXferCount == 0U)
{
/* Disable the UART Parity Error Interrupt and RXNE interrupts */
CLEAR_BIT(huart->Instance->CR1, (USART_CR1_RXNEIE_RXFNEIE | USART_CR1_PEIE));
/* Disable the UART Error Interrupt: (Frame error, noise error, overrun error) */
CLEAR_BIT(huart->Instance->CR3, USART_CR3_EIE);
/* Rx process is completed, restore huart->RxState to Ready */
huart->RxState = HAL_UART_STATE_READY;
/* Clear RxISR function pointer */
huart->RxISR = NULL;
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
/*Call registered Rx complete callback*/
huart->RxCpltCallback(huart);
#else
/*Call legacy weak Rx complete callback*/
HAL_UART_RxCpltCallback(huart);
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */
}
}
else
{
/* Clear RXNE interrupt flag */
__HAL_UART_SEND_REQ(huart, UART_RXDATA_FLUSH_REQUEST);
}
}
看了以上函数代码,就会恍然大悟。原来回调函数的是在这里引用的。weak意味着这是个弱定义函数,意思即我帮你声明定义了这个函数,但是函数内部什么内容都没有。对于使用者来说,需要重新编写这个回调函数,然后就会覆盖掉之前的定义。 可以看到这个弱定义函数里面就一句没啥作用的代码。这是为了防止编译器报错。
特别说明
前面提到每一次中断都会调用RxISR函数,而这个函数就是UART_RxISR_8BIT函数。这个函数有一个小坑。就是它每一次进去都会把中断使能给关掉(为啥这么坏),所以特别注意就是在回调函数还得引用HAL_UART_Receive_IT函数重新使能中断。
整体逻辑
整体逻辑如下图所示,只有第四个函数需要重新定义,其他函数都只需要引用即可。
配置函数修改
在上面的配置函数有配置串口接收中断函数,也就是__HAL_UART_ENABLE_IT(&Uart2Handle,UART_IT_RXNE); 。这句代码在很多教程都会存在。实际上这段代码注释掉也是可以运行的。原因在HAL_UART_Receive_IT函数当中,最后也会开启这个接收中断。
任务函数编写
前面我们已经创建了任务,现在弄懂了原理。就可以开始编写。
extern TaskHandle_t UartRos_Task_Handle; //任务手柄
/* ROS通信任务实现 */
void UartRos_Task(void* parameter)
{
uint8_t aRxBuffer; //接收中断缓冲
while(1)
{
HAL_UART_Receive_IT(&Uart2Handle, (uint8_t *)&aRxBuffer, 1); //触发接收中断
/*获取任务通知,没获取则一直等待;pdTRUE代表退出后通知值执行清0操作*/
ulTaskNotifyTake(pdTRUE,portMAX_DELAY);
HAL_UART_Transmit(&Uart2Handle, (uint8_t *)&aRxBuffer, 1,0xFFFF); //将收到的信息发送出去
}
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
BaseType_t pxHigherPriorityTaskWoken = pdFALSE;
/* 发送通知给Uart_ROS任务 */
vTaskNotifyGiveFromISR((TaskHandle_t)UartRos_Task_Handle, &pxHigherPriorityTaskWoken);
/*如果被唤醒的任务被现在执行的任务优先级高,则需要进行一次任务切换*/
portYIELD_FROM_ISR(pxHigherPriorityTaskWoken);
}
在任务当中,需要存在while(1)死循环,这是FreeRTOS规定的。每一次在初始都调用HAL_UART_Receive_IT函数, 这是为了重新触发接收中断。
ulTaskNotifyTake函数是在等待通知,这个通知来自于回调函数。
而下面回调函数则负责发送通知给该任务,即回调函数不负责处理具体事情,只负责发通知,具体事情在任务当中完成。这也是FreeRTOS建议的,数据处理尽量不要在中断当中去做,因为这会堵塞整个系统的运行,有可能导致系统崩溃。
在该次示例当中,HAL_UART_Receive_IT只接受1个字节数据,然后就进入回调函数。这个可以根据实际情况更改,填写x字节,那就接收x字节后再进入回调函数。
当任务在等待通知时,处于堵塞状态。CPU不会停下来,而是去完成其他任务,这样不会造成CPU资源的浪费。当回调函数发来通知,任务继续运行下去,把收到的数据发送出去。
测试如下
发送的test2,因为每次只接受1个字节数据,所以这里其实是进入了5次回调函数,一次次把接收到的数据发出来,只是速度很快,就像一起发出来的。
|