NVMEM(非易失性存储器)框架是处理非易失性存储(如 EEPROM、eFuse 等)的核心层。 这些设备的驱动过去存储在drivers/misc/
中,大多数时候,每个驱动都必须实现自己的 API 来处理相同的功能,要么是针对内核用户,要么是为了将其内容公开给用户空间。 事实证明,这些驱动严重缺乏抽象代码。 此外,内核中对这些设备数量的支持不断增加,导致了大量的代码重复。
在内核中引入该框架旨在解决上述问题。 它还为消费类设备引入了 DT 表示,以便从 NVMEM 获取它们所需的数据(MAC 地址、SoC/修订 ID、部件号等)。 本章首先介绍 NVMEM 数据结构,这些数据结构是遍历框架所必需的,然后我们将查看 NVMEM 提供程序驱动,从中我们将了解如何向消费者公开 NVMEM 内存区域。 最后,我们将了解 NVMEM 消费者驱动因素,以利用提供商公开的内容。
在本章中,我们将介绍以下主题:
- 介绍 NVMEM 数据结构和 API
- 编写 NVMEM 提供程序驱动
- NVMEM 消费者驱动 API
以下是本章的前提条件:
- C 语言编程技巧
- 内核编程和设备驱动开发技能
- Linux 内核 v4.19.X 源代码,可从https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags获得
NVMEM 是一个减少了 API 和数据结构集的小型框架。 在本节中,我们将介绍这些 API 和数据结构,以及作为此框架的基础的单元的概念。
NVMEM 基于生产者/消费者模式,就像第 4 章,冲击公共时钟框架中描述的时钟框架。 NVMEM 设备只有一个驱动,公开设备单元,以便消费者驱动可以访问和操作它们。 虽然 NVMEM 设备驱动必须包含<linux/nvmem-provider.h>
,但消费者必须包含<linux/nvmem-consumer.h>
。 该框架只有几个数据结构,其中包括struct nvmem_device
,如下所示:
struct nvmem_device {
const char *name;
struct module *owner;
struct device dev;
int stride;
int word_size;
int id;
int users;
size_t size;
bool read_only;
int flags;
nvmem_reg_read_t reg_read;
nvmem_reg_write_t reg_write; void *priv;
[...]
};
这个结构实际上抽象了真实的 NVMEM 硬件。 它是在设备注册时由框架创建并填充的。 也就是说,它的字段实际上是用struct nvmem_config
中的字段的完整副本设置的,如下所述:
struct nvmem_config {
struct device *dev;
const char *name;
int id;
struct module *owner;
const struct nvmem_cell_info *cells;
int ncells;
bool read_only;
bool root_only;
nvmem_reg_read_t reg_read; nvmem_reg_write_t reg_write;
int size;
int word_size;
int stride;
void *priv;
[...]
};
此结构是 NVMEM 设备的运行时配置,提供有关该设备的信息或访问其数据单元的助手函数。 在设备注册时,其大部分字段用于填充新创建的nvmem_device
结构。
结构中字段的含义如下所示(知道这些字段用于构建底层struct nvmem_device
):
-
dev
是父设备。 -
name
是此 NVMEM 设备的可选名称。 它与填充的id
一起使用,以构建完整的设备名称。 最终的 NVMEM 设备名称将为<name><id>
。 最好在名称中附加-
,这样全名就可以有这样的模式:<name>-<id>
。 这就是 PCF85363 驱动中使用的。 如果省略,将使用nvmem<id>
作为默认名称。 -
id
是此 NVMEM 设备的可选 ID。 如果name
为NULL
,则忽略它。 如果设置为-1
,内核将负责为设备提供唯一 ID。 -
owner
是拥有此 NVMEM 设备的模块。 -
cells
是预定义的 NVMEM 单元数组。 这是可选的。 -
ncells
是单元格中元素的数量。 -
read_only
将此设备标记为只读。 -
root_only
指示此设备是否只能由根用户访问。 -
reg_read
和reg_write
分别是框架用来读写数据的底层回调。 它们的定义如下:typedef int (*nvmem_reg_read_t)(void *priv, unsigned int offset, void *val, size_t bytes); typedef int (*nvmem_reg_write_t)(void *priv, unsigned int offset, void *val, size_t bytes);
-
size
表示设备的大小。 -
word_size
是此设备的最小读/写访问粒度。stride
是最小的读/写访问跨度。 它的原理已经在前面的章节中解释过了。 -
priv
是传递给读/写回调的上下文数据。 例如,它可能是一个更大的结构,包裹着这个 NVMEM 设备。
以前,我们使用术语数据单元格。 数据单元表示 NVMEM 器件中的存储区(或数据区)。 这也可能是设备的全部内存。 实际上,数据单元格将分配给消费者驱动。 这些内存区域由框架使用两种不同的数据结构维护,具体取决于我们是在消费者端还是在提供者端。 这些是提供者的struct nvmem_cell_info
结构和消费者的struct nvmem_cell
结构。 在 NVMEM 核心代码中,内核使用nvmem_cell_info_to_nvmem_cell()
从前一个结构切换到第二个结构。
这些结构介绍如下:
struct nvmem_cell {
const char *name;
int offset;
int bytes;
int bit_offset;
int nbits;
struct nvmem_device *nvmem;
struct list_head node;
};
另一个数据结构,即struct nvmem_cell
,如下所示:
struct nvmem_cell_info {
const char *name;
unsigned int offset;
unsigned int bytes;
unsigned int bit_offset;
unsigned int nbits;
};
如您所见,前面的两个数据结构共享几乎相同的属性。 让我们来看看它们的含义,如下所示:
name
是单元格的名称。offset
是单元相对于整个硬件数据寄存器的偏移量(起始位置)。bytes
是从offset
开始的数据单元格的大小(以字节为单位)。- 信元可以具有位级粒度。 对于这些单元,应设置
bit_offset
以指定单元内的位偏移量,并应根据感兴趣区域的大小(以位为单位)定义nbits
。 nvmem
是此单元所属的 NVMEM 设备。node
用于在整个系统范围内跟踪小区。 此字段最终显示在nvmem_cells
列表中,该列表包含系统上所有可用的单元,而不管它们属于哪种 NVMEM 设备。 该全局列表实际上受互斥锁nvmem_cells_mutex
保护,两者都是在drivers/nvmem/core.c
中静态定义的。
为了阐明前面的解释,让我们以具有以下配置的单元格为例:
static struct nvmem_cellinfo mycell = {
.offset = 0xc,
.bytes = 0x1,
[...],
}
在前面的示例中,如果我们认为.nbits
和.bit_offset
都等于0
,这意味着我们对单元格的整个数据区域感兴趣,在我们的例子中是 1 字节大小。 但是,如果我们只对第 2 到 4 位(实际上是 3 位)感兴趣呢? 结构将如下所示:
staic struct nvmem_cellinfo mycell = {
.offset = 0xc,
.bytes = 0x1,
.bit_offset = 2,
.nbits = 2 [...]
}
重要音符
前面的例子仅用于教学目的。 尽管您可以在驱动代码中预定义单元,但建议您依赖设备树来声明单元,准确地说,我们将在本章后面的NVMEM 提供程序的设备树绑定部分中看到这一点。
使用者和提供者驱动都不应创建struct nvmem_cell
的实例。 当生产者提供单元信息的数组时,或者当消费者请求单元时,NVMEM 核心在内部处理这一问题。
到目前为止,我们已经了解了该框架提供的数据结构和 API。 但是,可以从内核或用户空间访问 NVMEM 设备。 此外,在内核中,为了让其他驱动访问设备存储,必须有一个公开设备存储的驱动。 这是生产者/消费者设计,其中提供者驱动是生产者,另一个驱动是消费者。 现在,让我们从这个框架的提供者(也就是生产者)开始。
提供者是公开设备内存的提供者,以便其他驱动(使用者)可以访问它。 这些驱动的主要任务如下:
- 提供关于设备数据表的适当 NVMEM 配置,以及允许您访问内存的例程
- 在系统中注册设备
- 提供设备树绑定文档
这就是提供者要做的全部事情。 机制/逻辑的大部分(其余)由 NVMEM Fra 框架的代码处理。
注册/注销 NVMEM 设备实际上是提供程序端驱动的一部分,它可以使用nvmem_register()
/nvmem_unregister()
函数或其托管版本devm_nvmem_register()
/devm_nvmem_unregister()
:
struct nvmem_device *nvmem_register(const struct nvmem_config *config)
struct nvmem_device *devm_nvmem_register(struct device *dev,
const struct nvmem_config *config)
int nvmem_unregister(struct nvmem_device *nvmem)
int devm_nvmem_unregister(struct device *dev,
struct nvmem_device *nvmem)
注册后,将创建/sys/bus/nvmem/devices/dev-name/nvmem
二进制条目。 在这些接口中,*config
参数是描述必须创建的 NVMEM 设备的 NVMEM 配置。 *dev
参数仅适用于托管版本,表示使用 NVMEM 设备的设备。 在成功路径上,这些函数返回指向nvmem_device
的指针,否则在出错时返回ERR_PTR()
。
另一方面,注销函数接受指向在注册函数的成功路径上创建的 NVMEM 设备的指针。 它们在成功取消注册时返回0
,否则返回负的错误。
存在嵌入非易失性存储的许多实时时钟(rtc)设备。 这种嵌入式存储器可以是 EEPROM 或电池后备 RAM。 查看include/linux/rtc.h
中的 RTC 设备数据结构,您会注意到有一些与 NVMEM 相关的字段,如下所示:
struct rtc_device {
[...]
struct nvmem_device *nvmem;
/* Old ABI support */
bool nvram_old_abi;
struct bin_attribute *nvram;
[...]
}
请注意前面结构摘录中的以下内容:
nvmem
抽象底层硬件内存。nvram_old_abi
是一个布尔值,它告诉是否使用旧的(现已弃用)NVRAM ABI 注册此 RTC 的 NVMEM,该 NVRAM ABI 使用/sys/class/rtc/rtcx/device/nvram
公开内存。 仅当您有使用此旧 ABI 界面的现有应用(您不想中断)时,此字段才应设置为true
。 新司机不应设置此设置。nvram
实际上是底层内存的二进制属性,RTC 框架仅用于旧的 ABI 支持;也就是说,如果nvram_old_abi
为true
。
可以通过RTC_NVMEM
内核配置选项启用与 RTC 相关的 NVMEM 框架 API。 此接口在drivers/rtc/nvmem.c
中定义,分别公开rtc_nvmem_register()
和rtc_nvmem_unregister()
,用于 RTC-NVMEM 注册和取消注册。 具体内容如下:
int rtc_nvmem_register(struct rtc_device *rtc,
struct nvmem_config *nvmem_config)
void rtc_nvmem_unregister(struct rtc_device *rtc)
rtc_nvmem_register()
成功时返回0
。 它接受有效的 RTC 设备作为其第一个参数。 这会对代码产生影响。 这意味着 RTC 的 NVMEM 应仅在实际 RTC 设备成功注册后注册。 换句话说,只有在rtc_register_device()
成功之后才会调用rtc_nvmem_register()
。 第二个参数应该是指向有效nvmem_config
对象的指针。 此外,正如我们已经看到的,此配置可以在堆栈中声明,因为它的所有字段都被完全复制以构建nvmem_device
结构。 相反的是rtc_nvmem_unregister()
,它取消注册 NVMEM。
让我们用 DS1307 RTC 驱动drivers/rtc/rtc-ds1307.c
的probe
函数的摘录来总结一下:
static int ds1307_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct ds1307 *ds1307;
int err = -ENODEV;
int tmp;
const struct chip_desc *chip;
[...]
ds1307->rtc->ops = chip->rtc_ops ?: &ds13xx_rtc_ops;
err = rtc_register_device(ds1307->rtc);
if (err)
return err;
if (chip->nvram_size) {
struct nvmem_config nvmem_cfg = {
.name = "ds1307_nvram",
.word_size = 1,
.stride = 1,
.size = chip->nvram_size,
.reg_read = ds1307_nvram_read,
.reg_write = ds1307_nvram_write,
.priv = ds1307,
};
ds1307->rtc->nvram_old_abi = true;
rtc_nvmem_register(ds1307->rtc, &nvmem_cfg);
}
[...]
}
前面的代码在注册 NVMEM 设备之前首先向内核注册 RTC,提供与 RTC 的存储空间相对应的 NVMEM 配置。 前面的代码是与 RTC 相关的,不是泛型的。 其他 NVMEM 设备必须让其驱动公开回调,NVMEM 框架将从用户空间或内核内部将任何读/写请求转发到回调。 下一节将解释如何做到这一点。
为了使内核和其他框架能够从/向 NVMEM 设备及其单元读/写数据,每个 NVMEM 提供程序必须公开几个允许这些读/写操作的回调。 该机制允许独立于硬件的使用者代码,因此来自使用者端的任何读/写请求都被重定向到底层提供者的读/写回调。 以下是每个提供程序必须遵守的读/写原型:
typedef int (*nvmem_reg_read_t)(void *priv, unsigned int offset,
void *val, size_t bytes);
typedef int (*nvmem_reg_write_t)(void *priv, unsigned int offset,
void *val, size_t bytes);
它们独立于 NVMEM 设备所在的底层总线。 nvmem_reg_read_t
用于从 NVMEM 设备读取数据。 priv
是 NVMEM 配置中提供的用户上下文,offset
是读取的开始位置,val
是必须存储读取数据的输出缓冲区,bytes
是要读取的数据的大小(实际上是字节数)。 此函数应在成功时返回成功读取的字节数,在出错时返回负错误代码。
另一方面,nvmem_reg_write_t
用于书写。 priv
的含义与读取相同,offset
是写入应开始的位置,val
是包含要写入的数据的缓冲区,bytes
是val
中应写入的数据的字节数。 bytes
不一定是val
的大小。 这个有趣的部分应该在成功时返回成功写入的字节数,在出错时返回负的错误代码。
现在我们已经了解了如何实现提供程序读/写回调,让我们看看如何使用设备树扩展提供程序功能。
NVMEM 数据提供程序没有任何特别的绑定。 应该相对于其父总线 DT 绑定来描述 IT。 这意味着,例如,如果它是 I2C 设备,则应该(相对于 I2C 绑定)将其描述为代表其后面的 I2C 总线的节点的子节点。 但是,有一个可选的read-only
属性使设备成为只读。 此外,每个子节点将被视为一个数据单元(NVMEM 设备中的存储区域)。
让我们考虑以下 MMIO NVMEM 设备及其子节点进行说明:
ocotp: ocotp@21bc000 {
#address-cells = <1>;
#size-cells = <1>;
compatible = "fsl,imx6sx-ocotp", "syscon";
reg = <0x021bc000 0x4000>;
[...]
tempmon_calib: calib@38 {
reg = <0x38 4>;
};
tempmon_temp_grade: temp-grade@20 {
reg = <0x20 4>;
};
foo: foo@6 {
reg = <0x6 0x2> bits = <7 2>
};
[...]
};
根据子节点中定义的属性,NVMEM 框架构建适当的nvmem_cell
结构,并将它们插入到系统范围的nvmem_cells
列表中。 以下是数据单元格绑定的可能属性:
reg
:此属性是必需的。 它是一个双单元属性,以字节为单位描述 NVMEM 设备内数据区域的偏移量(该属性的第一个单元)和以字节为单位的大小(该属性的第二个单元)。bits
:这是一个可选的两个单元格的属性,它以位为单位指定偏移量(从0
到7
的可能值)和reg
属性指定的地址范围内的位数。
在提供者节点中定义了数据单元格之后,可以使用nvmem-cells
属性将这些数据单元格分配给使用者,该属性是 NVMEM 提供者的 phandle 列表。 此外,还应该有一个nvmem-cell-names
属性,它的主要用途是命名每个数据单元格。 因此,这个分配的名称可以用来使用使用者 API 查找适当的数据单元格。 以下是一个分配示例:
tempmon: tempmon {
compatible = "fsl,imx6sx-tempmon", "fsl,imx6q-tempmon";
interrupt-parent = <&gpc>;
interrupts = <GIC_SPI 49 IRQ_TYPE_LEVEL_HIGH>;
fsl,tempmon = <&anatop>;
clocks = <&clks IMX6SX_CLK_PLL3_USB_OTG>;
nvmem-cells = <&tempmon_calib>, <&tempmon_temp_grade>;
nvmem-cell-names = "calib", "temp_grade";
};
在Documentation/devicetree/bindings/nvmem/nvmem.txt
中提供了完整的 NVMEM 设备树绑定。
我们刚刚遇到了驱动(所谓的生产者)的实现,它们公开了 NVMEM 设备的存储。 虽然情况并不总是如此,但内核中可能还有其他驱动需要访问生产者(也就是提供者)公开的存储。 下一节将详细描述这些驱动。
NVMEM 消费者是访问生产者公开的存储的驱动。 这些驱动可以通过包含<linux/nvmem-consumer.h>
来拉入 NVMEM 消费者 API,这将引入以下基于单元的 API:
struct nvmem_cell *nvmem_cell_get(struct device *dev,
const char *name);
struct nvmem_cell *devm_nvmem_cell_get(struct device *dev,
const char *name);
void nvmem_cell_put(struct nvmem_cell *cell);
void devm_nvmem_cell_put(struct device *dev,
struct nvmem_cell *cell);
void *nvmem_cell_read(struct nvmem_cell *cell, size_t *len);
int nvmem_cell_write(struct nvmem_cell *cell, void *buf, size_t len);
int nvmem_cell_read_u32(struct device *dev, const char *cell_id,
u32 *val);
以devm_
前缀的 API 是资源管理版本,将在任何可能的情况下使用。
也就是说,消费者接口完全依赖于生产者公开(部分)其单元以便其他人可以访问它们的能力。 如前所述,这种提供/暴露单元的能力应该通过设备树来实现。 devm_nvmem_cell_get()
用于获取与通过nvmem-cell-names
属性分配的名称相关的给定单元格。 如果可能,nvmem_cell_read
API 总是读取整个单元格大小(即nvmem_cell->bytes
)。 它的第三个参数len
是一个输出参数,它保存正在读取的nvmem_config.word_size
的实际数量(实际上,它大部分时间保存1
,这意味着一个字节)。
在成功读取时,len
指向的内容将等于单元格中的字节数:*len = nvmem_cell->bytes
。 另一方面,nvmem_cell_read_u32()
将单元格值读取为u32
。
下面的代码抓取分配给上一节描述的tempmon
节点的单元格,并读取它们的内容:
static int imx_init_from_nvmem_cells(struct platform_device *pdev)
{
int ret; u32 val;
ret = nvmem_cell_read_u32(&pdev->dev, "calib", &val);
if (ret)
return ret;
ret = imx_init_calib(pdev, val);
if (ret)
return ret;
ret = nvmem_cell_read_u32(&pdev->dev, "temp_grade", &val);
if (ret)
return ret;
imx_init_temp_grade(pdev, val);
return 0;
}
在这里,我们已经了解了这个框架的消费者和生产者两个方面。 通常,驱动需要向用户空间公开他们的服务。 NVMEM 框架(就像其他 Linux 内核框架一样)可以透明地处理向用户空间公开 NVMEM 服务。 下一节将详细说明这一点。
与大多数内核框架一样,NVMEM 用户空间界面依赖于sysfs
。 在系统中注册的每个 NVMEM 设备都有一个在/sys/bus/nvmem/devices
中创建的目录项,以及在该目录中创建的一个nvmem
二进制文件(您可以在该文件上使用hexdump
甚至echo
),该文件表示设备的内存。 完整路径的模式如下:/sys/bus/nvmem/devices/<dev-name>X/nvmem
。 在此路径模式中,<dev-name>
是生产者驱动提供的nvmem_config.name
名称。 以下代码摘录显示了 NVMEM 核心如何构造<dev-name>X
模式:
int rval;
rval = ida_simple_get(&nvmem_ida, 0, 0, GFP_KERNEL);
nvmem->id = rval;
if (config->id == -1 && config->name) {
dev_set_name(&nvmem->dev, "%s", config->name);
} else {
dev_set_name(&nvmem->dev, "%s%d", config->name ? : "nvmem",
config->name ? config->id : nvmem->id);
}
前面的代码说明如果nvmem_config->id == -1
,则省略模式中的X
,并且只使用nvmem_config->name
来命名sysfs
目录项。 如果设置了nvmem_config->id != -1
和nvmem_config->name
,它将与驱动设置的nvmem_config->id
字段(在图案中为X
)一起使用。 但是,如果驱动未设置nvmem_config->name
,则内核将使用nvmem
字符串和已生成的 ID(在模式中为X
)。
重要音符
无论定义什么单元,NVMEM 框架都会通过 NVMEM 二进制文件(而不是单元)公开整个寄存器空间。 从用户空间访问单元格需要事先知道它们的偏移量和大小。
然后,可以使用hexdump
或简单的cat
命令在用户空间中读取 NVMEM 内容,这要归功于sysfs
界面。 例如,假设我们有一个 I2C EEPROM,位于地址 0x55 的 I2C 编号 2 上,并在系统上注册为 NVMEM 设备,则其sysfs
路径为/sys/bus/nvmem/devices/2-00550/nvmem
。 以下是您如何写/读一些内容:
cat /sys/bus/nvmem/devices/2-00550/nvmem
echo "foo" > /sys/bus/nvmem/devices/2-00550/nvmem
cat /sys/bus/nvmem/devices/2-00550/nvmem
现在我们已经看到了 NVMEM 寄存器是如何向用户空间公开的。 虽然这一节很短,但我们已经涵盖了足够多的内容,足以从用户空间利用这个框架。
在本章中,我们介绍了 NVMEM 框架在 Linux 内核中的实现。 我们从生产者端和消费者端介绍了它的 API,并讨论了如何从用户空间使用它。 我毫不怀疑这些设备在嵌入式世界中占有一席之地。
在下一章中,我们将通过看门狗设备来解决可靠性问题,讨论如何设置这些设备并编写它们的 Linux 内核驱动。