Linux内核驱动开发——动态加载驱动与platform
LKM(Linux 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.ko insmod xxx.ko→ LKM 模块入口函数执行- 模块代码里:
- 分配并构造
platform_device结构体,填写外设资源 platform_device_register()向 platform 总线注册这个设备- 注册对应的
platform_driver驱动 - 总线匹配成功,调用
probe函数,初始化硬件
- 分配并构造
rmmod xxx.ko→ 模块退出函数- 先 unregister platform 设备、释放资源
- 模块从内核卸载
注意顺序:可以先注册 driver 再注册 device,也可以反过来,platform 总线会自动做匹配。
为什么要这么做
驱动开发调试
在做 platform 驱动调试,每次改设备资源(寄存器基地址、中断号),如果用 DTS,必须重新编译 dtb、更新固件,非常麻烦。
动态注册方案:全部写在 ko 模块代码里,只需要重新编译 ko,insmod/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 = 0x20E0068 - end:资源结束物理地址 一段连续资源的最后一个地址。
32 位寄存器占 4 字节:start + 4 -1
公式:end = start + 长度 - 1 - name:资源名称字符串,方便驱动里区分多个资源("sw_mux"、"pad"),只是给人看的,内核不依赖这个名字匹配
- flags:资源类型标记,核心! 常用 flag:
IORESOURCE_MEM:内存映射 IO(绝大多数外设寄存器用这个!你代码这里 IORESOURCE_REG 很少用,推荐改成 IORESOURCE_MEM)IORESOURCE_IO:x86 传统 IO 端口,ARM 基本不用IORESOURCE_IRQ:中断资源IORESOURCE_REG:寄存器资源标记,平台设备可以用,但 i.MX 驱动惯例是IORESOURCE_MEM
struct 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.1- id=0/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_table,driver.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置1,GPIO1_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芯片 dtsi
- dtc 编译 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。
- 驱动代码编译成
.ko(LKM 模块),放在文件系统
编写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 点亮LED:GPIO输出低电平 */ static void led1_on(void) { *gpio1_dr &= ~(1 << 3); // 将bit3清零,输出低电平,LED点亮 } /* * @brief 熄灭LED:GPIO输出高电平 */ 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 release(close):应用调用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个u32(4组<物理地址,长度>) 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_driver
- platform 总线做匹配:拿 driver 里的
compatible字符串,和 DTB 生成的 platform_device 里compatible对比- 匹配成功 → 调用
probe函数 - probe 中:从 device 读取 reg / 中断资源,ioremap,申请中断,创建设备节点,初始化硬件

- 匹配成功 → 调用
rmmod xxx.ko:调用platform_driver_unregister(),执行remove,释放资源,卸载驱动模块;但 platform_device 还在(来自 dtb,不会消失)
关键点:rmmod 只是卸载驱动,DTB 生成的设备还在系统里。再 insmod,driver 重新注册,再次匹配、执行 probe。

更多推荐


所有评论(0)