WDG 看门狗-stm32入门

这一节,我们要学习的内容是 STM32 里的看门狗。STM32 内置两个看门狗,分别是独立看门狗(IWDG)和窗口看门狗(WWDG),两者的作用基本相同,只是侧重点不一样。

那看门狗,简单来说,就是程序运行的一个保障措施,我们得在程序中定期的喂狗,如果程序出问题卡死了,没有在规定的时间里喂狗,那么看门狗硬件电路就会自动帮我们复位一下,防止程序长时间卡死。这就像是我们写了个程序,然后突然没动静了,我们就会习惯性的去按一下复位;或者说手机、电脑,卡死不动了,我们也会习惯性的重启来解决。那看门狗就是完成这样一个操作的硬件电路,在程序卡死的情况下,自动帮我们复位一下。

好,然后我们还是先看一下本节程序的现象。本节一共两个程序:独立看门狗和窗口看门狗。

先看一下独立看门狗的程序,简单看一下程序内容,程序进来,首先执行判断,看一下这个程序是因为看门狗复位而从头执行的,还是因为刚上电或者按复位键而从头执行的,如果是看门狗复位,OLED 第二行显示独立看门狗 RST 的字符串;如果是正常复位,OLED 第三行显示正常 RST 的字符串。之后是独立看门狗初始化,设定的最大喂狗时间是 1000 ms,这样,我们在主循环里就得不断的调用喂狗函数执行喂狗,且喂狗的间隔不能超过上面配置的 1000 ms,目前喂狗的间隔是 200+600 = 800ms,这样主循环不断运行起来,独立看门狗就不会执行复位。最后在主循环开始的这个位置,加入了一个按键获取键码,在这个函数里面,我们采用的是阻塞式写法,也就是按键按下不放的话,程序就会卡死在这个函数,所以,我们把这个函数放在这里,按下按键不放,模拟程序卡死的状况,返回值,按键键码暂时不需要,这样,喂狗程序无法及时执行,独立看门狗就会复位,让程序从头开始执行。那我们下载看一下现象,复位一下,这是正常复位,所以第三行显示正常复位的 RST 字符串,之后,主循环不断执行,并在第四行显示 FEED,表示喂狗,然后我们按上面的按键,按住不放,这时主循环卡死,第二行就会显示独立看门狗 RST,这个复位就是独立看门狗产生的了,那这就是独立看门狗的程序现象。
简单来说,就是如果不及时喂狗,程序就会复位。

接着我们来看第二个程序,窗口看门狗。这个窗口看门狗和独立看门狗类似,都是不喂狗,就复位,但是窗口看门狗对喂狗时间要求更严格一些,它有个喂狗时间的窗口,必须在这个时间窗口内喂狗,喂狗晚了,复位,喂狗早了,也复位。看一下,程序进来,首先还是判断复位来源,是窗口看门狗导致的复位,还是正常上电和复位键的复位;之后,窗口看门狗初始化,这里设置的喂狗时间窗口时 30~50ms,喂狗的间隔必须在 30~50ms 之间,喂早喂晚都不行,然后看主循环里。WWDG_SetCounter 这一条是喂狗程序,上面 Delay 的时间间隔是 20+20 = 40 ms,目前的间隔,位于时间窗口内;然后获取键码,还是模拟程序卡死。这个程序,下载看一下,首先还是正常复位,第三行显示正常的 RST,之后主循环,第四行一直闪烁 FEED,表示喂狗,然后按上面的按键,程序卡死,没有及时喂狗,这样第二行就会显示,窗口看门狗产生复位了,当然,按按键只能模拟晚喂狗,早喂狗,也会复位,这个现象,我们写程序的时候再来演示吧。

好,程序现象就演示到这里。看完这两个代码的演示,相信大家对看门狗就已经了解的大差不差了吧。其实看门狗也不是很复杂,就是个自动复位电路。

那接下来先看一下看门狗的简介。

1. WDG 看门狗

1.1 WDG 简介

WDG(Watchdog)看门狗

它的作用,顾名思义,其实就是“看大门”,不过这里的大门,表示的是程序。

看门狗可以监控程序的运行状态,当程序因为设计漏洞、硬件故障、电磁干扰等原因,出现卡死或跑飞现象时,看门狗能及时复位程序,避免程序陷入长时间的罢工状态,保证系统的可靠性和安全性。

那写过程序代码的都知道,程序的设计是非常讲究逻辑的。每一种状态,每一种可能,都要在写程序的时候预先注意到,否则,一旦出现了程序没有预料到的情况,程序经常就会出现卡死、跑飞、胡乱运行的状况。这一点,在我们当前这些简单的测试程序中还很少出现,因为这些程序其实并不复杂,任务也很简单;但是,对于一些大型项目,各种状态和可能,都是非常非常多的,在写程序的时候,一不小心,就会留下 bug;或者说,在大型项目中,bug 根本无法避免,因为可能的状态太多了,总有那么些意想不到的情况发生,让你的程序卡死或崩溃。那解决办法呢?

  1. 第一,就是我们程序员要有丰富的经验,避免一些常见的 bug;
  2. 第二,就是程序要经常迭代,发现 bug 后及时修补;
  3. 第三,就是我们的看门狗了,出现卡死崩溃现象后,帮我们按一下复位,虽然说不能解决 bug 本身吧,但是也可以极大的提高程序的健壮性,因为很多 bug 都是偶然发生的,简单的复位一下,就会有很大概率,让程序走向正轨。

那在这里写的除了程序本身的设计漏洞呢?还会有硬件故障、电磁干扰等原因,让程序出问题。比如硬件故障,我们想读取传感器的数据,结果传感器坏了,总是死等,那程序不就卡死了嘛;电磁干扰,这个一般出现在恶劣的环境中,我们都知道,很强的电磁干扰,可能会让这些电子元器件失灵,也就是程序跑飞现象,程序不知道跑到哪个奇怪的地方了。那假设你的设备遇到了一阵强电磁干扰,有看门狗的话,干扰过后,程序复位,回到正轨;没有看门狗的话,可能受到干扰,程序就永远地卡死在某个地方了,对吧。所以,出现状况时,我们需要看门狗及时复位程序,避免程序陷入长时间的罢工状态。
最后,再说两点,一个是对于硬件故障,如果是关键的设备故障,复位也没用的话,那看门狗也无法力挽狂澜,因为看门狗就是简单的复位一下;第二个是对于程序设计漏洞,这里主要针对的是无法预料的漏洞,而不是说,我有看门狗了,我就在程序中,写大量的死循环,或者不规划程序,到处跳来跳去,然后卡死了,就用看门狗来托底,这样恐怕不太好,合理规划程序是第一,然后可以预料的漏洞,尽量直接写好处理方法,最后,才是看门狗托底,这样才是比较好的思路。

看门狗本质上是一个定时器,当指定时间范围内,程序没有执行喂狗(重置计数器)操作时,看门狗硬件电路就自动产生复位信号

这里,说的就是看门狗的工作逻辑了。看门狗,其实就是定时器,看一下结构,它的结构和定时器是非常相似的,只不过是,定时器溢出,产生中断;而看门狗定时器溢出,直接产生复位信号。然后喂狗操作,其实也就是重置这个计数器,这是一个递减计数器,减到 0 之后就复位,那程序正常运行时,为了避免复位,就得在这个计数器减到 0 之前,及时把计数值加大点,对吧,这个操作,就是喂狗。如果你程序卡死了,没有及时加大这个计数器,那么减到 0 之后,就自动复位了,这就是看门狗的工作逻辑,是不是也不难。

STM32内置两个看门狗

  • 独立看门狗(IWDG):独立运行/工作,对时间精度要求较低

独立运行,就是独立看门狗的时钟是专用的 LSI,内部低速时钟,即使主时钟出现问题了,看门狗也能正常工作,这也是独立看门狗,独立的得名原因。对时间精度要求较低就是独立看门狗只有一个最晚时间界限,你喂狗间隔只要不超过这个最晚界限就行了,你说 我很快的喂,疯狂的喂,连续不断的喂,那都没问题。

  • 窗口看门狗(WWDG):要求看门狗在精确计时窗口起作用

它相比较独立看门狗,就严格一些了。要求看门狗在精确计时窗口起作用,意思就是喂狗的时间有个最晚的界限,也有个最早的界限,必须在这个界限的窗口内喂狗,这也是窗口看门狗,窗口的得名原因。因为对于独立看门狗来说,可能程序就卡死在喂狗的部分了;或者程序跑飞,但是喂狗代码也意外执行了;或者程序有时候很快喂狗,有时候又比较慢喂狗,那这些状态,独立看门狗就检测不到了,但是窗口看门狗是可以检测到这些问题的,因为它对喂狗的时间,可以卡的很死,快了,慢了,都不行。

最后,窗口看门狗使用的是 APB1 的时钟,它没有专用的时钟,所以不算是独立。好,这些,就是看门狗的基本介绍了。

接下来,先看一下独立看门狗。

1.2 独立看门狗

1.2.1 IWDG 框图

看一下框图。
在这里插入图片描述

这个框图可以类比定时器的时基单元来看。我们看一下定时器的时基单元,时基单元由预分频器、计数器和重装载寄存器组成,左边是输入时钟,比如是 72M,首先经过分频,比如 2 分频,那么计数器的驱动时钟就是 72/2 = 36M,之后计数器可以自增,也可以自减,看门狗使用的是自减运行,那自减到 0 后,定时器产生更新事件和中断,而看门狗是直接产生复位;另外,重装值,定时器是在更新事件重装,而看门狗需要我们在自减到 0 之前,手动重装,因为减到 0 就复位了,正常运行情况下,肯定是不能让它减到 0 的,那这个手动重装计数器的操作,就是喂狗。

看完了定时器,接着来看这个独立看门狗。左边是预分频器,下面是计数器,上面是重装寄存器,这基本就是一样的结构。那预分频器之前,输入时钟是 LSI,内部低速时钟,时钟频率为 40KHz,之后,时钟进入预分频器进行分频,这个预分频器只有 8 位,所以它最大只能进行 256 分频,上面这个预分频寄存器 IWDG_PR,可以配置分频系数,这个 PR 和定时器的 PSC 是一个意思,它们都是 Prescaler 的缩写,可能不是一个人设计的,所以这手册里很多缩写都不太一样,不过大家要知道,它们其实是一个意思。之后后面,经过预分频器分频之后,时钟驱动递减计数器,每来一个时钟,自减一个数,另外这个计数器是 12 位的,所以最大值是 212 - 1 = 4095,然后,当自减到 0 之后,产生 IWDG 复位,正常运行时,为了避免复位呢,我们可以提前在重装载寄存器写一个值,IWDG_RLR,和定时器的 ARR 是一样的,RLR 是 Reloader,ARR 是 Auto Reloader,那当我们预先写好值之后,在运行过程中,我们在这个键寄存器里,写一个特定数据,控制电路,进行喂狗,这时重装值就会复制到当前的计数器中,这样计数器就会回到重装值重新自减运行了,然后,有个状态寄存器 SR,这就是标志电路运行的状态了,其实这个 SR 里没什么东西,就只有两个更新同步位,基本不用看。最后,上面这些寄存器位于 1.8V 供电区,下面主要的工作电路,都位于 VDD 供电区,所以这下面写了,看门狗功能处于 VDD 供电区,即在停机和待机模式时仍能正常工作,上一节我们也说过,独立看门狗,也是唤醒待机模式的 4 个条件之 1。

那有关独立看门狗的框图,就看到这里。

1.2.2 IWDG键寄存器

接下来再介绍一下这个键寄存器,这个东西我们之前没见过,它有什么用呢?

键寄存器本质上是控制寄存器,用于控制硬件电路的工作。

比如我们刚才说的喂狗操作,就是通过在键寄存器写入 0xAAAA 完成的,那为什么要用键寄存器呢?我直接定义一个控制寄存器,其中再定义一个位,这一位写入 1,就喂狗,这样不也行嘛。我们继续看第二条

在可能存在干扰的情况下,一般通过在整个键寄存器写入特定值来代替控制寄存器写入一位的功能,以降低硬件电路受到干扰的概率

为什么能降低干扰呢。你看,独立看门狗工作的环境是什么?是程序可能跑飞,可能受到电磁干扰,程序做出任何操作都是有可能的,如果你只在寄存器中设置一个位,那这一位就有可能会在误操作中,变成 1 或者变成 0,这个概率是比较大的,所以单独设置 1 位就来执行控制,在这里比较危险。这时,我们就可以通过在整个寄存器写入一个特定值,来代替写入一个位的操作。

写入键寄存器的值作用
0xCCCC启用独立看门狗
0xAAAAIWDG_RLR中的值重新加载到计数器(喂狗)
0x5555解除 IWDG_PR 和 IWDG_RLR 的写保护
0x5555之外的其他值启用 IWDG_PR 和 IWDG_RLR 的写保护

比如,这里键寄存器是 16 位的,只有在键寄存器写入 0xAAAA,这个特定的数,才会执行喂狗的操作,这样就会降低误操作的概率,比如这时程序跑飞,胡乱地设置各个寄存器,键寄存器也受到了影响,它可能会变成 0000、FFFF、1234,等等等等,它可以随机变为任何数,但是它恰好变成 0xAAAA 这个数概率是非常小的。这就像是一个密码一样,只有输对密码,才能执行功能,所以瞎试,一般是试不出来的。另外,我们也可以看出来,这些密码都设置的很刁钻,CCCC 是 1100 1100 1100 1100,AAAA 是 1010 1010 1010 1010,5555 是 0101 0101 0101 0101,都是 1、0 交替混合的键值。

那看一下键值和作用,最后两条,是写保护的逻辑,意思就是执行指令,必须写入指定的键值,所以指令抗干扰能力是很强的。但是这里,还有 PR、SR 和 RLR 三个寄存器,它们也要有防止误操作的功能,SR 是只读的,这个不用保护;剩下的,对 PR 和 RLR 的写操作,可以设置一个写保护措施,然后只有在键寄存器写入 5555,才能解除写保护,一旦写入其他值,PR 和 RLR 再次被保护,这样 PR 和 RLR,就跟随键寄存器一起被保护了起来,防止误操作,这就是键寄存器设计的用途。

1.2.3 IWDG超时时间

接着看一下独立看门狗的超时时间,也就是定时器的溢出时间。

这里有个公式,超时时间:TIWDG = TLSI × PR预分频系数 × (RL + 1)
其中:TLSI = 1 / FLSI

这个公式,用的是时间计算,你用频率计算,也是一样的。超时频率,就等于 LIS 的频率/预分频/重装值,对应定时器的话,就是 72M/(PSC + 1)/(ARR + 1),这是异曲同工。那这里通过时间计算呢,就这里来理解,LSI 是输入时钟,40 KHz,FLSI 就是 40K,TLSI就是周期,等于 40K 分之一,计算器算一下,1/40K = 0.025ms,所以 TLSI 这里是 0.025ms,每隔 0.025ms,来一个输入时钟,之后输入时钟进行分频,相当于计数时间加倍,加多少倍呢?下面有个 PR 寄存器和分频系数的对应关系,这里并不是 PR 写几,就是 几+1 分频,它只有这几个固定的分频系数,比如 PR 写入 2,那就是 16 分频,对应公式里,计数时间就要乘 16;最后,RL,就是 RLR,计数目标,乘个 RL + 1,就是最终的超时时间了,比如 RL 给个 99,那这样式子里的各部分值都确定好了。计算一下,0.025 ms × 16 × 100,值大家自己算一下,这就是当前这些参数下的超时时间。那下面这个表里,各个参数的最短时间和最长时间,为什么是这些值,大家就知道了吧。比如第一行,PR 写入 0 时,预分频为 4,TLSI 是固定的 0.025ms,最短时间 RL 给 0,RL + 1 = 1,0.025 ms × 4 × 1 = 0.1ms,对应表里的这个 0.1ms,最长时间,RL 给最大 0xFFF,即 4095,RL + 1 = 4096,0.02 ms × 4 × 4096 = 409.6ms,对应表里的 409.6 ms,之后的都是类似的计算方法。比如 PR 给 1,预分频系数为 8,那整体上来看,最短时间和最长时间,都是上面的二倍;下面分频系数每次变为二倍,时间也都逐渐 2 倍递增。
在这里插入图片描述

这就是独立看门狗超时时间的计算。简单来说,和定时器定时时间的计算是一样的。好,那到这里,我们独立看门狗的内容就讲完了,其中包括硬件电路如何工作,如何通过键寄存器去控制电路运行,和如何写入 PR 和 RL 寄存器,来确定超时时间,这就是独立看门狗。

1.3 窗口看门狗

那接下来,我们就进入窗口看门狗的学习。

窗口看门狗,从功能上来说,和独立看门狗还是比较像的,大体上看,只是比独立看门狗多了个最早喂狗时间的限制。

但是等会学的时候你就会发现,这个窗口看门狗,无论是框图的设计,还是寄存器的分布和命名规则,或是程序的操作流程和独立看门狗,都不是一个思路,可能是两个看门狗侧重点不一样吧,当然我感觉应该还是因为这两个外设不是同一个人设计的,所以设计的思路有所不同,这个大家先有个准备。

1.3.1 WWDG 框图

在这里插入图片描述
那我们看一下框图,这是窗口看门狗的结构。左下角是时钟源部分,这个时钟源是 PCLK1;右边这个是预分频器,它这个预分频器名字又变了,叫 WDGTB,实际上和独立看门狗的 PR,定时器的 PSC 都是一个东西;上面这个是 6 位递减计数器 CNT,这个计数器是位于控制寄存器 CR 里的,计数器和控制寄存器合二为一了,然后窗口看门狗没有重装寄存器,那如何重装计数器进行喂狗呢,这个,我们直接在 CNT 写入数据就行了,想写多少就写多少;之后上面是窗口值,也就是喂狗的最早时间界限,就写到这里存起来;最后,左边就是输出信号的操作逻辑了,什么情况下会产生复位,就由这几个逻辑门来确定。

好,这是这个框图的大体结构。可以分为这几部分。

之后我们来详细看一下它的工作流程。

首先还是从左下角开始看,时钟来源是 PCLK1,也就是 APB1 的时钟,这个时钟默认是 36MHz,所以就是 36M 的时钟进来,进来之后,还是先经过一个预分频器进行分频,这个和独立看门狗的预分频器,定时器的预分频器,都是一个作用:就是灵活地调节后面计数器的时钟频率,同时预分频系数也是计算计数器溢出时间的重要参数。

那接着,分频之后的时钟,驱动计数器进行计数,这个计数器和独立看门狗一样,也是一个递减计数器,每来一个时钟,自减一次,不过这个计数器比较特殊,从图上看,这里写了 T6~T0,总共是 7 个位,但是下面却写的是 6 位递减计数器,这是为什么呢?那这其实是因为这个计数器只有 T5~T0 这 6 位是有效的计数值,最高位 T6,这里用来当作溢出标志位,T6 位等于 1 时,表示计数器没溢出,T6 位等于 0 时,表示计数器溢出,不过对于硬件电路来说,T6 位其实也是计数器的一部分,只不过 T6 位被单独拎出来,当作标志位了而已。

举个例子,比如这个计数器,初始值,我们给 111 1111,那么来一个计数脉冲,值减 1,变为 111 1110,再来一个,变为 111 1101,以此类推,不断自减,直到减为 100 0000,好,减到这个数时,是一个关键节点,此时包括 T6 位在内的数,是 100 0000,转为十六进制是 0x40,0x40 要特别记一下,等会还会用到。也就是说,此时,如果把 T6 位也当作计数器的一部分,那计数器的值实际上才减一半;但是,如果我们把 T6 位剥离出去,当作溢出标志位,低 6 位,当作计数器,那此时的状态就是标志位为 1,计数器为 00 0000,已经减到 0 了,再减一次,下一个值是什么呢?就是 011 1111,这时最高位 T6,由 1 变为 0,即代表计数器溢出,这时,最高位 T6,就会通过线路产生复位信号,这就是这个计数器的工作流程和溢出条件。
总结一下就是,如果你把 T6 位看作是计数器的一部分,那就是整个计数器,值减到 0x40 之后溢出;而如果你把 T6 位当成溢出标志位,低 6 位,当作计数器,那就是低 6 位的计数值减到 0 之后溢出。这一点,尤其要搞清楚,因为我们后面还会多次提到,并且它的描述,有时候是把 T6 位和计数器当作一个整体,有时候又是把 T6 位给拎出来的,所以这里一定要理解清楚,要不然等会就搞迷糊了。

好,计数器的部分我就介绍完了。接着看左边的复位信号输出部分,首先这个 WDGA,是窗口看门狗的激活位,也就是使能 WDGA 写入 1,启用窗口看门狗,使能位作用于这个与门,与门的这种连接方式,我们之前遇到很多次了,它的作用,就类似于一个开关,左边是控制信号,右边是输入,上边是输出,控制信号给 1,则输出等于输入,开关导通;控制信号给 0,则输出等于 0,与输入无关,开关断开。
那开关右边,就是复位信号的来源了,这里有两个来源,用或门连接,也就是两个来源任意一个,都可以复位。
其中下面这一路,来源于溢出标志位 T6,当计数器溢出时 T6 等于 0,然后输入进来,这里输入有个小圆圈,代表输入取反,所以 0 变为 1,或门有效,输出 1,当最后这个使能位给 1,开启看门狗后,这个溢出信号就直接通向复位了。所以,下面这一路的意思就是,T6 位一旦等于 0,就表示计数器溢出,就产生复位信号,那在程序正常运行状态下,我们必须始终保证 T6 位为 1,这样才能避免复位。好,至此,下面这一块实现的功能,和独立看门狗基本是一样的,如果不及时喂狗,6 位的计数器减到 0 后,就产生复位。
接下来,喂狗的时间的最早界限,由上面这一块来实现,首先,我们要计算一个最早界限的计数值,写入到这里的 W6~W0 中,这些值,写入之后是固定不变的,之后,一旦我们执行写入 CR 操作时,这个与门开关就会打开,写入 CR,其实就是写入计数器,也就是喂狗,在喂狗时,这个比较器开始工作,一旦它比较,我们当前的计数器 T6:0 > 窗口值 W6:0,比较结果就 = 1,这个 1,通过或门,也可以去申请复位,这就是喂狗最早时间窗口的实现流程,就是喂狗的时候,我把当前计数值和我预设的窗口值进行比较,如果发现你的狗余粮还非常充足,你喂的这么频繁,那肯定有问题啊,我就给你复位一下,不让你喂太早了,那这,就是窗口看门狗的全部内容了。

喂狗太晚,6 位计数器减到 0 了,复位;喂狗太早,计数器的值超过窗口值了,复位,这就是窗口看门狗。

那结构看完,我们继续看窗口看门狗的工作特性。

1.3.2 WWDG 工作特性

通过刚才的介绍,这些应该就很好理解了。

递减计数器 T[6:0] 的值小于 0x40 时,WWDG 产生复位

注意这里写的是 T[6:0],包含 T6 位,所以是值减到 0x40 之后,再减一次就复位。

递减计数器T[6:0]在窗口W[6:0]外被重新装载时,WWDG产生复位

这就是不能过早喂狗。

递减计数器 T[6:0] 等于 0x40 时可以产生早期唤醒中断(EWI),用于重装载计数器以避免 WWDG 复位

这是一个新的知识点。注意这里是等于 0x40 时,可以产生一个早期中断,上面是小于 0x40 时,产生复位,0x40,这时 T6 还是 1,还没有溢出,再减一个数,变为 0x3F 了,才是溢出,所以说,这里的意思就是,减到 0x40 时,产生中断,然后再减一个数,到 0x3F 时,产生复位,那这样,这个中断其实就是在溢出的前一刻发生的,所以,这个中断也可以称作“死前中断”,马上就要溢出复位了,再提醒一下你,要不要干点啥。所以在这个早期唤醒中断里,我们一般可以用来执行一些紧急操作,比如保存重要数据、关闭危险设备等等;或者,还有一种写法,就是虽然超时喂狗了,但是我可以在中断里执行一些代码进行解决;或者这个任务不是很危险,超时了我就只想做一些提示,不想让它复位了,这样的话,我们就可以在这个早期唤醒中断里,直接执行喂狗,阻止系统复位,然后提示一些信息就完事了,这样也是一种思路。

定期写入WWDG_CR寄存器(喂狗)以避免WWDG复位

在这里插入图片描述
最后,看一下下面这个工作示意图。纵轴是 T[6:0],包含 T6 位的 CNT,递减计数器,喂狗之后,在这个数值往下递减,如果这个计数值还很大,高于这个 W[6:0] 窗口值了,你就不能急着喂狗,也就是在这一块,是不允许刷新的;之后,当计数值小于窗口值了,进入到刷新窗口,可以喂狗,当然喂狗也不能太晚啊,要在减到 3F 之前喂,3F 这个值,就是 T6 位刚等于 0 的时刻,所以下面这里,T6 位,在这个时刻变为 0,同时复位信号也在这个时刻发送,这就是这个示意图展示的内容,我们就清楚了。

然后看下面:

1.3.3 WWDG 超时时间

我们看一下窗口看门狗的超时时间计算,这里有两个公式。
超时时间:
TWWDG = TPCLK1 × 4096 × WDGTB预分频系数 × (T[5:0] + 1)
其中:TPCLK1 = 1 / FPCLK1

就是喂狗的最晚时间。这个和刚才独立看门狗的基本一样。这里要多乘一个 4096,是因为这里 PCLK1 进来之后,其实是先执行了一个固定的 4096 分频,这里框图没画出来,实际上是有的,因为 36 M 的频率,还是太快了,先来个固定分频,给降一降,所以到这里,公式计算我们直接多乘一个 4096 就行了。然后 WDGTB 预分频系数,这里直接写的是分频系数,WDGTB 的值和分频系数之间的关系,看下面图里的这个公式,就是分频系数 = 2WDGTB,再对应这个表,当 WDGTB = 0 时,就是 1 分频;WDGTB = 1,就是 2 分频;WDGTB = 2,就是 4 分频;WDGTB = 3,就是 8 分频,分频系数只有这 4 种选择,如果在表里左边补上这么一列分频系数,那这个表是不是就和刚才独立看门狗这里是一样的了,这就是寄存器的值和分频系数的对应关系。最后再看计数器,这里写的是 T[5:0],这个一定要注意,刚才我们写的都是 T[6:0],包含 T6 位,而这里计算公式里,是 T[5:0],是不包含 T6 位的,这样的话,T[5:0],就是减到 0 后溢出,所以超时时间的公式,直接把 T[5:0] 拿来计算就行了。这是超时时间的公式。

窗口时间:
TWIN = TPCLK1 × 4096 × WDGTB预分频系数 × (T[5:0] - W[5:0])
其中:TPCLK1 = 1 / FPCLK1

就是喂狗的最早时间。这个公式手册里并没有给出,不过也总结出来了。这个和上面差不多,主要区别就是后面这里,上面这个超时时间,是计数器减到 0 的时间,而这个,是计数器减到窗口值的时间,所以我们拿 T[5:0] - W[5:0] 就可以算出窗口时间了,同时也要注意,这个 W[5:0] 也是不包含 W6 这一位的。然后 WWDG 框图这里,从这里可以看出,计数器等于窗口值时,就已经可以喂狗了,所以这里公式后面,就不用再 +1 了,这就是窗口时间的公式。

在这里插入图片描述
最后看一下上面这个图,其中超时时间的公式,和上面这个一样,然后下面这个表的时间,怎么得到的,和上面独立看门狗那里类似。比如 WDGTB = 0 时,分频系数为 1,代入公式,WDGTB预分频系数就是 1,然后 TPCLK1 是 1/36M,当计数器给最小值 0 时,得到这个最小超时时间;当计数器给最大值时,6 位的计数器,最大值是 26-1 = 63,这样计算,得到这个最大超时时间。用计算器演示一下,先 1/36000 得到周期是 2.77……e-5,单位是 ms,然后 × 4096 再 × 1,当 T = 0 时, T+1 = 1,再 × 1,得到最小超时时间就是这个 0.113 ms,和表里的一样;当 T = 63 时, T+1 = 64,再 × 64,得到最大超时时间就是这个 7.28 ms,和表里的一样,这就是表里数据的计算方法。然后下面都是同样的,也有规律,分频系数逐渐 2 倍递增,最小时间和最大时间,也都是 2 倍递增,这就是这个表里的超时时间。然后窗口时间,手册里并没有给表和数值,大家按照这个公式,套进去计算就行,一般我们先确定超时时间,确定 T 的值,然后再计算窗口时间,就很好算了。

好,这就窗口看门狗的全部内容。

最后,我们总结一下这两个看门狗的差别。

IWDG独立看门狗WWDG窗口看门狗
复位计数器减到0后计数器T[5:0]减到0后、过早重装计数器
中断早期唤醒中断
时钟源LSI(40KHz)PCLK1(36MHz)
预分频系数4、8、32、64、128、2561、2、4、8
计数器12位,比较多6位(有效计数),比较少
超时时间0.1ms~26214.4ms113us~58.25ms
喂狗方式在键寄存器写入喂狗指令,固定值 RLR 重装到计数器直接写入计数器,写多少重装多少
防误操作键寄存器和写保护,基本很难受到干扰无,并没有这个设计
用途独立工作,对时间精度要求较低要求看门狗在精确计时窗口起作用

独立看门狗,时钟又慢,分频又大,计数器位数又多,自然超时时间就大一些,是 0.1ms~26s 多。
窗口看门狗,时钟快,分频小,计数器位数少,所以超时时间比较小,是 113us~58.25ms

对于用途来说,大多数场景用独立看门狗就已经足够了,因为大部分情况下,我们想要的功能,就是简单的程序卡死或跑飞了,帮我复位一下就行,当你有更高的需求时,再考虑用窗口看门狗。

好,到这里,我们的内容就介绍完了。接下来,我们还是过一遍手册,本节的内容,主要涉及手册里的第 17 章,独立看门狗和第 18 章,窗口看门狗。依次看一下,独立看门狗,开始是一些简介、主要性能和功能描述,这些我们刚才都介绍过。然后有个硬件看门狗,如果用户在选择字节(就是选项字节)中启用了“硬件看门狗”功能,在系统上电复位后,看门狗会自动开始运行,意思就是上电自启动,可以防止你程序在开启看门狗之前就失效了,会更加安全一些,那这个配置,需要在选项字节里进行。之后寄存器保护,写入 5555 进行保护,防止误操作,之后是独立看门狗的框图和超时时间表,这个刚才详细分析过,然后这里有个注,意思就是这个内部的 40K 时钟,它其实并不是很精确,会在 30K 到 60K 之间变化,所以我们在计算这个超时时间时,不要卡的太死,要给这些变化多留一些余量。

最后,就是寄存器描述。键寄存器,这里是一些键值和它对应的指令,另外可以看到,这里只有启动看门狗的指令,没有关闭看门狗的,实际上无论是独立看门狗还是窗口看门狗,一旦启用,就无法关闭了,这样可以防止误操作给它又关了,更安全一些,既然养狗了,怎么会让你随便弃养呢,是吧,这是这个设计。然后,预分频寄存器、重装载寄存器、状态寄存器,这些看着都比较简单了吧,然后寄存器就完了。

另外可以注意到,这个看门狗的 CNT 计数器,它并没有提供给我们进行读写,所以看门狗运行的时候,内部计数器计到哪个值了,我们是不知道的,这个对于调试和观察实验现象来说,可能不太友好,不过影响也不大。

这就是独立看门狗的部分。

接下来看窗口看门狗。简介、主要特性,大家自己看,这个框图,我们也详细分析过。之后是一些介绍,这里也有一句话:在系统复位后,看门狗总是处于关闭状态,设置 WWDG_CR 寄存器的 WDGA 位能够开启看门狗,随后它不能再被关闭,除非发生复位,窗口看门狗也是开了就不能随便关了,除非复位。然后下面递减计数器,递减计数器处于自由运行状态,即使看门狗被禁止,递减计数器仍继续递减计数,当看门狗被启用时,T6 位必须被设置,以防止立即产生一个复位,也就是我们在开启的时候,一定要首次给个重装值,并且 T6 位给 1,以防止开的时候,立刻就复位了。然后下面还有一些描述,大家自己看,之后是窗口看门狗框图、超时时间表和计算公式,我们也都介绍过。

最后,就是寄存器描述。第一个是控制寄存器,WDGA 是激活位,后面 T[6:0],它这里写的是 7 位计数器,但实际上 T6 位是标志位,低 6 位才是有效计数值。然后为什么这个计数器非要和控制寄存器挤在一起呢?可能是为了让开启看门狗和重装寄存器能同时进行吧。之后配置寄存器,里面是中断配置、预分频配置和窗口值。最后是状态寄存器,然后就是寄存器总表了。

整体上来看,手册里对看门狗的描述都不是很多,大家可以再自己看一遍。好,本小节到这里就结束了,我们下一小节来学习代码部分。

2. 两个看门狗的功能案例

这一小节,我们来学习看门狗的代码部分。

2.1 独立看门狗

那还是先看一下接线图。

2.1.1 硬件电路

在这里插入图片描述
这两个看门狗代码的接线都非常简单。这里右下角接个 OLED,用于显示;然后上面 PB1 口,接一个按键,用于阻塞喂狗,这样就行了。

这就是接线图。然后看一下面包板,右下角是 OLED 显示屏,然后再在 PB1 口接个按键就行了。这就是硬件接线部分。

2.1.2 代码整体框架

那接下来,我们就在 OLED 显示屏工程的基础上来测试独立看门狗的功能。

对于看门狗的部分,代码也不是很多,所以我们也不单独建模块了,直接在主函数里写就行。

根据我们上一小节的知识点,我们总结一下独立看门狗的配置流程。首先,看一下 IWDG 框图,从左到右。

  1. 第一步,应该就是开启时钟了吧。只有这个 LSI 时钟开启了,独立看门狗才能运行,所以初始化独立看门狗之前,LSI 必须得开启。但是,这个开启 LSI 的代码并不需要我们来写,为什么呢?我们看一下手册,在第 6 章,6.2 时钟,6.2.9 看门狗时钟,这个地方隐藏比较深,在这里,有一句话,写的是,如果独立看门狗已经由硬件选项或软件启动,LSI 振荡器将被强制在打开状态,并且不能被关闭,在 LSI 振荡器稳定后,时钟供应给 IWDG。这个意思就是说,如果我们开启了独立看门狗,那么 LSI 就会跟随强制打开,等 LSI 稳定后,就可以自动为独立看门狗提供时钟了。所以,我们这里的第一步,开启 LSI 的时钟,就不需要我们再写代码来执行了。那接着,我们直接进入下一步。继续往右
  2. 下一步,就是写入预分频器和重装寄存器了。当然,在写入这两个寄存器之前,不要忘了这两个寄存器的写保护,要先写入 0x5555 这个键值,解除写保护,然后再写入预分频和重装值。所以,这里的第二步,就是解除写保护。
  3. 随后第三步,是写入预分频和重装值。预分频和重装值,具体写入多少呢?我们可以通过超时时间公式来计算。
  4. 最后,当这些配置工作做完之后我们就可以执行 0xCCCC 这一条指令,来启动独立看门狗了。然后在主循环里,我们可以不断执行 0xAAAA 这一条指令来进行喂狗,这就是独立看门狗的配置流程。

接下来,我们回到代码,看一下我们这些操作都对应库函数里的哪些函数吧。那我们找到 iwdg.h,看一下最后面的函数声明。独立看门狗,总共有这么些函数,还是非常简单的。

void IWDG_WriteAccessCmd(uint16_t IWDG_WriteAccess);//写使能控制。可以转到定义看一下,可以看到,写使能的操作,就是在键寄存器,写入 IWDG_WriteAccess 这个参数,这个参数,可以是 Enable 或 Disable,再转到定义,可以看到 Enable 就是 0x5555,Disable 就是 0x0000,这一点,就对应我们 IWDG 键寄存器里的键寄存器写入 5555,就是解除写保护,写使能;写入其他值,比如 0000,就是启用写保护,写失能,所以这里第一个函数,我们就清楚了。
void IWDG_SetPrescaler(uint8_t IWDG_Prescaler);//写预分频器
void IWDG_SetReload(uint16_t Reload);//写重装值。
//这两个也非常简单,转到定义看一下,写预分频器就是写 PR 寄存器;写重装值就是写入 RLR 寄存器,对应 IWDG 框图就是写入预分频和重装载这两个寄存器。
void IWDG_ReloadCounter(void);//重新装载寄存器,就是喂狗。转到定义,它的操作就是,在键寄存器写入 KR_KEY_Reload 这个值,这个值,转到定义,可以看到,就是 0xAAAA,正好,也是对应我们 IWDG 键寄存器里的喂狗的操作
void IWDG_Enable(void);//启用独立看门狗。这个函数执行了什么操作,想必大家不用看就已经知道了吧。转到定义看一下,它就是在键寄存器写入 KR_KEY_Enable 这个值,这个值,就是 0xCCCC,对应我们 IWDG 键寄存器里的启用独立看门狗的操作
FlagStatus IWDG_GetFlagStatus(uint16_t IWDG_FLAG);//获取标志位状态。
  • void IWDG_WriteAccessCmd(uint16_t IWDG_WriteAccess); 写使能控制。可以转到定义看一下,可以看到,写使能的操作,就是在键寄存器,写入 IWDG_WriteAccess 这个参数,这个参数,可以是 Enable 或 Disable,再转到定义,可以看到 Enable 就是 0x5555,Disable 就是 0x0000,这一点,就对应我们 IWDG 键寄存器里的键寄存器写入 5555,就是解除写保护,写使能;写入其他值,比如 0000,就是启用写保护,写失能,所以这里第一个函数,我们就清楚了。
  • void IWDG_SetPrescaler(uint8_t IWDG_Prescaler); 写预分频器
    void IWDG_SetReload(uint16_t Reload); 写重装值
    这两个也非常简单,转到定义看一下,写预分频器就是写 PR 寄存器;写重装值就是写入 RLR 寄存器,对应 IWDG 框图就是写入预分频和重装载这两个寄存器
  • void IWDG_ReloadCounter(void); 重新装载寄存器,就是喂狗。转到定义,它的操作就是,在键寄存器写入 KR_KEY_Reload 这个值,这个值,转到定义,可以看到,就是 0xAAAA,正好,也是对应我们 IWDG 键寄存器里的喂狗的操作
  • void IWDG_Enable(void); 启用独立看门狗。这个函数执行了什么操作,想必大家不用看就已经知道了吧。转到定义看一下,它就是在键寄存器写入 KR_KEY_Enable 这个值,这个值,就是 0xCCCC,对应我们 IWDG 键寄存器里的启用独立看门狗的操作

好,以上就是独立看门狗的全部函数。整体上来看,都是非常简单的吧。

然后除了这里的函数,我们还要看一个 RCC 里的函数。就是之前示例代码演示的,我们想知道,程序复位的时候,它是看门狗导致的复位,还是上电或复位键导致的复位,这个判断,可以通过 RCC 里的一个标志位来实现。那我们打开 rcc.h,看一下函数:

FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG);
void RCC_ClearFlag(void);

我们需要用到这个查看标志位的函数。然后查看完标志位,不要忘了再调用下面这个函数,清除一下。那转到查看标志位的函数的定义看一下,这个 RCC 标志位,可以查看哪些事件呢,看一下参数,RCC 标志位对应的事件主要分为两种,一种是 HSI、HSE 等等,这些时钟的 Ready,就是判断时钟是不是准备好了,这个功能,我们在 RTC 那节开启时钟的时候,是不是用过;接着另一种就是各种 Reset 标志位,其中 Pin Reset,就是按复位键复位的时候会置 1,POR/PDR 就是上一节介绍的上电复位和掉电复位,Software 是软件复位,Independent Watchdog 是独立看门狗复位,Window Watchdog 是窗口看门狗复位,最后一个是低功耗的复位。

那我们本节,主要会查看 RCC_FLAG_IWDGRSTRCC_FLAG_WWDGRST 这两个独立看门狗和窗口看门狗的复位标志位,以此来判断,这个复位到底是不是看门狗引起的,所以这个函数,大家先了解一下。

好,到这里,库函数我们就看完了,接下来开始写代码。

  1. 那在初始化独立看门狗之前,我们先把前面的准备工作完成以下。首先先来个
OLED_ShowString(1, 1, "IWDG TEST");

这条代码就是写着玩的,指示一下这是独立看门狗的测试程序。

  1. 然后我们使用一下 RCC 查看标志位的函数,区分一下独立看门狗的复位和普通的复位,方便观察现象。

这里我们可以调用刚才说的 RCC 获取标志位的函数,只有一个参数。转到定义看一下,我们需要使用 RCC_FLAG_IWDGRST 这个参数,查看一下独立看门狗复位的标志位;返回值是 SET 或 RESET,所以我们套个 if,如果查看独立看门狗复位标志位 == SET,那就说明,本次复位是独立看门狗导致的;否则,那就说明,这只是一次普通的复位。

然后标志位置 1 之后,在 if 里面,别忘了再清除一下标志位,所以我们调用刚才说的 RCC_ClearFlag,参数没有。这里必须要清楚标志位,因为根据实测,这个标志位即使按下复位键,也不会自动清零。如果你置 1 之后,不给它清零,那下次,即使是正常的复位键复位,它也会判断为看门狗复位,这样实验现象就错了。

然后,接下来,我们就在 if 和 else 里,写一下看门狗复位和正常复位指定的代码。如果是看门狗的复位,我们闪烁一个字符串,这是独立看门狗复位时候的现象,就是在第 2 行,闪烁一下这个 IWDGRST 字符串;然后 else 里的代码,我们在第 3 行,闪烁一个 RST 字符串,这是正常复位时候的现象。

/*判断复位信号来源*/
if (RCC_GetFlagStatus(RCC_FLAG_IWDGRST) == SET)	//如果是独立看门狗复位
{
	OLED_ShowString(2, 1, "IWDGRST");			//OLED闪烁IWDGRST字符串
	Delay_ms(500);
	OLED_ShowString(2, 1, "       ");			//把这个字符串清除
	Delay_ms(100);								//当然最后这个 Delay 也可以不要,影响不大
		
	RCC_ClearFlag();							//清除标志位
}
else											//否则,即为其他复位
{
	OLED_ShowString(3, 1, "RST");				//OLED闪烁RST字符串
	Delay_ms(500);
	OLED_ShowString(3, 1, "   ");				//把这个字符串清除
	Delay_ms(100);
}

好,现在,先看一下目前的实验现象。编译下载看一下,目前 OLED 显示屏里第一行显示 IWDG TEST;按下复位,第 3 行显示正常的 RST,这是目前的现象。

然后回到代码,我们继续来写。接下来,我们就要初始化独立看门狗了。

  1. 开启 LSI 的时钟。

这个刚才说过,不需要我们手动开启,代码也不用写。

  1. 解除写保护。

显然,我们需要调用这个函数:

IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);

参数,转到定义,我们需要使用 IWDG_WriteAccess_Enable,写使能,就是解除写保护。这样第二步就完成了。

  1. 配置预分频和重装值。

配置预分频和重装值,我们需要调用这两个函数:

IWDG_SetPrescaler(IWDG_Prescaler_16);//写预分频器
IWDG_SetReload(2499);//写重装值。

好,现在我们来计算一下,这两个参数的值。看一下 IWDG 超时时间的计算公式和表格,在计算之前,我们需要确定一下,我们想要设定的超时时间,这个超时时间具体是多少,需要根据你的项目要求来,如果你没有特别的要求,卡死后,过个一两秒再复位,也不影响的话,那这个超时时间可以设置的稍微大一些。

比如现在设置,超时时间为 1000ms,也就是 1s,那就是要求,喂狗时间间隔,不能超过 1000ms,所以公式这里,是 1000ms,TLSI 是 1/40 KHz,等于 0.025ms。

然后,PR 和 RL,都是待定的数值,并且它们的值,可以有多种组合,并不是固定的,随着预分频系数的不同,RL 也对应的有不同的选择。

我们可以看一下下面这个表,目前,我们要设置 1000ms。可以看到,这里前两个预分频是不能选择的,因为这两个分频系数太小,以至于计数器计数的最长时间都达不到 1000ms,所以对于 1000ms 的超时时间,前两个预分频系数,不能选择。然后剩下的这些分频,都可以选择,因为它们计数的最短时间和最长时间,都包含了 1000ms。当然,在这些可选项中,我们应该优先选择预分频系数小的,这样可以最大化利用计数器的值来减小时间误差。因为有的时候,带入这个公式计算得到的 RL 值是一个小数,但 RL 只能给整数,所以四舍五入取整就会造成误差,这时,如果预分频小,时钟快,那么取整后造成的误差就比较小,这个误差大家可以自己带入几个值,算一下,就知道了。

所以目前对于 1000ms 的超时时间,这个 16 分频,就是最佳选择。当然如果你不介意这点误差,下面这些,也都是可以选择的,影响不大,毕竟我们这个独立看门狗,对时钟要求也不严格,而且,LSI 时钟,也会有很大误差的。那这里 PR 预分频系数,我们选择 16 分频,最后,就只剩一个 RL 了,简单算一下,RL 就等于 1000/0.025/16,再减 1 了,用计算器算一下,2499,就是要写入的重装值。当然这里计算结果直接是一个整数,不会因为取整造成误差。

那回到程序,这里的配置,我们就计算好了,预分频,转到定义,我们选择这个 16 分频;重装值,转到定义,这个值必须在 0~FFF 之间,即 0~4096,我们计算的结果,2499,处于这个范围,所以这里直接写 2499。这样超时时间就确定好了,是 1000ms。

接着进入下一步。

  1. 启动开门狗。

我们就可以直接调用这个函数,直接启动看门狗了。

IWDG_ReloadCounter();							//重装计数器,喂狗
IWDG_Enable();									//独立看门狗使能

当然,在启动之前,我们可以先喂一次狗,这样启动之后的第一个喂狗周期,就是 1000ms,这样严谨一些。

所以,在这里,我们先喂一次狗,参数没有,这样 CNT 的初始值,就是重装值 2499,那最后,我们再调用这个 Enable,启动看门狗。

这样,看门狗的初始化,就完成了。同时这里,喂狗或使能的时候,会在键寄存器写入 5555 之外的值,这时就又顺便给寄存器写保护了,所以写完寄存器之后,我们就不用再手动执行写保护了。好,这就是独立看门狗的初始化。

接下来,我们来测试喂狗的逻辑。首先复制喂狗的代码,在主循环里不断喂狗,喂狗的时间间隔,我们先 Delay 个 800ms,这样,看一下现象。

整体代码:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"

int main(void)
{
	/*模块初始化*/
	OLED_Init();						//OLED初始化
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "IWDG TEST");
	
	/*判断复位信号来源*/
	if (RCC_GetFlagStatus(RCC_FLAG_IWDGRST) == SET)	//如果是独立看门狗复位
	{
		OLED_ShowString(2, 1, "IWDGRST");			//OLED闪烁IWDGRST字符串
		Delay_ms(500);
		OLED_ShowString(2, 1, "       ");
		Delay_ms(100);
		
		RCC_ClearFlag();							//清除标志位
	}
	else											//否则,即为其他复位
	{
		OLED_ShowString(3, 1, "RST");				//OLED闪烁RST字符串
		Delay_ms(500);
		OLED_ShowString(3, 1, "   ");
		Delay_ms(100);
	}
	
	/*IWDG初始化*/
	IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);	//独立看门狗写使能
	IWDG_SetPrescaler(IWDG_Prescaler_16);			//设置预分频为16
	IWDG_SetReload(2499);							//设置重装值为2499,独立看门狗的超时时间为1000ms
	IWDG_ReloadCounter();							//重装计数器,喂狗
	IWDG_Enable();									//独立看门狗使能
	
	while (1)
	{
		IWDG_ReloadCounter();						//重装计数器,喂狗
		
		Delay_ms(800);
	}
}

编译下载看一下,按一下复位键,可以看到,除了第一次复位,之后,都不会再复位了,因为,目前我们的喂狗时间满足要求。

接着,修改程序,我们把喂狗时间改成 1200ms,再看一下,下载,可以看到,这时会不断的显示独立看门狗复位,因为,目前我们的喂狗时间不满足要求。那再改一下时间,我们看一下临界时间是不是我们设定的 1000ms,直接改成 1000ms,看一下,可以看到,对于 1000ms,它是不复位的;然后加大点,1010ms,试一下,可以看到,这样就不行了,所以喂狗的临界时间,大概就是 1000ms 左右,因为这个 LSI 可能有些误差,并且代码执行,也需要一些时间,所以如果你实测的时间和这个不太一样,这也是正常的,只要这个时间是在 1000ms 左右就行,我们一般需要多留一些时间余量,不会卡得这么死。

好,那程序现象,我们就验证完成了。接下来我们来完善一下最终的程序吧。

首先,我们把按键功能拿过来,在 mian.c 的最上面,包含 Key.h,初始化,来个 Key_Init();,在主循环里,我们可以直接调用 Key_GetNum();。按住按键不放,主循环就会阻塞,主循环阻塞,不能及时喂狗,独立看门狗就会复位。

然后下面再来个闪烁字符串,这样看着更清楚一点。Delay 的时间,先给 200ms,再给 600ms,加起来,主循环执行的时间,大概就是 800多ms,距离 1000ms 的超时时间,还有大概 200ms 的余量,这就是最终的程序代码。

整体代码:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"

int main(void)
{
	/*模块初始化*/
	OLED_Init();						//OLED初始化
	Key_Init();							//按键初始化
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "IWDG TEST");
	
	/*判断复位信号来源*/
	if (RCC_GetFlagStatus(RCC_FLAG_IWDGRST) == SET)	//如果是独立看门狗复位
	{
		OLED_ShowString(2, 1, "IWDGRST");			//OLED闪烁IWDGRST字符串
		Delay_ms(500);
		OLED_ShowString(2, 1, "       ");
		Delay_ms(100);
		
		RCC_ClearFlag();							//清除标志位
	}
	else											//否则,即为其他复位
	{
		OLED_ShowString(3, 1, "RST");				//OLED闪烁RST字符串
		Delay_ms(500);
		OLED_ShowString(3, 1, "   ");
		Delay_ms(100);
	}
	
	/*IWDG初始化*/
	IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);	//独立看门狗写使能
	IWDG_SetPrescaler(IWDG_Prescaler_16);			//设置预分频为16
	IWDG_SetReload(2499);							//设置重装值为2499,独立看门狗的超时时间为1000ms
	IWDG_ReloadCounter();							//重装计数器,喂狗
	IWDG_Enable();									//独立看门狗使能
	
	while (1)
	{
		Key_GetNum();								//调用阻塞式的按键扫描函数,模拟主循环卡死
		
		IWDG_ReloadCounter();						//重装计数器,喂狗
		
		OLED_ShowString(4, 1, "FEED");				//OLED闪烁FEED字符串
		Delay_ms(200);								//喂狗间隔为200+600=800ms
		OLED_ShowString(4, 1, "    ");				//再把 FEED 清除
		Delay_ms(600);
	}
}

我们测试一下,下载可以看到,正常情况下,OLED 第 4 行不断显示喂狗,程序不会复位。然后按住 PB1 的按键不放,程序阻塞,就会引起独立看门狗复位,这就是独立看门狗的程序现象。

好,第一个代码,到这里,我们就介绍完了。接下来,我们来学习下一个代码,窗口看门狗。

2.2 窗口看门狗

2.2.1 硬件电路

在这里插入图片描述
窗口看门狗的接线也是一样的。

2.2.2 代码整体框架

回到工程文件夹,我们复制一下独立看门狗的工程,在这个工程的基础上修改。

其实这两个看门狗的代码结构都差不多。我们直接在这里改一下。

首先,初始化这里,还是这个流程。显示的字符串,我们改成 WWDG TEST。

下面获取标志位的,也是一样。不过,标志位得改一下,转到定义看一下,这里选择 WWDG,窗口看门狗的复位标志位 RCC_FLAG_WWDGRST 替换这里的参数,下面显示的字符串也改一下。这样,复位的显示就改好了。

之后独立看门狗的初始化,全都删掉,待会儿在这里,我们对应写上窗口看门狗的初始化即可。

最后,主循环里的独立看门狗喂狗,删掉,待会儿写上窗口看门狗的喂狗。

好,那窗口看门狗的测试框架,我们就改好了。先编译一下,没问题。

接着,我们看一下初始化流程,先看一下窗口看门狗的框图。这里,因为窗口看门狗的时钟来源是 PCLK1,所以

  1. 我们需要开启窗口看门狗 APB1 的时钟

这个第一步,需要我们自己来执行,不会像独立看门狗那样自动开启。

  1. 之后,就是配置各种寄存器了

比如,预分频和窗口值,窗口看门狗没有写保护,所以第二步就可以直接写这些寄存器了。

  1. 最后,写入控制寄存器 CR

控制寄存器包含,看门狗使能位,计数器溢出标志位和计数器有效位,这些东西需要一起设置,放在第三步统一执行。之后,在运行过程中,我们不断向计数器写入想要的重装值,这样就可以进行喂狗了

这就是窗口看门狗的操作流程。那流程看完,我们再看一下库函数,找一下库函数,我们打开 wwdg.h,看一下函数定义,这些是窗口看门狗的库函数,看一下

void WWDG_DeInit(void);//恢复缺省配置
//初始化配置就用这两个函数。
void WWDG_SetPrescaler(uint32_t WWDG_Prescaler);//写入预分频器
void WWDG_SetWindowValue(uint8_t WindowValue);//写入窗口值
void WWDG_EnableIT(void);//使能中断。因为它只有一个中断,所以不需要指定参数了。
void WWDG_SetCounter(uint8_t Counter);//写入计数器,喂狗就用这个函数
void WWDG_Enable(uint8_t Counter);//使能窗口看门狗,初始化之后,启动看门狗就用这个函数。另外这个函数还有一个参数,并且和上面这个 SetCounter,参数是一样的。为什么这么设置呢?看一下手册的 18.3,这个上一小节的最后说过,因为递减计数器是自由运行状态,你在使能的时候,计数器可能是任何值,为了避免刚一使能,就立马复位,所以我们在使能的时候,需要顺便同时喂一下狗,那在库函数里的体现就是,使能函数里也有个参数,需要指定使能的时候,喂狗多少。
//然后最后两个就是获取标志位和清除标志位的函数了
FlagStatus WWDG_GetFlagStatus(void);
void WWDG_ClearFlag(void);

void WWDG_Enable(uint8_t Counter); 使能窗口看门狗,初始化之后,启动看门狗就用这个函数。

  • 另外这个函数还有一个参数,并且和上面这个 SetCounter,参数是一样的。为什么这么设置呢?看一下手册的 18.3,这个上一小节的最后说过,因为递减计数器是自由运行状态,你在使能的时候,计数器可能是任何值,为了避免刚一使能,就立马复位,所以我们在使能的时候,需要顺便同时喂一下狗,那在库函数里的体现就是,使能函数里也有个参数,需要指定使能的时候,喂狗多少。

这些就是窗口看门狗的库函数。接下来,我们就可以开始写代码了。

回到 main.c 里,我们来初始化窗口看门狗。

  1. 开启时钟。

这里需要调用:

RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE);	//开启WWDG的时钟

这样我们就开启了窗口看门狗的 PCK1 时钟。

  1. 接着,设置预分频和窗口值。

设置预分频和窗口值,我们需要调用这两个函数,参数待会儿再算:

WWDG_SetPrescaler(WWDG_Prescaler_8);			//设置预分频为8
WWDG_SetWindowValue(0x40 | 21);					//设置窗口值,窗口时间为30ms
  1. 最后,就是使能了。

我们调用这个函数,参数也放着:

WWDG_Enable(0x40 | 54);							//使能并第一次喂狗,超时时间为50ms
  1. 最后,主循环里,不断写入计数器,进行喂狗

我们调用这个函数,参数也放着:

WWDG_SetCounter(0x40 | 54);					//重装计数器,喂狗

好,流程就是这些。

现在我们有这几个待定的参数,这些参数决定了喂狗的最晚时间和最早时间,怎么计算呢?我们看一下 WWDG 超时时间,这里是计算公式,在计算之前,我们需要根据项目要求,确定想要设定的超时时间(最晚时间)和窗口时间(最早时间),这里测试的话,我就自己随便定了。

比如我想要设定的超时时间是 50ms,窗口时间是 30ms。那我们首先需要带入第一个公式,确定一下预分频和喂狗要给的计数器值,看一下下面的表,我想要设置的超时时间是 50ms,所以这里只能选择最后一个分频系数,因为只有最后一个的时间范围包含 50ms,前面 3 个都计不了这么久。那你说,我想设置 60ms 的超时时间怎么办呢?从这个表来看,窗口看门狗是计不到 60ms 这么久的,它最大只能计 58.25ms,除非你改变 PCLK1 的时钟频率,不过并不建议这样做。

那预分频我们就确定好了,WDGTB 选择 3,预分频系数就是 23 = 8,TPCLK1 是 1/36M,超时时间,我们设定为 50ms,这样算一下 T 就非常简单了吧,我们打开计算器,首先,1/36M,36M 只给 3 个 0,这样算得是 2.778e-5 这个结果,单位是 ms,我们记忆一下,然后, 50ms/记忆值/4096/8 = 54.93164,取整为 55,这里取整,就会有一点点误差了,那所以这里 T + 1 = 55,最终得到,T[5:0],应该设定为 54。

这样超时时间的参数,就确定下来了,预分频系数给 8,T[5:0] 给 54,对应的超时时间就是 50ms。

回到程序,预分频,转到定义,我们需要选择这个 8。之后的 T[5:0] 应该是重装值,但是窗口看门狗并没有重装寄存器,它是通过直接写入计数器来实现重装的,在使能的时候,我们需要首次写入重装值,之后喂狗的时候,我们也需要不断地写入重装值,所以我们把计算的 54,写入到 WWDG_Enable 和 WWDG_SetCounter 里来。当然现在还需要做一个步骤,就是这个 54,只是 T[5:0] 的值,看一下 WWDG 框图这里,T[5:0] 是 T5~T0,还有一个 T6 位,也是必须要设置为 1 的,所以在 54 的基础上,我们需要或上 0x40(| 0x40),也就是把控制寄存器次高位的 T6 位设置为 1。所以代码这里加上一个或 0x40,这里,用按位或,或者用加号,都可以,因为 54 这个值比较小,没有触及到次高位,所以这时或和加,效果是一样的。

另外,这里,控制寄存器还有一个最高位,WDGA 使能位,我们看一下这两个函数内部,对使能位,是怎么操作的呢。首先,使能函数,这个参数进来,或上 CR_WDGA_Set,这个 CR_WDGA_Set,可以看到,它就是 0x80,也就是把最高位设置为 1 了,所以这样,CR 最高位为 1,使能;之后看一下喂狗的 SetCounter, 参数进来后,先与上 BIT_Mask,再写入 CR 寄存器,BIT_Mask,可以看到,是 0x7F,也就是最高位清零,所以这里,写入的最高位是 0,最高位是使能位,喂狗的时候,会给使能位写 0,那这样,我们在喂狗的时候,会不会又同时把看门狗给关闭了呢?这个不用担心,因为上一小节的手册 18.3 这里,我们也说过,启动看门狗之后,看门狗是不能再被关闭的,所以即使喂狗的时候,最高位写入了 0,这个操作也是无效的。

同样,在代码这里,也有个注释,意思是,写入 T[6:0] 来配置计数器,不需要再做 “读-改-写” 这样的操作了。这个 “读-改-写” 的操作,在库函数里面经常出现,可以跟大家介绍一下,比如 WWDG_SetWindowValue 这里,就是一个典型的 “读-改-写” 操作:

  1. 第一步,先把寄存器读到临时变量里;
  2. 第二步,用 |=、&= 的操作,改变临时变量的指定几位;
  3. 第三步,把临时变量,写回到寄存器里。

这样做的好处就是:

  1. 可以单独改变寄存器的某几位而不影响其他位的值。
  2. 如果连续更改多次不同的位,这样的操作效率比较高。
  3. 所有更改的位在最终写回到寄存器时同时生效。

这就是 “读-改-写” 这个操作。然后看 WWDG_SetPrescaler 这里,是不是也是类似的操作,其他地方,大家可以自己再看看。

好,回到这里,这里不用再 “读-改-写” 了,因为,对最高位的 WDGA 写 0 没有任何作用,所以,你就可以直接写入计数值,不用考虑这个操作会影响到最高位了,这就是这两个函数,对控制寄存器的操作。

那回到主程序,我们继续计算这个窗口值,看一下 WWDG 超时时间这里的公式。计算窗口值之前,我们一定要先把超时时间算出来,算好超时时间,预分频系数和 T 就确定好了,下面再算窗口时间就是易如反掌了,那刚才我们算出,预分频系数为 8,T 为 54,TPCLK1 是 1/36M,窗口时间,我们想指定为 30ms,再算窗口值,那就简单了,所以,打开计算器,30ms/刚才保存的记忆值/4096/8 = 32.9,取整到 33,所以这一块,T-W = 33,W = T - 33 = 54 - 33 = 21,最终计算得到窗口值 W[5:0],需要给 21。

回到程序,WWDG_SetWindowValue 的参数这里,我们要给 21。同理,这个窗口值也是只有 W[5:0],低 6 位,W6 这一位,我们也需要或上 0x40,给它置 1。这样,窗口值就确定好了。

那到这里,我们的各个参数就配置完成了,目前的参数,超时时间,是 50ms,窗口时间,是 30ms,也就是我们主循环的喂狗周期,要在 30ms~50ms 的范围内。我们把这两个延时,都改成 20ms,目前延时加起来是 40ms,当然其他代码的执行,也需要花一些时间,目前应该是 40多ms,只要不超过 50ms 就行。

这就是目前的程序,不过,目前的程序这样写,其实是有问题的,大家能看出来问题在哪吗?我们先看一下实验现象,编译下载看一下,可以看到,目前一直都在触发窗口看门狗的复位,这时为什么呢?我们主循环一直都是 40多ms 的循环喂狗啊,是处于这个范围的,为什么会一直触发窗口看门狗复位呢?那么问题在哪里呢?答案就是 WWDG_EnableWWDG_SetCounter 这两条语句,它们离得太近了。你看,程序初始化,执行到 WWDG_Enable 这里时,使能看门狗,同时执行第一次喂狗,然后执行,执行,执行,执行到 WWDG_SetCounter 这里,很快就又执行了第二次喂狗,从WWDG_EnableWWDG_SetCounter 的时间间隔,肯定是小于 30ms 的,属于是喂狗太快了。所以,看门狗会立刻复位,下次执行到 WWDG_SetCounter 这里的时候,他又会复位,所以就陷入到了循环复位卡死的处境。解决方法呢?就是把喂狗的代码放在延时之后,就行了,第一次喂狗在WWDG_Enable 这里,先经过 40ms 的延时,再执行第二次喂狗 WWDG_SetCounter,这样就能避免第一次和第二次喂狗时间过短的问题了。

如果你觉得这个使能第一次喂狗和下面这里第二次喂狗中间的时间不好把握的话,也可以把使能的代码和下面的放在一起,并且加个 if 判断和标志位,判断,如果是第一次执行,就执行上面这条,使能顺便喂狗的程序;如果不是第一次执行,就执行下面这条,单独的喂狗程序,这样也行。

那我们先恢复成原来的代码,再测试看一下。

整体代码:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"

int main(void)
{
	/*模块初始化*/
	OLED_Init();						//OLED初始化
	Key_Init();							//按键初始化
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "WWDG TEST");
	
	/*判断复位信号来源*/
	if (RCC_GetFlagStatus(RCC_FLAG_WWDGRST) == SET)	//如果是窗口看门狗复位
	{
		OLED_ShowString(2, 1, "WWDGRST");			//OLED闪烁WWDGRST字符串
		Delay_ms(500);
		OLED_ShowString(2, 1, "       ");
		Delay_ms(100);
		
		RCC_ClearFlag();							//清除标志位
	}
	else											//否则,即为其他复位
	{
		OLED_ShowString(3, 1, "RST");				//OLED闪烁RST字符串
		Delay_ms(500);
		OLED_ShowString(3, 1, "   ");
		Delay_ms(100);
	}
	
	/*开启时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE);	//开启WWDG的时钟
	
	/*WWDG初始化*/
	WWDG_SetPrescaler(WWDG_Prescaler_8);			//设置预分频为8
	WWDG_SetWindowValue(0x40 | 21);					//设置窗口值,窗口时间为30ms
	WWDG_Enable(0x40 | 54);							//使能并第一次喂狗,超时时间为50ms
	
	while (1)
	{
		Key_GetNum();								//调用阻塞式的按键扫描函数,模拟主循环卡死
		
		OLED_ShowString(4, 1, "FEED");				//OLED闪烁FEED字符串
		Delay_ms(20);								//喂狗间隔为20+20=40ms
		OLED_ShowString(4, 1, "    ");
		Delay_ms(20);
		
		WWDG_SetCounter(0x40 | 54);					//重装计数器,喂狗
	}
}

下载,可以看到,现在,程序就没有复位了。并且,第四行,在很快地闪烁 FEED,表示喂狗;超时喂狗,会导致复位;然后过快喂狗呢,这里不太好模拟,我们直接改程序吧,目前是 40ms;当然 OLED 显示的函数其实也是比较耗时的,我们先注释掉,Delay 直接改成 30ms,这是最快的窗口值,下载试试看,现在在不断复位,说明 30ms 是过快喂狗了;我们加大到 31ms,再试试看,可以看到,它已经不复位了,说明 31ms 是没问题的,这就说明,我们最快喂狗的窗口值确实是 30ms 左右;然后测试一下 50ms 的超时时间呢,Delay 改成 50,试一下,可以看到,目前已经在复位了;然后改小点,49,再试一下,可以看到,目前就不复位了,这说明超时时间,确实是 50ms 左右。

通过实测,大概验证了我们计算参数的正确性,其他时间的参数,大家可以再自行计算,然后验证一下。好,目前窗口看门狗的程序,到这里也就介绍完了。

我们看门狗的知识点,到这里也就结束了。

  • 22
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值