Linux字符设备驱动

本文详细介绍了Linux字符设备驱动的创建过程,包括设备号的动态分配、字符设备结构体的定义、初始化、添加与删除,以及如何自动创建设备节点。通过示例代码展示了如何使用cdev和file_operations结构体实现设备的打开、关闭和读写操作。此外,还提供了LED驱动的源码,演示了如何控制硬件设备。
摘要由CSDN通过智能技术生成

Linux字符设备驱动

Linux的各种设备都以文件的形式存放在 /dev 目录下,称为设备文件。应用程序可以打开、关闭和读写这些设备文件,完成对设备的操作,就像操作普通的数据文件一样。Linux为了管理这些设备,为设备编了号,每个设备号分为主设备号和次设备号。主设备号用来区分不同类型的设备,而次设备号用来区分同一类型的多个设备。
设备号的范围:
主设备号范围:0~4095(212)
次设备号范围:0~1048575(220)

创建字符设备驱动的步骤:(函数中的参数要根据实际情况更改)

注意:以下函数的具体含义在后面有说明。
1.申请设备号:alloc_chrdev_region(&devid, 0, 1, "test");
2.定义字符设备结构体:struct cdev test_cdev;
3.初始化定义的字符设备,主要是将字符设备与对应的file_operation文件操作集进行绑定:cdev_init(&test_cdev, &test_fops);
4.向 Linux系统添加字符设备 (cdev结构体变量 ):cdev_add(&testcdev, devid, 1);
5.自动创建设备节点,先创建类class_create(),再创建设备device_create()
6.卸载驱动的时候一定要使用 cdev_del函数从 Linux内核中删除相应的字符设备:cdev_del(&testcdev);

/*以上步骤的抽象示例,实际可用的字符驱动源码在文末*/
dev_t devid; /* 声明设备号 */

alloc_chrdev_region(&devid, 0, 1, "test"); /* 申请设备号 */

struct cdev test_cdev; //定义字符设备结构体

//定义该设备相应的文件操作集
static struct file_operations test_fops = {
	.owner = THIS_MODULE,
	/* 其他具体的初始项 */
};

test_cdev.owner = THIS_MODULE;
cdev_init(&test_cdev, &test_fops); /* 初始化cdev结构体变量 */
cdev_add(&test_cdev, devid, 1); /* 添加字符设备 */

cdev_del(&testcdev);/* 删除cdev,当卸载驱动时 */

1. 设备号的分配

1.1 静态分配设备号

静态分配设备号就是人为地为一个设备指定一个设备号,指定的设备号需要是当前系统尚未使用的设备号,否则会影响设备的使用。
查看当前系统所有已经使用了的主设备号的命令: cat /proc/devices
注意:静态分配设备号较麻烦,需要查看当前系统已经使用的设备号,推荐使用动态分配设备号的方法。
静态注册设备号函数如下:

函数int register_chrdev_region(dev_t *dev, unsigned count,const char *name);
参数 dev设备号的起始值。类型是dev_t 类型
参数 count要申请的次设备号的个数。
参数 name设备名字
返回值成功返回0,失败返回负数

1.2 动态分配设备号

由于静态分配设备号可能存在冲突问题,因此建议使用动态分配设备号。在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可,设备号的动态注册设备号函数如下:

函数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设备名字
返回值成功返回0,失败返回负数。使用动态分配会优先使用255 到234

1.3 释放设备号

设备号释放函数如下:

函数void unregister_chrdev_region(dev_t from, unsigned count);
参数 from要释放的设备号
参数 count表示从from 开始,要释放的设备号数量

2. 注册字符类设备

在Linux中使用cdev结构体表示一个字符设备,cdev结构体的定义如下:

struct cdev { 
	struct kobject kobj; 
	struct module *owner; 
	const struct file_operations *ops; //字符设备文件操作集合
	struct list_head list; 
	dev_t dev; //设备号
	unsigned int count; 
 };

编写字符设备驱动之前需要定义一个cdev结构体变量,这个变量就表示一个字符设备,定义字符设备结构体:struct cdev test_cdev;
注册字符类设备要使用到的函数:

函数void cdev_init(struct cdev *, const struct file_operations *);
第一个参数要初始化的cdev
第二个参数文件操作集cdev->ops = fops; //实际就是把文件操作集写给ops
功能cdev_init()函数用于初始化cdev 的成员,并建立cdev 和file_operations 之间的连接。
函数int cdev_add(struct cdev *, dev_t, unsigned);
第一个参数cdev 的结构体指针
第二个参数设备号
第三个参数次设备号的数量
功能cdev_add()函数用于向Linux 系统添加字符设备(cdev 结构体变量)
函数void cdev_del(struct cdev *);
第一个参数cdev 的结构体指针
功能cdev_del()函数用于向Linux 系统删除字符设备(cdev 结构体变量)

3. 自动创建设备节点

自动创建设备节点需要先创建类函数,然后再使用这个类函数去创建设备函数,从而会在 /dev 目录下创建相应的设备节点。

3.1 类的创建和删除

函数struct class *__class_create(struct module *owner, const char *name);
参数 owner一般为THIS_MODULE
参数 name类名字
返回值指向结构体class 的指针,也就是创建的类。

卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy,函数原型如下:

函数void class_destroy(struct class *cls);
参数 cls要删除的类

3.2 设备函数的创建和删除

函数struct device *device_create(struct class *class,struct device *parent,dev_t devt,void *drvdata,const char *fmt, …)
参数 class设备要创建哪个类下面
参数 parent父设备,一般为NULL,也就是没有父设备
参数 devt设备号
参数 drvdata设备可能会使用的一些数据,一般为NULL
参数 fmt设备名字,如果设置fmt=xxx 的话,就会生成/dev/xxx 这个设备文件
返回值返回创建好的设备

卸载驱动的时候需要删除掉创建的设备,设备删除函数为 device_destroy,函数原型如下:

函数void device_destroy(struct class *class, dev_t devt)
参数 class要删除的设备所处的类
参数 devt要删除的设备号

LED驱动源码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/device.h>

//定义字符设备的名称
#define DEV_NAME            "led_chrdev"
//定义字符设备的设备数量
#define DEV_CNT                 (3)

//定义字符设备的设备号
static dev_t devid;
struct class *led_chrdev_class;

struct led_chrdev {
	struct cdev dev;	//定义字符设备结构体
	unsigned int __iomem *va_dr;	//数据寄存器虚拟地址指针
	unsigned int __iomem *va_gdir;	//输入输出方向寄存器虚拟地址指针
	unsigned int __iomem *va_iomuxc_mux;	//端口复用寄存器虚拟地址指针
	unsigned int __iomem *va_ccm_ccgrx;	//时钟寄存器虚拟地址指针
	unsigned int __iomem *va_iomux_pad;	//电气属性寄存器虚拟地址指针

	unsigned long pa_dr;	//装载数据寄存器(物理地址)的变量
	unsigned long pa_gdir;	//装载输出方向寄存器(物理地址)的变量
	unsigned long pa_iomuxc_mux;	//装载端口复用寄存器(物理地址)的变量
	unsigned long pa_ccm_ccgrx;	//装载时钟寄存器(物理地址)的变量
	unsigned long pa_iomux_pad;	//装载电气属性寄存器(物理地址)的变量

	unsigned int led_pin;	//LED的引脚
	unsigned int clock_offset;	//时钟偏移地址
};

static struct led_chrdev led_cdev[DEV_CNT] = {
	{.pa_dr = 0x0209C000, .pa_gdir = 0x0209C004, .pa_iomuxc_mux = 0x20E006C, .pa_ccm_ccgrx = 0x20C406C,
	 .pa_iomux_pad = 0x20E02F8, .led_pin = 4, .clock_offset = 26},
	{.pa_dr = 0x20A8000, .pa_gdir = 0x20A8004, .pa_iomuxc_mux = 0x20E01E0, .pa_ccm_ccgrx = 0x20C4074,
	 .pa_iomux_pad = 0x20E046C, .led_pin = 20, .clock_offset = 12},
	{.pa_dr = 0x20A8000, .pa_gdir = 0x20A8004, .pa_iomuxc_mux = 0x20E01DC, .pa_ccm_ccgrx = 0x20C4074,
	 .pa_iomux_pad = 0x20E0468, .led_pin = 19, .clock_offset = 12},
};

static int led_chrdev_open(struct inode *inode, struct file *filp)
{
	unsigned int val = 0;
	//结构体的首地址 = container_of(结构体变量中某个成员的地址,结构体类型,该结构体变量的具体名字)
	//inode->i_cdev 指向找到的 cdev
	struct led_chrdev *led_cdev = (struct led_chrdev *)container_of(inode->i_cdev, struct led_chrdev, dev);
	//filp->private_data保存着自定义设备结构体的地址
	filp->private_data = container_of(inode->i_cdev, struct led_chrdev, dev);

	printk("open file success!\n");

	led_cdev->va_dr = ioremap(led_cdev->pa_dr, 4);
	led_cdev->va_gdir = ioremap(led_cdev->pa_gdir, 4);
	led_cdev->va_iomuxc_mux = ioremap(led_cdev->pa_iomuxc_mux, 4);
	led_cdev->va_ccm_ccgrx = ioremap(led_cdev->pa_ccm_ccgrx, 4);
	led_cdev->va_iomux_pad = ioremap(led_cdev->pa_iomux_pad, 4);

	val = ioread32(led_cdev->va_ccm_ccgrx);
	val &= ~(3 << led_cdev->clock_offset);
	val |= (3 << led_cdev->clock_offset);
	iowrite32(val, led_cdev->va_ccm_ccgrx);

	iowrite32(5, led_cdev->va_iomuxc_mux);

	iowrite32(0x1F838, led_cdev->va_iomux_pad);

	val = ioread32(led_cdev->va_gdir);
	val &= ~(1 << led_cdev->led_pin);
	val |= (1 << led_cdev->led_pin);
	iowrite32(val, led_cdev->va_gdir);

	val = ioread32(led_cdev->va_dr);
	val |= (0x01 << led_cdev->led_pin);
	iowrite32(val, led_cdev->va_dr);

	return 0;
}

static int led_chrdev_release(struct inode *inode, struct file *filp)
{
	struct led_chrdev *led_cdev = (struct led_chrdev *)container_of(inode->i_cdev, struct led_chrdev, dev);
	iounmap(led_cdev->va_dr);
	iounmap(led_cdev->va_gdir);
	iounmap(led_cdev->va_iomuxc_mux);
	iounmap(led_cdev->va_ccm_ccgrx);
	iounmap(led_cdev->va_iomux_pad);

	printk("close file success!\n");
	return 0;
}

static ssize_t led_chrdev_write(struct file *filp, const char __user * buf, size_t count, loff_t * ppos)
{
	char kbuf[2] = {0};
	unsigned long val = 0;

	struct led_chrdev *led_cdev = (struct led_chrdev *)filp->private_data;
	// copy_from_user 从应用层传递数据到内核层
	if(copy_from_user(kbuf, buf, count) != 0){
		printk("copy_from_user error\n");
		return -1;
	}

	val = ioread32(led_cdev->va_dr);
	printk("kbuf[0] is %d", kbuf[0]);
	if (kbuf[0] == 0)
		val &= ~(0x01 << led_cdev->led_pin);
	else
		val |= (0x01 << led_cdev->led_pin);
	iowrite32(val, led_cdev->va_dr);

	return 0;
}

static struct file_operations led_chrdev_fops = {
	.owner = THIS_MODULE,
	.open = led_chrdev_open,
	.release = led_chrdev_release,
	.write = led_chrdev_write,
};

static __init int led_chrdev_init(void)
{
	int i = 0;
	dev_t cur_dev;
	printk("led chrdev init\n");
	/*第一步
	 *采用动态分配的方式,获取设备编号,次设备号为0,
	 *设备名称为 DEV_NAME ,可通过命令cat /proc/devices查看
	 *DEV_CNT为3,当前申请3个次设备编号
    */
	alloc_chrdev_region(&devid, 0, DEV_CNT, DEV_NAME);

	/*自动创建设备节点,先创建类*/
	led_chrdev_class = class_create(THIS_MODULE, "led_chrdev");

	for (; i < DEV_CNT; i++) {
		/*关联字符设备结构体cdev与文件操作结构体file_operations*/
		cdev_init(&led_cdev[i].dev, &led_chrdev_fops);
		led_cdev[i].dev.owner = THIS_MODULE;

		cur_dev = MKDEV(MAJOR(devid), MINOR(devid) + i);

		/*添加设备至cdev_map散列表中
		 *向 Linux系统添加字符设备 (cdev结构体变量 )
		*/
		cdev_add(&led_cdev[i].dev, cur_dev, 1);

		/*自动创建设备节点,创建设备*/
		device_create(led_chrdev_class, NULL, cur_dev, NULL,
			      DEV_NAME "%d", i);
	}

	return 0;
}

static __exit void led_chrdev_exit(void)
{
	int i;
	dev_t cur_dev;
	printk("led chrdev exit\n");

	for (i = 0; i < DEV_CNT; i++) {
		cur_dev = MKDEV(MAJOR(devid), MINOR(devid) + i);

		device_destroy(led_chrdev_class, cur_dev);

		cdev_del(&led_cdev[i].dev);

	}
	unregister_chrdev_region(devid, DEV_CNT);
	class_destroy(led_chrdev_class);

}

module_init(led_chrdev_init);
module_exit(led_chrdev_exit);

MODULE_AUTHOR("learn");
MODULE_LICENSE("GPL");


测试LED驱动的应用程序源码

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

// input "./app 0" in command line to open led
int main(int argc, char *argv[])
{
    char buf[2] = {0};

    // atoi()将字符串转为整型
    buf[0] = atoi(argv[1]);

    printf("led_cdev test\n");
    //打开设备
    int fd = open("/dev/led_chrdev0", O_RDWR);
    if(fd>0)
        printf("led_chrdev0 open success\n");
    else
        printf("led_chrdev0 open fail\n");
    //写入数据
    write(fd, buf, sizeof(buf));
    //写入完毕,关闭文件
    close(fd);
    sleep(1);
    //打开设备
     fd = open("/dev/led_chrdev1", O_RDWR);
    if(fd>0)
        printf("led_chrdev1 open success\n");
    else
        printf("led_chrdev1 open fail\n");
    
    //写入数据
    write(fd, buf, sizeof(buf));
    //写入完毕,关闭文件
    close(fd);
    sleep(1);
     //打开设备
     fd = open("/dev/led_chrdev2", O_RDWR);
    if(fd>0)
        printf("led_chrdev2 open success\n");
    else
        printf("led_chrdev2 open fail\n");
    //写入数据
    write(fd, buf, sizeof(buf));
    //写入完毕,关闭文件
    close(fd);
    sleep(1);

    buf[0] = 1; //close led
    //关闭设备
    fd = open("/dev/led_chrdev0", O_RDWR);
    write(fd, buf, sizeof(buf));
    close(fd);
     fd = open("/dev/led_chrdev1", O_RDWR);
    write(fd, buf, sizeof(buf));
    close(fd);
     fd = open("/dev/led_chrdev2", O_RDWR);
    write(fd, buf, sizeof(buf));
    close(fd);

    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值