STM32

STM32F1检测启动类型及BKP寄存器的使用

在 STM32F103 上使用 HAL 库进行上电启动检测时,建议将检测代码放在 main() 函数的刚开始处,必须位于所有外设初始化之前(或紧跟在 HAL_Init() 之后)。
这是因为 STM32F103 的 PINRSTF(引脚复位标志)在很多情况下会与 PORRSTF(上电复位标志)同时置位。因此,最稳妥的逻辑是排除法:优先排除软件复位和看门狗复位,如果仅有上电或引脚标志,则判定为冷启动(上电启动)。

一、 核心代码实现

直接将以下代码复制到你的 main.c 中:

/* 私有变量定义 */
uint8_t is_power_on = 0; // 0: 异常/软件/看门狗复位, 1: 纯上电/硬复位启动

int main(void)
{
  /* 1. 初始化 HAL 库 */
  HAL_Init();

  /* 2. 检测复位源 (必须在配置时钟和清除标志前执行) */
  if (__HAL_RCC_GET_FLAG(RCC_FLAG_SFTRST) != RESET)
  {
      // 软件复位 (例如调用了 NVIC_SystemReset)
      is_power_on = 0;
  }
  else if (__HAL_RCC_GET_FLAG(RCC_FLAG_IWDGRST) != RESET || __HAL_RCC_GET_FLAG(RCC_FLAG_WWDGRST) != RESET)
  {
      // 看门狗复位 (独立看门狗或窗口看门狗超时)
      is_power_on = 0;
  }
  else if (__HAL_RCC_GET_FLAG(RCC_FLAG_PORRST) != RESET)
  {
      // 纯正的上电复位 / 掉电复位
      is_power_on = 1;
  }
  else if (__HAL_RCC_GET_FLAG(RCC_FLAG_PINRST) != RESET)
  {
      // 外部 NRST 引脚复位 (按了板子上的 RESET 键)
      // 注意:上电时此标志也可能同时置 1,若排除了前面几种,这里通常也视为冷启动
      is_power_on = 1;
  }
  else
  {
      is_power_on = 0;
  }

  /* 3. 必须立即清除复位标志,否则下次复位时旧标志还在,导致判断出错 */
  __HAL_RCC_CLEAR_RESET_FLAGS();

  /* 4. 后续系统时钟和外设初始化 */
  SystemClock_Config();
  MX_GPIO_Init();
  // ... 其他外设初始化

  /* 5. 根据判断结果执行不同的业务逻辑 */
  if (is_power_on == 1)
  {
      // 执行上电特有的操作,比如历史数据清零、屏幕显示欢迎 LOGO 等
  }
  else
  {
      // 执行复位后的恢复操作,比如从备份寄存器读取之前的运行状态
  }

  while (1)
  {
      // 主循环
  }
}

二、 开发注意事项

  1. 时钟配置前检测:有些工程师喜欢把检测放在 SystemClock_Config() 之后,虽然有时可行,但最安全的做法是尽早检测并清除,防止有些外设初始化时内部触发了微小的复位信号扰乱寄存器。
  2. 硬件调试器干扰:使用 ST-Link 或 J-Link 下载程序或在线仿真(Debug)时,调试器会向 NRST 引脚发送复位信号,这会强行触发 RCC_FLAG_PINRST。因此,测试上电逻辑时,请断开仿真器,彻底断电后重新上电观察。
    如果在冷启动时,你需要保存某些数据不被清空(即便上电复位也能留存),我们可以利用 STM32F103 的 BKP(备份寄存器) 来实现。

在 STM32F103 中,BKP(备份寄存器) 是一块非常特殊的区域。即使单片机发生上电复位、看门狗复位或软件复位,只要后备电源(Vbat 引脚接了纽扣电池)不断电,BKP 里的数据就不会丢失。
结合上一步的“上电检测”逻辑,我们可以用 BKP 寄存器来实现:上电后判断数据是否有效,若有效则恢复运行;若是真正意义上的第一次掉电(Vbat 也断了),则重新初始化数据。

一、 核心实现步骤

要在 HAL 库中使用 BKP 寄存器,需要开启 PWR(电源控制) 和 BKP(备份时钟),并解除保护。

  1. 编写 BKP 读写函数

首先在 main.c 中添加以下两个简易的读写辅助函数(STM32F103 有 42 个 16 位的备份寄存器,这里我们使用第 1 个 BKP_DR1):

/**
  * @brief  向BKP寄存器写入一个16位数据
  * @param  BKP_DR: 目标寄存器 (如 BKP_DR1, BKP_DR2 等)
  * @param  Data: 要写入的16位数据
  */
void BKP_Write(uint32_t BKP_DR, uint16_t Data)
{
    __HAL_RCC_PWR_CLK_ENABLE();  // 开启PWR时钟
    __HAL_RCC_BKP_CLK_ENABLE();  // 开启BKP时钟
    HAL_PWR_EnableBkUpAccess();  // 取消备份区域写保护

    HAL_RTCEx_BKUPWrite(&hrtc, BKP_DR, Data); // 写入数据 (需要引入RTC句柄)

    HAL_PWR_DisableBkUpAccess(); // 恢复备份区域写保护
}

/**
  * @brief  从BKP寄存器读取一个16位数据
  */
uint16_t BKP_Read(uint32_t BKP_DR)
{
    __HAL_RCC_PWR_CLK_ENABLE();
    __HAL_RCC_BKP_CLK_ENABLE();

    return (uint16_t)HAL_RTCEx_BKUPRead(&hrtc, BKP_DR);
}

注:hrtc 是 RTC(实时时钟)的句柄。如果你在 CubeMX 中没有开启 RTC,HAL_RTCEx_BKUPWrite 传入 NULL 或者声明一个临时的 RTC_HandleTypeDef hrtc; 即可,因为 BKP 的读写实际上只依赖 PWR 和 BKP 时钟。

二、 整合:上电检测 + 完整业务逻辑

我们在 main() 函数中将复位源检测与BKP标志位检查结合起来。这里我们定义一个标志暗号 0x5A5A:

/* 私有变量 */
uint8_t is_power_on = 0;     // 0: 异常复位, 1: 正常冷启动
uint16_t saved_run_count = 0;// 模拟需要掉电保存的数据

int main(void)
{
  HAL_Init();

  /* 1. 检测复位源 */
  if (__HAL_RCC_GET_FLAG(RCC_FLAG_SFTRST) != RESET || 
      __HAL_RCC_GET_FLAG(RCC_FLAG_IWDGRST) != RESET || 
      __HAL_RCC_GET_FLAG(RCC_FLAG_WWDGRST) != RESET)
  {
      is_power_on = 0; // 软件或看门狗导致的异常复位
  }
  else
  {
      is_power_on = 1; // 纯上电或按了Reset键的冷启动
  }
  __HAL_RCC_CLEAR_RESET_FLAGS(); // 清除标志

  /* 2. 时钟和外设初始化 */
  SystemClock_Config();
  MX_GPIO_Init();
  // 如果用到了RTC,MX_RTC_Init() 也会在这里执行

  /* 3. 核心 BKP 数据恢复与判断逻辑 */
  // 读取 BKP_DR1 中的“数据有效标志暗号”
  if (BKP_Read(RTC_BKP_DR1) == 0x5A5A)
  {
      /* 说明:Vbat电池有电,之前系统运行过并成功写入了数据 */
      // 从 BKP_DR2 读取你需要保护的业务数据
      saved_run_count = BKP_Read(RTC_BKP_DR2);

      if (is_power_on == 1)
      {
          // 情况 A:正常的断电后重新上电,且电池有电,数据完好
          // 可以在这里恢复设备状态
      }
      else
      {
          // 情况 B:看门狗复位或软件复位,数据完好
          // 可以在这里做故障恢复逻辑
      }
  }
  else
  {
      /* 说明:这是芯片这辈子“第一次”上电,或者 Vbat 纽扣电池没电/被拔掉了 */
      // 1. 初始化你的业务数据
      saved_run_count = 0; 

      // 2. 写入暗号和初始数据,防止下次上电再丢失
      BKP_Write(RTC_BKP_DR1, 0x5A5A);       // 写入有效标志
      BKP_Write(RTC_BKP_DR2, saved_run_count); // 写入初始数据
  }

  /* --- 模拟后续的业务运行 --- */
  while (1)
  {
      // 模拟每隔一段时间,业务数据发生改变,更新到 BKP 中
      HAL_Delay(5000);
      saved_run_count++;

      // 数据变了,随时同步到 BKP 中,确保哪怕下一秒突然断电数据也在
      BKP_Write(RTC_BKP_DR2, saved_run_count);
  }
}

三、 避坑指南

  1. Vbat 引脚不可悬空:如果你想测试“完全断电后数据不丢失”,STM32F103 的 VBAT 引脚(Pin 1)必须接一颗 3V 纽扣电池。如果悬空或接地,一旦主电源 VCC 断开,BKP 里的数据瞬间清空。
  2. 容量限制:STM32F103 的 BKP 空间很小。
    • 小容量/中容量(如 C8T6, RCT6):有 10 个 16位寄存器(BKP_DR1 到 BKP_DR10)。
    • 大容量(如 ZET6, VET6):有 42 个 16位寄存器(BKP_DR1 到 BKP_DR42)。