文章目录
-
- 一、前言
-
- [1.1 技术背景](#1.1 技术背景)
- [1.2 应用场景](#1.2 应用场景)
- [1.3 本文目标](#1.3 本文目标)
- 二、环境准备
-
- [2.1 硬件要求](#2.1 硬件要求)
- [2.2 软件环境](#2.2 软件环境)
- 三、独立看门狗(IWDG)实现
-
- [3.1 IWDG工作原理](#3.1 IWDG工作原理)
- [3.2 IWDG配置代码](#3.2 IWDG配置代码)
- [3.3 主程序实现](#3.3 主程序实现)
- 四、窗口看门狗(WWDG)实现
-
- [4.1 WWDG工作原理](#4.1 WWDG工作原理)
- [4.2 WWDG配置代码](#4.2 WWDG配置代码)
- 五、IWDG与WWDG对比
- 六、故障排查与问题解决
-
- [6.1 常见问题](#6.1 常见问题)
- [6.2 调试技巧](#6.2 调试技巧)
- 七、总结
-
- [7.1 核心知识点](#7.1 核心知识点)
- [7.2 最佳实践](#7.2 最佳实践)
- [7.3 扩展学习](#7.3 扩展学习)
- [7.4 学习资源](#7.4 学习资源)
一、前言
1.1 技术背景
在嵌入式系统开发中,系统稳定性是至关重要的。由于电磁干扰、软件缺陷或硬件故障等原因,微控制器可能会出现程序"跑飞"或陷入死循环的情况。看门狗(Watchdog)机制就是一种用于监控系统运行状态、在异常情况下自动复位系统的硬件保护机制。
STM32F103系列微控制器内置了两种看门狗:
- 独立看门狗(IWDG):基于独立的低速内部RC振荡器(LSI),即使主时钟失效也能正常工作
- 窗口看门狗(WWDG):基于主时钟,提供精确的定时窗口监控
1.2 应用场景
看门狗广泛应用于以下场景:
- 工业控制系统:确保关键设备持续稳定运行
- 物联网设备:远程设备需要自我恢复能力
- 汽车电子:安全关键系统必须可靠
- 消费电子:提升产品稳定性和用户体验
1.3 本文目标
通过本教程,你将学会:
- 理解IWDG和WWDG的工作原理和区别
- 掌握看门狗的配置和初始化方法
- 实现喂狗操作和系统复位处理
- 处理看门狗相关的故障排查
技术栈:
- 芯片型号:STM32F103C8T6
- 开发环境:STM32CubeIDE
- 固件库:HAL库
- 调试工具:ST-Link V2
二、环境准备
2.1 硬件要求
| 项目 | 规格 |
|---|---|
| 开发板 | STM32F103C8T6最小系统板 |
| 调试器 | ST-Link V2 |
| 电源 | USB 5V供电 |
| LED | 3个(用于状态指示) |
2.2 软件环境
STM32CubeIDE安装:
- 访问ST官网下载STM32CubeIDE
- 安装并配置工作空间
- 安装STM32F1系列HAL库
工程创建:
- File → New → STM32 Project
- 选择MCU:STM32F103C8Tx
- 配置时钟:HSE Crystal,72MHz
- 启用SWD调试
三、独立看门狗(IWDG)实现
3.1 IWDG工作原理
独立看门狗使用内部低速RC振荡器(LSI,约40kHz)作为时钟源,具有完全独立的时钟,即使主时钟失效也能正常工作。
关键特性:
- 自由运行的递减计数器
- 时钟由独立的RC振荡器提供
- 当计数器减到0时产生复位
- 在键寄存器写入特定值时重装载计数器
是
否
系统启动
初始化IWDG
启动计数器
程序正常运行?
定时喂狗
计数器减到0
系统复位
3.2 IWDG配置代码
📄 创建文件:
Core/Inc/iwdg_handler.h
c
/**
* @file iwdg_handler.h
* @brief 独立看门狗处理头文件
*
* 本文件定义了IWDG相关的函数声明和配置参数
*/
#ifndef __IWDG_HANDLER_H
#define __IWDG_HANDLER_H
#include "stm32f1xx_hal.h"
/* IWDG超时时间计算(基于40kHz LSI)
* 超时时间 = (Prescaler / LSI_Freq) * ReloadValue
* 例如:Prescaler=64, Reload=625
* 超时时间 = (64/40000) * 625 = 1秒
*/
#define IWDG_PRESCALER IWDG_PRESCALER_64 // 预分频系数64
#define IWDG_RELOAD_VALUE 625 // 重装载值625
#define IWDG_TIMEOUT_MS 1000 // 超时时间1秒
/**
* @brief 初始化独立看门狗
* @retval HAL_StatusTypeDef 初始化状态
*/
HAL_StatusTypeDef IWDG_Init(void);
/**
* @brief 喂狗操作(刷新看门狗计数器)
* @retval HAL_StatusTypeDef 操作状态
*/
HAL_StatusTypeDef IWDG_Refresh(void);
/**
* @brief 检查是否为看门狗复位
* @retval uint8_t 1表示是看门狗复位,0表示不是
*/
uint8_t IWDG_IsReset(void);
/**
* @brief 清除复位标志
*/
void IWDG_ClearResetFlag(void);
#endif /* __IWDG_HANDLER_H */
📄 创建文件:
Core/Src/iwdg_handler.c
c
/**
* @file iwdg_handler.c
* @brief 独立看门狗处理实现
*
* 本文件实现了IWDG的初始化、喂狗和状态检测功能
*/
#include "iwdg_handler.h"
#include <stdio.h>
/**
* @brief IWDG句柄结构体
*
* HAL库使用此结构体管理IWDG外设
*/
static IWDG_HandleTypeDef hiwdg;
/**
* @brief 初始化独立看门狗
*
* 配置IWDG的预分频器和重装载值,启动看门狗
*
* @retval HAL_OK 初始化成功
* @retval HAL_ERROR 初始化失败
*/
HAL_StatusTypeDef IWDG_Init(void)
{
HAL_StatusTypeDef status;
/* 配置IWDG参数 */
hiwdg.Instance = IWDG; // IWDG外设实例
hiwdg.Init.Prescaler = IWDG_PRESCALER; // 设置预分频器
hiwdg.Init.Reload = IWDG_RELOAD_VALUE; // 设置重装载值
/* 初始化IWDG */
status = HAL_IWDG_Init(&hiwdg);
if (status != HAL_OK) {
printf("IWDG初始化失败!\r\n");
return HAL_ERROR;
}
printf("IWDG初始化成功,超时时间:%d ms\r\n", IWDG_TIMEOUT_MS);
return HAL_OK;
}
/**
* @brief 喂狗操作
*
* 向IWDG键寄存器写入重装载值,刷新计数器
* 必须在计数器减到0之前调用此函数
*
* @retval HAL_OK 刷新成功
* @retval HAL_ERROR 刷新失败
*/
HAL_StatusTypeDef IWDG_Refresh(void)
{
HAL_StatusTypeDef status;
/* 刷新IWDG计数器 */
status = HAL_IWDG_Refresh(&hiwdg);
if (status != HAL_OK) {
printf("IWDG喂狗失败!\r\n");
return HAL_ERROR;
}
return HAL_OK;
}
/**
* @brief 检查是否为看门狗复位
*
* 通过检查RCC_CSR寄存器的IWDGRSTF位判断
*
* @retval 1 是看门狗复位
* @retval 0 不是看门狗复位
*/
uint8_t IWDG_IsReset(void)
{
/* 检查IWDG复位标志位 */
if (__HAL_RCC_GET_FLAG(RCC_FLAG_IWDGRST)) {
return 1;
}
return 0;
}
/**
* @brief 清除复位标志
*
* 清除RCC_CSR寄存器中的所有复位标志
* 必须在读取复位原因后调用
*/
void IWDG_ClearResetFlag(void)
{
/* 清除所有复位标志 */
__HAL_RCC_CLEAR_RESET_FLAGS();
}
3.3 主程序实现
📄 创建文件:
Core/Src/main_iwdg.c
c
/**
* @file main_iwdg.c
* @brief IWDG测试主程序
*
* 本程序演示IWDG的基本使用,包括初始化、喂狗和复位检测
*/
#include "main.h"
#include "iwdg_handler.h"
#include <stdio.h>
/* 私有函数声明 */
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART1_UART_Init(void);
/* UART句柄 */
UART_HandleTypeDef huart1;
/**
* @brief 主函数
*
* 程序入口,初始化外设,启动IWDG,进入主循环
*/
int main(void)
{
/* HAL库初始化 */
HAL_Init();
/* 配置系统时钟 */
SystemClock_Config();
/* 初始化外设 */
MX_GPIO_Init();
MX_USART1_UART_Init();
/* 检查是否为看门狗复位 */
if (IWDG_IsReset()) {
printf("\r\n========================================\r\n");
printf("系统由IWDG看门狗复位重启!\r\n");
printf("========================================\r\n");
IWDG_ClearResetFlag();
} else {
printf("\r\n系统正常启动\r\n");
}
/* 初始化IWDG */
if (IWDG_Init() != HAL_OK) {
printf("IWDG初始化失败,系统停止!\r\n");
while (1) {
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); // LED闪烁表示错误
HAL_Delay(100);
}
}
printf("系统正常运行,开始喂狗...\r\n");
/* 主循环 */
while (1) {
/* 正常喂狗 */
if (IWDG_Refresh() != HAL_OK) {
printf("喂狗失败!\r\n");
}
/* LED闪烁表示系统运行正常 */
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
/* 模拟正常工作的延时(500ms,小于超时时间1秒) */
HAL_Delay(500);
printf("系统运行正常,已喂狗\r\n");
}
}
/**
* @brief 模拟死循环(用于测试看门狗复位)
*
* 调用此函数会导致看门狗超时复位
* 仅在测试时使用
*/
void SimulateDeadLoop(void)
{
printf("进入死循环,等待看门狗复位...\r\n");
while (1) {
/* 不喂狗,等待复位 */
}
}
/**
* @brief 模拟长时间任务(用于测试看门狗复位)
*
* 延时超过看门狗超时时间,触发复位
*/
void SimulateLongTask(void)
{
printf("开始长时间任务(2秒)...\r\n");
HAL_Delay(2000); // 2秒延时,超过1秒超时时间
printf("长时间任务完成\r\n");
}
/**
* @brief GPIO初始化
*/
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* 使能GPIOC时钟 */
__HAL_RCC_GPIOC_CLK_ENABLE();
/* 配置PC13为输出(板载LED) */
GPIO_InitStruct.Pin = GPIO_PIN_13;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
}
/**
* @brief USART1初始化(用于printf输出)
*/
static void MX_USART1_UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK) {
Error_Handler();
}
}
/**
* @brief 系统时钟配置
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/* 配置HSE和PLL */
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
Error_Handler();
}
/* 配置系统时钟 */
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) {
Error_Handler();
}
}
/**
* @brief 错误处理函数
*/
void Error_Handler(void)
{
__disable_irq();
while (1) {
/* 错误时LED快速闪烁 */
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
HAL_Delay(50);
}
}
/* printf重定向 */
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
PUTCHAR_PROTOTYPE
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
return ch;
}
四、窗口看门狗(WWDG)实现
4.1 WWDG工作原理
窗口看门狗基于主时钟(PCLK1)工作,提供更精确的监控机制。与IWDG不同,WWDG要求喂狗操作必须在特定的时间窗口内完成。
关键特性:
- 基于主时钟,可精确控制时序
- 具有可编程的窗口值
- 过早或过晚喂狗都会触发复位
- 可产生早期唤醒中断(EWI)
时间轴
是
否
是
否
是
计数器=127
递减计数
计数器 > 窗口值?
禁止喂狗
复位
允许喂狗
喂狗成功
计数器=0x40?
早期唤醒中断
计数器<0x40?
复位
4.2 WWDG配置代码
📄 创建文件:
Core/Inc/wwdg_handler.h
c
/**
* @file wwdg_handler.h
* @brief 窗口看门狗处理头文件
*/
#ifndef __WWDG_HANDLER_H
#define __WWDG_HANDLER_H
#include "stm32f1xx_hal.h"
/* WWDG配置参数
* 时钟源:PCLK1 = 36MHz (APB1)
* 预分频:8
* 计数器时钟 = 36MHz / 4096 / 8 = 1.098kHz
* 超时时间 = (127-64) / 1.098kHz = 57.4ms
* 窗口值 = 80
* 窗口时间 = (127-80) / 1.098kHz = 42.8ms
*/
#define WWDG_PRESCALER WWDG_PRESCALER_8 // 预分频系数8
#define WWDG_COUNTER 127 // 计数器初始值
#define WWDG_WINDOW 80 // 窗口值
#define WWDG_EWI_ENABLE 1 // 启用早期唤醒中断
/**
* @brief 初始化窗口看门狗
* @retval HAL_StatusTypeDef 初始化状态
*/
HAL_StatusTypeDef WWDG_Init(void);
/**
* @brief 喂狗操作
* @retval HAL_StatusTypeDef 操作状态
*/
HAL_StatusTypeDef WWDG_Refresh(void);
/**
* @brief 检查是否为WWDG复位
* @retval uint8_t 1表示是WWDG复位,0表示不是
*/
uint8_t WWDG_IsReset(void);
/**
* @brief WWDG早期唤醒中断回调
*/
void WWDG_EarlyWakeupCallback(void);
#endif /* __WWDG_HANDLER_H */
📄 创建文件:
Core/Src/wwdg_handler.c
c
/**
* @file wwdg_handler.c
* @brief 窗口看门狗处理实现
*/
#include "wwdg_handler.h"
#include <stdio.h>
static WWDG_HandleTypeDef hwwdg;
/**
* @brief 初始化窗口看门狗
*/
HAL_StatusTypeDef WWDG_Init(void)
{
HAL_StatusTypeDef status;
/* 使能WWDG时钟 */
__HAL_RCC_WWDG_CLK_ENABLE();
/* 配置WWDG参数 */
hwwdg.Instance = WWDG;
hwwdg.Init.Prescaler = WWDG_PRESCALER; // 预分频
hwwdg.Init.Window = WWDG_WINDOW; // 窗口值
hwwdg.Init.Counter = WWDG_COUNTER; // 计数器初始值
hwwdg.Init.EWIMode = WWDG_EWI_ENABLE ? // 早期唤醒中断
WWDG_EWI_ENABLE : WWDG_EWI_DISABLE;
/* 初始化WWDG */
status = HAL_WWDG_Init(&hwwdg);
if (status != HAL_OK) {
printf("WWDG初始化失败!\r\n");
return HAL_ERROR;
}
printf("WWDG初始化成功\r\n");
printf(" 计数器值:%d\r\n", WWDG_COUNTER);
printf(" 窗口值:%d\r\n", WWDG_WINDOW);
printf(" 必须在计数器%d到%d之间喂狗\r\n", WWDG_WINDOW, 0x40);
return HAL_OK;
}
/**
* @brief 喂狗操作
*/
HAL_StatusTypeDef WWDG_Refresh(void)
{
HAL_StatusTypeDef status;
/* 刷新WWDG计数器 */
status = HAL_WWDG_Refresh(&hwwdg);
if (status != HAL_OK) {
printf("WWDG喂狗失败!\r\n");
return HAL_ERROR;
}
return HAL_OK;
}
/**
* @brief 检查是否为WWDG复位
*/
uint8_t WWDG_IsReset(void)
{
if (__HAL_RCC_GET_FLAG(RCC_FLAG_WWDGRST)) {
return 1;
}
return 0;
}
/**
* @brief WWDG中断服务程序
*/
void WWDG_IRQHandler(void)
{
HAL_WWDG_IRQHandler(&hwwdg);
}
/**
* @brief 早期唤醒中断回调
*
* 当计数器减到0x40时触发此中断
* 可以在此中断中执行紧急保存操作
*/
void HAL_WWDG_EarlyWakeupCallback(WWDG_HandleTypeDef *hwwdg)
{
printf("WWDG早期唤醒中断!计数器即将溢出\r\n");
/* 可以在这里执行紧急操作,如保存关键数据 */
}
五、IWDG与WWDG对比
| 特性 | IWDG(独立看门狗) | WWDG(窗口看门狗) |
|---|---|---|
| 时钟源 | 内部LSI(40kHz) | PCLK1(36MHz) |
| 独立性 | 完全独立,主时钟失效也能工作 | 依赖主时钟 |
| 精度 | 较低(RC振荡器有偏差) | 较高 |
| 窗口功能 | 无 | 有(必须在窗口内喂狗) |
| 中断 | 无 | 早期唤醒中断(EWI) |
| 适用场景 | 防止程序跑飞 | 精确时序监控 |
六、故障排查与问题解决
6.1 常见问题
问题1:看门狗无法初始化
现象:
IWDG初始化失败!
原因分析:
- 时钟配置错误
- 参数超出有效范围
- HAL库版本不匹配
解决方案:
c
/* 检查LSI时钟是否使能 */
if (__HAL_RCC_GET_FLAG(RCC_FLAG_LSIRDY) == RESET) {
/* LSI未就绪,手动使能 */
__HAL_RCC_LSI_ENABLE();
/* 等待LSI稳定 */
uint32_t timeout = 1000;
while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSIRDY) == RESET) {
if (--timeout == 0) {
printf("LSI启动超时!\r\n");
return HAL_ERROR;
}
}
}
问题2:系统频繁复位
现象:
系统不断重启,串口输出复位信息
原因分析:
- 喂狗周期太长,超过超时时间
- 程序中有死循环或长时间阻塞
- 中断优先级配置错误
解决方案:
c
/* 方案1:缩短喂狗周期 */
while (1) {
IWDG_Refresh(); // 每次循环都喂狗
/* 长时间任务分段执行 */
for (int i = 0; i < 10; i++) {
DoPartialWork(i); // 分段处理
IWDG_Refresh(); // 中间喂狗
}
}
/* 方案2:使用中断喂狗 */
void TIM2_IRQHandler(void)
{
if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE)) {
__HAL_TIM_CLEAR_IT(&htim2, TIM_IT_UPDATE);
IWDG_Refresh(); // 定时器中断中喂狗
}
}
问题3:WWDG过早喂狗导致复位
现象:
系统由WWDG看门狗复位重启!
原因分析:
- 喂狗时间早于窗口值
- 窗口值设置不合理
解决方案:
c
/* 检查当前计数器值 */
uint32_t counter = WWDG->CR & 0x7F;
printf("当前计数器值:%lu\r\n", counter);
/* 确保在窗口内喂狗 */
if (counter <= WWDG_WINDOW && counter > 0x40) {
WWDG_Refresh();
} else {
printf("不在喂狗窗口内,跳过喂狗\r\n");
}
6.2 调试技巧
使用调试器观察看门狗寄存器:
c
/* 在调试时暂停看门狗 */
void Debug_IWDG_Pause(void)
{
/* 设置调试寄存器,暂停IWDG */
DBGMCU->CR |= DBGMCU_CR_DBG_IWDG_STOP;
}
/* 恢复看门狗 */
void Debug_IWDG_Resume(void)
{
DBGMCU->CR &= ~DBGMCU_CR_DBG_IWDG_STOP;
}
打印看门狗状态:
c
void Print_Watchdog_Status(void)
{
printf("===== 看门狗状态 =====\r\n");
printf("IWDG状态:\r\n");
printf(" KR寄存器:0x%08X\r\n", (unsigned int)IWDG->KR);
printf(" PR寄存器:0x%08X\r\n", (unsigned int)IWDG->PR);
printf(" RLR寄存器:0x%08X\r\n", (unsigned int)IWDG->RLR);
printf(" SR寄存器:0x%08X\r\n", (unsigned int)IWDG->SR);
printf("WWDG状态:\r\n");
printf(" CR寄存器:0x%08X\r\n", (unsigned int)WWDG->CR);
printf(" CFR寄存器:0x%08X\r\n", (unsigned int)WWDG->CFR);
printf(" SR寄存器:0x%08X\r\n", (unsigned int)WWDG->SR);
}
七、总结
7.1 核心知识点
- IWDG特点:独立时钟、简单可靠、适合防止程序跑飞
- WWDG特点:精确时序、窗口监控、适合时间敏感应用
- 喂狗时机:必须在超时前完成,WWDG还需在窗口内
- 复位检测:通过RCC_CSR寄存器判断复位原因
7.2 最佳实践
- 合理设置超时时间:既要保证响应速度,又要避免过于频繁喂狗
- 分散喂狗点:在关键代码路径多处喂狗,避免单点故障
- 记录复位原因:保存复位日志,便于问题分析
- 调试时暂停看门狗:避免调试过程中触发复位
7.3 扩展学习
- 结合RTC实现低功耗模式下的看门狗管理
- 使用备份寄存器保存复位计数和错误信息
- 实现多级看门狗策略(IWDG+WWDG同时使用)
7.4 学习资源
官方文档:
官方GitHub: