资讯动态

主从架构下中断驱动通信:UART与GPIO事件线的工程实践

发布时间:2026/9/16 13:22:46 来源:尧图企业网站定制
简介面向嵌入式开发者的ARM Keil I2C主从模式中断处理示例工程聚焦在I2C总线通信中如何利用中断机制提升实时性与CPU利用率适合需要掌握主设备发起通信、从设备响应以及中断服务程序编写的开发者。资源内共11个文件包含I2C主从中断测试C源码、LPC17xx库配置文件、Keil与EWARM工程文件.uvproj/.uvopt/.ewp/.ewd、启动配置.ini/.mac及makefile等可同时用于Keil MDK或IAR环境压缩包仅20KB结构紧凑方便快速导入。已有103人学习属于轻量但典型的I2C外设参考例程。通过阅读源码和工程配置可以了解I2C初始化、中断服务程序设计、主发送与接收操作、从设备地址与数据帧格式构造、错误状态监测及中断优先级配置等关键知识点并能结合调试技巧在真实板卡上运行验证对嵌入式通信外设开发有直接借鉴意义。1. 拿到 Master_Slave_Interrupt 先分清主从两侧的中断职责Master_Slave_Interrupt.rar_Master/Slave这个标题看着像随手打的包名但里面浓缩的是嵌入式最常见也最容易做烂的一套结构主从两台设备通信数据不是靠轮询“挤”出来的而是靠中断事件驱动。做得好的工程主机永远不发“你忙完了吗”这种空问从机只有状态翻转才拉线上报做得差的工程一个 32 字节的状态帧能把 CPU 占用拖到 40% 往上。这套机制覆盖 SPI、I2C、UART 三类总线的从机上报场景核心难点不在协议本身而在中断优先级怎么分、中断里哪些事绝对不能做、以及启动阶段两个设备“谁先跑、谁先发”的时序问题上。适合正在写驱动、调板子或者被现场“偶发丢帧”折磨的固件工程师往下看博主这里直接把能落地的那套方案讲透。2. 主从架构里的中断模型从事件到数据的完整路径2.1 为什么“从机主动上报”必须靠中断而非轮询主从架构里最常见的错误认知是主机“勤快点”就行主循环里每 5ms 扫一遍从机寄存器发现变化就读取。这种轮询方案在小数据量、低波特率的演示工程里能跑一旦从机数量增加到 8 个、波特率拉到 57600问题立刻暴露。以一个从机上报 16 字节状态帧为例做对比。主机主频 72MHz波特率 115200单字节传输耗时约 86.8us16 字节完整帧耗时约 1.39ms。轮询模式下主机为了不漏事件扫描周期必须小于事件最短保持时间。如果从机事件脉宽只有 2ms主机就得把扫描周期压到 2ms 以内而每次扫描总线上挂着 8 个从机地址每组地址查询也是 1.39ms8 个从机一轮就是 11ms。这个矛盾是数学上的再怎么优化裸循环都没用。中断模型把“通知”和“读取”解耦从机用 GPIO 拉一条事件线主机收到中断后先记录事件类型再在合适的时机发起总线读取。这时候主机主循环可以继续干别的比如处理显示刷新、按键扫描只要保证中断响应延迟满足从机事件保持时间即可。对于 2ms 脉宽的事件72MHz 内核从触发到进入 ISR 的延迟通常在 5~20us 量级余量非常大。2.2 从机侧信号到主机 CPU 的两种中断回路实际工程里主从中断有两种典型接法很多同事分不清以致于在排查“有事件但主机没反应”时无从下手。第一种是 GPIO 事件线加 UART 数据通道。从机检测到自身状态变化拉高一个 GPIO 引脚或者输出一个下降沿脉冲主机把该引脚配置为外部中断输入。主机进入 GPIO ISR 后先把事件标志置位主循环或定时器轮询里发现标志位再去读从机数据。这种结构的优点是事件通知延时极低缺点是主机侧要有空闲任务来消费这个标志如果数据量大会变成“中断通知 查询取数”的组合。第二种是纯 UART 字节中断驱动。从机直接把状态帧作为一包数据发过来主机在 UART 的 RXNE 中断里逐字节接收并存进环形缓冲区解析在缓冲层完成。这种结构省掉 GPIO 线但主机必须保证 UART 中断优先级足够高否则波特率稍高就容易丢字节。事件通知链路GPIO UART 组合 从机状态变化 - GPIO 拉高 - 主机 EXTI 中断触发 - 置 event_flag - 主循环检测 - 发起 UART 读取 - 从机响应的数据帧逐字节进入 RX 中断 - 存入环形缓冲 - 帧解析这套链路里每一个箭头都是一个可以观测的点。排查问题时按箭头逐段点亮比闷头改代码效率高得多。GPIO 中断只负责“告诉你来了”UART 中断只负责“把数据搬走”解析则放到线程或主循环里这是主从结构里中断职责划分的基本盘。2.3 中断里的时间预算从触发到数据可用的最坏情形设计主从中断系统不能只看平均响应时间要看最坏情况。一个被高频定时器中断比如 1ms 系统节拍打断的外部中断叠加在 UART 已有 8 字节待接收的队列上最坏延迟会达到几十微秒。对于 115200 波特率的 UART一个字节时间约 86.8us如果主机 RX 中断优先级低于当前正在执行的中断那么后续字节会持续进硬件移位寄存器直到 RXNE 标志被处理。超过两个字节时间没读走就产生溢出ORE 错误位。因此设计原则是UART 接收中断优先级应高于任何非实时任务中断低于系统硬故障处理即可。GPIO 事件线中断如果只做置位不读总线数据优先级可以放低一级。博主一般这样设置中断源抢占优先级子优先级说明UART RX数据通道10逐字节接收禁用会被丢数据GPIO EXTI事件通知20只置标志不在此处读总线定时器节拍30维护系统 tick可被前两者打断软件触发的 DMA 完成21用于批量从机数据搬运表格不是固定的关键是保持“数据通道 事件通知 系统节拍”的相对顺序。很多人直接把 GPIO 中断优先级放得比 UART 高搞反了。GPIO 中断及时执行只是少置一个标志UART 中断不及时执行直接丢帧。3. Master/Slave 中断收发的最小可复现实现3.1 从机侧用 GPIO 事件线和 UART 发送组装最小上报单元先从从机侧开始写因为从机是事件源逻辑最单一。以下代码以 STM32F1 系列库函数风格编写读者可平移到 GD32、APM32 或其他 Cortex-M 系列改动只在寄存器名字上。/* 从机侧外部事件触发 UART 帧上报 */ #define EVENT_SIGNAL_PIN GPIO_Pin_1 /* PA1 接到主机的外部中断输入 */ #define FSM_EVENT_SYSERR (0xA1) #define FSM_EVENT_TEMP (0xA2) volatile uint8_t g_have_event 0; volatile uint8_t g_event_code 0; /* 外部事件源按键模拟、传感器阈值中断、掉电检测等 */ void EXTI1_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line1) ! RESET) { g_have_event 1; g_event_code FSM_EVENT_TEMP; /* 这里固定为温度事件实际按业务判断 */ EXTI_ClearITPendingBit(EXTI_Line1); } } void Slave_SendEventFrame(uint8_t event_code) { uint8_t frame[6] {0x5A, 0xA5, 0x01, event_code, 0x00, 0x00}; frame[4] frame[2] ^ frame[3]; /* 校验字节地址与事件码的按位异或 */ frame[5] (frame[0] frame[1] frame[2] frame[3] frame[4]) 0xFF; /* 轮询发送两个字节之间间隔极短优先级不被其他中断抢占 */ for (uint8_t i 0; i sizeof(frame); i) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, frame[i]); } g_have_event 0; } int main(void) { /* GPIO、USART1、中断配置在此省略 */ while (1) { if (g_have_event) { Slave_SendEventFrame(g_event_code); } /* 其他业务代码 */ } }这段代码把事件记录和事件发送分开。EXTI1_IRQHandler里只置标志和记录类型不在中断里执行 UART 发送函数因为USART_SendData加等待 TXE 标志的耗时可能达到 800us 以上6 字节 x 86.8us如果从机中断里同时还有别的实时任务这个时间是不可接受的。Slave_SendEventFrame里的校验用了两层前一个校验字节用地址 ^ 事件码后一个校验字节用 5 字节累加和取低八位。这两层不是冗余设计的前者用于快速判别地址和事件是否匹配后者用于捕获多字节串行传输中的奇数位翻转。主机解析时先查第一个校验不等第二个校验就能决定是否丢弃减少无效解析开销。3.2 主机侧UART 接收中断加环形缓冲区落盘主机侧要解决的核心问题是“帧是分字节到达的不能等整帧都到了再去处理”。合理的做法是每收到一字节就放进环形缓冲区解析器在缓冲区里寻找帧头。/* 主机侧UART RX 中断 环形缓冲 帧解析 */ #define RING_BUF_SIZE 256 volatile uint8_t ring_buf[RING_BUF_SIZE]; volatile uint16_t ring_head 0; volatile uint16_t ring_tail 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t byte USART_ReceiveData(USART1); uint16_t next (ring_head 1) % RING_BUF_SIZE; if (next ! ring_tail) /* 缓冲未满才写满了丢掉 */ { ring_buf[ring_head] byte; ring_head next; } USART_ClearITPendingBit(USART1, USART_IT_RXNE); } }环形缓冲区的容量计算公式容量 一帧最大字节数 * 最大未处理帧数 1。本例一帧 6 字节主机即使连续接收到 40 帧后才被主循环消费缓冲区 256 字节也足够。缓冲区满以后策略是直接丢新字节比覆盖旧数据安全——宁可丢最新的不能把未解析的帧污染掉。接收中断里只做了两件事从数据寄存器取字节、写入内存。这两步的耗时在 72MHz 主频下不到 1us远小于一个字节的到达间隔86.8us不会出现溢出。3.3 帧解析状态机与超时保护环形缓冲区只是解决了“存储”问题解析要单独做。博主不建议在 UART 中断里直接解析帧因为解析过程有分支跳转和循环闯入中断会加长关中断时间。解析放到主循环或低优先级任务里uint8_t FrameParser_FindEvent(uint8_t *event_code) { static uint8_t state 0; static uint8_t frame[6]; static uint8_t idx 0; while (ring_head ! ring_tail) { uint8_t b ring_buf[ring_tail]; ring_tail (ring_tail 1) % RING_BUF_SIZE; switch (state) { case 0: if (b 0x5A) { frame[0] b; state 1; } break; case 1: if (b 0xA5) { frame[1] b; state 2; } else if (b ! 0x5A) { state 0; } break; case 2: frame[2] b; /* 从机地址 */ state 3; break; case 3: frame[3] b; /* 事件码 */ state 4; break; case 4: frame[4] b; /* 校验1 */ state 5; break; case 5: frame[5] b; /* 校验2 */ state 0; if ((frame[4] (frame[2] ^ frame[3])) (frame[5] (frame[0] frame[1] frame[2] frame[3] frame[4]) 0xFF)) { *event_code frame[3]; return 1; } break; } } return 0; }这个状态机把“寻找帧头”拆成两步0x5A后必须紧跟0xA5有效过滤了串口线缆上的随机毛刺。case 2和case 3各收一字节没有引入额外的 timeout 机制。博主建议有经验的读者给它加一层“解析超时”一旦进入case 2后超过 20 字节时间没收到后续帧强制回到case 0。这个超时检查放在主循环里比较合适用一个last_tick记录每次case 2转移的时刻。4. 中断参数整定与启动阶段最容易踩的坑4.1 典型参数表波特率、启动延时、复位时序主从系统联调时先核对一张参数表再动手改代码是最省时间的路径。博主列一份调试经验值读者按自己的 MCU 型号微调参数项经验值检查手段从机事件脉冲宽度不小于 50us示波器测量从机 GPIO 保持时间主机 EXTI 中断触发边沿与从机一致上升沿/下降沿逻辑分析仪看事件线与 UART TX 的先后UART 波特率偏差不超过 ±2%两端用同源晶振或用 16 倍过采样自动纠偏主机启动后首字节等待时间50~200ms从机代码里实际延时后发同步帧从机首次上报重试次数3 次每次间隔 10ms抓包看是否有 ACK 返回第 4 行和第 5 行专门针对启动阶段的“撞车”问题。很多主从系统里带有一个出厂烧录的 boot 监控程序上电后会在串口打印类似to interrupt normal startup, press enter的提示等待一小段窗口超时后跳转到用户代码。如果主机上电后立刻发起总线同步帧而此刻从机还停在 boot 程序里同步帧的字节会被 boot 当作控制台输入消费掉进入 debug 命令解析。4.2 启动阶段“同步帧被 boot 劫持”的处理博主在带产线联调时遇到过一模一样的问题主机 30ms 就主动发同步帧从机的 boot 监控要等 100ms 才让出串口结果从机 100ms 内收到的所有字节全部进了 boot 的 console 处理逻辑用户协议层的收发缓存为空等跳转到用户程序后主机已经重试完三次并把从机标记为掉线。解决办法不是改 boot而是从机在自己用户程序的初始化代码里主动等主机。从机启动后先进入“静默等待同步帧”状态不发送任何字节直到收到主机发来的特定同步帧如0x5A 0x5C 0x00才回送一个带从机地址的连接确认帧然后进入正常业务循环。这样从机的 boot 窗口结束后主机可能在重试周期内早发出去了也没关系——从机用户程序一旦开始运行收到的第一个合法同步帧会立即触发应答。主机只需要把重试次数从 3 次提高到 5 次总时长控制在 1 秒内。这里的关键参数是两个boot 窗口时长和主机重试总时长。它们必须满足主机首包起始时刻 重试总时长 boot 窗口时长 50ms。否则从机永远收不到第一个同步帧。这两个数值都要写入协议设计文档光在代码里改没人记得。4.3 中断服务函数里不能碰的雷区与现场排查清单这块内容不算新技术但踩的人最多。列一张排查清单照着查大概率能定位偶发问题不要在 UART 中断里调用printf或任何阻塞型输出实时性会被打印时间吃掉现场表现为“只有插上调试器才正常”。不要在 GPIO 中断里直接读 I2C/SPI 从机寄存器这类总线的时钟拉伸或 ACK 等待会让中断挂死。检查 NVIC 配置是否在USART1_IRQHandler里意外调用了__disable_irq()这会导致后续 RTC 或看门狗中断被屏蔽系统进入“假死”。从机发送帧时若用了USART_IT_TXE中断方式而不是轮询要注意 TXE 中断在发送缓冲区空时触发频率极高容易把低优先级中断饿死。博主见过最多的问题是主机侧的 UART 中断里放了一个软件延时等待从机应答把整个中断上下文阻塞掉 2ms 以上。这种问题在低波特率下尤其致命115200 波特率下 2ms 意味着 23 个字节时间后续数据全部溢出丢失。5. 进阶多从机事件合并与中断延迟的实测手法多从机系统中 GPIO 引脚资源紧张常见的做法是把多个从机的事件线做“线与”全部接到主机的一个外部中断引脚上。从机正常不拉线发生事件时拉低该引脚主机外部中断触发后扫描所有从机的寄存器确认到底是谁拉低了这根线。这样解决 GPIO 不足但要注意“线与”必须用开漏输出而且主机侧该引脚要启用上拉。如果某个从机在上电瞬间意外拉低事件线主机会被这个虚假事件打断扫描全部从机也查不到状态变化——排查时看事件线上是否有持续低电平即可定位问题从机。事件合并后需要实测中断延迟来验证系统余量。博主习惯用 GPIO 翻转法从机发生事件的同一时刻翻转一个测试引脚主机中断服务函数的第一句话翻转另一个引脚用逻辑分析仪测量这两个引脚翻转沿的时间差。该差值就是完整的中断延迟链路包含硬件响应、压栈、NVIC 仲裁、进入 ISR 的时钟周期数。72MHz 下这个值在 12~50us 之间都能接受如果超过 100us要检查是否有关中断过长的临界区。中断延迟测试的同时给测试引脚测出最坏值后还要反过来校验“从机事件脉宽”的设计值。博主一般要求从机事件脉宽至少是实测最坏延迟的 5 倍达不到就把从机事件拉高逻辑放在定时器中断里执行造成脉宽可精确控制的效果。另一个实用细节事件线和 UART 就绪状态是紧耦合的主机的 GPIO 中断触发后如果 UART 的 RXNE 还没收到数据可以在 ISR 里设置一个 2ms 的定时器中断续延检查这样 GPIO 中断不阻塞 UART既保证事件第一时间被记录又给从机 UART 发送留足了时间。最后这条续延技巧博主在多个量产项目里验证下来是兼顾响应速度和系统稳定性的平衡点。本文还有配套的精品资源点击获取

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

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

免费获取报价