STM32——外部中断
本文讲解STM32的中断系统和外部中断,中断系统是管理和执行中断的逻辑结构,外部中断是众多能产生中断的外设之一,所以本节内容见就借用外部系统学习中断系统。
一、中断系统
-
相关定义
1. 中断
-
定义:在主程序运行过程中,出现了特定的中断触发条件(中断源)(比如对于外部中断来说,可以是引脚发生了电平跳变,对于定时器来说,可以是定时的时间到了,对于串口通信来说,可以是接收到了数据),当这些时间发生时,情况比较紧急(比如外部中断来了,如果不来处理,下一个跳变信号就跟着过来了;比如串口接收中断来了,如果不来读取接收到的数据,那下一个数据再过来,就会把原来的数据覆盖掉,),所以当中断过来时,CPU 暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行。
-
使用中断系统,能够极大地提高程序的效率,比如如果没有中断系统,为了防止外部中断被忽略或者串口数据被覆盖,那主程序就只能不断地查询是否有这些事件发生,不能再干其它事情了,比如如果没有定时器中断,那主程序只有靠Delay函数,才能实现定时的功能,但有了中断系统之后,主程序就可以放心执行其它事情,有中断的时候再去处理,这样效率就可以得到提升。
2. 中断优先
-
定义:当有多个中断源同时申请中断时,CPU 会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源。
-
中断的优先级是我们根据程序设计的需求,自己设置的,中断优先级是为了在多个中断同时申请时,判断一下,应该先处理哪一个。如果事件非常紧急,那就把优先级设置高一些,如果不是那么紧急,那就可以把优先级设置低一些,这样可以更好地安排这些中断事件,防止紧急的事件被别的中断耽误。其中中断优先级还有很多种分类,下文在详细介绍。
3. 中断嵌套
-
定义:当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU 再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回。能否进行中断嵌套,也是由中断优先级来决定的。
-
中断执行流程
1. 主程序与中断程序
-
主程序:这是正常运行的程序,通常是系统或应用程序的主要逻辑部分。
-
中断程序:当特定的中断条件触发时,CPU 会暂停当前正在运行的主程序,转而执行中断程序,中断来了,主程序都得立即暂停,程序由硬件电路自动跳转到中断程序中。中断程序处理完成后,CPU 会返回到主程序的断点处继续执行。
2. 断点
-
断点:这是主程序被中断的位置。当 CPU 响应中断时,会记录下当前主程序执行到的位置,这个位置就是断点。为了程序能在中断返回后继续原来的工作,在中断执行前,会对程序的现场进行保护
3. 中断执行流程
-
单级中断:图中左侧展示了单级中断的执行流程。主程序在运行过程中遇到断点,CPU 暂停主程序,转而执行中断程序。中断程序执行完毕后,CPU 返回到主程序的断点处继续执行。
-
嵌套中断:图中中间和右侧展示了嵌套中断的执行流程。当主程序在运行中断程序时,又有新的中断触发,CPU 会暂停当前的中断程序,转而执行新的中断程序。新的中断程序执行完毕后,CPU 返回到之前的中断程序的断点处继续执行,直到所有中断程序执行完毕,最后返回到主程序的断点处继续执行。
4. 代码示例
-
图的右边展示了 C 语言代码示例,包括主程序(
main
函数)和中断程序(EXTI0_IRQHandler
函数)。主程序在一个无限循环中运行,而中断程序在特定条件下被调用。
-
STM32中的中断
1. STM32 中断概述
-
STM32 微控制器具有 68 个可屏蔽中断通道,这些中断通道涵盖了多个外设,包括 EXTI(外部中断)、TIM(定时器)、ADC(模数转换器)、USART(通用同步 / 异步收发器)、SPI(串行外设接口)、I2C(集成电路总线)和 RTC(实时时钟)等。
2. NVIC 管理中断
-
STM32 使用 NVIC(嵌套向量中断控制器)来统一管理中断。每个中断通道都拥有 16 个可编程的优先等级。这意味着可以对优先级进行分组,进一步设置抢占优先级和响应优先级。
-
抢占优先级:决定了中断能否打断正在执行的低优先级中断。
-
响应优先级:在抢占优先级相同的情况下,决定了中断的响应顺序。
-
3. 中断向量表
-
图片的右侧展示了一个中断向量表(中断地址的列表),列出了各个中断源及其对应的地址。每个中断源都有一个唯一的地址,当中断发生时,程序会跳转到相应的地址去执行中断服务程序。这个地址的作用是,因为程序中的中断函数,它的地址是由编译器来分配的,是不固定的,但是中断跳转,由于硬件的限制,只能跳到固定的地址执行程序,所以为了能让硬件跳转到一个不固定的中断函数,这里就需要在内存中定义里一个地址的列表,这个列表地址是固定的,中断发生后就跳到这个固定位置,然后在这个固定位置,由编译器,再加上一条跳转到中断函数的代码,这样中断跳转就可以跳转到任意位置了。
-
不过用C语言编程的话,是不需要管这个中断向量表的,因为编译器都做好了。
其中灰色的是内核的中断,下面白色的是STM32外设的中断。
4. 中断处理流程
-
当有中断请求时,NVIC 会根据中断的优先级来决定是否响应。如果中断优先级高于当前正在执行的程序或中断的优先级,NVIC 会暂停当前任务,保存当前状态,然后跳转到相应的中断服务程序执行。执行完中断服务程序后,再恢复之前的状态,继续执行被中断的任务。
二、NVIC
-
NVIC基本结构
-
NVIC 的位置和作用
-
NVIC(Nested Vectored Interrupt Controller 嵌套向量中断控制器)位于内核中,是处理中断的关键部件。它负责管理和处理来自各个外设的中断请求。用来统一分配中断优先级和管理中断的。
-
当有中断发生时,NVIC 会根据中断的优先级来决定是否响应,并将 CPU 引导到相应的中断服务程序。
-
-
中断源
-
图中列出了一些常见的中断源,包括 EXTI(外部中断)、TIM(定时器)、ADC(模数转换器)和 USART(通用同步 / 异步收发器)。这些外设在需要 CPU 处理某些事件时会向 NVIC 发送中断请求。
-
每个中断源通过一条线连接到 NVIC,表示它们可以向 NVIC 发送中断请求。
-
这里每个中断源线上画了个斜杠,上面写个n,这个意思是一个外设可能会同时占用多个中断通道,所以这里有n条线。
-
-
优先级设置
-
然后NVIC只有一个输出口,NVIC根据每个中断的优先级分配中断的先后顺序,NVIC 内部有多个优先级,图中显示了优先级 0 到优先级 15。不同的中断可以被分配到不同的优先级。之后,通过右边这一个输出口就告诉CPU,你该处理哪个中断。对于中断先后顺序分配的任务,CPU不需要知道。
-
当多个中断同时发生时,NVIC 会根据优先级来决定先处理哪个中断。优先级高的中断会先得到处理。
-
(举例)比如这个CPU是一个医生,如果医院只有医生的话,当看病的人很多时,医生就得安排一下先看谁,后看谁,如果有紧急的病人,那还得让紧急的病人最先来,这个安排先后次序的任务很繁琐,会影响医生看病的效率,所以医院就安排了一个叫号系统,来病人了统一取号,并且根据病人的等级,分配一个优先级,然后叫号系统看一下现在在排队的病人,优先叫号紧急的病人,最终叫号系统给医生输出的就是一个一个排好队的病人,医生就可以专心看病了,这个叫号系统在STM32里就是NVIC。
-
-
NVIC的优先级分组
1. NVIC 优先级的决定因素
-
NVIC 的中断优先级由优先级寄存器的 4 位二进制的数(0~15)决定,分别对应16个优先级。这 4 位可以进行切分,分为高 n 位的抢占优先级和低 4-n 位的响应优先级。
-
这个优先级的数是值越小,优先级越高,0就是最高优先级,抢占优先级高的中断可以中断嵌套,响应优先级高的中断可以优先排队。
2. 抢占优先级和响应优先级的作用
-
抢占优先级:抢占优先级高的中断可以中断嵌套,即当一个低抢占优先级的中断正在执行时,如果有一个更高抢占优先级的中断请求到来,CPU 会暂停当前中断处理程序,转而去处理更高优先级的中断。
-
响应优先级:响应优先级高的中断可以优先排队。当多个中断具有相同的抢占优先级时,响应优先级高的中断会先被处理。如果抢占优先级和响应优先级均相同,则按中断号排队。
-
(举例)对于紧急的病人,其实有两种形式的优先,一种是,上一个病人在看病,外面排队了很多病人,当上一个病人看完后,紧急的病人即使是后来的,也会最先进去看病,这种相当于插队的优先级,就叫响应优先级,响应优先级高的,可以插队提前看病。另外,如果这个病人更加紧急,并且此时已经有人在看病了,那他还可以不等上个人看完,直接冲到医生的屋里,让上一个病人先靠边站,先给他先看病,等他看完了,然后上一个病人再继续,上一个病人结束了,叫号系统再看有没有人来,这种形式的优先就是中断嵌套,这种决定是不是可以中断嵌套的优先级,就叫抢占优先级。抢占优先级高的,可以进行中断嵌套。
3. NVIC 优先级分组方式
-
为了把每个中断的16个优先级,在区分为抢占优先级和相应优先级,图片中的表格详细列出了不同分组方式下抢占优先级和响应优先级的设置:
-
分组 0:
-
抢占优先级:0 位,取值为 0
-
响应优先级:4 位,取值为 0~15
-
-
分组 1:
-
抢占优先级:1 位,取值为 0~1
-
响应优先级:3 位,取值为 0~7
-
-
分组 2:
-
抢占优先级:2 位,取值为 0~3
-
响应优先级:2 位,取值为 0~3
-
-
分组 3:
-
抢占优先级:3 位,取值为 0~7
-
响应优先级:1 位,取值为 0~1
-
-
分组 4:
-
抢占优先级:4 位,取值为 0~15
-
响应优先级:0 位,取值为 0
-
-
通过这种优先级分组方式,STM32 的 NVIC 可以灵活地处理不同外设的中断请求,确保系统在处理多个中断时能够按照合理的优先级顺序进行处理,从而提高系统的稳定性和可靠性。
这个分组方式在程序中是自己来选择的,选好分组方式之后,在配置优先级的时候,就要注意抢占优先级和响应优先级的取值范围了,不要超出这个表里规定的取值范围。
三、EXIT(例子中的病人)
-
EXIT简介
1. EXTI 定义
-
EXTI 是 External Interrupt 的缩写,意思是外部中断。
2. EXTI 的工作原理
-
EXTI 可以监测指定 GPIO 口的电平信号。当指定的 GPIO 口产生电平变化时,EXTI 将立即向 NVIC 发出中断申请。经过 NVIC 裁决后,即可中断 CPU 主程序,使 CPU 执行 EXTI 对应的中断程序。
3. EXTI 支持的触发方式
-
触发方式:支持上升沿(电平从低电平变到高电平的瞬间触发中断)、下降沿(高电平变到低电平的瞬间触发中断)、双边沿(上升沿和下降沿都可以触发中断)和软件触发(程序里执行一句代码,就能触发中断)。
4. EXTI 支持的 GPIO 口
-
GPIO 口:支持所有 GPIO 口,但相同的 Pin 不能同时触发中断(比如PA0和PB0不能同时用,或者,PA1、PB1、PC1这样的,端口GPIO_Pin一样的,只能选1个作为中断引脚)。
5. EXTI 的通道数
-
通道数:有 16 个 GPIO_Pin(GPIO_Pin_0~GPIO_Pin_15),外加 PVD 输出、RTC 闹钟、USB 唤醒、以太网唤醒。
-
后面跟着的这四个东西其实是来"蹭网"的,因为这个外部中断有个功能,就是从低功耗模式的停止模式下唤醒STM32,那对于PVD电源电压监测,当从电源从电压过低恢复时,就需要PVD借助一下外部中断退出停止模式,对于RTC闹钟来说,有时候为了省电,RTC定一个闹钟之后,STM32会进入停止模式,等到闹钟响的时候再唤醒,这t也需要借助外部中断,还有USB唤醒、以太网唤醒,也都是类似的作用。
6. EXTI 的触发响应方式
-
触发响应方式:支持中断响应和事件响应。
-
中断相应:就是申请中断,让CPU执行中断函数。正常的引脚电平变化触发中断,
-
事件响应:是STM32对外部中断增加的一种额外的功能,当外部中断检测到引脚电平变化时,正常的流程是选择触发中断,但是在STM32中,也可以选择触发一个事件,如果选择触发事件,那外部中断的信号就不会通向CPU了,而是通向其它外设,用来触发其它外设的操作,比如触发ADC转换、触发DMA等。不会触发中断,而是触发别的外设操作,属于外设之间的联合工作。
-
EXIT的基本结构
-
EXTI 的组成部分
-
中断引脚选择(AFIO):位于图的左侧,颜色为红色。这部分负责选择中断引脚,包括 GPIOA、GPIOB、GPIOC 等。每个端口有 16 个引脚,即每个端口可以选择 16 个引脚作为中断源。选择GPIOA、GPIOB、GPIOC其中的一个GPIO外设的16个引脚里选择其中一个连接到后面EXTI的通道里。
-
边沿检测及控制(EXTI):位于图的中间,颜色为橙色。这部分负责检测引脚的边沿变化(上升沿、下降沿等)并进行控制,同时,下面这4个蹭网的外设也是并列接进来的。它有 20 个输入通道,分别对应不同的外部中断源,包括 EXTI0 到 EXTI19_5,以及 PVD、RTC、USB、ETH 等,这些是接到了NVIC,是用来触发中断的。其中ST公司觉得这20个输出太多了,比较占用NVIC的通道资源,所以就把其中外部中断的9~5,和15~10,给分到一个通道里,也就是说,外部中断的9~5会触发同一个中断函数,15~10也会触发同一个中断函数,所以在程序中,需要再根据标志位来区分到底是哪个中断进来的。下面有20条输出线路到了其它外设,这就是用来触发其它外设操作的,也就是前面说的事件响应。
-
NVIC(嵌套向量中断控制器):位于图的右侧,颜色为蓝绿色。这部分负责管理和处理中断请求,决定中断的优先级和处理顺序。
-
-
信号流向
-
外部设备(如 GPIOA、GPIOB、GPIOC 等)的引脚信号首先进入中断引脚选择(AFIO)模块。这些设备通过 16 条信号线连接到 AFIO。
-
AFIO 选择合适的引脚后,将信号传递给边沿检测及控制(EXTI)模块。EXTI 模块有 20 个输入通道,分别处理不同的中断源。
-
最后,EXTI 模块将中断请求发送给 NVIC,NVIC 根据中断优先级和设置来处理中断。
-
-
具体的外部中断源
-
EXTI0 - EXTI19_5:这些是用于 GPIO 引脚的外部中断源,用于检测引脚的电平变化。
-
PVD(Programmable Voltage Detector):可编程电压检测器,用于监测电源电压。
-
RTC(Real - Time Clock):实时时钟,用于定时和闹钟功能。
-
USB(Universal Serial Bus):用于 USB 设备的中断。
-
ETH(Ethernet):用于以太网设备的中断。
-
-
基本结构中的内部电路
1. AFIO复用IO口
AFIO (Alternate Function Input/Output,复用功能输入 / 输出)的主要功能
-
AFIO 主要用于引脚复用功能的选择和重定义。它允许用户灵活地配置芯片的引脚功能,以满足不同的应用需求。
AFIO 在 STM32 中的主要任务
-
在 STM32 中,AFIO 主要完成两个任务:
-
复用功能引脚重映射:这意味着可以将某些内置外设的功能从一个引脚重新映射到另一个引脚上。例如,将 USART1 的 TX 和 RX 功能从默认引脚重新映射到其他可用引脚上。
-
中断引脚选择:AFIO 用于选择外部中断的引脚。例如,选择哪些 GPIO 引脚可以作为外部中断源,并配置它们的触发方式(上升沿、下降沿等)。
-
图示说明
-
图片中包含一个表格,表格标题为 “图 20 外部中断通用 IO 映射”。表格展示了如何通过 AFIO 的相关寄存器(如 AFIO_EXTICRx 寄存器)来选择外部中断的引脚。
-
中间是一系列数据选择器(它的符号是一个梯形,有多个输入,一个输出,在侧面有选择控制端,根据控制端的数据,从输入选择一个接到输出)。
-
例如,在 AFIO_EXTICR1 寄存器的 EXTI1 位,可以选择 PA1、PB1、PC1 等引脚作为外部中断 1(EXTI1)的输入引脚。类似地,在 AFIO_EXTICR4 寄存器的 EXTI15 位,可以选择 PA15、PB15、PC15 等引脚作为外部中断 15(EXTI15)的输入引脚。尾号都是相同的,然后通过数据选择器,最终选择一个,连接到EXTI1或EXTI15上。
-
2. EXIT框图
总体结构
-
图中展示了外部中断 / 事件控制器(EXTI)的整体框架。EXTI 主要用于处理外部中断和事件,确保系统能够正确响应外部信号。
输入线
-
图的右侧有 20 根输入线,这些输入线连接到外部设备,用于接收外部中断和事件信号。
逻辑电路
-
边沿检测电路:检测输入信号的边沿变化,根据上升沿或下降沿触发选择寄存器的设置来决定是否触发中断。
-
脉冲发生器:用于生成脉冲信号,确保中断信号的正确触发。进入前经过一个或门和一个与门
-
事件屏蔽寄存器:用于控制是否允许事件信号通过,类似于中断屏蔽寄存器的功能,但针对事件信号。(与中断屏蔽寄存器作用一样用于开关控制)最后通过一个脉冲发生器,到其它外设。这个脉冲发生器就是给一个电平脉冲,用来触发其它外设的动作。
寄存器
-
上升沿触发选择寄存器 和 下降沿触发选择寄存器:用于选择中断触发的边沿类型。可以选择上升沿、下降沿或双边沿触发。
-
软件中断事件寄存器:允许软件触发中断事件,提供了一种通过软件控制中断的方式。与输入进来的数据一起经过一个或门后,兵分两路,上一路是触发中断的,下一路是触发事件的。
-
请求挂起寄存器:用于记录当前是否有中断请求正在等待处理。当外部设备触发中断时,相应的位会被置位。用于接受兵分两路的信号的其中通向触发中断的那个信号。相当于一个中断标志位。可以读取这个寄存器判断是哪个通道触发的中断,如果中断挂起寄存器置1,它就会继续向左走,和中断屏蔽寄存器共同进入一个与门,然后是至NVIC中断控制器。
-
中断屏蔽寄存器:用于控制是否允许中断信号通过。当该寄存器中的某位被设置时,对应的中断信号将被屏蔽,无法触发中断。(起到开关的作用,可以配置中断屏蔽寄存器的值来控制信号是否通过)
主要组件
-
AMBA APB 总线:位于图的最上方,是系统的主要总线,用于连接各个组件。可以通过总线访问这些寄存器。
-
外设接口:连接到 AMBA APB 总线,负责与外部设备通信。
-
PCLK2:时钟信号,为 EXTI 提供时钟源,确保各个组件同步工作。
与 NVIC 的连接
-
图中显示了 EXTI 与 NVIC(嵌套向量中断控制器)的连接。NVIC 负责管理和处理系统中的所有中断,EXTI 通过至 NVIC 中断控制器的连接将中断请求传递给 NVIC。
(重点)总结
对于外部中断来说,刚学习完它的原理和结构,那到底什么样的设备需要用到外部中断呢,使用外部中断有什么好处呢?
使用外部中断模块的特性:对于STM32来说,想要获取的信号是外部驱动的很快的突发信号,比如旋转编码器的输出信号,可能很久都不会拧它,这时不需要STM432做任何事,但是一拧它,就会有很多脉冲波形需要STM32接收,这个信号是突发的,STM32不知道什么时候会来,同时它是外部驱动的,STM32只能被动读取,最后这个信号非常快,STM32稍微晚一点来读取,就会错过很多波形,那对于这种情况来说,就可以考虑使用STM32的外部中断了。有脉冲过来,STM32立即进入中断函数处理,没有脉冲的时候,STM32就专心做其它事情。
另外还有,比如红外遥控接收头的输出,接收到遥控数据之后,会输出一段波形,这个波形转瞬即逝,并且不会等你,所以就需要用外部中断来读取。
最后还有按键,虽然它的动作也是外部驱动的突发事件,但并不推荐用外部中断来读取按键,因为用外部中断不好处理按键抖动和松手检测的问题,对于按键来说,它的输出波形也不是转瞬即逝的,所以要求不高的话可以在主程序中循环读取,如果不想用主循环读取的话,可以考虑一下定时器中断读取的方式,这样既可以做到后台读取按键值、不阻塞主程序,也可以很好地处理按键抖动和松手检测的问题。
四、硬件模块-旋转编码器
1. 旋转编码器的定义和功能
-
定义:旋转编码器是用来测量位置、速度或旋转方向的装置。
-
工作原理:当它的旋转轴旋转时,其输出端可以输出与旋转速度和方向对应的方波信号。通过读取方波信号的频率和相位信息,即可得知旋转轴的速度和方向。
-
以下面第一个图为例:这是编码器最简单的样式,使用的是对射式红外传感器来测速,为了测速,还需要配合一个光栅编码盘,当编码盘转动的时候,红外传感器的红外光就会出现遮挡、透过、遮挡、透过这样的现象,对应模块输出的电平就是高低电平交替的方波,这个方波的个数表示了转过的角度,方波的频率表示转速,就可以用外部中断来捕获这个方波的边沿,以此判断位置和速度。但是这个模块只有一路输出,正转反转输出波形没法区分,所以这种测速方法只能测位置和速度,不能测量旋转方向,那为了进一步测量方向,我们就可以用后面的几种编码器。
2. 旋转编码器的类型
-
机械触点式:这种类型的旋转编码器通过机械触点来检测旋转轴的位置和运动。
-
霍尔传感器式:利用霍尔效应来检测磁场变化,从而确定旋转轴的位置和运动。
-
光栅式:通过光的干涉或衍射原理来检测旋转轴的位置和运动。
3. 图片中的实物展示
-
图片中展示了几种不同类型的旋转编码器实物:
-
中间的是旋转编码器,左边是它的外观,右边是它的内部拆解结构,内部是用金属触点来进行通断的,它的编码盘也是一系列像光栅一样的东西,同时这个金属盘的位置是经过设计的,它能让两侧触点的通断产生一个90度的相位差,最终配合外部电路就可以通过A B相的方波确定正反向旋转。
-
第三个图是直接附在电机后面的编码器,是霍尔传感器形式编码器,中间是一个圆形磁铁,边上有两个位置错开的霍尔传感器,当磁铁旋转时,通过霍尔传感器,就可以输出正交的方波信号。
-
最后一个是独立的编码器元件,它的输入轴转动时,输出就会有波形。
-
4. 硬件电路
硬件电路概述
-
图中展示了一个简单的硬件电路,主要包括一个旋转编码器模块和一些外围电路元件。
-
电路的设计目的是为了使旋转编码器能够正常工作,通常用于测量旋转角度、速度或方向。
电路元件
-
旋转编码器模块:位于图的右侧,标注为 “旋转编码器模块”。它有三个引脚,分别是 A、B 和 C。这些引脚通常用于输出编码器的脉冲信号,以便于测量旋转参数。
-
电阻(R1、R2、R3、R4):阻值均为 10KΩ,分别连接在电路的不同位置。这些电阻可能用于限流或分压,确保电路中的电流和电压在合适的范围内。
-
电容(C1、C2):电容值为 0.1uF,连接在编码器模块的 A 和 B 引脚附近。这些电容可能用于滤波,减少电路中的噪声和干扰。
电源和接地
-
电源(VCC):电路中有多个 VCC 节点,表示电源正极。电源电压为 3.3V,确保电路中的元件能够正常工作。
-
接地(GND):电路中有多个 GND 节点,表示电源负极。接地确保电路中的电流能够形成回路。
未使用的模块
-
图中提到 “上面按键的两根线,这个模块并没有使用,是悬空的”,这表明电路中可能有一些预留的接口或模块没有被使用,这些未使用的部分可能是为了将来的扩展或其他功能而预留的。
触点的硬件电路
-
中间圆形的下面有两个触点,旋转轴旋转时,这两个触点以相位相差90度的方式交替导通,因为这只是个开关信号,所以要配合外围电路才能输出高低电平,由于外围电路接上拉电阻,默认没旋转的状态下默认为高电平,当旋转时,内部这里触点导通,那这个点就直接被拉低到GND了,再通过R3输出,A端口就是低电平了,这个R3是一个输出限流电阻,它是为了防止模块引脚电流过大的4,这个C1是输出滤波电容,可以防止一些输出信号抖动。右边电路和左边一样不做分析。
电路连接
-
旋转编码器模块的 A、B 和 C 引脚分别通过电阻和电容连接到电源和接地。具体连接方式如下:
-
A 引脚通过 R1(上拉电阻) 连接到 VCC,通过 C1 连接到 GND。
-
B 引脚通过 R2 连接到 VCC,通过 C2 连接到 GND。
-
C 引脚通过 R4 连接到 VCC。
-
五、对射式红外传感器计次的代码详解
1. 硬件电路
右侧的OLED用于显示程序的现象,左下角接了一个对射式红外传感器的模块,接线也十分简单,VCC和GND分别接电源的正负极,OD数字输出端随意选择一个GPIO口接上即可,这里接到了PB14的端口。
2. 原理
当我们的挡光片或者编码盘在这个对射式红外传感器中间经过时,这个DO就会输出电平变化的信号,这个电平跳变就会触发STM32 PB14号口的中断。在中断函数中执行变量++的程序,再在主循环中调用OLED显示这个变量。
3. 代码的编写
(1)传感器模块的封装
-
在HardWare文件下,建立CountSensor的.c和.h文件。
-
模块的初始化函数(void CountSensor_Init(void))
-
需要在初始化函数中进行中断的配置,根据前文中介绍的EXIT结构中的结构图,将GPIO到NVIC这一路出现的外设模块都配置好,把这条信号电路打通就可以了
-
第一步:配置RCC,把这里涉及的外设的时钟都打开
-
/*开启时钟*/ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //开启AFIO的时钟,外部中断必须开启AFIO的时钟
-
其中EXIT和NVIC这两个外设的时钟始终是打开的,不需要再开启时钟。
-
EXTI作为一个独立外设,按理说应该是需要开启时钟的,但是寄存器里面却没有EXTI时钟的控制位,这个原因手册里也没找到,网上也没有确切的答案,可能是和EXTI唤醒有关,或者是其它的一些电路设计上的考虑,暂时不需要管,只需要知道EXIT不需要开启时钟即可。
-
另外NVIC也不需要开启时钟,是因为NVIC是内核的外设,内核的外设都是不需要开启时钟的,RCC管理的都是内核外的外设,所以RCC管不着NVIC。
-
-
如果不确定哪个外设是接在哪个总线上的?可以转到这个函数的定义,看一下上面这个参数列表,有的话,就是这个总线上的外设。如下图是APB2总线上的外设。
-
补充:STM32F103C8T6的总线分别是:
-
AHB总线(Advanced High-performance Bus)
-
APB1总线(Advanced Peripheral Bus 1)
-
APB2总线(Advanced Peripheral Bus 2)
-
-
-
第二步:配置GPIO,把端口配置为输入模式
-
/*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引脚初始化为上拉输入
-
当其他外设使用GPIO的情况,如果你不清楚该配置为什么模式? 可以看一下这个参考手册,在GPIO这一章有一个外设的GPIO配置表,里面有写每个外设的各个引脚都需要配置为什么模式,如下图在图表的最后,EXTI输入线,他给的推荐配置就是浮空、上拉或者下拉。所以在这里,就给一个GPIO_Mode_IPU,上拉输入,默认为高电平的输入方式。
-
-
第三步:配置AFIO,选择使用的这一路GPIO,连接到后面的EXTI
-
/*AFIO选择中断引脚*/ GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);//将外部中断的14号线映射到GPIOB,即选择PB14为外部中断引脚
-
这个函数虽然是GPIO开头,但实际上里面是操作的AFIO的寄存器,所以这个函数实际上是AFIO的函数,其中上面这里简介写的是,选择GPIO pin作为外部中断线。第一个参数是GPIO_PortSource,选择某个GPIO外设作为外部中断源,这个参数可以是GPIO_PortSourceGPIOx,其中x可以是A到G,由于我们使用的是用的是PB14号引脚,所以把x改成B就行了。第二个参数是GPIO_PinSource,指定要配置的外部中断线,这个参数可以是GPIO_PinSourcex,其中x可以是0到15,然后x改成14,代表连接PB14号口的第14个中断线路。这样就完成了AFIO外部中断引脚选择配置就完成了。
-
当执行完这个函数后,AFIO的第14个数据选择器就拨好了,其中输入端被拨到了GPIOB外设上,对应的就是PB14号引脚,输出端固定连接的是EXTI的第14个中断线路,这样,PB14号引脚的电平信号就可以顺利通过AFIO,进入到后级EXTI电路了。
-
补充:AFIO外设,ST公司并没有给它分配专门的库函数文件,它的库函数是和GPIO在一个文件里的,还需要学习一下关于AFIO的有关库函数。
-
-
AFIO有关库函数
void GPIO_AFIODeInit(void)
-
参数
-
(void)
:这个函数没有参数。调用时不需要传入任何数据。
-
-
可能的功能
-
这个函数是用来复位AFIO外设的。调用一下这个函数,AFIO外设的配置就会全部清除。
-
void GPIO_PinLockConfig (GPIO_TypeDef* GPIOx,uint16_t GPIO_Pin)
-
参数
-
GPIO_TypeDef* GPIOx
:这是一个指向GPIO_TypeDef
结构体的指针类型参数。GPIO_TypeDef
通常用于定义 GPIO 端口(例如 GPIOA、GPIOB 等),GPIOx
表示要配置的 GPIO 端口。 -
uint16_t GPIO_Pin
:这是一个 16 位无符号整数类型参数。GPIO_Pin
用于指定要配置锁定功能的 GPIO 引脚。可以通过位操作来选择单个或多个引脚。
-
-
功能
-
这个函数用于配置 GPIO 引脚的锁定功能,调用这个函数,参数指定某个引脚,那这个引脚的配置就会被锁定,防止意外更改。在某些嵌入式系统中,为了防止意外修改 GPIO 引脚的配置,可以使用引脚锁定功能。一旦引脚被锁定,其配置寄存器(如模式、速度、上下拉等)将不能被修改,直到系统复位。通过调用
GPIO_PinLockConfig
函数,可以对指定的 GPIO 端口和引脚进行锁定配置,确保其配置的稳定性和安全性。
-
void GPIO_EventOutputConfig (uint8_t GPIO_PortSource,uint8_t GPIO_PinSource)
-
参数
-
uint8_t GPIO_PinSource
:这是一个 8 位无符号整数类型参数,用于指定 GPIO 引脚源。用于选择具体的引脚,如引脚 0、引脚 1 等。 -
uint8_t GPIO_PortSource
:这是一个 8 位无符号整数类型参数,用于指定 GPIO 端口源。例如,可能用于选择 GPIOA、GPIOB 等端口。
-
-
功能
-
这个函数用于配置 GPIO /AFIO的事件输出功能。在某些嵌入式系统中,GPIO 引脚可以用于触发外部事件或作为某些事件的输出。通过指定
GPIO_PortSource
和GPIO_PinSource
,可以选择具体的 GPIO 端口和引脚来配置事件输出功能。
-
void GPIO_EventOutputCmd (FunctionalState NewState)
-
参数:
-
FunctionalState NewState
:这个参数用于指定新的功能状态,通常用于使能或禁用事件输出功能。FunctionalState
是一个枚举类型,常见的值有ENABLE
和DISABLE
。
-
-
功能:
-
这个函数用于控制 GPIO 事件输出的使能或禁用。通过传入
FunctionalState
类型的参数(如ENABLE
或DISABLE
),可以控制之前配置的 GPIO 事件输出功能是否生效。
-
void GPIO_PinRemapConfig(
uint32_t GPIO_Remap,FunctionalState NewState)
-
参数
-
uint32_t GPIO_Remap
:这是一个 32 位无符号整数类型参数,用于指定 GPIO 重映射的配置。不同的值对应不同的引脚重映射选项,可以选择重映射的方式。 -
FunctionalState NewState
:这个参数用于指定新的功能状态,通常用于使能或禁用重映射功能。FunctionalState
可能是一个枚举类型,常见的值有ENABLE
和DISABLE
。
-
-
功能
-
这个函数用于配置 GPIO 引脚的重映射功能。在某些嵌入式系统中,为了方便 PCB 布线或实现特定功能,某些 GPIO 引脚可以通过重映射来改变其默认的功能映射关系。通过传入
GPIO_Remap
参数,可以选择具体的重映射配置,而NewState
参数则用于控制是否启用该重映射。
-
void GPIO_EXITLineConfig(
uint8_t GPIO_PortSource,uint8_t GPIO_PinSource)
-
参数
-
uint8_t GPIO_PortSource
:这是一个 8 位无符号整数类型参数,用于指定 GPIO 端口源。例如,可能用于选择 GPIOA、GPIOB 等端口。 -
uint8_t GPIO_PinSource
:这是一个 8 位无符号整数类型参数,用于指定 GPIO 引脚源。用于选择具体的引脚,如引脚 0、引脚 1 等。
-
-
功能
-
调用这个函数,就可以配置AFIO的数据选择器,来选择我们想要的中断引脚。
-
-
第四步:配置EXTI,选择边沿触发方式,比如上升沿、下降沿或者双边沿。 还有选择触发响应方式,可以选择中断响应(为主)和事件响应
-
/*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外设
-
EXTI_Init函数里面只有一个参数,就是EXTI初始化的结构体,因为EXTI只有一个,所以不需要像GPIO那样,先指定要配置的哪个EXTI了。再根据结构体参数的定义配置结构体参数即可。
-
当前的配置是,将EXTI的第14个线路配置为中断模式,下降沿触发,然后开启中断,这样PB14的电平信号就能够通过EXTI通向下一级NVIC了。
-
EXTI有关库函数
void EXTI_DeInit(void);
-
功能:这个函数用于将外部中断(EXTI)相关的设置恢复到默认的初始化状态。
-
参数:无参数(
void
)。 -
返回值:无返回值(
void
)。
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);
-
功能:用于初始化外部中断(EXTI)。它可能会根据传入的初始化结构体来配置中断的触发方式、中断线等参数。
-
参数:
-
EXTI_InitTypeDef* EXTI_InitStruct
:这是一个指向EXTI_InitTypeDef
结构体的指针,该结构体可能包含了外部中断初始化所需的参数,如中断触发方式(上升沿、下降沿等)和中断线。
-
-
返回值:无返回值(
void
)。
void EXTI_StructInit(EXTI_InitTypeDef* EXTI_InitStruct);
-
功能:用于将传入的
EXTI_InitTypeDef
结构体初始化为默认值。 -
参数:
-
EXTI_InitTypeDef* EXTI_InitStruct
:这是一个指向EXTI_InitTypeDef
结构体的指针,该结构体将被初始化为默认值。
-
-
返回值:无返回值(
void
)。
void EXTI_GenerateSWInterrupt(uint32_t EXTI_Line);
-
功能:用于软件触发指定的外部中断线。这意味着通过软件指令来模拟外部中断事件的发生。如果只需要外部引脚触发中断,就不需要这个函数。
-
参数:
-
uint32_t EXTI_Line
:这是一个 32 位无符号整数,表示要触发的外部中断线。
-
-
返回值:无返回值(
void
)。
FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line);
-
功能:用于获取指定外部中断线的标志位状态。这个标志位可能用于判断是否有中断事件发生。查看状态寄存器中的外部中断标志位是否置1。用于在主程序中查看标志位。
-
参数:
-
uint32_t EXTI_Line
:这是一个 32 位无符号整数,表示要获取标志位状态的外部中断线。
-
-
返回值:
-
FlagStatus
:这是一个枚举类型,表示标志位的状态,可能是SET
(设置)或RESET
(重置)。
-
void EXTI_ClearFlag(uint32_t EXTI_Line);
-
功能:用于清除指定外部中断线的标志位。在处理完中断事件后,通常需要清除标志位,以便下一次中断事件能够被正确检测。对状态寄存器中置1的外部中断标志位进行清除。用于在主程序中清除标志位。
-
参数:
-
uint32_t EXTI_Line
:这是一个 32 位无符号整数,表示要清除标志位的外部中断线。
-
-
返回值:无返回值(
void
)。
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);
-
功能:用于获取指定外部中断线的中断状态。这个函数可能用于判断是否有中断请求正在等待处理。用于在中断函数中查看标志位。
-
参数:
-
uint32_t EXTI_Line
:这是一个 32 位无符号整数,表示要获取中断状态的外部中断线。
-
-
返回值:
-
ITStatus
:这是一个枚举类型,表示中断状态,可能是SET
(设置)或RESET
(重置)。
-
void EXTI_ClearITPendingBit(uint32_t EXTI_Line);
-
功能:用于清除指定外部中断线的中断挂起位。在处理完中断请求后,通常需要清除挂起位,以便下一次中断请求能够被正确检测。用于在中断函数中清除标志位。
-
参数:
-
uint32_t EXTI_Line
:这是一个 32 位无符号整数,表示要清除中断挂起位的外部中断线。
-
-
返回值:无返回值(
void
)。
-
第五步:配置NVIC,给我们这个中断选择一个合适的优先级
-
/*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外设
-
对于分组方式整个芯片只能用一种,所以按理说这个分组的代码整个工程只需要执行一次就行了,如果把它放在模块里面进行分组,那你要确保每个模块分组都选的是同一个,也可以把这个代码放在主函数的最开始,这样模块里就不用再进行分组了。
-
第一个IRQChannel参数,跳转定义,表示指定中断通道来开启或关闭,这个参数可以是IROnType里面的一个值,下面还有个括号,写的是对于完整的STM32中断通道列表,请参考stm32f10x.h文件,这个意思是这个IROnType的定义不在这个文件,要到stm32f10xh里面去找,所以那还是选择IROnType,Ctrl+F搜索一下,如果直接在这里搜索,是搜不到的,需要把下面的搜索范围由当前文件换成当前工程,然后在搜索,可以看到,是跳到了stm32f10x.h文件里,在上面那个列表里面进行选择参数。可以看到列表里有非常多的中断通道,因为这个库函数可以兼容所有的F1系列芯片,但是不同的芯片中断通道列表是不一样的,所以这里有很多条件编译,用来选择需要使用芯片的中断通道列表,可以点一下这左边的减号,把所有的条件编译都折叠起来,然后本节芯片是MD中等密度的,所以只需要展开这个MD的条件编译即可,其他的不用看了。在这个表里,我们就可以找到这个EXTI15_10_IRQn,STM32的EXTI10到EXTI15都是合并到了这个通道里,把这个参数方法进结构体变量即可。这样通道就指定好了。
-
后面的参数配置按照定义中的规定根据需求配置即可,其中指定所选通道的抢占优先级和响应优先级的参数,这两个参数的值是可以是0到15,具体的值可以参考这个表里的描述,由于前面选择了分组2,那抢占优先级和响应优先级的取值范围就都是0~3,因为本节这个程序的中断只有一个,所以中断优先级的配置也是是非常随意的,所以这里就给抢占优先级和响应优先级都设置为1,优先级是在多个中断源同时申请,产生拥挤时才有作用,由于这只有一个中断,优先级就随便了。
-
NVIC有关库函数
因为NVIC是内核外设,所以它的库函数是被ST发配到杂项这里来了,打开misc.h文件,拖到最后,这里有NVIC的四个函数和Systick的一个函数。
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
-
功能:这个函数用于配置 NVIC 的优先级分组。在 ARM Cortex - M 系列微控制器中,中断优先级由抢占优先级和子优先级组成,这个函数用于确定这两部分在优先级寄存器中的分配方式。
-
参数:
-
uint32_t NVIC_PriorityGroup
:这是一个 32 位无符号整数,表示优先级分组的配置值。不同的值对应不同的抢占优先级和子优先级的位数分配。 -
配置优先级分组:先占优先级和从占优先级,这里先占优先级就是抢占优先级,从占优先级就是响应优先级。具体参数如下图:
-
-
返回值:无返回值(
void
)。
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);
-
功能:用于初始化 NVIC 的中断设置。这个函数根据传入的结构体参数配置特定中断的优先级、使能状态等。
-
参数:
-
NVIC_InitTypeDef* NVIC_InitStruct
:这是一个指向NVIC_InitTypeDef
结构体的指针,该结构体包含了中断初始化所需的参数,如中断通道、优先级、使能状态等。
-
-
返回值:无返回值(
void
)。
void NVIC_SetVectorTable(uint32_t NVIC_VectTab, uint32_t Offset);
-
功能:用于设置向量表的位置和偏移量。在某些情况下,可能需要将向量表重定位到不同的内存区域。
-
参数:
-
uint32_t NVIC_VectTab
:这是一个 32 位无符号整数,表示向量表的基地址。 -
uint32_t Offset
:这是一个 32 位无符号整数,表示向量表的偏移量。
-
-
返回值:无返回值(
void
)。
void NVIC_SystemLPConfig(uint8_t LowPowerMode, FunctionalState NewState);
-
功能:用于配置系统的低功耗模式。这在嵌入式系统中用于优化功耗。
-
参数:
-
uint8_t LowPowerMode
:这是一个 8 位无符号整数,表示低功耗模式的类型。 -
FunctionalState NewState
:这是一个枚举类型,表示是否启用低功耗模式(ENABLE
或DISABLE
)。
-
-
返回值:无返回值(
void
)。
void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource);
-
功能:用于配置 SysTick 定时器的时钟源。SysTick 定时器是一个基本的定时设备,常用于操作系统的定时任务。
-
参数:
-
uint32_t SysTick_CLKSource
:这是一个 32 位无符号整数,表示 SysTick 定时器的时钟源选择(例如,选择内部时钟或外部时钟)。
-
-
返回值:无返回值(
void
)。
-
最后通过NVIC,外部中断信号就能进入CPU了,这样CPU才能收到中断信号,才能跳转到中断函数里执行中断程序。
-
模块中的中断函数(void EXTI15_10_IRQHandler(void))
-
通过对初始化函数的编写外部中断的信号从GPIO到AFIO,再到EXTI,再到NVIC,最终通向CPU,这样才能让CPU由主程序跳转到中断程序执行,这里的中断函数便是中断程序需要执行的函数。其中在STM32中,中断函数的名字都是固定的,每个中断通道都对应一个中断函数,中断函数的名字可以参考一下启动文件(startup_stm32f10x_md.s),这里面以IRQHandler结尾的字符串就是中断函数的名字,其中EXTI15_10_IRQHandler这一项就是EXTI15_10的中断函数,复制到传感器模块中编写。
-
函数的编写
-
注意:中断函数都是无参数无返回值的。
-
第一步:中断标志位的判断
-
目的是确保是程序想要的中断源触发的这个函数,因为这个函数EXTI10到EXTI15都能进来,所以要先判断一下是不是程序想要的EXTI14进来的,这时就需要到exti.h里看一下,利用库函数EXTI_GetlTStatus,检查一下参数。
-
-
第二步:中断程序的编写
-
第三步:清除中断标志位
-
因为只有中断标志位置1了,程序就会跳转到中断函数,如果不清除中断标志位,那它就会一直申请中断,这样程序就会不断响应中断,执行中断函数,那程字就卡死在中断函数里了,所以每次中断程序结束后,都应该清除一下中断标志位,利用库函数EXTI_ClearITPendingBit清除中断标志位。
-
-
补充:中断函数不需要在主程序中调用,他是自动执行的,所以也就不需要再头文件中声明。
-
/** * 函 数: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号线的中断标志位 //中断标志位必须清除 //否则中断将连续不断地触发,导致主程序卡死 } }
-
-
-
模块中的计数函数(uint16_t CountSensor_Get(void))
-
/** * 函 数:获取计数传感器的计数值 * 参 数:无 * 返 回 值:计数值,范围:0~65535 */ uint16_t CountSensor_Get(void) { return CountSensor_Count; }
-
(2)主函数的编写
1. 头文件包含
-
#include "stm32f10x.h"
:引入 STM32F10x 系列微控制器的标准外设库头文件,提供了对微控制器硬件资源(如端口、中断、定时器等)的访问接口。 -
#include "Delay.h"
:自定义的延时函数头文件,用于在程序中实现各种延时操作,比如在传感器数据读取或 OLED 显示更新时可能需要适当延时。 -
#include "OLED.h"
:OLED 显示屏驱动相关的头文件,包含了用于初始化 OLED、在 OLED 上显示字符串和数字等操作的函数声明。 -
#include "CountSensor.h"
:计数传感器(对射式红外传感器或旋转编码器)相关的头文件,应该包含了传感器初始化、获取计数值等操作的函数声明。
2. main
函数
-
模块初始化:
-
OLED_Init();
:调用 OLED 初始化函数,对 OLED 显示屏进行初始化设置,包括配置相关引脚、通信协议等,以便后续能够正确地在 OLED 上显示信息。 -
CountSensor_Init();
:调用计数传感器初始化函数,初始化对射式红外传感器或旋转编码器,可能涉及配置传感器的引脚模式、中断(如果有)、定时器(如果用于计数)等,使传感器能够正常工作并开始计数。
-
-
显示静态字符串:
-
OLED_ShowString(1, 1, "Count:");
:在 OLED 显示屏的第 1 行第 1 列显示字符串 "Count:",用于提示后续显示的数字是计数值。
-
-
主循环:
-
while (1)
:这是一个无限循环,确保程序持续运行。 -
OLED_ShowNum(1, 7, CountSensor_Get(), 5);
:在 OLED 显示屏的第 1 行第 7 列开始显示计数值,计数值通过CountSensor_Get()
函数获取,并且显示宽度为 5 位(如果计数值不足 5 位,可能会在前面补 0 显示)。这个函数会不断被调用,实现 OLED 上计数值的实时更新,即只要计数值发生变化,就会在 OLED 上显示新的值。 -
然后挡一下传感器,就加1,再挡一下,就再加1,由于程序中用的是下降沿触发,在移开挡光片的时候触发中断,如果修改参数为上升沿触发,就是这档的时候数字+1,如果更改为上升沿下降沿都触发,就是遮挡和移开的时候都加1。
-
六、旋转编码器计次的代码详解
1. 硬件电路
右侧的OLED用于显示程序的现象,左边接了一个旋转编码器的模块,上面VCC和GND接正负极,下面这两个A,B相的输出引脚,分别接到STM32的PB0和PB1两个引脚,这些连接通常用于读取旋转编码器的状态,以便在程序中处理旋转事件。
2. 代码的编写
(1)旋转编码器模块的封装
-
在HardWare文件下,建立Encoder的.c和.h文件。
-
模块初始化函数
知识补充:这里,只初始化一个外部中断其实也是可以完成功能的,因为对于这个编码器而言,正向旋转时,A,B相输出的是上面的波形,反向旋转时,输出的是下面的波形,如果把一相的下降沿用作触发中断,在中断时刻读取另一相的电平,正转就是高电平,反转就是低电平,这样就能区分旋转方向了。
-
但是这样有一些小瑕疵,比如正转的时候,由于A相先出现下降沿,所以刚开始动,就进中断了,而反转是A相后出现下降沿,所以就是已经转到位了,才进入中断,这样实际上也没有太大的影响,但是并不完美。所以本节程序准备的就是,A,B相都触发中断,只有在B相下降沿和A相低电平时,才判断为正转,在A相下降沿和B相低电平时,才判断为反转,这样就能保证正转反转都是转到位了,才执行数字加减的操作。
代码修改:
首先初始化时钟,GPIOB和AFIO,这些不用改的。
接着初始化GPIO,程序中用的是PBO和PB1,所以改一下,变成GPIO_Pin_0或上GPIO_Pin_1。
然后是AFIO中断引脚选择,把这个这个14改成0,将第0个线路拨到GPIOB上,然后复制粘贴一下这个代码,再将第1个线路拨到GPIOB上,这样AFIO就完成了。
然后是EXTI部分,把这个指定的中断线改成EXTILine0或上EXTI_Line1,这样就能同时把第0条线路和第1条线路都初始化为中断模式、下降沿触发。
接着中断分组,不用更改。
接着下面的中断优先级,这个要对两个通道分别设置优先级,复制一下下面结构体参数的这部分代码,定义结构体变量的就不用复制了,这个变量可以重复使用,上面的改成EXTI0_IROn,优先级抢占和响应都还是1;下面的改成EXTI1_IROn,响应优先级改低一些,设置为2,这样同时初始化EXTI0和EXTI1的代码就gdua
/**
* 函 数:旋转编码器初始化
* 参 数:无
* 返 回 值:无
*/
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外设
}
-
中断程序函数
-
打开启动文件,找到这两个中断的中断函数,这里EXTIO和EXTI1是分别独占一个函数的。
-
这里这两个中断是进的两个函数,如果使用的是9_5和15_10的这些中断,那只能写一个中断函数,这样就只需要把两个中断函数中的两个if(用于检查和清除标志位的语句)并列地放在一个函数里就行了。
-
再根据上一个工程的中断函数的编写方式,编写这两个通道的中断函数。
-
因为这里需要正反转,所以就定义一个带符号的变量Encoder_Count。然后EXTI0里,先判断一下另一个引脚的电平(if(GPIO ReadlnputDataBit(GPIOPin1) == 0),如果是,那就是反转,就Encoder_Count--,(这里的正反转也可以自己定义);然后在EXTI1里,if(GPIO ReadlnputDataBit(GPIOB, GPIO Pin_0) == 0),如果是,那就是正转,就Encoder_Count++。这样中断对这个变量的加减就完成了。
-
-
-
/** * 函 数: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号线的中断标志位 //中断标志位必须清除 //否则中断将连续不断地触发,导致主程序卡死 } }
返回旋转编码器获取增量值的函数
-
在这里面就不直接返回Encoder_Count这个变量了,本节程序打算返回每次调用这个Get函数之后,返回Count的变化值,用于外部加减一个变量,所以在这里需要返回Count,然后把Count清0,那因为返回Count之后,函数就结束了,没法清0了,所以先定义一个临时变量Temp,把Count先赋值给Temp,再把Count清0,最后返回Temp。
-
/** * 函 数:旋转编码器获取增量值 * 参 数:无 * 返 回 值:自上此调用此函数后,旋转编码器的增量值 */ int16_t Encoder_Get(void) { /*使用Temp变量作为中继,目的是返回Encoder_Count后将其清零*/ /*在这里,也可以直接返回Encoder_Count 但这样就不是获取增量值的操作方法了 也可以实现功能,只是思路不一样*/ int16_t Temp; Temp = Encoder_Count; Encoder_Count = 0; return Temp; }
(2)主程序的编写
1. 头文件包含
-
#include "stm32f10x.h"
:引入 STM32F10x 系列微控制器的标准外设库头文件,提供了对微控制器硬件资源(如端口、中断、定时器等)的访问接口,是操作 STM32 相关硬件的基础。 -
#include "Delay.h"
:自定义的延时函数头文件,在程序中可能用于实现一些简单的延时操作,比如在 OLED 显示更新或读取旋转编码器数据时,适当的延时可以确保数据稳定或操作有序。 -
#include "OLED.h"
:OLED 显示屏驱动相关的头文件,其中包含了用于初始化 OLED、在 OLED 上显示字符串和有符号数字等操作的函数声明,使得能够方便地控制 OLED 显示屏进行信息展示。 -
#include "Encoder.h"
:旋转编码器相关的头文件,应该包含了对旋转编码器进行初始化以及获取其增量值等操作的函数声明,是实现旋转编码器功能的关键。
2. 全局变量定义
-
int16_t Num;
:定义了一个 16 位有符号整数变量Num
,它将用于存储旋转编码器调节后的数值,并且这个数值会在 OLED 屏幕上显示。 -
这里的变量Num,需要调用Encoder模块对它进行加减。
3. main
函数
-
模块初始化:
-
OLED_Init();
:调用 OLED 初始化函数,对 OLED 显示屏进行初始化设置,包括配置相关引脚为合适的模式(如输出模式用于控制显示数据传输)、设置通信协议(如 I2C 或 SPI)等,确保 OLED 能够正常接收和显示数据。 -
Encoder_Init();
:调用旋转编码器初始化函数,初始化旋转编码器相关硬件设置,可能涉及配置编码器连接的引脚为输入模式(用于读取编码器的脉冲信号)、设置中断(如果编码器使用中断方式检测旋转)、初始化定时器(如果需要定时器辅助计数或定时读取编码器数据)等,使旋转编码器能够正常工作并准备好检测旋转操作。
-
-
显示静态字符串:
-
OLED_ShowString(1, 1, "Num:");
:在 OLED 显示屏的第 1 行第 1 列显示字符串 "Num:",用于提示后续显示的数字是由旋转编码器调节的变量Num
的值。
-
-
主循环:
-
while (1)
:这是一个无限循环,保证程序持续运行,不断检测和处理旋转编码器的操作以及更新 OLED 显示。 -
Num += Encoder_Get();
:通过调用Encoder_Get()
函数获取自上一次调用该函数后旋转编码器的增量值(例如,编码器顺时针旋转可能产生正值增量,逆时针旋转可能产生负值增量),然后将这个增量值累加到变量Num
上,实现对Num
的动态调节。因为这个Get函数返回的是调用这个函数的间隔里,旋转编码器产生的正负脉冲数,所以这个返回值直接+=给Num,就能对Num进行加减操作了。 -
OLED_ShowSignedNum(1, 5, Num, 5);
:在 OLED 显示屏的第 1 行第 5 列开始显示有符号整数Num
的值,显示宽度为 5 位(如果Num
的绝对值小于 5 位数字,可能会在前面补 0 或空格显示;如果是负数,会显示负号)。这个函数会不断被调用,实时更新 OLED 上显示的Num
值,以反映旋转编码器的调节结果。
-
七、中断编程的建议
-
在这个中断函数里,最好不要执行耗时过长的代码,中断函数要简短快速,别刚进中断就执行一个Delay多少毫秒这样的代码,因为中断是处理突发的事情,如果你为了一个突发的事情待着中断里不出来了,那主程序就会受到严重的阻塞。
-
最好不要在中断函数和主函数调用相同的函数或者操作同一个硬件,尤其是硬件相关的函数,比如OLED显示函数,如果既在主程序里调用OLED,又在中断里调用OLED,OLED就会显示错误。在主程序中,OLED刚显示一半,进中断了,结果中断里还是OLED显示函数,那OLED就挪到其它地方显示了,这时还没有问题,但当中断结束之后,需要继续原来的显示,这时就出问题了,因为硬件的显示位置被挪到其他地方了,所以再回来的时候,继续显示的内容就会跟着跑到其它地方去,这就会造成问题,虽然在中断进入和退出的时候,会有保护现场和恢复现场,但这只能保证CPU程序能正常返回不出问题,对于外部硬件的话,并没有在进入中断时,进行现场保护,所以中断返回后,就出问题了,那为了避免这样的问题,就最好不要在主程序和中断程序里,操作可能产生冲突的硬件。在实现功能的时候,可以在中断里操作变量或者标志位,当中断返回时,再对这个变量进行显示和操作,这样既能保证中断函数的简短快速,又能保证不产生冲突的硬件操作。