主从架构下中断驱动通信:UART与GPIO事件线的工程实践
2026/9/16 13:22:42 网站建设 项目流程

简介:面向嵌入式开发者的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.8us,16 字节完整帧耗时约 1.39ms。轮询模式下,主机为了不漏事件,扫描周期必须小于事件最短保持时间。如果从机事件脉宽只有 2ms,主机就得把扫描周期压到 2ms 以内,而每次扫描总线上挂着 8 个从机地址,每组地址查询也是 1.39ms,8 个从机一轮就是 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 2case 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 发送留足了时间。最后这条续延技巧,博主在多个量产项目里验证下来,是兼顾响应速度和系统稳定性的平衡点。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询