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 外设等设计的虚拟总线,经典用法两种:

  1. 静态方式(设备树 DTS):在 dts 里写platform_device节点,内核启动解析 DTS,自动创建 platform 设备;驱动写platform_driver,总线匹配后执行 probe。

设备是内核启动阶段一次性建好,写死在设备树里,要增删设备就得改 DTS、重编 dtb。

  1. 动态注册:在内核代码里调用 platform_device_alloc() → 填充资源(IO 地址、中断、寄存器、自定义数据)→ platform_device_register(),在代码里手动生成 platform 设备,不依赖 DTS 节点。

设备不是来自 dtb,是模块跑起来之后,在内存动态生成。

LKM 模块内部动态注册 platform 设备

流程:

  1. 编译得到xxx.ko
  2. insmod xxx.ko → LKM 模块入口函数执行
  3. 模块代码里:
    • 分配并构造platform_device结构体,填写外设资源
    • platform_device_register() 向 platform 总线注册这个设备
    • 注册对应的platform_driver驱动
    • 总线匹配成功,调用probe函数,初始化硬件
  4. 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; // 内核资源树链表,用户驱动不用管
};
  1. start:资源起始物理地址 寄存器基地址,例 GPIO1_IO3_SW_MUX = 0x20E0068
  2. end:资源结束物理地址 一段连续资源的最后一个地址。
    32 位寄存器占 4 字节:start + 4 -1
    公式:end = start + 长度 - 1
  3. name:资源名称字符串,方便驱动里区分多个资源("sw_mux"、"pad"),只是给人看的,内核不依赖这个名字匹配
  4. flags:资源类型标记,核心! 常用 flag:
    1. IORESOURCE_MEM:内存映射 IO(绝大多数外设寄存器用这个!你代码这里 IORESOURCE_REG 很少用,推荐改成 IORESOURCE_MEM)
    2. IORESOURCE_IO:x86 传统 IO 端口,ARM 基本不用
    3. IORESOURCE_IRQ:中断资源
    4. 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;
};
  1. const char *name 平台总线匹配的核心。 内核平台总线做匹配规则:platform_driver里面.name和platform_device的.name字符串完全相等,就触发probe函数。 

  2. int id 设备编号。

  • id = -1:表示只创建单个设备,设备名就是led1,不是led1.0、led1.1
  • id=0/1/2:多个同名字设备,生成 led1.0、led1.1 多个设备
  1. struct device dev 内嵌通用device结构体,所有 Linux 设备都继承这个基础设备模型。
  • .release = release:必须设置! 内核设备模型:当设备引用计数减到 0,就调用 release 回调函数,用于资源释放。如果不写,内核会报 oops 或者警告。
  1. u32 num_resources 资源数组里面一共有多少个struct resource元素。 sizeof(res)/sizeof(res[0])自动计算数组元素个数。

  2. 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动态加载驱动程序

  1. 编写板级xxx.dts,添加 platform 设备节点,写compatible、reg、interrupt 等硬件资源;#include芯片 dtsi

  2. dtc 编译 dts → xxx.dtb二进制,烧录到板子
    make dtbs      编译所有的设备树文件
    make xxx.dtb   编译xxx.dts生成xxx.dtb

    编译之前修改makefile文件,添加对应的dtb编译依赖选项

  3. U-Boot 启动,把内核镜像 zImage + dtb加载到内存,启动 Linux 内核

  4. 内核启动早期解析 DTB,遍历所有节点,遇到 platform 类型节点,自动创建platform_device,挂到 platform 总线。这一步不需要驱动,只要内核读到 dtb,设备就已经存在总线上,静静等待 driver。

  5. 驱动代码编译成.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警告
  6. 执行 insmod xxx.ko:模块入口module_init运行,调用platform_driver_register(),向 platform 总线注册platform_driver
     
  7. platform 总线做匹配:拿 driver 里的compatible字符串,和 DTB 生成的 platform_device 里compatible对比
    • 匹配成功 → 调用probe函数
    • probe 中:从 device 读取 reg / 中断资源,ioremap,申请中断,创建设备节点,初始化硬件

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

更多推荐