主要介绍linux驱动开发,有待完善
①字驱动模块的加载和卸载
(1)Linux 驱动有两种运行方式,第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),模块的加载和卸载注册函数如下:
module_init(xxx_init); //module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当使用“insmod”命令加载驱动的时候,xxx_init 这个函数就会被调用。它会将函数地址加入到相应的节区 section 中,这样开机的时候就可以自动加载模块了。初始化函数,成功返回0,并在/sys/module 下新建一个以模块名为名的目录;非0初始化失败
module_exit(xxx_exit); //module_exit()函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用。主要是用于释放初始化阶段分配的内存,分配的设备号等
(2)驱动编译完生成.ko后终端执行命令
insmod/modprobe drv.ko #模块加载命令
rmmod/modprobe -r drv.ko #模块卸载命令
②字符设备注册与注销
一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块的出口函数 xxx_exit 中进行
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) //注销设备
其中参数为:
major:主设备号,Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分,关于设备号后面会详细讲解。
name:设备名字,指向一串字符串。
fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。
③实现设备的具体操作函数
需要实现 file_operations 结构体中的 open、read、write 和 release。file_operation 就是把系统调用和驱动程序关联起来的关键数据结构。这个结构的每一个成员都对应着一个系统调用。读取 file_operation 中相应的函数指针,接着把控制权转交给函数指针指向的函数,从而完成了 Linux 设备驱动程序的工作。
(1)open 函数
·file 结构体:内核中用 file 结构体来表示每个打开的文件,每打开一个文件,内核会创建一个结构体,并将对该文件上的操作函数传递给该结构体的成员变量 f_op,当文件所有实例被关闭后,内核会释放这个结构体。
·inode 结构体:VFS inode 包含文件访问权限、属主、组、大小、生成时间、访问时间、最后修改时间等信息。
④内核模块信息声明函数
函数 | 功能 |
---|---|
MODULE_LICENSE() | 表示模块代码接受的软件许可协议,Linux 内核遵循 GPL V2 开源协议,内核模块与 linux 内核保持一致即可。 |
MODULE_AUTHOR() | 描述模块的作者信息 |
MODULE_DESCRIPTION() | 对模块的简单介绍 |
MODULE_ALIAS() | 给模块设置一个别名 |
在 linux 中,我们使用设备编号来表示设备,主设备号区分设备类别,次设备号标识具体的设备。设备号的组成,dev_t 是__u32 类型的,是一个 32 位的数据类型。其中高 12 位为主设备号,低 20 位为次设备号。主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。我们创建一个字符设备的时候,首先要的到一个设备号,分配设备号的途径有静态分配和动态分配。
拿到设备的唯一 ID,我们需要实现 file_operation 并保存到 cdev 中,实现 cdev 的初始化;然后我们需要将我们所做的工作告诉内核,使用 cdev_add() 注册 cdev。最后我们还需要创建设备节点,以便我们后面调用 file_operation 接口。
①分配和释放设备号
//如果没有指定设备号的话就使用如下函数来申请设备号
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
//果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号
int register_chrdev_region(dev_t from, unsigned count, const char *name)
//释放掉设备号
void unregister_chrdev_region(dev_t from, unsigned count)
②新的字符设备注册方法
(1)字符设备结构
cdev 结构体被内核用来记录设备号,而在使用设备时,我们通常会打开设备节点,通过设备节点的 inode 结构体、file 结构体最终找到 file_operations 结构体,并从 file_operations 结构体中得到操作设备的具体方法,设备号的申请和归还引用如下:
static struct cdev chrdev;
struct cdev *cdev_alloc(void); //动态分配
(2) 初始化 cdev,将该结构体与我们的字符设备结构体相关联
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
(3)设备注册和注销
int cdev_add(struct cdev *p, dev_t dev, unsigned count) //cdev_add 函数用于向内核的 cdev_map 散列表添加一个新的字符设备
(4)使用 cdev_del 函数从 Linux 内核中删除相应的字符设备
void cdev_del(struct cdev *p)
③自动创建设备节点
Linux 中设备节点是通过“mknod”命令来创建的,在驱动中实现自动创建设备节点的功能以后,使用 modprobe 加载驱动模块成功的话就会自动在/dev 目录下创建对应的设备文件。
(1)mdev 机制:使用 busybox 构建根文件系统的时候,busybox 会创建一个 udev 的简化版本—mdev,所以在嵌入式 Linux 中我们使用mdev 来实现设备节点文件的自动创建与删除,Linux 系统中的热插拔事件也由 mdev 管理,在/etc/init.d/rcS 文件中如下语句:
echo /sbin/mdev > /proc/sys/kernel/hotplug
(2)创建和删除类
struct class *class_create (struct module *owner, const char *name) //类创建函数
void class_destroy(struct class *cls); //卸载驱动程序的时候需要删除掉类
(3)创建设备
struct device *device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...) //创建一个设备并将其注册到文件系统
void device_destroy(struct class *class, dev_t devt) //删除使用 device_create 函数创建的设备
①内存管理单元 MMU,其主要功能如下:
·保护内存,设置存储器的访问权限,设置虚拟存储空间的缓冲特性
·完成虚拟空间到物理空间的映射。对于 32 位的处理器来说,虚拟地址范围是 2^32=4GB,我们的开发板上有 512MB 的 DDR3,这 512MB 的内存就是物理内存,经过 MMU 可以将其映射到整个 4GB 的虚拟空间
②地址转换函数
(1)ioremap 地址映射函数函数
void __iomem *ioremap(phys_addr_t paddr, unsigned long size)
void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype)
参数:
·paddr:被映射的 IO 起始地址(物理地址);
·size:需要映射的空间大小,以字节为单位;
·mtype:ioremap 的类型,可以选择 MT_DEVICE、MT_DEVICE_NONSHARED、
MT_DEVICE_CACHED 和 MT_DEVICE_WC,ioremap 函数选择 MT_DEVICE。
返回值:一个指向 __iomem 类型的指针,当映射成功后便返回一段虚拟地址空间的起始地址,我们可以通过访问这段虚拟地址来实现实际物理地址的读写操作
(2)iounmap取消地址映射函数
void iounmap(void *addr)
void iounmap(volatile void __iomem *addr)
③I/O 内存访问函数
注意我们在操作这段被映射后的地址空间时应该使用 linux 提供的 I/O 访问函数(如:iowrite8()、iowrite16()、iowrite32()、ioread8()、ioread16()、ioread32() 等)
(1)读操作函数
·u8 readb(const volatile void __iomem *addr)
·u16 readw(const volatile void __iomem *addr)
·u32 readl(const volatile void __iomem *addr)
(2)写操作函数
·void writeb(u8 value, volatile void __iomem *addr)
·void writew(u16 value, volatile void __iomem *addr)
·void writel(u32 value, volatile void __iomem *addr)
④文件私有数据:
一般很多的 linux 驱动都会将文件的私有数据 private_data 指向设备结构体,其保存了用户自定义设备结构体的地址。自定义结构体的地址被保存在 private_data 后,可以通过读、写等操作通过该私有数据去访问设备结构体中的成员,这样做体现了 linux 中面向对象的程序设计思想。
struct newchrled_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
①头文件调用
#include
:包含了内核加载 module_init()/卸载 module_exit() 函数和内核模块信息相关函数的声明
#include:包含一些内核模块相关节区的宏定义__init,__exit
#include:包含内核提供的各种函数,如 printk
#include:dev_t 的数据类型表示设备号
#include
#include:结构体完整
#include
#include
#include
#include
#include
#include
#include
②函数主体部分
6)测试app编写
①头文件调用
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
②主题函数编写
7)测试步骤
①编写驱动程序的Makefile
KERNELDIR := /home/sky/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH := $(shell pwd)
obj-m := newchrled.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
②编译测试app
make
arm-linux-gnueabihf-gcc ledApp.c -o ledApp
sudo cp chrdevbase.ko chrdevbaseApp /home/sky/linux/nfs/rootfs/lib/modules/4.1.15/ -f
③终端命令
depmod //第一次加载驱动的时候需要运行此命令
modprobe led.ko //加载驱动
ls /dev/newchrled -l //查看/dev/newchrdev 这个设备节点文件是否存在
mknod /dev/led c 200 0 //驱动加载成功以后创建“/dev/led”设备节点
./ledApp /dev/led 1 //打开 LED 灯
rmmod newchrled.ko //卸载驱动
①设备树的作用就是描述一个硬件平台的硬件资源。这个“设备树”可以被 bootloader(uboot) 传递到内核,内核可以从设备树中获取硬件信息,有两个特点:
• 第一,以“树状”结构描述硬件资源。例如本地总线为树的“主干”在设备树里面称为“根节点”,挂载到本地总线的IIC总线、SPI总线、UART总线为树的“枝干”在设备树里称为“根节点的子节点”,IIC总线下的IIC设备不止一个,这些“枝干”又可以再分。
• 第二,设备树可以像头文件(.h 文件)那样,一个设备树文件引用另外一个设备树文件,这样可以实现“代码”的重用。
②一般.dts 描述 板级信息(也就是开发板上有哪些 IIC 设备、SPI 设备等),.dtsi 描述 SOC 级信息(也就是 SOC 有 几个 CPU、主频是多少、各个外设控制器信息等)。 那么将.dts 编译为.dtb需要用到 DTC 工具!DTC 工具源码在 Linux 内核的 scripts/dtc 目录下,在linux根目录下使用命令:
make dtbs //单独编译设备树
③如何确定编译哪一个 DTS 文件呢?我们就以 I.MX6ULL 这款芯片对应的板子为例来看一下,打开 arch/arm/boot/dts/Makefile,里面有imx6ull-alientek-emmc.dtb \,当选中 I.MX6ULL 这个 SOC 以后(CONFIG_SOC_IMX6ULL=y),所有使用到I.MX6ULL 这个 SOC 的板子对应的.dts 文件都会被编译为.dtb
① 设备树也支持头文件,设备树的头文件扩展名为.dtsi
#include
#include "imx6ull.dtsi"
②设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键—值对。格式如:
label: node-name@unit-address{
属性 1 = …
属性 2 = …
属性 3 = …
子节点…
}
其参数:
·“node-name”是节点名字,为 ASCII 字符串
·“unit-address”一般表示设备的地址或寄存器首地址,如果某个节点没有地址或者寄存器的话也可以不要,比如“cpu@0”,它的值要和节点“reg”属性的第一个地址一致。
设备树源码中常用的几种数据形式如下所示:
·label:节点标签
(1)字符串
compatible = "arm,cortex-a7";
(2)32 位无符号整数
reg = <0>;
③标准属性
(1)compatible 属性也叫做“兼容性”属性,格式为"manufacturer,model",如:
compatible = “fsl,imx6ul-evk-wm8960”,“fsl,imx-audio-wm8960”;
·manufacturer 表示厂商
·model 一般是模块对应的驱动名字
(2)model 属性值也是一个字符串,一般 model 属性描述设备模块信息,如:
model = “wm8960-audio”;
(4)#address-cells 和#size-cells 属性
这两个属性的值都是无符号 32 位整形,#address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位),#size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)。一般 reg 属性都是和地址有关的内容,和地址相关的信息有两种:起始地址和地址长度,reg 属性的格式一为:
reg =
每个“address length”组合表示一个地址范围,其中 address 是起始地址,length 是地址长度,#address-cells 表明 address 这个数据所占用的字长,#size-cells 表明 length 这个数据所占用的字长
(5)reg 属性,reg 属性的值一般是(address,length)对
(6)ranges属性值可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字矩阵,ranges 是一个地址映射/转换表,ranges 属性每个项目由子地址、父地址和地址空间长度这三部分组成:
·child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells 确定此物理地址所占用的字长。
·parent-bus-address:父总线地址空间的物理地址,同样由父节点的#address-cells 确定此物理地址所占用的字长。
·length:子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长。
如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换,对于我们所使用的 I.MX6ULL 来说,子地址空间和父地址空间完全相同,因此会在 imx6ull.dtsi中找到大量的值为空的 ranges 属性
④根节点 compatible 属性
通过根节点的 compatible 属性可以知道我们所使用的设备,一般第一个值描述了所使用的硬件设备名字,比如这里使用的是“imx6ull-14x14-evk”这个设备,第二个值描述了设备所使用的 SOC,比如这里使用的是“imx6ull”这颗 SOC。Linux 内核会通过根节点的 compoatible 属性查看是否支持此设备,如果支持的话设备就会启动 Linux 内核。
(1)使用设备树之前设备匹配方法
(2)使用设备树以后的设备匹配方法
Linux 内核通过根节点 compatible 属性找到对应的设备的函数调用过程:
⑤特殊节点
(1)aliases 子节点,主要功能就是定义别名,不过我们一般会在节点命名的时候会加上 label,然后通过&label来访问节点,这样也很方便
(2) chosen 子节点,chosen 节点主要是为了 uboot 向 Linux 内核传递数据,重点是 bootargs 参数
提取属性值的 of 函数 (内核源码/include/linux/of.h)
内存映射相关 of 函数 (内核源码/drivers/of/address.c)
①查找节点函数
(1)根据节点路径寻找节点函数of_find_node_by_path 函数(内核源码/include/linux/of.h)
struct device_node *of_find_node_by_path(const char \*path)
参数:
• path:指定节点在设备树中的路径。
返回值:
• device_node:结构体指针,如果查找失败则返回 NULL,否则返回 device_node 类型的结构体指针,它保存着设备节点的信息。
(2)根据节点名字寻找节点函数 of_find_node_by_name 函 数 (内 核 源码/include/linux/of.h)
struct device_node *of_find_node_by_name(struct device_node \*from,const char \*name);
参数:
• from:指定从哪个节点开始查找,它本身并不在查找行列中,只查找它后面的节点,如果设置为 NULL 表示从根节点开始查找。
• name:要寻找的节点名。
返回值:device_node结构体指针,如果查找失败则返回 NULL,否则返回 device_node 类型的结构体指针,它保存着设备节点的信息。
(3)根据节点类型寻找节点函数 of_find_node_by_type 函 数 (内 核 源码/include/linux/of.h)
struct device_node *of_find_node_by_type(struct device_node \*from,const char \*type)
参数:
• from:指定从哪个节点开始查找,它本身并不在查找行列中,只查找它后面的节点,如果设置为 NULL 表示从根节点开始查找。
• type:要查找节点的类型,这个类型就是 device_node-> type。
返回值:device_node 类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL
(4)根据节点类型和 compatible 属性寻找节点函数of_find_compatible_node 函 数 (内 核 源码/include/linux/of.h)
struct device_node *of_find_compatible_node(struct device_node \*from,const char \*type, const char \*compatible)
参数:
• from:指定从哪个节点开始查找,它本身并不在查找行列中,只查找它后面的节点,如果设置为 NULL 表示从根节点开始查找。
• type:要查找节点的类型,这个类型就是 device_node-> type。 • compatible:要查找节点的 compatible 属性。
返回值:device_node 类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL。
(5)根据匹配表寻找节点函数of_find_matching_node_and_match 函数 (内核源码/include/linux/of.h)
static inline struct device_node *of_find_matching_node_and_match(struct device_node \*from, const struct of_device_id \*matches, const struct of_device_id \*\*match)
参数:
• from:指定从哪个节点开始查找,它本身并不在查找行列中,只查找它后面的节点,如果设置为 NULL 表示从根节点开始查找。
• matches:源匹配表,查找与该匹配表想匹配的设备节点。
•match:找到的匹配的 of_device_id。
返回值:device_node 类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL。
②查找父/子节点的 OF 函数
(1)寻找父节点函数of_get_parent 函数 (内核源码/include/linux/of.h)
struct device_node *of_get_parent(const struct device_node \*node)
(2)寻找子节点函数 of_get_next_child 函 数 (内 核 源码/include/linux/of.h)
struct device_node *of_get_next_child(const struct device_node \*node,␣ ,→struct device_node \*prev)
参数:
• node:指定谁(节点)要查找它的子节点。
• prev:前一个子节点,寻找的是 prev 节点之后的节点。这是一个迭代寻找过程,例如寻找第二个子节点,这里就要填第一个子节点。参数为 NULL 表示寻找第一个子节点。
返回值:device_node 类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL。
③提取属性值的 OF 函数,此结构体同样定义在文件 include/linux/of.h 中
(1)of_find_property 函数,用于查找指定的属性
property *of_find_property(const struct device_node *np,
const char *name,
int *lenp)
参数:
·np:设备节点。
·name: 属性名字。
·lenp:属性值的字节数
返回值:找到的属性。
(2)of_property_count_elems_of_size 函数用于获取属性中元素的数量
int of_property_count_elems_of_size(const struct device_node *np,
const char *propname,
int elem_size)
参数:
·np:设备节点。
·proname: 需要统计元素数量的属性名字。
·elem_size:元素长度。
返回值:得到的属性元素数量。
(3)of_property_read_u32_index 函数用于从属性中获取指定标号的 u32 类型数据值
int of_property_read_u32_index(const struct device_node *np,
const char *propname,
u32 index,
u32 *out_value)
参数:
·np:设备节点。
·proname: 要读取的属性名字。
·index:要读取的值标号。
·out_value:读取到的值
返回值:0 读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没有
要读取的数据,-EOVERFLOW 表示属性值列表太小。
(4)of_property_read_u8_array 函数、of_property_read_u16_array 函数、of_property_read_u32_array 函数、of_property_read_u64_array 函数这 4 个函数分别是读取属性中 u8、u16、u32 和 u64 类型的数组数据
(5)of_property_read_u8 函数用于读取这种只有一个整形值的属性
(6)of_property_read_string 函数用于读取属性中字符串值
int of_property_read_string(struct device_node *np,
const char *propname,
const char **out_string)
参数:
·np:设备节点。
·proname: 要读取的属性名字。
·out_string:读取到的字符串值。
返回值:0,读取成功,负值,读取失败。
(7)of_n_addr_cells 函数用于获取#address-cells 属性值
int of_n_addr_cells(struct device_node *np)
(8)of_size_cells 函数用于获取#size-cells 属性值
int of_n_size_cells(struct device_node *np)
④其他常用的 OF 函数
(1)of_device_is_compatible 函数用于查看节点的 compatible 属性是否有包含 compat 指定的字符串,也就是检查设备节点的兼容性
int of_device_is_compatible(const struct device_node *device,
const char *compat)
参数:
·device:设备节点。
·compat:要查看的字符串。
返回值:0,节点的 compatible 属性中不包含 compat 指定的字符串;正数,节点的 compatible属性中包含 compat 指定的字符串。
(2)of_get_address 函数用于获取地址相关属性,主要是“reg”或者“assigned-addresses”属性值
const __be32 *of_get_address(struct device_node *dev,
int index,
u64 *size,
unsigned int *flags)
参数:
·dev:设备节点。
·index:要读取的地址标号。
·size:地址长度。
·flags:参数,比如 IORESOURCE_IO、IORESOURCE_MEM 等
返回值:读取到的地址数据首地址,为 NULL 的话表示读取失败。
(3)of_translate_address 函数负责将从设备树读取到的地址转换为物理地址
u64 of_translate_address(struct device_node *dev,const __be32 *in_addr)
参数:
·dev:设备节点。
·in_addr:要转换的地址。
返回值:得到的物理地址,如果为 OF_BAD_ADDR 的话表示转换失败。
(4)of_address_to_resource 函数
int of_address_to_resource(struct device_node *dev,
int index,
struct resource *r)
·dev:设备节点。
·index:地址资源标号。
·r:得到的 resource 类型的资源值。
返回值:0,成功;负值,失败
(5)of_iomap 函数用于直接内存映射
void __iomem *of_iomap(struct device_node *np,
int index)
参数:
·np:设备节点。
·index:reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0。
返回值:经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。
Linux4.4 以后引入了动态设备树(Dynamic DeviceTree),我们这里翻译为“设备树插件”。设备树插件可以理解为主设备树的“补丁”它动态的加载到系统中,并被内核识别。例如我们要在系统中增加 RGB 驱动,那么我们可以针对 RGB 这个硬件设备写一个设备树插件,然后编译、加载到系统即可,无需从新编译整个设备树,格式如下:
/dts-v1/;
/plugin/;
/ {
fragment@0 {
target-path = "/";
__overlay__ {
/* 在此添加要插入的节点 */
};
};
• 第 1 行:用于指定 dts 的版本。
• 第 2 行:表示允许使用未定义的引用并记录它们,设备树插件中可以引用主设备树中的节点,而这些“引用的节点”对于设备树插件来说就是未定义的,所以设备树插件应该加上“/plugin/”。
• 第 6 行:指定设备树插件的加载位置,默认我们加载到根节点下,既“target-path =“/”。
• 第 7-8 行:我们要插入的设备及节点或者要引用(追加)的设备树节点放在 overlay {… };内。
5)设备树下的 LED 驱动实验
①修改设备树文件
在根节点“/”下创建一个名为“alphaled”的子节点,打开 imx6ull-alientek-emmc.dts 文件,在根节点“/”最后面输入如下所示内容:
alphaled {
#address-cells = <1>;
#size-cells = <1>;
compatible = "atkalpha-led";
status = "okay";
reg = < 0X020C406C 0X04 /* CCM_CCGR1_BASE */
0X020E0068 0X04 /* SW_MUX_GPIO1_IO03_BASE */
0X020E02F4 0X04 /* SW_PAD_GPIO1_IO03_BASE */
0X0209C000 0X04 /* GPIO1_DR_BASE */
0X0209C004 0X04 >; /* GPIO1_GDIR_BASE */
};
①pinctrl 子系统
(1)获取设备树中 pin 信息。
(2)根据获取到的 pin 信息来设置 pin 的复用功能
(3)根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等。
②GPIO子系统简介
gpio 子系统顾名思义,就是用于初始化 GPIO 并且提供相应的 API 函数,比如设置 GPIO为输入输出,读取 GPIO 的值等
①imx6ull.dtsi 这个文件是芯片厂商官方将芯片的通用的部分单独提出来的一些设备树配置。在iomuxc 节点中汇总了所需引脚的配置信息,pinctrl 子系统存储使用着 iomuxc节点信息。
②打开 imx6ull-alientek-emmc.dts,&iomuxc就是向 iomuxc 节点追加数据。imx6ull.dtsi 会引用 imx6ull-pinfunc.h 这个头文件,而imx6ull-pinfunc.h 又会引用 imx6ul-pinfunc.h 这个头文件,其宏定义为
(1)mux_reg是MUX引脚复用选择寄存器偏移地址
(2)conf_reg ,引脚(PAD)属性控制寄存器偏移地址。
(3)input_reg 暂且称为输入选择寄存器偏移地址,没有的话就不需要设置
(4)mux_mode是 引 脚 复 用 选 择 寄 存 器 模 式 选 择 位 的 值
(5)input_val 是输入选择寄存器的值
(6)pins的值为conf_reg 寄存器值
①函数 pinctrl_register,此函数用于向 Linux 内核注册一个 PIN 控制器
struct pinctrl_dev *pinctrl_register(struct pinctrl_desc *pctldesc,
struct device *dev,
void *driver_data)
参数 pctldesc 非常重要,因为此参数就是要注册的 PIN 控制器,PIN 控制器用于配置 SOC的 PIN 复用功能和电气特性参数, pctldesc 是 pinctrl_desc 结构体类型指针
在 imx6ull.dtsi 文件中的 GPIO 子节点记录着 GPIO 控制器的寄存器地址
• compatible :与 GPIO 子系统的平台驱动做匹配。
• reg :GPIO 寄存器的基地址,GPIO4 的寄存器组是的映射地址为 0x20a8000-0x20ABFFF
• interrupts :描述中断相关的信息
• clocks :初始化 GPIO 外设时钟信息
• gpio-controller :表示 gpio4 是一个 GPIO 控制器
• #gpio-cells :表示有多少个 cells 来描述 GPIO 引脚
• interrupt-controller :表示 gpio4 也是个中断控制器
• #interrupt-cells : 表示用多少个 cells 来描述一个中断
• gpio-ranges :将 gpio 编号转换成 pin 引脚,<&iomuxc 0 94 17>,表示将 gpio4 的第 0 个引脚引脚映射为 97,17 表示的是引脚的个数。
①gpio_request 函数用于申请一个 GPIO 管脚
int gpio_request(unsigned gpio, const char *label)
函数参数和返回值含义如下:
·gpio:要申请的 gpio 标号,使用 of_get_named_gpio 函数从设备树获取指定 GPIO 属性信息,此函数会返回这个 GPIO 的标号。
·label:给 gpio 设置个名字。
·返回值:0,申请成功;其他值,申请失败。
②gpio_free 函数如果不使用某个 GPIO 了,那么就可以调用 gpio_free 函数进行释放
void gpio_free(unsigned gpio)
函数参数和返回值含义如下:
·gpio:要释放的 gpio 标号。
·返回值:无。
③gpio_direction_input 函数此函数用于设置某个 GPIO 为输入,函数原型如下所示:
int gpio_direction_input(unsigned gpio)
函数参数和返回值含义如下:
·gpio:要设置为输入的 GPIO 标号。
·返回值:0,设置成功;负值,设置失败。
④gpio_direction_output 函数此函数用于设置某个 GPIO 为输出,并且设置默认输出值
int gpio_direction_output(unsigned gpio, int value)
函数参数和返回值含义如下:
·gpio:要设置为输出的 GPIO 标号。
·value:GPIO 默认输出值。
·返回值:0,设置成功;负值,设置失败。
⑤gpio_get_value 函数此函数用于获取某个 GPIO 的值(0 或 1),此函数是个宏
#define gpio_get_value __gpio_get_value
int __gpio_get_value(unsigned gpio)
函数参数和返回值含义如下:
·gpio:要获取的 GPIO 标号。
·返回值:非负值,得到的 GPIO 值;负值,获取失败。
⑥gpio_set_value 函数此函数用于设置某个 GPIO 的值,此函数是个宏,定义如下
#define gpio_set_value __gpio_set_value
void __gpio_set_value(unsigned gpio, int value)
函数参数和返回值含义如下:
·gpio:要设置的 GPIO 标号。
·value:要设置的值。
①of_gpio_named_count 函数用于获取设备树某个属性里面定义了几个 GPIO 信息
int of_gpio_named_count(struct device_node *np, const char *propname)
函数参数和返回值含义如下:
·np:设备节点。
·propname:要统计的 GPIO 属性。
·返回值:正值,统计到的 GPIO 数量;负值,失败。
②of_gpio_count 此函数统计的是“gpios”这个属性的 GPIO 数量
int of_gpio_count(struct device_node *np)
函数参数和返回值含义如下:
·np:设备节点。
·返回值:正值,统计到的 GPIO 数量;负值,失败。
③of_get_named_gpio 函数获取 GPIO 编号
int of_get_named_gpio(struct device_node *np,
const char *propname,
int index)
函数参数和返回值含义如下:
·np:设备节点。
·propname:包含要获取 GPIO 信息的属性名。
·index:GPIO 索引,因为一个属性里面可能包含多个 GPIO,此参数指定要获取哪个 GPIO的编号,如果只有一个 GPIO 信息的话此参数为 0。
·返回值:正值,获取到的 GPIO 编号;负值,失败。
①添加 pinctrl 节点
pinctrl_led: ledgrp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0 /* LED0 */
>;
};
②添加 LED 设备节点
gpioled {
#address-cells = <1>;
#size-cells = <1>;
compatible = "atkalpha-gpioled";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_led>;
led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
status = "okay";
};
并发是指多个执行单元同时、并行执行,而并发的执行单元对共享资源 (硬件资源和软件上的全局变量、静态变量等) 的访问则很容易导致竞态。
竞态的定义是在并发的执行单元对共享资源 (硬件资源和软件上的全局变量、静态变量等) 的访问。对应到我们的 linux 系统就是多个线程对于共享资源的相互竞争访问,而不是按照一定的顺序访问;从而造成不可控的错误,从竞态的定义可知,竞态产生需要两个条件,第一,存在共享资源,第二,对于共享资源进行竞争访问。解决竞态的主要路径是当有一个执行单元在访问共享资源时,其他的执行单元禁止访问,根据共享资源的种类以及实际应用场景我们有多种解决方法可选:
原子操作指不能再进一步分割的操作,原子操作分为整型原子操作和位原子操作。
①整型原子操作
Linux 内核定义了叫做 atomic_t 的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量,此结构体定义在 include/linux/types.h 文件中:
typedef struct {int counter;} atomic_t;
②位原子操作函数(宏定义)
函数 | 描述 |
---|---|
void set_bit(int nr, void *p) | 将 p 地址的第 nr 位置 1。 |
void clear_bit(int nr,void *p) | 将 p 地址的第 nr 位清零。 |
void change_bit(int nr, void *p) | 将 p 地址的第 nr 位进行翻转。 |
int test_bit(int nr, void *p) | 获取 p 地址的第 nr 位的值。 |
int test_and_set_bit(int nr, void *p) | 将 p 地址的第 nr 位置 1,并且返回 nr 位原来的值。 |
int test_and_clear_bit(int nr, void *p) | 将p 地址的第 nr 位清零,并且返回 nr 位原来的值 |
int test_and_change_bit(int nr, void *p) | 将p地址的第 nr 位翻转,并且返回 nr 位原来的值 |
当一个线程要访问某个共享资源的时候首先要先获取相应的锁,锁只能被一个线程持有,只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。对于自旋锁而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,Linux 内核使用结构体 spinlock_t 表示自旋锁
①自旋锁基本 API 函数表
函数 | 描述 |
---|---|
DEFINE_SPINLOCK(spinlock_t lock) | 定义并初始化一个自选变量。 |
int spin_lock_init(spinlock_t *lock) | 初始化自旋锁。 |
void spin_lock(spinlock_t *lock) | 获取指定的自旋锁,也叫做加锁。 |
void spin_unlock(spinlock_t *lock) | 释放指定的自旋锁。 |
int spin_trylock(spinlock_t *lock) | 尝试获取指定的自旋锁,如果没有获取到就返回 0 |
int spin_is_locked(spinlock_t *lock) | 检查指定的自旋锁是否被获取,如果没有被获取就返回非 0,否则返回 0。 |
②线程与中断并发访问处理 API 函数
函数 | 描述 |
---|---|
void spin_lock_irq(spinlock_t *lock) | 禁止本地中断,并获取自旋锁。 |
void spin_unlock_irq(spinlock_t *lock) | 激活本地中断,并释放自旋锁。 |
void spin_lock_irqsave(spinlock_t *lock,unsigned long flags) | 保存中断状态,禁止本地中断,并获取自旋锁。 |
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) | 将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁。 |
③下半部竞争处理函数
函数 | 描述 |
---|---|
void spin_lock_bh(spinlock_t *lock) | 关闭下半部,并获取自旋锁。 |
void spin_unlock_bh(spinlock_t *lock) | 打开下半部,并释放自旋锁。 |
④读写自旋锁
函数 | 描述 |
---|---|
DEFINE_RWLOCK(rwlock_t lock) | 定义并初始化读写锁 |
void rwlock_init(rwlock_t *lock) | 初始化读写锁。 |
读锁 | |
void read_lock(rwlock_t *lock) | 获取读锁。 |
void read_unlock(rwlock_t *lock) | 释放读锁。 |
void read_lock_irq(rwlock_t *lock) | 禁止本地中断,并且获取读锁。 |
void read_unlock_irq(rwlock_t *lock) | 打开本地中断,并且释放读锁。 |
void read_lock_irqsave(rwlock_t *lock, unsigned long flags) | 保存中断状态,禁止本地中断,并获取读锁。 |
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags) | 将中断状态恢复到以前的状态,并且激活本地中断,释放读锁。 |
void read_lock_bh(rwlock_t *lock) | 关闭下半部,并获取读锁。 |
void read_unlock_bh(rwlock_t *lock) | 打开下半部,并释放读锁。 |
写锁 | |
void write_lock(rwlock_t *lock) | 获取写锁。 |
void write_unlock(rwlock_t *lock) | 释放写锁。 |
void write_lock_irq(rwlock_t *lock) | 禁止本地中断,并且获取写锁。 |
void write_unlock_irq(rwlock_t *lock) | 打开本地中断,并且释放写锁。 |
void write_lock_irqsave(rwlock_t *lock,unsigned long flags) | 保存中断状态,禁止本地中断,并获取写锁。 |
void write_unlock_irqrestore(rwlock_t *lock,unsigned long flags) | 将中断状态恢复到以前的状态,并且激活本地中断,释放读锁。 |
void write_lock_bh(rwlock_t *lock) | 关闭下半部,并获取读锁。 |
void write_unlock_bh(rwlock_t *lock) | 打开下半部,并释放读锁。 |
⑤顺序锁
函数 | 描述 |
---|---|
DEFINE_SEQLOCK(seqlock_t sl) | 定义并初始化顺序锁 |
void seqlock_ini seqlock_t *sl) | 初始化顺序锁。 |
顺序锁写操作 | |
void write_seqlock(seqlock_t *sl) | 获取写顺序锁。 |
void write_sequnlock(seqlock_t *sl) | 释放写顺序锁。 |
void write_seqlock_irq(seqlock_t *sl) | 禁止本地中断,并且获取写顺序锁 |
void write_sequnlock_irq(seqlock_t *sl) | 打开本地中断,并且释放写顺序锁。 |
void write_seqlock_irqsave(seqlock_t *sl,unsigned long flags) | 保存中断状态,禁止本地中断,并获取写顺序锁。 |
void write_sequnlock_irqrestore(seqlock_t *sl,unsigned long flags) | 将中断状态恢复到以前的状态,并且激活本地中断,释放写顺序锁。 |
void write_seqlock_bh(seqlock_t *sl) | 关闭下半部,并获取写读锁。 |
void write_sequnlock_bh(seqlock_t *sl) | 打开下半部,并释放写读锁。 |
顺序锁读操作 | |
unsigned read_seqbegin(const seqlock_t *sl) | 读单元访问共享资源的时候调用此函数,此函数会返回顺序锁的顺序号。 |
unsigned read_seqretry(const seqlock_t *sl,unsigned start) | 读结束以后调用此函数检查在读的过程中有没有对资源进行写操作,如果有的话就要重读 |
·因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场
合。
·因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
·如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。
Linux 内核使用 semaphore 结构体表示信号量,结构体内容如下所示:
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
函数 | 描述 |
---|---|
DEFINE_SEAMPHORE(name) | 定义一个信号量,并且设置信号量的值为 1。 |
void sema_init(struct semaphore *sem, int val) | 初始化信号量 sem,设置信号量值为 val。 |
void down(struct semaphore *sem) | 获取信号量,因为会导致休眠,因此不能在中断中使用。 |
int down_trylock(struct semaphore *sem); | 尝试获取信号量,如果能获取到信号量就获取,并且返回 0。如果不能就返回非 0,并且不会进入休眠。 |
int down_interruptible(struct semaphore *sem) | 获取信号量,和 down 类似,只是使用 down 进入休眠状态的线程不能被信号打断。而使用此函数进入休眠以后是可以被信号打断的。 |
void up(struct semaphore *sem) | 释放信号量 |
互斥访问表示一次只有一个线程可以访问共享资源,不能递归申请互斥体。在我们编写 Linux 驱动的时候遇到需要互斥访问的地方建议使用 mutex
①定义互斥体
struct mutex {
/* 1: unlocked, 0: locked, negative: locked, possible waiters */
atomic_t count;
spinlock_t wait_lock;
};
②互斥体 API 函数
函数 | 描述 |
---|---|
DEFINE_MUTEX(name) | 定义并初始化一个 mutex 变量。 |
void mutex_init(mutex *lock) | 初始化 mutex。 |
void mutex_lock(struct mutex *lock) | 获取 mutex,也就是给 mutex 上锁。如果获取不到就进休眠。 |
void mutex_unlock(struct mutex *lock) | 释放 mutex,也就给 mutex 解锁。 |
int mutex_trylock(struct mutex *lock) | 尝试获取 mutex,如果成功就返回 1,如果失败就返回 0。 |
int mutex_is_locked(struct mutex *lock) | 判断 mutex 是否被获取,如果是的话就返回1,否则返回 0。 |
int mutex_lock_interruptible(struct mutex *lock) | 使用此函数获取信号量失败进入休眠以后可以被信号打断。 |
./arch/arm/boot/dts/ 目录下的 imx6ull.dtsi 设备树文件
①顶层中断控制器找到“interrupt-controller”节点
②搜“intc”即可找到“gpc 一级子中断控制器”
③搜“gpc”即可找到“二级子中断控制器”
④按键设备树节点配置
①request_irq 中断注册函数
②中断注销函数 free_irq
③中断处理函数irqreturn_t (*irq_handler_t)(int irq, void * dev);
④中断的使能和禁用函数
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)
⑤关闭和开启全局中断相关函数
local_irq_enable()
local_irq_disable()
•上半部分:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
•下半部分:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。
①软中断
Linux 内核使用结构体 softirq_action 表示软中断,softirq_action结构体定义在文件 include/linux/interrupt.h 中
(1)注册软中断函数
void open_softirq(int nr, void (*action)(struct softirq_action *))
(2)注册好软中断以后需要通过 raise_softirq 函数触发
void raise_softirq(unsigned int nr)
(3)软中断“中断向量表”
(4)中断 interrupt-controller 节点
②tasklet
tasklet 是基于软中断实现,相比软中断 tasklet 使用起来更简单,最重要的一点是在多 CPU 系统中同一时间只有一个 CPU 运 行 tasklet,所以并发、可重入问题就变得很容易处理(一个 tasklet 甚至不用去考虑),而且使用时也比较简单
(1)在驱动中使用 tasklet_struct 结构体定义一个 tasklet
(2) tasklet 初始化函数
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long),
unsigned long data);
(3)在上半部,也就是中断处理函数中调用 tasklet_schedule 函数就能使 tasklet 在合适的时间运行
void tasklet_schedule(struct tasklet_struct *t)
③工作队列
如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet
(1) 使用work_struct 结构体定义工作
#define INIT_WORK(_work, _func)
(2)work_func_t func用于指定“工作”的处理函数
(3)工作初始化函数
#define DECLARE_WORK(n, f)
(4)工作的调度函数
bool schedule_work(struct work_struct *work)
·#interrupt-cells,指定中断源的信息 cells 个数。
·interrupt-controller,表示当前节点为中断控制器。
·interrupts,指定中断号,触发方式等。
·interrupt-parent,指定父中断,也就是中断控制器。
①通过 irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号
unsigned int irq_of_parse_and_map(struct device_node *dev,
int index)
函数参数和返回值含义如下:
·dev:设备节点。
·index:索引号,interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息。
·返回值:中断号。
②可以使用 gpio_to_irq 函数来获取 gpio 对应的中断号,函数原型如下:
int gpio_to_irq(unsigned int gpio)
函数参数和返回值含义如下:
·gpio:要获取的 GPIO 编号。
·返回值:GPIO 对应的中断号。
①设备负责提供硬件资源而驱动代码负责去使用这些设备提供的硬件资源。并由总线将它们联系起来。这样子就构成以下图形中的关系。
• 设备 (device) :挂载在某个总线的物理设备;
• 驱动 (driver) :与特定设备相关的软件,负责初始化该设备以及提供一些操作该设备的操作方式;
• 总线(bus) :负责管理挂载对应总线的设备以及驱动;
• 类 (class) :对于具有相同功能的设备,归结到一种类别,进行分类管理;
②/sys/bus 目录下的每个子目录都是注册好了的总线类型。这里是设备按照总线类型分层放置的目录结构,每个子目录 (总线类型) 下包含两个子目录——devices 和 drivers 文件夹;其中 devices 下是该总线类型下的所有设备,而这些设备都是符号链接,它们分别指向真正的设备 (/sys/devices/下);如下图 bus 下的 usb 总线中的 device 则是 Devices 目录下/pci()/dev 0:10/usb2 的符号链接。而 drivers下是所有注册在这个总线上的驱动,每个 driver 子目录下是一些可以观察和修改的 driver 参数。.
③系统启动之后会调用 buses_init 函数创建/sys/bus 文件目录,这部分系统在开机时已经帮我们准备好了,接下去就是通过总线注册函数 bus_register 进行总线注册,注册完总线后在总线的目录下生成 devices 文件夹和 drivers 文件夹,最后分别通过 device_register 以及 driver_register 函数注册相对应的设备和驱动。
①总线
(1)在内核中使用结构体 bus_type 来表示总线(内核源码/include/linux/device.h)
(2)注册总线 API(内核源码/drivers/base/bus.c)
int bus_register(struct bus_type *bus);
·参数: bus: bus_type 类型的结构体指针
·返回值:成功返回0,失败返回负数
(3)注销总线 API(内核源码/drivers/base/bus.c)
void bus_unregister(struct bus_type *bus);
·参数: bus :bus_type 类型的结构体指针
·返回值:无
②设备
(1)在内核使用 device 结构体来描述我们的物理设备 (内核源码/include/linux/device.h)
(2)内核也提供相关的 API 来注册设备 (内核源码/driver/base/core.c)
int device_register(struct device *dev);
·参数: dev :struct device 结构体类型指针
·返回值:成功返回0,失败返回负数
(3)内核注销设备 (内核源码/driver/base/core.c)
void device_unregister(struct device *dev);
·参数: dev :struct device 结构体类型指针
·返回值:无
③驱动
(1)使用device_driver结构体来描述我们的驱动(内核源码/include/linux/device.h)
(2)driver_register 函数注册驱动,成功注册的驱动会记录在/sys/bus//drivers 目录(内核源码/include/linux/device.h)
int driver_register(struct device_driver *drv);
·参数: drv :struct device_driver 结构体类型指针
·返回值:成功返回0,失败返回负数
(3)driver_unregister 函数注销驱动
void driver_unregister(struct device_driver *drv);
参数:
· drv :struct device_drive 结构体类型指针
·返回值:无
注册新的总线、设备或驱动时,内核会在对应的地方创建一个新的目录,内核中以 attribute 结构体来描述/sys 目录下的文件
①内核中以 attribute 结构体来描述/sys 目录下的文件(内核源码/include/linux/sysfs.h)
② attribute_group 结 构 体(内核源码/include/linux/sysfs.h)
bus_type、device、device_driver 结构体中都包含了一种数据类型 struct attribute_group,它是多个 attribute 文件的集合,利用它进行初始化,可以避免一个个注册 attribute。
③设备属性文件接口device_attribute(内核源码/include/linux/device.h)
④驱动属性文件driver_attribute(内核源码/include/linux/device.h)
⑤总线属性文件接口bus_attribute(内核源码/include/linux/device.h)
Linux 提出了 platform 这个虚拟总线,平台总线用于管理、挂载那些没有相应物理总线的设备,对应的设备驱动则被称为平台驱动。
①平台总线
platform 总线是 bus_type 的一个具体实例,platform 总线提供了四种匹配方式,并且这四种方式存在着优先级:设备树机制 >ACPI匹配模式 >id_table 方式 > 字符串比较。
(1)platform_bus_type 平台总线结构体(内核源码/driver/base/platform.c)
struct bus_type platform_bus_type = {
.name = "platform",
.dev_groups = platform_dev_groups,
.match = platform_match,
.uevent = platform_uevent,
.pm = &platform_dev_pm_ops,
};
(2)platform_bus_init 函 数 (内核源码/driver/base/platform.c)
②平台设备
平台驱动需要实现 probe 函数,当平台总线成功匹配驱动和设备时,则会调用驱动的 probe 函数,在该函数中使用上述的函数接口来获取资源,以初始化设备,最后填充结构体 platform_driver,调用 platform_driver_register 进行注册。
(1)使用 platform_device 结构体来描述平台设备(内核源码/include/linux/platform_device.h)
(2)使用结构体resource (内核源码/include/linux/ioport.h)来保存设备所提供的资源,比如设备使用的中断编号,寄存器物理地址等
(3)注册平台设备platform_device_register 函数(内核源码/drivers/base/platform.c)
int platform_device_register(struct platform_device *pdev)
参数:
·pdev: platform_device 类型结构体指针
·返回值:成功返回0,失败返回负数
(4)平台注销函数platform_device_unregister函数(内核源码/drivers/base/platform.c)
参数:
· pdev: platform_device 类型结构体指针
·返回值:无
③平台驱动
(1)描述平台驱动platform_driver结构体(内核源码/include/platform_device.h)
(2)id_table表(内核源码/include/linux/mod_devicetable.h),元素类型为platform_device_id
struct platform_device_id {
char name[PLATFORM_NAME_SIZE];
kernel_ulong_t driver_data;
};
(3)注册/注销平台驱动(内核源码/drivers/base/platform.c)
int platform_driver_register(struct platform_driver *drv);
void platform_driver_unregister(struct platform_driver *drv);
platform 驱动框架分为总线、设备和驱动,其中总线是 Linux 内核提供的,在使用设备树的时候, 我们只需要实现 platform_driver 即可。
①在设备树中创建设备节点
gpioled {
#address-cells = <1>;
#size-cells = <1>;
compatible = "atkalpha-gpioled";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_led>;
led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
status = "okay";
};
②编写 platform 驱动的时候要注意兼容属性
static const struct of_device_id leds_of_match[] = {
{ .compatible = "atkalpha-gpioled" }, /* 兼容属性 */
{ /* Sentinel */ }
};
① 编写 Makefile 文件
② 声明一个总线结构体并创建一个总线 xbus,实现 match 方法,对设备和驱动进行匹配
③ 声明一个设备结构体,挂载到我们的 xbus 总线中
④ 声明一个驱动结构体,挂载到 xbus 总线,实现 probe、remove 方法
⑤ 将总线、设备、驱动导出属性文件到用户空间。
按键、鼠标、键盘、触摸屏等都属于输入(input)设备,Linux 内核为此专门做了一个叫做 input子系统的框架来处理输入事件。输入子系统分为了 Drivers(驱动层)、Input Core(输入子系统核心层)、handlers(事件处理层)三部分
• Drivers 主要实现对硬件设备的读写访问,设置中断,并将硬件产生的事件转为 Input Core定义的规范提交给 Handlers;
• Input Core 起到承上启下的作用,为 Drivers 提供了规范及接口,并通知 Handlers 对事件进行处理;
• Handlers 并不涉及硬件方面的具体操作,是一个纯软件层,包含了不同的解决方案,如按键、键盘、鼠标、游戏手柄等。
最终所有输入设备的输入信息将被抽象成以下结构体:
struct input_event 结 构 体 (内 核 源码/include/uapi/linux/input.h)
在输入子系统中 input_dev 代表一个具体的输入设备,input_dev 结构体
①输入子系统事件类型 (内核源码/include/uapi/linux/input-event-codes.h)
#define EV_SYN 0x00 /* 同步事件 */
#define EV_KEY 0x01 /* 按键事件 */
#define EV_REL 0x02 /* 相对坐标事件 */
#define EV_ABS 0x03 /* 绝对坐标事件 */
#define EV_MSC 0x04 /* 杂项(其他)事件 */
#define EV_SW 0x05 /* 开关事件 */
#define EV_LED 0x11 /* LED */
#define EV_SND 0x12 /* sound(声音) */
#define EV_REP 0x14 /* 重复事件 */
#define EV_FF 0x15 /* 压力事件 */
#define EV_PWR 0x16 /* 电源事件 */
#define EV_FF_STATUS 0x17 /* 压力状态事件 */
②输入子系统—按键键值(内 核 源码/include/uapi/linux/input-event-codes.h)
① input_dev 申 请 函 数 (内 核 源码/drivers/input/input.c)
struct input_dev *input_allocate_device(void)
② input_dev 释 放 函 数 (内 核 源码/drivers/input/input.c)
void input_free_device(struct input_dev *dev)
① input_dev 注 册 函 数 (内 核 源码/drivers/input/input.c)
int input_register_device(struct input_dev *dev)
② input_dev 注 销 函 数 (内 核 源码/drivers/input/input.c)
void input_unregister_device(struct input_dev *dev)
void input_event(struct input_dev *dev,
unsigned int type,
unsigned int code,
int value)
此函数用于上报指定的事件以及对应的值,函数参数和返回值含义如下:
·dev:需要上报的 input_dev。
·type: 上报的事件类型,比如 EV_KEY。
·code:事件码,也就是我们注册的按键值,比如 KEY_0、KEY_1 等等。
·value:事件值,比如 1 表示按键按下,0 表示按键松开。
·返回值:无。
①通 用 的 上 报 事 件 函 数 (内 核 源 码 include/linux/input.h)
static inline void input_report_key(struct input_dev *dev,
unsigned int code, int value) {
input_event(dev, EV_KEY, code, !!value);
}
②上报按键事件及发送上报结束事件 (内核源码include/linux/input.h)
void input_sync(struct input_dev *dev)
Framebuffer 子系统是用一个视频输出设备从包含完整的帧数据的一个内存缓冲区中来驱动一个视频显示设备。也就是说 Framebuffer 是一块内存保存着一帧的图像,向这块内存写入数据就相当于向屏幕中写入数据
①fb 的 file_operations 操作集定义在 drivers/video/fbdev/core/fbmem.c 文件中
②Linux 内核将所有的 Framebuffer 抽象为一个叫做 fb_info 的结构体,LCD 的驱动就是构建 fb_info,并且向系统注册 fb_info的过程。fb_info 结构体定义在 include/linux/fb.h 文件里
③NXP 官方编写的 Linux 下的 LCD 驱动,打开 imx6ull.dtsi,然后找到 lcdif节点,可以看出 lcdif 节点的 compatible 属性值为“fsl,imx6ul-lcdif”和“fsl,imx28-lcdif”
④搜索compatible 属性值即可找到 I.MX6ULL 的 LCD 驱动文件,这个文件为 drivers/video/fbdev/mxsfb.c。可以看出LCD 驱动文件是一个标准的 platform 驱动,当驱动和设备匹配以后mxsfb_probe 函数就会执行。
⑤调用 mxsfb_init_fbinfo 函数初始化 fb_info,重点是 fb_info 的 var、fix、fbops,screen_base 和 screen_size。其中 fbops 是 Framebuffer 设备的操作集,NXP 提供的 fbops 为
mxsfb_ops
⑥mxsfb_probe 函数的主要工作内容为:
(1)申请 fb_info。
(2)初始化 fb_info 结构体中的各个成员变量。
(3)初始化 eLCDIF 控制器。
(4)使用 register_framebuffer 函数向 Linux 内核注册初始化好的 fb_info。
int register_framebuffer(struct fb_info *fb_info)
函数参数和返回值含义如下:
·fb_info:需要上报的 fb_info。
·返回值:0,成功;负值,失败。
①LCD 所使用的 IO 配置。
打开 imx6ull-alientek-emmc.dts 文件,在 iomuxc 节点中找到pinctrl_lcdif_dat(RGB LCD 的 24 根数据线配置项)、pinctrl_lcdif_ctrl(RGB LCD 的 4 根控制线配置项,包括 CLK、 ENABLE、VSYNC 和 HSYNC)和pinctrl_pwm1(LCD 背光 PWM 引脚配置项)
②LCD 屏幕节点修改,修改相应的属性值,换成我们所使用的 LCD 屏幕参数。
在 imx6ull-alientek-emmc.dts 文件中找到 lcdif 节点,就是向 imx6ull.dtsi 文件中的 lcdif 节点追加的内容
③LCD 背光节点信息修改,要根据实际所使用的背光 IO 来修改相应的设备节点信息。
(1)在 imx6ull-alientek-emmc.dts 中找到pinctrl_pwm1IO 的配置
(2)在 imx6ull-alientek-emmc.dts 文件中找到向 pwm1追加的内容,设置 pwm1 所使用的 IO 为 pinctrl_pwm1
(3)在 imx6ull-alientekemmc.dts 文件中找到backlight,修改亮度属性
i2c 总线包括 i2c 设备 (i2c_client) 和 i2c 驱动 (i2c_driver), 当我们向 linux 中注册设备或驱动的时候,按照 i2c 总线匹配规则进行配对,配对成功,则可以通过 i2c_driver 中.prob 函数创建具体的设备驱动。在现代 linux 中,i2c 设备不再需要手动创建,而是使用设备树机制引入,设备树节点是与 paltform 总线相配合使用的。所以需先对 i2c 总线包装一层 paltform 总线,当设备树节点转换为平台总线设备时,我们在进一步将其转换为 i2c 设备,注册到 i2c 总线中。
①运行机制
(1)注册 I2C 总线
(2)将 I2C 驱动添加到 I2C 总线的驱动链表中
( 3)遍历 I2C 总线上的设备链表,根据 i2c_device_match 函数进行匹配,如果匹配调用i2c_device_probe 函数
(4)i2c_device_probe 函数会调用 I2C 驱动的 probe 函数
②相关结构体
(1)i2c_adapter 结构体 (内核源码/include/linux/i2c.h):i2c_ 适配器对应一个 i2c 控制器,是用于标识物理 i2c 总线以及访问它所需的访问算法的结构。
(2) i2c_algorithm结构体(内核源码/include/linux/i2c.h):用于指定访问总线(i2c)的算法,在这里就是用于指定外部访问 i2c 总线的接口
③i2c 总线相关函数
(1) i2c 总线定义 (内核源码/drivers/i2c/i2c-corebase.c)
struct bus_type i2c_bus_type = {
.name = "i2c",
.match = i2c_device_match,
.probe = i2c_device_probe,
.remove = i2c_device_remove,
.shutdown = i2c_device_shutdown,
};
(2)i2c设备和 i2c驱动匹配规则i2c_device_match (内核源码/drivers/i2c/i2c-core-base.c)
• of_driver_match_device:设备树匹配方式,比较 I2C 设备节点的 compatible 属性和of_device_id 中的 compatible 属性
• acpi_driver_match_device: ACPI 匹配方式
• i2c_match_id: i2c 总线传统匹配方式,比较 I2C 设备名字和 i2c 驱动的 id_table->name 字段是否相等
①I2C 适配器驱动的主要工作就是初始化 i2c_adapter 结构体变量,然后设置 i2c_algorithm 中的 master_xfer 函数。完成以后通过 i2c_add_numbered_adapter或 i2c_add_adapter 这两个函数向系统注册设置好的 i2c_adapter
②I.MX6U 的 I2C 适配器驱动文件为 drivers/i2c/busses/i2c-imx.c,当设备和驱动匹配成功以后 i2c_imx_probe 函数就会执行,i2c_imx_probe 函数就会完成 I2C 适配器初始化工作
③i2c_imx_probe 函数主要的工作就是一下两点:
(1)初始化 i2c_adapter,设置 i2c_algorithm 为 i2c_imx_algo,最后向 Linux 内核注册i2c_adapter。
(2)初始化 I2C1 控制器的相关寄存器。
④i2c_adapter总线注册 /注销函数
int i2c_add_adapter(struct i2c_adapter *adapter)
int i2c_add_numbered_adapter(struct i2c_adapter *adap)
void i2c_del_adapter(struct i2c_adapter * adap)
①相关结构体
(1) i2c_client 结构体 (内核源码/include/linux/i2c.h):表示 i2c 从设备
(2) i2c_driver 结构体 (内核源码/include/linux/i2c.h):i2c 设备驱动程序
②注册、注销相关函数
(1)注册一个 i2c 适配器 (内核源码/drivers/i2c/i2ccore-base.c)
int i2c_add_adapter(struct i2c_adapter *adapter)
(2)注册一个 i2c 设备驱动 (内核源码/drivers/i2c/i2ccore-base.c)
int i2c_register_driver(struct module *owner,
struct i2c_driver *driver)
函数参数和返回值含义如下:
·owner:一般为 THIS_MODULE。
·driver:要注册的 i2c_driver。
·返回值:0,成功;负值,失败。
(3)注销 I2C 设备驱动
void i2c_del_driver(struct i2c_driver *driver)
③收发消息相关函数(内核源码/drivers/i2c/i2c-core-base.c)
(1)对 I2C 设备寄存器进行读写操作
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
函数参数和返回值含义如下:
·adap:所使用的 I2C 适配器,i2c_client 会保存其对应的 i2c_adapter。
·msgs:I2C 要发送的消息,i2c_msg 结构体定义在 include/uapi/linux/i2c.h 文件中
·num:消息数量,也就是 msgs 的数量。
·返回值:负值,失败,其他非负值,发送的 msgs 数量。
(2) I2C 数据发送函数 i2c_master_send
int i2c_master_send(const struct i2c_client *client,
const char *buf,
int count)
函数参数和返回值含义如下:
·client:I2C 设备对应的 i2c_client。
·buf:要发送的数据。
·count:要发送的数据字节数,要小于 64KB
·返回值:负值,失败,其他非负值,发送的字节数。
(3)I2C 数据接收函数,这两个函数最终都会调用i2c_transfer
int i2c_master_recv(const struct i2c_client *client,
char *buf,
int count)
函数参数和返回值含义如下:
·client:I2C 设备对应的 i2c_client。
·buf:要接收的数据。
·count:要接收的数据字节数,要小于 64KB
·返回值:负值,失败,其他非负值,发送的字节数。
① 修改设备树 imx6ull-alientek-emmc.dts
(1)找到pinctrl_i2c1 子节点,对IO 修改或添加
(2)在 &i2c1 节点下添加 ap3216c 的设备子节点
②AP3216C 驱动编写
spi 设备驱动涉及到字符设备驱动、SPI 核心层、SPI 主机驱动,具体功能如下:
• SPI 核心层:提供 SPI 控制器驱动和设备驱动的注册方法、注销方法、SPI 通信硬件无关接口函数。
• SPI 主机驱动:主要包含 SPI 硬件体系结构中适配器 (spi 控制器) 的控制,用于产生 SPI 读写时序。
• SPI 设备驱动:通过 SPI 主机驱动与 CPU 交换数据。
SPI 主机驱动的核心就是申请 spi_master,然后初始化 spi_master,最后向 Linux 内核注册spi_master。
①Linux 内核使用 spi_master 表示 SPI 主机驱动,定义在 include/linux/spi/spi.h
(1)spi_master 申请与释放
struct spi_master *spi_alloc_master(struct device *dev,
unsigned size)
函数参数和返回值含义如下:
·dev:设备,一般是 platform_device 中的 dev 成员变量。
·size:私有数据大小,可以通过 spi_master_get_devdata 函数获取到这些私有数据。
·返回值:申请到的 spi_master。
(2)spi_master 的释放 函数
void spi_master_put(struct spi_master *master)
(3)spi_master 的注册与注销
int spi_register_master(struct spi_master *master)
void spi_unregister_master(struct spi_master *master)
②ecspi3设备树节点(内 核 源码/arch/arm/boot/dts/imx6ull.dtsi)
compatible 属性有两个值“fsl,imx6ul-ecspi”和“fsl,imx51-ecspi”,在 Linux 内核源码中搜素这两个属性值即可找到 I.MX6U 对应的 ECSPI(SPI)主机驱动。I.MX6U 的 ECSPI 主机驱动文件为 drivers/spi/spi-imx.c。当设备和驱动匹配成功以后 spi_imx_probe 函数就会执行。spi_imx_probe 函数会从设备树中读取相应的节点属性值,申请并初始化 spi_master,最后调用 spi_bitbang_start 函数(spi_bitbang_start 会调用 spi_register_master 函数)向 Linux 内核注册spi_master。
当总线注册成功之后,会在 sys/bus 下面生成一个 spi 总线,然后在系统中新增一个设备类,sys/class/目录下会可以找到 spi_master 类。
①spi 总线定义(内核源码/drivers/spi/spi.c)
struct bus_type spi_bus_type = {
.name = "spi",
.dev_groups = spi_dev_groups,
.match = spi_match_device,
.uevent = spi_uevent,
};
② SPI 设备和驱动的匹配函数为 spi_match_device
①spi_driver 结构体(内核源码/include/linux/spi/spi.h)
spi_driver 和 i2c_driver、platform_driver 基本一样,当 SPI 设备和驱动匹配成功以后 probe 函数就会执行。
(1)ecspi 设备注册函数:int spi_register_driver(struct spi_driver *sdrv)
(2)SPI 设备驱动注销函数void spi_unregister_driver(struct spi_driver *sdrv)
②spi_transfer 读写操作结构体(内核源码/include/linux/spi/spi.h)
③spi_transfer 需要组织成spi_message 结构体
(1)spi_message初始化函数
void spi_message_init(struct spi_message *m)
(2)spi_message 初始化完成以后需要将 spi_transfer 添加到 spi_message 队列中
void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)
④spi_message 准备好以后既可以进行数据传输了
(1)同步传输会阻塞的等待 SPI 数据传输完成
int spi_sync(struct spi_device *spi, struct spi_message *message)
(2)异步传输不会阻塞的等到 SPI 数据传输完成
int spi_async(struct spi_device *spi, struct spi_message *message)
①修改设备树
(1)添加 ICM20608 所使用的 IO,在 iomuxc 节点中添加一个新的子节点来描述 ICM20608 所使用的 SPI 引脚,子节点名字为 pinctrl_ecspi3
pinctrl_ecspi3: icm20608 {
fsl,pins = <
MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20 0x10b0 /* CS */
MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK 0x10b1 /* SCLK */
MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO 0x10b1 /* MISO */
MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI 0x10b1 /* MOSI */
>;
};
(2)在 ecspi3 节点追加 icm20608 子节点
&ecspi3 {
fsl,spi-num-chipselects = <1>;
cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_ecspi3>;
status = "okay";
spidev: icm20608@0 {
compatible = "alientek,icm20608";
spi-max-frequency = <8000000>;
reg = <0>;
};
};
①块设备只能以块为单位进行读写访问,块是 linux 虚拟文件系统(VFS)基本的数据传输单位。字符设备是以字节为单位进行数据传输的,不需要缓冲。
②块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,等到条件成熟以后在一次性将缓冲区中的数据写入块设备中
①block_device 结构体
linux 内核使用 block_device 表示块设备, block_device 为 一 个 结 构 体 ,定义 在include/linux/fs.h 文件中
(1)注册块设备
int register_blkdev(unsigned int major, const char *name)
函数参数和返回值含义如下:
·major:主设备号。
·name:块设备名字。
·返回值:如果参数 major 在 1~255 之间的话表示自定义主设备号,那么返回 0 表示注册成功,如果返回负值的话表示注册失败。
(2)注销块设备
void unregister_blkdev(unsigned int major, const char *name)
函数参数和返回值含义如下:
·major:要注销的块设备主设备号。
·name:要注销的块设备名字。
②gendisk 结构体
linux 内核使用 gendisk 来描述一个磁盘设备,定义在 include/linux/genhd.h
(1)申请 gendisk
struct gendisk *alloc_disk(int minors)
函数参数和返回值含义如下:
·minors:次设备号数量,也就是 gendisk 对应的分区数量。
·返回值:成功:返回申请到的 gendisk,失败:NULL。
(2)删除 gendisk
void del_gendisk(struct gendisk *gp)
函数参数和返回值含义如下:
·gp:要删除的 gendisk。
·返回值:无。
(3)将 gendisk 添加到内核
void add_disk(struct gendisk *disk)
函数参数和返回值含义如下:
·disk:要添加到内核的 gendisk。
·返回值:无。
(4)设置 gendisk 容量
void set_capacity(struct gendisk *disk, sector_t size)
函数参数和返回值含义如下:
·disk:要设置容量的 gendisk。
·size:磁盘容量大小,注意这里是扇区数量。块设备中最小的可寻址单元是扇区,一个扇区一般是 512 字节,有些设备的物理扇区可能不是 512 字节。不管物理扇区是多少,内核和块设备驱动之间的扇区都是 512 字节。所以 set_capacity 函数设置的大小就是块设备实际容量除以512 字节得到的扇区数量。比如一个 2MB 的磁盘,其扇区数量就是(210241024)/512=4096。
(5)调整 gendisk 引用计数
truct kobject *get_disk(struct gendisk *disk) //增加 gendisk 的引用计数
void put_disk(struct gendisk *disk) //减少 gendisk 的引用计数
③block_device_operations 结构体
和字符设备的 file _operations 一样,块设备也有操作集,为结构体 block_device_operations,此结构体定义在 include/linux/blkdev.h 中
请求队列(request_queue)里面包含的就是一系列的请求(request),request 里面有一个名为“bio”的成员变量,类型为 bio 结构体指针。真正的数据就保存在 bio 里面,所以我们需要从 request_queue 中取出一个一个的 request,然后再从每个 request 里面取出 bio,最后根据 bio 的描述讲数据写入到块设备,或者从块设备中读取数据。request_queue、request 和 bio 之间的关系:
①请求队列 request_queue,定义在文件 include/linux/blkdev.h
(1)初始化请求队列
request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
·rfn:请求处理函数指针,每个 request_queue 都要有一个请求处理函数,请求处理函数
request_fn_proc 原型如下:
void (request_fn_proc) (struct request_queue *q)
·lock:自旋锁指针,需要驱动编写人员定义一个自旋锁,然后传递进来。,请求队列会使用这个自旋锁。
·返回值:如果为 NULL 的话表示失败,成功的话就返回申请到的 request_queue 地址。
(2)删除请求队列
void blk_cleanup_queue(struct request_queue *q)
(3)request_queue 申请函数
struct request_queue *blk_alloc_queue(gfp_t gfp_mask)
(4)为 blk_alloc_queue 函数申请到的请求队列绑定一个“制造请求”函数
void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)
函数参数和返回值含义如下:
·q:需要绑定的请求队列,也就是 blk_alloc_queue 申请到的请求队列。
·mfn:需要绑定的“制造”请求函数,函数原型如下:
void (make_request_fn) (struct request_queue *q, struct bio *bio)
②请求 request(include/linux/blkdev.h)
(1)从request_queue中依次获取每个request
request *blk_peek_request(struct request_queue *q)
函数参数和返回值含义如下:
·q:指定 request_queue。
·返回值:request_queue 中下一个要处理的请求(request),如果没有要处理的请求就返回NULL
(2)获取到下一个要处理的请求以后就要开始处理这个请求
void blk_start_request(struct request *req)
(3)一步到位处理请求
struct request *blk_fetch_request(struct request_queue *q)
{
struct request *rq;
rq = blk_peek_request(q);
if (rq)
blk_start_request(rq);
return rq;
}
③bio 结构
(1)bio 是个结构体,定义在 include/linux/blk_types.h 中
(2)bio、bvec_iter 以及 bio_vec 这三个机构体之间的关系
既然 bio 是块设备最小的数据传输单元,那么 bio 就有必要描述清楚这些信息,其中 bi_iter 这个结构体成员变量就用于描述物理存储设备地址信息,比如要操作的扇区地址。bi_io_vec 指向 bio_vec 数组首地址,bio_vec 数组就是 RAM 信息,比如页地址、页偏移以及长度.三者关系如图所示:
(3)遍历请求中的 bio使用函数__rq_for_each_bio,这是一个宏
#define __rq_for_each_bio(_bio, rq) \
if ((rq->bio)) \
for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)
(4)遍历 bio 中的所有段,此函数也是一个宏
#define bio_for_each_segment(bvl, bio, iter) \
__bio_for_each_segment(bvl, bio, iter, (bio)->bi_iter)
(5)通知 bio 处理结束
bvoid bio_endio(struct bio *bio, int error)
函数参数和返回值含义如下:
·bio:要结束的 bio。
·error:如果 bio 处理成功的话就直接填 0,如果失败的话就填个负值,比如-EIO。
·返回值:无
内部的 MAC 外设会通过 MII 或者 RMII 接口来连接外部的 PHY 芯片,MII/RMII 接口用来传输网络数据。另外主控需要配置或读取 PHY 芯片,也就是读写 PHY 的内部寄存器,所以还需要一个控制接口,叫做 MIDO,MDIO 很类似 IIC,也是两根线,一根数据线叫做 MDIO,一根时钟线叫做 MDC。
PHY 芯片寄存器地址空间为 5 位,地址 0~31 共 32 个寄存器,IEEE 定义了 0~15 这 16 个寄存器的功能,16~31 这 16 个寄存器由厂商自行实现。
①LAN8720A 中断管理
LAN8720A 的器件管理接口支持非 IEEE 802.3 规范的中断功能。当一个中断事件发生并且相应事件的中断位使能,LAN8720A 就会在 nINT(14 脚)产生一个低电平有效的中断信号。LAN8720A 的中断系统提供两种中断模式:主中断模式和复用中断模式。主中断模式是默认中断模式,LAN8720A 上电或复位后就工作在主中断模式,当模式控制/状态寄存器(十进制地址为 17)的 ALTINT 位为 0 时 LAN8720A 工作在主模式,当 ALTINT 位为 1 时工作在复用中断模式。
②PHY 地址设置
MDIO 最多可以控制 32 个 PHY 芯片,LAN8720A 通过设置 RXER/PHYAD0引脚来设置其 PHY 地址,默认情况下为 0
RXER/PHYAD0 引脚状态 | PHY 地址 |
---|---|
上拉 | 0X01 |
下拉(默认) | 0X00 |
③nINT/REFCLKO 配置
nINTSEL 引脚(2 号引脚)用于设置 nINT/REFCLKO 引脚(14 号引脚)的功能。当 LAN8720A 工作在 REF_CLK In 模式时,50MHz 的外部时钟信号应接到 LAN8720 的XTAL1/CKIN 引脚(5 号引脚)上。为了降低成本,LAN8720A 可以从外部的 25MHz 的晶振中产生 REF_CLK 时钟。到要使用此功能时应工作在 REF_CLK Out 模式时
nINTSEL 引脚值 | 模式 | nINT/REFCLKO 引脚功能 |
---|---|---|
nINTSEL= 0 | REF_CLK Out 模式 | nINT/REFCLKO 作为 REF_CLK 时钟源 |
nINTSEL = 1(默认) | REF_CLK In 模式 | nINT/REFCLKO 作为中断引脚 |
④LAN8720A 内部寄存器
(1)BCR(Basic Control Rgsister)寄存器,地址为 0
(2)BSR(Basic Status Register)寄存器,地址为 1。此寄存器为 PHY 的状态寄存器,通过读取 BSR寄存器的值我们可以得到当前的连接速度、双工状态和连接状态等
(3) PHY ID 寄存器 1 和 ID 寄存器 2,地址为 2 和 3,这两个寄存器都是 PHY 的 ID 寄存器器,这两个寄存器组成一个 32 位的唯一 ID 值。IEEE 规定了一叫做 OUI 的 ID组成方式,全称是 Organizationally Unique Identifier,OUI 一共 32 位,分为三部分:22 位的 ID+6位厂商型号 ID+4 位厂商版本 ID
(4)特殊控制/状态寄存器,此寄存器地址为 31,寄存器内容是LAN8720A 厂商自定义的
①net_device 结构体
Linux 内核使用 net_device 结构体表示一个具体的网络设备。网络驱动的核心就是初始化 net_device 结构体中的各个成员变量,然后将初始化完成以后的 net_device 注册到 Linux 内核中。net_device 结构体定义在 include/linux/netdevice.h 中
(1)申请 net_device,这是一个宏
#define alloc_netdev(sizeof_priv, name, name_assign_type, setup) \
alloc_netdev_mqs(sizeof_priv, name, name_assign_type, setup, 1, 1)
函数参数和返回值含义如下:
·sizeof_priv:私有数据块大小。
·name:设备名字。
·setup:回调函数,初始化设备的设备后调用此函数。
·txqs:分配的发送队列数量。
·rxqs:分配的接收队列数量。
·返回值:如果申请成功的话就返回申请到的 net_device 指针,失败的话就返回 NULL。
(2)删除 net_device
void free_netdev(struct net_device *dev)
(3)注册 net_device
int register_netdev(struct net_device *dev)
函数参数和返回值含义如下:
·dev:要注册的 net_device 指针。
·返回值:0 注册成功,负值 注册失败。
(4)注销 net_device
void unregister_netdev(struct net_device *dev)
②void unregister_netdev(struct net_device *dev)
netdev_ops,为 net_device_ops 结构体指针类型,这就是网络设备的操作集。net_device_ops 结构体定义在 include/linux/netdevice.h 文件中
③sk_buff 结构体定义在 include/linux/skbuff.h 中
(1)dev_queue_xmit 函数将网络数据发送出去,函数定义在 include/linux/netdevice.h 中
static inline int dev_queue_xmit(struct sk_buff *skb)
函数参数和返回值含义如下:
·skb:要发送的数据,这是一个 sk_buff 结构体指针,sk_buff 是 Linux 网络驱动中一个非常重要的结构体,网络数据就是以 sk_buff 保存的,各个协议层在 sk_buff 中添加自己的协议头,最终由底层驱动讲 sk_buff 中的数据发送出去。网络数据的接收过程恰好相反,网络底层驱动将接收到的原始数据打包成 sk_buff,然后发送给上层协议,上层会取掉相应的头部,然后将最终的数据发送给用户。
·返回值:0 发送成功,负值 发送失败。
(2)上层接收数据的话使用 netif_rx 函数,但是最原始的网络数据一般是通过轮询、中断或 NAPI的方式来接收。netif_rx 函数定义在 net/core/dev.c 中
int netif_rx(struct sk_buff *skb)
函数参数和返回值含义如下:
·skb:保存接收数据的 sk_buff,。
·返回值:NET_RX_SUCCESS 成功,NET_RX_DROP 数据包丢弃。
(3)分配 sk_buff(include/linux/skbuff.h)
static inline struct sk_buff *alloc_skb(unsigned int size,
gfp_t priority)
函数参数和返回值含义如下:
·size:要分配的大小,也就是 skb 数据段大小。
·priority:为 GFP MASK 宏,比如 GFP_KERNEL、GFP_ATOMIC 等。
·返回值:分配成功的话就返回申请到的 sk_buff 首地址,失败的话就返回 NULL。
(4)申请一个用于接收的 skb_buff
static inline struct sk_buff *netdev_alloc_skb(struct net_device *dev,
unsigned int length)
函数参数和返回值含义如下:
·dev:要给哪个设备分配 sk_buff。
·length:要分配的大小。
·返回值:分配成功的话就返回申请到的 sk_buff 首地址,失败的话就返回 NULL。
(5)释放 sk_buff
void kfree_skb(struct sk_buff *skb)
void dev_kfree_skb (struct sk_buff *skb) //网络设备使用
(6) skb_put 函数,此函数用于在尾部扩展 skb_buff的数据区
unsigned char *skb_put(struct sk_buff *skb, unsigned int len)
函数参数和返回值含义如下:
·skb:要操作的 sk_buff。
·len:要增加多少个字节。
·返回值:扩展出来的那一段数据区首地址。
(7)skb_push 函数用于在头部扩展 skb_buff 的数据区
unsigned char *skb_push(struct sk_buff *skb, unsigned int len)
函数参数和返回值含义如下:
·skb:要操作的 sk_buff。
·len:要增加多少个字节。
·返回值:扩展完成以后新的数据区首地址。
(8)sbk_pull 函数用于从 sk_buff 的数据区起始位置删除数据
unsigned char *skb_pull(struct sk_buff *skb, unsigned int len)
函数参数和返回值含义如下:
·skb:要操作的 sk_buff。
·len:要删除的字节数。
·返回值:删除以后新的数据区首地址。
(9)sbk_reserve 函数用于调整缓冲区的头部大小, 让skb_buff 的 data 和 tail 同时后
移 n 个字节即可
static inline void skb_reserve(struct sk_buff *skb, int len)
函数参数和返回值含义如下:
·skb:要操作的 sk_buff。
·len:要增加的缓冲区头部大小。
④网络 NAPI 处理机制
NAPI 是一种高效的网络处理技术。NAPI 的核心思想就是不全部采用中断来读取网络数据,而是采用中断来唤醒数据接收服务程序,在接收服务程序中采用 POLL 的方法来轮询处理数据
(1)初始化 NAPI
首先要初始化一个 napi_struct 实例,使用 netif_napi_add 函数,此函数定义在 net/core/dev.c
void netif_napi_add(struct net_device *dev,
struct napi_struct *napi,
int (*poll)(struct napi_struct *, int),
int weight)
函数参数和返回值含义如下:
·dev:每个 NAPI 必须关联一个网络设备,此参数指定 NAPI 要关联的网络设备。
·napi:要初始化的 NAPI 实例。
·poll:NAPI 所使用的轮询函数,非常重要,一般在此轮询函数中完成网络数据接收的作
·weight:NAPI 默认权重(weight),一般为 NAPI_POLL_WEIGHT。
(2)删除 NAPI使用 netif_napi_del 函数即可,函数原型如下:
void netif_napi_del(struct napi_struct *napi)
(3)使能 NAPI,使用函数 napi_enable,函数原型如下:
inline void napi_enable(struct napi_struct *n)
函数参数和返回值含义如下:
·n:要使能的 NAPI。
·返回值:无。
(4)关闭 NAPI使用 napi_disable 函数即可,函数原型如下:
void napi_disable(struct napi_struct *n)
(5)检查 NAPI 是否可以进行调度
inline bool napi_schedule_prep(struct napi_struct *n)
函数参数和返回值含义如下:
·n:要检查的 NAPI。
·返回值:如果可以调度就返回真,如果不可调度就返回假。
(6)如果可以调度的话就进行调度,使用__napi_schedule 函数完成 NAPI 调度
void __napi_schedule(struct napi_struct *n)
(7)NAPI 处理完成以后需要调用 napi_complete 函数来标记 NAPI 处理完成
inline void napi_complete(struct napi_struct *n)