FEATURED · 精选文章

串口发送以及接受

发布时间 / 2026/8/17 20:28:43
来源 / 创域科博编辑部
栏目 / 资讯中心
串口发送以及接受 正常串口尝试发送字符串在开始 printf 重定向之前我们先按照常规方式配置串口并尝试直接发送字符串验证串口基础功能是否正常。这里使用 STM32CubeMX 生成初始化代码串口选择 USART1波特率设置为 115200数据位 8 位、无校验、1 位停止位。在 main 函数中我们直接调用 HAL_UART_Transmit 发送一段字符串代码如下uint8_t msg[] Hello STM32\r\n; HAL_UART_Transmit(huart1, msg, strlen((char*)msg), 1000);编译下载后打开串口助手可以看到正常输出 Hello STM32。这说明串口底层驱动已经工作正常接下来就可以在此基础上实现 printf 重定向。printf重定向代码要让 printf 输出到串口核心思路是重写底层字符输出函数 fputc把标准输出重定向到 USART1。这样 printf 内部每次输出一个字符时都会调用我们自定义的 fputc从而把数据发送到串口。在 Keil 工程中需要勾选 Use MicroLIB否则 printf 会占用大量资源甚至无法正常工作。同时要包含头文件 stdio.h 和 string.h否则 strlen 和 printf 无法使用。重定向的核心代码如下#include stdio.h #include string.h int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 0xFFFF); return ch; }完成上述配置后就可以在代码中直接使用 printf 输出格式化字符串例如printf(Temperature: %.2f C\r\n, 25.36);记得写头文件不然strlen和printf无法使用很多初学者在重定向后编译报错提示 strlen 或 printf 未定义原因就是缺少头文件。stdio.h 提供了 printf 和 FILE 类型的声明string.h 提供了 strlen 的声明。缺少任何一个头文件编译器都无法识别这些函数。记得打开魔术棒勾选使用LIB在 Keil 中点击魔术棒Options for Target切换到 Target 选项卡勾选 Use MicroLIB。MicroLIB 是面向嵌入式场景的精简 C 库体积小、占用资源少并且对 printf 浮点输出支持更友好。如果不勾选默认使用标准 C 库链接时可能报错或导致程序体积过大。Printf尝试发送到串口完成上述重定向配置后我们在 main 函数中调用 printf 发送数据验证是否成功输出到串口。示例代码如下printf(System Start OK\r\n); printf(Counter %d\r\n, counter); printf(Voltage %.2f V\r\n, 3.31);编译下载后打开串口助手可以看到 printf 输出的内容正常显示在串口助手中。如果输出乱码请检查串口助手的波特率是否与代码中配置的 115200 一致如果完全没有输出请检查 fputc 重定向是否生效以及是否勾选了 Use MicroLIB。USB转RTT原理图阻塞接受为了在电脑上实时监测代码进度我们需要让 STM32 能够接收上位机发送的指令。这里采用阻塞模式接收即程序会一直等待直到收到数据后才继续往下执行。在开始之前请确保已经完成了前面的 printf 重定向配置。调用阻塞模式接收函数阻塞接收的核心函数是 HAL_UART_Receive它会一直阻塞等待直到接收到指定长度的数据或超时。调用方式如下uint8_t rx_data 0; HAL_UART_Receive(huart1, rx_data, 1, 0xFFFF);上面代码表示从 USART1 接收 1 个字节数据存入 rx_data超时时间设为 0xFFFF约 26 秒。在超时时间内如果没有收到数据函数会返回 HAL_TIMEOUT收到数据则返回 HAL_OK。HAL_UART_Receive函数结构和传入参数类型以及所代表的含义(其实用法和发送函数HAL_UART_Transmit差不多)HAL_UART_Receive 的函数原型如下HAL_StatusTypeDef HAL_UART_Receive( UART_HandleTypeDef *huart, // 串口句柄指向 huart1 uint8_t *pData, // 接收缓冲区指针存放收到的数据 uint16_t Size, // 要接收的字节数 uint32_t Timeout // 超时时间单位毫秒 );四个参数的含义分别是huart串口句柄传入 huart1 表示使用 USART1。pData指向接收缓冲区的指针收到的数据会写入这里。Size期望接收的字节数例如接收 1 个字节就填 1。Timeout超时时间毫秒超过该时间仍未收到数据则返回 HAL_TIMEOUT。可以看到它的参数结构和发送函数 HAL_UART_Transmit 非常相似区别只是数据方向相反Transmit 是发送缓冲区数据到串口Receive 是从串口接收数据到缓冲区。主函数代码uint8_t rx_data 0; while (1) { printf(等待接收指令...\r\n); // 阻塞等待接收 1 个字节 if (HAL_UART_Receive(huart1, rx_data, 1, 0xFFFF) HAL_OK) { printf(收到指令: 0x%02X\r\n, rx_data); // 收到数据后回传确认信息 HAL_UART_Transmit(huart1, rx_data, 1, 1000); } }代码结构解释printf之前已经写了重定向函数可以使用——提示已经开始运行“阻塞接收”Receive——阻塞接收程序会一直等待直到收到数据Transmit——确认信息把收到的数据原样回传给上位机整个流程是程序先通过 printf 打印提示信息然后进入阻塞接收状态等待上位机发送数据一旦收到数据就通过 printf 打印收到的内容并用 Transmit 把数据回传确认随后进入下一轮循环继续等待。串口接收模式介绍前面我们使用了阻塞模式接收串口数据这种方式实现简单但程序在等待数据时会一直卡住无法处理其他任务。为了提升程序的实时性和响应能力本节介绍更常用的中断接收模式。中断接收的核心思想是CPU 平时可以专心执行主循环任务一旦串口收到数据硬件会自动触发中断CPU 暂停当前任务去处理接收到的数据处理完再回到原来的任务继续执行。此章节会学到的HAL API本节主要涉及以下 HAL 库函数和回调函数建议先对照官方手册了解它们的声明位置HAL_UART_Receive_IT以中断方式接收指定长度的数据函数立即返回数据到达后由中断回调处理。HAL_UART_RxCpltCallback接收完成回调函数当一帧数据接收完成后由 HAL 库自动调用用户在这里编写数据处理逻辑。__HAL_UART_ENABLE_IT使能串口中断的宏通常在初始化时调用。下面结合图片逐步说明如何在 CubeMX 中配置串口中断并编写对应的回调函数。上图展示了串口中断接收的整体流程配置中断、使能接收、等待数据到达、触发中断回调、在回调中处理数据并重新开启下一次接收。CubeMX中配置打开串口中断在 CubeMX 中选中 USART1在 NVIC Settings 选项卡中勾选 USART1 global interrupt使能串口全局中断。这样当串口收到数据时硬件就会触发中断请求进入中断服务函数。配置完成后重新生成代码CubeMX 会自动在 stm32f1xx_it.c 中生成 USART1_IRQHandler 中断服务函数并在其中调用 HAL_UART_IRQHandler 处理中断。串口接收中断函数结构中断服务函数的结构如下它由硬件触发负责把中断事件交给 HAL 库处理void USART1_IRQHandler(void) { HAL_UART_IRQHandler(huart1); }HAL_UART_IRQHandler 内部会根据中断标志位自动调用对应的回调函数例如接收完成时调用 HAL_UART_RxCpltCallback。优先初始化在使用中断接收之前必须在 main 函数中调用 HAL_UART_Receive_IT 启动一次接收否则即使中断已使能也不会接收数据。建议在初始化阶段就调用例如放在 while(1) 之前uint8_t rx_data 0; HAL_UART_Receive_IT(huart1, rx_data, 1);注意HAL_UART_Receive_IT 只接收一次指定长度的数据接收完成后需要再次调用才能继续接收下一帧因此通常会在回调函数末尾重新调用它。找到对应的回调函数HAL 库提供了多个回调函数分别对应不同的中断事件。接收完成对应的是 HAL_UART_RxCpltCallback我们需要在用户代码中重写这个函数。可以在 stm32f1xx_it.c 或单独的用户文件中实现推荐在 main.c 中实现方便管理。上图展示了 HAL 库中回调函数的定义位置和调用关系可以看到 HAL_UART_IRQHandler 在接收完成后会调用 HAL_UART_RxCpltCallback。回调函数编写在回调函数中编写数据处理逻辑并在末尾重新开启下一次接收形成连续接收的循环void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { // 处理收到的数据例如回传 HAL_UART_Transmit(huart1, rx_data, 1, 1000); // 重新开启下一次中断接收 HAL_UART_Receive_IT(huart1, rx_data, 1); } }这样主循环可以专心执行其他任务串口数据到达时由中断自动处理大大提高了程序的实时性。串口控制LED灯掌握了中断接收后我们可以做一个综合小实验通过串口发送指令控制开发板上的 LED 灯。例如发送字符 1 点亮 LED发送字符 0 熄灭 LED。在回调函数中根据收到的数据控制 GPIO 即可void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { if (rx_data 1) { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); // 点亮 } else if (rx_data 0) { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); // 熄灭 } HAL_UART_Receive_IT(huart1, rx_data, 1); } }编译下载后在串口助手中发送 1 或 0即可看到 LED 灯随之点亮或熄灭。这个实验把中断接收、GPIO 控制和串口通信结合起来是理解嵌入式交互的经典入门练习。其他配置与上方相同记得在MX上配置好LED的GPIO端口串口接收不定长中断前面我们使用 HAL_UART_Receive_IT 接收固定长度的数据每次只能接收 1 个字节。但在实际项目中上位机发送的指令长度往往是不固定的例如发送 LED_ON 或 LED_OFF 这样的字符串。如果仍然按固定长度接收就需要预先知道数据长度使用起来很不方便。本节介绍如何通过中断方式接收不定长数据并判断一帧数据何时结束。不定长接收的核心思路接收不定长数据通常采用以下两种方式之一空闲中断IDLE当串口在一段时间内没有收到新数据时硬件触发空闲中断表示一帧数据接收完毕。这是最常用的方式适合大多数场景。固定结束符约定一个特殊字符如 \r\n 或 \n作为一帧数据的结束标志收到结束符就认为一帧数据接收完成。下面以空闲中断方式为例说明如何在 HAL 库中实现不定长接收。函数和回调函数实现不定长接收主要涉及以下 HAL 库函数和回调函数HAL_UARTEx_ReceiveToIdle_IT以中断方式接收数据直到收到指定长度数据或检测到空闲IDLE时停止。这是不定长接收的核心函数。HAL_UARTEx_RxEventCallback接收事件回调函数当数据接收完成或检测到空闲时由 HAL 库自动调用。用户在这个回调中处理收到的数据。__HAL_UART_ENABLE_IT使能串口中断的宏用于开启空闲中断等。下面结合图片逐步说明如何在 CubeMX 中配置并编写对应的回调函数。上图展示了 HAL 库中与不定长接收相关的函数声明位置可以看到 HAL_UARTEx_ReceiveToIdle_IT 和 HAL_UARTEx_RxEventCallback 都定义在 stm32f1xx_hal_uart.h 中。上图展示了 HAL_UARTEx_ReceiveToIdle_IT 函数的原型和参数说明它的参数与 HAL_UART_Receive_IT 类似区别在于它会在检测到空闲时自动停止接收。上图展示了 HAL_UARTEx_RxEventCallback 回调函数的定义位置这个回调函数需要我们在用户代码中自行实现。回调函数位置与 HAL_UART_RxCpltCallback 类似HAL_UARTEx_RxEventCallback 也需要在用户代码中重写。推荐在 main.c 中实现方便统一管理。下面两张图分别展示了回调函数的声明位置和调用关系可以看到HAL_UARTEx_RxEventCallback 在 HAL_UART_IRQHandler 内部被调用当检测到空闲中断或接收长度达到设定值时触发。不定长接收代码示例下面给出一个完整的不定长接收示例。首先在 main 函数中启动接收uint8_t rx_buffer[64] {0}; uint16_t rx_len 0; // 启动不定长接收最多接收 64 字节检测到空闲时停止 HAL_UARTEx_ReceiveToIdle_IT(huart1, rx_buffer, 64);然后在回调函数中处理收到的数据并重新开启下一次接收void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart-Instance USART1) { rx_len Size; // 实际接收到的字节数 // 回传收到的数据确认接收成功 HAL_UART_Transmit(huart1, rx_buffer, rx_len, 1000); // 清空缓冲区重新开启下一次不定长接收 memset(rx_buffer, 0, sizeof(rx_buffer)); HAL_UARTEx_ReceiveToIdle_IT(huart1, rx_buffer, 64); } }代码说明HAL_UARTEx_ReceiveToIdle_IT启动不定长接收第二个参数是接收缓冲区第三个参数是最大接收长度。当收到数据后出现空闲或接收长度达到最大值时接收停止。Size 参数回调函数的第二个参数表示本次实际接收到的字节数通过它我们可以知道一帧数据的长度。重新开启接收与固定长度接收一样接收完成后必须再次调用 HAL_UARTEx_ReceiveToIdle_IT才能继续接收下一帧数据。注意事项在使用不定长接收时有几点需要特别注意空闲中断的触发条件空闲中断是在串口接收线上出现一段空闲时间通常是一个字节的传输时间后触发。如果上位机连续发送数据没有停顿空闲中断不会触发此时需要依靠最大长度限制来结束接收。缓冲区大小接收缓冲区要足够大避免数据溢出。如果一帧数据可能超过缓冲区大小需要适当增大缓冲区或采用环形缓冲区的方式。回调函数中不要做耗时操作回调函数运行在中断上下文中应尽量只做数据拷贝和标志位设置耗时操作如数据处理、打印等应放到主循环中执行。不同芯片的差异部分 STM32 系列如 F4、H7对 HAL_UARTEx_ReceiveToIdle_IT 的支持更完善F1 系列也支持该函数但需要确认所用 HAL 库版本是否包含此接口。小结本节介绍了串口不定长中断接收的实现方法核心是使用 HAL_UARTEx_ReceiveToIdle_IT 配合 HAL_UARTEx_RxEventCallback 回调函数。相比固定长度接收不定长接收更符合实际项目需求可以灵活处理各种长度的指令。建议读者结合前面的 LED 控制实验尝试用不定长接收实现 LED_ON / LED_OFF 字符串指令控制加深理解。多串口应用在实际项目中一块开发板往往不止一个串口例如 USART1 用于调试打印USART2 用于与传感器或蓝牙模块通信。本节介绍如何在一个工程中同时使用多个串口并解决 printf 重定向与多串口之间的冲突问题。上图展示了多串口应用的整体接线与配置思路不同外设分别挂接在不同的串口上通过各自的收发引脚完成数据交互。注意一个重定向只能对应一个串口所以现在封装一个字符打印的函数printf 重定向的核心是重写 fputc而 fputc 内部固定调用某一个串口例如 huart1。这意味着默认情况下printf 只能输出到 USART1无法直接输出到其他串口。如果想让不同串口都能打印就需要封装一个通用的字符打印函数把串口句柄作为参数传入。下面给出一个封装示例通过一个自定义函数实现向指定串口发送字符串// 向指定串口发送字符串 void UART_SendString(UART_HandleTypeDef *huart, const char *str) { HAL_UART_Transmit(huart, (uint8_t *)str, strlen(str), 1000); } // 使用示例向 USART1 打印 UART_SendString(huart1, Hello USART1\r\n); // 使用示例向 USART2 打印 UART_SendString(huart2, Hello USART2\r\n);这样无论哪个串口需要输出都可以通过传入对应的句柄来实现避免了 printf 只能绑定单一串口的限制。上图展示了封装函数的实现细节可以看到通过传入不同的串口句柄即可灵活控制输出目标。最终代码完整的多串口示例代码如下包含两个串口的初始化和收发逻辑// 初始化两个串口由 CubeMX 生成 // huart1: 调试串口波特率 115200 // huart2: 通信串口波特率 9600 int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); MX_USART2_UART_Init(); // 向两个串口分别打印启动信息 UART_SendString(huart1, USART1 Ready\r\n); UART_SendString(huart2, USART2 Ready\r\n); while (1) { // 主循环任务 } }实验现象编译下载后打开两个串口助手分别对应 USART1 和 USART2 的 USB 转串口可以看到两个串口都正常输出了各自的启动信息说明多串口配置和封装函数均工作正常。上图展示了两个串口同时输出的实验现象验证了多串口应用的可行性。串口节课大重点DMA前面介绍的串口收发方式无论是阻塞模式还是中断模式数据搬运都需要 CPU 参与。当数据量较大或通信频率较高时CPU 会被频繁打断影响系统整体性能。DMADirect Memory Access直接存储器访问可以在不占用 CPU 的情况下在内存和外设之间直接搬运数据是串口高效通信的关键技术。本节作为串口章节的重点详细介绍 DMA 的原理、配置和使用方法。上图展示了 DMA 在串口通信中的整体架构CPU 只需发起传输请求之后数据搬运由 DMA 控制器自动完成传输完成后通过中断通知 CPU。DMA传输方式与参数DMA 传输涉及几个关键参数理解它们对正确配置至关重要传输方向内存到外设发送、外设到内存接收、内存到内存数据搬移。数据宽度字节8 位、半字16 位、字32 位需与外设寄存器宽度匹配。传输模式普通模式传输一次后停止和循环模式自动循环传输适合连续数据流。优先级当多个 DMA 通道同时请求时高优先级通道优先获得总线控制权。上图展示了 DMA 传输方式与参数的配置界面可以看到方向、模式、优先级等选项。DMA简介DMA 是一种无需 CPU 干预即可在外设与内存之间搬运数据的技术。以串口发送为例传统方式下 CPU 需要逐字节把数据写入发送寄存器使用 DMA 后CPU 只需配置好源地址、目的地址和传输长度DMA 控制器就会自动完成全部数据搬运传输完成后通过中断通知 CPU。这样 CPU 可以腾出时间处理其他任务大幅提升系统效率。上图详细介绍了 DMA 的基本概念和工作原理重点理解它如何替代 CPU 完成数据搬运。特性STM32 的 DMA 控制器具有以下主要特性支持内存到外设、外设到内存、内存到内存三种传输方向。支持普通模式和循环模式循环模式适合音频、ADC 采样等连续数据流场景。每个通道支持独立配置优先级可灵活调度多个 DMA 请求。支持传输完成、半传输、传输错误等多种中断便于程序及时响应。上图列出了 DMA 控制器的详细特性建议对照芯片参考手册逐条理解。功能框图DMA 控制器的功能框图展示了其内部结构包括多个通道、仲裁器和外设请求映射关系。理解功能框图有助于掌握 DMA 的工作流程外设发出请求后仲裁器根据优先级选择通道DMA 控制器按配置完成数据搬运。上图是 DMA 控制器的功能框图可以看到通道、仲裁器与外设请求之间的连接关系。CubeMX初始化在 CubeMX 中配置串口 DMA 的步骤如下在 USART1 的 DMA Settings 选项卡中点击 Add 添加发送USART1_TX和接收USART1_RX两个 DMA 请求。发送通道选择 Normal 模式接收通道选择 Circular 模式便于连续接收。数据宽度均设为 Byte优先级按需设置。重新生成代码CubeMX 会自动生成 DMA 初始化函数 MX_DMA_Init 和串口 DMA 句柄。上图展示了 CubeMX 中串口 DMA 的配置界面注意发送和接收通道的模式选择。函数配置完成后主要使用以下 HAL 库函数进行 DMA 收发HAL_UART_Transmit_DMA以 DMA 方式发送数据函数立即返回发送完成后触发回调。HAL_UART_Receive_DMA以 DMA 方式接收数据配合循环模式可实现连续接收。HAL_UART_TxCpltCallback发送完成回调函数在发送结束后由 HAL 库自动调用。HAL_UART_RxCpltCallback接收完成回调函数在接收结束后由 HAL 库自动调用。下面给出一个使用 DMA 发送和接收的示例uint8_t tx_buffer[] DMA Test\r\n; uint8_t rx_buffer[64] {0}; // 以 DMA 方式发送数据 HAL_UART_Transmit_DMA(huart1, tx_buffer, strlen((char*)tx_buffer)); // 以 DMA 方式接收数据循环模式可连续接收 HAL_UART_Receive_DMA(huart1, rx_buffer, 64); // 发送完成回调 void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { // 发送完成可在此处理后续逻辑 } }上图展示了 DMA 相关 HAL 函数的声明位置和参数说明建议对照官方手册深入学习。上图展示了 DMA 收发回调函数的定义位置用户需要在代码中自行实现这些回调。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻