RT-Thread 物理磁盘驱动编写指南:深入 `struct rt_blk_disk` 与 `rt_blk_disk_ops`

原创2026-10-03 13:57:54403 阅读
文章标签:操作系统嵌入式物联网嵌入式OSRTOS

RT-Thread 物理磁盘驱动编写指南:深入 struct rt_blk_disk 与 rt_blk_disk_ops

RT-Thread 的 blk 块设备层将单个物理存储后端(NVMe、SDIO/MMC、SCSI 磁盘等)封装为统一的块设备,并自动解析分区表、生成可供 DFS 与应用使用的卷设备节点。本文以官方驱动编写指南(disk.md)为核心,结合 blk.h 与 blk.c 源码,系统讲解磁盘驱动从结构体填充、ops 实现到注册/注销的完整流程。读完本文,你将掌握如何为自有硬件编写一个可被 RT-Thread 分区探测与文件系统挂载的物理磁盘驱动。


一、blk 层架构:物理磁盘与卷的分层模型

在 RT-Thread 块设备子系统中,struct rt_blk_disk 代表"物理后备存储"——由驱动实现的、连续 LBA 地址空间中的一块存储介质。blk 核心层将其包装成一个 rt_device(块设备类 RT_Device_Class_Block),随后为核心层创建的卷(volume)子设备提供父级支撑。

三层结构如下:

层 数据结构 说明
物理磁盘 struct rt_blk_disk 本文主角,驱动通过 rt_blk_disk_ops 访问底层介质
分区表 磁盘上的元数据(GPT / DFS) 由 blk_partition.c 解析
卷(分区) struct rt_blk_device 面向应用与 DFS 的设备节点,如 sda1、mmcsd0p1

数据流方向:驱动(NVMe / SDIO / SCSI SD / 自研) → rt_blk_disk_ops → struct rt_blk_disk(rt_hw_blk_disk_register 注册,内部自动触发 rt_blk_disk_probe_partition)→ struct rt_blk_device × N → DFS / 应用 / POSIX devio。

应用层通常打开卷设备(如 sda1)而非裸磁盘设备,除非探测逻辑创建了 partno == 0 的整盘卷。卷设备的读写会通过 blk_dev.c 转发到父磁盘节点,偏移量自动加上 sector_start。

实现文件:components/drivers/block/blk.c;头文件:components/drivers/include/drivers/blk.h。相关配套文档见 blk.md(应用层 API 与卷层)与 partitions.md(分区探测)。


二、struct rt_blk_disk:注册前的字段填充

在调用 rt_hw_blk_disk_register 之前,驱动必须正确填充 struct rt_blk_disk 各成员。该结构体定义于 blk.h:

struct rt_blk_disk
{
    struct rt_device parent;

    const struct rt_blk_disk_ops *ops;
#ifdef RT_USING_DM
    struct rt_dm_ida *ida;
#endif

    rt_uint32_t read_only   : 1;
    rt_uint32_t parallel_io : 1;
    rt_uint32_t removable   : 1;
#define RT_BLK_DISK_MAGIC 0xbdaabdaa
    rt_uint32_t __magic;

    rt_uint32_t partitions;
#define RT_BLK_PARTITION_NONE (-1)
#define RT_BLK_PARTITION_MAX  (RT_UINT32_MAX >> 1)
    rt_int32_t max_partitions;
    rt_list_t part_nodes;

    struct rt_spinlock lock;
    struct rt_semaphore usr_lock;
};

各成员的所有权归属与含义如下表:

成员 归属 规则 / 含义
parent 调用方 内嵌的 rt_device。注册前必须设置设备名(parent.parent.name 首字符 ≠ '\0',即非空字符串),否则注册返回 -RT_EINVAL
ops 调用方 必填,即下文要讲的 struct rt_blk_disk_ops
ida 调用方(RT_USING_DM) 非 NULL,与所有卷子设备共享,用于 rt_dm_ida_alloc 分配设备号;NULL 时注册直接失败
read_only 调用方 / 核心 若 ops->write == NULL,核心会在注册时强制置 read_only = RT_TRUE(见 blk.c)
parallel_io 调用方 RT_TRUE 时磁盘读写跳过 usr_lock 互斥(NVMe 即使用此模式)
removable 调用方 可移动介质提示(如 SDIO 从 host 控制器属性设置)
max_partitions 调用方 分区解析器的数量上限;RT_BLK_PARTITION_NONE 表示禁用探测;RT_BLK_PARTITION_MAX 表示不限制
partitions 核心 已注册的 rt_blk_device 节点数量(探测后由核心维护)
part_nodes 核心 卷列表,元素类型为 struct rt_blk_device
lock 核心 自旋锁,保护分区链表、注销与擦除路径
usr_lock 核心 信号量(PRIO 优先级继承),parallel_io == 0 时串行化 I/O
__magic 核心 注册时置为 RT_BLK_DISK_MAGIC(0xbdaabdaa),供核心与 MSH 命令识别合法磁盘节点

其中 RT_BLK_PARTITION_NONE、RT_BLK_PARTITION_MAX 的定义与 struct rt_blk_device(卷结构体:partno、partition、sector_start、sector_count、disk 回指等)均在同头文件中。


三、struct rt_blk_disk_ops:驱动能力回调集

驱动通过 struct rt_blk_disk_ops 暴露介质访问能力,定义于 blk.h:

struct rt_blk_disk_ops
{
    rt_ssize_t (*read)(struct rt_blk_disk *disk, rt_off_t sector, void *buffer,
                       rt_size_t sector_count);
    rt_ssize_t (*write)(struct rt_blk_disk *disk, rt_off_t sector, const void *buffer,
                        rt_size_t sector_count);
    rt_err_t (*getgeome)(struct rt_blk_disk *disk, struct rt_device_blk_geometry *geometry);
    rt_err_t (*sync)(struct rt_blk_disk *disk);
    rt_err_t (*erase)(struct rt_blk_disk *disk);
    rt_err_t (*autorefresh)(struct rt_blk_disk *disk, rt_bool_t is_auto);
    rt_err_t (*control)(struct rt_blk_disk *disk, struct rt_blk_device *blk, int cmd, void *args);
};

各回调的契约如下:

回调 是否必需 契约
read 必选(实践中) sector、sector_count 以 512 字节扇区为单位(除非 getgeome 声明其他扇区大小)。返回实际读出的扇区数,失败返回 < 0 错误码
write 可选 置 NULL 表示磁盘只读(核心自动设 read_only)。寻址语义与 read 相同
getgeome 必选(实践中) 填充 sector_count(总容量扇区数)、bytes_per_sector(每扇区字节数)、block_size(擦除块大小,面向 Flash 类介质)。分区代码与 DFS 均依赖此回调
sync 可选 刷写缓存;在注销与 RT_DEVICE_CTRL_BLK_SYNC 控制命令时被调用
erase 可选 整盘擦除;磁盘节点的 ERASE IOCTL 会先移除全部分区再调用
autorefresh 可选 对应 RT_DEVICE_CTRL_BLK_AUTOREFRESH(如 SD 卡的自动刷新场景)
control 可选 厂商自定义 IOCTL;blk 参数在裸磁盘路径为 NULL,在卷转发路径为非 NULL

扇区寻址约定

  • 参数是整盘上的 LBA 扇区索引,而不是字节偏移。
  • getgeome()->bytes_per_sector 必须与 read/write 实际使用的扇区大小一致。
  • getgeome()->sector_count 表示整盘容量(扇区数)。
  • 若 parallel_io 为真,read/write 可能并发交叠执行——驱动内部必须自带锁或队列深度控制。

struct rt_device_blk_geometry 定义于 components/drivers/include/drivers/classes/block.h,字段为 sector_count(rt_uint64_t)、bytes_per_sector、block_size(均 rt_uint32_t)。


四、注册核心流程:rt_hw_blk_disk_register

rt_hw_blk_disk_register 是驱动接入 blk 层的唯一入口,实现在 blk.c。其执行序列为:

步骤 动作
1 校验 disk、ops、设备名、ida(DM 模式),任一非法返回 -RT_EINVAL
2 rt_dm_ida_alloc(disk->ida) 为磁盘分配设备号,失败返回 -RT_EFULL
3 初始化 usr_lock(信号量,初值 1,RT_IPC_FLAG_PRIO)、part_nodes 链表、lock 自旋锁
4 在 parent 上挂接 blk_ops 或 blk_parallel_ops(取决于 parallel_io;RT_USING_DEVICE_OPS 启用时用 ops 表,否则直接赋值函数指针)
5 若 !ops->write,置 read_only = 1
6 device_set_blk_fops 挂文件系统操作,rt_device_register 以 RD/WR 标志注册设备(flags 初始为 RT_DEVICE_FLAG_RDONLY,非只读时追加 RT_DEVICE_FLAG_WRONLY)
7 调用 rt_blk_disk_probe_partition(disk) 触发分区探测——探测错误被忽略,不影响注册成功与否

返回值语义:仅返回注册过程本身的错误;分区探测失败不会导致注册失败。因此注册成功后应主动检查 disk->partitions 或直接尝试打开卷设备,判断介质是否含有可用分区。

内部设备操作(disk 节点)

注册后,磁盘节点的 rt_device 读写/控制按 parallel_io 分支:

操作 parallel_io == 0 parallel_io == 1
read/write 先 rt_sem_take(&disk->usr_lock, RT_WAITING_FOREVER) 再调用 ops->read/write,最后释放 直接调用 ops->read/write,不加锁
control blk_control 分发到 ops 同上

blk_control(blk.c)的标准处理包括:RT_DEVICE_CTRL_BLK_GETGEOME(转发 ops->getgeome)、RT_DEVICE_CTRL_BLK_SYNC(在 usr_lock + lock 下调用 ops->sync)、RT_DEVICE_CTRL_BLK_ERASE(要求 ref_count == 1,先 blk_remove_all 清空全部分区再执行 ops->erase,否则返回 -RT_EBUSY)、RT_DEVICE_CTRL_BLK_AUTOREFRESH(转发 ops->autorefresh(disk, !!args));未识别命令落入 default 分支转发 ops->control(disk, RT_NULL, cmd, args)。

特别地:对磁盘节点发送 RT_DEVICE_CTRL_BLK_PARTITION 固定返回 -RT_EINVAL——分区信息只能通过卷设备查询。

此外,在启用 RT_USING_DFS_MNTTABLE 时,核心还会通过 INIT_ENV_EXPORT(blk_dfs_mnt_table) 自动挂载所有块设备及其卷;启用 MSH 时提供 list_blk 命令(blk.c),以 lsblk 风格输出每个磁盘/分区的 MAJ:MIN、可移动标志(RM)、容量(自动换算 B/K/M/G/T/P/E)、只读标志(RO)、类型(disk/part)与挂载点。


五、注销流程:rt_hw_blk_disk_unregister

rt_hw_blk_disk_unregister(blk.c)的注销序列为:

步骤 动作
1 disk->parent.ref_count > 0 → 返回 -RT_EBUSY(设备仍被占用)
2 若存在 ops->sync,先刷写(失败则中止注销并 LOG_E)
3 blk_remove_all——逐个注销所有 rt_blk_device 卷节点
4 rt_dm_ida_free 释放磁盘设备号,rt_device_unregister 注销设备节点

注销前务必先卸载 DFS 文件系统、关闭所有打开句柄(close 到 ref_count == 0),否则返回 -RT_EBUSY。热插拔场景的标准动作:拔卡 → 注销;插卡 → 重新注册(此时会重新触发分区探测)。


六、辅助 API

API 作用
rt_blk_disk_probe_partition(disk) 仅在 disk->partitions == 0 时重新运行分区解析器;详见 partitions.md
rt_blk_disk_get_capacity(disk) 调用 getgeome 返回 sector_count(总容量,扇区数)
rt_blk_disk_get_logical_block_size(disk) 调用 getgeome 返回 bytes_per_sector(逻辑块大小,字节)

这两个获取函数实现在 blk.c:内部先执行 ops->getgeome,成功则返回对应几何字段,disk == NULL 返回 -RT_EINVAL,回调失败则透传错误码。


七、分区探测如何工作(驱动侧须知)

注册的最后一步 rt_blk_disk_probe_partition(blk_partition.c)执行如下判定:

  1. disk == NULL → -RT_EINVAL;
  2. disk->partitions != 0 → 已探测过,直接成功返回;
  3. max_partitions == RT_BLK_PARTITION_NONE → -RT_EEMPTY(禁用分区表解析);
  4. 依次调用 partition_listi 中的解析器;
  5. 解析器返回 -RT_ENOMEM → 中止并返回 -RT_ENOMEM;
  6. 解析器返回 0(成功)→ 停止;
  7. 所有解析器均未匹配 → 回退创建整盘卷:blk_put_partition(disk, NULL, 0, total_sectors, 0)。

因此即使介质为空白盘或未知分区格式,探测仍会生成一个覆盖整盘的卷设备(partno == 0),保证 DFS 可以挂载。解析器顺序由 blk_partition.c 的 partition_list[] 决定:EFI(GPT)在前,DFS 在后,第一个返回 0 的解析器胜出。

Kconfig 开关见 partitions/Kconfig:

选项 解析器文件 作用
RT_USING_BLK components/drivers/block/ 主开关(Kconfig) 使能整个 blk 层
RT_BLK_PARTITION_EFI partitions/efi.c 保护性 MBR + GPT(UEFI 风格),默认 y
RT_BLK_PARTITION_DFS partitions/dfs.c DFS 传统分区表(扇区 0 的超块布局),依赖 RT_USING_DFS,默认 y

卷设备的命名规则在 blk_dev.c 的 disk_add_blk_dev 中:若磁盘名末字符是字母 a–z(如 sda),卷命名为 sdap1(%sp%d);否则(如 mmcsd0、nvme0n1)卷命名为 mmcsd0p1、nvme0n1p2(%s%d)。


八、树内集成实例:四种成熟驱动模板

NVMe(components/drivers/nvme/nvme.c)

NVMe 驱动为**每个命名空间(namespace)**创建独立的 rt_blk_disk(nvme.c):

  • 命名:rt_dm_dev_set_name(&ndev->parent.parent, "%sn%u", nvme->name, nsid)——命名空间从 1 起,得到 nvme0n1 风格名称;
  • parallel_io = RT_TRUE(NVMe 硬件天然支持并发队列),ops = &nvme_blk_ops,ida = &nvme_ida;
  • max_partitions = RT_BLK_PARTITION_MAX(不限分区数);
  • Identify 完成后调用 rt_hw_blk_disk_register(&ndev->parent)。

SDIO / MMC(components/drivers/sdio/dev_block.c)

rt_mmcsd_blk_probe(dev_block.c)的要点:

  • 几何信息取自卡 CSD:bytes_per_sector = MMCSD_SECTOR_SIZE(512)、block_size = card->card_blksize、sector_count = card->card_capacity * (1024 / MMCSD_SECTOR_SIZE);
  • 注册前先 mmcsd_set_blksize(card)(代码注释特别强调"Set blk size before partitions probe");
  • removable = controller_is_removable(host),max_partitions = RT_MMCSD_MAX_PARTITION(默认 16,见 Kconfig 与 dev_block.c);
  • 名称取自 host->name,卷设备形如 hostp1(mmcsd0p1)。

SCSI 磁盘(components/drivers/scsi/scsi_sd.c)

scsi_sd_probe(scsi_sd.c)流程:

  • 先用 rt_scsi_read_capacity10 读容量,若 last_block == 0xffffffff 再升级到 rt_scsi_read_capacity16(大容量磁盘);
  • 几何:bytes_per_sector = block_size、block_size = block_size、sector_count = last_block + 1;
  • 命名:rt_dm_dev_set_name(&ssd->parent.parent, "sd%c%c", letter_name(ssd->sd_id)),得到 sda、sdb 风格;
  • parallel_io 直接取自 SCSI host(sdev->host->parallel_io),ida = &scsi_sd_ida,max_partitions = RT_BLK_PARTITION_MAX;
  • 注销路径 scsi_sd_remove 先释放 sd_ida 槽位再调 rt_hw_blk_disk_unregister。

SCSI CD-ROM(components/drivers/scsi/scsi_cdrom.c)

采用相同的 blk 注册模式;光学介质通常为只读(write 缺省或只读标志),且 max_partitions = RT_BLK_PARTITION_NONE(scsi_cdrom.c)——CD 盘无分区表概念,禁用探测。

另外 virtio-blk.c 也使用了 RT_BLK_PARTITION_MAX,可作为虚拟化场景的参考。


九、最小驱动骨架(可直接照抄)

综合以上模式,一个最小可用的物理磁盘驱动骨架如下(结构参照 blk.md 的 Minimal driver pattern):

static const struct rt_blk_disk_ops my_blk_ops = {
    .read     = my_read,
    .write    = my_write,
    .getgeome = my_getgeome,
    .sync     = my_sync,      /* 可选:刷写缓存 */
    /* .erase / .autorefresh / .control 按需实现 */
};

static struct rt_dm_ida my_ida = RT_DM_IDA_INIT(MY_DISK);

static rt_ssize_t my_read(struct rt_blk_disk *disk, rt_off_t sector,
                          void *buffer, rt_size_t sector_count)
{
    /* 按 LBA 扇区寻址;若 parallel_io,需自带并发保护 */
    return my_hw_read(sector, buffer, sector_count);
}

static rt_err_t my_getgeome(struct rt_blk_disk *disk,
                            struct rt_device_blk_geometry *geometry)
{
    geometry->bytes_per_sector = MY_SECTOR_SIZE;   /* 如 512 */
    geometry->block_size       = MY_ERASE_SIZE;    /* Flash 擦除块大小 */
    geometry->sector_count     = MY_TOTAL_SECTORS; /* 整盘容量 */
    return RT_EOK;
}

void my_disk_register(struct my_hw *hw)
{
    struct rt_blk_disk *disk = &hw->blk;

    disk->ops = &my_blk_ops;
    disk->ida = &my_ida;
    disk->parallel_io = RT_FALSE;   /* ops 可重入时才设 RT_TRUE */
    disk->removable = RT_FALSE;
    disk->max_partitions = RT_BLK_PARTITION_MAX; /* 或按需限定数量 */
    rt_dm_dev_set_name(&disk->parent.parent, "sda"); /* 示例命名 */

    rt_hw_blk_disk_register(disk);  /* 注册并自动触发分区探测 */
}

注意:在启用 RT_USING_DM 的配置下,ida 必须使用 RT_DM_IDA_INIT 初始化并赋给 disk->ida,且磁盘名必须在注册前通过 rt_dm_dev_set_name 设置完成。


十、DM 命名与 IDA 分配清单

步骤 动作
1 每个磁盘家族(family)定义并初始化一个 IDA,例如 scsi_sd_ida、nvme_ida、sdio_ida
2 注册前把 disk->ida 指向对应 IDA
3 磁盘名通过 rt_dm_dev_set_name(&disk->parent.parent, ...) 生成
4 卷设备由探测逻辑自动命名(disk_add_blk_dev)——不要自行注册 rt_blk_device,除非你绕开 blk_put_partition 手动管理卷

每个卷在注册时还会 rt_dm_ida_alloc(disk->ida) 占用一个 IDA 槽位,且与磁盘共享同一个 master_id。


十一、驱动注意事项速查

主题 指引
DMA / 缓冲区 若调用方传入非 DMA 安全指针,必须在 ops 内部做 bounce 缓冲拷贝
容量变化 先 rt_hw_blk_disk_unregister,更新几何信息,再重新注册
热插拔 拔卡 → rt_hw_blk_disk_unregister;插卡 → 重新注册(自动重跑分区探测)
GPT + DFS 并存 同时使能两个 Kconfig 解析器;partition_list[] 中 EFI 先执行
自定义 IOCTL 实现 control(disk, blk, cmd, args);卷路径未识别命令由 blk_control 的 default 分支转发(此时 blk 非 NULL)
只读介质 ops 中省略 write,核心自动置只读,写路径返回 -RT_ENOSYS
注销前置条件 先卸载 DFS、关闭所有句柄;ref_count > 0 时返回 -RT_EBUSY
擦除语义 磁盘 ERASE 会先移除全部分区;卷设备上 ERASE 仅在 disk->partitions <= 1 时允许,否则 -RT_EIO

延伸阅读

  • blk.md — 应用层 API、卷设备与 DFS 集成
  • partitions.md — GPT/EFI 与 DFS 分区解析器细节
  • ata.md — AHCI → SCSI → blk 链路
  • blk.h — 核心数据结构与 API 声明
  • blk.c — 注册/注销/控制核心实现
  • blk_partition.c — 分区探测实现
登录后查看全文
rt-thread