[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设备驱动(生产者)和客户端驱动(消费者)的桥梁。
- 生产者(Provider / NVMEM设备驱动):
- 这是控制物理NVMEM芯片(如I2C EEPROM, SPI Flash, SoC内部EFUSE)的驱动。
- 它会填充一个
struct nvmem_config
结构体,其中最重要的是提供了两个回调函数:.read
和.write
,这两个函数知道如何从物理硬件中读写原始字节。 - 它通过
devm_nvmem_register()
将自己注册到NVMEM核心。
- 消费者(Consumer / 客户端驱动):
- 这是需要从NVMEM中获取数据的驱动,例如一个以太网驱动。
- 它通过
devm_nvmem_get()
或nvmem_cell_get()
来请求访问一个NVMEM设备或一个特定的“单元”。
- 设备树(The Glue / The Map):
- 设备树是连接两者的关键。
- 生产者的设备树节点会包含一系列子节点,每个子节点定义一个“单元”,包括其
reg = <offset length>
。 - 消费者的设备树节点会使用
nvmem-cells
和nvmem-cell-names
属性来声明它需要哪些单元。例如:1
2
3
4
5
6
7
8
9
10
11ethernet@deadbeef {
nvmem-cells = <&mac_address>;
nvmem-cell-names = "mac-address";
};
eeprom@50 {
compatible = "atmel,24c02";
...
mac_address: mac-address@0 {
reg = <0xfa 0x6>;
};
};
- NVMEM核心 (
core.c
) 的作用:- 当以太网驱动调用
nvmem_cell_get(dev, "mac-address")
时,NVMEM核心会:- 解析以太网设备的
nvmem-cells
和nvmem-cell-names
属性。 - 找到名为
"mac-address"
的单元,它对应一个phandle&mac_address
。 - 通过phandle找到EEPROM设备节点下的
mac_address
子节点。 - 确定这个单元属于哪个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
来创建新的设备和驱动类别。
双总线架构:
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
)与多个不同的、特定于产品的布局描述相结合,大大提高了代码的复用性。
初始化流程 (
nvmem_init
):- 函数首先调用
bus_register
注册nvmem_bus_type
,创建/sys/bus/nvmem/
。 - 紧接着,调用
nvmem_layout_bus_register
注册布局总线。 - 初始化过程是原子性的:如果布局总线注册失败,它会立刻调用
bus_unregister
来注销已经成功注册的主总线,确保系统不会处于一个不一致的中间状态。 subsys_initcall
确保了这个核心框架在任何具体的NVMEM设备驱动加载之前被初始化。
- 函数首先调用
退出流程 (
nvmem_exit
):- 退出函数
nvmem_exit
严格遵循与初始化相反的顺序进行清理:首先注销布局总线,然后注销主总线。这是标准的内核资源管理实践,确保了资源的有序释放。
- 退出函数
代码分析
1 | // 定义NVMEM的主总线类型。 |
drivers/nvmem/layouts.c
NVMEM布局总线实现:数据解析与物理设备的解耦
本代码片段实现了NVMEM(非易失性内存)子系统中的“布局总线”(nvmem-layout
)。其核心功能是提供一个专门的软件总线,用于匹配和管理NVMEM的数据布局驱动。这个总线本身不与任何物理硬件交互,它的唯一职责是充当“翻译层”或“解析器”,将从物理NVMEM设备(如EEPROM)读出的原始字节流,解析成有意义的数据单元(cells
),如MAC地址、校准参数等。
实现原理分析
这段代码是NVMEM子系统将物理I/O与数据解析分离这一核心设计理念的具体体现。它定义并注册了一个名为 “nvmem-layout” 的新bus_type
。
总线回调函数:
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 device
和struct device_driver
指针中获取NVMEM布局层专用的struct nvmem_layout
和struct nvmem_layout_driver
指针。然后,它调用具体布局驱动提供的probe
函数。在这个probe
函数里,驱动会解析设备树中定义的各个数据单元(cells),并建立起从“名称”到“在NVMEM中的偏移和长度”的映射。.remove = nvmem_layout_bus_remove
: 当设备或驱动被移除时调用,执行与probe
相反的清理操作。
注册函数 (
nvmem_layout_bus_register
):- 这是一个简单的封装函数,它唯一的工作就是调用
bus_register
,将nvmem_layout_bus_type
注册到内核设备模型中。此函数由NVMEM核心初始化代码(如前一个示例中的nvmem_init
)调用。
- 这是一个简单的封装函数,它唯一的工作就是调用
通过这种方式,一个通用的物理NVMEM驱动(如at24
I2C EEPROM驱动)可以保持不变,而开发者可以通过在设备树中指定不同的nvmem-layout
子节点和compatible
属性,来加载不同的布局驱动,从而适应不同产品中相同物理芯片但数据格式各异的情况。
代码分析
1 | // 宏定义,用于从通用的 device_driver 指针安全地获取其宿主结构体 nvmem_layout_driver 的指针。 |