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;
}