RTThread使用DMA串口接收数据不连续的问题

RTThread使用DMA接收串口数据的问题

问题/现象

使用RTThread的DMA接收串口数据,数据不连续,即IDLE中断没有起到作为一个frame的判定.

经过对serial和drv_uarts源码的分析,得出原因:

graph RX_INT[USART1_IRQHandler] -->process1(...) process1 -->rx_isr1[dma_recv_isr] rx_isr1 -->flag1{isr_flag} flag1 -->|UART_RX_DMA_IT_IDLE_FLAG| serial_isr1[rt_hw_serial_isr] serial_isr1 -->event{event} DMA_INT[UART1_DMA_RX_IRQHandler] -->process2(...) process2 -->rx_isr2[dma_recv_isr] rx_isr2 -->flag2{isr_flag} flag2 -->|UART_RX_DMA_IT_HT_FLAG| serial_isr2[rt_hw_serial_isr] flag2 -->|UART_RX_DMA_IT_TC_FLAG| serial_isr2[rt_hw_serial_isr] serial_isr2 -->event{event} event -->|RT_SERIAL_EVENT_RX_DMADONE|do_something(...)

从上图可知,发生IDLE中断时,USART1_IRQHandler调用的是和UART1_DMA_RX_IRQHandler相同的接口 —— rt_hw_serial_isr.

这就造成无法区分是IDLE中断还是DMA中断.


不改变源码的情况下,仍使用DMA+IDLE中断,目前这两种方式是比较好的

解决方式①

  • 接收数据
/* 接收数据回调函数 */
static rt_err_t uart2_input(rt_device_t dev, rt_size_t size)
{
    rt_err_t result;
    result = rt_sem_release(&serial2_sem);/*通知serial_thread_entry线程,有数据了*/
    if ( result == RT_EOK)
    {
        rt_kprintf("sem release error!\n");
    }
    return result;
}

static void serial_thread_entry(void *parameter)
{
    rt_err_t result;
    rt_uint8_t c = 0;
    rt_uint8_t i = 0, rx_state = SERIAL2_STATE_WAIT_FRAME;
    rt_device_t serial2 = rt_device_find("uart2");
    struct frame_msg msg;
    while (1) {
        /* 使用信号量触发接收数据 */
        switch (rx_state) {
            case SERIAL2_STATE_WAIT_FRAME: {
                result = rt_sem_take(&serial2_sem, RT_WAITING_FOREVER);
                if (result == RT_EOK) {
                    rt_kprintf("%s: frame start\n", __func__);
                    rx_state = SERIAL2_STATE_RECV_DATA;
                    i = 0;
                }
                break;
            }
            case SERIAL2_STATE_RECV_DATA: {
                /* 读取一段数据 */
                while (rt_device_read(serial2, 0, &c, 1) != 0) {
                    framebuf[i] = c;
                    i++;
                    if (i >= UART2_RB_BUFSZ) {
                        i = 0;
                        rx_state = SERIAL2_STATE_BAD_FRAME;/* 坏帧 */
                        rt_kprintf("%s: frame abandon.\n", __func__);
                        break;
                    }
                }

                result = rt_sem_take(&serial2_sem, 10);/* 等待下一段 */
                if (result == -RT_ETIMEOUT) {
                    /* 给其它线程通信 */
                    framebuf[i] = '\0';
                    msg.data = framebuf;
                    msg.size = i;
                    result = rt_mq_send_wait(&rtc_rx_mq, (void*)&msg, sizeof(struct frame_msg), 10);
                    if (result != RT_EOK) {
                        rt_kprintf("%s: msgqueue send error-[%d]\n", __func__, result);
                    }
                    rx_state = SERIAL2_STATE_WAIT_FRAME;/* 一帧结束 */
                    rt_kprintf("%s: frame end\n", __func__);
                } else {
                    /*接收下一段数据*/
                    rt_kprintf("(%d):%d\n", __LINE__, i);
                }
                break;
            }
#if BUG_FIXED
            case SERIAL2_STATE_BAD_FRAME: {
                /* 读取一段数据 */
                while (rt_device_read(serial2, 0, &c, 1) != 0) {
                    framebuf[i] = c;
                    i++;
                    if (i >= UART2_RB_BUFSZ) {
                        i = 0;
                        rt_kprintf("%s: bad frame.\n", __func__);
                        break;
                    }
                }

                result = rt_sem_take(&serial2_sem, 10);/* 等待下一段 */
                if (result == -RT_ETIMEOUT) {
                    /* 丢弃该帧 */
                    rx_state = SERIAL2_STATE_WAIT_FRAME;/* 一帧结束 */
                    rt_kprintf("%s: bad frame end\n", __func__);
                } else {
                    /*接收下一段数据*/
                    rt_kprintf("(%d):%d\n", __LINE__, i);
                }
                break;
            }
#endif
            default:
                break;
        }
    }
}s

解决方式②

把信号量换成消息队列,邮箱,事件应该也都是可以的.


其它问题

1.局部变量的生存期

代码中的几个局部变量在线程中具有了和线程一样长的生存期周期

life_circle(serial_thread_entry) = life_circle(variable(i))
                                = life_circle(variable(rx_state))
                                = life_circle(variable(serial2))
                                = ...
2.这样是不是意味着thread的stack一直被占用?

嗯,是的.

3.帧的长度与帧的超时判定

帧与帧之间的间隔不能小于10个 OStick ,我这边 OStick=1ms .
上面代码中存在几个问题 (2023.05.04):

① 数据过快.
stack overflow.
② 单帧超过buffer大小 (UART2_RB_BUFSZ)
程序流程异常,这种情况是模拟,恶意注入长数据造成异常
③ 开启了DMA的HT,TC,以及UART的IDLE中断.
这三个中断调用 uart_input() 函数,从而信号量 serial2_sem 会多次+1.这会导致读取完一帧数据后,仍然存在多个sem信号,这就会需要 rt_sem_take(,10) 将这些额外的sem信号消除掉.

  • BUG修复(2023.05.05)

BUG_FIXED 注释的那部分,为主要添加的代码(还有上面一些零碎的逻辑).

  • BUG_FIXED 主要修复了 BUG② ,理想情况下帧间隔是 10 OSTicks,但是因为有信号量 serial2_sem 多次+1的问题,所以,帧间隔将大于 10 OSTicks,为了避免发送的数据帧失效,建议通过实际测试来取值,即帧的发送频率波特率,一帧数据的长度有关系

\[freq(帧的发送频率) = function(波特率,一帧数据的长度) \]

\[freq(帧的发送频率) \propto 波特率 \]

\[freq(帧的发送频率) \propto 1/一帧数据的长度 \]

  • BUG① 的问题是因为没有加下面这部分代码:
if (i >= UART2_RB_BUFSZ) {
   i = 0;
   rt_kprintf("%s: bad frame.\n", __func__);
   break;
}
  • BUG③ 也没办法解决,除非去掉HT,TC中断处理中dma_recv_isr()函数.不过这可能会丢帧,丢数据.

总结

上面的程序已经实现了使用RTThread提供的,串口DMA+IDLE中断的方式,接收一帧数据,并且对'bad frame'的处理,具有了"防御性编程"的意味.
使用时,需要对帧的发送频率进行控制,否则会出现'bad frame'的情况.

建议:使用信号量接收串口数据时,不要用 RT_DEVICE_FLAG_DMA_RX 来处理,因为会触发HT,TC,IDLE事件.会导致uart2_input()回调函数调用多次,然后,信号量的值>1的情况.见上面分析的③.

补充:
RT-Thread的I/O设备之UART设备是有不合理之处的.
1.UART设备在开启 RT_DEVICE_FLAG_DMA_RX 时,启用了HT,TC,IDLE三个事件来触发中断.

posted @ 2023-05-03 23:39  当最后一片树叶落下  阅读(734)  评论(0编辑  收藏  举报