1. 背景知识:Flash 的特点
在 STM32 等单片机中,Flash 是用来存储程序代码的存储器。它有以下几个重要特性:
-
不能像 RAM 那样直接写入:要修改 Flash 的内容,必须先擦除(Erase),然后再编程(Program)。而且擦除是以"扇区"为单位的,不能只擦除几个字节。
-
擦除后数据全变成 0xFF:写入时只能把 1 写成 0,不能把 0 写成 1。所以必须先擦除,把所有位变成 1,然后再编程写入需要的 0。
-
有寿命限制:擦写次数有限(通常几万次)。
-
有 Bank 的概念 :一些大容量芯片把 Flash 分成两个 Bank,每个 Bank 独立管理,可以同时操作。但对我们来说,关键是知道地址范围:通常 Bank1 从
0x08000000开始,大小比如 1MB;Bank2 从0x08100000开始,也是 1MB。具体要看芯片型号。 -
扇区大小 :不同芯片扇区大小不同,可能是 2KB、4KB、128KB 等。代码中用的
SECTOR_SIZE应该是宏定义,需要根据实际芯片确定。
2. 用到的 HAL 库函数
HAL_FLASHEx_Erase
cs
HAL_StatusTypeDef HAL_FLASHEx_Erase(FLASH_EraseInitTypeDef *pEraseInit, uint32_t *SectorError);
这个函数用于擦除 Flash 的扇区。参数是一个结构体指针,结构体定义如下:
cs
typedef struct
{
uint32_t TypeErase; /* 擦除类型:扇区擦除还是整片擦除 */
uint32_t Banks; /* 要擦除的 Bank(当整片擦除时有用) */
uint32_t Sector; /* 起始扇区号 */
uint32_t NbSectors; /* 要擦除的扇区数量 */
} FLASH_EraseInitTypeDef;
-
TypeErase:我们一般用FLASH_TYPEERASE_SECTORS,表示扇区擦除。 -
Banks:指定操作哪个 Bank,如FLASH_BANK_1或FLASH_BANK_2。 -
Sector:起始扇区索引(注意不是地址,是扇区编号,从 0 开始)。 -
NbSectors:连续擦除几个扇区。 -
SectorError:如果擦除失败,这个指针会被写入出错的扇区编号。
HAL_FLASH_Program
cs
HAL_StatusTypeDef HAL_FLASH_Program(uint32_t TypeProgram, uint32_t FlashAddress, uint32_t DataAddress);
这个函数用于编程(写入)Flash。
-
TypeProgram:编程方式,决定一次写入多少字节。常见的有:-
FLASH_TYPEPROGRAM_BYTE:字节写入 -
FLASH_TYPEPROGRAM_HALFWORD:半字(2 字节) -
FLASH_TYPEPROGRAM_WORD:字(4 字节) -
FLASH_TYPEPROGRAM_DOUBLEWORD:双字(8 字节) -
FLASH_TYPEPROGRAM_QUADWORD:四字(16 字节,即 128 位)。这正是代码中使用的。
-
-
FlashAddress:要写入的 Flash 地址,必须对齐到编程单位。比如用 QUADWORD,地址必须 16 字节对齐。 -
DataAddress:指向源数据的指针(在 RAM 中),这个地址只需要 32 位对齐(因为函数内部会按字读取)。 -
返回值:
HAL_OK表示成功,否则失败。
3. 函数 WriteFirmware 详解
这个函数负责把内存中的固件数据写入 Flash 的指定地址。
cs
static int WriteFirmware(uint8_t *firmware_buf, uint32_t len, uint32_t flash_addr)
-
firmware_buf:指向存放固件数据的缓冲区(在 RAM 中)。 -
len:固件数据的长度(字节数)。 -
flash_addr:目标 Flash 起始地址(比如0x08040000)。
3.1 局部变量
cs
FLASH_EraseInitTypeDef tEraseInit;
uint32_t SectorError;
uint32_t sectors = (len + (SECTOR_SIZE - 1)) / SECTOR_SIZE; // 计算需要擦除的扇区总数
uint32_t flash_offset = flash_addr - 0x08000000; // 相对于 Flash 基址的偏移
uint32_t bank_sectors; // 当前 Bank 剩余的扇区数
uint32_t erased_sectors = 0; // 本次实际擦除的扇区数
-
sectors:因为擦除以扇区为单位,所以要计算出覆盖整个固件需要多少个扇区。公式(len + SECTOR_SIZE - 1) / SECTOR_SIZE是常见的向上取整。 -
flash_offset:Flash 基址是0x08000000,用目标地址减去基址得到偏移。这个偏移用于计算扇区号:扇区号 = 偏移 / 扇区大小。 -
bank_sectors:用来记录当前 Bank 从起始扇区开始到 Bank 末尾还有多少个扇区。 -
erased_sectors:记录本次实际擦除了几个扇区(可能少于需要的总数,因为可能跨 Bank)。
3.2 解锁 Flash
cs
HAL_FLASH_Unlock();
Flash 默认是写保护的,必须先解锁才能擦除或编程。解锁通常需要向特定寄存器写入密钥序列。HAL 函数封装了这一步。
3.3 擦除 Bank1 部分
cs
/* erase bank1 */
if (flash_offset < 0x100000)
{
tEraseInit.TypeErase = FLASH_TYPEERASE_SECTORS;
tEraseInit.Banks = FLASH_BANK_1;
tEraseInit.Sector = flash_offset / SECTOR_SIZE;
bank_sectors = (0x100000 - flash_offset) / SECTOR_SIZE;
if (sectors <= bank_sectors)
erased_sectors = sectors;
else
erased_sectors = bank_sectors;
tEraseInit.NbSectors = erased_sectors;
if (HAL_OK != HAL_FLASHEx_Erase(&tEraseInit, &SectorError))
{
g_pUpdateUART->Send(... "HAL_FLASHEx_Erase Failed\r\n" ...);
HAL_FLASH_Lock();
return -1;
}
flash_offset += erased_sectors * SECTOR_SIZE;
}
为什么分 Bank?
假设芯片有两个 Bank,每个 Bank 大小 1MB(0x100000 字节)。如果目标地址在 Bank1 范围内(偏移 < 0x100000),那么先擦除 Bank1 中需要的扇区。但如果固件很长,可能会跨越到 Bank2,所以需要分段处理。
计算起始扇区 :flash_offset / SECTOR_SIZE 得到从 Bank1 起始算起的扇区号。注意,Bank1 的扇区号是从 0 开始的,正好对应偏移除以扇区大小。
计算当前 Bank 剩余扇区数 :(0x100000 - flash_offset) / SECTOR_SIZE 表示从当前偏移到 Bank1 末尾还有多少个扇区。
决定本次擦除多少扇区:
-
如果需要的总扇区数
sectors小于等于剩余扇区数,那么一次擦完:erased_sectors = sectors。 -
否则,只能先擦完当前 Bank 的剩余扇区:
erased_sectors = bank_sectors。
执行擦除 :调用 HAL_FLASHEx_Erase,如果失败,则发送错误信息,锁定 Flash,返回 -1。
更新偏移和剩余扇区数 :擦除后,已经处理了 erased_sectors 个扇区,所以 flash_offset 增加 erased_sectors * SECTOR_SIZE,以便后续处理 Bank2。同时,sectors 也要减去擦除的数量,但代码中是在擦除 Bank2 之前才减的,这里要注意逻辑顺序。
3.4 准备擦除 Bank2
cs
sectors -= erased_sectors;
flash_offset -= 0x100000;
-
sectors -= erased_sectors:减去 Bank1 中已擦除的扇区数,得到还剩下多少扇区需要擦除。 -
flash_offset -= 0x100000:因为接下来要处理 Bank2,而flash_offset之前是相对于 Flash 基址的偏移,现在减去 Bank1 的大小,得到相对于 Bank2 起始的偏移。这样在 Bank2 内,扇区号又可以按flash_offset / SECTOR_SIZE计算。
3.5 擦除 Bank2 部分
cs
/* erase bank2 */
if (sectors)
{
tEraseInit.TypeErase = FLASH_TYPEERASE_SECTORS;
tEraseInit.Banks = FLASH_BANK_2;
tEraseInit.Sector = flash_offset / SECTOR_SIZE;
bank_sectors = (0x100000 - flash_offset) / SECTOR_SIZE;
if (sectors <= bank_sectors)
erased_sectors = sectors;
else
erased_sectors = bank_sectors;
tEraseInit.NbSectors = erased_sectors;
if (HAL_OK != HAL_FLASHEx_Erase(&tEraseInit, &SectorError))
{
g_pUpdateUART->Send(... "HAL_FLASHEx_Erase Failed\r\n" ...);
HAL_FLASH_Lock();
return -1;
}
}
逻辑与 Bank1 类似,只是操作的是 Bank2。如果还有剩余扇区(应该不会超过 Bank2 大小),就擦除它们。注意这里没有更新 flash_offset 和 sectors,因为这是最后一步了。
3.6 编程(写入)Flash
cs
/* program */
len = (len + 15) & ~15;
for (int i = 0; i < len; i += 16)
{
if (HAL_OK != HAL_FLASH_Program(FLASH_TYPEPROGRAM_QUADWORD, flash_addr, (uint32_t)firmware_buf))
{
g_pUpdateUART->Send(... "HAL_FLASH_Program Failed\r\n" ...);
HAL_FLASH_Lock();
return -1;
}
flash_addr += 16;
firmware_buf += 16;
}
为什么要对齐长度?
len = (len + 15) & ~15; 这句将长度向上取整到 16 的倍数。因为后面以 16 字节为单位写入,如果固件长度不是 16 的倍数,最后多出的部分也要写入(可能会写入一些多余数据,但 Flash 中未使用的区域本来就是 0xFF,不影响)。注意:这可能会在 Flash 末尾多写一些 0 或随机数据,但固件本身长度是准确的,我们只在有效长度内写数据,而循环却按对齐后的长度执行,会导致超出固件实际长度的部分也去读 firmware_buf,可能越界!这是一个潜在 bug。实际上,应该在循环中判断是否还有数据,或者只写实际长度,对齐是为了满足硬件要求,但源缓冲区应该只包含有效数据。这里可能存在隐患,但通常固件长度就是对齐的,或者编程函数会处理部分写入?我们暂且按代码逻辑理解。
循环写入 :每次写入 16 字节(FLASH_TYPEPROGRAM_QUADWORD),地址增加 16,源指针也增加 16。注意,HAL_FLASH_Program 的第三个参数是 DataAddress,它应该是源数据在内存中的地址,这里直接强制转换为 uint32_t 传入,函数内部会从该地址读取数据。所以 firmware_buf 必须指向有效的 RAM 区域。
错误处理:任何一次写入失败,就发送错误信息,锁定 Flash,返回 -1。
3.7 锁定 Flash
cs
HAL_FLASH_Lock();
return 0;
所有操作完成后,重新锁定 Flash,防止意外写入。
4. 函数 WriteFirmwareInfo 详解
这个函数专门用于烧写固件信息结构体到 Flash 的配置区域(CFG_OFFSET)。结构体大小是固定的,所以不需要传入长度。
cs
static int WriteFirmwareInfo(PFirmwareInfo ptFirmwareInfo)
{
FLASH_EraseInitTypeDef tEraseInit;
uint32_t SectorError;
uint32_t flash_addr = CFG_OFFSET;
uint8_t *src_buf = (uint8_t *)ptFirmwareInfo;
HAL_FLASH_Unlock();
4.1 擦除配置扇区
cs
/* erase bank2 */
tEraseInit.TypeErase = FLASH_TYPEERASE_SECTORS;
tEraseInit.Banks = FLASH_BANK_2;
tEraseInit.Sector = (flash_addr - 0x08000000 - 0x100000) / SECTOR_SIZE;
tEraseInit.NbSectors = 1;
假设 CFG_OFFSET 位于 Bank2 内(例如 0x081FE000),那么先计算它在 Bank2 内的扇区号:
-
flash_addr - 0x08000000得到总偏移。 -
再减去 Bank1 大小
0x100000,得到相对于 Bank2 起始的偏移。 -
除以扇区大小,得到扇区号。
这里只擦除一个扇区,因为固件信息结构体很小(32 字节),肯定在一个扇区内。
cs
if (HAL_OK != HAL_FLASHEx_Erase(&tEraseInit, &SectorError))
{
g_pUpdateUART->Send(... "HAL_FLASHEx_Erase Failed\r\n" ...);
HAL_FLASH_Lock();
return -1;
}
4.2 编程写入
cs
/* program */
for (int i = 0; i < sizeof(FirmwareInfo); i += 16)
{
if (HAL_OK != HAL_FLASH_Program(FLASH_TYPEPROGRAM_QUADWORD, flash_addr, (uint32_t)src_buf))
{
g_pUpdateUART->Send(... "HAL_FLASH_Program Failed\r\n" ...);
HAL_FLASH_Lock();
return -1;
}
flash_addr += 16;
src_buf += 16;
}
结构体大小是 32 字节,所以循环两次,每次写 16 字节。
4.3 锁定
cs
HAL_FLASH_Lock();
return 0;
5. 需要注意的细节
-
地址对齐 :
HAL_FLASH_Program要求目标地址必须与编程单位对齐(这里 16 字节)。如果传入的flash_addr没有对齐,函数内部可能会触发错误。在调用WriteFirmware之前,应该确保flash_addr是 16 字节对齐的。通常固件烧录地址都是扇区对齐的,所以没问题。 -
跨 Bank 处理 :代码中假设两个 Bank 大小都是 1MB(0x100000)。如果芯片不同,这个值可能需要修改。更好的做法是从芯片头文件中获取宏定义,比如
FLASH_BANK1_SIZE。 -
扇区大小 :
SECTOR_SIZE需要根据芯片实际定义,例如有的芯片是 2KB,有的可能是 128KB。这个值在擦除和计算扇区号时非常关键。 -
长度对齐的隐患 :前面提到的
len = (len + 15) & ~15可能会导致源缓冲区越界。因为如果len不是 16 的倍数,向上取整后循环会多读一些字节,而firmware_buf后面可能没有分配那么多内存。正确的做法是只写实际长度,但硬件要求必须 16 字节对齐写,所以最后几个不足 16 字节的部分需要特殊处理(比如读出原有内容合并后再写,但通常固件长度本身就是 16 的倍数,或者尾部填充 0xFF)。这里可能是假设固件长度已经是对齐的。 -
错误处理:擦除或编程一旦失败,函数立即返回 -1,但此时可能部分扇区已擦除,导致原有程序被破坏。所以实际工程中可能需要更完善的恢复机制,比如记录擦除状态,或者支持断点续传。不过对于学习来说,这样简单处理也可以。
-
解锁/锁定的配对:每次操作前解锁,操作后锁定,防止其他代码误操作。
6. 生活例子:把新菜谱写进菜谱本
故事设定:你要更新一本菜谱本
你家里有一本很厚的菜谱本 ,它分上下两册,上册叫 Bank1,下册叫 Bank2。每册有 1000 页(这个数字只是比喻,实际页数由芯片决定)。每页可以写 1000 个字(相当于 SECTOR_SIZE,比如 2KB 或 128KB)。菜谱本有一个锁扣,平时锁着,要修改里面的内容必须先打开锁(解锁 Flash)。
你从网上买了一份新菜谱 (新固件),想把其中一部分内容替换掉。新菜谱写在几张稿纸上(内存中的 firmware_buf),总共有 3500 个字(len = 3500)。你想从菜谱本的第 500 页开始,把这 3500 个字抄进去(flash_addr = 0x08000000 对应的页码,但这里我们用页码代替地址,比如第 500 页对应 flash_addr)。
但是菜谱本有个规矩:不能直接在原有的字上涂改,必须先把整页撕掉(擦除),然后才能在新的一页上写字(编程)。而且撕页必须整页撕,不能只撕半页。
现在,你按照以下步骤操作:
准备工作
cs
FLASH_EraseInitTypeDef tEraseInit;
uint32_t SectorError;
uint32_t sectors = (len + (SECTOR_SIZE - 1)) / SECTOR_SIZE;
uint32_t flash_offset = flash_addr - 0x08000000;
uint32_t bank_sectors;
uint32_t erased_sectors = 0;
-
sectors:新菜谱有 3500 个字,每页能写 1000 字,所以需要(3500+999)/1000 = 4.5 → 向上取整 = 5页。也就是说,你得准备 5 页空白页才能抄下全部菜谱。 -
flash_offset:你想从第 500 页开始写,但菜谱本的总页码是从第 0 页算起的(0x08000000 对应第 0 页),所以偏移就是 500 页。 -
bank_sectors:等一下会用来计算当前册还剩多少页。 -
erased_sectors:记录本次实际撕掉了多少页。
第一步:打开锁扣
cs
HAL_FLASH_Unlock();
你掏出钥匙,打开菜谱本的锁扣,现在可以动里面的页了。
第二步:处理上册(Bank1)
cs
if (flash_offset < 0x100000)
0x100000 在这里是 1MB 的字节数,如果按每页 1000 字来算,1MB 对应多少页?我们暂且不管具体数字,你只需要知道:如果起始页码小于上册的总页数(比如上册有 1000 页),那么就先处理上册。在这个比喻里,假设上册有 1000 页(0 到 999),下册从 1000 页开始。
因为起始页是 500,小于 1000,所以进入上册处理。
cs
tEraseInit.TypeErase = FLASH_TYPEERASE_SECTORS;
你决定只撕掉部分页(不是整本撕)。
cs
tEraseInit.Banks = FLASH_BANK_1;
你告诉自己要操作的是上册。
cs
tEraseInit.Sector = flash_offset / SECTOR_SIZE;
起始页码:500 页(因为 flash_offset 就是 500,SECTOR_SIZE 是 1 页,所以扇区号就是 500)。
cs
bank_sectors = (0x100000 - flash_offset) / SECTOR_SIZE;
计算从第 500 页开始,到上册末尾(第 999 页)还有多少页:(1000 - 500) = 500 页。
cs
if (sectors <= bank_sectors)
erased_sectors = sectors;
else
erased_sectors = bank_sectors;
你需要 5 页,而上册还有 500 页可用,所以本次可以撕掉全部需要的 5 页:erased_sectors = 5。
cs
tEraseInit.NbSectors = erased_sectors;
告诉自己要撕掉 5 页。
cs
if (HAL_OK != HAL_FLASHEx_Erase(&tEraseInit, &SectorError))
{
g_pUpdateUART->Send(... "HAL_FLASHEx_Erase Failed\r\n" ...);
HAL_FLASH_Lock();
return -1;
}
你开始动手撕页。如果撕的时候某一页粘得太紧撕坏了(擦除失败),你就用手机(串口)发个消息:"撕页失败!",然后重新锁上本子,退出操作。
cs
flash_offset += erased_sectors * SECTOR_SIZE;
撕掉 5 页后,你撕到的最后一页是第 504 页。为了下一步处理,把偏移更新到下一页,即第 505 页。但注意,这里 flash_offset 还是相对于本子开头的偏移,所以现在是 505。
第三步:准备处理下册(如果需要)
cs
sectors -= erased_sectors;
flash_offset -= 0x100000;
-
sectors -= erased_sectors:你已经撕了 5 页,需要的总页数从 5 变成 0,所以不需要再处理下册了。 -
flash_offset -= 0x100000:这一步是为了把偏移转换到下册的坐标系。因为上册已经处理完,如果还有剩余页要撕,就需要在下册里撕。这里减去上册的总页数(1000 页),得到相对于下册起始的偏移。但本例中 sectors 已经为 0,所以不会进入下册处理。
第四步:处理下册(如果 sectors > 0)
cs
/* erase bank2 */
if (sectors)
{
// ... 类似上册的代码,只是 Banks 改为 FLASH_BANK_2
}
如果刚才需要 10 页,而上册只有 5 页,那么撕完上册 5 页后,sectors 还剩 5 页,就需要进入下册,从第 0 页(下册起始)开始撕 5 页。
第五步:抄写新菜谱
cs
/* program */
len = (len + 15) & ~15;
你需要把新菜谱抄到撕好的空白页上。但是,菜谱本有个要求:每次必须抄写一整页,而且每页必须写满 16 个字(这里用 16 字比喻 128 位) 。所以你要把新菜谱的长度调整成 16 的倍数。3500 字,加上 15 后是 3515,再取掉低 4 位(按位与 ~15)得到 3504?不对,应该是向上取整到 16 的倍数:3500 向上取 16 的倍数是 3504?实际上 3500/16=218.75,所以应该是 219*16=3504。但代码中 (len+15)&~15 确实会得到 3504。但你的新菜谱只有 3500 字,最后 4 个字怎么办?可能稿纸上后面还有空白,或者你需要在末尾补 0(但这里没体现)。我们暂且认为稿纸长度本来就是 16 的倍数,或者多出的部分无关紧要。
cs
for (int i = 0; i < len; i += 16)
{
if (HAL_OK != HAL_FLASH_Program(FLASH_TYPEPROGRAM_QUADWORD, flash_addr, (uint32_t)firmware_buf))
{
g_pUpdateUART->Send(... "HAL_FLASH_Program Failed\r\n" ...);
HAL_FLASH_Lock();
return -1;
}
flash_addr += 16;
firmware_buf += 16;
}
你拿起笔,一页一页地抄写。每次抄 16 个字,从第 500 页开始,抄完一页,页码加 1(这里 +16 是因为地址增加 16 字节,对应到页码就是加 1 页),稿纸指针也往后移 16 个字。
如果抄到某一页手抖写错了(编程失败),你就发消息"抄写失败!",锁上本子退出。
第六步:锁上本子
cs
HAL_FLASH_Lock();
return 0;
全部抄完后,你合上锁扣,大功告成。
额外的小标签:WriteFirmwareInfo
现在你还要在菜谱本的固定位置贴一个小标签 ,上面记录新菜谱的版本、大小、校验码等信息。这个位置在 CFG_OFFSET,假设是在下册的最后一页(第 1999 页)。
cs
static int WriteFirmwareInfo(PFirmwareInfo ptFirmwareInfo)
{
FLASH_EraseInitTypeDef tEraseInit;
uint32_t SectorError;
uint32_t flash_addr = CFG_OFFSET;
uint8_t *src_buf = (uint8_t *)ptFirmwareInfo;
-
flash_addr:标签要贴的页码(第 1999 页)。 -
src_buf:你手中的小纸条(内存中的结构体数据)。
解锁
cs
HAL_FLASH_Unlock();
再次打开锁扣。
撕掉标签所在的那一页
cs
tEraseInit.TypeErase = FLASH_TYPEERASE_SECTORS;
tEraseInit.Banks = FLASH_BANK_2;
tEraseInit.Sector = (flash_addr - 0x08000000 - 0x100000) / SECTOR_SIZE;
tEraseInit.NbSectors = 1;
计算标签所在页码在下册内的页号:先算出总页码(1999),减去上册的 1000 页,得到在下册内的页码 999,然后除以页大小(1)得到扇区号 999。只撕这一页(NbSectors = 1)。
cs
if (HAL_OK != HAL_FLASHEx_Erase(&tEraseInit, &SectorError))
{
g_pUpdateUART->Send(... "HAL_FLASHEx_Erase Failed\r\n" ...);
HAL_FLASH_Lock();
return -1;
}
撕掉这一页,如果失败就报错。
贴上小标签
cs
for (int i = 0; i < sizeof(FirmwareInfo); i += 16)
{
if (HAL_OK != HAL_FLASH_Program(FLASH_TYPEPROGRAM_QUADWORD, flash_addr, (uint32_t)src_buf))
{
g_pUpdateUART->Send(... "HAL_FLASH_Program Failed\r\n" ...);
HAL_FLASH_Lock();
return -1;
}
flash_addr += 16;
src_buf += 16;
}
小标签有 32 个字(sizeof(FirmwareInfo) = 32),每次贴 16 个字,分两次贴完。第一次贴前 16 个字,第二次贴后 16 个字。
锁上
cs
HAL_FLASH_Lock();
return 0;
贴完锁上,完成。
为什么代码要分 Bank 处理?
因为菜谱本太厚,分上下两册管理。当你需要从第 500 页开始抄写 800 页的内容时,上册只有从 500 到 999 共 500 页可用,不够,所以必须撕完上册的 500 页,然后转到下册继续撕剩下的 300 页。代码中的 flash_offset -= 0x100000 就是为了把坐标系切换到下册。
为什么地址要减 0x08000000?
菜谱本的第一页叫"第 0 页",对应 Flash 的起始地址 0x08000000。当你拿到一个绝对页码(比如 flash_addr = 0x08040000),要转换成从第 0 页开始的偏移,就减去 0x08000000。这样你就能用偏移除以页大小得到真正的页码。
为什么编程前要调整长度?
硬件要求每次写入必须是 16 字节对齐,就像菜谱本要求每次必须写满一页(16 个字)。如果新菜谱长度不是 16 的倍数,最后不足一页也要凑满一页,但稿纸上可能没有那么多字,所以这里存在风险。实际工程中,通常固件长度就是 16 的倍数,或者会在尾部填充 0xFF 来对齐。
7. 总结
烧写 Flash 的代码并不复杂,核心就是"擦除再编程",但要处理好地址计算、跨 Bank、对齐等问题。理解这些细节后,你就能自己实现或修改 Bootloader 的烧录功能了。如果有不清楚的地方,可以随时提问!