Linux driver(一)

驱动

Device Driver,全称设备驱动程序,是添加到操作系统中的特殊程序,其中包含有关硬件设备的信息,此信息能够使计算机与相应的设备进行通信。驱动相当于硬件的接口,操作系统只有通过这个接口,才能控制硬件设备的工作。驱动程序是硬件厂商根据操作系统编写的配置文件,可以说没有驱动程序,计算机中的硬件就无法工作。操作系统不同,硬件的驱动程序也不同,各个硬件厂商为了保证硬件的兼容性及增强硬件的功能会不断地升级驱动程序。

分类:

  • 字符设备驱动:字符设备提供连续的数据流,支持按字节/字符来传输数据,只能顺序读取,不支持随机读取,例如键盘、串口等
  • 块设备驱动:支持随机读取,可以自行确定读取数据的位置,但数据只能以固定大小(块或者块的整数倍)传输,例如各种存储设备。
  • 网络驱动:网络设备在Linux内核中是唯一不体现一切皆文件的驱动架构,不同于字符设备和块设备,网络设备驱动没有设备号和/dev设备文件(节点),只有接口名,并且使用套接字来实现数据收发

 Linux 应用程序对驱动程序的调用过程:

设备树

Device Tree,描述设备树的文件叫做 DTS(Device Tree Source),这DTS 文件采用树形结构描述板级设备,即开发板上的设备信息,比如CPU 数量、 内存基地址、IIC 接口上接了哪些设备、SPI 接口上接了哪些设备等。Linux 内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/device-tree 目录下根据节点名字创建不同文件夹

DTS 为设备树源码文件(dts源文件不能直接被内核驱动所使用,必须被编译成二进制文件才能被内核驱动使用)。
DTB 为将DTS 编译以后得到的二进制文件。
DTC为将DTS编译成DTB的工具。

dts语法:

1、头文件

设备树的头文件扩展名为 .dtsi,dtsi文件一般用来描述一些通用的硬件信息资源,然后会被dts文件引入,这样dts文件中就会包含dtsi文件中的内容。dts中也可以包含.h文件。

#include <xxx.h>
#include "xxx.dtsi"
/include/ “xxx.dtsi”

2、设备节点

下图为设备节点的示例:

  

设备树中的基本单元被称为节点(node),格式为:

[label:] node-name[@unit-address] {
    [properties definitions]
    [child nodes]
};
例:
uart0: uart@fe001000 {
    compatible="ns16550";
    reg=<0xfe001000 0x100>;
};
修改节点:
&uart0 {
    status = “disabled”;
};
或
&{/uart@fe001000}  {
    status = “disabled”;
};

label:给node起的便于引用的名字,可省略

node-name:节点名字 

 unit-address:表示设备的地址或寄存器首地址,若某节点没有地址或者寄存器的话可省略

特殊节点:

  • /chosen:根节点的孩子节点,它不是一个真实的设备, 主要是为了 uboot 向 Linux 内核传递数据,主要属性为bootargs、stdout-path、stdin-path
  • /aliases:根节点的孩子节点,用于定义一个或多个别名属性,每条别名属性会为一个设备节点的路径名设置一个别名,别名即为别名属性的属性名,属性值则是设备节点的路径名。
    aliases { 
        serial0 = "/simple-bus@fe000000/serial@llc500"; 
        ethernet0 = "/simple-bus@fe000000/ethernet@31c000"; 
        can0 = &flexcan1;
    };
  • /memory:所有设备树都需要一个memory设备节点,它描述了系统的物理内存布局。
  • /cpus

2、节点属性 

属性取值可以为arrays of cells(cell 就是一个32位数据,一个或多个cell用尖括号括起来,并以空格隔开, 64位数据使用2个cell表示)、字符串、bytestring(1个或多个字节,以空格隔开并用中括号包围起来)或前三种的混合(用“,”隔开)。

  • compatible:字符串列表,表示兼容性,用于将设备和驱动绑定起来。例如,对于某个LED,内核中有A、B、C三个驱动都支持它,则可以这样写:
    led {
        compatible = “A”, “B”, “C”;
    };
    
  • model:字符串,描述设备模块信息,用来准确地定义这个硬件是什么
  • phandle:可以为节点指定一个全局唯一的数字标识符。这个标识符可以被需要引用该节点的另一个节点使用。例如:
    pic@10000000 {
        phandle = <1>;
        interrupt-controller;
    };
    another-device-node {
        interrupt-parent = <1>;   // 使用phandle值为1来引用上述节点
    };
  • status:字符串,和设备状态有关
  • reg:(address, length) 对,用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息。例如:一个设备有两个寄存器块,一个的地址是0x3000,占据32字节;另一个的地址是0xFE00,占据256字节,表示如下:
    reg = <0x3000 0x20 0xFE00 0x100>;
  • ranges:空或 (child-bus-address,parent-bus-address,length) ,child-bus-address:子总线地址空间的物理地址,由父节点的 #address-cells 确定此物理地址所占用的字长。parent-bus-address:父总线地址空间的物理地址,同样由父节点的 #address-cells 确定此物理地址所占用的字长。length:子地址空间的长度,由父节点的 #size-cells 确定此地址长度所占用的字长
  • name:字符串,用于记录节点名字,目前几乎已弃用
  • device_type:字符串,只用于 cpu 节点或者 memory 节点,用于描述设备的 FCode
  • interrupt-controller:没有值,用在中断控制器的设备节点中,表明这个节点是一个中断控制器
  • interrupt-parent:用于可产生中断且中断信号连接到某中断控制器的设备的设备节点,用于表示该设备的中断信号连接到了哪个中断控制器,该属性的值通常是中断控制器设备节点的数字标识(phandle)
  • #address-cells、#size-cells:无符号 32 位整型,用于描述节点的地址信息。#address-cells 决定了节点 reg 属性中地址信息所占用的字长(32 位), #size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)。例如:
    soc {
        #address-cells = <2>;
        #size-cells = <1>;
        serial {
            compatible = "xxx";
            reg = <0x4600 0x5000 0x100>;  /*地址信息是:0x00004600 00005000,长度信息是:0x100*/
            };
    };

 3、常用OF操作函数

  • of_find_node_by_name:通过节点名字查找指定节点。函数使用方式如下:

    struct device_node *of_find_node_by_name(struct device_node *from,
    										const char *name);
    
  • of_find_node_by_type:通过 device_type 属性查找指定的节点

  • of_find_compatible_node

  • of_find_matching_node_and_match:通过 of_device_id 匹配表来查找指定的节点

  • of_find_node_by_path

驱动开发步骤(以字符设备为例)

1、修改设备树文件

打开.dts文件,在根节点下添加子节点内容,修改完之后make一下.dts文件得到.dtb文件,使用该.dtb文件启动Linux内核,Linux启动成功后可进入到/proc/device-tree/目录中查看是否有新增的子节点。

make dtbs

2、编写驱动程序

驱动程序里面一般要包含如下内容:

  • 定义file_operations结构体
  • xxx_init()和xxx_exit()函数
  • 用户操作函数
  • GPL协议、入口/出口加载

1)分配设备号:

  • 主设备号:用来标识与设备文件相连的驱动程序,反映设备类型( 如USB设备,硬盘设备等)
  • 次设备号:用来确定具体的设备,即驱动程序操作的是哪个设备(区分同类型的设备)

例如在/dev下使用ls -l命令 得到:

crw-rw-rw- 1 root root 1, 3 Feb23 1999 null
crw-rw-rw- 1 root root 1, 5 Feb23 1999 zero
//其中1,1表示主设备号,3,5表示次设备号,并且/dev/null和/dev/zero都由驱动程序1管理

静态申请:找一个没用的设备号,用register_chrdev_region函数注册设备号

动态分配:用alloc_chrdev_region函数,让内核分配。

释放/注销设备号:unregister_chrdev_region函数

int register_chrdev_region(dev_t from, unsigned count, const char *name)
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
			const char *name)
//*name指设备名字,会出现在/proc/devices和sysfs中

2)cdev结构体

cdev结构体用于描述一个字符设备。struct inode结构体的联合体中有struct cdev结构体指针,将来找到对应的struct cdev结构体会对该指针赋值;在用open打开设备节点时从struct inode结构体中获取初次设备号,然后用这个主次设备号去chrdevs全局变量中找到对应的struct cdev结构体,通过cdev的ops指针就能找到设备的操作函数。

struct cdev {
    struct kobject kobj;
    struct module *owner;//填充时,值要为 THIS_MODULE,表示模块
    const struct file_operations *ops;//这个file_operations结构体,注册驱动的关键,要填充成这个结构体变量
    struct list_head list;
    dev_t dev;//设备号,主设备号+次设备号
    unsigned int count;//次设备号个数
};
  • 分配cdev:cdev_alloc函数分配一个struct cdev结构,动态申请一个cdev内存
  • 初始化cdev:cdev_init(struct cdev *dev,struct file_operations *fops)函数主要对struct cdev结构体做初始化, 最重要的就是建立cdev 和 file_operations之间的连接
  • 添加cdev:cdev_add(struct cdev *dev,dev_t devnum,unsigned int count)函数向内核注册一个struct cdev结构,即正式通知内核由struct cdev *p代表的字符设备已经可以使用
  • 注销cdev:cdev_del(cdev *dev)函数向内核注销一个struct cdev结构,即正式通知内核由struct cdev *p代表的字符设备已经不可以使用了

 注:register_chrdev函数是比较老的内核注册形式,它不仅注册了设备号,还完成了cdev 初始化以及cdev 注册

static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops) //注册
static inline void unregister_chrdev(unsigned int major, const char *name) //注销

2)创建设备文件

  • 手动创建:用mknod(超级用户)在/dev下创建设备文件,例如mknod /dev/scull0 c 254 0中scull0是,c表示字符设备,254为主设备号,0为次设备号。rm用于删除设备。(cat /proc/devices可以查看申请到的设备名、设备号)
  • 自动创建:
    创建设备:
    class_create(struct module *owner, const char *name);
    device_create(struct class *class, struct device *parent,dev_t devt, const char *fmt, ...);
    删除设备:
    device_destroy();
    class_destroy();

与设备文件有关的三大结构体:

  • struct file:该结构体定义在include/linux/fs.h中定义,其代表一个打开的文件,系统中的每个打开的文件在内核空间都有一个关联的 struct file。它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。
  • struct inode:用来表示一个静态文件的,每个文件都会对应唯一的struct inode结构体,结构体里描述了文件的详细信息。
  • struct file_operations:把系统调用和驱动程序关联起来的关键数据结构。这个结构的每一个成员都对应着一个系统调用。读取file_operation中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的工作。

区别:struct inode结构体是描述静态的文件,struct file结构体描述动态的文件(也就是打开的文件);每个文件只有唯一的struct inode结构体,但是可以有多个struct file结构体,文件每被打开一次就多一个 struct file结构体。

3) module_init和module_exit函数

module_init(xxx_init); //函数module_init声明xxx_init为驱动入口函数,当加载驱动的时候xxx_init函数就会被调用
module_exit(xxx_exit); 

module_init函数用来向 Linux内核注册一个模块加载函数,参数 xxx_init就是需要注册的具体函数,当使用“ insmod”命令加载驱动的时候 xxx_init这个函数就会被调 用。 module_exit()函数用来向 Linux内核注册一个模块卸载函数,参数 xxx_exit就是需要注册的具体函数,当使用“ rmmod”命令卸载具体驱动的时候 xxx_exit函数就会被调用。所以一般在xxx_init函数里进行对硬件初始化、中断函数、向内核注册驱动程序等工作,在xxx_exit里面就需要对驱动程序的卸载做一些回收工作。

3、编译(驱动程序可以编译到kernel中,这样当Linux内核启动时就会自动运行驱动程序,也可以单独编译成.ko文件并加载。)

编写驱动程序的Makefile文件,然后make一下得到.ko文件

4、编写应用程序并编译

5、驱动模块加载/卸载、运行

将.ko文件和编译好的应用程序拷贝到指定文件夹下,加载驱动模块,然后运行应用程序

加载模块前需执行:
depmod
动态加载模块:
modprobe led 或 insmod led.ko 
查看模块:
lsmod
卸载模块:
modprobe -r led.ko 或 rmmod led.ko

---------------------------------------------------------------------------------------------------------------------------------设备树/驱动移植以后考虑…… 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值