跳到主要内容

旧版本(已经废弃)

最快调用方法

直接把官网下载的两个头文件都导入

tjc_usart_hmi.htjc_usart_hmi.c

image-20251116152438123

把头文件改成你对应的头文件

image-20251116152510615

然后在main.c当中引入头文件和设定串口接受命令固定长度

可能存在的不足

tjc_usart_hmi.h定义了一个串口接受中断,如果在main.c中定义的串口1没有使用我之前说的串口缓冲区,就得把这个函数删除然后去主函数重新定义或者也定义一个单独的.c.h处理串口接收

image-20251116152635575

image-20251116152857468

这个是我设定的一个使用帧缓冲区 (xUART1结构体)处理不定长数据的回调

特性环形缓冲区 (RingBuffer_t)帧缓冲区 (xUART1结构体)
设计目标流式数据持续接收、动态处理整帧数据一次性接收、批量处理
数据结构头尾指针 + 循环队列双缓冲 + 固定长度
适用场景连续指令、实时交互、随机访问不定长数据帧、空闲触发
处理方式边收边解析,可堆积多条指令收完整帧再处理,帧间需空闲

定义一个新的发送函数减少中断的调用次数

方案:独立 receive.c/h 模块(推荐)

将串口和串口屏功能完全封装在独立模块中,main.c 只通过接口函数访问。


好的,非常乐意为您整理一份详尽的Markdown笔记。这份笔记将清晰地对比两种代码实现,并深入解释其背后的工作原理和思想差异。


嵌入式串口回显:阻塞发送 vs. DMA非阻塞发送实现对比

场景描述

在嵌入式开发中,一个常见的调试需求是:当MCU的串口(如UART1)接收到来自上位机(PC)的一帧数据后,将其内容和长度通过同一个串口回显(Echo)给上位机,以便确认数据接收是否正确。

目标:接收数据buf,长度为len。向上位机发送格式为 RX[len]: buf\r\n 的字符串。


方法一:阻塞式发送(分步阻塞)

这是最直观的实现方式,使用printfHAL_UART_Transmit等阻塞函数,按顺序一步步完成发送。

1. 代码实现

/* main.c 的主循环中 */

if (UART1_IsDataReady()) // 检查是否有新数据帧到达
{
uint8_t buf[512]; // 接收缓冲区
uint16_t len = 0; // 用于存储接收到的数据长度

// 从自定义的接收驱动中获取数据和长度
len = UART1_GetReceivedData(buf, sizeof(buf));

if (len > 0)
{
/* 回显:通过printf和HAL库函数分三次阻塞发送 */

// 第1步:发送前缀,CPU在此等待,直到"RX[len]: "发送完毕
printf("RX[%u]: ", len);

// 第2步:发送实际数据,CPU在此等待,直到buf中的len个字节发送完毕
HAL_UART_Transmit(&huart1, buf, len, HAL_MAX_DELAY);

// 第3步:发送换行符,CPU在此等待,直到"\r\n"发送完毕
printf("\r\n");
}
}

2. 代码解析

  1. UART1_GetReceivedData(): 从我们之前设计的串口驱动中,取出已经接收完成的一帧数据。返回值len代表实际收到的字节数。
  2. printf("RX[%u]: ", len);: 将字符串前缀(如"RX[10]: ")通过串口发送。重点: printf函数会一直占用CPU,直到这个字符串的所有字符都发送完毕才会返回。
  3. HAL_UART_Transmit(...): 发送接收到的原始数据buf重点: 这个函数同样会阻塞CPU,直到len个字节全部发送完毕。
  4. printf("\r\n");: 发送结束的换行符。同样,CPU会在此阻塞。

3. 工作流程:CPU监工模式

CPU就像一个监工,它必须亲自处理和等待每一部分的发送任务。在整个发送期间,while(1)主循环是停滞的,CPU无法响应任何其他事件(如按键、其他串口数据等)。


方法二:DMA非阻塞发送(一次性提交)

这种方法是现代嵌入式开发的首选。它遵循“先在内存中准备好完整数据,然后一次性提交给DMA后台发送”的原则。

1. 代码实现

/* main.c 的主循环中 */

if (UART1_IsDataReady()) // 检查是否有新数据帧到达
{
uint8_t buf[512]; // 接收缓冲区
uint16_t len = 0; // 用于存储接收到的数据长度

// 从自定义的接收驱动中获取数据和长度 (这一步和方法一完全相同)
len = UART1_GetReceivedData(buf, sizeof(buf));

if (len > 0)
{
/* 回显:先拼接完整数据包,再调用一次DMA非阻塞发送 */

// 步骤1: 准备一个足够大的临时发送缓冲区
uint8_t tx_buf[600];
int tx_len = 0; // 用于实时计算已拼接数据包的长度

// 步骤2: 使用snprintf拼接前缀,操作在内存中,瞬时完成
tx_len = snprintf((char*)tx_buf, sizeof(tx_buf), "RX[%u]: ", len);

// 步骤3: 使用memcpy追加接收到的数据,操作在内存中,瞬时完成
memcpy(tx_buf + tx_len, buf, len);
tx_len += len; // 更新总长度

// 步骤4: 使用strcpy追加换行符,操作在内存中,瞬时完成
strcpy((char*)tx_buf + tx_len, "\r\n");
tx_len += 2;

// 步骤5: 调用DMA发送函数,将拼接好的完整数据包一次性提交
// 函数立即返回,CPU被解放,DMA硬件在后台负责实际发送
UART1_DMASendData(tx_buf, tx_len);
}
}

2. 代码解析

  1. uint8_t tx_buf[600];: 定义一个临时的、足够大的数组,用来在内存中构建我们将要发送的完整数据包。
  2. snprintf, memcpy, strcpy: 这一系列函数都是内存操作函数。它们将前缀、数据和后缀快速地拼接在一起,存入tx_buf。这些操作的执行速度极快(微秒级),CPU几乎没有耗时。
  3. UART1_DMASendData(tx_buf, tx_len);: 这是核心。我们调用自己封装的DMA发送函数,把数据包的起始地址 (tx_buf)总长度 (tx_len) 告诉DMA控制器。
  4. 重点: UART1_DMASendData函数在配置好DMA后会立即返回,主循环继续执行。CPU的任务到此结束,它完全不用关心数据是怎么一个字节一个字节发送出去的,这个体力活完全交给了硬件DMA。

3. 工作流程:CPU老板模式

CPU像一个老板,它快速准备好一份任务清单(内存里的tx_buf),然后把它交给快递员(DMA控制器)并告诉他送货地址(串口外设)。之后,老板就去忙别的重要工作了(执行主循环其他任务)。


直观对比:假设收到5字节数据 "hello"

len 的值为 5buf 的内容是 {'h','e','l','l','o'}

步骤阻塞方式DMA非阻塞方式
1printf("RX[5]: ");
➡️ CPU等待,直到7个字节发完。
snprintf(...)
➡️ 内存中准备好"RX[5]: ",瞬间完成
2HAL_UART_Transmit(buf, 5);
➡️ CPU等待,直到"hello"发完。
memcpy(...)
➡️ 内存中追加"hello",瞬间完成
3printf("\r\n");
➡️ CPU等待,直到2个字节发完。
strcpy(...)
➡️ 内存中追加"\r\n",瞬间完成
4(无)UART1_DMASendData(tx_buf, 14);
➡️ 函数立即返回,CPU继续运行主循环。DMA开始在后台发送14字节。
结果CPU被阻塞了约 (7+5+2)*10 / 115200 ≈ 1.2msCPU几乎没有被阻塞,系统保持流畅。

结论

  • len的含义从未改变:它始终是UART1_GetReceivedData返回的“实际接收到的字节数”。
  • 改变的是“使用策略”
    • 阻塞方式:用len作为参数,多次调用阻塞函数,让CPU去等待。
    • DMA方式:用len作为参数,调用内存函数来准备数据,最后让DMA去发送。

对于任何需要兼顾多个任务(如通信、按键、传感器、屏幕刷新)的嵌入式系统,DMA非阻塞方式都是无可争议的更优选择。它将CPU从繁重的、重复的数据搬运中解放出来,极大地提升了系统的实时性和吞吐量。初期投入时间学习和适应这种模式,将使项目在后期开发和维护中受益无穷。

新版本(串口整合的9个函数)

陶晶驰串口屏使用心得