资讯动态

基于STM32F0的MODBUS RTU从机实现与FLASH掉电保存

发布时间:2026/9/20 9:15:28 来源:尧图企业网站定制
简介基于STM32F0标准函数库的MODBUS RTU从机示例工程完整实现功能码03读取数据、06修改地址和波特率以及10写多个参数并通过内部FLASH实现参数掉电记忆。程序刻意保持简洁易懂的编程风格在读取、写入、错误回应等关键环节均添加清晰处理逻辑适合初学者逐步理解RTU帧结构、CRC校验和状态机设计学会后可快速移植到任意单片机平台。压缩包共245个文件以C语言源码34个.c、48个.h为核心附带Keil工程文件、链接脚本、编译中间文件以及生成好的.hex固件另有PDF说明供参考整个包仅5.74MB目录结构清晰便于定位修改与二次开发。目前已有723人学习下载对想入门MODBUS协议或希望基于STM32实现从机通信的开发者来说是一份上手门槛较低的实用参考。1. 从一次现场抄表失败说起三年前给一台恒压供水柜做远程监控主站用的是倍福PLC从站是某国产仪表MODBUSRTU通信结果PLC那边持续报超时。拿串口助手去抓包发现从站设备根本没回应——表里地址是1PLC却拿着地址1去读可仪表的从机地址寄存器早被上次调试改成5了掉电后没保存复位又跳回1。这种「改完地址重启又还原」的坑在MODBUSRTU从机设备里太常见。所以后来我干脆自己用STM32F0写从机把功能码03、06、10全做齐参数存进内部FLASH掉电不丢。程序刻意写得很直白不像某些老工程师把状态机封装到看不懂新人拿到手能顺着逻辑走换个单片机也能平移。这篇就把整个实现拆开讲从协议解析到FLASH磨损控制甚至连地址和波特率在线修改后如何可靠掉电记忆一次说清楚。2. 功能码03/06/10的协议解析状态机设计2.1 从底层串口到帧完整性的判定MODBUSRTU是半双工主从协议从机不能主动发数据只能回主站请求。帧格式固定为地址1字节、功能码1字节、数据区N字节、CRC16低字节在前、高字节在后。STM32F0标准库下我一般用USART接收中断逐字节缓冲配合定时器判断帧结束。这里有个关键点RTU模式下两帧之间至少要有3.5个字符时间的静默波特率9600时约4ms115200时约0.3ms。用定时器做超时判断比在中断里数空闲时间更稳。接收缓冲用环形队列或固定数组都行我的做法是开一个rx_buf[64]和rx_len每收到一个字节就存进去同时重置一个软件定时器。如果超时时间内没有下一个字节就认为一帧收完了置frame_ready标志。主循环里检测到标志就进入协议解析。这样做的优势是解析逻辑完全搬出中断Modbus寄存器读写耗时操作不会阻塞串口接收。// 串口中断服务函数简化 void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t ch USART_ReceiveData(USART1); if (rx_len 64) { rx_buf[rx_len] ch; } timeout_cnt 0; // 超时计数器清零 } }定时器中断里做超时递增timeout_cnt每1ms加一次超过3.5字符时间就frame_ready 1。字符时间跟波特率有关粗算公式是1000.0 / baud * 11 * 4毫秒我习惯直接取10ms固定值兼容所有常用波特率也不会误切帧。注意CRC校验要在主循环里做不要在中断里做否则长帧会拖死中断优先级。2.2 功能码03读取保持寄存器的响应拼装功能码03是主站读保持寄存器请求帧格式地址、0x03、起始地址高字节、起始地址低字节、寄存器数量高、寄存器数量低、CRC。从机收到后要校验起始地址和数量是否越界越界就要回异常码0x02非法数据地址。STM32F0标准库的寄存器定义直接用#define数组最简单因为F0没有像F1那样的位段操作也不影响。我定义一个uint16_t reg_table[32]把需要暴露给MODBUS的变量全部映射进去。比如第0个寄存器放从机地址、第1个放波特率码、第2个往后放控制参数。03功能码响应时从表里取数据注意MODBUS是大端传输需要把16位数据拆成高字节在前发出。static void modbus_send_03(uint16_t start_addr, uint16_t reg_cnt) { uint8_t resp[64]; resp[0] slave_addr; resp[1] 0x03; resp[2] reg_cnt * 2; // 字节数 for (int i 0; i reg_cnt; i) { uint16_t val reg_table[start_addr i]; resp[3 i*2] val 8; resp[4 i*2] val 0xFF; } uint16_t len 3 reg_cnt * 2; uint16_t crc cal_crc16(resp, len); resp[len] crc 0xFF; resp[len] crc 8; uart_send_bytes(resp, len); }响应帧长度是3 2*NCRC附加在最后低字节在前。这里有个容易错的地方请求里起始地址是0基的比如读寄存器1到3起始地址填0、数量填4协议栈内部不做偏移处理直接用数组下标。这样代码虽然简单但会导致MODBUS地址和实际寄存器编号差1我在项目中直接保持0基省得后期绕晕。2.3 功能码06写单个寄存器的确认逻辑功能码06是写单个保持寄存器请求格式地址、0x06、寄存器地址高、低、数据高、低、CRC。正常响应是原帧原样返回。实现时同样要先判断寄存器地址是否在合法范围内但06写寄存器往往涉及「写后生效」的动作比如改从机地址、波特率。如果直接改reg_table掉电就没了。所以06的流程应该分两步先更新reg_table再标记参数需要写入FLASH。另一个关键是立即生效还是重启生效。改波特率时如果立刻把串口波特率切换过去主站可能来不及收到响应帧此时主站会认为写操作失败。更稳妥的做法是06写寄存器后正常返回响应帧然后延迟200ms再切换波特率主站收到响应后等片刻再按新波特率通信。我在程序里用一个param_change_flag写寄存器时置标志主循环检测到标志后先保存FLASH再延时并重新初始化串口。static void modbus_handle_06(uint8_t *frame, uint8_t len) { uint16_t addr (frame[2] 8) | frame[3]; uint16_t val (frame[4] 8) | frame[5]; if (addr REG_NUM || addr REG_SW_VERSION) { send_exception(func_code, 0x02); // 非法数据地址 return; } reg_table[addr] val; send_raw(frame, len); // 正常响应原样返回 if (addr REG_SLAVE_ID || addr REG_BAUDRATE) { system_param_dirty 1; // 标记需要存FLASH } if (addr REG_BAUDRATE) { delay_ms(200); // 等待响应发送完成 uart_set_baudrate(decode_baudrate(val)); } }这里有个细节原本样返回的帧是包含CRC的直接发回就行不需要重新计算。但记住要在发完后再修改串口寄存器否则USART上还有数据没发完就被改了波特率最后一个字节基本都会错乱。另外REG_SW_VERSION做成只读寄存器写操作直接返回异常防止误改。2.4 功能码10写多个寄存器的数据搬运功能码10十六进制0x10是写多个保持寄存器请求帧地址、0x10、起始地址高、低、寄存器数量高、低、字节数、数据区、CRC。数据区每寄存器占2字节大端。注意「字节数」应等于寄存器数量乘2如果主站填错了最好先做一致性校验不一致回异常码0x03非法数据值。实现时把请求里的数据逐个填入reg_table然后同样需要检查每个寄存器是否可写。某些寄存器比如版本号不允许写写到该地址时要回异常。我习惯先全部校验再写入不能用边写边检查否则会导致前几个寄存器已改、后几个失败主站不知道状态。static void modbus_handle_10(uint8_t *frame) { uint16_t start (frame[2] 8) | frame[3]; uint16_t cnt (frame[4] 8) | frame[5]; uint8_t byte_cnt frame[6]; if (byte_cnt ! cnt * 2 || start cnt REG_NUM) { send_exception(frame[1], 0x03); return; } for (int i 0; i cnt; i) { uint16_t val (frame[7 i*2] 8) | frame[8 i*2]; if (start i REG_SW_VERSION) { send_exception(frame[1], 0x02); return; } reg_table[start i] val; } // 响应地址、功能码、起始地址、数量、CRC uint8_t resp[8] {slave_addr, 0x10, frame[2], frame[3], frame[4], frame[5], 0, 0}; uint16_t crc cal_crc16(resp, 6); resp[6] crc 0xFF; resp[7] crc 8; uart_send_bytes(resp, 8); system_param_dirty 1; }这里要注意异常码选择地址越界用0x02数据值非法用0x03。很多新人会把这两者混用主站那边如果按照Modbus规范解析收到0x02和0x03处理路径完全不同一个会报地址错误一个会报参数错误。另外10功能码成功后如果修改的是从机地址或波特率同样要走掉电记忆和延迟生效流程不能和06分开写。2.5 CRC16校验的查表法实现CRC16-MODBUS是必写的函数。可以用位循环计算但F0主频48MHz位运算法每帧64字节最坏要跑几百微秒主循环里还好。但既然要做到「容易看懂」我更推荐查表法代码量反而更短。核心是这个表static const uint8_t crc_table[] { 0x00,0xC1,0x81,0x40,0x01,0xC0,0x80,0x41, // ... 省略中间表项 0x01,0xC0,0x80,0x41,0x00,0xC1,0x81,0x40 }; uint16_t cal_crc16(uint8_t *data, uint16_t len) { uint8_t crc_hi 0xFF, crc_lo 0xFF; for (uint16_t i 0; i len; i) { uint8_t idx crc_hi ^ data[i]; crc_hi crc_lo ^ crc_table[idx * 2]; crc_lo crc_table[idx * 2 1]; } return (uint16_t)(crc_hi 8 | crc_lo); }查表法的表生成规则是多项式0xA001的256项预计算结果网上能搜到完整数组。计算时初始值为0xFFFF最后得到的crc_hi是高字节发送时先发低字节。如果你懒得维护表也可以用逐位法但要注意F0的内核是Cortex-M0没有硬件除法也没有位反转指令纯逐位算CRC耗时是查表法的十几倍。我实际测试过115200波特率下主站每10ms轮询一次查表法占CPU不到1%逐位法却可能跑到5%在中断里会影响其他外设处理。3. 基于STM32F0内部FLASH的参数掉电记忆设计3.1 为什么不用外部EEPROM很多STM32项目喜欢外挂AT24C02I2C接口简单是简单但带来三个问题一是增加物料成本二是I2C时序在电磁干扰强的工控现场容易丢数据三是如果单片机本身有剩余FLASH不利用太浪费。STM32F0系列内部FLASH容量从16KB到256KB不等程序通常用不到一半剩余几KB足够存参数。而且内部FLASH是映射在地址空间里的读起来像数组一样快掉电保存不需要额外电源。当然内部FLASH不是万能的。它的擦写寿命是1万次如果每次修改参数都立即擦写一天改10次也能用三年基本够用。但如果设备频繁远程改参数或者日志型应用每秒写一次就必须做磨损均衡或磨损检测。这个后面会讲。另外内部FLASH擦除粒度是页F0的页大小通常是1KB写时要先擦整页擦除期间代码执行会暂停因为CPU从同一块FLASH取指所以擦写时要把中断屏蔽或放在RAM里执行否则串口可能丢字节。3.2 扇区划分与参数存储布局我的做法是把内部FLASH最后两页预留出来一页存当前参数一页存备份参数。地址规划如下假设F0的FLASH基址是0x08000000总大小64KB程序编译后占用不超过40KB那么我把0x0800F800到0x0800FFFF两页1KB/页留给参数存储。其实用页基址更简单#define PARAM_FLASH_ADDR 0x0800F800即可。如果编译器不链接这两页内容根本不用担心代码覆盖因为链接脚本里FLASH大小我改成了LENGTH 0x400016KB只留用户能用的低半区。存储格式我定义成一个结构体typedef struct { uint16_t slave_id; // 从机地址 uint16_t baudrate_code; // 波特率码 uint16_t parity; // 校验位: 0无 1偶 2奇 uint16_t data[8]; // 预留参数 uint32_t magic; // 魔数 0xA5A5A5A5 uint16_t crc16; // 结构体CRC } sys_param_t;结构体大小必须对齐最好用__packed或手动指定2字节对齐。CRC校验覆盖结构体从首地址到magic之前的所有字节存数据时先算CRC再写入。读数据时先读magic是否为0xA5A5A5A5再算CRC是否匹配。两者不满足就回退到默认参数。这里有个实际问题写入时如果突然掉电可能写到一半FLASH保存不了备份页的作用就体现出来了——写入新页前先备份旧页到另外一页写完新页后校验成功才把备份页标记为无效。3.3 FLASH写入与擦除的函数封装STM32F0标准库提供FLASH_ProgramHalfWord因为F0是Cortex-M0不支持双字写入只能按16位编程。擦除则是FLASH_ErasePage(page_addr)。注意编程前必须擦除而且擦除后全页是0xFF。写参数时先解锁FLASH再擦页再逐半字写入。标准库的时序代码很简单关键是加好锁和解锁void flash_write_sys_param(sys_param_t *param) { FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_WRPERR | FLASH_FLAG_PGERR); FLASH_ErasePage(PARAM_FLASH_ADDR); // 擦除参数页 uint16_t *p (uint16_t *)param; uint32_t addr PARAM_FLASH_ADDR; for (int i 0; i sizeof(sys_param_t) / 2; i) { FLASH_ProgramHalfWord(addr, p[i]); addr 2; } FLASH_Lock(); }这里强调一点FLASH_ProgramHalfWord编程地址必须是半字对齐的如果结构体里有奇数字节底层会异常。所以结构体成员尽量都是16位或32位且总大小是偶数。另外擦除页会一次性清掉整页1KB如果你把其他不变数据混在同一页里存擦除时会把别人也擦掉所以参数页一定要独立。3.4 掉电检测与保存时机MEMORY管理上最重要的是确定写入FLASH的时机。不能每次写寄存器都立刻擦写FLASH也不能拖太久。我的策略是修改参数后置dirty1主循环每10ms轮询一次如果dirty为1且距上次写入超过2秒就执行一次FLASH写入。2秒的延时是为了合并连续多次修改比如主站一次性写多个寄存器时只触发一次保存。但仅靠软件轮询不够真正掉电瞬间CPU还能运行几十毫秒因为电源电容放电。我在电源输入端接了一个分压电阻到ADC通道主循环检测ADC值低于阈值就立即保存参数然后死循环等待掉电。这个用ADC检测的代码在大多数工控板上都能复现硬件成本就两个电阻。保存动作本身要快擦页加写40个半字总共需要时间约50ms擦页大概20-50ms所以ADC检测要在电压跌落到单片机最小工作电压4.5V左右之前就触发。一般来说5V供电时设阈值4.0V比较合适。void main_loop(void) { while (1) { if (adc_voltage 4.0f) { if (system_param_dirty) { flash_write_sys_param(sys_param); system_param_dirty 0; } while (1); // 等待掉电 } // 正常处理Modbus帧... } }这个做法能确保掉电瞬间数据还在但如果掉电太快比如电源拔插间隔小于50msADC还没触发就没了。所以我加了第二道保险在主循环里一旦把参数写入reg_table并置dirty先立刻更新一份RAM副本到备份缓冲同时安排一个低优先级的延迟保存。ADC掉电检测负责最后兜底。3.5 FLASH磨损均衡并不复杂STM32F0的内部FLASH寿命标称1万次擦写。如果一天改10次参数一年3650次约3年寿命。对于大部分设备够了但如果需要更强可以做一个简单的磨损均衡池把4页甚至8页FLASH作为一个循环存储区每页存储区头部写一个序列号写新数据时找序列号最大的那一页的下一页写入新数据并将旧页无效化。这样寿命按页数翻倍。实现起来不复杂但调试时要格外小心因为页擦除顺序和CRC校验逻辑都要覆盖到。对于这个课程项目我建议先做单页直接存储等整个协议栈通了再扩展磨损均衡。因为磨损均衡的坑在于如果设备在擦除新页和写入新页之间掉电数据可能两页都损坏恢复逻辑要正确处理。工程上一半设备电控柜里一年也不会改几次参数单页足够。如果你做的是带电池供电的仪表频繁上报设置那再用双页轮流。4. 从机地址与波特率在线修改的实现与联动4.1 参数寄存器的地址映射我定义了如下寄存器表这个表同时承担协议响应和界面显示清晰易读寄存器地址名称读写属性说明0x0000SLAVE_ID可写从机地址1-2470x0001BAUDRATE_CODE可写0:9600 1:19200 2:38400 3:1152000x0002PARITY_CODE可写0:无校验 1:偶 2:奇0x0010SW_VERSION只读固件版本号如0x01010x0011RUN_STATUS只读运行状态字寄存器数组直接初始化uint16_t reg_table[0x20] { [0x0000] 1, // 默认地址1 [0x0001] 3, // 默认115200 [0x0002] 0, [0x0010] 0x0101, };从机地址为0时MODBUS协议规定广播从机应处理请求但不回复。我在帧解析入口就判断如果slave_addr0直接解析并执行写操作但不发送任何响应。但也要注意我们内部FLASH里保存的地址不能为0否则下次上电从机不响应无法再通过总线修改。所以06或10写SLAVE_ID时要做范围校验1到247超出就回异常。4.2 波特率切换的安全序列波特率切换比改地址更容易出问题因为双方必须同时切换。安全序列应该是主站发06写波特率寄存器。从机收到后立即回正常响应同时置dirty1。从机延时200ms确保响应帧已发送完成。从机切换USART分频寄存器的值。主站等待200ms后使用新的波特率重新发起请求。我在切换波特率的函数里加上从机侧串口复位逻辑因为F0在修改BRR寄存器后有些库函数版本需要重新使能USART否则会丢第一个字节。void uart_set_baudrate(uint32_t baud) { uint32_t pclk SystemCoreClock; // 48MHz USART1-BRR pclk / baud; // 复位接收状态清空残余数据 USART_ClearFlag(USART1, USART_FLAG_RXNE); USART_ReceiveData(USART1); }这里有个隐藏问题切换到低波特率时主站如果在切换瞬间还在发数据从机可能收到半个字节的杂波。我的做法是切换前清空接收缓冲和协议状态机再延时等待一段时间这会儿主站肯定也在等待延时不会立刻发数据。如果主站软件写得不够好不等待就发那就需要从机侧容忍一帧异常。所以稳妥起见切换波特率动作放在接收空闲的定时器里做并临时关掉接收中断。4.3 参数掉电记忆与上电恢复上电时主流程先读FLASH里的参数校验通过就用存储值覆盖reg_table的默认值否则保留编译默认值。这是标准做法sys_param_t param; if (read_from_flash(param) PARAM_OK) { reg_table[0x0000] param.slave_id; reg_table[0x0001] param.baudrate_code; reg_table[0x0002] param.parity; } else { // 使用默认参数并写一次FLASH使默认参数也掉电记忆 param.slave_id 1; param.baudrate_code 3; param.parity 0; flash_write_sys_param(param); }这个位置建议放在main函数最前面。注意初始化串口时要使用读出的波特率所以FLASH读取必须先于串口初始化。如果用户上一次改成了罕见波特率比如0xFFFF错误值要做好校验防止配置混乱。我的baudrate_code取值范围限定0-3超出就回退到115200。读到正确的参数后还有一个容易忽略的点程序里使用slave_addr全局变量作为响应帧来源而reg_table[0]是外部可写的。写寄存器修改SLAVE_ID后必须同步更新slave_addr。不能傻乎乎在发送响应帧的地方每次都去查reg_table那样响应帧的源地址会被modify成自己造成协议层自杀式行为。我在06和10的处理函数里统一调用sync_addr_from_table()。4.4 与蓝牙BLE或PLC主站对接时的注意事项整个工程写好之后我通常用USB转RS485接电脑调试有时候也直接用现成的蓝牙BLE转串口模块做无线参数配置。用BLE时有一个坑蓝牙模块的串口波特率默认可能是9600如果STM32F0上电默认是115200两者不通要先通过另外一个UART口或者按键进入固件升级模式把从机改成9600。所以建议在设备上做一个物理按键按下时以低速9600启动系统这样就永远有个救活手段。倍福PLC走MODBUSRTU时主站一般的寄存器地址是1基而我们的协议栈是0基。倍福里的配置需要把起始地址填成0数量按实际要读的寄存器个数填。还要注意倍福的字节序默认是Big-EndianST的寄存器表也是大端方向一致不需要额外转换。但如果主站是西门子S7-1200默认小端读回来需要在PLC里做字交换。这是跨平台协议栈最容易被忽略的。5. 验证方法串口抓包、错误回应与边界测试5.1 用串口助手模拟主站帧写完代码不要急着接PLC先用USB转485接到电脑上的串口调试助手。手动发送以下十六进制帧来验证。03功能码读从机地址寄存器地址1发送01 03 00 00 00 01 CRC16。CRC可以用在线工具算以自己写的cal_crc16计算结果为准。响应帧应该是01 03 02 00 01 CRC其中数据区00 01表示从机地址为1。06功能码写从机地址为5发送01 06 00 00 00 05 CRC。正常响应是原帧回显。此时我们重新读地址寄存器发送05 03 00 00 00 01 CRC看是否回应。如果不回应说明从机地址没同步或串口波特率被改了。再读的话地址被改了主站要用新地址。10功能码写多个寄存器例如把寄存器0写成1、寄存器1写成3发送01 10 00 00 00 02 04 00 01 00 03 CRC。响应是01 10 00 00 00 02 CRC。建议把这几个帧写进一个批处理脚本用一个支持定时发送的串口助手每200ms循环发送。另外一定要测试错误回应发送一个不存在的寄存器地址比如01 03 00 40 00 01 CRC应该收到01 83 02 CRC功能码最高位置1异常码02。这是很多人漏掉的功能。5.2 掉电记忆的实测方法改地址和波特率之后关闭电源等待5秒重新上电再用新的地址和波特率通信。能通就说明FLASH写入成功。如果上电后按旧地址通信却无响应而按新地址能通说明存储没问题但是加载顺序有bug。正常调试时我习惯在初始化和加载参数之间打一个调试串口日志但不建议在量产的设备上保留日志输出。再测试一个更严苛的场景在写06改地址的响应帧刚发出后、还没触发FLASH写之前快速断电。这时FLASH里还是旧地址下次上电旧地址有效。这种半途掉电的情况如果项目要求极高需要把参数写入也做成事务型先在备份页写好再标记主页有效。但我们在单片机没有RTOS的环境下简单起见我采用「写FLASH前先擦除擦除后立即写入」的顺序万一擦除后写入前掉电备份页还有旧参数可回滚。具体备份页操作如下void backup_and_write(sys_param_t *param) { // 先擦备份页把当前参数拷贝过去 FLASH_ErasePage(PARAM_BAK_ADDR); write_param_struct(PARAM_BAK_ADDR, sys_param); // 再擦主参数页并写入新值 FLASH_ErasePage(PARAM_FLASH_ADDR); write_param_struct(PARAM_FLASH_ADDR, param); }这里额外耗了一半时间但更安全。如果你的主站断电频率高务必要加。备份页的恢复逻辑是上电时先读主参数页CRC错误就读备份页备份页也坏就恢复出厂设置。5.3 边界测试清单以下是我每次交付MODBUSRTU从机程序前必跑的测试项可以直接当成测试用例用编号测试操作期望结果1发送CRC错误的帧无响应不能触发任何寄存器变化2发送地址为0的广播帧执行写操作但无任何响应303读起始地址数量越界回异常码0x02406写只读寄存器回异常码0x02510写数据字节数不符回异常码0x036连续快速发送100帧每帧都有正常响应或无响应不能死机7写地址后立刻重新读使用新地址才能通信8写波特率后断电再上电上电后新波特率生效9两帧间隔不足3.5字符从机应只处理第一帧忽略粘包帧测试6特别重要。F0在48MHz下如果主站用115200发送每帧间隔很短我们的超时定时器可能还没到就把两帧合并成一帧。解决办法是把超时时间从3.5字符放宽到5字符牺牲一点响应速度换稳定性。另外CRC错误帧如果不丢弃就会让状态机错位所以解析函数里发现CRC错必须把接收缓冲清零。我在frame_ready后、解析前强制调用rx_len0这个动作绝不能少。5.4 从机端用逻辑分析仪验证时序最后一步是接一个USB逻辑分析仪速率8MHz以上即可抓RS485芯片的RO引脚电平变化。看帧之间的间隔是否大于3.5字符时间以及响应帧是否在主站请求结束后立刻发出。常见问题是主站请求还没发完从机收到中间字节就开始组装帧导致CRC算错。我在串口中断里用时间戳记录相邻字节间隔若连续两个字节间隔小于3.5字符但总帧长超过了RTU最大长度256字节就判断为非法帧并丢弃。这个技巧也建议你加上。比如逻辑分析仪截图帧间隙请求最后一字节到响应第一字节之间通常有几十微秒因为USART接收中断到主循环解析需要时间这是正常的。如果大于10ms说明代码在主循环里做了耗时操作比如FLASH擦写这会严重影响响应时间。解决方法是把FLASH写请求放入一个标志位不要在帧解析的瞬间执行擦写而是移到主循环的尾部。这也是我前面一直强调dirty标志位的目的。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价