杰理 SDK 文档中心
首页
首页
  • SDK 概述与入门

    • SDK 总览
    • 支持芯片与蓝牙认证
    • 工程结构导航
  • 开发环境与构建

    • 环境搭建与工具链安装
    • 编译指南与工程选择
    • 烧录与生产工具
  • BLE 透传/数传应用

    • 透传应用框架与处理模块
    • 透传与数传示例
    • 多连接与自定义服务示例
    • FindMy 与查找网络示例
  • HID 人机交互应用

    • 键盘与按键设备示例
    • 鼠标设备示例
    • 遥控器示例
    • HID 蓝牙应用模块
  • 公共 BSP 模块

    • 按键、编码器与红外输入
    • 传感器驱动
    • LED 与显示控制
    • 串口与 USB 通信
    • 存储、参数与时钟
    • 电源与温度管理
    • 消息、内存与系统配置
    • OTA 升级框架
  • 蓝牙协议栈与库

    • BLE 控制器与协议栈适配
    • 经典蓝牙 BR/EDR 支持
    • 第三方蓝牙协议
    • 设备管理框架
    • DUT 测试与射频认证
  • 构建系统与开发工具

    • Makefile 构建系统
    • 固件后处理与配置工具
    • 辅助脚本与库合并
  • 文档与硬件资料

    • AT 命令参考
    • 硬件参考资料
    • SDK 文档与在线资源

设备管理框架

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 的设备模型一致:

  1. 统一抽象:每种介质实现一份 struct device_operations 函数表(online/init/open/read/write/bulk_read/bulk_write/seek/ioctl/close),对上层暴露完全一致的行为。
  2. 链接期注册:通过 REGISTER_DEVICE / REGISTER_DEVICES 宏把 struct dev_node 放进专门的 .device 段,由链接脚本收集成 device_node_begin[] ~ device_node_end[] 数组,系统启动时由 devices_init() 统一遍历初始化——不需要手工维护注册表。
  3. 按名访问:上层通过设备名(如 "sd0"、"udisk0"、"sfc"、"ext_flsh")调用 dev_open() 拿到句柄,之后所有读写都走句柄,与具体介质无关。
  4. 应用层索引: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,
};

来源:device_list.c

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)只需三步:

  1. 实现 struct device_operations 函数表(可参考 sd_dev_ops / mass_storage_ops 模式,device_list.c 第 111-160 行)。
  2. 用 REGISTER_DEVICE / REGISTER_DEVICES 宏声明 struct dev_node(.device 段自动收集)。
  3. 在 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
Prev
第三方蓝牙协议
Next
DUT 测试与射频认证