超时解析和DMA+空闲中断,笔记

这种方法非常灵活,不需要知道数据具体长度,也不依赖特殊结束符。接下来,我们就用代码来实现这个逻辑。

  • 超时解析法: 利用数据传输的间歇时间来判断。如果两个字节之间的时间间隔超过某个阈值(比如 10ms),就认为上一帧数据已经结束了。
  • 它的核心思想是:
  • 设置一个接收缓冲区(就是一个数组),用来存放收到的字节。
  • 启动 UART 接收(通常使用中断方式,每收到一个字节就触发一次中断)。
  • 在中断服务函数里:
    • 将收到的字节存入缓冲区。
    • 记录当前收到字节的时间(或者说重置一个计时器)。
    • 再次启动下一次接收。
  • 在主循环(或一个定时任务)里,不断检查:当前时间距离上次收到字节的时间,是否超过了预设的超时时间?
  • 如果超过了超时时间,并且缓冲区里有数据,就说明一帧数据接收完毕!可以对缓冲区里的数据进行处理了。处理完后,清空缓冲区,准备接收下一帧。

首先需要定义几个全局变量:

uint8_t uart_rx_buffer[UART_RX_BUFFER_SIZE];(缓冲区)

一个 `uint8_t` 类型的数组,用于存放串口接收到的每一个字节。

UART_RX_BUFFER_SIZE

定义了数组的大小。

uint16_t uart_rx_index;(计数器)

记录当前接收多少字节,新字节来了,计数器加1,满了清零。

uint32_t uart_rx_ticks;(计时器)

记录最后一个字节放到数组的时间。

extern volatile uint32_t uwTick;(系统时钟)

这是由 STM32 HAL 库提供的一个全局变量,像一个精准的秒表,每毫秒自动递增。我们用它来获取当前时间,与 `uart_rx_ticks` 比较。

#define UART_TIMEOUT_MS 100  (超时规则)

定义了一个常量,表示我们能容忍的数据到达的最大时间间隔(100毫秒)。如果超过这个时间没新字节来,我们就认为这一批字节发送完了。

extern UART_HandleTypeDef huart1;(串口控制器)

 这是 HAL 库中代表具体串口硬件(如 USART1)的结构体。我们需要通过它来操作串口,比如启动接收、发送数据等。

#include <string.h>

 还需要引入这个头文件

先把全局变量定义好。

之后注册一个回调函数,放在usart_app.c里

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	if (huart->Instance == USART1)
	{
		uart_rx_ticks = uwTick;
		uart_rx_index++;
		HAL_UART_Receive_IT(&huart1, &uart_rx_buffer[uart_rx_index], 1);
	}
}

 之后写个超时处理函数

void uart_task(void)
{
	if (uart_rx_index == 0)
		return;

	if (uwTick - uart_rx_ticks > UART_TIMEOUT_MS) // 核心判断
	{
        // "uart_rx_buffer" 里从第0个到第 "uart_rx_index - 1" 个
        // 一帧数据
		my_printf(&huart1, "uart data: %s\n", uart_rx_buffer);
        // (在这里加入你自己的处理逻辑,比如解析命令控制LED)
        // --- 结束 --- 

		//计数器归零
		memset(uart_rx_buffer, 0, uart_rx_index);
		uart_rx_index = 0;
	}
    // 如果没超时,啥也不做,等下次再检查
}

来个例子,比如发送AB点亮一个灯

//超时处理函数
void uart_task(void)
{
    // 1. 检查货架:如果计数器为0,说明没货或刚处理完,休息。
	if (uart_rx_index == 0)
		return;

    // 2. 检查手表:当前时间 - 最后收货时间 > 规定的超时时间?
	if (uwTick - uart_rx_ticks > UART_TIMEOUT_MS) // 核心判断
	{
        // --- 3. 超时!开始理货 --- 
        // "uart_rx_buffer" 里从第0个到第 "uart_rx_index - 1" 个
        // 就是我们等到的一整批货(一帧数据)
				// my_printf(&huart1, "uart data: %s\n", uart_rx_buffer);
				my_printf(&huart1, "RX: %.*s\n", uart_rx_index, uart_rx_buffer);//防止越界
				if (uart_rx_index >= 2 && uart_rx_buffer[0] == 'A' && uart_rx_buffer[1] == 'B')
        {
						ucLed[0] = 1;
            my_printf(&huart1, "LED ON!\n");
        }
        // (在这里加入你自己的处理逻辑,比如解析命令控制LED)
        // --- 理货结束 --- 

		// 4. 清理现场:把处理完的货从货架上拿走,计数器归零
		memset(uart_rx_buffer, 0, uart_rx_index);
		uart_rx_index = 0;
	}
    // 如果没超时,啥也不做,等下次再检查
}

然后去串口调试就可以点亮一个灯了。

进阶DMA+空闲中断待更新...

二编:

对于串口数据处理,常见有三种方式:轮询、中断、DMA。

  • 超时解析是一种策略,而非底层机制,需依赖轮询、中断或 DMA 实现。

  • DMA (Direct Memory Access): 使用 DMA 控制器在外设和内存之间直接传输数据,只需在传输开始和结束(或出错)时通知 CPU(通常通过中断)。
     优点: CPU 占用极低,效率最高,能处理高速、大量的数据流,不易出错。
     缺点: 配置相对复杂一点。
    适用场景: 高波特率通信;需要传输大量数据(如文件、日志);需要接收不定长数据包(DMA + IDLE 中断)。强烈推荐在性能要求较高的场景使用。

  • 配置提示: 强烈建议在需要处理大量或高速数据时启用 DMARX DMA 通常配置为循环模式 (Circular Mode) 配合 IDLE 中断 使用,以高效接收不定长数据。TX DMA 配置为普通模式 (Normal Mode),在发送前启动一次即可。

关于配置如下

 

之后具体模式的选择,

  • 选 Normal:数据长度固定或需精确控制每次传输(如 TX 发送)。

  • 选 Circular:数据持续到达且长度不定(如 RX 接收),搭配 IDLE 中断实现超时解析。

 

场景一:高效接收不定长数据包 (常用)
  • 配置:
    • 启用 USARTx_RX DMA,模式设置为 Circular
    • 分配一个足够大的接收缓冲区 (e.g., uint8_t rx_buffer[256];)。
    • 启用 UART 的 IDLE Line 中断。
场景二:发送大量数据
  • 配置:
    • 启用 USARTx_TX DMA,模式设置为 Normal
    • 准备好要发送的数据缓冲区 (e.g., uint8_t tx_data[] = "Hello World!";)。

 

例子一:

接收不定长数据包:

 

 

//初始化部分,放在串口初始化里面
//	HAL_UART_Receive_IT(&huart1, &uart_rx_buffer[0], 1);
	HAL_UARTEx_ReceiveToIdle_DMA(&huart1, uart_rx_dma_buffer, sizeof(uart_rx_dma_buffer));
	__HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);
//定义的全局变量
uint8_t uart_rx_buffer[128] = {0};
uint16_t uart_rx_index;
uint32_t uart_rx_ticks;
#define UART_TIMEOUT_MS 100
uint8_t uart_rx_dma_buffer[128] = {0};//可选择扩充缓冲区
struct rt_ringbuffer uart_ringbuffer;
uint8_t uart_dma_buffer[128] = {0};

 #include "ringbuffer.h"这个文件要引入,放于Componets文件夹。

.c文件的全部代码

#include "usart_app.h"
#include "stdlib.h"
#include "stdarg.h"
#include "string.h"
#include "stdio.h"
#include "usart.h"

uint8_t uart_rx_buffer[128] = {0};
uint16_t uart_rx_index;
uint32_t uart_rx_ticks;
#define UART_TIMEOUT_MS 100
uint8_t uart_rx_dma_buffer[128] = {0};//可选择扩充缓冲区
struct rt_ringbuffer uart_ringbuffer;
uint8_t uart_dma_buffer[128] = {0};
struct rt_ringbuffer uart_ringbuffer;
uint8_t ringbuffer_pool[128];

int my_printf(UART_HandleTypeDef *huart, const char *format, ...)
{
	char buffer[512]; // 临时存储格式化后的字符串
	va_list arg;      // 处理可变参数
	int len;          // 最终字符串长度

	va_start(arg, format);
	// 安全地格式化字符串到 buffer
	len = vsnprintf(buffer, sizeof(buffer), format, arg);
	va_end(arg);

	// 通过 HAL 库发送 buffer 中的内容
	HAL_UART_Transmit(huart, (uint8_t *)buffer, (uint16_t)len, 0xFF);
	return len;
}


//中断回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    // 1. 核对身份:是 USART1 的快递员吗?
	if (huart->Instance == USART1)
	{
        // 2. 更新收货时间:记录下当前时间
		uart_rx_ticks = uwTick;
        // 3. 货物入库:将收到的字节放入缓冲区(HAL库已自动完成)
        //    并增加计数器
        //    (注意:实际入库由 HAL_UART_Receive_IT 触发,这里只更新计数)
		uart_rx_index++;
        // 4. 准备下次收货:再次告诉硬件,我还想收一个字节
		HAL_UART_Receive_IT(&huart1, &uart_rx_buffer[uart_rx_index], 1);
	}
}

/**
 * @brief UART DMA接收完成或空闲事件回调函数
 * @param huart UART句柄
 * @param Size 指示在事件发生前,DMA已经成功接收了多少字节的数据
 * @retval None
 */
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
	// 1. 确认是目标串口 (USART1)
	if (huart->Instance == USART1)
	{
		// 2. 紧急停止当前的 DMA 传输 (如果还在进行中)
		//    因为空闲中断意味着发送方已经停止,防止 DMA 继续等待或出错
		HAL_UART_DMAStop(huart);

		rt_ringbuffer_put(&uart_ringbuffer,uart_rx_dma_buffer,Size);
		
		// 5. 清空 DMA 接收缓冲区,为下次接收做准备
		//    虽然 memcpy 只复制了 Size 个,但清空整个缓冲区更保险
		memset(uart_rx_dma_buffer, 0, sizeof(uart_rx_dma_buffer));

		// 6. **关键:重新启动下一次 DMA 空闲接收**
		//    必须再次调用,否则只会接收这一次
		HAL_UARTEx_ReceiveToIdle_DMA(&huart1, uart_rx_dma_buffer, sizeof(uart_rx_dma_buffer));

		// 7. 如果之前关闭了半满中断,可能需要在这里再次关闭 (根据需要)
		__HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);
	}
}


//超时处理函数
void uart_task(void)
{
//    // 1. 检查货架:如果计数器为0,说明没货或刚处理完,休息。
//	if (uart_rx_index == 0)
//		return;

//    // 2. 检查手表:当前时间 - 最后收货时间 > 规定的超时时间?
//	if (uwTick - uart_rx_ticks > UART_TIMEOUT_MS) // 核心判断
//	{
//        // --- 3. 超时!开始理货 --- 
//        // "uart_rx_buffer" 里从第0个到第 "uart_rx_index - 1" 个
//        // 就是我们等到的一整批货(一帧数据)
//				// my_printf(&huart1, "uart data: %s\n", uart_rx_buffer);
//				my_printf(&huart1, "RX: %.*s\n", uart_rx_index, uart_rx_buffer);//防止越界
//				if (uart_rx_index >= 2 && uart_rx_buffer[0] == 'A' && uart_rx_buffer[1] == 'B')
//        {
//						ucLed[0] = 1;
//            my_printf(&huart1, "LED ON!\n");
//        }
//        // (在这里加入你自己的处理逻辑,比如解析命令控制LED)
//        // --- 理货结束 --- 

//		// 4. 清理现场:把处理完的货从货架上拿走,计数器归零
//		memset(uart_rx_buffer, 0, uart_rx_index);
//		uart_rx_index = 0;
//		//    // 5. 将UART接收缓冲区指针重置为接收缓冲区的起始位置
//		//    huart1.pRxBuffPtr = uart_rx_buffer;
//	}
    uint16_t length;
	
	length = rt_ringbuffer_data_len(&uart_ringbuffer);
	
	if(length == 0) return;
	
	rt_ringbuffer_get(&uart_ringbuffer,uart_dma_buffer,length);

	my_printf(&huart1, "uart data: %s\n", uart_dma_buffer);

	// 清空接收缓冲区,将接收索引置零
	memset(uart_dma_buffer, 0, sizeof(uart_dma_buffer));
}

 rt_ringbuffer_init(&uart_ringbuffer, ringbuffer_pool, sizeof(ringbuffer_pool));记得在循环前初始化。

如果要加入事件处理逻辑:

//超时处理函数
void uart_task(void)
{
//    // 1. 检查货架:如果计数器为0,说明没货或刚处理完,休息。
//	if (uart_rx_index == 0)
//		return;

//    // 2. 检查手表:当前时间 - 最后收货时间 > 规定的超时时间?
//	if (uwTick - uart_rx_ticks > UART_TIMEOUT_MS) // 核心判断
//	{
//        // --- 3. 超时!开始理货 --- 
//        // "uart_rx_buffer" 里从第0个到第 "uart_rx_index - 1" 个
//        // 就是我们等到的一整批货(一帧数据)
//				// my_printf(&huart1, "uart data: %s\n", uart_rx_buffer);
//				my_printf(&huart1, "RX: %.*s\n", uart_rx_index, uart_rx_buffer);//防止越界
//				if (uart_rx_index >= 2 && uart_rx_buffer[0] == 'A' && uart_rx_buffer[1] == 'B')
//        {
//						ucLed[0] = 1;
//            my_printf(&huart1, "LED ON!\n");
//        }
//        // (在这里加入你自己的处理逻辑,比如解析命令控制LED)
//        // --- 理货结束 --- 

//		// 4. 清理现场:把处理完的货从货架上拿走,计数器归零
//		memset(uart_rx_buffer, 0, uart_rx_index);
//		uart_rx_index = 0;
//		//    // 5. 将UART接收缓冲区指针重置为接收缓冲区的起始位置
//		//    huart1.pRxBuffPtr = uart_rx_buffer;
//	}
    uint16_t length;
	
	length = rt_ringbuffer_data_len(&uart_ringbuffer);
	
	if(length == 0) return;
	
	rt_ringbuffer_get(&uart_ringbuffer,uart_dma_buffer,length);

	my_printf(&huart1, "uart data: %s\n", uart_dma_buffer);
	
	//新增指令处理逻辑
    if(length >= 5 && strncmp((char*)uart_dma_buffer, "HELLO", 5) == 0) {
       ucLed[0] = 1; // 点亮LED
        my_printf(&huart1, "LED ON!\n");
    }

	// 清空接收缓冲区,将接收索引置零
	memset(uart_dma_buffer, 0, sizeof(uart_dma_buffer));
}

 更多例子待更......

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值