第2章:固件基础概念

各位同学,今天我们来聊聊固件。说实话,很多刚入行的工程师对「固件」的理解就是「写在芯片里的程序」。嗯,这个说法没错,但太笼统了。我在做蓝牙协议栈的这些年里,见过太多因为固件概念不清导致的低级错误——有人把固件当普通软件升级,结果把Flash刷坏了;有人搞不清固件版本号,导致产线批量返工。所以这一章,我们先把基础打牢。

什么是固件?

固件(Firmware),说白了就是「固化在硬件中的软件」。它介于硬件和纯软件之间,是让芯片「活过来」的那层代码。

我个人的理解是这样的:

  • 硬件是身体——CPU、内存、外设接口
  • 固件是灵魂——告诉硬件怎么初始化、怎么工作
  • 应用软件是行为——用户能看到的界面和功能

举个例子,你手里的蓝牙耳机。芯片本身只是一块硅片,通电后啥也不会。但固件一加载,它就知道怎么扫描、怎么配对、怎么传输音频。没有固件,芯片就是一块废铁。

核心定义:固件是存储在非易失性存储器(如Flash、ROM)中的程序代码,负责硬件初始化、底层驱动和协议栈运行。它不像普通软件那样频繁修改,但也不是硬件那样一成不变。

固件 vs 软件:到底差在哪?

这个问题我面试时经常问。很多人答不上来,或者答得很模糊。其实区别很清晰:

对比维度 固件 普通软件
存储介质 Flash、ROM、EEPROM 硬盘、SSD、内存
更新频率 低(几个月甚至几年一次) 高(随时可能更新)
运行环境 裸机或RTOS,资源极度受限 操作系统,资源丰富
与硬件关系 直接操作寄存器、中断、DMA 通过系统调用、API间接访问
调试方式 JTAG、串口、LED闪烁 IDE断点、日志、远程调试
出错后果 设备变砖,可能无法恢复 程序崩溃,重启即可

你看,区别还是挺大的。我记得有一次,团队里一个新人把固件当普通软件写,在while循环里用了malloc动态分配内存。结果呢?跑着跑着堆就溢出了,蓝牙连接断断续续。排查了两天才找到原因——嵌入式环境里,动态内存分配要慎之又慎。

我的经验:写固件时,脑子里要时刻想着「资源受限」这四个字。Flash只有256KB,RAM只有64KB,CPU主频可能只有几十MHz。你写的每一行代码,都要精打细算。

蓝牙固件的存储介质:Flash vs ROM

蓝牙芯片里,固件到底存在哪?这得看芯片的设计。我接触过的方案里,主要有两种:

1. ROM(只读存储器)

ROM里的固件是芯片出厂时就烧录好的,用户改不了。它通常包含最基础的启动代码和Bootloader。好处是稳定、安全,坏处是——嗯,你猜到了——没法升级。

早期的一些蓝牙芯片,比如CSR的某些老型号,就把蓝牙协议栈的核心部分放在ROM里。我当时做项目时遇到一个坑:芯片的ROM里有个HCI层的bug,导致某些手机连不上。但ROM改不了啊,只能通过外挂Flash打补丁。那叫一个折腾。

2. Flash(闪存)

现在主流的蓝牙芯片,比如Nordic nRF5系列、TI CC26xx系列、乐鑫ESP32系列,都用Flash存固件。Flash可以擦写,所以固件能升级。这也是为什么你的蓝牙耳机能OTA升级——其实就是把新固件写到Flash里。

Flash又分几种:

  • 内部Flash:集成在芯片内部,速度快,但容量有限(通常256KB~2MB)
  • 外部Flash:通过SPI或QSPI接口外挂,容量大(可达16MB以上),但速度慢一些
  • NOR Flash vs NAND Flash:NOR支持XIP(原地执行),代码可以直接在Flash上跑;NAND不行,得先拷贝到RAM里。蓝牙固件一般用NOR Flash,因为启动快、可靠性高

注意:Flash有擦写寿命限制,一般是1万到10万次。OTA升级频繁的话,要考虑磨损均衡。我曾经见过一个产品,每天OTA升级一次,半年后Flash就挂了。嗯,设计时一定要算好这个账。

固件版本管理:别让版本号坑了你

说到版本管理,我真是有一肚子话要说。做蓝牙固件开发,版本号不是随便写的。它关系到产线烧录、OTA升级、问题追溯。搞不好,就是一场灾难。

版本号的组成

我习惯用三段式:主版本号.次版本号.修订号

  • 主版本号:重大架构变更,不兼容升级(比如从BLE 4.2升级到5.0)
  • 次版本号:新增功能,但向后兼容(比如加了Mesh组网功能)
  • 修订号:Bug修复、性能优化(比如修复了断连问题)

举个例子:v2.3.1,表示主版本2,次版本3,第1次修订。

版本信息存在哪?

固件的版本信息通常存在Flash的固定地址,比如:

// 假设Flash基地址为0x08000000
// 在链接脚本中预留4字节存放版本号
#define FIRMWARE_VERSION_ADDR  0x08001000

// 版本号结构体
typedef struct {
    uint8_t major;
    uint8_t minor;
    uint16_t revision;
    char build_date[12];  // "2024-01-15"
} firmware_version_t;

// 烧录时写入
const firmware_version_t g_fw_version __attribute__((section(".version_section"))) = {
    .major = 2,
    .minor = 3,
    .revision = 1,
    .build_date = "2024-01-15"
};

这样,不管是产线烧录还是OTA升级,都能通过读取这个地址拿到版本号。我在项目中就靠这个定位过问题——客户说设备连不上,我让他读一下版本号,发现是v1.0.0的旧固件,有个已知的配对bug。升级到v1.0.1就好了。

版本兼容性检查

OTA升级时,一定要做版本兼容性检查。我曾经踩过一个坑:新固件改了Flash分区布局,但没做版本判断,结果旧固件升级后,配置数据全乱了。从那以后,我都在Bootloader里加一段版本检查逻辑:

// 伪代码:OTA升级前的版本检查
bool check_version_compatibility(firmware_version_t *new_ver) {
    // 主版本不同,必须全量升级(不能增量)
    if (new_ver->major != CURRENT_MAJOR) {
        return false;  // 需要全量擦除
    }
    // 次版本不同,检查配置兼容性
    if (new_ver->minor != CURRENT_MINOR) {
        // 可能需要迁移配置数据
        migrate_config();
    }
    // 修订号不同,直接覆盖
    return true;
}

避坑指南:我曾经在产线上遇到过一个问题——同一批芯片,有的烧了v1.0,有的烧了v1.1,但外壳印的版本号都一样。后来排查发现,是烧录脚本里版本号写死了。从那以后,我坚持把版本号放在代码里,用编译日期自动生成,而不是手动填写。

小结

这一章我们聊了固件的基本概念。说白了,固件就是芯片的「操作系统」,但它比普通软件更贴近硬件、更受限、也更需要谨慎对待。Flash和ROM各有优劣,选型时要看项目需求。版本管理看似小事,但做不好会出大问题。

下一章,我们会深入蓝牙固件的启动流程——芯片上电后,固件到底是怎么一步步把蓝牙协议栈跑起来的?嗯,那才是真正精彩的部分。