Linux NVMEM子系统为Non-Volatile类型存储提供统一内核处理框架,主要用于实现EEPROM、Efuse等非易失存储器的统一管理。NVMEM子系统整体架构相对简单,主要包括以下几个部分组成。
典型的nvmem执行框架如下所示。
本节目录如下所示。
对于nvmem子系统,其主要实现代码在drivers/nvmem目录下的core.c,实现了nvmem的注册,移除相关的接口。对于驱动来说,只需要直接注册即可使用,主要涉及接口如下所示。
//在系统中注册nvmem设备(devm管理)
struct nvmem_device *devm_nvmem_register(struct device *dev, const struct nvmem_config *config)
//在系统中注册nvmem设备
struct nvmem_device *nvmem_register(const struct nvmem_config *config)
//移除已经注册的nvmem设备
void nvmem_unregister(struct nvmem_device *nvmem)
其中最重要的就是nvmem_config和nvmem_config两个选项,具体说明如下。
enum nvmem_type {
NVMEM_TYPE_UNKNOWN = 0, //未知类型
NVMEM_TYPE_EEPROM, //epprom
NVMEM_TYPE_OTP, //otp
NVMEM_TYPE_BATTERY_BACKED, //电池备份区
NVMEM_TYPE_FRAM, //fram存储器
};
struct nvmem_config {
struct device *dev; //父设备,nvmem所在的节点设备(需要)
const char *name; //可选名称,nvmem在系统中的节点名称(可选)
int id; //可选设备ID,如果名称不为NULL时和name共同构成节点名称(可选)
struct module *owner; //模块所有者,一般为THIS_MODULE(需要)
const struct nvmem_cell_info *cells; //预定义NVMEM单元的可选数组(可选)
int ncells; //单元格中的元素个数(可选)
const struct nvmem_keepout *keepout; //可选的keepout范围数组(可选)
unsigned int nkeepout; //keepout范围数组中的元素数(可选)
enum nvmem_type type; //nvmem类型
bool read_only; //设备只允许读取(需要)
bool root_only; //设备只允许root用户访问(需要)
bool ignore_wp; //写保护脚由供应商管理(nvmem不处理写保护硬件,可选)
struct device_node *of_node; //定义用于取代父节点的of_node(可选)
bool no_of_node; //是否使用父节点的of_node(可选)
nvmem_reg_read_t reg_read; //定义读取nvmem的回调函数(需要)
nvmem_reg_write_t reg_write; //定义写入nvmem的回调函数(需要)
nvmem_cell_post_process_t cell_post_process; //针对供应商特定的单元数据后处理的回调(可选)
int size; //设备的存储总长度(需要)
int word_size; //定义设备的读/写一个的长度(需要)
int stride; //连续访问时,访问的最小间隔步幅(需要)
void *priv; //模块的私有数据,保存操作(可选)
/* To be only used by old driver/misc/eeprom drivers */
bool compat; //兼容老的eeprom的参数
struct device *base_dev;
};
struct nvmem_device {
struct module *owner; //模块所有者,一般为THIS_MODULE
struct device dev; //父设备,nvmem所在的节点设备
int stride; //连续访问时,访问的最小间隔步幅
int word_size; //定义设备的读/写一个的长度
int id; //可选设备ID,如果名称不为NULL时和name共同构成节点名称
struct kref refcnt; //设备内核引用计数机制
size_t size; //设备的存储总长度
bool read_only; //设备只允许读取
bool root_only; //设备只允许root用户访问
int flags; //支持兼容的标志位
enum nvmem_type type; //nvmem类型
struct bin_attribute eeprom; //eeprom属性文件
struct device *base_dev; //兼容老的eeprom的参数
struct list_head cells; //预定义NVMEM单元的列表
const struct nvmem_keepout *keepout; //可选的keepout范围数组
unsigned int nkeepout; //keepout范围数组中的元素数
nvmem_reg_read_t reg_read; //定义读取nvmem的回调函数
nvmem_reg_write_t reg_write; //定义写入nvmem的回调函数
nvmem_cell_post_process_t cell_post_process; //针对供应商特定的单元数据后处理的回调
struct gpio_desc *wp_gpio; //控制写保护的控制引脚
void *priv; //模块的私有数据,保存操作(可选)
};
nvmem provider驱动在系统中注册关联内核文件和系统硬件的接口,可以用于访问eeprom,otocp,以及读写内部寄存器信息等;一般用于内部的存储空间(如otocp)或者外部的EPPROM(挂载在i2c总线)中;不过因为设备上并不存在EPPROM,因此这里以一段内核中的数据作为存储空间,来验证nvmem的应用。
nvmem驱动实现思路如下所示。
如此就可以实现完整的nvmem驱动,具体流程如下所示。
/ {
//...
usr_nvmem {
compatible = "rmk,usr_nvmem"; //设备节点标签,用于驱动匹配
mem,size = <1024>; //定义内部支持的存储空间大小
status = "okay";
};
}
//匹配的是根节点的compatible属性
static const struct of_device_id nvmem_of_match[] = {
{ .compatible = "rmk,usr-nvmem" },
{ /* Sentinel */ }
};
static struct platform_driver platform_driver = {
.driver = {
.name = "nvmem",
.of_match_table = nvmem_of_match,
},
.probe = nvmem_probe,
.remove = nvmem_remove,
};
static int __init nvmem_module_init(void)
{
platform_driver_register(&platform_driver);
return 0;
}
static void __exit nvmem_module_exit(void)
{
platform_driver_unregister(&platform_driver);
}
在probe函数中执行配置,解析设备树节点,同时基于nvmem子系统创建文件节点用于读写访问。
// nvmem信息结构体
struct nvmem_data {
struct mutex lock;
struct device* dev;
struct nvmem_device *nvmem;
char *mem_buffer;
u32 byte_len;
};
// @nvmem_read: nvmem读函数,读取内部数据(可以是具体的内存地址,寄存器,外部的eeprom等)
// @priv: 私有数据,注册时config.priv设置
// @off: 读取的偏移地址
// @val: 读取的数据,已经转换为内核地址,使用memcpy拷贝即可
// @count: 读取的长度
// @return: 0-成功,-失败
static int nvmem_read(void *priv, unsigned int off, void *val, size_t count)
{
struct nvmem_data* nvmem = (struct nvmem_data*)priv;
if (off + count >= nvmem->byte_len) {
dev_err(nvmem->dev, "nvmem_read, off:%d, count:%d, out of range!\n", off, count);
return -ENOMEM;
}
mutex_lock(&nvmem->lock);
memcpy(val, &nvmem->mem_buffer[off], count);
mutex_unlock(&nvmem->lock);
dev_info(nvmem->dev, "nvmem_read, off:%d, count:%d!\n", off, count);
return 0;
}
// @nvmem_write: nvmem写函数,外部数据写入到内核中(可以是具体的内存地址,寄存器,外部的eeprom等)
// @priv: 私有数据,注册时config.priv设置
// @off: 写入的偏移地址
// @val: 写入的数据,已经转换为内核地址,使用memcpy拷贝即可
// @count: 写入的长度
// @return: 0-成功,-失败
static int nvmem_write(void *priv, unsigned int off, void *val, size_t count)
{
struct nvmem_data* nvmem = (struct nvmem_data*)priv;
if (off + count >= nvmem->byte_len) {
dev_err(nvmem->dev, "nvmem_read, off:%d, count:%d, out of range!\n", off, count);
return -ENOMEM;
}
mutex_lock(&nvmem->lock);
memcpy(&nvmem->mem_buffer[off], val, count);
mutex_unlock(&nvmem->lock);
dev_info(nvmem->dev, "nvmem_write, off:%d, count:%d!\n", off, count);
return 0;
}
static int nvmem_probe(struct platform_device *pdev)
{
int ret;
struct nvmem_data* nvmem;
struct nvmem_config config;
struct device *dev = &pdev->dev;
nvmem = devm_kzalloc(dev, sizeof(*nvmem), GFP_KERNEL);
if (!nvmem) {
dev_err(&pdev->dev, "devm_kzalloc nvmem failed!\n");
return -ENOMEM;
}
ret = of_property_read_u32(dev->of_node, "mem,size", &nvmem->byte_len);
if (ret < 0) {
dev_warn(dev, "of_property_read_u32 failed, use default:%d\n", DEFAULT_MEM_SIZE);
nvmem->byte_len = DEFAULT_MEM_SIZE;
}
mutex_init(&nvmem->lock);
nvmem->dev = dev;
nvmem->mem_buffer = devm_kzalloc(dev, nvmem->byte_len, GFP_KERNEL);
if (IS_ERR(nvmem->mem_buffer)) {
dev_err(&pdev->dev, "devm_kzalloc mem_buffer failed!\n");
return -ENOMEM;
}
// 清除config配置
memset((char *)&config, 0, sizeof(config));
config.name = "kernel_nvmem"; //定义nvmem的名称
config.dev = dev;
config.read_only = 0; //定义存储是否可写
config.root_only = 0; //仅允许root用户访问
config.owner = THIS_MODULE;
config.compat = true;
config.base_dev = dev;
config.reg_read = nvmem_read; //读取回调函数
config.reg_write = nvmem_write; //写入回调函数
config.priv = nvmem;
config.stride = 1; //连续访问时,访问的间隔步幅
config.word_size = 1; //最小可读写/访问粒度(访问设备的基本字节单位)
config.size = nvmem->byte_len; //存储块的总长度(单位字节)
nvmem->nvmem = devm_nvmem_register(dev, &config);
if (IS_ERR(nvmem->nvmem)) {
dev_err(&pdev->dev, "devm_nvmem_register failed!\n");
return PTR_ERR(nvmem->nvmem);
}
dev_info(&pdev->dev, "[kernel_nvmem]register %s success!\n", config.name);
return 0;
}
编译加载成功后,显示如下。
# epprom/nvmem访问节点
/sys/devices/platform/user_nvmem/epprom
/sys/devices/platform/user_nvmem/kernel_nvmem0/nvmem
# 向epprom里写入数据
echo "hello test" > /sys/devices/platform/user_nvmem/epprom
# 从epprom中读出数据
hexdump -C /sys/devices/platform/user_nvmem/epprom
本节的详细驱动代码见:nvmem设备驱动文件
应用层使用open, read, write, lseek, close即可访问epprom数据,代码如下。
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#define NVMEM_DEVICE_FILE "/sys/devices/platform/usr_nvmem/eeprom"
int main(int argc, const char *argv[])
{
char w_buffer[64];
char r_buffer[64] = {0};
int fd;
int ret;
fd = open(NVMEM_DEVICE_FILE, O_RDWR | O_NDELAY);
if (fd == -1) {
printf("open %s error", NVMEM_DEVICE_FILE);
return -1;
}
// 写入数据
for (int i = 0; i < 64; i++) {
w_buffer[i] = i;
}
lseek(fd, 100, SEEK_SET);
ret = write(fd, w_buffer, 64);
if (ret < 0) {
printf("write failed:%d\n", ret);
goto __exit;
}
// 读取数据
lseek(fd, 100, SEEK_SET);
ret = read(fd, r_buffer, sizeof(r_buffer));
if (ret < 0) {
printf("read failed:%d\n", ret);
goto __exit;
}
// 比较数据
for (int i = 0; i < 64; i++) {
if (r_buffer[i] != w_buffer[i]) {
printf("no equal, %d:%d\n", r_buffer[i], w_buffer[i]);
goto __exit;
}
}
printf("nvmem test success\n");
__exit:
close(fd);
return 0;
}
交叉编译后运行,测试结果如下所示,表示读写测试成功。
本节的详细驱动代码见:nvmem设备驱动文件
nvmem consumer则是内核中以cell的形式操作nvmem资源的模块。对于consumer的模块并没有限制,可以是cpu时钟管理模块、温控模块、i2c器件模块等;常用于驱动加载中获取配置参数和校准功能等;关于nvmem consumer的接口说明如下所示。
// 获取nvmem_cell资源,用于后续处理
// id为设备树中的nvmem-cell-names信息
struct nvmem_cell *nvmem_cell_get(struct device *dev, const char *id)
// 释放已经获取的nvmem_cell资源
void nvmem_cell_put(struct nvmem_cell *cell);
// 获取nvmem_cell资源(带内核管理资源接口)
struct nvmem_cell *devm_nvmem_cell_get(struct device *dev, const char *id);
// 读取cell中的数据,返回长度和数据指针
void *nvmem_cell_read(struct nvmem_cell *cell, size_t *len)
// 向cell中写入指定长度的数据
int nvmem_cell_write(struct nvmem_cell *cell, void *buf, size_t len);
// 读取u8,u16,u32,u64长度的数据
int nvmem_cell_read_u8(struct device *dev, const char *cell_id, u8 *val);
int nvmem_cell_read_u16(struct device *dev, const char *cell_id, u16 *val);
int nvmem_cell_read_u32(struct device *dev, const char *cell_id, u32 *val);
int nvmem_cell_read_u64(struct device *dev, const char *cell_id, u64 *val);
注意1: 对于cell的读取和写入,都要和cell中的数据长度一致,也就是cell节点中reg属性的第二个长度,这个可以参考nvmem_cell_read和nvmem_cell_write内部实现,会检测是否与cell长度一致,不符合长度会直接返回错误。
注意2:nvmem_cell_read返回的指针,是使用kzalloc申请的内核指针,读取完成后要进行释放,否则会有内存泄漏问题。
上述读取类型中,比较关键的就是nvmem_cell结构体,其格式如下所示。
struct nvmem_cell_entry {
const char *name; // 单元名称
int offset; // 单元偏移
int bytes; // 单元字节数
int bit_offset; // 位偏移量
int nbits; // 位长度
struct device_node *np; // 设备节点指针
struct nvmem_device *nvmem; // 关联的NVMEM设备指针
struct list_head node; // 链表节点
};
struct nvmem_cell {
struct nvmem_cell_entry *entry; // 单元条目指针
const char *id; // 单元标识符
};
不过在我使用版本的kernel中,这两个结构体定义在”/driver/nvmem/core.c”中;虽然能够使用struct nvmem_cell定义变量,但并不能访问对象内容,只能依赖官方提供的接口操作。
了解了相关接口,就可以基于此实现nvmem consumer的应用。关于cell操作的应用比较简单,主要步骤如下
cell往往在具体驱动中只作为数据被提取,用于其它功能的配置;如果没有具体的应用,数据并没有实际的意义;这里以最简单的设备文件应用来测试说明。
usr_nref {
compatible = "rmk,usr_nref";
nvmem-cells = <&nvmem_user_cell>,
<&ocotp_cfg0>,
<&ocotp_cfg1>;
nvmem-cell-names = "nvmem_user_cell", "uid-high", "uid-low";
status = "okay";
};
usr_nvmem {
compatible = "rmk,usr_nvmem";
#address-cells = <1>; //指定子节点寄存器内地址的位数
#size-cells = <1>; //指定子节点寄存器内长度的位数
status = "okay";
nvmem_user_cell: nvmem_user_cell@10 {
reg = <0x10 64>;
};
};
// 读取cell内部的数据
static ssize_t nref_show(struct device *dev, struct device_attribute *attr, char *buf)
{
char *pbuffer;
int len;
struct nvmem_ref_data *chip;
// 读取cell对应nvmem节点数据
// 必定读取cell中指定的长度
chip = container_of(attr, struct nvmem_ref_data, user_cell_attr);
pbuffer = nvmem_cell_read(chip->nref_cell, &len);
if (IS_ERR(pbuffer)) {
dev_err(dev, "cell read failed, ret:%ld, len %d!\n", PTR_ERR(pbuffer), len);
return 0;
}
dev_info(dev, "cell read success, len: %d!\n", len);
memcpy(buf, pbuffer, len);
kfree(pbuffer);
return len;
}
static ssize_t nref_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
int ret, len;
struct nvmem_ref_data *chip;
char *pbuffer;
ssize_t size;
// 拷贝数据到cell地址中
chip = container_of(attr, struct nvmem_ref_data, user_cell_attr);
pbuffer = nvmem_cell_read(chip->nref_cell, &len);
count = count>len?len:count;
memcpy(pbuffer, buf, count);
dev_info(dev, "cell write buffer:%d\n", count);
// 将数据写入到cell中,cell要求写入长度等于entry->bytes
size = nvmem_cell_write(chip->nref_cell, pbuffer, len);
if (size != len) {
dev_err(dev, "cell write failed, ret:%d!\n", ret);
}
kfree(pbuffer);
return count;
}
static int nref_probe(struct platform_device *pdev)
{
int ret;
static struct nvmem_ref_data *chip;
chip = devm_kzalloc(&pdev->dev, sizeof(*chip), GFP_KERNEL);
if (!chip) {
dev_err(&pdev->dev, "[devm_kzalloc]malloc failed!\n");
return -ENOMEM;
}
platform_set_drvdata(pdev, chip);
chip->pdev = pdev;
// 创建user_cell对应文件,获取系统的cell资源
chip->user_cell_attr.attr.name = "user_cell";
chip->user_cell_attr.attr.mode = 0666;
chip->user_cell_attr.show = nref_show;
chip->user_cell_attr.store = nref_store;
ret = device_create_file(&pdev->dev, &chip->user_cell_attr);
if (ret != 0) {
dev_err(&pdev->dev, "device create user_cell_attr file failed!\n");
return -ENOMEM;
}
chip->nref_cell = devm_nvmem_cell_get(&pdev->dev, "nvmem_user_cell");
if (IS_ERR(chip->nref_cell)) {
dev_err(&pdev->dev, "get nvmem cell failed!\n");
goto exit_cell_get;
}
// 创建uid_high文件
chip->uid_high_attr.attr.name = "uid_high";
chip->uid_high_attr.attr.mode = 0444;
chip->uid_high_attr.show = uid_high_show;
ret = device_create_file(&pdev->dev, &chip->uid_high_attr);
if (ret != 0) {
dev_err(&pdev->dev, "device create uid_high_attr file failed!\n");
goto exit_uid_high_create_file;
}
// 创建uid_low文件
chip->uid_low_attr.attr.name = "uid_low";
chip->uid_low_attr.attr.mode = 0444;
chip->uid_low_attr.show = uid_low_show;
ret = device_create_file(&pdev->dev, &chip->uid_low_attr);
if (ret != 0) {
dev_err(&pdev->dev, "device create uid_low_attr file failed!\n");
goto exit_uid_low_create_file;
}
dev_info(&pdev->dev, "nref driver init success!\n");
return 0;
exit_uid_low_create_file:
device_remove_file(&pdev->dev, &chip->uid_high_attr);
exit_uid_high_create_file:
nvmem_cell_put(chip->nref_cell);
exit_cell_get:
device_remove_file(&pdev->dev, &chip->user_cell_attr);
return -ENODEV;
}
创建完成后,使用如下命令即可访问cell节点对应的文件。
# 读取user_cell节点内数据
hexdump -C /sys/devices/platform/usr_nref/user_cell
# 向节点写入数据(内部做了处理,保证写入长度和cell一致)
echo "123" > /sys/devices/platform/usr_nref/user_cell
效果如下所示。
关于nvmem_consumer的实现,参考代码如下所示。
本节的详细驱动代码见:linux nvmem_ref代码。
直接开始下一节说明: 块设备管理框架