目录
前文的发送已经很好的实现了发送一个数据,但是接收端仅介绍了每次接收一个字符,如果是双方通信,例如发送端以串口发送如下:
1,125,238,475,359
其中1表示命令号;125表示传感器1的数据,238表示传感器2的数据,475表示传感器3的数据,359表示传感器4的数据。
2,19,45,37,28
其中2表示命令号;19表示设定模块1数据,45表示设定模块2数据,37表示设定模块3数据,28表示设定模块4数据。
可以看到两个命令是不等长的(但是每一个相同命令号的应是等长的)
不等长意味着不能接收到第一个字符进行计数,固定长度后停止。
一、标志位结束法
1、实现原理
在数据尾部加入特殊字符,一般在工业界采用'\r''\n'来作为帧尾,也就是说串口收到数据后进行数组的存储,当连续收到\r\n,则表示该帧结束。
2、代码
cpp
void USART1_IRQHandler(void) //串口1中断服务程序
{
u8 Res;
#if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
OSIntEnter();
#endif
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是0x0d 0x0a结尾)
{
Res =USART_ReceiveData(USART1); //读取接收到的数据
if((USART_RX_STA&0x8000)==0)//接收未完成
{
if(USART_RX_STA&0x4000)//接收到了0x0d
{
if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
else USART_RX_STA|=0x8000; //接收完成了
}
else //还没收到0X0D
{
if(Res==0x0d)USART_RX_STA|=0x4000;
else
{
USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
USART_RX_STA++;
if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收
}
}
}
}
#if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
OSIntExit();
#endif
}
#endif
在主函数判断即可实现。
cpp
if(USART_RX_STA&0x8000)
{
len=USART_RX_STA&0x3fff;//得到此次接收到的数据长度
printf("\r\n您发送的消息为:\r\n\r\n");
for(t=0;t<len;t++)
{
USART_SendData(USART1, USART_RX_BUF[t]);//向串口1发送数据
while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);//等待发送结束
}
USART_RX_STA=0; //一定要清除接收标志
}
3、优缺点
优点:
-
易于解析:
- 固定的结束符 :使用固定的字符如
\r\n
作为帧结束符,可以让接收端程序简单地检测到数据帧的结束,容易实现数据包的分帧与解析。 - 简化的协议设计 :接收端只需要检查收到的数据流中是否出现了
\r\n
,就能知道一帧数据的结尾,不需要复杂的帧长度或校验机制。
- 固定的结束符 :使用固定的字符如
-
广泛兼容性:
- 标准化的结束符 :
\r\n
是许多通信协议中标准的结束符(如ASCII协议、Modbus、Telnet等)。采用此约定的通信系统非常普遍,兼容性好。 - 跨平台支持 :不同的操作系统(如Windows、Linux)和编程环境对
\r\n
的支持也非常广泛,使得协议更具跨平台的兼容性。
- 标准化的结束符 :
-
错误检测能力:
- 如果在数据流中间收到了
\r\n
,而没有完全接收到完整的帧,接收端可以利用这种信息判断数据不完整或者错误,增加了一定的容错能力。
- 如果在数据流中间收到了
-
便于调试和查看:
- 在一些串口调试工具中,
\r\n
作为帧尾,可以方便地让开发人员快速识别数据帧的结束,增强了调试过程的可视性。
- 在一些串口调试工具中,
缺点:
-
数据中可能出现特殊字符:
- 数据内容与帧尾冲突 :如果数据本身有
\r
或\n
字符(比如文本数据或二进制数据),会导致接收端错误地认为数据帧结束,从而导致错误的帧解析。虽然可以使用转义字符或特殊的约定(如用某些分隔符代替\r\n
),但这种方法可能增加协议的复杂性。
- 数据内容与帧尾冲突 :如果数据本身有
-
数据帧的长度不定:
- 使用
\r\n
作为帧结束符时,每个数据帧的长度可能不一样,造成接收端需要动态调整内存或缓冲区来存储不同长度的数据帧。对于高数据速率或大数据量传输的情况,可能需要更精确的控制和优化。
- 使用
-
性能问题:
- 逐字符处理 :如果数据量较大,每次接收时需要逐字符检测是否遇到
\r\n
,这样会增加CPU负担,特别是在高数据速率下。 - 帧边界检测开销 :每次检测
\r\n
的过程可能会影响实时性,尤其是在嵌入式系统中,可能需要额外的处理时间来确认是否达到了帧尾。
- 逐字符处理 :如果数据量较大,每次接收时需要逐字符检测是否遇到
-
不适合二进制数据:
- 如果传输的是二进制数据流,
\r\n
可能与数据中的其他字节重叠,导致误判帧的结束。这种情况下需要采用其他更可靠的帧分隔符,如固定长度或基于长度的协议。
- 如果传输的是二进制数据流,
-
对实时性要求较高的系统不太适用:
- 对于实时性要求较高的系统,帧的结束符可能导致额外的延迟,尤其是在接收到大数据流时,可能需要等待完整的
\r\n
符号才能解析一帧数据,造成一定的延迟。
- 对于实时性要求较高的系统,帧的结束符可能导致额外的延迟,尤其是在接收到大数据流时,可能需要等待完整的
总结:
使用 \r\n
作为帧结束符有许多优点,如简单易用、协议兼容性好和易于调试。但也有一些缺点,主要是在数据帧长度不确定、数据中可能出现\r
或\n
字符,以及性能和实时性问题。
在设计串口通信协议时,使用\r\n
作为帧尾是一个不错的选择,尤其适合字符型数据和简单的协议,但对于高吞吐量、大数据量或包含二进制数据的通信场景,可能需要使用更精确的帧分隔方案(如固定长度帧或基于长度的协议)。
二、串口空闲中断
比如给上位机给单片机一次性发送了8个字节,就会产生8次RXNE中断,1次IDLE中断。
空闲态中断会在串口接收线处于空闲状态一段时间时触发。结合空闲态中断,接收端可以基于空闲时间来判断帧的结束。
cpp
// 使能串口接收中断
USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE);
// 使能串口空闲中断
USART_ITConfig(DEBUG_USARTx, USART_IT_IDLE, ENABLE);
1、中断读取
可以看到在总中断中判断当前是接收中断还是空闲中断,如果是空闲中断则单次数据结束。此处可以添加一个flag标志,在main中判断即可。
cpp
// 串口中断服务函数
void DEBUG_USART_IRQHandler(void)
{
if(USART_GetITStatus(DEBUG_USARTx,USART_IT_RXNE)!=RESET)
{
test_data.data[test_data.len] = USART_ReceiveData(DEBUG_USARTx);
if(++test_data.len > data_size - 2) //让第 data_size - 1 位置一直是空终止位
{
test_data.len = 0; //演示从简处理,如果接收超过尺寸从头开始覆盖
}
}
if(USART_GetITStatus(DEBUG_USARTx,USART_IT_IDLE)!=RESET)
{
USART_ReceiveData(DEBUG_USARTx); //查阅参考手册 软件序列清除标志位流程
test_data.data[test_data.len] = '\0'; //以空闲中断认为接收完成 设置空终止位
test_data.flag = 1; //赋值标志作为程序判断是否完成
}
}
2、DMA接收
可以看到在总中断中判断当前是否空闲中断,如果是空闲中断则单次数据结束。此处可以添加一个flag标志,在main中判断即可。如果空闲则把接收数据做个处理或输出一个标志。
cpp
void USART1 IROHandler(void)
{
uint8 ClearFlag;
uint16 count =0:
//处理接收空闲中断
if(USART_GetITStatuS(USART1,USART_IT_IDLE)!=RESET)
{
//清除标志:先读SR,再读DR
ClearFlag = USART1->SR;
ClearFlag = USART1->DR;
/*获取接收数据个数 */
count = DMA_GetCurrDataCounter(DMA1 Channe15);
Touch_tx_flag=1024-count; //已收到多少个数据
/*处理数据,将DMA搬运的数据 复制到发送处 当然不用发送就不用写这个了*/
memcpy((uint8 t *)Touch tx buffer,(uint8 t *)Touch rx buffer,Touch tx flag);
/*DMA接收复位 */
DMA_Cmd(DMA1_Channel5,DISABLE);
DMA_SetCurrDatacounter(DMA1_channe15,1024);
DMA_Cmd(DMA1_Channe15,ENABLE);
}
}
在main中,记得清除flag等。
3、优缺点
优点:
-
无额外字符干扰:
- 不需要额外添加特殊字符(如
\r\n
)来标识数据帧的结束,因此对于数据中可能包含这些特殊字符的情况(如文本或二进制数据流),不会发生干扰。
- 不需要额外添加特殊字符(如
-
实时性较好:
- 空闲中断通常会在数据接收完成后触发,能够实时感知数据传输的结束,避免了轮询和等待帧结束标志的时间延迟。
-
适用于二进制数据:
- 该方法适合于包含二进制数据的协议,因为它不依赖于数据内容,可以避免数据帧中包含分隔符或特殊字符的冲突。
缺点:
-
可能出现误判:
- 如果数据传输的速度非常慢,或者接收的数据量小且间隔较长,空闲时间过长可能会被误判为一帧数据结束。例如,如果数据传输中有较长的静默期,空闲中断可能会提前触发。
-
空闲检测延迟:
- 空闲状态的判断是基于一定的时间窗口(如几百微秒到几毫秒),如果数据帧没有及时接收完整,可能会导致检测到空闲状态后才处理数据,增加处理延迟。
-
硬件资源消耗:
- 空闲中断需要硬件中断资源,如果系统中有多个串口或者大量的空闲中断,可能会增加中断处理的复杂性,影响系统的实时性。
-
实时性较低:
- 由于空闲状态是基于"静默期"判断的,它的实时性可能比其他基于定时器或流控制的方案要低。尤其是高速数据流时,空闲中断可能没有办法及时捕获数据帧结束的时机。
三、定时器空闲超时
1、实现原理
可以看到串口空闲虽好,但是无法调节时间,因此可以和定时器进行结合。例如如果收到一次数据且进入了空闲中断或接收中断(可自行选择),idle_timeout_count开始递减至0,主函数可以判断如果为0说明数据已结束,则可以处理接收数据的数组。
2、代码
cpp
#include "stm32f10x.h"
#define UART_RX_BUFFER_SIZE 1024
#define IDLE_TIMEOUT 1000 // 空闲时长设置为1000ms,即1秒
// 接收缓冲区和索引
uint8_t uart_rx_buffer[UART_RX_BUFFER_SIZE];
uint16_t uart_rx_index = 0;
volatile uint32_t idle_timeout_count = 0; // 计时器,用于空闲时长控制
void USARTx_Init(void)
{
// USART1初始化,略过,参照前面的配置
// 启用空闲中断
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
// 配置定时器来模拟空闲时长
SysTick_Config(SystemCoreClock / 1000); // 每1ms中断一次
}
void SysTick_Handler(void)
{
if (idle_timeout_count > 0) {
idle_timeout_count--;
}
}
void USART1_IRQHandler(void)
{
// 判断是否是空闲中断
if (USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) {
// 清除空闲中断标志
USART_ClearITPendingBit(USART1, USART_IT_IDLE);
// 读取当前接收到的数据
uint8_t received_byte = USART_ReceiveData(USART1);
// 将接收到的数据存入缓冲区
uart_rx_buffer[uart_rx_index++] = received_byte;
// 重置空闲计时器
idle_timeout_count = IDLE_TIMEOUT; // 重新开始空闲计时
// 如果缓冲区已满,处理数据帧
if (uart_rx_index >= UART_RX_BUFFER_SIZE) {
process_uart_data(uart_rx_buffer, uart_rx_index);
uart_rx_index = 0; // 重置索引,准备接收下一帧数据
}
}
}
void process_uart_data(uint8_t* data, uint16_t len)
{
// 这里可以对接收到的数据进行帧解析、校验等操作
// 例如输出数据,或者将数据存入其他地方处理
for (uint16_t i = 0; i < len; i++) {
printf("Received byte: %c\n", data[i]);
}
}
3、优缺点
优点:
- 灵活性高: 用户可以调整
IDLE_TIMEOUT
变量的值来控制数据帧的结束时长,增加灵活性。比如可以设定不同的数据帧结束时间以适应不同的应用场景。 - 响应及时: 通过定时器的使用,可以确保在一定时间内没有新的数据时,我们能及时处理完接收到的帧。
缺点:
- 额外的定时器开销: 需要使用系统定时器(如
SysTick
)来检测空闲时长,可能会对系统的其他定时任务产生一定影响。 - 延迟可能较长: 如果设置的空闲时长较长,可能会导致数据处理的延迟。需要根据实际应用的需求平衡延迟和空闲时间。