目录
[0 前言](#0 前言)
[1 中断相关概念描述](#1 中断相关概念描述)
[1.1 什么是中断?](#1.1 什么是中断?)
[1.2 异常与中断](#1.2 异常与中断)
[1.3 中断嵌套](#1.3 中断嵌套)
[2 NVIC 详解](#2 NVIC 详解)
[2.1 NVIC简介](#2.1 NVIC简介)
[2.2 中断优先级原理](#2.2 中断优先级原理)
[2.3 中断优先级分组](#2.3 中断优先级分组)
[2.3.1 举例说明](#2.3.1 举例说明)
[2.4 中断编程要点](#2.4 中断编程要点)
[2.4.1 配置并使能外设中断源](#2.4.1 配置并使能外设中断源)
[2.4.2 配置NVIC(优先级与使能)](#2.4.2 配置NVIC(优先级与使能))
[2.4.3 编写中断服务函数 (ISR)](#2.4.3 编写中断服务函数 (ISR))
[3 EXTI(外部中断/事件控制器)](#3 EXTI(外部中断/事件控制器))
[3.1 EXTI简介](#3.1 EXTI简介)
[3.2 中断与事件的区别](#3.2 中断与事件的区别)
[3.3 EXTI功能框图](#3.3 EXTI功能框图)
[3.3.1 核心信号路径分析](#3.3.1 核心信号路径分析)
[3.3.2 中断生成路径(红色虚线路径)](#3.3.2 中断生成路径(红色虚线路径))
[3.3.3 事件生成路径(绿色虚线路径)](#3.3.3 事件生成路径(绿色虚线路径))
[3.3.4 中断与事件的关键区别总结](#3.3.4 中断与事件的关键区别总结)
[3.4 EXTI 中断线映射关系](#3.4 EXTI 中断线映射关系)
[3.4.1 EXTI Line 0~15 ------ GPIO 外部中断线](#3.4.1 EXTI Line 0~15 —— GPIO 外部中断线)
[3.4.2 AFIO_EXTICRx 寄存器的作用说明](#3.4.2 AFIO_EXTICRx 寄存器的作用说明)
[3.4.3 EXTI Line 16~19 ------ 固定的内部事件线](#3.4.3 EXTI Line 16~19 —— 固定的内部事件线)
[3.5 EXTI初始化结构体](#3.5 EXTI初始化结构体)
[4 EXTI 外部中断配置流程](#4 EXTI 外部中断配置流程)
[4.1 开启相关外设时钟](#4.1 开启相关外设时钟)
[4.2 配置GPIO模式](#4.2 配置GPIO模式)
[4.3 配置 GPIO 与 EXTI 中断线的映射关系](#4.3 配置 GPIO 与 EXTI 中断线的映射关系)
[4.4 初始化并使能 EXTI 中断线](#4.4 初始化并使能 EXTI 中断线)
[4.5 配置 NVIC(中断优先级与通道)](#4.5 配置 NVIC(中断优先级与通道))
[4.5.1 配置中断优先级分组](#4.5.1 配置中断优先级分组)
[4.5.2 配置 EXTI15_10 中断通道](#4.5.2 配置 EXTI15_10 中断通道)
[4.6 编写外部中断服务函数](#4.6 编写外部中断服务函数)
[5 相关元器件简介](#5 相关元器件简介)
[5.1 对射式红外传感器](#5.1 对射式红外传感器)
[5.1.1 传感器概述](#5.1.1 传感器概述)
[5.1.2 硬件结构与工作原理](#5.1.2 硬件结构与工作原理)
[5.1.3 输出逻辑说明](#5.1.3 输出逻辑说明)
[5.2 旋转编码器模块](#5.2 旋转编码器模块)
[5.2.1 旋转编码器概述](#5.2.1 旋转编码器概述)
[5.2.2 工作原理](#5.2.2 工作原理)
[6 本章节实验](#6 本章节实验)
[6.1 对射式红外传感器计次](#6.1 对射式红外传感器计次)
[6.1.1 实验目标](#6.1.1 实验目标)
[6.1.2 硬件设计](#6.1.2 硬件设计)
[6.1.3 软件设计](#6.1.3 软件设计)
[6.1.4 实验现象](#6.1.4 实验现象)
[6.2 旋转编码器计次](#6.2 旋转编码器计次)
[6.2.1 实验目标](#6.2.1 实验目标)
[6.2.2 硬件设计](#6.2.2 硬件设计)
[6.2.3 软件设计](#6.2.3 软件设计)
[6.2.3.1 判断旋转方向机制](#6.2.3.1 判断旋转方向机制)
[6.2.4 实验现象](#6.2.4 实验现象)
0 前言
STM32 拥有一套功能完善、结构清晰的中断系统,其核心是 嵌套向量中断控制器(NVIC,Nested Vectored Interrupt Controller)。在 STM32 中,几乎所有片上外设------如 GPIO、定时器、串口、ADC 等------都具备产生中断请求的能力,并统一由 NVIC 进行管理和调度。
由于中断机制贯穿 STM32 各类外设的工作过程,是理解外设运行逻辑与系统实时行为的关键基础,本文将对 STM32 中断系统进行系统讲解,为后续 EXTI、定时器中断、串口中断等内容建立清晰、统一的认知框架。
1 中断相关概念描述
1.1 什么是中断?
在 STM32 中,中断是一种由硬件或软件触发的程序控制机制。当外部或内部事件发生时,CPU 会在当前指令执行完成后,自动暂停正在运行的主程序,转而跳转到对应的**中断服务程序(Interrupt Service Routine, ISR)**执行预先编写的处理逻辑。
中断服务程序执行完成后,CPU 会自动返回中断发生的位置,继续执行原来的程序流程。如图所示:

通过中断机制,处理器无需反复轮询外设状态,就能对突发事件做出及时响应,从而显著提高系统的执行效率和实时性。
1.2 异常与中断
在 Cortex-M 内核架构中,异常(Exception) 与 **中断(Interrupt)**在机制上是统一的概念,二者本质上都是能够打断 CPU 正常执行流程的事件。区别主要在事件来源:
- 异常:通常指由 Cortex-M 内核自身产生的事件,例如复位、硬件错误、系统服务调用(SVC)等。
- 中断:通常指由片上外设或外部信号产生的事件,例如 GPIO 外部中断、定时器更新中断、串口接收中断等。
虽然命名不同,但在 NVIC 管理体系中,异常与中断共用同一套向量表和优先级管理机制,统一由 NVIC 进行调度和控制。
1.3 中断嵌套
STM32 支持中断嵌套机制。当一个中断服务程序正在执行时,如果此时有一个优先级更高的中断请求到达,CPU 会暂停当前中断服务程序,转而执行高优先级的中断服务程序。
高优先级中断处理完成后,CPU 会按照嵌套顺序逐级返回,继续执行先前被中断的程序。如图所示:

中断嵌套是实现复杂实时系统的重要机制,能够保证关键事件的及时响应。
注意:中断服务函数(ISR)不需要、也不允许由用户手动调用。当中断条件满足时,系统会自动跳转到对应的 ISR 执行。用户只需按需求编写 ISR 内的处理逻辑即可。
2 NVIC 详解
2.1 NVIC简介
嵌套向量中断控制器NVIC(Nested Vectored Interrupt Controller) 是集成在 Cortex-M 内核内部的中断控制单元,它主要负责:
- 管理中断源: 决定哪些外设可以发出中断。
- 优先级判定: 当多个中断同时请求时,裁决谁先执行。允许中断嵌套。
- 中断向量表管理:将中断请求与对应的 ISR 地址关联。
**注意:**STM32 仅使用了 Cortex-M 设计中的部分优先级位(通常是高 4 位),这决定了其最大优先级等级为2\^4 = 16级。
2.2 中断优先级原理
中断优先级是中断系统的核心规则,它决定了CPU 在多个中断同时到来时的响应顺序。
在 STM32F103 中,每个可编程中断拥有 4 位有效的优先级位。这 4 位可以通过优先级分组被划分为两部分:抢占优先级(Preemption Priority) 和 响应优先级(Sub Priority)。
- **抢占优先级:**决定是否能"插队"。如果 A 的抢占优先级高于 B,A 可以直接打断正在执行的 B,实现中断嵌套。
- **响应优先级:**决定"排队顺序"。如果两个中断的抢占优先级相同,它们之间不能相互打断。若同时到达,NVIC 会根据响应优先级决定先让谁执行。
- **自然优先级:**如果抢占和响应优先级都相同,则参考中断向量表中的物理编号(由硬件设计决定),编号越小,优先级越高。
2.3 中断优先级分组
在 STM32 中,硬件留给每个中断的优先级空间只有 4 个比特位(Bit)。分组的本质,就是决定这 4 位中,多少位拨给抢占,多少位拨给响应。
| 分组值 | 抢占优先级位数 | 响应优先级位数 | 抢占优先级取值范围 | 响应优先级取值范围 |
|---|---|---|---|---|
| 0 | 0 位 | 4 位 | 0 | 0~15 |
| 1 | 1 位 | 3 位 | 0~1 | 0~7 |
| 2 | 2 位 | 2 位 | 0~3 | 0~3 |
| 3 | 3 位 | 1 位 | 0~7 | 0~1 |
| 4 | 4 位 | 0 位 | 0~15 | 0 |
2.3.1 举例说明
设定背景: 选择分组 2。假设系统中有三个中断ABC:
| 中断 | 抢占优先级 | 响应优先级 |
|---|---|---|
| A (EXTI0) | 0 | 0 |
| B (USART1) | 1 | 0 |
| C (TIM2) | 1 | 1 |
1、抢占优先级:决定"能否插队"(嵌套关系)
核心逻辑: 抢占优先级不同时,高优先级(数值越小越高)可中断低优先级的运行。
- 场景: 串口 B (USART1) 正在执行。
- 触发: 此时外部中断A (EXTI0) 触发。
- 结果: 允许嵌套。因为 A 的抢占优先级(0)高于 B(1),CPU 会暂停 B,转而执行 A,执行完 A 后再回到 B。
- 结论: 抢占优先级是实现"急症先看"的硬性标准。
2、响应优先级:决定"同时到达谁先走"(排队关系)
核心逻辑: 抢占优先级相同时,两个中断不能互相打断。若同时到达,由响应优先级决定先后。
- 场景: 串口 B (USART1) 和定时器 C (TIM2) 同时触发。
- 结果: B 先执行。因为两者的抢占优先级相同(都是 1),此时对比响应优先级,B(0)高于 C(1)。
- 结论: 响应优先级是在抢占级别相同下的"内部排序"。
3、特殊情况:正在运行时的"非嵌套"逻辑
核心逻辑: 即使响应优先级更高,只要抢占优先级相同,就无法实现嵌套。
- 场景: 定时器 C (TIM2) 正在执行。
- 触发: 此时串口 B (USART1) 触发。
- 结果: 不能打断,需排队。虽然 B 的响应优先级(0)高于 C(1),但它们的抢占优先级相同。B 必须等待 C 执行完毕后才能开始。
- 结论: 同级抢占,永不嵌套。
通常,中断优先级分组在整个系统中只设置一次,它针对的是所有中断。后续为某个具体中断设置优先级时,只需要在这个分组规则下指定其对应的抢占和响应优先级即可。
2.4 中断编程要点
使用 STM32 标准库配置并使用中断,一般遵循三个层级的步骤:
- 在外设层配置并使能外设中断源(外设产生 IRQ);
- 在内核层(NVIC)配置优先级并使能对应中断通道(CPU 响应控制);
- 在软件层编写中断服务例程(ISR),并在 ISR 中完成事件处理与必要的中断清除。
下面按这三层级详细说明(以 EXTI 外部中断为例)。
2.4.1 配置并使能外设中断源
通过配置外设相关寄存器,使外设在检测到指定硬件事件时,能够沿着预设的中断通道向中断控制器(NVIC)产生有效的中断请求信号(Interrupt Request,IRQ),从而允许 CPU 对该事件作出中断响应。
该步骤的本质,是构建并打通从外设中断触发事件到 NVIC 之间的中断信号传输通路。
在默认状态下,外设硬件事件仅影响其内部状态,而不会触发内核中断请求;只有在完成相关配置后,外设在捕捉到指定的逻辑跳变(如 GPIO 下降沿)时,才能向 NVIC 产生中断请求。
以 EXTI 外部中断为例,配置过程通常包括以下三步:
(1)GPIO 引脚初始化
将中断引脚配置为输入模式(如上拉输入),以便正确采集外部电平变化。
cpp
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
(2)GPIO 引脚映射到 EXTI 中断线
通过 AFIO ,将指定 GPIO 引脚映射到对应的 EXTI 中断线,建立 GPIO 与 EXTI 之间的硬件连接关系。
cpp
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1);
(3)EXTI 外设初始化
配置 EXTI 中断线的工作模式、触发方式,并使能中断线,使其能够在满足条件时产生中断请求。
cpp
EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_Init(&EXTI_InitStructure);
完成上述配置后,从 GPIO 事件 → EXTI 中断线 → NVIC 的中断传递通道被打通,外设在满足设定的硬件条件时即可向 NVIC 发出中断请求。
2.4.2 配置NVIC(优先级与使能)
NVIC(嵌套向量中断控制器)负责管理来自各个外设的中断请求,并决定 CPU 是否响应、何时响应以及响应顺序。
在标准库中,需要使用 NVIC_InitTypeDef 结构体并调用 NVIC_Init() 完成配置,关键内容包括:
- 中断通道选择
- 抢占优先级设置
- 响应优先级设置
- 中断通道使能
在配置具体中断前,需先设置 NVIC 的优先级分组(整个工程只需配置一次):
cpp
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
随后配置对应的中断通道,例如 EXTI0:
cpp
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
只有在 NVIC 中使能对应 IRQ 通道后,CPU 才会真正响应外设发出的中断请求。
2.4.3 编写中断服务函数 (ISR)
中断服务函数(Interrupt Service Routine, ISR)用于描述 CPU 在响应中断后需要执行的具体操作。
ISR 的函数名必须与启动文件 startup_stm32f10x_hd.s 中向量表定义的名称完全一致,例如:
cpp
void EXTI0_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line0) == SET)
{
// 中断处理代码
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
通常,所有中断服务函数统一编写在 stm32f10x_it.c 文件中,便于管理和维护。
若函数名错误或未定义,链接器将无法正确绑定中断向量,导致中断无法执行。
到此,一个完整、规范的 STM32 标准库中断配置流程即告完成。
关键 :在中断服务函数中,必须调用库函数检查并清除对应的中断挂起标志位(如
EXTI_GetITStatus()和EXTI_ClearITPendingBit()),否则 CPU 会认为中断一直存在,从而不断重复进入该中断。
3 EXTI(外部中断/事件控制器)
3.1 EXTI简介
EXTI(External Interrupt / Event Controller,外部中断/事件控制器)是 STM32 中专门用于管理 GPIO 外部信号 以及 部分内部事件源 的中断/事件控制模块。
EXTI 内部共提供 20 条中断/事件线(EXTI Line 0 ~ Line 19),能够对这些输入信号的电平变化进行检测,并根据用户配置,产生 中断请求 或 事件脉冲。
其主要特性包括:
-
**触发方式灵活:**支持上升沿触发、下降沿触发、双边沿触发以及软件触发,能够满足按键检测、信号同步等多种应用场景。
-
**GPIO 资源覆盖全面:**所有 GPIO 端口均可作为 EXTI 输入源使用。但需要注意的是,相同引脚编号的 GPIO(如 PA0、PB0、PC0)在同一时刻只能有一个被映射到对应的 EXTI 线,不能同时作为中断源。
-
**中断线数量与分类明确:**EXTI 共包含:
16 条 GPIO 专用中断线(Line 0 ~ Line 15),分别对应 GPIO 的 Pin0 ~ Pin15
4 条内部功能中断线(Line 16 ~ Line 19),固定连接至特定内部事件源,用于:
PVD 电源电压监测输出
RTC 闹钟事件
USB 唤醒事件
以太网唤醒事件(仅部分型号支持)
- **双重响应机制:**EXTI 支持两种工作模式
中断模式(Interrupt):通过 NVIC 触发 CPU 响应,执行中断服务函数
事件模式(Event):产生内部事件脉冲,直接触发其他外设动作,无需 CPU 参与
3.2 中断与事件的区别
理解 中断 与 事件 的区别是掌握 EXTI 的关键:
- **中断(Interrupt):**软件级响应。EXTI 检测到触发条件后,向 NVIC 发出中断请求,最终导致 CPU 跳转执行中断服务函数。
- **事件(Event):**硬件级响应。EXTI 产生内部脉冲信号,用于直接触发其他外设(如 ADC、DMA),无需 CPU 参与,效率更高。
在低功耗模式下,芯片内部的 WIC(唤醒中断控制器) 可以监测 EXTI 产生的中断或事件,用于唤醒处理器和整个系统。
3.3 EXTI功能框图
EXTI的功能框图包含了 EXTI 模块最核心的工作机制。理解该功能框图,可以从整体上把握 外部信号是如何被 EXTI 接收、判断、筛选,并最终产生中断或事件的。
在 EXTI 功能框图中,很多信号线旁边标有斜杠并注明 "/20",这表示 EXTI 内部存在 20 条结构完全相同的中断/事件线路(EXTI Line0 ~ Line19)。因此,只要理解其中任意一条线路的工作原理,其余线路也可以类推。

EXTI 从功能上可以分为两大部分:
- 中断生成路径(最终进入 NVIC,由 CPU 执行中断服务函数)
- 事件生成路径(产生一个硬件脉冲信号,直接触发其他外设)
这两条路径在前端电路上是共用的,但在后端控制和输出形式上完全不同。
3.3.1 核心信号路径分析
无论是产生 中断(Interrupt) 还是 事件(Event),EXTI 的前端信号处理流程是完全相同的,其核心工作机制如下。
(1)输入信号源
EXTI 控制器内部共包含 20 条中断/事件输入线(EXTI0 ~ EXTI19),其来源可分为两类:
- **EXTI0 ~ EXTI15:**可通过 AFIO 进行映射,对应到任意一个 GPIO 端口的同号引脚(如 PA0、PB0、PC0 等)
- **EXTI16 ~ EXTI19:**固定连接到片内外设事件源(如 PVD、RTC、USB 等),不经过 GPIO 映射
这些输入线的信号本质上都是数字信号,通常以电平跳变(上升沿或下降沿)的形式出现。
(2)边沿检测电路
输入信号进入 EXTI 的边沿检测模块,该模块由以下两个寄存器控制:
- 上升沿触发选择寄存器:
EXTI_RTSR - 下降沿触发选择寄存器:
EXTI_FTSR
通过对这两个寄存器相应位的配置,EXTI 可实现三种触发方式:
- 仅上升沿触发
- 仅下降沿触发
- 上升沿和下降沿同时触发
边沿检测电路以输入线信号为检测对象,当检测到被允许的电平跳变瞬间时,会产生一个 瞬时有效的触发脉冲信号 输出到后续逻辑;若未检测到合法边沿,则不会产生触发信号。
(3)或门电路(硬件触发与软件触发合并)
边沿检测模块的输出信号,会与软件中断/事件寄存器 EXTI_SWIER 的输出,通过一个"或门"电路进行合并。这意味着 EXTI 的触发来源可以是:
- 外部硬件触发:GPIO 或外设信号产生电平跳变;
- 软件触发 :程序主动写
EXTI_SWIER,人为产生一次触发事件;
只要两者之一产生有效信号,EXTI 后续逻辑就会被激活。从这里开始,信号将分别进入中断生成路径和事件生成路径。
3.3.2 中断生成路径(红色虚线路径)
中断生成路径的最终目标是:在满足触发条件且未被屏蔽的前提下,向 NVIC 提交中断请求,并由 CPU 进入对应的中断服务函数(ISR)执行处理逻辑。该路径描述的是 EXTI 产生中断时的完整硬件---软件协同流程。
(4)与门电路(中断屏蔽控制)
由边沿检测电路或软件触发寄存器 EXTI_SWIER 合并后的触发信号,会进入中断生成路径中的"与门"控制结构。该与门的另一输入来自 中断屏蔽寄存器 EXTI_IMR(Interrupt Mask Register) ,其作用是决定对应的 EXTI 线是否允许产生中断请求。
- **当 EXTI_IMR 对应位为 0 时:**表示该 EXTI 线路被屏蔽。此时即便前端已经检测到有效的边沿触发信号,触发请求也会被阻断,与门输出无效,中断不会被生成;
- **当 EXTI_IMR 对应位为 1 时:**表示该线路未被屏蔽,触发信号才被允许通过,进入后续的中断生成流程。
在中断未被屏蔽且触发条件成立的前提下,EXTI 控制器才会将该中断请求记录到 挂起寄存器 EXTI_PR(Pending Register) 中,对应位置 1,用于表明该 EXTI 线路已经成功产生中断请求并处于挂起状态。EXTI_PR 的本质作用是保存已发生的中断请求状态,同时作为 EXTI 向 NVIC 提交中断请求的硬件依据。
提示 :在 ST 给出的 EXTI 功能框图中,请求挂起寄存器 EXTI_PR 的位置容易造成理解偏差,使人误以为其在中断屏蔽判断之前参与中断生成逻辑。需要明确的是,EXTI_PR 并不承担"是否产生中断"的判定功能,而仅用于记录已经发生且被允许的中断请求状态。从硬件逻辑上看,只有在中断屏蔽寄存器 EXTI_IMR 对应位使能的前提下,EXTI 才会将中断请求写入 EXTI_PR,并进一步提交给 NVIC。
(5)进入 NVIC
EXTI_PR 中记录的挂起信息会被送入 NVIC。NVIC 根据当前系统的中断使能状态、优先级分组配置以及抢占关系,判断该中断请求是否能够被立即响应,并在条件满足时向 CPU 发出中断请求。
当 CPU 响应该中断后,当前程序执行流被打断,转而跳转执行对应的中断服务函数(ISR)。在 ISR 中,必须由软件主动清除 EXTI_PR 中对应的挂起标志位,其方式是向该位写 1。若未清除该标志位,该中断请求将持续处于挂起状态,CPU 在中断返回后会再次进入同一中断,最终导致程序反复进入中断、无法正常运行。
中断生成路径属于软件级处理流程,其显著特点是需要 NVIC 与 CPU 的参与,适用于在中断中执行判断、运算、状态更新等较为复杂的控制逻辑场景。
3.3.3 事件生成路径(绿色虚线路径)
事件生成路径的作用是:在满足触发条件时,直接向片上其他外设输出一个事件脉冲信号,用于触发其特定动作(如 TIM 启动、ADC 开始转换等)。该过程属于硬件级联动机制,不经过 NVIC,不会引发 CPU 中断,也不需要任何软件参与。
(6)与门电路(事件屏蔽控制)
来自或门电路(硬件触发与软件触发合并) 的触发信号,与**事件屏蔽寄存器 EXTI_EMR(Event Mask Register)**的对应位信号,通过一个"与门"进行逻辑合并。
EXTI_EMR 的作用是控制对应 EXTI 线是否允许产生事件输出:
- **当 EXTI_EMR 对应位为 0 时:**表示事件被屏蔽,即使前端已经检测到有效触发信号,与门输出仍为无效,不会产生事件;
- **当 EXTI_EMR 对应位为 1 时:**表示事件未被屏蔽,触发信号才被允许进入后续事件生成电路。
需要注意的是,事件路径仅受 EXTI_EMR 控制,与中断屏蔽寄存器 EXTI_IMR 以及挂起寄存器 EXTI_PR 均无直接关系。
(7)脉冲发生器
当事件被允许且触发条件成立时,与门的输出信号进入脉冲发生器电路。该电路会将持续时间不确定的触发信号转换为一个固定宽度的短脉冲,以满足外设对事件触发信号的时序要求。
(8)事件脉冲输出
脉冲发生器输出的事件脉冲可直接连接至片上其他外设,用作硬件触发源,例如:
- 用于触发定时器 TIM 的启动、复位或捕获操作;
- 用于触发 ADC 开始一次转换;
- 用于驱动 DMA 等其他硬件模块的工作。
通过事件路径,可以实现由外部信号或软件触发信号自动联动外设动作,整个过程完全由硬件完成,不占用 CPU 资源,响应延迟极小,特别适合对实时性要求较高的外设协同控制场景。
3.3.4 中断与事件的关键区别总结
| 特性 | 中断 (Interrupt) | 事件 (Event) |
|---|---|---|
| 目的 | 通知CPU处理异步事件,运行软件服务函数。 | 通知其他外设硬件,触发其硬件动作。 |
| 响应主体 | CPU(软件) | 其他外设(硬件,如TIM, ADC) |
| 核心寄存器 | 中断屏蔽寄存器 (EXTI_IMR) |
事件屏蔽寄存器 (EXTI_EMR) |
| 结果寄存器 | 挂起寄存器 (EXTI_PR) |
无(直接产生脉冲) |
| 本质 | 软件级响应流程 | 硬件级信号联动 |
重要提示 :EXTI外设挂载在APB2总线 上,在编程配置其寄存器时,需确保已使能APB2总线上EXTI的时钟(通常通过
RCC_APB2PeriphClockCmd使能RCC_APB2Periph_AFIO来实现,因为EXTI的引脚复用功能与AFIO相关)。
3.4 EXTI 中断线映射关系
STM32F103 的 EXTI 控制器一共提供 20 条中断 / 事件线(EXTI Line 0~19) ,其中 Line 0~15 用于 GPIO 外部中断 ,Line 16~19 固定连接到特定的内部外设事件源。
3.4.1 EXTI Line 0~15 ------ GPIO 外部中断线
EXTI Line 0~15(即 EXTI0 ~ EXTI15)分别对应 GPIO 的 Pin 0 ~ Pin 15,共 16 条外部中断线。
需要特别注意的是:
- EXTI 线只与"引脚编号"对应,而不与具体 GPIO 端口绑定
- 每一条 EXTI 线,都可以连接到 任意 GPIO 端口中"编号相同"的那个引脚
例如:
- EXTI0 → PA0 / PB0 / PC0 / ...
- EXTI1 → PA1 / PB1 / PC1 / ...
- ...
- EXTI15 → PA15 / PB15 / PC15 / ...
这种 "同号引脚任意映射" 的能力,并不是硬件自动完成的,而是通过 AFIO(Alternate Function I/O,复用功能 I/O)模块中的 AFIO_EXTICRx 寄存器 来配置实现的。如图所示:

在同一时刻,每一条 EXTI 线只能连接一个 GPIO 引脚。也就是说:
- 如果 EXTI0 已映射到 PA0
- 那么 PB0、PC0 等引脚 即使电平发生变化,也不会触发 EXTI0 中断
这是因为 EXTI0 的输入源在 AFIO 中只能选择一个端口。
3.4.2 AFIO_EXTICRx 寄存器的作用说明
AFIO_EXTICRx 是 EXTI 与 GPIO 之间的"连线配置寄存器",其本质作用是:指定某一条 EXTI 线,最终监听哪一个 GPIO 端口的同号引脚。
例如:
- 将 EXTI0 映射到 PA0
- 将 EXTI1 映射到 PB1
- 将 EXTI13 映射到 PC13
因此可以总结为一句话:EXTI 线决定"编号",AFIO 决定"端口"。
**注:**在配置 EXTI 的 GPIO 映射关系之前,必须先使能 AFIO 外设时钟,否则对 AFIO_EXTICRx 寄存器的配置不会生效。
3.4.3 EXTI Line 16~19 ------ 固定的内部事件线
EXTI Line 16~19 不与 GPIO 相连,而是 固定连接到芯片内部的特定外设事件源,无需、也不能通过 AFIO 进行映射配置。
对应关系如下:
- Line 16:连接到PVD(Programmable Voltage Detector,电源电压监测)输出
- Line 17:连接到RTC 闹钟事件
- Line 18:连接到USB 唤醒事件
- Line 19:连接到以太网唤醒事件(仅适用于互联型产品)
这些 EXTI 线通常用于:
- 低功耗模式下的系统唤醒
- 特定外设状态变化的快速响应
3.5 EXTI初始化结构体
EXTI 使用 EXTI_InitTypeDef 结构体进行配置,主要包括中断线、触发方式、工作模式等参数。
cpp
EXTI_InitTypeDef EXTI_InitStructure; //定义结构体变量
EXTI_InitStructure.EXTI_Line = EXTI_Line14; //选择配置外部中断的14号线
EXTI_InitStructure.EXTI_LineCmd = ENABLE; //指定外部中断线使能
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //指定外部中断线为中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //指定外部中断线为下降沿触发
EXTI_Init(&EXTI_InitStructure); //将结构体变量交给EXTI_Init,配置EXTI外设
4 EXTI 外部中断配置流程
以下是一个完整的、使用标准库配置 GPIO 外部中断的步骤,以实验《5-1 对射式红外传感器计次》为例展示配置流程。
在实验中,对射式红外传感器的 DO 输出端 连接至 STM32 的 PB14 引脚。当挡光片遮挡红外光路时,传感器输出由高电平变为低电平,从而触发 下降沿外部中断,在中断服务函数中完成一次计数,并将计数结果实时显示在 OLED 屏幕上。
由于 PB14 对应 EXTI14,而 EXTI14 属于 EXTI15_10 中断通道,因此本例完整展示了 GPIO → AFIO → EXTI → NVIC → 中断服务函数 的标准配置流程。
4.1 开启相关外设时钟
外部中断的配置涉及 GPIO、AFIO、EXTI、NVIC 等多个模块,其中:
- GPIO 和 AFIO 挂载在 APB2 总线
- AFIO 负责 GPIO 与 EXTI 线的映射关系
因此,在任何配置之前,必须先开启 GPIOB 和 AFIO 的时钟:
cpp
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 开启 GPIOB 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 开启 AFIO 时钟(EXTI 映射必须)
注意 :如果未开启 AFIO 时钟,即使后续调用
GPIO_EXTILineConfig(),EXTI 映射也不会生效。
4.2 配置GPIO模式
对射式红外传感器的 DO 引脚在无遮挡时输出高电平,遮挡时输出低电平。因此,将 PB14 配置为 上拉输入模式,使其在空闲状态下保持稳定高电平。
cpp
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14; // PB14
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
这样,当挡光片经过时,PB14 会产生高 → 低的下降沿,用于触发外部中断。
4.3 配置 GPIO 与 EXTI 中断线的映射关系
PB14 对应的是 EXTI14,但 EXTI 并不会自动知道应监听哪个 GPIO 端口,因此需要通过 AFIO 模块进行映射配置。
cpp
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);
该语句的含义是:将 EXTI14 中断线 的输入源,连接到 GPIOB 的第 14 号引脚(PB14)
注意:同一条 EXTI 线(如 EXTI14)在同一时刻只能连接一个 GPIO 引脚,PB14、PA14、PC14 不能同时作为 EXTI14 的触发源。
4.4 初始化并使能 EXTI 中断线
完成映射后,需要对 EXTI14 本身进行配置,包括:
- 中断 / 事件模式选择
- 触发方式
- 使能中断线
cpp
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line14; // 选择 EXTI14
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; // 中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发
EXTI_InitStructure.EXTI_LineCmd = ENABLE; // 使能中断线
EXTI_Init(&EXTI_InitStructure);
这里选择 下降沿触发,正好对应传感器被遮挡时的电平变化特性。
4.5 配置 NVIC(中断优先级与通道)
4.5.1 配置中断优先级分组
在 STM32 中,NVIC 的优先级分组针对整个系统只需配置一次。
cpp
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
该分组方式表示:
- 抢占优先级:2 位(0~3)
- 响应优先级:2 位(0~3)
4.5.2 配置 EXTI15_10 中断通道
由于 EXTI10 ~ EXTI15 共用一个 NVIC 通道,因此 EXTI14 对应的中断号是 EXTI15_10_IRQn。
cpp
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
4.6 编写外部中断服务函数
EXTI14 属于 EXTI15_10 通道,因此中断服务函数名必须为:
cpp
void EXTI15_10_IRQHandler(void)
该函数由启动文件中的中断向量表自动调用,无需在主程序中手动调用。
cpp
void EXTI15_10_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line14) == SET) // 判断是否为 EXTI14 触发
{
/* 可选:再次读取引脚电平,减少抖动影响 */
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) == 0)
{
CountSensor_Count++; // 计数值加 1
}
EXTI_ClearITPendingBit(EXTI_Line14); // 清除中断挂起标志
}
}
注意:必须在中断服务函数中手动清除 EXTI 的中断挂起标志位,否则 CPU 会不断重复进入该中断,导致程序"卡死"。
5 相关元器件简介
5.1 对射式红外传感器
5.1.1 传感器概述
对射式红外传感器(又称槽型光耦传感器、计数测速传感器模块)是一种常见的 光电开关型传感器,广泛用于 脉冲计数、转速检测、位置限位和物体检测等场合。

该类传感器通常由 红外发射管 和 红外接收管 组成,两者相对安装,中间形成一个固定宽度的"光槽"。当光路未被遮挡时,接收端可以正常接收到红外光;当有物体进入槽中遮挡光路时,接收状态发生变化,从而在输出端产生明确的电平跳变信号。
接口简介:
| 引脚名 | 功能说明 |
| VCC | 电源正极(3.3V~5V) |
| GND | 电源负极 |
| DO | 数字输出(TTL 电平) |
| AO | 模拟输出(本模块未接出/不起作用) |
|---|
在本实验中,对射式红外传感器用于检测挡光片遮挡的次数,并通过 STM32 的 外部中断(EXTI) 实现计数。
5.1.2 硬件结构与工作原理
电路原理图如图所示:

该模块内部主要由以下几部分构成:
- 槽型红外光耦器件: 发射端持续发射红外光,接收端检测光强变化。模块槽宽通常为 5 mm,适合插入码盘、挡光片等结构。
- **比较器电路(LM393):**接收端的模拟信号经 LM393 比较器处理后,输出稳定、波形边沿陡峭和抗干扰能力强的数字信号。
- **状态指示灯(LED):**用于指示 DO 输出状态,便于调试与观察。
在本实验中,仅需使用 VCC、GND 和 DO 三个引脚。具体连接方式如下:
- VCC 接入 STM32 的 3.3V 电源
- GND 连接 STM32 的地线
- DO 引脚接至 PB14(EXTI14)
PB14 配置为上拉输入模式,并通过 EXTI 下降沿触发外部中断。
5.1.3 输出逻辑说明
在本实验所使用的对射式红外传感器模块中,其输出逻辑关系如下表所示:
| 光路状态 | 接收管状态 | DO 输出电平 | 指示灯状态 |
|---|---|---|---|
| 无遮挡 | 导通 | 低电平 | 亮 |
| 有遮挡 | 截止 | 高电平 | 灭 |
由此可知:
- 当物体进入光路、产生遮挡时,DO 引脚电平由低电平跳变为高电平,产生一个上升沿;
- 当物体离开光路、遮挡消失时,DO 引脚电平由高电平跳变为低电平,产生一个下降沿;
因此,从信号角度来看:开始遮挡和移除遮挡,都会各自产生一次明确的边沿信号。因此,在工程中既可以使用上升沿触发,也可以使用下降沿触发外部中断。在本工程中,外部中断被配置为下降沿触发(EXTI_Trigger_Falling),其原因是:
- 下降沿对应遮挡结束(物体完全通过)的时刻;
- 该时刻信号稳定、抖动相对较小;
- 更符合"一次完整通过 → 计数一次"的应用语义;
当然,在实际应用中,如果希望在 物体刚进入光路时即完成计数,也可以将 EXTI 触发方式改为 上升沿触发(EXTI_Trigger_Rising),二者在硬件条件允许的情况下本质上并无对错之分,只是计数时机不同。
5.2 旋转编码器模块
5.2.1 旋转编码器概述
旋转编码器(Rotary Encoder)是一种用于测量旋转位置、旋转速度以及旋转方向 的传感器件。当编码器的旋转轴发生转动时,其输出端会产生与旋转状态相关的方波信号 。通过读取这些方波信号的频率 和相位关系,即可判断旋转轴的:
- 转动方向(顺时针 / 逆时针)
- 转动速度
- 相对位移(步进量)
因此,旋转编码器特别适合需要手动调节的人机交互场景,主要包括以下典型应用:
- 旋钮式输入控制
- 音量和频率调节
- 菜单导航选择
- 参数调整(如高度、温度等)
实物如图所示:

接口简介:

其中,A 相与 B 相信号通常接入 STM32 的 GPIO 或 EXTI 引脚,用于方向和计数判断。
5.2.2 工作原理
EC11 旋转编码器在旋转轴发生顺时针或逆时针转动时,会在输出端产生一组具有固定相位差的方波信号,供单片机或微控制器进行采集与分析。
EC11 编码器具有 A 相 和 B 相 两路信号输出,这两路信号均为方波信号,且它们之间始终保持 90° 的相位差 。这种具有固定相位差的双路编码方式称为正交编码信号(Quadrature Encoder Signal)。
下图给出了旋转轴分别沿顺时针方向 和逆时针方向转动时,A 相与 B 相的典型输出波形。

如图所示,旋转方向与两相信号的相位先后关系如下:
- **顺时针旋转(CW):**A 相信号超前 B 相信号 90°
- **逆时针旋转(CCW):**B 相信号超前 A 相信号 90°
在实际应用中,单片机只需判断 A 相与 B 相中哪一路信号先发生电平变化,即可可靠地确定旋转方向。具体的代码实现我在6.2.3小节去解释。
6 本章节实验
6.1 对射式红外传感器计次
6.1.1 实验目标
通过本实验,完成基于对射式红外传感器的计次功能,实现被测物体经过传感器时的自动计数。并能够掌握以下内容:
- 使用 GPIO 输入模式 读取外部传感器信号
- 掌握 EXTI 外部中断 的基本配置流程(GPIO → AFIO → EXTI → NVIC)
- 理解 中断服务函数(ISR) 的执行机制与使用规范
- 学会在中断中进行 事件计数,并通过全局变量保存结果
- 将传感器采集结果实时显示在 OLED 显示屏 上
6.1.2 硬件设计

6.1.3 软件设计
软件部分主要由 计数传感器驱动模块 和 主程序显示逻辑 两部分组成。
- 计数传感器使用 PB14 引脚 作为信号输入端口,配置为 上拉输入模式
- 通过 AFIO 将 PB14 映射到 EXTI14 外部中断线
- 外部中断触发方式设置为 边沿触发,当传感器输出信号发生变化时进入中断
- 在 EXTI15_10 中断服务函数 中,对中断来源进行判断,并对计数变量进行自增
- 中断处理完成后,及时清除中断标志位,防止中断重复触发
主函数中完成 OLED 和计数模块的初始化,并在主循环中周期性读取计数值,将结果实时显示在 OLED 屏幕上,实现计数结果的可视化。
具体代码如下:
main.c文件:
cpp
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "CountSensor.h"
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
CountSensor_Init(); //计数传感器初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "Count:"); //1行1列显示字符串Count:
while (1)
{
OLED_ShowNum(1, 7, CountSensor_Get(), 5); //OLED不断刷新显示CountSensor_Get的返回值
}
}
CountSensor.c文件:
cpp
#include "stm32f10x.h" // Device header
volatile uint16_t CountSensor_Count; //全局变量,用于计数
/**
* 函 数:计数传感器初始化
* 参 数:无
* 返 回 值:无
*/
void CountSensor_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //开启AFIO的时钟,外部中断必须开启AFIO的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB14引脚初始化为上拉输入
/*AFIO选择中断引脚*/
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);//将外部中断的14号线映射到GPIOB,即选择PB14为外部中断引脚
/*EXTI初始化*/
EXTI_InitTypeDef EXTI_InitStructure; //定义结构体变量
EXTI_InitStructure.EXTI_Line = EXTI_Line14; //选择配置外部中断的14号线
EXTI_InitStructure.EXTI_LineCmd = ENABLE; //指定外部中断线使能
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //指定外部中断线为中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //指定外部中断线为下降沿触发
EXTI_Init(&EXTI_InitStructure); //将结构体变量交给EXTI_Init,配置EXTI外设
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
//即抢占优先级范围:0~3,响应优先级范围:0~3
//此分组配置在整个工程中仅需调用一次
//若有多个中断,可以把此代码放在main函数内,while循环之前
//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; //选择配置NVIC的EXTI15_10线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
}
/**
* 函 数:获取计数传感器的计数值
* 参 数:无
* 返 回 值:计数值,范围:0~65535
*/
uint16_t CountSensor_Get(void)
{
return CountSensor_Count;
}
/**
* 函 数:EXTI15_10外部中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void EXTI15_10_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line14) == SET) //判断是否是外部中断14号线触发的中断
{
/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) == 0)
{
CountSensor_Count ++; //计数值自增一次
}
EXTI_ClearITPendingBit(EXTI_Line14); //清除外部中断14号线的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
6.1.4 实验现象
程序下载并运行后:
- OLED 屏幕上显示固定字符串
Count:,并实时显示当前计数值 - 当有物体经过对射式红外传感器、遮挡红外光路时,计数值 自动加 1
- 计数结果随遮挡次数增加而递增,系统运行稳定,无明显误触发现象

实验现象表明,外部中断触发和计数逻辑工作正常,成功实现了对射式红外传感器的计次功能。
6.2 旋转编码器计次
6.2.1 实验目标
通过本实验,实现对 旋转编码器旋转方向和旋转步数 的检测,并完成可正负变化的计次功能。并能够掌握以下要点:
- 理解 旋转编码器 A / B 相正交信号 的基本工作原理
- 学会使用 多路 EXTI 外部中断 同时处理编码器两相信号
- 掌握通过 读取另一相电平判断旋转方向 的实现方法
- 理解"增量式计数"的设计思想,并与绝对计数方式进行区分
- 将编码器的旋转结果实时显示在 OLED 显示屏 上
6.2.2 硬件设计

6.2.3 软件设计
软件部分主要由 旋转编码器驱动模块 和 主程序变量调节与显示逻辑 组成。
(1)旋转编码器驱动模块
- 旋转编码器的 A 相与 B 相 分别连接至 PB0 和 PB1 引脚,均配置为 上拉输入模式(GPIO_Mode_IPU),用于稳定空闲状态下的输入电平;
- 通过 AFIO 将 PB0、PB1 分别映射到 EXTI0 和 EXTI1 外部中断线;
- EXTI0 和 EXTI1 均配置为 下降沿触发的外部中断,用于捕获编码器信号的有效跳变;
- 在中断服务函数中,当某一相产生下降沿中断时,立即读取 另一相 当前的电平状态,根据两相信号的相位关系,判断旋转方向并对编码器计数变量
Encoder_Count进行 自增或自减; - 提供
Encoder_Get()接口函数,用于获取自上次调用以来的 编码器增量值,并在返回后自动清零,避免重复累加
(2)主程序变量调节与显示逻辑
在主程序中定义一个可正负变化的变量 Num,在 while(1) 循环中:
- 周期性调用
Encoder_Get()获取旋转编码器的 增量值 - 将该增量值累加到
Num上,实现数值的连续调节 - 通过 OLED 实时显示
Num的当前值
最终实现:通过旋转编码器对变量进行正反方向调节,并实时显示结果。
6.2.3.1 判断旋转方向机制
旋转编码器在轴旋转过程中,A 相和 B 相会持续输出一组具有固定相位差(90°)的方波信号。程序将 A 相和 B 相分别配置为外部中断源,并在每一路信号的下降沿进入对应的中断服务函数,在其中判断旋转方向。
(1)EXTI0(A 相,PB0)中断触发时的判断逻辑
当 A 相(PB0)出现下降沿,程序进入 EXTI0_IRQHandler中断处理函数,函数中会检测PB1的电平,若为高电平,则表示轴旋转方向为逆时针方向,不执行Encoder_Count--,反之则为顺时针方向,执行Encoder_Count--。判断规则如下:
| PB1 电平 | 相位关系说明 | 旋转方向判定 | 程序行为 |
|---|---|---|---|
| 高电平 | A 相领先 B 相 | 逆时针 | 不执行计数 |
| 低电平 | B 相领先 A 相 | 顺时针 | Encoder_Count-- |
即:在 A 相下降沿触发中断时,若 B 相为低电平,则认为当前旋转方向为顺时针,对计数变量执行自减操作。
(2)EXTI1(B 相,PB1)中断触发时的判断逻辑
当 B 相(PB1)出现下降沿,程序进入 EXTI1_IRQHandler中断处理函数,函数中会检测PB0的电平,若为高电平,则表示轴旋转方向为顺时针方向,不执行执行Encoder_Count++,反之则为逆时针方向,执行Encoder_Count++。判断规则如下:
| PB0 电平 | 相位关系说明 | 旋转方向判定 | 程序行为 |
|---|---|---|---|
| 高电平 | B 相领先 A 相 | 顺时针 | 不执行计数 |
| 低电平 | A 相领先 B 相 | 逆时针 | Encoder_Count++ |
即:在 B 相下降沿触发中断时,若 A 相为低电平,则认为当前旋转方向为逆时针,对计数变量执行自增操作。
具体代码如下:
main.c文件:
cpp
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Encoder.h"
int16_t Num; //定义待被旋转编码器调节的变量
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
Encoder_Init(); //旋转编码器初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "Num:"); //1行1列显示字符串Num:
while (1)
{
Num += Encoder_Get(); //获取自上此调用此函数后,旋转编码器的增量值,并将增量值加到Num上
OLED_ShowSignedNum(1, 5, Num, 5); //显示Num
}
}
Encoder.c文件:
cpp
#include "stm32f10x.h" // Device header
volatile int16_t Encoder_Count; //全局变量,用于计数旋转编码器的增量值
/**
* 函 数:旋转编码器初始化
* 参 数:无
* 返 回 值:无
*/
void Encoder_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //开启AFIO的时钟,外部中断必须开启AFIO的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB0和PB1引脚初始化为上拉输入
/*AFIO选择中断引脚*/
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0);//将外部中断的0号线映射到GPIOB,即选择PB0为外部中断引脚
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1);//将外部中断的1号线映射到GPIOB,即选择PB1为外部中断引脚
/*EXTI初始化*/
EXTI_InitTypeDef EXTI_InitStructure; //定义结构体变量
EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1; //选择配置外部中断的0号线和1号线
EXTI_InitStructure.EXTI_LineCmd = ENABLE; //指定外部中断线使能
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //指定外部中断线为中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //指定外部中断线为下降沿触发
EXTI_Init(&EXTI_InitStructure); //将结构体变量交给EXTI_Init,配置EXTI外设
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
//即抢占优先级范围:0~3,响应优先级范围:0~3
//此分组配置在整个工程中仅需调用一次
//若有多个中断,可以把此代码放在main函数内,while循环之前
//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; //选择配置NVIC的EXTI0线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn; //选择配置NVIC的EXTI1线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; //指定NVIC线路的响应优先级为2
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
}
/**
* 函 数:旋转编码器获取增量值
* 参 数:无
* 返 回 值:自上此调用此函数后,旋转编码器的增量值
*/
int16_t Encoder_Get(void)
{
/*使用Temp变量作为中继,目的是返回Encoder_Count后将其清零*/
/*在这里,也可以直接返回Encoder_Count
但这样就不是获取增量值的操作方法了
也可以实现功能,只是思路不一样*/
int16_t Temp;
Temp = Encoder_Count;
Encoder_Count = 0;
return Temp;
}
/**
* 函 数:EXTI0外部中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void EXTI0_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line0) == SET) //判断是否是外部中断0号线触发的中断
{
/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) //PB0的下降沿触发中断,此时检测另一相PB1的电平,目的是判断旋转方向
{
Encoder_Count --; //此方向定义为反转,计数变量自减
}
}
EXTI_ClearITPendingBit(EXTI_Line0); //清除外部中断0号线的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
/**
* 函 数:EXTI1外部中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void EXTI1_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line1) == SET) //判断是否是外部中断1号线触发的中断
{
/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0) //PB1的下降沿触发中断,此时检测另一相PB0的电平,目的是判断旋转方向
{
Encoder_Count ++; //此方向定义为正转,计数变量自增
}
}
EXTI_ClearITPendingBit(EXTI_Line1); //清除外部中断1号线的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
6.2.4 实验现象
程序运行后:
- OLED 屏幕上显示
Num:以及对应的数值 - 顺时针旋转编码器,数值按固定步进 递增
- 逆时针旋转编码器,数值按固定步进 递减
- 旋转停止后,数值保持不变,不会出现自动变化现象

实验现象表明,系统能够正确识别旋转编码器的旋转方向,并稳定获取旋转增量,实现了基于外部中断的旋转编码器计次功能。