news 2026/9/8 13:13:21

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
i.MX6ULL Platform设备与驱动匹配机制详解:从设备树到probe调用

第一次给i.MX6ULL写驱动的时候,我盯着设备树里的一堆节点想不明白:这个叫“平台总线”的东西到底挂在哪条硬件总线上?为什么驱动里填了一个compatible字符串,probe函数就会被自动调用?后来把Platform设备和驱动的匹配机制彻底搞懂了,之前很多“玄学”才真正消失。

这篇文章就是围绕i.MX6ULL上的Platform设备与驱动匹配机制展开的,适合正在学Linux驱动开发、尤其是用i.MX6ULL这样的ARM SoC做嵌入式项目的开发者。我会从内核为什么要弄一条虚拟总线讲起,再把设备树、platform_device、platform_driver三者如何“对上眼”的过程拆开,最后结合i.MX6ULL的板级环境给出一套可以照着跑的最小示例和排查套路。

1. Platform总线为什么存在:没有物理总线,也要有统一的设备管理

1.1 i.MX6ULL的片上外设并不“挂在”任何物理总线上

i.MX6ULL是NXP基于Cortex-A7内核设计的嵌入式处理器,芯片内部集成了UART、I2C、SPI、GPIO、SDIO、以太网MAC等大量外设控制器。这些外设有一个共同特点:它们的寄存器就映射在芯片的某个内存地址上,CPU直接通过地址访问,不存在一条像USB、PCIe那样可以枚举设备的物理总线。

那问题来了:Linux的设备模型要求任何一个设备都必须归属于某条总线,因为bus_type上挂着device和driver两个链表,内核要靠“设备加入总线时遍历驱动、驱动注册时遍历设备”这套逻辑来完成配对。没有总线归属,设备生命周期管理、电源管理、sysfs属性导出、自动加载驱动这些机制全都无从谈起。

所以内核做了一个非常务实的决定:凭空造一条platform总线。它不对应任何真实硬件,只是一个软件概念,专门收纳那些“直接挂在CPU地址空间上、无法被标准物理总线枚举”的设备。i.MX6ULL内部几乎所有的片上外设控制器,在Linux眼里都是platform_device。

1.2 从设备树节点到platform_device的转化时机

在i.MX6ULL上,设备树(dtb)由U-Boot加载进内存,内核启动早期会把FDT格式的二进制数据解析成设备树结构,然后调用of_platform_default_populate_init等机制,把设备树里符合条件的节点一个个转换成platform_device,挂到platform总线上。

哪些节点会被转化?最核心的条件是:这个节点带有compatible属性。比如imx6ull.dtsi里gpio控制器节点是这样定义的:

gpio5: gpio@020ac000 { compatible = "fsl,imx6ul-gpio", "fsl,imx6ull-gpio"; reg = <0x020ac000 0x4000>; interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>; ... };

节点里有compatible,内核就认为这是一个“平台设备”,把它变成platform_device注册到platform总线。这里请记住:设备树里节点的名字五花八门不重要,compatible才是驱动和设备匹配的关键身份证

当然,并不是所有compatible节点都会变成platform_device。如果节点挂在i2c总线下,它会被i2c控制器驱动识别为i2c_client;挂在spi总线下会变成spi_device。节点能不能展开成platform_device,还取决于父节点的总线类型和compatible是否包含“simple-bus”之类的标识。这一点后面排查问题时会专门说。

1.3 platform_device和platform_driver各自的角色

platform_device描述“硬件有什么”,比如寄存器地址、中断号、DMA通道、时钟等资源,这些信息在设备树里写清楚,内核帮助生成resource结构;platform_driver描述“软件怎么操作这个硬件”,比如probe函数、remove函数、电源管理回调。

这两个角色经过platform总线的match撮合,一旦配对成功,内核就会调用platform_driver的probe函数,驱动开发者的主要工作就是在这个函数里初始化硬件、注册字符设备、建立中断处理等。probe被调用,说明驱动和设备已经“领证”,之后开发者的代码才开始真正执行。

那么配对的具体规则是什么?这是整个机制里最核心的部分。

2. 匹配机制拆解:compatible、id_table、driver.name三层逻辑

2.1 bus_type->match是被调用的总入口

platform总线的类型定义在drivers/base/platform.c里,其中match函数是platform_match。当发生两种情况时,这个函数会被调用:

  • 一个platform_device注册到platform总线时,内核会遍历总线上所有platform_driver,逐个调用platform_match,看有没有驱动愿意认领这个设备。
  • 一个platform_driver注册到platform总线时,内核会遍历总线上所有platform_device,逐个调用platform_match,看有没有设备可以被这个驱动接管。

理解了触发时机,你就明白为什么probe总会在“设备注册”或“驱动注册”之后的某个时刻被调用。如果你只写了一个驱动并insmod加载,但设备树里没有对应设备,probe永远不会执行;反过来,设备树上写了节点,但内核里没有注册匹配的驱动,设备就一直在总线上“无人认领”。

2.2 匹配顺序:先看设备树,再看ID表,最后比名字

platform_match函数的匹配逻辑大致按下面顺序执行:

  1. 优先使用设备树匹配:如果设备有of_node,并且驱动的of_match_table不为空,就遍历of_match_table里的每一条of_device_id,把它的compatible字符串和设备树节点compatible属性里的字符串逐一比较,任何一个相等就算匹配成功。
  2. 然后尝试ACPI匹配,在嵌入式ARM设备上基本不会走到,i.MX6ULL也用不到。
  3. 如果上面都没有成功,再看驱动的id_table,调platform_match_id函数,拿platform_device的name字段去和id_table中每条id_entry的name字段比较。
  4. 如果连id_table都没有,那就直接比较platform_driver.driver.name和platform_device的name是否相同。

注意第1步和第3步的关系:即使设备树节点存在,只要of_match_table里没有对应项,内核并不会立即放弃,还会继续尝试id_table和名字匹配。但在实际设备树开发中,强烈建议直接走第1步,因为这是语义最清晰、最不易出错的方式。

我把三种匹配方式整理一下,方便对比:

匹配方式关键字段设备树环境推荐度说明
DT compatible匹配驱动.of_match_table[].compatible 与 设备节点compatible最推荐直观、稳定、标准做法
id_table name匹配驱动.id_table[].name 与 platform_device.name少用设备树生成的device name不好控制,容易踩坑
直接比较driver.name驱动.driver.name 与 platform_device.name不推荐设备树环境下几乎不可靠

2.3 compatible里的“厂商,型号”为什么必须完全一致

设备树里常见这种写法:

compatible = "myvendor,mydevice";

驱动侧对应的of_device_id数组:

static const struct of_device_id mydevice_of_match[] = { { .compatible = "myvendor,mydevice", }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, mydevice_of_match);

compatible字符串的比较是纯字符串比较,不能有多余空格,不能大小写不一致,逗号必须是英文半角逗号。很多初学者在设备树里写的是myvendor, mydevice(逗号后面多了一个空格),驱动里写的是myvendor,mydevice,结果死活匹配不上,查半天发现是一个空格的问题。

另外,设备树节点的compatible可以写多个值,比如:

compatible = "myvendor,mydevice", "vendor,generic-device";

匹配时内核会先用第一个值去遍历of_match_table,没找到再用第二个值。这种写法在Linux里叫“板级兼容+芯片级兼容”,目的是让同系列硬件可以共用驱动。实际开发中,驱动侧只要在其中任何一个值上命中即可。

2.4 一次完整匹配的“时间线”

以i.MX6ULL启动过程为例,一次成功的匹配大致是这样的:

  1. U-Boot加载设备树dtb,传入内核。
  2. 内核解压设备树,构建of_node节点树。
  3. of_platform_default_populate等流程把带compatible属性的节点转成platform_device,注册到platform总线。
  4. platform总线触发match,此时对应的platform_driver可能还没注册,匹配暂时失败,设备进入“待认领”状态。
  5. 驱动代码执行platform_driver_register(或module_platform_driver宏展开后的注册函数),向platform总线注册platform_driver。
  6. platform总线再次触发match,逐一遍历总线上尚未绑定驱动的设备。
  7. 设备树compatible和驱动of_match_table命中。
  8. 内核调用驱动的probe函数,传入platform_device指针,驱动开发者拿到设备资源,开始初始化硬件。

如果驱动是编译进内核的,第5步会在内核启动更早阶段就完成,第3步生成设备时直接匹配成功,probe几乎和设备注册同时发生,所以你看到的现象就是“设备树节点写着写着,probe就跑了”。

3. i.MX6ULL上从零写一个Platform驱动动:设备树、驱动框架与验证

3.1 硬件环境与最小目标

在i.MX6ULL的板子上验证这套机制,最简单的方式不是直接去驱动UART或GPIO控制器,而是先加一个自定义的虚拟设备节点,跑通“设备树生成设备、驱动匹配、probe打印”的最小链路。这个最小链路一旦成立,后面不管换什么外设,本质流程都是一样的。

我用的平台是正点原子或野火这类常见的i.MX6ULL开发板,内核版本4.x/5.x都可以,设备树文件对应arch/arm/boot/dts/imx6ull-14x14-evk.dts。如果你用的是其他板子,找到自己的板级dts文件即可。

3.2 在设备树上添加自定义节点

在板级dts的根节点/内部添加一个子节点:

/ { mydevice { compatible = "myvendor,mydevice"; reg = <0x020c406c 0x4>; status = "okay"; }; };

这里reg随便填了一个i.MX6ULL的寄存器地址空间,只是为了演示资源获取。实际驱动里,probe函数可以用platform_get_resource拿到这段region。

修改完dts后重新编译。如果环境里单独编译设备树:

make imx6ull-14x14-evk.dtb

然后确认新的dtb被U-Boot加载。很多老手在这翻车:改了dts但U-Boot加载的还是旧dtb,启动后设备树根本没变,驱动自然匹配不上。

3.3 驱动的完整代码框架

驱动代码的结构其实很固定,核心就四件事:定义of_device_id、定义platform_driver、实现probe/remove、注册模块。

#include <linux/module.h> #include <linux/platform_device.h> #include <linux/of.h> #include <linux/io.h> static int mydevice_probe(struct platform_device *pdev) { struct resource *res; dev_info(&pdev->dev, "mydevice probe success\n"); res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (res) { dev_info(&pdev->dev, "reg phys addr: 0x%llx, size: %lld\n", (unsigned long long)res->start, (unsigned long long)resource_size(res)); } return 0; } static int mydevice_remove(struct platform_device *pdev) { dev_info(&pdev->dev, "mydevice remove\n"); return 0; } static const struct of_device_id mydevice_of_match[] = { { .compatible = "myvendor,mydevice", }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, mydevice_of_match); static struct platform_driver mydevice_driver = { .probe = mydevice_probe, .remove = mydevice_remove, .driver = { .name = "mydevice", .of_match_table = mydevice_of_match, }, }; module_platform_driver(mydevice_driver); MODULE_LICENSE("GPL");

代码里最关键的就是MODULE_DEVICE_TABLE(of, mydevice_of_match)。这一步的作用是导出of_device_id表,让内核模块加载时通过modinfo能看到这个驱动支持的compatible列表。如果不写,很多情况下of_match_table虽然在模块内可见,但在某些工具链配置下会出现“驱动加载了但匹配不上”的诡异问题。

of_match_table = mydevice_of_match把表挂到驱动上,platform_match在第一步就会拿到这张表。

module_platform_driver是一个便捷宏,展开后相当于:

module_init(platform_driver_register(&mydevice_driver)); module_exit(platform_driver_unregister(&mydevice_driver));

它同时承担注册和注销工作,比自己写module_init/module_exit更不容易漏。

3.4 编译、加载与验证

把驱动编译成模块:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- M=/path/to/driver/dir modules

把生成的.ko拷贝到板子上,插入模块:

insmod mydevice.ko

立刻去看dmesg:

dmesg | tail -20

正常应该看到:

mydevice mydevice.0: mydevice probe success mydevice mydevice.0: reg phys addr: 0x20c406c, size: 4

“mydevice.0”这个带编号的名字是内核给platform_device自动生成的,后缀0一般是同名设备序号。

在sysfs里也能看到绑定关系。设备侧:

ls -l /sys/bus/platform/devices/mydevice.0/

如果驱动已经绑定,里面会有一个driver符号链接指向驱动的sysfs目录。驱动侧:

ls /sys/bus/platform/drivers/mydevice/

正常会有mydevice.0这个条目,表示该设备已经被这个驱动接管。如果目录下什么都没有,说明驱动注册了但没有设备匹配,或者设备注册了但驱动还没匹配上。

3.5 probe里还能做什么

匹配成功后,开发者可以在probe里做很多事:用platform_get_resource获取reg和irq资源,用of_property_read_u32读取设备树自定义属性,用ioremap映射寄存器地址,用devm_request_irq注册中断,用misc_register或cdev注册设备节点。probe函数成功返回0,设备和驱动才算正式绑定;返回负值,内核会认为probe失败,绑定关系解除,之后除非驱动或设备状态变化,否则不会自动重试。

我个人建议第一次验证时,probe里只做dev_info输出,确认机制通了再逐步加代码。这样定位问题时,可以明确区分“是匹配没成功,还是probe里面某一步失败”,排查成本低很多。

4. 匹配失败的排查链路:问题、原因与sysfs诊断方法

4.1 设备存在但就是没driver:先从设备树侧找原因

我在项目里见过最多的匹配失败案例,不是驱动代码写错了,而是设备树侧就没生成预期的platform_device。遇到这种情况,先按下面顺序排查。

第一步,确认设备树真的被加载了。在板子上执行:

cat /proc/device-tree/mydevice/compatible

如果设备树解析成功,这个文件存在,内容是myvendor,mydevice。如果文件不存在,要么节点没写进dts,要么U-Boot加载的dtb不是新编译的。

第二步,确认节点被展开成platform_device:

ls /sys/bus/platform/devices/ | grep mydevice

没有输出,说明内核压根没把它变成platform_device。常见原因有两个:

  • 节点没有compatible属性,或者status = "disabled",被of_platform跳过。
  • 节点挂在某个非platform总线下,比如i2c节点或spi节点下的子节点,它们会被对应总线框架接管,不会生成platform_device。

举个例子,如果把自定义节点放到&i2c1 { ... }下面,它就会被i2c子系统当作地址设备等待探测,而不是成为platform_device。这在初学时非常容易踩坑:明明写了compatible,却总匹配不上platform驱动,一查发现节点位置不对。

第三步,确认设备树节点里的compatible和驱动of_match_table里的字符串完全一致。为了保险,可以反编译dtb再查一遍:

dtc -I dtb -O dts -o out.dts imx6ull-14x14-evk.dtb grep -n "myvendor,mydevice" out.dts

这一步能排除“代码里看着是对的、实际编译进dtb的是旧版本”这种隐藏问题。

4.2 设备树正常,驱动也加载了,但probe没执行

如果/sys/bus/platform/devices/下面能看到设备名,/sys/bus/platform/drivers/下面也能看到驱动名,两者就是没绑定,就要看匹配环节。

先检查驱动是不是真的注册到了platform总线。有可能ko文件insmod了,但module_init没执行成功。看dmesg里有没有加载报错,比如符号缺失、内存不足、probe函数初始化失败之类。不过module_platform_driver方式下,驱动注册基本不会失败,失败大多出在匹配逻辑上。

is_match的关键在of_match_table。检查点:

  • of_device_id数组最后有没有{ /* sentinel */ }空结构体。内核遍历表依赖这个哨兵项,缺了它遍历可能出现未定义行为,匹配结果不可控。
  • 表里compatible字段是否和设备树完全一致。建议在驱动里对of_match_table里的字符串做一次“肉眼比对+字节数比对”,空格、大小写、逗号位置都要看。
  • 确认驱动编译时,of_match_table确实被编译进目标文件。可以modinfo mydevice.ko看有没有alias: of:N...T...C...类似信息,这个是MODULE_DEVICE_TABLE导出的效果。如果完全没有alias,说明表没生效。

还有一个容易被忽略的点:即使of_match_table匹配失败,只要驱动id_table里有匹配项,或者driver.name与platform_device.name一致,platform_match依然可能返回true。但在设备树环境下,platform_device的name来自内核of_device_make_bus_id生成的设备名,一般形如“mydevice.0”,很难预先猜到,所以靠名字匹配属于碰运气,开发时不要依赖它。

4.3 用sysfs手动绑定来验证驱动本身没问题

如果设备存在、驱动存在、匹配又查不出问题,可以用sysfs的bind/unbind接口手动触发绑定和解除绑定。先把设备从当前驱动上解绑(如果没有绑定可以忽略):

echo mydevice.0 > /sys/bus/platform/drivers/mydevice/unbind

然后手动绑定:

echo mydevice.0 > /sys/bus/platform/drivers/mydevice/bind

每执行一条命令,立刻看dmesg。如果bind成功,probe被调用;如果bind报错,错误信息会直接告诉你失败原因,比如设备忙、probe返回值错误等。这个技巧在调试驱动时非常高效,比反复insmod/rmmod省事得多。

驱动卸载不想让probe再介入时,也可以用unbind先把设备摘下来,再rmmod驱动,避免remove与probe交叉调用的混乱。

4.4 probe为何返回了错误:设备和驱动解除绑定

probe函数返回非0值会被内核视为probe失败,设备和驱动之间的绑定关系被撤销。常见几种情况:

  • 寄存器资源获取失败:platform_get_resource返回NULL,处理时直接return -ENXIO。
  • ioremap失败:地址空间无效或内存不足。
  • 中断申请失败:devm_request_irq返回非0。
  • 某些初始化子步骤失败:比如时钟使能失败、GPIO请求失败。

排查此类问题最简单的办法,就是在probe里每一步都加上dev_err或dev_info打印,逐步缩小范围。等到驱动逻辑稳定后,再把这些调试打印移除或转为dev_dbg。还有一个容易被忽略的点:devm_系列API申请的资源在probe失败时会被自动释放,这是好事,避免忘记资源回收导致的内存泄漏,但反过来,如果你在probe失败后还继续访问已经释放的资源,就会踩到野指针,调试起来非常痛苦。

4.5 模块加载与设备树展开的优先级问题

在设备树驱动开发中,还有一个经典困惑:驱动被编译成模块时,设备树里的设备可能在系统启动时就已经注册,但驱动模块还没加载。操作系统会先把这个设备挂起在总线上等待驱动,不会因为“暂时没有驱动”就把它删除。

所以在i.MX6ULL上,只要驱动模块最终能被加载,probe就会在insmod之后被触发。这个“先有设备、后有驱动”的动态匹配能力,正是Linux设备模型相比传统查询式初始化的优势所在。反过来,如果驱动先注册、设备后生成(比如热插拔设备),platform_match同样会在设备注册时把驱动找出来。理解这一点,就不会纠结probe到底在哪个瞬间执行的问题。

4.6 一张排查顺序表

把上面这些经验整理成一张排查顺序表,按序号执行,基本能覆盖绝大多数匹配失败场景:

序号检查项方法/命令失败说明
1dtb是否加载了新配置cat /proc/device-tree/mydevice/compatible文件不存在说明节点没进设备树
2设备是否生成ls /sys/bus/platform/devices/ | grep mydevice没有设备说明节点没被展开
3驱动是否注册ls /sys/bus/platform/drivers/ | grep mydevice没有驱动说明module_init失败
4compatible是否一致检查dts和of_match_table字符串多空格、大小写、逗号都会导致失败
5是否已经绑定ls -l /sys/bus/platform/devices/mydevice.0/driver无链接说明绑定未建立
6手动绑定测试echo mydevice.0 > .../bindbind报错会给出直接原因
7probe执行结果在probe内加dev_info打印定位失败发生在哪一步

4.7 其他值得留意的坑位

除了上面说的,还有几个在特定条件下才会遇到,但遇到了就非常难查的情况。

一个是设备树节点存在,但父节点的compatible包含simple-bus与否会影响子节点是否被展开成platform_device。如果你的自定义节点直接挂在根节点下,根节点本身会被特殊处理,所以一般没问题;但如果挂在某个外设控制器节点下,需要确认那个控制器的驱动是否已经把子节点注册为platform_device,或者总线的class类型是否符合预期。

另一个是设备树里status = "okay"的坑。如果你在dtsi里定义了一个节点,板级dts里通过&xxx覆盖它,但覆盖时忘了写status = "okay",或者写成disabled,设备就不会生效。这个事在调试I2C、SPI外设时经常出现,Platform设备节点同理。

还有MODULE_DEVICE_TABLEof_match_table的关系。很多人以为of_match_table填了数组就行,忘了导出alias。虽然大部分情况下内核照样能匹配,但如果你使用了自动加载机制,比如udev、mdev在设备出现时自动modprobe驱动,alias缺失就可能导致驱动不自动加载,看起来像匹配失败。嵌入式系统里如果手动insmod,这个问题不容易暴露,但如果做了modprobe自动加载,就要留意。

5. 最后说一点个人经验

这套Platform匹配机制第一次接触时很容易被各种术语绕晕:platform_device、platform_driver、of_match_table、MODULE_DEVICE_TABLE……但换个角度看,它就是“设备树提供数据、驱动提供逻辑、平台总线负责撮合”的三方协作。设备是“甲方”,把硬件资源写在dts里;驱动是“乙方”,把操作逻辑写在C代码里;总线就是“中介”,通过compatible这个暗号让双方相认。暗号对上了,probe就是签约仪式。

想通这一层,再去学i2c_driver、spi_driver,你会发现骨架几乎一样,只是总线实现细节不同。把Platform这个模型吃透,Linux驱动开发就已经迈过最重要的一道门槛。

补充一个调试小技巧:在驱动里临时把probe函数里的dev_info全改成dev_err,这样日志等级更高,不容易被dmesg级别过滤掉。还有,在调试阶段建议把printk开关打开,通过/proc/sys/kernel/printk临时调整日志级别,能少走不少弯路。等机制跑通,再把调试信息清理干净。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 13:13:15

穿戴设备SPI NOR Flash选型避坑:从低功耗到OTA的5个实战经验

前阵子帮朋友救一个智能穿戴项目的板子&#xff0c;现象很典型&#xff1a;样机休眠时整机电流比预估高了 20μA&#xff0c;找了一圈最后定位到 SPI FLASH 上——芯片明明是好的&#xff0c;代码也没跑飞&#xff0c;纯粹是选型和电路设计时埋的雷。那段时间我把 SPI FLASH 的…

作者头像 李华
网站建设 2026/9/8 13:12:27

opencode实战:统一终端AI编程助手,玩转多模型与Skills

如果你最近在折腾终端里的 AI 编程助手&#xff0c;大概会频繁看到一个名字&#xff1a;opencode。我是在 Claude Code 和 Codex CLI 之间来回切换时被迫注意到它的——每个工具绑定一家模型厂商&#xff0c;换一种模型就要换一套操作方式&#xff0c;不同项目之间还不能共享一…

作者头像 李华
网站建设 2026/9/8 13:12:01

NVIDIA投资SSI:算力提升10倍背后的I/O瓶颈突破与并行文件系统解析

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

作者头像 李华
网站建设 2026/9/8 13:09:12

LabWindows/CVI上位机开发实战:简易计算器工程与状态机设计全解析

简介&#xff1a;基于CVI&#xff08;CodeVisionAVR&#xff09;开发环境实现的简易计算器完整工程&#xff0c;面向AVR嵌入式开发初学者和需要快速搭建计算器功能的开发者。该项目支持多位数加、减、乘、除&#xff0c;可自定义运算规则与保留小数位数&#xff0c;并内置菜单退…

作者头像 李华
网站建设 2026/9/8 13:08:13

单片机毕设选题推荐:基于 STM32 的多传感器融合智能柜体管控装置设计 基于 STM32 的阈值可配置智能柜体环境调控系统实现(013007)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/9/8 13:06:17

Deepseek Harness插件解析:给AI模型套上可控缰绳

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

作者头像 李华