父设备驱动创建子设备 “父设备驱动创建子设备”可以理解成Linux 一开始只能发现一个“大设备”等这个大设备的驱动probe()跑起来以后父驱动再告诉内核“我内部/下游还有几个小设备请把它们也注册出来并让各自的驱动去匹配。”这类情况不是一次性发现所有硬件而是分层发现。1. 为什么会有“父设备”和“子设备”因为很多硬件不是一个简单设备而是一个“设备里面套设备”。比如PCIe FPGA 板卡 ├── BAR0 控制寄存器 ├── DMA Engine ├── 内部 I2C Controller │ ├── EEPROM │ └── 温度传感器 ├── 内部 SPI Controller │ └── SPI Flash └── 网络 MAC └── 外部 PHYLinux 一开始通过 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_clientSPI bus driver 会注册spi_device子设备。(内核文档)2. 普通发现 vs 父驱动创建子设备先对比一下。情况 A设备直接被总线发现比如 PCIe FPGAPCIe 枚举 ↓ 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 FPGAplatform_deviceplatform_driverPCIe FPGA 内部 I2C 控制器i2c_adapter然后创建i2c_clienti2c_driverSoC SPI 控制器spi_devicespi_driverMFD 多功能芯片多个platform_device多个platform_driver网卡 MACMDIO bus 上的 PHY devicePHY driver复杂 PCIe 设备auxiliary_deviceauxiliary_driverMFD 是典型例子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你的父驱动是structpci_driverfpga_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 驱动大概是这样staticintfpga_pci_probe(structpci_dev*pdev,conststructpci_device_id*id){structfpga_priv*priv;privdevm_kzalloc(pdev-dev,sizeof(*priv),GFP_KERNEL);pci_enable_device(pdev);pci_request_regions(pdev,fpga-pcie);priv-bar0pci_iomap(pdev,0,0);pci_set_drvdata(pdev,priv);/* * 这里开始创建子设备 */fpga_register_dma_child(priv);fpga_register_i2c_child(priv);fpga_register_gpio_child(priv);return0;}注意重点父驱动不是直接调用子驱动 probe()而是父驱动注册一个 child device Linux driver core 再去匹配对应 child driver 匹配成功后才调用 child driver 的 probe()这和 PCIe 枚举后匹配pci_driver是同一个思想只是发现动作从“PCI core”变成了“父驱动”。7. 子设备如何拿到父设备资源这是你最容易卡住的地方。子设备本身可能没有真实独立的 PCIe BAR。比如fpga-i2c的寄存器其实在父设备 BAR0 的某个 offsetBAR0 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 0x10008. 和设备树里的 child node 有什么关系有些子设备不是父驱动“凭空决定”的而是设备树提前描述了层级关系。例如fpga_pcie0 { compatible mycompany,fpga-pcie; i2c2000 { compatible mycompany,fpga-i2c; reg 0x2000 0x1000; temp48 { compatible ti,tmp102; reg 0x48; }; }; gpio3000 { compatible mycompany,fpga-gpio; reg 0x3000 0x1000; }; };Linux 不是一定启动时就把所有层级都变成独立设备。对于某些 bus 类型子设备通常要等父 bus/controller driverprobe()后注册出来。Linux 设备树文档明确提到I2C bus driver 会为每个 child node 注册i2c_clientSPI bus driver 会注册spi_devicechildren。(内核文档)所以设备树只是“描述硬件结构”。真正把子设备注册到 Linux 设备模型里的通常还是父设备驱动或对应 bus core。9. 例子一I2C 控制器创建 I2C 子设备这是最经典的父子设备模型。硬件结构SoC / FPGA └── I2C Controller ├── EEPROM 0x50 └── 温度传感器 0x48Linux 过程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 buttonLinux 不能只把它当一个普通 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 PHYMAC 可能是 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_inputGPIO 接入 gpiolib 后用户态或内核其他驱动可以按 GPIO 框架使用。SPI Flash 接入 MTD 后可以按 MTD 分区管理。这比你在父驱动里自己造一堆/dev/fpga_temp、/dev/fpga_gpio、/dev/fpga_flash更规范。15. 对你做 PCIe FPGA DMA 的实际启发如果你的 FPGA 只是一个简单 DMA EndpointPCIe 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 驱动它们底层硬件都不一样但上层都注册成structi2c_adapter并实现master_xfer()所以你的 FPGA-I2C 也是其中一种 I2C adapter。2. 适配层到底适配了什么Linux I2C core 给你的请求是标准形式structi2c_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_ADDRstruct i2c_msg.flags决定写I2C_CTRL.READ/WRITEstruct i2c_msg.len写I2C_LENmsg-buf写数据写I2C_TXDATAmsg-buf读数据读I2C_RXDATA等待传输完成轮询/中断等待I2C_STATUS.DONENACK / timeout转换成-ENXIO/-ETIMEDOUT4. 所以最终可以这样理解你的理解可以总结成一句非常准确的话注册i2c_adapter后Linux 上层仍然走标准 I2C 调用路径你的 FPGA-I2C adapter driver 作为桥接层把这些标准 I2C 调用翻译成 PCIe BAR 寄存器读写FPGA 内部 I2C 控制器再根据这些寄存器产生真实 I2C 时序。也就是标准 I2C 软件接口 ↓ 适配 PCIe BAR 寄存器接口 ↓ 控制 FPGA I2C 硬件状态机 ↓ 驱动 真实 I2C 总线重点记住标准的是 Linux 上层接口不标准的是 FPGA 寄存器adapter driver 的作用就是把两者接起来。