新闻详情

新闻详情

首页 / 资讯中心 / 详情

Linux Platform设备驱动匹配机制详解与调试实践

发布时间:2026/9/4 13:35:10来源:尧图网络
Linux Platform设备驱动匹配机制详解与调试实践
i.MX6ULL 这块板子玩过一段时间嵌入式 Linux 的人应该都不陌生。它性价比高资料全很多入门开发板都是拿它做的。但真正从裸机过渡到 Linux 驱动开发时很多人会被总线、设备、驱动这几个概念搞得晕头转向尤其是 Platform 这套机制光看内核源码里的结构体就能劝退一片。这篇文章我想专门把 Platform 设备和驱动匹配这件事讲透。它不是什么高深莫测的技术说白了就是 Linux 内核为了管理那些“不靠硬件总线直接枚举”的设备搞出来的一条虚拟总线。理解了它你写任何字符设备驱动、硬件外设驱动心里都会更有底知道驱动是怎么被找到的设备是怎么被挂上的匹配失败时又该怎么查。1. 为什么需要 Platform 机制1.1 从 I2C、SPI 设备说起的背景很多人第一次听到 Platform是在看 I2C 或 SPI 控制器的驱动代码时。以 I2C 为例I2C 控制器本身是 SoC 内部集成的它的寄存器地址、中断号都是固定写在芯片手册里的。而挂在 I2C 总线上的外部芯片比如 EEPROM、温湿度传感器它们是通过 I2C 协议去访问的有明确的设备地址。这里有个关键点I2C 控制器和挂在它下面的传感器它们的“发现方式”完全不同。I2C 控制器挂在 SoC 的内存地址空间上不能用 I2C 协议去枚举它。而传感器挂在 I2C 总线上可以通过扫描设备地址去探测。所以 Linux 内核需要一种机制来处理“控制器这类设备”——它们存在于内存空间但没有一种现成的硬件总线协议可以主动枚举它们。如果不处理那这些 SoC 内部外设驱动就没法统一管理。1.2 设备模型中的总线、设备、驱动三件套Linux 设备模型的核心就是总线、设备、驱动。总线是连接设备和驱动的桥梁。我们平时最熟悉的 PCI、USB、I2C、SPI 都是真实存在的物理总线。设备通过总线上的枚举机制被发现驱动通过总线的匹配逻辑找到自己负责的设备。但 SoC 内部集成的那些外设控制器比如 GPIO 控制器、串口控制器、DMA 控制器它们不挂在 PCI 或 USB 上也不属于 I2C 或 SPI。该怎么办呢答案就是“虚拟总线”——Platform 总线。Platform 总线没有物理实体它在内核初始化时注册到设备模型里。所有挂在它上面的设备叫 Platform 设备所有处理这些设备的驱动叫 Platform 驱动。匹配机制就是在这条总线上进行的。1.3 一句话理解 Platform 的价值Platform 机制解决的核心问题就是让 SoC 内部外设也能纳入 Linux 设备模型的统一管理框架。设备与驱动通过这条虚拟总线进行匹配匹配成功后调用驱动的 probe 函数完成从“硬件描述”到“驱动接管”的衔接。基于这个基础接下来进入实操层面的核心数据结构。2. Platform 设备与驱动的核心数据结构2.1 struct platform_device 关键成员解析先看设备端。内核里用struct platform_device来描述一个 Platform 设备。这个结构体定义在include/linux/platform_device.h主体内容如下struct platform_device { const char *name; int id; bool id_auto; struct device dev; u32 num_resources; struct resource *resource; const struct platform_device_id *id_entry; /* ... */ };几个核心成员的作用要很清楚name设备名。这个字符串在旧式匹配里是核心标识设备与驱动靠它做字符串比较。id设备编号。同类型设备可以有多个实例用 id 区分如果只有一个实例一般填PLATFORM_DEVID_NONE即 -1。dev内嵌的struct device结构体这是 Linux 设备模型的核心包含 release 回调、of_node 指针等。num_resources和resource描述设备的硬件资源主要包括寄存器地址范围、中断号。对驱动来说这是拿到硬件访问入口的关键。id_entry当通过 ID 表匹配成功时内核会把匹配到的表项指针赋给它驱动可以通过它区分同系列芯片的不同型号。我在初学的时候有一个容易忽略的点struct platform_device里嵌了struct device但驱动拿到的往往不是platform_device本身而是里面的pdev-dev。所有设备模型相关的操作如devm_*系列函数、of_*系列函数都是围绕这个内嵌的struct device展开。2.2 struct platform_driver 与 probe/remove 回调驱动端对应的是struct platform_driverstruct 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; /* ... */ };重点是这几个probe匹配成功后的入口函数。几乎所有驱动的主要初始化逻辑都在这里完成包括获取资源、映射寄存器、注册字符设备、创建设备节点等。remove设备移除或驱动卸载时的清理函数。对应 probe 里的每一步申请这里都要做逆操作。driver内嵌的标准驱动结构体里面的of_match_table、name字段在匹配过程中扮演关键角色。id_table传统的 ID 匹配表用于非设备树方式下的兼容匹配。2.3 platform_device_id 与 of_device_id 区别这里是最容易混淆的地方。struct platform_device_id用于“非设备树”的匹配场景结构比较简单struct platform_device_id { char name[PLATFORM_NAME_SIZE]; kernel_ulong_t driver_data; };而struct of_device_id用于设备树Device Tree简称 DT匹配场景struct of_device_id { char name[32]; char type[32]; char compatible[128]; const void *data; };两种结构的匹配本质完全不同。platform_device_id是内核自己维护的一张 ID 表用于传统板级文件board file方式of_device_id则是和设备树里的compatible属性进行比对。在 i.MX6ULL 上我们普遍使用设备树所以重点是of_device_id。3. 匹配的顺序与内核源码实现3.1 platform_match 函数的完整流程匹配逻辑的核心在drivers/base/platform.c的platform_match函数里。它的调用路径是总线触发bus-match然后调用到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); /* 第一阶段OF 匹配 */ if (of_driver_match_device(dev, drv)) return 1; /* 第二阶段ACPI 匹配 */ if (acpi_driver_match_device(dev, drv)) return 1; /* 第三阶段ID 表匹配 */ if (pdrv-id_table) return platform_match_id(pdev, pdrv-id_table) ! NULL; /* 第四阶段名字直接比较 */ return (strcmp(pdev-name, drv-name) 0); }看到这个函数匹配顺序就一目了然了。内核会优先走设备树匹配没有设备树或设备树匹配失败时再尝试 ID 表最后退回名字比较。3.2 设备树 compatible 属性的匹配逻辑在现代内核环境里尤其 i.MX6ULL 这类芯片跑 4.x/5.x/6.x 内核第一阶段基本决定了绝大多数匹配结果。of_driver_match_device最终会调用of_match_device核心是比较设备树节点的compatible属性和驱动of_match_table里的compatible字符串。看一个常见设备树节点的写法ecspi1 { status okay; spidev0 { compatible rohm,dh2228fv; reg 0; spi-max-frequency 4000000; }; };上面节点的compatible是rohm,dh2228fv。驱动侧的of_match_table就要写static const struct of_device_id my_spi_dt_ids[] { { .compatible rohm,dh2228fv }, { /* sentinel */ } };匹配时内核把设备节点下的compatible字符串数组逐个与驱动表里的字符串比较只要有一个相等就算匹配成功。这里还有个细节设备树里的compatible可以写成多个字符串例如compatible myvendor,mydevice, generic,fallback;内核匹配时会把myvendor,mydevice先和驱动表比对不行就换第二个generic,fallback。这个机制对“同一个驱动兼容不同厂商芯片”非常有帮助。比如一个 GPIO 扩展芯片驱动可以写两个 compatible 兼容不同厂商的同类芯片内核就会依次尝试匹配。3.3 匹配成功后内核自动执行 probe匹配成功之后总线核心代码会调用驱动里的probe函数。这个流程不是驱动作者手动触发的而是内核在设备注册或驱动注册时自动完成的。有两种常见触发路径先注册设备后注册驱动。驱动注册时总线会遍历所有挂在该总线上的设备逐个匹配匹配成功即调用 probe。先注册驱动后注册设备。设备注册时总线同样会遍历驱动列表找到匹配的驱动并调用 probe。两种路径殊途同归最后都会走到really_probe函数在那里完成driver-probe(dev)的调用。我实际调试时经常利用这个特性驱动编译成模块时先insmod驱动再让设备节点出现的情况很常见。比如使用configfs动态创建设备节点时驱动已经加载设备一创建就立即 probe调试起来非常顺。4. 手写一个最小 Platform 驱动4.1 完整源码示例与注释下面我写一个最简单的 Platform 驱动它的目的不是操作具体硬件而是把注册、匹配、probe、remove 的完整流程串起来方便你直观理解机制本身。这个驱动在 i.MX6ULL 上可以直接编译加载。#include linux/module.h #include linux/platform_device.h #include linux/of_device.h static int my_platform_probe(struct platform_device *pdev) { struct resource *res; struct device *dev pdev-dev; dev_info(dev, my_platform_probe enter\n); /* resource 资源解析示例 */ res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (res) { dev_info(dev, mem resource: start0x%llx, end0x%llx\n, (unsigned long long)res-start, (unsigned long long)res-end); } res platform_get_resource(pdev, IORESOURCE_IRQ, 0); if (res) { dev_info(dev, irq resource: %d\n, (int)res-start); } return 0; } static int my_platform_remove(struct platform_device *pdev) { dev_info(pdev-dev, my_platform_remove enter\n); return 0; } static const struct of_device_id my_platform_of_match[] { { .compatible myvendor,mydevice }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_platform_of_match); static struct platform_driver my_platform_driver { .probe my_platform_probe, .remove my_platform_remove, .driver { .name my_platform, .of_match_table my_platform_of_match, }, }; module_platform_driver(my_platform_driver); MODULE_LICENSE(GPL); MODULE_DESCRIPTION(A minimal platform driver example);这段代码虽然短但已经把 Platform 驱动的骨架完整搭出来了。注意probe里用了platform_get_resource去获取内存和中断资源这在真实驱动里太常见了。4.2 Makefile 编写与编译要点在 i.MX6ULL 的开发环境中编译这个模块Makefile 有两种常见写法。第一种是直接利用内核源码树编译obj-m : my_platform.o KERN_DIR : /path/to/your/kernel/source all: $(MAKE) -C $(KERN_DIR) M$(PWD) modules clean: $(MAKE) -C $(KERN_DIR) M$(PWD) clean如果你的环境使用 SDK比如 Yocto 或 Buildroot 导出的交叉编译工具链还可以这样写CC arm-linux-gnueabihf-gcc KDIR : /path/to/kernel/build/directory obj-m my_platform.o all: $(MAKE) ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -C $(KDIR) M$(PWD) modules编译完会生成my_platform.ko。把它拷贝到板子上用insmod加载正常情况下 dmesg 里会出现my_platform_probe enter。注意如果设备树里没有对应的compatible myvendor,mydevice节点probe不会被调用。加载驱动不会报错但 probe 没有任何反应这是排查时最容易遇到的“假死”状态。4.3 设备树端如何添加测试节点要让上面的驱动真正 probe 成功必须在设备树里添加对应节点。在 i.MX6ULL 的板级 dts 文件里添加一个最简单的根节点下的子节点即可/ { my_device: mydevice0 { compatible myvendor,mydevice; reg 0x0209c000 0x100; interrupts 0 30 IRQ_TYPE_LEVEL_HIGH; }; };编译设备树替换板子上的 dtb重启后加载驱动就能看到 probe 打印的信息。实测下来reg中的地址我用的是 i.MX6ULL 某个空闲寄存器段实际项目中你需要替换成自己外设的真实地址。中断号也要参考芯片手册和 interrupts 扩展属性定义来填写不同内核版本对 interrupts 的解析方式会略有差异。5. 设备注册方式与如何手动触发匹配5.1 传统静态注册方式platform_device_register在设备树还没完全普及的老内核里尤其 3.x 时代设备是通过platform_device_register手动注册的。典型做法是在板级文件里定义struct platform_device然后调用注册函数static struct resource my_lcd_resources[] { { .start 0x021c0000, .end 0x021c0fff, .flags IORESOURCE_MEM, }, { .start 38, .end 38, .flags IORESOURCE_IRQ, }, }; static struct platform_device my_lcd_device { .name my_lcd, .id -1, .num_resources ARRAY_SIZE(my_lcd_resources), .resource my_lcd_resources, };然后在机器初始化代码里platform_device_register(my_lcd_device);对应的驱动里of_match_table就不需要了匹配靠id_table或者 name 直接比较。这种方式现在基本被设备树淘汰但理解它有助于读老代码特别是网上很多零几年的帖子、博客全是这类写法。5.2 现代设备树自动注册方式现代内核里设备树节点在启动阶段解析时会由内核自动创建platform_device。以 i.MX6ULL 为例设备树里所有带compatible属性的节点如果它不属于 I2C、SPI、PCI 等物理总线就会被转换为 platform 设备。有个特殊规则值得记即使是 I2C 或 SPI 控制器节点它们自身也会按 platform 设备处理。因为控制器本身不挂在 I2C/SPI 总线上挂在物理总线上的是控制器下面的子节点。这就出现了一个常见疑问为什么设备树里 i2c 控制器节点的 compatible 和某个 platform 驱动的 of_match_table 匹配了probe 却被调用答案很简单因为控制器节点被转换成了 platform 设备。这个细节理解了很多“为什么设备树没生效”的问题就迎刃而解。5.3 通过 sysfs 观察设备与驱动绑定状态当设备与驱动都注册成功后可以通过 sysfs 查看绑定关系。这是排查驱动是否匹配成功的利器。# 查看某个设备的 driver 链接 ls -l /sys/devices/platform/mydevice/driver # 列出当前所有 platform 设备 ls /sys/devices/platform/ # 查看某个驱动的绑定设备列表 ls -l /sys/bus/platform/drivers/my_platform/如果设备与驱动匹配成功/sys/bus/platform/drivers/my_platform/目录下会有一个指向设备的符号链接。如果没有这个链接说明匹配过程失败了。此外还可以手动强制绑定和解绑# 解绑 echo mydevice /sys/bus/platform/drivers/my_platform/unbind # 重新绑定 echo mydevice /sys/bus/platform/drivers/my_platform/bind这个方法在调试 probe 流程时非常有用。驱动代码改完重新 insmod 前可以先解绑设备再重新绑定不需要重启板子。6. 匹配失败排查技巧与常见错误6.1 设备树 compatible 写错的表现compatible 字符串是开发中写错频率最高的点。一个字节对不上匹配就会静默失败。典型错误包括大小写不一致。内核比较字符串是严格区分大小写的。厂商前缀漏写。dh2228fv和rohm,dh2228fv是两回事。逗号前多了空格。例如rohm, dh2228fv虽然人眼看着好像差不多但内核不会做任何模糊处理。设备树里写错但驱动表格里多敲了字符这种低级错误只能靠仔细复查。检查方法很直接# 查看实际解析出的 compatible 属性 ls /sys/firmware/devicetree/base/mydevice/compatible cat /sys/firmware/devicetree/base/mydevice/compatible这个方法能验证 dts 编译后的二进制是否真的包含你期望的字符串。设备树里属性的字符串末尾会有\0直接cat可能看到两个字符串连在一起这是正常的。6.2 驱动加载成功但不 probe 的排查顺序碰到insmod成功、设备节点也有但 probe 不进按下面的顺序排查先确认设备树里有对应节点ls /sys/firmware/devicetree/base/下找节点名。再确认节点的 compatible 内容与驱动表完全一致通过cat方式查看。然后确认驱动里的of_match_table被正确赋值有时只写了MODULE_DEVICE_TABLE但platform_driver.driver.of_match_table忘了赋值这也会导致不匹配。最后看 dmesg 是否有其他错误信息比如 probe 过程中 GPIO 申请失败、中断注册失败等。这几条按顺序查下来90% 的问题都能定位。6.3 platform_driver 与 platform_device 注册顺序的坑明明代码没问题但 probe 就是不调用这往往是注册顺序导致的。特别是在不使用设备树而是代码里手动注册设备的时候。一个典型的例子static int __init my_init(void) { platform_driver_register(my_driver); platform_device_register(my_device); return 0; }如果设备与驱动都在同一个 init 函数里注册确实无论谁先谁后总线都能触发匹配。但如果设备注册被放在另一个模块里且那个模块加载失败或加载顺序靠后驱动先加载时就找不到设备自然不 probe。排查这类问题看 dmesg 中设备注册和驱动注册的时间顺序配合 sysfs 里的绑定状态就能准确定位。6.4 使用 dump 手段验证匹配过程当问题实在难以定位时我习惯在platform_match函数上加打印。但这需要重新编译内核比较麻烦。更轻量的做法是echo file drivers/base/platform.c p /sys/kernel/debug/dynamic_debug/control前提是内核开启了 dynamic_debug 功能。开启后内核会打印 platform 总线匹配过程中的关键信息包括是否进入 OF 匹配、匹配失败的原因等。这个方法在调试复杂驱动时极其好用强烈建议学会。7. 实战经验i.MX6ULL 常见外设驱动的匹配实例7.1 GPIO 控制器驱动的 Platform 匹配流程i.MX6ULL 的 GPIO 控制器在设备树里的描述大致如下gpio1: gpio0209c000 { compatible fsl,imx6ull-gpio, fsl,imx6ul-gpio; reg 0x0209c000 0x4000; interrupts GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH, GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH; gpio-controller; #gpio-cells 2; interrupt-controller; #interrupt-cells 2; };驱动侧的 of_match_table 会写成static const struct of_device_id mxc_gpio_dt_ids[] { { .compatible fsl,imx6ul-gpio }, { .compatible fsl,imx6ull-gpio }, { /* sentinel */ } };因为节点里的 compatible 是按顺序排列的先fsl,imx6ull-gpio再fsl,imx6ul-gpio。如果驱动表里只写了fsl,imx6ul-gpio匹配依然会成功因为内核会依次拿设备树里的字符串去比设备树第二个字符串和驱动匹配上了就通过。这也解释了为什么很多 imx6ull 板子沿用 imx6ul 的设备树和驱动大量兼容就是这么实现的。7.2 串口驱动中 Platform 的匹配细节i.MX6ULL 的 UART 控制器在 dts 里的 compatible 通常是compatible fsl,imx6ul-uart, fsl,imx6q-uart, fsl,imx21-uart;这个写法是兼容不同 SoC 的经典模式。驱动里static const struct of_device_id imx_uart_dt_ids[] { { .compatible fsl,imx6q-uart, .data imx_uart_devdata[IMX6Q_UART] }, { .compatible fsl,imx21-uart, .data imx_uart_devdata[IMX21_UART] }, { .compatible fsl,imx6ul-uart, .data imx_uart_devdata[IMX6UL_UART] }, { /* sentinel */ } };这里of_device_id里的data字段非常有用。同一个驱动可以匹配多个不同 SoC 的串口控制器匹配成功后从of_match_device拿到对应的 data就能确定当前芯片型号对应的寄存器配置、时钟配置等差异。这个模式在高复用驱动里随处可见。7.3 从设备树到驱动 probe 的完整链路再来梳理一次完整链路U-Boot 启动加载 dtb 到内存传给内核。内核启动过程中unflatten_device_tree将 dtb 解析成设备树节点的数据结构。of_platform_bus_probe或of_platform_populate遍历节点为符合条件的节点创建 platform_device。platform_device 注册到 platform 总线上触发总线匹配。platform_match按顺序判断 OF 匹配、ACPI、ID 表、名字比较。匹配成功调用driver-probe(pdev)。probe 中通过of_*系列 API 读取设备树属性通过platform_get_resource获取寄存器中断资源启动设备。这条链路理解了后面写任何类型的驱动都有清晰的坐标系不会再被“设备树和驱动之间到底怎么连起来的”这个问题卡住。8. 设备与驱动匹配机制带来的设计红利8.1 设备与驱动解耦的工程意义Platform 匹配机制最大的好处是设备描述与驱动逻辑完全分离。设备树只负责描述“有什么硬件、资源在哪里、配置是什么”驱动只负责“拿到资源、初始化、提供功能”。两者的连接点是那个小小的 compatible 字符串或 id_table 表项。这个设计在实际项目里带来的好处非常明显。同一款驱动换一个板子、换一个引脚、换一个中断号只需改设备树不需要动驱动代码。我维护过多个硬件版本共用一个驱动的情况驱动源码几乎没有改过全靠设备树配置差异来适配。8.2 热插拔概念在 Platform 场景的延伸虽然 Platform 总线不像 USB、PCI 那样真正支持硬件热插拔但机制上完全支持运行时动态添加和删除设备。比如某些 FPGA 逻辑动态加载的场景可以让内核在运行时创建一个 platform_device触发对应驱动 probe完成协处理器初始化。这种用法在工业控制领域很常见。基于这个思路还可以通过platform_device_register_simple快速创建一个临时设备专门用于测试驱动是否正确platform_device_register_simple(test_device, -1, res, ARRAY_SIZE(res));这在调试早期非常方便不需要改设备树一条代码就能验证驱动逻辑。8.3 扩展思考从 Platform 到其他总线驱动的迁移理解 Platform 匹配后再去看 I2C、SPI 甚至 USB 驱动会发现套路几乎一致总线定义匹配规则设备注册时触发总线 match匹配成功后调用 probe。区别只是在 match 函数内部的具体实现不同。比如 I2C 的匹配会先看i2c_device_id表再看设备树 compatibleSPI 的匹配会看spi_device_id表、of_match_table还有spi_driver.id_table。掌握 Platform 这一套等于拿到了整个 Linux 设备驱动模型的钥匙后续学任何总线驱动都会非常快。9. 常见报错信息与排查速查表现象可能原因排查手段insmod 成功dmesg 无任何 probe 打印compatible 不匹配或 of_match_table 未赋值检查 sysfs 下的绑定状态用 dynamic_debug 打开平台总线日志dmesg 报Failed to create device link设备树节点引用了一个不存在的设备检查设备树中clocks、interrupts、power-domains等属性引用probe 报ioremap failedreg属性范围不当或已被其他驱动占用检查cat /proc/iomem确认地址范围probe 报irq get failedinterrupts属性格式错误检查interrupt-parent与#interrupt-cells是否配置正确设备在 sysfs 下不存在设备树节点没有被转换成 platform device确认节点是否放在正确的总线节点下status属性是否为okay驱动 remove 不执行设备被 busy 引用无法释放检查是否有进程占用设备节点文件这张表是我实际调试中积累出来的高频问题清单基本覆盖了 Platform 驱动开发时的 80% 问题。10. 实际调试中的一点经验补充调试 Platform 驱动匹配问题最高效的工具门槛最低的还是dmesg加 sysfs 两手抓。优先确认设备和驱动都在系统里存在再确认绑定关系最后才去怀疑代码逻辑。顺序反了很容易在错误的方向上浪费时间。还有个小技巧设备树节点可以临时加一个属性作为调试标记mydevice: mydevice0 { compatible myvendor,mydevice; reg 0x0209c000 0x100; my_debug_flag 1; };驱动里用of_property_read_u32读取这个属性根据它的值决定是否打印调试信息。这个方法改起来非常快比在驱动里改宏定义重新编译整个模块要舒服得多特别适合现场调试的场景。最后说一个很多人问过的问题为什么我改了设备树重启之后没有生效大概率是 dtb 没替换成功。U-Boot 有可能从固定分区读取 dtb也可能从网络加载 dtb两者路径不同容易混淆。排查方式是在 U-Boot 里打印printenv fdtfile或看启动日志中设备树加载的路径确认加载的是不是自己编译生成的 dtb。这个问题排查清楚剩下的就都是常规操作了。
网站建设高端定制企业官网
RELATED

相关资讯

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

较早相关资讯

最新相关资讯

嵌入式工程师薪资差距解析:从3K到100W的技术栈与职业发展路径 2026/9/4 14:32:20

嵌入式工程师薪资差距解析:从3K到100W的技术栈与职业发展路径

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

阅读更多 →
PPT Master:如何把一份 PDF 变成原生可编辑的 PPT——开源 AI PPT 生成工具完整指南 2026/9/4 14:32:20

PPT Master:如何把一份 PDF 变成原生可编辑的 PPT——开源 AI PPT 生成工具完整指南

PPT Master:如何把一份 PDF 变成原生可编辑的 PPT——开源 AI PPT 生成工具完整指南 【免费下载链接】ppt-master AI turns documents or topics into real, native PowerPoint decks—with native shapes, transitions and animations, data-backed charts and tab…

阅读更多 →
content-research-writer 上手指南:把查资料、改大纲、要反馈交给 Claude 2026/9/4 14:32:20

content-research-writer 上手指南:把查资料、改大纲、要反馈交给 Claude

content-research-writer 上手指南:把查资料、改大纲、要反馈交给 Claude 【免费下载链接】awesome-claude-skills A curated list of awesome Claude Skills, resources, and tools for customizing Claude AI workflows 项目地址: https://gitcode.com/GitHub_T…

阅读更多 →
Upscayl 免费 AI 图像放大完整指南:本地运行,把低清图放大 4 倍 2026/9/4 14:32:20

Upscayl 免费 AI 图像放大完整指南:本地运行,把低清图放大 4 倍

Upscayl 免费 AI 图像放大完整指南:本地运行,把低清图放大 4 倍 【免费下载链接】upscayl 🆙 Upscayl - #1 Free and Open Source AI Image Upscaler for Linux, MacOS and Windows. 项目地址: https://gitcode.com/GitHub_Trending/up/upscayl IM 转一手就糊…

阅读更多 →
【千问大模型API申请教程】 2026/9/4 14:32:20

【千问大模型API申请教程】

目录 1.进入官网 2.点击API-Key 3.点击创建我的API-key 4.填写内容 5.创建成功,查看api复制即可进行调用 1.进入官网 通义大模型_企业拥抱 AI 时代首选-阿里云 2.点击API-Key 3.点击创建我的API-key 4.填写内容 5.创建成功,查看api复制即可进行调用…

阅读更多 →
ERPNext ERP系统:一套完整可5分钟跑起来的开源企业资源规划套件 2026/9/4 14:29:19

ERPNext ERP系统:一套完整可5分钟跑起来的开源企业资源规划套件

ERPNext ERP系统:一套完整可5分钟跑起来的开源企业资源规划套件 【免费下载链接】erpnext Free and Open Source Enterprise Resource Planning (ERP) 项目地址: https://gitcode.com/GitHub_Trending/er/erpnext ERPNext 是一个 100% 开源的 ERP 系统&#…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

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

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