diff --git a/documentation/6.components/device-driver/device_driver_model/nvmem/README_zh.md b/documentation/6.components/device-driver/device_driver_model/nvmem/README_zh.md new file mode 100644 index 0000000000..ab7985183e --- /dev/null +++ b/documentation/6.components/device-driver/device_driver_model/nvmem/README_zh.md @@ -0,0 +1,883 @@ +# NVMEM 框架 + +## 1. 概述 + +### 1.1 什么是 NVMEM? + +NVMEM(Non-Volatile Memory)是一个用于访问非易失性存储器的框架,提供了统一的接口来读写各种类型的非易失性存储设备,如: +- EEPROM(I2C、SPI) +- OTP(One-Time Programmable)存储器 +- eFuse +- NVRAM +- 电池支持的 SRAM +- Flash 存储器的特定区域 + +### 1.2 RT-Thread 中的实现 + +RT-Thread 的 NVMEM 框架基于 Linux 内核的 NVMEM 子系统设计,提供: +- **基于 Cell 的组织**:将存储器划分为命名的单元(cells) +- **设备树集成**:通过设备树描述存储器布局 +- **消费者/提供者模型**:驱动程序可以是 NVMEM 的消费者或提供者 +- **类型化访问**:支持 u8/u16/u32/u64 的直接读取 +- **位级访问**:支持比特级的精确访问 +- **写保护支持**:GPIO 控制的写保护 + +## 2. Kconfig 配置 + +### 2.1 启用 NVMEM 支持 + +``` +Device Drivers ---> + [*] Using NVMEM (Non-Volatile Memory) device drivers +``` + +**配置选项**: +- `RT_USING_NVMEM`:启用 NVMEM 框架支持 + +### 2.2 在 menuconfig 中的位置 + +``` +RT-Thread Configuration + → Device Drivers + → Using NVMEM (Non-Volatile Memory) device drivers +``` + +## 3. 设备树绑定 + +### 3.1 NVMEM 提供者 + +定义一个 NVMEM 设备(如 EEPROM): + +```dts +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 */ + }; +}; +``` + +### 3.2 NVMEM 消费者 + +引用 NVMEM cells: + +```dts +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"; +}; +``` + +### 3.3 写保护 GPIO + +带写保护引脚的 EEPROM: + +```dts +eeprom@50 { + compatible = "atmel,24c256"; + reg = <0x50>; + + /* 写保护 GPIO */ + wp-gpios = <&gpio1 10 GPIO_ACTIVE_HIGH>; + + mac_address: mac-addr@0 { + reg = <0x0 0x6>; + }; +}; +``` + +## 4. 应用层 API + +### 4.1 Cell 获取操作 + +#### 4.1.1 rt_nvmem_get_cell_by_name + +通过名称获取 NVMEM cell。 + +```c +struct rt_nvmem_cell *rt_nvmem_get_cell_by_name(struct rt_device *dev, + const char *name); +``` + +**参数**: +- `dev`:设备指针 +- `name`:cell 名称(设备树中的 nvmem-cell-names) + +**返回值**: +- 成功:cell 指针 +- 失败:RT_NULL + +**示例**: +```c +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; +} +``` + +#### 4.1.2 rt_nvmem_get_cell_by_index + +通过索引获取 NVMEM cell。 + +```c +struct rt_nvmem_cell *rt_nvmem_get_cell_by_index(struct rt_device *dev, + rt_uint32_t index); +``` + +**参数**: +- `dev`:设备指针 +- `index`:cell 索引(从 0 开始) + +**返回值**: +- 成功:cell 指针 +- 失败:RT_NULL + +#### 4.1.3 rt_nvmem_put_cell + +释放 NVMEM cell 引用。 + +```c +void rt_nvmem_put_cell(struct rt_nvmem_cell *cell); +``` + +**参数**: +- `cell`:要释放的 cell 指针 + +### 4.2 数据访问操作 + +#### 4.2.1 rt_nvmem_cell_read + +从 cell 读取数据。 + +```c +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`:要读取的字节数 + +**返回值**: +- 成功:实际读取的字节数 +- 失败:负错误码 + +**示例**: +```c +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; +} +``` + +#### 4.2.2 rt_nvmem_cell_write + +向 cell 写入数据。 + +```c +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`:要写入的字节数 + +**返回值**: +- 成功:实际写入的字节数 +- 失败:负错误码 + +**注意**: +- 写操作可能受写保护 GPIO 限制 +- 某些 NVMEM 设备可能是只读的 + +### 4.3 类型化读取操作 + +#### 4.3.1 rt_nvmem_cell_read_u8 + +读取 8 位无符号整数。 + +```c +rt_err_t rt_nvmem_cell_read_u8(struct rt_nvmem_cell *cell, + rt_off_t *offset, + rt_uint8_t *val); +``` + +#### 4.3.2 rt_nvmem_cell_read_u16 + +读取 16 位无符号整数。 + +```c +rt_err_t rt_nvmem_cell_read_u16(struct rt_nvmem_cell *cell, + rt_off_t *offset, + rt_uint16_t *val); +``` + +#### 4.3.3 rt_nvmem_cell_read_u32 + +读取 32 位无符号整数。 + +```c +rt_err_t rt_nvmem_cell_read_u32(struct rt_nvmem_cell *cell, + rt_off_t *offset, + rt_uint32_t *val); +``` + +#### 4.3.4 rt_nvmem_cell_read_u64 + +读取 64 位无符号整数。 + +```c +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`:成功 +- 负错误码:失败 + +**示例**: +```c +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); +} +``` + +## 5. 应用层使用示例 + +### 5.1 以太网驱动:从 EEPROM 读取 MAC 地址 + +```c +#include +#include +#include +#include + +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, +}; +``` + +### 5.2 读取板卡 ID 和序列号 + +```c +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; +} +``` + +## 6. 驱动实现接口 + +### 6.1 NVMEM 设备注册 + +#### 6.1.1 rt_nvmem_device_register + +注册 NVMEM 设备。 + +```c +rt_err_t rt_nvmem_device_register(struct rt_nvmem_device *nvmem); +``` + +**参数**: +- `nvmem`:NVMEM 设备结构体 + +**返回值**: +- `RT_EOK`:成功 +- 负错误码:失败 + +#### 6.1.2 rt_nvmem_device_unregister + +注销 NVMEM 设备。 + +```c +rt_err_t rt_nvmem_device_unregister(struct rt_nvmem_device *nvmem); +``` + +#### 6.1.3 rt_nvmem_device_append_cell + +向 NVMEM 设备添加 cell。 + +```c +rt_err_t rt_nvmem_device_append_cell(struct rt_nvmem_device *nvmem, + struct rt_nvmem_cell_info *info); +``` + +### 6.2 NVMEM 设备结构 + +```c +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; /* 私有数据 */ +}; +``` + +### 6.3 NVMEM 操作接口 + +```c +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); +}; +``` + +### 6.4 I2C EEPROM 驱动示例 + +```c +#include +#include +#include +#include +#include + +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, +}; +``` + +## 7. 最佳实践 + +### 7.1 消费者最佳实践 + +1. **总是检查返回值**: +```c +cell = rt_nvmem_get_cell_by_name(dev, "mac-address"); +if (!cell) { + /* 处理错误 */ +} +``` + +2. **及时释放 cells**: +```c +rt_nvmem_put_cell(cell); +``` + +3. **验证数据有效性**: +```c +if (rt_nvmem_cell_read(cell, RT_NULL, data, size) == size) { + /* 检查数据是否有效 */ + if (is_data_valid(data)) { + use_data(data); + } +} +``` + +4. **提供默认值**: +```c +if (rt_nvmem_cell_read_u32(cell, RT_NULL, &value) != RT_EOK) { + value = DEFAULT_VALUE; +} +``` + +### 7.2 提供者最佳实践 + +1. **实现错误处理**: +```c +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; + } + + /* 执行读取 */ + /* ... */ +} +``` + +2. **处理写保护**: +```c +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; +} +``` + +3. **实现页写入**: +```c +/* 对于 EEPROM,尊重页边界 */ +while (size > 0) { + rt_size_t chunk = RT_MIN(size, page_size); + /* 写入块 */ + /* 等待写周期 */ + rt_thread_mdelay(write_cycle_time); +} +``` + +## 8. 故障排除 + +### 8.1 常见问题 + +**问题:获取 cell 失败** +``` +错误:rt_nvmem_get_cell_by_name() 返回 RT_NULL +``` +**解决方案**: +- 检查设备树中的 nvmem-cells 和 nvmem-cell-names 属性 +- 验证 NVMEM 提供者已正确注册 +- 确认 cell 名称拼写正确 + +**问题:读取操作返回错误** +``` +错误:rt_nvmem_cell_read() 返回负值 +``` +**解决方案**: +- 检查 NVMEM 设备是否可访问 +- 验证偏移和大小是否在范围内 +- 检查总线通信(I2C、SPI) + +**问题:写入操作失败** +``` +错误:rt_nvmem_cell_write() 失败 +``` +**解决方案**: +- 检查设备是否为只读 +- 验证写保护 GPIO 是否正确配置 +- 确认写入时序(等待写周期) + +### 8.2 调试技巧 + +1. **启用 NVMEM 调试日志**: +```c +#define NVMEM_DEBUG +``` + +2. **验证设备树配置**: +```bash +# 检查 NVMEM 节点 +cat /proc/device-tree/eeprom@50/compatible +``` + +3. **测试读写操作**: +```c +/* 读取测试 */ +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"); +``` + +## 9. 性能考虑 + +### 9.1 读取优化 + +1. **批量读取**: +```c +/* 好:一次读取所有数据 */ +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); +} +``` + +2. **缓存数据**: +```c +/* 在初始化时读取,然后缓存 */ +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; +} +``` + +### 9.2 写入优化 + +1. **页对齐写入**: +```c +/* 对齐到页边界以获得最佳性能 */ +offset = ALIGN(offset, page_size); +``` + +2. **最小化写周期**: +```c +/* 仅写入更改的数据 */ +if (memcmp(old_data, new_data, size) != 0) { + rt_nvmem_cell_write(cell, RT_NULL, new_data, size); +} +``` + +## 10. 相关模块 + +- **OFW**:设备树解析和 cell 查找 +- **I2C/SPI**:EEPROM 设备的总线接口 +- **GPIO**:写保护引脚控制 +- **Platform**:NVMEM 驱动注册 + +## 11. 参考资料 + +- `components/drivers/include/drivers/nvmem.h`:NVMEM API 头文件 +- `components/drivers/nvmem/`:NVMEM 核心实现 +- Linux Kernel NVMEM 子系统文档 +- 设备树规范 - NVMEM 绑定