一、问题现象:逻辑分析仪正常,单片机却丢数据
在实际开发中,我们经常会遇到这样的情况:
- 逻辑分析仪上显示所有字节都完整发送,没有丢失
- 但单片机的接收缓冲区中却少了一个或多个字节
- 这种情况通常发生在连续高速数据传输时
例如,逻辑分析仪显示的完整数据为: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位停止位),因此字节到达间隔为:
plaintext
10 bits / 115200 bps ≈ 86.8 μs
也就是说,每86.8微秒就会有一个新的字节到达串口。
2. 高优先级中断抢占CPU
如果系统中存在更高优先级的中断,并且该中断的服务时间较长,就会导致串口接收中断无法及时响应。
例如,一个高优先级的中断服务时间为5ms,在这5ms内会有:
plaintext
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. 优化中断服务函数
将中断服务函数的时间尽量缩短,只做必要的操作,将复杂的处理逻辑放到主循环或任务中。
c
// 错误的中断服务函数(时间过长)
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的负担,避免数据丢失。
c
// 初始化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. 使用环形缓冲区
使用环形缓冲区可以有效解决数据丢失问题,即使中断响应不及时,也可以缓存更多的数据。
c
// 环形缓冲区结构
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. 提高串口中断优先级
如果串口中断的优先级较低,会被其他高优先级中断抢占,导致数据丢失。可以通过提高串口中断优先级来解决这个问题。
c
// 配置中断优先级
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传输、采用环形缓冲区以及合理配置中断优先级。在实际项目中,建议根据具体应用场景和硬件资源,综合运用这些方法,确保串口通信的稳定可靠。