Linux 字符设备驱动(旧)

本文详细介绍了Linux字符设备驱动的开发过程,包括file_operations结构体的作用,驱动模块的加载与卸载,字符设备的注册与注销,以及如何添加设备操作函数。还涉及到设备号的概念和分配方法,以及通过实例展示了如何编写和编译字符设备驱动代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

字符设备驱动

字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。

file_operations结构体

每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合

struct file_operations {
	struct module *owner;  // 拥有该结构体模块的指针,一般设置为 THIS_MODULE
	loff_t (*llseek) (struct file *, loff_t, int); // 修改文件读写位置函数
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); // 读
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); // 写
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iterate) (struct file *, struct dir_context *);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
    // poll 轮询函数,用于查询设备是否可以进行非阻塞的读写
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    // unlocked_ioctl函数提供对于设备的控制功能,与应用程序中的ioctl函数对应。32位系统程序调用
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    // compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于这个是64位系统的程序调用
	int (*mmap) (struct file *, struct vm_area_struct *);
    // mmap 函数用于将将设备的内存映射到进程空间中(也就是用户空间)
	int (*mremap)(struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *); // 打开设备
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
    // release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*aio_fsync) (struct kiocb *, int datasync);
    // aio_fsync 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的数据。
	int (*fasync) (int, struct file *, int);
    // fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **, void **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
};

字符驱动开发步骤

驱动模块的加载与卸载

#include <linux/init.h>  // module_init、module_exit函数的头文件所在地

/*驱动入口函数 当使用“insmod mydev.ko”命令或“modprobe mydev.ko”加载驱动的时候,
    mydev_init 这个函数就会被调用*/
static int __init mydev_init(void)
{
    return 0;
}

/*驱动出口函数 当使用“rmmod mydev.ko”命令或者“modprobe -r mydev.ko”卸载具体驱动的时候 
	mydev_exit 函数就会被调用*/
static void __exit mydev_exit(void)
{
}

module_init(mydev_init); // 注册加载函数
module_exit(mydev_exit); // 注册卸载函数

字符设备注册于注销

当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。在/linux/fs.h中是这样定义的。

#include <linux/fs.h>


/*注册字符设备 返回值小于0表示注册失败*/
static inline int register_chrdev(unsigned int major, const char *name,
				  const struct file_operations *fops)
{
	return __register_chrdev(major, 0, 256, name, fops);
}

/*unregister_chrdev 函数用户注销字符设备*/
static inline void unregister_chrdev(unsigned int major, const char *name)
{
	__unregister_chrdev(major, 0, 256, name);
}
// major:主设备号,Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分
// 输入命令“cat /proc/devices”可以查看当前已经被使用掉的设备号
// name:设备名字,指向一串字符串。
// fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量
#include <linux/init.h>  // module_init、module_exit函数的头文件所在地
#include <linux/fs.h>   //添加文件描述符,设备注册注销头文件

#define MYMAJOR	 	200
#define MYNAME		"my_dev"

static struct file_operations my_fops;

/*驱动入口函数 当使用“insmod mydev.ko”命令或“modprobe mydev.ko”加载驱动的时候,
    mydev_init 这个函数就会被调用*/
static int __init mydev_init(void)
{
    int ret = 0;
    ret = register_chrdev(MYMAJOR,MYNAME,&my_fops);
    if(ret < 0)
    {
        printk(KERN_ERR "register_chrdev fail\n");
    }
    printk(KERN_INFO "register_chrdev success...\n");
    return 0;
}

/*驱动出口函数 当使用“rmmod mydev.ko”命令或者“modprobe -r mydev.ko”卸载具体驱动的时候 
	mydev_exit 函数就会被调用*/
static void __exit mydev_exit(void)
{
    printk(KERN_INFO "mydev_exit\n");
    unregister_chrdev(MYMAJOR,MYNAME);
}

module_init(mydev_init); // 注册加载函数
module_exit(mydev_exit); // 注册卸载函数

添加设备具体操作函数

#include <linux/init.h>  // module_init、module_exit函数的头文件所在地
#include <linux/fs.h>   //添加文件描述符,设备注册注销头文件

#define MYMAJOR	 	200
#define MYNAME		"my_dev"

/*打开设备*/
static int my_dev_open (struct inode *inode, struct file *file)
{
    return 0;
}
/*读设备*/
static ssize_t my_dev_read (struct file *file, char __user *buf, 
                            size_t cnt, loff_t * offt)
{
    return 0;
}
/*写设备*/
static ssize_t my_dev_write (struct file *file, const char __user *buf,
                         size_t cnt, loff_t *offt)
{
    return 0;
}
/*释放设备*/
static int my_dev_close (struct inode *inode, struct file *file)
{
    return 0;
}

static struct file_operations my_fops = {
    .owner		= THIS_MODULE,		// 惯例,直接写即可
	.open		= my_dev_open,		// 打开设备
    .read       = my_dev_read,		// 读设备
    .write		= my_dev_write,		// 写设备
	.release	= my_dev_close,		// 关闭设备
};

/*驱动入口函数 当使用“insmod mydev.ko”命令或“modprobe mydev.ko”加载驱动的时候,
    mydev_init 这个函数就会被调用*/
static int __init mydev_init(void)
{
    int ret = 0;
    ret = register_chrdev(MYMAJOR,MYNAME,&my_fops);
    if(ret < 0)
    {
        printk(KERN_ERR "register_chrdev fail\n");
    }
    printk(KERN_INFO "register_chrdev success...\n");
    return 0;
}

/*驱动出口函数 当使用“rmmod mydev.ko”命令或者“modprobe -r mydev.ko”卸载具体驱动的时候 
	mydev_exit 函数就会被调用*/
static void __exit mydev_exit(void)
{
    printk(KERN_INFO "mydev_exit\n");
    unregister_chrdev(MYMAJOR,MYNAME);
}

module_init(mydev_init); // 注册加载函数
module_exit(mydev_exit); // 注册卸载函数

添加 LICENSE 和作者信息(字符设备模板)

最后我们需要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加。此时最后就得到了一个字符设备的基础模板。

#include <linux/init.h>  // module_init、module_exit函数的头文件所在地
#include <linux/fs.h>   //添加文件描述符,设备注册注销头文件
#include <linux/module.h>  // 添加module_XXX宏定义头文件
   
#define MYMAJOR	 	200
#define MYNAME		"my_dev"

/*打开设备*/
static int my_dev_open (struct inode *inode, struct file *file)
{
    return 0;
}
/*读设备*/
static ssize_t my_dev_read (struct file *file, char __user *buf, 
                            size_t cnt, loff_t * offt)
{
    return 0;
}
/*写设备*/
static ssize_t my_dev_write (struct file *file, const char __user *buf,
                         size_t cnt, loff_t *offt)
{
    return 0;
}
/*释放设备*/
static int my_dev_close (struct inode *inode, struct file *file)
{
    return 0;
}

static struct file_operations my_fops = {
    .owner		= THIS_MODULE,		// 惯例,直接写即可
	.open		= my_dev_open,		// 打开设备
    .read       = my_dev_read,		// 读设备
    .write		= my_dev_write,		// 写设备
	.release	= my_dev_close,		// 关闭设备
};

/*驱动入口函数 当使用“insmod mydev.ko”命令或“modprobe mydev.ko”加载驱动的时候,
    mydev_init 这个函数就会被调用*/
static int __init mydev_init(void)
{
    int ret = 0;
    ret = register_chrdev(MYMAJOR,MYNAME,&my_fops);
    if(ret < 0)
    {
        printk(KERN_ERR "register_chrdev fail\n");
    }
    printk(KERN_INFO "register_chrdev success...\n");
    return 0;
}

/*驱动出口函数 当使用“rmmod mydev.ko”命令或者“modprobe -r mydev.ko”卸载具体驱动的时候 
	mydev_exit 函数就会被调用*/
static void __exit mydev_exit(void)
{
    printk(KERN_INFO "mydev_exit\n");
    unregister_chrdev(MYMAJOR,MYNAME);
}

module_init(mydev_init); // 注册加载函数
module_exit(mydev_exit); // 注册卸载函数

// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL");				// 描述模块的许可证
MODULE_AUTHOR("dongfang");			// 描述模块的作者
MODULE_DESCRIPTION("module test");	// 描述模块的介绍信息
MODULE_ALIAS("------");				// 描述模块的别名信息

设备号

Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。在/linux/types.h中定义了typedef __u32 __kernel_dev_t;typedef __kernel_dev_t dev_t;dev_t是32位的数据类型,其中高 12 位为主设备号,低 20 位为次设备号。因此 Linux系统中主设备号范围为 0~4095。

设备号组成

在文件 include/linux/kdev_t.h 中提供了几个关于设备号的操作函数(本质是宏)

#define MINORBITS	20   // 次设备号位数
#define MINORMASK	((1U << MINORBITS) - 1) // 次设备号掩码

#define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))  // 主设备号
#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))   // 次设备号
#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))       // 组成设备号dev_t

设备号分配

  • 静态分配

    也就是上面模板的形式。一般要查看cat /proc/devices中已经使用的,不要冲突。

    // 静态申请设备号
    int register_chrdev_region(dev_t from, unsigned count, const char *name)
    // from 起始设备号
    // count 设备数量
    // name 设备名字
    // 成功返回0,失败返回负数
    /*设备号的释放函数*/
    void unregister_chrdev_region(dev_t from, unsigned count);
    // from:要释放的设备号。
    // count:表示从 from 开始,要释放的设备号数量。
    
  • 动态分配

    在注册字设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可。

    #include <linux/fs.h> // 头文件包含
    /*设备号的申请函数*/
    int alloc_chrdev_region(dev_t *dev, unsigned baseminor, 
                            unsigned count, const char *name);
    // dev:保存申请到的设备号。传出参数
    // baseminor:次设备号起始地址,alloc_chrdev_region 可以申请一段连续的多个设备号,
    // 这些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递增。
    // 一般 baseminor 为 0,也就是说次设备号从 0 开始。
    // count:要申请的设备号数量。
    // name:设备名字
    
    /*设备号的释放函数*/
    void unregister_chrdev_region(dev_t from, unsigned count);
    // from:要释放的设备号。
    // count:表示从 from 开始,要释放的设备号数量。
    

字符设备驱动实验(模拟)

my_dev 设备有两个缓冲区,一个为读缓冲区,一个为写缓冲区,这两个缓冲区的大小都为 100 字节。在应用程序中可以向 my_dev 设备的写缓冲区中写入数据,从读缓冲区中读取数据。

my_dev 驱动代码编写

#include <linux/init.h>   // module_init、module_exit函数的头文件所在地
#include <linux/fs.h>     //添加文件描述符,设备注册注销头文件
#include <linux/module.h> // 添加module_XXX宏定义头文件
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>

#define MYMAJOR 200
#define MYNAME "my_dev"

static char read_buf[100];  // 读缓冲区
static char write_buf[100]; // 写缓冲区
static char kernel_data[] = {"kernel data!"}; 

/*打开设备*/
static int my_dev_open(struct inode *inode, struct file *file)
{
    printk("chrdevbase open!\r\n");
    return 0;
}
/*读设备*/
static ssize_t my_dev_read(struct file *file, char __user *buf,
                           size_t cnt, loff_t *offt)
{
    int ret = 0;
    /*向用户空间发送数据*/
    memcpy(read_buf,kernel_data,sizeof(kernel_data));
    ret = copy_to_user(buf,read_buf,cnt);
    if(ret == 0)
    {
        printk(KERN_INFO "read success...\n");
    }else
    {
        printk(KERN_INFO "read fail\n");
    }
    return 0;
}
/*写设备*/
static ssize_t my_dev_write(struct file *file, const char __user *buf,
                            size_t cnt, loff_t *offt)
{
    int ret = 0;
    /*接收用户空间的数据*/
    ret = copy_from_user(write_buf,buf,cnt);
    if(ret == 0)
    {
        /*将写入的数据打印出来*/
        printk(KERN_INFO "write success %s ...\n",write_buf); 
    }else
    {
        printk(KERN_INFO "write fail\n");
    }
    return 0;
}
/*释放设备*/
static int my_dev_close(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "release dev \n");
    return 0;
}

static struct file_operations my_fops = {
    .owner = THIS_MODULE,    // 惯例,直接写即可
    .open = my_dev_open,     // 打开设备
    .read = my_dev_read,     // 读设备
    .write = my_dev_write,   // 写设备
    .release = my_dev_close, // 关闭设备
};

/*驱动入口函数 当使用“insmod my_dev.ko”命令或“modprobe my_dev.ko”加载驱动的时候,
    mydev_init 这个函数就会被调用*/
static int __init mydev_init(void)
{
    int ret = 0;
    ret = register_chrdev(MYMAJOR,MYNAME,&my_fops);
    if(ret < 0)
    {
        printk(KERN_ERR "register_chrdev fail\n");
    }
    printk(KERN_INFO "register_chrdev success...\n");
    return 0;
}

/*驱动出口函数 当使用“rmmod my_dev.ko”命令或者“modprobe -r my_dev.ko”卸载具体驱动的时候 
	mydev_exit 函数就会被调用*/
static void __exit mydev_exit(void)
{
    printk(KERN_INFO "mydev_exit\n");
    unregister_chrdev(MYMAJOR,MYNAME);
}

module_init(mydev_init); // 注册加载函数
module_exit(mydev_exit); // 注册卸载函数

// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL");             // 描述模块的许可证
MODULE_AUTHOR("dongfang");         // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("------");            // 描述模块的别名信息

Makefile编写

KERNELDIR := /home/haitu/Desktop/Linux/NXP/NXP_linux
CURRENT_PATH := $(shell pwd)
obj-m := my_dev.o

build: kernel_modules
kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

APP 代码编写

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

static char usr_data[] = {"usr data!"};

int main(int argc, char* argv[])
{
    int fd, ret;
    char* file_name;
    char read_buf[100],write_buf[100];

    if(argc != 3)  // 命令行参数过少
    {
        printf("used error \n");
        return -1;
    }

    file_name = argv[1];

    fd = open(file_name,O_RDWR);  // 打开设备文件

    if(atoi(argv[2]) == 1) // 读取数据
    {
        ret = read(fd,read_buf,50);
        if(ret < 0)
        {
            printf("read error \n");
        }else
        {
            printf("read success %s \n",read_buf);
        }
    }

    if(atoi(argv[2]) == 2) // 写数据
    {
        memcpy(write_buf,usr_data,sizeof(usr_data));
        ret = write(fd,write_buf,50);
        if(ret < 0)
        {
            printf("write file %s err \n",file_name);
        }
    }

    ret = close(fd);
    if(ret < 0)
    {
        printf("close error\n");
        return -1;
    }

    return 0;
}

编译:arm-linux-gnueabihf-gcc APP.c -o APPfile APP显示的信息说明这个可执行文件是 32 位 LSB 格式,ARM 版本的,因此 APP 只能在 ARM 芯片下运行。执行APP文件./APP my_dev 1。使用modprobe加载驱动,需要手动创建设备节点,mknod /dev/my_dev c 200 0

关于printk

printk 相当于 printf 的孪生兄妹,printf 运行在用户态,printk 运行在内核态。printk 可以根据日志级别对消息进行分类,一共有 8 个消息级别,这 8 个消息级别定义在文件 include/linux/kern_levels.h 里面

#define KERN_EMERG	KERN_SOH "0"	/*  紧急事件,一般是内核崩溃*/
#define KERN_ALERT	KERN_SOH "1"	/*  必须立即采取行动*/
#define KERN_CRIT	KERN_SOH "2"	/*  临界条件,比如严重的软件或硬件错误*/
#define KERN_ERR	KERN_SOH "3"	/*  错误状态,一般设备驱动程序中使用KERN_ERR 报告硬件错误*/
#define KERN_WARNING	KERN_SOH "4"	/* 警告信息,不会对系统造成严重影响 */
#define KERN_NOTICE	KERN_SOH "5"	/* 有必要进行提示的一些信息 */
#define KERN_INFO	KERN_SOH "6"	/* 提示性的信息 */
#define KERN_DEBUG	KERN_SOH "7"	/* 调试信息 默认就是这个级别的 */

// 在linux/printk.h文件中定义如下,#define CONSOLE_LOGLEVEL_DEFAULT 7
printk("KERN_DEBUG : ------ ");
printk(KERN_INFO"KERN_INFO : ------ ");

LED 驱动实验(直接操作寄存器)

重点就是编写 LinuxI.MX6UL 引脚控制驱动。

地址映射

MMU 全称叫做 Memory Manage Unit,也就是内存管理单元。在老版本的 Linux 中要求处理器必须有 MMU,但是现在Linux 内核已经支持无 MMU 的处理器了。MMU 主要完成的功能如下:

①、完成虚拟空间到物理空间的映射。

②、内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。

虚拟地址(VA,Virtual Address)、物理地址(PA,PhyscicalAddress)。对于 32 位的处理器来说,虚拟地址范围是 2^32=4GB,我们的开发板上有 512MB 的 DDR3,这 512MB 的内存就是物理内存,经过 MMU 可以将其映射到整个 4GB 的虚拟空间。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z3FojHBU-1681260356942)(D:\Soft\Typora\Picture\image-20230410085909478.png)]

开启MMU后,配置GPIO就不能直接使用物理地址进行配置,而是要将物理地址转换为虚拟地址,在进行配置。

  • ioremap
// 1、ioremap 函数
#define ioremap(cookie,size)		__arm_ioremap((cookie), (size), MT_DEVICE)

void __iomem *__arm_ioremap(phys_addr_t phys_addr, size_t size, 
                            unsigned int mtype)
{
	return arch_ioremap_caller(phys_addr, size, mtype,
		__builtin_return_address(0));
}
// phys_addr:要映射给的物理起始地址。
// size:要映射的内存空间大小。
// mtype:ioremap 的类型,可以选择 MT_DEVICE、MT_DEVICE_NONSHARED、
// MT_DEVICE_CACHED 和 MT_DEVICE_WC,ioremap 函数选择 MT_DEVICE。
// 返回值:__iomem 类型的指针,指向映射后的虚拟空间首地址。
#define MX53_DPLL1_BASE		0x63f80000
#define SZ_16K				0x00004000
static void __iomem *pll_base;
pll_base = ioremap(MX53_DPLL1_BASE, SZ_16K);
// MX53_DPLL1_BASE映射16K的虚拟地址,首地址为pll_base
  • iounmap
// 2、iounmap 函数
void iounmap (volatile void __iomem *addr)
// iounmap 只有一个参数 addr,此参数就是要取消映射的虚拟地址空间首地址
iounmap(pll_base);  // 取消虚拟地址pll_base

I/O 内存访问函数

使用 ioremap 函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux 内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。

// 读操作函数
static inline u8 readb(const volatile void __iomem *addr);
static inline u16 readw(const volatile void __iomem *addr);
static inline u32 readl(const volatile void __iomem *addr);
// readb、readw 和 readl 这三个函数分别对应 8bit、16bit 和 32bit 读操作
// 参数 addr 就是要读取写内存地址
// 返回值就是读取到的数据

// 写操作函数
static inline void writeb(u8 value, volatile void __iomem *addr)
static inline void writew(u16 value, volatile void __iomem *addr)
static inline void writel(u32 value, volatile void __iomem *addr)
// writeb、writew 和 writel 这三个函数分别对应 8bit、16bit 和 32bit 写操作
// 参数 value 是要写入的数值,addr 是要写入的地址

LED 灯驱动程序编写

#include <linux/init.h>   // module_init、module_exit函数的头文件所在地
#include <linux/fs.h>     //添加文件描述符,设备注册注销头文件
#include <linux/module.h> // 添加module_XXX宏定义头文件
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/device.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <asm/io.h>   // 包含readl等IO操作函数
#include <linux/gpio.h>
#include <linux/errno.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>

#define MYMAJOR 200
#define MYNAME "my_led"

#define LED_OFF     0   /* 关灯 */
#define LED_ON      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;   // 设置IO时钟虚拟地址
static void __iomem *SW_MUX_GPIO1_IO03; // 设置IO复用虚拟地址
static void __iomem *SW_PAD_GPIO1_IO03; // 设置IO属性虚拟地址
static void __iomem *GPIO1_DR;          // 设置IO输出功能虚拟地址
static void __iomem *GPIO1_GDIR;        // led虚拟地址

void led_switch(u8 sta)
{
    u32 val = 0;
    if(sta == LED_ON)  /*开灯*/
    {
        val = readl(GPIO1_DR);
        val &= ~(1 << 3); 
        writel(val, GPIO1_DR);
    }else if(sta == LED_OFF)  /*关灯*/
    {
        val = readl(GPIO1_DR);
        val|= (1 << 3);
        writel(val, GPIO1_DR);
    } 
}

/*打开设备*/
static int my_dev_open(struct inode *inode, struct file *file)
{
    printk("chrdevbase open!\r\n");
    return 0;
}
/*读设备*/
static ssize_t my_dev_read(struct file *file, char __user *buf,
                           size_t cnt, loff_t *offt)
{
    return 0;
}
/*写设备*/
static ssize_t my_dev_write(struct file *file, const char __user *buf,
                            size_t cnt, loff_t *offt)
{
    int ret = 0;
    unsigned char data_buf[1];
    unsigned char led_stat;
    /*接收用户空间的数据*/
    ret = copy_from_user(data_buf,buf,cnt);
    if(ret < 0)
    {
        printk(KERN_INFO "write fail ...\n");
        return -EFAULT; 
    }

    led_stat = data_buf[0]; /* 获取状态值 */

    if(led_stat == LED_ON)
    {
        led_switch(LED_ON);
    }else if(led_stat == LED_OFF)
    {
        led_switch(LED_OFF);
    }

    return 0;
}
/*释放设备*/
static int my_dev_close(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "release dev \n");
    printk("------- %s -------\n", __FUNCTION__);
    return 0;
}

static struct file_operations my_fops = {
    .owner = THIS_MODULE,    // 惯例,直接写即可
    .open = my_dev_open,     // 打开设备
    .read = my_dev_read,     // 读设备
    .write = my_dev_write,   // 写设备
    .release = my_dev_close, // 关闭设备
};

/*驱动入口函数 当使用“insmod my_dev.ko”命令或“modprobe my_dev.ko”加载驱动的时候,
    mydev_init 这个函数就会被调用*/
static int __init mydev_init(void)
{
    int ret = 0;
    u32 val = 0;

    /* 初始化 LED */
    /* 1、寄存器地址映射 */
    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);

    /* 2、使能 GPIO1 时钟 */
    val = readl(IMX6U_CCM_CCGR1);
    val &= ~(3 << 26); /* 清除以前的设置 */
    val |= (3 << 26); /* 设置新值 */
    writel(val, IMX6U_CCM_CCGR1);

    /* 3、设置 GPIO1_IO03 的复用功能,将其复用为
    * GPIO1_IO03,最后设置 IO 属性。
    */
    writel(5, SW_MUX_GPIO1_IO03);

    /* 寄存器 SW_PAD_GPIO1_IO03 设置 IO 属性 */
    writel(0x10B0, SW_PAD_GPIO1_IO03);

    /* 4、设置 GPIO1_IO03 为输出功能 */
    val = readl(GPIO1_GDIR);
    val &= ~(1 << 3); /* 清除以前的设置 */
    val |= (1 << 3); /* 设置为输出 */
    writel(val, GPIO1_GDIR);

    /* 5、默认关闭 LED */
    val = readl(GPIO1_DR);
    val |= (1 << 3); 
    writel(val, GPIO1_DR);

    /* 6、字符设备注册 */
    ret = register_chrdev(MYMAJOR,MYNAME,&my_fops);
    if(ret < 0)
    {
        printk(KERN_ERR "register_chrdev fail\n");
    }
    printk(KERN_INFO "register_chrdev success...\n");
    return 0;
}

/*驱动出口函数 当使用“rmmod my_dev.ko”命令或者“modprobe -r my_dev.ko”卸载具体驱动的时候 
	mydev_exit 函数就会被调用*/
static void __exit mydev_exit(void)
{
    /* 取消映射 */
    iounmap(IMX6U_CCM_CCGR1);
    iounmap(SW_MUX_GPIO1_IO03);
    iounmap(SW_PAD_GPIO1_IO03);
    iounmap(GPIO1_DR);
    iounmap(GPIO1_GDIR);
    
    printk(KERN_INFO "mydev_exit\n");
    unregister_chrdev(MYMAJOR,MYNAME);
}

module_init(mydev_init); // 注册加载函数
module_exit(mydev_exit); // 注册卸载函数

// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL");             // 描述模块的许可证
MODULE_AUTHOR("dongfang");         // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("------");            // 描述模块的别名信息

Makefile

KERNELDIR := /home/haitu/Desktop/Linux/NXP/NXP_linux
CURRENT_PATH := $(shell pwd)
obj-m := my_led.o

build: kernel_modules
kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

APP.c

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

int main(int argc, char* argv[])
{
    int fd, ret;
    char* file_name;
    char data_buf[1];

    if(argc != 3)  // 命令行参数过少
    {
        printf("used error \n");
        return -1;
    }

    file_name = argv[1];

    fd = open(file_name,O_RDWR);  // 打开设备文件

    data_buf[0] = atoi(argv[2]); /* 要执行的操作:打开或关闭 */

    ret = write(fd, data_buf, sizeof(data_buf));
    if(ret < 0)
    {
        printf("write error \n");
        close(fd);
        return -1;
    }

    ret = close(fd);
    if(ret < 0)
    {
        printf("close error\n");
        return -1;
    }

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值