这一节我们来学习 STM32 的 PWR 电源控制。
其中,我们重点学习的主要就是 3 种低功耗模式:睡眠模式、停机模式和待机模式。
低功耗模式的目的呢?简单明了,就是省电,这对于一些使用电池供电,又需要长时间待机的设备,十分重要。
所以我们本节课的任务,就是学习电源控制部分,看看如何配置这些低功耗模式,让 STM32 在空闲时,能够尽可能地节省电量。
好,那先看一下本节最终的程序现象。本节总共有 4 个程序,分别是 13-1 修改主频;13-2 睡眠模式+串口发送+接收;13-3 停止模式+对射式红外传感器计次;13-4 待机模式+实时时钟。因为低功耗模式的代码都不是很多,所以多给大家演示几个。
那我们先看一下第一个程序,修改主频。修改主频不属于 3 种低功耗模式,但是也是降低 STM32 功耗的一种方法,这个代码非常简单,就是初始化显示一下 SystemCoreClock
这个变量,这个变量指示了当前主频频率,之后,主循环,以 1s 为周期,显示 Running,再清除。那我们看一下程序现象,目前可以看到,第一行 SCKCLK,系统主频,是 36 MHz;第二行,我们让它以 1s 为周期显示,但现在实际上是 2s 的周期,这是因为系统主频,正常情况下是 72 MHz,现在我们降频到 36 MHz 了,所以运行时间,就是原来的 2 倍。那这就是修改主频这个程序的现象,有关这个 system 文件的详细解释,还有配置主频的更多玩法,我们写程序的时候,再来介绍。
那在这个程序的哪个部分,我修改了主频呢?答案是在 system_stm32f10x.c 文件里,system 这个文件,里面就是用来配置时钟的,在 110 行这个位置,它给我们预留好了配置时钟的宏定义,我们只需要将其中一个宏定义解除注释,就能直接配置系统的主频,非常简单。目前看到,我是把 36 M 的宏解除注释了,所以目前主频就是 36 M,这个文件在我改之前默认是 72 M,如果我们把 72 M 解除注释,把 36 M 注释掉,这样系统主频就是 72 M 了,想配置为其他的频率,也是同理,先把原来的注释掉,再解除对应频率的注释即可,是不是非常简单。
我们编译下载看一下,可以看到,目前显示主频是 72 M,Running 闪烁的周期是 1s,这是默认主频 72M 的正常现象。
如果文件图标带有钥匙标志说明此文件为只读,必须先解除只读才能修改程序,解除只读的方法为:文件夹里找到文件 -> 右键 -> 属性 ->取消只读。
接着继续看第二个程序,睡眠模式 + 串口的发送和接收,那这个程序就是从串口那一节直接复制过来的,这个代码的功能就是,当收到一个字节时,中断触发,置标志位,主循环查询到标志位时,读取数据,并用串口发送出去,在这个功能后面,我又新加了一段代码,就是用来配置睡眠模式的,关键代码实际上就一句 __WFI();
执行完这一句,芯片进入睡眠,睡眠的目的是如果 STM32 一直没收到数据,那这个主循环也会一直查询标志位,这是无意义的耗电操作,那不如我们就让它睡眠,收到数据后,自动退出睡眠模式,执行一遍任务后,继续睡眠,这样在空闲时,芯片一直在睡眠,可以降低系统功耗。那我们下载看一下程序现象,目前是串口发送加接收,我们按照串口那一节的接线接好串口,然后打开串口,随便发送一个数据,可以看到,STM32 成功接收,并回传了这个数据,这个现象和没使用睡眠模式的是一样的,但是,细节就在于,看一下第二行,只有在我们发送数据时刻,OLED 才会显示一次 Running,在空闲时,芯片一直都在睡眠,这样就是在不影响程序功能的前提下,使用睡眠模式节约电量,这就是这个程序的功能。
另外,还要重点提醒一下,芯片在 3 种低功耗模式下,是没法直接再下载程序的,你看现在是下载成功的,如果直接再点下载,就会提示报错,这是因为芯片现在在睡眠,不会理你调试端口了,解决方法也很简单,需要我们有一些操作:
- 第一步,我们按住复位键不放;
- 第二步,点下载按钮;
- 第三步,及时松开复位键。这样就能下载成功了。
在我们本节 3 种低功耗模式下,都需要这样下载程序,大家注意一下。另外,如果你不小心禁用了调试端口,其实也可以这样来解决。
接着继续看,第三个程序,停止模式 + 对射红外计次,这个程序是在之前外部中断那一节的代码基础上修改的,类似的操作,主循环中加入了 Running 的指示和最后进入停止模式的代码。那下载看一下程序现象,每遮挡一次,执行一次计次,也显示一下 Running,在没有外部中断信号时,STM32 处于停止模式,可以省电,这就是第三个程序的现象。
最后我们来看一下第四个程序,待机模式 + 实时时钟,这个程序是在实时时钟的基础上,加入了待机模式,目前这个程序,使用的是 LSE 外部低速时钟,如果你没有 RTC 晶振,或者 RTC 晶振不起振,也可以使用 LSI 内部低速时钟,LSI 在待机模式下,可以继续工作。然后在主循环中可以加入唤醒后要执行的功能,在进入待机模式之前,可以关闭各个外部连接的模块,以最大化省电,我目前是用 OLED_Clear();
模拟了一下,那这个程序,会用实时时钟设定闹钟,每隔一段时间,会自动唤醒一次,这里我演示的是,每隔 10s 唤醒一次,唤醒之后,执行一遍程序任务,随后继续待机。那看一下程序现象,复位一下,可以看到 OLED 上显示了当前时间和闹钟,随后进入待机,然后等一会闹钟触发之后,自动唤醒一次,设定新的闹钟,执行程序功能,之后继续待机,等待下一次唤醒,这就是使用 RTC 和闹钟配合待机模式的自动唤醒程序。非常适合那种,需要每隔一段时间操作一次,空闲时间又需要最大化省电的设备。
好,那程序现象,我们就看到这里。接下来看一下 PWR 的理论部分。
1. PWR 理论部分
1.1 PWR 简介
PWR(Power Control,电源控制)
PWR 就是 Power 的缩写。
PWR 的作用:PWR 负责管理 STM32 内部的电源供电部分,可以实现 可编程电压监测器 和 低功耗模式 的功能。
PWR 有一部分是硬件的介绍,就是告诉你,内部供电电路的结构是啥样的,这些是设计硬件电路时要考虑的,暂时不涉及程序。涉及程序的功能,主要就是两个,一个是可编程电压监测器,另一个是低功耗模式。
可编程电压监测器(PVD)可以监控VDD电源电压,当 VDD 下降到 PVD 阀值以下或上升到 PVD 阀值之上时,PVD 会触发中断,用于执行紧急关闭任务。
这个功能预想的场景应该是,使用电池供电,或者对安全要求比较高的设备,如果供电电压在逐渐下降,在电压过低的情况下,可能会导致内部或者外部电路发送不确定的错误,为了避免不确定的因素,在电源电压低于设定的阈值时,我们可以主动出击,提前发出警告,并且关闭比较危险的设备,这是 PVD 的设计。不过 PVD 这个功能不是我们本节课的重点,我们暂时也不演示代码,大家了解即可,后续需要的话可以再研究。
低功耗模式* 包括睡眠模式(Sleep)、停机模式(Stop)和待机模式(Standby),作用就是:可在系统空闲时,降低STM32的功耗,延长设备使用时间。尤其是像一些用电池供电的设备,对空闲时候的耗电量是有极大要求的,比如数据采集设备,车钥匙,遥控器,报警器等等。
这些产品都有特点,就是在它们的生命周期里,绝大部分时间,都是空闲状态。但是我们知道,单片机程序一旦开始,正常运行的状态下,程序永远都不会停下来,所以主程序的最后,一般都是个死循环,即使需要空闲,让程序停下来,那也得来个空循环让程序一直转圈卡住。但是,程序运行就会耗电,空循环的耗电量也是很大的,比如遥控器,如果不用它的时候,程序一直空循环,那么用不到几天,电池就没电了,这显然是不行的。
所以说对于这些设备,我们需要这样的低功耗模式,在空闲状态时,关闭不必要的硬件,比如我直接把 CPU 断电,或者关闭时钟,这样程序自然就不会运行了。但是在低功耗模式下,我们也需要保留必要的唤醒电路,比如串口接收数据的中断唤醒、外部中断唤醒、RTC 闹钟唤醒,等等,在需要设备工作时,STM32 能够立刻重新投入工作,这样才行,如果你只考虑进入低功耗而不考虑唤醒,STM32 一睡不醒,那不就跟直接断电没区别了嘛。所以,低功耗模式,我们要考虑关闭哪些硬件,保留哪些硬件,以及如何去唤醒,当然关闭越多的硬件,设备越省电,唤醒就越麻烦,这也是这 3 种低功耗模式在设计时,所区分的地方。
这是本节的重点内容。英文大概记一下,前面两个应该都很熟悉,主要记一下 Standby,表示待机模式,这个在很多其他芯片里,都有出现。
那简介就看到这里,接着我们看一下电源框图。
1.2 电源框图
这个图就是 STM32 内部的供电方案。其中有些部分我们之前也已经了解过,现在再来看看。整体上看,这个图可以分为 3 个部分。
-
最上面是模拟供电,叫做 VDDA(VDD Analog)。VDDA 供电区域,主要负责模拟部分的供电,其中包括 AD 转换器、温度传感器、复位模块、PLL 锁相环,这些电路的供电正极是 VDDA,负极是 VSSA。其中 AD 转换器还有两根参考电压的供电脚,叫做 VREF+ 和 VREF-,这两个脚在引脚多的型号里会单独引出来。在引脚少的型号,比如我们这个 C8T6,VREF+ 和 VREF- 在内部就已经分别接到了 VDDA 和 VSSA 了。
-
中间是数字部分供电,包括两块区域,VDD 供电区域和 1.8V 供电区域。左边部分是 VDD 供电区域,其中包括 IO 电路、待机电路、唤醒逻辑和独立看门狗;右边部分是 VDD 通过电压调节器,降压到 1.8V,提供给后面这一块的 1.8V 供电区域,1.8V 区域包括 CPU 核心、存储器和内置数字外设。可以看出,STM32 内部的大部分关键电路,CPU、存储器和外设,其实都是以 1.8V 的低电压运行的,当这些外设需要与外界进行交流时,才会通过 IO 电路转换到 3.3V;所以我们从外部看好像 STM32 内部全是 3.3V,但实际上它内部的 CPU、外设等,都是以 1.8V 供电运行,使用低电压运行的主要目的是降低功耗,电压越低,内部电路运行的功耗就相对越低。
当然我们其实也不用了解这么多,主要是记住,CPU 核心、存储器和数字外设,都属于 1.8V 供电区域;而待机电路,唤醒逻辑等,属于 VDD 供电区域,它们的位置要清楚。然后电压调节器它的作用是给 1.8V 区域供电,因为后面我们会提到这个 1.8V 区域和电压调节器,你要知道是啥。 -
下面是后备供电,叫做 VBAT(V Battery)。VBAT 后备供电区域其中包括 LSE 32K 晶体振荡器、后备寄存器、RCC BDCR 寄存器和 RTC。RTC BDCR 是 RCC 的寄存器,叫备份域控制寄存器,也是和后备区域有关的寄存器,所以也可以由 VBAT 供电。然后上面有个低电压检测器,可以控制开关,VDD 有电时,由 VDD 供电;VDD 没电时,由 VBAT 供电。
好,那这些就是电源框图的介绍了。大家需要了解的是,STM32 内部有哪几部分供电区域,以及每部分供电区域里都有啥,这就是这个框图的内容。
然后继续看下两个图,分别是上电复位和掉电复位还有可编程电压监测器。这两个内容了解即可,快速介绍一下。
1.3 上电复位和掉电复位
首先是上电复位和掉电复位。这个的意思是,当 VDD 或者 VDDA 电压过低时,内部电路直接产生复位,让 STM32 复位住,不要乱操作,这个复位和不复位的界限之间,设置了一个 40 mV 迟滞电压,大于上限 POR(Power On Reset)时解除复位,小于下限 PDR(Power Down Reset)时复位。这是一个典型的迟滞比较器,设置两个阈值的作用,就是防止电压在某个阈值附近波动时,造成输出也来回抖动。下面的复位信号 Reset,是低电平有效的,所以在前面和后面,电压过低时,是复位的,中间电压正常的时候,不复位。
那这个电压上限和下限,具体是多少 V 呢,还有这里解除复位,还有个滞后时间,是多久呢,这些参数,可以看一下 STM32 数据手册,在 5.3.3 内嵌复位和电源控制模块特性里有个表,这里写了上电或掉电复位阈值,下降沿,也就是 PDR 掉电复位的阈值下限,典型值是 1.88V;上升沿,也就是 PDR 上电复位的阈值上限,典型值是 1.92V;1.92 - 1.88 V 就是迟滞的阈值,40 mV,所以如果忽略迟滞的话,简单来说,就是大于 1.9V 上电,低于 1.9V 掉电。然后最后一行,就是 TRSTTEMPO 复位持续时间,典型值是 2.5 ms,这就是这个上电复位和掉电复位。知道一下就行,也不需要我们操作啥的。
1.4 可编程电压监测器
然后下面这个是可编程电压监测器,简称 PVD。它的工作流程和上面那个上电复位和掉电复位差不多,都是监测 VDD 和 VDDA 的供电电压。但是 PVD 的区别就是,首先它这个阈值电压是可以使用程序指定的,可以自定义调节,调节的范围可以看一下数据手册,在内嵌复位和电源控制模块特性的表的上面就是 PVD 的阈值,配置 PLS 寄存器的 3 个位,可以选择右边这么多的阈值,因为这里也同样是迟滞比较,所以有两个阈值,可选范围是 2.2V 到 2.9V 左右,PVD 上限和下限之间的迟滞电压是 100 mV。可以看到,PVD 的电压是比上电掉电复位的电压要高的。
画个图就是 3.3V 是正常的供电,当这个电压降低在 2.9V 到 2.2V 之间,属于 PVD 监测的范围,可以通过 PVD 设置一个警告线,之后再降低到 1.9V,就是复位电路的监测范围,低于 1.9V,直接复位住,不让动了,这是这两个电压监测的工作任务。那当然,PVD 触发之后,芯片还是能正常工作的,只不过是电源电压过低,该提醒一下用户了。
所以看一下下面这个 PVD 输出,这个是正逻辑,电压过低时为 1,电压正常时为 0,这个信号,可以去申请中断,在上升沿或者下降沿时,触发中断,以此提醒程序进行适当的处理。另外,这个 PVD 的中断申请是通过外部中断实现的,我们可以看一下外部中断这一节的 EXTI 基本结构图,可以看到,PVD 输出的信号是跑到这里来了,所以如果要使用 PVD 的话,记得要配置外部中断。然后下面这里还有 RTC,这个是 RTC 的闹钟信号,也有借道外部中断,其实 RTC 自己是有中断的,那为啥还要借道外部中断呢?因为低功耗模式设计的是,只有外部中断可以唤醒停止模式,其他这些设备,也想唤醒停止模式的话就可以通过借道外部中断来实现。其中后面这两个 USB 和 ETH,也都只有它们的 WeakUp,唤醒信号接过来了,目的,也是为了唤醒停止模式,这个了解一下。
好,这两个电压监测的内容,就介绍这么多。
接着,我们来看本节的重点,低功耗模式。
1.5 低功耗模式
这个表,是低功耗模式一览,其中对 3 种模式进行了详细的对比。
我们看一下,第一列就是有哪几种模式;第二列是如何配置,才能进入我们想要的模式;第三列是对于这些模式,进入之后,如何去唤醒,也就是模式的退出,毕竟我们肯定也不想让它一睡不醒,该干活的时候,还是要醒来干活的;最后三列,是每种模式对电路的操作,关闭了哪些东西,就是哪些电路不能用了,保留了哪些东西,就是哪些电路还是正常工作的,这些我们要清楚,那依次来看一下。
首先,低功耗模式有 3 种:睡眠、停机和待机。这 3 种模式,从上到下,关闭的电路越来越多,对应的,从上到下,是越来越省电,同时,从上到下,也是越来越难唤醒的,这符合我们的常识,睡得越深,关的越多,越省电,越难叫醒。
- 首先看一下睡眠模式,这是浅睡眠,相当于打了个盹。如何进入呢?这里写了,直接调用 WFI 或者 WFE,即可进入,这两个东西是内核的指令,对应库函数里,也有对应的函数,直接调用函数即可。其中 WFI 的意思是 Wait For Interrupt,等待中断,意思就是,我先睡了,如果有中断发生的话,再叫我起来,所以对应的唤醒条件是任一中断,调用 WFI 进入的睡眠模式,任何外设发生任何中断时,芯片就会立刻醒来,因为中断发生了,所以醒来之后的第一件事,一般就是处理中断函数;然后下面 WFE,意思是 Wait For Event,等待事件,对应的唤醒条件是,唤醒事件,这个事件可以是外部中断配置为事件模式,也可以是使能了中断,但是没有配置 NVIC,调用 WFE 进入的睡眠模式,产生唤醒事件时,会立刻醒来,醒来之后,一般不需要进中断函数,直接从睡的地方继续运行,这就是 WFI 和 WFE 的作用。相同点是,调用任意一个之后,芯片都进入睡眠,不同点是,WFI 进入的得用中断唤醒,WFE 进入的得用事件唤醒。
之后看一下睡眠模式对电路的影响,对 1.8V 区域时钟的影响是,CPU 时钟关,对其他时钟和 ADC 时钟无影响;对 VDD 区域时钟的影响是,无;对电压调节器的操作是,开。所以睡眠模式对电路的影响就是只把 CPU 时钟关了,对其他电路没有任何操作。CPU 时钟关了,程序就会暂停,不会继续运行了,CPU 不运行,芯片功耗就会降低。
好,睡眠模式,我们就清楚了,它关的东西很少,就只是把 CPU 时钟关了。程序暂停运行,寄存器的数据都还在。它的唤醒条件也是比较宽松,任何的风吹草动,CPU 都会醒来,开始干活。所以睡眠模式,相当于大脑打了个盹,身体还在工作,在省电程度上,评级为“一般省电”。
另外这里还可以看出,关闭电路通常有两个做法:一个是关闭时钟,另一个是关闭电源。关闭时钟,所有的运算和涉及时序的操作都会暂停,但是寄存器和存储器里面保存的数据还可以维持,不会消失;关闭电源,就是电路直接断电,电路的操作和数据都会直接丢失。所以,关闭电源,比关闭时钟更省电,这个表里的第 4、5 这两列,就是对 1.8V 区域和 VDD 区域的时钟控制;然后这个电压调节器,刚才看了,它实际上就是 1.8V 区域的电源,如果电压调节器关,就代表直接把 1.8V 区域断电,这个了解一下。
-
然后继续看第二个,停机模式。如何进入停机模式呢?首先 SLEEPDEEP 为设置为 1,告诉 CPU,你可以放心的睡,进入深度睡眠模式;另外,PDDS 这一位,用来区分它是停机模式还是下面的待机模式,PDDS = 0,进入停机模式,PDDS = 1,进入待机模式,所以想要进入停机模式,PDDS 要事先设置为 0;之后,LPDS 用来设置最后这个电压调节器,是开启,还是进入低功耗模式,LPDS = 0,电压调节器开启,LPDS = 1,电压调节器进入低功耗。最后,当我们把这些位提前设置好了,最后再调用 WFI 或者 WFE,芯片就可以进入停止模式了。然后停止模式的唤醒,因为这个模式下,芯片睡得更深,关的东西更多,所以唤醒条件就苛刻一些,是任一外部中断。刚才睡眠模式是任一中断,所有外设的中断都行,现在停止模式,要求就是只有外部中断才能唤醒了,其他中断唤醒不了。刚才我们还提到了,PVD、RTC 闹钟、USB 唤醒、ETH 唤醒,借道了外部中断,所以这 4 个信号,也可以唤醒停止模式,另外这里并没有区分 WFI 和 WFE,其实也可以想象得到,WFI 要用外部中断的中断模式唤醒,WFE 要用外部中断的事件模式唤醒,这是对应的。
之后看,停止模式对电路有哪些操作呢?首先,关闭所有 1.8V 区域的时钟,这意思就是,不仅 CPU 不能运行了,外设也运行不了,定时器,正在定时的,会暂停,串口,收发数据,也会暂停;不过由于没关闭电源,所以 CPU 和外设的寄存器数据都是维持原状的。之后下一个,HSI 和 HSE 的振荡器关闭,既然 CPU 和外设时钟都关了,那这两个高速时钟,显然也没用了,所以 HSI 内部高速时钟和 HSE 外部高速时钟,会关闭;当然,它没提到的 LSI 内部低速时钟和 LSE 外部低速时钟,这两个并不会主动关闭,如果开启过这两个时钟,还可以继续运行。最后,电压调节器,这里可以选择是开启,或者处于低功耗模式,刚才说了,这个电压调节器是由这个 LPDS 位控制的,这个开启和低功耗模式有啥区别呢?其实区别不大,电压调节器无论是开启还是低功耗,都可以维持 1.8V 区域寄存器和存储器的数据内容。区别就是,低功耗模式更省电一些,同时,低功耗模式在唤醒时,要花更多的时间;相反,电压调节器开启的话,就是更耗电一些,唤醒更快了。
那这些就是停止模式的介绍。主要操作就是,把运行的高速时钟都关了,CPU 和外设,都暂停工作,但是电压调节器并没有关,存储器和寄存器数据可以维持原样。它的唤醒条件比较苛刻,只能通过外部中断唤醒,所以停止模式,相当于整个人都罢工了,脑子不工作,身体也不工作,只有有人用外部中断过来敲我,我才会醒来干活。在省电程度上,评级为“非常省电”。 -
最后我们看第三种,待机模式。进入的话,和停机模式差不多,首先 SLEEPDEEP 也是置 1,即将深度睡眠,然后 PDDS 置 1,表示即将进入待机模式,最后调用 WFI 或者 WFE,就可以进入待机模式了。然后看一下唤醒条件,普通外设的中断和外部中断,都无法唤醒待机模式,待机模式,只有这几个指定的信号才能唤醒:第一个是 WKUP 引脚的上升沿,WKUP 引脚,可以看一下引脚定义表,里面 PA0-WKUP 指示了 WKUP 引脚的位置就是 PA0 的位置。之后继续,第二个是 RTC 闹钟事件,这个我们的示例代码和上一节 RTC 提到过,RTC 闹钟可以唤醒待机模式,应用场景就是,芯片每隔一段时间自动工作一次。第三个,是 NRST 引脚上的外部复位,意思就是按一下复位键,它也是能唤醒的。最后一个,IWDG 独立看门狗复位,这个了解一下就行,看门狗我们之后介绍。好,可以看出,待机模式只有这指定的 4 个信号能唤醒,其他信号都唤醒不了,唤醒条件最为苛刻。之后待机模式,对电路的操作,基本上是能关的全都关了,1.8V 区域的时钟关闭,两个高速时钟关闭,电压调节器关闭,这个意味着 1.8V 区域的电源关闭,内部的存储器和寄存器的数据全部丢失。但是和停止模式一样,它并不会主动关闭 LSI 和 LSE 两个低速时钟,因为这两个时钟还要维持 RTC 和独立看门狗的运行,所以不会关闭。这就是待机模式的介绍,主要操作就是把能关的全都关掉,只保留几个唤醒的功能,当然,配合 RTC 和独立看门狗的低速时钟,也可以正常工作。所以待机模式,相当于这个人直接下班回家睡觉了,没有指定的这几个事,它是不会轻易回来工作的。在省电程度上,待机模式评级为“极为省电”。
好,以上就是这 3 种低功耗模式的详细介绍以及它们之间的区别,相信大家对这几种模式已经有了大概的了解了吧,接下来我们对这里的一些细节问题再额外补充和总结一下。
1.5.1 模式选择
首先是模式选择的问题。刚才上面的表述,出现了很多寄存器的位。其中这些模式,又有一些更细的划分,比如睡眠模式有 SLEEP-NOW 和 SLEEP-ON-EXIT 的区别;停机模式有电压调节器开启和低功耗的区别。我们如何配置才能指定某个模式呢?那看这个图,就比较清晰了。
当然这些寄存器,实际上库函数已经帮我们封装好了,不用我们自己配置的。但是多了解一些,对我们理解程序还是有很大帮助的。
首先有一句:执行 WFI(Wait For Interrupt,等待中断)或者 WFE(Wait For Event,等待事件)指令后,STM32进入低功耗模式。就是说这两个指令是最终开启低功耗模式的触发条件,配置其他的寄存器,都要在这两个指令之前。
看一下这个图,首先,一旦 WFI 或者 WFE 执行了,芯片咋知道他要进入哪种低功耗模式呢?那它就会按照这个流程来判断。
首先看看 SLEEPDEEP 位是 1 还是 0,如果 SLEEPDEEP = 0,就是浅睡眠,对应的就是睡眠模式;如果 SLEEPDEEP = 1,表示要进入深度睡眠模式,对应的是停机或者待机模式,停机和待机,都可以叫做深度睡眠模式。在普通的睡眠模式下,还有个细分的功能,通过 SLEEPONEXIT 位来决定,这一位等于 0 时,无论程序在哪里调用 WFI/WFE,都会立刻进入睡眠;这一位等于 1 时,执行 WFI/WFE 之后,它会等待中断退出,等所有中断处理完成之后,再进入睡眠,这个可能考虑到中断还有一些紧急的任务,最好不要被睡眠打断了,所以先等等也无妨。当然这两种细分模式,我们一般可以不用管,只要我们不在中断函数里调用 WFI/WFE,那其实它们的效果是一样的;我们 WFI/WFE 可以放在主程序里,如果主程序执行到了,自然也代表中断处理完成了;如果你想在中断函数里调用 WFI/WFE,并且想中断结束后再睡眠,才需要考虑下面这个模式。
然后继续看,进入深度睡眠模式,它会继续判断 PDDS 这一位,如果 PDDS = 0,就进入的是停机模式;如果 PDDS = 1,就进入的是待机模式。在停机模式下,它会继续判断 LPDS 位,如果 LPDS = 0,就是停机模式且电压调节器开启;如果 LPDS = 1,就是停机模式且电压调节器低功耗,电压调节器低功耗的特性就是:更省电,但是唤醒延迟更高。
那这些,就是模式选择的一个判断流程。通过这个图,看的应该就比较清晰了。最后我们再分别总结一些这三种模式的一些特性。
1.5.2 睡眠模式
首先是睡眠模式。
- 执行完 WFI/WFE 指令后,STM32 进入睡眠模式,程序暂停运行,唤醒后程序从暂停的地方继续运行。
一般我们可以在主循环的最后,执行一下 WFI/WFE。主循环执行一遍,就睡眠;然后唤醒后,主循环又执行一遍,再睡眠;每唤醒一次,主循环执行一遍。
-
SLEEPONEXIT 位决定 STM32 执行完 WFI 或 WFE 后,是立刻进入睡眠,还是等 STM32 从最低优先级的中断处理程序中退出时进入睡眠。
-
在睡眠模式下,所有的 I/O 引脚都保持它们在运行模式时的状态
比如,如果你在程序里进行点灯,灯点亮了,再进入睡眠,灯仍然是亮的,GPIO 引脚的高低电平在睡眠时是维持原样的。
- WFI指令进入睡眠模式,可被任意一个NVIC响应的中断唤醒
- WFE指令进入睡眠模式,可被唤醒事件唤醒
这个唤醒事件还是有些复杂的。可以看一下手册,在手册睡眠模式这一节,有对睡眠模式的唤醒事件描述,其中说了,唤醒事件可以通过下述方式产生。
- 第一种,在外设控制寄存器中使能一个中断,而不在 NVIC 中使能;并且,还要在内核的系统控制寄存器中使能 SEVONPEND(Send Event On Pend)位,这样才能产生唤醒事件,并且唤醒后,要及时清除挂起位才行。
- 第二种,就是配置 EXTI 为事件模式,这个直接配置即可。
所以看到,这个事件唤醒还是有点麻烦的,你要是觉得麻烦,直接使用中断唤醒的方式,也是可以的,还简单一些。这就是事件唤醒的一些描述。
那睡眠模式总结的一些知识点就这么多。
1.5.3 停止模式
接下来看停止模式。
- 执行完 WFI/WFE 指令后,STM32 进入停止模式,程序暂停运行,唤醒后程序从暂停的地方继续运行
和睡眠模式一样。因为睡眠模式和停止模式存储器和寄存器的内容都可以维持,所以唤醒后,程序可以直接在暂停的地方继续运行。
- 1.8V供电区域的所有时钟都被停止,PLL、HSI和HSE被禁止,SRAM和寄存器内容被保留下来。
这个刚才那个一览表介绍过。CPU 和外设的时钟都停止,但是没有断电,SRAM 和寄存器的数据还可以维持。
- 在停止模式下,所有的I/O引脚都保持它们在运行模式时的状态
这个和睡眠模式一样。GPIO 在进入睡眠或者停止模式时暂停,并且高低电平维持暂停前一刻的状态。
- 当一个中断或唤醒事件导致退出停止模式时,HSI被选为系统时钟
这一条是涉及编程的注意事项。我们的程序,默认在 SystemInit 函数里的配置,是使用的 HSE 外部高速时钟,通过 PLL 倍频,得到 72 MHz 主频;但是进入停止模式后,PLL 和 HSE 都停止了,而且在退出停止模式时,它并不会再自动帮我们开启 PLL 和 HSE,而是默认用 HSI 的 8 MHz,直接作为主频,所以如果你忽略了这个问题,那么就会出现一个现象,你程序刚上电,是 72 MHz 的主频,但是进入停止模式,再换醒之后,就变成了 8 MHz 的主频了,这是一个问题。所以我们一般在停止模式唤醒后,第一时间就是重新启动 HSE,配置主频为 72 MHz,这个操作也不麻烦,配置的函数它都帮我们写好了,我们只需要再调用一下 SystemInit 就行,这是这个问题。
- 当电压调节器处于低功耗模式下,系统从停止模式退出时,会有一段额外的启动延时
这个就是刚才电压调节器开启和低功耗模式的区别了。电压调节器低功耗,更省电,但是从停止模式退出时,会有一段额外的启动延时。
- WFI指令进入停止模式,可被任意一个EXTI中断唤醒
- WFE指令进入停止模式,可被任意一个EXTI事件唤醒
最后两条是 WFI 和 WFE 的区别。停止模式,只能通过 EXTI 唤醒。中断模式唤醒 WFI,事件模式唤醒 WFE。
那以上就是停止模式的相关知识点。
1.5.4 待机模式
最后,我们看一下待机模式。
- 执行完WFI/WFE指令后,STM32进入待机模式,唤醒后程序从头开始运行
这个和上面两个模式就有些区别了。待机模式下唤醒,程序是从头开始运行的,因为待机模式把内部大部分电路的电源直接断了,数据都丢失了。唤醒之后,程序也无法继续,只能从头开始。
- 整个1.8V供电区域被断电,PLL、HSI和HSE也被断电,SRAM和寄存器内容丢失,只有备份的寄存器和待机电路维持供电
能断的都断掉,不过备份寄存器和待机电路还是可以维持供电的。
在待机模式下,所有的I/O引脚变为高阻态(浮空输入)
对于输出来说,既不输出高电平,也不输出低电平,呈现高阻态。对于输入来说,不上拉也不下拉,呈现浮空输入状态。实际上 GPIO 在配置里没有高阻态这个配置,它其实就是浮空输入配置,浮空输入,对于输出而言,就是高阻态。所以说,如果你提前点了个灯,进入待机模式后,无论这个灯是高电平点亮还是低电平点亮,它都会熄灭,GPIO 对外不输出高低电平,也不流过电流。
- WKUP引脚的上升沿、RTC闹钟事件的上升沿、NRST引脚上外部复位、IWDG复位退出待机模式。
这 4 个是待机模式的唤醒条件,刚才也说过。
那这些,就是待机模式的一些描述。
到这里,我们这个知识点,就介绍完了。
1.6 数据手册和参考手册
最后,我们分别看一下数据手册和参考手册。
1.6.1 数据手册
首先看一下数据手册。刚才我们说了,睡眠模式是一般省电,停止模式是非常省电,待机模式是极为省电。说了这么多省电,到底有多省呢?我们看一下数据手册,用数据说话。在手册这里,5.3.5 供电电流特征,有介绍,这里测试条件比较多,表也比较多,另外耗电量在不同工作条件下都是不同的,做产品的话,具体值还是以实测为准,现在我们就大概看一下官方给的一些测试表,看一下各个模式的耗电范围。首先这个表是从 Flash 运行,正常运行模式下的供应电流,条件是外部时钟,使能所有外设和关闭所有外设,然后各个主频下的电流消耗,在右边,可以看到,耗电电流区间是几到几十,单位是 mA,最高是 50mA 左右,最低是 7mA 左右。上下对比,使能所有外设,比关闭所有外设更耗电,所以,为了省电,不需要的外设,我们可以把它的时钟关掉;另外,对于频率来说,降低主频,对于省电,也是很划算的,降低主频后,耗电电流,下降也很明显。所以,如果你需要设备连续运行,并且对于主频和性能没那么高要求的话,降低主频也是一个不错的选择,这就是这个表的数据。
然后下面这个,是从 RAM 运行的耗电数据。整体上来看,比上面这个表低一些,但是差别不大。然后下面这个图里,可以看出主频、温度和耗电的关系,主频和耗电大概上是一个正比的关系,72M,耗电 40mA 左右,频率降低一半,36M,耗电大概也降低一半,20mA 左右,36M 再降低一半,是 18M,可以看到这个相近的 16M,差不多电流也降低了一半,16M 再降低一半,8M,电流继续降低一半,所以主频越低,耗电越低,主频每降低一半,耗电大概就也降低一半,当然只是大概一半的关系,并不是严格对应的;然后再看温度,温度升高,耗电量也是升高的,当然影响并不是很大。这就是这个图可以看出来的特征。
接着继续,我们看睡眠模式,这是睡眠模式下的供应电流,它的耗电也是 mA 级别的,从几 mA 到几十 mA 不等。但是整体上来看,比正常运行的耗电是低一些的,比如正常运行的情况下,最大刚才看了是 50mA 左右;在睡眠模式下,这个最大电流,降低到了 30mA 左右,会省一些电,但是耗电也是 mA 级别的,只能算是一般省电。
之后继续,我们看下一个表,这是停机模式下的供应电流和待机模式下的供应电流,这些是测试条件,大家可以仔细看看。那看右边的数据呢?首先,这个电流的单位是 μA 级别,在停机模式下,3.3V 供电时耗电电流典型值是 14~24 μA,这个就非常省电了;在待机模式下,电流会进一步降低,典型值是 2~3 μA 左右,可以算是极为省电了。举个例子呢?为了方便计算,我近似取个值,假设正常运行电流是 30mA,停机模式电流是 30μA,待机模式是 3μA,那么对应一个 300mAh 的电池来说,正常运行能用 10 个小时,停机模式能用 1 万个小时,待机模式能用 10 万个小时,这个对比,差别就比较大了吧。所以如果你的产品使用了电池供电,低功耗模式,还是要考虑用一下的。
最后我们看到,备份区域的供应电流,也非常低,RTC 开启的情况下,也只需要 1.4 μA,所以备用电池接上一个,基本不用担心没电的。
那有关 STM32 各个状态下的电量消耗,我们就看到这里。剩下的一些表格和数据,大家可以自己再看看。
最后,我们还是照例,看一下参考手册。
1.6.2 参考手册
我们本节介绍的内容,位于参考手册第 4 节,电源控制 PWR。
看一下,首先是整体的电源框图,内部电源有哪些区域,通过哪些引脚供电,这个了解一下;下面有一些详细的解释,比如独立的 AD 转换器供电和参考电压,这些是 VDDA 和 VREF 相关引脚的介绍,100 脚和 144 脚封装有单独的 VREF,64 脚或更少的封装,没有 VREF,它们在芯片内部与 ADC 的电源相联;之后下面是电池备份区域的,这里有一些警告和建议,这个我们上一节讲 RTC 电路的时候说过。
当然这里还有一个注,写的是因为模拟开关只能通过少量的电流(3mA),在输出模式下使用 PC13~PC15 的 I/O 口功能是有限制的,速度必须限制在 2MHz 以下,最大负载为 30pF;而且这些 I/O 口绝对不能当作电流源,如驱动 LED。
它这里特意强调了,PC13~PC15 端口绝对不能驱动 LED。但有意思的是,我们这个最小系统板,它上面有两个 LED,一个是电源指示灯,另一个是接在 GPIO 口的测试灯,并且,这个接在 GPIO 口的测试灯,它就正好位于 PC13,所以说,我们这个最小系统板的电路设计,违背了这条注意事项。不过好在,这个贴片的小 LED,电流并不是很大,目前也没有什么问题。但是它要是真的导致 GPIO 损坏,你也不能怪它没提醒你,所以,我们在做产品之前,还是要仔细阅读手册,知道的越多,就越不容易犯错。
然后继续,下面是电压调节器,有 3 种模式,正常运转、低功耗和停止供电。再看下面,这是上电复位和掉电复位的介绍,这个图和功能,我们也讲过。下面是可编程电压监测器,大家可以看一下。
之后,就是低功耗模式的介绍了,低功耗一览表,总共有睡眠、停机、待机,三种模式。下面是一些省电建议,首先就是降低系统时钟,降频后,功耗会明显降低,刚才通过那个数据手册也看过;然后是外部时钟的控制,不需要的外设,我们可以把时钟给关掉,以减少功耗;另外,在睡眠之前,也可以关闭所有外设的时钟来省电,不过这样,睡眠模式下的外设,就干不了活了。
然后下面是睡眠模式,执行 WFI 或 WFE 进入睡眠,可以选择立刻睡眠,或者等中断结束后再睡眠;退出睡眠呢,在这里也有介绍,和我们介绍的一样。
接着是停止模式,如何进入,如何退出,以及一些注意事项,大家可以自己再看看。
然后是待机模式,这些介绍也可以再看看。
最后这里有个低功耗模式下的自动唤醒,这里的意思就是,使用 RTC 可以在停止模式或待机模式下定期唤醒芯片,也就是我们第四个示例代码演示的现象。
然后下面就是一些寄存器描述了。控制寄存器,里面是一些控制位,比如 BKP 位,取消后备区域写保护;PLS,选择 PVD 电平;PDDS,选择是停止模式还是待机模式;LPDS,选择停止模式下电压调节器是开始还是低功耗;这些位就是在这里定义的。
然后,控制/状态寄存器,里面是一些控制位和标志位。比如使能 WKUP 引脚和一些标志位。最后就是寄存器总表了,寄存器不多,总共就两个。
好,那到这里,本节 PWR 的知识点我们就介绍完了。我们下一小节,来学习低功耗模式的相关代码。
2. 四个低功耗模式功能案例
本小节我们来学习低功耗模式的代码部分。本节代码都是在之前代码的基础上修改而来的,所以接线基本是和之前对应代码的一样。
2.1 修改主频
2.1.1 硬件电路
首先看一下接线图。
这个代码不需要接其他的模块。所以只接个显示屏显示一下即可,非常简单。
那看一下面包板,这几个代码的接线,我都已经事先接好了,因为之前都演示过,所以这里就不再过多演示接线部分了。
2.1.2 代码整体框架
在这个工程里,我们主要的任务就是研究一下 system_stm32f10x.c 和 .h 这两个文件,这两个文件之前也介绍过一点,这里呢,我们就在更进一步研究一下,看一看这些代码是怎么运作的,顺便,我们再利用它里面给我们提供的宏,完成修改主频的任务,降低主频,可以用来降低功耗,也算是契合本小节的主题了。
那我们来看一下,首先这两个 system 文件,是用来配置系统时钟的,也就是配置 RCC 时钟树。这个 RCC 时钟树,我们可以看一下 TIM 定时器章节的 RCC 时钟树框图。
这个图就是 RCC 时钟树的全部电路,左边是 4 个时钟源,HSI、HSE、LSE、LSI 用于提供时钟,右边就是各个外设,就是使用时钟的地方。我们用的最多的就是,AHB 时钟、APB1 时钟和 APB2 时钟,另外还有一些时钟,它们的来源不是 AHB 和 APB,比如 I2S 的时钟,直接来源于 SYSCLK;USB 的时钟,直接来源于 PLL,当然这些特例我们就不过多关心了。
我们主要关心的就是这个外部的 8MHz 晶振,它如何进行选择,如何倍频,才能得到这个 72MHz 的 SYSCLK,系统主频;然后系统主频如何去分配,才能得到 AHB、APB1 和 APB2 的时钟频率。在我们之前的章节里,我们一直保持了默认的配置,就是晶振接的是 8M,主频是 72M,AHB 和 APB2 是 72M,APB1 是 36M,但其实,这些都不是绝对的,可以根据自己的需求进行更改。
当然建议一般还是不要改吧,毕竟目前绝大多数程序都是按照默认的配置来写的,随意更改可能会造成一些问题,
回到程序,我们来看一下,它这里是怎么配置这个 RCC 时钟树的,那这个 .c 文件最开始写了一堆注释,这个注释就是对这个 system 文件的介绍,读懂这些注释,就差不多能理解这个文件了。所以注释我们还是得好好看一看的,英文不好的话,用用翻译软件就行,难得它写了这么多注释,不看白不看。
那这里注释给大家简单说一下。第一条的意思就是,system 这两个文件,提供了两个外部可调用的函数和一个外部可调用的变量,两个函数是 SystemInit();
和 SystemCoreClockUpdate();
,一个变量是 SystemCoreClock
,这一点我们可以看一下头文件,有一个外部可调用的变量和两个外部可调用的函数,头文件总共就这么多东西,那正好,和我们这里注释是对应的。
然后这 3 个东西的用途呢,右边有解释。SystemInit();
这个函数用来配置时钟树的,也是这整个文件最主要的东西,使用 HSE 配置主频为 72M,就是这个函数干的活;并且这个函数,在复位后,执行 main 函数之前,在启动文件里自动调用了,所以 main 函数一进来,时钟就配置好了,不用我们操心。
之后,下面这个变量,SystemCoreClock
,表示主频频率的值,我们想知道目前的主频是多少,直接显示一下这个变量就行。
最后一个函数,SystemCoreClockUpdate();
,这个是用来更新上面这个变量的,因为这个变量只有最开始的一次赋值,之后如果我们改变了主频频率,这个值不会自动跟着变换,所以我们就需要调用一下这个函数,根据当前时钟树的配置,更新一下上面这个变量。
好,这就是这个文件对外提供的一些函数和变量。然后后面这几条注释大家自己看一下。
我们接着往后看,下面就是用来更改主频的宏定义了,这里写的是,解除对应的注释来选择想要的系统主频,重要提示,大家自己看。
下面是配置的地方,如果你使用的是 VL 这些设备,也就是超值系列,那可选主频只有两个,HSE 的 8M 和 24M;否则的话,可以选择这么多主频,8M、24M、36M、48M、56M 和 72M,当前解除注释的是 72M,所以主频默认就是 72M。这个 #if
叫做预编译,主要是用来兼容不同型号的设备,意思就是如果我们 define
了这些东西,那整个文件就是 if 这里的有效,下面这些代码相当于没写,不用看;否则 #else
的话,就是下面这些代码有效,上面的这些相当于没写。所以在看这里带有预编译的代码时,我们一定要清楚当前我们的设备是啥,F103C8T6 是 MD,中容量,非超值系列,所以我们就看下面这一块,在这里可以修改主频频率。如果你想改成 36M,那就把 72M 的宏注释掉,再把 36M 的宏解除注释,当然现在先不改,等会再改。
当然目前这个文件是只读的,它提示我无法更改,在这个文件列表里,这些文件的图标带了个钥匙符号,就代表它是只读的,解除只读的方法,我们打开工程文件夹,或者在选项卡这里右键,点打开包含的文件夹,这样它就可以直接打开文件夹并定位到这个文件,在这里,我们右键,属性,把只读的勾去掉,确定。好,我们回到 keil,可以看到这个文件的钥匙图标已经没有了,这时我们就可以修改代码了。
如果你想整个文件夹或者整个工程的文件都取消只读,那就在外面,对整个文件夹操作,然后把这个只读去掉就行了。
那我们简单的修改一个宏定义,它是如何作用于电路的配置的呢?我们继续往后看,首先我们要找到 SystemInit,这是最先调用的函数,它里面的代码,这些都是直接操作寄存器的写法,看不懂没关系,它这里都有注释的嘛。可以看到第一步,是开启 HSI,也就是默认使用的是内部的 8M 晶振;之后这些操作,可以看到都是各种 Reset,各种 Disable,作用就是恢复缺省配置;最后,恢复完之后,调用 SetSysClock 函数。
那我们转到这个函数,继续挖掘,这个函数里,我们就能知道,为什么改变宏定义就可以修改主频了。可以看到,这个函数实际上就是一个分配函数,根据你定义的不同宏定义,选择执行不同的配置函数,比如你前面解除了这个 72M 的宏,那它就执行设置时钟到 72M 这个函数;如果你解除了 36M 的宏,那就执行设置时钟到 36M 的函数,根据你所选的宏,执行不同的函数。
我们当前解除的是默认的 72M 这个,那就继续挖掘这个函数。转到定义,好,到这里,这才是正式的时钟配置部分。
配置第一步,是使能 HSE,外部高速时钟;第二步是一个循环,等待 HSERDY 或者超时退出,接下来根据 HSERDY 标志位,给 HSEState 置 1 或 0,如果 HSEState 等于 1,表示晶振启动成功,进入 if。配置第一步是设置 Flash 的等待,这个不用多管;之后配置,HCLK、PCLK2 和 PCLK1 的分频器,HCLK 就是 AHB 的时钟,PCLK 就是 APB 的时钟,可以看到这里的分频分别是 1、1、2,对应这个时钟树就是 1 分频、1 分频,2 分频,如果 SYSCLK 是 72M 的话,那 AHB 和 APB2 就是 72M,APB1 是 36M。那继续往后,是 #ifdef
,CL 是互联型,我们这个是 MD,所以 #ifdef 里面不看,我们看 #else
里的,可以看到这里配置的是锁相环,HSE 是 8M,锁相环选择 9 倍频,最终锁相环输出就是 72M,之后就是,使能锁相环,等待锁相环准备就绪,选择锁相环的输出作为系统时钟,最后,等待锁相环成为系统时钟,对应时钟树的图,刚才执行的配置就是选择外部的 8M 晶振作为锁相环输入,锁相环执行 9 倍频,输出的 72M,选择为 SYSCLK,这就是库函数默认的时钟配置。
这个 SystemInit 函数,干的就是这些活。然后代码最后还有个 else,写的是,如何 HSE 启动失败,应用程序会有错误的时钟,用户可以在这里加一些代码解决这个错误。同时我们也可以看出,如果 HSE 没接或者 HSE 坏了,那么上面的代码都不执行,程序默认会使用最开始启动的 HSI,内部 8M 时钟,作为系统主频,所以如果你发现你的主频变成了 8M,那可能就是外部晶振有问题了,你可以在这个 else 里加一些代码,如果确实执行到了 else 里面,那就说明确实是 HSE 有问题。
好。这就是这个 system 文件里代码的作用。然后往上看看,除了有这个 SetTo72 的,还有 SetTo56 的,这里面的执行流程几乎是一样的,主要区别就是锁相环的倍频是 7 倍,HSE7 = 56M;之后继续,这是 HSE6 = 48M;再往上,这是 (HSE/2)* 9 = 36M,HSE/2,说明这个 HSE 进来之后,走的是 RCC 时钟树的下面这一路 /2 的;然后再往上,(HSE/2)* 6 = 24M;最后这个,是直接使用 HSE 作为系统时钟了,没有通过锁相环倍频,主频就是 8M,对应时钟树,HSE 进来,就是走的从 4-16MHz HSE OSC 到 HSE 这一路。
那最后总结一下。首先是 SystemInit 函数,进来首先启动 HSI,之后就是各种恢复缺省配置,最后调用 SetSysClock,SetSysClock 是一个分配函数,根据我们前面解除注释的宏定义,选择执行不同的配置函数,比如 SetSysClockTo72、To56、To48 等等,最后在 To72 等这些函数里,才是真正的配置。比如 To72 的配置是,选择 HSE 作为锁相环输入,之后锁相环进行 9 倍频,再选择锁相环输出作为主频,这样主频就是 72M 了,这就是配置时钟的整个流程。
好,那流程说完了,接下来我们就来完成代码吧。
首先,我们刚才说了。有个变量,可以表示当前主频,那我们显示一下看看。
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
int main(void) {
OLED_Init();
//先来个字符串显示当前主频
OLED_ShowString(1, 1, "SYSCLK:");
OLED_ShowNum(1, 8, SystemCoreClock, 8);
while(1){
}
}
这样就行了。然后这个 system 的头文件不需要再包含了,因为 stm32f10x.h 这个头文件里面已经包含过了。那目前我们来测试看一下。
编译下载看一下,目前显示的主频确实就是 72M,没问题。然后我们继续测试,
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
int main(void) {
OLED_Init();
//先来个字符串显示当前主频
OLED_ShowString(1, 1, "SYSCLK:");
OLED_ShowNum(1, 8, SystemCoreClock, 8);
while(1){
//首先,我们在主循环里加个 Delay,表示一下当前代码运行的速度。
OLED_ShowString(2, 1, "Running");
Delay_ms(500);
OLED_ShowString(2, 1, " ");//再显示个空格,把 Running 擦掉
Delay_ms(500);
//这就相当于闪烁 Running 字符串。闪烁周期是 1s
}
}
下载看一下。可以看到,目前现象一切正常,Running 闪烁周期是 1s。
接下来我们就来开始修改主频了。那修改主频呢,我们就到 system_stm32f10x.c 这个文件的最开始,比如修改成 36M,36M 是 72M 的一半,这个方便观察。
这样就行了,下载看一下,可以看到,第一行的主频频率,显示的就是 36M,第二行 Running 闪烁也变慢了,目前闪烁周期是 2s,说明主频确实降低了一半,这就完成了。修改主频还是非常简单的吧,那其他的频率,大家都可以自己再尝试一下。
然后还有一个问题说一下,就是修改主频后,有很多涉及计时的计算都要重新匹配一下。比如这个 Delay 函数,我代码默认使用的是 72M 主频,转到定义看一下,这里 Delay_us 时,默认是 72,所以在 72M 主频下,Delay 的时间是正确的,降低主频后,Delay 的时间不能自适应变化。如果你想在任何主频下,Delay 的时间都是正确的,那我们程序就别写死了,可以用这个 SystemCoreClock 变量来做自适应,把这个变量也代入计算就行。另外从这里我们也可以看出来,修改主频是一个牵一发而动全身的事情,改完之后,很多涉及精准计时的计算,都要做好匹配的工作,所以一般条件下,是不推荐随便修改主频的,除非是真的有需求,并且可以协调好每个需要计算的地方。
好,到这里,我们第一个代码就完成了,主要是学习一下 system 这两个文件的工作流程,另外再用它提供给我们的宏定义,修改一下主频,那第一个代码就是这样。
接下来我们来学习第二个代码。
2.2 睡眠模式+串口发送+接收
2.2.1 硬件电路
首先看一下接线图。
这个和串口那一节的接线一样,拿出一个CH340 USB 转串口模块,GND 和 STM32 共地,RXD 接在 PA9,对应 STM32 的 TX 引脚;TXD 接在 PA10,对应 STM32 的 RX 引脚,发送和接收交叉连接,别搞错了。
2.2.2 代码整体框架
回到工程文件夹,复制一下 9-2 的工程,改个名字,打开工程,编译一下。
在这个工程的基础上,我们要为它加入低功耗的代码。
那假设我们目前要用这个 STM32 做一个下位机,下位机接收电脑串口发过来的指令,然后执行相应的功能,电脑随时都有可能通过串口发送指令,当然也可以几个小时、几天都不发指令,为了随时能响应指令,STM32 就得时刻准备着。比如我这里主循环,就一直在不断地检查标志位,但是你如果一直不发指令,我这些操作都没啥意义,还比较费电;当然你可能说把这段代码放在中断里就行了,但是即使主循环是空的,它 CPU 也是在不断耗电的,所以,对于这种靠中断触发,没有中断的时候,就没什么事的代码,我们就可以给它加入低功耗模式,没事的时候就低功耗,中断来了,再醒来干活就行了。
那对于当前这个代码,我们可以加入哪一种低功耗模式呢?首先,睡眠模式,这是可以的,CPU 时钟关闭,程序不再执行,但是外设的时钟不会关,USART 硬件电路还是可以接收数据的,USART 收到数据后,产生中断,唤醒 CPU,所以睡眠模式可以。之后,停机模式,这个叫停机模式,或者叫停止模式,都是一个意思,因为手册里一会写的是停机模式,一会写的是停止模式,我这也是停机模式、停止模式都有出现过,大家知道是一个东西就行,那继续看,停止模式行不行呢,这个模式下,所有 1.8V 区域的时钟都关了,CPU 和外设都不能运行,那自然 USART 也收不到数据,产生不了中断了,并且,USART 的中断,也不能唤醒停止模式,所以,当前这个程序功能,用不了停止模式。最后待机模式,自然也不行了。
所以,目前这个程序功能,只能加上睡眠模式,一般省电了。那开始写代码。
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
uint8_t RxData; //定义用于接收串口数据的变量
int main(void)
{
OLED_Init(); //OLED初始化
OLED_ShowString(1, 1, "RxData:"); //显示静态字符串
Serial_Init(); //串口初始化
while (1)
{
if (Serial_GetRxFlag() == 1) //检查串口接收数据的标志位
{
RxData = Serial_GetRxData(); //获取串口接收的数据
Serial_SendByte(RxData); //串口将收到的数据回传回去,用于测试
OLED_ShowHexNum(1, 8, RxData, 2); //显示串口接收的数据
}
//在睡眠模式之前呢,我们先加一个 Running 的指示,看一下不加睡眠的情况。
OLED_ShowString(2, 1, "Running"); //OLED闪烁Running,指示当前主循环正在运行
Delay_ms(100);
OLED_ShowString(2, 1, " ");
Delay_ms(100);
//这是正常的情况。
}
}
我们先下载看一下,可以看到,即使我们现在并没有发送数据,主循环也是在不断运行的。那我们打开串口助手,随便发送一个数据,可以看到,数据收发是没问题的,但是,没有数据发送时,主循环还在不断运行,这是耗电操作,我们可以用睡眠模式来优化这部分损耗。
那怎么使用睡眠模式呢,很简单,我们在主循环的最后,加上一个 WFI 指令即可。WFI 指令的格式是:
__WFI(); //执行WFI指令,CPU睡眠,并等待中断唤醒
这就是 WFI 指令,格式就按照这个来,当然睡眠模式还有一个指令是 WFE。它两的区别就是唤醒方式不同,WFI 是中断唤醒,WFE 是事件唤醒。当然推荐是使用 WFI 中断唤醒,因为事件模式配置可能比较麻烦,另外中断模式唤醒,正好可以在带有中断的程序修改,比较方便。
所以我们工程就只演示 WFI 了,那转到定义看一下,它对应的就是一个 CPU 的汇编指令,再往下就看不到定义了,所以这个指令格式记住就行,那我们的睡眠模式,其实简单的加上这一句就完成了。当然通过模式选择里的流程图,这里其实还涉及两个寄存器的位,一个是 SLEEPDEEP,另一个是 SLEEPONEXIT,这两个位,其实库函数并没有给我们提供一个比较方便的配置方法,所以我们暂时就不配了,直接使用默认值 0,它进入的就是默认的睡眠模式,立刻睡眠。
如果确实想要配置的话,得用操作寄存器的方式完成,这两个位,位于内核的系统控制块,我们需要打开参考文档,打开 Cortex-M3 编程手册,然后打开第 4 章,内核外设,4.4 系统控制块,SCB,这里有个系统控制寄存器 SCR。
可以看到,这里就是和低功耗相关的 3 个位,第一个 SEVONPEND,是事件唤醒睡眠模式需要设置的位;第二个 SLEEPDEEP,决定是进入睡眠模式,还是深度睡眠模式;第三个 SLEEPONEXIT,确定是立刻睡眠,还是等中断结束再睡。
那如果你想配置这 3 个位的话,得在程序中,用操作寄存器的方式实现。比如在 WFI 上面,写个 SCB 外设的 SCR 寄存器等于 0x… …(SCB->SCR = 0x...;
),这个值,你得对照着寄存器的位来给。当然我们不要求这么多,进入睡眠模式的话,这些位暂时就不配了,保持默认为 0 就行。
那睡眠模式的代码就是这样,我们编译下载看一下。可以看到,这里第二行,就不再闪烁 Running 了,这说明主循环目前是停下来了,CPU 正在睡眠;那我们用串口助手发送一个数据试试,可以看到,每发送一次,Running 闪烁一次,接收区回传一次数据,这说明程序在收到数据之后,可以唤醒工作一次,对吧。在不影响程序功能的前提下,CPU 能在空闲时睡眠,节约用电,这就是睡眠模式的作用。
那回到程序,分析一下程序的执行流程。首先程序开始,初始化,把串口配置好,之后进入主循环,检查一下标志位,Running 闪烁一次,在主循环的最后,执行 WFI,这时 CPU 就会立刻睡眠,程序是不是就停在 WFI 指令这里了啊,这时 CPU 睡眠,但是各个外设,比如 USART,还是工作状态;等到我们用串口助手发送数据时,USART 外设收到数据,产生中断,唤醒 CPU,睡眠模式唤醒之后,程序在暂停的地方继续运行,所以程序会运行到 WFI 之后;但是唤醒之后,中断也是会立刻申请的,所以程序在跳回到 while 循环开头之前,先进入 USART 的中断函数,在中断函数这里,读取数据,置 RxFlag,清除 RXNE,之后,回到主循环;然后回到 while 循环的开头,这时 RxFlag 刚刚置 1,所以 if 成立,执行数据回传和显示的功能,唤醒的功能执行完之后,Running 闪烁一次;最后,程序又来到 WFI 的位置了,这是一个新的睡眠指令,所以到这里,CPU 再次进入睡眠,正好我们需要它唤醒后执行的功能也执行完了,这时再进入睡眠,那就是恰到好处了。
之后,等待下一个数据发过来时,程序再重新执行一遍这个流程,这样就是这整个程序的执行步骤。所以睡眠模式,我们一般可以在 while 循环的最后加一个 WFI 指令,每来一个中断,先进中断函数,再执行一遍程序功能,这样就能实现我们想要的功能了。
那这些,就是睡眠模式的代码,我们就介绍完了。接下来我们就继续看下一个代码。
2.3 停止模式+对射式红外传感器计次
2.3.1 硬件电路
首先看一下接线图。
这个和外部中断那一节的接线一样。我们拿出对射式红外模块,VCC 和 GND 接上供电,DO 输出,接到 STM32 的 PB14 引脚。
2.3.2 代码整体框架
这一个代码我们要学习停止模式,停止模式只能通过外部中断触发(唤醒),所以和停止模式相关的代码,肯定得用外部中断,那我们复制 5-1 的工程。
那对于这个代码呢,其实思路和上一个睡眠模式是非常像的。你看这个 CountSensor_Get() 总是 Get,但是如果外部一直没有中断信号的话,那这个 Get 就是没有意义的耗电操作,如果几个小时,几天都没有外部中断触发计数,那我这个 while 循环也没有必要一直刷新,对吧。
所以对于这个代码,在空闲的时候,我们可以让它进入低功耗;又因为这个代码可以使用外部中断触发唤醒,所以我们可以让它进入更为省电的停止模式,在停止模式下 1.8V 区域的时钟关闭,CPU 和外设都没有时钟了。但是外部中断的工作是不需要时钟的,这一点从代码里也可以看出来,你看初始化的时候,根本就没有开启 EXTI 时钟的参数,这也是 EXIT 能在时钟关闭的情况下工作的原因,因为它不需要时钟。
那确定好用停止模式之后,我们来开始写代码。写代码之前,我们还是得看一下库函数。刚才讲的睡眠模式其实都只是内核的操作,睡眠模式涉及的几个寄存器也都是在内核里,跟 PWR 外设关系不大,所以刚才我们都没用到 PWR 的库函数;不过现在停止模式,涉及到内核之外的电路操作,这就需要用到 PWR 外设了,我们看一下库函数。(pwr.h)这些就是 PWR 外设的相关函数。
void PWR_DeInit(void);//恢复缺省配置
void PWR_BackupAccessCmd(FunctionalState NewState);//使能后备区域的访问,这个上一节用过。
//这两个是跟 PVD 相关的函数
void PWR_PVDCmd(FunctionalState NewState);//使能 PVD 功能
void PWR_PVDLevelConfig(uint32_t PWR_PVDLevel);//配置 PVD 的阈值电压
//如果你需要用 PVD 的话,就先指定阈值,然后 Cmd 使能一下即可,这是 PVD 的功能
void PWR_WakeUpPinCmd(FunctionalState NewState);//使能位于 PA0 位置的 WKUP 引脚。这个配合待机模式使用,待机模式可以用 WKUP 引脚的上升沿唤醒。当然用不用这个功能,是有一个开关的,如果你需要开启 WKUP 引脚唤醒功能的话,那就得调用这个函数,使能一下。
void PWR_EnterSTOPMode(uint32_t PWR_Regulator, uint8_t PWR_STOPEntry);//进入停止模式,调用这个函数就可以进入停止模式了,这是我们一会儿要用到的
void PWR_EnterSTANDBYMode(void);//进入待机模式,调用这个函数进入待机模式,这是我们下一个代码要用到的。
//最后两个,是获取标志位和清除标志位的函数。
FlagStatus PWR_GetFlagStatus(uint32_t PWR_FLAG);
void PWR_ClearFlag(uint32_t PWR_FLAG);
好,库函数我们就清楚了,接下来我们来开始写代码。
首先加入停机模式之前,我们还是加一个 Running 的指示,在主循环下面加一个。
OLED_ShowString(2, 1, "Running");
Delay_ms(100);
OLED_ShowString(2, 1, " ");//再把 Running 清除
Delay_ms(100);
当然,这些代码并不是必须的,加它主要是用来指示主循环是不是在不断运行的,方便我们观察实验现象,那目前我们先看一下当前代码的现象。编译,然后下载的步骤注意一下,按住复位键不放,下载,松手,看一下,遮挡一下这个传感器,可以看到数值是在计数,但是,在没有中断信号的时候,它还是在不断地 Running,这个耗电操作,我们可以用停止模式来优化一下。
按一下复位键,在唤醒上一个代码的睡眠模式的一秒期间,马上点击下载。低功耗期间不能下载。
所以回到程序,我们来加入停止模式的代码。停止模式和待机模式,都需要 PWR 外设干活,所以首先我们要开启 PWR 的时钟。在主循环上面,来个
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
这个时钟一定不要忘了开。如果外设没开时钟,那么写入寄存器是无效的,读取寄存器也全都是 0,这个现象大家可以自己试一下。
然后时钟开启之后,我们想让它在主循环的最后进入停止模式,那就可以直接调用这个函数
PWR_EnterSTOPMode(PWR_Regulator_ON, PWR_STOPEntry_WFI);
这个函数有两个参数,转到定义看一下。第一个参数是指定电压调节器在停止模式里的状态,开启或者低功耗,这个就随意了,目前选择开启。之后第二个参数,是停止模式的入口参数,是选择 WFI 指令进入停止模式,还是选择 WFE 指令进入停止模式,这里就选择 WFI 指令进入。这样进入停止模式的代码就完成了。
那这一句代码,里面干了什么呢?可以转到定义看一下函数内部。看一下,第一步,读取 PWR_CR 寄存器,放在临时变量里;第二步,清除 PDDS 和 LPDS 位,其中清除 PDDS,就代表选择停止模式,也就是模式选择中的 PDDS = 0,那在这一步的判断,会执行进入停止模式的操作;然后第三步,根据我们指定的参数,设置 LPDS 位,如果你这个参数选择调压器开启,那这里 LPDS 就等于 0,如果选择调压器低功耗,那这里 LPDS 就等于 1,这一步就对应模式选择中的 LPDS 的判断,LPDS = 0,调压器开启,LPDS = 1,调压器低功耗;之后下一步,把临时变量写入到 PWR_CR 寄存器,参数生效;最后,设置内核里的 SLEEPDEEP 位,这里 |= SLEEPDEEP 位的掩码,就是把 SLEEPDEEP 置 1,那对应模式选择中的 SLEEPDEEP 的判断,它就会执行深度睡眠模式的流程。至此,SLEEPDEEP、PDDS 和 LPDS 都已配置好,最后我们就可以调用 WFI 或者 WFE 指令了,可以看到,程序最后,就是一个 if,如果你第二个参数给的是 WFI,那它就执行 WFI 指令,否则,执行 WFE 指令,那执行到这个位置,芯片就会进入停止模式,程序暂停运行。然后最后还有一行,这一行就是停止模式唤醒之后,才能执行到,可以看到,在退出停止模式后,它很贴心的帮我们把 SLEEPDEEP 位又清零了。
好,这些,就是进入停止模式这个函数执行的内容,配置各种寄存器和调用指令在这一个函数里就都完成了。那在主函数里,我们调用这一个函数就可以了。
现在测试,看一下程序现象。按住复位键,下载,松手,看一下。现在,可以看到,在空闲时,Running 是没有闪烁了,这说明主循环是停止运行了,我们遮挡试一下呢?可以看到,每遮挡一次,Running 闪烁一下。不过我们还注意到一个现象,比如现在我按一下复位键,可以看到,复位之后的第一次 Running 闪烁很快,是正常的时间,而我们遮挡之后,Running 闪烁就变得很慢了,这是什么原因呢?那在停止模式里提到过,原因就在这里,退出停止模式时,HSI 被选为系统时钟,也就是,在我们首次复位后,SystemInit 函数配置的是,HSE * 9 倍频的 72M 主频,所以复位后,第一次 Running 闪烁很快,而之后,进入停止模式再退出时,默认时钟就变成了 HSI 了,HSI 是 8M,所以唤醒之后的程序,运行就会明显变慢,这就是刚才现象的原因。
那找到原因了,解决方法也很简单,我们只需要在退出停止模式之后,重新调用 SystemInit,重新启动 HSE,配置 72M 的主频就行了。所以在这里,我们简单的来一个
SystemInit();
就可以了,现在试一下看看。编译,复位,下载,看一下,再试试看,可以看到,这时遮挡之后,Running 的闪烁仍然是正常的,那这样,我们就解决了停止模式退出后,主频变为 8M 的问题了。
好,这就是停止模式的代码。然后这个代码的执行流程和上一个睡眠模式是非常类似的。首先复位后,初始化,之后进入主循环,然后进入 PWR_EnterSTOPMode
函数,执行到 __WFI
这一句 WFI 指令后,程序立刻暂停;等外部中断发生时,芯片唤醒,这时程序先进入到外部中断函数里,执行一遍;中断函数退出后,程序再到 __WFI
后面,继续运行,然后函数退出;执行 SystemInit
,配置时钟,之后 while 循环回到开头,执行一遍主循环的内容;最后,就是再执行这个函数,进入停止模式了,这是程序执行流程。
那到这里,我们第三个程序也就写完了,最后,我们来看第四个程序。
2.4 待机模式+实时时钟
首先看一下接线图。
首先是右下角接上 OLED 显示屏,然后 PA0 这里,引出来了一根线,这个线的意思就是我们可以手动把 PA0 接到 3.3V,或者断开。因为 PA0 还有个功能 WKUP,可以唤醒待机模式,WKUP 引脚上升沿有效,但是这个地方也不好接按键的,所以我们可以直接用一根线短接到 3.3V,来手动产生一个上升沿,测试 WKUP 引脚是不是有效果。
然后实时时钟,还有个备用电源 VBAT,这个接不接都行,因为我们需要在待机模式下唤醒,唤醒之后,没有主电源,程序运行不了,所以主电源是不能断电的。那主电源不断电,备用电源接不接,就都是一样的现象了。
2.4.2 代码整体框架
回到工程文件夹,复制 12-2 实时时钟的工程。
好,在这个工程里,我们的主要任务就是:第一,设置 RTC 闹钟;第二,进入待机模式;第三,使用闹钟信号,唤醒待机模式。
那在开始代码之前呢,我们先把目前的显示布局重新规划一下。目前 OLED 上已经没有位置了。
OLED_ShowString(1, 1, "CNT:XXXX-XX-XX");//那我们第一行,就用来显示 CNT,这个是秒计数器
OLED_ShowString(2, 1, "ALR:XX:XX:XX");//第二行,用来显示 ALR,这个是闹钟值
OLED_ShowString(3, 1, "ALRF:");//第三行,显示 ALRF,这个是闹钟标志位
之后下面,年月日时分秒就不显示了。CNT 的位置,放在 1 行 6 列,DIV 也不要了。
现在看一下现象,按复位键,下载,松手,目前看到,现在是第一行显示秒数,后面的,还没有东西。
回到代码,目前,我们先实现第一个任务,设定闹钟。我们可以在 while 循环上面这里设定,在每次复位后设定闹钟值,闹钟值也不用特别去记的,我们直接设置为当前秒数 +10,这样每次复位后,闹钟就都是 10s 后了。
怎么设定闹钟呢?我们到 rtc.h 里,看一下库函数,很明显,我们调用这个 SetAlarm,就行了,那放到这里,设定闹钟。
RTC_SetAlarm(RTC_GetCounter() + 10);
闹钟值也是一个 32 位的值,我们直接给它一个 RTC_GetCounter(),这是当前的秒数,然后 +10,就是设定闹钟为 10s 后,这样闹钟就设置好了。
之后在下面,我们还计划显示一下这个值,但是这里有个问题,就是这个闹钟寄存器,它是只写的,写进去之后,就读不出来了。怎么看出寄存器是只写还是只读呢?我们可以看一下手册的寄存器描述部分,打开 RTC 的寄存器描述,这个闹钟寄存器,可以看到,这个寄存器的下面,有一排字母 w,这个就是对应位的读写特征,只有一个字母 w,就代表对应的位只能写入,不能读出。然后上面,比如这个 CNT 寄存器,对应的位都是 rw,这个就代表可读可写;再往上,比如这个 DIV 余数寄存器,这里写的是 r,就代表只能读不能写,这就是最常见的几种读写特性。那有关这些读写特征的字母意思,可以看一下文档的第一章,这里有个表,里面各种缩写和对应的意思,大家可以自行看一下。
那回到代码,因为这个闹钟寄存器是只写的,库函数也没有 RTC_GetAlarm 这样的函数,所以为了避免写进去就不知道是啥了,我们可以在写之前,给它存下来。就是这样,先定义一个变量
uint32_t Alarm = RTC_GetCounter() + 10;//它等于我们要写入的闹钟值
RTC_SetAlarm(Alarm);//之后,把这个 Alarm 变量写入进去。
最后显示的话,就直接显示这个变量就行了。所以是
OLED_ShowNum(2, 6, Alarm, 10);
那这样,我们闹钟设定和显示的代码就完成了。
之后,随着 CNT 的增大,CNT 会和 ALR 相等,然后触发闹钟标志位置 1,如果开启了闹钟中断,那还会进一步进入中断函数。在这个代码,我们暂时就不开中断了,直接显示一下闹钟标志位,看看闹钟响的时候,会不会置 1,所以在主循环里,在 3 行 6 列,我们不断刷新显示 RTC_GetFlagStatus,获取标志位,标志位是 RTC_FLAG_ALR,闹钟标志位,显示长度为 1。
OLED_ShowNum(3, 6, RTC_GetFlagStatus(RTC_FLAG_ALR), 1);//闹钟标志位 的值
这样,闹钟标志位我们就能看到了。然后主循环后面,我们我们也还是加一个 Running 的指示看一下。
OLED_ShowString(4, 1, "Running");
Delay_ms(100);
OLED_ShowString(4, 1, " ");//再把 Running 清除
Delay_ms(100);
那现在,我们看一下程序现象。编译下载看一下,目前看到第一行的 CNT 不断自增,第二行的 ALR 是复位后 10s,第三行是闹钟标志位,然后等一下,CNT = ALR,ALRF 置 1,并且发现,它是 CNT = ALR 的最后时刻,ALRF 才置 1。
好这样,我们的闹钟测试是没问题的,回到代码,我们现在就可以加入待机模式来测试了。要想进入待机模式呢?我们需要使用 PWR 外设,使用 PWR 外设之前,还是别忘了开启时钟。虽然在这个代码里,RTC 初始化里面,已经开启过了 PWR 的时钟,但是我们使用待机模式的话,最好还是再开一次,保持每部分代码的独立性。所以可以复制开启 PWR 的时钟这一条,放到主函数里,开启 PWR 的时钟。
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);//开启 PWR 的时钟
时钟已经开启了,再开一遍不会有任何问题;但是时钟没开启,外设就不会工作。如果你认为,在 RTC 初始化里面已经开过了,外面这条代码就省掉,那我们待机模式的代码就会对 RTC 代码具有依赖性,增大了代码之间的耦合性,没有 RTC,待机模式就不能正常工作;现在有 RTC,代码没问题,但是如果你之后改变程序,我不用 RTC 了,一旦把 RTC 的代码去掉,就很可能会引起很隐蔽的 bug,所以,该写的代码我们最好还是要写完整,这是这个注意事项。
之后,进入待机模式,还是同样的操作,我们在主循环的最后,调用函数,到 pwr.h 里找一下。显然调用这个 EnterSTANDBYMode 就行了,放到这里。
PWR_EnterSTANDBYMode();
参数没有。那这个函数干了什么活呢?我们转到定义看一下,可以看到,第一步是清除 WakeUp 标志位;第二步是选择 STANDBY 模式,实际上就是把 PDDS 置 1,PDDS 为 1,表示进入待机模式,对吧;第三步是 SLEEPDEEP 置 1,进入深度睡眠,然后 #if define 这个不用看;最后一步,调用 WFI 指令,进入待机模式。可以看到,对于待机模式,其实并没有区分 WFI 和 WFE,库函数这里也不能选择,统一都调用的是 WFI,这一点,也不难理解,WFI 和 WFE 的区别就是唤醒方式的不同。看一下低功耗模式,对于待机模式,它的唤醒条件是指定的 4 个信号,没有对中断唤醒和事件唤醒做出区分,所以我们就也不用区分了,直接理解为,调用 WFI 进入待机,然后这 4 个信号都能唤醒,就行了。
好,这里函数内部的功能我们就清楚了。接下来,我们进行测试,下载看一下,复位,可以看到复位后,Running 闪烁一次,进入待机,当然这时 CNT 也不会刷新了。然后等一等,过一会,闹钟触发,待机模式唤醒,CNT 和闹钟值刷新一下,Running 闪烁一次,这就是目前程序的现象。
经过实测,很多芯片唤醒后,ALRF 并不会置 1,不必纠结,能唤醒就没问题。
并且,每次唤醒之后,闹钟值都重新设定,通过这一现象,我们可以确定,待机模式唤醒后,程序是从头开始执行的,因为我们闹钟设定的代码在 while 循环之前,如果程序不是从头开始的,那闹钟值也不会更新。
然后在进入待机模式之后,高速时钟也都会关闭,在退出待机模式时,程序从头开始执行,在程序刚开始的时候,自动调用 SystemInit,初始化时钟,所以待机模式,我们就不用像停止模式那样,自己调用 SystemInit 了。
另外,在执行完这个函数进入待机模式之后,这个函数之后的代码,就再也执行不到了,因为待机模式退出,程序从头开始,执行到进入待机模式的函数,它又会待机,下次继续从头开始,所以,待机模式之后的代码执行不到,并且这个 while 循环,实际上也只有执行一遍的机会,把这个 while 循环去掉也是可以的。
那最后,待机模式对标的是极度省电,光 STM32 自己一个省电,那也不行。所以,一般在进入待机模式之前,我们都要尽可能的把外部挂的模块都关掉,你要说,我 STM32 待机了,外面还有个显示屏在显示,或者外面还有个电机在嗡嗡的转,外挂模块的耗电远超 STM32 本身,这时再给 STM32 待机,不管外部模块的话,那不就是捡了芝麻,丢了西瓜了嘛。所以,STM32 进入待机模式之前,一定要把外部接的模块,能停的都停掉,能断电的都断掉,这样才能最大化省电。这个功能需要精心设计硬件电路,可以选一个带有使能端的稳压器来实现,或者自己设计电路,把其他模块的供电加一个开关。那程序这里,就简单模拟一下了,在进入待机模式之前,来个 OLED_Clear,清屏,表示一下,当然为了看的清晰一点,再闪烁一个字符串吧,放在 4 行 9 列,显示个 STANDBY,显示停留时间给长点,来个 1s。
OLED_ShowString(4, 9, "STANDBY");
Delay_ms(1000);
OLED_ShowString(4, 9, " ");//再把 Running 清除
Delay_ms(100);
OLED_Clear();
这样看一下,复位,下载,松手,看一下,复位一下,最开始显示一些信息,进入待机,OLED 清屏,等待一会,闹钟触发之后,唤醒一次,这就是目前程序的现象。
那之后,我们还可以测试一下 WKUP 引脚唤醒的功能。这个其实非常简单,我们看一下库函数,调用这个 PWR_WakeUpPinCmd,放到初始化这里,参数给个 ENABLE,就这一条代码就行了。
PWR_WakeUpPinCmd(ENABLE);
那你可能会问,目前使用到了 GPIO 引脚,需不需要 GPIO 初始化呀,这个是不需要的,看一下手册,PWR 的寄存器描述,这里写了,使能 WKUP 引脚后,WKUP 引脚被强置为输入下拉的配置,所以不用再 GPIO 初始化了。
好,这样,我们试一下,WKUP 的上升沿能不能唤醒待机。复位,下载,松手,看一下,WKUP 引脚默认是下拉的,引脚悬空,就是低电平,然后我们把这个线插到高电平,试一下,可以看到,WKUP 接高电平的时刻,待机模式被唤醒了,当然,WKUP 引脚也不局限于手动给电平,外部的模块,或者别的传感器信号,也都可以接过来,用于唤醒 STM32,这就是 WKUP 引脚的唤醒功能。
好,到这里,我们本节的代码,就全部演示完成了。
整体代码:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyRTC.h"
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
MyRTC_Init(); //RTC初始化
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //开启PWR的时钟
//停止模式和待机模式一定要记得开启
/*显示静态字符串*/
OLED_ShowString(1, 1, "CNT :");
OLED_ShowString(2, 1, "ALR :");
OLED_ShowString(3, 1, "ALRF:");
/*使能WKUP引脚*/
PWR_WakeUpPinCmd(ENABLE); //使能位于PA0的WKUP引脚,WKUP引脚上升沿唤醒待机模式
/*设定闹钟*/
uint32_t Alarm = RTC_GetCounter() + 10; //闹钟为唤醒后当前时间的后10s
RTC_SetAlarm(Alarm); //写入闹钟值到RTC的ALR寄存器
OLED_ShowNum(2, 6, Alarm, 10); //显示闹钟值
while (1)
{
OLED_ShowNum(1, 6, RTC_GetCounter(), 10); //显示32位的秒计数器
OLED_ShowNum(3, 6, RTC_GetFlagStatus(RTC_FLAG_ALR), 1); //显示闹钟标志位
OLED_ShowString(4, 1, "Running"); //OLED闪烁Running,指示当前主循环正在运行
Delay_ms(100);
OLED_ShowString(4, 1, " ");
Delay_ms(100);
OLED_ShowString(4, 9, "STANDBY"); //OLED闪烁STANDBY,指示即将进入待机模式
Delay_ms(1000);
OLED_ShowString(4, 9, " ");
Delay_ms(100);
OLED_Clear(); //OLED清屏,模拟关闭外部所有的耗电设备,以达到极度省电
PWR_EnterSTANDBYMode(); //STM32进入停止模式,并等待指定的唤醒事件(WKUP上升沿或RTC闹钟)
/*待机模式唤醒后,程序会重头开始运行*/
}
}
最后的最后,我还进行了一个小实验,就是验证一下待机模式,到底是不是像手册里说的那样省电,手册里说待机模式的电流,只有 3μA 左右,这个是不是真的呢?为此进行了测试,这里,单独找了个板子,把这个电源供电线正极给剪断,串联一个万用表测电流。当然,最开始直接测试,待机模式的电流,高达 1点多 mA,远超手册里说的 3μA;那首先很明显,这个电源指示灯肯定是耗电的,所以先把这个电源指示灯去掉了,再测试,电流仍然有几百 μA,那说明还有别的东西耗电;之后,就把板子背面的这个 LDO 稳压器去掉了,这个 LDO 虽然我们并没有用到,但是接上它,就会有电流损耗;最后,去掉电源指示灯和 LDO 之后,待机模式的电流就下降到 3μA 了。
接下来这里给大家演示一下最后现象,大家自己就不用再拆板子了。首先,我们改一下程序,在程序中,为了方便测试,我们把这个时间延长一点,STANDBY 显示时间给个 10s,待机闹钟,设置大点,100s,这样测试一下。
我们先把这个板子下载一下程序,先换到下载程序的 STLINK。然后,复位,下载,松手,之后再把这个板子换到测试电路上实验,接上供电,看一下。首先万用表拨到 200mA 电流档,可以看到,最开始是正常工作的电流,目前显示是 20多 mA,复位一下看看,正常工作就是 20多 mA;之后等一会,等它进入待机,好,这时,可以看到供电电流瞬间变为 0 了,换到更小的 200μA 档,可以看到,目前待机的供电电流是 3.3,单位是 μA,这就和手册上的数据,基本是一致的了。
这样,我们从硬件上也验证了待机模式代码的正确性。另外,从这个实验中我们也可以看出,使用待机模式省电,一定要想方设法把外部能关的一切耗电电路都关掉,否则,你的待机模式就无法做到真正的极度省电,这就是这个实验。
好,那到这里,我们本节的内容也就全部结束了。