父设备驱动创建子设备”可以理解成:

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 的作用,就是把两者接起来。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐