分区布局策略:连续分区 vs 动态分区、分区大小对齐原则、预留空间与坏块管理

分区布局这事儿,说白了就是给 Bootloader 和它的小伙伴们安排住处。住得挤了容易打架,住得散了找起来费劲。我这些年调过的板子,十块里有八块启动出问题,最后查下来都是分区布局埋的雷。

连续分区 vs 动态分区:两种思路的博弈

先聊聊连续分区。这是最老派的做法——把 Flash 从头到尾切成几块,每块干固定的事。比如 0x0000~0x8000 放 Bootloader,0x8000~0x10000 放参数区,后面放应用。

连续分区的核心优势:简单、可靠、好调试。你写个烧录脚本,直接按地址写就行,不用管什么分配算法。

但缺点也明显——灵活性差。我记得有个项目,产品迭代到第三版时 Bootloader 体积涨了 30%,原来的分区装不下了。改分区表?那得动量产工具、升级脚本、回滚方案…折腾了两周。

动态分区就不一样了。它像内存管理一样,按需分配空间。常见做法是用一个分区表头,记录每个分区的起始地址、大小、校验值。启动时先解析表头,再跳转。

// 动态分区表结构示例
typedef struct {
    uint32_t magic;          // 魔数,用于校验
    uint32_t version;        // 分区表版本
    uint32_t entry_count;    // 分区条目数
    partition_entry_t entries[MAX_PARTITIONS];
} partition_table_t;

typedef struct {
    uint32_t type;           // 分区类型:bootloader/param/app/factory
    uint32_t offset;         // 起始偏移
    uint32_t size;           // 分区大小
    uint32_t crc32;          // 分区内容校验
    uint8_t  flags;          // 属性:只读/可升级/备份
} partition_entry_t;

动态分区的好处是空间利用率高。你想想看,如果某个分区只需要 4KB,你就不用硬塞给它 64KB。但代价是代码复杂度上去了,而且分区表本身必须绝对可靠——表坏了,整个系统就瞎了。

对比维度 连续分区 动态分区
实现复杂度 低,直接地址映射 中高,需解析表头
空间利用率 低,有碎片 高,按需分配
启动速度 快,固定地址跳转 稍慢,需查表
升级灵活性 差,改分区需重烧 好,可在线调整
可靠性风险 低,结构简单 中,分区表是关键点

我的建议:小容量 Flash(≤1MB)用连续分区就够了。大容量或者需要 OTA 的产品,上动态分区。我做过一个 8MB Flash 的 IoT 项目,用了动态分区,后期加了个诊断分区,没动任何硬件。

分区大小对齐原则:别小看这个细节

分区对齐,很多人觉得无所谓。不就是多几个字节的事吗?嗯,我以前也这么想,直到踩了坑。

那次是给一个 Nor Flash 做 Bootloader,分区起始地址没按 4KB 对齐。结果擦除操作总是把隔壁分区的数据带走了——因为 Flash 的擦除最小单位是 sector,你从非对齐地址擦,它连邻居一起擦。那周我排查了三天,最后发现是分区表里写了个 0x1001 的起始地址。

对齐原则:分区起始地址和大小,必须对齐到 Flash 的擦除块大小(sector/block)。Nor Flash 通常是 4KB 或 64KB,NAND Flash 是 128KB 或 256KB。

具体怎么做?我一般这样处理:

  1. 查手册——确认 Flash 的擦除块大小,别猜。
  2. 宏定义对齐值——用 #define FLASH_SECTOR_SIZE 4096 这种。
  3. 对齐宏——写两个工具宏:
// 向上对齐到 sector 边界
#define ALIGN_UP(addr, align)   (((addr) + (align) - 1) & ~((align) - 1))
// 向下对齐
#define ALIGN_DOWN(addr, align) ((addr) & ~((align) - 1))

// 使用示例
uint32_t bootloader_end = BOOTLOADER_BASE + BOOTLOADER_SIZE;
uint32_t param_start    = ALIGN_UP(bootloader_end, FLASH_SECTOR_SIZE);

另外,分区大小本身也要对齐。比如你给参数区分配 10KB,但 Flash 的 sector 是 4KB,那实际占用的空间是 12KB(3 个 sector)。多出来的 2KB 虽然用不上,但必须预留,否则擦除时会出问题。

经验之谈:我习惯在分区表里同时记录「请求大小」和「实际占用大小」。这样调试时能一眼看出对齐浪费了多少空间。

预留空间与坏块管理:给未来留条后路

预留空间,说白了就是「别把 Flash 塞满」。我见过有人把分区排得严丝合缝,结果产品上市后想加个功能,没地方了。改分区?量产工具、升级流程全得动,成本极高。

一般我会预留 10%~20% 的空间,具体看产品生命周期。如果预计会有 3~5 次大版本升级,预留 20% 不过分。这些空间可以放在 Flash 末尾,作为「应急分区」或「扩展分区」。

坏块管理这块,NAND Flash 是重灾区。Nor Flash 基本没有坏块问题,但 NAND 出厂就有坏块,而且用着用着还会新出。

我曾经在一个 NAND 方案上吃过亏——Bootloader 直接写在固定块上,没做坏块处理。结果某批次芯片的第 0 块就是坏的,整批板子起不来。后来改了方案:

  • Bootloader 放前 4 块,每块都是完整副本,启动时按顺序尝试。
  • 分区表放两份,主表和备份表在不同块组。
  • 坏块表单独分区,启动时先加载坏块表,再解析其他分区。
// 坏块管理简化流程
int load_partition_table(void) {
    // 先尝试主分区表
    if (read_block(PRIMARY_TBL_BLOCK, &table) == 0) {
        if (verify_crc(&table) == 0) {
            return 0; // 主表可用
        }
    }
    // 主表坏了,尝试备份
    if (read_block(BACKUP_TBL_BLOCK, &table) == 0) {
        if (verify_crc(&table) == 0) {
            // 修复主表
            write_block(PRIMARY_TBL_BLOCK, &table);
            return 0;
        }
    }
    // 都坏了,进恢复模式
    return -1;
}

避坑指南:我曾经遇到一个坑——坏块标记在 OOB 区,但 Bootloader 的 NAND 驱动没读 OOB。结果坏块被当成好块用,数据写进去就读不出来了。所以,NAND 驱动一定要读 OOB,别偷懒。

预留空间和坏块管理结合起来,还有个技巧:把预留空间分散到各个分区后面,而不是集中放在末尾。这样每个分区都有「自留地」,某个分区出现坏块时,可以用后面的预留块替换,不影响其他分区。

分区布局策略核心逻辑 连续分区布局 Bootloader (64KB) 参数区 (32KB) 应用区 (剩余空间) 动态分区布局 分区表头 (4KB) Boot (48KB) 参数 (20KB) App (?) 预留空间 (动态调整) 对齐原则:起始地址 & 大小 → 对齐到擦除块边界 Nor Flash: 4KB / 64KB sector | NAND Flash: 128KB / 256KB block 对齐宏: ALIGN_UP(addr, sector_size) 确保不越界擦除 预留空间策略 • 预留 10%~20% 总空间 • 分散预留 vs 集中预留 • 应急分区 / 扩展分区 坏块管理要点 • Bootloader 多副本 • 分区表主备双份 • 坏块表独立分区 + OOB 标记

最后说一句,分区布局没有银弹。连续分区简单可靠,动态分区灵活高效,选哪个取决于你的产品定位和团队能力。但不管选哪种,对齐、预留、坏块管理这三件事,一个都不能少。我见过太多项目在这上面翻车了——嗯,希望你不是下一个。