Linux驱动入门之如何写一个字符驱动 :)

这是博主第一次写开放式技术性文章,写这篇文章的目的既是为了总结学到的知识,也是为了能帮助新手,因为在学习的过程中也是面向某度编程,导致出现了很多的坑,既有相关帖子的问题,也有自己的问题,我会将其中的坑一一注明,尽可能地让大家在少踩坑的过程中写出一个驱动。

一、一个要解决加减法问题的驱动
题目:

通过内核驱动程序,在安卓终端实现加减运算  

要求:a. 算法在内核驱动中实现,通过ioctl调用;
      b. 应用程序调用驱动算法实现加减法运算,在安卓终端输入
./fun 3 + 5,输出结果8;

要解决这个问题,我们就要了解linux驱动的相关知识、编译工具的使用、安卓调试工具ADB的使用、linux驱动调试方法以及Makefile的编写等知识,重在应用怎么调用驱动这个过程。

二、基础知识大梳理

  1. linux驱动知识

(1)linux驱动分类

linux中一切皆文件,linux系统把硬件设备标志为特殊的文件,分为以下三种:

image.png
图片来自于在此期间的笔记,已经很充分了,我们这次主要的任务就是完成用户态到内核态的切换,从切换方式可知我们是通过系统调用来使用驱动的。

  1. 怎么写
    依然拿出草根老师的图: image.png

驱动的实现具体就是三部,分为底层(自己写的)open()、write()、read()、ioctl()函数的实现,struct file_operations、初始化函数以及退出函数,具体程序放到后面。
实现驱动计算得出结果有2种办法:
(1)、计算后用return返回计算值,直接输出结果
(2)、开辟空间保存,然后用read()读取

既然要玩,果然要追求刺激,我们选择第二种方法。我的程序参考了草根老师的驱动程序,程序写的非常漂亮,而且草根老师程序足足更改了三次,完善了很多,最后的版本可以驱动多个同类设备并且可以动态申请节点,但是帖子有些内容不全面而且也有问题,这也是我写这篇文章的初衷,那就是写一个真真可以调用的驱动而不是输出hello,我会在最后把他们的博客内容放在最后,通过阅读我的以及他们的博客,相信后人可以很顺利写出一个可以跑的字符驱动。
  1. 来聊聊ioctl()
    ioctl函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对ioctl的支持,用户就可以在用户程序中使用ioctl函数来控制设备的I/O通道,它需要传递三个参数,包含文件动态信息的文件结构体,命令以及参数。第一个在应用程序里反映的就是文件描述符,cmd则是判断语句里需要用到的,linux为cmd设计了具体的规则以及命令生成函数以便于程序人员开发自己的命令,第三个参数就用来传递参数,既可以传递整数也可以传递指针,但是指针的传递需要在程序里面进行检验,否则有可能造成系统奔溃。CMD的知识需要大家自己学习一下,命令的规则比较复杂,相对来说网上资料也比较多,大家完全可以自己学习一下,而且ioctl()函数在 2.6以后的内核中改版了,需要注意一下。

5.什么是编译?什么是交叉编译?什么是连接
因为大家很多都是从IDE时代过来的,对编译的概念很模糊,但是到linux环境中必须要了解这些知识。
image.png
6.Makefile、.ko文件
Makefile文件用来让大家实现自动编译,从而生成驱动文件.ko,详细的语法以及知识,大家可以自己去学习一下,这些也是一名嵌入式工程师的必修课。下面贴一下我的Makefile

KERN_DIR = /studio/...
all:
    make -C $(KERN_DIR) M=`pwd` modules CROSS_COMPILE=aarch64-linux-gnu-
clean:
    make -C $(KERN_DIR) M=`pwd` modules clean
    rm -rf modules.order
obj-m    += in_proc.o

其中KERN_DIR就是你所用linux系统kernel内核的路径,需要提一下的是,在make命令之前需要先编译一下内核。
7.ADB工具使用
主要用一下几条指令:

ADB connect IP
ADB root
ADB remount
ADB push  windows路径 /bin
补充一条 ADB devices ADB didconnect,因为ADB经常断连接所以需要查看和排除连接问题

二、避坑指南

  1. ioctl版本

ioctl在改版之后也有2种,主要是为了解决系统兼容的问题,分为compat_ioctl与unlocked_ioctl,安卓系统也分为32位与64位版本,32位系统,内核版本64位则需要使用compat_ioctl,如果系统和内核版本都是32位或者64位则会调用unlock_ioctl,这一点如果不注意就会发现调用ioctl时候会报错,需要大家注意。

  1. 编译工具
    在安卓跑驱动的时候需要用专门的工具来编译应用程序,还有交叉编译生成驱动文件的编译器,都有详细的版本要求,需要大家结合自己的设备选择。

三、源码阅读

开始带大家读代码,大部分的问题都藏在代码中或者可以通过梳理代码解决,RTFSC!

先上驱动代码

#include 
#include 
#include 
#include         //函數指針存放
#include 
#include 
#include 
#include 
#include "in_proc.h"


//指定的主设备号
#define MAJOR_NUM 250
/*
struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
};
 */
//注册自己的设备号

struct mycdev {      
    int len;
    unsigned int buffer[50];
    struct cdev cdev;
};

MODULE_LICENSE("GPL");     //許可說明

//设备号
static dev_t dev_num = {0};

//全局gcd
struct mycdev *gcd;

//设备类
struct class *cls;

//获得用户传递的数据,根据它来决定注册的设备个数
static int ndevices = 1;
module_param(ndevices, int, 0644);
MODULE_PARM_DESC(ndevices, "The number of devices for register.\n");


//打开设备
static int dev_fifo_open(struct inode *inode, struct file *file)
{
    
    struct mycdev *cd;
    
    //用struct file的文件私有数据指针保存struct mycdev结构体指针
    cd = container_of(inode->i_cdev,struct mycdev,cdev);   //该函数可以根据结构体成员获取结构体地址
    file->private_data = cd;
    printk( KERN_CRIT "open success!\n");

    return 0;
}

//读设备
static ssize_t dev_fifo_read(struct file *file, char __user *ubuf, size_t size, loff_t *ppos)
{
    int n ;
    int ret;
    unsigned int *kbuf;
    struct mycdev *mycd = file->private_data;
    
    //printk(KERN_CRIT "read *ppos : %lld\n",*ppos);    //光标位置

    if(*ppos == mycd->len)
        return 0;

    //请求大小 > buffer剩余的字节数 :读取实际记的字节数
    if(size > mycd->len - *ppos)
        n = mycd->len - *ppos;
    else
        n = size;

    //从上一次文件位置指针的位置开始读取数据
    kbuf = mycd->buffer+*ppos;
    //拷贝数据到用户空间
    ret = copy_to_user(ubuf,kbuf,n*sizeof(kbuf));
    if(ret != 0)
        return -EFAULT;
    
    //更新文件位置指针的值
    *ppos += n;
    
    printk(KERN_CRIT "dev_fifo_read success!\n");

    return n;
}

//写设备
static ssize_t dev_fifo_write(struct file *file, const char __user *ubuf, size_t size, loff_t *ppos)
{
    int n;
    int ret;
    unsigned int *kbuf;
    struct mycdev *mycd = file->private_data;

    printk("write *ppos : %lld\n",*ppos);
    
    //已经到达buffer尾部了
    if(*ppos == sizeof(mycd->buffer))
        return -1;

    //请求大小 > buffer剩余的字节数(有多少空间就写多少数据)
    if(size > sizeof(mycd->buffer) - *ppos)
        n = sizeof(mycd->buffer) - *ppos;
    else
        n = size;

    //从上一次文件位置指针的位置开始写入数据
    kbuf = mycd->buffer + *ppos;

    //拷贝数据到内核空间
    ret = copy_from_user(kbuf, ubuf, n*sizeof(ubuf));
    if(ret != 0)
        return -EFAULT;

    //更新文件位置指针的值
    *ppos += n;
    
    //更新dev_fifo.len
    mycd->len += n;

    printk("dev_fifo_write success!\n");
    return n;
}

//linux 内核在2.6以后,已经废弃了ioctl函数指针结构,取而代之的是unlocked_ioctl
long dev_fifo_unlocked_ioctl(struct file *file, unsigned int cmd,unsigned long arg)
{
    int ret = 0;
    struct mycdev *mycd = file->private_data;

    struct ioctl_data val;    //定义结构体

    printk( KERN_CRIT "in dev_fifo_ioctl,sucessful!\n");

    if(_IOC_TYPE(cmd) != DEV_FIFO_TYPE)     //检测命令
        {
            printk(KERN_CRIT "CMD ERROR\n");
            return -EINVAL;
        }

    /*if(_IOC_NR(cmd) > DEV_FIFO_NR)
        {
            printk(KERN_CRIT "CMD 1NUMBER ERROR\n");
            return -EINVAL;
        }*/
    switch(cmd){
        case DEV_FIFO_CLEAN:
            printk("CMD:CLEAN\n");
            memset(mycd->buffer, 0, sizeof(mycd->buffer));
            break;

        case DEV_FIFO_SETVALUE:
            printk("CMD:SETVALUE\n");
            mycd->len = arg;
            break;

        case DEV_FIFO_GETVALUE:
            printk("CMD:GETVALUE\n");
            ret = put_user(mycd->len, (int *)arg);
            break;

        case DEV_FIFO_SUM:       //求和
            printk( KERN_CRIT "It is CMD:SUM!!!!\n");
            //检查指针是否安全|获取arg传递地址
            if(copy_from_user(&val,(struct ioctl_data*)arg,sizeof(struct ioctl_data))){
                ret = -EFAULT;
                goto RET;
            }
            //printk( KERN_CRIT "CMD:SUM!!!!!\n");
            memset(mycd->buffer,0,DEV_SIZE);    //清空区域 
            val.buf[1] = val.buf[0]+val.buf[2];   //计算结果
            //printk(KERN_CRIT "result is %d\n",val.buf[1]);
            memcpy(mycd->buffer,val.buf,sizeof(val.buf)*val.size);
            /*for(i = 0;i < 50;i++)
               printk( KERN_CRIT "%d\n",mycd->buffer[i]);*/
            mycd->len = val.size;
            file->f_pos = 0;
            break;

         case DEV_FIFO_DEC:       //求差
            printk(KERN_CRIT "It is CMD:DEC!!!!\n");
            //检查指针是否安全|获取arg传递地址
            if(copy_from_user(&val,(struct ioctl_data*)arg,sizeof(struct ioctl_data))){
                ret = -EFAULT;
                goto RET;
            }
             memset(mycd->buffer,0,DEV_SIZE);    //清空区域 
             val.buf[1] = val.buf[0]-val.buf[2];   //计算结果
             memcpy(mycd->buffer,val.buf,sizeof(val.buf)*val.size);
              /*printk(KERN_CRIT"size is :%d\n",val.size);
              for(i = 0;i < 50;i++)
               printk( KERN_CRIT "%d\n",mycd->buffer[i]);*/
             mycd->len = val.size;
             file->f_pos = 0;
             break;

        default:
            return -EFAULT;      //错误指令
    }
    RET:
    return ret;
}


//设备操作函数接口
static const struct file_operations fifo_operations = {
    .owner = THIS_MODULE,
    .open = dev_fifo_open,
    .read = dev_fifo_read,
    .write = dev_fifo_write,
    .compat_ioctl = dev_fifo_unlocked_ioctl,
};


//模块入口
int __init dev_fifo_init(void)
{
    int i = 0;
    int n = 0;
    int ret;
    struct device *device;
    
    gcd = kzalloc(ndevices * sizeof(struct mycdev), GFP_KERNEL);   //内核内存正常分配,可能通过睡眠来等待,此函数等同于kmalloc()
    if(!gcd){
        return -ENOMEM;    //內存溢出
    }

    //设备号 : 主设备号(12bit) | 次设备号(20bit)
    dev_num = MKDEV(MAJOR_NUM, 0);

    //静态注册设备号
    ret = register_chrdev_region(dev_num,ndevices,"dev_fifo");   
    if(ret < 0){

        //静态注册失败,进行动态注册设备号
        ret = alloc_chrdev_region(&dev_num,0,ndevices,"dev_fifo");
        if(ret < 0){
            printk("Fail to register_chrdev_region\n");
            goto err_register_chrdev_region;
        }
    }
    
    //创建设备类
    cls = class_create(THIS_MODULE, "dev_fifo");
    if(IS_ERR(cls)){
        ret = PTR_ERR(cls);
        goto err_class_create;
    }
    
    printk("ndevices : %d\n",ndevices);
    
    for(n = 0;n < ndevices;n ++){
        //初始化字符设备
        cdev_init(&gcd[n].cdev,&fifo_operations);

        //添加设备到操作系统
        ret = cdev_add(&gcd[n].cdev,dev_num + n,1);
        if (ret < 0){
            goto err_cdev_add;
        }
        //导出设备信息到用户空间(/sys/class/类名/设备名)
        device = device_create(cls,NULL,dev_num + n,NULL,"dev_fifo%d",n);
        if(IS_ERR(device)){
            ret = PTR_ERR(device);
            printk("Fail to device_create\n");
            goto err_device_create;    
        }
    }
    printk("Register dev_fito to system,ok!\n");

    
    return 0;

err_device_create:
    //将已经导出的设备信息除去
    for(i = 0;i < n;i ++){
        device_destroy(cls,dev_num + i);    
    }

err_cdev_add:
    //将已经添加的全部除去
    for(i = 0;i < n;i ++)
    {
        cdev_del(&gcd[i].cdev);
    }

err_class_create:
    unregister_chrdev_region(dev_num, ndevices);

err_register_chrdev_region:
    return ret;

}

void __exit dev_fifo_exit(void)
{
    int i;

    //删除sysfs文件系统中的设备
    for(i = 0;i < ndevices;i ++){
        device_destroy(cls,dev_num + i);    
    }

    //删除系统中的设备类
    class_destroy(cls);
 
    //从系统中删除添加的字符设备
    for(i = 0;i < ndevices;i ++){
        cdev_del(&gcd[i].cdev);
    }
    
    //释放申请的设备号
    unregister_chrdev_region(dev_num, ndevices);

}


module_init(dev_fifo_init);
module_exit(dev_fifo_exit);

接下来是应用代码:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "in_proc.h"

static void Hello_Panel(void)        //操作提示界面
{
    printf("Welcome to the panel!\n");
    printf("eg:A +(-) B\n");   
}

static int Char_prosess(char po)  //符号读取
{
    if(po == '+')  
        return 0;
    else{ 
        if(po == '-')
            return 1;
        else
            return -1;
    }

}


int main(int argc, const char* argv[])
{
    int ret = 0,fd = 0,n = 0,Char_error = 2;

    unsigned int Result_buf[20];

    struct ioctl_data my_data;
    my_data.size = argc-1;


    /*printf("%d\n",argc-1 );         //测试使用
       for(n = 0;n < argc-1;n++)
    {
         my_data.buf[n] = (int)(argv[n+1]);
         printf("%s\n",my_data.buf[n]);
    }*/
   
   
    Char_error = Char_prosess(*argv[2]);
    printf("%d\n", Char_error);

    for(n = 0;n < argc-1;n++){      //参数读取
        my_data.buf[n] = atoi((argv[n+1]));
        //printf("%d\n",my_data.buf[n]);
    }
    
    switch(Char_error){        //测试使用
        case 0:
             printf("right:+ \n");
             break;

        case 1:
             printf("right:- \n");
             break;

        case -1:
             printf("error!!!\n");
             Hello_Panel();
             return -1;
             break;
    }

    fd = open("/dev/dev_fifo0",O_RDWR);
    if(fd < 0){
        perror("Fail to open");
        return -1;
    }
    //printf("open sucessful!!,fd is :%d\n", fd);
    
    if(Char_error == 0)            //加法
        ioctl(fd,DEV_FIFO_SUM,&my_data);
    else
         ioctl(fd,DEV_FIFO_DEC,&my_data);
 
   ret = read(fd,Result_buf,3);
   
   if(Char_error == 0)
    printf("%d + %d = %d\n",Result_buf[0],Result_buf[2],Result_buf[1]);
   else
    if(Char_error == 1)
      printf("%d - %d = %d\n",Result_buf[0],Result_buf[2],Result_buf[1]);
   /*for(n = 0;n < 3;n++){
      printf("result is:%d\n",Result_buf[n]);
   }*/

   close(fd);

    return 0;
}

代码重要的地方已经注释,无非就是按驱动的流程,开始造轮子,组装,启动以及停止四个步骤,还是需要读者自己树立代码,多读,多去理解前辈的思想。整个代码风格有些怪异,因为楼主在接受新风格毒打(哈哈哈),还没完全把新风格融到自己原来的风格中,所以有些地方处理的不伦不类,并且程序没有经过优化,可能会让一些前辈恼火,欢迎前辈们给与楼主以指教!

 最后,留个坑,大家慢慢填吧!

四.写在最后的话
参考博客,非常感谢前人的努力
http://blog.chinaunix.net/uid-26833883-id-4371047.html
https://www.cnblogs.com/yangguang-it/p/8681579.html
https://www.cnblogs.com/eleclsc/p/11533682.html
linus曾说:My name is Linus, and I am your God”,相信大家可以感受“神”的气场吧,所以努力吧,说不定哪天也可以在内核的某个角落看到author一栏有各位的名字,hahaha

你可能感兴趣的:(c,linux,android)