设备管理框架
AW33N BLE SDK 的设备管理框架(Device Management Framework)是一套基于"设备节点 + 操作函数表"的类 Linux 设备模型,负责把片内 Flash(SFC)、SD 卡、U 盘(UDISK)、外挂 Flash 等存储介质抽象为统一命名的设备,并为上层应用提供统一的打开、读写、控制、关闭接口。
Purpose and Scope
本文档讲解 SDK 中设备管理框架的完整实现,包括:
- 核心数据结构与注册机制(
struct dev_node/struct device_operations/struct device) - 设备管理 API(
dev_open/dev_byte_read/dev_bulk_write/dev_ioctl等) - 应用层设备索引封装(
device_open/device_close/device_status/device_online) - 设备实例注册(SD、U 盘、NOR Flash 等)与平台数据配置
- 设备热插拔检测、状态查询与异常处理
以下内容属于其他页面主题,不在本文展开:USB 协议栈本身(参见 USB 相关页面)、文件系统层(FAT/exFAT 挂载)、OTA 升级流程的细节(仅提及 try_to_upgrade 入口)。
Overview
在嵌入式 SDK 中,上层逻辑(如文件系统、升级模块、录音、蓝牙协议栈)需要访问多种存储介质。不同介质的驱动接口千差万别(SD 卡需要检测在线状态、U 盘依赖 USB 主机栈、NOR Flash 走 SPI),如果上层直接调用各驱动,代码会充满条件编译和平台耦合。
设备管理框架解决这一问题的思路与 Linux 的设备模型一致:
- 统一抽象:每种介质实现一份
struct device_operations函数表(online/init/open/read/write/bulk_read/bulk_write/seek/ioctl/close),对上层暴露完全一致的行为。 - 链接期注册:通过
REGISTER_DEVICE/REGISTER_DEVICES宏把struct dev_node放进专门的.device段,由链接脚本收集成device_node_begin[]~device_node_end[]数组,系统启动时由devices_init()统一遍历初始化——不需要手工维护注册表。 - 按名访问:上层通过设备名(如
"sd0"、"udisk0"、"sfc"、"ext_flsh")调用dev_open()拿到句柄,之后所有读写都走句柄,与具体介质无关。 - 应用层索引:
device_app进一步把设备名映射为整型索引(UDISK_INDEX、SD0_INDEX…),缓存已打开句柄,提供带重试的关闭和在线状态位图,方便业务代码以枚举方式管理设备。
这套框架是 SDK 中文件系统、升级、录音等模块共用的基础设施,理解它有助于排查"设备打不开""读写失败""拔卡后句柄失效"一类问题。
Architecture
flowchart TD
subgraph sg_App["应用层 (apps/app)"]
FS["文件系统 / 升级 / 录音等业务"]
DevApp["device_app<br/>device_open/device_close/device_status/device_online"]
end
subgraph sg_Core["核心框架 (apps/include_lib/dev_mg)"]
DevMg["dev_mg 管理逻辑<br/>dev_open/dev_ioctl/dev_byte_read/dev_bulk_write"]
DevNodeList["设备节点表<br/>device_node_begin[] ~ device_node_end[]<br/>(.device 链接段)"]
end
subgraph sg_Drivers["设备驱动 (bsp/device 等)"]
SD["sd_dev_ops<br/>(sd0 / sd_cdrom / sd_enc)"]
UDisk["mass_storage_ops<br/>(udisk0, USB 主机栈)"]
NOR["norflash_dev_ops / sfc_dev_ops<br/>(ext_flsh / sfc)"]
end
FS -->|"设备索引"| DevApp
DevApp -->|"设备名"| DevMg
DevMg -->|"遍历查找"| DevNodeList
DevNodeList -->|"匹配 ops"| SD
DevNodeList -->|"匹配 ops"| UDisk
DevNodeList -->|"匹配 ops"| NOR
架构分三层。核心框架 dev_mg 只依赖"设备节点表"这一链接期产物,不关心任何具体驱动;驱动层通过注册 struct device_operations 接入;应用层通过 device_app 的索引封装与核心框架交互。
分层职责
| 层 | 关键文件 | 职责 |
|---|---|---|
| 应用封装层 | apps/app/bsp/device/device_app.c | 设备名↔索引映射、句柄缓存、带重试关闭、在线位图 |
| 核心框架 | apps/include_lib/dev_mg/device.h | 数据结构定义、dev_* 系列 API、链接段注册宏 |
| 驱动注册层 | apps/app/bsp/device/device_list.c | 各介质平台数据与 device_operations 实例化 |
核心设计意图:把"设备是什么"与"设备怎么用"彻底解耦。业务代码永远只看到 void *device 句柄和统一的 dev_* 调用,新增一种存储介质只需在 device_list.c 增加节点,无需改动任何上层代码。
核心框架:数据结构与注册机制
设备操作函数表 struct device_operations
device.h 定义了框架的核心抽象——每个设备必须实现的操作函数表:
struct device_operations {
bool (*online)(const struct dev_node *node);
int (*init)(const struct dev_node *node, void *);
int (*open)(const char *name, struct device **device, void *arg);
int (*read)(struct device *device, void *buf, u32 len, u32);
int (*write)(struct device *device, void *buf, u32 len, u32);
int (*bulk_read)(struct device *device, void *buf, u32 len, u32);
int (*bulk_write)(struct device *device, void *buf, u32 len, u32);
int (*seek)(struct device *device, u32 offset, int orig);
int (*ioctl)(struct device *device, u32 cmd, u32 arg);
int (*close)(struct device *device);
};
来源:device.h
设计要点:
online用于热插拔检测:SD 卡/U 盘这类可移除介质必须实现,框架通过它判断设备是否仍在位。read/write是字节流接口,bulk_read/bulk_write是扇区/块接口:文件系统一般走 bulk 接口(按扇区),业务代码按需选择。最后一个u32参数在不同介质上含义不同(偏移或扇区号),由驱动自行解释。- 函数指针允许为 NULL:如只读介质(
sd_cdrom)的write/bulk_write为NULL,框架或调用方据此判断能力边界。
设备节点 struct dev_node 与句柄 struct device
struct dev_node {
const char *name;
const struct device_operations *ops;
void *priv_data;
};
struct device {
atomic_t ref;
void *private_data;
const struct device_operations *ops;
void *platform_data;
void *driver_data;
};
来源:device.h
dev_node是静态注册信息:设备名 + 操作表 + 驱动私有数据,由宏放进.device段。device是运行时句柄:open成功后由框架创建,ref是原子引用计数(atomic_t),platform_data指向编译期的平台配置(如 SPI 引脚、检测方式),driver_data给驱动保存运行状态。
链接段注册宏
#define REGISTER_DEVICE(node) \
const struct dev_node node sec_used(.device)
#define REGISTER_DEVICES(node) \
const struct dev_node node[] sec_used(.device)
来源:device.h
REGISTER_DEVICE 注册单个节点,REGISTER_DEVICES 注册数组。sec_used(.device) 是 GCC 段属性,把所有节点集中到 .device 段;链接脚本中定义 device_node_begin[] / device_node_end[] 边界符号(见 device.h 的 extern 声明),dev_mg 通过 set_device_node() 拿到区间后即可遍历。这套机制让"新增设备 = 声明一个节点",无需修改任何管理代码。
dev_* 核心 API
device.h 声明的对外接口(实现位于 dev_mg 模块):
| API | 说明 |
|---|---|
int devices_init(void) | 初始化所有注册设备,仅由 devices_init_api() 调用,不可单独使用 |
int devices_init_api(void) | 系统级初始化入口 |
bool dev_online(const char *name) | 查询设备是否在线 |
void *dev_open(const char *name, void *arg) | 按名打开设备,返回句柄 |
int dev_ioctl(void *device, int cmd, u32 arg) | 控制命令(扇区数、擦除、格式化等) |
int dev_close(void *device) | 关闭设备,释放引用 |
int dev_byte_read/write(...) | 字节流读写 |
int dev_bulk_read/write(...) | 块(扇区)读写 |
struct dev_node_mg *set_device_node(...) | 设置设备节点区间(链接段边界) |
int device_status_emit(const char *name, u8 status) | 设备状态事件上报(配合消息系统) |
来源:device.h
设备名使用宏统一约定:__SD0_NANE "sd0"、__UDISK "udisk0"、__SFC_NANE "sfc"、__EXT_FLASH_NANE "ext_flsh"、__SD_CDROM "sd_cdrom"、__SD_ENC "sd_enc"、__OTG "otg"(__OTG 为预留)。上层代码建议引用这些宏而不是硬编码字符串,避免改名时遗漏。
应用层:设备索引与状态管理(device_app)
device_app 是业务代码与核心框架之间的"薄封装",把字符串设备名收敛为整型索引。索引定义如下:
enum {
UDISK_INDEX = 0,
SD0_INDEX,
#ifdef D_SFC_DEVICE_EN
INNER_FLASH_RO,
INNER_FLASH_RW,
#endif
#if TFG_EXT_FLASH_EN
EXT_FLASH_RW,
#endif
MAX_DEVICE,
NO_DEVICE = 0xff,
};
来源:device_app.h
索引与设备名一一对应(device_app.c 中的 device_name 表):
static const char device_name[MAX_DEVICE][9] = {
{"udisk0"},
{"sd0"},
#ifdef D_SFC_DEVICE_EN
{"sfc"},
{"sfc"},
#endif
#if TFG_EXT_FLASH_EN
{"ext_flsh"},
#endif
};
static void *p_device[MAX_DEVICE];
来源:device_app.c
注意 INNER_FLASH_RO 与 INNER_FLASH_RW 指向同一个设备名 "sfc"——同一个物理介质可以通过不同索引表达"只读/可写"两种访问视图。
打开与句柄缓存
void *device_open(u32 index)
{
if (index >= MAX_DEVICE) {
return NULL;
}
#ifdef D_SFC_DEVICE_EN
if (INNER_FLASH_RO == index) {
return NULL;
}
#endif
if (NULL == p_device[index]) {
p_device[index] = dev_open((void *)&device_name[index][0], 0);
}
return p_device[index];
}
来源:device_app.c
设计意图:句柄是懒加载 + 缓存的。首次访问才调用 dev_open(),之后直接返回缓存指针,避免反复打开同一介质带来的开销;INNER_FLASH_RO 只读视图被强制返回 NULL,防止业务误写系统区。
带重试的关闭
device_close() 在底层关闭失败时最多重试 20 次,成功后才清空句柄缓存;若最终失败返回 E_PDEV_FAIL。这种"关闭也要可靠"的设计对写缓存介质很重要——SD 卡/U 盘在拔插瞬间关闭可能失败,立即重试通常能成功,避免句柄泄漏。
在线状态查询与热插拔
u32 device_online(void)
{
u32 online = 0;
for (u32 i = 0; i < MAX_DEVICE; i++) {
#ifdef D_SFC_DEVICE_EN
if (INNER_FLASH_RO == i) {
online |= BIT(i);
continue;
}
#endif
if (E_DEV_OFFLINE != device_status(i, 1)) {
online |= BIT(i);
}
}
return online;
}
来源:device_app.c
device_online() 返回位图,每位代表一个索引在线;固定介质(INNER_FLASH_RO)恒为在线。device_status(index, mode) 则先查 dev_online(),设备掉线且 mode != 0 时主动关闭失效句柄,然后二次确认,最终返回 E_DEV_LOST(刚丢失)或 E_DEV_OFFLINE(确认离线)——这个"先关后验"的顺序保证业务拿到的状态是新鲜的。
设备状态流转
stateDiagram-v2
[*] --> Online: dev_open 成功
Online --> Online: dev_byte_read / dev_bulk_write
Online --> Offline: 拔卡/拔出U盘
Offline --> Closing: device_status(index, mode!=0)
Closing --> Closed: dev_close 重试成功
Closing --> Closing: 关闭失败重试(≤20次)
Closed --> [*]: 句柄缓存置 NULL
Offline --> [*]: 不主动关闭(mode==0)
核心调用流程
以"打开 SD 卡并按块读取"为例,完整链路如下:
sequenceDiagram
participant App as 业务代码
participant DA as device_app
participant MG as dev_mg 框架
participant NL as 设备节点表(.device段)
participant SD as sd_dev_ops
App->>DA: device_open(SD0_INDEX)
DA->>DA: p_device[SD0_INDEX] 为空?
alt 缓存命中
DA-->>App: 返回缓存句柄
else 首次打开
DA->>MG: dev_open("sd0", 0)
MG->>NL: 遍历 dev_node 匹配 name
NL-->>MG: 命中 sd 节点
MG->>SD: ops->open(name, &device, arg)
SD-->>MG: 创建设备句柄
MG-->>DA: 返回句柄
DA->>DA: 写入 p_device[SD0_INDEX]
DA-->>App: 返回句柄
end
App->>MG: dev_bulk_read(handle, buf, sector, num)
MG->>SD: ops->bulk_read(device, buf, len, sector)
SD-->>MG: 读取结果
MG-->>App: 返回结果
驱动实例注册示例
device_list.c 中 SD 卡驱动的完整注册展示了"平台数据 + 操作表"的组合模式:
const struct device_operations sd_dev_ops = {
.init = sdx_dev_init,
.online = sdx_dev_online,
.open = sdx_dev_open,
.read = sdx_dev_byte_read,
.write = sdx_dev_byte_write,
.bulk_read = sdx_dev_read,
.bulk_write = sdx_dev_write,
.ioctl = sdx_dev_ioctl,
.close = sdx_dev_close,
};
bulk_read/bulk_write 接 sdx_dev_read/sdx_dev_write(扇区级),read/write 接 sdx_dev_byte_read/sdx_dev_byte_write(字节级),二者通过 ioctl 能力配合文件系统使用。U 盘驱动 mass_storage_ops 的 init 为 NULL(由 USB 主机栈按需初始化),只读 CD-ROM(sd_cdrom)的 write 为 NULL,体现了函数表可空的设计自由度。
配置选项
设备管理框架的配置分散在驱动注册处与编译宏中,主要开关如下:
| 配置项 | 类型 | 默认行为 | 说明 |
|---|---|---|---|
D_SFC_DEVICE_EN | 编译宏 | 按工程配置 | 使能片内 SFC 设备,启用 INNER_FLASH_RO/RW 索引并加入 "sfc" 节点 |
TFG_EXT_FLASH_EN | 编译宏 | 按工程配置 | 使能外挂 NOR Flash("ext_flsh"),同时需要 SPI 平台数据 |
TFG_SD_EN | 编译宏 | 按工程配置 | 使能 SD 卡设备 "sd0" 及其 sd_dev_ops |
TCFG_UDISK_ENABLE | 编译宏 | 按工程配置 | 使能 U 盘设备 "udisk0"(依赖 USB 主机栈) |
SD_CDROM_EN | 编译宏 | 关闭 | 使能只读 CD-ROM 设备视图 "sd_cdrom" |
TFG_SDPG_ENABLE | 编译宏 | 关闭 | SD 卡上电控制,使能时 power = set_sd_power,否则 NULL |
SD0_PLATFORM_DATA_BEGIN | 结构体初始化 | — | SD 卡平台数据:端口、速率、检测模式(CMD/CLK/IO)、数据宽度、优先级 |
NORFLASH_DEV_PLATFORM_DATA_BEGIN | 结构体初始化 | — | NOR Flash 平台数据:SPI 硬件号、CS 引脚、读宽度、SPI 参数 |
SPI 平台数据 | 结构体数组 | — | spix_p_data[]:SPI 时钟/引脚/主从/模式/波特率(默认 10 MHz) |
配置点来源:device_list.c、device_app.h
SD 卡检测支持三种模式(SD_CMD_DECT 命令检测、SD_CLK_DECT 时钟检测、SD_IO_DECT GPIO 检测),通过 detect_mode / detect_func / detect_io / detect_io_level 配置;示例工程默认使用 CLK 检测且低电平表示有卡(见 device_list.c 第 87-101 行)。
失败模式、边界情况与并发
设备离线与句柄失效
SD 卡/U 盘是热插拔介质,句柄可能随时失效。框架的处理链:dev_online() 返回假 → device_status(index, 1) 主动 dev_close() 清缓存 → 二次 dev_online() 确认,返回 E_DEV_LOST 或 E_DEV_OFFLINE。业务代码应在每次大块读写前检查 device_status(),读写返回非零后按"设备丢失"处理,而不是继续重试。
关闭失败与重试
device_close() 最多重试 20 次(u32 retry = 20),期间底层驱动可能正处于忙状态;重试耗尽后返回 E_PDEV_FAIL,且不清空 p_device[index] 缓存,保证后续仍可再次尝试关闭。调用方需注意:关闭失败不等于设备损坏,可能是介质正在写回缓存。
越界与非法索引
device_open / device_close / device_obj / device_status 均校验 index >= MAX_DEVICE,越界返回 NULL 或 E_IDEV_ILL;device_close 对已关闭的索引打印提示并返回 0(幂等),device_obj 返回 NULL 表示未打开。NO_DEVICE = 0xff 作为"无设备"哨兵值供业务使用。
只读视图的保护
INNER_FLASH_RO 是系统区只读视图:device_open 直接返回 NULL、device_close/device_status 直接返回 0、device_online 恒置位——从 API 层面杜绝了对系统 Flash 的误写,属于"编译期能力 + 运行时拦截"的双重保护。
并发与重入
struct device 的 ref 字段使用 atomic_t 原子引用计数,说明框架设计上允许句柄跨任务共享(例如文件系统任务与升级任务同时持有)。但 device_app 的 p_device[] 缓存无锁,业务上应保证同一索引的 device_open/device_close 串行调用;SD 卡检测回调(detect_func)通常运行在中断/低优先级任务上下文,不应在回调内直接调用阻塞型 dev_* 接口。
使用示例
按索引打开并查询在线状态
/* 打开 SD 卡,检查是否在线 */
u32 online = device_online();
if (online & BIT(SD0_INDEX)) {
void *sd = device_open(SD0_INDEX);
if (sd) {
/* 使用 sd 句柄进行读写 */
}
}
来源:device_app.c(
device_online位图语义)、device_app.h(索引枚举)
升级入口调用
#if defined(TFG_DEV_UPGRADE_SUPPORT) && (1 == TFG_DEV_UPGRADE_SUPPORT)
void device_update(u8 update_dev)
{
try_to_upgrade((char *)device_name[update_dev], TFG_UPGRADE_FILE_NAME);
}
#endif
来源:device_app.c
device_update 把设备索引翻译成设备名传给升级模块,展示"索引封装 + 按名调用"的典型用法——升级模块完全不知道目标介质是 SD 卡还是 U 盘。
API 参考
void *device_open(u32 index)
按索引打开设备并缓存句柄。
- 参数:
index—UDISK_INDEX/SD0_INDEX/INNER_FLASH_RW/EXT_FLASH_RW等 - 返回:设备句柄;
index >= MAX_DEVICE、INNER_FLASH_RO或打开失败时返回NULL - 注意:首次调用才真正执行
dev_open(),后续返回缓存
u32 device_close(u32 index)
关闭设备,最多重试 20 次。
- 参数:
index— 设备索引 - 返回:
0成功;E_IDEV_ILL非法索引;E_PDEV_FAIL重试后仍失败 - 注意:幂等,重复关闭已关闭设备返回 0
u32 device_status(u32 index, bool mode)
查询设备在线状态;mode != 0 时自动清理离线句柄。
- 返回:
0在线;E_DEV_LOST刚丢失(mode 模式下清理后仍未恢复);E_DEV_OFFLINE确认离线;E_IDEV_ILL非法索引
u32 device_online(void)
返回在线设备位图(BIT(index) 置位表示在线),固定介质恒置位。
void device_update(u8 update_dev)
按设备索引触发固件升级(TFG_DEV_UPGRADE_SUPPORT=1 时编译),内部调用 try_to_upgrade(设备名, 升级文件名)。
核心框架 API(dev_mg)
bool dev_online(const char *name)— 查询命名设备是否在线void *dev_open(const char *name, void *arg)— 打开设备返回句柄int dev_close(void *device)— 关闭句柄int dev_byte_read(void *device, void *buf, u32 offset, u32 len)— 字节流读int dev_byte_write(void *device, void *buf, u32 offset, u32 len)— 字节流写int dev_bulk_read(void *device, void *buf, u32 sector, u32 sector_num)— 扇区块读int dev_bulk_write(void *device, void *buf, u32 sector, u32 sector_num)— 扇区块写int dev_ioctl(void *device, int cmd, u32 arg)— 设备控制(命令码见ioctl_cmds.h)int devices_init_api(void)— 系统启动时初始化全部注册设备
扩展点
新增一种存储介质(例如 SPI NAND)只需三步:
- 实现
struct device_operations函数表(可参考sd_dev_ops/mass_storage_ops模式,device_list.c第 111-160 行)。 - 用
REGISTER_DEVICE/REGISTER_DEVICES宏声明struct dev_node(.device段自动收集)。 - 在
device_app的device_name[]与device_app.h枚举中增加索引(受MAX_DEVICE约束,数组第二维 9 字节存不下超长设备名时需同步调整)。
无需改动 dev_mg 核心代码——这正是链接段注册机制的设计红利。
测试与验证建议
SDK 未提供独立的设备管理单元测试文件,框架正确性主要依赖以下运行路径验证:系统启动时 devices_init_api() 是否成功初始化全部节点(日志 [device_list]/[device_app]);SD 卡拔插时 device_status 是否从在线态迁移到 E_DEV_LOST;U 盘枚举后 udisk0 能否被 dev_open 打开并完成 dev_bulk_read。排查问题时建议先确认 .device 段中节点数量(device_node_end - device_node_begin),再逐层检查 dev_online → dev_open → bulk_read 的返回值。
Related Links
- device.h(核心框架头文件)
- device_app.c(应用层设备管理实现)
- device_app.h(设备索引枚举)
- device_list.c(设备注册与平台数据)
- USB 主机栈与 U 盘驱动:
apps/app/bsp/common/usb/device/(usb_device.c、usb_device_config.c) - 设备驱动接口补充:
apps/include_lib/device/device_drive.h