4. NvM 服务接口与作业机制

NvM 对上层应用暴露的并不是“直接读写 Flash”的函数,而是一组以 Block 为单位的异步服务接口。理解这些接口的关键,不在于记住函数名,而在于理解每个接口背后“请求—排队—执行—回调”的作业机制。NvM 之所以采用异步模型,是因为底层 Fee/Fls 的擦写操作耗时可达毫秒甚至数十毫秒量级,若同步阻塞,将直接破坏任务周期与实时性。

一、三类核心服务接口的语义差异

NvM 的服务接口按数据流向和来源可分为三类,它们对 RAM 镜像、NV 存储和默认值的使用方式完全不同:

  • NvM_ReadBlock:把 NV 存储中的数据读入指定的 RAM 镜像。若该 Block 从未写入过(NV 中无有效数据),NvM 会依据配置决定是否用 ROM 默认值填充 RAM。
  • NvM_WriteBlock:把 RAM 镜像中的当前数据写入 NV 存储。这是唯一会真正改变 NV 内容的常规接口。
  • NvM_RestoreBlockDefaults:不访问 NV 存储,直接把 ROM 默认值(或配置的初始化回调结果)写入 RAM 镜像,用于恢复出厂/初始状态。

三者的数据源与目标可用下图直观区分。图中同时标出了异步回调的返回路径,这是理解 NvM 作业机制的核心。

NvM 三类服务接口的数据流向与异步回调 NV 存储 Fee / Fls (非易失) ROM 默认值 NvMBlockDefault RAM 镜像 NvMBlockRam (应用可见) NvM_ReadBlock NvM_WriteBlock NvM_RestoreBlockDefaults 异步完成回调 NvM_JobEndNotification / NvM_JobErrorNotification

二、作业队列:请求如何被排队与调度

当应用调用 NvM_ReadBlock 或 NvM_WriteBlock 时,NvM 并不会立即操作 Fee,而是把该请求封装成一个“作业”放入队列。队列分为两个优先级:

  • 标准队列(Standard Job Queue):由应用主动发起的读写请求进入此队列,按 FIFO 顺序处理。
  • 高优先级队列(High-Priority Job Queue):用于多块请求(Multi-Block Request)或需要优先完成的作业,可插队到标准队列之前。

队列中的作业由 NvM 的主函数 NvM_MainFunction 周期性驱动。每次调用时,NvM 从队列取出一个作业,向 Fee 发起对应的读/写/擦除请求,然后等待 Fee 的完成通知。这意味着:同一时刻 NvM 只处理一个作业,其余请求都在队列中等待。因此,如果应用在短时间内连续写入多个 Block,实际完成时间会随队列长度线性增长。

一个常见的工程误区是:在 10ms 任务中调用 NvM_WriteBlock 后立即读取 RAM 镜像,认为数据已落盘。实际上此时作业可能还在队列中,NV 内容尚未更新。正确做法是等待回调通知,或通过 NvM_GetErrorStatus 查询作业状态。

三、异步处理与回调机制

NvM 的异步性体现在两个层面:接口调用立即返回,以及完成结果通过回调上报。配置中需要为每个 Block 或全局指定两个回调函数:

  • NvM_JobEndNotification:作业成功完成时调用。
  • NvM_JobErrorNotification:作业失败(如 Fee 返回错误、CRC 校验失败)时调用。

回调函数运行在 NvM_MainFunction 的上下文中,因此必须保持简短,不能阻塞或调用可能再次触发 NvM 作业的接口。典型做法是在回调中设置一个标志位,由应用任务轮询该标志后再进行后续处理。

四、一个可参考的写入作业时序示例

假设应用需要保存一份标定参数(Block ID = 0x0010,长度 64 字节),配置为立即写入(Immediate Write)。以下是一个合理的作业时序示例:

/* 应用任务中发起写入请求 */
NvM_WriteBlock(0x0010, &CalibRamMirror);
/* 函数立即返回,作业进入标准队列 */

/* NvM_MainFunction 周期调度 */
/* 第1次调用:取出作业,向 Fee 发起写请求 */
/* Fee 执行 Flash 编程,耗时约 8ms(示例值) */
/* Fee 完成后通知 NvM */

/* 第2次调用:NvM 检查 Fee 结果 */
/* 成功 → 调用 NvM_JobEndNotification */
/* 失败 → 调用 NvM_JobErrorNotification */

/* 应用回调中设置标志 */
void NvM_JobEndNotification(void) {
    CalibWriteDone = TRUE;
}

/* 应用任务轮询标志后继续 */
if (CalibWriteDone) {
    CalibWriteDone = FALSE;
    /* 执行后续逻辑 */
}

在这个示例中,从调用 NvM_WriteBlock 到回调触发,实际耗时取决于 Fee 的编程时间和 NvM_MainFunction 的调度周期。若 MainFunction 周期为 5ms,Fee 编程 8ms,则总延迟约为 10~15ms。这个量级在标定保存场景中通常可接受,但在高频数据记录场景中必须评估队列积压风险。

五、接口使用中的关键约束

NvM 服务接口并非可以随意调用,以下约束直接影响工程实现的正确性:

  • Block ID 必须有效:传入未配置的 Block ID 会返回 NVM_E_PARAM_BLOCK_ID 错误,作业不会入队。
  • RAM 镜像地址必须与配置一致:NvM_ReadBlock 和 NvM_WriteBlock 的第二个参数应指向该 Block 配置的 RAM 块,否则数据可能被写入错误位置。
  • 不可重入:同一 Block 在上一个作业未完成前再次发起请求,NvM 会返回 NVM_E_BLOCK_PENDING 或直接拒绝,具体行为取决于配置。
  • 初始化顺序:必须先调用 NvM_Init,再调用 NvM_ReadAll 或 NvM_WriteAll,最后才允许应用发起单块读写。
在 NvM_ReadAll 未完成前调用 NvM_ReadBlock,可能读到未初始化的 RAM 镜像。建议应用通过 NvM_GetErrorStatus 或全局状态标志确认 ReadAll 完成后再访问数据。

六、作业状态查询与错误处理

除了回调,NvM 还提供 NvM_GetErrorStatus 用于查询指定 Block 的最近一次作业结果。返回类型为 NvM_RequestResultType,常见取值包括:

返回值含义典型处理
NVM_REQ_OK作业成功完成正常继续
NVM_REQ_NOT_OK作业失败记录 DTC,尝试重写
NVM_REQ_PENDING作业仍在队列或执行中等待,不可重复发起
NVM_REQ_INTEGRITY_FAILEDCRC 校验失败触发 RestoreBlockDefaults
NVM_REQ_BLOCK_SKIPPEDBlock 被跳过(如未配置)检查配置

在工程实践中,建议对关键 Block 的写入失败实施“重试 + 降级”策略:首次失败后延迟一个任务周期重试,连续三次失败则记录故障并切换到默认值运行。这样既避免了无限重试阻塞队列,也保证了系统在存储异常时仍可降级工作。

理解 NvM 服务接口与作业机制,本质上是在理解“请求与执行分离”这一设计哲学。应用只负责表达意图,NvM 负责排队、调度和结果通知。只有把回调、队列和状态查询三者结合使用,才能写出既满足实时性又保证数据可靠性的存储代码。