赞
踩
驱动与底层硬件直接打交道,充当了硬件与应用软件中间的桥梁。
Linux系统主要部分:内核、shell、文件系统、应用程序
Linux驱动分为三个基础大类:字符设备驱动,块设备驱动,网络设备驱动。
Linux利用CPU实现内核态和用户态
Linux实现内核态和用户态切换
ARM Linux的系统调用实现原理是采用swi软中断从用户态切换至内核态
X86是通过int 0x80中断进入内核态
Linux只能通过系统调用和硬件中断从用户空间进入内核空间
Linux 驱动有两种运行方式
模块加载函数
module_init(xxx_init);
模块卸载函数
module_exit(xxx_exit);
模块许可证明
MODULE_LICENSE("GPL") //添加模块 LICENSE 信息 ,LICENSE 采用 GPL 协议
模块参数(可选)
模块参数是一种内核空间与用户空间的交互方式,只不过是用户空间 --> 内核空间单向的,他对应模块内部的全局变量
模块信息(可选)
MODULE_AUTHOR("songwei") //添加模块作者信息
模块打印 printk
printk在内核中用来记录日志信息的函数,只能在内核源码范围内使用。和printf非常相似。
printk函数主要做两件事情:①将信息记录到log中 ②调用控制台驱动来将信息输出
printk 可以根据日志级别对消息进行分类,一共有 8 个日志级别
#define KERN_SOH "\001"
#define KERN_EMERG KERN_SOH "0" /* 紧急事件,一般是内核崩溃 */
#define KERN_ALERT KERN_SOH "1" /* 必须立即采取行动 */
#define KERN_CRIT KERN_SOH "2" /* 临界条件,比如严重的软件或硬件错误*/
#define KERN_ERR KERN_SOH "3" /* 错误状态,一般设备驱动程序中使用KERN_ERR 报告硬件错误 */
#define KERN_WARNING KERN_SOH "4" /* 警告信息,不会对系统造成严重影响 */
#define KERN_NOTICE KERN_SOH "5" /* 有必要进行提示的一些信息 */
#define KERN_INFO KERN_SOH "6" /* 提示性的信息 */
#define KERN_DEBUG KERN_SOH "7" /* 调试信息 */
以下代码就是设置“gsmi: Log Shutdown Reason\n”这行消息的级别为 KERN_EMERG。
printk(KERN_DEBUG"gsmi: Log Shutdown Reason\n");
如果使用 printk 的时候不显式的设置消息级别,那 么printk 将会采用默认级别MESSAGE_LOGLEVEL_DEFAULT,默认为 4。
在 include/linux/printk.h 中有个宏 CONSOLE_LOGLEVEL_DEFAULT,定义如下:
#define CONSOLE_LOGLEVEL_DEFAULT 7
CONSOLE_LOGLEVEL_DEFAULT 控制着哪些级别的消息可以显示在控制台上,此宏默认为 7,意味着只有优先级高于 7 的消息才能显示在控制台上。
这个就是 printk 和 printf 的最大区别,可以通过消息级别来决定哪些消息可以显示在控制台上。默认消息级别为 4,4 的级别比 7 高,所示直接使用 printk 输出的信息是可以显示在控制台上的。
MAJOR // 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。
MINOR //用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。
MKDEV //用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。
CPU只能访问虚拟地址,不能直接向寄存器地址写入数据,必须得到寄存器物理地址在Linux系统中对应的虚拟地址。
物理内存和虚拟内存之间的转换,需要用到: ioremap 和 iounmap两个函数
ioremap,用于获取指定物理地址空间对应的虚拟地址空间
/*
phys_addr:要映射给的物理起始地址(cookie)
size:要映射的内存空间大小
mtype: ioremap 的类型,可以选择 MT_DEVICE、 MT_DEVICE_NONSHARED、MT_DEVICE_CACHED 和 MT_DEVICE_WC,
ioremap 函数选择 MT_DEVICE
返回值: __iomem 类型的指针,指向映射后的虚拟空间首地址
*/
#define ioremap(cookie,size) __arm_ioremap((cookie), (size),MT_DEVICE)
void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype)
{
return arch_ioremap_caller(phys_addr, size, mtype, __builtin_return_address(0));
}
例:获取某个寄存器对应的虚拟地址
#define addr (0X020E0068) // 物理地址
static void __iomem* va; //指向映射后的虚拟空间首地址的指针
va=ioremap(addr, 4); // 得到虚拟地址首地址
iounmap,卸载驱动使用 iounmap 函数释放掉 ioremap 函数所做的映射。
参数 addr:要取消映射的虚拟地址空间首地址
iounmap(va);
当外部寄存器或外部内存映射到内存空间时,称为 I/O 内存。但是对于 ARM 来说没有 I/O 空间,因此 ARM 体系下只有 I/O 内存(可以直接理解为内存)。
使用 ioremap 函数将寄存器的物理地址映射到虚拟地址后,可以直接通过指针访问这些地址,但是 Linux 内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。
读操作函数
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)
readb、 readw 和 readl 分别对应 8bit、 16bit 和 32bit 读操作,参数 addr 就是要读取写内存地址,返回值是读取到的数据
写操作函数
void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)
writeb、 writew 和 writel分别对应 8bit、 16bit 和 32bit 写操作,参数 value 是要写入的数值, addr 是要写入的地址。
Device Tree是一种描述硬件的数据结构,以便于操作系统的内核可以管理和使用这些硬件,包括CPU或CPU,内存,总线和其他一些外设。
Linux内核从3.x版本之后开始支持使用设备树,可以实现驱动代码与设备的硬件信息相互的隔离,减少了代码中的耦合性
引入设备树之前:一些与硬件设备相关的具体信息都要写在驱动代码中,如果外设发生相应的变化,那么驱动代码就需要改动。
引入设备树之后:通过设备树对硬件信息的抽象,驱动代码只要负责处理逻辑,而关于设备的具体信息存放到设备树文件中。如果只是硬件接口信息的变化而没有驱动逻辑的变化,开发者只需要修改设备树文件信息,不需要改写驱动代码。
1.根节点:\
2.设备节点:nodex
①节点名称:node
②节点地址:node@0, @后面即为地址
3.属性:属性名称(Property name)和属性值(Property value)
4.标签
#include <dt-bindings/input/input.h>
#include "imx6ull.dtsi"
设备树也支持头文件,设备树的头文件扩展名为.dtsi。在.dts 设备树文件中,还可以通过“#include”来引用.h、 .dtsi 和.dts 文件。
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,
每个节点都通过一些属性信息来描述节点信息,属性就是键—值对。
label: node-name@unit-address
label:节点标签,方便访问节点:通过&label访问节点,追加节点信息
node-name:节点名字,为字符串,描述节点功能
unit-address:设备的地址或寄存器首地址,若某个节点没有地址或者寄存器,可以省略
设备树源码中常用的几种数据形式
1.字符串: compatible = "arm,cortex-a7";设置 compatible 属性的值为字符串“arm,cortex-a7”
2.32位无符号整数:reg = <0>; 设置reg属性的值为0
3.字符串列表:字符串和字符串之间采用“,”隔开
compatible = "fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand";
设置属性 compatible 的值为“fsl,imx6ull-gpmi-nand”和“fsl, imx6ul-gpmi-nand”。
compatible属性(兼容属性)
cpp "manufacturer,model" manufacturer:厂商名称 model:模块对应的驱动名字
例:
imx6ull-alientekemmc.dts 中 sound 节点是 音频设备节点,采用的欧胜(WOLFSON)出品的 WM8960, sound 节点的 compatible 属性值如下:
cpp compatible = "fsl,imx6ul-evk-wm8960","fsl,imx-audio-wm8960";
属性值有两个,分别为“fsl,imx6ul-evk-wm8960”和“fsl,imx-audio-wm8960”,其中“fsl”表示厂商是飞思卡尔,“imx6ul-evk-wm8960”和“imx-audio-wm8960”表示驱动模块名字。
sound这个设备首先使用第一个兼容值在 Linux 内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值查。
一般驱动程序文件会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,如果设备节点的 compatible 属性值和 OF 匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。
在根节点来说,Linux 内核会通过根节点的 compoatible 属性查看是否支持此设备,如果支持的话设备就会启动 Linux 内核。如果不支持的话那么这个设备就没法启动 Linux 内核。
model属性
model 属性值是一个字符串,一般 model 属性描述设备模块信息。
status属性
status 属性和设备状态有关的, status 属性值是字符串,描述设备的状态信息。
#address-cells 和#size-cells 属性
用于描述子节点的地址信息,reg属性的address 和 length的字长。
//每个“address length”组合表示一个地址范围,
//其中 address 是起始地址, length 是地址长度,
//#address-cells 表明 address 这个数据所占用的字长,
// #size-cells 表明 length 这个数据所占用的字长.
reg = <address1 length1 address2 length2 address3 length3……>
reg属性
reg 属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息, reg 属性的值一般是(address, length)对.
例
uart1: serial@02020000 {
compatible = "fsl,imx6ul-uart",
"fsl,imx6q-uart", "fsl,imx21-uart";
reg = <0x02020000 0x4000>;
interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_UART1_IPG>,
<&clks IMX6UL_CLK_UART1_SERIAL>;
clock-names = "ipg", "per";
status = "disabled";
};
uart1 的父节点 aips1: aips-bus@02000000 设置了#address-cells = <1>、 #sizecells = <1>,因此 reg 属性中 address=0x02020000, length=0x4000。都是字长为1.
ranges属性
ranges属性值可以为空或者按照( child-bus-address , parent-bus-address , length )格式编写的数字
ranges 是一个地址映射/转换表, ranges 属性每个项目由子地址、父地址和地址空间长度这三部分组成。
如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换。
child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells 确定此物理地址所占用的字长
parent-bus-address: 父总线地址空间的物理地址,同样由父节点的#address-cells 确定此物理地址所占用的字长
length: 子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长
特殊节点
在根节点“/”中有两个特殊的子节点: aliases 和 chosen
aliases
aliases {
can0 = &flexcan1;
can1 = &flexcan2;
...
usbphy0 = &usbphy1;
usbphy1 = &usbphy2;
};
aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。
但是,一般会在节点命名的时候会加上 label,然后通过&label来访问节点。
chosen
chosen 不是一个真实的设备, chosen 节点主要是为了 uboot 向 Linux 内核传递数据(bootargs 参数)。
Linux 内核提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_” (称为OF 函数)
Linux 内核使用 device_node 结构体来描述一个节点:
struct device_node {
const char *name; /* 节点名字 */
const char *type; /* 设备类型 */
phandle phandle;
const char *full_name; /* 节点全名 */
struct fwnode_handle fwnode;
struct property *properties; /* 属性 */
struct property *deadprops; /* removed 属性 */
struct device_node *parent; /* 父节点 */
struct device_node *child; /* 子节点
...
}
通过节点名字查找指定的节点:of_find_node_by_name
struct device_node *of_find_node_by_name(struct device_node *from,const char *name)
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
name:要查找的节点名字。
返回值: 找到的节点,如果为 NULL 表示查找失败。
通过 device_type 属性查找指定的节点:of_find_node_by_type
struct device_node *of_find_node_by_type(struct device_node *from, const char *type)
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
type:要查找的节点对应的 type 字符串, device_type 属性值。
返回值: 找到的节点,如果为 NULL 表示查找失败
通过device_type 和 compatible两个属性查找指定的节点:of_find_compatible_node
struct device_node *of_find_compatible_node(struct device_node *from,
const char *type,
const char *compatible)
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
type:要查找的节点对应的 type 字符串,device_type 属性值,可以为 NULL
compatible: 要查找的节点所对应的 compatible 属性列表。
返回值: 找到的节点,如果为 NULL 表示查找失败
通过 of_device_id 匹配表来查找指定的节点:of_find_matching_node_and_match
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: of_device_id 匹配表,在此匹配表里面查找节点。
match: 找到的匹配的 of_device_id。
返回值: 找到的节点,如果为 NULL 表示查找失败
通过路径来查找指定的节点:of_find_node_by_path
inline struct device_node *of_find_node_by_path(const char *path)
path:设备树节点中绝对路径的节点名,可以使用节点的别名
返回值: 找到的节点,如果为 NULL 表示查找失败
Linux 内核中使用结构体 property 表示属性
struct property {
char *name; /* 属性名字 */
int length; /* 属性长度 */
void *value; /* 属性值 */
struct property *next; /* 下一个属性 */
unsigned long _flags;
unsigned int unique_id;
struct bin_attribute attr;
}
查找指定的属性:of_find_property
property *of_find_property(const struct device_node *np,
const char *name,
int *lenp)
np:设备节点。
name: 属性名字。
lenp:属性值的字节数,一般为NULL
返回值: 找到的属性。
获取属性中元素的数量(数组):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:元素长度。
返回值: 得到的属性元素数量
从属性中获取指定标号的 u32 类型数据值:of_property_read_u32_index
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 表示属性值列表太小
读取属性中 u8、 u16、 u32 和 u64 类型的数组数据
of_property_read_u8_array
of_property_read_u16_array
of_property_read_u32_array
of_property_read_u64_array
int of_property_read_u8_array(const struct device_node *np,
const char *propname,
u8 *out_values,
size_t sz)
np:设备节点。
proname: 要读取的属性名字。
out_value:读取到的数组值,分别为 u8、 u16、 u32 和 u64。
sz: 要读取的数组元素数量。
返回值: 0:读取成功;
负值: 读取失败
-EINVAL 表示属性不存在
-ENODATA 表示没有要读取的数据
-EOVERFLOW 表示属性值列表太小
读取属性中字符串值: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,读取成功,负值,读取失败
获取 #address-cells 属性值:of_n_addr_cells ,获取 #size-cells 属性值:of_size_cells 。
int of_n_addr_cells(struct device_node *np)
int of_n_size_cells(struct device_node *np)
np:设备节点。
返回值: 获取到的#address-cells 属性值。
返回值: 获取到的#size-cells 属性值。
内存映射
of_iomap 函数用于直接内存映射,前面通过 ioremap 函数来完成物理地址到虚拟地址的映射,采用设备树以后就可以直接通过 of_iomap 函数来获取内存地址所对应的虚拟地址。这样就不用再去先获取reg属性值,再用属性值映射内存。
of_iomap 函数本质上也是将 reg 属性中地址信息转换为虚拟地址,如果 reg 属性有多段的话,可以通过 index 参数指定要完成内存映射的是哪一段, of_iomap 函数原型如下:
void __iomem *of_iomap(struct device_node *np, int index)
np:设备节点。
index: reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0。
返回值: 经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。
例
#if 1
/* 1、寄存器地址映射 */
IMX6U_CCM_CCGR1 = ioremap(regdata[0], regdata[1]);
SW_MUX_GPIO1_IO03 = ioremap(regdata[2], regdata[3]);
SW_PAD_GPIO1_IO03 = ioremap(regdata[4], regdata[5]);
GPIO1_DR = ioremap(regdata[6], regdata[7]);
GPIO1_GDIR = ioremap(regdata[8], regdata[9]);
#else //第一对:起始地址+大小 -->映射 这样就不用获取reg的值
IMX6U_CCM_CCGR1 = of_iomap(dtsled.nd, 0);
SW_MUX_GPIO1_IO03 = of_iomap(dtsled.nd, 1);
SW_PAD_GPIO1_IO03 = of_iomap(dtsled.nd, 2);
GPIO1_DR = of_iomap(dtsled.nd, 3);
GPIO1_GDIR = of_iomap(dtsled.nd, 4);
#endif
int ret;
u32 regdate[14];
const char *str;
struct property *proper;
/* 1 、获取设备节点: */
dtb_led.nd = of_find_node_by_path("/songwei_led");
if(dtb_led.nd == NULL)
{
printk("songwei_led node can not found!\r\n");
return -EINVAL;
}else
{
printk("songwei_led node has been found!\r\n");
}
/* 2 、获取 compatible 属性内容 */
proper = of_find_property(dtb_led.nd ,"compatible",NULL);
if(proper == NULL)
{
printk("compatible property find failed\r\n");
} else
{
printk("compatible = %s\r\n", (char*)proper->value);
}
/* 3 、获取 status 属性内容 */
ret = of_property_read_string(dtb_led.nd, "status", &str);
if(ret < 0)
{
printk("status read failed!\r\n");
}else
{
printk("status = %s\r\n",str);
}
/* 4 、获取 reg 属性内容 */
ret = of_property_read_u32_array(dtb_led.nd, "reg", regdate, 10);
if(ret < 0)
{
printk("reg property read failed!\r\n");
}else
{
u8 i = 0;
printk("reg data:\r\n");
for(i = 0; i < 10; i++)
printk("%#X ", regdate[i]);
printk("\r\n");
}
/* 驱动入口函数 */
static int __init xxx_init(void)
{
/* 入口函数具体内容 */
return 0;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
/* 出口函数具体内容 */
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(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)
这种注册函数会将后面所有的次设备号全部占用,而且主设备号需要我们自己去设置,现在不推荐这样使用。
一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块的出口函数 xxx_exit 中进行。
#define ioremap(cookie,size) __arm_ioremap((cookie), (size),
MT_DEVICE)
void __iomem * __arm_ioremap(phys_addr_t phys_addr,
size_t size,
unsigned int mtype)
{
return arch_ioremap_caller(phys_addr,
size,
mtype,
__builtin_return_address(0));
}
返回值: __iomem 类型的指针,指向映射后的虚拟空间首地址。
建立映射:映射的虚拟地址 = ioremap(IO内存起始地址,映射长度);
一旦映射成功,访问对应的虚拟地址就相当于访问对应的IO内存 。
void iounmap (volatile void __iomem *addr)
应用层和内核层是不能直接进行数据传输的。 要想进行数据传输, 要借助下面的这两个函数
static inline long 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)
to:目标地址
from:源地址
n:将要拷贝数据的字节数
返回值:成功返回 0, 失败返回没有拷贝成功的数据字节数
#define CHRDEVBASE_MAJOR 200 //手动设置主设备号
#define CHRDEVBASE_NAME "chrdevbase" //设备名称
//内核缓存区
static char readbuf[100]; //读数据缓存
static char writebuf[100]; //写数据缓存
static char kerneldata[] = {"kernel data!"}; //测试数据
//硬件寄存器
#define GPIO_TEST_BASE (0x01234567) //宏定义寄存器映射地址
static void __iomem *GPIO_TEST; // __iomem 类型的指针,指向映射后的虚拟空间首地址
//打开设备
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
return 0;
}
// 从设备读取数据
static ssize_t chrdevbase_read(struct file *filp , char __user *buf , size_t cnt , loff_t *offt)
{
int retvalue = 0;
unsigned char databuf[1];
// 读取硬件寄存器
#if 0
//读取寄存器状态
databuf[0] = readl(GPIO_TEST);
retvalue = copy_to_user(buf , databuf, cnt);
//读取内核内存
#else
//测试数据拷贝到读数据缓存中
memcpy(readbuf , kerneldata , sizeof(kerneldata));
//内核中数据(读缓存)拷贝到用户空间
retvalue = copy_to_user(buf , readbuf , cnt);
#endif
if(retvalue == 0) printk("kernel senddate ok!\n");
else printk("kernel senddate failed!\n");
return 0;
}
//向设备写数据
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt , loff_t *offt)
{
int retvalue = 0;
//写硬件寄存器
#if 0
writel(buf[0],GPIO_TEST);
//写内核缓存
#else
//用户数据拷贝到内核空间(写缓存)
retvalue = copy_from_user(writebuf , buf ,cnt);
#endif
if(retvalue == 0) printk("kernel recevdate : %s\n",writebuf);
else printk("kernel recevdate failed!");
return 0;
}
//关闭/释放设备
static int chrdevbase_release(struct inode *inode , struct file *filp)
{
return 0;
}
//设备操作函数
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
/* 驱动入口函数 */
static int __init chrdevbase_init(void)
{
int retvalue = 0;
//寄存器物理映射,物理地址映射到虚拟地址指针
GPIO_TEST= ioremap(GPIO_TEST_BASE, 4);
//注册字符设备驱动
retvalue = register_chrdev(CHRDEVBASE_MAJOR, //主设备号
CHRDEVBASE_NAME, //设备名称
&chrdevbase_fops); //设备操作函数集合
if(retvalue < 0) printk("chrdevbase driver register failed\n");
printk("chrdevbase_init()\r\n");
return 0;
}
/* 驱动出口函数 */
static void __exit chrdevbase_exit(void)
{
//解除寄存器映射
iounmap(GPIO_TEST);
//注消字符设备驱动
unregister_chrdev(CHRDEVBASE_MAJOR , //主设备号
CHRDEVBASE_NAME); //设备名称
printk("chrdevbase_exit()\r\n");
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
MODULE_LICENSE("GPI");//GPL模块许可证
MODULE_AUTHOR("songwei");//作者信息
加载驱动模块后,需手动创建驱动节点文件
mknod /dev/chrdevbase c 200 0
上面的驱动框架,当使用 modprobe 加载驱动程序以后还需要使用命令mknod手动创建设备节点。
在 Linux 下通过 udev(用户空间程序) 来实现设备文件的创建与删除,但是在嵌入式 Linux 中使用mdev 来实现设备节点文件的自动创建与删除, Linux 系统中的热插拔事件也由 mdev 管理。
设备文件系统有devfs,mdev,udev这三种
上述设备号为开发者挑选一个未使用的进行注册。Linux驱动开发推荐使用动态分配设备号。
动态申请设备号
int alloc_chrdev_region(dev_t *dev,
unsigned baseminor,
unsigned count,
const char *name)
dev:保存申请到的设备号。
baseminor: 次设备号起始地址,该函数可以申请一段连续的多个设备号,初始值一般为0
count: 要申请的设备号数量。
name:设备名字。
静态申请设备号
int register_chrdev_region(dev_t from, unsigned count, const char *name);
from - 要申请的起始设备号
count - 设备号个数
name - 设备号在内核中的名称
返回0申请成功,否则失败
释放设备号
void unregister_chrdev_region(dev_t from, unsigned count)
from:要释放的设备号。
count: 表示从 from 开始,要释放的设备号数量。
申请设备号模板
//创建设备号
if (newchrled.major) //定义了设备号就静态申请
{
newchrled.devid = MKDEV(newchrled.major, 0);
register_chrdev_region(newchrled.devid,
NEWCHRLED_CNT,
NEWCHRLED_NAME);
}
else //没有定义设备号就动态申请
{
alloc_chrdev_region(&newchrled.devid,
0,
NEWCHRLED_CNT,
NEWCHRLED_NAME);//申请设备号
newchrled.major = MAJOR(newchrled.devid); //获取分配号的主设备号
newchrled.minor = MINOR(newchrled.devid); // 获取分配号的次设备号
}
在 Linux 中使用 cdev 结构体表示一个字符设备
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;//操作函数集合
struct list_head list;
dev_t dev;//设备号
unsigned int count;
};
在 cdev 中有两个重要的成员变量:ops 和 dev,字符设备文件操作函数集合file_operations 以及设备号 dev_t。
初始化cdev结构体变量
void cdev_init(struct cdev *cdev,
const struct file_operations *fops);
例
struct cdev testcdev;
//设备操作函数
static struct file_operations test_fops = {
.owner = THIS_MODULE,
//其他具体的初始项
};
testcdev.owner = THIS_MODULE;
//初始化 cdev 结构体变量
cdev_init(&testcdev, &test_fops);
将设备添加到内核
cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用 cdev_init 函数完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备。
将cdev添加到内核同时绑定设备号。
其实这里申请设备号和注册设备在第一中驱动中直接使用register_chrdev函数完成者两步操作
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
p - 要添加的cdev结构
dev - 绑定的起始设备号
count - 设备号个数
例
cdev_add(&testcdev, devid, 1); //添加字符设备
将设备从内核注销
卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备
void cdev_del(struct cdev *p);
p - 要添加的cdev结构
例
cdev_del(&testcdev); //删除 cdev
上面的驱动框架,当使用 modprobe 加载驱动程序以后还需要使用命令mknod手动创建设备节点。
在驱动中实现自动创建设备节点的功能以后,使用 modprobe 加载驱动模块成功的话就会自动在/dev 目录下创建对应的设备文件。
自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添加自动创建设备节点相关代码。
创建一个class类
struct class *class_create(struct module *owner, const char *name);
删除一个class类
void class_destroy(struct class *cls); // cls要删除的类
创建设备
还需要在类下创建一个设备,使用 device_create 函数在类下面创建设备。
成功会在 /dev 目录下生成设备文件。
struct device *device_create(struct class *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...)
*class——设备类指针,
*parent——父设备指针,
devt——设备号,
*drvdata——额外数据,
*fmt——设备文件名
删除设备
卸载驱动的时候需要删除掉创建的设备
void device_destroy(struct class *class, dev_t devt);
class——设备所处的类
devt——设备号
/* newchrled设备结构体 */
struct newchrled_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
struct newchrled_dev newchrled; /* led设备 */
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &newchrled; /* 设置私有数据 */
return 0;
}
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
struct newchrled_dev *dev = (struct newchrled_dev *)filp->private_data;
return 0;
}
#define NEWCHR_CNT 1
#define NEWCHR_NAME "NEWCHR"
//内核缓存区
static char readbuf[100]; //读数据缓存
static char writebuf[100]; //写数据缓存
static char kerneldata[] = {"kernel data!"}; //测试数据
//硬件寄存器
#define GPIO_TEST_BASE (0x01234567) //宏定义寄存器映射地址
static void __iomem *GPIO_TEST; // __iomem 类型的指针,指向映射后的虚拟空间首地址
/* newchr设备结构体 */
struct newchr_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
struct newchrled_dev newchr; /* newchr设备 */
//打开设备
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
filp->private_data = &newchr; /* 设置私有数据 */
return 0;
}
// 从设备读取数据
static ssize_t chrdevbase_read(struct file *filp , char __user *buf , size_t cnt , loff_t *offt)
{
int retvalue = 0;
unsigned char databuf[1];
//读取私有数据
struct newchr_dev *dev = (struct newchr_dev *)filp->private_data;
// 读取硬件寄存器
#if 0
//读取寄存器状态
databuf[0] = readl(GPIO_TEST);
retvalue = copy_to_user(buf , databuf, cnt);
//读取内核内存
#else
//测试数据拷贝到读数据缓存中
memcpy(readbuf , kerneldata , sizeof(kerneldata));
//内核中数据(读缓存)拷贝到用户空间
retvalue = copy_to_user(buf , readbuf , cnt);
#endif
if(retvalue == 0) printk("kernel senddate ok!\n");
else printk("kernel senddate failed!\n");
return 0;
}
//向设备写数据
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt , loff_t *offt)
{
int retvalue = 0;
//读取私有数据
struct newchr_dev *dev = (struct newchr_dev *)filp->private_data;
//写硬件寄存器
#if 0
writel(buf[0],GPIO_TEST);
//写内核缓存
#else
//用户数据拷贝到内核空间(写缓存)
retvalue = copy_from_user(writebuf , buf ,cnt);
#endif
if(retvalue == 0) printk("kernel recevdate : %s\n",writebuf);
else printk("kernel recevdate failed!");
return 0;
}
//关闭/释放设备
static int chrdevbase_release(struct inode *inode , struct file *filp)
{
return 0;
}
//设备操作函数
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
/* 驱动入口函数 */
static int __init chrdevbase_init(void)
{
int retvalue = 0;
//寄存器物理映射,物理地址映射到虚拟地址指针
GPIO_TEST= ioremap(GPIO_TEST_BASE, 4);
//申请设备号
if(newchr.major) //静态申请
{
newchr.devid = MKDEV(newchr.major , 0);
register_chrdev_region(newchr.devid, NEWCHR_CNT,NEWCHR_NAME);
}else //动态申请
{
alloc_chrdev_region(&newchr.devid , 0 , NEWCHR_CNT , NEWCHR_NAME);
newchr.major = MAJOR(newchr.devid);
newchr.minor = MINOR(newchr.devid);
}
printk("newche major=%d,minor=%d\r\n",newchr.major , newchr.minor);
//字符串设备初始化、注册添加到内核
newchr.cdev.owner = THIS_MODULE;
cdev_init(&newchr.cdev , &newchr_fops);
cdev_add(&newchr.cdev , newchr.devid ,NEWCHR_LED_CNT);
//创建设备类
newchr.class = class_create(THIS_MODULE , NEWCHR_NAME);
if(IS_ERR(newchr.class))
{
return PTR_ERR(newchr.class);
}
//创建类的实例化设备 ,dev下面创建文件
newchr.device = device_create(newchr.class , NULL , newchr.devid ,NULL ,NEWCHR_NAME);
if(IS_ERR(newchr.device))
{
return PTR_ERR(newchr.device);
}
return 0;
}
/* 驱动出口函数 */
static void __exit chrdevbase_exit(void)
{
//解除寄存器映射
iounmap(GPIO_TEST);
//删除cdev字符串设备
cdev_del(&newchr.cdev);
//释放设备号
unregister_chrdev_region(newchr.devid , NEWCHR_CNT);
//具体设备注销
device_destroy(newchr.class, newchr.devid);
//类注销
class_destroy(newchr.class);
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
MODULE_LICENSE("GPI");//GPL模块许可证
MODULE_AUTHOR("songwei");//作者信息
Linux 内核提供了 pinctrl 子系统和 gpio 子系统用于 GPIO 驱动。
开发时只需要在设备树里面设置好某个 pin 的相关属性即可,其他的初始化工作均由 pinctrl 子系统来完成。
在设备树里面创建一个节点来描述 PIN 的配置信息。pinctrl 子系统一般在iomuxc子节点下,所有需要配置用户自己的pinctrl需要在该节点下添加。
例
iomuxc: iomuxc@020e0000 {
compatible = "fsl,imx6ul-iomuxc";
reg = <0x020e0000 0x4000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_hog_1>;
imx6ul-evk {
pinctrl_hog_1: hoggrp-1 {
fsl,pins = <
MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059
.......
>;
};
......
}
设备树是通过属性来保存信息的,因此需要添加一个属性,属性名字一定要为** fsl,pins **
&iomuxc {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_hog_1>;
imx6ul-evk {
......
pinctrl_led: ledgrp{
fsl,pins = <
MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10b0
>;
};
......
};
在根节点“/”下创建 LED 灯节点,节点名为“gpioled”
只需要关注gpioled设备节点下的pinctrl-names 和 pinctrl-0 两条语句, 这两句就是引用在 iomuxc 中配置的 pinctrl 节点
test {
pinctrl-names = "default", "wake up";
pinctrl-0 = <&pinctrl_test>;
pinctrl-1 = <&pinctrl_test_2>;
/* 其他节点内容 */
};
pinctrl-names = “default”, “wake up”; 设备的状态, 可以有多个状态, default 为状态 0, wake up 为状态 1。
pinctrl-0 = <&pinctrl_test>;第 0 个状态所对应的引脚配置, 也就是 default 状态对应的引脚在 pin controller 里面定义好的节点 pinctrl_test里面的管脚配置。
pinctrl-1 = <&pinctrl_test_2>;第 1 个状态所对应的引脚配置, 也就是 wake up 状态对应的引脚在 pin controller 里面定义好的节点 pinctrl_test_2里面的管脚配置。
例
gpioled {
#address-cells = <1>;
#size-cells = <1>;
compatible = "songwei-gpioled";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_led>;
led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
status = "okay";
};
当使用 pinctrl 子系统将引脚的复用设置为 GPIO,可以使用 GPIO 子系统来操作GPIO
通过 GPIO 子系统功能要实现:
在具体设备节点中添加GPIO信息
gpioled {
#address-cells = <1>;
#size-cells = <1>;
compatible = "songwei-gpioled";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_led>;
//gpio信息
led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
status = "okay";
};
gpio_request
gpio_request 函数用于申请一个 GPIO 管脚,在使用一个 GPIO 之前一定要使用 gpio_request进行申请。
int gpio_request(unsigned gpio, const char *label)
gpio_free
如果不使用某个 GPIO ,需要调用 gpio_free 函数进行释放。
void gpio_free(unsigned gpio); // gpio:要释放的 gpio 标号。
gpio_direction_input
设置某个 GPIO 为输入
int gpio_direction_input(unsigned gpio) //gpio:要设置为输入的 GPIO 标号。
gpio_direction_output
设置某个 GPIO 为输出,并且设置默认输出值。
int gpio_direction_output(unsigned gpio, int value)
gpio_get_value
获取某个 GPIO 的值(0 或 1)
#define gpio_get_value __gpio_get_value
int __gpio_get_value(unsigned gpio)
gpio_set_value
设置某个 GPIO 的值
#define gpio_set_value __gpio_set_value
void __gpio_set_value(unsigned gpio, int value)
of_gpio_named_count
获取设备树某个属性里面定义了几个GPIO 信息。
int of_gpio_named_count(struct device_node *np, const char *propname)
of_gpio_count
此函数统计的是gpios属性的 GPIO 数量,而 of_gpio_named_count 函数可以统计任意属性的GPIO 信息
int of_gpio_count(struct device_node *np)
of_get_named_gpio
获取 GPIO 编号,在Linux 内核中关于 GPIO 的 API 函数都要使用 GPIO 编号,此函数会将设备树中类似<&gpio5 7 GPIO_ACTIVE_LOW>的属性信息转换为对应的 GPIO 编号。
int of_get_named_gpio(struct device_node *np,
const char *propname,
int index)
int ret = 0;
/* 1 、获取设备节点:alphaled */
gpio_led.nd = of_find_node_by_path("/gpioled");
if(gpio_led.nd == NULL)
{
printk("songwei_led node can not found!\r\n");
return -EINVAL;
}else
{
printk("songwei_led node has been found!\r\n");
}
/* 2、 获取设备树中的 gpio 属性,得到 LED 所使用的 LED 编号 */
gpio_led.led_gpio = of_get_named_gpio(gpio_led.nd,"led-gpio",0);
if(gpio_led.led_gpio < 0)
{
printk("can't get led-gpio");
return -EINVAL;
}
printk("led-gpio num = %d\r\n", gpio_led.led_gpio);
/* 3、设置 GPIO1_IO03 为输出,并且输出高电平,默认关闭 LED 灯 */
ret = gpio_direction_output(gpio_led.led_gpio, 1);
if(ret < 0)
{
printk("can't set gpio!\r\n");
}
Linux 系统是个多任务操作系统,会存在多个任务同时访问同一片内存区域,这些任务可能会相互覆盖这段内存中的数据,造成内存数据混乱。我们需要对共享数据进行相应的保护处理。
并发:多个执行单元同时进行或多个执行单元微观串行执行,宏观并行执行。
竞争:并发的执行单元对共享资源(硬件资源和软件上的全局变量)的访问而导致的竞争状态。
临界资源: 多个进程访问的资源,共享数据段
临界区:多个进程访问的代码段
原子操作是指不能再进一步分割的操作。一般原子操作用于整形变量或者位操作。
Linux 内核定义了叫做 atomic_t 的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量,此结构体定义在 include/linux/types.h 文件中
typedef struct {
int counter;
} atomic_t;
如果要使用原子操作 API 函数,首先要先定义一个 atomic_t 的变量,
atomic_t a; //定义 a
atomic_t b = ATOMIC_INIT(0); //定义原子变量 b 并赋初值为 0
原子操作API函数
原子位操作 API 函数
例
/* gpioled设备结构体 */
struct gpioled_dev{
......
int led_gpio; /* led所使用的GPIO编号 */
atomic_t lock; /* 原子变量 */
};
struct gpioled_dev gpioled; /* led设备 */
static int led_open(struct inode *inode, struct file *filp)
{
/* 通过判断原子变量的值来检查LED有没有被别的应用使用 */
if (!atomic_dec_and_test(&gpioled.lock))
{
atomic_inc(&gpioled.lock); /* 小于0的话就加1,使其原子变量等于0 */
return -EBUSY; /* LED被使用,返回忙 */
}
......
}
static int led_release(struct inode *inode, struct file *filp)
{
struct gpioled_dev *dev = filp->private_data;
/* 关闭驱动文件的时候释放原子变量 */
atomic_inc(&dev->lock);
return 0;
}
static int __init led_init(void)
{
int ret = 0;
/* 初始化原子变量 */
atomic_set(&gpioled.lock, 1); /* 原子变量初始值为1 */
......
return 0;
}
static void __exit led_exit(void)
{
......
}
对于自旋锁而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线程 B 不会进入休眠状态或者说去做其他的处理,直到线程A释放自旋锁,线程B才可以访问共享资源。
由于等待自旋锁会浪费处理器时间,降低系统性能,所以自旋锁的持有时间不能太长,所以自旋锁适用于短时期的轻量级加锁。
例
/* gpioled设备结构体 */
struct gpioled_dev{
......
int dev_stats; /* 设备使用状态,0,设备未使用;>0,设备已经被使用 */
spinlock_t lock; /* 自旋锁 */
};
struct gpioled_dev gpioled; /* led设备 */
static int led_open(struct inode *inode, struct file *filp)
{
......
spin_lock_irqsave(&gpioled.lock, flags); /* 上锁 */
if (gpioled.dev_stats)
{ /* 如果设备被使用了 */
spin_unlock_irqrestore(&gpioled.lock, flags);/* 解锁 */
return -EBUSY;
}
gpioled.dev_stats++; /* 如果设备没有打开,那么就标记已经打开了 */
spin_unlock_irqrestore(&gpioled.lock, flags);/* 解锁 */
return 0;
}
static int led_release(struct inode *inode, struct file *filp)
{
......
/* 关闭驱动文件的时候将dev_stats减1 */
spin_lock_irqsave(&dev->lock, flags); /* 上锁 */
if (dev->dev_stats)
{
dev->dev_stats--;
}
spin_unlock_irqrestore(&dev->lock, flags);/* 解锁 */
return 0;
}
/*
* @description : 驱动入口函数
* @param : 无
* @return : 无
*/
static int __init led_init(void)
{
int ret = 0;
/* 初始化自旋锁 */
spin_lock_init(&gpioled.lock);
......
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit led_exit(void)
{
......
}
Linux 内核提供了信号量机制,信号量常常用于控制对共享资源的访问。它是一个计数器,常用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
例
/* gpioled设备结构体 */
struct gpioled_dev{
......
struct semaphore sem; /* 信号量 */
};
struct gpioled_dev gpioled; /* led设备 */
static int led_open(struct inode *inode, struct file *filp)
{
......
/* 获取信号量 */
if (down_interruptible(&gpioled.sem)) { /* 获取信号量,进入休眠状态的进程可以被信号打断 */
return -ERESTARTSYS;
}
#if 0
down(&gpioled.sem); /* 不能被信号打断 */
#endif
return 0;
}
static int led_release(struct inode *inode, struct file *filp)
{
......
up(&dev->sem); /* 释放信号量,信号量值加1 */
return 0;
}
/* 设备操作函数 */
static struct file_operations gpioled_fops = {
......
};
static int __init led_init(void)
{
int ret = 0;
/* 初始化信号量 */
sema_init(&gpioled.sem, 1);
......
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit led_exit(void)
{
......
}
Linux 提供了专门的互斥体mutex (等效信号量为1) 。互斥访问表示一次只有一个线程可以访问共享资源,不能递归申请互斥体(死锁)。在 Linux 驱动的时遇到需要互斥访问的地方一般使用 mutex。
/* gpioled设备结构体 */
struct gpioled_dev{
......
struct mutex lock; /* 互斥体 */
};
struct gpioled_dev gpioled; /* led设备 */
static int led_open(struct inode *inode, struct file *filp)
{
......
/* 获取互斥体,可以被信号打断 */
if (mutex_lock_interruptible(&gpioled.lock)) {
return -ERESTARTSYS;
}
#if 0
mutex_lock(&gpioled.lock); /* 不能被信号打断 */
#endif
return 0;
}
static int led_release(struct inode *inode, struct file *filp)
{
......
/* 释放互斥锁 */
mutex_unlock(&dev->lock);
return 0;
}
/* 设备操作函数 */
static struct file_operations gpioled_fops = {
......
};
static int __init led_init(void)
{
......
/* 初始化互斥体 */
mutex_init(&gpioled.lock);
......
}
static void __exit led_exit(void)
{
......
}
内核必须管理系统的运行时间以及当前的日期和时间。
硬件为内核提供了一个系统定时器用以计算流逝的时间, 系统定时器以某种频率自行触发时钟中断,该频率可以通过编程预定, 称节拍率。
当时钟中断发生时, 内核就通过一种特殊中断处理程序对其进行处理。 内核知道连续两次时钟中断的间隔时间。 这个间隔时间称为节拍(tick) 。内核就是靠这种已知的时钟中断来计算墙上时间和系统运行时间。
系统定时器频率是通过静态预处理定义的(HZ), 在系统启动时按照 Hz 对硬件进行设置。一般 ARM 体系结构的节拍率多数都等于 100。
在编译 Linux 内核的时候可以通过图形化界面设置系统节拍率, 按照如下路径打开配置界面:
-> Kernel Features
-> Timer frequency ( [=y])
Linux 内核会使用 CONFIG_HZ 来设置自己的系统时钟。
# undef HZ
# define HZ CONFIG_HZ
# define USER_HZ 100
# define CLOCKS_PER_SEC (USER_HZ)
宏 HZ 就是 CONFIG_HZ,HZ=100,后面编写 Linux驱动的时候会常常用到 HZ,因为 HZ 表示一秒的节拍数,也就是频率。
全局变量 jiffies 用来记录自系统启动以来产生的节拍的总数。 启动时, 内核将该变量初始化为 0, 每次时钟中断处理程序都会增加该变量的值。
因为一秒内时钟中断的次数等于 Hz, 所以 jiffes 一秒内增加的值为 Hz, 系统运行时间以秒为单位计算, 就等于time = jiffes/Hz( jiffes = seconds*HZ)
当 jiffies 变量的值超过它的最大存放范围后就会发生溢出, 对于 32 位无符号长整型, 最大取值为 2^32-1,在溢出前, 定时器节拍计数最大为 4294967295, 如果节拍数达到了最大值后还要继续增加的话, 它的值会回绕到 0。
如果 unkown 超过 known 的话, time_after 函数返回真, 否则返回假。
如果 unkown 没有超过 known的话 time_before 函数返回真, 否则返回假。
time_after_eq 函数在time_after上,多判断等于这个条件, time_before_eq 也类似
unsigned long timeout;
timeout = jiffies + (2 * HZ); /* 超时的时间点 */
....
/* 判断有没有超时 */
if(time_before(jiffies, timeout))
{
/* 超时未发生 */
}
else
{
/* 超时发生 */
}
为了方便开发, Linux 内核提供了几个 jiffies 和 ms、 us、 ns 之间的转换函数
定时 10ms
jiffies +msecs_to_jiffies(10)
定时 10us
jiffies +usecs_to_jiffies(10)
Linux 内核定时器采用系统时钟来实现,只需要提供超时时间(定时值)和定时处理函数即可。、
内核定时器并不是周期性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器。
Linux 内核使用 timer_list 结构体表示内核定时器
struct timer_list
{
struct list_head entry;
unsigned long expires; /* 定时器超时时间, 单位是节拍数 */
struct tvec_base *base;
void (*function)(unsigned long); /* 定时处理函数指针 */
unsigned long data; /* 要传递给 function 函数的参数 */
int slack;
};
......
#define CLOSE_CMD (_IO(0XEF, 0x1)) /* 关闭定时器 */
#define OPEN_CMD (_IO(0XEF, 0x2)) /* 打开定时器 */
#define SETPERIOD_CMD (_IO(0XEF, 0x3)) /* 设置定时器周期命令 */
......
/* timer设备结构体 */
struct timer_dev{
......
int timeperiod; /* 定时周期,单位为ms */
struct timer_list timer;/* 定义一个定时器*/
spinlock_t lock; /* 定义自旋锁 */
};
struct timer_dev timerdev; /* timer设备 */
static int timer_open(struct inode *inode, struct file *filp)
{
......
timerdev.timeperiod = 1000; /* 默认周期为1s */
ret = led_init(); /* 初始化LED IO */
if (ret < 0) {
return ret;
}
return 0;
}
//ioctl函数,和应用层ioctl对应,和open,close类似
static long timer_unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct timer_dev *dev = (struct timer_dev *)filp->private_data;
int timerperiod;
unsigned long flags;
switch (cmd) {
case CLOSE_CMD: /* 关闭定时器 */
del_timer_sync(&dev->timer);
break;
case OPEN_CMD: /* 打开定时器 */
spin_lock_irqsave(&dev->lock, flags);
timerperiod = dev->timeperiod;
spin_unlock_irqrestore(&dev->lock, flags);
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(timerperiod));
break;
case SETPERIOD_CMD: /* 设置定时器周期 */
spin_lock_irqsave(&dev->lock, flags);
dev->timeperiod = arg;
spin_unlock_irqrestore(&dev->lock, flags);
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(arg));
break;
default:
break;
}
return 0;
}
/* 设备操作函数 */
static struct file_operations timer_fops = {
.owner = THIS_MODULE,
.open = timer_open,
.unlocked_ioctl = timer_unlocked_ioctl,
};
/* 定时器回调函数 */
void timer_function(unsigned long arg)
{
struct timer_dev *dev = (struct timer_dev *)arg;
static int sta = 1;
int timerperiod;
unsigned long flags;
sta = !sta; /* 每次都取反,实现LED灯反转 */
gpio_set_value(dev->led_gpio, sta);
/* 重启定时器 */
spin_lock_irqsave(&dev->lock, flags);
timerperiod = dev->timeperiod;
spin_unlock_irqrestore(&dev->lock, flags);
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(dev->timeperiod));
}
/*
* @description : 驱动入口函数
* @param : 无
* @return : 无
*/
static int __init timer_init(void)
{
......
/* 6、初始化timer,设置定时器处理函数,还未设置周期,所有不会激活定时器 */
init_timer(&timerdev.timer);
timerdev.timer.function = timer_function;
timerdev.timer.data = (unsigned long)&timerdev;
return 0;
}
static void __exit timer_exit(void)
{
gpio_set_value(timerdev.led_gpio, 1); /* 卸载驱动的时候关闭LED */
del_timer_sync(&timerdev.timer); /* 删除timer */
#if 0
del_timer(&timerdev.tiemr);
#endif
......
}
/* 命令值 */
#define CLOSE_CMD (_IO(0XEF, 0x1)) /* 关闭定时器 */
#define OPEN_CMD (_IO(0XEF, 0x2)) /* 打开定时器 */
#define SETPERIOD_CMD (_IO(0XEF, 0x3)) /* 设置定时器周期命令 */
int main(int argc, char *argv[])
{
int fd, ret;
char *filename;
unsigned int cmd;
unsigned int arg;
unsigned char str[100];
if (argc != 2) {
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
fd = open(filename, O_RDWR);
if (fd < 0) {
printf("Can't open file %s\r\n", filename);
return -1;
}
while (1) {
printf("Input CMD:");
ret = scanf("%d", &cmd);
if (ret != 1) { /* 参数输入错误 */
gets(str); /* 防止卡死 */
}
if(cmd == 1) /* 关闭LED灯 */
cmd = CLOSE_CMD;
else if(cmd == 2) /* 打开LED灯 */
cmd = OPEN_CMD;
else if(cmd == 3) {
cmd = SETPERIOD_CMD; /* 设置周期值 */
printf("Input Timer Period:");
ret = scanf("%d", &arg);
if (ret != 1) { /* 参数输入错误 */
gets(str); /* 防止卡死 */
}
}
ioctl(fd, cmd, arg); /* 控制定时器的打开和关闭 */
}
close(fd);
}
ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。有些命令是实在找不到对应的操作函数, 拓展一些file_operations给出的接口中没有的自定义功能,则需要使用到ioctl()函数。一些没办法归类的函数就统一放在ioctl这个函数操作中,通过指定的命令来实现对应的操作。
需要规定一些命令码,这些命令码在应用程序和驱动程序中需要保持一致。应用程序只需向驱动程序下发一条指令码,用来通知它执行哪条命令。
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, (...)arg);
fd:文件描述符
request:命令码,应用程序通过下发命令码来控制驱动程序完成对应操作。
(…)arg:可变参数arg,一些情况下应用程序需要向驱动程序传参,参数就通过arg来传递。只能传递一个参数,但内核不会检查这个参数的类型。那么就有两种传参方式:只传一个整数,传递一个指针。
返回值:如果ioctl执行成功,它的返回值就是驱动程序中ioctl接口给的返回值,驱动程序可以通过返回值向用户程序传参。但驱动程序最好返回一个非负数,因为用户程序中的ioctl运行失败时一定会返回-1并设置全局变量errorno。
驱动程序的ioctl函数体中,实现了一个switch-case结构,每一个case对应一个命令码,case内部是驱动程序实现该命令的相关操作。
#include <linux/ioctl.h>
long (*unlocked_ioctl) (struct file * fp, unsigned int request, unsigned long args);
long (*compat_ioctl) (struct file * fp, unsigned int request, unsigned long args);
inode和fp用来确定被操作的设备
request就是用户程序下发的命令
args就是用户程序在必要时传递的参数
返回值:可以在函数体中随意定义返回值,这个返回值也会被直接返回到用户程序中。通常使用非负数表示正确的返回,而返回一个负数系统会判定为ioctl调用失败。
unlocked_ioctl在无大内核锁(BKL)的情况下调用。64位用户程序运行在64位的kernel,或32位的用户程序运行在32位的kernel上,都是调用unlocked_ioctl函数。
compat_ioctl是64位系统提供32位ioctl的兼容方法,也在无大内核锁的情况下调用。即如果是32位的用户程序调用64位的kernel,则会调用compat_ioctl。如果驱动程序没有实现compat_ioctl,则用户程序在执行ioctl时会返回错误Not a typewriter。
在字符设备驱动开发中,一般情况下只要实现 unlocked_ioctl 函数即可,因为在 vfs 层的代码是直接调用 unlocked_ioctl 函数
ioctl函数的第二个参数 cmd 为用户与驱动的协议,理论上可以为任意 int 型数据,,但是为了确保该协议的唯一性,ioctl 命令应该使用更科学严谨的方法赋值,在linux中,提供了一种 ioctl 命令的统一格式,将 32 位 int 型数据划分为四个位段,如下图所示:
dir(direction):ioctl 命令访问模式(数据传输方向),占据 2 bit,可以为 _IOC_NONE、_IOC_READ、_IOC_WRITE、_IOC_READ | _IOC_WRITE,分别指示了四种访问模式:无数据、读数据、写数据、读写数据
type(device type):设备类型,占据 8 bit,也称为 “幻数” 或者 “魔数”,可以为任意 char 型字符,例如‘a’、’b’、’c’ 等等,其主要作用是使 ioctl 命令有唯一的设备标识
nr(number):命令编号/序数,占据 8 bit,可以为任意 unsigned char 型数据,取值范围 0~255,如果定义了多个 ioctl 命令,通常从 0 开始编号递增
size:与体系结构相关,ARM下占14bit(_IOC_SIZEBITS),如果数据是int,内核给这个赋的值就是sizeof(int)。
在内核中,提供了宏接口以生成上述格式的 ioctl 命令:
#include/uapi/asm-generic/ioctl.h
#define _IOC(dir,type,nr,size) \
(((dir) << _IOC_DIRSHIFT) | \
((type) << _IOC_TYPESHIFT) | \
((nr) << _IOC_NRSHIFT) | \
((size) << _IOC_SIZESHIFT))
宏 _IOC() 衍生的接口来直接定义 ioctl 命令
#include/uapi/asm-generic/ioctl.h
/* used to create numbers */
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
_IO(type, nr):用来定义不带参数的ioctl命令
_IOR(type,nr,size):用来定义用户程序向驱动程序写参数的ioctl命令
_IOW(type,nr,size):用来定义用户程序从驱动程序读参数的ioctl命令
_IOWR(type,nr,size):用来定义带读写参数的驱动命令
内核还提供了反向解析 ioctl 命令的宏接口:
// include/uapi/asm-generic/ioctl.h
/* used to decode ioctl numbers */
#define _IOC_DIR(nr) (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
#define _IOC_TYPE(nr) (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
#define _IOC_NR(nr) (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
#define _IOC_SIZE(nr) (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)
_IOC_DIR(nr) :提取方向
_IOC_TYPE(nr) :提取幻数
_IOC_NR(nr) :提取序数
_IOC_SIZE(nr) :提取数据大小
中断是指 CPU 在执行程序的过程中, 出现了某些突发事件急待处理, CPU 必须暂停当前程序的执行,转去处理突发事件, 处理完毕后又返回原程序被中断的位置继续执行。
获取中断号函数
每个中断都有一个中断号,通过中断号即可区分不同的中断。在 Linux 内核中使用一个 int 变量表示中断号,
或者中断号, 中断信息一般写到了设备树里面, 可以通过 irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号。
unsigned int irq_of_parse_and_map(struct device_node *dev,int index)
使用 GPIO 的话,可以使用 gpio_to_irq 函数来获取 gpio 对应的中断号
int gpio_to_irq(unsigned int gpio)
申请中断函数
Linux 内核中要想使用某个中断是需要申请的, request_irq 函数用于申请中断,
request_irq函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用 request_irq 函数。
request_irq 函数会激活(使能)中断,所以不需要手动去使能中断。
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev)
irq:要申请中断的中断号
handler:中断处理函数,当中断发生会执行此中断处理函数
flags:中断标志,可以在文件 include/linux/interrupt.h 里面查看所有的中断标志
name:中断名字,设置以后可以在/proc/interrupts 文件中看到对应的中断名字
dev: 如果将 flags 设置为 IRQF_SHARED, dev 用来区分不同的中断,一般情况下将dev 设置为设备结构体, dev 会传递给中断处理函数 irq_handler_t 的第二个参数。
返回值: 0 中断申请成功,负值中断申请失败,如果返回-EBUSY 表示中断已经被申请了
中断标志
中断释放函数
中断使用完成以后就要通过 free_irq 函数释放掉相应的中断。 如果中断不是共享的,free_irq 会删除中断处理函数并且禁止中断。
void free_irq(unsigned int irq,void *dev)
中断处理函数
使用 request_irq 函数申请中断的时候需要设置中断处理函数
irqreturn_t (*irq_handler_t) (int, void *)
第一个参数:要中断处理函数要相应的中断号
第二个参数:一个指向 void 的指针,是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备,dev 也可以指向设备数据结构
返回值:irqreturn_t 类型
irqreturn_t 类型定义如下所示:
enum irqreturn {
IRQ_NONE = (0 << 0),
IRQ_HANDLED = (1 << 0),
IRQ_WAKE_THREAD = (1 << 1),
};
typedef enum irqreturn irqreturn_t;
irqreturn_t 是个枚举类型, 一共有三种返回值。 一般中断服务函数返回值使用如下形式
return IRQ_RETVAL(IRQ_HANDLED)
中断使能和禁止函数
enable_irq 和 disable_irq 用于使能和禁止指定的中断。
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)
void disable_irq_nosync(unsigned int irq)
irq:要禁止的中断号
disable_irq 函数要等到当前正在执行的中断处理函数执行完才返回, 因此需要保证不会产生新的中断, 并且确保所有已经开始执行的中断处理程序已经全部退出。
disable_irq_nosync 函数调用以后立即返回, 不会等待当前中断处理程序执行完毕。
使能/关闭全局中断
local_irq_enable()
local_irq_disable()
在打开或者关闭全局中断时,要考虑到别的任务的感受,要保存中断状态,处理完后要将中断状态恢复到以前的状态
local_irq_save(flags)
local_irq_restore(flags)
为保证系统实时性, 中断服务程序必须足够简短,如果都在中断服务程序中完成, 则会严重降低中断的实时性,
所以, linux 系统提出了一个概念: 把中断服务程序分为两部分: 上半部-下半部 。主要目的就是实现中断处理函数的快进快出
中断服务程序分为上半部(top half)和下半部(bottom half),上半部负责读中断源,并在清中断后登记中断下半部,而耗时的工作在下半部处理。
上半部只能通过中断处理程序实现, 下半部的实现目前有 3 种实现方式, 分别为: 软中断、 tasklet 、工作队列(work queues)
Linux 内核使用结构体 softirq_action 表示软中断
struct softirq_action
{
void (*action)(struct softirq_action *);
};
在 kernel/softirq.c 文件中一共定义了 10 个软中断
static struct softirq_action softirq_vec[NR_SOFTIRQS];
NR_SOFTIRQS 是枚举类型
enum
{
HI_SOFTIRQ=0, /* 高优先级软中断 */
TIMER_SOFTIRQ, /* 定时器软中断 */
NET_TX_SOFTIRQ, /* 网络数据发送软中断 */
NET_RX_SOFTIRQ, /* 网络数据接收软中断 */
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ, /* tasklet 软中断 */
SCHED_SOFTIRQ, /* 调度软中断 */
HRTIMER_SOFTIRQ, /* 高精度定时器软中断 */
RCU_SOFTIRQ, /* RCU 软中断 */
NR_SOFTIRQS
};
一共有 10 个软中断,数组 softirq_vec 有 10 个元素。 softirq_action 结构体中的 action 成员变量就是软中断的服务函数。
数组 softirq_vec 是个全局数组,因此所有的 CPU(对于 SMP 系统而言)都可以访问到,每个 CPU 都有自己的触发和控制机制,并且只执行自己所触发的软中断。但是各个 CPU 所执行的软中断服务函数确是相同的,都是数组 softirq_vec 中定义的 action 函数。
要使用软中断,必须先使用 open_softirq 函数注册对应的软中断处理函数
void open_softirq(int nr, void (*action)(struct softirq_action *))
//nr:要开启的软中断,也就是上面的10个软中断
//action:软中断对应的处理函数
注册好软中断以后需要通过 raise_softirq 函数触发
void raise_softirq(unsigned int nr)
//nr:要触发的软中断
tasklet是通过软中断实现的, 软中断用轮询的方式处理, 假如是最后一种中断, 则必须循环完所有的中断类型, 才能最终执行对应的处理函数。
为了提高中断处理数量,改进处理效率, 产生了 tasklet 机制。 tasklet 采用无差别的队列机制, 有中断时才执行, 免去了循环查表之苦。
tasklet 机制的优点:
调用 tasklet 以后, tasklet 绑定的函数并不会立马执行, 而是有中断以后, 经过一个很短的不确定时间在来执行。
Linux 内核使用 tasklet_struct 结构体来表示 tasklet
struct tasklet_struct
{
struct tasklet_struct *next; /* 下一个 tasklet */
unsigned long state; /* tasklet 状态 */
atomic_t count; /* 计数器, 记录对 tasklet 的引用数 */
void (*func)(unsigned long); /* tasklet 执行的函数 */
unsigned long data; /* 函数 func 的参数 */
};
如果要使用 tasklet, 必须先定义一个 tasklet, 然后使用 tasklet_init 函数初始化 tasklet
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long),
unsigned long data);
使用宏 DECLARE_TASKLET 一次性完成 tasklet 的定义和初始化, DECLARE_TASKLET 定义在include/linux/interrupt.h 文件中
DECLARE_TASKLET(name, func, data)
在上半部中断处理函数中调用 tasklet_schedule 函数就能使 tasklet 在合适的时间运行
void tasklet_schedule(struct tasklet_struct *t)
杀死 tasklet 使用 tasklet_kill 函数,这个函数会等待 tasklet 执行完毕, 然后再将它移除。 该函数可能会引起休眠, 所以要禁止在中断上下文中使用。
tasklet_kill(struct tasklet_struct *t)
tasklet模板
/* 定义 taselet */
struct tasklet_struct testtasklet;
/* tasklet 处理函数 */
void testtasklet_func(unsigned long data)
{
/* tasklet 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
......
/* 调度 tasklet */
tasklet_schedule(&testtasklet);
......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
......
/* 初始化 tasklet */
tasklet_init(&testtasklet, testtasklet_func, data);
/* 注册中断处理函数 */
request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
......
}
工作队列(workqueue) 是实现中断下文的机制之一, 是一种将工作推后执行的形式。
工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度
工作队列tasklet 机制有什么不同呢?
tasklet 也是实现中断下文的机制, 最主要的区别是 tasklet不能休眠, 而工作队列是可以休眠的。 所以, tasklet 可以用来处理比较耗时间的事情, 而工作队列可以处理非常复杂并且更耗时间的事情。因此如果要推后的工作可以睡眠就可以选择工作队列,否则的话就只能选择软中断或tasklet。
Linux 内核使用 work_struct 结构体表示一个工作
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func; /* 工作队列处理函数 */
};
这些工作组织成工作队列,工作队列使用 workqueue_struct 结构体表示
struct workqueue_struct {
struct list_head pwqs;
struct list_head list;
struct mutex mutex;
int work_color;
int flush_color;
atomic_t nr_pwqs_to_flush;
struct wq_flusher *first_flusher;
struct list_head flusher_queue;
struct list_head flusher_overflow;
struct list_head maydays;
struct worker *rescuer;
int nr_drainers;
int saved_max_active;
struct workqueue_attrs *unbound_attrs;
struct pool_workqueue *dfl_pwq;
char name[WQ_NAME_LEN];
struct rcu_head rcu;
unsigned int flags ____cacheline_aligned;
struct pool_workqueue __percpu *cpu_pwqs;
struct pool_workqueue __rcu *numa_pwq_tbl[];
};
Linux 内核使用工作者线程(worker thread)来处理工作队列中的各个工作, Linux 内核使用worker 结构体表示工作者线程
每个 worker 都有一个工作队列,工作者线程处理自己工作队列中的所有工作。在驱动开发中,只需要定义工作(work_struct)即可,关于工作队列和工作者线程基本不用去管。
struct worker {
union {
struct list_head entry;
struct hlist_node hentry;
};
struct work_struct *current_work;
work_func_t current_func;
struct pool_workqueue *current_pwq;
bool desc_valid;
struct list_head scheduled;
struct task_struct *task;
struct worker_pool *pool;
struct list_head node;
unsigned long last_active;
unsigned int flags;
int id;
char desc[WORKER_DESC_LEN];
struct workqueue_struct *rescue_wq;
};
初始化工作:INIT_WORK
#define INIT_WORK(_work, _func)
_work :要初始化的工作(work_struct)
_func :工作对应的处理函数
工作的创建和初始化:DECLARE_WORK
#define DECLARE_WORK(n, f)
n :定义的工作(work_struct)
f: 工作对应的处理函数
工作的调度函数: schedule_work
bool schedule_work(struct work_struct *work)
工作队列模块
/* 定义工作(work) */
struct work_struct testwork;
/* work 处理函数 */
void testwork_func_t(struct work_struct *work);
{
/* work 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
......
/* 调度 work */
schedule_work(&testwork);
......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
......
/* 初始化 work */
INIT_WORK(&testwork, testwork_func_t);
/* 注册中断处理函数 */
request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
......
}
如果一个设备需要用到中断功能,需要在设备树中配置好中断属性信息, 因为设备树是用来描述硬件信息的, 然后 Linux 内核通过设备树配置的中断属性来配置中断功能。
例:imx6ull中断控制器节点
intc:interrupt-controller @00a01000
{
compatible = "arm,cortex-a7-gic";
#interrupt - cells = < 3>;
interrupt - controller;
reg = <0x00a01000 0x1000>,
<0x00a02000 0x100>;
};
gpio5 : gpio @020ac000{
compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
reg = <0x020ac000 0x4000>;
interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};
①#interrupt-cells:此中断控制器下设备的 cells 大小,一般会使用 interrupts 属性描述中断信息, #interrupt-cells 描述了 interrupts 属性的 cells 大小, 一条信息有几个cells。 每个 cells 都是 32 位整型值, 对于 ARM 处理的 GIC 来说, 一共有 3 个 cells。
②interrupt-controller 节点为空, 表示当前节点是中断控制器。
③interrupts :描述中断源信息, 对于 gpio5 来说一共有两条信息: 中断类型是 SPI, 触发电平是 IRQ_TYPE_LEVEL_HIGH, 中断源 一个是74, 一个是 75
④interrupt-parent,指定父中断,也就是中断控制器。
这里的 IO 指的是 Input/Output(输入/输出):是应用程序对驱动设备的输入/输出操作。
阻塞IO操作是指在执行设备操作时, 若不能获得资源, 则挂起进程直到满足可操作的条件后再进行操作。
被挂起的进程进入睡眠状态, 被从调度器的运行队列移走, 直到等待的条件被满足该进程会唤醒。
在阻塞访问时, 不能获取资源的进程将进入休眠, 它将 CPU 资源让给其他进程。 因为阻塞的进程会进入休眠状态, 所以必须确保有一个地方能够唤醒休眠的进程。唤醒进程最大可能发生在中断函数里面, 因为在硬件资源获得的同时往往伴随着一个中断。Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作。
如图,应用程序调用 read 函数从设备中读取数据,当设备不可用或数据未准备好的时候就会进入到休眠态。等设备可用的时候就会从休眠态唤醒,然后从设备中读取数据返回给应用程序。
int fd;
int data = 0;
fd = open("/dev/xxx_dev", O_RDWR); /* 阻塞方式打开,默认是阻塞 */
ret = read(fd, &data, sizeof(data)); /* 读取数据 */
非阻塞IO操作是在不能进行设备操作时, 并不挂起, 要么放弃, 要么不停地查询, 直至可以进行操作为止。非阻塞的进程则不断尝试, 直到可以进行 I/O。
应用程序使用非阻塞访问方式从设备读取数据,当设备不可用或数据未准备好的时候会立即向内核返回一个错误码,表示数据读取失败。应用程序会再次重新读取数据,一直往复循环,直到数据读取成功。
若用户以非阻塞的方式访问设备文件, 则当设备资源不可获取时, 设备驱动的 xxx_read() 、 xxx_write( ) 等操作应立即返回, read( ) 、 write( ) 等系统调用也随即被返回, 应用程序收到-EAGAIN 返回值。
int fd;
int data = 0;
fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式打开 */
ret = read(fd, &data, sizeof(data)); /* 读取数据 */
fd = open(filename, O_RDWR);
if (fd < 0)
{
printf("Can't open file %s\r\n", filename);
return -1;
}
ret = read(fd, &data, sizeof(data));
if (ret < 0)
{
printf("don't know how to read\n");
/* 数据读取错误或者无效 */
} else
{
/* 数据读取正确 */
if (data) /* 读取到数据 */
printf("key value = %#X\r\n", data);
}
在 Linux 驱动程序中,使用等待队列( Wait Queue) 来实现阻塞进程的唤醒。
Linux 内核的等待队列是以双循环链表为基础数据结构, 与进程调度机制紧密结合, 能够用于实现核心的异步事件通知机制。
它有两种数据结构: 等待队列头(wait_queue_head_t) 和等待队列项(wait_queue_t)。等待队列头和等待队列项中都包含一个 list_head 类型的域作为连接件。 它通过一个双链表 把等待 task的头和等待的进程列表链接起来。
等待队列头
如果要在驱动中使用等待队列,必须创建并初始化一个等待队列头。等待队列头使用结构体 wait_queue_head_t 来表示
struct __wait_queue_head {
spinlock_t lock; //自旋锁
struct list_head task_list; //链表头
};
typedef struct __wait_queue_head wait_queue_head_t;
定义好等待队列头以后需要初始化, 使用 init_waitqueue_head 函数初始化等待队列头。
void init_waitqueue_head(wait_queue_head_t *q)
//q:要初始化的等待队列头
等待队列项
每个访问设备的进程都是一个队列项, 当设备不可用时就要将这些进程对应的等待队列项添加到等待队列里面。结构体 wait_queue_t 表示等待队列项
struct __wait_queue {
unsigned int flags;
void *private;
wait_queue_func_t func;
struct list_head task_list;
};
typedef struct __wait_queue wait_queue_t;
使用宏 DECLARE_WAITQUEUE 定义并初始化一个等待队列项
DECLARE_WAITQUEUE(name, tsk)
//name:等待队列项的名字
//tsk:等待队列项属于哪个任务(进程), 一般设置为 current
在内核中 current 相当于全局变量 , 表 示 当 前 进 程 。所以DECLARE_WAITQUEUE是给当前正在运行的进程创建并初始化了一个等待队列项。
添加/删除队列
当设备不可访问的时就需要将进程对应的等待队列项 添加到前面创建的等待队列头中, 只有添加到等待队列头中以后进程才能进入休眠态。
void add_wait_queue(wait_queue_head_t *q,wait_queue_t *wait)
//q: 等待队列项要加入的等待队列头
//wait:要加入的等待队列项
当设备可以访问后再将进程对应的等待队列项从等待队列头中移除即可。
void remove_wait_queue(wait_queue_head_t *q,wait_queue_t *wait)
//q: 要删除的等待队列项所处的等待队列头
//wait:要删除的等待队列项
唤醒等待睡眠进程
当设备可以使用的时就要唤醒进入休眠态的进程, 唤醒可以使用如下两个函数
void wake_up(wait_queue_head_t *q) //功能: 唤醒所有休眠进程
void wake_up_interruptible(wait_queue_head_t *q)//功能: 唤醒可中断的休眠进程
//q :要唤醒的等待队列头
这两个函数会将这个等待队列头中的所有进程都唤醒。
wake_up 函 数 可 以 唤 醒 处 于TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状 态 的 进 程 ,
wake_up_interruptible 函数只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程
等待事件
除了主动唤醒以外, 也可以设置等待队列等待某个事件, 当这个事件满足以后就自动唤醒等待队列中的进程。
调用的时要确认 condition 值是真还是假, 如果调用 condition 为真, 则不会休眠。
wait_event(wq, condition)
//不可中断的阻塞等待, 让调用进程进入不可中断的睡眠状态,
//在等待队列里面睡眠直到 condition变成真, 被内核唤醒。
wait_event_interruptible(queue,condition)
//可中断的阻塞等待, 让调用进程进入可中断的睡眠状态,
//直到 condition 变成真被内核唤醒或被信号打断唤醒。
wait_event_timeout(queue,condition,timeout)
//功能和 wait_event 类似,但是此函数可以添加超时时间,以 jiffies 为单位。
//如果所给的睡眠时间为负数则立即返回 。
//如果在睡眠期间被唤醒,且condition 为真则返回剩余的睡眠时间,
//否则继续睡眠直到到达或超过给定的睡眠时间,然后返回 0 。
wait_event_interruptible_timeout(queue,condition,timeout)
//与 wait_event_timeout 函数类似, 如果在睡眠期间被信号打断则返回 ERESTARTSYS 错误码。
使用等待队列实现阻塞访问重点注意两点:
使用模板
/* imx6uirq 设备结构体 */
struct imx6uirq_dev
{
......
wait_queue_head_t r_wait; /* 读等待队列头 */
};
struct imx6uirq_dev imx6uirq; /* irq 设备 */
static irqreturn_t key0_handler(int irq, void *dev_id)
{
struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;
tasklet_schedule(&dev->irqkeydesc[0].testtasklet);
printk("tasklet ok\n");
return IRQ_RETVAL(IRQ_HANDLED);
}
static void testtasklet_func(unsigned long data)
{
......
if(atomic_read(&dev->releasekey)) /* 完成一次按键过程 */
{
/* wake_up(&dev->r_wait); */
wake_up_interruptible(&dev->r_wait);
}
}
static int keyio_init(void)
{
......
/* 初始化等待队列头 */
init_waitqueue_head(&imx6uirq.r_wait);
return 0;
}
static ssize_t imx6uirq_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt)
{
......
#if 0
/* 加入等待队列,等待被唤醒, 也就是有按键按下 */
ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey));
if (ret) {
goto wait_error;
}
#else
DECLARE_WAITQUEUE(wait, current); /* 定义一个等待队列 */
if(atomic_read(&dev->releasekey) == 0) /* 没有按键按下 */
{
add_wait_queue(&dev->r_wait, &wait); /* 将等待队列添加到等待队列头 */
__set_current_state(TASK_INTERRUPTIBLE);/* 设置任务状态 */
schedule(); /* 进行一次任务切换 */
if(signal_pending(current)) /* 判断是否为信号引起的唤醒 */
{
ret = -ERESTARTSYS;
goto wait_error;
}
__set_current_state(TASK_RUNNING); /* 将当前任务设置为运行状态 */
remove_wait_queue(&dev->r_wait, &wait); /* 将对应的队列项从等待队列头删除 */
}
#endif
......
wait_error:
set_current_state(TASK_RUNNING); /* 设置任务为运行态 */
remove_wait_queue(&dev->r_wait, &wait); /* 将等待队列移除 */
return ret;
}
static struct file_operations imx6uirq_fops =
{
.owner = THIS_MODULE,
.open = imx6uirq_open,
.read = imx6uirq_read,
};
如果用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式,即轮询。 poll、 epoll 和 select 可以用于处理轮询。
应用程序通过 select、 epoll 或 poll 函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调用 select、 epoll 或 poll 函数的时,设备驱动程序中的 poll 函数就会执行,因此需要在设备驱动程序中编写 poll 函数。
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout)
从一个设备文件中读取数据,要定义一个 fd_set 变量,这个变量要传递给参数 readfds。当定义好一个 fd_set 变量以后可以使用如下所示几个宏进行操作:
void FD_ZERO(fd_set *set)
void FD_SET(int fd, fd_set *set)
void FD_CLR(int fd, fd_set *set)
int FD_ISSET(int fd, fd_set *set)
超时时间使用结构体 timeval 表示, 当 timeout 为 NULL 的时候就表示无限期的等待。
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 微妙 */
};
应用层select函数非阻塞访问模块
void main(void)
{
int ret, fd; /* 要监视的文件描述符 */
fd_set readfds; /* 读操作文件描述符集 */
struct timeval timeout; /* 超时结构体 */
fd = open("dev_xxx", O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */
FD_ZERO(&readfds); /* 清除 readfds */
FD_SET(fd, &readfds); /* 将 fd 添加到 readfds 里面 */
/* 构造超时时间 */
timeout.tv_sec = 0;
timeout.tv_usec = 500000; /* 500ms */
ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
switch (ret)
{
case 0: /* 超时 */
printf("timeout!\r\n");
break;
case -1: /* 错误 */
printf("error!\r\n");
break;
default: /* 可以读取数据 */
if(FD_ISSET(fd, &readfds)) /* 判断fd 文件描述符是否在readfds里面 */
{
/* 使用 read 函数读取数据 */
}
break;
}
}
在单个线程中, select 函数能够监视的文件描述符数量有最大的限制,一般为 1024。可以修改内核将监视的文件描述符数量改大。这时可以使用 poll 函数,poll 函数本质上和 select 没有太大的差别,但是 poll 函数没有最大文件描述符限制。
int poll(struct pollfd *fds,
nfds_t nfds,
int timeout)
pollfd 结构体
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 请求的事件 */
short revents; /* 返回的事件 */
};
fd 是要监视的文件描述符,如果 fd 无效,则events 监视事件也无效,并且 revents返回 0。
events 是要监视的事件,可监视的事件类型如下:
revents 是返回的事件, 由 Linux 内核设置具体的返回事件。
应用层 poll 函数非阻塞访问模块
void main(void)
{
int ret;
int fd; /* 要监视的文件描述符 */
struct pollfd fds;
fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */
/* 构造结构体 */
fds.fd = fd;
fds.events = POLLIN; /* 监视数据是否可以读取 */
ret = poll(&fds, 1, 500); /* 轮询文件是否可操作,超时 500ms */
if (ret) /* 数据有效 */
{
......
/* 读取数据 */
......
}
else if (ret == 0) /* 超时 */
{
......
}
else if (ret < 0) /* 错误 */
{
......
}
}
selcet 和 poll 函数都会随着所监听的 fd 数量的增加,出现效率低下的问题,而且poll 函数每次必须遍历所有的描述符来检查就绪的描述符,这个过程很浪费时间。
epoll 就是为处理大并发而准备的,一般常常在网络编程中使用 epoll 函数。
应用程序需要先使用 epoll_create 函数创建一个 epoll 句柄
int epoll_create(int size)
epoll 句柄创建成功以后使用 epoll_ctl 函数向其中添加要监视的文件描述符以及监视的事件
int epoll_ctl(int epfd,
int op,
int fd,
struct epoll_event *event)
监视的事件类型为 epoll_event 结构体类型指针
struct epoll_event {
uint32_t events; /* epoll 事件 */
epoll_data_t data; /* 用户数据 */
};
最后通过 epoll_wait 函数来等待事件的发生
int epoll_wait(int epfd,
struct epoll_event *events,
int maxevents,
int timeout)
epoll 更多的是用在大规模的并发服务器上,因为在这种场合下 select 和 poll 并不适合。当设计到的文件描述符比较少的时候就适合用 selcet 和 poll。
当应用程序调用 select 或 poll 函数来对驱动程序进行非阻塞访问,驱动程序file_operations 操作集中的 poll 函数就会执行。
unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait)
需要在驱动程序的 poll 函数中调用 poll_wait 函数, poll_wait 函数不会引起阻塞,只是将应用程序添加到 poll_table 中
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
驱动层poll函数模板(和应用层select、poll对应)
/* imx6uirq 设备结构体 */
struct imx6uirq_dev
{
......
wait_queue_head_t r_wait; /* 读等待队列头 */
};
struct imx6uirq_dev imx6uirq; /* irq 设备 */
static irqreturn_t key0_handler(int irq, void *dev_id)
{
struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;
tasklet_schedule(&dev->irqkeydesc[0].testtasklet);
printk("tasklet ok\n");
return IRQ_RETVAL(IRQ_HANDLED);
}
static void testtasklet_func(unsigned long data)
{
......
if(atomic_read(&dev->releasekey)) /* 完成一次按键过程 */
{
/* wake_up(&dev->r_wait); */
wake_up_interruptible(&dev->r_wait);
}
}
static int keyio_init(void)
{
......
/* 初始化等待队列头 */
init_waitqueue_head(&imx6uirq.r_wait);
return 0;
}
static ssize_t imx6uirq_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt)
{
int ret = 0;
unsigned char keyvalue = 0;
unsigned char releasekey = 0;
struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
if (filp->f_flags & O_NONBLOCK) /* 非阻塞访问 */
{
if(atomic_read(&dev->releasekey) == 0) /* 没有按键按下,返回-EAGAIN */
return -EAGAIN;
}
else /* 阻塞访问 */
{
/* 加入等待队列,等待被唤醒,也就是有按键按下 */
ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey));
if (ret)
{
goto wait_error;
}
}
......
wait_error:
return ret;
......
}
unsigned int imx6uirq_poll(struct file *filp, struct poll_table_struct *wait)
{
unsigned int mask = 0;
struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
poll_wait(filp, &dev->r_wait, wait); /* 将等待队列头添加到poll_table中 */
if(atomic_read(&dev->releasekey)) { /* 按键按下 */
mask = POLLIN | POLLRDNORM; /* 返回PLLIN */
}
return mask;
}
static struct file_operations imx6uirq_fops =
{
.owner = THIS_MODULE,
.open = imx6uirq_open,
.read = imx6uirq_read,
.poll = imx6uirq_poll,
};
阻塞IO和非阻塞IO都需要应用程序主动去查询设备的使用情况。Linux 提供了异步通知机制,驱动程序能主动向应用程序发出通知。
信号是在软件层次上对中断的一种模拟,驱动可以通过主动向应用程序发送信号的方式通知可以访问,应用程序获取到信号后就可以从驱动设备中读取或者写入数据。
注册信号处理函数
应用程序根据驱动程序所使用的信号来设置信号的处理函数,应用程序使用 signal 函数来设置信号的处理函数。
sighandler_t signal(int signum, sighandler_t handler)
信号中断处理函数
typedef void (*sighandler_t)(int)
在处理函数执行相应的操作即可。
将本应用程序的进程号告诉给内核
fcntl系统调用可以用来对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
使用 fcntl(fd, F_SETOWN, getpid())将本应用程序的进程号告诉给内核。
开启异步通知
主要是通过 fcntl 函数设置进程状态为 FASYNC,经过这一步,驱动程序中的 fasync 函数就会执行。
flags = fcntl(fd, F_GETFL); /* 获取当前的进程状态 */
fcntl(fd, F_SETFL, flags | FASYNC); /* 开启当前进程异步通知功能 */
应用程序模板
static int fd = 0; /* 文件描述符 */
static void sigio_signal_func(int signum)
{
int err = 0;
unsigned int keyvalue = 0;
err = read(fd, &keyvalue, sizeof(keyvalue));
if(err < 0) {
/* 读取错误 */
} else {
printf("sigio signal! key value=%d\r\n", keyvalue);
}
}
int main(int argc, char *argv[])
{
int flags = 0;
char *filename;
if (argc != 2) {
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
fd = open(filename, O_RDWR);
if (fd < 0) {
printf("Can't open file %s\r\n", filename);
return -1;
}
/* 设置信号SIGIO的处理函数 */
signal(SIGIO, sigio_signal_func);
fcntl(fd, F_SETOWN, getpid()); /* 设置当前进程接收SIGIO信号 */
flags = fcntl(fd, F_GETFL); /* 获取当前的进程状态 */
fcntl(fd, F_SETFL, flags | FASYNC); /* 设置进程启用异步通知功能 */
while(1) {
sleep(2);
}
close(fd);
return 0;
}
内核要使用异步通知需要在驱动程序中定义一个 fasync_struct 结构体的指针变量
一般将 fasync_struct 结构体指针变量定义到设备结构体中即可。
struct fasync_struct {
spinlock_t fa_lock;
int magic;
int fa_fd;
struct fasync_struct *fa_next;
struct file *fa_file;
struct rcu_head fa_rcu;
};
struct fasync_struct *async_queue; /* 异步相关结构体 */
然后需要在设备驱动中实现 file_operations 操作集中的 fasync 函数。
int (*fasync) (int fd, struct file *filp, int on)
fasync 函数里面一般通过调用 fasync_helper 函数来初始化前面定义的 fasync_struct 结构体指针
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
fasync_helper 函数的前三个参数就是 fasync 函数的三个参数,第四个参数就是要初始化的 fasync_struct 结构体指针变量。
当应用程序通过fcntl(fd, F_SETFL, flags | FASYNC)改变fasync 标记的时,驱动程序file_operations 操作集中的 fasync 函数就会执行。
当设备可以访问的时候,驱动程序需要向应用程序发出信号,相当于产生中断。 kill_fasync函数负责发送指定的信号
void kill_fasync(struct fasync_struct **fp, int sig, int band)
最后,在关闭驱动文件的时候需要在 file_operations 操作集中的 release 函数中释放 fasync_struct,fasync_struct 的释放函数为 fasync_helper。
xxx_fasync(-1, filp, 0); /* 删除异步通知 */
xxx_fasync 函数就是file_operations 操作集中的 fasync 函数。
驱动程序模板
/* imx6uirq设备结构体 */
struct imx6uirq_dev{
......
struct fasync_struct *async_queue; /* 异步相关结构体 */
};
struct imx6uirq_dev imx6uirq; /* irq设备 */
void timer_function(unsigned long arg)
{
......
if(atomic_read(&dev->releasekey)) { /* 一次完整的按键过程 */
if(dev->async_queue)
kill_fasync(&dev->async_queue, SIGIO, POLL_IN); /* 释放SIGIO信号 */
}
......
}
/*
* @description : fasync函数,用于处理异步通知
* @param - fd : 文件描述符
* @param - filp : 要打开的设备文件(文件描述符)
* @param - on : 模式
* @return : 负数表示函数执行失败
*/
static int imx6uirq_fasync(int fd, struct file *filp, int on)
{
struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
return fasync_helper(fd, filp, on, &dev->async_queue); /* 初始化fasync_struct 结构体指针*/
}
static int imx6uirq_release(struct inode *inode, struct file *filp)
{
return imx6uirq_fasync(-1, filp, 0); /* 删除异步通知 */
}
/* 设备操作函数 */
static struct file_operations imx6uirq_fops = {
......
.poll = imx6uirq_poll,
.fasync = imx6uirq_fasync,
.release = imx6uirq_release,
};
Linux 内核完全由 C 语言和汇编语言写成, 但是却频繁用到了面向对象的设计思想。
在设备驱动方面,为同类的设备设计了一个框架, 框架中的核心层则实现了该设备通用的一些功能。同样的, 如果具体的设备不想使用核心层的函数, 它可以重载之。
return_type core_funca(xxx_device * bottom_dev, param1_type param1, param1_type param2)
{
if(bottom_dev->funca)
return bottom_dev->funca(param1, param2);
/* 核心层通用的 funca 代码 */
bottom_dev->funca_ops1(); /*通用的步骤代码 A */
...
bottom_dev->funca_ops2(); /*通用的步骤代码 B */
...
bottom_dev->funca_ops3(); /*通用的步骤代码 C */
}
上述 core_funca 的实现中, 会检查底层设备是否重载了 funca(), 如果重载了, 就调用底层的代码, 否则直接使用通用层的。 这样做的好处是, 核心层的代码可以处理绝大多数该类设备的funca()对应的功能,只有少数特殊设备需要重新实现 funca()。
将设备信息从设备驱动中剥离开来,驱动使用标准方法去获取到设备信息 (比如从设备树中获取到设备信息),根据获取到的设备信息来初始化设备。
驱动只负责驱动,设备只负责设备,总线法将两者进行匹配。
这就是 Linux 中的总线(bus)、驱动(driver)和设备(device)模型,即驱动分离。
当向系统注册一个驱动的时,总线会在设备中查找与之匹配的设备,如果有,就将两者联系起来。同样的,当向系统中注册一个设备的时候,总线就会在驱动中查找与之匹配的驱动,如果有,也联系起来。 Linux 内核中大量的驱动程序都采用总线、驱动和设备模式。
在 Linux 2.6 以后的设备驱动模型中, 需关心总线、 设备和驱动这 3 个实体, 总线将设备和驱动绑定。
Linux 设备和驱动通常都需要挂接在一种总线上, 对于本身依附于 PCI、 USB、 I2C、 SPI 等的设备而言, 这自然不是问题。但是在嵌入式系统里面, 在 SoC 系统中集成的独立外设控制器、 挂接在 SoC内存空间的外设等却不依附于此类总线。
基于这一背景, Linux 发明了一种虚拟的总线, 称为 platform 总线, 相应的设备称为 platform_device, 驱动称为 platform_driver。 平台总线模型就是把原来的驱动C文件给分成了俩个 C 文件,一个是 device.c(描述硬件信息,设备树可替代), 一个是 driver.c(驱动信息) 。
在 Linux 内核中, 用platform_driver结构体表示platform驱动,platform_driver 结构体定义指定名称的平台设备驱动注册函数和平台设备驱动注销函数
struct platform_driver {
int (*probe)(struct platform_device *);
int (*remove)(struct platform_device *);
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*resume)(struct platform_device *);
struct device_driver driver;
const struct platform_device_id *id_table;
bool prevent_deferred_probe;
};
platform_device_id 结构体内容如下:
struct platform_device_id {
char name[PLATFORM_NAME_SIZE];
kernel_ulong_t driver_data;
};
device_driver 结构体定义内容如下:
struct device_driver {
const char *name;
struct bus_type *bus;
struct module *owner;
const char *mod_name; /* used for built-in modules */
bool suppress_bind_attrs; /* disables bind/unbind via sysfs */
const struct of_device_id *of_match_table; //设备树的驱动匹配表
const struct acpi_device_id *acpi_match_table;
int (*probe) (struct device *dev);
int (*remove) (struct device *dev);
void (*shutdown) (struct device *dev);
int (*suspend) (struct device *dev, pm_message_t state);
int (*resume) (struct device *dev);
const struct attribute_group **groups;
const struct dev_pm_ops *pm;
struct driver_private *p;
};
of_match_table表就是采用设备树时驱动使用的匹配表,也是数组,每个匹配项都为 of_device_id 结构体类型
struct of_device_id {
char name[32];
char type[32];
char compatible[128];
const void *data;
};
compatible: 在支持设备树的内核中, 就是通过设备节点的compatible属性值和of_match_table中每个项目的 compatible 成员变量进行比较, 如果有相等的就表示设备和此驱动匹配成功。
驱动和设备匹配成功后,驱动会从设备里面获得硬件资源, 匹配成功了后, driver.c 要从 device.c(或者是设备树) 中获得硬件资源, 那么 driver.c 就是在 probe 函数中获得的。
在 platform 平台下用platform_device结构体表示platform设备, 如果内核支持设备树的话就不用使用 platform_device 来描述设备, 使用设备树去描述platform_device即可。
struct platform_device {
const char *name;
int id;
bool id_auto;
struct device dev;
u32 num_resources;
struct resource *resource;
const struct platform_device_id *id_entry;
char *driver_override; /* Driver name to force a match */
/* MFD cell pointer */
struct mfd_cell *mfd_cell;
/* arch specific additions */
struct pdev_archdata archdata;
};
Linux 内核使用 resource结构体表示资源
struct resource {
resource_size_t start;
resource_size_t end;
const char *name;
unsigned long flags;
struct resource *parent, *sibling, *child;
};
start 和 end 分别表示资源的起始和终止信息,对于内存类的资源,表示内存起始和终止地址, name 表示资源名字, flags 表示资源类型
使用 platform_device_register 函数将设备信息注册到 Linux 内核中
int platform_device_register(struct platform_device *pdev)
如果不再使用 platform可以通过 platform_device_unregister 函数注销掉相应的 platform设备
void platform_device_unregister(struct platform_device *pdev)
platform设备和platform驱动,相当于把设备和驱动分离了, 需要 platform 总线进行配, platform 设备和 platform 驱动进行内核注册时, 都是注册到总线上。
在 Linux 内核中使用 bus_type 结构体表示总线
struct bus_type {
const char *name; /* 总线名字 */
const char *dev_name;
struct device *dev_root;
struct device_attribute *dev_attrs;
const struct attribute_group **bus_groups; /* 总线属性 */
const struct attribute_group **dev_groups; /* 设备属性 */
const struct attribute_group **drv_groups; /* 驱动属性 */
int (*match)(struct device *dev, struct device_driver *drv);
int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
int (*probe)(struct device *dev);
int (*remove)(struct device *dev);
void (*shutdown)(struct device *dev);
int (*online)(struct device *dev);
int (*offline)(struct device *dev);
int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev);
const struct dev_pm_ops *pm;
const struct iommu_ops *iommu_ops;
struct subsys_private *p;
struct lock_class_key lock_key;
};
match 函数:完成设备和驱动之间匹配的,总线使用 match 函数来根据注册的设备来查找对应的驱动,或者根据注册的驱动来查找相应的设备,因此每一条总线都必须实现此函数。
match 函数有两个参数: dev 和 drv,这两个参数分别为 device 和 device_driver 类型,即设备和驱动。
platform 总线是 bus_type 的一个具体实例
struct bus_type platform_bus_type = {
.name = "platform",
.dev_groups = platform_dev_groups,
.match = platform_match,
.uevent = platform_uevent,
.pm = &platform_dev_pm_ops,
};
platform_match 匹配函数, 用来匹配注册到 platform 总线的设备和驱动。
查看platform_match函数,如何匹配驱动和设备的
static int platform_match(struct device *dev,struct device_driver *drv)
{
struct platform_device *pdev = to_platform_device(dev);
struct platform_driver *pdrv = to_platform_driver(drv);
/*When driver_override is set,only bind to the matching driver*/
if (pdev->driver_override)
return !strcmp(pdev->driver_override, drv->name);
/* Attempt an OF style match first */
if (of_driver_match_device(dev, drv))
return 1;
/* Then try ACPI style match */
if (acpi_driver_match_device(dev, drv))
return 1;
/* Then try to match against the id table */
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;
/* fall-back to driver name match */
return (strcmp(pdev->name, drv->name) == 0);
}
驱动和设备的匹配有四种方法。
OF 类型的匹配
设备树采用的匹配方式,of_driver_match_device 函数定义在文件 include/linux/of_device.h 中。
device_driver 结构体(设备驱动)中有个名为of_match_table的成员变量,此成员变量保存着驱动的compatible匹配表,
设备树中的每个设备节点的 compatible 属性会和 of_match_table 表中的所有成员比较,查看是否有相同的条目,如果有的话就表示设备和此驱动匹配,设备和驱动匹配成功以后 probe 函数就会执行。
ACPI 匹配方式
id_table 匹配
每个 platform_driver 结构体(设备驱动)有一个 id_table成员变量,保存了很多 id 信息。这些 id 信息存放着这个 platformd 驱动所支持的驱动类型
名字匹配
如果第三种匹配方式的 id_table 不存在的话就直接比较驱动和设备的 name 字段,如果相等的话就匹配成功。
对于支持设备树的 Linux 版本号,一般设备驱动为了兼容性都支持设备树和无设备树两种匹配方式。即第一种匹配方式一般都会存在,第三种和第四种只要存在一种就可以,一般用的最多的还是第四种,直接比较驱动和设备的 name 字段。
int main(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
unsigned char databuf[1];
unsigned char readbuf[1];
if(argc != 3)
{
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
fd = open(filename, O_RDWR);
if(fd < 0)
{
printf("file %s open failed!\r\n", argv[1]);
return -1;
}
databuf[0] = atoi(argv[2]); /* 要执行的操作:打开或关闭 */
if(databuf[0] == 2)
{
retvalue = read(fd,readbuf,sizeof(readbuf));
if(retvalue < 0)
{
printf("read file %s failed!\r\n",filename);
}
else
{
printf("read date: %x\r\n",readbuf[0]);
}
}else
{
/* 向/dev/led 文件写入数据 */
retvalue = write(fd, databuf, sizeof(databuf));
if(retvalue < 0)
{
printf("LED Control Failed!\r\n");
close(fd);
return -1;
}
}
retvalue = close(fd);
if(retvalue < 0)
{
printf("file %s close failed!\r\n", argv[1]);
return -1;
}
return 0;
}
/*
* 寄存器地址定义
*/
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)
#define REGISTER_LENGTH 4
/*
* 设备资源信息,也就是LED0所使用的所有寄存器
*/
static struct resource led_resources[] = {
[0] = {
.start = CCM_CCGR1_BASE,
.end = (CCM_CCGR1_BASE + REGISTER_LENGTH - 1),
.flags = IORESOURCE_MEM,
},
[1] = {
.start = SW_MUX_GPIO1_IO03_BASE,
.end = (SW_MUX_GPIO1_IO03_BASE + REGISTER_LENGTH - 1),
.flags = IORESOURCE_MEM,
},
[2] = {
.start = SW_PAD_GPIO1_IO03_BASE,
.end = (SW_PAD_GPIO1_IO03_BASE + REGISTER_LENGTH - 1),
.flags = IORESOURCE_MEM,
},
[3] = {
.start = GPIO1_DR_BASE,
.end = (GPIO1_DR_BASE + REGISTER_LENGTH - 1),
.flags = IORESOURCE_MEM,
},
[4] = {
.start = GPIO1_GDIR_BASE,
.end = (GPIO1_GDIR_BASE + REGISTER_LENGTH - 1),
.flags = IORESOURCE_MEM,
},
};
/*
* platform设备结构体
*/
static struct platform_device leddevice = {
.name = "imx6ul-led",
.id = -1,
.dev = {
.release = &led_release,
},
.num_resources = ARRAY_SIZE(led_resources),
.resource = led_resources,
};
static int __init leddevice_init(void)
{
return platform_device_register(&leddevice);
}
static void __exit leddevice_exit(void)
{
platform_device_unregister(&leddevice);
}
module_init(leddevice_init);
module_exit(leddevice_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("songwei");
#define LEDDEV_CNT 1 /* 设备号长度 */
#define LEDDEV_NAME "platled" /* 设备名字 */
#define LEDOFF 0
#define LEDON 1
/* 寄存器名 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;
/* leddev设备结构体 */
struct leddev_dev{
......
};
struct leddev_dev leddev; /* led设备 */
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.read = led_read,
.open = led_open,
.write = led_write,
};
/*
* @description : flatform驱动的probe函数,当驱动与
* 设备匹配以后此函数就会执行
* @param - dev : platform设备
* @return : 0,成功;其他负值,失败
*/
static int led_probe(struct platform_device *dev)
{
int i = 0;
int ressize[5];
u32 val = 0;
struct resource *ledsource[5];
printk("led driver and device has matched!\r\n");
/* 1、获取资源 */
for (i = 0; i < 5; i++) {
ledsource[i] = platform_get_resource(dev, IORESOURCE_MEM, i); /* 依次MEM类型资源 */
if (!ledsource[i]) {
dev_err(&dev->dev, "No MEM resource for always on\n");
return -ENXIO;
}
ressize[i] = resource_size(ledsource[i]);
}
......
}
static int led_remove(struct platform_device *dev)
{
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
......
}
/* platform驱动结构体 */
static struct platform_driver led_driver = {
.driver = {
.name = "imx6ul-led", /* 驱动名字,用于和设备匹配 */
},
.probe = led_probe,
.remove = led_remove,
};
static int __init leddriver_init(void)
{
return platform_driver_register(&led_driver);
}
static void __exit leddriver_exit(void)
{
platform_driver_unregister(&led_driver);
}
module_init(leddriver_init);
module_exit(leddriver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("songwei");
#define LEDDEV_CNT 1 /* 设备号长度 */
#define LEDDEV_NAME "dts_platled" /* 设备名字 */
#define LEDOFF 0
#define LEDON 1
/* leddev设备结构体 */
struct leddev_dev{
......
struct device_node *node; /* LED设备节点 */
int led0; /* LED灯GPIO标号 */
};
struct leddev_dev leddev; /* led设备 */
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.read = led_read,
.open = led_open,
.write = led_write,
};
/*
* @description : flatform驱动的probe函数,当驱动与
* 设备匹配以后此函数就会执行
* @param - dev : platform设备
* @return : 0,成功;其他负值,失败
*/
static int led_probe(struct platform_device *dev)
{
......
/* 6、初始化IO */
leddev.node = of_find_node_by_path("/gpioled");
if (leddev.node == NULL){
printk("gpioled node nost find!\r\n");
return -EINVAL;
}
leddev.led0 = of_get_named_gpio(leddev.node, "led-gpio", 0);
if (leddev.led0 < 0) {
printk("can't get led-gpio\r\n");
return -EINVAL;
}
gpio_request(leddev.led0, "led0");
gpio_direction_output(leddev.led0, 1); /* led0 IO设置为输出,默认高电平 */
return 0;
}
/*
* @description : platform驱动的remove函数,移除platform驱动的时候此函数会执行
* @param - dev : platform设备
* @return : 0,成功;其他负值,失败
*/
static int led_remove(struct platform_device *dev)
{
gpio_set_value(leddev.led0, 1); /* 卸载驱动的时候关闭LED */
gpio_free(leddev.led0); /* 释放IO */
......
}
/* 匹配列表 */
static const struct of_device_id led_of_match[] = {
{ .compatible = "songwei-gpioled" },
{ /* Sentinel */ }
};
/* platform驱动结构体 */
static struct platform_driver led_driver = {
.driver = {
.name = "imx6ul-led", /* 驱动名字,用于和设备匹配 */
.of_match_table = led_of_match, /* 设备树匹配表 */
},
.probe = led_probe,
.remove = led_remove,
};
static int __init leddriver_init(void)
{
return platform_driver_register(&led_driver);
}
static void __exit leddriver_exit(void)
{
platform_driver_unregister(&led_driver);
}
module_init(leddriver_init);
module_exit(leddriver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("songwei");
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。