linux驱动基础

  1. linux驱动基础
    1. struct file_operations
      1. open()
      2. release()
      3. read()
      4. write()
      5. ioctl()
    2. ioremap()
    3. ioctl()

linux驱动基础

struct file_operations

struct file_operations { 

    struct module *owner;//拥有该结构的模块的指针,一般为THIS_MODULES 

    loff_t (*llseek) (struct file *, loff_t, int);//用来修改文件当前的读写位置 
     
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);//从设备中同步读取数据 
     
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);//向设备发送数据
     
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一个异步的读取操作 
     
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一个异步的写入操作 
     
    int (*readdir) (struct file *, void *, filldir_t);//仅用于读取目录,对于设备文件,该字段为NULL 
     
    unsigned int (*poll) (struct file *, struct poll_table_struct *); //轮询函数,判断目前是否可以进行非阻塞的读写或写入 
     
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); //执行设备I/O控制命令 
     
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //不使用BLK文件系统,将使用此种函数指针代替ioctl 
     
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long); //在64位系统上,32位的ioctl调用将使用此函数指针代替 


    int (*mmap) (struct file *, struct vm_area_struct *); //用于请求将设备内存映射到进程地址空间
     
    int (*open) (struct inode *, struct file *); //打开 
     
    int (*flush) (struct file *, fl_owner_t id); 
     
    int (*release) (struct inode *, struct file *); //关闭 
     
    int (*fsync) (struct file *, struct dentry *, int datasync); //刷新待处理的数据 
     
    int (*aio_fsync) (struct kiocb *, int datasync); //异步刷新待处理的数据 
     
    int (*fasync) (int, struct file *, int); //通知设备FASYNC标志发生变化 
     
    int (*lock) (struct file *, int, struct file_lock *); 
     
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); 
     
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); 
     
    int (*check_flags)(int); 
     
    int (*flock) (struct file *, int, struct file_lock *);
     
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
     
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); 
     
    int (*setlease)(struct file *, long, struct file_lock **); 

};

每个成员详细解析看:Linux 字符设备驱动结构(四)—— file_operations 结构体知识解析_linux operations结构体-CSDN博客

下列讲解重要成员:

open()

原型:int (*open) (struct inode *, struct file *) –打开设备

在操作设备前必须先调用open函数打开文件,可以干一些需要的初始化操作。当然,如果不实现这个函数的话,驱动会默认设备的打开永远成功。打开成功时open返回0。

release()

原型:int (*release) (struct inode *, struct file *) —关闭设备

当设备文件被关闭时内核会调用这个操作,当然这也可以不实现,函数默认为NULL。关闭设备永远成功。

这两个函数已经讲过,这里不再赘述,主要看下面几个函数

read()

原型:ssize_t (*read) (struct file * filp, char __user * buffer, size_t size , loff_t * p); —内核读

filp :为进行读取信息的目标文件,

buffer :为对应放置信息的缓冲区(即用户空间内存地址);

size :为要读取的信息长度;

p :为读的位置相对于文件开头的偏移,在读取信息后,这个指针一般都会移动,移动的值为要读取信息的长度值

write()

原型:ssize_t (*write) (struct file * filp, const char __user * buffer, size_t count, loff_t * ppos); —内核写

filp :为目标文件结构体指针;

buffer :为要写入文件的信息缓冲区;

count :为要写入信息的长度;

ppos :为当前的偏移位置,这个值通常是用来判断写文件是否越界

两个函数的作用分别是 从 设备中获取数据发送数据给设备 ,应用程序中与之对应的也有 write() 函数及 read() 函数。

len = read(fd,buf,len ) 与 len = write(fd,buf,size)

需要注意的是用户层的第一个参数传入文件描述fd,而内核层第一个参数传入的是目标文件结构体指针

我们知道,应用程序工作在用户空间,而驱动工作在内核空间,二者不能直接通信的,那我们用何种方法进行通信呢?下面介绍一下内核中的 memcpycopy_from_usercopy_to_user,虽然说内核中不能使用C库提供的函数,但是内核也有一个memcpy的函数,用法跟C库中的一样。

copy_from_user 从用户态复制内容,入内核 write

copy_to_user 从内核态复制内容,用户态进行 read

其实在这里,我们可以思考,既然拷贝的功能上面的_memcpy() 函数就可以实现,为什么还要封装成 copy_to_user()和copy_from_user()呢?答案是_memcpy() 函数是有缺陷的,譬如 我们在用户层调用函数时传入的不是字符串,而是一个不能访问或修改的地址,那样就会造成系统崩溃 。

出于上面的原因, 内核和用户态之间交互的数据时必须要先对数据进行检测 ,如果数据是安全的,才可以进行数据交互。上面的函数就是memcpy的改进版,在memcpy功能的基础上加上的检查传入参数的功能,防止有些人有意或者无意的传入无效的参数。

ioctl()

原型:int ioctl(int handle, int cmd,[int *argdx, int argcx]);

​ ioctl是设备驱动程序中对设备的I/O通道进行管理的函数 。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。

功能:控制I/O设备

include/asm/ioctl.h中定义的宏的注释:

#define   _IOC_NRBITS        8                               //序数(number)字段的字位宽度,8bits
#define   _IOC_TYPEBITS      8                               //幻数(type)字段的字位宽度,8bits
#define   _IOC_SIZEBITS      14                              //大小(size)字段的字位宽度,14bits
#define   _IOC_DIRBITS       2                               //方向(direction)字段的字位宽度,2bits
#define   _IOC_NRMASK       ((1 << _IOC_NRBITS)-1)    //序数字段的掩码,0x000000FF
#define   _IOC_TYPEMASK     ((1 << _IOC_TYPEBITS)-1)  //幻数字段的掩码,0x000000FF
#define   _IOC_SIZEMASK     ((1 << _IOC_SIZEBITS)-1)   //大小字段的掩码,0x00003FFF
#define   _IOC_DIRMASK      ((1 << _IOC_DIRBITS)-1)    //方向字段的掩码,0x00000003
#define   _IOC_NRSHIFT     0                                //序数字段在整个字段中的位移,0
#define   _IOC_TYPESHIFT   (_IOC_NRSHIFT+_IOC_NRBITS)       //幻数字段的位移,8
#define   _IOC_SIZESHIFT   (_IOC_TYPESHIFT+_IOC_TYPEBITS)   //大小字段的位移,16
#define   _IOC_DIRSHIFT    (_IOC_SIZESHIFT+_IOC_SIZEBITS)   //方向字段的位移,30
#define _IOC_NONE    0U     //没有数据传输
#define _IOC_WRITE   1U     //向设备写入数据,驱动程序必须从用户空间读入数据
#define _IOC_READ    2U     //从设备中读取数据,驱动程序必须向用户空间写入数据
#define _IOC(dir,type,nr,size) \
       (((dir)  << _IOC_DIRSHIFT) | \
        ((type) << _IOC_TYPESHIFT) | \
        ((nr)   << _IOC_NRSHIFT) | \
        ((size) << _IOC_SIZESHIFT))
 
//构造无参数的命令编号
#define _IO(type,nr)             _IOC(_IOC_NONE,(type),(nr),0)
//构造从驱动程序中读取数据的命令编号
#define _IOR(type,nr,size)     _IOC(_IOC_READ,(type),(nr),sizeof(size))
//用于向驱动程序写入数据命令
#define _IOW(type,nr,size)    _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
//用于双向传输
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))
 
//从命令参数中解析出数据方向,即写进还是读出
#define _IOC_DIR(nr)          (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
//从命令参数中解析出幻数type
#define _IOC_TYPE(nr)              (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
//从命令参数中解析出序数number
#define _IOC_NR(nr)           (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
//从命令参数中解析出用户数据大小
#define _IOC_SIZE(nr)         (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)


#define IOC_IN            (_IOC_WRITE << _IOC_DIRSHIFT)
#define IOC_OUT           (_IOC_READ << _IOC_DIRSHIFT)
#define IOC_INOUT         ((_IOC_WRITE|_IOC_READ) << _IOC_DIRSHIFT)
#define IOCSIZE_MASK      (_IOC_SIZEMASK << _IOC_SIZESHIFT)
#define IOCSIZE_SHIFT     (_IOC_SIZESHIFT)

二、ioctl的必要性

如果不用ioctl的话,也可以实现对设备I/O通道的控制。例如,我们可以在驱动程序中实现write的时候检查一下是否有特殊约定的数据流通过,如果有的话,那么后面就跟着控制命令(一般在socket编程中常常这样做)。但是如果这样做的话,会导致代码分工不明,程序结构混乱,程序员自己也会头昏眼花的。所以,我们就 使用ioctl来实现控制的功能 。要记住,用户程序所作的 只是通过命令码(cmd)告诉驱动程序它想做什么,至于怎么解释这些命令和怎么实现这些命令,这都是驱动程序要做的事情 。

三、 ioctl如何实现

内核中,一般在file_operation中的unlocked_ioctl()成员函数进行实现

在驱动程序中实现的ioctl函数体内,实际上是有一个 switch{case}结构 ,每一个case对应一个命令码,做出一些相应的操作。怎么实现这些操作,这是每一个程序员自己的事情。因为设备都是特定的,这里也没法说。关键在于怎样组织命令码,因为在ioctl中 命令码 是唯一联系用户程序命令和驱动程序支持的途径。

命令码的组织是有一些讲究的,因为我们一定要做到命令和设备是一一对应的,这样才不会将正确的命令发给错误的设备,或者是把错误的命令发给正确的设备,或者是把错误的命令发给错误的设备。这些错误都会导致不可预料的事情发生,而当程序员发现了这些奇怪的事情的时候,再来调试程序查找错误,那将是非常困难的事情。所以在Linux核心中是这样定义一个命令码的:

| 设备类型 | 序列号 | 方向 |数据尺寸|

|————-|———-|——-|————|

| 8 bit | 8 bit | 2 bit | 8~14 bit |

|————-|———-|——-|————-|

这样一来,一个命令就变成了一个 整数形式的命令码 ;但是命令码非常的不直观,所以Linux Kernel中提供了一些宏。这些宏可根据便于理解的字符串生成命令码,或者是从命令码得到一些用户可以理解的字符串以标明这个命令对应的设备类型、设备序列号、数据传送方向和数据传输尺寸。

我们在前面PWM驱动程序中也定义了命令宏:

#define   MAGIC_NUMBER    'k'
#define   BEEP_ON    _IO(MAGIC_NUMBER    ,0)
#define   BEEP_OFF   _IO(MAGIC_NUMBER    ,1)
#define   BEEP_FREQ  _IO(MAGIC_NUMBER    ,2)

这里必须要提一下的,就是 “幻数”MAGIC_NUMBER , “幻数”是一个字母,数据长度也是8,用一个特定的字母来标明设备类型,这和用一个数字是一样的,只是更加利于记忆和理解。

实例时刻,当然只是部分代码:

#define  MAGIC_NUMBER    'k'
#define  BEEP_ON    _IO(MAGIC_NUMBER    ,0)
#define  BEEP_OFF   _IO(MAGIC_NUMBER    ,1)
#define  BEEP_FREQ  _IO(MAGIC_NUMBER    ,2)
#define  BEPP_IN_FREQ 100000

static void beep_freq(unsigned long arg)
{
    writel(BEPP_IN_FREQ/arg, timer_base +TCNTB0  );
    writel(BEPP_IN_FREQ/(2*arg), timer_base +TCMPB0 );
}

static long beep_ioctl(struct file *filep, unsigned int cmd, unsigned long arg)
{
    switch(cmd)
    {
        case BEEP_ON:
            fs4412_beep_on();
            break;
        case BEEP_OFF:
            fs4412_beep_off();
            break;
        case BEEP_FREQ:
            beep_freq( arg );
            break;
        default :
            return -EINVAL;
    }
}

测试代码如下:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/ioctl.h>

#define  MAGIC_NUMBER    'k'
#define   BEEP_ON    _IO(MAGIC_NUMBER    ,0)
#define   BEEP_OFF   _IO(MAGIC_NUMBER    ,1)
#define   BEEP_FREQ   _IO(MAGIC_NUMBER    ,2)

main()
{
    int fd;

    fd = open("/dev/beep",O_RDWR);
    if(fd<0)
    {
        perror("open fail \n");
        return ;
    }
    
    ioctl(fd,BEEP_ON);
    
    sleep(6);
    ioctl(fd,BEEP_OFF);    
    
    close(fd);

}

ioremap()

ioremap 函数用于将物理设备内存映射到内核虚拟地址空间,以便内核模块能够访问硬件寄存器或设备内存。

拿最简单的LED驱动来说,我们的驱动程序是在虚拟的内存上面跑的,但是最终,LED的点亮还是必须靠GPIO管脚的高低电平来控制。我们的虚拟的内存通过ioremap这个映射函数和实际的硬件上面的寄存器对应起来的

ioremap宏定义在asm/io.h内:

#define ioremap(cookie,size)           __ioremap(cookie,size,0)

__ioremap函数原型为(arm/mm/ioremap.c):

void __iomem * __ioremap(unsigned long phys_addr, size_t size, unsigned long flags);

参数:

phys_addr:要映射的起始的IO地址

size:要映射的空间的大小

flags:要映射的IO空间和权限有关的标志

该函数返回映射后的内核虚拟地址(3G-4G). 接着便可以通过读写该返回的内核虚拟地址去访问之这段I/O内存资源。

iounmap

*void iounmap(void * addr);*

iounmap函数用于取消ioremap()所做的映射,原型如下

在将I/O内存资源的物理地址映射成核心虚地址后,理论上讲我们就可以象读写RAM那样直接读写I/O内存资源了。为了保证驱动程序的跨平台的可移植性,我们应该使用Linux中特定的函数来访问I/O内存资源,而不应该通过指向核心虚地址的指针来访问。

读写I/O的函数如下所示:

a – writel()

   writel()往内存映射的 I/O 空间上写数据,wirtel()  I/O 上写入 32 位数据 (4字节)。

原型:void writel (unsigned char data , unsigned short addr )

b – readl()

  readl() 从内存映射的 I/O 空间上读数据,readl 从 I/O 读取 32 位数据 ( 4 字节 )。

原型:unsigned char readl (unsigned int addr )

变量 addr 是 I/O 地址。

示例:

三、使用实例
还是拿我们写PWM驱动的实例来解析

1、这里我们先定义了一些寄存器,这里使用的地址均是物理地址:

#define GPD0CON       0x114000a0  
#define TIMER_BASE    0x139D0000             
#define TCFG0         0x0000                 
#define TCFG1         0x0004                              
#define TCON          0x0008               
#define TCNTB0        0x000C            
#define TCMPB0        0x0010 

2、为了使用内存映射,我们需先定义指针用来保存内存映射后的地址:

static unsigned int *gpd0con;  
static void *timer_base;  

注意:这里timer_base 指针指向的类型设为 void, 主要因为上面使用了地址偏移,使用void 更有利于我们使用;

3、使用ioremap() 函数进行内存映射,并将映射的地址赋给我们刚才定义的指针

gpd0con = ioremap(GPD0CON,4);  
timer_base = ioremap(TIMER_BASE,0x14); 
 

4、得到地址后,可以调用 writel() 、readl() 函数进行相应的操作

writel ((readl(gpd0con)&~(0xf<<0)) | (0x2<<0),gpd0con);  
writel ((readl(timer_base +TCFG0  )&~(0xff<<0)) | (0xff <<0),timer_base +TCFG0);   
writel ((readl(timer_base +TCFG1 )&~(0xf<<0)) | (0x2 <<0),timer_base +TCFG1 );   

writel (500, timer_base +TCNTB0  );  
writel (250, timer_base +TCMPB0 );  
writel ((readl(timer_base +TCON )&~(0xf<<0)) | (0x2 <<0),timer_base +TCON );   

可以看到,这里先从相应的地址中读取数据,修改完毕后,再利用writel函数进行数据写入。

ioctl()

在 Linux 系统中,ioctl(Input/Output Control)是一个强大的系统调用,它允许用户空间的应用程序与设备驱动程序进行通信,发送特定的命令和接收响应

在用户空间中,ioctl 函数的原型通常定义为:

int ioctl(int fd, unsigned long cmd, ...);

file 结构体代表与文件描述符相关的文件操作信息,cmd 是从用户空间传递的命令,arg 是与命令相关的参数。

在内核模块中,必须实现一个 ioctl 函数来处理用户空间发来的命令。 unlock

image-20260112122258469

上电复位: 每次用的第一次通电,都默认复位一次

image-20260113111245379

image-20260113114406495

IMX6ULL Linux 启动流程:


1. Boot ROM(芯片固化)
   ├── 初始化基本时钟
   ├── 从启动设备加载SPL
   └── 验证并跳转到SPL
        ↓
2. SPL(Secondary Program Loader)
   ├── 初始化DDR内存
   ├── 初始化更多外设
   ├── 加载U-Boot
   └── 跳转到U-Boot
        ↓
3. U-Boot(Bootloader)
   ├── 初始化完整硬件
   ├── 加载设备树(dtb)
   ├── 加载Linux内核(zImage)
   ├── 设置启动参数(ATAGS或设备树)
   └── 跳转到内核入口
        ↓
4. Linux 内核启动
   ├── 解压内核(如果压缩)
   ├── 汇编启动代码(head.S)
   ├── 初始化MMU(内存管理单元)
   ├── 初始化内核数据结构
   ├── 解析设备树
   ├── 初始化各个子系统
   │    ├── 内存管理
   │    ├── 进程调度
   │    ├── 中断系统
   │    ├── 时钟框架
   │    └── 设备模型
   ├── 挂载根文件系统
   └── 启动init进程(PID=1)
        ↓
5. 用户空间启动
   ├── init进程启动
   ├── 执行初始化脚本
   ├── 启动各种服务
   └── 显示登录界面

image-20260113130809396

image-20260113130835795

image-20260113130902571


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 351134995@qq.com

×

喜欢就点赞,疼爱就打赏