十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32主从I2C通信中中断设计:从EXTI到NVIC的完整指南

STM32主从I2C通信中中断设计:从EXTI到NVIC的完整指南 简介面向嵌入式开发者这份ARM Keil环境下的I2C主从模式中断通信示例工程基于LPC17xx平台演示如何用中断服务程序驱动主设备发起读写、从设备响应并完成多字节数据交换。工程共11个文件压缩包仅20KB包含C源文件、头文件、Keil与IAR的工程配置uvproj、ewp、初始化脚本ini、mac及说明文档结构紧凑便于直接导入工程进行仿真与调试。已有103人学习浏览。示例重点覆盖I2C初始化时的时钟频率、从设备地址与数据速率配置中断服务程序的标志清除与数据收发处理主发送、主接收的操作流程从设备地址与数据帧的构造以及超时、仲裁丢失、应答错误等异常排查。借助Keil调试工具可设置断点、单步执行并查看寄存器与内存状态适合正在调试I2C总线或希望系统理解裸机中断驱动开发的读者。1. 主从结构里的 Interrupt不是可选的加速项而是控制面一个 Master 和一个 Slave 通过 I2C/SPI 交换数据时最容易陷入的误区是把中断当作性能优化先把时序算好再考虑加不加。实际调过带低功耗、多机告警或在线升级的系统就会明白中断线才是第一个要定义的控制面。从设备休眠时 Master 只能靠轮询去“碰运气”双方很容易变成互相等待的死锁。“Master/Slave Interrupt”这类工程命名通常代指一个同时存在两条通路的系统数据通路走 I2C、SPI、UART 或其他物理总线状态通路走一根专门的中断引脚。这根线不只是简单的高/低电平它承载着事件通知、错误上报和启动模式选择三种语义。一个下降沿可能代表从设备已完成上次操作也可能代表它醒来后正在等待主机下发地址。这篇文章不打算把中断抽象成“写个回调函数就完事”而是把边沿触发、NVIC 优先级、I2C 事件标志、boot 选择这几个点串起来给出可以照着改、也值得往后翻的完整路径。2. 先分清触发边沿和中断源Master/Slave 中断的 4 种形态在放置第一行代码前我会先画一张连接表哪个设备的哪个引脚能产生中断中断脚默认电平是什么设备睡眠时谁会去拉这根线。Master/Slave 系统里最常出现的不是“没有中断”而是把中断配置成了谁都说不清的电平状态结果第一次能触发第二次要等几十毫秒才恢复边沿。2.1 外部中断线从设备必须先学会“被叫醒”外部中断对应 STM32 里的 EXTI是主从之间最直接、最容易被看见的中断形态。从设备进入 STOP/Standby 后I2C 外设时钟被关闭总线上的地址匹配无法唤醒它这时 Master 必须通过一个普通 GPIO 先产生边沿把从设备唤醒再启动一次 I2C 传输。下面这段初始化代码是我常用的 EXTI 配置GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_SYSCFG_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_0; GPIO_InitStruct.Mode GPIO_MODE_IT_FALLING; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0); HAL_NVIC_EnableIRQ(EXTI0_IRQn);参数说明GPIO_MODE_IT_FALLING表示下降沿触发要求 Master 默认输出高电平需要通知时拉低再拉高。GPIO_PULLUP保证从设备上电瞬间这条线不会悬空进入随机电平因为如果从设备先启动、Master 后启动线上没有上拉时会因为浮空产生一次误触发。HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0)给中断分配抢占优先级 1、子优先级 0从设备里这组数值一般比 I2C 外设中断低避免数据读写被打断。2.2 外设中断I2C/SPI 的事件中断与错误中断第二类中断来自通信外设本身。对 I2C 从设备来说地址匹配会产生ADDR事件发送数据时数据寄存器空会产生TXIS接收数据时数据寄存器满会产生RXNE。这些标志位如果不及时清除下一个字节传输就被卡住。Master 侧则更关心传输结束和错误标志总线仲裁失败、NACK、超时。不要把这类中断当作“数据搬运完成提醒”它更接近协议层的错误处理入口。2.3 主设备向从设备发的“中断”实际上是一种边沿信号在标准 Master/Slave 拓扑里Slave 主动上报通常通过一根INT#线。因此说“Master 给 Slave 发中断”时实际含义是 Master 把某个 GPIO 配置成输出在一段时间内拉低或拉高Slave 用 EXTI 捕捉这次跳变。方向问题经常在多头主从设计中搞反如果两个 MCU 都想发中断给对方必须使用两根独立引脚共用一根开漏线也能做但需要额外规定谁先拉低、谁释放、谁检测电平恢复。开漏共享线上若有人把引脚配置成推挽输出低电平会把总线锁死这是很常见的低级错误。2.4 一张表决定触发模式与上下拉的匹配使用场景推荐触发方式默认电平典型场景单次事件通知下降沿高带上拉从设备唤醒、数据就绪数据流持续请求上升沿低带下拉从设备请求 Master 持续发送暂停/复位语义低电平高带上拉暂停启动、进入 boot 等待多设备共享告警低电平触发高开漏多个从设备共同拉低一根线边沿触发适合一次性通知因为从设备只要捕获到跳变就够了电平触发适合需要持续保持状态的情况例如“正常启动被中断”这种要求 Master 拉低多长时间都有效的场景。下面这段代码展示了电平模式与中断使能的配合GPIO_InitStruct.Mode GPIO_MODE_IT_FALLING; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_0); NVIC_ClearPendingIRQ(EXTI0_IRQn); NVIC_EnableIRQ(EXTI0_IRQn);LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_0)在使能前先清掉 Pending 位很关键。开发阶段经常遇到一个问题上电时引脚恰好处于低电平一旦使能中断NVIC 立刻响应看起来像“从未触发过中断”实际上是触发标志残留先清再使能可以避免这次误判。3. 用 STM32 HAL 写出 Master 侧的 I2C 中断工程硬件上把 Slave 的INT引脚接在 Master 的 PA0I2C1 用 PB6/PB7主从共地。不要为了省一根线把 Slave 的INT直接并联到 I2C 的 SCL 或 SDA 上中断信号和数据总线混在一起后任何一个从设备拉低都会破坏总线时序。3.1 初始化顺序先 I2C后 EXTI最后 NVIC启动阶段如果先打开 EXTISlave 上电瞬间的毛刺会触发中断而 I2C 还未初始化回调函数里一旦调用 I2C 发送就会死机。反过来先初始化 I2C、后初始化 EXTI 更安全因为从设备即使提前准备好也只会等 Master 主动去读。static void MX_I2C1_Init(void) { hi2c1.Instance I2C1; hi2c1.Init.ClockSpeed 100000; hi2c1.Init.DutyCycle I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 0x31; hi2c1.Init.AddressingMode I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode I2C_DUALADDRESS_DISABLE; hi2c1.Init.GeneralCallMode I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(hi2c1) ! HAL_OK) { Error_Handler(); } }ClockSpeed在 100k 模式下信号沿比较缓适合排查问题规模样机验证通过后再改成 400k。NoStretchMode保持默认DISABLE让从设备可以拉低 SCL 以延长等待时间如果误设为ENABLE从设备无法进行时钟拉伸主从速率不一致时数据会错位。3.2 Master 发送命令前等待中断线释放传输流程一般是Master 先等待INT引脚变为有效电平再执行HAL_I2C_Master_Transmit_IT。volatile uint8_t slave_ready 0; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin GPIO_PIN_0) { slave_ready 1; __HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin); } } void master_send(uint8_t addr, uint8_t cmd) { while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_SET) { __WFI(); } uint8_t data[2] { addr, cmd }; HAL_I2C_Master_Transmit_IT(hi2c1, 0x32, data, 2); }slave_ready是中断回调写入的全局标志主循环里应优先检查它而不是轮询引脚电平__WFI()让 CPU 在等待期间进入休眠降低功耗。如果INT线因为外部干扰始终没有被拉低MCU 会一直等待因此量产代码建议给这个循环加超时计数连续等待超过 500ms 就返回错误不要硬等。3.3 传输完成回调与错误回调各管一件事中断式传输结束后芯片会进HAL_I2C_MasterTxCpltCallback出错则进HAL_I2C_ErrorCallback。错误回调里要区分HAL_I2C_ERROR_AFNACK和HAL_I2C_ERROR_TIMEOUTNACK 表示地址错误或从设备掉线超时表示总线被挂死。参数初始值关注点I2C ClockSpeed100000400k 时注意上拉电阻阻值OwnAddress10x31Master 自己的从机地址EXTI Pin ModeGPIO_MODE_IT_FALLING与 Slave 拉低方式要一致__WFI 等待时间无超时加计数器防止死等4. Slave 侧事件状态机中断位不清下一次主从就卡死从设备如果只处理数据不处理中断状态会出现一种现象第一次通信正常第二次开始 Master 发数据后 Slave 不回应。原因是ADDR或TXIS标志没有清除外设认为总线事件还没结束。4.1 从设备初始化成监听模式I2C 从设备初始化时要调用HAL_I2C_EnableListen_IT否则地址匹配后不会产生中断回调。void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode) { if (TransferDirection I2C_DIRECTION_TRANSMIT) { tx_buffer[0] status_register; HAL_I2C_Slave_Transmit_IT(hi2c, tx_buffer, 1); } else { HAL_I2C_Slave_Receive_IT(hi2c, rx_buffer, 1); } }回调入口处不需要手动清ADDRHAL 在跳进回调前已经处理了主要标志。这里最关键的是TransferDirection的判断Master 读从设备时方向是I2C_DIRECTION_TRANSMITMaster 写从设备时是I2C_DIRECTION_RECEIVE。反了就出现从设备把状态寄存器当成写入目标整个状态机全乱。4.2 从设备接收完成后再通知 Master从设备收到完整命令后通常要置位一个内部标志再把INT线拉低Master 看到INT拉低后才会发起下一次读操作。void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c) { HAL_GPIO_WritePin(INT_GPIO_Port, INT_Pin, GPIO_PIN_RESET); command_received 1; }command_received会被主循环消费INT引脚保持低电平直到 Master 读取状态。这样实现的语义是“我有一件事要告诉你”而不是“我此刻正在传输”。配合低功耗时从设备可以先进入HAL_PWR_EnterSTOPMode等 EXTI 唤醒后再执行 I2C 传输。4.3 多个中断标志的清除顺序决定了链路稳定性标志含义清除方式ADDR地址匹配读 SR1/SR2或 HAL 自动清除TXIS发送数据寄存器空写 DRRXNE接收数据寄存器非空读 DRAF应答失败产生 STOP 条件BTF字节传输完成读/写 DR 或产生 STOP清除顺序一般是“先处理数据再处理控制位”。不要在RXNE还没被读取时就调用HAL_I2C_Slave_Transmit_IT那会让外设认为当前数据还未处理完新的传输永远不会开始。// 错误示范先发后收 HAL_I2C_Slave_Transmit_IT(hi2c1, tx, 1); uint8_t tmp rx_buffer[0];// 正确做法先取走数据再切换方向 uint8_t tmp rx_buffer[0]; HAL_I2C_Slave_Transmit_IT(hi2c1, tx, 1);5. 用一根中断线“打断正常启动”主从系统的 boot 模式切换不少调试器串口启动时会出现一行提示Press Enter to interrupt normal startup。这行字的本质是一个极短的时间窗口启动程序先等待按键如果超时未按下就跳转到应用如果按下了则进入引导命令行。嵌入式主从系统可以把它换成引脚信号Master 在 Slave 复位后立刻拉低INTSlave 的 bootloader 检测到该低电平就不进入正常运行流程而进入 DFU 升级或诊断模式。5.1 启动阶段中断引脚是“状态锁存器”而不是边沿检测器正常运行时我们只关心边沿但启动检测阶段需要关心的是引脚当前电平。因为 Slave 上电后 Master 可能还没来得及拉低等 Slave 真正开始执行时边沿已经过去了只有锁存电平状态才能判断“曾经被中断过”。5.2 启动代码示例void check_boot_mode(void) { LL_GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin BOOT_INT_PIN; GPIO_InitStruct.Mode LL_GPIO_MODE_INPUT; GPIO_InitStruct.Pull LL_GPIO_PULL_UP; LL_GPIO_Init(BOOT_INT_PORT, GPIO_InitStruct); if (LL_GPIO_IsInputPinSet(BOOT_INT_PORT, BOOT_INT_PIN) 0) { // Master 在复位期间拉低了 INT // 中断正常启动进入 IAP 升级 jump_to_dfu(); return; } jump_to_application(); }这段代码没有配置 EXTI只读取电平却能达到同样的控制目的。它的优点是启动阶段不依赖中断控制器NVIC 未初始化也不会漏事件缺点是如果 Master 在复位结束后才拉低Slave 可能已经把INT当普通输入读完导致进入不了升级。因此斜率要求 Master 必须在复位线释放前设置好引脚电平。5.3 实际板级联动时序阶段Master 行为Slave 行为Slave 中断线状态复位拉低 BOOT_INT上电执行启动引导低间隔保持低读取 BOOT_INT 电平低决策释放 BOOT_INT进入 DFU高正常运行输出高应用执行等待 I2C高最好把这个引脚配置成开漏输出并加上拉电阻。这样 Master 控制释放时INT会被上拉电阻恢复为高即使 Slave 提前配置成输出也不会和 Master 打架。6. 收尾验证整条中断链路时必须盯住的 3 个边界中断链路验证不能只点一个调试灯。我们要确认的是IO 状态、触发沿、NVIC 标志、外设标志在同一个时间点的一致性。6.1 先读数据再清标志无论是 EXTI 还是 I2C 中断只要出现误触发第一个怀疑对象就是清标志时机。正确的做法是先把RXDATA读走再清RXNE倒过来会让数据在下一轮被覆盖。用逻辑分析仪抓SDA、SCL和INT三根线时正常波形里INT的下降沿应该出现在 I2C 传输完成后而不是传输过程中。6.2 用一段状态打印函数辅助定位void dump_int_status(void) { printf(EXTI_PR%08lX, I2C_SR1%04X, NVIC%08lX\r\n, (unsigned long)EXTI-PR1, (unsigned int)hi2c1.Instance-SR1, (unsigned long)NVIC-ISPR[0]); }EXTI-PR1里的PR0对应 PA0 的中断标志I2C_SR1的ADDR、AF位能看出 I2C 中断是否卡在某个地址事件NVIC-ISPR[0]则显示当前是否有 Pending 中断没有响应。三个值一起看能快速把问题定位到“引脚根本没跳变”“跳变但 NVIC 没响应”还是“外设标志没清”这三类。6.3 中断优先级反了会造成死锁Master 的 EXTI 中断如果优先级高于 I2C 中断而EXTI_Callback里又调用HAL_I2C_Master_Transmit_IT则先打断 I2C 传输再去发起 I2C 传输时间一长会重入。建议 EXTI 优先级设为低于 I2C 中断让数据传输先完成再进入状态处理。这样虽然中断延迟会略高但不会破坏正在进行的总线时序。本文还有配套的精品资源点击获取
返回列表