编写 Bootloader 实现烧录功能

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_1FLASH_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_offsetsectors,因为这是最后一步了。

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. 需要注意的细节

  1. 地址对齐HAL_FLASH_Program 要求目标地址必须与编程单位对齐(这里 16 字节)。如果传入的 flash_addr 没有对齐,函数内部可能会触发错误。在调用 WriteFirmware 之前,应该确保 flash_addr 是 16 字节对齐的。通常固件烧录地址都是扇区对齐的,所以没问题。

  2. 跨 Bank 处理 :代码中假设两个 Bank 大小都是 1MB(0x100000)。如果芯片不同,这个值可能需要修改。更好的做法是从芯片头文件中获取宏定义,比如 FLASH_BANK1_SIZE

  3. 扇区大小SECTOR_SIZE 需要根据芯片实际定义,例如有的芯片是 2KB,有的可能是 128KB。这个值在擦除和计算扇区号时非常关键。

  4. 长度对齐的隐患 :前面提到的 len = (len + 15) & ~15 可能会导致源缓冲区越界。因为如果 len 不是 16 的倍数,向上取整后循环会多读一些字节,而 firmware_buf 后面可能没有分配那么多内存。正确的做法是只写实际长度,但硬件要求必须 16 字节对齐写,所以最后几个不足 16 字节的部分需要特殊处理(比如读出原有内容合并后再写,但通常固件长度本身就是 16 的倍数,或者尾部填充 0xFF)。这里可能是假设固件长度已经是对齐的。

  5. 错误处理:擦除或编程一旦失败,函数立即返回 -1,但此时可能部分扇区已擦除,导致原有程序被破坏。所以实际工程中可能需要更完善的恢复机制,比如记录擦除状态,或者支持断点续传。不过对于学习来说,这样简单处理也可以。

  6. 解锁/锁定的配对:每次操作前解锁,操作后锁定,防止其他代码误操作。


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 的烧录功能了。如果有不清楚的地方,可以随时提问!

相关推荐
数据库小组3 小时前
2026 年,MySQL 到 SelectDB 同步为何更关注实时、可观测与可校验?
数据库·mysql·数据库管理工具·数据同步·ninedata·selectdb·迁移工具
华科易迅3 小时前
MybatisPlus增删改查操作
android·java·数据库
Kethy__4 小时前
计算机中级-数据库系统工程师-计算机体系结构与存储系统
大数据·数据库·数据库系统工程师·计算机中级
SHoM SSER4 小时前
MySQL 数据库连接池爆满问题排查与解决
android·数据库·mysql
熬夜的咕噜猫4 小时前
MySQL备份与恢复
数据库·oracle
jnrjian5 小时前
recover database using backup controlfile until cancel 假recover,真一致
数据库·oracle
lifewange5 小时前
java连接Mysql数据库
java·数据库·mysql
大妮哟6 小时前
postgresql数据库日志量异常原因排查
数据库·postgresql·oracle
还是做不到嘛\.6 小时前
Dvwa靶场-SQL Injection (Blind)-基于sqlmap
数据库·sql·web安全
不写八个6 小时前
PHP教程004:php链接mysql数据库
数据库·mysql·php