尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

4.0EXTI外部中断

4.0EXTI外部中断 中断 CPU正在干自己的事情突然发生了一个需要处理的事件CPU暂时停下当前工作去处理这个事件处理完再回来。68个可屏蔽中断通道是什么STM32里面有很多东西都可以产生中断。1. 按键/外部引脚GPIO外部中断EXTIPA0上的按键按下 ↓ EXTI产生中断 ↓ CPU处理2. 定时器定时器计时到指定时间可以产生中断。TIM2 ↓ 计时1000ms ↓ 产生中断 ↓ 执行中断函数这样你就不用Delay_ms(1000);一直傻等了。3. 串口比如 USART 收到一个字节USART ↓ 收到数据 ↓ 产生中断 ↓ CPU处理例如电脑给 STM32 发ASTM32可以在收到A的时候自动进入串口中断。4. ADCADC转换完成以后ADC开始转换 ↓ 转换完成 ↓ 产生中断 ↓ CPU处理所以你看到EXTI TIM ADC USART SPI I2C RTC ...不同的硬件事件都可以“叫醒”CPU让CPU去处理。什么叫“可屏蔽中断”可屏蔽中断 可以通过配置让CPU暂时不理它。按键中断你可以配置允许 → 按键来了处理 禁止 → 按键来了不处理所以叫可屏蔽Maskable你可以把它理解成“这个电话我可以接也可以先不接。”什么是 NVIC这个就是你这张图第二条最重要的东西。使用NVIC统一管理中断NVIC 全名Nested Vectored Interrupt Controller中文一般叫嵌套向量中断控制器NVIC就是STM32里面专门负责管理中断的“大管家”。为什么需要 NVIC假设同时发生按键中断 定时器中断 串口中断CPU不可能三个同时执行。所以必须决定谁先处理 谁后处理这就是中断优先级。什么叫“优先级”假设按键中断优先级 2 定时器中断优先级 1 串口中断优先级 3一般可以理解成1最高 2其次 3最低那么同时来的时候定时器 ↓ 先处理 ↓ 按键 ↓ 再处理 ↓ 串口什么叫“嵌套”正在执行普通程序 ↓ 低优先级中断来了 ↓ 进入低优先级中断 ↓ 突然又来了一个更高优先级中断 ↓ 暂停当前这个低优先级中断 ↓ 去执行高优先级中断 ↓ 高优先级中断结束 ↓ 回来继续低优先级中断 ↓ 结束 ↓ 回到主程序这就是中断嵌套可以理解成主程序 └── 中断A └── 中断B每个中断通道都拥有16个可编程的优先级这意味着优先级 0 1 2 3 ... 15一共16级。但这里有一个非常容易踩坑的地方数字越小优先级越高也就是说0 1 2 3不是15最高而是0最高 15最低可对优先级进行分组进一步设置抢占优先级和响应优先级这句话非常重要。STM32的中断优先级实际上可以进一步拆成两部分抢占优先级 响应优先级你现在可以先建立这张脑图STM32中断 │ ┌──────────┴──────────┐ ↓ ↓ 中断来源 NVIC │ │ ┌──────┼──────┐ 管理中断 ↓ ↓ ↓ │ EXTI TIM USART ↓ ↓ ↓ ↓ 优先级 按键 定时器 串口 │ ┌─┴─┐ ↓ ↓ 抢占 响应外设发生事件 ↓ 产生中断请求 ↓ NVIC管理 ↓ 判断优先级 ↓ CPU响应中断 ↓ 进入中断服务函数 ↓ 处理事情 ↓ 清除中断标志 ↓ 返回原来的程序STM32的优先级总空间是4 bit因此总共有 2416 种优先级组合“优先级分组”只是把这4 bit拆分成抢占优先级和响应优先级两者的位数加起来仍然是4。EXTI 是什么EXTIExternal Interrupt/Event Controller外部中断 / 事件控制器是 STM32 单片机里的一个外设。 它的核心作用是监测指定 GPIO 引脚的电平变化当引脚电平发生符合要求的跳变时不需要 CPU 循环查询引脚状态就能主动向 CPU 发起中断请求让 CPU 暂停当前主程序去执行对应的中断处理函数也可以不经过 CPU直接触发其他硬件外设的动作。简单说它是 GPIO 的 “电平变化报警器”用来替代 CPU 轮询大幅节省 CPU 资源。EXTI 基本结构拆解1. 输入层GPIO 端口所有 GPIO 端口GPIOA、GPIOB、GPIOC……的 16 个引脚Pin0~Pin15都可以作为 EXTI 的信号输入源。关键限制相同引脚编号不能同时使用。比如 PA0、PB0、PC0 不能同时触发 EXTI0 中断同一时刻只能从所有端口的 0 号引脚里选一个接入 EXTI0 通道。2. 路由层AFIO 中断引脚选择AFIO复用功能 IO在这里的核心功能是中断引脚路由多选一开关它接收所有 GPIO 端口的引脚信号对每个引脚编号Pin0~Pin15做一次 “二选一 / 多选一”比如 Pin0 通道选择是 PA0、PB0 还是 PC0 接入后续的 EXTI。输入是多组 GPIO 的 16 根引脚线输出仍然是 16 根线对应 EXTI 的 16 个 GPIO 中断通道。3. 核心层EXTI 边沿检测及控制这是 EXTI 外设的本体主要完成 3 件事边沿检测与触发配置可以配置触发规则上升沿电平从低变高、下降沿电平从高变低、双边沿任意跳变都触发、软件触发代码手动触发无需外部信号。只有符合配置的电平变化才会触发后续动作。通道管理一共管理 20 个通道前 16 个通道EXTI0 ~ EXTI15对应 GPIO 的 Pin0~Pin15是最常用的外部引脚中断。后 4 个内部通道PVD电源电压监测、RTC实时时钟闹钟、USB 唤醒、ETH以太网唤醒属于芯片内部外设的唤醒 / 中断信号也挂载在 EXTI 上。双模式输出中断模式把中断信号发给 NVIC最终打断 CPU执行中断服务函数最常用。事件模式不经过 CPU直接输出一个硬件事件脉冲给其他外设比如触发 DMA、启动 ADC响应更快、不占用 CPU适合低功耗场景。4. 裁决层NVIC嵌套向量中断控制器接收 EXTI 发来的中断请求负责中断优先级配置、中断屏蔽、最终裁决是否打断 CPU 当前程序。注意一个硬件设计细节EXTI0 ~ EXTI4每个通道都有独立的 NVIC 中断通道各自对应一个中断函数。EXTI5 ~ EXTI9共用一个 NVIC 中断通道图中 EXTI9_5。EXTI10 ~ EXTI15共用一个 NVIC 中断通道图中 EXTI15_10。 共用通道的引脚触发中断时会进入同一个中断服务函数需要在函数里通过标志位手动判断是哪个引脚触发的。最下方的 “其它外设”则对应事件模式的输出EXTI 直接把事件信号发给其他硬件外设不经过 CPU 和 NVIC。EXTI 核心特性触发方式支持上升沿触发、下降沿触发、双边沿触发、软件触发四种。引脚覆盖所有 GPIO 引脚都能配置为外部中断但同编号 Pin 不能同时使用。通道总数16 个 GPIO 引脚通道 4 个内部外设通道共 20 个通道。两种响应方式中断响应CPU 跳转执行中断函数适合需要软件处理的场景。事件响应纯硬件联动不占用 CPU适合高速、低功耗的硬件触发场景。GPIO 引脚电平发生跳变 → AFIO 路由将信号传入对应 EXTI 通道 → EXTI 检测到符合配置的边沿 → 置起中断标志位向 NVIC 发送中断请求 → NVIC 根据优先级判断是否响应 → CPU 暂停主程序跳转到对应中断服务函数执行 → 执行完毕后返回主程序。一、AFIO 复用 IO 口1. AFIO 是什么AFIO 全称Alternate Function I/O复用功能输入输出端口是 STM32 中负责引脚功能分配的 “硬件开关矩阵”。 STM32 的单个物理引脚通常同时具备 GPIO、串口、定时器、SPI 等多种功能AFIO 的核心作用就是决定引脚最终连接到哪一个外设功能同时负责外部中断的引脚路由选择。2. AFIO 的两大核心任务对应图中文字说明复用功能引脚重映射很多外设的引脚有默认位置通过 AFIO 的重映射寄存器可以把外设的引脚切换到另一组备用引脚。比如 USART1 默认使用 PA9/PA10开启重映射后可以换到 PB6/PB7方便 PCB 布线和引脚资源调配。中断引脚选择EXTI 路由这是和 EXTI 强相关的核心功能对应右侧的「外部中断通用 I/O 映像」框图。3. 右侧框图外部中断引脚选择逻辑右侧的图直观展示了 AFIO 如何把 GPIO 连接到 EXTI 通道一共 16 个 EXTI 通道EXTI0 ~ EXTI15每个通道固定对应一个引脚编号Pin0 ~ Pin15。每个 EXTIx 通道都是一个多选一多路开关以 EXTI0 为例它的输入端是 PA0、PB0、PC0、PD0、PE0、PF0、PG0 所有端口的 0 号引脚。通过AFIO_EXTICR1寄存器的EXTI0[3:0]这 4 位配置选择其中一个端口的 0 号引脚接入到 EXTI0 通道。以此类推EXTI1 由AFIO_EXTICR1的对应位配置EXTI15 由AFIO_EXTICR4的对应位配置。关键结论这就是 “相同 Pin 号不能同时触发中断” 的硬件原因 —— 每个 EXTIx 通道同一时刻只能选一个端口的对应引脚比如选了 PA0 做外部中断PB0、PC0 就不能同时使用 EXTI0。二、EXTI 内部详细框图这张图是 EXTI 外设的完整内部硬件结构我们按信号从右到左的流向拆解整体分为输入检测、中断输出、事件输出三大部分。1. 输入级边沿检测与触发配置最右侧是 20 根输入线16 路 GPIO 4 路内部外设信号从这里进入 EXTI边沿检测电路负责识别引脚的电平跳变。上升沿触发选择寄存器配置哪些通道开启「低电平→高电平」的上升沿触发。下降沿触发选择寄存器配置哪些通道开启「高电平→低电平」的下降沿触发。两个寄存器同时开启即为双边沿触发任意电平跳变都能触发。检测到符合配置的跳变后电路输出有效信号到下一级。2. 触发汇总硬件触发 软件触发中间的或门是触发信号的汇总点两个输入任意一个有效就会输出触发信号一路来自前面的边沿检测电路对应外部硬件触发。另一路来自软件中断事件寄存器对应软件触发直接写寄存器就能手动模拟触发不需要外部电平变化。这一级的输出信号会同时分成两条路径中断响应路径和事件响应路径。3. 路径一中断响应输出到 NVIC这是最常用的路径最终会打断 CPU 执行中断函数请求挂起寄存器触发信号到来时对应通道的挂起标志位会被硬件置 1表示该通道有中断请求待处理。我们在中断服务函数里清除中断标志操作的就是这个寄存器。中断屏蔽寄存器相当于每个通道的中断独立开关。对应位为 1 时中断放行为 0 时即使有触发也不会向 NVIC 发送请求。与门 输出至 NVIC只有 “挂起标志置 1” 且 “中断未被屏蔽” 同时满足时与门才会输出有效信号最终把中断请求送到 NVIC由 NVIC 裁决是否打断 CPU 主程序。4. 路径二事件响应输出到其它外设这是纯硬件联动路径不经过 CPU事件屏蔽寄存器和中断屏蔽寄存器功能类似单独控制每个通道的事件输出是否开启。与门 脉冲发生器事件未被屏蔽且触发信号有效时信号进入脉冲发生器输出一个固定宽度的硬件脉冲。 这个脉冲直接送到芯片内部其它外设如 DMA、ADC、定时器可以直接触发外设工作全程不占用 CPU。5. 顶部寄存器访问总线所有配置寄存器屏蔽、触发选择、软件触发、挂起等都通过APB 总线由 CPU 读写配置工作时钟由 PCLK2 提供。我们写代码配置 EXTI本质就是通过 APB 总线操作这些寄存器。中断 vs 事件的核心区别中断需要 CPU 响应进入中断服务函数执行代码有软件处理过程会打断主程序适合需要逻辑处理的场景。事件纯硬件动作直接产生脉冲联动外设不经过 CPU响应速度极快适合低功耗唤醒、高速硬件触发场景。旋转编码器简介旋转编码器是一种测量旋转状态的传感器核心作用是检测转轴的「旋转位置、转速、旋转方向」。 它的工作逻辑很简单转轴转动时编码器会输出两路方波电信号我们读取方波的频率就能算出转速频率越快转速越高读取两路方波的相位先后顺序就能判断正转还是反转统计脉冲总数就能算出旋转的角度和当前位置。三种常见类型机械触点式靠内部金属弹片 / 触点的通断输出信号成本很低常见于旋钮调节比如音量旋钮、菜单选择旋钮缺点是有机械磨损寿命短转动时会有触点抖动。霍尔传感器式靠磁场变化触发霍尔元件输出信号无机械接触寿命长、耐震动很多小型减速电机自带的编码器大多是这种。光栅式光电式内部有带缝隙的光栅盘靠透光 / 遮光触发光电管输出信号精度最高、支持高转速工业伺服电机、数控机床的高精度编码器基本都是这类。配图从左到右大致对应光栅码盘结构、旋钮式机械编码器、带编码器的减速电机、工业级光电编码器。硬件电路原理这是单片机外接旋转编码器最经典、最常用的接口电路1. 右侧模块框图这是简化后的接线示意编码器模块供电VCC接 3.3V 电源GND接地。信号输出一共引出 3 个引脚A、B、C其中 A、B 是两相脉冲信号分别接到单片机的PB0、PB1引脚C 是公共端直接接地。2. 左侧详细电路A/B 相对称设计这是完整的信号调理电路作用是给编码器供电、滤除干扰抖动保证单片机读到稳定的信号每一路的功能如下上拉电阻 R1、R210KΩ机械编码器本身是无源的触点开关没有上拉就输出不了稳定的高低电平。接上拉电阻到 VCC 后触点断开时A/B 引脚被拉成高电平触点接通时A/B 引脚通过内部触点连到 C地变成低电平由此产生高低变化的方波。RC 滤波电路R3/C1、R4/C2R3/R410KΩ C1/C20.1uF组成低通滤波。机械编码器的触点在通断瞬间会产生 “弹跳抖动”出现很多杂乱的窄脉冲霍尔 / 光电编码器也可能有电源干扰毛刺。RC 滤波可以把这些抖动、干扰滤掉让信号变平滑避免单片机误判脉冲数、计错角度。C 引脚是编码器内部所有触点的公共端这里直接接地作为电平的参考零点。补充A/B 相怎么判断方向和速度编码器正常转动时A、B 两路方波会保持90° 相位差也叫正交信号正转时A 相信号的上升沿 / 下降沿总是比 B 相提前出现反转时B 相信号的上升沿 / 下降沿总是比 A 相提前出现。单片机通过检测两路信号的跳变顺序就能判断旋转方向统计脉冲总数就能计算旋转角度 / 位置统计 1 秒内的脉冲个数就能算出转速。对射式红外传感器计次硬件代码按这个流程来写驱动//主函数 #include stm32f10x.h #include Delay.h #include OLED.h #include sensor.h int main(void) { sensorinit(); OLED_Init(); OLED_ShowString(1,1,COUNT:); while (1) { OLED_ShowNum(2,1,count,8); if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_6)0)//如果没东西解锁 { lock0; } if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0)0)//按键按下时归0 { count0; } } } //传感器驱动 #include stm32f10x.h // Device headerV void sensorinit() { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//开启GPIOBAFIO时钟,EXTI和NVIC无需手动开启 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_ModeGPIO_Mode_IPU; GPIO_InitStructure.GPIO_PinGPIO_Pin_6|GPIO_Pin_0; GPIO_InitStructure.GPIO_SpeedGPIO_Speed_50MHz; GPIO_Init(GPIOB,GPIO_InitStructure); GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource6);//AFIO把P6接到EXTI //AFIO每个芯片只能规定一次如果重复规定分组必须相同 EXTI_InitTypeDef EXTI_InitStructure; EXTI_InitStructure.EXTI_Line EXTI_Line6; EXTI_InitStructure.EXTI_Mode EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger EXTI_Trigger_Falling; EXTI_InitStructure.EXTI_LineCmd ENABLE; EXTI_Init(EXTI_InitStructure);//EXTI配置 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); NVIC_InitTypeDef NVIC_InitSturcture; NVIC_InitSturcture.NVIC_IRQChannel EXTI9_5_IRQn; NVIC_InitSturcture.NVIC_IRQChannelPreemptionPriority0; NVIC_InitSturcture.NVIC_IRQChannelSubPriority0; NVIC_InitSturcture.NVIC_IRQChannelCmdENABLE; NVIC_Init(NVIC_InitSturcture); //NVIC配置 } volatile uint16_t count0; uint8_t lock0; uint16_t GetCount(void) { return count; } void EXTI9_5_IRQHandler (void)//中断是自行触发的。 { if(EXTI_GetITStatus(EXTI_Line6)SET) { if(lock0) { lock1; count; } EXTI_ClearITPendingBit(EXTI_Line6);//清除中断标记 } } //库文件 #ifndef __sensor_H #define __sensor_H void sensorinit(void); uint16_t GetCount(void); extern volatile uint16_t count;//外部计次用 extern uint8_t lock;//传感器式防止抖动。 #endif //OLED的三个文件详情请看上期旋转编码器计次1. 旋转编码器 AB 相的本质旋转编码器输出两路方波信号通常称为A 相和B 相。它们之间的相位差为90°也就是一个周期内一路信号的变化总是比另一路超前或滞后四分之一周期。静止时由于你的 GPIO 配置为GPIO_Mode_IPU上拉输入编码器内部开关断开两个引脚都被上拉到高电平所以PA0 1A 相PA1 1B 相旋转时编码器内部触点依次接地变为低电平然后释放形成方波。由于机械结构或光栅排列A 和 B 不会同时变化而是有一个先、一个后这就是相位差。2. 根据你的代码反推电平变化你的配置是PA0 下降沿触发中断中断里读 PA1 电平如果 PA1 0 →count--如果 PA1 1 →count你观察到向右转是 向左转是 -右转正转count在 PA0 下降沿发生时PA1 必须为高电平1因为代码中else count。也就是说右转时PA0 先于 PA1 变为低电平。一个完整的右转“步进”一个 detent电平变化如下步骤PA0 (A)PA1 (B)说明静止11两相都高101A 先接地 → PA0 下降沿此时 B1所以 count200B 也跟着接地310A 释放回到高电平PA0 上升沿不触发中断411B 释放回到静止波形示意右转textPA0: 1‾‾‾‾\_____/‾‾‾‾‾‾‾‾↓下降沿PA1: 1‾‾‾‾‾‾‾‾\_____/‾‾‾‾↑此时B1左转反转count--在 PA0 下降沿发生时PA1 必须为低电平0因为代码中if(PA10) count--。也就是说左转时PA1 先于 PA0 变为低电平。一个完整的左转“步进”电平变化如下步骤PA0 (A)PA1 (B)说明静止11两相都高110B 先接地PA1 下降沿但你没有配置 PA1 中断200A 跟着接地 → PA0 下降沿此时 B0所以 count--301B 释放PA1 上升沿411A 释放回到静止波形示意左转textPA0: 1‾‾‾‾‾‾‾‾\_____/‾‾‾‾↓下降沿PA1: 1‾‾‾‾\_____/‾‾‾‾‾‾‾‾此时B03. 为什么相位差能判断方向核心就是在一个信号的边沿例如 A 的下降沿去观察另一个信号的电平就能知道谁先谁后。右转A 先变化所以在 A 下降沿时B 还没变保持原来的高电平 → 读到 1。左转B 先变化所以在 A 下降沿时B 已经变低了 → 读到 0。这就是正交编码器方向判断的经典方法。4. 总结你的编码器工作状态静止PA01, PA11右转PA1 先保持高PA0 先产生下降沿中断里读到PA11count左转-PA1 先变成低然后 PA0 才下降中断里读到PA10count--//主函数 #include stm32f10x.h #include Delay.h #include OLED.h #include rotaryencoding.h int main(void) { OLED_Init(); rotaryencoding(); OLED_ShowString(1, 1, count); while (1) { OLED_ShowSignedNum(2,1,count,8); } } //驱动 #include stm32f10x.h // Device header //当想配置两个中断时GPIO和EXTI的配置可以使用|符号其余不行。 void rotaryencoding(void) //时钟开启 { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //配置GPIO GPIO_InitTypeDef GPIO_Initstructure; GPIO_Initstructure.GPIO_ModeGPIO_Mode_IPU; GPIO_Initstructure.GPIO_PinGPIO_Pin_0|GPIO_Pin_1; GPIO_Initstructure.GPIO_SpeedGPIO_Speed_50MHz; GPIO_Init(GPIOA,GPIO_Initstructure); //配置AFIO GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); //配置EXTI EXTI_InitTypeDef EXTI_Structure; EXTI_Structure.EXTI_LineEXTI_Line0; EXTI_Structure.EXTI_LineCmdENABLE; EXTI_Structure.EXTI_Trigger EXTI_Trigger_Falling; EXTI_Structure.EXTI_ModeEXTI_Mode_Interrupt; EXTI_Init(EXTI_Structure); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); //配置EXTI0-NVIC NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelCmdENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority0; NVIC_InitStructure.NVIC_IRQChannelSubPriority0; NVIC_Init(NVIC_InitStructure); } volatile int32_t count0; volatile uint8_t lock0; void EXTI0_IRQHandler(void) //中断0 { if(EXTI_GetITStatus(EXTI_Line0)SET) { if(lock0) { lock1; if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_1)0) count--; else count; EXTI_ClearITPendingBit(EXTI_Line0); } lock0; } } //驱动 #ifndef __rotaryencoding_H #define __rotaryencoding_H void rotaryencoding(void); extern int32_t count; extern uint8_t lock; #endif
返回列表