欢迎光临
我们一直在努力

57.串口数据丢失问题深度解析:从现象到解决方案

一、问题现象:逻辑分析仪正常,单片机却丢数据

在实际开发中,我们经常会遇到这样的情况:

  • 逻辑分析仪上显示所有字节都完整发送,没有丢失
  • 但单片机的接收缓冲区中却少了一个或多个字节
  • 这种情况通常发生在连续高速数据传输时

例如,逻辑分析仪显示的完整数据为:31 7E 04 A2 19 C0 55 8B,但单片机接收到的数据却变成了:31 7E 04 A2 – C0 55 8B,中间少了一个字节。

二、测试条件分析

要解决这个问题,首先需要明确测试条件:

  • 波特率:115200
  • 数据位:8位
  • 校验位:无
  • 停止位:1位
  • 传输方式:连续发送,无数据间隔
  • 是否使用DMA:未使用

三、问题原因分析

1. 波特率与字节到达间隔

115200波特率下,每个字节包含10位(1位起始位 + 8位数据位 + 1位停止位),因此字节到达间隔为:

10 bits / 115200 bps ≈ 86.8 μs

也就是说,每86.8微秒就会有一个新的字节到达串口。

2. 高优先级中断抢占CPU

如果系统中存在更高优先级的中断,并且该中断的服务时间较长,就会导致串口接收中断无法及时响应。

例如,一个高优先级的中断服务时间为5ms,在这5ms内会有:

5ms / 86.8μs ≈ 57个字节

到达串口。但很多单片机的串口只有一个接收寄存器,或者硬件缓冲区很浅,无法缓存这么多字节。

3. 硬件缓冲区溢出

当旧数据没有被及时取走,新的字节又到达时,硬件缓冲区就会溢出。此时,硬件会置位接收溢出标志(ORE/Overrun),后续的字节就会丢失。

例如,串口硬件缓冲区只有4个字节深度,当第5个字节到达时,缓冲区已满,第5个字节就会丢失。

四、判断是否会丢数据的方法

要判断是否会丢数据,需要比较两个时间量:

  • 最长不可服务时间:系统中最长的中断服务时间
  • 硬件可缓存时间:硬件缓冲区深度 × 字节到达间隔
  • 如果最长不可服务时间 > 硬件可缓存时间,就会发生数据丢失。

    例如:

    • 最长不可服务时间:5ms
    • 硬件缓冲区深度:4字节
    • 硬件可缓存时间:4 × 86.8μs = 347.2μs
    • 5ms > 347.2μs,因此会发生数据丢失

    五、解决方案

    1. 优化中断服务函数

    将中断服务函数的时间尽量缩短,只做必要的操作,将复杂的处理逻辑放到主循环或任务中。

    // 错误的中断服务函数(时间过长)
    void USART1_IRQHandler(void) {
    if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
    uint8_t data = USART_ReceiveData(USART1);
    // 复杂的处理逻辑(时间过长)
    process_data(data);
    USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    }
    }

    // 正确的中断服务函数(只做必要操作)
    void USART1_IRQHandler(void) {
    if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
    uint8_t data = USART_ReceiveData(USART1);
    // 只将数据存入环形缓冲区
    ring_buffer_put(&rx_buffer, data);
    USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    }
    }

    2. 使用DMA传输

    对于连续高速数据传输,优先使用DMA(直接存储器访问),这样可以减轻CPU的负担,避免数据丢失。

    // 初始化DMA
    void DMA_Init(void) {
    DMA_InitTypeDef DMA_InitStructure;

    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

    DMA_DeInit(DMA1_Channel5);
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)rx_dma_buffer;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
    DMA_InitStructure.DMA_BufferSize = RX_BUFFER_SIZE;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
    DMA_Init(DMA1_Channel5, &DMA_InitStructure);

    DMA_Cmd(DMA1_Channel5, ENABLE);
    }

    // 主循环中处理数据
    int main(void) {
    // 初始化
    USART_Init();
    DMA_Init();

    while (1) {
    // 处理DMA接收到的数据
    process_dma_data();
    }
    }

    3. 使用环形缓冲区

    使用环形缓冲区可以有效解决数据丢失问题,即使中断响应不及时,也可以缓存更多的数据。

    // 环形缓冲区结构
    typedef struct {
    uint8_t *buffer;
    uint32_t size;
    uint32_t head;
    uint32_t tail;
    uint32_t count;
    } RingBuffer;

    // 初始化环形缓冲区
    void ring_buffer_init(RingBuffer *rb, uint8_t *buffer, uint32_t size) {
    rb->buffer = buffer;
    rb->size = size;
    rb->head = 0;
    rb->tail = 0;
    rb->count = 0;
    }

    // 向环形缓冲区写入数据
    uint8_t ring_buffer_put(RingBuffer *rb, uint8_t data) {
    if (rb->count >= rb->size) {
    return 0; // 缓冲区已满
    }
    rb->buffer[rb->tail] = data;
    rb->tail = (rb->tail + 1) % rb->size;
    rb->count++;
    return 1;
    }

    // 从环形缓冲区读取数据
    uint8_t ring_buffer_get(RingBuffer *rb, uint8_t *data) {
    if (rb->count == 0) {
    return 0; // 缓冲区为空
    }
    *data = rb->buffer[rb->head];
    rb->head = (rb->head + 1) % rb->size;
    rb->count–;
    return 1;
    }

    // 定义环形缓冲区
    #define RX_BUFFER_SIZE 64
    uint8_t rx_buffer[RX_BUFFER_SIZE];
    RingBuffer rx_ring_buffer;

    // 初始化
    ring_buffer_init(&rx_ring_buffer, rx_buffer, RX_BUFFER_SIZE);

    4. 提高串口中断优先级

    如果串口中断的优先级较低,会被其他高优先级中断抢占,导致数据丢失。可以通过提高串口中断优先级来解决这个问题。

    // 配置中断优先级
    void NVIC_Config(void) {
    NVIC_InitTypeDef NVIC_InitStructure;

    // 配置串口中断优先级
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 最高优先级
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    // 配置其他中断的优先级(较低)
    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
    }

    六、总结

    串口数据丢失是一个常见但可预防的问题。通过分析字节到达间隔、中断服务时间和硬件缓冲区深度之间的关系,可以准确判断数据丢失的风险。解决方案包括优化中断服务函数、使用DMA传输、采用环形缓冲区以及合理配置中断优先级。在实际项目中,建议根据具体应用场景和硬件资源,综合运用这些方法,确保串口通信的稳定可靠。

     

    赞(0)
    未经允许不得转载:171主机测评 » 57.串口数据丢失问题深度解析:从现象到解决方案
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址