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_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 还提供 NvM_GetErrorStatus 用于查询指定 Block 的最近一次作业结果。返回类型为 NvM_RequestResultType,常见取值包括:
| 返回值 | 含义 | 典型处理 |
|---|---|---|
| NVM_REQ_OK | 作业成功完成 | 正常继续 |
| NVM_REQ_NOT_OK | 作业失败 | 记录 DTC,尝试重写 |
| NVM_REQ_PENDING | 作业仍在队列或执行中 | 等待,不可重复发起 |
| NVM_REQ_INTEGRITY_FAILED | CRC 校验失败 | 触发 RestoreBlockDefaults |
| NVM_REQ_BLOCK_SKIPPED | Block 被跳过(如未配置) | 检查配置 |
在工程实践中,建议对关键 Block 的写入失败实施“重试 + 降级”策略:首次失败后延迟一个任务周期重试,连续三次失败则记录故障并切换到默认值运行。这样既避免了无限重试阻塞队列,也保证了系统在存储异常时仍可降级工作。
理解 NvM 服务接口与作业机制,本质上是在理解“请求与执行分离”这一设计哲学。应用只负责表达意图,NvM 负责排队、调度和结果通知。只有把回调、队列和状态查询三者结合使用,才能写出既满足实时性又保证数据可靠性的存储代码。