STM32从零到量产开发:四路继电器工业控制模块开发 - Flash 参数掉电保存模块(flash_param)设计说明

1. 模块概述

本模块负责工业控制模块核心运行参数的掉电保存与恢复。针对 STM32F103RC 内部 Flash 特性,实现了参数校验、磨损均衡、跨重启运行时间统计等工业级功能。

核心功能:

  • 参数持久化:从机地址、波特率、通信使能、故障安全配置等掉电不丢失。
  • 数据完整性:Magic 魔术字 + CRC16 双重校验,防止 Flash 损坏或误读。
  • 寿命保护:磨损均衡策略,避免高频擦写导致 Flash 损坏。
  • 运行统计:累计复位次数与运行秒数(跨重启连续累计)。

2. 存储架构

2.1 Flash 地址规划

  • 目标芯片:STM32F103RC (256KB Flash, 2KB/Page)
  • 存储位置:内部 Flash 最后一页 0x0803F800
  • 页大小:2KB (0x800)

2.2 参数结构体设计 (param_t)

采用 4 字节对齐设计,避免 Cortex-M 非对齐访问异常:

typedef struct {
    uint16_t magic;          // 校验魔术字 (0x5A5A)
    uint8_t  slave_addr;     // 从机地址 1~247
    uint8_t  baud_index;     // 波特率索引 0~4
    uint8_t  link_enable;    // 通信使能 0/1
    uint8_t  fail_safe_en;   // 故障安全使能 0/1
    uint8_t  fail_safe_sec;  // 故障安全超时(秒)
    uint8_t  reserved;       // 4字节对齐填充
    uint32_t reset_count;    // 累计复位次数
    uint32_t run_sec_base;   // 运行秒数基准
    uint32_t crc;            // CRC16 校验
} param_t;

3. 出厂默认参数

参数 默认值 说明
从机地址 0x01 Modbus 从站地址
波特率索引 0x00 对应 9600 bps
通信使能 0x01 默认启用通信
故障安全使能 0x01 默认启用(工业安全)
故障安全超时 10秒 默认超时时间

4. 对外公共接口

函数 说明
FlashParam_Init() 初始化:从 Flash 加载参数,校验失败则恢复出厂默认
FlashParam_Get() 获取当前参数指针(RAM 副本)
FlashParam_Save() 修改并保存参数到 Flash(支持部分更新)
FlashParam_SaveResetCount() 仅更新复位次数(低频调用)
FlashParam_ResetDefault() 恢复出厂默认参数并保存
FlashParam_GetRunSeconds() 获取累计运行秒数(基准 + 本次上电增量)

5. 关键设计亮点

5.1 Magic + CRC16 双重校验

  • Magic 校验:快速判断 Flash 是否已初始化(0x5A5A
  • CRC16 校验:确保参数数据完整性,防止位翻转或 Flash 损坏

5.2 磨损均衡策略

  • 参数仅在用户主动修改(Modbus 06/10 功能码)时写入 Flash
  • 复位次数缓存在 RAM,随其他参数一并写入,避免每次复位都擦写 Flash

5.3 运行秒跨重启连续累计

  • run_sec_base:掉电保存的运行秒数基准
  • FlashParam_GetRunSeconds() = run_sec_base + 本次上电增量
  • 确保设备运行时间统计准确且断电不丢失

5.4 恢复出厂保留设备履历

  • FlashParam_ResetDefault() 恢复出厂参数时,保留 reset_countrun_sec_base
  • 确保设备运维统计数据不丢失

5.5 Flash 擦写安全保护

  • 写入前校验参数范围合法性(地址 1~247、波特率索引 0~4)
  • 非法参数直接返回错误,不执行 Flash 擦写

5.6 参数范围钳位

  • FlashParam_Save() 内部对参数进行合法性校验
  • 超出范围的参数自动钳位到合法值或拒绝写入

6. 工程化设计细节

6.1 结构体 4 字节对齐

  • reserved 字段填充,确保后续 uint32_t 字段 4 字节对齐
  • 避免 Cortex-M 非对齐访问异常

6.2 与 Modbus 协议协作

  • FlashParam_Save() 由 Modbus 06/10 功能码触发
  • 参数修改后立即生效,无需重启

6.3 与故障安全联动

  • fail_safe_enfail_safe_sec 参数直接影响 Modbus_CheckFailSafe() 行为
  • 默认启用故障安全,防止上位机死机导致设备失控

7. 设计总结

设计维度 具体措施
数据完整性 Magic + CRC16 双重校验
Flash 寿命 磨损均衡,避免高频擦写
运行统计 跨重启连续累计,断电不丢失
安全性 参数范围钳位,非法值拒绝写入
可维护性 恢复出厂保留履历,便于运维追溯

该模块设计充分考虑了工业环境的可靠性要求,兼顾了数据安全性、Flash 寿命保护和运维统计需求,是工业控制设备参数管理的标准实践。

/**
  ******************************************************************************
  * @file    flash_param.h
  * @brief   Flash 参数掉电保存模块 - 对应产品规格书默认出厂参数
  *
  * 存储位置 (规格书 2):
  *   内部 Flash 最后一页 0x0803F800 (STM32F103RC 256KB, 2KB/页)
  *
  * 存储内容 (规格书 4.4 保持寄存器):
  *   - 从机地址 (1~247)
  *   - 波特率索引 (0~4)
  *   - 通信使能 (0/1)
  *   - 复位次数 (累计)
  *
  * 校验机制:
  *   结构体含 magic 字段 + crc16 校验, 防止误读未初始化 Flash
  *
  * 磨损均衡策略:
  *   - 参数仅在用户修改(通过 Modbus 06/10)时才写入 Flash
  *   - 复位次数缓存在 RAM, 随参数一并写入, 避免每次复位都擦写 Flash
  ******************************************************************************
  */
#ifndef __FLASH_PARAM_H
#define __FLASH_PARAM_H

#ifdef __cplusplus
extern "C" {
#endif

#include "main.h"
#include "bsp_rs485.h"

/* Flash 参数页地址 (STM32F103RC 最后一页) */
#define PARAM_FLASH_ADDR        0x0803F800u
#define PARAM_FLASH_PAGE_SIZE   0x800u    /* 2KB */

/* 参数校验魔术字 */
#define PARAM_MAGIC             0x5A5Au

/* 出厂默认值 (规格书第8章) */
#define DEFAULT_SLAVE_ADDR      0x01u
#define DEFAULT_BAUD_INDEX      0x00u     /* 索引0 = 9600 bps (见规格书 4.4) */
#define DEFAULT_LINK_ENABLE     0x01u

/* #1 通信故障安全默认参数 (工业安全默认开启, 防止上位机死机导致设备失控) */
#define DEFAULT_FAILSAFE_EN     0x01u     /* 默认启用故障安全 */
#define DEFAULT_FAILSAFE_SEC    0x0Au     /* 默认超时 10 秒 */

/* 参数合法范围 */
#define SLAVE_ADDR_MIN          1u
#define SLAVE_ADDR_MAX          247u
#define FAILSAFE_EN_MAX         1u       /* 仅 0/1 */
/* fail_safe_sec: 0~255, 0 表示禁用(配合 fail_safe_en=0) */

/**
  * @brief  参数结构体 (存储在 Flash 中)
  * @note   字段顺序经过对齐设计: magic(uint16)+slave/baud/link/fail_en/fail_sec(5字节)
  *        +reserved(1字节) 共 8 字节, 之后 reset_count/run_sec_base/crc 均为 uint32,
  *        保证 4 字节对齐, 避免 Cortex-M 非对齐访问异常。
  */
typedef struct {
    uint16_t magic;         /* 校验魔术字 PARAM_MAGIC */
    uint8_t  slave_addr;    /* 从机地址 1~247 */
    uint8_t  baud_index;    /* 波特率索引 0~4 */
    uint8_t  link_enable;   /* 通信使能 0/1 (0=静默模式不回复) */
    uint8_t  fail_safe_en;  /* 通信故障安全使能 0/1 (#1 功能) */
    uint8_t  fail_safe_sec; /* 故障安全超时(秒), 0=禁用 (#1 功能) */
    uint8_t  reserved;      /* 4字节对齐填充, 使后续 uint32 字段对齐 */
    uint32_t reset_count;   /* 累计复位次数 */
    uint32_t run_sec_base;  /* 累计运行秒数基准(掉电保存), 用于运行小时统计 (#4) */
    uint32_t crc;           /* CRC16 校验 (除本字段外) */
} param_t;

/**
  * @brief  初始化参数模块: 从 Flash 加载, 校验失败则用出厂默认值
  * @note   同时将复位次数 +1 (缓存在 RAM, 不立即写 Flash)
  */
void FlashParam_Init(void);

/**
  * @brief  获取当前参数指针 (RAM 副本, 直接读取)
  * @retval 参数结构体指针
  */
const param_t *FlashParam_Get(void);

/**
  * @brief  修改并保存参数到 Flash
  * @param  slave_addr:  新从机地址 (0 表示不修改)
  * @param  baud_index:  新波特率索引 (0xFF 表示不修改)
  * @param  link_enable: 新通信使能 (0xFF 表示不修改)
  * @param  fail_safe_en: 新故障安全使能 (0xFF 表示不修改)
  * @param  fail_safe_sec: 新故障安全超时秒 (0xFF 表示不修改)
  * @retval 0=成功, -1=参数非法, -2=Flash 写入失败
  */
int8_t FlashParam_Save(uint8_t slave_addr, uint8_t baud_index, uint8_t link_enable,
                       uint8_t fail_safe_en, uint8_t fail_safe_sec);

/**
  * @brief  仅更新复位次数到 Flash (低频调用)
  * @retval 0=成功, -2=失败
  */
int8_t FlashParam_SaveResetCount(void);

/**
  * @brief  恢复出厂默认参数并保存
  */
void FlashParam_ResetDefault(void);

/**
  * @brief  获取累计运行秒数 (#4 运行小时统计)
  * @retval 自设备首次运行以来的累计秒数 (已保存基准 + 本次上电增量)
  * @note   与 Param_UpdateRunSec 配套, 实时准确且断电不丢失。
  */
uint32_t FlashParam_GetRunSeconds(void);

#ifdef __cplusplus
}
#endif
#endif /* __FLASH_PARAM_H */

/**
  ******************************************************************************
  * @file    flash_param.c
  * @brief   Flash 参数掉电保存实现 (内部 Flash 最后一页 0x0803F800)
  *
  * 地址推导:
  *   STM32 片内程序 Flash 起始永远是 0x08000000,
  *   STM32F103RET6/RCT6(256KB Flash == 0x40000,每页 2KB = 0x800)
  *   Flash 末尾边界:0x08000000 + 0x40000 = 0x08040000
  *   最后一页起始   = 0x08040000 - 0x800   = 0x0803F800
  *
  * 模块功能:
  *   存储从机地址 / 波特率 / 链路使能 / 故障安全参数 / 复位次数 / 运行秒基准,
  *   结构含 magic + CRC16 校验防止误读未初始化 Flash。
  *
  * 数据流:
  *   上电: Flash 最后一页 -> memcpy 到 RAM 副本 s_param -> magic + CRC 校验
  *         -> 校验失败则 Param_LoadDefault 填出厂值并回写
  *         -> 范围合法性钳位 -> reset_count +1 -> 立即回写 Flash
  *   运行: 业务层 FlashParam_Get() 直接读 RAM 副本 s_param (零 Flash 访问)
  *   落盘: FlashParam_Save / SaveResetCount / ResetDefault
  *         -> 更新 RAM 副本 -> Param_UpdateRunSec 折算运行秒
  *         -> 重算 CRC -> Param_WriteFlash 擦除并按半字编程
  *   复位次数与运行秒基准在 #4 运行小时统计中跨重启连续累计。
  *
  * 解耦说明:
  *   1. 上层只通过 FlashParam_Get() 拿只读指针,不感知 Flash 地址与 CRC 细节。
  *   2. 全部 Flash 擦写动作收敛在 Param_WriteFlash 一处,便于统一管控擦写寿命。
  *   3. 运行秒折算只在保存时进行 (避免频繁擦写),实时值由 FlashParam_GetRunSeconds
  *      用 "已保存基准 + 本次上电增量" 现算,两者共用基线 s_run_base_uptime。
  ******************************************************************************
  */
#include "bsp_storage.h"
#include "main.h"
#include <string.h>

/* -------------------------- API 快速导航 --------------------------
 * void            FlashParam_Init(void);                 上电加载参数, 复位次数 +1 并回写
 * int8_t          FlashParam_Save(uint8_t slave_addr, uint8_t baud_index,
 *                                 uint8_t link_enable, uint8_t fail_safe_en,
 *                                 uint8_t fail_safe_sec);
 *                                                        修改并落盘 (0 成功 / -1 非法 / -2 写失败)
 * int8_t          FlashParam_SaveResetCount(void);       仅折算运行秒并回写 (0 / -2)
 * void            FlashParam_ResetDefault(void);         恢复出厂 (保留设备履历)
 * const param_t  *FlashParam_Get(void);                  获取 RAM 参数副本只读指针
 * uint32_t        FlashParam_GetRunSeconds(void);        累计运行秒 (基准 + 本次上电增量)
 * ------------------------------------------------------------------ */

/* -------------------------- 区块 2:模块内部静态全局变量 -------------------------- */
/* RAM 参数副本 (运行时使用, 所有 FlashParam_Get 返回其地址) */
static param_t s_param;

/* #4 运行秒基准对应的"上电秒数"基线: 用于增量折算运行小时,
 * 避免每次 Param_UpdateRunSec 把当前上电时长整段重复累加 (修复虚高 bug) */
static uint32_t s_run_base_uptime = 0u;

/* -------------------------- 内部私有函数前置声明 -------------------------- */
static uint16_t Param_CRC16(const uint8_t *data, uint16_t len);
static uint16_t Param_CalcCRC(const param_t *p);
static uint8_t  Param_WriteFlash(const param_t *p);
static void     Param_LoadDefault(param_t *p);
static void     Param_UpdateRunSec(void);

/* -------------------------- 区块 4:对外公共业务 API -------------------------- */
/**
  * @brief  初始化参数模块:从 Flash 加载,校验失败则用出厂默认值
  * @retval 无
  * @note   同时将复位次数 +1 并立即写回 Flash;用当前上电秒数初始化运行小时折算基线。
  *         【关联指引】必须在系统时钟/HAL 初始化之后调用;FlashParam_Get 返回的 s_param 在此填充
  */
void FlashParam_Init(void)
{
    const param_t *flash_param = (const param_t *)PARAM_FLASH_ADDR;

    /* 从 Flash 读取 */
    memcpy(&s_param, flash_param, sizeof(param_t));

    /* 校验 magic + CRC:magic 防止读到未编程的空白 Flash(全 0xFF),
       CRC 防止掉电写坏导致的半截脏数据被当成合法参数 */
    if ((s_param.magic != PARAM_MAGIC) ||
        ((uint16_t)s_param.crc != Param_CalcCRC(&s_param)))
    {
        /* Flash 未编程或损坏, 使用出厂默认值 */
        Param_LoadDefault(&s_param);
        /* 首次写入 (reset_count 此时为 0) */
        Param_WriteFlash(&s_param);
    }

    /* 参数范围合法性检查 */
    if (s_param.slave_addr < SLAVE_ADDR_MIN || s_param.slave_addr > SLAVE_ADDR_MAX)
    {
        s_param.slave_addr = DEFAULT_SLAVE_ADDR;
    }
    if (s_param.baud_index > BAUD_INDEX_MAX)
    {
        s_param.baud_index = DEFAULT_BAUD_INDEX;
    }
    if (s_param.link_enable > 1u)
    {
        s_param.link_enable = DEFAULT_LINK_ENABLE;
    }
    /* #1 故障安全参数合法性检查 */
    if (s_param.fail_safe_en > FAILSAFE_EN_MAX)
    {
        s_param.fail_safe_en = DEFAULT_FAILSAFE_EN;
    }
    /* fail_safe_sec 为 0~255, 任意值均合法(0 表示禁用, 由 en 控制) */

    /* 复位次数 +1 (本次上电) */
    s_param.reset_count++;
    /* 立即写回: STM32F103 Flash 擦写寿命 1 万次, 工业设备每天重启 ≤ 10 次可使用约 2.7 年 */
    s_param.crc = (uint32_t)Param_CalcCRC(&s_param);
    Param_WriteFlash(&s_param);

    /* #4 用当前上电秒数初始化运行小时折算基线, 避免把本次上电时长重复计入 */
    s_run_base_uptime = HAL_GetTick() / 1000u;
}

/**
  * @brief  修改并保存参数到 Flash
  * @param[in] slave_addr:   新从机地址 (0 表示不修改)
  * @param[in] baud_index:   新波特率索引 (0xFF 表示不修改)
  * @param[in] link_enable:  新通信使能 (0xFF 表示不修改)
  * @param[in] fail_safe_en: 新故障安全使能 (0xFF 表示不修改)
  * @param[in] fail_safe_sec:新故障安全超时秒 (0xFF 表示不修改)
  * @retval 0=成功, -1=参数非法, -2=Flash 写入失败
  * @note   先参数校验,再更新 RAM 副本,保存前折算运行时间(#4),最后擦写 Flash。
  *         【关联指引】被 Modbus 06/10 写保持寄存器、RS232 Shell ADDR/BAUD/FS 命令调用
  */
int8_t FlashParam_Save(uint8_t slave_addr, uint8_t baud_index, uint8_t link_enable,
                       uint8_t fail_safe_en, uint8_t fail_safe_sec)
{
    /* 参数校验 */
    /* slave_addr 用 0 作"不修改"哨兵(Modbus 地址 0 是广播地址, 不可作从机地址) */
    if (slave_addr != 0u && (slave_addr < SLAVE_ADDR_MIN || slave_addr > SLAVE_ADDR_MAX))
    {
        return -1;
    }
    if (baud_index != 0xFFu && baud_index > BAUD_INDEX_MAX)
    {
        return -1;
    }
    if (link_enable != 0xFFu && link_enable > 1u)
    {
        return -1;
    }
    if (fail_safe_en != 0xFFu && fail_safe_en > FAILSAFE_EN_MAX)
    {
        return -1;
    }
    /* 255(0xFF) 为"不修改"哨兵,合法取值 0..254,故上限为 0xFEu */
    if (fail_safe_sec != 0xFFu && fail_safe_sec > 0xFEu)
    {
        return -1;
    }

    /* 更新 RAM 副本 (0/0xFF 表示不修改) */
    if (slave_addr != 0u)
    {
        s_param.slave_addr = slave_addr;
    }
    if (baud_index != 0xFFu)
    {
        s_param.baud_index = baud_index;
    }
    if (link_enable != 0xFFu)
    {
        s_param.link_enable = link_enable;
    }
    if (fail_safe_en != 0xFFu)
    {
        s_param.fail_safe_en = fail_safe_en;
    }
    if (fail_safe_sec != 0xFFu)
    {
        s_param.fail_safe_sec = fail_safe_sec;
    }
    /* #4 保存前折算运行时间, 使运行小时跨重启连续 */
    Param_UpdateRunSec();
    s_param.crc = (uint32_t)Param_CalcCRC(&s_param);

    /* 写入 Flash */
    if (Param_WriteFlash(&s_param) != 0u)
    {
        return -2;
    }
    return 0;
}

/**
  * @brief  仅更新复位次数到 Flash(低频调用)
  * @retval 0=成功, -2=失败
  * @note   保存前同样折算运行时间(#4),保持运行小时连续
  */
int8_t FlashParam_SaveResetCount(void)
{
    /* #4 折算运行时间后写回 */
    Param_UpdateRunSec();
    s_param.crc = (uint32_t)Param_CalcCRC(&s_param);
    if (Param_WriteFlash(&s_param) != 0u)
    {
        return -2;
    }
    return 0;
}

/**
  * @brief  恢复出厂默认参数并保存
  * @retval 无
  * @note   保留 reset_count / run_sec_base(属"设备履历",不应因恢复出厂清零)。
  *         【关联指引】被 RS232 Shell FACTORY 命令调用
  */
void FlashParam_ResetDefault(void)
{
    /* 保存当前复位次数与运行时间基准, 恢复出厂后恢复
     * (reset_count / run_sec_base 属于"设备履历", 不应因恢复出厂而清零) */
    uint32_t saved_reset_count = s_param.reset_count;
    uint32_t saved_run_sec     = s_param.run_sec_base;

    Param_LoadDefault(&s_param);
    s_param.reset_count  = saved_reset_count;
    s_param.run_sec_base = saved_run_sec;
    s_param.crc = (uint32_t)Param_CalcCRC(&s_param);
    Param_WriteFlash(&s_param);
}

/**
  * @brief  获取当前参数指针(RAM 副本,直接读取)
  * @retval 参数结构体指针
  * @note   【关联指引】所有业务模块(Modbus/RS232 Shell/RS485)均通过本接口读取从机地址、波特率等配置
  */
const param_t *FlashParam_Get(void)
{
    return &s_param;
}

/**
  * @brief  获取累计运行秒数 (#4 运行小时统计)
  * @retval 自设备首次运行以来的累计秒数 (已保存基准 + 本次上电增量)
  * @note   与 Param_UpdateRunSec 配套: 已保存基准 + 本次上电已流逝增量,
  *         实时准确且断电后不丢失(断电前最后一次保存已折算)。
  *         【关联指引】被 SystemApp_GetRunSeconds / Modbus 输入寄存器 IR_ADDR_RUNHOURS 调用
  */
uint32_t FlashParam_GetRunSeconds(void)
{
    uint32_t now   = HAL_GetTick() / 1000u;
    /* now <= 基线说明 HAL_GetTick 已回绕,增量按 0 处理,保证运行秒不倒退 */
    uint32_t delta = (now > s_run_base_uptime) ? (now - s_run_base_uptime) : 0u;

    return s_param.run_sec_base + delta;
}

/* -------------------------- 区块 5:内部私有底层处理函数 -------------------------- */
/**
  * @brief  软件 CRC16 计算(Modbus 多项式 0xA001,初始 0xFFFF)
  * @param[in] data: 待计算数据指针
  * @param[in] len:  数据长度
  * @retval 16位 CRC 校验值
  * @note   被 Param_CalcCRC 与 Param_WriteFlash 流程调用,为最底层原语
  */
static uint16_t Param_CRC16(const uint8_t *data, uint16_t len)
{
    uint16_t crc = 0xFFFFu;
    uint16_t i, j;

    for (i = 0u; i < len; i++)
    {
        crc ^= (uint16_t)data[i];
        for (j = 0u; j < 8u; j++)
        {
            /* 最低位为 1 才异或多项式,这是 Modbus CRC16 的标准逐位算法 */
            if (crc & 0x0001u)
            {
                crc = (uint16_t)((crc >> 1u) ^ 0xA001u);
            }
            else
            {
                crc >>= 1u;
            }
        }
    }
    return crc;
}

/**
  * @brief  计算参数结构体 CRC(排除末尾 crc 字段)
  * @param[in] p: 参数结构体指针
  * @retval 16位 CRC 校验值
  * @note   长度用 sizeof 自动推导,新增参数字段时无需修改本函数
  */
static uint16_t Param_CalcCRC(const param_t *p)
{
    /* CRC 覆盖 magic ~ reset_count (不含末尾 crc 字段) */
    uint16_t crc_len = (uint16_t)(sizeof(param_t) - sizeof(uint32_t));

    return Param_CRC16((const uint8_t *)p, crc_len);
}

/**
  * @brief  写一页 Flash(擦除 + 按半字编程)
  * @param[in] p: 待写入参数结构体指针
  * @retval 0=成功, 1=擦除失败, 2=编程失败
  * @note   全文件唯一的 Flash 擦写出口;任一步失败都会先 Lock 再返回,
  *         保证不会把 Flash 控制器留在解锁状态
  */
static uint8_t Param_WriteFlash(const param_t *p)
{
    FLASH_EraseInitTypeDef erase;
    uint32_t page_error = 0u;
    HAL_StatusTypeDef st;
    uint16_t i;
    const uint16_t *p16 = (const uint16_t *)p;
    uint16_t words = (uint16_t)((sizeof(param_t) + 1u) / 2u);  /* 半字个数 */

    HAL_FLASH_Unlock();

    /* 擦除一页 */
    erase.TypeErase   = FLASH_TYPEERASE_PAGES;
    erase.Banks       = FLASH_BANK_1;
    erase.PageAddress = PARAM_FLASH_ADDR;
    erase.NbPages     = 1u;
    st = HAL_FLASHEx_Erase(&erase, &page_error);
    if (st != HAL_OK)
    {
        HAL_FLASH_Lock();
        return 1u;
    }

    /* 按半字编程 (F1 最小编程单位) */
    for (i = 0u; i < words; i++)
    {
        st = HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD,
                               PARAM_FLASH_ADDR + (uint32_t)(i * 2u),
                               p16[i]);
        if (st != HAL_OK)
        {
            HAL_FLASH_Lock();
            return 2u;
        }
    }

    HAL_FLASH_Lock();
    return 0u;
}

/**
  * @brief  填充默认参数(出厂值 + 计算 CRC)
  * @param[out] p: 待填充参数结构体指针
  * @retval 无
  * @note   【关联指引】故障安全默认开启(#1),运行小时基准清零(#4);
  *         供首次上电与恢复出厂两处调用
  */
static void Param_LoadDefault(param_t *p)
{
    /* 清零整个结构体 (消除编译器 padding 字节的不确定性) */
    memset(p, 0, sizeof(param_t));
    p->magic         = PARAM_MAGIC;
    p->slave_addr    = DEFAULT_SLAVE_ADDR;
    p->baud_index    = DEFAULT_BAUD_INDEX;
    p->link_enable   = DEFAULT_LINK_ENABLE;
    p->fail_safe_en  = DEFAULT_FAILSAFE_EN;   /* #1 故障安全默认开启 */
    p->fail_safe_sec = DEFAULT_FAILSAFE_SEC;  /* #1 默认 10 秒 */
    p->reset_count   = 0u;
    p->run_sec_base  = 0u;                     /* #4 运行秒基准(累计运行小时) */
    p->crc           = (uint32_t)Param_CalcCRC(p);
}

/**
  * @brief  将本次上电运行时间折算进基准 (#4 运行小时)
  * @retval 无
  * @note   运行小时 = (run_sec_base + 本次上电已流逝增量) / 3600。
  *         仅累加"距上次折算的增量" (now - s_run_base_uptime), 而非整段
  *         HAL_GetTick(), 否则每保存一次参数就把当前上电时长重复计入,
  *         导致运行小时虚高。Flash 仅在参数保存时写入(避免频繁擦写),
  *         故保存前把已流逝的增量累加进 run_sec_base, 跨重启后运行小时连续。
  *         【关联指引】被 FlashParam_GetRunSeconds() 读取同一基线 s_run_base_uptime
  */
static void Param_UpdateRunSec(void)
{
    uint32_t now = HAL_GetTick() / 1000u;

    /* 仅在时间前进时累加,防止 HAL_GetTick 回绕导致基准被倒扣 */
    if (now > s_run_base_uptime)
    {
        s_param.run_sec_base = s_param.run_sec_base + (now - s_run_base_uptime);
    }
    s_run_base_uptime = now;
}

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐