背景
公司一款产品用到了兆易创新的GD32 系列MCU,具体型号是GD32F470,上面跑的一直是裸机程序,最近手头工作不多,就想给它改造成FreeRTOS版本,改造过程中发现串口出现丢包,原因是ISR收到单个字节数据后,负责拼装成帧的rx task经常因得不到调度而丢数据。
思路
不能在接收任务里拼装每个字节
考虑到每收到一个字节就要唤醒一次rx task,在115200波特率 下貌似不太可行,因为8N1默认配置下,一个字节需要10个波特,那么每秒能传输11520字节,折算下来每个字节仅持续0.087ms,远小于FreeRTOS的最小调度间隔1ms,所以rx task不能处理单个字节,而是处理整帧
在中断回调里拼装字节,在任务里处理整帧
既然不能在任务里拼装字节,那只能在ISR里做了,但串口 是个字节流协议 ,并没有帧起始、帧结尾等定界机制,需要开发人员自己定义,这也是原来裸机代码里在ISR里运行一个状态机的原因
走查裸机代码,发现有个IDLE中断被注释掉了,注释说开启该中断会导致系统卡死,奇怪,那这个中断有啥用?翻看GD32的用户手册(1000多页的那个),明白了,该终端在rx端口连续10个clock都是高电平时触发,而我们的业务程序对串口的使用频率很低,那在串口空闲时可不一直触发嘛。
想到一点,能不能让IDLE中断动态开启和关闭?在收到第一个字节时开启,在收到IDLE中断后再关闭,因为此时对端(Linux系统)肯定已经将数据帧发完了,接下来UART总线会持续很长一段时间的空闲,关闭IDLE中断是合理的,而且也避免系统被卡死。
实际代码
ISR代码
void USART5_IRQHandler(void)
{
if (RESET != usart_interrupt_flag_get(USART5, USART_INT_FLAG_RBNE))
{
if (g_uart5_rx_offset == 0) { //收到frame第一个字节时开启IDLE中断
usart_interrupt_enable(USART5, USART_INT_IDLE);
}
g_uart5_rx_buf[g_uart5_rx_offset++] = (uint8_t)usart_data_receive(USART5);
}
if (RESET != usart_interrupt_flag_get(USART5, USART_INT_FLAG_IDLE))
{
usart_data_receive(USART5); // clear interrupt
usart_interrupt_disable(USART5, USART_INT_IDLE); // 收到IDLE中断说明frame收完,可以关闭IDLE中断了
if (g_uart5_rx_offset > 0)
{
g_uart5_rx_cnt = g_uart5_rx_offset; // 业务app检测到g_uart5_rx_cnt非0就认为收到frame
g_uart5_rx_offset = 0;
}
}
}
接收任务代码
void TaskMain(void *param)
{
while (1)
{
if (g_uart5_rx_cnt > 0)
{
LOG_INFO("UART5 received a %u bytes frame\n", g_uart5_rx_cnt);
if (g_uart5_rx_cnt < 5)
{
g_uart5_rx_cnt = 0;
LOG_ERROR("too short frame!\n");
continue;
}
memcpy(g_uart5_rx_frame, g_uart5_rx_buf, g_uart5_rx_cnt); // 备份rx buf,避免被ISR覆盖
process_test_frame(g_uart5_rx_frame, g_uart5_rx_cnt);
g_uart5_rx_cnt = 0; // 及时清零,避免task重复处理同一个frame
}
}
}
测试效果
实测RTOS版的串口接收任务不再丢包。
总结
以前没写过MCU的串口驱动,所以不知道IDLE中断算不算常识,但从我司的代码情况来看,还是有一些工程师是不知道的,希望能帮助到他们。
————————————————
版权声明:本文为CSDN博主「六个九十度」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/happen23/article/details/163570808
|
|