Linux驱动——学习笔记


一、烧写Linux系统到inand

1、烧写u-boot到inand
    tftp 30008000 u-boot.bin
    movi write u-boot 30008000
2、烧写Linux内核到inand
    tftp 30008000 zImage-qt
    movi write kernal 30008000
3、烧写文件系统到inand
    开发板中已经有文件系统,无须再烧,设置启动参数即可。
4、设置启动环境变量
setenv bootcmd "movi read kernel 30008000;bootm 30008000"
setenv bootargs "console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext3"

二、挂载ubuntu系统的文件夹到开发板

目的:是为了把编译好驱动程序下载到共享目下。
1、进入开发板系统 root 123456
2、检查开板的ip地址
3、在启动脚本添加以下内容:vi /etc/init.d/rcS
    mount -t nfs -o nolock 192.168.1.66:/nfs /mnt

三、Linux内核下的C语言编程

1、命名习惯
    普通代码:
        #define PI 3.14159
        int minVal,maxVal;
        void sendData(void);
    内核代码:
        #define PI 3.14159
        int min_val,max_val;
        void send_data(void);

2、GNU C与ANSI C
    GNU编译器是专门为Linux内核所设计的一款编译器,而且Linux内核中也使用大量只有GNU编译才支持的C语言语法,因此只有GNU编译器才能编译内核代码和Linux驱动程序。
    a、零长数组
        typedef struct {
            int len;
            char data[0];
        }Data;

        Data* dp = malloc(sizeof(Data)+1024);
        dp->len = 1024;

        send(sockfd,dp,sizeof(dp->len+sizeof(*dp)),0);
    b、case范围
        switch(n)
        {
            case 1 ... 5: break;
            case 6 ... 9: break;
        }
    c、typeof
        定义一个宏求两个变量的最大值。
        #define MIN(a,b) ((a)>(b)?(a):(b))
        #define MIN(type,a,b) ({type _a=num1++;type _b=num2++;_a>_b?_a:_b;})

        MIN(int,num1++,num2++) => num1++ > num2++ ? num1++ : num2++;
        #define MIN(a,b) ({typeof(a) _a=a; typeof(b) _b=b; _a>_b?_a:_b;})

        定义一个宏交换两个变量值。
        #define swap(a,b) ({typeof(a) t=a; a=b; b =t;})

    d、可变参长数宏
        int printf(const char *format, ...);
        #define debug(ftm,arg,...) printf(ftm,##arg)
    e、标号元素
        ANSI C
        struct Student stu = {
            name:"hehe",
            sex:'m',
            age:18
        };

        GNU C
        struct Student stu = {
            .name = "hehe",
            .sex = 'm',
            .age = 18
        };

3、do{ } while(0)
    定义一个宏函数,用来释放堆内存。
    #define FREE_HEAP(p) do{free(p); p = NULL;}while(0)

    int func(void)
    {
        if(NULL != p)
            FREE_HEAP(p)
        else
            return -1;
    }

4、goto语句
    一般的企业代码中禁止使用goto语句,因为goto可以跳转到函数内的任意位置,会打破原有分支、循环结构、或业务逻辑,所以goto是应用层代码中是一种非常危险的语句,但在内核中却有它独到的功能。
    int example(void)
    {
        if(!register_a())
        {
            goto err1;
        }

        if(!register_b())
        {
            goto err2:
        }

        if(!register_c())
        {
            goto err3:
        }

        // work

    err3:
        unregister_c();
    err2:
        unregister_b();
    err1:
        unretister_a();
    err:
        return ret;
    }
    在内核中,资源的申请与释放必须是逆序,而goto语句能够把错误处理的代码写的安全、严谨而有简洁。

四、内核模块

通过分析Linux内核的编译过程,了解到一个实事:Linux内核是由很多模块组成了。
1、内核以模块的形式组成的好处
    a、可以灵活的进行裁剪
    b、可以动态加载或卸载
    c、可以尽量控制内核的大小
2、内核模块的特点
    a、内核模块代码最终是要加入内核中的
    b、内核模块代码工作在内核态。
    c、内核模块代码一旦出错,可能会导致整个内核(操作)崩溃。
    d、在Linux系统中驱动程序是一个内核模块,它按照内核模块的格式进行编写。
3、内核模块的结构
    a、模块加载函数(必须)
        模块加载内核时自动执行此函数,相当于内核模块的入口函数。
        此函数一般要做些初始化工作、申请内存、资源,注册相关信息。
    b、模块卸载函数(必须)
        模块被卸载时自动执行。
        此函数一般要做些资源的释放、销毁的工作。
    c、模块声明许可证(必须)
    d、模块作者信息声明(可选)
    e、其它一些自定义函数,可以被模块加载函数调用。
4、内核模块的加载与卸载
    insmod hello.ko
    rmmod hello.ko

五、printfk函数

printfk是在内核中运行的向控制台输出显示信息的函数,与printf函数的用法基本一致。
内核中的消息是分等级的,定义在include/linux/kernal.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            */
    printk 默认的等级是 KERN_NOTICE
在操作系统中可以设置显示消息等级,数字越大,级别超低。
    cat  /proc/sys/kernel/printk 可以看当前系统设置的消息等级
    echo "5" > /proc/sys/kernel/printk

一、模块参数

在加载模块时可以像应用程序一样附加一些参数。
定义变量:用来存储数据。
注册变量:让内核允许加载模块时变量被赋值。
    module_param(变量名,类型,权限);
    类型:
        byte、short、ushort、int、uint、long、ulong、charp
    权限:
        #define S_IRUSR 00400
        #define S_IWUSR 00200
        #define S_IXUSR 00100
传递参数:insmod xxx.ko 变量名=数据
模块导出符号:
    EXPORT_SYMBOL_GPL(符号);
    导出的符号(变量名、函数名)可以被其他模块使用,使用前声明一下即可。

二、驱动概述

什么是驱动:就是控制硬件工作的软件。
在Linux系统下,驱动程序工作在内核中,以内核模块形式存在,它能能够控制硬件工作,或者提供控制硬件的接口。
驱动程序提供接口归内核调用,然后内核再统一抽象成文件(设备文件),代应用的程序以操作文件式进行控制硬件。
应用程序工作在用户态,驱动程序工作在内核态,它们之间不能直接联系,需要按照内核提供的接口(open/read/write/close)来传递数据。
在Linux系统下一切皆文件。

三、驱动的类型

字符设备:在IO过程中以字符或字节为单位进行数据传输,数据也要按照一定的顺序进行读写。
    操作这种设备的接口有:open/close/read/write
    常见的字符设备有:鼠标、键盘、串口、LED灯、温度传感器、湿度传感器等。
    字符设备由于功能杂乱,设备也混乱,一般都是小厂家生产的没有统一的接口,所以大多数据需要编写驱动的都是字符设备。
块设备:以块为单位进行数据的读写,用记在读写数据时必须最少读一块,这种设备一般都带缓冲区,而且数据量一般都很大,数据在读写时一般不会立即到达硬件。
    操作这种设备的接口有:open/close/read/write/sync/fsync/fdatasync。
    常见的块设备有:硬盘、SD卡、Nand、iNand。
    虽然存储介质不同,但都有一个控制器不需要外部直接操作,只需要按照控制器的接口来操作即可。
网络设备:具有网络通信协议的设备,网络设备没有设备文件,必须使用socket进行操作。
    操作这种设备的接口有:send/recv/sendto/recvfrom。
    网络设备都非常标准的、规范,所以有了万能的网卡驱动,顶多需要移植一下,也不需要编写驱动。

四、设备文件

在Linux系统会把硬件抽象成文件来制作(网上除外)。
查看设备文件:ls -l /dev
每个字母表示文件的类型:
    -   普通文件
    d   目录文件
    l   链接文件
    c   字符设备文件
    b   块设备文件
    p   管道文件
    s   socket文件
设备号:主设备号+从设备号组成
    主设备号:表示硬件的类型,1~254
    从设备号:硬件的编号,0~255
    它是在驱动程序中确定的,可以让内核自动生成,也可以手动确定,或者手动创建。
    注意:设备不能重复。

五、字符设备驱动

1、字符设备驱动分析
    drivers\char\Cs5535_gpio.c
    a、按照内核模块格式编写
    b、创建设备号、初始化、添加
    c、实现文件操作函数
2、设备号
    dev_t dev_id = MKDEV(主设备号,从设备号);
    功能:生成一个设备号
    返回值:主设备号+从设备号的合体
    注意:不一定能使用。

    int register_chrdev_region(dev_t from, unsigned count, const char *name)
    功能:注册希望使用的设备号,如果已经被占用则失败。
    from:希望使用的设备号,MKDEV的返回值
    count:设备的数量
    name:设备文件名
    返回值:成功返回0

    int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)
    dev:内核分配的设备号,返回值
    baseminor:设备号的起始值
    count:设备的数量
    name:设备名
    返回值:成功返回0

    MAJOR(dev_id) 解析出主设备号
    MINOR(dev_id) 解析出从设备号

    void unregister_chrdev_region(dev_t from, unsigned count)
    功能:释放设备号
    from:设备号
    count:数量
3、字符设备结构体
    struct cdev {
        struct kobject kobj;  // 内嵌的对象
        struct module *owner; // 模块名
        const struct file_operations *ops; // 文件操作结构体(函数指针)
        struct list_head list;
        dev_t dev; // 设备号
        unsigned int count; // 设备数量
    };

    void cdev_init(struct cdev *cdev, const struct file_operations *fops);
    功能:初始化字符设备结构体
    cdev:字符设备结构体
    fops:文件操作结构体

    int cdev_add(struct cdev *p, dev_t dev, unsigned count)
    功能:添加字符设备到内核
    p:被初始化过的字符设备结构体
    dev:注册过的设备号
    count:设备数量

    struct file_operations {
        // 模块名
        struct module *owner; 
        // 设备文件位置指针
        loff_t (*llseek) (struct file *, loff_t, int);
        // read函数
        ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
        // write
        ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
        // poll
        unsigned int (*poll) (struct file *, struct poll_table_struct *);
        // ioctl
        int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
        // mmap
        int (*mmap) (struct file *, struct vm_area_struct *);
        // open
        int (*open) (struct inode *, struct file 
        // flush
        int (*flush) (struct file *, fl_owner_t id);
        // close
        int (*release) (struct inode *, struct file *);
    }

六、设备文件

1、手动创建
    mknod 创建设备文件的命令。
    cat /proc/devices 查看内核分配的主设备号
    mknod /dev/char_v1 c 250 0

2、自动创建
内核提供一组函数用于自动创建设备文件,设备类对动对应的数据结构:struct class
    class_create->*__class_create
    struct class *__class_create(struct module *owner, const char *name,struct lock_class_key *key)
    功能:在内核中创建设备类 /sys/class/xxx
    owner:模块名
    key:设备类的名字
    返回值:设备类指针

    struct device *device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...)
    功能:/sys/class/xxx/在dev目录下自动创建设备文件
    class:设备类指针,class_create函数的返回值
    parent:设备结构体指针,兼容各种类型的设备,此处填写字符设备结构指针。
    devt:设备号
    drvdata:创建设备文件时的附加信息
    fmt:设备文件名

void device_destroy(struct class *class, dev_t devt)
功能:删除设备
class:设备类指针,class_create函数的返回值
devt:设备号

void class_destroy(struct class *cls)
功能:从内核中删除设备类

一、驱动程序中操作GPIO

在内核中已经把各个GPIO定义为宏,不需要再找地址了,在头文件 arch/arm/mach-sv5pv210/include/mach/gpio.h。
int gpio_request(unsigned gpio, const char *label)
功能:申请GPIO资源
gpio:GPIO管脚的编号,在gpio.h头文件中有定义
label:给GPIO管脚取个名字

void gpio_free(unsigned gpio)
功能:释放GPIO资源

int gpio_direction_input(unsigned gpio)
功能:设置GPIO管脚为输入模式

int gpio_direction_output(unsigned gpio, int value)
功能:设置GPIO管脚为输出模式

int gpio_get_value(unsigned gpio)
功能:从GPIO管脚读取数据

void gpio_set_value(unsigned gpio, int value)
功能:向GPIO管脚写入数据

二、ioctl

int ioctl(int d, int request, ...);
功能:从应用层来调用内核层的ioctl函数,对设备进行设置。
d:文件描述符,open函数的返回值
request:命令
...:附加参数,存储在用户层,以指针的方式交给内核层。
返回值:成功返回0,失败返回-1。

int (*ioctl) (struct inode *, struct file *, unsigned int cmd, unsigned long arg);
功能:响应应用层的ioctl函数,对硬件进行设置。
cmd:对应应用层request
arg:实际上是一个指针,它第指向用户层的一段数据。

三、内核的内存管理

unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)
功能:从用户层拷贝数据到内核层
to:指向内核层空间的指针
from:指向用户层空间的指针
n:要拷贝的字节数据
返回值:成功拷贝的字节数

long copy_to_user(void __user *to,const void *from, unsigned long n)
功能:从内核层拷贝数据到用户层
to:指向用户层空间的指针
from:指向内核层空间的指针
n:要拷贝的字节数据
返回值:成功拷贝的字节数

void *kmalloc(int size)
功能:与标准C中的用法一致,其实就是调用malloc,kmalloc只是一个宏名。

void kfree(void *where)
功能:释放内存

Author: Ikaros
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source Ikaros !
评论
 Previous
ONVIF 设备发现(网络摄像头)——实例笔记 ONVIF 设备发现(网络摄像头)——实例笔记
相关配置ONVIF官网:http://www.onvif.org/gSOAP安装配置:gSOAP安装配置+使用案例参考+参考链接操作系统:CentOS7 资料参考:许振坪的ONVIF专栏:传送门onvif开发之设备发现功能的实现Linux下
Next 
Linux系统移植——学习笔记 Linux系统移植——学习笔记
一、u-boot工程1、BootLoader介绍BootLoader是操作系统运行之前要执行的一段程序,它负责初始化硬件设备、建立内容空间映射,从而操作系统的运行做好准备,是一个专门加载操作系统的程序。 对于嵌入式系统而言,没
  TOC