[toc]

drivers/nvmem/core.c NVMEM核心(NVMEM Core) 非易失性内存的统一访问框架

历史与背景

这项技术是为了解决什么特定问题而诞生的?

NVMEM(Non-Volatile Memory)子系统的诞生是为了解决一个在嵌入式系统中普遍存在的问题:如何以一种标准的、统一的方式来访问各种小型、非易失性存储设备中的原始数据

在此框架出现之前,内核中没有一个专门的子系统来处理这类需求,导致了实现上的混乱:

  • 缺乏统一接口:一个以太网驱动需要从板载的I2C EEPROM中读取MAC地址,而一个无线网卡驱动可能需要从SoC内部的EFUSE(电子熔丝)中读取校准数据。这些驱动不得不自己去实现与特定存储芯片(EEPROM, EFUSE, OTP等)的交互逻辑,或者依赖于特定平台的私有接口。
  • 驱动间强耦合:设备驱动(消费者)被迫要知道提供数据的存储芯片(生产者)的具体细节。如果硬件设计发生变化,例如将MAC地址从EEPROM移到了SPI Flash的一个分区中,那么设备驱动的代码就需要进行重大修改。
  • 代码重复与不可移植:每个需要读取配置数据的驱动都在重复造轮子,代码难以在不同平台间移植。

NVMEM子系统的核心目标就是创建一个通用的生产者/消费者模型,让提供非易失性数据的驱动(如EEPROM驱动)和消费这些数据的驱动(如网卡驱动)彻底解耦。

它的发展经历了哪些重要的里程碑或版本迭代?

NVMEM子系统是一个相对现代的框架,其发展核心是**单元(Cell)概念的确立和与设备树(Device Tree)**的深度集成。

  • 框架的建立:NVMEM子系统被引入,定义了核心的nvmem_device结构和生产者/消费者的交互模式。
  • “单元”(Cell)抽象的引入:这是最重要的里程碑。一个NVMEM设备(如一块EEPROM)通常不会被一个驱动完整地使用,而是存储了多个不相关的数据项(如MAC地址、序列号、校准数据)。“单元”就是对这些数据项的抽象,它在原始的二进制数据之上定义了具名的、有偏移和长度的分区。这使得消费者驱动可以按名称请求数据(“我需要mac_address这个单元”),而无需关心它在物理存储中的具体位置。
  • 与设备树的深度融合:该框架与设备树紧密绑定。NVMEM设备的布局(即所有“单元”的定义)和消费者与生产者的关联关系,都通过一套标准的设备树绑定来描述,实现了完全的硬件配置与驱动代码分离。

目前该技术的社区活跃度和主流应用情况如何?

NVMEM子系统已经成为Linux内核中处理工厂数据、配置信息和身份标识等小型非易失性数据的事实标准。它被广泛应用于各种嵌入式和物联网设备中。

  • 主流应用
    • 以太网/Wi-Fi驱动:读取MAC地址。
    • 无线电驱动(如Wi-Fi, Bluetooth):读取校准数据。
    • 安全子系统:读取设备唯一的序列号或加密密钥。
    • 工业设备:读取工厂设定的配置参数。
    • SoC驱动:读取芯片的版本或特性信息。

核心原理与设计

它的核心工作原理是什么?

NVMEM核心(nvmem/core.c)是连接NVMEM设备驱动(生产者)和客户端驱动(消费者)的桥梁。

  1. 生产者(Provider / NVMEM设备驱动)
    • 这是控制物理NVMEM芯片(如I2C EEPROM, SPI Flash, SoC内部EFUSE)的驱动。
    • 它会填充一个struct nvmem_config结构体,其中最重要的是提供了两个回调函数:.read.write,这两个函数知道如何从物理硬件中读写原始字节。
    • 它通过devm_nvmem_register()将自己注册到NVMEM核心。
  2. 消费者(Consumer / 客户端驱动)
    • 这是需要从NVMEM中获取数据的驱动,例如一个以太网驱动。
    • 它通过devm_nvmem_get()nvmem_cell_get()来请求访问一个NVMEM设备或一个特定的“单元”。
  3. 设备树(The Glue / The Map)
    • 设备树是连接两者的关键。
    • 生产者的设备树节点会包含一系列子节点,每个子节点定义一个“单元”,包括其reg = <offset length>
    • 消费者的设备树节点会使用nvmem-cellsnvmem-cell-names属性来声明它需要哪些单元。例如:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      ethernet@deadbeef {
      nvmem-cells = <&mac_address>;
      nvmem-cell-names = "mac-address";
      };
      eeprom@50 {
      compatible = "atmel,24c02";
      ...
      mac_address: mac-address@0 {
      reg = <0xfa 0x6>;
      };
      };
  4. NVMEM核心 (core.c) 的作用
    • 当以太网驱动调用nvmem_cell_get(dev, "mac-address")时,NVMEM核心会:
      1. 解析以太网设备的nvmem-cellsnvmem-cell-names属性。
      2. 找到名为"mac-address"的单元,它对应一个phandle &mac_address
      3. 通过phandle找到EEPROM设备节点下的mac_address子节点。
      4. 确定这个单元属于哪个NVMEM设备(EEPROM),并读取其偏移(0xfa)和长度(0x6)。
    • 当以太网驱动之后调用nvmem_cell_read()时,NVMEM核心会调用EEPROM驱动注册的.read回调函数,并传入计算好的物理偏移和长度,最终从硬件中读取数据并返回给消费者。

它的主要优势体现在哪些方面?

  • 完全解耦:消费者驱动完全不知道数据是来自I2C EEPROM还是SPI Flash,它只通过名称来请求数据。
  • 硬件配置与代码分离:NVMEM的布局完全在设备树中定义,修改硬件布局(如改变MAC地址的偏移)无需重新编译内核驱动。
  • 标准化和代码复用:提供了一套标准的API,避免了每个驱动都去实现私有接口。
  • 抽象:“单元”的概念提供了一个比原始二进制blob更高级、更易于管理的抽象层。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 不适用于大块或可变数据:该框架专为小型的、布局固定的数据设计。它不是一个文件系统,也不适合用来替代MTD(Memory Technology Devices)来管理整个Flash分区。
  • 只读是主要场景:虽然框架支持写入,但大多数NVMEM设备(如EFUSE, OTP)是只读或一次性写入的,因此其设计更偏向于读取操作。
  • 开销:对于最简单的场景,引入完整的NVMEM框架和设备树描述,可能比直接在平台驱动中硬编码要复杂一些,但换来的是长期的可维护性和可移植性。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

NVMEM是任何需要从硬件中读取唯一的、工厂编程的、小块固定数据的场景的标准解决方案。

  • 读取MAC地址:一个以太网驱动需要一个唯一的MAC地址,这个地址通常被烧录在板载的EEPROM或SoC的EFUSE中。
  • 读取序列号:系统需要一个全球唯一的序列号用于身份认证,该序列号存储在NVMEM中。
  • 无线校准数据:Wi-Fi或蜂窝网络模块的射频性能需要在工厂进行校准,校准参数(如功率补偿值)被存储在NVMEM中,驱动在初始化时必须读取这些数据才能正常工作。
  • 加密密钥:对于需要安全启动或数据加密的设备,硬件唯一的加密密钥可以被烧录在EFUSE等一次性编程的NVMEM中。

是否有不推荐使用该技术的场景?为什么?

  • 存储用户数据或文件:不推荐。应使用成熟的文件系统(如ext4, f2fs)和块设备或MTD设备。
  • 描述硬件配置:不推荐。如果一个值是描述硬件本身的、对于所有同型号板子都相同的配置(例如,某个设备的I2C地址),那么这个值应该直接作为属性写在设备树节点里,而不是存储在NVMEM中。NVMEM用于存储实例唯一的数据,设备树用于描述板级硬件布局。
  • 管理整个Flash分区:不推荐。应使用MTD子系统,它提供了分区管理、坏块处理等更适合大块闪存的功能。

对比分析

请将其 与 其他相似技术 进行详细对比。

特性 NVMEM Subsystem MTD Subsystem Device Tree Properties
核心功能 提供对**小型、具名数据单元(Cell)**的统一访问。 提供对大型原始闪存分区的块级访问接口。 静态描述硬件配置
数据模型 具名的、有偏移和长度的数据单元 分区 (Partition),类似于磁盘分区,有擦除块(Eraseblock)的概念。 键值对(Key-Value pairs)。
典型数据 MAC地址, 序列号, 校准数据, 密钥 (几十到几百字节)。 内核镜像, 文件系统, 大型固件 (KB, MB, GB)。 I2C地址, 中断号, GPIO引脚号, 寄存器地址。
数据来源 外部EEPROM, SoC内部EFUSE/OTP等。 NAND/NOR Flash芯片。 静态的.dts源文件,编译到内核中。
读写特性 读为主,写为辅。许多后端是只读或一次性写入。 支持读、写、擦除。为处理闪存特性而设计(如损耗均衡)。 只读。在运行时是固定不变的。
适用场景 读取实例唯一的、工厂编程的数据。 挂载文件系统,存储大型、可变的数据或固件。 描述板级硬件,为驱动提供其工作所需的配置参数。

drivers/nvmem/core.c

NVMEM核心初始化:创建nvmem主总线与布局总线

本代码片段负责Linux内核NVMEM(非易失性内存)子系统的核心初始化。其主要功能是建立NVMEM框架所需的基础总线结构。具体来说,它注册了两个关键的总线类型:一个是用于物理NVMEM设备(如EEPROM、OTP)的nvmem总线,另一个是用于描述这些设备内部数据结构的nvmem-layout(布局)总线。这种分离式设计是NVMEM子系统的核心思想,它将物理设备驱动与数据布局的解析解耦。

实现原理分析

此代码的实现完全基于Linux的统一设备模型,通过注册bus_type来创建新的设备和驱动类别。

  1. 双总线架构:

    • nvmem_bus_type: 这是主总线。物理NVMEM设备的驱动程序(例如,一个at24 EEPROM驱动)会创建一个nvmem_device,这个设备将被注册到nvmem总线上。此操作会在sysfs中创建/sys/bus/nvmem/目录,所有物理NVMEM设备和它们的驱动都会挂载在这里。它关心的是如何从物理介质(如I2C、SPI接口)读取字节流。
    • nvmem_layout_bus_register(): 这是一个辅助函数(定义在别处),它负责注册一个独立的“布局总线”,通常名为nvmem-layout。这个总线不关心物理介质,只关心如何解析从物理设备读出的字节流。例如,一个“布局驱动”可以定义某个NVMEM设备的前6个字节是MAC地址,接下来的32字节是校准数据。这种设计允许一个通用的物理设备驱动(如at24)与多个不同的、特定于产品的布局描述相结合,大大提高了代码的复用性。
  2. 初始化流程 (nvmem_init):

    • 函数首先调用bus_register注册nvmem_bus_type,创建/sys/bus/nvmem/
    • 紧接着,调用nvmem_layout_bus_register注册布局总线。
    • 初始化过程是原子性的:如果布局总线注册失败,它会立刻调用bus_unregister来注销已经成功注册的主总线,确保系统不会处于一个不一致的中间状态。
    • subsys_initcall确保了这个核心框架在任何具体的NVMEM设备驱动加载之前被初始化。
  3. 退出流程 (nvmem_exit):

    • 退出函数nvmem_exit严格遵循与初始化相反的顺序进行清理:首先注销布局总线,然后注销主总线。这是标准的内核资源管理实践,确保了资源的有序释放。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 定义NVMEM的主总线类型。
static const struct bus_type nvmem_bus_type = {
// .name: 指定总线在sysfs中的名称。此操作将创建 /sys/bus/nvmem 目录。
.name = "nvmem",
};

// nvmem_init: NVMEM子系统的初始化函数。
static int __init nvmem_init(void)
{
int ret;

// 注册NVMEM主总线。
ret = bus_register(&nvmem_bus_type);
if (ret)
return ret;

// 注册NVMEM布局总线。
ret = nvmem_layout_bus_register();
if (ret)
// 如果布局总线注册失败,则注销已经注册成功的主总线以保持一致性。
bus_unregister(&nvmem_bus_type);

return ret;
}

// nvmem_exit: NVMEM子系统的退出/清理函数。
static void __exit nvmem_exit(void)
{
// 以与初始化相反的顺序注销总线。
// 首先注销布局总线。
nvmem_layout_bus_unregister();
// 然后注销主总线。
bus_unregister(&nvmem_bus_type);
}

// 将nvmem_init注册为子系统初始化调用,确保它在驱动加载前执行。
subsys_initcall(nvmem_init);
// 将nvmem_exit注册为模块退出调用。
module_exit(nvmem_exit);

// 标准的模块作者和描述信息。
MODULE_AUTHOR("Srinivas Kandagatla <srinivas.kandagatla@linaro.org>");
MODULE_AUTHOR("Maxime Ripard <maxime.ripard@free-electrons.com>");
MODULE_DESCRIPTION("nvmem Driver Core");

drivers/nvmem/layouts.c

NVMEM布局总线实现:数据解析与物理设备的解耦

本代码片段实现了NVMEM(非易失性内存)子系统中的“布局总线”(nvmem-layout)。其核心功能是提供一个专门的软件总线,用于匹配和管理NVMEM的数据布局驱动。这个总线本身不与任何物理硬件交互,它的唯一职责是充当“翻译层”或“解析器”,将从物理NVMEM设备(如EEPROM)读出的原始字节流,解析成有意义的数据单元(cells),如MAC地址、校准参数等。

实现原理分析

这段代码是NVMEM子系统将物理I/O与数据解析分离这一核心设计理念的具体体现。它定义并注册了一个名为 “nvmem-layout” 的新bus_type

  1. 总线回调函数: nvmem_layout_bus_type结构体定义了总线的核心行为:

    • .match = nvmem_layout_bus_match: 这是设备与驱动的匹配函数。它直接调用of_driver_match_device,这意味着“布局设备”和“布局驱动”之间的匹配是完全通过设备树(Device Tree)的compatible属性来完成的。在设备树中,一个物理NVMEM设备节点下会有一个nvmem-layout子节点,该子节点的compatible字符串将决定哪个布局驱动会被加载。
    • .probe = nvmem_layout_bus_probe: 当match成功后,设备模型核心会调用此函数。它首先通过container_of宏从通用的struct devicestruct device_driver指针中获取NVMEM布局层专用的struct nvmem_layoutstruct nvmem_layout_driver指针。然后,它调用具体布局驱动提供的probe函数。在这个probe函数里,驱动会解析设备树中定义的各个数据单元(cells),并建立起从“名称”到“在NVMEM中的偏移和长度”的映射。
    • .remove = nvmem_layout_bus_remove: 当设备或驱动被移除时调用,执行与probe相反的清理操作。
  2. 注册函数 (nvmem_layout_bus_register):

    • 这是一个简单的封装函数,它唯一的工作就是调用bus_register,将nvmem_layout_bus_type注册到内核设备模型中。此函数由NVMEM核心初始化代码(如前一个示例中的nvmem_init)调用。

通过这种方式,一个通用的物理NVMEM驱动(如at24 I2C EEPROM驱动)可以保持不变,而开发者可以通过在设备树中指定不同的nvmem-layout子节点和compatible属性,来加载不同的布局驱动,从而适应不同产品中相同物理芯片但数据格式各异的情况。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// 宏定义,用于从通用的 device_driver 指针安全地获取其宿主结构体 nvmem_layout_driver 的指针。
#define to_nvmem_layout_driver(drv) \
(container_of_const((drv), struct nvmem_layout_driver, driver))
// 宏定义,用于从通用的 device 指针安全地获取其宿主结构体 nvmem_layout 的指针。
#define to_nvmem_layout_device(_dev) \
container_of((_dev), struct nvmem_layout, dev)

// nvmem_layout_bus_match: NVMEM布局总线的设备-驱动匹配函数。
// @dev: 设备指针。
// @drv: 驱动指针。
// 返回值: 匹配成功返回1,否则返回0。
static int nvmem_layout_bus_match(struct device *dev, const struct device_driver *drv)
{
// 直接使用设备树的匹配机制。
// 它会比较设备节点(dev)的 "compatible" 属性和驱动(drv)支持的 "compatible" 列表。
return of_driver_match_device(dev, drv);
}

// nvmem_layout_bus_probe: NVMEM布局总线的probe函数,在匹配成功后被内核调用。
// @dev: 已成功匹配到驱动的设备指针。
// 返回值: 成功则为0,失败则为负数错误码。
static int nvmem_layout_bus_probe(struct device *dev)
{
// 从通用驱动指针转换为特定的NVMEM布局驱动指针。
struct nvmem_layout_driver *drv = to_nvmem_layout_driver(dev->driver);
// 从通用设备指针转换为特定的NVMEM布局设备指针。
struct nvmem_layout *layout = to_nvmem_layout_device(dev);

// 进行有效性检查,具体的布局驱动必须同时提供probe和remove函数。
if (!drv->probe || !drv->remove)
return -EINVAL;

// 调用具体布局驱动的probe函数,将控制权和特定数据结构传递过去。
return drv->probe(layout);
}

// nvmem_layout_bus_remove: NVMEM布局总线的remove函数,在设备或驱动移除时被调用。
// @dev: 正在被移除的设备指针。
static void nvmem_layout_bus_remove(struct device *dev)
{
struct nvmem_layout_driver *drv = to_nvmem_layout_driver(dev->driver);
struct nvmem_layout *layout = to_nvmem_layout_device(dev);

// 调用具体布局驱动的remove函数执行清理工作。
return drv->remove(layout);
}

// 定义NVMEM布局总线类型结构体。
static const struct bus_type nvmem_layout_bus_type = {
// .name: 总线在sysfs中的名称,将创建 /sys/bus/nvmem-layout 目录。
.name = "nvmem-layout",
// .match: 指定设备与驱动的匹配函数。
.match = nvmem_layout_bus_match,
// .probe: 指定匹配成功后的探测函数。
.probe = nvmem_layout_bus_probe,
// .remove: 指定设备或驱动移除时的清理函数。
.remove = nvmem_layout_bus_remove,
};

// nvmem_layout_bus_register: NVMEM布局总线的注册函数。
// 返回值: 成功则为0,失败则为负数错误码。
int nvmem_layout_bus_register(void)
{
// 调用设备模型核心函数来注册上面定义的总线。
return bus_register(&nvmem_layout_bus_type);
}