这种方法非常灵活,不需要知道数据具体长度,也不依赖特殊结束符。接下来,我们就用代码来实现这个逻辑。
- 超时解析法: 利用数据传输的间歇时间来判断。如果两个字节之间的时间间隔超过某个阈值(比如 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 中断)。强烈推荐在性能要求较高的场景使用。 -
配置提示: 强烈建议在需要处理大量或高速数据时启用 DMA。RX 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)); }
更多例子待更......