linux驱动开发4之字符设备驱动原理及led编程

时间:2024-04-09 15:56:06

1.系统整体工作原理

1)应用层->API->设备驱动->硬件

2)API:open、read、write、close等

3)驱动源码中提供真正的open、read、write、close等函数实体

2.file_operations结构体(#include <linux/fs.h>)

linux驱动开发4之字符设备驱动原理及led编程

1)元素主要是函数指针,用来挂接实体函数地址

2)每个设备驱动都需要一个该结构体类型的变量

3)设备驱动向内核注册时提供该结构体类型的变量

3.注册字符设备驱动

1)为何要注册驱动

2)谁去负责注册

3)向谁注册

4)注册函数从哪里来

5)注册前怎样?注册后怎样?注册产生什么结果?

4.register_chrdev详解(#include <linux/fs.h>)

linux驱动开发4之字符设备驱动原理及led编程

linux驱动开发4之字符设备驱动原理及led编程

include/linux/Fs.h

1)作用,驱动向内核注册自己的file_operations

2)参数

3)inline和static

5.内核如何管理字符设备驱动

1)内核中有一个数组用来存储注册的字符设备驱动,但数组的大小是一定的,比如255

2)register_chrdev内部将我们要注册的驱动的信息(主要是 )存储在数组中相应的位置

3)cat /proc/devices查看内核中已经注册过的字符设备驱动(和块设备驱动)

linux驱动开发4之字符设备驱动原理及led编程linux驱动开发4之字符设备驱动原理及led编程

4)好好理解主设备号(major)的概念

6.回顾和展望

1)回顾:inline、static等关键字

2)回顾:/proc文件系统的作用

3)展望:将来深入学习驱动时可以去跟register_chrdev到内部看,验证我们上面讲的原理

7.思路和框架

1)目的:给空模块添加驱动壳子

2)核心工作量:file_operations及其元素填充、注册驱动

8.如何动手写驱动代码

1)脑海里先有框架,知道自己要干嘛

2)细节代码不需要一个字一个字敲,可以到内核中去寻找参考代码复制过来改

3)写下的所有代码必须心里清楚明白,不能似懂非懂

9.开始动手

1)先定义file_operations结构体变量

2)open和close函数原型确定、内容填充

static int led_open(struct inode *ip, struct file *fp)

{

    printk(KERN_INFO "led_open\n");

    return 0;

}

 

static int led_release(struct inode *ip, struct file *fp)

{

    printk(KERN_INFO "led_release\n");

    return 0;

}

 

 

//定义一个file_operations结构体变量

static const struct file_operations led_fops = {

    .owner      = THIS_MODULE,

    //.llseek   = no_llseek,

    //.read     = led_read,

    //.write    = led_write,

    .open       = led_open, //将来应用open打开这个设备时实际调用的函数

    .release    = led_release,

};

10.注册驱动

1)主设备号的选择

2)返回值的检测

// 模块安装函数

static int __init chrdev_init(void)

{  

    int ret = -1;

    printk(KERN_INFO "chrdev_init led_driver init\n");

    //在module_init宏调用的函数中去注册字符设备驱动

    ret = register_chrdev(MYMAJOR, MYNAME, &led_fops);

    if(ret != 0)

    {

        printk(KERN_ERR "led_driver register error!\n");

        return -EINVAL;

    }

    printk(KERN_INFO "led_driver register success!\n");

   

   

    return 0;

}

11.驱动测试

1)编译等 make && make cp

2)insmod并且查看设备注册的现象

3)rmmod并且查看设备注销的现象

linux驱动开发4之字符设备驱动原理及led编程

12.让内核自动分配主设备号

1)为什么要让内核自动分配

    //major传0进去表示让内核帮我们自动分配一个合适的空白的没被使用的主设备号

    //如果分配失败就会返回负数

    mymajor = register_chrdev(0, MYNAME, &led_fops);

    if(mymajor != 0)

    {

        printk(KERN_ERR "led_driver register error!\n");

        return -EINVAL;

    }

    printk(KERN_INFO "led_driver register success!\n");

2)如何实现?

#include <linux/module.h>       // module_init  module_exit

#include <linux/init.h>         // __init   __exit

#include <linux/fs.h>

 

#define MYMAJOR 200

#define MYNAME  "led_driver"

 

int mymajor;

 

static int led_open(struct inode *ip, struct file *fp)

{

    printk(KERN_INFO "led_open\n");

    return 0;

}

 

static int led_release(struct inode *ip, struct file *fp)

{

    printk(KERN_INFO "led_release\n");

    return 0;

}

 

 

//定义一个file_operations结构体变量

static const struct file_operations led_fops = {

    .owner      = THIS_MODULE,

    //.llseek   = no_llseek,

    //.read     = led_read,

    //.write    = led_write,

    .open       = led_open, //将来应用open打开这个设备时实际调用的函数

    .release    = led_release,

};

 

// 模块安装函数

static int __init chrdev_init(void)

{  

 

    printk(KERN_INFO "chrdev_init led_driver init\n");

    //在module_init宏调用的函数中去注册字符设备驱动

    //major传0进去表示让内核帮我们自动分配一个合适的空白的没被使用的主设备号

    //如果分配失败就会返回负数

    mymajor = register_chrdev(0, MYNAME, &led_fops);

    if(mymajor < 0)

    {

        printk(KERN_ERR "led_driver register error!\n");

        return -EINVAL;

    }

    printk(KERN_INFO "led_driver register success! mymajor:%d.\n", mymajor);

   

   

    return 0;

}

 

// 模块下载函数

static void __exit chrdev_exit(void)

{

    printk(KERN_INFO "led_driver unregister!\n");

    //在module_exit宏调用的函数去注销字符设备驱动

   

    unregister_chrdev(mymajor, MYNAME);

   

}

 

 

module_init(chrdev_init);

module_exit(chrdev_exit);

 

// MODULE_xxx这种宏作用是用来添加模块描述信息

MODULE_LICENSE("GPL");              // 描述模块的许可证

MODULE_AUTHOR("aston");             // 描述模块的作者

MODULE_DESCRIPTION("module test");  // 描述模块的介绍信息

MODULE_ALIAS("alias xxx");          // 描述模块的别名信息

3)测试

13.驱动设备文件的创建

1)何为设备文件

2)设备文件的关键信息是:设备号 = 主设备号 + 次设备号,使用ls -l去查看设备文件,就可以得到这个设备文件对应的主次设备号。

3)使用mknod创建设备文件:mknod /dev/xxx c 主设备号 次设备号:C表示字符设备驱动

使用主次设备号来绑定/dev目录下的XXX文件

linux驱动开发4之字符设备驱动原理及led编程

linux驱动开发4之字符设备驱动原理及led编程

14.写应用来测试驱动

1)还是原来的应用

2)open、write、read、close等

3)实验现象预测和验证

#include <stdio.h>

#include <string.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <unistd.h>

 

#define FILE    "/dev/test" //刚才mknod创建的dev设备

 

 

int main(void)

{

    int fd = -1;

    //第一步:打开文件

    fd = open(FILE, O_RDWR);

    if(fd < 0)

    {

        perror("open:");

        return -1;

    }

    printf("open %s success!.\n", FILE);

   

    //第二步:读写文件

   

   

    //第三步:关闭文件

    close(fd);

    return 0;

}

linux驱动开发4之字符设备驱动原理及led编程

15.总结

1)整体流程梳理、注意分层

2)后续工作:添加读写接口

16.添加读写接口

16.1在驱动中添加

ssize_t led_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)

{

    printk(KERN_INFO "led_device_led_read!\n");

   

    return 0;

}

 

static ssize_t led_write( struct file * file, const char __user * buf,

                size_t count, loff_t *ppos )

{

    printk(KERN_INFO "led_device_led_write!\n");

   

    return 0;

}

16.2在应用中添加

//第二步:读写文件

    write(fd,"test for led driver!", 20);

    //read(fd, buf, 100);

16.3测试

linux驱动开发4之字符设备驱动原理及led编程

17.应用和驱动之间的数据交换

1)copy_from_user,用来将数据从用户空间复制到内核空间

2)copy_to_user

注意:复制是和mmap的映射相对应去区分的

ssize_t led_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)

{

    printk(KERN_INFO "led_device_led_read success!\n");

   

    return 0;

}

 

static ssize_t led_write( struct file * file, const char __user * buf,

                size_t count, loff_t *ppos )

{

    printk(KERN_INFO "led_device_led_write success!\n");

   

    return 0;

}

18.完成writeread函数

ssize_t led_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)

{

    int ret = -1;

    printk(KERN_INFO "led_device_led_read success!\n");

   

    //读取内核中kbuf的数据

    ret = copy_to_user(buf, kbuf, size);

    if(ret != 0)

    {

        printk(KERN_ERR "copy_to_user fail!\n");

    }

    printk(KERN_INFO "copy_to_user success!\n");

   

    return 0;

}

 

static ssize_t led_write( struct file * file, const char __user * buf,

                size_t count, loff_t *ppos )

{

    int ret = -1;

    printk(KERN_INFO "led_device_led_write success!\n");

   

    //使用copy_from_user函数将应用层传过来的buf内容拷贝到驱动空间的一个buf中

    ret = copy_from_user(kbuf, buf, count);

   

    if(ret != 0)

    {

        printk(KERN_ERR "copy_from_user fail!\n");

    }

    printk(KERN_INFO "copy_from_user success!\n");

    //下面就是对数据的操作,根据数据去操作硬件,下面就是操作硬件代码

   

   

    return 0;

}

copy_from_user函数的返回值定义,和常规有点不同。返回值如果成功复制则返回0,如果 不成功复制则返回尚未成功复制剩下的字节数。

static inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)

static inline long copy_to_user(void __user *to, const void *from, unsigned long n)

19.读写回环测试

linux驱动开发4之字符设备驱动原理及led编程

20.总结

1)目前为止应用已经能够读写驱动(中的内存)

2)后续工作:添加硬件操作代码

21.驱动中如何操控硬件

21.1还是那个硬件

1)硬件物理原理不变

2)硬件操作接口(寄存器)不变

3)硬件操作代码不变

21.2哪里不同了?

1)寄存器地址不同。原来是直接用物理地址,现在需要用该物理地址在内核虚拟地址空间相对应的虚拟地址。寄存器的物理地址是CPU设计时决定的,从datasheet中查找到的。

2)编程方法不同。裸机中习惯直接用函数指针操作寄存器地址,而kernel中习惯用封装好的io读写函数来操作寄存器,以实现最大程度可移植性。

22.内核的虚拟地址映射方法

1)为什么需要虚拟地址映射

2)内核中有2套虚拟地址映射方法:动态和静态

3)静态映射方法的特点:

         内核移植时以代码的形式硬编码,如果要更改必须改源代码后重新编译内核

         在内核启动时建立静态映射表,到内核关机时销毁,中间一直有效

         对于移植好的内核,你用不用他都在那里

4)动态映射方法的特点:

         驱动程序根据需要随时动态的建立映射、使用、销毁映射

         映射是短期临时的

23.如何选择虚拟地址映射方法

1)2种映射并不排他,可以同时使用

2)静态映射类似于C语言中全局变量,动态方式类似于C语言中malloc堆内存

3)静态映射的好处是执行效率高,坏处是始终占用虚拟地址空间;动态映射的好处是按需使用虚拟地址空间,坏处是每次使用前后都需要代码去建立映射&销毁映射(还得学会使用那些内核函数的使用)

20.静态映射操作LED

20.1关于静态映射要说的

1)不同版本内核中静态映射表位置、文件名可能不同

2)不同SoC的静态映射表位置、文件名可能不同

3)所谓映射表其实就是头文件中的宏定义

20.2三星版本内核中的静态映射表

1)主映射表位于:arch/arm/plat-s5p/include/plat/map-s5p.h

linux驱动开发4之字符设备驱动原理及led编程

CPU在安排寄存器地址时不是随意乱序分布的,而是按照模块去区分的。每一个模块内部的很多个寄存器的地址是连续的。所以内核在定义寄存器地址时都是先找到基地址,然后再用基地址+偏移量来寻找具体的一个寄存器。

map-s5p.h中定义的就是要用到的几个模块的寄存器基地址。

map-s5p.h中定义的是模块的寄存器基地址的虚拟地址。

2)虚拟地址基地址定义在:arch/arm/plat-samsung/include/plat/map-base.h

#define S3C_ADDR_BASE    (0xFD000000)          // 三星移植时确定的静态映射表的基地址,表中的所有虚拟地址都是以这个地址+偏移量来指定的

linux驱动开发4之字符设备驱动原理及led编程

3)GPIO相关的主映射表位于:arch/arm/mach-s5pv210/include/mach/regs-gpio.h

linux驱动开发4之字符设备驱动原理及led编程

表中是GPIO的各个端口的基地址的定义

4)GPIO的具体寄存器定义位于:arch/arm/mach-s5pv210/include/mach/gpio-bank.h

linux驱动开发4之字符设备驱动原理及led编程

20.3.参考裸机中的操作方法添加LED操作代码

1)宏定义

// 模块安装函数

static int __init chrdev_init(void)

{  

 

    printk(KERN_INFO "chrdev_init led_driver init\n");

    //在module_init宏调用的函数中去注册字符设备驱动

    //major传0进去表示让内核帮我们自动分配一个合适的空白的没被使用的主设备号

    //如果分配失败就会返回负数

    mymajor = register_chrdev(0, MYNAME, &led_fops);

    if(mymajor < 0)

    {

        printk(KERN_ERR "led_driver register error!\n");

        return -EINVAL;

    }

    printk(KERN_INFO "led_driver register success! mymajor:%d.\n", mymajor);

    //inmod时执行硬件操作

    // led初始化,也就是把GPJ0CON中设置为输出模式

    rGPJ0CON = 0x11111111;

    // led亮

    rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));

 

    return 0;

}

// 模块下载函数

static void __exit chrdev_exit(void)

{

    printk(KERN_INFO "led_driver unregister!\n");

    //在module_exit宏调用的函数去注销字符设备驱动   

    unregister_chrdev(mymajor, MYNAME);

    // led灭

    rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));  

}

2)在init和exit函数中分别点亮和熄灭LED

20.4实践测试

1)insmod和rmmod时观察LED亮灭变化

linux驱动开发4之字符设备驱动原理及led编程

2)打印出寄存器的值和静态映射表中的分析相对比

20.3将代码移动到openclose函数中去

21.添加驱动中的写函数

1)先定义好应用和驱动之间的控制接口,这个是由自己来定义的。譬如定义为:应用向驱动写"on"则驱动让LED亮,应用向驱动写"off",驱动就让LED灭

2)应用和驱动的接口定义做的尽量简单,譬如用1个字目来表示。譬如定义为:应用写"1"表示灯亮,写"0"表示让灯灭。

22.2 写应用来测试写函数

App.c中的代码:

#include <stdio.h>

#include <string.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <unistd.h>

#define FILE    "/dev/test" //刚才mknod创建的dev设备

char buf[100];

int main(void)

{

    int fd = -1;

    //第一步:打开文件

    fd = open(FILE, O_RDWR);

    if(fd < 0)

    {

        perror("open:");

        return -1;

    }

    printf("open %s success!.\n", FILE); 

    //第二步:读写文件   

    while(1)

    {

        printf("请输入对led灯的操作:\n");

        memset(buf, 0, sizeof(buf));

        scanf("%s", buf);

        if(!strcmp(buf, "on"))

        {

            write(fd,"1", 20);

            memset(buf, 0, sizeof(buf));

            read(fd, buf, 100);

            printf("灯亮读出来的内容是:%s.\n", buf);

        }

        else if(!strcmp(buf, "off"))

        {

            write(fd,"0", 20);

            memset(buf, 0, sizeof(buf));

            read(fd, buf, 100);

            printf("灯灭读出来的内容是:%s.\n", buf);

        }

        else

        {

            return 0;

        }       

    }      

    return 0;

}

Module_test.c中的代码:

#include <linux/module.h>       // module_init  module_exit

#include <linux/init.h>         // __init   __exit

#include <linux/fs.h>

#include <asm/uaccess.h>

#include <mach/regs-gpio.h>

#include <mach/gpio-bank.h>

#include <linux/string.h>

#define MYMAJOR 200

#define MYNAME  "led_driver"

#define GPJ0CON     S5PV210_GPJ0CON

#define GPJ0DAT     S5PV210_GPJ0DAT

#define rGPJ0CON    *((volatile unsigned int *)GPJ0CON)

#define rGPJ0DAT    *((volatile unsigned int *)GPJ0DAT)

char kbuf[100];

int mymajor;

void led_on(void)

{

    // led初始化,也就是把GPJ0CON中设置为输出模式

    rGPJ0CON = 0x11111111;

    // led亮

    rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));

}

void led_off(void)

{

    // led初始化,也就是把GPJ0CON中设置为输出模式

    rGPJ0CON = 0x11111111;

    // led亮

    rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));

}

static int led_open(struct inode *ip, struct file *fp)

{

    printk(KERN_INFO "led_device_open\n");

    printk(KERN_INFO "operaton:led_on/led_off\n");

    return 0;

}

ssize_t led_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)

{

    int ret = -1;

    printk(KERN_INFO "led_device_led_read success!\n");   

    //读取内核中kbuf的数据

    ret = copy_to_user(buf, kbuf, size);

    if(ret != 0)

    {

        printk(KERN_ERR "copy_to_user fail!\n");

    }

    printk(KERN_INFO "copy_to_user success!\n");

   

    return 0;

}

static ssize_t led_write( struct file * file, const char __user * buf,

                size_t count, loff_t *ppos )

{

    int ret = -1;

    printk(KERN_INFO "led_device_led_write success!\n");   

    //使用copy_from_user函数将应用层传过来的buf内容拷贝到驱动空间的一个buf中

    memset(kbuf, 0, sizeof(kbuf));

    ret = copy_from_user(kbuf, buf, count);  

    if(ret != 0)

    {

        printk(KERN_ERR "copy_from_user fail!\n");

    }

    printk(KERN_INFO "copy_from_user success!\n");

    //下面就是对数据的操作,根据数据去操作硬件,下面就是操作硬件代码

    if(kbuf[0] == '1')

    {

        led_on();

    }

    else if(kbuf[0] == '0')

    {

        led_off();

    }

    else

    {

        return 0;

    }  

/*  if(!strcmp(kbuf, "on"))

    {

        led_on();

    }

    else if(!strcmp(kbuf, "off"))

    {

        led_off();

    }

    else

    {

        return 0;

    }   

*/ 

    return 0;

}

static int led_release(struct inode *ip, struct file *fp)

{

    printk(KERN_INFO "led_device_release\n");

    return 0;

}

//定义一个file_operations结构体变量

static const struct file_operations led_fops = {

    .owner      = THIS_MODULE,

    .read       = led_read,

    .write      = led_write,

    .open       = led_open, //将来应用open打开这个设备时实际调用的函数

    .release    = led_release,

};

// 模块安装函数

static int __init chrdev_init(void)

{  

    printk(KERN_INFO "chrdev_init led_driver init\n");

    //在module_init宏调用的函数中去注册字符设备驱动

    //major传0进去表示让内核帮我们自动分配一个合适的空白的没被使用的主设备号

    //如果分配失败就会返回负数

    mymajor = register_chrdev(0, MYNAME, &led_fops);

    if(mymajor < 0)

    {

        printk(KERN_ERR "led_driver register error!\n");

        return -EINVAL;

    }

    printk(KERN_INFO "led_driver register success! mymajor:%d.\n", mymajor);

    /*

    //inmod时执行硬件操作

    // led初始化,也就是把GPJ0CON中设置为输出模式

    rGPJ0CON = 0x11111111;

    // led亮

    rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));

    printk(KERN_INFO "GPJ0CON = 0x%p.\n", GPJ0CON);

    printk(KERN_INFO "GPJ0DAT = 0x%p.\n", GPJ0DAT);

    */

    return 0;

}

// 模块下载函数

static void __exit chrdev_exit(void)

{

    printk(KERN_INFO "led_driver unregister!\n");

    //在module_exit宏调用的函数去注销字符设备驱动  

    unregister_chrdev(mymajor, MYNAME);

    /*

    // led灭

    rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));

    */

}

module_init(chrdev_init);

module_exit(chrdev_exit);

// MODULE_xxx这种宏作用是用来添加模块描述信息

MODULE_LICENSE("GPL");              // 描述模块的许可证

MODULE_AUTHOR("aston");             // 描述模块的作者

MODULE_DESCRIPTION("module test");  // 描述模块的介绍信息

MODULE_ALIAS("alias xxx");          // 描述模块的别名信息

linux驱动开发4之字符设备驱动原理及led编程

22.3驱动和应用中来添加读功能

23.动态映射操作LED

23.1如何建立动态映射

1)request_mem_region,向内核申请(报告)需要映射的内存资源。

2)ioremap,真正用来实现映射,传给他物理地址他给你映射返回一个虚拟地址

23.2如何销毁动态映射

1)iounmap

2)release_mem_region

注意:映射建立时,是要先申请再映射;然后使用;使用完要解除映射时要先解除映射再释放申请。

23.3代码实践

1)2个寄存器分开独立映射

2)2个寄存器在一起映射