第二章:参数存储机制深度解析——参数在Flash与EEPROM中的存储结构、参数加载流程

大家好,我是你们的飞控固件讲师。今天我们来聊聊ArduPilot里一个非常核心、但又容易被忽视的话题——参数到底是怎么存起来的?

你想想看,每次你通过Mission Planner修改一个参数,点“写入参数”,那个数值到底去了哪里?是直接写进了Flash?还是先去了EEPROM?下次飞控上电,它又是怎么把这些参数一个个找回来的?

嗯,这些问题,如果你只是做二次开发,可能一辈子都碰不到。但如果你想深入理解ArduPilot的底层架构,或者想自己移植飞控到新的硬件上,那这块知识就是绕不开的坎。我个人习惯把参数系统比作飞控的“记忆体”——它记住了你所有的调参偏好,丢了它,飞控就是个“失忆症”患者。


2.1 参数存储的物理介质:Flash vs EEPROM

首先,我们要搞清楚两个概念:Flash和EEPROM。很多初学者会把它们混为一谈,其实差别很大。

特性 Flash EEPROM
读写单位 按页(Page)擦除,按字节写入 按字节读写
擦写寿命 约1万~10万次 约10万~100万次
写入速度 较快(毫秒级) 较慢(毫秒级,但单字节操作更灵活)
典型用途 存储程序代码、大块数据 存储配置参数、校准数据
在ArduPilot中的角色 存储固件本身 存储用户参数(默认方式)

说白了,Flash就像一本厚厚的书,你只能整页整页地撕掉重写;而EEPROM就像一张便签纸,你可以随时擦掉某个字重写。参数这种需要频繁修改、但每次改动量很小的数据,天然适合EEPROM。

核心结论:ArduPilot默认将参数存储在EEPROM中。但在某些硬件上(比如STM32F4系列),EEPROM是模拟出来的——它实际上是在Flash中划出了一块区域,用软件模拟EEPROM的行为。这一点,我在移植飞控到新板子时踩过坑,后面会细说。


2.2 参数在EEPROM中的存储结构

参数不是随便往EEPROM里一丢就完事的。ArduPilot有一套严谨的存储格式。我直接上代码,大家感受一下。

每个参数在EEPROM中占用固定的存储单元,我们称之为Param结构体:

// 来自 AP_Param/AP_Param.h
class AP_Param {
public:
    // 参数在EEPROM中的存储格式
    struct ParamHeader {
        uint8_t type;       // 参数类型(整数、浮点、向量等)
        uint8_t size;       // 数据大小(字节数)
        uint16_t id;        // 参数ID(唯一标识)
    };
    
    // 参数值紧随Header之后存储
    // 整个参数在EEPROM中的布局:
    // [Header (4字节)] [Value (N字节)]
};

你看,每个参数在EEPROM里由两部分组成:

  • 参数头(ParamHeader):4个字节,记录了类型、大小和ID。
  • 参数值:紧随其后,长度由size字段决定。

所有参数在EEPROM中是连续存储的,一个接一个,没有空隙。我画了一张图,帮你理解这个布局:

EEPROM参数存储布局 EEPROM地址空间(从0x0000开始) Header1 Value1 Header2 Value2 Header3 Value3 (大) Header4 Value4 ... 后续参数依次排列,直到遇到结束标记(Sentinel) 结束标记:type=0, size=0, id=0 (表示参数列表结束)

这里有个关键点:参数在EEPROM中的顺序,和你在代码中定义的顺序不一定一致。ArduPilot在编译时会为每个参数分配一个唯一的ID,存储时按ID排序。这样做的好处是——即使你后续在代码中新增了参数,也不会影响已有参数的位置。

小技巧:如果你在调试时发现参数值错乱,可以尝试“重置参数为默认值”。这个操作的本质就是重新格式化EEPROM参数区,把所有参数恢复成代码中定义的默认值。我曾经在开发一个新硬件时,因为EEPROM地址偏移搞错了,导致参数全乱套,最后就是用这个办法救回来的。


2.3 参数加载流程:从上电到就绪

好了,现在我们知道参数是怎么存的了。那飞控上电后,它是怎么把这些参数读出来的呢?这个过程,我称之为“参数加载三部曲”。

第一步:扫描EEPROM,构建参数表

飞控上电后,AP_Param::load_all()函数会被调用。它会从EEPROM的起始地址开始,逐个读取参数头:

// 简化后的加载流程
void AP_Param::load_all()
{
    uint16_t eeprom_index = 0;
    
    while (true) {
        // 1. 读取参数头
        ParamHeader header;
        read_header_from_eeprom(eeprom_index, header);
        
        // 2. 检查是否为结束标记
        if (header.type == 0 && header.size == 0 && header.id == 0) {
            break;  // 参数列表结束
        }
        
        // 3. 根据ID查找对应的参数对象
        AP_Param *param = find_by_id(header.id);
        
        // 4. 如果找到了,从EEPROM读取值并赋值
        if (param != nullptr) {
            param->read_value_from_eeprom(eeprom_index + sizeof(header));
        }
        
        // 5. 移动到下一个参数
        eeprom_index += sizeof(header) + header.size;
    }
}

这个过程就像在图书馆里按索书号找书——每个参数都有一个唯一的ID(索书号),系统拿着这个ID去内存中维护的“参数注册表”里查找对应的变量地址,然后把EEPROM里的值填进去。

第二步:处理缺失参数

如果EEPROM里存储的参数ID,在当前的固件代码中找不到对应的参数对象,会发生什么?

嗯,这种情况很常见。比如你升级了固件,新固件删除了某个旧参数,或者新增了一个参数。ArduPilot的处理策略是:

  • 旧参数(EEPROM有,代码无):直接跳过,忽略它。它占用的EEPROM空间不会被回收,但也不会造成问题。
  • 新参数(代码有,EEPROM无):使用代码中定义的默认值。同时,这个参数会被标记为“未存储”,下次保存参数时会被写入EEPROM。

注意:如果你频繁地在不同版本的固件之间来回刷,EEPROM里可能会积累大量“孤儿参数”。虽然不会导致崩溃,但会浪费存储空间。我建议每隔一段时间做一次“完整参数重置”,清理一下。

第三步:参数校验与默认值回退

加载完成后,ArduPilot还会做一次完整性检查。它会检查每个参数的值是否在合法范围内(比如,RC1_MAX不能小于RC1_MIN)。如果发现非法值,会将其重置为默认值。

这一步非常重要。我记得有一次,我在调试一个四轴飞行器时,不小心把MOT_HOVER_LEARN参数写成了一个超大值,结果飞控上电后电机直接满速旋转。幸亏有参数校验机制,它检测到值越界,自动回退到了默认值,才没酿成事故。


2.4 参数保存流程:从内存到持久化

说完了加载,我们再聊聊保存。当你点击“写入参数”时,背后发生了什么?

// 简化后的保存流程
bool AP_Param::save_all()
{
    // 1. 锁定参数,防止并发修改
    lock();
    
    // 2. 遍历所有已注册的参数
    for (auto ¶m : registered_params) {
        // 3. 如果参数被修改过(dirty标志),则写入EEPROM
        if (param.is_dirty()) {
            write_param_to_eeprom(param);
            param.clear_dirty();
        }
    }
    
    // 4. 写入结束标记
    write_sentinel_to_eeprom();
    
    // 5. 解锁
    unlock();
    
    return true;
}

这里有个优化点:ArduPilot不会每次都把全部参数写一遍,它只写那些被修改过的参数(通过dirty标志位判断)。这大大减少了EEPROM的擦写次数,延长了寿命。

避坑指南:我曾经遇到过一个问题——在飞行中通过地面站修改参数,结果飞控突然重启,导致EEPROM写入不完整,参数损坏。后来我加了一个“双缓冲”机制:先写入临时区域,校验通过后再复制到正式区域。虽然ArduPilot官方没有采用这个方案,但如果你在做自己的飞控系统,这个思路值得参考。


2.5 不同硬件平台的差异

最后,我想提一下不同硬件平台在参数存储上的差异。ArduPilot支持多种MCU,它们的EEPROM实现方式各不相同:

硬件平台 EEPROM实现方式 注意事项
AVR(如Arduino Mega) 硬件EEPROM,独立存储空间 容量小(通常1KB~4KB),参数数量受限
STM32F1/F4 Flash模拟EEPROM(软件实现) 需要预留Flash页,注意擦写均衡
STM32H7 硬件EEPROM(部分型号)或Flash模拟 性能更好,但配置更复杂
Linux(如树莓派、NuttX) 文件系统模拟(存储为文件) 参数存储在 /fs/microsd/APM/param.parm

对于STM32平台,Flash模拟EEPROM有一个经典问题:写入次数限制。Flash的擦写寿命只有1万次左右,如果你频繁保存参数(比如每秒保存一次),用不了多久Flash就会报废。ArduPilot的解决方案是——只在参数真正改变时才写入,并且使用“磨损均衡”算法,轮流使用多个Flash页。

嗯,这部分内容比较深,如果你感兴趣,可以去看看AP_Param/AP_Param_Flash.cpp这个文件,里面实现了完整的Flash模拟EEPROM逻辑。


好了,关于参数存储机制,今天就讲到这里。总结一下:参数存储在EEPROM中,每个参数由Header和Value组成,按ID排序连续存储。上电时,飞控扫描EEPROM构建参数表,处理缺失参数,并进行合法性校验。保存时,只写入被修改的参数,减少擦写次数。

这些知识,说白了就是让你明白——你每次点“写入参数”时,飞控内部到底在忙活些什么。理解了这些,你就能更好地诊断参数相关的问题,也能在移植飞控时少走弯路。

如果你在实际项目中遇到了参数丢失、参数错乱的问题,不妨先从EEPROM的存储布局入手排查。我个人的经验是,90%的参数问题都出在地址偏移或写入不完整上。


专注资料整理