1)实验平台:正点原子STM32MP157开发板 2)购买链接:https://item.taobao.com/item.htm?&id=629270721801 3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-318813-1-1.html 4)正点原子官方B站:https://space.bilibili.com/394620890 5)正点原子STM32MP157技术交流群:691905614
本章节我们来学习STM32MP1的窗口看门狗(WWDG)的使用方法,我们使用窗口看门狗的中断功能来喂狗,并通过LED灯来观察喂狗和复位情况。 本章将分为如下几个小节: 15.1、WWDG简介; 15.2、WWDG实验; 15.1 WWDG简介 15.1.1 STM32MP157看门狗
- 看门狗介绍 STM32MP157有3个看门狗,其中两个独立看门狗(IWDG1和IWDG2)给MPU使用,另一个窗口看门狗(WWDG1)给MCU使用。IWDG看门狗有独立时钟,由LSI驱动,即使主时钟发生故障它也能保持活动状态,而WWDG的时钟由APB1时钟分频后得到时钟驱动,最大为104.5MHz。
图15.1.1. 1 STM32MP157看门狗资源 2. 窗口看门狗框图 窗口看门狗(WWDG)跟独立看门狗(IWDG1)一样,是一个递减计数器。独立看门狗计数器递减为0的时候将产生复位,如果在递减到0之前进行喂狗则不会产生复位。窗口看门狗就不一样了,窗口看门狗递减到0X40的时候如果不喂狗,到下一个计数0X3F的时候就会产生复位,如果要喂狗,也要在一定的时间范围内喂狗才不会导致复位,我们用一个图来说明窗口看门狗的工作过程:
图15.1.1. 2 WWDG框图 从WWDG框图中我们可以知道,WWDG有一个输入时钟(时钟pclk),经过一个4096的分频器,再经过一个分频系数(2 WDGTB)可编程的预分频器以后才给一个7位递减计数器提供时钟,然后递减计数器开始工作。递减计数器按照这个时钟频率,从初始值往下逐一递减,每递减一次就用了一个固定的时钟周期,从而实现递减计时。我们来看看框图中的几个信号,如下表所示:
表15.1.1. 1 WWDG内部输入/输出信号 对框图的说明如下: ●pclk pclk时钟是由PCLK1提供的,最大可为104.5MHz,后面的分频系数(2WDGTB)由用户去配置,那么给WWDG时钟频率=PCLK1/(40962WDGTB)。 ●T[6:0] 控制寄存器的低7 位WWDG_CR[6:0]用于递减计数,也就是图中的计数器T[6:0],它的初始值用户可配置,可以配置这7位都为1,这个就是最大的初始值(0X7F)。递减计数器每计数一次的时间间隔=(40962WDGTB)/ Fpclk,如果要计数N次,则要用的时间为:N*(4096*2WDGTB)/ Fpclk。递减计数器最低可以递减到0X3F。 ●W[6:0] 配置寄存器的低7位WWDG_CFR[6:0]也就是图中的W[6:0],用于与控制寄存器的低7 位的计数器(T[6:0])的值进行比较,WWDG_CFR[6:0]的值也就是我们说的上限值,由用户设置,不能设置为0X40,最大可以设置等于递减计数器的初始值。 ●T6位 就是控制寄存器的第6位,即递减计数器T[6:0]的最高位,当递减计数器从0X40递减到0X3F的时候就产生复位,而第6位从1变成0。0X40就是窗口的下限值,此值是固定的。 我们用一张图来说明WWDG的工作过程:
图15.1.1. 3 WWDG工作示意图 上图中,窗口看门狗的递减计数器的初始值由用户配置,上限值用户通过设置WWDG_CFR的第06位的值来决定(A点),下限值是固定的,为0x40(B点)。寄存器WWDG_CR的第06位用来存储看门狗的计数器值,此值是可以更新的,在逐渐递减,如果在递减到A之前进行喂狗的话,系统就复位,如果递减到A和B之间进行喂狗的话,系统不会复位,如果递减到B还不喂狗,如果有使能了提前唤醒中断,则进入此中断,如果没有使能提前唤醒中断,当递减计数器到了下一个计数0X3F(即T6位为0这一刻)的时候系统就复位了。之所以形象地称为窗口,就是前面说的,其喂狗时间是一个有上下限的范围(窗口),喂狗的时间不能过早也不能过晚。 (1)STM32MP157的窗口看门狗在以下两种情况之一会产生复位: ①当喂狗的时候,如果递减计数器的值大于设定数值WWDG_CFR[6:0](上限值)时; ②当递减计数器的数值从 0x40 减到 0x3F 时(T6位跳变为0时)。 (2)这里有几点要注意的: ①喂狗是指对递减计数器进行清零(重新装载计数器的值,也叫刷新窗口),让计时器重新开始递减计数。 ②设置的窗口上限值必须大于下窗口值0x40(最大可以设为0X7F),否则窗口看门狗就没有窗口了。 ③独立看门狗没有中断,超时直接复位。而窗口看门狗有中断,例如窗口看门狗的提前唤醒中断(EWI),如果启动了窗口看门狗并且允许提前唤醒中断,当递减计数器等于0x40时则触发提前唤醒中断,此中断可以用于喂狗以避免复位或者做复位前的一些函数操作,例如备份重要的数据、告警提示等等。 ④一旦启用看门狗,除系统重置外,不能禁用WWDG。 (3)窗口看门狗的超时公式 知道了窗口看门狗的工作原理,下面学习如何计算窗口看门狗的超时公式。这里的超时(或者说溢出时间)是说:看门狗计数器从初始值递减到0X3F的时候经过了多长的时间,也就是WWDG计数器刷新时间范围(介于最大值和最小值之间)。
其中: TWWDG:WWDG超时时间(单位为ms); Fpclk1:APB1的时钟频率(单位为Khz); WDGTB[2:0]:WWDG的预分频系数,用户配置,可以选0~7,如果选为0,20=1,如果选为7,则27=128。 T[5:0]:窗口看门狗的计数器的低5位,由用户配置,最小为0x00,最大为0x3F,配置的越大,超时时间越长。 根据以上公式,假设Fpclk1=104.5Mhz,那么可以得到最小与最大超时时间,如下表所示:
表15.1.1. 2 104.5MHz时钟下WWDG的最小最大超时表 我们后面的实验参数设置为: PCLK1为104.5MHz;WDGTB=4;WWDG_CR[6:0]初始值=T[5:0]=0x3F;窗口值WWDG_CFR[6:0]= W[6:0]=0X57。即WDGTB分频系数为4,计时器初始值为128,窗口值为95。那么最大超时时间为40.14ms,计时器从128开始逐一递减,窗口值为95,则计算出窗口值对应的时间为:
在看门狗运行以后的第0ms20.70ms之间喂狗的话,系统就复位。在看门狗运行后的第20.70ms40.14ms之间喂狗的话,系统就不会复位,在40.14ms以后没有喂狗,系统就自动复位了。
图15.1.1. 4 WWDG工作示意图 3. 为什么要WWDG 窗口看门狗可以检查程序是否按预定逻辑运行,可以用于检测系统故障和检测软件错误。通过计算程序正常运行的时间来设置刷新看门狗的窗口阈值,这样可以保证在规定的时间范围内刷新看门狗。如果程序跑飞了,想要在规定的时间范围内喂狗是很困难的,程序跑飞导致没有及时喂狗,系统就复位了,我们就可以检测出程序出现异常了。 4. MCU的复位源 MCU的可能复位源有4个:系统复位、由MPU产生的复位、WWDG1复位(wwdg1_out_rst)、由MCU本身产生的复位。WWDG1以APB1时钟作为时钟,并提供复位和提前唤醒中断信号,此信号可以被MPU(GIC)和MCU(NVIC)中断控制器接收,发生超时(wwdg1_out_rst信号)时,WWDG1还会对MCU产生复位,如果WWDG1模块复位了MCU,该信号也被路由到EXTI,以唤醒MPU内核。
图15.1.1. 5 WWDG和MCU复位 15.1.2 WWDG寄存器 WWDG寄存器,我们重点介绍以下3个:
- 控制寄存器(WWDG_CR) 窗口看门狗的控制寄存器描述如下图所示:
图15.1.2. 1 WWDG_CR寄存器 该寄存器只有低八位有效,其中T[6:0]用来存储看门狗的计数器的值,随时更新的,每隔(4096×2^ WDGTB[2:0])PCLK个周期减1。当该计数器的值从0X40变为0X3F的时候,将产生看门狗复位。 WDGA位则是看门狗的激活位,该位由软件置1,启动看门狗,并且一定要注意的是该位一旦设置,就只能在硬件复位后才能清零了。 2. 配置寄存器(WWDG_CFR) 配置寄存器描述如下图所示:
图15.1.2. 2 WWDG_CFR寄存器 该寄存器中的EWI位是提前唤醒中断,如果该位置1,当递减计数器等于0x40时产生提前唤醒中断,我们就可以及时喂狗以避免WWDG复位,且该中断仅在复位后由硬件清除。因此,我们一般都会用该位来设置中断,当窗口看门狗的计数器值减到0X40的时候,如果该位设置,并开启了中断,则会产生中断,我们可以在中断里面向WWDG_CR重新写入计数器的值,来达到喂狗的目的。注意这里在进入中断后,必须在不大于1个窗口看门狗计数周期的时间(在pclk3频率为120M且WDGTB[2:0]为0的条件下,该时间为34.13us)内重新写WWDG_CR,否则,看门狗将产生复位! W[6:0] 为7 位窗口值,包含用于与递减计数器进行比较的窗口值。 WDGTB[2:0] 为定时器时基,可按如下方式修改预分频器的时基: 000:CK 计数器时钟 (PCLK div 4096) 分频器 1 001:CK 计数器时钟 (PCLK div 4096) 分频器 2 010:CK 计数器时钟 (PCLK div 4096) 分频器 4 011:CK 计数器时钟 (PCLK div 4096) 分频器 8 100:CK 计数器时钟 (PCLK div 4096) 分频器 16 101:CK 计数器时钟 (PCLK div 4096) 分频器 32 110:CK 计数器时钟 (PCLK div 4096) 分频器 64 111:CK 计数器时钟 (PCLK div 4096) 分频器 128 3. 状态寄存器(WWDG_SR)
图15.1.2. 3 WWDG_SR寄存器 该寄存器用来记录当前是否有提前唤醒的标志。该寄存器仅有位0有效,其他都是保留位。当计数器值达到0x40时,此位由硬件置1。它必须通过软件写0来清除。对此位写1无效。即使中断未被使能,在计数器的值达到0X40的时候,此位也会被置1。 4. 时钟使能寄存器 另外,要用WWDG的话,得使能WWDG的时钟,WWDG挂在APB1上,所以要配置寄存器RCC_MC_APB1ENSETR的第28位,将其置1来开启WWDG时钟,在后面的实验中,STM32CubeIDE生成的初始化代码会自动为我们做好这一步操作。
图15.1.2. 4 RCC_MC_APB1ENSETR寄存器 5. 其它寄存器 除了上面几个重要的寄存器以外,还有: WWDG硬件配置寄存器(WWDG_HWCFGR),用于配置看门狗时钟预分频为4096;WWDG版本注册寄存器(WWDG_VERR),用于记录IP版本主要修订信息;WWDG_SIDR和WWDG ID寄存器。这些寄存器我们用不到,可以不管。 15.1.3 WWDG的HAL库驱动 WWDG的HAL库驱动代码在stm32mp1xx_hal_wwdg.c和stm32mp1xx_hal_wwdg.h文件中。
- 结构体和句柄 (1)WWDG_InitTypeDef结构体 WWDG_InitTypeDef结构体在stm32mp1xx_hal_wwdg.h文件中有定义,主要就是用于配置WWDG的分频值、窗口值、递减计数器初始值以及是否开启提前唤醒中断。通过对此结构体成员赋值就可以完成WWDG这些参数的初始化配置。
typedef struct
{
uint32_t Prescaler; /* 指定WWDG的预分频器值 */
/* 指定要与递减计数器进行比较的WWDG窗口值,此窗口值必须在数字0x40和0x7F之间 */
uint32_t Window;
/* 指定WWDG递减计数器的初始值,此初始值必须是0x40和0x7F之间的数字 */
uint32_t Counter;
/* 指定是否使用提前唤醒中断 */
uint32_t EWIMode ;
} WWDG_InitTypeDef;
(2)WWDG_TypeDef WWDG_TypeDef结构体在stm32mp157axx_cm4.h文件中有定义,主要是定义WWDG寄存器的偏移地址。在前面我们也多次强调过,外设的寄存器都在stm32mp157axx_cm4.h文件中通过结构体封装好了。WWDG涉及的寄存器不多,如下:
typedef struct
{
__IO uint32_t CR; /* WWDG控制寄存器,地址偏移量:0x00 */
__IO uint32_t CFR; /* WWDG配置寄存器,地址偏移量:0x04 */
__IO uint32_t SR; /* WWDG状态寄存器,地址偏移量:0x08 */
uint32_t RESERVED1[249]; /* 保留,0x0C-0x3EC */
__IO uint32_t HWCFGR; /* WWDG硬件配置寄存器,地址偏移量:0x3F0 */
__IO uint32_t VERR; /* WWDG版本寄存器,地址偏移量:0x3F4 */
__IO uint32_t IPIDR; /* WWDG标识寄存器,地址偏移量:0x3F8 */
__IO uint32_t SIDR; /* WWDG大小ID寄存器,地址偏移量:0x3FC */
} WWDG_TypeDef;
(3)WWDG_HandleTypeDef句柄定义 WWDG_HandleTypeDef句柄在stm32mp1xx_hal_wwdg.h文件中有定义,HAL库中通过句柄操作来操作外设。
#if (USE_HAL_WWDG_REGISTER_CALLBACKS == 1)
typedef struct __WWDG_HandleTypeDef
#else
typedef struct
#endif
{
WWDG_TypeDef *Instance; /* 寄存器基地址 */
WWDG_InitTypeDef Init; /* WWDG所需参数 */
#if (USE_HAL_WWDG_REGISTER_CALLBACKS == 1)
/* WWDG提前唤醒中断回调函数 */
void (* EwiCallback)(struct __WWDG_HandleTypeDef *hwwdg);
/* WWDG Msp初始化回调函数 */
void (* MspInitCallback)(struct __WWDG_HandleTypeDef *hwwdg);
#endif
} WWDG_HandleTypeDef;
这里提一下USE_HAL_WWDG_REGISTER_CALLBACKS这个宏,此宏定义默认为0,在stm32mp1xx_hal_conf.h文件中有定义。
/* 这是可以使用寄存器回调的模块列表 */
#define USE_HAL_ADC_REGISTER_CALLBACKS 0u
#define USE_HAL_CEC_REGISTER_CALLBACKS 0u
#define USE_HAL_DAC_REGISTER_CALLBACKS 0u
#define USE_HAL_I2C_REGISTER_CALLBACKS 0u
#define USE_HAL_RNG_REGISTER_CALLBACKS 0u
#define USE_HAL_SPI_REGISTER_CALLBACKS 0u
#define USE_HAL_UART_REGISTER_CALLBACKS 0u
#define USE_HAL_USART_REGISTER_CALLBACKS 0u
#define USE_HAL_WWDG_REGISTER_CALLBACKS 0u
如果这些宏定义为0,则默认使用HAL库中的回调函数,HAL库中默认的回调函数基本上都是弱定义的,所以用户可以重新定义一个同名的回调函数。那如果这些宏定义为1的话,则用户可以自己注册回调函数,原来弱定义的回调函数就被用户注册的回调函数替代了。用户注册WWDG回调函数部分在stm32mp1xx_hal_wwdg.h文件中有,如下:
#if (USE_HAL_WWDG_REGISTER_CALLBACKS == 1)
/**
* @brief 注册用户WWDG回调函数,代替弱的预定义回调函数
* @param hwwdg:WWDG句柄
* @param CallbackID:要注册的回调的ID,此参数可以是下列值之一
* @arg @ref HAL_WWDG_EWI_CB_ID:提前唤醒中断回调ID
* @arg @ref HAL_WWDG_MSPINIT_CB_ID :MspInit 回调ID
* @param pCallback:指向回调函数的指针
* @retval 状态
*/
HAL_StatusTypeDef HAL_WWDG_RegisterCallback(WWDG_HandleTypeDef *hwwdg, HAL_WWDG_CallbackIDTypeDef CallbackID, pWWDG_CallbackTypeDef pCallback)
{
HAL_StatusTypeDef status = HAL_OK;
if(pCallback == NULL)
{
status = HAL_ERROR;
}
else
{
switch(CallbackID)
{
case HAL_WWDG_EWI_CB_ID:
hwwdg->EwiCallback = pCallback;
break;
case HAL_WWDG_MSPINIT_CB_ID:
hwwdg->MspInitCallback = pCallback;
break;
default:
status = HAL_ERROR;
break;
}
}
return status;
}
/**
* @brief 取消注册WWDG回调函数,WWDG回调被重定向到弱(收费)预定义的回调函数。
* @param hwwdg:WWDG句柄
* @param CallbackID:要注册的回调的ID,此参数可以是下列值之一
* @arg @ref HAL_WWDG_EWI_CB_ID:提前唤醒中断回调ID
* @arg @ref HAL_WWDG_MSPINIT_CB_ID :MspInit 回调ID
* @retval status
*/
HAL_StatusTypeDef HAL_WWDG_UnRegisterCallback(WWDG_HandleTypeDef *hwwdg, HAL_WWDG_CallbackIDTypeDef CallbackID)
{
HAL_StatusTypeDef status = HAL_OK;
switch(CallbackID)
{
case HAL_WWDG_EWI_CB_ID:
hwwdg->EwiCallback = HAL_WWDG_EarlyWakeupCallback;
break;
case HAL_WWDG_MSPINIT_CB_ID:
hwwdg->MspInitCallback = HAL_WWDG_MspInit;
break;
default:
status = HAL_ERROR;
break;
}
return status;
}
#endif
其中HAL_WWDG_RegisterCallback是用于注册用户指定的回调函数的代码,我们就关注两行红色的代码,pCallback是一个指针,指向用户自定义的回调函数。此函数的代码也就是通过CallbackID来注册或者说指定用户自定义的WWDG的提前唤醒中断回调函数以及MspInit 回调函数,HAL库里的弱定义的回调函数就不用了(或者说被用户指定的回调函数替代了)。
HAL_WWDG_UnRegisterCallback函数表示取消注册,我们也是看上面两行标红的代码,函数指针指向了HAL库里的回调函数,也就是使用HAL库里的回调函数,不使用用户自定义的回调函数。取消注册就是注销用户自定义的回调函数。
我们后面的实验就默认使用HAL库里弱定义的回调函数,大家想自定义回调函数也是可以的。下面我们来看看HAL库中的API函数。
- HAL库中的API函数 (1)HAL_WWDG_Init ●函数功能:HAL_WWDG_Init函数根据关联到句柄的WWDG_InitTypeDef中的参数来初始化WWDG。 ●函数参数: hwwdg:指向WWDG_HandleTypeDef结构的指针,该结构包含指定WWDG的配置信息。 ●函数返回值:枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时)
1 HAL_StatusTypeDef HAL_WWDG_Init(WWDG_HandleTypeDef *hwwdg)
2 {
3 /* 检查WWDG句柄分配 */
4 if (hwwdg == NULL)
5 {
6 return HAL_ERROR;
7 }
8 /* 检查参数 */
9 assert_param(IS_WWDG_ALL_INSTANCE(hwwdg->Instance));
10 assert_param(IS_WWDG_PRESCALER(hwwdg->Init.Prescaler));
11 assert_param(IS_WWDG_WINDOW(hwwdg->Init.Window));
12 assert_param(IS_WWDG_COUNTER(hwwdg->Init.Counter));
13 assert_param(IS_WWDG_EWI_MODE(hwwdg->Init.EWIMode));
14
15 #if (USE_HAL_WWDG_REGISTER_CALLBACKS == 1)
16 /* 重置回调指针 */
17 if(hwwdg->EwiCallback == NULL)
18 {
19 hwwdg->EwiCallback = HAL_WWDG_EarlyWakeupCallback;
20 }
21 if(hwwdg->MspInitCallback == NULL)
22 {
23 hwwdg->MspInitCallback = HAL_WWDG_MspInit;
24 }
25 /* 初始化底层硬件 */
26 hwwdg->MspInitCallback(hwwdg);
27 #else
28 /* 初始化底层硬件 */
29 HAL_WWDG_MspInit(hwwdg);
30 #endif
31 /* 设置WWDG计数器值 */
32 WRITE_REG(hwwdg->Instance->CR, (WWDG_CR_WDGA | hwwdg->Init.Counter));
33 /* 设置WWDG预分频器和窗口 */
34 WRITE_REG(hwwdg->Instance->CFR, (hwwdg->Init.EWIMode | hwwdg->Init.Prescaler | hwwdg->Init.Window));
35 /* 返回功能状态 */
36 return HAL_OK;
37 }
(2)HAL_WWDG_Refresh ●函数功能:刷新WWDG ●函数参数: hwwdg:指向WWDG_HandleTypeDef结构的指针,该结构包含指定WWDG的配置信息。 ●函数返回值:枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时) HAL_StatusTypeDef HAL_WWDG_Refresh(WWDG_HandleTypeDef hwwdg) { / 将WWDG计数器值写入WWDG CR寄存器以刷新 */ WRITE_REG(hwwdg->Instance->CR, (hwwdg->Init.Counter)); return HAL_OK; } HAL_WWDG_Refresh函数很简单,调用WRITE_REG函数,将句柄中设置的计数器的初始值赋给WWDG_CR寄存器的低6位,从而实现刷新计时器。 (3)HAL_WWDG_IRQHandler函数 ●函数功能:处理WWDG中断请求 ●函数参数: hwwdg:指向WWDG_HandleTypeDef结构的指针,该结构包含指定WWDG的配置信息。 ●函数返回值:枚举型,HAL_OK(成功)、HAL_ERROR(错误)、HAL_BUSY(串口忙碌)、HAL_TIMEOUT(超时) ●注意:如果开启了提前唤醒中断,当递减计数器达到值0x40时可以产生提前唤醒中断,如果特定的安全操作或必须在实际复位产生之前执行数据记录,则可以使用提前唤醒中断(EWI)
1 void HAL_WWDG_IRQHandler(WWDG_HandleTypeDef *hwwdg)
2 {
3 /* 检查是否启用了提前唤醒中断 */
4 if (__HAL_WWDG_GET_IT_SOURCE(hwwdg, WWDG_IT_EWI) != RESET)
5 {
6 /* 检查是否发生了WWDG提前唤醒中断 */
7 if (__HAL_WWDG_GET_FLAG(hwwdg, WWDG_FLAG_EWIF) != RESET)
8 {
9 /* 清除WWDG提前唤醒标志 */
10 __HAL_WWDG_CLEAR_FLAG(hwwdg, WWDG_FLAG_EWIF);
11
12 #if (USE_HAL_WWDG_REGISTER_CALLBACKS == 1)
13 /* 提前唤醒注册回调函数 */
14 hwwdg->EwiCallback(hwwdg);
15 #else
16 /* 提前唤醒回调函数 */
17 HAL_WWDG_EarlyWakeupCallback(hwwdg);
18 #endif
19 }
20 }
21 }
HAL_WWDG_IRQHandler处理中断请求函数也是比较简单。
第4行,程序通过读取WWDG_CFR寄存器的EWI位来判断是否已经开启提前唤醒中断,如果已经开启提前唤醒中断,则通过第7行,读取WWDG_SR寄存器的提前唤醒中断标志EWIF来判断是否有提前唤醒中断发生,如果有发生提前唤醒中断,则先清除提前唤醒中断在去调用提前唤醒中断回调函数来做相应的处理。在外部中断实验章节,我们讲过,发生中断以后,进入中断处理函数以后记得清理中断标志位,如果不清理,则会导致执行完回调函数以后还会重新进入中断,导致程序卡在中断中,无法返回主函数。
第12~18行,如果用户有自定义回调函数,则执行用户定义的提前唤醒中断回调函数,如果用户没有自定义,则默认执行HAL库里的回调函数。
(4)其它函数 此外还有HAL_WWDG_MspInit初始化函数和HAL_WWDG_EarlyWakeupCallback提前唤醒中断回调函数,这两个函数都是弱定义的。STM32CubeIDE会自动为我们生成一个新的HAL_WWDG_MspInit函数,用于开启HSEM和设置中断优先级分组。而HAL_WWDG_EarlyWakeupCallback函数的代码需要我们手动去实现。 15.2 WWDG实验 15.2.1 硬件设计
- 例程功能 在程序中开启提前唤醒中断,并在回调函数中实现自动喂狗。程序运行后,先点亮LED0 100ms的时间,再初始化WWDG,然后再关闭LED0,当发生喂狗时,LED1会出现翻转,当发生MCU复位时,会看到LED0再次点亮。我们就是通过LED1灯的翻转来观察是否有在中断喂狗的。
- 硬件资源 1)LED灯: LED0 LED1 总线 PI0 PF3 AHB4 表15.2.1. 1 硬件资源表 2)窗口看门狗:位于APB1总线上。
- 原理图 窗口看门狗属于STM32MP157的内部资源,只需要软件设置好即可正常工作。我们通过LED0和LED1来指示STM32MP157的复位情况和窗口看门狗的喂狗情况。 15.2.2 软件设计 本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\11、M4 CubeIDE裸机驱动例程\CubeIDE_project\ 8 WWDG。
- 程序流程图
图15.2.2. 1程序流程图 2. 生成工程 (1)配置LED灯引脚 按照跑马灯实验配置PI0和PF3为推挽输出、上拉、高速模式,User Label保持和之前设置的一致,因为我们后面会使用前面实验的led.h和led.c文件。当然,也可以直接在跑马灯实验的工程上直接配置WWDG,省去重新创建工程的麻烦。
图15.2.2. 2配置LED引脚 (2)配置WWDG 打开左边的System CoreWWDG1,配置WWDG参数,我们就按照前面说的,配置WDGTB分频值为16,计时器初始值为128,窗口值为95,并使能提前唤醒中断。
图15.2.2. 3配置WWDG参数 在NVIC Settings处使能看门狗中断,此时中断抢占优先级和子优先级均为0:
图15.2.2. 4使能WWDG中断 0优先级是最高的,而且系统级别的中断以及滴答定时器中断的优先级也是0,所以我们到NVIC设置处配置看门狗中断优先级,设置中断优先级分组为2,抢占优先级和子优先级均为3,设置为其它优先级也是可以的,我们在前面的外部中断实验章节有分析过。
图15.2.2. 5配置WWDG中断优先级 (3)时钟和工程配置 时钟的话,我们就配置PCLK1为104.5MHz,采用内部时钟或者外部时钟都可以,如果采用外部时钟,可以参考前面时钟系统章节来配置,这里我们采用默认的HSI,配置如下:
图15.2.2. 6配置PCLK1时钟为104.5MHz 配置完时钟,返回Project Manager配置界面,勾选Generate peripheral initialization as a pair of ".c/.h’ files per peripheral选项,这样可以独立生成对应外设的初始化.h和.c 文件(这么做也是为了不让外设的初始化代码生成在main.c文件中,方便查看,也避免main.c文件变得臃肿):
图15.2.2. 7配置生成独立的初始化代码 (4)生成初始化代码 按下“Ctrl+S”保存配置,生成工程,如下图,可以看到在工程中多了一个gpio.c文件和wwdg.c文件。gpio.c文件我们前面有介绍过,主要是完成gpio的初始化。wwdg.c主要是完成WWDG的初始化。
图15.2.2. 8生成工程 3. 添加用户代码 (1)添加跑马灯驱动 将跑马灯实验创建的BSP文件夹拷贝到Src文件夹下,如下图:
图15.2.2. 9添加LED初始化代码 (2)在main.c中添加如下代码:
led_init(); /* 关闭 LED0和LED1 */
LED0(0); /* 打开 LED0 */
HAL_Delay(100);
/* while循环中添加如下代码 */
LED0(1); /* 关闭 LED0 */
图15.2.2. 10 main.c中添加逻辑代码 (3)在led.c.c文件中添加代码如下
#include "./Include/led.h"
#include "wwdg.h"
void led_init(void)
{
LED0(1); /* 关闭 LED0 */
LED1(1); /* 关闭LED1 */
}
/**
* @brief 窗口看门狗中断服务回调函数
* @param 无
* @note 此函数会被HAL_WWDG_IRQHandler()调用
* @retval 无
*/
void HAL_WWDG_EarlyWakeupCallback(WWDG_HandleTypeDef *hwwdg)
{
HAL_WWDG_Refresh(&hwwdg1); /* 刷新窗口看门狗值 */
LED1_TOGGLE(); /* LED1灯翻转 */
}
图15.2.2. 11自定义回调函数 其中在led_init函数中,先将LED1关闭了,我们先添加代码,测试现象,后面我们再体分析代码。 4. 编译和测试 保存修改后点击工具栏的小锤子进行编译,编译无报错后,按照第4.1.6小节连接好开发板和ST-Link,进入Debug模式。进入Debug以后,点击继续运行按钮来运行调试,可以看到开发板底板的LED0 灯先是亮了一下就灭了,然后接着就是LED1在不停闪烁,说明程序在喂狗。 5. 工程代码分析 gpio.c文件我们就不分析了。下面我们来看看其它文件: (1)wwdg.c文件 wwdg.c文件的内容如下:
1 /**
2 * @brief 初始化窗口看门狗
3 * @note fprer:分频系数(WDGTB),范围:0~7,表示2^WDGTB分频
4 * Fwwdg=PCLK1(APB1)/(4096*2^fprer). 一般PCLK1=104.5Mhz
5 * 溢出时间=(4096*2^fprer)*(T[6:0]-0X3F)/PCLK1
6 * 假设fprer=4,T[6:0]=0X7f,W[6:0]=0X5F,PCLK1=104.5Mhz
7 * 则溢出时间=4096*16*64/104.5Mhz=40.14ms
8 * @retval 无
9 */
10 #include "wwdg.h"
11
12 WWDG_HandleTypeDef hwwdg1; /* 窗口看门狗句柄 */
13
14 /* WWDG1初始化功能 */
15 void MX_WWDG1_Init(void)
16 {
17 hwwdg1.Instance = WWDG1; /* 操作WWDG1 */
18 hwwdg1.Init.Prescaler = WWDG_PRESCALER_16; /* 设置分频系数WDGTB */
19 hwwdg1.Init.Window = 95; /* 设置窗口值 */
20 hwwdg1.Init.Counter = 127; /* 设置计数器值 */
21 /* 使能窗口看门狗提前唤醒中断 */
22 hwwdg1.Init.EWIMode = WWDG_EWI_ENABLE;
23 if (HAL_WWDG_Init(&hwwdg1) != HAL_OK) /* 初始化WWDG */
24 {
25 Error_Handler();
26 }
27
28 }
29 /**
30 * @brief WWDG底层驱动
31 * @param wwdgHandle:窗口看门狗句柄
32 * @note 此函数会被HAL_WWDG_Init()调用
33 * @retval 无
34 */
35 void HAL_WWDG_MspInit(WWDG_HandleTypeDef* wwdgHandle)
36 {
37 if(wwdgHandle->Instance==WWDG1)
38 {
39 /* WWDG1时钟使能 */
40 __HAL_RCC_WWDG1_CLK_ENABLE();
41 /* WWDG1中断优先级初始化 */
42 HAL_NVIC_SetPriority(WWDG1_IRQn, 3, 3);
43 /* WWDG1中断使能 */
44 HAL_NVIC_EnableIRQ(WWDG1_IRQn);
45 }
46 }
第12行,定义一个句柄hwwdg1; 第17~22行,将设置的窗口值、分频系数WDGTB、计数器初始值、提前唤醒中断使能赋值给句柄hwwdg1; 第23~26行,通过句柄赋值实现WWDG初始化,初始化后的WWDG,窗口值为95,分频系数WDGTB为4,计数器初始值为128。 第40行,开启WWDG时钟,WWDG挂在APB1总线上,通过将APB1外设使能设置寄存器RCC_MC_APB1ENSETR的第28位WWDG1EN置1来开启WWDG的时钟。 第42~44行,设置WWDG的抢占优先级和子优先级为3,并使能WWDG,WWDG1_IRQn是个中断号,在stm32mp157dxx_cm4.h文件中有定义,我们之前也多次讲过中断号和中断向量表以及中断服务函数的关系,这里就不再讲解了。 (2)stm32mp1xx_hal_msp.c文件 stm32mp1xx_hal_msp.c文件下就一个HAL_MspInit函数,主要是开启HSEM,设置中断优先级分组为2。HAL_MspInit函数最后会被HAL_Init函数调用,完成HAL库的初始化。 #include “main.h” /* 初始化全局MSP / void HAL_MspInit(void) { / 开启HSEMEN外设时钟 / __HAL_RCC_HSEM_CLK_ENABLE(); / 设置中断优先级分组为2 / HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2); } (3)stm32mp1xx_it.c文件 stm32mp1xx_it.c文件我们前面也是多次介绍过,这里只贴出和WWDG相关的代码: / 中断服务函数 */ void WWDG1_IRQHandler(void) { /*中断请求函数 */ HAL_WWDG_IRQHandler(&hwwdg1); } 我们在前面API函数分析部分有介绍此函数,中断服务函数中通过调用中断请求函数来完成中断,而中断请求函数中调用了回调函数HAL_WWDG_EarlyWakeupCallback,此回调函数使用的是HAL库里默认的弱定义函数,需要我们自己定义一个回调函数来实现中断的处理工作。 (4)led.c文件 led.c文件是从前面的跑马灯实验拷贝过来用的,我们在led.c文件中定义了回调函数HAL_WWDG_EarlyWakeupCallback以实现刷新计数器(喂狗),当然回调函数也可以定义在其它地方。也就是,当递减计数器从初始值128递减到0X40(10进制为64)的时候就发生提前唤醒中断,我们就在中断中喂狗,所以系统就不会复位了,每次喂狗,LED1会翻转一次。每当计数器从128递减到64的时候,LED1翻转一次,然后中断里刷新计数器,计数器又重新从128开始递减,递减到了64的时候又开始喂狗。计数器从128递减到64的时候也就过了40.14ms,这个时间很短是吧,所以你会发现LED1灯在频繁翻转。如果程序设计有缺陷跑飞了,要在这么短的时间内去喂狗还是有些困难的,所以窗口看门狗常用于检测系统缺陷。 (5)main.c文件 main.c文件的时钟初始化代码我们在前面的时钟系统章节有分析过,这里就不粘贴出来了。
1 #include "main.h"
2 #include "wwdg.h"
3 #include "gpio.h"
4
5 /* USER CODE BEGIN Includes */
6 #include "./BSP/Include/led.h"
7 /* USER CODE END Includes */
8
9 void SystemClock_Config(void);
10
11 int main(void)
12 {
13 HAL_Init();
14
15 if(IS_ENGINEERING_BOOT_MODE())
16 {
17 /* 初始化系统时钟 */
18 SystemClock_Config();
19 }
20
21 /* 初始化已经配置的GPIO */
22 MX_GPIO_Init();
23 led_init(); /* 关闭 LED0和LED1 */
24 LED0(0); /* 打开 LED0 */
25 HAL_Delay(100); /* 先延时100ms再打开WWDG,让LED0的变化"可见" */
26 MX_WWDG1_Init(); /* 初始化WWDG */
27 /* USER CODE BEGIN 2 */
28
29 while (1)
30 {
31 /* USER CODE BEGIN 3 */
32 LED0(1); /* 关闭 LED0 */
33 }
34 /* USER CODE END 3 */
35 }
main.c的代码比较简单,也就是先关闭LED1,再打开LED0,然后再初始化WWDG,再在while循环中将LED0关闭。所以实验中会看到,LED0是先亮了一下,当程序进入主函数以后LED0就关闭了,接着就是发生提前唤醒中断,中断中会喂狗,然后就看到LED1在闪烁。