Skip to content

Latest commit

 

History

History
397 lines (307 loc) · 23.4 KB

File metadata and controls

397 lines (307 loc) · 23.4 KB

十二、利用 NVMEM 框架

NVMEM(非易失性存储器)框架是处理非易失性存储(如 EEPROM、eFuse 等)的核心层。 这些设备的驱动过去存储在drivers/misc/中,大多数时候,每个驱动都必须实现自己的 API 来处理相同的功能,要么是针对内核用户,要么是为了将其内容公开给用户空间。 事实证明,这些驱动严重缺乏抽象代码。 此外,内核中对这些设备数量的支持不断增加,导致了大量的代码重复。

在内核中引入该框架旨在解决上述问题。 它还为消费类设备引入了 DT 表示,以便从 NVMEM 获取它们所需的数据(MAC 地址、SoC/修订 ID、部件号等)。 本章首先介绍 NVMEM 数据结构,这些数据结构是遍历框架所必需的,然后我们将查看 NVMEM 提供程序驱动,从中我们将了解如何向消费者公开 NVMEM 内存区域。 最后,我们将了解 NVMEM 消费者驱动因素,以利用提供商公开的内容。

在本章中,我们将介绍以下主题:

  • 介绍 NVMEM 数据结构和 API
  • 编写 NVMEM 提供程序驱动
  • NVMEM 消费者驱动 API

技术要求

以下是本章的前提条件:

NVMEM 数据结构和 API 简介

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。 如果nameNULL,则忽略它。 如果设置为-1,内核将负责为设备提供唯一 ID。

  • owner是拥有此 NVMEM 设备的模块。

  • cells是预定义的 NVMEM 单元数组。 这是可选的。

  • ncells是单元格中元素的数量。

  • read_only将此设备标记为只读。

  • root_only指示此设备是否只能由根用户访问。

  • reg_readreg_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 配置,以及允许您访问内存的例程
  • 在系统中注册设备
  • 提供设备树绑定文档

这就是提供者要做的全部事情。 机制/逻辑的大部分(其余)由 NVMEM Fra 框架的代码处理。

NVMEM 设备(取消)注册

注册/注销 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 设备中的 NVMEM 存储

存在嵌入非易失性存储的许多实时时钟(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_abitrue

可以通过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.cprobe函数的摘录来总结一下:

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 设备及其单元读/写数据,每个 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是包含要写入的数据的缓冲区,bytesval中应写入的数据的字节数。 bytes不一定是val的大小。 这个有趣的部分应该在成功时返回成功写入的字节数,在出错时返回负的错误代码。

现在我们已经了解了如何实现提供程序读/写回调,让我们看看如何使用设备树扩展提供程序功能。

NVMEM 提供程序的设备树绑定

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:这是一个可选的两个单元格的属性,它以位为单位指定偏移量(从07的可能值)和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 消费者驱动 API

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_readAPI 总是读取整个单元格大小(即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

与大多数内核框架一样,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 != -1nvmem_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 内核驱动。