5.26 EEPROM设备
5.26 EEPROM设备
📚 本节导读
学习时长: 约 25 分钟
难度级别: ⭐⭐⭐☆☆
前置知识: C 语言基础、I2C 总线基础、5.1 设备驱动框架、5.17 FAL
本节讲解 OneOS 的 EEPROM 设备。EEPROM 是可通过 I2C 总线访问的按字节擦写的非易失存储,常用于存储掉电不丢失的小量配置数据(如校准参数、设备号)。OneOS 基于 OS_EEPROM_INFO 声明式注册和 FAL 集成,让开发者只需声明芯片信息,框架自动完成 I2C 总线连接、地址计算、FAL 注册。
本节会带你:
- 理解 EEPROM 的声明式注册机制(把设备信息放进
os_eeprom_info链接器段); - 看懂 I2C 从机地址的计算(
EEPROM_ADDR_HEAD+ 地址引脚A2A1A0+ 大容量芯片的扩展地址位); - 逐个拆解
eeprom_probe/eeprom_bus_init/eeprom_calculate_address/eeprom_write_page/eeprom_erase_block的完整源码; - 通过完整示例掌握 EEPROM 的声明、初始化与 FAL 访问。
说明:本节所有源码均取自项目
drivers/eeprom/eeprom.c与drivers/eeprom/eeprom_info.c,与项目源码逐字一致(含原始英文注释)。
🎯 学习目标
- 理解 OneOS EEPROM 设备的声明式注册机制
- 掌握
os_eeprom_info_t结构体的配置 - 理解 I2C 从机地址的计算(
0x50+ 地址引脚 + 扩展位) - 逐个拆解总线初始化、地址计算、读写擦除的完整源码
- 了解 EEPROM 与 FAL 的集成方式
一、概述
1.1 EEPROM 解决什么问题
一句话核心:EEPROM 设备适用于通过 I2C 访问 AT24Cxx 系列 EEPROM 存储掉电不丢失的小量数据的场景。它让你"声明一颗芯片,框架自动连好总线、算好地址、注册进 FAL,应用直接用 FAL 分区读写"。
为什么用 EEPROM 框架:
- 手写 I2C 读写 EEPROM 需要处理地址字节宽度(8/16 位)、页写限制、写周期等待,繁琐易错。框架封装成
os_i2c_client_read/write+ 自动等待写周期。 - 通过
OS_EEPROM_INFO声明式注册,一个宏声明一颗芯片,可挂多颗。 - 自动注册到 FAL,上层用
fal_part_read/write就能访问,无需关心 I2C 细节。
1.2 EEPROM 与 Flash 的区别(为什么选型要区分)
| 特性 | EEPROM | NOR Flash(SFlash) |
|---|---|---|
| 擦写单位 | 按字节擦写 | 按页/块擦写 |
| 寿命 | 通常 100 万次 | 通常 10 万次 |
| 速度 | 较慢 | 较快 |
| 用途 | 小量、频繁改写的配置数据 | 代码、大块数据存储 |
关键理解:EEPROM 适合"频繁改小量数据"(如计数器、校准参数),而 Flash 适合"大块、较少改写"(如固件、日志)。选型看数据量、改写频率和寿命需求。
1.3 EEPROM 设备结构
eeprom.c 定义了一个内部结构 struct eeprom,把 I2C 客户端与 FAL Flash 关联起来:
来源:drivers/eeprom/eeprom.c
struct eeprom
{
struct os_i2c_client client;
fal_flash_t flash;
struct os_eeprom_info *info;
};字段详解(源码无注释,下表补充说明,不属于源码):
| 字段 | 含义 |
|---|---|
client | I2C 客户端(bus + client_addr),eeprom_bus_init 填充 |
flash | FAL Flash 对象(fal_flash_t),eeprom_register_fal 填充并注册 |
info | 指向 os_eeprom_info_t 声明表项(eeprom_probe 关联) |
struct data_addr 表示计算出的 I2C 数据地址:
struct data_addr
{
uint8_t width;
uint16_t addr;
};字段详解(源码无注释,下表补充说明,不属于源码):
| 字段 | 含义 |
|---|---|
width | 片内地址字节宽度:1(容量 ≤ 256B)或 2(容量 > 256B) |
addr | 地址值(width=2 时经 bswap_16 字节交换,I2C 先发高字节) |
关键理解 —— width 的含义:AT24C 系列容量不同,片内地址位数不同。
- 容量 ≤ 256 字节:片内地址是 1 字节(
width = 1)。 - 容量 > 256 字节:片内地址是 2 字节(
width = 2)。
1.4 EEPROM 使用场景
1.4.1 一句话核心
EEPROM 设备适用于通过 I2C 访问 AT24Cxx 系列 EEPROM、存储小量掉电不丢失数据的场景,尤其适合需要频繁改写的配置数据(Flash 寿命不够时)。
1.4.2 典型应用场景
场景一:声明一颗 AT24C256 并注册进 FAL
OS_EEPROM_INFO at24c256_dev = {
.flash_name = "at24c256",
.bus_name = "i2c0",
.capacity = 32768, /* 32KB */
.page_size = 64,
.addr_input = ADDR_INPUT_A2A1A0_000,
.write_cycle = 5,
};为什么用 EEPROM:eeprom_probe 在 OS_INIT_LEVEL_DEVICE 阶段自动遍历 os_eeprom_info 段,把每颗芯片连好总线、注册进 FAL,应用无需手动初始化。
场景二:通过 FAL 分区读写 EEPROM
part = fal_part_find("cfg"); /* 找 cfg 分区 */
fal_part_read(part, 0, buf, sizeof(buf));
fal_part_unlock(part);
fal_part_erase(part, 0, fal_part_block_size(part));
fal_part_write(part, 0, buf, sizeof(buf));
fal_part_lock(part);为什么用 EEPROM:FAL 把 EEPROM 封装成 Flash 设备,应用用统一的 FAL API 访问,跨芯片可移植。
1.4.3 选型边界表(何时不该用它)
| 场景 | 该用什么 | 为什么 |
|---|---|---|
| 大容量、大块数据存储 | NOR Flash(SFlash)/ NAND | EEPROM 容量小(通常 ≤512KB) |
| 需要文件系统 | Block 设备 + 文件系统 | EEPROM 通常作为 FAL 分区用,不直接挂文件系统 |
| 无需频繁改写的代码/固件 | NOR Flash | Flash 写入更快,适合固件 |
二、核心数据结构与配置
2.1 EEPROM 设备信息 os_eeprom_info_t
来源:drivers/eeprom/eeprom.h
struct os_eeprom_info
{
char *flash_name;
char *bus_name;
os_size_t capacity;
os_size_t page_size;
uint8_t addr_input;
os_size_t write_cycle;
};
typedef struct os_eeprom_info os_eeprom_info_t;| 字段 | 说明 |
|---|---|
flash_name | 注册到 FAL 时用的设备名(如 "at24c256") |
bus_name | I2C 总线名(如 "i2c0") |
capacity | 芯片容量(字节) |
page_size | 页大小(字节),决定一次写操作的最大长度 |
addr_input | 硬件地址引脚 A2A1A0 的电平组合 |
write_cycle | 写周期(毫秒),写后需等待的时间 tWR |
2.2 地址引脚配置宏
/*address inupt pin lelve:A2A1A0*/
#define ADDR_INPUT_A2A1A0_000 0x00
#define ADDR_INPUT_A2A1A0_001 0x01
#define ADDR_INPUT_A2A1A0_010 0x02
#define ADDR_INPUT_A2A1A0_011 0x03
#define ADDR_INPUT_A2A1A0_100 0x04
#define ADDR_INPUT_A2A1A0_101 0x05
#define ADDR_INPUT_A2A1A0_110 0x06
#define ADDR_INPUT_A2A1A0_111 0x07关键理解 —— 为什么有 8 种:AT24C 芯片有 A2 A1 A0 三个硬件地址引脚(板上通过跳线接高/低),组合出 8 种从机地址,所以一条 I2C 总线上最多可挂 8 颗同型号芯片(不同地址)。addr_input 就是这三根引脚的接线电平。
2.3 设备信息声明宏
#define OS_EEPROM_INFO static OS_USED OS_SECTION("os_eeprom_info") const os_eeprom_info_tOS_EEPROM_INFO 把声明放入 os_eeprom_info 链接器段,eeprom_probe 遍历该段逐个注册(见 3.2 节)。
2.4 常见 EEPROM 型号配置参考
| 型号 | 容量 | 页大小 | I2C 地址 |
|---|---|---|---|
| AT24C02 | 256B | 8B | 0x50 + A2A1A0 |
| AT24C08 | 1KB | 16B | 0x50 + A2 |
| AT24C32 | 4KB | 32B | 0x50 + A2A1A0 |
| AT24C64 | 8KB | 32B | 0x50 + A2A1A0 |
| AT24C128 | 16KB | 64B | 0x50 + A2A1A0 |
| AT24C256 | 32KB | 64B | 0x50 + A2A1A0 |
| AT24C512 | 64KB | 128B | 0x50 + A2A1A0 |
注意:容量 ≤ 1KB 的小芯片(如 AT24C08)可能只有部分地址引脚参与寻址,具体看芯片手册。
三、核心源码详解
说明:以下源码均取自
drivers/eeprom/eeprom.c,与项目源码逐字一致(含原始英文注释)。
3.1 eeprom_probe —— 遍历声明表并注册
一句话定位:在 OS_INIT_LEVEL_DEVICE 阶段,遍历 os_eeprom_info 段中所有 EEPROM 声明,逐颗完成总线连接与 FAL 注册。
完整源码(drivers/eeprom/eeprom.c,与项目一致):
static int eeprom_probe(void)
{
uint8_t index;
struct eeprom *epm;
os_err_t ret = OS_SUCCESS;
#if defined(__CC_ARM) || defined(__CLANG_ARM) /* ARM MDK Compiler */
extern const int os_eeprom_info$$Base;
extern const int os_eeprom_info$$Limit;
const struct os_eeprom_info *table = (struct os_eeprom_info *)&os_eeprom_info$$Base;
int num = (struct os_eeprom_info *)&os_eeprom_info$$Limit - table;
#elif defined(__ICCARM__) || defined(__ICCRX__) /* for IAR Compiler */
const struct os_eeprom_info *table = (struct os_eeprom_info *)__section_begin("os_eeprom_info");
int num = (struct os_eeprom_info *)__section_end("os_eeprom_info") - table;
#elif defined(__GNUC__) /* for GCC Compiler */
extern const int __os_eeprom_info_start;
extern const int __os_eeprom_info_end;
const struct os_eeprom_info *table = (struct os_eeprom_info *)&__os_eeprom_info_start;
int num = (struct os_eeprom_info *)&__os_eeprom_info_end - table;
#endif
for (index = 0; index < num; index++)
{
epm = os_calloc(1, sizeof(struct eeprom));
OS_ASSERT(epm);
epm->info = (struct os_eeprom_info *)&table[index];
ret = eeprom_bus_init(epm);
OS_ASSERT(ret == OS_SUCCESS);
ret = eeprom_register_fal(&(epm->flash));
OS_ASSERT(ret == OS_SUCCESS);
}
return ret;
}
OS_INIT_CALL(eeprom_probe, OS_INIT_LEVEL_DEVICE, OS_INIT_SUBLEVEL_MIDDLE);分步详解:
第 1 步:按编译器获取声明表(条件 编译器类型)
#if defined(__CC_ARM) || defined(__CLANG_ARM) /* ARM MDK Compiler */
extern const int os_eeprom_info$$Base;
extern const int os_eeprom_info$$Limit;
const struct os_eeprom_info *table = (struct os_eeprom_info *)&os_eeprom_info$$Base;
int num = (struct os_eeprom_info *)&os_eeprom_info$$Limit - table;
#elif defined(__ICCARM__) || defined(__ICCRX__) /* for IAR Compiler */
const struct os_eeprom_info *table = (struct os_eeprom_info *)__section_begin("os_eeprom_info");
int num = (struct os_eeprom_info *)__section_end("os_eeprom_info") - table;
#elif defined(__GNUC__) /* for GCC Compiler */
extern const int __os_eeprom_info_start;
extern const int __os_eeprom_info_end;
const struct os_eeprom_info *table = (struct os_eeprom_info *)&__os_eeprom_info_start;
int num = (struct os_eeprom_info *)&__os_eeprom_info_end - table;
#endif详细解释:不同工具链用不同方式获取 os_eeprom_info 链接器段的起止地址,计算 table(段起始)和 num(段内 EEPROM 声明数量)。
第 2 步:逐个遍历并注册(无条件执行)
for (index = 0; index < num; index++)
{
epm = os_calloc(1, sizeof(struct eeprom));
OS_ASSERT(epm);
epm->info = (struct os_eeprom_info *)&table[index];
ret = eeprom_bus_init(epm);
OS_ASSERT(ret == OS_SUCCESS);
ret = eeprom_register_fal(&(epm->flash));
OS_ASSERT(ret == OS_SUCCESS);
}
return ret;
}
OS_INIT_CALL(eeprom_probe, OS_INIT_LEVEL_DEVICE, OS_INIT_SUBLEVEL_MIDDLE);详细解释:为每颗 EEPROM 分配 struct eeprom,关联其 info,调用 eeprom_bus_init 连接 I2C 总线,再调用 eeprom_register_fal 注册进 FAL。OS_INIT_CALL(eeprom_probe, OS_INIT_LEVEL_DEVICE, OS_INIT_SUBLEVEL_MIDDLE) 让它在设备初始化阶段自动执行。
eeprom_probe() 条件走向总结:
eeprom_probe() 条件走向总结:
├── 按编译器(__CC_ARM / __ICCARM__ / __GNUC__)获取 os_eeprom_info 段起始与数量 num
├── for index in [0, num):
│ ├── os_calloc 分配 struct eeprom
│ ├── epm->info = &table[index]
│ ├── eeprom_bus_init(epm) → 连 I2C 总线
│ └── eeprom_register_fal(&epm->flash) → 注册进 FAL
└── 返回 ret(OS_INIT_CALL 在 DEVICE 阶段执行)3.2 eeprom_bus_init —— 连接 I2C 总线并计算从机地址
完整源码(drivers/eeprom/eeprom.c,与项目一致):
static int eeprom_bus_init(struct eeprom *epm)
{
epm->client.bus = (struct os_i2c_bus_device *)os_device_open_s(epm->info->bus_name);
if (epm->client.bus == OS_NULL)
{
LOG_E(DBG_TAG, "invalid i2c bus.");
return OS_INVAL;
}
epm->client.client_addr = EEPROM_ADDR_HEAD | epm->info->addr_input;
return OS_SUCCESS;
}分步详解:
第 1 步:打开 I2C 总线(条件 bus == OS_NULL)
epm->client.bus = (struct os_i2c_bus_device *)os_device_open_s(epm->info->bus_name);
if (epm->client.bus == OS_NULL)
{
LOG_E(DBG_TAG, "invalid i2c bus.");
return OS_INVAL;
}详细解释:os_device_open_s(bus_name) 打开名为 bus_name 的 I2C 总线设备。打开失败返回 OS_INVAL。
第 2 步:计算从机地址(无条件执行)
epm->client.client_addr = EEPROM_ADDR_HEAD | epm->info->addr_input;详细解释:关键计算。EEPROM 的 I2C 从机地址 = 固定头 EEPROM_ADDR_HEAD(0x50,即 1010xxx 高 7 位中的 1010000)按位或上地址引脚 addr_input。
数值演算(假设 addr_input = ADDR_INPUT_A2A1A0_000 = 0x00):
| 计算项 | 值 | 二进制 |
|---|---|---|
EEPROM_ADDR_HEAD | 0x50 | 0101 0000 |
addr_input | 0x00 | 0000 0000 |
| 位或结果 | 0x50 | 0101 0000 |
数值演算(假设 addr_input = ADDR_INPUT_A2A1A0_111 = 0x07):
| 计算项 | 值 | 二进制 |
|---|---|---|
EEPROM_ADDR_HEAD | 0x50 | 0101 0000 |
addr_input | 0x07 | 0000 0111 |
| 位或结果 | 0x57 | 0101 0111 |
所以
A2A1A0 = 000时地址是0x50,A2A1A0 = 111时地址是0x57,共 8 种(0x50 ~ 0x57)。
eeprom_bus_init() 条件走向总结:
eeprom_bus_init() 条件走向总结:
├── bus = os_device_open_s(bus_name)
├── bus == OS_NULL(I2C 总线不存在)→ LOG_E + 返回 OS_INVAL
└── client_addr = EEPROM_ADDR_HEAD(0x50) | addr_input → 返回 OS_SUCCESS3.3 eeprom_calculate_address —— 计算片内数据地址
一句话定位:根据容量计算片内地址的字节宽度(1/2 字节),并把扩展地址位写入 I2C 从机地址的高位。
完整源码(drivers/eeprom/eeprom.c,与项目一致):
static void eeprom_calculate_address(struct eeprom *epm, uint32_t addr, struct data_addr *dat_addr)
{
uint8_t i;
int32_t cap_shift;
uint8_t quot;
uint8_t remain;
uint32_t ext_addr;
cap_shift = os_ffs(epm->info->capacity) - 1;
quot = cap_shift >> 3;
remain = cap_shift & 0x07;
if (quot <= 1)
{
dat_addr->width = 1;
dat_addr->addr = (uint16_t)(addr & 0xff);
}
else if (quot == 2)
{
dat_addr->width = 2;
dat_addr->addr = (uint16_t)bswap_16(addr & 0xffff);
}
else
{
os_kprintf("unsupported capacity.\r\n");
}
if (remain > 0 && remain < 4)
{
ext_addr = addr >> (8 * quot);
for (i = 0; i < remain; i++)
{
eeprom_set_extend_addr_bit(epm, ext_addr, i);
}
}
}分步详解:
第 1 步:计算容量对数(无条件执行)
cap_shift = os_ffs(epm->info->capacity) - 1;
quot = cap_shift >> 3;
remain = cap_shift & 0x07;详细解释:os_ffs(capacity) - 1 得到容量对应的地址位数(log2(capacity))。quot = cap_shift >> 3 是"地址位数 / 8",得到完整地址字节数;remain = cap_shift & 0x07 是除 8 的余数(不足一字节的额外位数)。
数值演算(容量 32768 = 32KB,如 AT24C256):
| 计算项 | 代入计算 | 结果 |
|---|---|---|
cap_shift | ffs(32768) - 1 = 16 - 1 | 15 |
quot | 15 >> 3 | 1(1 字节) |
remain | 15 & 0x07 | 7 |
第 2 步:按容量选择地址宽度(条件 quot <= 1 / quot == 2)
if (quot <= 1)
{
dat_addr->width = 1;
dat_addr->addr = (uint16_t)(addr & 0xff);
}
else if (quot == 2)
{
dat_addr->width = 2;
dat_addr->addr = (uint16_t)bswap_16(addr & 0xffff);
}
else
{
os_kprintf("unsupported capacity.\r\n");
}详细解释:
quot <= 1:容量 ≤ 256B(cap_shift ≤ 8),片内地址 1 字节,width = 1,地址取低 8 位。quot == 2:容量 ≤ 64KB(cap_shift ≤ 16),片内地址 2 字节,width = 2,地址取低 16 位并字节交换(bswap_16,I2C 需先发高字节)。
数值演算(AT24C08,容量 1KB,quot = 1,读地址 0x1234):quot = 1 走 quot <= 1 分支,width = 1,addr = 0x1234 & 0xff = 0x34;remain = cap_shift & 7 = 2(1KB → cap_shift=10 → remain=2),满足 remain < 4,会设 2 个扩展地址位(见第 3 步)。
注意(重要):
remain必须 < 4 才会设扩展位(源码if (remain > 0 && remain < 4))。对于大容量芯片(如 AT24C256 的 32KB,cap_shift=15、remain=7),remain=7不满足remain < 4,不会设扩展位,此时width=1(只发 1 个基础地址字节)。这是该驱动的实际行为——remain为 1~3 的小容量/中容量芯片(如 1KB 的 AT24C08,remain=2)才用扩展地址位。
关键理解:
quot决定"基础地址字节数"(width),remain决定"还有几个高地址位需要放进 I2C 从机地址的高位"(仅当1 ≤ remain ≤ 3时,由eeprom_set_extend_addr_bit写入 A2/A1/A0)。
第 3 步:处理扩展地址位(条件 remain > 0 且 remain < 4)
if (remain > 0 && remain < 4)
{
ext_addr = addr >> (8 * quot);
for (i = 0; i < remain; i++)
{
eeprom_set_extend_addr_bit(epm, ext_addr, i);
}
}详细解释:若 remain 在 1~3 之间,把 addr 右移 8*quot 位得到高地址 ext_addr,然后逐位调用 eeprom_set_extend_addr_bit 把 ext_addr 的对应位写入 I2C 从机地址的高位(A2/A1/A0)。
关键理解 —— 为什么部分芯片要把高位放从机地址:对 remain 为 1~3 的芯片(如 AT24C08 等小容量,片内地址略超 8 位但不多),除发 1 个基础地址字节外,多余的高位地址会通过 I2C 从机地址的 A2/A1/A0 位扩展(eeprom_set_extend_addr_bit 见 3.4 节)——本质是"用从机地址位扩展片内地址空间"。注意:此机制只对 1 ≤ remain ≤ 3 生效;remain ≥ 4 的大容量芯片(如 32KB)本驱动不设扩展位(见第 2 步的注意)。
3.4 eeprom_set_extend_addr_bit 与位操作宏
完整源码(drivers/eeprom/eeprom.c,与项目一致):
#define bswap_16(a) ((((uint16_t)(a) << 8) & 0xff00) | (((uint16_t)(a) >> 8) & 0xff))
#define set_bit(x, n) (x | (1 << n))
#define clear_bit(x, n) (x & (~(1 << n)))
#define get_bit(x, n) ((x >> n) & 0x01)
#define EEPROM_ADDR_HEAD (0x50)
static void eeprom_set_extend_addr_bit(struct eeprom *epm, uint32_t addr, uint8_t bit)
{
uint16_t client_addr = epm->client.client_addr;
if (get_bit(addr, bit) == 0x01)
{
epm->client.client_addr = set_bit(client_addr, bit);
}
else
{
epm->client.client_addr = clear_bit(client_addr, bit);
}
}分步详解:
第 1 步:读取地址的某一位并写入从机地址(条件 get_bit(addr, bit))
uint16_t client_addr = epm->client.client_addr;
if (get_bit(addr, bit) == 0x01)
{
epm->client.client_addr = set_bit(client_addr, bit);
}
else
{
epm->client.client_addr = clear_bit(client_addr, bit);
}详细解释:get_bit(addr, bit) 取 addr 的第 bit 位。为 1 则 set_bit(client_addr, bit) 把从机地址第 bit 位置 1;为 0 则 clear_bit 清零。这样就把高地址的 bit 映射到了从机地址的 A2/A1/A0 位上。
eeprom_set_extend_addr_bit() 条件走向总结:
eeprom_set_extend_addr_bit() 条件走向总结:
├── get_bit(addr, bit) == 1 → client_addr 的第 bit 位置 1
└── get_bit(addr, bit) == 0 → client_addr 的第 bit 位清零3.5 eeprom_write_page —— 写一页数据
完整源码(drivers/eeprom/eeprom.c,与项目一致):
static int eeprom_write_page(struct eeprom *epm, uint8_t *buff, uint32_t addr, uint32_t size)
{
int ret;
struct data_addr dat_addr;
if (size > epm->info->page_size)
{
return OS_FAILURE;
}
eeprom_calculate_address(epm, addr, &dat_addr);
ret = os_i2c_client_write((struct os_i2c_client *)&(epm->client),
(uint32_t)(dat_addr.addr),
dat_addr.width,
(uint8_t *)buff,
size);
if (ret != OS_SUCCESS)
{
return OS_EIO;
}
/*it takes tWR between STOP and START*/
os_task_msleep(epm->info->write_cycle);
return OS_SUCCESS;
}分步详解:
第 1 步:校验页大小(条件 size > page_size)
if (size > epm->info->page_size)
{
return OS_FAILURE;
}详细解释:一次写入不能超过页大小(如 AT24C256 的 64B),否则跨页写会出错,返回 OS_FAILURE。
第 2 步:计算地址并写数据(条件 ret != OS_SUCCESS)
eeprom_calculate_address(epm, addr, &dat_addr);
ret = os_i2c_client_write((struct os_i2c_client *)&(epm->client),
(uint32_t)(dat_addr.addr),
dat_addr.width,
(uint8_t *)buff,
size);
if (ret != OS_SUCCESS)
{
return OS_EIO;
}详细解释:先算片内地址(宽度和地址值),再调 os_i2c_client_write 发送。写失败返回 OS_EIO。
第 3 步:等待写周期(无条件执行)
/*it takes tWR between STOP and START*/
os_task_msleep(epm->info->write_cycle);
return OS_SUCCESS;
}详细解释:关键。EEPROM 写入需要内部编程时间(tWR),写完后必须等待 write_cycle 毫秒(睡眠),否则下一次操作还没等芯片写完就发指令会失败。返回 OS_SUCCESS。
3.6 eeprom_erase_block —— 擦除一个"块"(实际是写满 0xFF)
完整源码(drivers/eeprom/eeprom.c,与项目一致):
static int eeprom_erase_block(struct eeprom *epm, uint32_t addr)
{
int ret = OS_SUCCESS;
uint8_t *buff;
if ((addr & (epm->info->page_size - 1)) != 0)
{
return OS_FAILURE;
}
buff = os_calloc(1, epm->info->page_size);
if (buff == OS_NULL)
{
LOG_E(DBG_TAG, "no memery");
return OS_NOMEM;
}
memset(buff, 0xff, epm->info->page_size);
ret = eeprom_write_page(epm, buff, addr, epm->info->page_size);
os_free(buff);
if (ret != OS_SUCCESS)
{
return OS_EIO;
}
return OS_SUCCESS;
}分步详解:
第 1 步:校验地址对齐(条件 地址未按页对齐)
if ((addr & (epm->info->page_size - 1)) != 0)
{
return OS_FAILURE;
}详细解释:addr & (page_size - 1) 检查地址是否是页大小的整数倍(对齐)。未对齐返回 OS_FAILURE。
第 2 步:申请并填满 0xFF(条件 buff == OS_NULL)
buff = os_calloc(1, epm->info->page_size);
if (buff == OS_NULL)
{
LOG_E(DBG_TAG, "no memery");
return OS_NOMEM;
}
memset(buff, 0xff, epm->info->page_size);详细解释:申请一页缓冲区,填成 0xFF。EEPROM 没有独立的擦除命令,它的"擦除"就是"把整页写入 0xFF"(擦除语义 = 恢复出厂 0xFF 状态)。
第 3 步:整页写入(条件 ret != OS_SUCCESS)
ret = eeprom_write_page(epm, buff, addr, epm->info->page_size);
os_free(buff);
if (ret != OS_SUCCESS)
{
return OS_EIO;
}
return OS_SUCCESS;
}详细解释:把整页 0xFF 写入,释放缓冲。写入失败返回 OS_EIO,成功返回 OS_SUCCESS。
eeprom_erase_block() 条件走向总结:
eeprom_erase_block() 条件走向总结:
├── addr 未按页对齐 → 返回 OS_FAILURE
├── buff 分配失败 → LOG_E + 返回 OS_NOMEM
├── 把整页填 0xFF
├── eeprom_write_page(整页 0xFF)
├── 写失败 → 返回 OS_EIO
└── 成功 → 返回 OS_SUCCESS3.7 eeprom_register_fal —— 把 EEPROM 注册成 FAL Flash(桥接机制)
一句话定位:把 EEPROM 封装成 fal_flash_t,填充几何参数和三个读写擦回调,再 fal_flash_register 注册进 FAL——这样应用就能用 fal_part_find/fal_part_read 访问 EEPROM。
完整源码(drivers/eeprom/eeprom.c,与项目逐字一致):
static int eeprom_register_fal(fal_flash_t *flash)
{
fal_flash_t *fal_flash = flash;
struct eeprom *epm = os_container_of(flash, struct eeprom, flash);
strncpy(fal_flash->name, epm->info->flash_name, sizeof(fal_flash->name));
fal_flash->capacity = epm->info->capacity;
fal_flash->block_size = epm->info->page_size;
fal_flash->page_size = epm->info->page_size;
fal_flash->ops.read_page = fal_eeprom_read_page;
fal_flash->ops.write_page = fal_eeprom_write_page;
fal_flash->ops.erase_block = fal_eeprom_erase_block;
return fal_flash_register(fal_flash);
}分步详解:
第 1 步:拷贝名称与几何参数(无条件执行)
strncpy(fal_flash->name, epm->info->flash_name, sizeof(fal_flash->name));
fal_flash->capacity = epm->info->capacity;
fal_flash->block_size = epm->info->page_size;
fal_flash->page_size = epm->info->page_size;详细解释:关键 —— 从 struct eeprom 的 flash 成员取 fal_flash_t(os_container_of(flash, struct eeprom, flash) 反推父结构)。把 EEPROM 的 flash_name 拷给 FAL Flash 名,容量/块大小/页大小都取自 info(注意:EEPROM 的 block_size = page_size,因为 EEPROM 按页擦,页即块)。
第 2 步:挂读写擦回调并注册(无条件执行)
fal_flash->ops.read_page = fal_eeprom_read_page;
fal_flash->ops.write_page = fal_eeprom_write_page;
fal_flash->ops.erase_block = fal_eeprom_erase_block;
return fal_flash_register(fal_flash);
}详细解释:把 FAL 的三个回调指向 fal_eeprom_*(它们内部转调 eeprom_read_bytes/eeprom_write_page/eeprom_erase_block),最后 fal_flash_register 注册。桥接关系:FAL 读写擦 → fal_eeprom_* → eeprom_* → os_i2c_client_* → I2C 总线。
fal_eeprom_write_page(FAL 写回调,跨页循环调用 eeprom_write_page,逐字一致):
static int fal_eeprom_write_page(fal_flash_t *flash, uint32_t page_addr, const uint8_t *buff, uint32_t page_nr)
{
uint32_t i, count = 0;
uint32_t offset = page_addr * flash->page_size;
uint32_t size = flash->page_size;
struct eeprom *epm = os_container_of(flash, struct eeprom, flash);
for (i = 0; i < page_nr; i++)
{
count += eeprom_write_page(epm, (uint8_t *)(buff + i * size), offset + i * size, flash->page_size);
}
return count;
}详细解释:FAL 可能一次要写多页(page_nr),但 EEPROM 单次写不能超页大小——所以循环逐页调用 eeprom_write_page,累计 count 返回。
关键理解 —— EEPROM 通过 FAL 提供的"块"语义:
fal_eeprom_erase_block也是循环逐页调用eeprom_erase_block(每页填0xFF)。所以对 EEPROM 而言,FAL 的一个"块"就是"页"(block_size = page_size),擦除"块"= 擦"页"= 写整页0xFF。FAL 分区读写最终都落到 I2C 字节级访问。
四、完整使用示例
示例:声明 EEPROM 并通过 FAL 访问
第 1 部分:声明 EEPROM 设备(在板级文件)
#include <eeprom.h>
/* 声明一颗 AT24C256:挂 i2c0、32KB、页 64B、A2A1A0=000、写周期 5ms */
OS_EEPROM_INFO at24c256_dev = {
.flash_name = "at24c256",
.bus_name = "i2c0",
.capacity = 32768, /* 32KB */
.page_size = 64,
.addr_input = ADDR_INPUT_A2A1A0_000,
.write_cycle = 5,
};第 2 部分:通过 FAL 读写 EEPROM
#include <fal/fal.h>
#include <string.h>
void eeprom_fal_demo(void)
{
fal_part_t *part;
uint8_t buf[64];
os_err_t ret;
/* 1. 找 cfg 分区(EEPROM 已由框架注册为 FAL Flash,分区表里配置) */
part = fal_part_find("cfg");
if (part == OS_NULL)
{
os_kprintf("part not found\r\n");
return;
}
/* 2. 写入数据(先解锁→擦除→写入→加锁) */
fal_part_unlock(part);
ret = fal_part_erase(part, 0, fal_part_block_size(part));
if (ret != 0)
{
os_kprintf("erase failed\r\n");
fal_part_lock(part);
return;
}
memset(buf, 0x00, sizeof(buf));
buf[0] = 0x01;
buf[1] = 0x02;
ret = fal_part_write(part, 0, buf, sizeof(buf));
if (ret != sizeof(buf))
{
os_kprintf("write failed\r\n");
fal_part_lock(part);
return;
}
fal_part_lock(part);
/* 3. 读回验证 */
memset(buf, 0x00, sizeof(buf));
ret = fal_part_read(part, 0, buf, sizeof(buf));
if (ret == sizeof(buf))
{
os_kprintf("read back: 0x%02x 0x%02x\r\n", buf[0], buf[1]);
}
}说明:示例展示 EEPROM 声明式注册 + FAL 访问的标准流程。实际效果:
eeprom_probe自动把at24c256连到i2c0并注册进 FAL;应用通过fal_part_*读写cfg分区。注意 EEPROM 写入需按页写(≤page_size),写周期由框架自动等待。
注意:
fal_part_erase一次擦一个块(即一页),fal_part_write一次写的数据不能超过页大小(AT24C256 为 64B)。FAL 分区表需把cfg分区配置在 EEPROM 上。
五、EEPROM 的适配(接入新 EEPROM 芯片 / 新 I2C 总线)
5.1 先认识适配层:EEPROM 框架怎么"长"到芯片上
drivers/eeprom/eeprom.c 是跨平台统一框架(eeprom_probe 遍历芯片表、eeprom_bus_init 连 I2C、eeprom_register_fal 进 FAL、读写擦),不碰具体 I2C 寄存器。真正操作 I2C 总线的是底层 I2C 驱动。EEPROM 驱动是声明式的——接入新 EEPROM 芯片,只需加一条 OS_EEPROM_INFO 声明,不需要改框架代码。
适配全景图:
┌──────────────────────────────────────────────────────────┐
│ 应用层(fal_part_* / 直接 eeprom_*) │
└──────────────────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ EEPROM 框架层(drivers/eeprom/eeprom.c,通用不改) │
│ eeprom_probe(遍历 os_eeprom_info 段)→ 连 I2C → 进 FAL │
└───────────────┬──────────────────────────┬────────────────┘
│ ①声明 OS_EEPROM_INFO │ ②I2C 客户端
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ 芯片表(sflash_info / eeprom_info 文件)★需声明★ │
│ OS_EEPROM_INFO 声明(flash_name/bus_name/容量/页/引脚) │
└───────────────┬──────────────────────────┬────────────────┘
│ os_i2c_client_* │ I2C 驱动
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ 硬件(AT24Cxx EEPROM 芯片 + I2C 总线) │
└──────────────────────────────────────────────────────────┘适配的关键洞察:接入新 EEPROM 芯片只需做两件事:① 用 OS_EEPROM_INFO 声明芯片信息(总线名、容量、页大小、地址引脚、写周期)放入 os_eeprom_info 段;② 确保对应 I2C 总线驱动已注册。eeprom_probe 启动时自动遍历声明表,逐颗连接并注册进 FAL。框架代码一行都不用改。
5.2 接口契约:接入 EEPROM 需要提供什么
| 层 | 内容 | 职责 | 说明 |
|---|---|---|---|
| 声明 | OS_EEPROM_INFO | 声明芯片(总线/容量/页/引脚/写周期) | 放入 os_eeprom_info 链接器段 |
| 总线 | I2C 设备驱动 | 提供 I2C 读写 | eeprom_bus_init 用 os_i2c_client_init 连接 |
| 注册 | eeprom_register_fal | 封装成 FAL Flash | 自动调用,无需手动 |
| 链接 | os_eeprom_info 段 | 收集芯片声明 | 链接脚本须含该段 |
5.3 接入新 EEPROM 芯片的标准流程
第 1 步:查芯片手册——确认容量、页大小(如 AT24C256 的 64B)、I2C 从机地址基础值(EEPROM_ADDR_HEAD=0x50)、A2A1A0 引脚电平、写周期 tWR。
第 2 步:声明 OS_EEPROM_INFO——用 OS_EEPROM_INFO 宏在芯片表文件声明实例,填 flash_name/bus_name/capacity/page_size/addr_input/write_cycle。
/* eeprom_info 声明:AT24C256 */
OS_EEPROM_INFO at24c256_info = {
.flash_name = "at24c256",
.bus_name = "i2c0",
.capacity = 32 * 1024, /* 32KB */
.page_size = 64, /* AT24C256 页 64B */
.addr_input = ADDR_INPUT_A2A1A0_000,
.write_cycle = 5, /* tWR = 5ms */
};第 3 步:确认 I2C 总线已注册——bus_name 须与板级 I2C 设备名一致(如 "i2c0"),eeprom_bus_init 才能 os_i2c_client_init 找到它。
第 4 步:确认链接脚本含 os_eeprom_info 段——OS_EEPROM_INFO 用 OS_SECTION,MDK/IAR/GCC 脚本都要有该段。
第 5 步:测试——启动看 eeprom_probe 日志;用 fal_part_read/write 读写验证,写后等 write_cycle 再读。
5.4 适配检查清单(快速自查)
| 检查项 | 说明 |
|---|---|
bus_name 与板级 I2C 设备名一致? | 不一致则 eeprom_bus_init 找不到 I2C 总线 |
地址引脚 addr_input 与板上 A2A1A0 接线一致? | 不一致则 I2C 找不到设备(从机地址错) |
链接脚本含 os_eeprom_info 段? | 无段则 OS_SECTION 不生效、声明进不了表 |
| 页大小填对? | 填错则跨页写失败或一次写超页 |
| I2C 从机地址基础值对? | 不同 EEPROM 型号基础地址可能不同(如部分用 0xA0),确认 EEPROM_ADDR_HEAD |
📝 本节小结
核心要点回顾
本节围绕 EEPROM 设备展开,核心要点如下:
1. 声明式注册
OS_EEPROM_INFO把设备信息放入os_eeprom_info段。eeprom_probe在OS_INIT_LEVEL_DEVICE阶段遍历注册,自动连总线 + 注册 FAL。
2. 从机地址计算
- 基础地址 =
EEPROM_ADDR_HEAD (0x50) | addr_input (A2A1A0),共 8 种。 - 大容量芯片把高地址位映射到从机地址的 A2/A1/A0(
eeprom_set_extend_addr_bit)。
3. 片内地址宽度
quot ≤ 1:1 字节地址;quot == 2:2 字节地址(字节交换)。remain决定扩展地址位数量。
4. 写与擦除
- 一次写不能超过页大小;写后必须等待
write_cycle(tWR)。 - EEPROM 无独立擦除命令,"擦除" = 整页写入
0xFF。
5. FAL 桥接
eeprom_register_fal把 EEPROM 封装成fal_flash_t,block_size = page_size(EEPROM 的"块"=页)。- FAL 读写擦 →
fal_eeprom_*(循环逐页)→eeprom_*→os_i2c_client_*→ I2C。
6. 硬件适配
- 接入新 EEPROM = 声明
OS_EEPROM_INFO(总线/容量/页/引脚/写周期)+ 确保 I2C 总线已注册,框架代码不用改。 bus_name须与板级 I2C 设备名一致,addr_input须与 A2A1A0 接线一致,链接脚本须含os_eeprom_info段。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
| 写数据超过页大小 | 一次写不能超过 page_size,否则跨页写失败(size > page_size 直接返回失败) |
| 忽略写周期 | 写后必须等待 write_cycle 毫秒,框架内部已 os_task_msleep,应用不要再手动覆盖 |
| 地址引脚接错 | addr_input 必须与板上 A2A1A0 接线一致,否则 I2C 找不到设备 |
| 大容量芯片地址搞错 | 超 256B 芯片的高位地址要放进从机地址 A2/A1/A0,框架自动处理,应用只需给正确片内偏移 |
| 把 EEPROM 当 Flash 直接擦 | EEPROM 没有擦除指令,"擦除"是整页写 0xFF,且按字节可写,无需先擦 |
| 忘记配置 FAL 分区表 | EEPROM 注册成 FAL Flash 后,还需在 FAL 分区表里配置 cfg 分区才能用 fal_part_find |
链接脚本缺 os_eeprom_info 段 | OS_EEPROM_INFO 用 OS_SECTION,无段则声明收不进表、eeprom_probe 遍历不到 |
| 误以为 EEPROM 支持页擦 | EEPROM 的"块"就是页,FAL 的 block_size 被设成 page_size,无独立大块擦除 |
bus_name 与 I2C 设备名不一致 | eeprom_bus_init 用 os_i2c_client_init(bus_name) 找总线,名字不一致则连不上 |
| I2C 从机基础地址配错 | 不同 EEPROM 型号 EEPROM_ADDR_HEAD 可能不同,确认后组合 addr_input 才正确 |
下一步
掌握了 EEPROM 存储设备后,接下来学习 MTD 内存技术设备:
- 5.27 MTD设备:了解 OneOS 的 MTD 双接口(字符/块)设备模型与页级读写擦除。