旧版本(已经废弃)
最快调用方法
直接把官网下载的两个头文件都导入
tjc_usart_hmi.h和tjc_usart_hmi.c
把头文件改成你对应的头文件
然后在main.c当中引入头文件和设定串口接受命令固定长度
可能存在的不足
在tjc_usart_hmi.h定义了一个串口接受中断,如果在main.c中定义的串口1没有使用我之前说的串口缓冲区,就得把这个函数删除然后去主函数重新定义或者也定义一个单独的.c.h处理串口接收


这个是我设定的一个使用帧缓冲区 (xUART1结构体)处理不定长数据的回调
| 特性 | 环形缓冲区 (RingBuffer_t) | 帧缓冲区 (xUART1结构体) |
|---|---|---|
| 设计目标 | 流式数据持续接收、动态处理 | 整帧数据一次性接收、批量处理 |
| 数据结构 | 头尾指针 + 循环队列 | 双缓冲 + 固定长度 |
| 适用场景 | 连续指令、实时交互、随机访问 | 不定长数据帧、空闲触发 |
| 处理方式 | 边收边解析,可堆积多条指令 | 收完整帧再处理,帧间需空闲 |
定义一个新的发送函数减少中断的调用次数
方案:独立 receive.c/h 模块(推荐)
将串口和串口屏功能完全封装在独立模块中,main.c 只通过接口函数访问。
好的,非常乐意为您整理一份详尽的Markdown笔记。这份笔记将清晰地对比两种代码实现,并深入解释其背后的工作原理和思想差异。
嵌入式串口回显:阻塞发送 vs. DMA非阻塞发送实现对比
场景描述
在嵌入式开发中,一个常见的调试需求是:当MCU的串口(如UART1)接收到来自上位机(PC)的一帧数据后,将其内容和长度通过同一个串口回显(Echo)给上位机,以便确认数据接收是否正确。
目标:接收数据buf,长度为len。向上位机发送格式为 RX[len]: buf\r\n 的字符串。
方法一:阻塞式发送(分步阻塞)
这是最直观的实现方式,使用printf和HAL_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. 代码解析
UART1_GetReceivedData(): 从我们之前设计的串口驱动中,取出已经接收完成的一帧数据。返回值len代表实际收到的字节数。printf("RX[%u]: ", len);: 将字符串前缀(如"RX[10]: ")通过串口发送。重点:printf函数会一直占用CPU,直到这个字符串的所有字符都发送完毕才会返回。HAL_UART_Transmit(...): 发送接收到的原始数据buf。重点: 这个函数同样会阻塞CPU,直到len个字节全部发送完毕。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. 代码解析
uint8_t tx_buf[600];: 定义一个临时的、足够大的数组,用来在内存中构建我们将要发送的完整数据包。snprintf,memcpy,strcpy: 这一系列函数都是内存操作函数。它们将前缀、数据和后缀快速地拼接在一起,存入tx_buf。这些操作的执行速度极快(微秒级),CPU几乎没有耗时。UART1_DMASendData(tx_buf, tx_len);: 这是核心。我们调用自己封装的DMA发送函数,把数据包的起始地址 (tx_buf) 和 总长度 (tx_len) 告诉DMA控制器。- 重点:
UART1_DMASendData函数在配置好DMA后会立即返回,主循环继续执行。CPU的任务到此结束,它完全不用关心数据是怎么一个字节一个字节发送出去的,这个体力活完全交给了硬件DMA。
3. 工作流程:CPU老板模式
CPU像一个老板,它快速准备好一份任务清单(内存里的tx_buf),然后把它交给快递员(DMA控制器)并告诉他送货地址(串口外设)。之后,老板就去忙别的重要工作了(执行主循环其他任务)。
直观对比:假设收到5字节数据 "hello"
len 的值为 5,buf 的内容是 {'h','e','l','l','o'}。
| 步骤 | 阻塞方式 | DMA非阻塞方式 |
|---|---|---|
| 1 | printf("RX[5]: "); ➡️ CPU等待,直到7个字节发完。 | snprintf(...) ➡️ 内存中准备好"RX[5]: ",瞬间完成。 |
| 2 | HAL_UART_Transmit(buf, 5); ➡️ CPU等待,直到"hello"发完。 | memcpy(...) ➡️ 内存中追加"hello",瞬间完成。 |
| 3 | printf("\r\n"); ➡️ CPU等待,直到2个字节发完。 | strcpy(...) ➡️ 内存中追加"\r\n",瞬间完成。 |
| 4 | (无) | UART1_DMASendData(tx_buf, 14);➡️ 函数立即返回,CPU继续运行主循环。DMA开始在后台发送14字节。 |
| 结果 | CPU被阻塞了约 (7+5+2)*10 / 115200 ≈ 1.2ms。 | CPU几乎没有被阻塞,系统保持流畅。 |
结论
len的含义从未改变:它始终是UART1_GetReceivedData返回的“实际接收到的字节数”。- 改变的是“使用策略”:
- 阻塞方式:用
len作为参数,多次调用阻塞函数,让CPU去等待。 - DMA方式:用
len作为参数,调用内存函数来准备数据,最后让DMA去发送。
- 阻塞方式:用
对于任何需要兼顾多个任务(如通信、按键、传感器、屏幕刷新)的嵌入式系统,DMA非阻塞方式都是无可争议的更优选择。它将CPU从繁重的、重复的数据搬运中解放出来,极大地提升了系统的实时性和吞吐量。初期投入时间学习和适应这种模式,将使项目在后期开发和维护中受益无穷。