恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

STM32F103双串口收发实战:中断与DMA实现稳定数据转发

  • 首页
  • 资讯中心
  • /
  • STM32F103双串口收发实战:中断与DMA实现稳定数据转发

相关资讯

尚硅谷 Vibe Coding|第三章(4) Claude Code深度使用与进阶技巧-常见问题排查,三种模式,案例实操:把 settings 改到 TaoToken 2026/10/15 18:41:36
火山引擎渠道伙伴10月精选课程,Agent时代的底层能力全解析 2026/10/15 18:41:36
R语言入门实战:从PDF教程到数据分析的完整路径 2026/10/15 18:36:36

最新资讯

Egg.js 4 突然爆火,原因是否归结于AI 原生落地需求爆发?TaoToken 统一 Key 通道实测
《MC++》学习–智能指针
foobox-cn 使用教程:为 foobar2000 换一套现代 DUI 皮肤的完整上手指南
Java+Vue酒店管理系统全栈开发实战
2024年五大技术趋势预测与工程实践指南
NPU与TPU架构解析:从脉动阵列到模型迁移实战

今日推荐

AI视频生成的时间一致性难题与时空建模演进
基于bbox的PDF文档解析:多栏重排与水印过滤实战
探矿数据进RAG前的清洗全攻略:TXT、Word、PDF、网页四类文档处理实践

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

STM32F103双串口收发实战:中断与DMA实现稳定数据转发

发布时间:2026/10/15 18:41:36
STM32F103双串口收发实战:中断与DMA实现稳定数据转发 简介STM32F103双串口收发工程是一份面向嵌入式开发者、已调试通过的实例代码解决在单个微控制器上同时使用两个串口收发数据的需求适合设备控制、上位机通信、调试信息输出等场景。资源采用结构体版本的实现方式可将串口号、波特率、收发引脚等统一配置并包含USART初始化、发送、接收等核心函数对时钟配置、BRR波特率计算、数据格式与中断使能有清晰说明便于维护和扩展。压缩包共143个文件、约2.21MB以C源码和H头文件为主同时包含Keil MDK工程文件uvprojx/uvoptx、编译生成的axf/hex、map/lst及中间文件可在MDK中直接打开、编译和烧录验证。已有2692人学习浏览。借助该工程初学者能理解STM32标准外设库的双串口驱动程序结构开发者也能快速复用稳定的收发流程缩短开发周期。1. 双串口收发到底在解决什么问题我接手过不少 STM32F103 的小项目很多同事一上来就只开一个串口用来打印日志结果真的要把数据传给下一级设备时才发现串口不够用。标题里写“stm32f103双串口收发调试通过”说白了就是让 USART1 和 USART2 同时工作一个口负责和上位机或传感器通信另一个口负责和调试终端或另一块板子通信两个口互不干扰收发都能跑通。这个需求在 IOT 网关、Modbus 转发、GPS 解析同时透传的场景里非常常见。本文会从硬件选型开始给出可复现的初始化、中断接收、数据转发代码再把我实际调试时踩过的乱码、丢字节、互锁、复位丢帧这些问题全部列出来按“现象-原因-解决”的方式讲清楚。适合手上有板子、想直接跑双串口或者已经跑通但总出怪问题的开发者。2. 选型与接线为什么是 USART1 USART2以及硬件上的坑2.1 先定串口资源USART1 和 USART2 的引脚分布STM32F103 里串口不止两个但双串口方案最常用的是 USART1 和 USART2原因是引脚好接和调试工具冲突少。USART1 的 TX 在 PA9RX 在 PA10USART2 的 TX 在 PA2RX 在 PA3。这两组引脚在 LQFP48、LQFP64 封装上都有即使是最小系统板也能直接引出。USART3 的 PB10/PB11 虽然也能用但很多板子上这两个引脚被其它外设占用干扰源多。我一般优先选 USART1 USART2一个用于与 PC 通信一个用于与外部设备通信。还需要注意 AFIO 时钟。使用 HAL 库时如果只开启 USART 时钟而不开启 GPIO 时钟引脚不会有输出如果用的是重映射引脚还要额外__HAL_RCC_AFIO_CLK_ENABLE()和__HAL_AFIO_REMAP_USARTx_ENABLE()。使用默认引脚时不需要重映射但 GPIO 的复用推挽配置一定不能少。下面这段是常见做法__HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_USART1_CLK_ENABLE(); __HAL_RCC_USART2_CLK_ENABLE(); GPIO_InitTypeDef gpio {0}; gpio.Pin GPIO_PIN_9 | GPIO_PIN_10; gpio.Mode GPIO_MODE_AF_PP; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, gpio); gpio.Pin GPIO_PIN_2 | GPIO_PIN_3; HAL_GPIO_Init(GPIOA, gpio);这段代码的关键点是PA9 和 PA10 复用为 USART1PA2 和 PA3 复用为 USART2。如果只配置了 USART 外设GPIO 没配成 AF_PP发送会没有波形接收也会一直读到高电平。2.2 最小硬件连接3.3V 电平与 TTL 转 USB 模块双串口调试时你至少需要两个 TTL 转 USB 模块或者一个支持双串口的调试工具。STM32F103 的串口电平是 3.3V TTL不能直接连 RS232 电平也不能直接连 5V 的串口设备否则长期工作可能烧引脚。常见做法是使用 CP2102 或 CH340 模块模块的 TX 接 STM32 的 RX模块的 RX 接 STM32 的 TX地线必须共地。GND 不接会导致现象非常奇葩能识别到串口但发出去的数据完全乱码或者只有第一次能收到。还要注意如果你用的是带 5V 电平输出的 USB 转 TTL 模块比如某些国产板载模块最好先确认模块方向。多数模块默认是 3.3V/5V 跳线可选调试 STM32F103 时把跳线放在 3.3V 侧。两个 USB 模块同时插在电脑上Windows 会枚举出两个 COM 口最好在设备管理器里记住对应关系或者用标签贴在模块上。否则你会在调试时反复猜自己到底在跟哪个口说话。2.3 硬件接线最容易翻车的三个细节第一串口交叉接线。TX 对 RX、RX 对 TX这个已经是常识但实际用杜邦线跑双串口时很容易把两根线插反。我建议每个串口用不同颜色的线USART1 的 TX 用红色RX 用黑色USART2 的 TX 用黄色RX 用绿色这样一眼能看出当前接的是哪个口。第二模块供电问题。TTL 转 USB 模块一般从 USB 取电如果模块还带着 3.3V 输出而你用它给 STM32 板子供电串口线又同时连接容易形成回流。最稳的做法是 STM32 板子单独供电USB 转 TTL 只接 TX、RX、GND不接 VCC。如果必须从模块取电确认模块的 LDO 能提供足够电流。第三串口地和外部设备地没连。双串口里有一个串口接的是外部传感器或单片机如果外部设备和 STM32 各自独立供电且没有共地会出现接收数据偶尔正确、偶尔错位的现象。用示波器看波形时看摆幅是否在 0~3.3V 之间如果低电平抬高到 1.5V 左右基本就是地电位不一致。3. 串口驱动配置从初始化到中断收发的完整代码3.1 HAL 库串口初始化参数怎么设时钟怎么开初始化串口时除了 GPIO 复用还要设置波特率、字长、停止位、校验位和硬件流控。对于常用调试场景我一般设成 115200-8-N-1也就是波特率 115200字长 8 位无校验1 位停止位。如果外部设备只支持 9600那另一个串口单独设成 9600两个串口的波特率可以不同各有各的huart句柄。UART_HandleTypeDef huart1; UART_HandleTypeDef huart2; void MX_USART1_UART_Init(void) { huart1.Instance USART1; huart1.Init.BaudRate 115200; huart1.Init.WordLength UART_WORDLENGTH_8B; huart1.Init.StopBits UART_STOPBITS_1; huart1.Init.Parity UART_PARITY_NONE; huart1.Init.Mode UART_MODE_TX_RX; huart1.Init.HwFlowCtl UART_HWCONTROL_NONE; huart1.Init.OverSampling UART_OVERSAMPLING_16; HAL_UART_Init(huart1); }OverSampling参数容易被忽略。默认 16 倍过采样抗干扰能力更好如果为了拉高波特率改成 8 倍线缆稍长就容易出错。双串口同时收发时我建议两个口都保留 16 倍过采样波特率最高撑到 460800 足够常见场景使用不要为了“看起来快”去牺牲稳定性。还有一点HAL 库的HAL_UART_Init内部会调用HAL_UART_MspInit如果你没有在MspInit里配置 GPIO 和中断使能外设初始化会不完整。很多人把 GPIO 初始化写在用户代码里然后发现干活不对原因就在这。3.2 中断接收的实现单字节中断与环形缓冲双串口同时收数据如果不用中断主循环轮询HAL_UART_Receive会阻塞两个口互相拖累。正确做法是接收用中断每收到一个字节就存进缓冲区主循环或者发送中断时再把数据处理掉。常见做法是每个串口配一个环形缓冲区我这里给一个简化版#define RX_BUF_SIZE 256 volatile uint8_t rx1_buf[RX_BUF_SIZE]; volatile uint8_t rx1_head 0; volatile uint8_t rx1_tail 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { uint8_t next_head; if (huart-Instance USART1) { next_head (rx1_head 1) % RX_BUF_SIZE; if (next_head ! rx1_tail) { rx1_buf[rx1_head] rx1_last_byte; // 已经在调用前保存 rx1_head next_head; } } }上面写法有个前提rx1_last_byte需要在接收中断的另一个回调里保存。更干净的写法是直接用 HAL 库提供的中断接收函数HAL_UART_Receive_IT(huart1, rx1_temp, 1)每次接收一个字节接收完进入回调然后在回调里再次调用HAL_UART_Receive_IT开启下一次接收。注意这里有个嵌套调用回调里不能等太久否则下一个字节来了而Receive_IT还没重新开启数据就丢了。这个细节是双串口丢字节最经典的原因之一。我建议的做法是在初始化时对每个串口调用一次HAL_UART_Receive_IT(huart1, rx1_temp, 1);然后在各自的回调里再把rx1_temp存入自己的缓冲区同时把接收函数重新准备好。两个串口的回调入口是同一个HAL_UART_RxCpltCallback所以在这个函数里用huart-Instance区分是谁触发的这是双串口跑起来的关键。3.3 发送的三种方式阻塞、中断、DMA 怎么选发送方式决定了主循环能不能跑得快。HAL_UART_Transmit是阻塞发送发送期间 CPU 会一直等待每个字节发完。如果波特率 115200发送 10 个字节大约耗时 0.9ms看起来不长但如果主循环还有其他实时任务比如 ADC 采样就会感到明显卡顿。双串口转发场景里我不会在中断回调里用阻塞发送因为中断里等发送会让串口接收中断被长期占用两个口互相阻塞。中断发送HAL_UART_Transmit_IT是异步的发送完成会有HAL_UART_TxCpltCallback回调。适合发送不定长数据但需要保证发送缓冲区在发送完成前不被改写不然会出现发到一半数据被覆盖的怪问题。DMA 发送适合大块数据比如把一段 sensor 原始数据直接转发到另一串口但需要开 DMA 时钟、配置 DMA 通道两个串口占用不同的 DMA 通道还要注意 DMA 中断优先级。我的建议是双串口转发量不大时优先用中断接收 中断发送如果数据量大接收端用 DMA 空闲中断发送端用 DMA。中间的“转发量不大”定义为每秒不超过几千字节这类场景占大多数。这里给一个中断发送的完整最小流程uint8_t tx_buf[64]; void send_uart1(uint8_t *data, uint16_t len) { memcpy(tx_buf, data, len); HAL_UART_Transmit_IT(huart1, tx_buf, len); }注意tx_buf是全局变量send_uart1返回后HAL_UART_Transmit_IT还会慢慢发期间不能再调用send_uart1否则缓冲区被改。常见的处理是加一个uart1_busy标志发送回调里清标志下次要发新的数据时先检查标志如果忙就排队。这个标志是实现双串口稳定转发的核心也是很多新手容易忽略的地方。4. 双串口之间的数据转发状态机与消息分割4.1 最简转发逻辑收到什么发什么有什么坑把 USART1 收到的每个字节原样转发到 USART2反过来也一样这是很多人理解的“双串口透传”。但这看着简单实际有一堆问题。最直接的坑是如果在HAL_UART_RxCpltCallback里直接调用HAL_UART_Transmit_IT(huart2, rx_byte, 1)当数据连续到达时上一次Transmit_IT还没完成下一次Receive_IT又触发了而第二次的数据没地方保存并且Transmit_IT在忙时会返回HAL_BUSY数据直接丢。所以收发必须各自有缓冲区或者队列不能一字节一字节同步转发。我见过很多翻车现场用阻塞发送转发一字节结果一帧 64 字节的数据转发完接收缓冲区溢出只前 30 字节正常后面的全部丢失。原因就是转发速度慢于接收速度而且没有流控。所以最简转发逻辑的正确形态是接收中断只管把字节放进环形缓冲区主循环里读取缓冲区再通过一个发送队列把数据交给串口中断。主循环的执行频率决定了转发吞吐量通常主循环跑 1ms 一次115200 波特率下完全够用。void main_loop(void) { uint8_t ch; while (ring_pop(rx1_ring, ch)) { uart2_send_queue_push(ch); } while (ring_pop(rx2_ring, ch)) { uart1_send_queue_push(ch); } }这段代码的含义是两个收环缓冲各管各的主循环把收到的字节交给对方的发送队列。发送队列再通过串口中断每次发送一个字节发完自动取下一个。这样做虽然看起来土但非常稳不会出现一个串口忙时另一个串口被阻塞的情况。4.2 用状态机拆帧帧头帧尾与长度约定如果只是简单转发不需要拆帧。但通常情况下你需要识别一帧完整的消息比如从传感器串口收到的数据是AA 55 01 02 03 04 7E你需要从流式数据里把AA 55当作帧头7E当作帧尾中间 4 个字节是有效数据。拆帧最稳的方法是用状态机而不要用缓冲区里去找“帧头”然后截取。用状态机的好处是无论数据是否对齐状态机都能正确同步。typedef enum { FRAME_WAIT_HEAD1, FRAME_WAIT_HEAD2, FRAME_DATA, FRAME_TAIL } frame_state_t; frame_state_t uart2_frame_state FRAME_WAIT_HEAD1; uint8_t rx2_frame_buf[64]; uint16_t rx2_frame_len 0; void uart2_frame_parse(uint8_t byte) { switch (uart2_frame_state) { case FRAME_WAIT_HEAD1: if (byte 0xAA) uart2_frame_state FRAME_WAIT_HEAD2; break; case FRAME_WAIT_HEAD2: if (byte 0x55) uart2_frame_state FRAME_DATA; else uart2_frame_state FRAME_WAIT_HEAD1; break; case FRAME_DATA: if (byte 0x7E) { uart2_frame_state FRAME_WAIT_HEAD1; // 处理完整帧 } else if (rx2_frame_len sizeof(rx2_frame_buf)) { rx2_frame_buf[rx2_frame_len] byte; } break; } }状态机的参数设置帧头可以定义成两个字节是为了降低误判率如果只需要单字节帧头状态可以合并为两个状态。rx2_frame_len在帧头识别成功后要清零否则上一次的数据会混进来。帧尾以外的其他字节如果和帧头相同比如数据里出现0xAA需要判断是否紧跟0x55如果是就当新帧处理。这个细节不处理会出现数据错位。状态机的好处是处理到一半出错时不需要清空整个缓冲区只要回到等待帧头状态即可。另一种常见方案是长度字段在帧头后加一个字节表示有效数据长度比如AA 55 04 01 02 03 04 7E其中04表示后面有 4 个数据字节。这种方案状态机会多一个FRAME_LEN状态记录收到指定字节数后直接跳到帧尾校验。这两种方案在双串口场景里都很好用推荐首选帧头 长度 校验比单纯帧头帧尾更不容易错判。4.3 双串口共用中断优先级要注意的实时性问题STM32F103 的 NVIC 支持抢占优先级和子优先级。USART1 的中断向量是USART1_IRQnUSART2 是USART2_IRQn。双串口同时工作时如果优先级设置不当其中一个串口可能长时间得不到响应。比如 USART1 抢占优先级设成 0USART2 设成 1当 USART2 正在接收时来了 USART1 中断USART1 会打断 USART2 的接收处理如果 USART2 的HAL_UART_Receive_IT还没处理完数据就会丢。我一般把两个串口的抢占优先级设成相同子优先级不同比如 USART1 抢占 1、子优先级 0USART2 抢占 1、子优先级 1。这样两个串口不会互相打断。如果配有 DMA还要把 DMA 中断优先级设置成不高于串口中断否则会出现“接收中断已经把数据放入缓冲区但 DMA 传输完成中断还没处理”的情况逻辑上是没问题但会增加延迟。在 HAL 库中中断抢占优先级初始化要放在HAL_UART_Init之后或者放在HAL_UART_MspInit中。错误地把 NVIC 设置在初始化之前虽然大多时候不影响但在极端时序下会导致第一个中断无法进入。另外一个容易被忽略的点是两个串口的HAL_UART_RxCpltCallback里都有可能重新调用HAL_UART_Receive_IT这个调用必须在中断上下文中执行不能延迟到主循环。很多人为了“减少中断占用时间”在回调里只设一个标志然后回主循环再开下一次接收。这样做在单串口时没问题双串口时一旦主循环被另一串口的数据阻塞接收就停了。所以必须坚持“处理一个字节后马上开下一次接收”的铁律处理的部分可以非常轻量比如只入队不解析。5. 调试记录与避坑清单现象-原因-解决5.1 串口一直接收乱码现象两个串口互相发送接收到的数据全是0xDF、0x0F之类无规律字节偶尔能对几个。原因波特率不匹配是最常见的但双串口里还有另一种情况USART1 的发送引脚 PA9 和 USART2 的接收引脚 PA3 都是复用推挽如果配置 GPIO 时没有逐个初始化PA2 或 PA3 被错误复用成 USART1 的信号就会导致数据串扰。解决检查 GPIO 复用配置每个串口独立配置引脚再检查波特率实际值和目标值是否一致可以用回环测试把 TX 和 RX 短接自发自收。如果回环正常但对外乱码那就是外部设备波特率或电平问题。5.2 第二个串口完全收不到数据现象USART1 收发正常USART2 打开串口助手后无论发什么都收不到。原因USART2 的时钟在某个初始化过程中被关掉了或者 GPIO 被重新初始化成普通输入。有些板载的调试串口默认占用 USART2 的引脚比如 PA2 和 PA3 连接了板上的电平转换芯片跳线没有断开。解决先量 PA2 和 PA3 的静态电平如果都是 0V检查板子原理图确认跳线是否把这两个引脚连接到别的器件然后检查HAL_UART_Init返回值如果返回HAL_BUSY说明外设没配置干净需要先DeInit再重新初始化。5.3 接收中断里丢字节现象每秒发送大量数据时比如 1000 字节接收端每帧少几个字节而且丢的位置不确定。原因在HAL_UART_RxCpltCallback里做了解析或转发导致中断耗时太长接收寄存器在新字节到达前还有数据没有取走。解决回调里只做“存缓冲区 重新开启Receive_IT”两件事解析和转发放到主循环。如果用了 DMA检查 DMA 循环模式和空闲中断是否都开启。单独使用普通接收中断时中断响应时间要优于一个字节时间115200 波特率下单字节时间为 86.8 微秒中断回调里超过这个时间就会丢。5.4 发送阻塞导致整个程序卡死现象转发数据过程中主循环偶尔卡住之后外部指令完全没反应。原因某个串口的发送队列满了而发送中断发送速度或者服务方没有及时取走数据导致队列阻塞。常见做法是使用阻塞发送HAL_UART_Transmit发送 1 个字节也有一定耗时一旦在中断里调用就会让中断挂起。解决发送统一走队列机制如果队列满了丢弃最旧的数据而不是阻塞等待中断发送的回调里继续发送下一字节不要在主循环里清队列。对于实时性要求高的场景可以适当提高串口中断优先级缩短发送队列的刷新时间。5.5 复位后第一次收到的第一帧数据总是丢现象上电后发送端先发一帧接收端收不到等发送端再发一帧数据正常。原因初始化完成后HAL_UART_Receive_IT已经开启但此时 gpio 刚完成配置外部设备已经提前发送了一帧帧头被接收中断取走后放入缓冲区但程序主循环还没来得及处理下一帧的数据已经在缓冲区里积累。另一个相关原因是使用 HAL 库时HAL_UART_Receive_IT注册是在初始化里但上电瞬间发送端和接收端的电平都在变化可能触发一次假中断。解决上电后先清空接收缓冲区延迟 200ms 再让外部设备发数据。可以这样处理初始化末尾调用一个clear_rx_buf()函数把环形缓冲区所有指针清零并且忽略这段时间产生的标志。6. 收尾一个让双串口更稳的小技巧用串口空闲中断可以大幅降低 CPU 的解析负担。所谓空闲中断是指串口在收到一字节后总线出现一段空闲时间超过一个字节时长就断言当前一帧接收完成。在双串口转发场景中很多数据帧不定长但帧与帧之间存在间隔。使用空闲中断配合 DMA可以让 DMA 自动收集一帧数据然后由程序处理。具体做法是配置 DMA 为循环模式接收缓冲区一般为 256 字节或 512 字节每次 DMA 传输后通过HAL_UARTEx_ReceiveToIdle_DMA启动接收。收到数据后寄存器里会有一个NDTR值表示还有多少空间没填用“缓冲区总大小 - NDTR”就知道这一次收到多少字节。两个串口同时使用 DMA 时注意区分 DMA 通道USART1 的 DMA 接收通道是 DMA1_Channel5USART2 的 DMA 接收通道是 DMA1_Channel6。如果使用了HAL_UARTEx_ReceiveToIdle_DMA接收中断回调里会有两个入口一个是半传输回调一个是空闲回调实际处理数据应该在空闲回调里。void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart-Instance USART1) { memcpy(uart1_rx_frame, uart1_dma_buf, Size); // 处理完整帧 } }这个技巧让我在调试跑 921600 波特率的双串口转发时再也没有出现过丢字节。原因很简单普通中断每收到一个字节都要进一次中断而空闲中断加 DMA 让整个帧只在完成时进一次中断。但是要注意空闲中断在一直接收数据、不定时发数据的环境下会失效因为总线始终不空闲所以适合通信间隔明显的场景。回忆起我最早调双串口时就是因为忘了在接收回调里重新开启HAL_UART_Receive_IT浪费了一整天。解决这个问题后双串口收发变得非常简单。后来我每次写完代码都先自查三件事一是两个串口的 NVIC 优先级是否冲突二是接收中断回调是否只做轻量操作三是发送是否用了队列而不是直接在中断里阻塞。做到这三点双串口基本不会出怪问题。希望帮到你。本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号