C:从环形缓冲区提取帧数据

嵌入式平台如何从数据流中提取一帧数据?

通过实例来说明问题:

A作为主机(上位机软件),B作为从机(STM32)硬件和电机连接,A和B之间通过串口通讯,使A能间接的控制电机正反转,速度等。

协议初定:

问题一:怎么判断帧数据开始信息?

通过定义一个固定的帧头,作为数据开始信号。记为0x01。

问题二:如何判断帧的长度?

帧的长度可以选择固定长度或可变长度。固定长度逻辑简单,检测到帧头后,往后读取固定的长度即可。可变长度灵活度高,一帧数据可以携带更多的信息量。那么可以在帧头后增加一个字节作为长度,记为0xXX,以便从机判断帧长。

问题三:数据格式?

数据格式按需编写,这里用16bit功能+16bit数据的方式。一组数据共4个字节。

问题四:如何校验帧完整性?

可以用两种方式:
1、增加帧尾作为数据结束标志,验证帧尾就知道数据是否完整。但是存在数据=帧尾、通讯异常的情况,这时候可能提取到了错误的数据。
2、采用通用校验方式,工业控制一般使用CRC16-MODBUS。这里增加两个字节作为检验码。这种方式可以保证数据不会出错。

至此:我们可以将通讯协议制定下来:

帧头帧长功能1数据1功能2数据2功能…数据…CRC16高位CRC16低位
0x01len0xXX0xXX0xXX0xXX0xXX0xXXCRC_HCRC_L

在理想状态下,从机在串口中断里面判断帧头,然后循环往后读取len长度的字节到数组里面去,最后验证一下CRC是否一致即可。


但实际使用往往面对更复杂的情形:

问题五:如果一帧数据没有提取完成,下一帧数据又过来了怎么处理?

这个问题其实就是要解决两件事情:什么时候接收数据?什么时候处理数据?
假设提取数据的函数为:UserExtractFrameData();该函数执行时间为1us。
那么由简入繁看看几种方式:

1、中断接收中断处理。(弊端太明显不再赘述)
2、中断保存到buff,中断外处理buff,处理完后buff清空。

uint8_t RecvBuff[50];
uint8_t RecvIndex=0;
void USART1_IRQHandler(void)                	//串口1中断服务函数
{
	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
	{
		RecvIndex ++;
		RecvBuff[RecvIndex] = (uint8_t)USART_ReceiveData(USART1);
		USART_ClearITPendingBit(USART1,USART_IT_RXNE);
	} 
}
void UserExtractFrameData()
{
	if(RecvIndex > FRAME_LEN)
	{
		...
		RecvIndex = 0;
	}
}

当传输速率>UserExtractFrameData()处理速度时,必然会导致丢数据。因为RecvIndex被清0了,这篇文章重点不是这个,这里不再赘述。

3、使用环形缓冲区。
环形缓冲区原理网上能找到很多,具体实现方式可以看这里 C环形缓冲区实现

使用环形缓冲区就可以很好的解决这个问题,将接收和处理彻底解耦,接收数据自动填入缓冲区,处理数据自动从缓冲区读取数据。当瞬间传输速率很大时,数据被保存到缓冲区不会丢失,等到传输空闲时就有足够的时间去处理。

void USART1_IRQHandler(void)                	//串口1中断服务程序
{
	u8 temp;
	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
	{
		temp =USART_ReceiveData(USART1);
		RingBuffWriteByte(temp);
		USART_ClearITPendingBit(USART1,USART_IT_RXNE);
   } 
}
void UserExtractFrameData()
{
   uint8_t len = RingBuffGetLength()
	if(len > FRAME_LEN)
	{
		RingBuffReadData(len,buff);
		....
	}
}

至此通过CRC和缓冲区,我们已经可以保证数据的完整性和正确性了,只需要从缓冲区拿数据出来比对协议就可以了。


那么接下来再分析一下更加复杂的情形:
问题六:假如主机采用的是一对多的方式,那么必然有大量的无效数据和类似数据,如何处理?

处理方式有很多,这里推荐一种字节流的处理方式。
处理思路:从缓冲区复制一个字节,如果是帧头,继续复制一个字节判断长度n,然后继续往后复制一个字节,判断是否是正确的功能码,再然后继续往后复制n个字节,直到判断CRC是否通过。只要遇到错误就读取帧头不处理即可。
无论主机发送多少数据,内存消耗只是环形缓冲区大小+有效数据大小。未检测到帧头前,处理速度>传输速率,所以环形缓冲区的大小可以不用设很大。但是有一种极端情况,就是主机发送的数据含有大量的帧头,且帧头后的功能码检测也是通过的,那就没救了,只能增大缓冲区大小。
经过验证可以高效率提取有效数据。3KB字节中提取最长32byte的有效数据,时间消耗100ms~220ms左右,主要是传输过程占用的时间。内存消耗仅100+32byte。
因为是给别人做的项目,这部分代码就不贴了~

问题七:假设数据长度较长,传输时间>>1us,传输数据到一半时,进入了处理数据的函数,处理函数由于没有接收到len长度的数据而判断为数据不完整错误,如何处理?

这个问题很好解决,我们可以再增加一个缓冲区pbuff[],先将数据搬到pbuff,直到接收到了len长度的数据,再去处理pbuff即可。
但这不是最好的解决方法,假如数据大小是1KB,那么环形缓冲区大小需要>1KB,再来一个pbuff也需要1KB内存,增加了一倍内存消耗。
因此在C环形缓冲区实现这里,我增加两个接口
uint8_t RingBuffCopyByte(uint8_t *pdata);
uint8_t RingBuffCopyData(uint8_t len, uint8_t *pdata);
处理函数将环形缓冲区数据复制到内部进行处理,处理结束后才会读掉n个字节,缺点是程序执行时间有所增大。

  • 2
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
你可以使用环形缓冲区来实现对音频数据读取环形缓冲区是一种循环队列的数据结构,可以在固定大小的缓冲区中循环存储数据。 下面是一个使用C语言实现环形缓冲区读取音频数据的示例代码: ```c #include <stdio.h> #define BUFFER_SIZE 1024 typedef struct { int read_index; int write_index; int count; char buffer[BUFFER_SIZE]; } CircularBuffer; void init_buffer(CircularBuffer* buffer) { buffer->read_index = 0; buffer->write_index = 0; buffer->count = 0; } int is_buffer_empty(CircularBuffer* buffer) { return buffer->count == 0; } int is_buffer_full(CircularBuffer* buffer) { return buffer->count == BUFFER_SIZE; } void write_to_buffer(CircularBuffer* buffer, char data) { if (!is_buffer_full(buffer)) { buffer->buffer[buffer->write_index] = data; buffer->write_index = (buffer->write_index + 1) % BUFFER_SIZE; buffer->count++; } } char read_from_buffer(CircularBuffer* buffer) { char data = '\0'; if (!is_buffer_empty(buffer)) { data = buffer->buffer[buffer->read_index]; buffer->read_index = (buffer->read_index + 1) % BUFFER_SIZE; buffer->count--; } return data; } int main() { CircularBuffer buffer; init_buffer(&buffer); // 模拟向缓冲区写入音频数据 for (int i = 0; i < BUFFER_SIZE; i++) { write_to_buffer(&buffer, 'A' + i); } // 从缓冲区读取音频数据并打印 while (!is_buffer_empty(&buffer)) { char data = read_from_buffer(&buffer); printf("%c ", data); } return 0; } ``` 在这个示例代码中,我们定义了一个名为`CircularBuffer`的结构体,包含读取索引、写入索引、缓冲区数据个数和实际缓冲区。我们提供了一些用于初始化、判断缓冲区状态、写入和读取数据函数。 你可以根据自己的需求修改缓冲区大小和数据类型。这个示例代码仅仅是一个基本实现,你可能需要根据实际情况进行扩展和优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值