news 2026/8/15 13:10:45

STM32 HAL库FLASH读写实战:从原理到可靠数据存储方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32 HAL库FLASH读写实战:从原理到可靠数据存储方案

1. 项目缘起:为什么需要手动管理FLASH?

在嵌入式开发,尤其是基于STM32这类MCU的项目中,我们常常会遇到一个看似简单却暗藏玄机的需求:如何把一些数据,比如设备的校准参数、用户的配置信息、运行日志或者OTA升级的标记,在系统掉电后依然能保存下来。最直接的想法就是用EEPROM,但对于很多没有内置EEPROM的STM32芯片,或者当我们需要存储的数据量较大时,片内FLASH就成了一个经济且可靠的选择。

然而,直接操作FLASH绝非像读写内存数组那么简单。我最初接触HAL库的FLASH操作时,就踩过不少坑:程序跑着跑着就HardFault了,数据写进去读出来是错的,甚至一不小心把程序代码给擦除了,导致芯片“变砖”。这些经历让我意识到,FLASH操作必须慎之又慎,它有一套严格的“交通规则”。

所以,这份笔记的目的,就是把我使用STM32 HAL库进行FLASH读写过程中积累的经验、踩过的坑和验证过的代码,系统地整理出来。它不仅仅是一份API调用手册,更侧重于理解背后的原理、掌握正确的流程和规避常见的陷阱。无论你是刚开始接触STM32,还是已经有一定经验但被FLASH困扰的开发者,希望这份笔记都能成为你手边一份实用的参考。

2. 理解STM32的FLASH:内存布局与操作特性

在动手写代码之前,我们必须先搞清楚操作对象——STM32的片内FLASH——到底是个什么东西。把它理解成一个特殊的、只能按特定规则访问的存储区域,是成功的第一步。

2.1 FLASH的内存映射

对于STM32F1、F4等系列,FLASH的起始地址通常是0x0800 0000。这是我们程序代码(.text段)存放的地方。当我们通过IDE(如Keil、IAR)下载程序时,烧录器就是把编译生成的二进制文件写入了这个区域。

注意:操作FLASH时,最首要的安全原则就是绝对不要擦写当前正在运行的程序所在的扇区(Sector)。否则会导致指令读取错误,立即引发崩溃。

FLASH的容量因芯片型号而异,比如STM32F103C8T6是64KB,STM32F407ZGT6是1MB。这些容量被划分为若干个大小相等的扇区(Sector)或页(Page)。擦除操作的最小单位就是一个扇区。这意味着,即使你只想修改一个字节,也必须先擦除整个包含该字节的扇区。

以STM32F407为例,其1MB的FLASH前四个扇区是16KB,第五个扇区是64KB,剩下的都是128KB。这个信息至关重要,因为擦除时必须指定正确的扇区编号。

2.2 FLASH的物理特性:擦除与编程

这是FLASH操作的核心约束,可以用两个词概括:先擦后写,按字操作

  1. 擦除(Erase):将目标扇区内的所有位(bit)从01变成1(对于STM32,通常擦除后状态是0xFF)。这是一个相对耗时的操作。
  2. 编程(Program):将擦除后为1的位,根据需要修改为0注意,你只能把1变成0,不能把0变成1(除非再次擦除)。这就是为什么必须先擦除的原因。编程的最小单位通常是“字”(Word,32位,4字节)或“双字”(Double Word,64位,8字节),具体取决于芯片和编程模式。

基于这个特性,一个常见的错误模式是:第一次写入数据0xAA(二进制1010 1010)成功。之后想不经过擦除,直接在同一地址写入0x550101 0101)。结果读出来的会是0x000000 0000)。因为第二次写入时,只能把10,无法把已经为0的位变回10xAA & 0x55 = 0x00

2.3 HAL库的抽象:FLASH_HandleTypeDef

HAL库通过一个名为FLASH_HandleTypeDef的结构体来管理FLASH操作状态。虽然对于简单的擦写,我们可能不直接频繁操作这个句柄,但理解它有助于排查复杂问题。

typedef struct { HAL_LockTypeDef Lock; // 锁,防止多任务/中断并发访问FLASH uint32_t ErrorCode; // 操作错误码,非常重要! uint32_t ProcedureOnGoing; // 内部状态标志 uint32_t Address; // 当前操作地址 uint32_t Bank; // FLASH Bank编号(部分芯片有双Bank) uint32_t Sector; // 当前操作的扇区 uint32_t VoltageRange; // 电压范围,影响编程时间 } FLASH_HandleTypeDef;

其中,ErrorCode成员是我们调试时的关键。当擦写函数返回错误(HAL_ERROR)时,可以通过HAL_FLASH_GetError()或直接检查句柄的ErrorCode来获取具体原因,常见错误有:

  • HAL_FLASH_ERROR_PROG:编程错误(如对齐错误、试图写保护区域)。
  • HAL_FLASH_ERROR_WRP:写保护错误。
  • HAL_FLASH_ERROR_OPTV:选项字节相关错误。

3. 实战:数据存储区的规划与定义

盲目地找一个FLASH地址就开始写是非常危险的。我们需要像城市规划一样,事先划定好“数据区”。

3.1 如何选择安全的存储地址?

基本原则是:避开程序代码区,选择靠后的、足够大的扇区。

方法一:通过链接脚本(.ld / .sct)定义这是最规范、最安全的方法。以Keil MDK(使用分散加载文件.scf)为例,你可以在项目选项中修改链接脚本,或者直接编辑.sct文件。

假设你的芯片有512KB FLASH(0x0800 0000 - 0x0807 FFFF),程序实际用了大概200KB。你可以把最后两个扇区(比如从0x0807 0000开始,共64KB)划给数据存储。

在Keil中,可以通过定义“Additional Code Sections”来实现。更通用的方法是在代码中,根据芯片的FLASH总大小和扇区布局,用#define硬编码一个绝对地址。虽然不够优雅,但对于小型固定项目是可行的。

// STM32F407VE, 512KB FLASH #define FLASH_TOTAL_SIZE (512 * 1024) // 512KB #define FLASH_BASE_ADDRESS (0x08000000UL) // 假设使用最后一个扇区(Sector 11, 128KB)的前32KB存储数据 #define USER_FLASH_START_ADDR (FLASH_BASE_ADDRESS + FLASH_TOTAL_SIZE - 32*1024) // 0x08078000 #define USER_FLASH_END_ADDR (FLASH_BASE_ADDRESS + FLASH_TOTAL_SIZE - 1) // 0x0807FFFF

方法二:使用编译器特性(C语言)GCC编译器允许使用__attribute__((section(".name")))将变量定义到指定的段。你需要在链接脚本中预先定义这个段的地址和大小。这种方法更灵活,但配置稍复杂。

3.2 定义数据结构

在固定地址存储数据,我们通常使用结构体,因为它能保证数据成员的顺序和内存布局。同时,为了便于管理和防止数据损坏,通常会采用“数据头+数据体”的格式,并可能实现简单的磨损均衡或双备份。

typedef struct { uint32_t magicNumber; // 魔数,用于标识数据块是否有效,例如 0xDEADBEEF uint32_t version; // 数据结构版本号,便于未来升级兼容 uint32_t crc32; // 对整个ConfigData的CRC校验值 // 以下是实际的应用数据 uint32_t deviceID; uint8_t networkConfig[32]; float calibrationFactor; // ... 其他配置项 } SystemConfig_t; // 我们的数据在FLASH中的存储形态,就是一个或多个这样的结构体 #define CONFIG_FLASH_ADDR USER_FLASH_START_ADDR

4. HAL库FLASH操作核心流程详解

掌握了原理,规划好了地址,现在进入最关键的实操环节。HAL库的FLASH操作遵循一个标准流程:解锁 -> 擦除 -> 编程 -> 上锁。

4.1 第一步:解锁FLASH(HAL_FLASH_Unlock)

FLASH接口默认是上锁的,以防止误操作。任何擦写操作前必须解锁。

HAL_StatusTypeDef status = HAL_FLASH_Unlock(); if (status != HAL_OK) { // 解锁失败,可能是硬件错误或FLASH处于忙状态 Error_Handler(); }

内部发生了什么?实际上,这个函数会向FLASH的密钥寄存器(FLASH_KEYR)依次写入两个特定的密钥(KEY1和KEY2)。如果写入顺序或值错误,FLASH会保持锁定,并可能触发总线错误。

4.2 第二步:擦除指定扇区(HAL_FLASHEx_Erase)

这是最可能出错的步骤之一。你需要正确配置一个擦除初始化结构体FLASH_EraseInitTypeDef

FLASH_EraseInitTypeDef EraseInitStruct; uint32_t SectorError = 0; // 用于接收擦除失败的扇区号 // 1. 填充擦除参数 EraseInitStruct.TypeErase = FLASH_TYPEERASE_SECTORS; // 按扇区擦除 EraseInitStruct.Banks = FLASH_BANK_1; // 对于单Bank芯片,就是BANK_1 // 最关键的两个参数:从哪个扇区开始,擦除几个扇区。 EraseInitStruct.Sector = GetSector(USER_FLASH_START_ADDR); // 需要自己实现GetSector函数 EraseInitStruct.NbSectors = 1; // 我们只擦除一个扇区 EraseInitStruct.VoltageRange = FLASH_VOLTAGE_RANGE_3; // 电压范围,通常为3(对应2.7V-3.6V) // 2. 执行擦除 status = HAL_FLASHEx_Erase(&EraseInitStruct, &SectorError); if (status != HAL_OK) { // 擦除失败,可以通过SectorError和HAL_FLASH_GetError()诊断 printf(“Erase failed at Sector: %lu, Error: 0x%08lX\r\n”, SectorError, HAL_FLASH_GetError()); HAL_FLASH_Lock(); // 失败后记得重新上锁 return; }

GetSector函数的实现:这个函数需要你根据芯片数据手册的FLASH扇区映射表来编写。例如对于STM32F407:

static uint32_t GetSector(uint32_t Address) { uint32_t sector = 0; if((Address < 0x08004000) && (Address >= 0x08000000)) sector = FLASH_SECTOR_0; else if((Address < 0x08008000) && (Address >= 0x08004000)) sector = FLASH_SECTOR_1; else if((Address < 0x0800C000) && (Address >= 0x08008000)) sector = FLASH_SECTOR_2; else if((Address < 0x08010000) && (Address >= 0x0800C000)) sector = FLASH_SECTOR_3; // ... 以此类推,直到最后一个扇区 else if((Address < 0x08100000) && (Address >= 0x080E0000)) sector = FLASH_SECTOR_11; // F407最后一个128KB扇区 else sector = 0xFFFFFFFF; // 无效地址 return sector; }

4.3 第三步:编程(写入)数据(HAL_FLASH_Program)

擦除成功后,扇区内容全为0xFF。现在可以写入数据了。HAL库提供了不同数据宽度的编程函数。

// 假设我们要写入一个32位的魔数到地址CONFIG_FLASH_ADDR uint32_t magic = 0xDEADBEEF; status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, // 编程类型:按字(32位)编程 CONFIG_FLASH_ADDR, // 目标地址 magic); // 要写入的数据 if (status != HAL_OK) { printf(“Program WORD failed at 0x%08lX\r\n”, CONFIG_FLASH_ADDR); HAL_FLASH_Lock(); return; } // 写入一个64位的数据(双字) uint64_t bigNumber = 0x1122334455667788ULL; status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, CONFIG_FLASH_ADDR + 4, bigNumber);

关于地址对齐的致命细节

  • FLASH_TYPEPROGRAM_WORD要求目标地址必须4字节对齐(地址的低2位为0)。
  • FLASH_TYPEPROGRAM_DOUBLEWORD要求目标地址必须8字节对齐(地址的低3位为0)。
  • FLASH_TYPEPROGRAM_HALFWORD(16位) 要求2字节对齐。 如果不对齐,编程操作会失败,并设置HAL_FLASH_ERROR_PROG错误码。这是新手最常踩的坑之一。在定义你的数据结构时,要使用__attribute__((aligned(8)))__packed等关键字仔细处理对齐问题。

4.4 第四步:重新上锁FLASH(HAL_FLASH_Lock)

所有操作完成后,务必上锁。这是一个好习惯,可以防止后续代码(或异常跳转)误操作FLASH。

HAL_FLASH_Lock();

5. 封装与优化:一个健壮的FLASH读写管理器

将上述步骤简单组合起来,只能算“能用”。要“好用”和“可靠”,我们需要进行封装,并加入错误处理、数据校验等机制。

5.1 封装核心函数

我们可以创建一个flash_manager.c/.h文件,提供清晰的接口。

// flash_manager.h typedef enum { FLASH_OK = 0, FLASH_ERR_UNLOCK, FLASH_ERR_ERASE, FLASH_ERR_WRITE, FLASH_ERR_ADDR, FLASH_ERR_CRC, FLASH_ERR_VERSION } Flash_Status_t; Flash_Status_t Flash_WriteConfig(SystemConfig_t *config); Flash_Status_t Flash_ReadConfig(SystemConfig_t *config); Flash_Status_t Flash_EraseSector(uint32_t sector_num);

5.2 加入数据校验(CRC)

直接信任FLASH中读出的数据是危险的。位翻转(虽然概率低)或程序异常写入都可能导致数据错误。因此,写入时计算CRC并存储,读取时重新计算并比对,是必不可少的环节。

// 在Flash_WriteConfig中,写入前计算CRC config->crc32 = calculate_crc32((uint8_t*)config + 8, sizeof(SystemConfig_t) - 8); // 假设magic和version在前8字节,不参与CRC // 然后执行擦除和编程操作,将整个config结构体写入FLASH // 在Flash_ReadConfig中,读取后验证CRC SystemConfig_t tempConfig; memcpy(&tempConfig, (void*)CONFIG_FLASH_ADDR, sizeof(SystemConfig_t)); uint32_t calculatedCrc = calculate_crc32((uint8_t*)&tempConfig + 8, sizeof(SystemConfig_t) - 8); if (tempConfig.magicNumber != EXPECTED_MAGIC) { return FLASH_ERR_VERSION; // 魔数不对,数据无效 } if (calculatedCrc != tempConfig.crc32) { return FLASH_ERR_CRC; // CRC校验失败,数据损坏 } // 校验通过,拷贝到输出参数 memcpy(config, &tempConfig, sizeof(SystemConfig_t)); return FLASH_OK;

5.3 实现简单的磨损均衡

FLASH每个扇区有擦写次数限制(通常10万次左右)。如果频繁更新同一个变量,会导致该扇区提前损坏。一个简单的策略是“双备份”:

  • 在存储区内划分两个大小相等的区域:Slot A和Slot B。
  • 每次更新数据时,写入到当前未使用的那个Slot。
  • 每个数据块都带有一个递增的序列号或时间戳。
  • 读取时,选择序列号最大的有效数据块。 这样,擦写次数就在两个扇区间分摊,寿命延长一倍。对于更复杂的场景,可以使用日志结构文件系统(如LittleFS)的思想。

5.4 中断与并发安全

关键点:在FLASH擦写期间,必须禁止所有中断!因为FLASH擦写期间,CPU无法从FLASH取指执行代码。如果此时发生中断,CPU试图跳转到中断向量表(也位于FLASH)去执行中断服务程序,就会导致总线错误和系统死锁。

HAL库的擦写函数内部通常会处理这个问题(操作前关中断,操作后恢复)。但为了绝对安全,尤其是在RTOS或复杂中断环境中,最好在调用擦写函数的外层手动加锁。

__disable_irq(); // 关总中断 Flash_Status_t status = Flash_WriteConfig(&myConfig); __enable_irq(); // 开总中断

在RTOS中,你可能还需要使用互斥锁(Mutex)来防止多个任务同时调用FLASH操作函数。

6. 深度排坑:那些让你抓狂的常见问题

即使按照流程操作,依然会遇到各种奇怪的问题。这里汇总了我遇到过的典型坑位。

6.1 “Error: Flash Download Failed - Cortex-Mx”

这个错误通常发生在通过调试器(如ST-Link, ULINK2)下载程序时,而不是在运行时。但它和我们的FLASH操作知识相关。

  • 原因1:FLASH保护未解除。你可能之前通过代码或选项字节(Option Bytes)开启了读保护(RDP)或写保护(WRP)。解决方法是:
    1. 使用STM32CubeProgrammer或ST-LINK Utility连接芯片,解除保护。
    2. 检查代码中是否误操作了选项字节。
  • 原因2:调试器配置错误。检查IDE中的Debug配置:
    1. 芯片型号是否选对?
    2. Flash Download配置里的算法文件(.FLM)是否正确?算法文件必须和你的芯片FLASH容量及扇区布局匹配。
    3. Reset and Run选项是否勾选?有时不勾选会导致下载后无法启动。
  • 原因3:硬件问题。Boot引脚(BOOT0/BOOT1)电平不对,导致芯片进入了系统存储器启动模式,而非用户FLASH启动。确保BOOT0为低电平(接GND)。电源不稳定也可能导致编程失败。

6.2 数据写入成功,但读出来是错的或全0

  • 检查地址对齐:如上所述,这是头号嫌犯。确保你写入的地址符合编程数据宽度的对齐要求。
  • 检查擦除操作:写入前,你真的成功擦除了那个扇区吗?在擦除函数后打印错误码,并读取目标地址内容,看是否全为0xFF。
  • 检查数据宽度:你调用HAL_FLASH_Program时,TypeProgram参数和你传入的数据类型、目标地址是否匹配?用32位函数去写一个8位变量会导致问题。
  • 检查编译器优化:某些激进的编译器优化可能会重排或消除你的FLASH操作代码。对于关键的FLASH操作函数或变量,可以尝试使用volatile关键字,或者将优化等级暂时调低(如-O0)进行测试。
  • 检查内存访问:直接使用*(uint32_t*)addr的方式读取FLASH是没问题的,因为FLASH是内存映射的。但确保你的地址是有效的FLASH地址。

6.3 操作FLASH后,程序跑飞或触发HardFault

  • 中断问题:这是最大可能。确保在擦写期间禁止了中断。检查是否在中断服务程序(ISR)中调用了FLASH操作函数?这非常危险。
  • 操作了代码区:你擦写的扇区,是否包含了当前正在运行的代码?哪怕是几行无关的库函数,也不行。务必仔细核对地址。
  • 堆栈溢出:FLASH操作函数可能会使用较多栈空间。如果堆栈设置得太小,可能导致溢出并破坏内存,进而引发HardFault。适当增大堆栈大小(在启动文件.s中修改)。
  • Cache问题(针对Cortex-M7等带Cache的芯片):在操作FLASH后,如果指令或数据Cache中缓存了旧地址的内容,可能导致CPU读到旧数据。需要在操作前后进行Cache的清理(Clean)和无效化(Invalidate)操作。例如使用SCB_CleanInvalidateDCache()函数。

6.4 选项字节(Option Bytes)误操作

选项字节是一片特殊的FLASH区域,用于配置芯片的读写保护、看门狗、复位模式等。误操作选项字节可能导致芯片无法下载或启动。

  • 除非你非常清楚在做什么,否则不要用代码去修改选项字节。
  • 如果必须修改,务必遵循数据手册的严格流程,通常包括解锁、擦除、编程、上锁,并且需要立即执行系统复位(HAL_FLASH_OB_Launch())以使新配置生效。
  • 修改选项字节前,最好先用STM32CubeProgrammer读取并备份当前配置。

7. 进阶话题:与HAL库的LL库、标准外设库对比

你可能会听到“HAL效率低,LL库或标准库更好”的说法。在FLASH操作这个具体场景下,我们来分析一下。

  • 标准外设库(Standard Peripheral Library, SPL):已停产,但很多老项目在用。它的FLASH操作函数(如FLASH_ErasePage,FLASH_ProgramWord)是直接操作寄存器,代码量小,效率高。但错误处理机制相对简单,且不同芯片系列的API可能有差异。
  • HAL库:提供了统一的、跨系列的API,错误处理完善(通过句柄的ErrorCode),并且集成了超时管理、状态机等,安全性更高,但带来了额外的代码体积和运行时开销(如状态检查、超时等待)。对于FLASH这种不频繁的操作,这点开销通常可以接受。
  • LL库(Low-Layer):可以看作是HAL库的“底层驱动”部分。它提供了接近寄存器操作的宏和函数,效率高,同时又保持了跨系列的一致性。例如,LL库提供了LL_FLASH_Program_DoubleWord这样的函数。你可以混合使用HAL和LL,例如用HAL进行解锁/上锁和错误管理,用LL库执行具体的擦写指令,以兼顾安全性和效率。
// 混合使用HAL和LL的示例 HAL_FLASH_Unlock(); // 使用LL库进行快速编程 LL_FLASH_Program_DoubleWord(addr, data64); // 检查操作是否完成(替代HAL的轮询等待) while(LL_FLASH_IsActiveFlag_BSY()) {}; // 检查错误标志 if(LL_FLASH_IsActiveFlag_PGERR()) { // 处理编程错误 } HAL_FLASH_Lock();

选择哪个库,取决于项目需求、团队习惯和对效率的极致追求。对于大多数应用,纯HAL库完全足够且更省心。

8. 实战案例:实现一个系统配置参数的掉电保存功能

让我们用一个完整的、可复用的例子来串联所有知识点。目标:将系统配置(网络参数、校准值等)保存在STM32F407的最后一个扇区。

步骤1:定义数据结构和存储地址

// flash_config.h #include “stdint.h” #include “crc32.h” // 假设你有CRC32计算库 #define FLASH_USER_CONFIG_SECTOR FLASH_SECTOR_11 // F407最后一个扇区 #define FLASH_USER_CONFIG_ADDR 0x080E0000UL // Sector 11的起始地址 #define CONFIG_MAGIC_NUMBER 0xCONFIG01 // 自定义魔数 #pragma pack(push, 1) // 按1字节对齐,避免结构体空洞导致地址计算错误 typedef struct { uint32_t magic; uint32_t version; uint32_t crc; // 实际配置数据 char ssid[32]; char password[64]; uint32_t ip_address; float adc_gain; float adc_offset; } sys_config_t; #pragma pack(pop)

步骤2:实现FLASH管理核心

// flash_manager.c #include “flash_manager.h” #include “stm32f4xx_hal.h” static uint32_t get_sector(uint32_t addr) { // ... 实现F407的地址到扇区号转换函数(见前文) } Flash_Status_t Flash_WriteConfig(sys_config_t *config) { HAL_StatusTypeDef hal_status; FLASH_EraseInitTypeDef erase_init; uint32_t sector_error; uint32_t *src_ptr = (uint32_t*)config; uint32_t addr = FLASH_USER_CONFIG_ADDR; uint32_t size_words = (sizeof(sys_config_t) + 3) / 4; // 计算需要写入的32位字数,向上取整 // 1. 计算并填充CRC config->crc = calculate_crc32((uint8_t*)config + 8, sizeof(sys_config_t) - 8); config->magic = CONFIG_MAGIC_NUMBER; config->version = 1; // 2. 解锁FLASH hal_status = HAL_FLASH_Unlock(); if (hal_status != HAL_OK) return FLASH_ERR_UNLOCK; // 3. 擦除目标扇区 erase_init.TypeErase = FLASH_TYPEERASE_SECTORS; erase_init.Sector = FLASH_USER_CONFIG_SECTOR; erase_init.NbSectors = 1; erase_init.VoltageRange = FLASH_VOLTAGE_RANGE_3; hal_status = HAL_FLASHEx_Erase(&erase_init, §or_error); if (hal_status != HAL_OK) { HAL_FLASH_Lock(); return FLASH_ERR_ERASE; } // 4. 按字(32位)编程数据 for(uint32_t i = 0; i < size_words; i++) { hal_status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr + (i * 4), src_ptr[i]); if (hal_status != HAL_OK) { HAL_FLASH_Lock(); return FLASH_ERR_WRITE; } } // 5. 上锁FLASH HAL_FLASH_Lock(); return FLASH_OK; } Flash_Status_t Flash_ReadConfig(sys_config_t *config) { sys_config_t *flash_config = (sys_config_t*)FLASH_USER_CONFIG_ADDR; // 1. 检查魔数 if (flash_config->magic != CONFIG_MAGIC_NUMBER) { return FLASH_ERR_VERSION; } // 2. 计算并校验CRC uint32_t calc_crc = calculate_crc32((uint8_t*)flash_config + 8, sizeof(sys_config_t) - 8); if (calc_crc != flash_config->crc) { return FLASH_ERR_CRC; } // 3. 拷贝数据到输出参数 memcpy(config, flash_config, sizeof(sys_config_t)); return FLASH_OK; }

步骤3:在主程序中安全调用

// main.c sys_config_t g_system_config; void load_configuration(void) { Flash_Status_t status = Flash_ReadConfig(&g_system_config); if (status != FLASH_OK) { printf(“No valid config in flash, using defaults.\r\n”); // 初始化默认配置 strcpy(g_system_config.ssid, “Default_SSID”); g_system_config.adc_gain = 1.0f; // ... // 尝试保存默认配置 save_configuration(); } } void save_configuration(void) { __disable_irq(); // 关键操作,关闭中断 Flash_Status_t status = Flash_WriteConfig(&g_system_config); __enable_irq(); if (status == FLASH_OK) { printf(“Configuration saved successfully.\r\n”); } else { printf(“Failed to save config, error: %d\r\n”, status); // 此处应根据错误码进行相应处理,如重试、告警等 } } int main(void) { // HAL初始化... load_configuration(); while(1) { // 主循环 if (/* 配置发生改变的条件 */) { save_configuration(); } } }

这个案例展示了从定义到读写的完整流程,包含了错误处理、数据校验和中断安全。你可以将它作为模板,根据自己芯片的型号和具体需求进行调整。记住,理解原理比复制代码更重要,清楚了每一步为什么这么做,你才能灵活应对各种变化和问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/15 13:08:51

AI大模型与数学 第34课 多元复合偏导数:多变量链式求导(10道AI梯度核心计算题)

课程前言 一元复合链式是单层网络梯度&#xff0c;多元复合链式法则是大模型反向传播的底层核心。 一元函数是一个因素对结果的影响&#xff0c;多元复合链式法则是多个因素相互作用对结果的影响。 我们为了寻求最佳结果&#xff0c;必须对影响结果因素的相互作用进行计算求导&…

作者头像 李华
网站建设 2026/8/15 13:08:33

Magpie:让旧软件和老游戏重获清晰的窗口超分辨率工具

Magpie&#xff1a;让旧软件和老游戏重获清晰的窗口超分辨率工具 【免费下载链接】Magpie A general-purpose window upscaler for Windows 10/11. 项目地址: https://gitcode.com/gh_mirrors/mag/Magpie 朋友那台 2015 年的笔记本&#xff0c;跑着一款 2D 老 RPG。游戏…

作者头像 李华
网站建设 2026/8/15 13:07:42

宝塔 Redis 安全配置和可用性完整检查步骤

宝塔 Redis 安全配置 可用性完整检查步骤 硬件 / 网络 / 系统 一一、阿里云 ECS 本机基础检查 1. ECS 控制台检查&#xff08;阿里云网页后台&#xff09; 实例状态&#xff1a;运行中&#xff0c;CPU / 内存 / 磁盘使用率是否长期飙高&#xff08;负载高会导致若依接口超…

作者头像 李华
网站建设 2026/8/15 13:05:48

如何快速提取虚幻引擎游戏资源:UEViewer 实用入门指南

如何快速提取虚幻引擎游戏资源&#xff1a;UEViewer 实用入门指南 【免费下载链接】UEViewer Viewer and exporter for Unreal Engine 1-4 assets (UE Viewer). 项目地址: https://gitcode.com/gh_mirrors/ue/UEViewer 当你打开一款游戏的安装目录&#xff0c;看到满屏的…

作者头像 李华
网站建设 2026/8/15 13:03:34

从502错误到网络协议:TCP/IP与HTTP实战解析与故障排查指南

1. 从一次“502 Bad Gateway”说起&#xff1a;为什么我们绕不开网络协议那天下午&#xff0c;我正在调试一个微服务间的接口调用。本地环境一切正常&#xff0c;信心满满地部署到测试服务器后&#xff0c;前端页面却弹出了一个刺眼的错误提示&#xff1a;unexpected status 50…

作者头像 李华
网站建设 2026/8/15 12:58:14

Python Requests自动化脚本实现微信小程序抢号:从HTTP请求分析到实战部署

1. 从“抢号”需求到技术方案的思考路径最近在帮朋友处理一个挺有意思的需求&#xff1a;他需要定期去某个微信小程序上“抢”一个预约号&#xff0c;这个号源非常紧张&#xff0c;几乎每次都是秒光。手动操作不仅费时费力&#xff0c;成功率还极低。他问我有没有什么“黑科技”…

作者头像 李华