Skip to content

字符设备

字符设备驱动

内核移植一章中有关于konfig的介绍,我们添加的驱动文件可以根据需求选择添加到menuconfig菜单中。本章节主要记录基本字符设备驱动开发的步骤。

Makefile

#若在PC上测试简单模块,则使用 uname命令获取内核路径
#KERNELDIR=/lib/modules/$(shell uname -r)/build/
KERNELDIR=/home/guo/Downloads/kernel-3.4.39
PWD=$(shell pwd)
all:
#当make的目标为all时,-C $(KERNELDIR) 指明跳转到内核源码目录下读取那里的Makefile;M=$(PWD) 表明然后返回到当前目录继续读入、执行当前的Makefile。
    make -C $(KERNELDIR) M=$(PWD) modules
    @echo "build on pc"

# M是个变量
clean:
    make -C $(KERNELDIR) M=$(PWD) clean
#编译成模块
obj-m:= hello.o

 #内核make语法规则相较于标准Makefile规则有所扩展

 #---------------------------------------
 #多文件编译,例如hello.c add.c
 # obj-m:=demo.o
 # demo-y += hello.o add.o

驱动相关命令

insmod :加载驱动 modprobe:更加智能,加载驱动时候也会加载依赖的驱动 rmmod:卸载驱动 lsmod:显示已安装的驱动 dmesg:查看消息,可以查看printk的所有消息

内核打印printk

该函数格式为printk(loglevel "XXXXXXX")

//include/linux/printk.h    
    #define KERN_EMERG  "<0>"   /* system is unusable           */
    #define KERN_ALERT  "<1>"   /* action must be taken immediately */
    #define KERN_CRIT   "<2>"   /* critical conditions          */
    #define KERN_ERR    "<3>"   /* error conditions         */
    #define KERN_WARNING    "<4>"   /* warning conditions           */
    #define KERN_NOTICE "<5>"   /* normal but significant condition */
    #define KERN_INFO   "<6>"   /* informational            */
    #define KERN_DEBUG  "<7>"   /* debug-level messages
// 0 - 7 (high - low)

通过命令cat /proc/sys/kernel/printk 查看打印级别设置

#   4       4          1       7
#终端级别 消息默认级别  最大级别  最小级别 
#-----------------------------------
#修改默认级别
echo 4 3 1 7 > /proc/sys/kernel/printk

#修改开发板对应的打印级别
vi rootfs/etc/init.d/rcS
echo 4 3 1 7 > /proc/sys/kernel/printk

获取命令行参数

* Standard types are:                                                                             
     *  byte, short, ushort, int, uint, long, ulong  (没有找到char!!!!!!!!)
     *  charp: a character pointer
     *  bool: a bool, values 0/1, y/n, Y/N.
     *  invbool: the above, only sense-reversed (N = true).

// 功能:接收命令行传递的参数
//  @name :变量的名字
//  @type :变量的类型
//  @perm :权限  0664  0775
module_param(name, type, perm) 
//功能:对变量的功能进行描述
//@_parm:变量
//@desc :描述字段
MODULE_PARM_DESC(_parm, desc)   

//hello:     
int a = 10;
module_param(a,int,0664);
MODULE_PARM_DESC(a,"this is lcd light(0-255)");

///sys/module/hello/paramters 可以查看参数
//在加载模块时设置参数
// sudo insmod hello.ko a=121

驱动三要素

#include <linux/init.h>
#include <linux/module.h>
//三要素:入口,出口,许可证
//__init将hello_init放到.init.text段中
static int __init  hello_init(void) 
{
      return 0;
}
//__exit将hello_exit放到.exit.text段中
static void __exit hello_exit(void)
{
}
module_init(hello_init);
//告诉内核驱动的入口地址
module_exit(hello_exit);
MODULE_LICENSE("GPL");

模块导出符号表

一个模块中的函数或者变量提供给其他模块使用,此时需要利用EXPORT_SYMBOL_GPL(symbol)导出符号。

  • 编译

先编译提供者,完成后会生成Module.symvers文件,将其拷贝至调用者的同级目录

  • 加载

先加载提供者,在加载调用者

  • 卸载

先卸载调用者再卸载提供者

字符设备驱动框架

  1. 分配设备号
  2. 设置file_operations结构体
  3. 注册字符设备
  4. 创建设备节点
设备号

设备是32位无符号数字

32 - 20 19 - 0
主设备号 次设备号
#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))

设备号申请方式

  1. 手动申请
/*
手动指定设备号
@from :驱动工程师指定的设备号
@count:设备的个数
@name :名字 cat /proc/devices
返回值:成功返回0,失败返回错误码
*/
int register_chrdev_region(dev_t from, unsigned count, const char *name)

  1. 自动申请设备号
/*
功能:内核申请设备号
@dev       :申请到的设备号
@baseminor :次设备号的起始值
@count     :设备的个数
@name      :名字 cat /proc/devices
返回值:成功返回0,失败返回错误码
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,vconst char *name)

//设备号释放的函数
void unregister_chrdev_region(dev_t from, unsigned count)
file_operations结构体
struct file_operations {
    struct module *owner;
    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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ...
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    ...
}

用户通过openreadwrite等接口调用,来调用驱动中相应的函数。

用户接口通过 系统调用,找到了VFS中相应sys_* 接口,具体过程以后补充

注册字符设备

  1. 该接口会默认创建255个设备 (待确认)
/* 
@major:主设备号  
              :如果你填写的值大于0,它认为这个就是主设备号
              :如果你填写的值为0,操作系统给你分配一个主设备号               
@name :名字   cat /proc/devices 
@fops :操作方法结构体
返回值:major>0 ,成功返回0,失败返回错误码(负数) vi -t EIO
            major=0,成功主设备号,失败返回错误码(负数)
*/
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
/* 
功能:注销一个字符设备驱动
@major:主设备号
@name:名字
*/
void unregister_chrdev(unsigned int major, const char *name)
  1. 手动注册
//-----------------------------------------------------------------
//   手动注册
//-----------------------------------------------------------------
/*  
 功能:字符设备驱动的注册
 @p    :向注册的对象
 @dev  :设备号
 @count:个数
 返回值:成功返回0,失败返回错误码 
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count)

void cdev_del(struct cdev *p)

//若使用cdev_add,需要在此之前初始化结构体,如下

 /*
功能:cdev结构体的初始化,将fops填充到cdev中
  @cdev :对象的地址
  @fops :操作方法结构体的地址
 */        
 void cdev_init(struct cdev *cdev, const struct file_operations *fops)

创建设备节点

  1. 自动创建设备节点
#include <linux/device.h>
/*
  自动创建设备节点:
  功能:向用户空间提交目录信息
  @owner :THIS_MODULE
  @name  :目录名字
  返回值:成功返回struct class *指针
  错误返回错误码,错误是指针,位于最高地址4K,使用宏IS_ERR(cls)转换   
*/
struct class * class_create(owner, name)    
//void class_destroy(struct class *cls)

/*          
 功能:向用户空间提交文件信息
  @class :录名字
  @parent:NULL
  @devt  :设备号
  @drvdata :NULLc
  @fmt   :文件的名字
  返回值:成功返回struct device *指针
            失败返回错误码指针       
*/      
struct device *device_create(struct class *class, struct device *parent,
                     dev_t devt, void *drvdata, const char *fmt, ...)
//void device_destroy(struct class *class, dev_t devt)


  1. 手动创建设备节点
sudo mknod hello (路径是任意) c/b  主设备号   次设备号
驱动框架

这里需要 一张图

/*
open()      read      write      close
----------------------------------------
       vfs
-----------------------------------------
fops.open fops.read fops.write fops.close
*/
struct inode{
    umode_t  i_mode;   //文件的权限
    unsigned long i_ino;//inode号
    dev_t   i_rdev;   //设备号    
    union {             //设备驱动的类型
        struct block_device *i_bdev; 
        struct cdev     *i_cdev;
    };    
};

/*
* 每个文件都有inode号,内核是如何通过inode 号建立联系?
*/

struct cdev {
        struct module *owner;     //THIS_MODULE
        const struct file_operations *ops; //操作方法结构体
        dev_t dev;                //设备号 32   12|20  2^32=4G
        unsigned int count;       //设备的个数
};

struct file{
  const struct file_operations    *f_op;
  ...
}

问题:如何通过fd来找到对应的驱动

调用open函数的时候产生fd,在进程中保存

struct task_struct{  //进程结构体
      struct files_struct *files; 
        /../ ----> struct file* fd_array[NR_OPEN_DEFAULT];

}

当成功打开文件时候,会在内核中创建struct file结构体,并且把这个结构体放到fd_array的数组中,这个数组的下标就是文件描述符

fd--->fd_array[fd]--->struct file--->fops--->(read write ioctl release)

file结构体中有fops操作方法结构体,这个fops是从inode结构体中拷贝得到的,inode中的fops是从cdev中得到的。

字符设备驱动实例
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <asm/io.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/slab.h>

#define  DRV_BUF_SIZE  128

#define  CLASS_NAME    "class_cdev" // class create name
#define  DEV_NAME      "cdev_demo"  // drives create name

char buf[DRV_BUF_SIZE] = {0};
struct cdev   *cdev;
struct class  *cdev_cls;
struct device *c_device;
dev_t  devno;    // 设备号
int major = 0;   // 主设备号

static int drv_open(struct inode *node, struct file *filp)
{
    printk("open cdrv-demo\n");
    return 0;
}

static ssize_t drv_write(struct file *file, const char __user *u_buf,size_t size, loff_t *offset)
{
    int ret = 0;
    if (size > DRV_BUF_SIZE) {
        return -EINVAL;
    }
    ret = copy_from_user(buf, u_buf, size);
    if (ret) {
        printk("copy from  user failed :%d\n", ret);
        return -EAGAIN;
    }
    printk("cdrv_demo write:%s\n",buf);
    return size;
}
static ssize_t drv_read(struct file *file,char __user *u_buf, size_t size, loff_t *offset)
{
    int ret = 0;
    if (size > DRV_BUF_SIZE) {
        return -EINVAL;
    }
    ret = copy_to_user(u_buf, buf, size);
    if (ret) {
        printk("copy to  user failed :%d\n", ret);
        return -EAGAIN;
    }
    printk("cdrv_demo read:%s\n",buf);
    return size;
}

static int drv_release(struct inode* node, struct file *filp)
{
    printk("cdrv_demo release\n");
    return 0;
}
static struct file_operations file_ops = {
    .open = drv_open,
    .write = drv_write,
    .read  = drv_read,
    .release = drv_release,
};

#define  TOTAL_OF_DEV   3
static int __init drv_demo_init(void)
{
    int ret = 0;
    int i = 0;
    //74 - 95行可以直接用 major = register_chrdev(0, DEV_NAME,&file_ops)代替

    //1. 分配对象
    cdev = cdev_alloc();
    if (cdev == NULL) {
         ret = -ENOMEM;
         goto err1;
    }
    //2. 初始化
    cdev_init(cdev,&file_ops);
    //3.设备号请
    if (major > 0) {
        ret = register_chrdev_region(MKDEV(major, 0),1, DEV_NAME);
    } else {
        ret = alloc_chrdev_region(&devno, 0, TOTAL_OF_DEV, CLASS_NAME);
    }
    if(ret < 0) {
        goto err2;
    }
    //4. 注册
    ret = cdev_add(cdev, devno, TOTAL_OF_DEV);
    if (ret) {
        printk("char device register failed\n");
        goto err3;
    }
    cdev_cls = class_create(THIS_MODULE, CLASS_NAME);
    if(IS_ERR(cdev_cls)) {
        ret = PTR_ERR(cdev_cls);
        goto err4;
    }
    major = MAJOR(devno);
    for (i = 0;  i < TOTAL_OF_DEV; i++) {
        c_device = device_create(cdev_cls,NULL, MKDEV(major, i),NULL, "cdev%d",i);  /* /proc/devices*/
        if(IS_ERR(c_device)) {
            ret = PTR_ERR(c_device);
            goto err5;
        }
    }
    printk("init done\n");
    return 0; //一定要有
err5:
    for (--i; i >= 0; i--) {
        device_destroy(cdev_cls,MKDEV(major,i));
    }
    class_destroy(cdev_cls);
err4:
    cdev_del(cdev);
err3:
    unregister_chrdev_region(MKDEV(major,0),TOTAL_OF_DEV);
err2:
    kfree(cdev);
err1:

    return ret;
}

static void __exit drv_demo_exit(void)
{
    int i = 0;
    for (i = 0; i < TOTAL_OF_DEV; i++) {
        device_destroy(cdev_cls, MKDEV(major,i));
    }
    class_destroy(cdev_cls);
    cdev_del(cdev);
    unregister_chrdev_region(devno, TOTAL_OF_DEV);
    kfree(cdev);
    free_irq(73, NULL);
}

module_init(drv_demo_init)
module_exit(drv_demo_exit)

MODULE_LICENSE("GPL");

高级驱动
#include <sys/ioctl.h>
/*
  @fd     : 打开文件产生的文件描述符
  @request: 请求码(读写|第三个参数传递的字节的个数),
                :在sys/ioctl.h中有这个请求码的定义方式。
  @...    :可写、可不写,如果要写,写一个内存的地址
*/
int ioctl(int fd, int request, ...);        

//驱动调用
struct file_operations fops {
    long (*unlocked_ioctl) (struct file *file, unsigned int request, unsigned long args);
  ...
}


#define _IO(type,nr)        
            _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size)  
            _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOW(type,nr,size)  
            _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOWR(type,nr,size)     
            _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))

//这些宏是帮助你完成请求码的封装的。

#define _IOC(dir,type,nr,size) \
            (((dir)  << _IOC_DIRSHIFT) | \
             ((type) << _IOC_TYPESHIFT) | \
             ((nr)   << _IOC_NRSHIFT) | \
             ((size) << _IOC_SIZESHIFT))


dir << 30 | size<<16 | type << 8 | nr << 0 
2           14         8          8
方向        大小       类型        序号

内核中已经使用的命令码的域在如下文档中已经声明了。vi kernel-3.4.39/Documentation/ioctl$ vi ioctl-number.txt