字符设备驱动-1

Linux设备驱动种类及层次

Linux设备驱动的种类:字符设备驱动,块设备驱动,网络设备驱动

字符设备驱动:按照字节流来访问,只能顺序访问,不能无序访问的设备。
块设备驱动:按照block(512字节)来访问,可以顺序访问,也可以无序访问的设备
网络设备驱动:网络设备驱动没有设备文件,网络设备驱动主要是用来实现网络数据的收发工作

在这里插入图片描述

字符设备驱动

字符设备驱动的框架结构

请添加图片描述

内核层的 led_driver是字符设备驱动(驱动可以控制灯的亮灭)
驱动安装后不能自动运行,意思就是说insmod安装驱动的时候只是提供入口,它不能来控制灯闪烁。因为驱动是服务于用户的(应用程序)。所以必须要有一个应用程序来控制灯的驱动,由驱动来控制灯。

驱动写好之后 应用程序怎么访问驱动呢?
文件类型有bsp-lcd这几种
灯是c 字符设备文件。
驱动就会在上层产生一个文件供应用程序访问。

例如:鼠标的例子
应用程序怎么从鼠标中读取数据?
cd /dev/input 路径下有mouse0 mouse1 mouse2 结点
如果想从里面读数据 把它当成一个文件 使用open打开这个文件 read读数据 close关闭
/dev/input/mouse0文件就是驱动对应的设备文件
(输入类设备在input下放,普通设备就直接在dev下)
这个文件相当于给驱动在上层开了一个窗口,通过这个文件能找到这个驱动,并通过这个文件能访问到这个驱动。

**字符设备文件与访问普通文件的区别是:**普通文件通过open read write 访问的时候是通过文件系统 块设备驱动最终在磁盘上存储的。
字符设备文件是一个特殊的文件,如果要访问字符设备文件,就相当于在访问字符设备驱动,通过字符设备驱动访问鼠标等硬件。

对于这个文件怎么能准确找到你的驱动的呢?
对于每一个驱动内核会分配一个唯一的设备号。
/dev/input/mouse0文件和设备号关联
在这里插入图片描述
通过设备号就可以在内核中找到唯一的驱动。
设备号的组成:例如 13,32
13 << 20 | 32 组合成一个设备号

此时应用程序要访问灯,
open read write close 访问 /dev/myled,
应用程序通过设备文件就可以找到驱动进而访问驱动,
当open调用的时候要打开设备文件,open是系统调用,系统调用底层通过什么来实现的?
(A7核运行的模式有7种)
软中断 : user模式会切换到svc模式下 就进入到内核空间了

进入到内核空间后,驱动中有这些函数接口led_open led_read led_write led_close

当上层的open调的时候,通过系统调用,通过软中断,最终进入到内核空间,通过设备号找到驱动后就会回调驱动中对应的函数。read write close 同理。
鼠标调用同理。

如果是普通文件他会通过文件系统访问块设备文件,然后把文件存在磁盘上。
而现在的/dev/myled 和/dev/input/mouse0 叫做设备文件。设备文件相当于驱动在上层的抽象。
等价于当你用open read write close时候最终调到设备驱动中的led_open led_read led_write led_close 接口

驱动有操作硬件的能力,但没有操作硬件的逻辑。
控制灯的亮灭是用户实现的,驱动能控制灯的亮或者灭,但是不知道怎么亮怎么灭。
就像网卡驱动一样,网卡驱动可以控制网卡收发数据,但是不知道收发那些数据。

电脑怎么知道open所对应的内核层的函数叫做led_open呢?
通过函数指针实现的。你的应用层open调用的时候,进入到内核空间后,会访问open函数指针。因为你把你的函数赋给了函数指针,所以可以通过函数指针回调到函数。
这些函数指针在内存中不能离散存在,所以把这些函数指放在了一个操作方法结构体中,叫做file_operations 又叫fops。

面试问题:请问同学简述字符设备驱动的流程

内核空间写led_open led_read led_write led_close 函数访问硬件。
驱动能向下访问硬件但是没有逻辑,所以驱动还要向上提供操作接口。
驱动就会在上层创建设备文件(设备结点),设备文件通过设备号找到驱动。上层的
open read write close访问的时候 就会通过设备号在内核找到驱动,回调fops的函数,返回led_open时就可以操作led硬件。

字符设备驱动相关API

#include <linux/fs.h>


int register_chrdev(unsigned int major, const char *name,
      const struct file_operations *fops)
功能:创建(注册)字符设备驱动
参数:
    @major:主设备号   //次设备号[0-255]
        major = 0:系统会自动分配主设备号
        major > 0:静态指定设备号(但有可能失败)
 @name:驱动的名字  cat /proc/devices
        linux@ubuntu:/dev/input$ cat /proc/devices 
        Character devices:
          1   mem
          4   /dev/vc/0
          4   tty
          |          |
         主设备号 设备的名字
 @fops:操作方法结构体
  struct file_operations {
            int (*open) (struct inode *, struct file *);
            ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
   ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
            int (*release) (struct inode *, struct file *);
        }
返回值:
    major=0:成功返回主设备号,失败返回错误码
 	major>0:成功返回0,失败返回错误码

void unregister_chrdev(unsigned int major, const char *name)
功能:注销字符设备驱动
参数:
 @major:主设备号
 @name:设备名字
返回值:无

register_chrdev 创建的是黑色的方块而不是fops
内部的实现先不管

字符设备驱动的实例

#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>

#define CNAME "mycdev"
int major;

int mycdev_open(struct inode* inode, struct file* file)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    return 0;
}
ssize_t mycdev_read(struct file* file,
    char __user* ubuf, size_t size, loff_t* offs)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    return 0;
}
ssize_t mycdev_write(struct file* file,
    const char __user* ubuf, size_t size, loff_t* offs)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    return 0;
}
int mycdev_close(struct inode* inode, struct file* file)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    return 0;
}
const struct file_operations fops = {
    .open = mycdev_open,  //函数指针指向驱动函数
    .read = mycdev_read,
    .write = mycdev_write,
    .release = mycdev_close,
};
//file_operations这个结构体内将mycdev_open这个函数赋给了open这个函数指针
//int mycdev_open(struct inode* inode, struct file* file)  
//函数指针的类型和函数的类型要相同
//相当于把open函数赋给了.open这个函数指针

static int __init mycdev_init(void)
{
    // 1.注册字符设备驱动
    major = register_chrdev(0, CNAME, &fops);
    if (major < 0) {
        printk("register_chrdev error\n");
        return -EAGAIN;
    }

    printk("register chrdev success,major = %d\n", major);
    return 0;
}
static void __exit mycdev_exit(void)
{
    // 2.注销字符设备驱动
    unregister_chrdev(major, CNAME);
}
module_init(mycdev_init);
module_exit(mycdev_exit);
MODULE_LICENSE("GPL");

字符设备驱动的测试流程

1.编译驱动

make arch=x86 modname=mycdev

2.安装驱动

sudo insmod mycdev.ko

3.查看驱动

dmesg --查看主设备号 240

[17773.487979] register char device driver success,major = 240

cat /proc/devices --查看主设备号

(查看驱动的这个步骤,还没有执行open read 等操作,因为还没有写应用程序,没有调用open read write )
(写应用程序的话,open是要打开一个设备文件的,现在在/dev下还没有设备文件,原因是驱动没有设置自动创建设备文件 )
(所以需要创建设备文件 设备结点)

4.创建设备文件

暂时用这一条命令 手动创建设备文件
后面会把此步骤自动化

sudo mknod /dev/mycdev c 240 0

mknod :创建设备文件的命令

/dev/mycdev:设备文件的路径及名字(路径和名字是任意的,但是习惯上放在dev下)

c : c字符设备 b块设备 (n 不是网络设备 网络设备没有设备结点)

235 :主设备号 -----这个主设备号是上面dmesg查看来的 需要自己查看

0 :次设备号[0-255]

5.编写应用程序
让应用程序访问设备文件,通过设备文件访问驱动的open read write 接口

#include <head.h>

int main(int argc, const char* argv[])
{
    int fd;
    char buf[128] = { 0 };
    if ((fd = open("/dev/mycdev", O_RDWR)) == -1)
        PRINT_ERR("open error");

    write(fd, buf, sizeof(buf));
    read(fd, buf, sizeof(buf));

    close(fd);
    return 0;
}

6.执行应用程序看现象

linux@ubuntu:~/work/day2/03mycdev$ ./a.out
test.c main 8
open error: Permission denied    //没有权限

添加权限

sudo ./a.out

或者

sudo chmod 0777 /dev/mycdev

在这里插入图片描述
结果说明:结果中可以看到我们的read 和write被调用到了,说明我们的应用程序 调用read和write成功。

用户空间和内核空间数据传递

上边的案例说明应用程序可以调到数据了,但是还不能实现相互的数据传输。

思考:
在这里插入图片描述

能不能用内核中的一个指针指向用户空间buffer[128]的字节,然后用指针取数据?
不能。如果在正在取数据(访问应用程序)的过程 ctrl+c 进程终止,内核会直接崩溃。

在这里插入图片描述
思考:能不能让用户空间的指针指向内核空间的一块区域取数据??
不能。因为用户空间访问内核空间只有唯一的一种方式----系统调用。

那么用户空间和内核空间想互用数据的话 方法只有一种:
如果内核空间想用用户空间的数据的话:
首先用户空间通过系统调用把地址告诉给内核,然后如果内核把这块空间搬移到内核空间来。
在这里插入图片描述
如果用户空间想用内核空间的数据的话:
首先通过系统调用在用户空间分配一块内存,把内核空间的数据拷贝到用户空间内。
在这里插入图片描述

用户空间和内核空间数据传递思想

用户空间和内核空间内存是隔离开的。
内核不能够通过直接指向用户空间内存地址,如果通过这个方式访问用户空间的数据,此时如果进程意外终止内核也会崩溃。
用户空间也不能够通过指针直接修改内核空间的数据,不安全,用户空间进入内核空间的方法只有一种系统调用。
所以不能通过指针的形式,让两者进程数据交互。
所以,为了克服上述缺点,如果用户和内核需要进行数据传输使用拷贝完成,即copy_to_user/copy_from_user函数完成。

数据传输的API

#include <linux/uaccess.h>

unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)
功能:将数据从内核空间拷贝到用户空间
参数:
    @to:用户空间的地址
	@from:内核空间的地址
	@n:大小(单位字节)
返回值:成功返回0,失败返回未拷贝的字节
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
功能:将数据从用户空间拷贝到内核空间
参数:
    @to:内核空间的地址
	@from:用户空间的地址
	@n:大小(单位字节)
返回值:成功返回0,失败返回未拷贝的字节

底层实现借助 memcpy 数据拷贝实现的
__开头的宏是给编译器使用的

用户空间和内核空间数据传输实例

在这里插入图片描述
mycdev.c

#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/uaccess.h>
#define CNAME "mycdev"
int major;
char kbuf[128] = { 0 };
int mycdev_open(struct inode* inode, struct file* file)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    return 0;
}
ssize_t mycdev_read(struct file* file,
    char __user* ubuf, size_t size, loff_t* offs)
{
    int ret;
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    if (size > sizeof(kbuf))
        size = sizeof(kbuf);
    ret = copy_to_user(ubuf, kbuf, size); 
    if(ret){
        printk("copy to user error\n");
        return -EIO;
    }
    return size;   //此处的返回要和应用层的返回值一致
}

ssize_t mycdev_write(struct file* file,
    const char __user* ubuf, size_t size, loff_t* offs)
{
    int ret;
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    if (size > sizeof(kbuf))
        size = sizeof(kbuf);
    ret = copy_from_user(kbuf, ubuf, size); 
    if(ret){
        printk("copy from user error\n");
        return -EIO;
    }
    return size;  //此处的返回要和应用层的返回值一致
}
int mycdev_close(struct inode* inode, struct file* file)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);

    return 0;
}
const struct file_operations fops = {
    .open = mycdev_open,
    .read = mycdev_read,
    .write = mycdev_write,
    .release = mycdev_close,
};
static int __init mycdev_init(void)
{
    // 1.注册字符设备驱动
    major = register_chrdev(0, CNAME, &fops);
    if (major < 0) {
        printk("register_chrdev error\n");
        return -EAGAIN;
    }

    printk("register chrdev success,major = %d\n", major);
    return 0;
}
static void __exit mycdev_exit(void)
{
    // 2.注销字符设备驱动
    unregister_chrdev(major, CNAME);
}
module_init(mycdev_init);
module_exit(mycdev_exit);
MODULE_LICENSE("GPL");

test.c

#include <head.h>

int main(int argc, const char* argv[])
{
    int fd;
    char buf[128] = "i am test data uaccess...";
    if ((fd = open("/dev/mycdev", O_RDWR)) == -1)
        PRINT_ERR("open error");

    write(fd, buf, sizeof(buf));

    memset(buf,0,sizeof(buf));
    read(fd, buf, sizeof(buf));
    printf("buf = %s\n",buf);

    close(fd);
    return 0;
}

注意:底层不能随用随定义变量

//在应用层这样写没问题 在底层不能这样写
int ret = copy_from_user(kbuf, ubuf, size); 

用户空间和内核空间数据传输练习

1.传输unsigned int类型的整数

2.传输img_t结构体变量

typedef struct{
 int width;
    int high;
}img_t;

驱动和上述2.3标题中的一样,应用程序如下:

#include <head.h>
typedef struct {
    int width;
    int high;
}img_t;
int main(int argc,const char * argv[])
{
    int fd;
    unsigned int data=1234;
    if((fd = open("/dev/mycdev",O_RDWR))==-1)
        PRINT_ERR("open error");
    
    write(fd,&data,sizeof(data));
    data=0;
    read(fd,&data,sizeof(data));
    printf("data = %d\n",data);

    img_t img = {1920,1080};
    write(fd,&img,sizeof(img));
    memset(&img,0,sizeof(img));
    read(fd,&img,sizeof(img));
    printf("width=%d,high=%d\n",img.width,img.high);

    close(fd);
    return 0;
}

在内核空间操作LED寄存器

内核空间能否直接操作寄存器?

对于LED灯的操作是通过操作寄存器完成的,寄存器的地址是物理地址。(在数据手册上看到的地址全是物理地址)
但是在Linux内核启动之后,在程序中使用的地址是虚拟地址,不能够在内核空间直接操作物理地址。(我们能够通过%p打印出来的地址全是虚拟地址)

如果想要在内核空间操作LED灯,就需要将LED灯的物理地址映射到内核空间对应的虚拟地址,在内核空间操作这个虚拟地址就相当于在到LED的寄存器的物理地址。

uboot启动阶段有没有打开mmu(内存映射单元)?没有。MMU是在内核启动的时候打开的。
MMU会将物理地址映射到0-4G的虚拟地址。
MMU映射过程:在物理地址上加一个3G的偏移,就得到了虚拟地址。

现在咱们的内核空间对应的物理地址都有虚拟地址。我们要把这块虚拟内存对应的物理地址改到LED灯对应的寄存器的物理地址上。

在这里插入图片描述

地址映射的API

#include <linux/io.h>

void  *ioremap(phys_addr_t offset, unsigned long size)
功能:完成地址映射
参数:
    @offset:物理地址
	@size:大小
返回值:成功返回虚拟地址,失败返回NULL
        
void iounmap(void *addr);      
功能:取消地址映射
参数:
    @addr:虚拟地址
返回值:无

注意:映射多少字节就取消映射多少字节

LED灯对应的物理地址

RCC_MP_AHB4ENSETR 0x50000a28 [4] 1 GPIOE时钟使能

GPIOx_MODER 0x50006000 [21:20] 01 输出

GPIOx_ODR 0x50006014 [10] 1 LED1亮 0 LED1灭

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值