新闻详情

新闻详情

首页 / 资讯中心 / 详情

Linux内核驱动开发——动态加载驱动与platform

发布时间:2026/9/27 7:00:13来源:尧图网络
Linux内核驱动开发——动态加载驱动与platform
LKMLinux Kernel Module就是内核动态模块不用重新编译烧录整个内核在这个模块里面代码不写死在设备树而是代码里动态创建、注册 platform 设备。 组合起来 一个内核模块加载 insmod 时在内存里临时创建 platform 设备rmmod 时销毁设备。① LKM 动态模块加载LKM Linux Kernel Module内核模块。传统驱动编译进内核镜像y内核启动就全部载入改驱动必须重新编译内核、烧固件。LKM编译成.ko内核模块文件运行时按需加载insmod xxx.ko不需要重启、不用重编内核不用就rmmod卸载。优点调试方便、减小内核体积、按需启用驱动。② 动态注册 platform 设备platform 总线是 Linux 为片上外设I2C 控制器、LCD、GPIO 外设等设计的虚拟总线经典用法两种静态方式设备树 DTS在 dts 里写platform_device节点内核启动解析 DTS自动创建 platform 设备驱动写platform_driver总线匹配后执行 probe。设备是内核启动阶段一次性建好写死在设备树里要增删设备就得改 DTS、重编 dtb。动态注册在内核代码里调用platform_device_alloc()→ 填充资源IO 地址、中断、寄存器、自定义数据→platform_device_register()在代码里手动生成 platform 设备不依赖 DTS 节点。设备不是来自 dtb是模块跑起来之后在内存动态生成。LKM 模块内部动态注册 platform 设备流程编译得到xxx.koinsmod xxx.ko→ LKM 模块入口函数执行模块代码里分配并构造platform_device结构体填写外设资源platform_device_register()向 platform 总线注册这个设备注册对应的platform_driver驱动总线匹配成功调用probe函数初始化硬件rmmod xxx.ko→ 模块退出函数先 unregister platform 设备、释放资源模块从内核卸载注意顺序可以先注册 driver 再注册 device也可以反过来platform 总线会自动做匹配。为什么要这么做驱动开发调试在做 platform 驱动调试每次改设备资源寄存器基地址、中断号如果用 DTS必须重新编译 dtb、更新固件非常麻烦。动态注册方案全部写在 ko 模块代码里只需要重新编译 koinsmod/rmmod 即可反复测试不用改设备树、不用重启板子。 适合裸机转 Linux 驱动、验证硬件寄存器、验证 probe 逻辑。动态加载设备代码struct resource结构体解析struct resource { resource_size_t start; // 资源起始地址物理地址 resource_size_t end; // 资源结束地址 const char *name; // 资源名字自定义字符串 unsigned long flags; // 资源类型标志位 struct resource *parent, *sibling, *child; // 内核资源树链表用户驱动不用管 };start资源起始物理地址 寄存器基地址例GPIO1_IO3_SW_MUX 0x20E0068end资源结束物理地址 一段连续资源的最后一个地址。32 位寄存器占 4 字节start 4 -1公式end start 长度 - 1name资源名称字符串方便驱动里区分多个资源sw_mux、pad只是给人看的内核不依赖这个名字匹配flags资源类型标记核心 常用 flagIORESOURCE_MEM内存映射 IO绝大多数外设寄存器用这个你代码这里 IORESOURCE_REG 很少用推荐改成 IORESOURCE_MEMIORESOURCE_IOx86 传统 IO 端口ARM 基本不用IORESOURCE_IRQ中断资源IORESOURCE_REG寄存器资源标记平台设备可以用但 i.MX 驱动惯例是IORESOURCE_MEMstruct platform_device结构体解析struct platform_device { const char *name; // 平台设备名字核心匹配关键字 int id; // 设备id号 bool id_auto; struct device dev; // 内嵌标准device结构体Linux设备模型核心 u32 num_resources; // resource资源数量 struct resource *resource; // 资源数组指针 const struct platform_device_id *id_entry; char *driver_override; /* 其他私有成员驱动开发基本不用 */ struct pdev_archdata *archdata; };const char *name平台总线匹配的核心。 内核平台总线做匹配规则platform_driver里面.name和platform_device的.name字符串完全相等就触发probe函数。int id设备编号。id -1表示只创建单个设备设备名就是led1不是led1.0、led1.1id0/1/2多个同名字设备生成 led1.0、led1.1 多个设备struct device dev内嵌通用device结构体所有 Linux 设备都继承这个基础设备模型。.release release必须设置内核设备模型当设备引用计数减到 0就调用 release 回调函数用于资源释放。如果不写内核会报 oops 或者警告。u32 num_resources资源数组里面一共有多少个struct resource元素。sizeof(res)/sizeof(res[0])自动计算数组元素个数。struct resource *resource指向struct resource资源数组的首地址内核通过这个指针拿到硬件资源列表。其余成员id_entry用于platform_device_id匹配表driver_override强制指定驱动入门平台设备开发基本不用。// 头文件内核初始化相关宏定义__init、__exit等 #include linux/init.h // 内核打印函数 printk #include linux/printk.h // 文件系统相关字符设备、文件操作结构体等 #include linux/fs.h // EXPORT_SYMBOL 导出符号宏 #include linux/export.h // 内核基础数据类型定义u32、uint等 #include linux/types.h // 模块加载、卸载、许可证等宏 #include linux/module.h // platform平台设备框架头文件平台设备相关结构体与API #include linux/platform_device.h // 平台设备名字驱动端要匹配这个名字才能probe #define DEV_NAME led1 // 寄存器物理地址定义i.MX6ULL 相关GPIO引脚复用、PAD、方向、数据寄存器 #define GPIO1_IO3_SW_MUX 0x20E0068 // GPIO1_IO3 引脚复用选择寄存器 #define GPIO1_IO3_SW_PAD 0x20E02F4 // GPIO1_IO3 PAD电气属性配置寄存器 #define GPIO1_IO3_DIR 0x209C004 // GPIO1方向寄存器bit3控制IO3输入/输出 #define GPIO1_IO3_DR 0x209C000 // GPIO1数据寄存器bit3控制IO3输出高低电平 // resource数组描述平台设备占用的硬件资源这里是4组寄存器地址 static struct resource res[4] { [0] { .start GPIO1_IO3_SW_MUX, // 寄存器起始物理地址 .end GPIO1_IO3_SW_MUX 4 - 1, // 占4字节32位寄存器 .name sw_mux, // 资源名字 .flags IORESOURCE_REG // 资源类型寄存器资源 }, [1] { .start GPIO1_IO3_SW_PAD, .end GPIO1_IO3_SW_PAD 4 - 1, .name sw_pad, .flags IORESOURCE_REG }, [2] { .start GPIO1_IO3_DIR, .end GPIO1_IO3_DIR 4 - 1, .name dir, .flags IORESOURCE_REG }, [3] { .start GPIO1_IO3_DR, .end GPIO1_IO3_DR 4 -1, .name dr, .flags IORESOURCE_REG }, }; // 设备release回调函数设备引用计数为0时调用必须提供防止内核警告 static void release(struct device *dev){} // 定义一个platform_device平台设备实例 static struct platform_device dev { .name DEV_NAME, // 设备名称和platform_driver的name匹配 .id -1, // id-1表示只有这一个实例不创建设备编号 .dev { .release release // 挂载release释放回调 }, .num_resources sizeof(res) / sizeof(res[0]), // 资源数量 .resource res // 资源数组指针 }; // 模块入口函数__init标记加载时执行内存可在初始化完成后释放 static int __init led_device_init(void) { // 注册平台设备到内核平台总线 int ret platform_device_register(dev); if(ret 0) { // 注册失败反注册打印错误码 platform_device_unregister(dev); printk(platform_device_register failed ... ret %d\n, ret); return ret; } printk(platform_device_register ...\n); return ret; } // 模块出口函数__exit标记模块卸载时执行 static void __exit led_device_exit(void) { // 卸载模块时注销平台设备 platform_device_unregister(dev); printk(platform_device_unregister ...\n); } // 指定模块入口 module_init(led_device_init); // 指定模块出口 module_exit(led_device_exit); // 模块许可证GPL否则内核污染警告部分内核API无法使用 MODULE_LICENSE(GPL);动态加载驱动代码static struct platform_driver 结构体解析struct platform_driver { // 设备和驱动匹配成功之后内核自动调用probe int (*probe)(struct platform_device *); // 设备移除的时候调用rmmod、设备热拔触发 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 struct device_driver driver; // 设备ID匹配表支持多个设备名匹配同一个驱动可选 const struct platform_device_id *id_table; // 禁止延迟probe一般不用 bool prevent_deferred_probe; };这里我只用到了其中的几个1.probe probe平台总线匹配成功后自动调用 probe。 传入pdev就是注册的那个 platform_device 指针在 probe 内部platform_get_resource()获取 resource 硬件资源ioremap映射寄存器硬件初始化、创建设备节点等2.remove remov驱动模块rmmod卸载时触发和 probe 反向操作iounmap释放虚拟地址映射销毁字符设备、sysfs 节点硬件复位释放所有内核资源3.driver { .name DEV_NAME }内嵌struct device_driver里面的.name用于平台总线名字匹配。⚠️ 重点这里没有写 id_table这种写法走【老的名字匹配机制】 内核拿drv.driver.name和platform_device.name字符串对比完全相同就匹配。如果写了.id_table内核优先使用 id_tabledriver.name就不再作为匹配依据。#include linux/init.h #include linux/printk.h #include linux/fs.h #include linux/export.h #include linux/types.h #include linux/miscdevice.h #include asm/io.h #include asm/string.h #include asm/uaccess.h #include linux/module.h #include linux/platform_device.h // 设备名称和platform设备name匹配 #define DEV_NAME led1 // 虚拟地址指针物理寄存器经过ioremap映射后的内核虚拟地址 static volatile unsigned long * gpio1_sw_mux; // GPIO复用功能选择寄存器 static volatile unsigned long * gpio1_sw_pad; // GPIO pad电气属性配置寄存器上下拉、速率等 static volatile unsigned long * gpio1_dir; // GPIO方向寄存器1输出0输入 static volatile unsigned long * gpio1_dr; // GPIO数据寄存器控制输出高低电平 /** * brief 初始化LED对应的GPIO硬件 * 设置引脚复用、PAD属性配置为输出并默认输出高电平(LED熄灭取决于硬件电路) */ static void led1_init(void) { *gpio1_sw_mux 5; // 设置引脚复用功能为GPIO模式数值5来自芯片手册 *gpio1_sw_pad 0x1079; // 配置PAD属性如上下拉、驱动能力、压摆率 *gpio1_dir | (1 3); // bit3置1GPIO1_IO03设置为输出模式 *gpio1_dr | (1 3); // bit3置1输出高电平LED默认熄灭 } /** * brief 点亮LED * bit3清零输出低电平LED点亮前提LED是低电平点亮 */ static void led1_on(void) { *gpio1_dr ~(1 3); } /** * brief 熄灭LED * bit3置1输出高电平LED熄灭 */ static void led1_off(void) { *gpio1_dr | (1 3); } /** * brief 设备文件open回调函数应用程序open(/dev/led1)触发 * param node 索引节点 * param file 文件结构体 * return 成功返回0 */ static int open(struct inode * node, struct file * file) { led1_init(); // 打开设备时初始化GPIO printk(led init ...\n); return 0; } /** * brief 设备read回调应用read()触发本驱动read无实际功能 * param file 文件结构体 * param buf 用户空间缓冲区 * param size 用户想要读取的字节数 * param offset 文件偏移 * return 返回0 */ static ssize_t read(struct file * file, char __user * buf, size_t size, loff_t * offset) { //copy_to_user(); // 预留内核数据拷贝到用户空间 printk(led read ...\n); return 0; } /** * brief 设备write回调应用write写/dev/led1触发接收ledon/ledoff命令 * param file 文件结构体 * param buf 用户空间数据缓冲区 * param size 写入长度 * param offset 文件偏移 * return 成功返回实际读取的字节数失败返回负错误码 */ static ssize_t write (struct file * file, const char __user * buf, size_t size, loff_t * offset) { // 接收用户传入命令字符串缓冲区 unsigned char data[10] {0}; // 防止缓冲区溢出取size和数组长度较小值 int len sizeof(data) size ? sizeof(data) : size; // copy_from_user把用户空间buf的数据拷贝到内核data缓冲区 int ret copy_from_user(data, buf, len); if(ret 0) return ret; // 判断命令字符串 if(!strncmp(data, ledon, len)) led1_on(); else if(!strncmp(data, ledoff, len)) led1_off(); else return -EINVAL; // 无效命令返回参数错误 printk(led write ...\n); return len; // 返回处理的字节数 } /** * brief 设备release回调应用close关闭文件时触发 * param node 索引节点 * param file 文件结构体 * return 成功返回0 */ static int close (struct inode * node, struct file * file) { led1_off(); // 关闭设备熄灭LED printk(led close ...\n); return 0; } // 文件操作集绑定open/read/write/release回调 static struct file_operations fops { .owner THIS_MODULE, .open open, .read read, .write write, .release close }; // 杂项设备结构体自动分配次设备号生成/dev/led1 static struct miscdevice misc { .minor MISC_DYNAMIC_MINOR, // 动态申请次设备号 .name DEV_NAME, // 设备文件名 /dev/led1 .fops fops // 绑定文件操作方法 }; /** * brief platform驱动probe函数设备树匹配成功后自动调用 * param pdev platform设备指针 * return 成功返回0失败返回负值 */ static int probe(struct platform_device * pdev) { int i 0; int ret misc_register(misc); // 注册杂项设备 if(ret 0) goto err_register; // 遍历platform设备的资源获取寄存器物理地址并ioremap映射 for(i 0; i pdev-num_resources; i) { if(!strcmp(pdev-resource[i].name, sw_mux)) { // ioremap物理地址 - 内核虚拟地址映射 gpio1_sw_mux ioremap(pdev-resource[i].start, pdev-resource[i].end - pdev-resource[i].start 1); } else if(!strcmp(pdev-resource[i].name, sw_pad)) { gpio1_sw_pad ioremap(pdev-resource[i].start, pdev-resource[i].end - pdev-resource[i].start 1); } else if(!strcmp(pdev-resource[i].name, dir)) { gpio1_dir ioremap(pdev-resource[i].start, pdev-resource[i].end - pdev-resource[i].start 1); } else if(!strcmp(pdev-resource[i].name, dr)) { gpio1_dr ioremap(pdev-resource[i].start, pdev-resource[i].end - pdev-resource[i].start 1); } else printk(the resource %s not found!\n, pdev-resource[i].name); } printk(led_init ********************\n); return ret; err_register: misc_deregister(misc); // 注册失败注销杂项设备 printk(led_init failed********************\n); return ret; } /** * brief platform驱动remove函数设备卸载时调用 * param pdev platform设备指针 * return 成功返回0 */ static int remove(struct platform_device * pdev) { // 解除ioremap地址映射释放虚拟地址 iounmap(gpio1_sw_mux); iounmap(gpio1_sw_pad); iounmap(gpio1_dir); iounmap(gpio1_dr); misc_deregister(misc); // 注销杂项设备 printk(led_exit ******************************\n); return 0; } // platform驱动结构体注册probe、remove回调和驱动名字 static struct platform_driver drv { .probe probe, .remove remove, .driver { .name DEV_NAME } }; /** * brief 模块入口函数insmod加载驱动执行 */ static int __init led_driver_init(void) { int ret platform_driver_register(drv); // 注册platform驱动 if(ret 0) { platform_driver_unregister(drv); return ret; } printk(platform_driver_register ...\n); return ret; } /** * brief 模块出口函数rmmod卸载驱动执行 */ static void __exit led_driver_exit(void) { platform_driver_unregister(drv); // 卸载platform驱动 printk(platform_driver_unregister ...\n); } module_init(led_driver_init); module_exit(led_driver_exit); MODULE_LICENSE(GPL); // 模块协议不加会内核污染警告静态DTB注册设备platform动态加载驱动程序编写板级xxx.dts添加 platform 设备节点写compatible、reg、interrupt 等硬件资源#include芯片 dtsidtc 编译 dts →xxx.dtb二进制烧录到板子make dtbs 编译所有的设备树文件 make xxx.dtb 编译xxx.dts生成xxx.dtb编译之前修改makefile文件添加对应的dtb编译依赖选项U-Boot 启动把内核镜像 zImage dtb加载到内存启动 Linux 内核内核启动早期解析 DTB遍历所有节点遇到 platform 类型节点自动创建platform_device挂到 platform 总线。这一步不需要驱动只要内核读到 dtb设备就已经存在总线上静静等待 driver。驱动代码编译成.koLKM 模块放在文件系统编写platform_driver代码#define DEV_NAME led1 // 杂项设备名称会在/dev下生成 /dev/led1 // 全局虚拟地址指针用于操作GPIO相关寄存器 static volatile unsigned long * gpio1_sw_mux; // GPIO复用选择寄存器虚拟地址 static volatile unsigned long * gpio1_sw_pad; // PAD电气属性配置寄存器虚拟地址 static volatile unsigned long * gpio1_dir; // GPIO方向寄存器虚拟地址 static volatile unsigned long * gpio1_dr; // GPIO数据寄存器虚拟地址 /* * brief LED硬件初始化配置引脚复用、PAD属性、GPIO方向默认输出高电平(LED熄灭) */ static void led1_init(void) { *gpio1_sw_mux 5; // 设置引脚复用功能5把IO配置成GPIO模式 *gpio1_sw_pad 0x1079; // 配置PAD电气属性上下拉、速率、驱动能力等 *gpio1_dir | (1 3); // bit3置1设置GPIO1_IO03为输出方向 *gpio1_dr | (1 3); // 数据寄存器bit3置1输出高电平LED默认熄灭 } /* * brief 点亮LEDGPIO输出低电平 */ static void led1_on(void) { *gpio1_dr ~(1 3); // 将bit3清零输出低电平LED点亮 } /* * brief 熄灭LEDGPIO输出高电平 */ static void led1_off(void) { *gpio1_dr | (1 3); // 将bit3置1输出高电平LED熄灭 } /* * brief open函数应用程序open打开/dev/led1时触发 * param node 设备节点inode * param file 文件结构体 * retval 成功返回0 */ static int open(struct inode * node, struct file * file) { led1_init(); // 打开设备时初始化LED硬件 printk(led init ...\n); return 0; } /* * brief read函数应用程序调用read读取/dev/led1触发本例未实现有效读取逻辑 * param file 文件结构体 * param buf 用户空间缓冲区 * param size 用户想要读取的字节数 * param offset 文件偏移 * retval 返回0 */ static ssize_t read(struct file * file, char __user * buf, size_t size, loff_t * offset) { //copy_to_user(); // 本demo注释没有向用户态回传数据 printk(led read ...\n); return 0; } /* * brief write函数应用write向/dev/led1写数据用于控制LED亮灭 * param file 文件结构体 * param buf 用户态传来的数据缓冲区 * param size 写入长度 * param offset 文件偏移 * retval 成功返回实际处理长度失败返回负错误码 */ static ssize_t write (struct file * file, const char __user * buf, size_t size, loff_t * offset) { unsigned char data[10] {0}; // 内核缓冲区存放从用户态拷贝的数据 // 取较小值防止缓冲区溢出 int len sizeof(data) size ? sizeof(data) : size; // copy_from_user把用户空间数据拷贝到内核空间 int ret copy_from_user(data, buf, len); if(ret 0) return ret; // 判断用户传入字符串匹配命令 if(!strncmp(data, ledon, len)) led1_on(); else if(!strncmp(data, ledoff, len)) led1_off(); else return -EINVAL; // 无效命令返回非法参数错误 printk(led write ...\n); return len; // 返回成功处理字节数 } /* * brief releaseclose应用调用close关闭/dev/led1时触发 * param node inode节点 * param file 文件结构体 * retval 0 */ static int close (struct inode * node, struct file * file) { led1_off(); // 关闭设备时熄灭LED printk(led close ...\n); return 0; } // 文件操作集绑定驱动对应的文件操作回调函数 static struct file_operations fops { .owner THIS_MODULE, .open open, .read read, .write write, .release close }; // 杂项设备结构体注册misc设备自动生成/dev节点 static struct miscdevice misc { .minor MISC_DYNAMIC_MINOR, // 动态分配次设备号不用手动指定 .name DEV_NAME, // 设备名 /dev/led1 .fops fops // 文件操作方法 }; /* * brief 模块入口函数insmod加载ko时执行 * 核心逻辑1. 根据路径查找DTS设备节点2.读取dts里compatible、自定义属性abc、reg寄存器资源 * 3.注册misc杂项设备4.ioremap物理地址映射为内核虚拟地址 */ static int __init led_init(void) { int i 0; int ret 0; const char * pcompatible NULL; const char * pabc NULL; unsigned int reg_array[8] {0}; // 存放从DTS reg读到的地址长度reg成对物理地址,size // 根据节点路径查找设备树节点 /led1 struct device_node * dev_node of_find_node_by_path(/led1); if(NULL dev_node) { printk(of_find_node_by_path not found /led1\n); return -1; } // 读取DTS节点中compatible字符串属性 of_property_read_string(dev_node, compatible, pcompatible); printk(pcompatible : %s\n, pcompatible); // 读取DTS自定义属性 abc of_property_read_string(dev_node, abc, pabc); printk(pabc : %s\n, pabc); // 读取reg属性最多读出8个u324组物理地址,长度 of_property_read_u32_array(dev_node, reg, reg_array, 8); for(i 0; i 4; i) { printk(reg[%d] %#X %#X\n, i, reg_array[i * 2], reg_array[i * 2 1]); } // 注册misc杂项设备 ret misc_register(misc); if(ret 0) goto err_register; // ioremap把DTS读到的物理寄存器地址映射成内核虚拟地址内核才能访问 gpio1_sw_mux ioremap(reg_array[0], reg_array[1]); gpio1_sw_pad ioremap(reg_array[2], reg_array[3]); gpio1_dir ioremap(reg_array[4], reg_array[5]); gpio1_dr ioremap(reg_array[6], reg_array[7]); printk(led_init ********************\n); return ret; err_register: // 注册失败资源回滚卸载misc设备 misc_deregister(misc); printk(led_init failed********************\n); return ret; } /* * brief 模块出口函数rmmod卸载模块执行 * 解除地址映射注销杂项设备释放资源 */ static void __exit led_exit(void) { iounmap(gpio1_sw_mux); iounmap(gpio1_sw_pad); iounmap(gpio1_dir); iounmap(gpio1_dr); misc_deregister(misc); printk(led_exit ******************************\n); } module_init(led_init); // 指定模块入口 module_exit(led_exit); // 指定模块出口 MODULE_LICENSE(GPL); // 模块协议必须声明否则内核报tainted警告执行insmod xxx.ko模块入口module_init运行调用platform_driver_register()向 platform 总线注册platform_driverplatform 总线做匹配拿 driver 里的compatible字符串和 DTB 生成的 platform_device 里compatible对比匹配成功 → 调用probe函数probe 中从 device 读取 reg / 中断资源ioremap申请中断创建设备节点初始化硬件rmmod xxx.ko调用platform_driver_unregister()执行remove释放资源卸载驱动模块但 platform_device 还在来自 dtb不会消失关键点rmmod 只是卸载驱动DTB 生成的设备还在系统里。再 insmoddriver 重新注册再次匹配、执行 probe。
网站建设高端定制企业官网
RELATED

相关资讯

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

较早相关资讯

最新相关资讯

告别模板站丑闻:3步搞定网站建设品牌塑造计划完整流程 2026/9/27 7:46:51

告别模板站丑闻:3步搞定网站建设品牌塑造计划完整流程

告别模板站丑闻:3步搞定网站建设品牌塑造计划完整流程 别再对着那些千篇一律的模板网站发呆了,真不够用。模板站最大的问题不是“丑”,而是它根本没法帮你建立品牌辨识度,用户看一眼就划走,留不下任何印象。想要做出有记忆点的官网,光靠套模板是死路一…

阅读更多 →
phpMyAdmin 导出插件(Export Plugin)开发完全指南:从 README 模板到源码级实现 2026/9/27 7:46:45

phpMyAdmin 导出插件(Export Plugin)开发完全指南:从 README 模板到源码级实现

数据库后端 【免费下载链接】phpmyadmin A web interface for MySQL and MariaDB 项目地址: https://gitcode.com/gh_mirrors/ph/phpmyadmin 点击查看 免费下载 本指南以 phpMyAdmin 官方仓库中 src/Plugins/Export/README.md 为骨架,完整讲解如何为 ph…

阅读更多 →
Native SDK 外部源通道实战:用 ChannelHandle 构建零轮询的 channel-monitor 2026/9/27 7:46:38

Native SDK 外部源通道实战:用 ChannelHandle 构建零轮询的 channel-monitor

桌面应用跨平台 【免费下载链接】native Toolkit for building native desktop apps 项目地址: https://gitcode.com/gh_mirrors/ze/native 点击查看 免费下载 本文以仓库中的 channel-monitor 示例 为骨架,系统讲解 Native SDK 的「外部源通道&#xf…

阅读更多 →
做外贸站怕被黑?3个免费工具教你搞懂有哪些网站做国外生意的 2026/9/27 7:46:25

做外贸站怕被黑?3个免费工具教你搞懂有哪些网站做国外生意的

做外贸站怕被黑?3个免费工具教你搞懂有哪些网站做国外生意的 昨天凌晨三点,四川成都一位做汽配出口的老板给我打电话,声音都在抖。他网站首页突然变成了一堆乱码广告,点进去全是赌博链接。这就是典型的网站被黑挂马,很多做外贸的朋友遇到这种情况根本不…

阅读更多 →
BLE Host 层 L2CAP --介绍篇 2026/9/27 7:46:18

BLE Host 层 L2CAP --介绍篇

引言L2CAP 是 BLE Host 层的“数据调度中心”。它用 CID 区分不同数据流,用 PSM 标识上层服务,并让 ATT、SMP、CoC 等协议能够共享同一条物理链路。1. L2CAP 在 BLE 协议栈中的位置BLE 协议栈通常分为 Controller 和 Host 两大部分:Controlle…

阅读更多 →
Substrate pallet-staking 深度解析:NPoS 质押模块的角色模型、收益分配与惩罚机制 2026/9/27 7:46:12

Substrate pallet-staking 深度解析:NPoS 质押模块的角色模型、收益分配与惩罚机制

区块链开发框架后端 【免费下载链接】substrate Substrate: The platform for blockchain innovators 项目地址: https://gitcode.com/gh_mirrors/su/substrate 点击查看 免费下载 导读 本文以 Substrate 仓库中的 frame/staking/README.md 为骨架,结合…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

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

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