父设备驱动创建子设备
“父设备驱动创建子设备”可以理解成:
Linux 一开始只能发现一个“大设备”;等这个大设备的驱动
probe()跑起来以后,父驱动再告诉内核:“我内部/下游还有几个小设备,请把它们也注册出来,并让各自的驱动去匹配。”
这类情况不是一次性发现所有硬件,而是分层发现。
1. 为什么会有“父设备”和“子设备”?
因为很多硬件不是一个简单设备,而是一个“设备里面套设备”。
比如:
PCIe FPGA 板卡
├── BAR0 控制寄存器
├── DMA Engine
├── 内部 I2C Controller
│ ├── EEPROM
│ └── 温度传感器
├── 内部 SPI Controller
│ └── SPI Flash
└── 网络 MAC
└── 外部 PHY
Linux 一开始通过 PCIe 枚举,只能看到:
一个 PCIe Endpoint
也就是:
Vendor ID / Device ID / BAR / MSI
但是 Linux 不一定天然知道这个 FPGA 里面还有:
DMA Engine
I2C Controller
SPI Controller
温度传感器
EEPROM
PHY
这些东西要么由设备树描述,要么由父驱动在运行时注册出来。Linux 设备树文档也说明,通用行为是:某些子设备会在父设备驱动的 probe() 阶段被注册,例如 I2C bus driver 会为子节点注册 i2c_client,SPI bus driver 会注册 spi_device 子设备。(内核文档)
2. 普通发现 vs 父驱动创建子设备
先对比一下。
情况 A:设备直接被总线发现
比如 PCIe FPGA:
PCIe 枚举
↓
Linux 创建 struct pci_dev
↓
匹配 pci_driver
↓
调用 FPGA PCIe 驱动 probe()
这里 FPGA 板卡是“直接设备”。
情况 B:父设备驱动创建子设备
比如 FPGA PCIe 驱动起来以后,发现 FPGA 里面实现了一个 I2C 控制器。
流程变成:
PCIe 枚举
↓
Linux 创建 struct pci_dev
↓
匹配 FPGA pci_driver
↓
调用 FPGA pci_driver.probe()
↓
父驱动映射 BAR
↓
父驱动发现/初始化内部 I2C Controller
↓
父驱动注册 i2c_adapter
↓
I2C core 再根据设备树/board info/手动信息创建 i2c_client
↓
匹配 i2c_driver
↓
调用温度传感器/EEPROM驱动 probe()
这里就有两层发现:
第一层:PCIe 发现 FPGA 板卡
第二层:FPGA 父驱动创建/注册内部总线和子设备
3. “父设备驱动创建子设备”到底创建了什么?
它不是直接“调用子驱动函数”,而是向 Linux 设备模型注册一个新的 device。
比如可能创建:
| 父设备 | 父驱动创建的子设备对象 | 子设备驱动框架 |
|---|---|---|
| PCIe FPGA | platform_device |
platform_driver |
| PCIe FPGA 内部 I2C 控制器 | i2c_adapter,然后创建 i2c_client |
i2c_driver |
| SoC SPI 控制器 | spi_device |
spi_driver |
| MFD 多功能芯片 | 多个 platform_device |
多个 platform_driver |
| 网卡 MAC | MDIO bus 上的 PHY device | PHY driver |
| 复杂 PCIe 设备 | auxiliary_device |
auxiliary_driver |
MFD 是典型例子:Linux MFD 子系统会把一个多功能父设备拆成多个子功能,文档中提到 MFD 设备会把 children 注册为 platform devices。(内核文档) Auxiliary bus 也是类似思想:把一个大功能拆成多个代表不同功能域的 child devices,便于不同驱动模块分别管理。(内核文档)
4. 为什么不让一个父驱动全部做完?
可以,但不推荐。
比如一个 PCIe FPGA 板卡里有:
DMA
I2C
SPI
GPIO
温度监控
风扇控制
网络接口
你当然可以写一个超级大的 pci_driver,里面全部自己实现。
但问题是:
1. 代码太大,难维护
2. 每个功能没有接入 Linux 对应子系统
3. 无法复用已有 I2C/SPI/GPIO/HWMON/PHY 框架
4. 用户态接口会很混乱
5. 电源管理、热插拔、资源释放更难
6. 不符合 Linux driver model 的分层思想
更规范的方式是:
PCIe 父驱动:
负责发现板卡、映射 BAR、管理全局资源
I2C 子驱动:
负责 I2C 控制器或 I2C 外设
SPI 子驱动:
负责 SPI flash 或 SPI ADC
GPIO 子驱动:
接入 gpiolib
温度传感器子驱动:
接入 hwmon
DMA 子驱动:
接入 dmaengine 或自定义 char/misc 接口
这样每个功能都能进入 Linux 对应框架。
5. 用 PCIe FPGA 举一个完整例子
假设你的 FPGA 板卡通过 PCIe 连接主机。
FPGA 内部有:
BAR0:
0x0000 - 0x0FFF 全局控制寄存器
0x1000 - 0x1FFF DMA 控制器寄存器
0x2000 - 0x2FFF I2C 控制器寄存器
0x3000 - 0x3FFF GPIO 控制器寄存器
Linux 一开始只能看到:
01:00.0 FPGA PCIe Endpoint
你的父驱动是:
struct pci_driver fpga_pci_driver;
父驱动 probe() 里做:
1. pci_enable_device()
2. pci_request_regions()
3. pci_iomap() 映射 BAR0
4. pci_set_master()
5. request_irq()
6. 初始化 FPGA 全局控制
7. 注册 DMA 子设备
8. 注册 I2C 控制器子设备
9. 注册 GPIO 子设备
这样 Linux 设备模型里会变成:
pci 0000:01:00.0
├── fpga-dma
├── fpga-i2c
└── fpga-gpio
然后:
fpga-dma → 匹配 DMA 子驱动
fpga-i2c → 匹配 I2C controller 子驱动
fpga-gpio → 匹配 GPIO 子驱动
6. 伪代码看一下
父 PCIe 驱动大概是这样:
static int fpga_pci_probe(struct pci_dev *pdev,
const struct pci_device_id *id)
{
struct fpga_priv *priv;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
pci_enable_device(pdev);
pci_request_regions(pdev, "fpga-pcie");
priv->bar0 = pci_iomap(pdev, 0, 0);
pci_set_drvdata(pdev, priv);
/*
* 这里开始创建子设备
*/
fpga_register_dma_child(priv);
fpga_register_i2c_child(priv);
fpga_register_gpio_child(priv);
return 0;
}
注意重点:
父驱动不是直接调用子驱动 probe()
而是:
父驱动注册一个 child device
Linux driver core 再去匹配对应 child driver
匹配成功后才调用 child driver 的 probe()
这和 PCIe 枚举后匹配 pci_driver 是同一个思想,只是发现动作从“PCI core”变成了“父驱动”。
7. 子设备如何拿到父设备资源?
这是你最容易卡住的地方。
子设备本身可能没有真实独立的 PCIe BAR。
比如 fpga-i2c 的寄存器其实在父设备 BAR0 的某个 offset:
BAR0 + 0x2000
所以父驱动创建子设备时,要把资源告诉子设备:
fpga-i2c:
reg_base = BAR0 + 0x2000
reg_size = 0x1000
irq = 父设备某个 MSI vector 或共享中断
子驱动 probe() 的时候拿到这些资源,然后只管理自己那一小段。
可以理解成:
父设备拥有整块 BAR
子设备只拿其中一段逻辑资源
例如:
父设备 BAR0 = 64KB
子设备 DMA:
offset 0x1000, size 0x1000
子设备 I2C:
offset 0x2000, size 0x1000
子设备 GPIO:
offset 0x3000, size 0x1000
8. 和设备树里的 child node 有什么关系?
有些子设备不是父驱动“凭空决定”的,而是设备树提前描述了层级关系。
例如:
fpga_pcie@0 {
compatible = "mycompany,fpga-pcie";
i2c@2000 {
compatible = "mycompany,fpga-i2c";
reg = <0x2000 0x1000>;
temp@48 {
compatible = "ti,tmp102";
reg = <0x48>;
};
};
gpio@3000 {
compatible = "mycompany,fpga-gpio";
reg = <0x3000 0x1000>;
};
};
Linux 不是一定启动时就把所有层级都变成独立设备。对于某些 bus 类型,子设备通常要等父 bus/controller driver probe() 后注册出来。Linux 设备树文档明确提到:I2C bus driver 会为每个 child node 注册 i2c_client,SPI bus driver 会注册 spi_device children。(内核文档)
所以设备树只是“描述硬件结构”。
真正把子设备注册到 Linux 设备模型里的,通常还是父设备驱动或对应 bus core。
9. 例子一:I2C 控制器创建 I2C 子设备
这是最经典的父子设备模型。
硬件结构:
SoC / FPGA
└── I2C Controller
├── EEPROM 0x50
└── 温度传感器 0x48
Linux 过程:
1. platform_driver 或 pci_driver 先绑定 I2C Controller
2. I2C Controller 驱动注册 i2c_adapter
3. I2C core 根据设备树/ACPI/board info 创建 i2c_client
4. i2c_client 匹配 i2c_driver
5. EEPROM/温度传感器驱动 probe()
I2C 文档说明,I2C 设备可以显式实例化,方式是填充 struct i2c_board_info 并调用 i2c_new_client_device();I2C client driver 文档也提到 i2c_new_client_device() 或 i2c_new_scanned_device() 通常发生在 I2C bus driver 中。(内核文档)
所以父设备创建子设备的本质就是:
I2C Controller 驱动先创建“总线”
I2C core 再在这条总线上创建“从设备”
10. 例子二:MFD 多功能芯片
MFD = Multi-Function Device,多功能设备。
例如一个 PMIC 芯片里面可能有:
PMIC
├── regulator
├── RTC
├── GPIO
├── watchdog
└── power button
Linux 不能只把它当一个普通 I2C 芯片处理。
更合理的是:
父驱动:PMIC core driver
子驱动:
regulator driver
rtc driver
gpio driver
watchdog driver
流程:
I2C/SPI 发现 PMIC
↓
PMIC 父驱动 probe()
↓
MFD core 注册多个 child platform_device
↓
regulator/rtc/gpio/watchdog 子驱动分别 probe()
Linux MFD/ACPI 相关文档也说明,MFD children 可以作为 platform devices 注册。(内核文档)
这就是父设备创建子设备最典型的场景。
11. 例子三:网卡 MAC 创建/连接 PHY 设备
网卡也经常是父子模型。
硬件结构:
Ethernet MAC
↓ MDIO bus
Ethernet PHY
MAC 可能是 SoC 内部的 platform 设备,也可能在 PCIe 网卡上。
但 PHY 往往在 MDIO 总线上。
流程:
MAC 驱动 probe()
↓
注册/使用 MDIO bus
↓
MDIO bus 扫描或根据固件描述注册 PHY
↓
PHY driver 匹配
↓
MAC driver 和 PHY driver 建立连接
Linux ACPI MDIO/PHY 文档提到,MDIO bus 上的 PHY 可以被注册,后续 MAC 通过引用这些 PHY 与网络接口建立连接。(内核文档)
所以网卡不是只有一个驱动,可能有:
MAC driver
PHY driver
MDIO bus driver
它们分层协作。
12. 例子四:复杂 PCIe 设备使用 auxiliary bus
有些 PCIe 设备很复杂,例如:
PCIe 加速卡
├── 管理功能
├── 网络功能
├── RDMA 功能
├── crypto 功能
└── telemetry 功能
父 PCIe 驱动可能负责:
PCIe BAR
MSI-X vectors
DMA resources
firmware communication
global reset
然后通过 auxiliary bus 注册多个子功能,让不同子驱动处理不同功能域。Linux auxiliary bus 文档说明,它用于一个驱动和一个或多个内核模块需要连接并访问由注册 auxiliary_device 的驱动分配的共享对象的场景。(Linux内核官网)
这类结构类似:
pci_driver probe()
↓
注册 auxiliary_device: net
注册 auxiliary_device: rdma
注册 auxiliary_device: crypto
↓
auxiliary_driver 分别 probe()
13. 父子设备和“设备发现路径”的关系
你之前问的是“设备发现路径决定驱动框架”。
现在加上父子设备后,应该这样理解:
第一层发现路径决定父设备驱动框架;
父设备驱动运行后,再决定是否创建第二层子设备;
第二层子设备再决定自己的驱动框架。
比如 PCIe FPGA:
第一层:
PCIe 枚举 → struct pci_dev → pci_driver
第二层:
父 pci_driver 创建 platform_device → platform_driver
第三层:
父 i2c controller driver 注册 i2c_adapter → i2c_client → i2c_driver
完整链路可能是:
PCIe RC 枚举 FPGA
↓
pci_dev
↓
FPGA pci_driver.probe()
↓
注册 FPGA 内部 I2C Controller
↓
platform_device / platform_driver
↓
I2C controller driver 注册 i2c_adapter
↓
I2C core 创建 temp sensor i2c_client
↓
temp sensor i2c_driver.probe()
所以不是只有一层发现,而是可以层层展开。
14. 为什么 Linux 这样设计?
因为 Linux 希望每个设备归对应子系统管理。
比如:
温度传感器 → hwmon
GPIO 控制器 → gpiolib
I2C 控制器 → i2c-core
SPI Flash → MTD
PHY → phylib
DMA 控制器 → dmaengine
网卡 → netdev
这样用户态和其他内核模块才能用统一接口访问。
例如温度传感器接入 hwmon 后,用户态可能看到:
/sys/class/hwmon/hwmonX/temp1_input
GPIO 接入 gpiolib 后,用户态或内核其他驱动可以按 GPIO 框架使用。
SPI Flash 接入 MTD 后,可以按 MTD 分区管理。
这比你在父驱动里自己造一堆 /dev/fpga_temp、/dev/fpga_gpio、/dev/fpga_flash 更规范。
15. 对你做 PCIe FPGA DMA 的实际启发
如果你的 FPGA 只是一个简单 DMA Endpoint:
PCIe Endpoint
├── BAR0 寄存器
├── DMA Engine
└── MSI 中断
那你可以只写一个:
pci_driver + 字符设备接口
结构:
pci_driver
↓
probe()
↓
BAR / DMA / IRQ
↓
miscdevice / cdev
不一定需要创建子设备。
但如果你的 FPGA 里面功能很多:
PCIe FPGA
├── DMA
├── GPIO
├── I2C
├── SPI
├── UART
├── PWM
└── 温度/电压监控
那更推荐:
父 PCIe 驱动:
负责 PCIe 总资源
子设备:
DMA 子设备
GPIO 子设备
I2C 子设备
SPI 子设备
UART 子设备
hwmon 子设备
这样每个功能接入对应 Linux 框架。
16. 你可以用这个判断是否需要创建子设备
| 情况 | 是否需要父驱动创建子设备 |
|---|---|
| FPGA 只有一套 DMA 寄存器,只给用户态 ioctl | 不一定需要 |
| FPGA 内部有多个独立功能块 | 建议需要 |
| 某个功能应该接入 Linux 标准子系统 | 建议需要 |
| FPGA 内部实现了 I2C/SPI/GPIO/UART 控制器 | 建议需要 |
| 一个设备需要多个内核模块分别管理 | 建议需要 |
| 只是几个简单状态寄存器 | 不一定需要,父驱动直接处理即可 |
| 功能之间共享 BAR、IRQ、DMA 资源 | 可以用父驱动统一管理,再分发给子设备 |
17. 最后用一句话总结
父设备驱动创建子设备,就是 Linux 先发现一个“大设备”,大设备驱动初始化后,再把它内部或下游的“小设备”注册到内核设备模型中,让这些小设备继续匹配自己的驱动。
对你的 PCIe FPGA 来说,可以记成:
PCIe 枚举只能发现 FPGA 这个 Endpoint;
FPGA 里面有什么功能,Linux 不一定天然知道;
父 PCIe 驱动 probe 后,可以根据 BAR/固件/设备树/硬件版本,
把 DMA、I2C、GPIO、SPI 等功能注册成子设备;
Linux 再分别调用这些子设备驱动的 probe()。
最核心的思维链是:
父设备被发现
↓
父驱动 probe()
↓
父驱动解析/初始化硬件内部结构
↓
父驱动注册子设备
↓
Linux driver core 匹配子驱动
↓
子驱动 probe()
↓
各子系统分别管理各自功能
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
为什么要注册成标准的IIC驱动?linux知道和不知道有啥区别吗?
可以按照标准的设备驱动总线模型访问外部的设备。对外统一了访问接口,与linux驱动进行了统一,使得I2C的控制器不论在LINUX还是FPGA,上层控制I2C的函数都是一致的。
Linux“知道”以后,并不是 Linux 自动会控制 FPGA 里的 I2C 控制器,而是你写的 FPGA-I2C adapter driver 把 Linux 标准 I2C 操作接口,适配/桥接到 PCIe BAR 寄存器操作流程上。
也就是:
Linux 标准 I2C 流程
↓
i2c_transfer() / i2c_smbus_xxx()
↓
Linux I2C core
↓
你的 fpga_i2c_master_xfer()
↓
PCIe BAR readl/writel
↓
FPGA I2C 寄存器
↓
FPGA I2C 状态机
↓
SCL/SDA
你的这句话可以改得更准确:
Linux 知道后,就可以把原本面向标准 I2C adapter 的上层接口,接到 FPGA-I2C adapter 上;而这个 adapter driver 作为适配层,把标准 I2C 消息转换成对 PCIe BAR 空间 I2C 寄存器的读写。
1. “原有操作 Linux 本身 IIC 控制器的接口”这句话稍微修正一下
不是“Linux 本身的 I2C 控制器”。
更准确是:
Linux I2C core 本来就有一套统一接口,
各种 I2C 控制器驱动都要适配这套接口。
比如:
SoC I2C 控制器驱动
USB-I2C 转接器驱动
GPIO bit-bang I2C 驱动
PCIe-FPGA-I2C 驱动
它们底层硬件都不一样,但上层都注册成:
struct i2c_adapter
并实现:
master_xfer()
所以你的 FPGA-I2C 也是其中一种 I2C adapter。
2. 适配层到底适配了什么?
Linux I2C core 给你的请求是标准形式:
struct i2c_msg {
addr;
flags;
len;
buf;
};
比如读一个 I2C 设备:
addr = 0x48
flags = I2C_M_RD
len = 2
buf = 接收缓冲区
你的适配层要把它翻译成 FPGA BAR 操作:
writel(0x48, I2C_ADDR)
writel(2, I2C_LEN)
writel(READ | START | STOP, I2C_CTRL)
轮询 I2C_STATUS.DONE
readl(I2C_RXDATA)
readl(I2C_RXDATA)
所以适配层做的是:
标准 I2C message
→ FPGA 私有寄存器操作
这就是你说的“桥接”。
3. 标准流程和 BAR 流程的对应关系
| Linux 标准 I2C 流程 | FPGA BAR 寄存器流程 |
|---|---|
i2c_transfer() |
调用你的 master_xfer() |
struct i2c_msg.addr |
写 I2C_ADDR |
struct i2c_msg.flags |
决定写 I2C_CTRL.READ/WRITE |
struct i2c_msg.len |
写 I2C_LEN |
msg->buf 写数据 |
写 I2C_TXDATA |
msg->buf 读数据 |
读 I2C_RXDATA |
| 等待传输完成 | 轮询/中断等待 I2C_STATUS.DONE |
| NACK / timeout | 转换成 -ENXIO / -ETIMEDOUT |
4. 所以最终可以这样理解
你的理解可以总结成一句非常准确的话:
注册
i2c_adapter后,Linux 上层仍然走标准 I2C 调用路径;你的 FPGA-I2C adapter driver 作为桥接层,把这些标准 I2C 调用翻译成 PCIe BAR 寄存器读写;FPGA 内部 I2C 控制器再根据这些寄存器产生真实 I2C 时序。
也就是:
标准 I2C 软件接口
↓ 适配
PCIe BAR 寄存器接口
↓ 控制
FPGA I2C 硬件状态机
↓ 驱动
真实 I2C 总线
重点记住:
标准的是 Linux 上层接口,不标准的是 FPGA 寄存器;adapter driver 的作用,就是把两者接起来。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)