新闻详情

新闻详情

首页 / 资讯中心 / 详情

i.MX6ULL Platform驱动匹配机制详解:设备树与probe调试

发布时间:2026/9/9 1:44:52来源:尧图网络
i.MX6ULL Platform驱动匹配机制详解:设备树与probe调试
直接说结论i.MX6ULL 的 Linux 驱动开发绕不开 Platform 机制。不管你是刚入门嵌入式 Linux 的新手还是在应用层写了好几年逻辑、现在想往下沉搞内核驱动的人只要你碰过设备树、碰过 GPIO、碰过 I2C 控制器驱动你其实都已经在和 Platform 总线打交道了。但很多人会卡在同一个问题上设备树里明明写了节点驱动也编进去了为什么 probe 就是不执行为什么匹配不上这篇文章我不打算照搬内核文档而是基于我自己在 i.MX6ULL 上跑通 Platform 设备与驱动匹配的完整过程把它掰开揉碎讲清楚。内容包括为什么需要 Platform 这个虚拟总线、设备侧怎么注册、驱动侧怎么匹配、匹配的优先级与源码实现、设备树在匹配过程中到底扮演什么角色以及最后我踩过的那些坑。1. 内容整体设计与思路拆解1.1 为什么 Linux 需要一条“虚拟总线”如果只看教科书你会觉得设备模型挺抽象。但换一个角度这事特别像公司里的人员分配机制。CPU 这边有大量的“硬件资源”寄存器地址、中断号、时钟、GPIO 编号。而驱动就是一个一个“岗位”需要有人认领这些资源来干活。问题是这些硬件并不像 PCI 设备那样天然自带厂商 ID、设备 ID插到总线上就能被枚举出来。SoC 内部集成的外设比如 i.MX6ULL 上的 UART、I2C、SPI、GPIO 控制器它们不是可插拔的地址是固定的中断是固定的没有标准枚举过程。那内核怎么知道哪个驱动对应哪个设备靠的不是硬件总线而是一种软件抽象也就是 Platform 总线。它把所有“挂不到真实总线上的设备”统一挂到一个虚拟总线上管理让设备侧和驱动侧通过匹配规则互相找到对方。1.2 i.MX6ULL 场景下 Platform 机制的核心作用i.MX6ULL 是 NXP 推出的 Cortex-A7 内核应用处理器在工业控制、物联网网关、带屏显的人机交互设备里用得非常多。它的外设资源不算特别复杂但足够典型。你写一个 LED 驱动本质上就是先定义一个 platform_driver然后在设备树里加一个 compatible 匹配节点内核启动的时候把设备和驱动配到一起最终调用驱动里的 probe 函数你在 probe 里做寄存器映射、GPIO 申请、初始化硬件。所以理解 Platform 机制实际上是在理解整个 Linux 设备驱动模型的基石。你在 i.MX6ULL 上跑通一个最简单的 Platform 驱动之后回头再看 I2C 驱动、SPI 驱动、串口驱动会发现套路几乎一样设备侧描述有什么资源驱动侧声明能处理什么设备总线负责牵线搭桥。1.3 为什么我选择从“匹配机制”切入因为“失败”总是发生在匹配阶段。很多人设备树节点写对了驱动代码看着也没问题但 probe 就是不调用。原因往往就是对匹配机制理解不透。到底是看 compatible 字符串还是看 platform_driver 里的 name 字段还是看 id_table优先级是怎么排的设备树里的 status disabled 会不会导致匹配失败如果你不把这个机制吃透就只能靠瞎试运气好试出来了换个平台又懵了。这篇文章就是要把匹配机制这一层彻底掀开从数据结构、注册流程、源码逻辑三个维度讲透最后配上我在 i.MX6ULL 上的实操记录。2. 核心细节解析与实操要点2.1 设备侧与驱动侧的核心数据结构先看设备侧。传统写法非设备树下你要定义一个 platform_device 结构体里面包含设备名、ID、资源数组等。但现代内核开发里尤其是 i.MX6ULL 这种使用设备树的内核4.x 之后你几乎不需要自己构造 platform_device因为内核在启动阶段会解析 DTB 文件把每一个 compatible 匹配的节点转换成 struct platform_device 结构体挂到 platform 总线上。从设备树转换来的 platform_device 结构体中关键字段包括name优先取设备树节点的 name 属性如果没有就取 node name。num_resources设备树节点里 reg、interrupts 属性会被解析成资源。resource资源数组包含了 IO 内存起始地址、结束地址、中断号等。dev.of_node指向设备树节点的指针驱动里常用of_*系列 API 去读取额外属性。再看驱动侧。你要定义一个 platform_driver 结构体核心字段是struct platform_driver { int (*probe)(struct platform_device *); int (*remove)(struct platform_device *); void (*shutdown)(struct platform_device *); int (*suspend)(struct platform_device *, pm_message_t state); int (*resume)(struct platform_device *); struct device_driver driver; const struct platform_device_id *id_table; ... };平时写驱动最主要的工作是填充driver.name、driver.of_match_table、probe和remove。driver.name用于和传统 platform_device 的 name 字段匹配of_match_table用于和设备树节点的 compatible 属性匹配。2.2 driver_register 与 device_register 的注册路径理解匹配之前先搞清楚注册路径。驱动侧你调用platform_driver_register(led_driver)最终会走到__platform_driver_register再调用driver_register。这里有一个关键动作driver_register 会触发总线上的 device 遍历。也就是说驱动注册的时候内核会遍历 platform 总线上已经注册的所有 platform_device逐个尝试匹配。如果匹配上立刻调用对应的 probe。设备侧设备树方式下内核在of_platform_bus_probe或of_platform_populate阶段把设备树节点一个个变成 platform_device 并注册到 platform 总线上。设备注册的时候同样会触发总线上已有驱动的匹配。这说明匹配动作是双向触发的不管哪一侧先注册只要另一侧注册进来总线就会做一次全量匹配。这个设计很重要也是理解“为什么设备树节点存在但 probe 没执行”的关键前提之一。2.3 i.MX6ULL 上写一个 Platform 驱动的最小框架在 i.MX6ULL 上代码框架大致是这样#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_device.h static int led_probe(struct platform_device *pdev) { struct resource *res; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) { dev_err(pdev-dev, no memory resource found\n); return -ENODEV; } dev_info(pdev-dev, led probe success, reg 0x%llx\n, (unsigned long long)res-start); return 0; } static int led_remove(struct platform_device *pdev) { dev_info(pdev-dev, led remove\n); return 0; } static const struct of_device_id led_of_match[] { { .compatible mycompany,board-led }, { } }; MODULE_DEVICE_TABLE(of, led_of_match); static struct platform_driver led_driver { .probe led_probe, .remove led_remove, .driver { .name board-led, .of_match_table led_of_match, }, }; module_platform_driver(led_driver); MODULE_LICENSE(GPL);注意module_platform_driver这个宏它展开后相当于module_init调用platform_driver_registermodule_exit调用platform_driver_unregister。别自己手写 init 和 exit容易漏掉错误处理。设备树部分在根节点下加一个子节点例如iomuxc { pinctrl_led: ledgrp { fsl,pins MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10b0 ; }; }; / { led { compatible mycompany,board-led; pinctrl-names default; pinctrl-0 pinctrl_led; reg 0x020c406c 0x4; status okay; }; };编译 DTB、烧录、加载驱动后如果一切正常控制台会打印led probe success, reg 0x20c406c2.4 匹配过程的核心优先级内核里匹配 platform 设备和驱动的核心函数是platform_match我直接说结论匹配顺序是首先尝试of_driver_match_device即用设备树节点的 compatible 属性去匹配驱动里的of_match_table。只要驱动填充了of_match_table而且设备是设备树节点转换来的优先走这条路径。如果上一步不成功再尝试acpi_driver_match_deviceARM 嵌入式场景基本用不到。如果还不成功尝试platform_match_id即用platform_device的 name 字段去匹配platform_driver的id_table数组中每一项的 name 字段。如果 id_table 为空或者没匹配上最后尝试platform_match_name即直接比较platform_device的 name 字段和platform_driver的driver.name字段。这里有一个特别容易让人误解的点很多人以为设备树节点里的 compatible 会和driver.name比较其实不会。compatible 只匹配of_match_table。如果驱动只设置了driver.name而没有设置id_table你要保证设备侧的 name 和它一致。但在设备树方式下设备侧 name 最终来源于设备树节点的 name 属性通常取 node name不是你随便定的 compatible 字符串。所以最稳妥的做法是驱动里同时填好driver.name和of_match_table。设备树里 compatible 写清楚。这样不管走哪条匹配路径都能兜底。3. 实操过程与匹配机制的源码级验证3.1 调试环境与准备工作我用的开发板是正点原子 i.MX6ULL 阿尔法内核版本 4.1.15交叉编译工具链为 arm-linux-gnueabihf-gcc。宿主机是 Ubuntu 18.04通过 NFS 挂载根文件系统驱动编译成模块.ko后拷贝到板子上加载方便调试。不建议一开始就把驱动编进内核里每次改代码都得重新烧镜像太浪费时间。编译成模块配合insmod、rmmod、dmesg循环调试效率高出不少。交叉编译环境准备好以后可以写一个 Makefileobj-m : led_platform.o KERNELDIR : /home/xxx/linux-imx-rel_imx_4.1.15_2.1.0_ga PWD : $(shell pwd) all: $(MAKE) -C $(KERNELDIR) M$(PWD) ARCHarm CROSS_COMPILEarm-linux-gnueabihf- modules clean: $(MAKE) -C $(KERNELDIR) M$(PWD) ARCHarm CROSS_COMPILEarm-linux-gnueabihf- clean注意 KERNELDIR 要指向你已经编译过的内核源码目录而且这个源码目录必须已经完成了内核编译生成了 Module.symvers 等文件否则编译模块时会出现找不到头文件或者版本信息不一致的问题。3.2 匹配过程源码追踪我建议你亲手打开内核源码追踪一遍路径是drivers/base/platform.c里的platform_match函数。static int platform_match(struct device *dev, struct device_driver *drv) { struct platform_device *pdev to_platform_device(dev); struct platform_driver *pdrv to_platform_driver(drv); /* Attempt an OF style match first */ if (of_driver_match_device(dev, drv)) return 1; /* Then try ACPI style match */ if (acpi_driver_match_device(dev, drv)) return 1; /* Then try to match against the id table */ if (pdrv-id_table) return platform_match_id(pdev, pdrv-id_table) ! NULL; /* fall-back to driver name match */ return (strcmp(pdev-name, drv-name) 0); }你看到这段代码之后就会明白of_driver_match_device是绝对的第一优先级。它内部调用of_match_device最终把设备节点的 compatible 属性和驱动of_match_table里的 compatible 逐个比较。字符串完全相等才算匹配。of_driver_match_device内部还会调用of_match_device它会尝试匹配of_match_table中每一个of_device_id。of_device_id结构体里除了 compatible还有 type 和 name 字段分别对应设备树节点的device_type和name属性。但现在的设备树基本都不写device_type了所以实际上就是 compatible 的字符串比较。这解释了为什么我强调 compatible 值必须一字不差。多一个空格、大小写不一致匹配都会失败而且内核不会给出任何直观的报错只会静默地不调用 probe。3.3 从设备树节点到 platform_device 的转换历程还有一个值得关注的点设备树节点到底是怎么变成 platform_device 的。在 i.MX6ULL 的启动阶段内核会调用of_platform_populate进行设备树节点的 platform 设备创建。它的逻辑是遍历设备树根节点下所有 compatible 为simple-bus或类似总线节点的子节点将子节点一个个转换为 platform_device。非 simple-bus 节点则根据具体驱动框架决定是否创建 platform_device。比如你根节点下直接挂一个led节点根节点/本身就是一个 platform 总线宿主的展开点所以 led 节点会被正常转换为 platform_device。如果你把 led 节点放在某个 I2C 控制器的子节点里那它就不会被转换为 platform_device而是由 I2C 核心框架处理生成 i2c_client走的是另一套匹配机制。这也是很多新手感到困惑的地方同样写一个 compatible 节点放在根节点下面 probe 能执行放在别的节点下面就不行。原因就在于设备树节点拓扑决定了你会用哪套总线机制。3.4 实际跑通的时序观察为了验证注册顺序我在驱动里加了module_init打印然后设备树里把节点状态设为status okay。开机后内核先完成设备树解析led 节点先被注册为 platform_device。此时我还没有 insmod 驱动模块所以设备挂在总线上没有驱动认领。等系统启动到用户态我执行insmod led_platform.ko控制台立刻打印出led_probe called, matched compatible mycompany,board-led这说明驱动注册的瞬间内核遍历了总线上已经存在的 platform_device找到了 compatible 匹配的设备立刻调用了 probe。反过来如果我先把驱动模块加载进去再用某种方式动态创建 platform_device比如写一个测试模块调用platform_device_register同样会在设备注册的瞬间触发 probe。这一点在实际开发中非常有用不要以为 probe 只会在开机阶段调用。比如你用mdev或udev做设备节点管理驱动加载时机和设备注册时机不同最终都不影响匹配结果。3.5 匹配成功后的资源获取方式匹配成功之后probe 函数里最重要的事情之一就是获取硬件资源。设备树里写的 reg 属性、interrupts 属性都会被转换成struct resource放在platform_device的资源数组里。读取方式有两种。第一种用platform_get_resourcestruct resource *res; res platform_get_resource(pdev, IORESOURCE_MEM, 0);设备树里写了几个 reg资源索引号就从 0 开始递增。比如reg 0x020c406c 0x4, 0x020e0100 0x4;第一个 reg 对应索引 0起始地址 0x020c406c长度 0x4第二个 reg 对应索引 1。第二种方式用设备树 API 直接读取属性。比如你要读clock-frequency、gpio这类自定义属性使用of_property_read_u32、of_get_named_gpio等。这种方式是没有 reg 属性时的常见路径。特别注意platform_get_resource 只能拿到标准资源比如 reg、interrupts。如果你需要读取 pinctrl-0 这种属性或者某个定量参数它是拿不到的必须用 of API。4. 常见问题与排查技巧实录4.1 设备和驱动都注册了但 probe 始终不打印这是我被问到最多的问题。排查路径按优先级走检查设备树节点状态status disabled的节点不会生成 platform_device。你要么删掉 status 属性要么显式写成status okay。检查 compatible 字符串是否完全一致。建议在驱动和设备树里把字符串复制出来用grep对比。检查内核是否真的加载了你修改后的 DTB。uboot 里有没有设置正确的fdt_file环境变量很多板子默认加载的是旧 DTB你以为改了实际上没生效。模块加载后去/sys/bus/platform/devices/下看有没有对应设备目录。如果有说明设备已注册。再看/sys/bus/platform/drivers/下有没有你的驱动目录。如果两者都存在但 probe 没打印大概率是匹配字符串的问题。一个快速验证方法在驱动里临时把of_match_table删掉只保留driver.name然后把设备树节点的 name 设置为与driver.name相同。如果此时 probe 执行了说明 compatible 匹配路径出了问题。4.2 编译模块时报错Unknown symbol 或版本 magic 不一致这个属于环境问题。最常见原因是模块使用内核源码编译但开发板运行的内核镜像不是由这份源码编译出来的。内核模块对内核版本和 vermagic 有严格检查。解决方法是确保交叉编译使用的内核源码目录和你烧录到板子上的 zImage 来源于同一套代码并且在你开始编译模块之前内核源码目录已经完整编译过一次。最好连设备树也重新编译烧录保证整体一致性。还有一种情况内核源码目录之前编译过但你后来改了内核配置比如改了 CONFIG_LOCALVERSION导致 vermagic 变了。直接执行make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- modules_prepare重新生成模块相关的头文件和符号信息。4.3 同一 compatible 被多个节点使用时如何区分设备有时候你只有一个驱动但板子上有多个同类型设备比如两个 LED、四路继电器。设备树里写两个节点compatible 相同这时候 probe 会被调用两次每次传入的 pdev 指向不同设备。要想区分它们可以用struct device里的id或者在设备树节点里添加自定义属性在 probe 里读取。我常用方法是在设备树里加一个reg或者label属性来区分。led0 { compatible mycompany,board-led; reg 0; label led0; }; led1 { compatible mycompany,board-led; reg 1; label led1; };在 probe 里u32 id; of_property_read_u32(pdev-dev.of_node, reg, id);这样就可以根据 id 初始化不同 GPIO 引脚或者控制不同外设。4.4 热拔插场景下 remove 不执行Platform 机制在 i.MX6ULL 这种嵌入式平台上设备基本是固定不动的一般不涉及热拔插。但如果你的平台支持设备树 overlay动态加载设备树片段那么卸载 overlay 时会触发 remove 调用。实际调试发现如果 remove 函数执行过程中出错比如 iounmap 的地址不对内核可能会报 stack trace但系统不一定崩溃。所以 remove 函数里要释放干净释放申请的中断、释放 GPIO、iounmap、释放内存等。4.5 debugfs 与 sysfs 辅助调试技巧除了看 dmesg我强烈建议利用 sysfs 来辅助排查。设备注册成功后在/sys/bus/platform/devices/下会存在设备目录。你可以查看ls /sys/bus/platform/devices/ cat /sys/bus/platform/devices/led.0/modalias其中 modalias 会显示出设备对应的 compatible 信息格式类似platform:led。这个信息就是驱动自动加载modules.autoload的依据之一。驱动注册成功后在/sys/bus/platform/drivers/下会存在驱动目录目录名就是driver.name。你还可以手动做绑定和解绑实验echo led.0 /sys/bus/platform/drivers/board-led/bindecho led.0 /sys/bus/platform/drivers/board-led/unbind如果你手动 bind 的时候提示No such device说明设备名不对去/sys/bus/platform/devices/下先确认设备目录名。如果手动 bind 后 probe 正常执行但开机时不执行问题大概率还是设备树状态或 DTB 未更新。4.6 对匹配失败问题的速查表现象常见原因排查手段probe 不打印compatible 不匹配grep 对比双方字符串probe 不打印status disabled检查设备树节点状态probe 不打印DTB 未更新检查 uboot fdt_file 参数probe 打印但硬件无动作资源获取失败检查 devm_ioremap 返回值probe 打印两次设备树有两个同 compatible 节点用 reg 属性区分insmod 报 version magic 错误内核源码与运行内核不一致统一编译环境并 modules_preparebind 失败设备名错误ls /sys/bus/platform/devices/5. 从匹配机制延伸i.MX6ULL 平台驱动开发的关键经验5.1 驱动分层与匹配机制的协同设计不要把 Platform 匹配机制看成孤立的玩意儿。实际项目里I2C 控制器驱动、SPI 控制器驱动、GPIO 控制器驱动本身都是 platform_driver它们先通过 Platform 机制匹配到 SoC 内部硬件资源完成初始化之后再注册自己的框架I2C 总线、SPI 总线最后为挂载其下的子设备提供读写接口。所以在 i.MX6ULL 上写驱动脑子里要有三层概念最底层platform_driver 负责匹配设备树节点获取寄存器、中断资源初始化硬件控制器。中间层注册具体的子系统框架例如i2c_add_adapter、spi_register_master。最上层具体设备的驱动比如挂在 I2C 总线上的触摸屏驱动挂在 SPI 总线上的 LCD 驱动。每一层的注册和匹配机制都类似但细节不同。吃透了 Platform 机制再学别的总线模型会轻松非常多。5.2 设备树是“硬件描述语言”不是“驱动配置语言”接触过不少工程师喜欢在设备树里塞各种自定义属性然后在驱动的 probe 里读出来做业务逻辑。这种做法偶尔用用可以但大规模使用就会导致设备树越来越臃肿语义混乱后期维护困难。设备树的核心作用是把硬件怎么接的、资源在哪里描述清楚。业务逻辑、策略、参数计算应该放在驱动代码或其他配置系统里。比如一个 LED 的闪烁频率如果用户需求频繁变化应该做成 sysfs 属性或者通过应用层 ioctl 控制而不是在设备树里反复改、反复烧写 DTB。5.3 调试效率决定开发效率我这个项目里最大的体会是驱动开发的主体时间不是写代码而是调试。所以前期把调试手段建立起来比什么都重要。推荐几个高效手段串口控制台打印等级调高loglevel8让所有 dev_dbg 都打出来。系统起来后用 NFS 挂根文件系统方便直接拷贝 ko 文件不用反复制作根文件系统镜像。在驱动里多用dev_info、dev_err这类带设备信息的打印比单纯printk更容易定位是哪个设备实例出问题。把设备树反编译出来检查在板子上执行dtc -I fs -O dts /proc/device-tree -o output.dts看看实际的设备树和你想的是不是一样。5.4 关于模块自动加载的补充如果你的驱动需要开机自动加载要么把驱动编进内核要么在根文件系统里配置 modprobe 自动加载。自动加载依赖 modules.alias 文件它是 depmod 根据MODULE_DEVICE_TABLE生成的。所以驱动代码里记得写MODULE_DEVICE_TABLE(of, led_of_match);没有这行即使设备匹配上了modprobe 也可能不知道该加载哪个模块。这行宏的作用是把of_match_table的内容导出到模块的 modinfo 信息里depmod 读取之后生成 alias。我见过太多人漏掉这一行导致手动 insmod 可以但重启后系统不会自动加载驱动。写在最后说实话Platform 设备与驱动匹配机制属于那种“学会了觉得很简单学之前觉得特神秘”的知识点。它的核心思想就是一句话用软件抽象出一条虚拟总线把不便于枚举的片上设备挂上去让设备和驱动通过一套规定的规则互相匹配。而 i.MX6ULL 恰好是特别适合练手这个机制的平台NXP 的参考手册清晰、资料多、硬件也简单板上一个 LED 就能做全流程验证。从我个人的经验来看学习这块内容最好的方式就是动手改代码、写个最简驱动、故意制造几次匹配失败然后自己去排查。每一次踩坑都会让你对机制的理解加深一层。今天分享的这些内容如果你在项目里也遇到了类似的问题尤其是 probe 不执行这一类完全可以对照上面的排查表一步步查希望对你有所帮助。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

路由策略与PBR策略路由实战:政企客户传输资源优先级保障方案全解析 2026/9/9 2:23:54

路由策略与PBR策略路由实战:政企客户传输资源优先级保障方案全解析

做设备商这几年,交付过不少路由和传输相关的项目,但真正让客户觉得“这方案落地值”的,往往不是设备转发能力多强,而是能不能把传输资源管明白。最近在给一家政企客户交付传输资源优先级保障方案时,核心工作就围绕四个…

阅读更多 →
鸿蒙应用启动优化实战:冷启动链路拆解与性能调优 2026/9/9 2:23:54

鸿蒙应用启动优化实战:冷启动链路拆解与性能调优

兄弟们,这个话题我憋了很久了。每次在群里看到有人发鸿蒙App的启动录屏,要么是点图标后白屏半天,要么是首页框架出来了但数据干等两秒,评论区一群人刷“挤牙膏”;而隔壁组的应用,冷启动直接秒开&#xff0c…

阅读更多 →
单元测试实战指南:从TestNG到Vue,构建可靠代码防线 2026/9/9 2:23:54

单元测试实战指南:从TestNG到Vue,构建可靠代码防线

说实话,早年听到“单元测试”这四个字,我内心是有点抗拒的。当时觉得,代码能跑起来就不错了,写一堆测试用例不是浪费时间吗?直到后来被线上故障按在地上摩擦过几次,才彻底扭转了这个观念。现在我的团队里&a…

阅读更多 →
单元测试标准与TestNG、Vue项目集成实践及常见报错排查 2026/9/9 2:23:54

单元测试标准与TestNG、Vue项目集成实践及常见报错排查

做单元测试这件事,我在不同团队里见过两种极端。一种是把测试当成KPI,只管把覆盖率冲到80%,跑起来全绿,实际连核心逻辑都没测到;另一种是彻底摆烂,觉得“我代码写得没问题,测试浪费时间”。其实…

阅读更多 →
Pikachu靶场详解:从搭建到Web漏洞实战通关 2026/9/9 2:23:54

Pikachu靶场详解:从搭建到Web漏洞实战通关

简介:Pikachu 靶场资源包(pikachu-master.zip)是一套面向网络安全学习者的综合漏洞演练环境,适合从入门到进阶的安全爱好者、渗透测试人员及高校学生使用,可用于在可控环境中理解并实践常见 Web 漏洞的攻防原理。包体共…

阅读更多 →
多层PCB阻抗控制实战:从传输线原理到工厂制程落地 2026/9/9 2:20:54

多层PCB阻抗控制实战:从传输线原理到工厂制程落地

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞