嵌入式Linux_字符设备驱动开发_驱动开发案例_LED编写代码详解(直接操作寄存器版)

一、重温字符设备驱动开发步骤

①本贴的目的

紧接着上一个帖子的内容:嵌入式Linux_字符设备驱动开发流程_最简单|最完整|最入门|零基础|保姆级|驱动开发案例_LED_(通过TFTP启动Linux内核和NFS挂载根文件系统+LED驱动编写+亮灭应用测试)
我们将在本贴:
根据字符设备驱动开发的步骤,一点一点的详解我们编写的驱动代码。

②重温字符设备驱动开发编写步骤

Ⅰ、字符设备驱动开发程序编写步骤:
一、定义字符设备结构体(包含当前设备信息)
二、定义文件操作函数及其结构体
①定义文件操作函数
②定义文件操作函数结构体
三、注册字符设备(模块入口部分函数)
①分配设备号
a、静态分配设备号(主动定义)
b、动态分配设备号(申请自动分配)
②初始化字符设备(初始化字符设备结构体并绑定文件操作函数)
③注册字符设备到内核
④创建字符设备类
⑤在该类下创建字符设备节点
四、注销字符设备部分(模块出口部分函数)
①从内核中删除字符设备
②注销设备号
③销毁设备节点
④销毁设备类
五、模块的入口出口函数绑定
①module_init(模块入口部分函数)
②module_exit(模块出口部分函数)
六、结尾处添加许可证|作者信息|描述|版本信息

③重温测试APP编写步骤

Ⅱ、测试APP程序编写步骤:
一、检查参数数量是否正确
二、获取设备文件名
三、打开设备文件
四、将控制命令写入设备文件
五、关闭设备文件

④重温字符设备从编写到使用的过程

完整的测试过程:
Ⅰ、字符设备驱动开发程序编写步骤
Ⅱ、测试APP程序编写步骤
Ⅲ、编写Makefile编译驱动和APP
Ⅳ、在开发板中加载驱动
Ⅴ、使用APP测试驱动功能
Ⅵ、卸载驱动

二、代码详解

①驱动代码的完整呈现

led.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

/**********************************************************
Copyright © WangYongyang Co., Ltd. All rights reserved.
文件名:	newchrled.c
作者:	汪永阳
版本:	v1.0
描述:	字符设备驱动开发__点亮LED(来源:正点原子)
日志:	v1.0 2024/5/27 汪永阳创建
************************************************************/

#define LED_WYY_CNT 1		  	/* 设备号个数 */
#define LED_WYY_NAME "led_wyy"
#define LEDOFF 0			
#define LEDON 1		

#define CCM_CCGR1_BASE				(0X020C406C)	
#define SW_MUX_GPIO1_IO03_BASE		(0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE		(0X020E02F4)
#define GPIO1_DR_BASE				(0X0209C000)
#define GPIO1_GDIR_BASE				(0X0209C004)

static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

struct led_wyy_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;		/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
};

struct led_wyy_dev led_wyy;	
void led_switch(u8 sta)
{
	u32 val = 0;
	if(sta == LEDON) {
		val = readl(GPIO1_DR);
		val &= ~(1 << 3);	
		writel(val, GPIO1_DR);
	}else if(sta == LEDOFF) {
		val = readl(GPIO1_DR);
		val|= (1 << 3);	
		writel(val, GPIO1_DR);
	}	
}

static int led_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &led_wyy; 
	return 0;
}

static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;
	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) {
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}

	ledstat = databuf[0];
	if(ledstat == LEDON) {	
		led_switch(LEDON);
	} else if(ledstat == LEDOFF) {
		led_switch(LEDOFF);	
	}
	return 0;
}

static int led_release(struct inode *inode, struct file *filp)
{
	return 0;
}

static struct file_operations led_wyy_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.read = led_read,
	.write = led_write,
	.release = 	led_release,
};



static int __init led_init(void)
{
	u32 val = 0;
  	IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
	SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
  	SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
	GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
	GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

	val = readl(IMX6U_CCM_CCGR1);
	val &= ~(3 << 26);	
	val |= (3 << 26);	
	writel(val, IMX6U_CCM_CCGR1);

	writel(5, SW_MUX_GPIO1_IO03);

	writel(0x10B0, SW_PAD_GPIO1_IO03);

	val = readl(GPIO1_GDIR);
	val &= ~(1 << 3);	
	val |= (1 << 3);	
	writel(val, GPIO1_GDIR);

	val = readl(GPIO1_DR);
	val |= (1 << 3);	
	writel(val, GPIO1_DR);

	if (led_wyy.major) {
		led_wyy.devid = MKDEV(led_wyy.major, 0);
		register_chrdev_region(led_wyy.devid, LED_WYY_CNT, LED_WYY_NAME);
	} else {
		alloc_chrdev_region(&led_wyy.devid, 0, LED_WYY_CNT, LED_WYY_NAME);
		led_wyy.major = MAJOR(led_wyy.devid);	
		led_wyy.minor = MINOR(led_wyy.devid);	
	}

	printk("led_wyy major=%d,minor=%d\r\n",led_wyy.major, led_wyy.minor);	
	led_wyy.cdev.owner = THIS_MODULE;
	cdev_init(&led_wyy.cdev, &led_wyy_fops);
	cdev_add(&led_wyy.cdev, led_wyy.devid, LED_WYY_CNT);
	led_wyy.class = class_create(THIS_MODULE, LED_WYY_NAME);
	if (IS_ERR(led_wyy.class)) {
		return PTR_ERR(led_wyy.class);
	}
	led_wyy.device = device_create(led_wyy.class, NULL, led_wyy.devid, NULL, LED_WYY_NAME);
	if (IS_ERR(led_wyy.device)) {
		return PTR_ERR(led_wyy.device);
	}
	return 0;
}

static void __exit led_exit(void)
{
	iounmap(IMX6U_CCM_CCGR1);
	iounmap(SW_MUX_GPIO1_IO03);
	iounmap(SW_PAD_GPIO1_IO03);
	iounmap(GPIO1_DR);
	iounmap(GPIO1_GDIR);
	
	cdev_del(&led_wyy.cdev);
	unregister_chrdev_region(led_wyy.devid, LED_WYY_CNT);
	device_destroy(led_wyy.class, led_wyy.devid);
	class_destroy(led_wyy.class);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wangyongyang");
MODULE_DESCRIPTION("A simple Linux char driver");
MODULE_VERSION("1.0");

②驱动代码详解

-----------------------------【包含的头文件:】-----------------------------

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#include <linux/types.h>:定义了内核中常用的基本数据类型,如 u8、u32 等无符号整数类型和 s8、s32 等有符号整数类型。
#include <linux/kernel.h>:提供了许多核心内核函数和宏定义,例如 printk(内核打印函数)、container_of(获取包含某成员变量的结构体指针)等。
#include <linux/delay.h>:提供了内核中的延迟函数,如 msleep(毫秒级延迟)、udelay(微秒级延迟)等。
#include <linux/ide.h>:主要包含了与 IDE 硬盘接口相关的函数和数据结构。
#include <linux/init.h>:定义了模块初始化和退出的宏,如 module_init 和 module_exit,这些宏用于指定模块的初始化函数和退出函数。
#include <linux/module.h>:提供了模块编程的基础设施,定义了模块的宏、函数和数据结构,如 MODULE_LICENSE、MODULE_AUTHOR 等。
#include <linux/errno.h>:定义了错误代码,例如 -EFAULT、-ENOMEM 等,用于表示各种错误类型。
#include <linux/gpio.h>:提供了与 GPIO(通用输入输出)相关的接口和函数,如 gpio_request、gpio_direction_output 等,用于配置和操作 GPIO 引脚。
#include <linux/cdev.h>:提供了字符设备的接口和数据结构,如 struct cdev 和相关的函数 cdev_init、cdev_add、cdev_del 等。
#include <linux/device.h>:提供了设备模型相关的函数和数据结构,如 struct class 和 struct device,用于设备的创建和管理。
#include <asm/mach/map.h>:提供了内存映射相关的宏和函数,如 ioremap、iounmap 等,用于将物理地址映射到虚拟地址空间。
#include <asm/uaccess.h>:提供了用户空间与内核空间之间的数据拷贝函数,如 copy_from_user、copy_to_user 等。
#include <asm/io.h>:提供了 I/O 端口操作的函数和宏,用于读写 I/O 端口和内存映射的 I/O 寄存器,如 readl、writel 等。

----------------------------【宏定义】----------------------------

#define LED_WYY_CNT 1
#define LED_WYY_NAME "led_wyy"
#define LEDOFF 0			
#define LEDON 1	

#define CCM_CCGR1_BASE				(0X020C406C)	
#define SW_MUX_GPIO1_IO03_BASE		(0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE		(0X020E02F4)
#define GPIO1_DR_BASE				(0X0209C000)
#define GPIO1_GDIR_BASE				(0X0209C004)

这些宏定义了设备的基本信息和用于控制LED的寄存器地址。
#define LED_WYY_CNT 1 字符设备的数量
#define LED_WYY_NAME "led_wyy" 字符设备的命名
#define LEDOFF 0 关灯
#define LEDON 1开灯
#define CCM_CCGR1_BASE (0X020C406C) 控制时钟的寄存器基地址
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068) 设置GPIO复用的寄存器基地址
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4) 设置GPIO功能的寄存器基地址
#define GPIO1_DR_BASE (0X0209C000) 设置GPIO高低电平的寄存器基地址
#define GPIO1_GDIR_BASE (0X0209C004) 设置GPIO输入还是输出的寄存器基地址

【经过内存映射以后的寄存器地址指针】
这里要对地址映射进行一个简单的说明:
Linux驱动开发也可以像裸机开发一样操作寄存器,但Linux不能直接对寄存器物理地址进行读写操作。
在Linux里面操作的都是虚拟地址,所以需要先将物理地址映射到虚拟地址。
实现这一功能的,就是MMU
MMU(Memory Management Unit,内存管理单元)是一种硬件模块,用于在CPU和内存之间实现虚拟内存管理。其主要功能是将虚拟地址转换为物理地址,同时提供访问权限的控制和缓存管理等功能。(传统32不能跑linux系统就是因为没有mmu)
获得物理物理地址对应的虚拟地址就是使用ioremap函数。

static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

这些指针会在驱动程序初始化时通过 ioremap 函数进行映射:

IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

----------------------------【定义字符设备结构体】----------------------------

struct led_wyy_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;		/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
};

struct led_wyy_dev led_wyy;	

----------------------------【LED控制函数】----------------------------

void led_switch(u8 sta)
{
	u32 val = 0;
	if(sta == LEDON) {
		val = readl(GPIO1_DR);
		val &= ~(1 << 3);	
		writel(val, GPIO1_DR);
	}else if(sta == LEDOFF) {
		val = readl(GPIO1_DR);
		val|= (1 << 3);	
		writel(val, GPIO1_DR);
	}	
}

val = readl(GPIO1_DR);从 GPIO1_DR 寄存器读取当前的值,存储在 val 中。
val &= ~(1 << 3);使用按位或操作设置 val 中第3位(对应于GPIO1_IO03),这意味着将对应引脚设置为高电平,通常表示LED灭。
writel(val, GPIO1_DR);writel 函数是用于写入I/O内存地址的一个宏或内联函数。它通常包含在内核头文件 <asm/io.h> 中。它用于向特定的内存地址写入一个32位的值,通常用于操作硬件寄存器。
----------------------------【定义文件操作函数及其结构体】----------------------------

static int led_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &led_wyy; 
	return 0;
}

static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;
	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) {
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}

	ledstat = databuf[0];
	if(ledstat == LEDON) {	
		led_switch(LEDON);
	} else if(ledstat == LEDOFF) {
		led_switch(LEDOFF);	
	}
	return 0;
}

static int led_release(struct inode *inode, struct file *filp)
{
	return 0;
}

static struct file_operations led_wyy_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.read = led_read,
	.write = led_write,
	.release = 	led_release,
};

解析:

static int led_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &led_wyy; 
	return 0;
}

文件操作函数:打开
参数:
inode:指向文件的 inode 结构的指针。
filp:指向文件结构的指针。
作用:led_open 函数在用户进程打开设备文件时被调用。
filp->private_data = &led_wyy;它将设备的私有数据结构 led_wyy 赋值给文件结构的 private_data 成员,这样在后续的操作(如读、写)中可以访问该设备的私有数据。

static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

文件操作函数:读
参数:
filp:指向文件结构的指针。
buf:指向用户空间缓冲区的指针。
cnt:要读取的字节数。
offt:文件偏移量指针。
作用:led_read 函数在用户进程读取设备文件时被调用。此实现只是返回 0,表示没有数据可读。通常会将设备的状态信息复制到用户空间,这里什么也没操作,因为这里只有点灯,没有读取的数据。

static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;

	retvalue = copy_from_user(databuf, buf, cnt);
	if (retvalue < 0) {
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}
	ledstat = databuf[0];
	if (ledstat == LEDON) {	
		led_switch(LEDON);
	} else if (ledstat == LEDOFF) {
		led_switch(LEDOFF);	
	}
	return 0;
}

文件操作函数:写
参数:
filp:指向文件结构的指针。
buf:指向用户空间缓冲区的指针。
cnt:要写入的字节数。
offt:文件偏移量指针。
作用:led_write 函数在用户进程向设备文件写入数据时被调用。它将用户空间的数据复制到内核空间的 databuf 缓冲区中,并根据数据决定是打开还是关闭LED。
copy_from_user(databuf, buf, cnt);将用户空间的数据复制到内核空间。
根据 databuf[0] 的值,调用 led_switch 函数来打开或关闭LED。
return 0:写操作成功,返回 0。

static int led_release(struct inode *inode, struct file *filp)
{
	return 0;
}

文件操作函数:释放
参数:
inode:指向文件的 inode 结构的指针。
filp:指向文件结构的指针。
作用:led_release 函数在用户进程关闭设备文件时被调用。此实现不执行任何操作,只是返回 0。通常可以在这里释放资源。

static struct file_operations led_wyy_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.read = led_read,
	.write = led_write,
	.release = led_release,
};

led_wyy_fops 结构体定义了操作设备文件的接口函数,这些函数会在用户进程对设备文件进行相应操作时被调用。
----------------------------【注册字符设备(模块入口部分函数)】----------------------------

static int __init led_init(void)
{
	u32 val = 0;
  	IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
	SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
  	SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
	GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
	GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

	val = readl(IMX6U_CCM_CCGR1);
	val &= ~(3 << 26);	
	val |= (3 << 26);	
	writel(val, IMX6U_CCM_CCGR1);

	writel(5, SW_MUX_GPIO1_IO03);

	writel(0x10B0, SW_PAD_GPIO1_IO03);

	val = readl(GPIO1_GDIR);
	val &= ~(1 << 3);	
	val |= (1 << 3);	
	writel(val, GPIO1_GDIR);

	val = readl(GPIO1_DR);
	val |= (1 << 3);	
	writel(val, GPIO1_DR);

	if (led_wyy.major) {
		led_wyy.devid = MKDEV(led_wyy.major, 0);
		register_chrdev_region(led_wyy.devid, LED_WYY_CNT, LED_WYY_NAME);
	} else {
		alloc_chrdev_region(&led_wyy.devid, 0, LED_WYY_CNT, LED_WYY_NAME);
		led_wyy.major = MAJOR(led_wyy.devid);	
		led_wyy.minor = MINOR(led_wyy.devid);	
	}
	printk("led_wyy major=%d,minor=%d\r\n",led_wyy.major, led_wyy.minor);	
	
	led_wyy.cdev.owner = THIS_MODULE;
	cdev_init(&led_wyy.cdev, &led_wyy_fops);
	cdev_add(&led_wyy.cdev, led_wyy.devid, LED_WYY_CNT);
	led_wyy.class = class_create(THIS_MODULE, LED_WYY_NAME);
	if (IS_ERR(led_wyy.class)) {
		return PTR_ERR(led_wyy.class);
	}
	led_wyy.device = device_create(led_wyy.class, NULL, led_wyy.devid, NULL, LED_WYY_NAME);
	if (IS_ERR(led_wyy.device)) {
		return PTR_ERR(led_wyy.device);
	}
	return 0;
}

解析:

IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

将物理地址映射到内核虚拟地址空间,前面说过的。

val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26);  // 清除第26位和27位
val |= (3 << 26);   // 设置第26位和27位为1,使能GPIO1的时钟
writel(val, IMX6U_CCM_CCGR1);

val = readl(IMX6U_CCM_CCGR1);从寄存器的地址读回原本寄存器值
val &= ~(3 << 26); 第26位和27位置0
val |= (3 << 26); 设置第26位和27位为1,使能GPIO1的时钟
writel(val, IMX6U_CCM_CCGR1);将改变后的寄存器的值写回寄存器的地址

writel(5, SW_MUX_GPIO1_IO03); 

复用为GPIO1_IO03。

writel(0x10B0, SW_PAD_GPIO1_IO03); 
/*寄存器SW_PAD_GPIO1_IO03设置IO属性
 *bit 16:0 HYS关闭
 *bit [15:14]: 00 默认下拉
 *bit [13]: 0 kepper功能
 *bit [12]: 1 pull/keeper使能
 *bit [11]: 0 关闭开路输出
 *bit [7:6]: 10 速度100Mhz
 *bit [5:3]: 110 R0/6驱动能力
 *bit [0]: 0 低转换率
 */
val = readl(GPIO1_GDIR);
val &= ~(1 << 3); 
val |= (1 << 3); 
writel(val, GPIO1_GDIR);

类似的寄存器操作,配置GPIO1_IO03引脚为输出。

val = readl(GPIO1_DR);
val |= (1 << 3);   
writel(val, GPIO1_DR);

设置GPIO1_IO03引脚输出高电平,默认关闭LED。

if (led_wyy.major) {
		led_wyy.devid = MKDEV(led_wyy.major, 0);
		register_chrdev_region(led_wyy.devid, LED_WYY_CNT, LED_WYY_NAME);
	} else {
		alloc_chrdev_region(&led_wyy.devid, 0, LED_WYY_CNT, LED_WYY_NAME);
		led_wyy.major = MAJOR(led_wyy.devid);	
		led_wyy.minor = MINOR(led_wyy.devid);	
	}
printk("led_wyy major=%d,minor=%d\r\n",led_wyy.major, led_wyy.minor);

----------------------------【分配设备号】----------------------------
首先,根据是否已定义主设备号来决定分配设备号的方法。(推荐使用动态分配法)
分支1:已定义主设备号

if (led_wyy.major) {
    led_wyy.devid = MKDEV(led_wyy.major, 0);
    register_chrdev_region(led_wyy.devid, LED_WYY_CNT, LED_WYY_NAME);
}

led_wyy.major:表示主设备号。如果这个值不为0,说明主设备号已经定义。
MKDEV(led_wyy.major, 0):使用宏MKDEV将主设备号和次设备号(0)组合成设备号devid。
register_chrdev_region(led_wyy.devid, LED_WYY_CNT, LED_WYY_NAME):注册设备号区域。
LED_WYY_CNT表示要注册的连续设备号的数量,这里为1。
LED_WYY_NAME是设备的名称。

分支2:未定义主设备号

} else {
    alloc_chrdev_region(&led_wyy.devid, 0, LED_WYY_CNT, LED_WYY_NAME);
    led_wyy.major = MAJOR(led_wyy.devid);
    led_wyy.minor = MINOR(led_wyy.devid);
}

alloc_chrdev_region(&led_wyy.devid, 0, LED_WYY_CNT, LED_WYY_NAME):动态分配设备号,存储在led_wyy.devid中。这里0表示次设备号起始值,LED_WYY_CNT表示要分配的设备号数量,LED_WYY_NAME是设备的名称。
led_wyy.major = MAJOR(led_wyy.devid):从分配到的设备号中提取主设备号并赋值给led_wyy.major。
led_wyy.minor = MINOR(led_wyy.devid):从分配到的设备号中提取次设备号并赋值给led_wyy.minor。

printk("led_wyy major=%d,minor=%d\r\n",led_wyy.major, led_wyy.minor);

分配好设备号之后,打印主次设备号信息。
----------------------------【初始化字符设备结构体并绑定文件操作函数】----------------------------

led_wyy.cdev.owner = THIS_MODULE;
cdev_init(&led_wyy.cdev, &led_wyy_fops);

设置设备的所有者:
led_wyy.cdev.owner = THIS_MODULE;:将字符设备的所有者设置为当前内核模块,以便内核可以管理模块的引用计数,防止模块在设备使用期间被卸载。
初始化字符设备:
cdev_init(&led_wyy.cdev, &led_wyy_fops);:初始化字符设备结构体,并将文件操作函数 led_wyy_fops 与字符设备关联起来,使得对该字符设备的文件操作会调用相应的函数(如 open、read、write、release 等)。
----------------------------【注册字符设备到内核】----------------------------

cdev_add(&led_wyy.cdev, led_wyy.devid, LED_WYY_CNT);

cdev_add 函数将 cdev 结构体注册到内核的字符设备子系统,使得该字符设备可以被用户空间访问。成功调用 cdev_add 后,用户空间程序可以通过设备文件(如 /dev/led)访问该字符设备,并触发相应的文件操作函数(如 open、read、write、release 等)。
----------------------------【创建字符设备类】----------------------------

    led_wyy.class = class_create(THIS_MODULE, LED_WYY_NAME);
	if (IS_ERR(led_wyy.class)) {
		return PTR_ERR(led_wyy.class);
	}

创建设备类:
class_create 函数创建一个设备类 LED_WYY_NAME,并将其指针赋值给 led_wyy.class。
检查是否创建成功:
IS_ERR 宏检查 led_wyy.class 是否是错误指针。
如果是错误指针,表示类创建失败,使用 PTR_ERR 提取错误码并返回。
----------------------------【在该类下创建设备节点】----------------------------

	led_wyy.device = device_create(led_wyy.class, NULL, led_wyy.devid, NULL, LED_WYY_NAME);
	if (IS_ERR(led_wyy.device)) {
		return PTR_ERR(led_wyy.device);
	}

创建设备:
device_create 函数创建一个设备节点,并将其指针赋值给 led_wyy.device。
检查是否创建成功:
IS_ERR 宏检查 led_wyy.device 是否是错误指针。
如果是错误指针,表示设备创建失败,使用 PTR_ERR 提取错误码并返回。
----------------------------【注销字符设备部分(模块出口部分函数)】----------------------------

static void __exit led_exit(void)
{
	iounmap(IMX6U_CCM_CCGR1);
	iounmap(SW_MUX_GPIO1_IO03);
	iounmap(SW_PAD_GPIO1_IO03);
	iounmap(GPIO1_DR);
	iounmap(GPIO1_GDIR);
	
	cdev_del(&led_wyy.cdev);
	unregister_chrdev_region(led_wyy.devid, LED_WYY_CNT);
	device_destroy(led_wyy.class, led_wyy.devid);
	class_destroy(led_wyy.class);
}

解析:

	iounmap(IMX6U_CCM_CCGR1);
	iounmap(SW_MUX_GPIO1_IO03);
	iounmap(SW_PAD_GPIO1_IO03);
	iounmap(GPIO1_DR);
	iounmap(GPIO1_GDIR);

iounmap 用于解除对先前通过 ioremap 获取的 I/O 内存区域的映射。
cdev_del(&led_wyy.cdev);调用 cdev_del 删除字符设备结构体 led_wyy.cdev。
unregister_chrdev_region(led_wyy.devid, LED_WYY_CNT);释放先前分配的设备号 led_wyy.devid。
device_destroy(led_wyy.class, led_wyy.devid);调用 device_destroy 销毁通过 device_create 创建的设备节点。
class_destroy(led_wyy.class);调用 class_destroy 销毁通过 class_create 创建的设备类。
----------------------------【模块的入口出口函数绑定】----------------------------

module_init(led_init);
module_exit(led_exit);

初始化和清理:
module_init(led_init);: 当模块加载时,调用 led_init 函数。
module_exit(led_exit);: 当模块卸载时,调用 led_exit 函数。
----------------------------【许可证、作者、描述、版本】----------------------------

MODULE_LICENSE("GPL");
MODULE_AUTHOR("wangyongyang");
MODULE_DESCRIPTION("A simple Linux char driver");
MODULE_VERSION("1.0");

MODULE_LICENSE("GPL");指定模块使用 GPL 许可证。
MODULE_AUTHOR("wangyongyang");指定模块的作者为 wangyongyang。
MODULE_DESCRIPTION("A simple Linux char driver");描述模块为一个简单的 Linux 字符设备驱动程序。
MODULE_VERSION("1.0");指定模块的版本为 1.0。

③用户测试APP代码的完整呈现

ledapp.c

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

/**********************************************************
Copyright © WangYongyang Co., Ltd. All rights reserved.
文件名:	xxx
作者:	汪永阳
版本:	v1.0
描述:	LED_APP_test
日志:	v1.0 2024/5/27 汪永阳创建
************************************************************/

#define LEDOFF 	0
#define LEDON 	1

int main(int argc, char *argv[])
{
	int fd, retvalue;
	char *filename;
	unsigned char databuf[1];
	
	if(argc != 3){
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];

	fd = open(filename, O_RDWR);
	if(fd < 0){
		printf("file %s open failed!\r\n", argv[1]);
		return -1;
	}

	databuf[0] = atoi(argv[2]);

	retvalue = write(fd, databuf, sizeof(databuf));
	if(retvalue < 0){
		printf("LED Control Failed!\r\n");
		close(fd);
		return -1;
	}

	retvalue = close(fd); 
	if(retvalue < 0){
		printf("file %s close failed!\r\n", argv[1]);
		return -1;
	}
	return 0;
}

④用户测试APP代码详解

----------------------------【头文件】----------------------------

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

stdio.h:标准输入输出函数,如 printf。
unistd.h:POSIX 操作系统 API 函数,如 close、write。
sys/types.h 和 sys/stat.h:文件系统数据类型和操作函数。
fcntl.h:文件控制函数,如 open。
stdlib.h:标准库函数,如 atoi。
string.h:字符串操作函数。
----------------------------【检查参数数量是否正确】----------------------------

	if(argc != 3){
		printf("Error Usage!\r\n");
		return -1;
	}

检查命令行参数的数量是否正确。如果参数数量不为 3,打印错误信息并退出程序。
----------------------------【获取设备文件名】----------------------------

	filename = argv[1];

从命令行参数获取设备文件名。
----------------------------【打开设备文件】----------------------------

fd = open(filename, O_RDWR);
if(fd < 0){
	printf("file %s open failed!\r\n", argv[1]);
	return -1;
}

尝试以读写模式打开指定的设备文件。如果打开失败,打印错误信息并退出程序。
----------------------------【读取命令参数】----------------------------

databuf[0] = atoi(argv[2]);

将命令行参数(打开或关闭命令)转换为整数,并存储在 databuf 中。
----------------------------【将控制命令写入设备文件】----------------------------

	retvalue = write(fd, databuf, sizeof(databuf));
	if(retvalue < 0){
		printf("LED Control Failed!\r\n");
		close(fd);
		return -1;
	}

将 databuf 中的值写入设备文件,以控制 LED 的状态。如果写入失败,打印错误信息并关闭文件。
----------------------------【关闭设备文件】----------------------------

retvalue = close(fd); 
if(retvalue < 0){
	printf("file %s close failed!\r\n", argv[1]);
	return -1;
}

三、使用方法及其响应过程

./ledapp /dev/led_wyy 1  # 打开LED
./ledapp /dev/led_wyy 0  # 关闭LED

./ledapp /dev/led_wyy 1执行ledapp文件,打开设备文件/dev/led_wyy,并写入数据1.

在这个例子中,/dev/led_wyy 是一个设备文件,而非普通文件。设备文件通常代表外设(如硬件设备),其操作会被内核中的设备驱动程序截获并处理:
/dev/led_wyy 与字符设备驱动led_wyy关联,该驱动实现了文件操作接口(open、read、write、close 等)。
例如,当 open 被调用时,内核会调用相应的设备驱动的 led_open 函数。
具体的设备驱动关联:
在驱动代码中,字符设备驱动通过 file_operations 结构体定义了对应的操作:

static struct file_operations led_wyy_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .read = led_read,
    .write = led_write,
    .release = led_release,
};

打开设备文件/dev/led_wyy—即应用程序调用 open("/dev/led_wyy", O_RDWR) 时,内核会调用 led_open 函数进行设备文件的打开操作。
写入数据1即写操作函数write(fd, databuf, sizeof(databuf)) 调用驱动程序的 led_write 函数:
./ledapp /dev/led_wyy 1通过向设备文件写入数据1,调用驱动程序的 led_write 方法,实现对 LED 状态的控制。
copy_from_user(databuf, buf, cnt) 将用户空间的数据拷贝到内核空间。
根据 databuf[0] 的值(1),调用 led_switch(LEDON) 打开 LED。

欢迎评论区纠错和讨论问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

善于伴随

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值