简介:一份基于STM32F103的双串口收发工程源码包,面向嵌入式入门开发者以及需要同时使用多串口完成数据交互与监控调试的工程师。资源以结构体方式封装串口资源,覆盖时钟配置、波特率设置、数据位/停止位/校验位、收发模式以及NVIC中断使能等关键环节,并提供完整收发示例与串口回环验证逻辑。压缩包内含143个文件,包含31个C源文件、34个H头文件、2个汇编启动文件、1个HEX烧录文件,以及uvprojx工程配置、sct链接脚本、map映射表等,整体大小2.21MB,可直接用Keil打开工程对照学习。已有2692人学习下载,适合在设备控制、多机通信、串口日志监控等场景中作为基础模板,能帮助读者理解USART初始化流程、结构体驱动的封装思路以及双串口协同工作的实现方法,快速搭建可调试的收发功能。
1. 双串口收发:为什么单串口能跑通,双串口就翻车
很多开发者在 stm32f103 上调单串口收发时都挺顺利:配置一个 USART、开一个接收中断、把数据塞进缓冲,收发就通了。但把第二个串口加上去,问题立刻冒出来——两个串口单独发都正常,一旦同时收发,数据就丢、错位,甚至整个程序卡死在某个中断里。“调试通过”这四个字,指的不只是两个串口各自能发能收,而是两套中断、两组缓冲、两种波特率放在一起还能稳定协作。这篇笔记把实际调过的方案拆开:从时钟配置、中断收发框架,到联调方法和踩坑记录,给出一套能在 stm32f103 上直接照抄的双串口收发实现。
2. 双串口底层准备:时钟、GPIO 复用与两个 USART 的初始化
2.1 时钟差异决定了两个串口的初始化顺序
stm32f103 的两个常用串口挂在不同的总线时钟上。USART1 挂在 APB2 上,典型时钟 72MHz;USART2 和 USART3 挂在 APB1 上,典型时钟 36MHz。这个差异直接决定了两件事:
- 使能时钟时要用不同的函数:USART1 用
RCC_APB2PeriphClockCmd,USART2 用RCC_APB1PeriphClockCmd。 - 同样配置 115200 波特率,两个外设内部的分频系数完全不同。库函数会自动算,但如果外部晶振不准,两个串口的实际波特率误差方向不一样,联调时容易发现一个串口正常、另一个串口间歇性丢字节。
初始化顺序我习惯固定成:先使能 GPIO 和 AFIO 时钟,再使能串口时钟,然后配 GPIO 复用,最后配 USART。如果先开串口再配 GPIO,上电瞬间引脚状态不稳定,偶尔会把第一帧数据收乱。
/* GPIOA 与 AFIO 时钟:USART1 和 USART2 都用 PA 口,所以一并打开 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); /* USART1 在 APB2 */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); /* USART2 在 APB1 */这段代码有两个容易遗漏的坑。第一个是RCC_APB2Periph_AFIO很多人不写,当你不做重映射、用默认引脚时确实不报错,一旦后续代码里用了重映射函数,系统会卡死在断言上。第二个是 USART1 和 USART2 的时钟函数不能搞混,把 USART2 的时钟误写成RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART2, ENABLE)在编译期不会报错,但运行时串口完全不工作,排查起来很费劲。
2.2 GPIO 复用:六根引脚先认清楚
双串口最省事的引脚分配是:USART1 用 PA9(TX)、PA10(RX),USART2 用 PA2(TX)、PA3(RX)。六根引脚互不冲突,不需要打开重映射,接线也方便。如果要考虑板子布局,USART1 还可以重映射到 PB6/PB7,但默认引脚方案在绝大多数场景下够用。
TX 引脚要配置成复用推挽输出,RX 引脚配置成浮空输入或上拉输入。如果外部电路已经做了上下拉,就配成浮空输入;如果是直接和模块对接没有额外电阻,我一般配成上拉输入,避免空闲时电平悬空产生乱码。
GPIO_InitTypeDef gpio; /* USART1_TX: PA9 */ gpio.GPIO_Pin = GPIO_Pin_9; gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); /* USART1_RX: PA10 */ gpio.GPIO_Pin = GPIO_Pin_10; gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &gpio); /* USART2_TX: PA2 */ gpio.GPIO_Pin = GPIO_Pin_2; gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); /* USART2_RX: PA3 */ gpio.GPIO_Pin = GPIO_Pin_3; gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &gpio);这段配置里最常见的错误是把 PA9 和 PA2 都写成同一个结构体,然后只改 Pin 不改 Mode,结果两个 TX 引脚配置不一致。实际调试时不会报错,但示波器抓波形会发现其中一个 TX 输出的电平摆幅不对。另一个值得注意的点是 GPIO_Speed 对串口这种低速外设没有实质影响,统一设 50MHz 即可,不要在这里纠结速度档位。
2.3 USART 参数初始化:同一个结构体别复用
两个串口的初始化逻辑完全一致,只是外设基地址不同。需要注意数据位、停止位、校验位要保持一致,硬件流控一般关闭,省下 CTS/RTS 两根引脚,三线接法就能和串口助手通信。
USART_InitTypeDef usart1_cfg; usart1_cfg.USART_BaudRate = 115200; usart1_cfg.USART_WordLength = USART_WordLength_8b; usart1_cfg.USART_StopBits = USART_StopBits_1; usart1_cfg.USART_Parity = USART_Parity_No; usart1_cfg.USART_HardwareFlowControl = USART_HardwareFlowControl_None; usart1_cfg.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, &usart1_cfg); USART_InitTypeDef usart2_cfg; usart2_cfg.USART_BaudRate = 115200; usart2_cfg.USART_WordLength = USART_WordLength_8b; usart2_cfg.USART_StopBits = USART_StopBits_1; usart2_cfg.USART_Parity = USART_Parity_No; usart2_cfg.USART_HardwareFlowControl = USART_HardwareFlowControl_None; usart2_cfg.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART2, &usart2_cfg); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); USART_Cmd(USART1, ENABLE); USART_Cmd(USART2, ENABLE);波特率参数说明:两个串口可以不同,比如一个 9600 对接老设备,一个 115200 对接传感器模块。库函数会自动根据 PCLK 计算分频,不需要手算。两个串口都用 115200 时保持参数一致最省心;波特率不同时,高波特率的那个串口中断更密集,在后面的 NVIC 配置里要给更高优先级。硬件流控字段按上面代码保持None,如果误开了 RTS/CTS,对方设备不支持时会直接收不到数据。
提示:USART_Init 内部有断言检查参数是否合法,若在初始化后修改某个外设配置,例如把 USART1 改成 8 位数据位,必须重新调用一次 USART_Init,不能只改结构体字段。
3. 中断收发框架:环形队列与双串口中断优先级设计
3.1 为什么选择中断接收加查询发送
双串口收发的方案有几种常见组合:轮询收发、中断接收加查询发送、中断收发、DMA 接收。轮询收发在裸机系统里最不推荐,两个串口同时来数据时,主循环只能一个个轮询,高波特率下很容易丢。中断接收加查询发送是 stm32f103 上最平衡的方案:接收靠硬件中断保证不丢字节,发送的数据量一般不大,查询发送足够。
查询发送的代价是发送过程中 CPU 会被占用,但发送一个字节在 115200 波特率下大约耗时 87us,发几十字节也就几毫秒,对大多数控制类应用完全可接受。真正有压力的是接收方向,尤其两个串口同时涌入数据时,任何一个中断处理慢了,另一个串口的数据寄存器就会被覆盖。
所以核心设计原则是:中断服务函数里只做一件事——把收到的字节放进属于自己的环形队列,然后立刻返回。所有协议解析、数据转发、逻辑处理全部放到主循环。这个原则在双串口场景里比单串口更严格,因为两个中断之间会互相抢占。
3.2 环形队列:写代码前先定好容量
双串口需要两个独立的队列,每个串口一个读指针、一个写指针。我不会用同一个结构体变量初始化两次,而是定义两个全局实例,避免中断函数里把数据塞错队列。
typedef struct { uint8_t data[256]; /* 队列存储区 */ volatile uint16_t head; /* 写指针,中断中更新 */ volatile uint16_t tail; /* 读指针,主循环中更新 */ } RingBuf; RingBuf uart1_rx = { .head = 0, .tail = 0 }; RingBuf uart2_rx = { .head = 0, .tail = 0 }; int RingBuf_Write(RingBuf *rb, uint8_t byte) { uint16_t next = (rb->head + 1) % sizeof(rb->data); if (next == rb->tail) { return -1; /* 队列满,丢弃该字节 */ } rb->data[rb->head] = byte; rb->head = next; return 0; } int RingBuf_Read(RingBuf *rb, uint8_t *byte) { if (rb->head == rb->tail) { return -1; /* 队列空 */ } *byte = rb->data[rb->tail]; rb->tail = (rb->tail + 1) % sizeof(rb->data); return 0; }设计要点有三个。第一,head和tail必须用volatile uint16_t。head在中断里写、主循环里读,tail在主循环里写、中断里读,没有 volatile 修饰时编译器可能优化掉一次读取,出现极其诡异的偶发丢数据。第二,判断队列满的条件是(head + 1) % size == tail,所以 256 字节的数组实际可用 255 字节。第三,返回 -1 表示满或空,调用方要处理这个返回值。中断函数里队列满时直接丢弃该字节并返回,这是最简单可靠的策略,后续如果担心丢数据,可以在这里加一个溢出计数器。
这个环形队列没有加锁。它在单写者单读者的模型下是安全的:写者只有对应串口的中断,读者只有主循环。双串口各自有独立的队列,也不会互相竞争。如果用到了 RTOS 并且把读操作放在多个任务里,就必须加临界区保护了。
3.3 两个中断服务函数:只进队列,不做解析
void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { uint8_t ch = USART_ReceiveData(USART1); RingBuf_Write(&uart1_rx, ch); } } void USART2_IRQHandler(void) { if (USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) { uint8_t ch = USART_ReceiveData(USART2); RingBuf_Write(&uart2_rx, ch); } }两个中断函数结构完全一样。USART_ReceiveData 会读取数据寄存器,读完之后 RXNE 位自动清零。这里的隐患是如果两个串口同时触发中断,优先级较低的那个中断会被挂起,只要中断函数足够短,挂起的延迟只有几个指令周期,不会丢数据。反过来,如果在中断里做协议解析、字符串拼接、延时等待,另一个串口的 RXNE 位在等待期间再次置位,数据寄存器被新数据覆盖,老数据就丢了。
主循环侧的处理方式:
uint8_t ch; while (1) { if (RingBuf_Read(&uart1_rx, &ch) == 0) { /* 处理 USART1 收到的数据 */ } if (RingBuf_Read(&uart2_rx, &ch) == 0) { /* 处理 USART2 收到的数据 */ } }两个队列轮流读取,优先级靠 NVIC 配置决定,而不是靠主循环里的轮询顺序。主循环读到了哪一帧数据就处理哪一帧,处理过程中新来的字节会继续被中断塞进队列,不会丢失。
3.4 NVIC 优先级:双串口稳定工作的关键
NVIC 配置是整个双串口方案里最容易翻车、也最常被忽视的部分。优先级分组的设置必须在 main 开头做一次,推荐分组 2,也就是 2 位抢占优先级、2 位子优先级,每个优先级数值范围 0 到 3。
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitTypeDef nvic; nvic.NVIC_IRQChannel = USART1_IRQn; nvic.NVIC_IRQChannelPreemptionPriority = 0; nvic.NVIC_IRQChannelSubPriority = 0; nvic.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&nvic); nvic.NVIC_IRQChannel = USART2_IRQn; nvic.NVIC_IRQChannelPreemptionPriority = 1; nvic.NVIC_IRQChannelSubPriority = 0; nvic.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&nvic);这里给 USART1 抢占优先级 0,USART2 抢占优先级 1。含义是:两个串口同时来中断时,USART1 优先进;USART2 中断执行过程中 USART1 来了能打断它;反过来不行。这样配置的理由是 USART1 挂在 APB2 上,数据吞吐能力更高,在多数项目里做主串口用。
优先级怎么选:如果两个串口波特率一样,可以把抢占优先级设为相同,让硬件按照中断号仲裁;如果波特率不同,高波特率的那个串口一定要抢占优先级低,否则它的中断服务频率高但优先级低,和低频串口有重叠时,高频串口的队列会连续溢出。波特率差距大时,这个影响非常明显。
注意:NVIC_PriorityGroupConfig 只能调用一次。第二次调用会把整个系统所有中断的优先级分组强制修改,如果之前其他外设已经初始化了中断,这会引发意料之外的中断行为。
4. 双串口联调:回环、交叉回环与波形验证
4.1 回环测试:一根杜邦线先验证单路
把 USART1 的 TX 和 RX 用杜邦线短接,串口助手向 USART1 发一个字节,程序把它原样发回去,看是否收到同样的字节。这个测试用来验证单个串口从物理通路、中断到环形队列的整条链路。
uint8_t ch; while (1) { if (RingBuf_Read(&uart1_rx, &ch) == 0) { while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); USART_SendData(USART1, ch); } }这里发送前等待USART_FLAG_TC,不是USART_FLAG_TXE。TXE 表示数据已经从数据寄存器移入移位寄存器,但最后一个字节的停止位可能还在发送中;TC 表示整个字节包括停止位已经发送完成。回环测试里如果用 TXE,串口助手显示的回显会少最后一位或乱码,这个细节在后续避坑章节还会展开。USART2 用同样的方式单独测一遍,确认两路都通。
4.2 交叉回环:验证两个串口的中断队列没有互相污染
单路回环通过后,把 USART1 的 TX 接到 USART2 的 RX,USART2 的 TX 接到 USART1 的 RX,形成交叉连接。然后写一段转发逻辑:
uint8_t ch; while (1) { /* USART1 收到的数据从 USART2 发出去 */ if (RingBuf_Read(&uart1_rx, &ch) == 0) { while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET); USART_SendData(USART2, ch); } /* USART2 收到的数据从 USART1 发出去 */ if (RingBuf_Read(&uart2_rx, &ch) == 0) { while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); USART_SendData(USART1, ch); } }外部设备接在 USART1 上发数据,数据会从 USART2 出来;接在 USART2 上发数据,从 USART1 出来。两边同时发长数据,观察是否出现丢字节、错位。这个测试专门验证两个中断服务函数是否拿错了队列。如果代码里服务函数写反了,单路回环测不出来,交叉回环立刻暴露。
交叉回环通过后,我还有一个小习惯:给两个串口的数据加不同的帧头,比如 USART1 发[U1]开头,USART2 发[U2]开头。联调时如果分不清哪路收发的数据串了,看帧头一眼就知道。
4.3 逻辑分析仪抓波形:验证波特率误差在可接受范围
回环测试只证明数据能通,不能证明波特率准确。用逻辑分析仪的串口解码功能,接在 TX 引脚上抓一段数据,它会自动识别波特率。识别出的值和配置值偏差在 2% 以内是安全的;超过 2%,长时间收发时会出现间歇性乱码。
误差来源八成不在 USART 配置里,而在系统时钟。stm32f103 使用外部 8MHz 晶振配 PLL 到 72MHz 时精度最好;使用内部 RC 时钟时误差可能到 1% 到 2%,两个串口的实际波特率都会偏移。
# 逻辑分析仪抓取后常见结论: # 配置 115200,实测 114980,偏差 0.19%,安全 # 配置 115200,实测 118900,偏差 3.2%,需要检查晶振与 PLL 配置如果实测偏差偏大,先检查SystemInit是否正确执行、外部晶振是否起振,再用示波器测 MCO 引脚输出的系统时钟来做最终确认。这一步能省下后面大量排查时间,不要跳过。
5. 双串口调试避坑:5 个翻车现象与排查路径
5.1 第二个串口完全收不到数据
现象:USART1 收发正常,USART2 在串口助手里什么也收不到,发送也没反应。
原因:最常见的是时钟使能漏了。USART2 挂在 APB1 上,有些人写完代码只检查了 GPIO 时钟,漏掉RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE)。另一个原因是引脚配置错误,PA3 被配成了推挽输出,RX 引脚不能正常读取电平。
解决:逐个查三处——RCC 时钟是否使能、GPIO 模式是否为输入、USART_Cmd 是否执行。我通常用调试器在USART_Init前后检查USART2->CR1寄存器,如果初始化后 CR1 的 UE 位没有置 1,说明初始化没有真正执行,一般是时钟或结构体参数问题。这个问题在单串口项目里不会出现,是双串口项目特有的第一步坑。
5.2 单路正常,双路同时收发丢首字节
现象:单路回环测试都通过,两路同时收发时,其中一路收到的每一段数据第一个字节丢失,后面内容完好。
原因:优先级配置不合理。低优先级的串口中断正在执行时,高优先级串口中断插进来,低优先级串口的 RXNE 中断被延迟;如果延迟时间超过了一个字节的传输时间,数据寄存器被新字节覆盖,产生溢出错误 ORE,后续 RXNE 中断被阻断,表现为丢首字节。
解决:把频繁收发的一路优先级配成抢占优先级 0,另一路配 1。也可以在中断服务函数里主动检查溢出标志并清掉:
if (USART_GetFlagStatus(USART2, USART_FLAG_ORE) != RESET) { USART_ReceiveData(USART2); /* 读 DR 同时清掉 ORE 与 RXNE */ }这段代码要放在 RXNE 分支之前。ORE 发生时数据寄存器可能已经有新字节,读一次 DR 能恢复中断接收能力,代价是最多丢一个字节。对控制类协议来说,丢一个字节,附加上下文里的帧头校验就能发现。如果一帧数据丢字节频繁,优先调优先级而不是在这里补。
5.3 用 TXE 判断发送,最后一个字节总丢
现象:发送一串数据,串口助手收到的内容末尾少一个字节。发送速度越快,丢的概率越高。
原因:USART_FLAG_TXE置位只表示数据已进入发送移位寄存器,停止位是否发完未知。如果发完最后一字节后立刻进入低功耗模式、关闭串口时钟或者切换波特率,最后一位停止位没送完,外部设备就不认为这字节完整。
解决:最后一个字节的发送要等待USART_FLAG_TC。批量发送时有两种做法,简单的是每个字节都等 TC,可靠但稍慢;更高效的是等 TXE 发送,最后一个字节再专门等 TC。裸机小数据量场景我直接用 TC,代码简单且行为一致。
void UART1_SendString(const char *s) { while (*s) { while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); USART_SendData(USART1, (uint8_t)*s++); } }5.4 一个串口进不了中断,另一个正常
现象:USART2 的中断收发正常,USART1 的接收中断从不触发;把主循环里的逻辑绕过中断,直接读USART1->DR又有数据。
原因:NVIC 配置问题。常见是两个串口复用了同一个 NVIC_InitTypeDef 结构体,只改了 PreemptionPriority 没改 IRQChannel,导致两次初始化写的是同一个中断通道;也有的是中断优先级分组在初始化函数里又调用了一次NVIC_PriorityGroupConfig,改变了整个系统仲裁方式。
解决:每个串口用独立的 NVIC_InitTypeDef 变量,IRQChannel 明确写上USART1_IRQn和USART2_IRQn。确保NVIC_PriorityGroupConfig只在 main 开头执行一次。在调试器里检查NVIC->ISER寄存器的对应位,能确认中断有没有真的被使能到。
5.5 环形队列收满 256 字节后,整个串口数据错乱
现象:短报文收发一切正常,连续发送几百字节后,后续数据全部错位,已经读出来的数据也出现乱码。
原因:head 或 tail 用了uint8_t类型,256 字节回绕时直接溢出变成 0,队列的判空判满逻辑全部失效。另一个原因是在 Keli 或 GCC 下开了char有符号优化,比较负数时逻辑反转。
解决:把 head 和 tail 统一为volatile uint16_t。队列大小保持 256 时,可以用位与运算替代取模,效率稍高:
#define RING_BUF_SIZE 256 uint16_t next = (rb->head + 1) & (RING_BUF_SIZE - 1);注意这个写法要求队列大小是 2 的幂,改成 512、1024 时同时要改掩码。这个坑最隐蔽的地方在于,单串口收发时即使 head 溢出也可能因为数据量小而不暴露,双串口联调时数据量一上去就原形毕露。
6. 双串口进阶:DMA 接收与帧边界处理
6.1 高波特率下把中断接收换成 DMA 空闲中断收帧
中断逐字节接收在 115200 波特率下没有压力,但到 460800 或 921600 时,每字节中断间隔只有 10us 左右,中断开销开始吃掉主循环时间。这个阶段我一般会切到 DMA 接收加空闲中断。DMA 把 USART 收到的数据直接搬进内存缓冲区,不进中断,只有检测到一帧传输结束(总线空闲)时才产生一次空闲中断。
DMA_InitTypeDef dma; dma.DMA_PeripheralBaseAddr = (uint32_t)&USART2->DR; dma.DMA_MemoryBaseAddr = (uint32_t)uart2_dma_buf; dma.DMA_DIR = DMA_DIR_PeripheralSRC; dma.DMA_BufferSize = 128; dma.DMA_PeripheralInc = DMA_PeripheralInc_Disable; dma.DMA_MemoryInc = DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; dma.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; dma.DMA_Mode = DMA_Mode_Circular; dma.DMA_Priority = DMA_Priority_High; DMA_Init(DMA1_Channel6, &dma); /* USART2_RX 对应 DMA1_Channel6 */ DMA_Cmd(DMA1_Channel6, ENABLE); USART_DMACmd(USART2, USART_DMAReq_Rx, ENABLE);USART1_RX 对应的通道是 DMA1_Channel5,USART1_TX 是 DMA1_Channel4,USART2_TX 是 DMA1_Channel7,这几个通道映射要记牢,写错之后数据不报错但收不到。DMA 接收开启后,原来的 RXNE 中断要关掉,否则会重复处理同一批数据。空闲中断 IDLE 用USART_GetITStatus(USART2, USART_IT_IDLE)读取,读完之后要连读一次 SR 和 DR 才能清掉标志。
6.2 粘包不是 DMA 的问题,是帧边界没定义好
DMA 循环模式收长数据时,缓冲区满了指针回绕,读侧要自己维护读位置,本质上还是环形队列的模型。真正容易翻车的是帧边界处理。如果协议本身没有固定帧头加长度字段,DMA 收进来的是连续的字节流,主循环根本不知道哪一帧结束。
我一般会在协议上做三层约束:固定帧头、包含长度字段、带累加和校验。空闲中断只能说明线上停止出现了一定时长的状态,可以作为分包参考,但不能完全依赖它。如果两个串口的数据都是同一套协议,那就准备两套解析状态机,分别在各自的数据缓冲上运行,解析完一个完整帧再触发应用层回调。这套思路比逐字节中断解析省太多 CPU,协议升级时只需要改解析状态机,不用动收发底层。
我在第一次把串口接收从中断切到 DMA 时,就是因为主循环里多了几个浮点运算,逐字节中断在 460800 波特率下开始感觉到卡顿。DMA 搬完数据后主循环每次只处理一帧,整个系统的节奏一下就清晰了。如果你现在还在 115200 波特率下跑,中断加环形队列完全够用;如果哪天把波特率提上去,记得先画一张 DMA 通道映射表再动手。希望帮到你。
本文还有配套的精品资源,点击获取