
1. 项目概述为什么需要读写内部FLASH与读取芯片ID在嵌入式开发尤其是基于STM32这类MCU的项目中读写内部FLASH和读取芯片唯一IDUID是两项非常基础但又至关重要的技能。这不仅仅是技术文档里的一个章节更是项目从原型走向产品化过程中绕不开的实际需求。我遇到过不少开发者在项目初期只关注功能实现等到需要做生产批次管理、软件加密、参数存储或者在线升级IAP时才回头来补课往往手忙脚乱。简单来说内部FLASH就是STM32芯片内置的“硬盘”。除了存放我们编译好的程序代码只读它还有一部分剩余空间可以被我们像操作EEPROM一样用来存储那些掉电不能丢失的数据比如设备的校准参数、用户配置、运行日志或者IAP升级时的备份程序。而芯片的唯一ID则是每一片STM32与生俱来的“身份证号”全球唯一。这个ID在防止软件被非法拷贝、实现设备身份认证、生产追溯以及生成唯一的加密密钥等场景下是不可或缺的。网上很多教程只给代码不讲为什么这么操作也不提操作背后的风险。比如直接擦除FLASH导致程序跑飞或者在不了解FLASH寿命的情况下频繁写入导致产品提前失效。这篇文章我将结合自己踩过的坑从原理到实操详细拆解如何在STM32上安全、高效地完成这两项任务并分享那些数据手册里不会写的注意事项。2. 核心原理与硬件基础剖析2.1 STM32内部FLASH的架构与特性STM32的内部FLASH存储器其物理结构和操作特性与我们熟悉的RAM或EEPROM有本质区别。理解这些区别是安全操作的前提。首先FLASH存储器是以“页”Page或“扇区”Sector为单位进行管理的。对于不同系列的STM32这个最小擦除单位的大小不同。例如STM32F1系列通常是1KB或2KB一页而STM32F4/F7/H7系列则可能以扇区划分大小从16KB到128KB甚至更大。最关键的原则是FLASH在写入前必须先擦除。而擦除的最小单位就是一个页或扇区擦除后的状态是所有位为‘1’即0xFF。写入操作只能将‘1’变成‘0’而不能将‘0’变回‘1’。这意味着如果你想修改某个地址已经写入的数据必须把整个它所在的页擦掉重写。其次FLASH有写入寿命限制。通常STM32内部FLASH的典型擦写寿命是1万次10k cycles。这个次数是针对同一个存储单元而言的。如果你频繁地在同一个地址更新数据很快就会达到寿命极限导致该区域数据不可靠。因此在实际应用中我们常采用“磨损均衡”的策略比如将数据在多个地址间轮转存储。再者对FLASH进行编程写入或擦除操作时内核必须从RAM中执行指令而不能从正在被操作的FLASH区域取指。这是因为操作本身会挂起对FLASH的访问。所以相关的库函数如HAL_FLASH_Program需要被链接到RAM中执行或者确保CPU在此期间从其他存储器如RAM运行代码。2.2 芯片唯一IDUnique Device ID的来源与用途每一颗STM32芯片在出厂时都被激光刻录了一个96位12字节的唯一ID。这个ID是固化在芯片硅片上的不可更改全球唯一。对于STM32F1系列这个ID的起始地址通常是0x1FFFF7E8对于F4系列是0x1FFF7A10。具体地址需要查阅对应型号的参考手册Reference Manual。这个96位ID的用途非常广泛软件加密/版权保护在程序启动时读取芯片ID与程序中预存的合法ID列表或通过该ID计算出的密钥进行比对。如果不匹配则拒绝运行或进入功能受限模式。这是防止产品固件被简单复制到其他芯片上运行的有效手段。生成唯一密钥在安全应用中可以将这个唯一的ID作为种子通过加密算法如AES, HMAC生成设备唯一的密钥用于通信加密或数据签名。生产追溯与设备标识在设备出厂时可以将芯片ID与产品序列号、生产批次等信息绑定存入数据库。便于后期质量追踪、维修和服务。网络节点地址在如CAN、LoRa等网络应用中可以用芯片ID的一部分作为设备的唯一物理地址避免地址冲突。注意虽然ID是唯一的但直接明文使用它并不安全。在防抄板场景中高级的做法是使用ID作为输入结合一个只有你知道的“盐值”Salt和复杂的哈希算法生成一个校验码。程序运行时动态计算并比对这样即使别人反汇编了你的代码也难以直接提取出有效的ID列表。2.3 操作FLASH的关键风险与硬件约束在动手写代码之前必须清醒认识到以下风险中断与代码位置在执行FLASH擦写操作期间必须禁止所有中断__disable_irq()因为任何中断服务程序的执行都可能需要从FLASH取指这会导致操作失败甚至芯片锁死。同时执行擦写操作的函数代码本身必须位于RAM中。HAL库通常已经处理了这一点但如果你是自己写的底层驱动或者使用标准外设库需要特别注意。一个简单的验证方法是在调试时查看调用擦写函数时的PC指针地址如果地址位于0x200xxxxxRAM区域则是正确的。操作时序与解锁STM32的FLASH控制器有一个锁机制FLASH_CR寄存器中的LOCK位上电后默认是锁定的以防止误操作。在进行任何编程或擦除操作前必须先向特定的密钥寄存器FLASH_KEYR写入两个解锁密钥KEY1和KEY2来解锁。HAL库提供了HAL_FLASH_Unlock()函数来完成这个操作。操作完成后最好再调用HAL_FLASH_Lock()上锁。电源稳定性FLASH擦写对电源电压非常敏感。必须在芯片规定的电压范围内通常就是正常工作电压进行操作。在电池供电且电压可能波动的系统中在擦写前检查电源电压是一个好习惯。跨页/扇区写入如果你要存储的数据结构大小超过了一个页你需要精心设计存储策略。不能简单地连续写入因为当写到页边界时继续写入下一个地址实际上属于另一个页而那个页如果没有被擦除写入会失败。通常需要先计算好数据所占用的页范围然后一次性擦除所有这些页再按顺序写入。3. 软件设计与驱动实现详解3.1 开发环境与工程配置要点本项目以STM32CubeIDE开发环境、HAL库为基础进行讲解。这些概念同样适用于Keil MDK或IAR EWARM只是具体的工程配置选项略有不同。首先在STM32CubeMX中初始化你的芯片时不需要为内部FLASH操作专门配置任何外设。FLASH控制器是内核相关的其驱动已经包含在HAL库中。你需要关注的是工程配置链接脚本.ld文件这是重中之重。默认的链接脚本会把所有代码和数据都放到FLASH中。我们需要手动划分出一块区域专门用于存储用户数据避免与程序代码区域冲突。 打开工程的STM32XXXXXX_FLASH.ld文件XXXXXX代表你的芯片型号找到MEMORY部分。你会看到类似下面的定义MEMORY { RAM (xrw) : ORIGIN 0x20000000, LENGTH 128K FLASH (rx) : ORIGIN 0x8000000, LENGTH 1024K }我们需要修改FLASH的定义将其分为两部分。假设我们的芯片有1024K FLASH我们打算将最后16K0xFC000 - 0xFFFFF作为数据存储区可以这样修改MEMORY { RAM (xrw) : ORIGIN 0x20000000, LENGTH 128K FLASH (rx) : ORIGIN 0x8000000, LENGTH 1008K /* 1024K - 16K */ DATA_FLASH (rx) : ORIGIN 0x80FC000, LENGTH 16K /* 最后16K */ }然后在SECTIONS部分你需要定义一个专门的段来映射到这个DATA_FLASH区域。但更常见的简单做法是不修改链接脚本而是在代码中直接使用一个绝对地址只要确保这个地址位于程序代码占用的范围之后并且是页对齐的。我推荐后一种方法因为它更简单直观适合中小项目。例如通过查看编译生成的map文件知道程序代码用到了0x8000000到0x801A000那么我们可以选择0x801C000作为数据存储起始地址。编译器优化对于从FLASH读取数据到变量特别是读取芯片ID的代码建议将被操作的变量声明为volatile防止编译器进行激进的优化导致读取不到最新值或产生奇怪的错误。3.2 FLASH读写驱动层代码实现我们将把FLASH操作封装成一个独立的模块flash_ops.c/h提高代码的复用性和可维护性。首先在头文件中定义一些关键参数和函数接口// flash_ops.h #ifndef __FLASH_OPS_H #define __FLASH_OPS_H #include “main.h” // 包含必要的HAL头文件 // 根据你的芯片型号定义FLASH参数以STM32F407VG1MB FLASH为例 #define FLASH_START_ADDR 0x08000000UL #define FLASH_END_ADDR 0x080FFFFFUL #define FLASH_PAGE_SIZE 0x4000UL // F407的扇区0大小为16KB #define FLASH_DATA_BASE_ADDR 0x080FC000UL // 假设使用最后16KB作为数据区 #define FLASH_DATA_END_ADDR (FLASH_DATA_BASE_ADDR 0x4000UL - 1) // 错误码定义 typedef enum { FLASH_OK 0, FLASH_ERROR_UNLOCK, FLASH_ERROR_ERASE, FLASH_ERROR_WRITE, FLASH_ERROR_ADDR, FLASH_ERROR_LOCK } Flash_Status_t; // 函数声明 Flash_Status_t FLASH_Erase_Sector(uint32_t sector_addr); Flash_Status_t FLASH_Write_Word(uint32_t addr, uint64_t data); Flash_Status_t FLASH_Write_Buffer(uint32_t addr, uint8_t *pData, uint32_t size); void FLASH_Read_Buffer(uint32_t addr, uint8_t *pBuffer, uint32_t size); uint32_t FLASH_Read_ChipID(void); // 读取芯片唯一ID这里以32位示例实际是96位 #endif接下来是实现文件中的关键函数。请注意以下代码包含了重要的安全操作逻辑和错误处理// flash_ops.c #include “flash_ops.h” #include string.h // 用于memcpy // 辅助函数获取地址所在的扇区号针对STM32F4系列其他系列需调整逻辑 static uint32_t Get_Sector(uint32_t addr) { uint32_t sector 0; if (addr 0x08004000) sector 0; else if (addr 0x08008000) sector 1; else if (addr 0x0800C000) sector 2; // ... 根据你的芯片手册补充所有扇区 else if (addr 0x080E0000) sector 11; else sector 12; // 最后128KB的扇区 return sector; } /** * brief 擦除指定地址所在的整个扇区 * param sector_addr: 扇区内的任意一个地址 * retval Flash_Status_t */ Flash_Status_t FLASH_Erase_Sector(uint32_t sector_addr) { HAL_StatusTypeDef hal_status; FLASH_EraseInitTypeDef EraseInitStruct; uint32_t SectorError 0; // 1. 地址合法性检查必须在数据区内 if (sector_addr FLASH_DATA_BASE_ADDR || sector_addr FLASH_DATA_END_ADDR) { return FLASH_ERROR_ADDR; } // 2. 解锁FLASH HAL_FLASH_Unlock(); // 3. 配置擦除参数 EraseInitStruct.TypeErase FLASH_TYPEERASE_SECTORS; EraseInitStruct.Banks FLASH_BANK_1; // 对于单Bank芯片 EraseInitStruct.Sector Get_Sector(sector_addr); EraseInitStruct.NbSectors 1; // 每次只擦除一个扇区安全 EraseInitStruct.VoltageRange FLASH_VOLTAGE_RANGE_3; // 根据芯片电压选择3表示2.7V-3.6V // 4. 执行擦除并关闭所有中断以确保操作原子性 __disable_irq(); hal_status HAL_FLASHEx_Erase(EraseInitStruct, SectorError); __enable_irq(); // 5. 检查擦除结果 if (hal_status ! HAL_OK) { // 可以在这里记录SectorError的具体值用于调试 HAL_FLASH_Lock(); return FLASH_ERROR_ERASE; } // 6. 上锁FLASH也可以留到所有写操作完成后统一上锁 HAL_FLASH_Lock(); return FLASH_OK; } /** * brief 写入一个双字64位到指定地址 * note STM32的FLASH编程以双字64位或字32位取决于系列为单位。 * 地址必须8字节对齐对于双字编程。 * param addr: 写入地址必须8字节对齐 * param data: 要写入的64位数据 * retval Flash_Status_t */ Flash_Status_t FLASH_Write_Word(uint32_t addr, uint64_t data) { // 地址对齐和范围检查 if ((addr 0x7) ! 0) return FLASH_ERROR_ADDR; // 地址不是8的倍数 if (addr FLASH_DATA_BASE_ADDR || addr FLASH_DATA_END_ADDR) return FLASH_ERROR_ADDR; HAL_FLASH_Unlock(); __disable_irq(); HAL_StatusTypeDef status HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr, data); __enable_irq(); HAL_FLASH_Lock(); return (status HAL_OK) ? FLASH_OK : FLASH_ERROR_WRITE; } /** * brief 写入一段数据缓冲区 * param addr: 起始地址建议8字节对齐以获得最佳性能 * param pData: 源数据缓冲区指针 * param size: 数据大小字节 * retval Flash_Status_t * note 这是一个简化版本假设目标区域已被擦除且数据大小是8字节的整数倍。 * 实际应用中需要处理非对齐和非整数倍的情况。 */ Flash_Status_t FLASH_Write_Buffer(uint32_t addr, uint8_t *pData, uint32_t size) { if (addr FLASH_DATA_BASE_ADDR || (addr size) FLASH_DATA_END_ADDR) { return FLASH_ERROR_ADDR; } HAL_FLASH_Unlock(); __disable_irq(); uint32_t *pSrc (uint32_t*)pData; uint32_t words_to_write (size 3) / 4; // 计算需要写入的32位字数向上取整 // 注意更严谨的做法是按芯片支持的最小编程单位双字或字来操作 // 这里以字32位编程为例适用于STM32F1等系列 for (uint32_t i 0; i words_to_write; i) { // 计算当前要写入的地址 uint32_t write_addr addr i * 4; // 确保地址不越界对于最后不足4字节的部分 uint32_t data_word; if ((i words_to_write - 1) (size % 4 ! 0)) { // 处理最后不足4字节的情况将剩余的字节拷贝到一个32位变量未使用的部分保持0xFF已擦除状态 uint8_t last_bytes size % 4; data_word 0xFFFFFFFF; // 默认全1 memcpy(data_word, pSrc i, last_bytes); // 只拷贝有效的字节 } else { data_word pSrc[i]; } HAL_StatusTypeDef status HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, write_addr, data_word); if (status ! HAL_OK) { __enable_irq(); HAL_FLASH_Lock(); return FLASH_ERROR_WRITE; } } __enable_irq(); HAL_FLASH_Lock(); return FLASH_OK; } /** * brief 从FLASH读取数据到缓冲区 * param addr: 起始地址 * param pBuffer: 目标缓冲区指针 * param size: 要读取的字节数 * note 读取操作无需解锁可以直接进行。 */ void FLASH_Read_Buffer(uint32_t addr, uint8_t *pBuffer, uint32_t size) { // 简单的地址范围检查 if (addr FLASH_START_ADDR (addr size) (FLASH_END_ADDR 1)) { memcpy(pBuffer, (uint8_t*)addr, size); } // 在实际产品代码中这里应该返回错误码而不是静默失败 } /** * brief 读取芯片的96位唯一ID并返回一个32位的哈希值示例 * retval 一个基于96位ID计算出的32位标识符 */ uint32_t FLASH_Read_ChipID(void) { // 以STM32F407为例96位UID起始地址为0x1FFF7A10 volatile uint32_t *pUID (volatile uint32_t*)0x1FFF7A10UL; uint32_t id_part1 pUID[0]; // 低32位 uint32_t id_part2 pUID[1]; // 中间32位 uint32_t id_part3 pUID[2]; // 高32位 // 简单地将三部分进行异或生成一个32位ID仅作示例实际应用应使用更复杂的哈希 uint32_t short_id id_part1 ^ id_part2 ^ id_part3; return short_id; }3.3 应用层数据存储策略设计有了底层驱动我们还需要一个应用层的存储管理策略。直接按地址读写非常容易出错一个好的策略能极大提升可靠性。方案一静态定长参数存储这是最简单的情况。比如你只需要存储10个校准参数每个参数是4字节的float。你可以定义一个结构体并固定存储在某个扇区的起始地址。typedef struct { float calib_factor; uint32_t serial_number; uint8_t device_mode; // ... 其他参数 uint32_t crc; // 用于校验数据完整性 } SystemParams_t; #define PARAMS_FLASH_ADDR FLASH_DATA_BASE_ADDR void Save_Params(SystemParams_t *params) { // 1. 计算CRC并填入结构体 params-crc Calculate_CRC((uint8_t*)params, sizeof(SystemParams_t) - 4); // 2. 擦除整个参数存储扇区 FLASH_Erase_Sector(PARAMS_FLASH_ADDR); // 3. 将整个结构体写入FLASH FLASH_Write_Buffer(PARAMS_FLASH_ADDR, (uint8_t*)params, sizeof(SystemParams_t)); } bool Load_Params(SystemParams_t *params) { // 1. 从FLASH读取结构体 FLASH_Read_Buffer(PARAMS_FLASH_ADDR, (uint8_t*)params, sizeof(SystemParams_t)); // 2. 校验CRC uint32_t stored_crc params-crc; uint32_t calc_crc Calculate_CRC((uint8_t*)params, sizeof(SystemParams_t) - 4); return (stored_crc calc_crc); }方案二简易磨损均衡与日志式存储适用于需要频繁更新且数据量不大的场景如系统运行时间计数器。我们使用两个扇区PageA和PageB轮换存储。每个扇区的第一个字4字节作为“状态字”0xFFFFFFFF表示空闲0xAAAAAAAA表示有效0x55555555表示已满或无效。每次需要更新数据时找到状态为“空闲”或“有效”的扇区将新数据写入该扇区的下一个可用位置并更新状态字。当一个扇区写满后将其状态标记为“已满”并切换到另一个扇区同时擦除“已满”的扇区使其变为“空闲”。 这种方法能将擦写次数分摊到两个扇区延长FLASH寿命。4. 实战演练从零开始构建一个完整的示例4.1 工程搭建与驱动集成假设我们使用STM32CubeIDE和一颗STM32F407VET6芯片。使用STM32CubeMX创建工程配置好时钟树使用外部晶振达到最大168MHz、调试接口SWD。其他外设如UART用于打印日志按需配置。在Project Manager中设置好工程名、路径选择Toolchain/IDE为STM32CubeIDE。生成代码点击GENERATE CODE用IDE打开工程。添加驱动文件在工程Src和Inc文件夹下分别创建flash_ops.c和flash_ops.h将上一节的代码复制进去。修改链接脚本可选如果你决定修改链接脚本以预留数据区右键工程 -Properties-C/C Build-Settings-Tool Settings-MCU GCC Linker-General取消勾选Use default linker script并指定你修改后的.ld文件路径。对于初学者我强烈建议先使用绝对地址法跳过此步。在main.c中包含头文件并测试/* Private includes ----------------------------------------------------------*/ #include “flash_ops.h” #include stdio.h // 如果使用printf /* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); int main(void) { HAL_Init(); SystemClock_Config(); // 初始化你的UART等外设... printf(“STM32 Internal FLASH R/W Chip ID Demo\r\n”); // 测试1读取芯片ID uint32_t myChipID FLASH_Read_ChipID(); printf(“Chip Unique ID (32-bit hash): 0x%08lX\r\n”, myChipID); // 测试2定义测试数据地址使用最后扇区的起始地址确保不会擦到程序 // 查找你的芯片手册确定最后一个扇区的地址。对于F407VE最后一个扇区是Sector 11起始地址0x080E0000 #define TEST_FLASH_ADDR 0x080E0000UL // 测试3擦除扇区 printf(“Erasing Sector at 0x%08lX...”, TEST_FLASH_ADDR); Flash_Status_t status FLASH_Erase_Sector(TEST_FLASH_ADDR); if (status FLASH_OK) { printf(“OK\r\n”); } else { printf(“FAILED! Error: %d\r\n”, status); while(1); } // 测试4写入数据 uint64_t test_data 0x0123456789ABCDEFULL; printf(“Writing 0x%016llX to 0x%08lX...”, test_data, TEST_FLASH_ADDR); status FLASH_Write_Word(TEST_FLASH_ADDR, test_data); if (status FLASH_OK) { printf(“OK\r\n”); } else { printf(“FAILED! Error: %d\r\n”, status); } // 测试5读取并验证 uint64_t read_back_data *(volatile uint64_t*)TEST_FLASH_ADDR; printf(“Data read back: 0x%016llX\r\n”, read_back_data); if (read_back_data test_data) { printf(“FLASH R/W Test PASSED!\r\n”); } else { printf(“FLASH R/W Test FAILED!\r\n”); } while (1) { // 主循环 } }4.2 调试技巧与结果验证使用调试器查看内存这是最直接的验证方式。在IDE的调试模式下暂停程序打开Memory Browser窗口输入你的测试地址如0x080E0000可以看到该地址开始的原始数据。擦除后应显示全FF写入后应显示你写入的数据。串口打印日志如上例所示通过串口将关键步骤和结果打印出来便于实时观察。验证芯片ID读取到的96位原始ID你可以去ST官网查询这颗芯片的编程手册核对ID的格式。通常同一个型号、同一批次的芯片其ID的前面部分厂商信息、晶圆号可能是相同的只有最后几位序列号不同。用简单的异或得到的32位哈希只要在批量生产中能区分每一颗芯片即可。边界条件测试尝试向未擦除的地址写入数据观察是否返回错误尝试写入非对齐地址观察程序行为。这些测试能帮你完善驱动程序的鲁棒性。5. 常见问题、故障排查与高级技巧5.1 典型错误与解决方案速查表现象/错误信息可能原因排查步骤与解决方案程序在擦写FLASH后卡死或跑飞1. 擦写期间发生了中断且中断服务程序位于正在被操作的FLASH扇区。2. 擦写函数的代码本身没有被加载到RAM中执行。1. 确保在HAL_FLASHEx_Erase和HAL_FLASH_Program调用前后有__disable_irq()和__enable_irq()。2. 检查map文件确认擦写函数或它调用的HAL库底层函数的链接地址在RAM区域0x200xxxxx。在CubeIDE中可以尝试将相关源文件如stm32f4xx_hal_flash.c的Code Generation属性设置为Execute in RAM右键文件 - Properties - C/C Build - Settings - Tool Settings - MCU GCC Compiler - Misc - Other flags 添加-mpure-code不更可靠的是修改链接脚本或使用__attribute__((section(“.RamFunc”)))。最省事的办法直接使用HAL库提供的函数它通常已处理好这一点。HAL_FLASH_Program返回错误HAL_ERROR1. 目标地址未擦除状态不是0xFF。2. 地址不对齐对于双字编程地址需8字节对齐。3. FLASH未成功解锁。4. 电压范围设置错误。1. 先擦除再写入。2. 确保传入的地址是8的倍数双字编程或4的倍数字编程。3. 检查HAL_FLASH_Unlock()的返回值。4. 检查FLASH_EraseInitTypeDef中的VoltageRange是否与芯片实际电压匹配。写入的数据读出来不正确1. 数据在写入过程中被中断打断。2. 发生了字节序大小端问题。3. 变量未声明为volatile编译器优化导致读取的是缓存值。1. 确保擦写操作在临界区关中断进行。2. 对于多字节数据如float, uint32_t注意内存中的存储顺序。写入和读取时使用相同类型的指针强制转换。3. 对指向FLASH地址的指针使用volatile关键字如volatile uint32_t *p (volatile uint32_t*)FLASH_ADDR;。使用FLASH_Write_Buffer写入非8字节倍数长度时出错驱动函数FLASH_Write_Buffer未正确处理末尾非对齐部分。参考3.2节中FLASH_Write_Buffer函数的实现它通过将剩余字节拷贝到一个全FF的32位变量中来处理“尾巴”。确保你的驱动有类似逻辑。在IAP应用中跳转到新程序后无法运行1. 新程序的向量表地址没有重设。2. 新程序起始地址的栈顶指针MSP值不正确。在跳转前需要先使用SCB-VTOR NEW_APP_ADDR 0xFFFFFF80;重设向量表偏移寄存器。并且跳转函数应该是一个纯汇编或内联汇编的函数直接加载新地址的MSP和PC值。5.2 高级技巧与经验分享为FLASH操作增加超时机制HAL库的擦写函数本身有超时但你可以在应用层再封装一层。例如在开始擦写前启动一个硬件定时器如果在预期时间内如100ms操作未完成可以通过检查FLASH_SR寄存器的忙标志则强制终止并复位FLASH控制器防止芯片死锁。数据校验至关重要永远不要相信FLASH存储是100%可靠的。除了在存储结构体末尾添加CRC32校验外对于关键数据可以采用“双备份投票”机制。即同一份数据存三份读取时取三者中相同的两个或者采用更复杂的ECC算法。利用芯片ID进行软件激活在产品发布时可以设计一个“激活”流程。设备首次运行时读取芯片ID结合一个产品密钥通过算法生成一个激活码。用户需要将这个激活码反馈给厂家厂家用同样的算法生成一个对应的响应码。设备验证响应码通过后才开启完整功能。这样即使程序被完整提取没有厂家的密钥也无法在新芯片上激活。调试阶段保护程序区在开发调试阶段如果你不确定数据存储地址是否安全一个笨办法但有效的方法是在FLASH_Erase_Sector函数里加入强硬限制禁止擦除前几个包含程序代码的扇区。例如判断sector_addr 0x08010000就直接返回错误。阅读官方例程ST官方提供的HAL库包STM32Cube_FW_Fx_Vx.x.x里在Projects目录下通常有FLASH相关的示例工程Examples/FLASH或Demonstrations。这些例程是最佳参考包含了完整的配置和操作流程。