EEPROM 与参数存储
本文档介绍 AC82N GP-MCU SDK 应用层中基于 IIC 总线的 EEPROM 读写驱动(iic_eeprom.c/h)以及上层参数存储的配置与使用方式,涵盖驱动实现细节、读写时序、重试与 ACK 轮询机制、演示用例与 VM(虚拟内存)参数区配置。
Purpose and Scope
本页覆盖以下内容:
- EEPROM 驱动层:
sdk/apps/common/eeprom/下的iic_eeprom.c与iic_eeprom.h,提供按地址读写 EEPROM 的eeprom_write()/eeprom_read()接口,以及底层 IIC 原语(iic_start、iic_tx_byte、iic_rx_byte等)的调用方式。 - 硬件/软件 IIC 切换:驱动内通过
_IIC_USE_HW宏选择硬件 IIC 或软件模拟 IIC 的编译路径。 - 应用层演示:
sdk/cpu/demo/iic_demo.c中基于i2c_master_*高层 API 的 EEPROM 读写示例。 - 参数存储配置:
sdk/apps/gp_mcu/app_config.c中 VM 参数存储空间(vm_max_page_align_size_config/vm_max_sector_align_size_config)的配置入口。
不覆盖的内容:IIC 控制器本身的寄存器级驱动、SPI Flash 通用存储(属于存储介质层,另有文档)、以及 VM 库内部的磨损均衡算法(该库以二进制形式提供,仓库中无源码)。
Overview
AC82N 作为 GPIO MCU(通用单片机),常需要保存少量掉电不丢失的参数(如音量、配对信息、校准记录、用户设置等)。本 SDK 提供两条参数持久化路径:
- 外挂 EEPROM(IIC 总线):通过
iic_eeprom驱动访问 AT24Cxx 类 EEPROM 芯片。EEPROM 器件地址固定为0xA0(写)/0xA1(读),单次写事务按 8 字节页 进行,写入后通过 ACK 轮询 等待器件内部写周期完成。 - 片上/片外 Flash 的 VM 参数区:SDK 的 VM(Virtual Memory)机制将参数存储映射到 SPI Flash 的一段空间,配置项
TCFG_VM_SIZE决定该空间大小(单位 KB),app_config.c将其换算为 page/sector 对齐的字节数供 VM 库使用。
设计意图:EEPROM 驱动把"字节级 IIC 传输"与"页写、写周期等待、重试"等 EEPROM 特有语义封装成两个简单函数,上层业务(参数读写)无需关心总线时序;VM 机制则面向频繁、小颗粒参数更新的场景,通过 Flash 上多扇区轮转实现磨损均衡与掉电安全。
Architecture
flowchart TD
subgraph sg_App["应用层 (Application)"]
AppLogic["业务逻辑 / 参数读写"]
VMCfg["app_config.c<br/>vm_*_size_config"]
end
subgraph sg_StorageSvc["存储服务层 (sdk/apps/common/eeprom)"]
EepromApi["eeprom_write / eeprom_read<br/>(iic_eeprom.c)"]
end
subgraph sg_IIC["IIC 总线驱动 (sdk/cpu)"]
IicPrim["iic_start / iic_stop<br/>iic_tx_byte / iic_rx_byte"]
IicMasterApi["i2c_master_read/write<br/>_nbytes_from_device_reg"]
end
subgraph sg_HW["硬件"]
EepromChip["外挂 EEPROM<br/>(AT24Cxx, 0xA0/0xA1)"]
Flash["SPI Flash (VM 参数区)"]
end
AppLogic -->|"eeprom_write/read"| EepromApi
AppLogic -->|"编译开关"| IicMasterApi
EepromApi --> IicPrim
IicMasterApi --> EepromChip
IicPrim --> EepromChip
AppLogic -->|"VM 库(二进制)"| Flash
VMCfg -->|"TCFG_VM_SIZE*1024"| Flash
各层职责:
- 应用层:业务代码直接调用
eeprom_write()/eeprom_read()读写参数,或通过 VM 库 API 管理 Flash 参数区;app_config.c负责提供 VM 空间尺寸等平台常量。 - 存储服务层:
iic_eeprom.c是唯一的 EEPROM 封装点,负责页拆分、写周期 ACK 轮询、重试与总线起止时序。 - IIC 总线驱动:提供底层字节传输原语与高层寄存器地址读写 API;EEPROM 驱动默认走硬件 IIC(
_IIC_USE_HW),也可切到软件模拟。 - 硬件层:外挂 EEPROM 芯片与 SPI Flash(VM 参数区落点)。
驱动实现:iic_eeprom
公共接口与编译开关
头文件 iic_eeprom.h 定义了唯一的开关宏与两个导出函数:
#ifndef _IIC_EEPROM_H_
#define _IIC_EEPROM_H_
#define EEPROM_EN 1
#if EEPROM_EN
#include "typedef.h"
void eeprom_write(int iic, u8 *buf, u32 addr, u32 len);
void eeprom_read(int iic, u8 *buf, u32 addr, u32 len);
#endif
#endif
Source: iic_eeprom.h
设计要点:
EEPROM_EN是总开关,置 0 可整体裁剪掉 EEPROM 支持,避免未使用功能占用代码空间——符合 MCU 固件的资源约束习惯。- 函数签名只暴露
iic(IIC 控制器编号)、buf、addr、len四个参数,没有页大小、器件地址等配置项。器件地址(0xA0/0xA1)和页大小(8 字节)被硬编码在实现内部,上层无需关心。
写操作实现:页拆分 + ACK 轮询
void eeprom_write(int iic, u8 *buf, u32 addr, u32 len)
{
int i;
u32 retry;
int ret;
u32 tx_len;
u32 offset;
#if 1
offset = 0;
while (offset < len) {
tx_len = len - offset > 8 ? 8 : len - offset;
retry = 100;
do {
iic_start(iic);
ret = iic_tx_byte(iic, EEPROM_WADDR);
if (!ret) {
if (--retry) {
continue;
} else {
goto __exit;
}
}
udelay(DELAY_CNT);
ret = iic_tx_byte(iic, addr + offset);
...
for (i = 0; i < tx_len - 1; i++) {
ret = iic_tx_byte(iic, buf[offset + i]);
...
}
ret = iic_tx_byte(iic, buf[offset + tx_len - 1]);
...
iic_stop(iic);
udelay(DELAY_CNT);
} while (0);
offset += tx_len;
do {
iic_start(iic);
ret = iic_tx_byte(iic, EEPROM_WADDR);
if (ret) {
break;
}
udelay(DELAY_CNT);
} while (1);
}
__exit:
iic_stop(iic);
#else
... /* 软件 IIC 分支:使用 iic_write_buf 批量发送 */
#endif
}
Source: iic_eeprom.c
写流程分两个阶段,逐页循环直至写完全部数据:
- 页写事务(内层
do { } while (0)):单次最多写 8 字节(tx_len = len - offset > 8 ? 8 : len - offset)。时序为:iic_start→ 发送器件写地址EEPROM_WADDR(0xA0)→ 发送片内地址addr + offset→ 依次发送tx_len个数据字节 →iic_stop。每步iic_tx_byte若返回失败(NACK/总线错误),retry从 100 递减重试;耗尽则goto __exit收尾并停止总线。 - 写周期等待(ACK 轮询):页数据发完后,EEPROM 进入内部编程(写周期),此时器件不响应总线。代码反复执行
iic_start+ 发送0xA0,直到器件返回 ACK(ret为真)——这标志着内部写周期完成,可以继续写下一页。这是 EEPROM 写操作的标准"轮询 ACK"做法,避免依赖固定的延时等待,既快又可靠。
设计意图:8 字节页大小对应常见 AT24C02 等器件的页写上限,避免跨页写导致的数据回绕;ACK 轮询替代 mdelay 固定延时,写周期时间随器件/电压变化时仍能保证正确性且不浪费等待时间。
读操作实现:重复起始 + 末字节 NACK
void eeprom_read(int iic, u8 *buf, u32 addr, u32 len)
{
int i;
u32 retry = 100;
int ret;
#if 1
do {
iic_start(iic);
ret = iic_tx_byte(iic, EEPROM_WADDR);
...
ret = iic_tx_byte(iic, addr);
...
iic_start(iic); /* 重复起始 (repeated START) */
ret = iic_tx_byte(iic, EEPROM_RADDR);
...
for (i = 0; i < len - 1; i++) {
buf[i] = iic_rx_byte(iic, 1); /* 前 len-1 字节回 ACK */
udelay(DELAY_CNT);
}
buf[len - 1] = iic_rx_byte(iic, 0); /* 最后一字节回 NACK */
iic_stop(iic);
udelay(DELAY_CNT);
} while (0);
#else
... /* 软件 IIC 分支:iic_read_buf 批量接收 */
#endif
}
Source: iic_eeprom.c
读时序为标准的 EEPROM 随机读:START → 写地址 0xA0 → 片内地址 addr → 重复起始(不停止总线,直接再次 iic_start)→ 读地址 0xA1 → 连续接收 len 个字节,前 len-1 字节回 ACK(iic_rx_byte(iic, 1)),最后一个字节回 NACK(iic_rx_byte(iic, 0))以通知器件结束本次读 → STOP。
硬件/软件 IIC 切换与测试函数
实现文件顶部通过宏选择总线实现:
#if 1 //0:软件iic, 1:硬件iic
#define _IIC_USE_HW
#endif
#include "iic_api.h"
#define EEPROM_RADDR 0xa1
#define EEPROM_WADDR 0xa0
#define DELAY_CNT 0
Source: iic_eeprom.c
_IIC_USE_HW定义后,读写走iic_start/iic_tx_byte/iic_rx_byte逐字节原语(硬件 IIC 也可用);注释掉则走iic_write_buf/iic_read_buf批量路径(软件模拟 IIC 分支,#else部分)。DELAY_CNT为字节间可选的额外延时计数(当前为 0),用于低速软件模拟或信号完整性调整的预留钩子。
文件末尾还有一个被 #if 0 禁用的 eeprom_test_main() 自测函数,它给出了 IIC 主机初始化配置的参考(struct iic_master_config:role = IIC_MASTER、SCL/SDA IO 选择、PORT_INPUT_PULLUP_10K 上拉、PORT_DRIVE_STRENGT_2p4mA 驱动强度、master_frequency = 100000、io_filter 等),需要联调 EEPROM 时可参考该配置使能并运行。
应用层演示:iic_demo
iic_demo.c 展示了一条更上层的调用路径——直接使用 IIC 主机 API i2c_master_read_nbytes_from_device_reg / i2c_master_write_nbytes_to_device_reg:
u8 eeprom_wbuf[32], eeprom_rbuf[32];
u8 eeprom_dev_addr = 0xa0;
u8 eeprom_retry_cnt = 10;
int eeprom_ret_len = 0;
for (u8 i = 0; i < sizeof(eeprom_rbuf); i++) {
eeprom_wbuf[i] = i % 26 + 'a';
eeprom_rbuf[i] = 0;
}
eeprom_ret_len = i2c_master_read_nbytes_from_device_reg(IIC_MASTER_INDEX, eeprom_dev_addr, &eeprom_reg_addr, 1, eeprom_rbuf, 31);
log_info("%s,len:%d", eeprom_rbuf, eeprom_ret_len);
...
while ((eeprom_ret_len != 8) && (--eeprom_retry_cnt)) {
eeprom_ret_len = i2c_master_write_nbytes_to_device_reg(IIC_MASTER_INDEX, eeprom_dev_addr, &eeprom_reg_addr, 1, eeprom_wbuf, 8);
}
Source: iic_demo.c
该示例揭示的关键约定:
- 器件地址
0xa0与驱动内EEPROM_WADDR一致;i2c_master_*_nbytes_from_device_reg这类 API 已内置"发送片内地址再读写数据"的完整事务,比iic_eeprom.c的裸原语更高一层。 - 写操作以
eeprom_ret_len != 8为成功判据并带 10 次重试,读操作后mdelay(20)——20 ms 覆盖了常见 EEPROM 的写周期时间(与驱动内 ACK 轮询是两种等价的写完成等待策略)。 eeprom_reg_addr在硬件 IIC 与软件 IIC 下取值不同(66 vs 64),演示了不同总线配置下片内起始地址可独立调整。
参数存储:VM(虚拟内存)配置
除外挂 EEPROM 外,AC82N 还通过 Jieli 的 VM(Virtual Memory)机制在 SPI Flash 上管理参数区。应用配置文件 sdk/apps/gp_mcu/app_config.c 提供了空间尺寸配置:
const int vm_max_page_align_size_config = TCFG_VM_SIZE * 1024L; //page对齐vm管理空间最大值配置
const int vm_max_sector_align_size_config = TCFG_VM_SIZE * 1024L; //sector对齐vm管理空间最大值配置
Source: app_config.c
要点:
TCFG_VM_SIZE(单位 KB)是工程级宏,通常在各板级配置头文件中定义;此处乘以1024L换算为字节,同时作为 page 对齐与 sector 对齐两个 VM 空间上限配置。- 两个值相等意味着 VM 管理空间按 Flash 扇区粒度规划,VM 库在其中进行扇区轮转、磨损均衡与掉电恢复(该库以二进制库形式随 SDK 提供,磨损均衡/日志式写入的实现细节不在本仓库源码中,属于平台库范畴)。
- 参数通过 VM 库的读写 API(如
vm_write/vm_read,由预编译库导出)访问,业务层无需直接操作 SPI Flash,也不受 EEPROM 容量限制。
选择路径的工程准则:参数总量小(≤ 数百字节)且更新频率低 → 外挂 EEPROM(本页 iic_eeprom 驱动);参数较多、更新频繁、或需要与固件/校准数据同区管理 → VM 参数区。
Core Flow:EEPROM 写入时序
sequenceDiagram
participant App as 业务层
participant Drv as eeprom_write (iic_eeprom.c)
participant IIC as IIC 总线 (iic_api)
participant Chip as EEPROM 芯片
App->>Drv: eeprom_write(iic, buf, addr, len)
loop 每页 (len 按 8 字节拆分)
Drv->>IIC: iic_start
Drv->>IIC: iic_tx_byte(0xA0) 器件写地址
alt ACK 失败
IIC-->>Drv: NACK (retry--, ≤100 次)
else ACK
Drv->>IIC: iic_tx_byte(addr+offset) 片内地址
Drv->>IIC: iic_tx_byte(buf[...]) 页数据 (≤8B)
Drv->>IIC: iic_stop
Note over Chip: 内部写周期开始
loop ACK 轮询 (写周期完成检测)
Drv->>IIC: iic_start + iic_tx_byte(0xA0)
IIC-->>Drv: ACK? (循环直到成功)
end
end
end
Drv-->>App: 返回 (写完成)
sequenceDiagram
participant App as 业务层
participant Drv as eeprom_read (iic_eeprom.c)
participant IIC as IIC 总线 (iic_api)
participant Chip as EEPROM 芯片
App->>Drv: eeprom_read(iic, buf, addr, len)
Drv->>IIC: iic_start
Drv->>IIC: iic_tx_byte(0xA0) 设置片内地址
Drv->>IIC: iic_tx_byte(addr)
Drv->>IIC: iic_start (重复起始, 不 STOP)
Drv->>IIC: iic_tx_byte(0xA1) 器件读地址
loop 前 len-1 字节
Drv->>IIC: iic_rx_byte(ack=1) 回 ACK
IIC-->>Drv: 数据字节
end
Drv->>IIC: iic_rx_byte(ack=0) 末字节回 NACK
Drv->>IIC: iic_stop
Drv-->>App: 返回 (buf 填充完毕)
写时序的关键在于两阶段设计:先完成总线级页写事务,再用 ACK 轮询吸收器件内部写周期(典型 5 ms 级),两者解耦使总耗时最短且不依赖固定延时。读时序则利用重复起始(repeated START)在一条总线上完成"定位地址 → 切换读模式"两个阶段,末字节 NACK 是结束读事务的标准信号。
配置选项
| 配置项 | 位置 | 类型 | 默认/取值 | 说明 |
|---|---|---|---|---|
EEPROM_EN | iic_eeprom.h | 宏 | 1 | EEPROM 功能总开关,置 0 裁剪驱动 |
_IIC_USE_HW | iic_eeprom.c | 宏 | 定义(#if 1) | 1 硬件 IIC;注释/0 走软件 IIC 批量分支 |
EEPROM_WADDR | iic_eeprom.c | 宏 | 0xa0 | 器件写地址(8 位格式) |
EEPROM_RADDR | iic_eeprom.c | 宏 | 0xa1 | 器件读地址 |
DELAY_CNT | iic_eeprom.c | 宏 | 0 | 字节间额外延时计数,预留信号完整性调优 |
EEPROM 页大小 | iic_eeprom.c | 常量 | 8 字节 | 单次写事务上限,对应 AT24C02 类页写限制 |
| 写重试次数 | iic_eeprom.c | 局部量 | 100 | 单字节发送失败后的重试上限 |
TCFG_VM_SIZE | 板级配置头 | 宏 | 工程定义(KB) | VM 参数区大小,经 app_config.c 换算为字节 |
vm_max_page_align_size_config | app_config.c | const int | TCFG_VM_SIZE * 1024L | VM 库 page 对齐管理空间上限 |
vm_max_sector_align_size_config | app_config.c | const int | TCFG_VM_SIZE * 1024L | VM 库 sector 对齐管理空间上限 |
master_frequency | 测试配置 eeprom_test_main | 字段 | 100000 Hz | IIC 主频参考值(软件 IIC 频率不精确) |
API Reference
void eeprom_write(int iic, u8 *buf, u32 addr, u32 len)
向 EEPROM 指定地址写入 len 字节数据。内部按 8 字节页拆分,逐页执行写事务,每页写完后通过 ACK 轮询等待内部写周期结束;任一字节连续失败达 100 次则中止(goto __exit),停止总线后返回。
参数:
iic(int):IIC 控制器编号(由 IIC 驱动注册表定义,如IIC_MASTER_INDEX)buf(u8 *):待写入数据缓冲addr(u32):EEPROM 片内起始地址len(u32):写入字节数
返回: 无(void)。失败时静默返回,调用方需自行确保地址/长度在器件容量内。
注意: 地址与长度不跨器件边界是调用方责任——addr 为 8 位传递(iic_tx_byte(iic, addr + offset)),器件容量一般 ≤ 256 字节/页地址空间。
void eeprom_read(int iic, u8 *buf, u32 addr, u32 len)
从 EEPROM 指定地址读出 len 字节。时序为:写地址 + 片内地址 → 重复起始 → 读地址 → 接收数据(前 len-1 字节回 ACK,末字节回 NACK)→ 停止。
参数:
iic(int):IIC 控制器编号buf(u8 *):接收缓冲(调用方保证 ≥len字节)addr(u32):片内起始地址len(u32):读取字节数
返回: 无(void)。数据直接写入 buf。
i2c_master_read_nbytes_from_device_reg / i2c_master_write_nbytes_to_device_reg
IIC 驱动层高层 API(demo 中使用):一条调用完成"写片内寄存器地址 + 读/写 N 字节"的完整事务,返回实际传输字节数。写侧通常以返回值是否等于请求长度判定成功(demo 中为 8),并配合 mdelay(20) 等待写周期。
故障模式、边界情况与并发
- 器件不在线/未上电:所有
iic_tx_byte均返回失败,写侧重试 100 次后经__exit退出,读侧retry耗尽后break。API 无返回值,错误对外不可见——上层若需故障感知,应在调用前做器件探测(如先发起一次读)或扩展 API 返回错误码。 - 写周期未完成:写侧用 ACK 轮询循环等待(
while(1)直到 ACK),若器件异常卡死可能造成死等;demo 路径则用固定mdelay(20)+ 重试上限(10 次)规避。 - 跨页/越界写:驱动不检查
addr+len是否超出器件容量;addr以 8 位传输,超过 255 的地址会回绕,需调用方按器件容量(如 256 B)规划参数布局。 - 总线上多设备冲突:
EEPROM_WADDR/RADDR固定,若总线挂载其他从机需注意地址不冲突;器件 A0/A1/A2 引脚若可配置,需同步修改这两个宏。 - 掉电与写中断:EEPROM 页写是原子性的(器件内部保证),但多页写中途掉电会留下部分页已更新、部分未更新的中间状态;对跨页参数建议使用版本号/校验和(CRC)做完整性校验。
- 并发:
eeprom_write/read均为阻塞式、无锁;若被多个任务/中断上下文同时调用,需由上层串行化(如互斥锁或专用存储任务),否则总线时序会被交错破坏。 - VM 掉电安全:VM 库通过扇区轮转保证参数区掉电一致性;但
vm_max_*_size_config若配置过小,参数写满后 VM 库会报空间不足,配置时需按"最大参数集 × 轮转份数"预留。
性能与运维注意
- 8 字节页写 + ACK 轮询使单页写耗时 ≈ 器件写周期(典型 5 ms 级),远小于固定 20 ms 延时策略;批量参数建议按页组织写入,一次
eeprom_write覆盖整页以减少写周期次数。 - IIC 频率由 IIC 驱动配置(demo 参考 100 kHz);器件手册写周期随电压/温度变化,ACK 轮询自动适应,无需按最坏情况留裕量。
DELAY_CNT为 0 时无字节间延时,若硬件走线较长出现偶发通信错误,可增大该值或降低总线频率。- EEPROM 有擦写寿命(典型 100 万次/字节);频繁更新的参数(如运行计数)建议放入 VM 区(Flash 磨损均衡)而非 EEPROM。
扩展点
- 换用大容量器件:页大小从 8 改为器件实际页大小(如 16/32/64 字节)即可提升写吞吐;
EEPROM_WADDR/RADDR按 A0-A2 配置调整。 - 软件 IIC 降级:注释
_IIC_USE_HW即切换到#else批量读写分支,便于在硬件 IIC 引脚被占用时复用任意 GPIO 模拟。 - 错误上报:当前 API 无返回值;可在
__exit/break处增加错误计数或断言钩子,把总线故障暴露给上层监控。 - 上层参数抽象:可在
eeprom_write/read之上封装"参数 ID → 地址映射 + 校验和"的键值存储层,使业务代码与地址布局解耦。
Related Links
- iic_eeprom.c — EEPROM 驱动实现(读写时序、页写、ACK 轮询)
- iic_eeprom.h — 驱动公共接口
- iic_demo.c — IIC/EEPROM 应用演示(
i2c_master_*API 用法) - app_config.c — 应用配置(VM 参数区尺寸、SFC 频率、晶振等待等)
- 相关页面:IIC 总线驱动(控制器层)与 SPI Flash 存储(介质层)请参见各自目录页;VM 库 API 由预编译平台库提供,详见 SDK 库文档。