NVMEM(Non-Volatile Memory)是一个用于访问非易失性存储器的框架,提供了统一的接口来读写各种类型的非易失性存储设备,如:
RT-Thread 的 NVMEM 框架基于 Linux 内核的 NVMEM 子系统设计,提供:
Device Drivers --->
[*] Using NVMEM (Non-Volatile Memory) device drivers
配置选项:
RT_USING_NVMEM:启用 NVMEM 框架支持RT-Thread Configuration
→ Device Drivers
→ Using NVMEM (Non-Volatile Memory) device drivers
定义一个 NVMEM 设备(如 EEPROM):
eeprom@50 {
compatible = "atmel,24c256";
reg = <0x50>;
#address-cells = <1>;
#size-cells = <1>;
/* 定义 NVMEM cells */
mac_address: mac-addr@0 {
reg = <0x0 0x6>; /* 偏移 0x0,长度 6 字节 */
};
board_id: board-id@6 {
reg = <0x6 0x2>; /* 偏移 0x6,长度 2 字节 */
};
serial_number: serial@8 {
reg = <0x8 0x10>; /* 偏移 0x8,长度 16 字节 */
};
calibration: calib@20 {
reg = <0x20 0x10>; /* 偏移 0x20,长度 16 字节 */
bits = <0 32>; /* 位偏移 0,位长度 32 */
};
};
引用 NVMEM cells:
ethernet@40028000 {
compatible = "vendor,eth";
reg = <0x40028000 0x1000>;
/* 引用 MAC 地址 cell */
nvmem-cells = <&mac_address>;
nvmem-cell-names = "mac-address";
};
adc@40012000 {
compatible = "vendor,adc";
reg = <0x40012000 0x400>;
/* 引用校准数据 */
nvmem-cells = <&calibration>;
nvmem-cell-names = "calibration";
};
带写保护引脚的 EEPROM:
eeprom@50 {
compatible = "atmel,24c256";
reg = <0x50>;
/* 写保护 GPIO */
wp-gpios = <&gpio1 10 GPIO_ACTIVE_HIGH>;
mac_address: mac-addr@0 {
reg = <0x0 0x6>;
};
};
通过名称获取 NVMEM cell。
struct rt_nvmem_cell *rt_nvmem_get_cell_by_name(struct rt_device *dev,
const char *name);
参数:
dev:设备指针name:cell 名称(设备树中的 nvmem-cell-names)返回值:
示例:
struct rt_nvmem_cell *cell;
cell = rt_nvmem_get_cell_by_name(dev, "mac-address");
if (!cell) {
rt_kprintf("Failed to get NVMEM cell\n");
return -RT_ERROR;
}
通过索引获取 NVMEM cell。
struct rt_nvmem_cell *rt_nvmem_get_cell_by_index(struct rt_device *dev,
rt_uint32_t index);
参数:
dev:设备指针index:cell 索引(从 0 开始)返回值:
释放 NVMEM cell 引用。
void rt_nvmem_put_cell(struct rt_nvmem_cell *cell);
参数:
cell:要释放的 cell 指针从 cell 读取数据。
rt_ssize_t rt_nvmem_cell_read(struct rt_nvmem_cell *cell,
rt_off_t *offset,
void *buf,
rt_size_t size);
参数:
cell:cell 指针offset:读取偏移(可为 RT_NULL,使用 cell 的偏移)buf:数据缓冲区size:要读取的字节数返回值:
示例:
rt_uint8_t mac[6];
rt_ssize_t ret;
ret = rt_nvmem_cell_read(cell, RT_NULL, mac, sizeof(mac));
if (ret != sizeof(mac)) {
rt_kprintf("Failed to read MAC address\n");
return -RT_ERROR;
}
向 cell 写入数据。
rt_ssize_t rt_nvmem_cell_write(struct rt_nvmem_cell *cell,
rt_off_t *offset,
const void *buf,
rt_size_t size);
参数:
cell:cell 指针offset:写入偏移(可为 RT_NULL)buf:要写入的数据size:要写入的字节数返回值:
注意:
读取 8 位无符号整数。
rt_err_t rt_nvmem_cell_read_u8(struct rt_nvmem_cell *cell,
rt_off_t *offset,
rt_uint8_t *val);
读取 16 位无符号整数。
rt_err_t rt_nvmem_cell_read_u16(struct rt_nvmem_cell *cell,
rt_off_t *offset,
rt_uint16_t *val);
读取 32 位无符号整数。
rt_err_t rt_nvmem_cell_read_u32(struct rt_nvmem_cell *cell,
rt_off_t *offset,
rt_uint32_t *val);
读取 64 位无符号整数。
rt_err_t rt_nvmem_cell_read_u64(struct rt_nvmem_cell *cell,
rt_off_t *offset,
rt_uint64_t *val);
参数:
cell:cell 指针offset:读取偏移(可为 RT_NULL)val:存储读取值的指针返回值:
RT_EOK:成功示例:
rt_uint32_t board_id;
if (rt_nvmem_cell_read_u32(cell, RT_NULL, &board_id) == RT_EOK) {
rt_kprintf("Board ID: 0x%08x\n", board_id);
}
#include <rtthread.h>
#include <rtdevice.h>
#include <drivers/ofw.h>
#include <drivers/nvmem.h>
struct eth_device {
struct rt_device parent;
struct rt_device_phy phy;
rt_uint8_t mac_addr[6];
/* 其他字段... */
};
static rt_err_t eth_load_mac_address(struct eth_device *eth,
struct rt_device *dev)
{
struct rt_nvmem_cell *cell;
rt_ssize_t ret;
/* 通过名称获取 MAC 地址 cell */
cell = rt_nvmem_get_cell_by_name(dev, "mac-address");
if (!cell) {
rt_kprintf("No MAC address in NVMEM\n");
return -RT_ERROR;
}
/* 读取 MAC 地址 */
ret = rt_nvmem_cell_read(cell, RT_NULL, eth->mac_addr, 6);
if (ret != 6) {
rt_kprintf("Failed to read MAC address: %d\n", ret);
rt_nvmem_put_cell(cell);
return -RT_ERROR;
}
/* 释放 cell */
rt_nvmem_put_cell(cell);
/* 验证 MAC 地址 */
if (eth->mac_addr[0] == 0x00 && eth->mac_addr[1] == 0x00 &&
eth->mac_addr[2] == 0x00 && eth->mac_addr[3] == 0x00 &&
eth->mac_addr[4] == 0x00 && eth->mac_addr[5] == 0x00) {
rt_kprintf("Invalid MAC address (all zeros)\n");
return -RT_ERROR;
}
rt_kprintf("MAC Address: %02x:%02x:%02x:%02x:%02x:%02x\n",
eth->mac_addr[0], eth->mac_addr[1], eth->mac_addr[2],
eth->mac_addr[3], eth->mac_addr[4], eth->mac_addr[5]);
return RT_EOK;
}
static rt_err_t eth_probe(struct rt_platform_device *pdev)
{
struct eth_device *eth;
rt_err_t ret;
eth = rt_calloc(1, sizeof(*eth));
if (!eth) {
return -RT_ENOMEM;
}
/* 从 NVMEM 加载 MAC 地址 */
ret = eth_load_mac_address(eth, &pdev->parent);
if (ret != RT_EOK) {
/* 使用默认或随机 MAC 地址 */
rt_kprintf("Using default MAC address\n");
eth->mac_addr[0] = 0x00;
eth->mac_addr[1] = 0x11;
eth->mac_addr[2] = 0x22;
eth->mac_addr[3] = 0x33;
eth->mac_addr[4] = 0x44;
eth->mac_addr[5] = 0x55;
}
/* 将 MAC 地址写入硬件寄存器 */
/* ... */
rt_platform_device_set_data(pdev, eth);
return RT_EOK;
}
static struct rt_platform_driver eth_driver = {
.name = "eth-driver",
.ids = (struct rt_ofw_node_id[]) {
{ .compatible = "vendor,ethernet" },
{ /* sentinel */ }
},
.probe = eth_probe,
};
static rt_err_t board_read_info(struct rt_device *dev)
{
struct rt_nvmem_cell *board_id_cell, *serial_cell;
rt_uint16_t board_id;
char serial[17] = {0}; /* 16 字节 + null */
rt_err_t ret;
/* 读取板卡 ID */
board_id_cell = rt_nvmem_get_cell_by_name(dev, "board-id");
if (board_id_cell) {
ret = rt_nvmem_cell_read_u16(board_id_cell, RT_NULL, &board_id);
if (ret == RT_EOK) {
rt_kprintf("Board ID: %u\n", board_id);
}
rt_nvmem_put_cell(board_id_cell);
}
/* 读取序列号 */
serial_cell = rt_nvmem_get_cell_by_name(dev, "serial-number");
if (serial_cell) {
ret = rt_nvmem_cell_read(serial_cell, RT_NULL, serial, 16);
if (ret > 0) {
rt_kprintf("Serial Number: %s\n", serial);
}
rt_nvmem_put_cell(serial_cell);
}
return RT_EOK;
}
注册 NVMEM 设备。
rt_err_t rt_nvmem_device_register(struct rt_nvmem_device *nvmem);
参数:
nvmem:NVMEM 设备结构体返回值:
RT_EOK:成功注销 NVMEM 设备。
rt_err_t rt_nvmem_device_unregister(struct rt_nvmem_device *nvmem);
向 NVMEM 设备添加 cell。
rt_err_t rt_nvmem_device_append_cell(struct rt_nvmem_device *nvmem,
struct rt_nvmem_cell_info *info);
struct rt_nvmem_device {
struct rt_device parent;
const struct rt_nvmem_ops *ops;
rt_size_t size; /* 总大小 */
rt_size_t word_size; /* 字大小 */
rt_size_t stride; /* 访问步长 */
rt_bool_t read_only; /* 只读标志 */
struct rt_ofw_node *np; /* 设备树节点 */
void *priv; /* 私有数据 */
};
struct rt_nvmem_ops {
rt_ssize_t (*read)(struct rt_nvmem_device *nvmem, rt_off_t offset,
void *buf, rt_size_t size);
rt_ssize_t (*write)(struct rt_nvmem_device *nvmem, rt_off_t offset,
const void *buf, rt_size_t size);
};
#include <rtthread.h>
#include <rtdevice.h>
#include <drivers/ofw.h>
#include <drivers/nvmem.h>
#include <drivers/i2c.h>
struct eeprom_priv {
struct rt_i2c_client *client;
struct rt_nvmem_device nvmem;
struct rt_gpio_pin *wp_gpio; /* 写保护 GPIO */
};
static rt_ssize_t eeprom_read(struct rt_nvmem_device *nvmem,
rt_off_t offset,
void *buf,
rt_size_t size)
{
struct eeprom_priv *priv = nvmem->priv;
struct rt_i2c_msg msgs[2];
rt_uint8_t addr_buf[2];
rt_ssize_t ret;
/* 设置地址(大端序) */
addr_buf[0] = (offset >> 8) & 0xFF;
addr_buf[1] = offset & 0xFF;
/* 写地址 */
msgs[0].addr = priv->client->client_addr;
msgs[0].flags = RT_I2C_WR;
msgs[0].buf = addr_buf;
msgs[0].len = 2;
/* 读数据 */
msgs[1].addr = priv->client->client_addr;
msgs[1].flags = RT_I2C_RD;
msgs[1].buf = buf;
msgs[1].len = size;
ret = rt_i2c_transfer(priv->client->bus, msgs, 2);
if (ret != 2) {
return -RT_ERROR;
}
return size;
}
static rt_ssize_t eeprom_write(struct rt_nvmem_device *nvmem,
rt_off_t offset,
const void *buf,
rt_size_t size)
{
struct eeprom_priv *priv = nvmem->priv;
struct rt_i2c_msg msg;
rt_uint8_t *write_buf;
rt_ssize_t ret;
rt_size_t written = 0;
rt_size_t page_size = 64; /* EEPROM 页大小 */
/* 禁用写保护 */
if (priv->wp_gpio) {
rt_pin_write(priv->wp_gpio->pin, PIN_LOW);
rt_thread_mdelay(1);
}
write_buf = rt_malloc(page_size + 2);
if (!write_buf) {
ret = -RT_ENOMEM;
goto out;
}
/* 逐页写入 */
while (size > 0) {
rt_size_t chunk = RT_MIN(size, page_size);
rt_size_t page_offset = offset % page_size;
/* 如果不是页对齐,调整写入大小 */
if (page_offset + chunk > page_size) {
chunk = page_size - page_offset;
}
/* 准备写缓冲区:地址 + 数据 */
write_buf[0] = (offset >> 8) & 0xFF;
write_buf[1] = offset & 0xFF;
rt_memcpy(&write_buf[2], buf + written, chunk);
/* 写入 */
msg.addr = priv->client->client_addr;
msg.flags = RT_I2C_WR;
msg.buf = write_buf;
msg.len = chunk + 2;
ret = rt_i2c_transfer(priv->client->bus, &msg, 1);
if (ret != 1) {
rt_kprintf("EEPROM write failed at offset %d\n", offset);
ret = -RT_ERROR;
goto cleanup;
}
/* 等待写周期完成(~5ms) */
rt_thread_mdelay(5);
offset += chunk;
written += chunk;
size -= chunk;
}
ret = written;
cleanup:
rt_free(write_buf);
out:
/* 重新启用写保护 */
if (priv->wp_gpio) {
rt_pin_write(priv->wp_gpio->pin, PIN_HIGH);
}
return ret;
}
static const struct rt_nvmem_ops eeprom_ops = {
.read = eeprom_read,
.write = eeprom_write,
};
static rt_err_t eeprom_probe(struct rt_platform_device *pdev)
{
struct eeprom_priv *priv;
struct rt_ofw_node *np = pdev->parent.ofw_node;
rt_uint32_t size;
rt_err_t ret;
priv = rt_calloc(1, sizeof(*priv));
if (!priv) {
return -RT_ENOMEM;
}
/* 获取 I2C 客户端 */
priv->client = (struct rt_i2c_client *)pdev->parent.parent;
/* 获取 EEPROM 大小 */
if (rt_ofw_prop_read_u32(np, "size", &size)) {
size = 32768; /* 默认 32KB */
}
/* 获取写保护 GPIO */
priv->wp_gpio = rt_ofw_get_named_pin(np, "wp-gpios", 0, NULL, NULL);
if (priv->wp_gpio) {
rt_pin_mode(priv->wp_gpio->pin, PIN_MODE_OUTPUT);
rt_pin_write(priv->wp_gpio->pin, PIN_HIGH); /* 初始启用写保护 */
}
/* 初始化 NVMEM 设备 */
priv->nvmem.parent = pdev->parent;
priv->nvmem.ops = &eeprom_ops;
priv->nvmem.size = size;
priv->nvmem.word_size = 1;
priv->nvmem.stride = 1;
priv->nvmem.read_only = RT_FALSE;
priv->nvmem.np = np;
priv->nvmem.priv = priv;
/* 注册 NVMEM 设备 */
ret = rt_nvmem_device_register(&priv->nvmem);
if (ret != RT_EOK) {
rt_kprintf("Failed to register NVMEM device\n");
rt_free(priv);
return ret;
}
rt_platform_device_set_data(pdev, priv);
rt_kprintf("EEPROM registered: %u bytes\n", size);
return RT_EOK;
}
static struct rt_platform_driver eeprom_driver = {
.name = "i2c-eeprom",
.ids = (struct rt_ofw_node_id[]) {
{ .compatible = "atmel,24c256" },
{ .compatible = "microchip,24lc256" },
{ /* sentinel */ }
},
.probe = eeprom_probe,
};
总是检查返回值:
cell = rt_nvmem_get_cell_by_name(dev, "mac-address");
if (!cell) {
/* 处理错误 */
}
及时释放 cells:
rt_nvmem_put_cell(cell);
验证数据有效性:
if (rt_nvmem_cell_read(cell, RT_NULL, data, size) == size) {
/* 检查数据是否有效 */
if (is_data_valid(data)) {
use_data(data);
}
}
提供默认值:
if (rt_nvmem_cell_read_u32(cell, RT_NULL, &value) != RT_EOK) {
value = DEFAULT_VALUE;
}
实现错误处理:
static rt_ssize_t nvmem_read(struct rt_nvmem_device *nvmem,
rt_off_t offset, void *buf, rt_size_t size)
{
/* 检查参数 */
if (offset + size > nvmem->size) {
return -RT_EINVAL;
}
/* 执行读取 */
/* ... */
}
处理写保护:
static rt_ssize_t nvmem_write(struct rt_nvmem_device *nvmem,
rt_off_t offset, const void *buf, rt_size_t size)
{
struct priv *priv = nvmem->priv;
/* 禁用写保护 */
if (priv->wp_gpio) {
rt_pin_write(priv->wp_gpio->pin, PIN_LOW);
}
/* 执行写入 */
/* ... */
/* 重新启用写保护 */
if (priv->wp_gpio) {
rt_pin_write(priv->wp_gpio->pin, PIN_HIGH);
}
return size;
}
实现页写入:
/* 对于 EEPROM,尊重页边界 */
while (size > 0) {
rt_size_t chunk = RT_MIN(size, page_size);
/* 写入块 */
/* 等待写周期 */
rt_thread_mdelay(write_cycle_time);
}
问题:获取 cell 失败
错误:rt_nvmem_get_cell_by_name() 返回 RT_NULL
解决方案:
问题:读取操作返回错误
错误:rt_nvmem_cell_read() 返回负值
解决方案:
问题:写入操作失败
错误:rt_nvmem_cell_write() 失败
解决方案:
启用 NVMEM 调试日志:
#define NVMEM_DEBUG
验证设备树配置:
# 检查 NVMEM 节点
cat /proc/device-tree/eeprom@50/compatible
测试读写操作:
/* 读取测试 */
rt_uint8_t test_data[16];
ret = rt_nvmem_cell_read(cell, RT_NULL, test_data, sizeof(test_data));
rt_kprintf("Read %d bytes\n", ret);
/* 转储数据 */
for (int i = 0; i < ret; i++) {
rt_kprintf("%02x ", test_data[i]);
}
rt_kprintf("\n");
批量读取:
/* 好:一次读取所有数据 */
rt_nvmem_cell_read(cell, RT_NULL, buffer, total_size);
/* 差:多次小读取 */
for (i = 0; i < count; i++) {
rt_nvmem_cell_read(cell, &offset, &buffer[i], 1);
}
缓存数据:
/* 在初始化时读取,然后缓存 */
static rt_uint8_t cached_mac[6];
static rt_bool_t mac_loaded = RT_FALSE;
if (!mac_loaded) {
rt_nvmem_cell_read(cell, RT_NULL, cached_mac, 6);
mac_loaded = RT_TRUE;
}
页对齐写入:
/* 对齐到页边界以获得最佳性能 */
offset = ALIGN(offset, page_size);
最小化写周期:
/* 仅写入更改的数据 */
if (memcmp(old_data, new_data, size) != 0) {
rt_nvmem_cell_write(cell, RT_NULL, new_data, size);
}
components/drivers/include/drivers/nvmem.h:NVMEM API 头文件components/drivers/nvmem/:NVMEM 核心实现