Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug] [stm32][uart v2]DMA RX接收拆包问题 #9533

Open
wdfk-prog opened this issue Oct 15, 2024 · 10 comments
Open

[Bug] [stm32][uart v2]DMA RX接收拆包问题 #9533

wdfk-prog opened this issue Oct 15, 2024 · 10 comments

Comments

@wdfk-prog
Copy link
Contributor

RT-Thread Version

master

Hardware Type/Architectures

STM32F407VGT6

Develop Toolchain

RT-Thread Studio

Describe the bug

#define BSP_USING_UART3
#define BSP_UART3_RX_USING_DMA
#define BSP_UART3_TX_USING_DMA
#define BSP_UART3_RX_BUFSIZE 256
#define BSP_UART3_TX_BUFSIZE 256
  • 复现步骤: 使用链接所示示例进行通讯;每次发送50字节,第3次发送时,拆包
    https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/uart/uart_v2/uart?id=%e4%b8%b2%e5%8f%a3%e8%ae%be%e5%a4%87%e4%bd%bf%e7%94%a8%e7%a4%ba%e4%be%8b
  • 原因:UART V2 DMA RX 开启了空闲中断+DMA全满半满中断;
    前两次仅触发空闲中断;第三次触发半满中断后->执行了回调函数唤醒线程接收数据;但是此时数据并未接收完成,导致又进入了空闲中断重复步骤,再次唤醒线程接收数据;导致接收的数据拆包断帧;
  • 观察同样原理实现的STM32自带的HAL_UARTEx_ReceiveToIdle_DMA,并没有对DMA全满半满做出处理;
  • 所以同样,uartV2的DMA RX实现,也不应该对半满全满中断做处理;
  • 之所以有做半满全满中断的处理;我认为是从uartV1借鉴过来的;uartV1没有开启空闲中断,是自行在半满全满执行数据拷贝;开启空闲中断后,不需要执行这两个步骤;
  • 解决方法
    方法1:注释掉uartV2的全满半满处理逻辑
    方法2:uartV2按照uartV1实现的DMA方式实现

Other additional context

No response

@milo-9
Copy link
Contributor

milo-9 commented Oct 25, 2024

这些东西不是bug,是使用者错误使用导致的。没有人保证过dma接收应该是怎么样的,serial就只是一个流而已,怎么处理和拆分数据是上层的事情。如果dma不开启满或者半满中断,那缓冲区不够怎么办?当然要及时通知上层把数据取走。

@wdfk-prog
Copy link
Contributor Author

这些东西不是bug,是使用者错误使用导致的。没有人保证过dma接收应该是怎么样的,serial就只是一个流而已,怎么处理和拆分数据是上层的事情。如果dma不开启满或者半满中断,那缓冲区不够怎么办?当然要及时通知上层把数据取走。

  • 主要是uart v2传入上层的情况只有,rx接收完成的标志;
  • 所以要么stm32 uart驱动内部自行实现,半满和全满的拷贝;并触发接收完成;那就跟uartv1逻辑是一样的
  • 目前是开了空闲中断,如果不屏蔽半满和全满的逻辑;会导致重复触发rx接收完成标志;导致接收数据的异常重复

@milo-9
Copy link
Contributor

milo-9 commented Oct 26, 2024

这些东西不是bug,是使用者错误使用导致的。没有人保证过dma接收应该是怎么样的,serial就只是一个流而已,怎么处理和拆分数据是上层的事情。如果dma不开启满或者半满中断,那缓冲区不够怎么办?当然要及时通知上层把数据取走。

  • 主要是uart v2传入上层的情况只有,rx接收完成的标志;
  • 所以要么stm32 uart驱动内部自行实现,半满和全满的拷贝;并触发接收完成;那就跟uartv1逻辑是一样的
  • 目前是开了空闲中断,如果不屏蔽半满和全满的逻辑;会导致重复触发rx接收完成标志;导致接收数据的异常重复

框架层并没有传入什么传入上层的标志,只有一个接收回调,这个回调的逻辑是用户自己编写的。再说接收完成的定义是什么?对于uart,接收到停止位就是接收完成。你所谓的帧这个概念根本不在里面。重复触发rx回调没有什么问题,中断模式下还每个字节都触发一次呢。

@wdfk-prog
Copy link
Contributor Author

这些东西不是bug,是使用者错误使用导致的。没有人保证过dma接收应该是怎么样的,serial就只是一个流而已,怎么处理和拆分数据是上层的事情。如果dma不开启满或者半满中断,那缓冲区不够怎么办?当然要及时通知上层把数据取走。

  • 主要是uart v2传入上层的情况只有,rx接收完成的标志;
  • 所以要么stm32 uart驱动内部自行实现,半满和全满的拷贝;并触发接收完成;那就跟uartv1逻辑是一样的
  • 目前是开了空闲中断,如果不屏蔽半满和全满的逻辑;会导致重复触发rx接收完成标志;导致接收数据的异常重复

框架层并没有传入什么传入上层的标志,只有一个接收回调,这个回调的逻辑是用户自己编写的。再说接收完成的定义是什么?对于uart,接收到停止位就是接收完成。你所谓的帧这个概念根本不在里面。重复触发rx回调没有什么问题,中断模式下还每个字节都触发一次呢。

  • 按照这里的逻辑,每次触发都会执行notify通知;按照原来的逻辑,我得在应用层在实现一次空闲判断逻辑;毕竟一帧数据过来,可能会进入2次或3次notify
  • 那就失去了开启空闲中断的意义了;
        /* Interrupt receive event */
        case RT_SERIAL_EVENT_RX_IND:
        case RT_SERIAL_EVENT_RX_DMADONE:
        {
            struct rt_serial_rx_fifo *rx_fifo;
            rt_size_t rx_length = 0;
            rx_fifo = (struct rt_serial_rx_fifo *)serial->serial_rx;
            rt_base_t level;
            RT_ASSERT(rx_fifo != RT_NULL);

            /* If the event is RT_SERIAL_EVENT_RX_IND, rx_length is equal to 0 */
            rx_length = (event & (~0xff)) >> 8;

            if (rx_length)
            { /* RT_SERIAL_EVENT_RX_DMADONE MODE */
                level = rt_hw_interrupt_disable();
                rt_serial_update_write_index(&(rx_fifo->rb), rx_length);
                rt_hw_interrupt_enable(level);
            }

            /* Get the length of the data from the ringbuffer */
            rx_length = rt_ringbuffer_data_len(&rx_fifo->rb);
            if (rx_length == 0) break;

            if (serial->parent.open_flag & RT_SERIAL_RX_BLOCKING)
            {
                if (rx_fifo->rx_cpt_index && rx_length >= rx_fifo->rx_cpt_index )
                {
                    rx_fifo->rx_cpt_index = 0;
                    rt_completion_done(&(rx_fifo->rx_cpt));
                }
            }
            /* Trigger the receiving completion callback */
            if (serial->parent.rx_indicate != RT_NULL)
                serial->parent.rx_indicate(&(serial->parent), rx_length);

            if (serial->rx_notify.notify)
            {
                serial->rx_notify.notify(serial->rx_notify.dev);
            }
            break;
        }

@milo-9
Copy link
Contributor

milo-9 commented Oct 27, 2024

这些东西不是bug,是使用者错误使用导致的。没有人保证过dma接收应该是怎么样的,serial就只是一个流而已,怎么处理和拆分数据是上层的事情。如果dma不开启满或者半满中断,那缓冲区不够怎么办?当然要及时通知上层把数据取走。

  • 主要是uart v2传入上层的情况只有,rx接收完成的标志;
  • 所以要么stm32 uart驱动内部自行实现,半满和全满的拷贝;并触发接收完成;那就跟uartv1逻辑是一样的
  • 目前是开了空闲中断,如果不屏蔽半满和全满的逻辑;会导致重复触发rx接收完成标志;导致接收数据的异常重复

框架层并没有传入什么传入上层的标志,只有一个接收回调,这个回调的逻辑是用户自己编写的。再说接收完成的定义是什么?对于uart,接收到停止位就是接收完成。你所谓的帧这个概念根本不在里面。重复触发rx回调没有什么问题,中断模式下还每个字节都触发一次呢。

  • 按照这里的逻辑,每次触发都会执行notify通知;按照原来的逻辑,我得在应用层在实现一次空闲判断逻辑;毕竟一帧数据过来,可能会进入2次或3次notify
  • 那就失去了开启空闲中断的意义了;
        /* Interrupt receive event */
        case RT_SERIAL_EVENT_RX_IND:
        case RT_SERIAL_EVENT_RX_DMADONE:
        {
            struct rt_serial_rx_fifo *rx_fifo;
            rt_size_t rx_length = 0;
            rx_fifo = (struct rt_serial_rx_fifo *)serial->serial_rx;
            rt_base_t level;
            RT_ASSERT(rx_fifo != RT_NULL);

            /* If the event is RT_SERIAL_EVENT_RX_IND, rx_length is equal to 0 */
            rx_length = (event & (~0xff)) >> 8;

            if (rx_length)
            { /* RT_SERIAL_EVENT_RX_DMADONE MODE */
                level = rt_hw_interrupt_disable();
                rt_serial_update_write_index(&(rx_fifo->rb), rx_length);
                rt_hw_interrupt_enable(level);
            }

            /* Get the length of the data from the ringbuffer */
            rx_length = rt_ringbuffer_data_len(&rx_fifo->rb);
            if (rx_length == 0) break;

            if (serial->parent.open_flag & RT_SERIAL_RX_BLOCKING)
            {
                if (rx_fifo->rx_cpt_index && rx_length >= rx_fifo->rx_cpt_index )
                {
                    rx_fifo->rx_cpt_index = 0;
                    rt_completion_done(&(rx_fifo->rx_cpt));
                }
            }
            /* Trigger the receiving completion callback */
            if (serial->parent.rx_indicate != RT_NULL)
                serial->parent.rx_indicate(&(serial->parent), rx_length);

            if (serial->rx_notify.notify)
            {
                serial->rx_notify.notify(serial->rx_notify.dev);
            }
            break;
        }

我在一开始就说了,这是你本来就用错了导致的,你所谓的一帧数据为什么不可以多次notify?数据都在环形缓冲区里,notify一次上层就可以消费一次,为什么要管是不是空闲?有谁规定空闲了就是一包数据结束了吗?那么在stm32里的空闲是固定的空闲一个字节,在别的MCU上这个说不定是可设置多少个字节呢?驱动本来就只管收数据,怎么处理数据是你自己去做的,不是驱动来做的。这就是个水管,你要多少水,可以一次性接满,也可以一次一次慢慢接,对结果没有影响。

@aozima
Copy link
Member

aozima commented Oct 27, 2024

引个题外话

最近做的应用,UART每接收到1个字节,自动记录当前精确时间戳,纳秒级。以供应用做细节处理。
如果在UART接收中断里面记录时间戳,会受到中断延迟影响。
后面改为使用DMA自动记录,不过波特率只能上到1Mbps。因为要精确时间戳,还不能开FIFO。
现在波特率想上到10Mbps,已经上了FPGA来做接收和同步记录时间戳了。

@wdfk-prog
Copy link
Contributor Author

wdfk-prog commented Oct 28, 2024

这些东西不是bug,是使用者错误使用导致的。没有人保证过dma接收应该是怎么样的,serial就只是一个流而已,怎么处理和拆分数据是上层的事情。如果dma不开启满或者半满中断,那缓冲区不够怎么办?当然要及时通知上层把数据取走。

  • 主要是uart v2传入上层的情况只有,rx接收完成的标志;
  • 所以要么stm32 uart驱动内部自行实现,半满和全满的拷贝;并触发接收完成;那就跟uartv1逻辑是一样的
  • 目前是开了空闲中断,如果不屏蔽半满和全满的逻辑;会导致重复触发rx接收完成标志;导致接收数据的异常重复

框架层并没有传入什么传入上层的标志,只有一个接收回调,这个回调的逻辑是用户自己编写的。再说接收完成的定义是什么?对于uart,接收到停止位就是接收完成。你所谓的帧这个概念根本不在里面。重复触发rx回调没有什么问题,中断模式下还每个字节都触发一次呢。

  • 按照这里的逻辑,每次触发都会执行notify通知;按照原来的逻辑,我得在应用层在实现一次空闲判断逻辑;毕竟一帧数据过来,可能会进入2次或3次notify
  • 那就失去了开启空闲中断的意义了;
        /* Interrupt receive event */
        case RT_SERIAL_EVENT_RX_IND:
        case RT_SERIAL_EVENT_RX_DMADONE:
        {
            struct rt_serial_rx_fifo *rx_fifo;
            rt_size_t rx_length = 0;
            rx_fifo = (struct rt_serial_rx_fifo *)serial->serial_rx;
            rt_base_t level;
            RT_ASSERT(rx_fifo != RT_NULL);

            /* If the event is RT_SERIAL_EVENT_RX_IND, rx_length is equal to 0 */
            rx_length = (event & (~0xff)) >> 8;

            if (rx_length)
            { /* RT_SERIAL_EVENT_RX_DMADONE MODE */
                level = rt_hw_interrupt_disable();
                rt_serial_update_write_index(&(rx_fifo->rb), rx_length);
                rt_hw_interrupt_enable(level);
            }

            /* Get the length of the data from the ringbuffer */
            rx_length = rt_ringbuffer_data_len(&rx_fifo->rb);
            if (rx_length == 0) break;

            if (serial->parent.open_flag & RT_SERIAL_RX_BLOCKING)
            {
                if (rx_fifo->rx_cpt_index && rx_length >= rx_fifo->rx_cpt_index )
                {
                    rx_fifo->rx_cpt_index = 0;
                    rt_completion_done(&(rx_fifo->rx_cpt));
                }
            }
            /* Trigger the receiving completion callback */
            if (serial->parent.rx_indicate != RT_NULL)
                serial->parent.rx_indicate(&(serial->parent), rx_length);

            if (serial->rx_notify.notify)
            {
                serial->rx_notify.notify(serial->rx_notify.dev);
            }
            break;
        }

我在一开始就说了,这是你本来就用错了导致的,你所谓的一帧数据为什么不可以多次notify?数据都在环形缓冲区里,notify一次上层就可以消费一次,为什么要管是不是空闲?有谁规定空闲了就是一包数据结束了吗?那么在stm32里的空闲是固定的空闲一个字节,在别的MCU上这个说不定是可设置多少个字节呢?驱动本来就只管收数据,怎么处理数据是你自己去做的,不是驱动来做的。这就是个水管,你要多少水,可以一次性接满,也可以一次一次慢慢接,对结果没有影响。

  • 你说的是没错的;
  • 但是这样应用程序是不是复杂了?本来我的应用场景下,硬件处理完已经是可以用的了,现在还得应用层再做一次?
  • 或许应该留个选项?

@milo-9
Copy link
Contributor

milo-9 commented Oct 28, 2024

这些东西不是bug,是使用者错误使用导致的。没有人保证过dma接收应该是怎么样的,serial就只是一个流而已,怎么处理和拆分数据是上层的事情。如果dma不开启满或者半满中断,那缓冲区不够怎么办?当然要及时通知上层把数据取走。

  • 主要是uart v2传入上层的情况只有,rx接收完成的标志;
  • 所以要么stm32 uart驱动内部自行实现,半满和全满的拷贝;并触发接收完成;那就跟uartv1逻辑是一样的
  • 目前是开了空闲中断,如果不屏蔽半满和全满的逻辑;会导致重复触发rx接收完成标志;导致接收数据的异常重复

框架层并没有传入什么传入上层的标志,只有一个接收回调,这个回调的逻辑是用户自己编写的。再说接收完成的定义是什么?对于uart,接收到停止位就是接收完成。你所谓的帧这个概念根本不在里面。重复触发rx回调没有什么问题,中断模式下还每个字节都触发一次呢。

  • 按照这里的逻辑,每次触发都会执行notify通知;按照原来的逻辑,我得在应用层在实现一次空闲判断逻辑;毕竟一帧数据过来,可能会进入2次或3次notify
  • 那就失去了开启空闲中断的意义了;
        /* Interrupt receive event */
        case RT_SERIAL_EVENT_RX_IND:
        case RT_SERIAL_EVENT_RX_DMADONE:
        {
            struct rt_serial_rx_fifo *rx_fifo;
            rt_size_t rx_length = 0;
            rx_fifo = (struct rt_serial_rx_fifo *)serial->serial_rx;
            rt_base_t level;
            RT_ASSERT(rx_fifo != RT_NULL);

            /* If the event is RT_SERIAL_EVENT_RX_IND, rx_length is equal to 0 */
            rx_length = (event & (~0xff)) >> 8;

            if (rx_length)
            { /* RT_SERIAL_EVENT_RX_DMADONE MODE */
                level = rt_hw_interrupt_disable();
                rt_serial_update_write_index(&(rx_fifo->rb), rx_length);
                rt_hw_interrupt_enable(level);
            }

            /* Get the length of the data from the ringbuffer */
            rx_length = rt_ringbuffer_data_len(&rx_fifo->rb);
            if (rx_length == 0) break;

            if (serial->parent.open_flag & RT_SERIAL_RX_BLOCKING)
            {
                if (rx_fifo->rx_cpt_index && rx_length >= rx_fifo->rx_cpt_index )
                {
                    rx_fifo->rx_cpt_index = 0;
                    rt_completion_done(&(rx_fifo->rx_cpt));
                }
            }
            /* Trigger the receiving completion callback */
            if (serial->parent.rx_indicate != RT_NULL)
                serial->parent.rx_indicate(&(serial->parent), rx_length);

            if (serial->rx_notify.notify)
            {
                serial->rx_notify.notify(serial->rx_notify.dev);
            }
            break;
        }

我在一开始就说了,这是你本来就用错了导致的,你所谓的一帧数据为什么不可以多次notify?数据都在环形缓冲区里,notify一次上层就可以消费一次,为什么要管是不是空闲?有谁规定空闲了就是一包数据结束了吗?那么在stm32里的空闲是固定的空闲一个字节,在别的MCU上这个说不定是可设置多少个字节呢?驱动本来就只管收数据,怎么处理数据是你自己去做的,不是驱动来做的。这就是个水管,你要多少水,可以一次性接满,也可以一次一次慢慢接,对结果没有影响。

  • 你说的是没错的;
  • 但是这样应用程序是不是复杂了?本来我的应用场景下,硬件处理完已经是可以用的了,现在还得应用层再做一次?
  • 或许应该留个选项?

这样并不是复杂了,这样才是简单了,你那样做是耦合的。项目大了是要使用原则和规范来约束的,而不是怎么方便怎么来。要有分层思想,驱动就是驱动,链路就是链路,应用就是应用,你觉得复杂你可以在驱动上再封装,而不是把你的想法揉进驱动里。

@logeexpluoqi
Copy link

这种流式的数据在接收的时候是不能有帧的概念的,需要通过上层来通过滑窗或者其它的方式来确定帧。
就算强行的让驱动来接收到空闲中断或者接收满中断后通知上层来处理,那么此时接收到的数据也不一定就是一个完整的帧,所以为了稳定,还是需要通过上层的软件来进行帧判断

@yangpengya
Copy link
Contributor

官方库HAL_UARTEx_ReceiveToIdle_DMA的实现是处理了Cplt和HalfCplt的,只是提供了weak回调,应用没有重写这个回调看起来就和没处理一样,而uartV2驱动默认实现了回调,其逻辑是一样的。实现这个回调无非就是想尽快的通知应用把数据取走,避免数据被覆盖,特别是缓存开的小的时候,这是通用的做法。
针对你这种应用场景,想通过空闲中断来判断一帧完整帧,把那个回调屏蔽了就行,只要缓存够大能保证不丢数据就完全没有问题。具体问题具体分析,搞个单片机整那么多名词,况且都是开源软件项目需要咋改就咋改。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants