5.29 Block设备
5.29 Block设备
📚 本节导读
学习时长: 约 25 分钟
难度级别: ⭐⭐⭐☆☆
前置知识: C 语言基础、存储基础知识、5.1 设备驱动框架、5.27 MTD 设备
本节讲解 OneOS 的 Block 块设备。块设备(Block Device)以固定大小的块(Block,通常 512 字节)为单位进行读写,是文件系统(如 FatFs、LittleFS)访问存储器的标准接口。OneOS 的 Block 设备通常把 MTD(5.27)等底层存储封装成统一的块设备接口。
本节会带你:
- 理解"什么是块设备、为什么文件系统需要它";
- 拆解
blk_dev_read/blk_dev_write/blk_dev_control的完整源码; - 掌握
os_blk_geometry几何信息与os_blk_ops操作函数表; - 掌握
block_device_register的注册流程与 Block/MTD 的关系。
说明:本节所有源码均取自项目
drivers/block/block_device.c,与项目源码逐字一致(含原始英文注释)。
🎯 学习目标
- 理解 Block 块设备的概念与设计
- 掌握
os_blk_geometry几何信息结构体 - 逐个拆解
blk_dev_read/blk_dev_write/blk_dev_control的完整源码 - 掌握
block_device_register的注册流程 - 理解 Block 设备与 MTD 设备的关系
一、概述
1.1 Block 设备解决什么问题
一句话核心:Block 设备适用于为文件系统提供统一、按块访问的存储接口。它让你"文件系统按块读写,底层存储(如 MTD/NAND)按自己的物理页/块工作,两者解耦"。
为什么用 Block 设备:
- 文件系统读写数据的基本单位是扇区/块(如 512 字节),而底层 Flash(NAND/NOR)的物理单位是页/块。Block 设备作为中间层,把"文件系统的块"映射到"底层的页/块"。
- 提供统一的
read_block/write_block接口和geometry(块大小、容量),文件系统通过OS_DEVICE_CTRL_BLK_GETGEOME获取这些信息。 - 与 MTD 的关系:MTD 提供原始 Flash 的页级访问,Block 设备在此基础上封装成文件系统友好的块级接口。
1.2 Block 设备在存储栈中的位置
文件系统(FatFs / LittleFS)
│ 以块(512B)为单位 read_block/write_block
▼
Block 设备(block_device.c) ← 本节
│ os_blk_ops->read_block/write_block
▼
MTD / NAND / SFlash(底层存储) ← 5.27 / 5.28 / 5.25关键理解:Block 设备是"文件系统"和"底层存储"之间的适配层。文件系统不需要知道底层是 NAND 还是 NOR,只需要 read_block(块号, 缓冲, 块数) 和几何信息。
1.3 Block 设备与 MTD 设备的对比
| 特性 | Block 设备 | MTD 设备 |
|---|---|---|
| 访问单位 | 块(扇区,如 512B) | 页/块 |
| 主要使用者 | 文件系统 | 底层驱动 / FAL |
| 接口 | read_block / write_block | read_page / write_page / erase_block |
| 定位 | 文件系统适配层 | 原始存储抽象层 |
1.4 Block 设备使用场景
1.4.1 一句话核心
Block 设备适用于为文件系统提供按块读写、带几何信息的存储接口的场景,尤其适合把 MTD 存储封装成文件系统可用的块设备。
1.4.2 典型应用场景
场景一:把 MTD 封装成块设备挂文件系统
MTD(nand0block)→ Block 设备(blk0)→ FatFs 挂载文件系统通过 Block 设备的 read_block/write_block 读写,底层由 MTD 的页操作实现。
场景二:文件系统初始化时获取几何信息
struct os_blk_geometry geometry;
os_device_control(dev, OS_DEVICE_CTRL_BLK_GETGEOME, &geometry);
/* geometry.block_size / geometry.capacity 用于格式化或容量判断 */为什么用 Block:OS_DEVICE_CTRL_BLK_GETGEOME 让文件系统在挂载时获取块大小和容量,确定格式化参数。
1.4.3 选型边界表(何时不该用它)
| 场景 | 该用什么 | 为什么 |
|---|---|---|
| 上层只需要按字节偏移读写 | 字符设备 / MTD 字符接口 | Block 按块访问,不适合字节粒度的随机小数据 |
| 只需要分区管理 | FAL(5.17) | FAL 在 MTD 之上做分区,不需要块设备 |
| 不需要文件系统 | 直接用 MTD / FAL | Block 主要是文件系统的适配层 |
二、核心数据结构
2.1 Block 几何信息 struct os_blk_geometry
来源:drivers/block/block_device.h
struct os_blk_geometry
{
uint32_t block_size; /* block size measured in bytes */
uint64_t capacity; /* capacity measured in bytes */
};| 字段 | 说明 |
|---|---|
block_size | 块(扇区)大小(字节),通常 512 |
capacity | 总容量(字节) |
关键理解:文件系统挂载时用
OS_DEVICE_CTRL_BLK_GETGEOME拿block_size/capacity,算出总块数 = capacity / block_size 用于分区/格式化。
2.2 Block 操作函数表 struct os_blk_ops
struct os_blk_ops
{
int (*read_block)(os_blk_device_t *blk, uint32_t block_addr, uint8_t *buff, uint32_t block_nr);
int (*write_block)(os_blk_device_t *blk, uint32_t block_addr, const uint8_t *buff, uint32_t block_nr);
};函数指针详解(源码无注释,下表补充说明,不属于源码):
| 函数指针 | 说明 |
|---|---|
read_block | 从 block_addr(块号)读 block_nr(块数)块到 buff |
write_block | 把 buff 写 block_nr(块数)块到 block_addr(块号) |
注意:
block_addr是块号(不是字节偏移),block_nr是块数。底层实现把块号乘block_size换算成字节地址再访问。
2.3 Block 设备结构体
来源:drivers/block/block_device.h
struct os_blk_device
{
struct os_device blk_dev;
struct os_blk_geometry geometry;
const struct os_blk_ops *blk_ops;
void *priv;
};字段详解(源码无注释,下表补充说明,不属于源码):
| 字段 | 含义 |
|---|---|
blk_dev | 首成员继承 os_device_t,注册为 OS_DEVICE_TYPE_BLOCK 设备 |
geometry | 几何信息(块大小/容量),注册前须填好 |
blk_ops | 底层块读写操作表(read_block/write_block) |
priv | 私有数据(如指向底层 MTD/FAL 设备) |
关键理解 ——
blk_dev首成员继承 +os_container_of:blk_dev_read/write/control里用os_container_of(dev, os_blk_device_t, blk_dev)从os_device_t*反推整个os_blk_device(因为blk_dev是首成员,地址相等)。所以注册时&blk_dev->blk_dev就是设备基址。
2.4 控制命令
来源:drivers/block/block_device.h,与项目逐字一致:
#define OS_DEVICE_CTRL_BLK_GETGEOME IOC_BLOCK(0) /* Get geometry information. */
#define OS_DEVICE_CTRL_BLK_SYNC IOC_BLOCK(1) /* Flush data to block device. */
#define OS_DEVICE_CTRL_BLK_AUTOREFRESH IOC_BLOCK(3) /* Block device : enter/exit auto refresh mode. */| 命令 | 含义 |
|---|---|
OS_DEVICE_CTRL_BLK_GETGEOME | 获取几何信息 |
OS_DEVICE_CTRL_BLK_SYNC | 把数据刷新到块设备 |
OS_DEVICE_CTRL_BLK_AUTOREFRESH | 进入/退出自动刷新模式 |
注意:当前
blk_dev_control只实现了GETGEOME(见 3.3),SYNC/AUTOREFRESH未单独处理(返回OS_SUCCESS,留给底层/后续扩展)。
三、核心源码详解
说明:以下源码均取自
drivers/block/block_device.c,与项目源码逐字一致(含原始英文注释)。
3.1 blk_dev_read —— 读若干块
一句话定位:把设备框架的 read 调用(pos = 块号,size = 块数)透传给 blk_ops->read_block。
完整源码(drivers/block/block_device.c,与项目一致):
static os_ssize_t blk_dev_read(os_device_t *dev, os_off_t pos, void *buffer, os_size_t size)
{
int ret;
os_blk_device_t *blk_dev = (os_blk_device_t *)os_container_of(dev, os_blk_device_t, blk_dev);
uint32_t block_addr = pos;
uint32_t block_nr = size;
ret = blk_dev->blk_ops->read_block(blk_dev, block_addr, buffer, block_nr);
if (ret == OS_SUCCESS)
return size;
return 0;
}分步详解:
第 1 步:把 pos/size 当作块号/块数(无条件执行)
uint32_t block_addr = pos;
uint32_t block_nr = size;
ret = blk_dev->blk_ops->read_block(blk_dev, block_addr, buffer, block_nr);详细解释:关键语义:块设备的 pos 是块号(不是字节偏移),size 是块数(不是字节数)。直接透传给 blk_ops->read_block。
第 2 步:返回结果(无条件执行)
if (ret == OS_SUCCESS)
return size;
return 0;
}详细解释:读成功返回块数 size,失败返回 0。
3.2 blk_dev_write —— 写若干块
完整源码(drivers/block/block_device.c,与项目一致):
static os_ssize_t blk_dev_write(os_device_t *dev, os_off_t pos, const void *buffer, os_size_t size)
{
int ret;
os_blk_device_t *blk_dev = (os_blk_device_t *)os_container_of(dev, os_blk_device_t, blk_dev);
uint32_t block_addr = pos;
uint32_t block_nr = size;
ret = blk_dev->blk_ops->write_block(blk_dev, block_addr, buffer, block_nr);
if (ret == OS_SUCCESS)
return size;
return 0;
}分步详解:
第 1 步:透传写调用(无条件执行)
uint32_t block_addr = pos;
uint32_t block_nr = size;
ret = blk_dev->blk_ops->write_block(blk_dev, block_addr, buffer, block_nr);详细解释:与读对称,pos 是块号、size 是块数,透传给 write_block。
第 2 步:返回结果(无条件执行)
if (ret == OS_SUCCESS)
return size;
return 0;
}详细解释:写成功返回块数,失败返回 0。
3.3 blk_dev_control —— 控制命令
一句话定位:处理 OS_DEVICE_CTRL_BLK_GETGEOME,把几何信息拷贝给调用者。
完整源码(drivers/block/block_device.c,与项目一致):
static os_err_t blk_dev_control(os_device_t *dev, int cmd, void *args)
{
os_blk_device_t *blk_dev = (os_blk_device_t *)os_container_of(dev, os_blk_device_t, blk_dev);
OS_ASSERT(blk_dev != OS_NULL);
if (cmd == OS_DEVICE_CTRL_BLK_GETGEOME)
{
struct os_blk_geometry *geometry;
geometry = (struct os_blk_geometry *)args;
if (geometry == OS_NULL)
{
return OS_FAILURE;
}
memcpy(geometry, &blk_dev->geometry, sizeof(struct os_blk_geometry));
}
return OS_SUCCESS;
}分步详解:
第 1 步:处理 GETGEOME 命令(条件 cmd == OS_DEVICE_CTRL_BLK_GETGEOME)
if (cmd == OS_DEVICE_CTRL_BLK_GETGEOME)
{
struct os_blk_geometry *geometry;
geometry = (struct os_blk_geometry *)args;
if (geometry == OS_NULL)
{
return OS_FAILURE;
}
memcpy(geometry, &blk_dev->geometry, sizeof(struct os_blk_geometry));
}
return OS_SUCCESS;
}详细解释:当命令是 OS_DEVICE_CTRL_BLK_GETGEOME 时,把 blk_dev->geometry(块大小、容量)通过 memcpy 拷贝给 args。args 为空返回 OS_FAILURE。其他命令暂不处理,返回 OS_SUCCESS。
blk_dev_control() 条件走向总结:
blk_dev_control() 条件走向总结:
├── cmd == OS_DEVICE_CTRL_BLK_GETGEOME
│ ├── args == OS_NULL → 返回 OS_FAILURE
│ └── memcpy(geometry, &blk_dev->geometry) → 返回 OS_SUCCESS
└── 其他 cmd → 返回 OS_SUCCESS3.4 block_device_register —— 注册 Block 设备
完整源码(drivers/block/block_device.c,与项目一致):
const static struct os_device_ops blk_ops = {
.read = blk_dev_read,
.write = blk_dev_write,
.control = blk_dev_control,
};
os_err_t block_device_register(os_blk_device_t *blk_dev, const char *name)
{
OS_ASSERT(blk_dev->blk_ops != OS_NULL);
OS_ASSERT(blk_dev->blk_ops->read_block != OS_NULL);
OS_ASSERT(blk_dev->blk_ops->write_block != OS_NULL);
/* block device */
blk_dev->blk_dev.type = OS_DEVICE_TYPE_BLOCK;
blk_dev->blk_dev.ops = &blk_ops;
os_device_register(&blk_dev->blk_dev, name);
return OS_SUCCESS;
}分步详解:
第 1 步:断言操作函数表完整(无条件执行)
OS_ASSERT(blk_dev->blk_ops != OS_NULL);
OS_ASSERT(blk_dev->blk_ops->read_block != OS_NULL);
OS_ASSERT(blk_dev->blk_ops->write_block != OS_NULL);详细解释:Block 设备必须实现 read_block 和 write_block 两个函数,缺失则断言失败(开发期发现配置错误)。
第 2 步:设置设备类型与操作表并注册(无条件执行)
blk_dev->blk_dev.type = OS_DEVICE_TYPE_BLOCK;
blk_dev->blk_dev.ops = &blk_ops;
os_device_register(&blk_dev->blk_dev, name);
return OS_SUCCESS;
}详细解释:设置 OS_DEVICE_TYPE_BLOCK 类型和 blk_ops 操作表(read/write/control),用 name 注册到设备框架,返回 OS_SUCCESS。
block_device_register() 条件走向总结:
block_device_register() 条件走向总结:
├── OS_ASSERT:blk_ops 及 read_block/write_block 非空
├── blk_dev.type = BLOCK;blk_dev.ops = &blk_ops
└── 以 name 注册 → 返回 OS_SUCCESS四、完整使用示例
示例:实现块设备并获取几何信息
第 1 部分:实现块读写函数并注册
#include <block_device.h>
#include <os_stddef.h>
/* 底层块读写(实际驱动中调用 MTD 页操作) */
static int my_blk_read(os_blk_device_t *blk, uint32_t block_addr,
uint8_t *buff, uint32_t block_nr) { return block_nr; }
static int my_blk_write(os_blk_device_t *blk, uint32_t block_addr,
const uint8_t *buff, uint32_t block_nr) { return block_nr; }
static const struct os_blk_ops my_blk_ops = {
.read_block = my_blk_read, .write_block = my_blk_write,
};
void block_device_init(void)
{
static os_blk_device_t blk_dev;
os_err_t ret;
/* 配置几何信息:块 512B,容量 128MB */
blk_dev.geometry.block_size = 512;
blk_dev.geometry.capacity = 128 * 1024 * 1024;
blk_dev.blk_ops = &my_blk_ops;
/* 注册为 blk0 */
ret = block_device_register(&blk_dev, "blk0");
if (ret != OS_SUCCESS)
{
os_kprintf("block register failed\r\n");
return;
}
}第 2 部分:文件系统初始化时获取几何信息
#include <device.h>
#include <os_stddef.h>
void blk_getgeome_demo(void)
{
os_device_t *dev;
struct os_blk_geometry geometry;
os_err_t ret;
/* 打开块设备 */
dev = os_device_open("blk0");
if (dev == OS_NULL)
{
os_kprintf("blk0 open failed\r\n");
return;
}
/* 获取几何信息(块大小、容量) */
ret = os_device_control(dev, OS_DEVICE_CTRL_BLK_GETGEOME, &geometry);
if (ret != OS_SUCCESS)
{
os_kprintf("get geometry failed\r\n");
return;
}
os_kprintf("block_size=%d, capacity=%llu\r\n",
geometry.block_size, geometry.capacity);
/* 计算总块数(文件系统分区用) */
os_kprintf("total blocks=%llu\r\n", geometry.capacity / geometry.block_size);
}说明:示例展示 Block 设备标准用法:实现
read_block/write_block+ 配置几何信息 →block_device_register注册 → 通过OS_DEVICE_CTRL_BLK_GETGEOME获取块大小/容量。实际效果:文件系统挂载时能拿到block_size和capacity用于格式化。
注意:Block 设备的
pos/size语义是块号/块数(不是字节)。geometry.block_size通常 512,但取决于底层,文件系统用它计算扇区布局。
五、Block 的适配(把新存储封装成块设备)
5.1 先认识适配层:Block 框架怎么"长"到存储上
drivers/block/block_device.c 是跨平台统一框架(read/write/control 透传 + 注册),不碰具体存储寄存器。真正读写具体存储(MTD/FAL/SD 卡)的是底层适配。把新存储封装成 Block 设备,只需实现 read_block/write_block + 填 geometry + 注册,框架层不用改。
适配全景图:
┌──────────────────────────────────────────────────────────┐
│ 文件系统(FatFs / LittleFS) │
└──────────────────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ Block 框架层(drivers/block/block_device.c,通用不改) │
│ blk_dev_read/write/control + block_device_register │
└───────────────┬──────────────────────────┬────────────────┘
│ ①实现 os_blk_ops │ ②block_device_register
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ 存储适配层(my_blk.c / fal_block.c / sdio block_dev.c) │
│ read_block / write_block(块号语义)★需实现★ │
└───────────────┬──────────────────────────┬────────────────┘
│ 底层 MTD / FAL / SDIO │
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ 硬件(NAND / NOR / SD 卡等存储) │
└──────────────────────────────────────────────────────────┘适配的关键洞察:接入新存储只需做两件事:① 实现 struct os_blk_ops 两个回调(read_block/write_block,块号语义);② 填 geometry(block_size/capacity)+ blk_ops 并 block_device_register 注册。框架层不用改。read_block/write_block 内部把块号换算成底层地址再访问。
5.2 接口契约:封装新存储需要实现什么
| 层 | 接口/回调 | 职责 | 说明 |
|---|---|---|---|
| 操作回调 | os_blk_ops.read_block | 读 block_nr 块 | block_addr=块号,换算底层地址 |
| 操作回调 | os_blk_ops.write_block | 写 block_nr 块 | 同上 |
| 几何信息 | os_blk_device.geometry | 块大小/容量 | 填对则文件系统分区/格式化正确 |
| 注册 | block_device_register | 注册为 BLOCK 设备 | 断言 read/write 非空 |
5.3 封装新存储的标准流程
第 1 步:确认底层存储的块语义——底层按什么单位读写(MTD 页 / FAL 块 / SD 扇区),决定 block_size。
第 2 步:实现 os_blk_ops——read_block/write_block,把块号 block_addr 乘 block_size 换算成底层字节地址,调底层 API 读写 block_nr 块。
第 3 步:填几何并注册——设 geometry.block_size/capacity 和 blk_ops,block_device_register(&blk_dev, "blk0")。
第 4 步:测试——用 os_device_control(GETGEOME) 验证几何;用文件系统挂载验证读写。
5.4 适配检查清单(快速自查)
| 检查项 | 说明 |
|---|---|
read_block/write_block 都实现? | block_device_register 断言非空,缺则失败 |
geometry 填对? | block_size/capacity 错则文件系统分区/格式化错误 |
| 块号换算对? | block_addr * block_size 换算底层地址,错则数据错位 |
pos/size 语义对? | 文件系统传的是块号/块数,不是字节 |
| 底层写前是否擦除? | 底层若是 Flash,写前需擦(Block 层不负责,靠底层/FAL) |
📝 本节小结
核心要点回顾
本节围绕 Block 块设备展开,核心要点如下:
1. Block 设备的定位
- 以固定块(通常 512B)为单位读写,是文件系统的标准接口。
- 是"文件系统"与"底层存储(MTD/NAND/SFlash)"之间的适配层。
2. 核心结构
os_blk_geometry:block_size+capacity。os_blk_ops:read_block+write_block。os_blk_device:继承os_device+ geometry + ops + priv。
3. 关键函数
| 函数 | 作用 |
|---|---|
blk_dev_read | pos=块号、size=块数,透传 read_block |
blk_dev_write | pos=块号、size=块数,透传 write_block |
blk_dev_control | GETGEOME 时拷贝几何信息 |
block_device_register | 断言 ops 完整,注册 OS_DEVICE_TYPE_BLOCK 设备 |
4. Block 与 MTD 关系
- MTD 提供原始 Flash 页级访问;Block 在其上封装成文件系统友好的块级接口。
5. 硬件适配
- 封装新存储 = 实现
os_blk_ops两回调(read_block/write_block,块号语义)+ 填geometry+block_device_register,框架层不用改。 - 底层若是 Flash,写前擦除由底层/FAL 负责,Block 层不处理。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
| 混淆块号与字节偏移 | Block 设备 pos 是块号、size 是块数,不是字节!调用 os_device_read(dev, block_no, buf, block_nr) |
| 忘记设置几何信息 | 注册前必须设置 geometry.block_size 和 capacity,否则文件系统拿不到正确信息 |
| 漏实现读写函数 | block_device_register 用 OS_ASSERT 强制要求 read_block/write_block |
| 在字节粒度上随机改数据 | Block 按块访问,若频繁改小块数据,应考虑 EEPROM/字符设备 |
| 忽略与 MTD 的分工 | Block 负责文件系统适配,MTD 负责原始存储;别把底层页细节混进 Block 层 |
| 底层块号换算错 | read_block/write_block 要把块号 block_addr 乘 block_size 换底层地址,错则数据错位 |
| 底层 Flash 未擦就写 | Block 层不负责擦除,底层若 Flash 需先擦(靠 FAL/底层),否则写入错 |
下一步
掌握了 Block 块设备后,接下来学习 Infrared 红外设备:
- 5.30 Infrared设备:了解 OneOS 基于 NEC 协议的红外遥控驱动与接收状态机。