乐趣区

关于c:Linux驱动入门之如何写一个字符驱动

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

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

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

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

要解决这个问题,咱们就要理解 linux 驱动的相干常识、编译工具的应用、安卓调试工具 ADB 的应用、linux 驱动调试办法以及 Makefile 的编写等常识,重在利用怎么调用驱动这个过程。

二、基础知识大梳理

  1. linux 驱动常识

(1)linux 驱动分类

linux 中所有皆文件,linux 零碎把硬件设施标记为非凡的文件,分为以下三种:


字符设施就是 LCD、触摸屏等设施,块设施就是如 U 盘、SD 卡、电脑硬盘等设施,而网络设备就是常见的网卡设施、蓝牙设施等。依据题目要求,这次的驱动就以一个简略的字符驱动开始。
(2)利用怎么调用驱动

!
两幅图都援用自两位前辈的帖子,再次感激前浪的开源分享精力!先是草根老师的图,比拟具体地介绍了应用程序怎么通过形象函数 open()、read()、wiite()、ioctl()函数实现关上文件获取文件描述符、寻找对应 inode,继而创立 struct file,保留文件信息,确认设施信息,拷贝 cdev 地址信息,进而通过这些信息找到咱们所写的底层 write()、ioctl()函数,实现相应的性能,数据从应用层、VFS 虚构文件系统再到驱动层再最终传达到硬件设施,通过比照 crystal_guang 老师第二幅图,能够理解 VFS 层的实现根底就是库以及内核。
2. 设么是内核态?什么是用户态

图片来自于在此期间的笔记,曾经很充沛了,咱们这次次要的工作就是实现用户态到内核态的切换,从切换形式可知咱们是通过零碎调用来应用驱动的。

  1. 怎么写
    仍然拿出草根老师的图:

驱动的实现具体就是三部,分为底层(本人写的)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 环境中必须要理解这些常识。

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 <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>        // 函數指針寄存
#include <linux/device.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
#include <linux/kernel.h>
#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 <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#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

退出移动版