
实战怎么把设备树和 /dev 节点真正连起来一、主线内核的“水土不服”拿到一块新的 ARM SoC 评估板烧录厂商提供的 BSP 镜像外设一切正常。但当你把内核升级到主线版本或者换了一块定制载板SPI 控制器不响应、I2C 设备探测不到、GPIO 中断进不来——这些问题不是偶发而是必然。原因很简单主线内核的驱动框架是通用的它不认识你的定制硬件。BSP 移植的核心就是在通用内核驱动和特定硬件之间建立映射。这个映射的桥梁是设备树Device Tree执行的载体是平台驱动Platform Driver。设备树描述“硬件长什么样”驱动代码实现“硬件怎么用”。本文以 SPI 控制器驱动移植为例聊聊从设备树编写到驱动加载、从 /dev 节点创建到用户态访问的全链路。二、内核是怎么找到你的硬件的内核启动时设备树被解析为扁平化的设备节点树。每个节点包含compatible属性、寄存器地址、中断号、时钟等硬件描述。平台总线的匹配规则其实就三步驱动注册时声明of_device_id表列出支持的compatible字符串。设备树节点中的compatible属性与驱动的of_device_id逐一比对。匹配成功后内核调用驱动的probe函数传入platform_device结构体。sequenceDiagram participant DTB as 设备树 Blob participant OF as OF 解析器 participant BUS as 平台总线 participant DRV as 平台驱动 participant DEV as /dev 节点 DTB-OF: 内核启动解析 DTB OF-BUS: 注册 platform_device BUS-DRV: compatible 匹配 DRV-DRV: probe() 执行 DRV-DRV: ioremap 映射寄存器 DRV-DRV: request_irq 注册中断 DRV-DEV: device_create 创建字符设备 DEV-DEV: 用户态 open/read/write关键数据结构的关系platform_device持有设备树解析出的资源内存、中断、时钟platform_driver持有操作方法集probe、remove、suspend、resume。两者通过compatible字符串绑定而非硬编码地址——这是内核驱动模型与裸机编程的根本区别。三、SPI 平台驱动实战寄存器映射到用户态接口以下代码实现一个基于 i.MX 系列 ECSPI 控制器的平台驱动包含完整的错误处理和资源管理。#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_device.h #include linux/io.h #include linux/clk.h #include linux/interrupt.h #include linux/cdev.h #include linux/fs.h #include linux/uaccess.h #include linux/mutex.h #define ECSPI_RXDATA 0x00 #define ECSPI_TXDATA 0x04 #define ECSPI_CONREG 0x08 #define ECSPI_CONFIGREG 0x0C #define ECSPI_STATREG 0x18 /* CONREG 位域定义 */ #define CONREG_ENABLE BIT(0) #define CONREG_XCH BIT(2) #define CONREG_SMC BIT(3) #define CONREG_BURST_LEN_SHIFT 20 struct ecspi_priv { void __iomem *base; /* 映射后的寄存器虚拟地址 */ struct clk *clk_ipg; /* IPG 时钟 */ struct clk *clk_per; /* PER 时钟 */ int irq; /* 中断号 */ struct cdev cdev; /* 字符设备 */ dev_t devt; /* 设备号 */ struct device *dev; /* 设备结构体 */ struct mutex buf_lock; /* 并发访问保护 */ wait_queue_head_t rx_wait;/* 接收完成等待队列 */ bool rx_done; /* 接收完成标志 */ u32 rx_data; /* 接收数据缓存 */ }; static irqreturn_t ecspi_irq_handler(int irq, void *dev_id) { struct ecspi_priv *priv dev_id; u32 stat; stat readl(priv-base ECSPI_STATREG); if (stat BIT(3)) { /* RX FIFO 非空中断 */ priv-rx_data readl(priv-base ECSPI_RXDATA); priv-rx_done true; wake_up_interruptible(priv-rx_wait); /* 清除中断标志写 1 清零 */ writel(stat, priv-base ECSPI_STATREG); } return IRQ_HANDLED; } static int ecspi_transfer_one(struct ecspi_priv *priv, u32 tx_val) { u32 conreg; int ret; mutex_lock(priv-buf_lock); priv-rx_done false; /* 写入发送数据 */ writel(tx_val, priv-base ECSPI_TXDATA); /* 启动交换置位 XCH 位硬件自动完成全双工收发 */ conreg readl(priv-base ECSPI_CONREG); conreg | CONREG_XCH; writel(conreg, priv-base ECSPI_CONREG); /* 等待接收完成超时 100ms 防止硬件卡死 */ ret wait_event_interruptible_timeout( priv-rx_wait, priv-rx_done, msecs_to_jiffies(100) ); mutex_unlock(priv-buf_lock); if (ret 0) { dev_err(priv-dev, SPI 传输超时或被中断\n); return -ETIMEDOUT; } return 0; } static ssize_t ecspi_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { struct ecspi_priv *priv filp-private_data; u32 tx_val; int ret; if (count sizeof(u32)) { /* SPI 按字传输不足 4 字节拒绝操作 */ return -EINVAL; } if (copy_from_user(tx_val, buf, sizeof(u32))) return -EFAULT; ret ecspi_transfer_one(priv, tx_val); if (ret) return ret; return sizeof(u32); } static ssize_t ecspi_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { struct ecspi_priv *priv filp-private_data; if (count sizeof(u32)) return -EINVAL; if (copy_to_user(buf, priv-rx_data, sizeof(u32))) return -EFAULT; return sizeof(u32); } static int ecspi_open(struct inode *inode, struct file *filp) { struct ecspi_priv *priv container_of(inode-i_cdev, struct ecspi_priv, cdev); filp-private_data priv; return 0; } static const struct file_operations ecspi_fops { .owner THIS_MODULE, .open ecspi_open, .read ecspi_read, .write ecspi_write, }; static int ecspi_probe(struct platform_device *pdev) { struct ecspi_priv *priv; struct resource *res; int ret; priv devm_kzalloc(pdev-dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv-dev pdev-dev; mutex_init(priv-buf_lock); init_waitqueue_head(priv-rx_wait); /* 获取并映射寄存器区域devm_ioremap_resource * 自动处理请求内存区域和映射失败时自动释放 */ res platform_get_resource(pdev, IORESOURCE_MEM, 0); priv-base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(priv-base)) return PTR_ERR(priv-base); /* 获取中断号设备树中 interrupts 属性解析 */ priv-irq platform_get_irq(pdev, 0); if (priv-irq 0) return priv-irq; ret devm_request_irq(pdev-dev, priv-irq, ecspi_irq_handler, IRQF_TRIGGER_NONE, dev_name(pdev-dev), priv); if (ret) { dev_err(pdev-dev, 中断注册失败: %d\n, ret); return ret; } /* 获取并使能时钟ECSPI 需要两个时钟域 */ priv-clk_ipg devm_clk_get(pdev-dev, ipg); if (IS_ERR(priv-clk_ipg)) return PTR_ERR(priv-clk_ipg); priv-clk_per devm_clk_get(pdev-dev, per); if (IS_ERR(priv-clk_per)) return PTR_ERR(priv-clk_per); ret clk_prepare_enable(priv-clk_ipg); if (ret) return ret; ret clk_prepare_enable(priv-clk_per); if (ret) goto disable_ipg; /* 配置 ECSPI 控制器8-bit 模式主模式 */ writel(CONREG_ENABLE | CONREG_SMC | (7 CONREG_BURST_LEN_SHIFT), priv-base ECSPI_CONREG); /* 注册字符设备 */ ret alloc_chrdev_region(priv-devt, 0, 1, ecspi_custom); if (ret) goto disable_clks; cdev_init(priv-cdev, ecspi_fops); priv-cdev.owner THIS_MODULE; ret cdev_add(priv-cdev, priv-devt, 1); if (ret) goto unregister_chrdev; /* 创建 /dev 节点用户态通过 /dev/ecspi0 访问 */ priv-dev device_create(class_create(THIS_MODULE, ecspi_custom), pdev-dev, priv-devt, priv, ecspi0); if (IS_ERR(priv-dev)) { ret PTR_ERR(priv-dev); goto del_cdev; } platform_set_drvdata(pdev, priv); dev_info(pdev-dev, ECSPI 驱动加载成功主设备号: %d\n, MAJOR(priv-devt)); return 0; del_cdev: cdev_del(priv-cdev); unregister_chrdev: unregister_chrdev_region(priv-devt, 1); disable_clks: clk_disable_unprepare(priv-clk_per); disable_ipg: clk_disable_unprepare(priv-clk_ipg); return ret; } static int ecspi_remove(struct platform_device *pdev) { struct ecspi_priv *priv platform_get_drvdata(pdev); device_destroy(priv-dev-class, priv-devt); cdev_del(priv-cdev); unregister_chrdev_region(priv-devt, 1); clk_disable_unprepare(priv-clk_per); clk_disable_unprepare(priv-clk_ipg); return 0; } static const struct of_device_id ecspi_of_match[] { { .compatible fsl,imx6ul-ecspi }, { .compatible fsl,imx8mm-ecspi }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, ecspi_of_match); static struct platform_driver ecspi_driver { .probe ecspi_probe, .remove ecspi_remove, .driver { .name ecspi_custom, .of_match_table ecspi_of_match, }, }; module_platform_driver(ecspi_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(embedded-dev); MODULE_DESCRIPTION(i.MX ECSPI 平台驱动);对应的设备树节点ecspi1 { #address-cells 1; #size-cells 0; fsl,spi-num-chipselect 1; cs-gpios gpio4 26 GPIO_ACTIVE_LOW; status okay; spidev0: spi0 { compatible fsl,imx6ul-ecspi; reg 0; spi-max-frequency 10000000; interrupts GIC_SPI 31 IRQ_TYPE_LEVEL_HIGH; clocks clk IMX6UL_CLK_ECSPI1, clk IMX6UL_CLK_ECSPI1; clock-names ipg, per; }; };四、那些代码里看不见的坑时钟、引脚与中断驱动代码能编译通过不代表硬件能工作。BSP 移植中有三个“隐性依赖”经常被忽略。时钟树未打通。i.MX 系列的时钟控制器CCM需要正确配置时钟源、分频和门控。设备树中clocks属性指向的时钟节点如果未在 CCM 驱动中注册clk_get会返回 -ENOENT。调试方法cat /sys/kernel/debug/clk/clk_summary确认目标外设时钟已使能且频率正确。引脚复用冲突。同一个 SoC 引脚可能被 SPI、UART、PWM 共享。设备树中pinctrl-0属性指定的引脚配置必须与原理图一致。如果两个设备节点声明了同一引脚的不同复用功能后加载的驱动会覆盖前者的配置。排查手段cat /sys/kernel/debug/pinctrl/pinctrl-maps。中断级联与亲和性。多个外设共享一个 SPI 中断号时内核需要在中断处理函数中做软件分派。如果设备树中中断号写错或触发类型不匹配中断要么永远不触发要么反复误触发导致 CPU 占满。验证方法cat /proc/interrupts观察中断计数是否随预期事件增长。flowchart TD A[驱动加载失败] -- B{dmesg 报什么错} B --|ENOENT| C[时钟节点未注册] B --|EBUSY| D[引脚复用冲突] B --|EINVAL| E[设备树属性缺失] C -- F[检查 CCM 驱动与 DT 时钟节点] D -- G[检查 pinctrl-maps 与原理图] E -- H[对照 SoC Datasheet 补全属性] A -- I[驱动加载成功但无响应] I -- J{中断计数是否增长} J --|否| K[检查 IRQ 号与触发类型] J --|是| L[检查寄存器配置与时序]还有一个容易被忽视的问题内核版本差异。主线 5.15 与厂商 4.14 的驱动 API 存在大量不兼容——clk_get的参数签名变了、dmaengine_prep_slave_sg的返回值类型改了、of_device_id的匹配优先级调整了。移植时必须逐个 API 核对目标内核的头文件不能照搬旧版代码。五、一些经验之谈BSP 移植不是“改改设备树就能跑”的简单工作而是一个需要硬件原理图、SoC 手册、内核源码三方交叉验证的系统工程。硬件确认先行对照原理图确认引脚复用、时钟源、中断号再动手写设备树。时钟树验证驱动加载后第一时间检查clk_summary确认外设时钟已使能。中断验证通过/proc/interrupts确认中断触发与计数排除中断级联问题。寄存器直读用devmem工具直接读取控制器寄存器确认配置值与预期一致。版本适配逐 API 核对目标内核版本的头文件不照搬旧版驱动代码。驱动移植的可靠性取决于对硬件细节的掌握程度。跳过任何一个验证步骤都可能埋下难以复现的偶发故障。