
最近在做一块基于STM32F030的小板子功能说起来很简单通过USART接收上位机下发的配置帧解析之后写入片内Flash掉电重启后再从Flash读出来恢复配置。听起来就是常规的参数存储功能但联调的时候踩了一个挺隐蔽的坑——只要程序在接收串口数据的窗口里顺便执行了Flash擦除或写入USART接收就丢数据而且丢得毫无规律。丢一两个字节还好关键是丢完之后整帧的CRC校验全错上位机不停重发越重发越乱。这篇文章就把这个问题从现象到根因再到解决的完整过程拆开讲一遍给同样在STM32F0这类Flash操作会阻塞CPU的平台上做串口通信的朋友做个参考。1. 问题现象好端端的串口一擦Flash就丢字节1.1 复现条件什么样的代码路径最容易触发先说清楚这个问题的触发环境避免上来就归因。我的板卡主控是STM32F030C8T6主频48MHz片内Flash 64KBUSART1工作在115200-8-N-1模式接收中断打开上位机每隔几百毫秒发送一帧64字节的配置数据。板子收到完整帧之后校验通过会把其中的关键参数存到Flash的最后一个扇区。问题在第一次联调就暴露了。测试流程是上位机先发一个进入参数配置模式的握手帧板子回复OK然后上位机以每帧几百毫秒的间隔连续下发大量配置帧板子每收完一帧就执行一次Flash擦写。现象是单独发一帧、写一次Flash数据完全正常连续快速下发时接收端偶尔出现一帧中缺失几个字节缺失位置不固定但大概率出现在Flash擦写动作之后的十几个字节内。如果擦写的是整页Page Erase丢字节的概率明显高于只写入几个字Program而且一旦CRC失败上位机立刻重发重发期间板子如果还在反复擦写Flash丢包率会进一步上升。还有一种更容易触发的场景升级固件的时候通过串口接收bin文件分块写入Flash。这个场景下Flash几乎是每个数据块都擦写一次串口接收和Flash操作全程交织在一起丢数据的现象更猛烈往往写不到一半收到的数据块提示校验失败整个升级流程中断。很多人在这个场景里还会误以为是自己协议栈写错了其实根子都在同一个地方。1.2 表象细节丢数据的分布规律我在排查之前先做了个简单的数据统计把接收到的每帧数据、CRC校验结果、以及当前是否正在执行Flash操作都打日志记录下来。统计了大概两百帧之后发现两条规律。第一丢字节不是均匀分布的而是集中在Flash操作结束后的第一个短窗口内。这个窗口的长度大概只有几毫秒但在这段时间里收到的字节整段消失不是乱码是像没收到一样干干净净地缺失。这说明不是信号干扰造成的位错误而是接收路径上确实没有把数据保存下来。第二Flash擦写动作比Flash写入动作更容易导致丢数据执行整片擦除Chip Erase时丢包概率几乎达到100%执行页擦除大约在60%左右只做双字写入在20%左右。这条规律非常重要因为它直接把怀疑对象指向了Flash操作的耗时长短——耗时越长丢得越厉害。第三把USART接收中断优先级提到最高也不能解决问题只是把丢数据的时机往后推了几个字节而已。这个现象说明问题的本质不是中断被低优先级任务抢占了而是CPU在某一段时间内根本没能力去响应中断。2. 根因挖掘Flash擦写期间中断为什么叫不应2.1 STM32F0的Flash操作到底有多慢很多人第一次接触STM32的Flash擦写都会忽略它的实时性代价。数据手册上只给了典型时间参数看起来不算太离谱但放进业务逻辑里就完全不是一回事。以STM32F030来说页擦除的典型时间大约20ms到40ms编程一个16位半字的典型时间大约几十一百微秒如果连续写很多个半字则每个半字都要耗费这个时间量级而且每写一个半字之前还要等待BSY标志清零。整片擦除就更夸张时间在几十毫秒到上百毫秒不等具体看芯片型号。这里说的时间不是普通函数调用那么简单。在Cortex-M0内核上CPU取指令和执行指令都要从Flash中读取而擦写动作发生时Flash控制器处于忙状态对Flash存储阵列的读操作会被硬件强行阻塞。也就是说如果你的程序代码本身就在Flash里执行擦除/编程函数期间CPU可能连下一条指令都取不出来整个执行流水线被冻住直到Flash控制器完成操作、释放总线。我用一个不严谨但很好理解的类比Flash擦写就像是图书馆里的管理员把唯一的一扇门堵住整理书架期间任何人都进不去哪怕你只是想去门口看一眼通知栏。CPU就是那个想进门的人中断请求就是门口有人按门铃可惜门被堵按再多次铃也白搭。2.2 USART接收侧只有一个临时工寄存器再看USART接收这件事。大多数标准串口外设在硬件上都有两级缓冲一个移位寄存器一个数据寄存器DR。移位寄存器负责把一个字节的每一位从RX引脚上收进来收完整了再把整个字节搬运到数据寄存器同时置上RXNE标志通知CPU数据到了快来取。问题在于这个数据寄存器容量只有1字节。它不是FIFO没有几级深的缓冲相当于门口只放了一个临时工——第一个数据到了临时工拿着数据等着CPU来取第二个数据跟着到了临时工手里还攥着上一个数据新数据根本没地方放只能触发溢出错误ORE把新字节丢掉。在正常接收时CPU只要在下一个字节到达之前读完DR就不会出问题。115200波特率下一个字节的传输时间大约是86.8微秒10位起始位1位数据8位停止位1位换算成48MHz主频就是大约4166个CPU周期而中断响应加简单读取DR的代码通常只需要几微秒时间非常充裕。可一旦Flash擦写把CPU冻结了20毫秒等于这个临时工要干等20毫秒别说一个字节两百多个字节都能把门口堵死。2.3 最关键的时间窗口RXNE置位到读出之间的空隙把上面两块拼接起来就得到了丢数据的关键机制。数据到达DR后RXNE置位中断控制器把中断请求挂起来但CPU此刻被Flash操作卡住取不出中断向量也执行不了中断服务程序。此时DR里的数据只能一直躺在寄存器里。等Flash操作完成CPU恢复执行首先要把当前这条被卡住了一段时间的指令跑完然后才能响应中断。可就在这个RXNE已置位、程序还没读取DR的窗口里又一个新字节到达了直接触发ORE数据被丢弃。为什么我会说越过中断优先级也救不回来因为中断优先级解决的是多个中断同时等待时的裁决顺序它没法解决CPU压根没法取指的问题。哪怕你把USART中断设成最高优先级CPU被Flash控制器冻结的时候中断照样进不去。有一点值得多说一句STM32F0的参考手册里对ORE异常的处理有明确要求——发生溢出后必须按顺序读SR寄存器再读DR寄存器才能把ORE标志清除否则后续接收会一直处于异常状态。我见过不少工程师只加了读DR、没读SR的操作导致机制上已经丢过数据之后串口就像半堵了一样连续丢数据其实不是半堵是ORE标志一直没被清干净。3. 排查链路从怀疑硬件到锁定Flash操作的全过程3.1 第一步排除波特率误差、接线和供电这种偶发丢字节的问题第一反应往往是检查硬件。我先用示波器量了USART1的RX引脚波形也检查了上位机的串口助手波特率设置确认两边都是115200没有偏。紧接着用逻辑分析仪抓了RX引脚上的完整波形抓下来之后和上位机发送的原始数据逐位对比发现数据在物理链路上一个位都没少波形完整、电平干净。这基本排除了RS232/RS485电平转换芯片异常、地线干扰、波特率漂移这些常见的硬件嫌疑。供电我也顺手查了板子用3.3V LDO供电示波器上看没有明显跌落Flash擦写瞬间的电流波动也不至于让主控复位。所以硬件这条线很快被排除了——问题一定出在软件接收路径上。3.2 第二步用抠代码做对照实验排硬件之后我开始做软件对照实验。方法很朴素先把Flash擦写函数临时注释掉用空循环延时模拟相同的时间开销观察串口接收是否丢数据。结果很有趣用空循环延时模拟相同时间时数据完全正常。这立刻让我意识到Flash操作本身和普通延时不一样它不只是耽误时间还附带了冻结CPU取指通道的效果。我又做了一组对照代码从Flash里执行时在擦写前后翻转一个GPIO再把同样一段擦写代码用启动时拷贝的方式放到SRAM里执行同样翻转GPIO。结果前者在擦写期间出现了近20毫秒的GPIO翻转迟滞后者翻转非常及时。这个对照直接证明了Flash操作卡的是取指通道而不只是CPU算不过来。3.3 第三步GPIO翻转测出真实中断延迟为了拿到直接的证据我把USART接收中断服务程序的第一行代码设成翻转PB1把Flash擦写包裹在PB0翻转之间然后用逻辑分析仪同时观察PB0、PB1和RX引脚三路信号。正常情况下的时序是RX脚出现起始位下降沿紧接着几十微秒内PB1就会有一个翻转说明中断响应及时。但一旦PB0进入高电平Flash擦写中RX上连续来了好几帧数据PB1纹丝不动直到PB0拉低擦写结束之后大约几个微秒PB1才猛翻一下。这个猛翻一下其实只处理了最后一次进中断时的数据之前那些到达的数据早就被ORE吃掉了。这个实验把问题定性得明明白白不是UART外设坏了不是中断配置错了是Flash擦写期间CPU丧失了响应中断的能力而UART又只有1字节的缓冲没有能力扛过这段冻结期。3.4 第四步查DMA是不是可用在定方案之前我顺手查了一下STM32F030的DMA资源。F030是有DMA控制器的1个DMA有5个通道其中USART1_RX可以挂接到某个通道上支持循环模式。这一点很重要因为如果芯片本身连DMA都没有那下面的方案就要换思路了。确认可用之后我心里基本有了底用DMA把数据从UART外设搬到SRAM绕开CPU取指被冻结的死局。4. 正解落地用DMA环形接收把数据藏到内存4.1 方案比选关中断、重传、DMA分别怎么选在打磨DMA方案之前我先把网上讨论较多的几种招数对比了一遍给它们各自的适用场景排了个序。关中断法Flash操作前关闭USART接收中断操作后重新打开。这个办法在思路上就有问题关中断只会让数据丢得更干净连ORE中断都进不来唯一的好处是你知道丢的是哪一段。如果配合上位机按块重传的机制勉强能接受但吞吐量很难看。延长接收超时把帧与帧之间的空闲时间拉长给CPU留出足够的Flash操作时间。这在演示Demo里管用但实际项目中一旦数据量增大、交互频率提高一样会撞上。治标不治本。上位机协议重传丢帧之后请求重传是最基础的可靠性兜底。我建议任何时候都保留这层机制但它不应该成为你处理Flash阻塞问题的唯一手段否则通信效率会随着Flash操作频率急剧下降。DMA接收环形缓冲区把接收动作从CPU手里剥离开由外设直接通过DMA搬进SRAM。Flash擦写期间CPU虽然冻结但DMA照常工作数据一字节都不会少。这是我在这个项目里最终采用的办法。方案是否需要CPU参与丢数据风险复杂度适用场景中断读取是高低对实时性要求极低的场景Flash操作前关中断是必然丢低极少量数据配合重传延迟Flash操作时间是中低演示、压力和频率极低DMA循环接收否极低中通信与存储并存的通用场景硬件RWW/双Bank无需处理可避免取决于选型新项目选型时可考虑4.2 DMA循环模式空闲中断的搭建流程DMA方案的关键思路是让DMA始终在后台把USART收到的每个字节拽进SRAMCPU什么时候去处理都不会丢数据但前提是你得知道哪些是新数据。我的做法是DMA循环模式配合USART空闲中断IDLE来切帧。DMA初始化部分核心配置如下#define RX_BUF_SIZE 512 uint8_t dma_rx_buf[RX_BUF_SIZE]; void USART1_DMA_RX_Init(void) { // 1. 打开DMA时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 2. 配置DMA通道选择USART1_RX作为请求源 DMA_InitTypeDef dmaInit; dmaInit.DMA_PeripheralBaseAddr (uint32_t)USART1-RDR; // 外设数据寄存器地址 dmaInit.DMA_MemoryBaseAddr (uint32_t)dma_rx_buf; // 内存目标地址 dmaInit.DMA_DIR DMA_DIR_PeripheralSRC; // 外设到内存 dmaInit.DMA_BufferSize RX_BUF_SIZE; // 缓冲区大小 dmaInit.DMA_PeripheralInc DMA_PeripheralInc_Disable; // 外设地址不递增 dmaInit.DMA_MemoryInc DMA_MemoryInc_Enable; // 内存地址递增 dmaInit.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; dmaInit.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; dmaInit.DMA_Mode DMA_Mode_Circular; // 循环模式重点 dmaInit.DMA_Priority DMA_Priority_High; dmaInit.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel3, dmaInit); // 实际通道号按芯片型号配置 // 3. 使能DMA通道 DMA_Cmd(DMA1_Channel3, ENABLE); // 4. 使能USART的接收DMA请求 USART_DMACmd(USART1, USART_DMAReq_RX, ENABLE); }需要注意STM32F0的DMA请求源和DMA通道的映射需要查具体型号的参考手册比如STM32F030C8T6上USART1_RX对应的DMA通道可能是DMA1_Channel3但换成F03xx的其他型号或F1系列可能就变了。这里代码只作为结构示例不要直接照搬通道号。接收侧用IDLE中断来判断一帧是否结束。当DMA把一串数据放进缓冲区后总线空闲的瞬间USART会置IDLE标志触发中断。在中断中做的事情是读取DMA当前剩余计数计算从上次处理到现在到底收到了多少新字节然后搬运到应用层可用的环形缓冲区或者直接交给协议解析函数。volatile uint16_t last_remain RX_BUF_SIZE; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_IDLE) ! RESET) { // 读SR后读DR清除IDLE标志标准库/寄存器惯例 USART_ReceiveData(USART1); USART_ClearITPendingBit(USART1, USART_IT_IDLE); // 获取当前DMA剩余未传输计数 uint16_t remain DMA_GetCurrDataCounter(DMA1_Channel3); uint16_t received (RX_BUF_SIZE - remain RX_BUF_SIZE - last_remain) % RX_BUF_SIZE; // 把新收到的 received 个字节从 dma_rx_buf 中取出写入环形缓冲 // 这里需要根据 last_remain 计算起始索引具体实现可查环形缓冲范式 ring_buffer_write_byte_stream(...); last_remain remain; } // 注意ORE溢出错误也可以在这个中断里顺手清掉 if (USART_GetITStatus(USART1, USART_IT_ORE) ! RESET) { USART_ReceiveData(USART1); USART_ClearITPendingBit(USART1, USART_IT_ORE); } }这段代码里的received计算方式在循环模式下有一个很容易写错的细节DMA剩余计数是递减的数据到达时剩余计数从RX_BUF_SIZE往下减减到0后循环模式会自动重载成RX_BUF_SIZE继续减。所以新数据量不能简单用RX_BUF_SIZE - remain算还必须结合上一次的last_remain做回绕处理。我把公式写成(RX_BUF_SIZE - remain RX_BUF_SIZE - last_remain) % RX_BUF_SIZE是考虑了两段都做归一化之后取差的逻辑实际项目里建议把它封装成独立函数并且用足够大的无符号类型存中间结果避免溢出。4.3 这套方案为什么能扛住Flash擦写DMA方案的精髓不是让CPU更快而是让CPU不需要在数据到达的瞬间参与搬运。USART收到一个字节后RDR寄存器就绪DMA控制器收到硬件请求自动做一个从RDR复制到SRAM的动作全程不经过CPU执行任何指令。Flash擦写期间CPU在等待取指但DMA控制器是独立于CPU总线的在F030上是独立的DMA总线可以访问SRAM它照样执行搬运。所以哪怕CPU被冻结20毫秒UART来的数据也会一个接一个地被DMA接住存进内存等到擦写结束、CPU恢复执行再慢慢从缓冲区里取出来分析。有人会问DMA搬数据用的数据总线会不会和CPU访问Flash的总线冲突在STM32F0这种低端Cortex-M0芯片上DMA和CPU共用系统总线的情况是存在的DMA的每个节拍确实会占用总线的带宽但DMA单字节搬运通常只需要几个周期优先级也较高对CPU的影响远小于中断被冻结的20毫秒影响。实测下来开启DMA接收后Flash擦写对通信的干扰几乎可以无视。还需要提醒一点即使数据被DMA搬到了内存IDLE中断本身仍然会被Flash操作延迟触发。但因为数据已经在缓冲区里了IDLE中断晚几十毫秒触发只是导致应用层晚点拿到数据不会再丢字节。这一点对理解整个方案很重要——DMA解决的是数据存储的问题而不是中断实时响应的问题。4.4 实际调试DMA接收时容易踩的坑第一缓冲区大小的选择。115200波特率下每字节约86.8微秒20毫秒的Flash页擦除窗口里大约会到达230个字节整片擦除可能上百毫秒会到达上千个字节。如果RX缓冲区只有64字节DMA一圈绕过之后之前的数据就会被新数据覆盖应用层还没来得及处理就被冲掉了。我建议缓冲区至少要比单次最长Flash操作期间到达的字节数大2倍而且尽量取2的幂方便做索引掩码。我的配置里用了512字节实际跑下来余量足够。第二ORE错误的处理不能省。DMA把RDR数据搬走后正常情况下不会产生ORE因为数据一直在被清空。但如果DMA还没配置好、或者DMA通道因为某种原因停止了UART接收端照样会溢出。我在IDLE中断里顺便处理ORE异常读SR再读DR把标志清掉防止故障后卡死。第三环形缓冲区的读写指针务必加上volatile。DMA中断和应用层主循环同时访问环形缓冲头尾指针不加volatile的话编译器优化很可能把读取结果缓存在寄存器里导致应用层看到的指针一直不变卡在好像没数据的状态。这个问题非常隐蔽我调试时一度以为是DMA没跑最后翻汇编才发现是指针被优化了。第四如果DMA接收的数据要给协议解析模块用建议在拷贝数据时临时关一下临界区保护。确切地说就是处理环形缓冲区时把DMA中断里的写入和主循环里的读取用临界区包起来避免一边读一边写导致索引错乱。虽然STM32F0的Cortex-M0没有硬件开关全局中断的PRIMASK之外更细粒度的机制但进出临界区的时间极短影响可忽略。5. 更深一层从RAM取指、分批擦写等进阶规避办法5.1 把中断向量表和ISR放到RAM执行DMA方案已经解决了问题但还有一个技术方向值得了解如果Flash操作无法避免能不能让CPU在Flash操作期间依然活着答案是把代码和中断向量表放到SRAM里执行。Cortex-M0内核支持通过SYSCFG的MEM_MODE位把SRAM重映射到地址0x00000000这样中断向量表可以从SRAM读取。把USART中断服务程序也放到SRAM段中断发生时CPU直接从SRAM取指令执行不依赖Flash。这种方法在一些bootloader项目里很常见因为bootloader经常要擦写应用区Flash同时又要保持通信接口不被中断。但这种做法有几个前置条件不是改一个链接脚本就完事。第一ISR及其调用的所有函数都必须放进SRAM否则一旦ISR里调用了Flash里的函数取指还是会卡住第二SRAM容量有限放不了太多代码所以ISR要尽量精简只做最基本的搬数据动作第三重映射之后要仔细处理启动代码和分散加载文件工程配置复杂度明显上升。对于大多数存几个参数这类场景我个人认为DMA方案已经足够没必要为了一个偶发操作引入这么重的工程改造。5.2 更温和的做法错峰、分批、缩短擦写时间如果不想动DMA也不想上RAM执行这种重型手段至少可以在业务策略上做文章。错峰是第一层。把Flash擦写动作安排在确定不会来新数据的时机比如等到一帧数据处理完、且距离下一帧预计到达时间还有足够余量的时候再动手。实际操作中就是在上位机协议里做握手机制上位机发开始下载帧板子回切换写入模式应答上位机收到应答后再开始拉长帧间隔。这种手段能把冲突概率降下来但不是根本解法。分批是第二层。如果一次要写入几KB的数据不要攒在末尾一次性擦写整个扇区那样冻结时间最长。可以每收到一小块比如256字节就擦写一次虽然擦写次数变多但单次阻塞时间控制在几毫秒丢数据概率大幅下降。代价是Flash磨损会变快如果产品对擦写寿命有硬性要求要权衡。缩短单次操作时间是第三层。如果只是想更新几个参数尽量只擦一个页不要图省事把整片都擦了。页擦除和整片擦除的时间差是数量级的对丢包率的影响也完全不同。5.3 硬件选型思路关注RWW特性如果还在项目选型阶段另一个思路是从硬件层面规避。很多中高端的STM32型号支持多Bank结构或者具备RWWRead-While-Write能力——擦写Flash的一个Bank时CPU可以从另一个Bank正常取指令中断完全不阻塞。比如带双Bank的STM32L4、部分STM32F4系列它们在做OTA升级时可以把APP放在Bank1升级代码放在Bank2擦写Bank1时程序从Bank2执行串口通信自然不掉链子。STM32F030这种入门级Cortex-M0芯片不具备这种能力因为整个Flash存储阵列是单Bank的擦写期间哪都读不了。所以如果项目需求是通信和Flash擦写高频并发我在选型建议里一定会写上优先考虑带双Bank或RWW特性的型号。从长远看这个选型成本远低于后面在代码里绕来绕去。6. 实测数据与几条原则性经验6.1 验证结果开关DMA前后的对比方案改完之后我把之前的测试用例重新跑了一遍。关闭DMA、仅靠中断接收时执行一次页擦除128字节的测试帧当场丢了27个字节而且ORE溢出标志必置开启DMA循环接收后同样的测试流程连续执行200次页擦除和100次整片擦除128字节帧全部完整接收CRC校验零失败。我还特意在一个更极端的情况下做了测试把USART波特率提到921600页擦除期间连续接收数据。921600波特率下每字节约10.85微秒20毫秒擦除窗口内约1840字节缓冲区用了2048字节才扛住。这说明缓冲区大小和波特率、Flash操作耗时的关系是不可忽视的项目里如果波特率很高内存要舍得给。6.2 我总结的三条排查原则经历了整个排查过程我总结出几条对同类问题通用的原则。第一遇到偶发丢数据先分清是路径问题还是时序问题。路径问题指UART配置错误、引脚冲突、中断优先级错误这类确定性因素一般从第一次传输就丢时序问题则表现为偶发、集中在某个外设动作之后这时候要优先怀疑是否有外设操作把CPU卡住了一段时间。第二Flash操作导致的丢数据不能只盯着中断优先级调。在Cortex-M0单Bank Flash上擦写冻结取指是硬件行为任何软件优先级设置都救不了。正确的突破口是让数据不再依赖CPU实时搬运DMA是第一选择。第三做任何改动之前先打日志、先测时序用数据确认根因再谈方案。我这次排查中用的GPIO翻转法和逻辑分析仪是嵌入式定位时序问题最廉价也最有效的手段。不要一上来就把代码推翻重写也不要一上来就怀疑是UART外设坏了用波形说话比猜靠谱得多。再说一个很容易被忽略的实操细节测试串口接收丢数据时很多人习惯用USB转串口工具直接连电脑但有些USB转串口芯片在高压差、劣质线缆、或者接收buffer不够的情况下本身也会丢数据。最好在嵌入式设备端打一个收到数据后的回环或计数统计确认设备真的没收到而不是电脑没发出去或者串口工具丢了。7. 一点补充串口烧录失败和运行时掉数据的关系前面主要讲了嵌入式设备运行过程中Flash操作导致USART丢数据的场景但顺着这个思路再说一个很容易混淆的延伸问题——用串口烧录器给STM32下载固件时遇到的flash download failed - cortex-m3或cannot load flash device description这类报错。这类报错在运行时掉数据问题里经常被混在一起讨论但成因完全不同。烧录失败通常是上位机下载工具与目标板通信握手阶段出问题例如目标板复位电路不稳定、波特率不匹配、串口芯片驱动异常或者下载工具在擦除Flash期间与目标板的DMA/中断配合不佳。它的本质是烧录器在操作Flash时和UART下载通道产生了类似的时间窗冲突只不过冲突的一方换成了调试器端。如果你把前面DMA环形缓冲的思路搬到串口下载固件这个场景有一个有效的小技巧下载工具通过串口发送数据块时目标端的bootloader用DMA把数据块搬进RAM校验通过后再一次性写入Flash区而不是边收边写。这样即使写Flash期间USART有残留数据到达也不会被当成下载数据误处理。很多成熟的串口IAP方案就是这么设计的数据块校验失败时还可以清空接收缓冲重新接收可靠性高不少。所以无论你是正在调试应用层参数存储还是在调串口烧录流程只要代码里同时出现了USART接收和Flash擦写这两个动作就应该立刻警惕并确认它们的时序关系。在低端MCU上这两个动作并存时DMA几乎是绕不开的最佳解法。