Linux驱动笔记

Linux驱动

Linux 驱动有两种运行方式,第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用命令加载驱动模块。在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 Linux 代码。而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。总之,将驱动编译为模块最大的好处就是方便开发,当驱动开发完成,确定没有问题以后就可以将驱动编译进Linux 内核中,当然也可以不编译进 Linux 内核中,具体看自己的需求。

模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 驱动入口函数 */
static int __init xxx_init(void){
return 0;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void){
}

module_init(xxx_init); //注册模块加载函数,调用insmod或者modprobe的时候会被调用
module_exit(xxx_exit); //注册模块卸载函数,调用rmmod的时候会被调用

MODULE_LICENSE(("GPL") //添加模块 LICENSE 信息,不加报错
MODULE_AUTHOR("Gnosis") //添加模块作者信息,可添加可不添加

字符设备驱动

字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。

设备号

为了方便管理,Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。Linux 提供了一个名为 dev_t 的数据类型表示设备号

旧字符设备驱动开发

对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。在模块init的时候注册,在exit的时候删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
...
/*file_operations结构体里面定义的各种操作函数*/
static int xxx_open(struct inode *inode, struct file *filp){
return 0;
}
static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt){
return 0;
}
static ssize_t chrtest_write(struct file *filp,const char __user *buf,size_t cnt, loff_t *offt){
return 0;
}
static int chrtest_release(struct inode *inode, struct file *filp){
return 0;
}
/*用于定义设备的操作集*/
static struct file_operations xxx_fops = {
.owner = THIS_MODULE, /*用于模块引用计数,基本上这种.owner都是防止模块在使用中被卸载*/
.open = xxx_open,
.read = xxx_read,
.write = xxx_write,
.release = xxx_release,
};

static int __init xxx_init(void){
/*这是旧的字符设备创建api,根据给定的主设备号,一次性占用该设备号下面的所有2^12个次设备号,并且直接完成设备的创建,因为太浪费资源且需要手动创建设备节点,基本上很少用了。里面的三个参数分别是主设备号,设备名称和操作集*/
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
return 0;
}

static void __exit xxx_exit(void){
/*卸载设备,注销设备号*/
static inline void unregister_chrdev(unsigned int major, const char *name)
}
...

新字符设备驱动开发

旧api会占用所以次设备号,且无法自动创建节点,还需要确定哪些主设备号没有用,所以新的方法就可以解决上述问题

alloc_chrdev_region/register_chrdev_region + cdev + class_create + device_create就可以解决上述问题,其中前两者是指定主次设备号或者动态分配主设备的方法,后面的cdev则是新方法里面用于表示一个字符设备的结构体,与设备号的注册分离开了,可以解耦设备号与操作集,之后的两个就是用于完成自动创建设备节点的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static int __init xxx_init(void)
{
/*判断有没有给定主设备号,有就自定义,没有就自动分配*/
if (int major) {
/*根据指定的数字合成一个完整的设备号*/
dev_t devid = MKDEV(int major, int minor);

/*新的api,用于根据指定的设备号注册设备号,第二个是要注册的数量,从minor开始递增*/
int register_chrdev_region(dev_t devid, unsigned count, const char *name);
} else {
/*自动分配的api,第一个是分配得到的设备号,第二个是起始次设备号,第三个是注册的设备数量,第四个是名称*/
int alloc_chrdev_region(dev_t *devid, unsigned baseminor, unsigned count, const char *name);

/*获取主次设备号*/
newleddev.major = MAJOR(dev_t devid);
newleddev.minor = MINOR(dev_t devid);
}

/*cdev通过函数cdev_init()初始化,主要工作就是在内核空间创建字符设备操作集。file_operations是字符驱动需要实现的具体操作集。*/
void cdev_init(struct cdev *cdev, const struct file_operations *fops);

/*cdev里面设置的.owner=THIS_MODULE用于保护模块,当这个模块加载的时候引用计数初始化为0,只要有进程使用open操作该驱动下面的设备的时候引用计数+1,close的时候-1,只有为0的时候模块才能被卸载。目的是防止加载模块后,open操作设备的时候外部调用rmmod卸载模块,导致无法正常操作设备出错*/
cdev.owner = THIS_MODULE;

/* cdev_add 将初始化好的 cdev 注册到内核全局字符设备映射表(kobj_map这个表存储着所有字符设备,这样子open才能找到对应的设备),建立设备号与 cdev 的关联,使设备对用户空间可见并可以被操作。第三个参数是添加的次设备数量,和次设备号数量对应*/
int cdev_add(struct cdev *cdev, dev_t devid, unsigned count);

/*因为cdev只会在内核空间创建字符设备操作集,但是不会在用户空间创建可见的接口,所以需要class_create在内核创建设备类,提供具体的设备容器,/sys/class就是存放设备类的地方,每个设备类下面的目录都是这个设备的属性文件。然后再调用device_create,在该类下面以给定的设备号实例化一个具体的设备节点,并且向用户空间发送uevent(设备树原始信息),然后运行在用户空间的udev程序(udev 是一个用户程序,在 Linux 下通过 udev 来实现设备文件的创建与删除,busybox里面是简化后的叫mdev。),接收到信息就会自动在/dev创建具体的设备目录,这个时候就可以在用户空间操作设备号对应的cdev创建的设备接口了,/dev存放的是需要数据传输复杂控制的设备节点。不需要复杂控制例如led这样子只需要点亮的设备,直接操作属性文件即可,如果一个设备类需要复杂操作或者数据传输才会再通过设备类创建设备节点*/
/* */
struct class *class_create(struct module *owner, const char *name);
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt);

return 0;
}

设备树

没有设备树的时候使用机器码来识别设备,非常不方便,于是就有了设备树用来描述设备,也就是开发板上的设备信息,比如CPU 数量、 内存基地址、IIC 接口上接了哪些设备、SPI 接口上接了哪些设备等等。设备树的文件叫做 DTS,这个 DTS 文件采用树形结构描述板级设备,DTB 是将DTS 编译以后得到的二进制文件,将.dts 编译为.dtb需要用到 DTC 工具

设备树也是用#include引用文件,其中这个imx6ull.dtsi文件就类似于头文件,一般.dtsi 文件用于描述 SOC 的内部外设信息,比如 CPU 架构、主频、外设寄存器地址范围,比如 UART、IIC 等等。这样子就可以给多个设备树文件引用,在具体的dts文件里面再编辑各种设备节点,避免重复写的麻烦

1
2
#include <dt-bindings/input/input.h>
#include "imx6ull.dtsi"

每个设备树只有一个”/“根节点,设备节点的命名格式为

1
label: node-name@unit-address

引入 label 的目的就是为了方便访问节点,可以直接通过&label 来访问这个节点而不需要输入完整的节点名字。其中node-name是节点名字,为 ASCII 字符串,节点名字应该能够清晰的描述出节点的功能,比如“uart1”就表示这个节点是 UART1 外设。unit-address一般表示设备的地址或寄存器首地址,如果某个节点没有地址或者寄存器的话unit-address可以不要

常用属性节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/ {
model = "Freescale i.MX6 ULL 14x14 EVK Board"; /*model属性值是一个字符串,一般model属性描述设备模块信息,比如名字什么的*/
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull"; /*用于设备匹配,是一个字符串列表*/
/*这个节点是一个特殊节点,aliases 的意思是“别名”,因此 aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点,不过一般会在节点命名的时候会加上 label,然后通过&label来访问节点*/
aliases {
spi0 = &ecspi1;
spi1 = &ecspi2;
spi2 = &ecspi3;
spi3 = &ecspi4;
};
/*chosen并不是一个真实的设备,chosen节点主要是为了uboot向 Linux 内核传递数据,重点是 bootargs 参数*/
chosen {
};

/*soc是片内控制器*/
soc {
/*这两个是用来配置子节点的reg属性的,参数为1代表,子节点的reg是1个地址和1个长度为一组*/
#address-cells = <1>;
#size-cells = <1>;
/*设备树的gpio节点*/
gpio1: gpio@0209c000 {
status: "okay" /*设备状态,okay表示可操作,disable表示不可操作,fail表示不可操作有错误*/
compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
reg = <0x0209c000 0x4000>; /*gpio寄存器基地址,格式由父节点的#address-cells和#size-cells决定*/
...
};
};
};

没有设备树之前,uboot 会向 Linux 内核传递一个叫做 machine id 的值,machine id也就是设备 ID,告诉 Linux 内核自己是个什么设备,看看 Linux 内核是否支持,Linux内核都用MACHINE_START和MACHINE_END来定义一个 machine_desc 结构体来描述这个设备

使用设备树之后,所有machine_desc是通过DT_MACHINE_START定义的,在链接过程中通过链接脚本将所有的machine_desc放置在特定的段(.arch.info.init),保证它们的内存地址连续排列。Linux设备树和内核会遍历这些machine_desc内部的dt_compat[](compatible字符串数组),与设备树根节点的compatible属性来匹配,具体流程就是,start_kernel()初始化内核大部分子系统,其中会调用setup_arch()架构初始化,然后setup_arch()会调用setup_machine_fdt()->of_flat_dt_match_machine(),最后里面使用while(get_next_compat)遍历所有desc下面的dt_compat[]数组与设备树根节点的compatible对比来完成匹配过程

设备树常用of函数,因为驱动里面想要获取设备树节点配置的信息,肯定需要各种读取函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//查找相关
of_find_node_by_name(); // 根据设备name属性值查找
of_find_compatible_node(); //根据compatible查找
of_find_node_by_path(); //根据路径查找
of_get_parent(); //查找父节点
of_get_next_child(); //查找下一个子节点
//获取属性值相关
of_find_property(); //查找指定的属性
of_property_count_elems_of_size(); //获取属性值中元素数量
of_property_read_u32_index(); //从属性中获取指定标号的u32类型数据值
of_property_ead_u8_array(); //一次性读取属性中各个类型的数组数据
of_property_ead_u16_array();
of_property_ead_u32_array();
of_property_ead_u64_array();
of_property_read_u8(); //读取只有一个整型值的属性
of_property_read_u16();
of_property_read_u32();
of_property_read_u64();
of_property_read_string(); //读取属性中的字符串
of_n_addr_cells(); //读取#address-cells值
of_n_size_cells(); //读取#size-cells值
//其它OF函数
of_device_is_compatible(); //查看节点的compatible属性是否包含特定字符串,也就是检查兼容性
of_get_address(); //获取地址相关属性
of_iomap(); //用于直接内存映射,类似于ioremap(),使用设备树之后大多用of_iomap()

pinctrl子系统

使用设备树之后,肯定不会再通过操作寄存器的方式去操作设备,Linux 内核提供了 pinctrlgpio 子系统用于GPIO 驱动

不管是stm32还是linux裸机开发,操作gpio的时候基本上都是先配置gpio的复用功能,电气属性(上下拉,驱动能力)等等,然后在设置gpio是输入还是输出,前者是pinctrl子系统的工作,后者是gpio子系统的工作

1
#define MX6UL_PAD_GPIO1_IO03_GPIO1_IO03 0x0068 0x02F4 0x0000 0x5 0x0	//pinctrl引脚配置(复用地址偏移,电器地址偏移,输入地址偏移,复用值,输入值)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
&iomuxc {
compatible = "fsl,imx6ul-iomuxc";
reg = <0x020e0000 0x4000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_hot_1>; //默认配置pinctrl_hot_1生效
imx6ul_evk {
...
pinctrl_led: ledgrp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO03_GPIO1_IO03 0x10b0 //最后一个是电器值,查看参考手册可知是100k下拉,推挽,100MHz,强驱动能力,慢压摆率
>;
};
};
};

接下来是pinctrl驱动是如何识别到设备树里面的pinctrl信息的,这个驱动一般是soc厂商编写好的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*Linux内核里面的pinctrl驱动会匹配上设备树里面的拥有这些compatible的节点,probe函数会被调用*/
static struct of_device_id imx6ul_pinctrl_of_match[] = {
{ .compatible = "fsl,imx6ul-iomuxc", .data = &imx6ul_pinctrl_info, },
{ .compatible = "fsl,imx6ull-iomuxc-snvs", .data = &imx6ull_snvs_pinctrl_info, },
{ /* sentinel */ }
};

static int imx6ul_pinctrl_probe(struct platform_device *pdev)
{
const struct of_device_id *match;
struct imx_pinctrl_soc_info *pinctrl_info;

match = of_match_device(imx6ul_pinctrl_of_match, &pdev->dev);

if (!match)
return -ENODEV;

pinctrl_info = (struct imx_pinctrl_soc_info *) match->data;
/*在probe里面会调用imx_pinctrl_probe*/
return imx_pinctrl_probe(pdev, pinctrl_info);
}

/*平台驱动,用于驱动和设备的分离和分层,当驱动和设备匹配上就会调用probe函数*/
static struct platform_driver imx6ul_pinctrl_driver = {
.driver = {
.name = "imx6ul-pinctrl",
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(imx6ul_pinctrl_of_match),
},
.probe = imx6ul_pinctrl_probe,
.remove = imx_pinctrl_remove,
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int imx_pinctrl_probe(struct platform_device *pdev,struct imx_pinctrl_soc_info *info)
{
...
/*这个函数里面会定义一个结构体pinctrl_desc,这个结构体里面有三个操作集pctlops(解析设备树信息),pmxops(配置引脚复用),confops(配置电气属性),这是实现引脚配置的工具。*/
struct pinctrl_desc *imx_pinctrl_desc;
...

imx_pinctrl_desc->name = dev_name(&pdev->dev);
imx_pinctrl_desc->pins = info->pins;
imx_pinctrl_desc->npins = info->npins;
imx_pinctrl_desc->pctlops = &imx_pctrl_ops; /*解析设备树信息*/
imx_pinctrl_desc->pmxops = &imx_pmx_ops; /*配置引脚复用*/
imx_pinctrl_desc->confops = &imx_pinconf_ops; /*配置电气属性*/
imx_pinctrl_desc->owner = THIS_MODULE;

/*imx_pinctrl_probe_dt()->imx_pinctrl_parse_functions()->imx_pinctrl_parse_groups(),imx_pinctrl_parse_groups里面则会获取设备树pinctrl节点配置信息并存储起来,留给pctlops工具解析*/
ret = imx_pinctrl_probe_dt(pdev, info);
...
/*然后调用pinctrl_register()传入这个pinctrl_desc并注册一个pinctrl_dev控制器,这个控制器就会调用desc里面的pctlops工具解析pinctrl配置信息,然后在真正使用设备时调用pmxops和confops完成配置*/
ipctl->pctl = pinctrl_register(imx_pinctrl_desc, &pdev->dev, ipctl);
if (!ipctl->pctl) {
dev_err(&pdev->dev, "could not register IMX pinctrl driver\n");
return -EINVAL;
}

dev_info(&pdev->dev, "initialized IMX pinctrl driver\n");

return 0;
}

gpio子系统

gpio子系统驱动也是一个平台驱动,检测设备树中gpio节点的compatible字符串列表,匹配上之后就会执行probe函数,一般会在这里先定义一个结构体,这个结构体会存储gpio控制器和gpio寄存器组。然后probe获取设备树里面的reg并通过内存映射得到gpio寄存器基地址在Linux内核中的虚拟地址,这样子内核就可以访问gpio的所有寄存器组了。然后是给gpio控制器定义一些gpio操作方法,将这些全部存储在那个结构体中,gpio驱动再向用户空间提供gpio操作函数,这样子就可以实现对gpio的访问和操作了。

1
2
3
4
5
6
7
8
9
10
gpio1: gpio@0209c000 {		/*设备树的gpio节点*/
compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio"; /*gpio驱动会根据这个来完成匹配*/
reg = <0x0209c000 0x4000>; /*gpio寄存器基地址*/
interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
gpio-controller; /*说明这是一个gpio控制器*/
#gpio-cells = <2>; /*led-gpios = <&gpio1 3 GPIO_ACTIVE_HIGH>;规定了后面写两个元素,一个引脚号一个极性*/
interrupt-controller;
#interrupt-cells = <2>;
};

在imx6ull里面体现为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*gpio_chip里面定义了各种gpio操作方法*/
struct gpio_chip {
...
int (*request)(struct gpio_chip *chip,unsigned offset);
void (*free)(struct gpio_chip *chip,unsigned offset);
int (*get_direction)(struct gpio_chip *chip,
...
};

/*bgpio_chip里面存储gpio_chip控制器和gpio寄存器地址*/
struct bgpio_chip {
struct gpio_chip gc;

unsigned long (*read_reg)(void __iomem *reg);
void (*write_reg)(void __iomem *reg, unsigned long data);

void __iomem *reg_dat;
void __iomem *reg_set;
void __iomem *reg_clr;
void __iomem *reg_dir;
};

struct mxc_gpio_port {
...
struct bgpio_chip bgc; /*这样子bgpio_chip就可以实现对gpio寄存器组的访问和控制*/
...
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

static int mxc_gpio_probe(struct platform_device *pdev)
{
struct mxc_gpio_port *port; /*这个里面有bgpio_chip*/
...
mxc_gpio_get_hw(pdev); /*mxc_gpio_get_hw()匹配gpio寄存器组,也就是会匹配下面的imx35_gpio_hwdata*/
...
iores = platform_get_resource(pdev, IORESOURCE_MEM, 0); /*platform_get_resource()获取reg*/
port->base = devm_ioremap_resource(&pdev->dev, iores); /*devm_ioremap_resource ()获取虚拟地址*/
...
}

/*里面的值就是gpio寄存器偏移量,基地址偏移之后就能得到gpio寄存器组,一般有很多个mxc_gpio_hwdata,根据硬件来匹配不同的寄存器组*/
static struct mxc_gpio_hwdata imx35_gpio_hwdata = {
.dr_reg = 0x00,
.gdir_reg = 0x04,
.psr_reg = 0x08,
.icr1_reg = 0x0c,
.icr2_reg = 0x10,
.imr_reg = 0x14,
.isr_reg = 0x18,
.edge_sel_reg = 0x1c,
.low_level = 0x00,
.high_level = 0x01,
.rise_edge = 0x02,
.fall_edge = 0x03,
};

pinctrl和gpio配合使用

一般这两个子系统是搭配使用的,先配置驱动能力再控制gpio,以下是一个led驱动

1
2
3
4
5
6
7
8
9
gpioled {			
#address-cells = <1>;
#size-cells = <1>;
compatible = "imx6ull-atk-gpio-led"; /*用于led驱动匹配*/
pinctrl-names = "default"; /*引脚名字默认*/
pinctrl-0 = <&pinctrl_led>; /*配置gpio复用和驱动能力*/
led-gpios = <&gpio1 3 GPIO_ACTIVE_HIGH>; /*控制gpio,设置高电平有效*/
status = "okay"; /*启用设备*/
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/*设备结构体*/
struct gpioled {
dev_t devid;
int major;
int minor;
struct cdev cdev;
struct class *class;
struct device *device;
struct device_node *nd;
int gpio_led;
};

struct gpioled gpioled;

static int gpioled_open(struct inode *inode,struct file *filp){
filp->private_data = &gpioled;
return 0;
}
static int gpioled_release(struct inode *inode,struct file *filp){
struct gpioled *dev = (struct gpioled*)filp->private_data;
return 0;
}
static ssize_t gpioled_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt){
struct gpioled *dev = (struct gpioled*)filp->private_data;
...
return 0;
}
/*操作集*/
static struct file_operations gpioled_fops = {
.owner = THIS_MODULE,
.write = gpioled_write,
.release = gpioled_release,
.open = gpioled_open,
};
/*驱动入口函数*/
static int __init gpioled_init(void){
int ret = 0;

/*注册设备号*/
gpioled.major = 0;
if(gpioled.major){
gpioled.devid = MKDEV(gpioled.major,0);
register_chrdev_region(gpioled.devid,GPIOLED_CNT,GPIOLED_NAME);
}else{
alloc_chrdev_region(&gpioled.devid,0,GPIOLED_CNT,GPIOLED_NAME);
gpioled.major = MAJOR(gpioled.devid);
gpioled.minor = MINOR(gpioled.devid);
}

/*注册字符设备操作集并添加字符设备节点*/
gpioled.cdev.owner = THIS_MODULE;
cdev_init(&gpioled.cdev,&gpioled_fops);
cdev_add(&gpioled.cdev,gpioled.devid,GPIOLED_CNT);
gpioled.class = class_create(THIS_MODULE,GPIOLED_NAME);
gpioled.device = device_create(gpioled.class,NULL,gpioled.devid,NULL,GPIOLED_NAME);


gpioled.nd = of_find_node_by_path("/gpioled"); /*获取设备树驱动节点内容*/
gpioled.gpio_led = of_get_named_gpio(gpioled.nd,"led-gpios",0); /*获取gpio控制节点*/
gpio_request(gpioled.gpio_led,"gpioled"); /*申请使用gpio*/
gpio_direction_output(gpioled.gpio_led,0); /*设置为输出,默认输出低电平*/
/*以上函数无法判断设备树里面gpio引脚是LOW还是HIGH,例如led-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>,设置了低电平有效,按理来说使用gpio_direction_output或者gpio_set_value设置0时,输出的应该是是高电平,然而实际上0就是低1就是高。使用gpiod_direction_input(),gpiod_direction_output(),gpiod_get_value(),gpiod_set_value()这些新的api才可以识别到设备树里面配置的高低极性,所以推荐使用后者*/
return 0;
}

Linux并发与竞争

Linux 系统是个多任务操作系统,会存在多个任务同时访问同一片内存区域,这些任务可能会相互覆盖这段内存中的数据,造成内存数据混乱。针对这个问题必须要做处理,严重的话可能会导致系统崩溃。现在的 Linux 系统并发产生的原因很复杂,总结一下有下面几个主要原因:

①、多线程并发访问,Linux 是多任务(线程)的系统,所以多线程访问是最基本的原因。

②、抢占式并发访问,从 2.6 版本内核开始,Linux 内核支持抢占,也就是说调度程序可以在任意时刻抢占正在运行的线程,从而运行其他的线程。

③、中断程序并发访问,这个无需多说,学过 STM32 的同学应该知道,硬件中断的权利可是很大的。

④、SMP(多核)核间并发访问,现在 ARM 架构的多核 SOC 很常见,多核 CPU 存在核间并发访问。

并发访问带来的问题就是竞争,所谓的临界区就是共享数据段,对于临界区必须保证一次只有一个线程访问,也就是要保证临界区是原子访问的。这里的原子访问就表示这一个访问是一个步骤,不能再进行拆分。如果多个线程同时操作临界区就表示存在竞争,我们在编写驱动的时候一定要注意避免并发和防止竞争访问。

原子操作

首先看一下原子操作,原子操作就是指不能再进一步分割的操作,是通过定义一个原子变量对这个变量进行保护的,这些原子操作可以确保对原子变量的操作是不会被分割的,但是只适用于整形变量

原子操作API

image-20251024230907571

自旋锁

当一个线程要访问某个共享资源的时候首先要先获取自旋锁,自旋锁只能被一个线程持有,只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。

注意,获取自旋锁的线程会禁用内核抢占,等待获取自旋锁的线程是忙等待而不是阻塞等待,所以要注意拿到锁的线程在执行临界区代码的时候不能阻塞或者休眠,否则会发生死锁。例如,A线程拿到了自旋锁就会禁用内核抢占,在执行临界区代码的时候进入阻塞或者休眠的话,就相当于自动放弃了CPU,然后线程B就会运行。然后B也要访问临界区的时候,锁被A拿着,B就只能等A释放锁,B的等待不是阻塞休眠,会一直占用CPU,这个时候A休眠结束了但是内核抢占又被禁用了,所以无法打断B,就形成了死锁

自旋锁API

image-20251024231054691

方式 功能 使用场景
DEFINE_SPINLOCK() 定义 + 初始化 全局 / 单独一把锁
spin_lock_init() 只初始化 结构体里的锁、动态创建的锁

拿到锁的线程只会禁用内核抢占而不会禁用中断,如果在执行临界区期间被中断打断,且中断也要获取锁访问共享资源,就又可能导致死锁。所以常用下面的API

image-20251024231217065

前两个API是直接强制关中断,解锁时直接强制开中断。不安全!因为如果进来之前中断本来就是关的,解锁后会被错误打开。所以一般在线程中使用 spin_lock_irqsave/spin_unlock_irqrestore,在中断中使用 spin_lock/spin_unlock

除了普通的自旋锁,还有读写锁和顺序锁,但是这两个一般是在Linux内核或者应用中使用,驱动开发用的不多

综合前面关于自旋锁的信息,我们需要在使用自旋锁的时候要注意一下几点:

①、因为在等待自旋锁的时候处于“自旋”状态,因此锁的持有时间不能太长,一定要短,否则的话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处理方式。

②、自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数,否则的话可能导致死锁。

③、不能递归申请自旋锁,因为一旦通过递归的方式申请一个你正在持有的锁,那么你就必须“自旋”,等待锁被释放,然而你正处于“自旋”状态,根本没法释放锁。结果就是自己把自己锁死了!

④、在编写驱动程序的时候我们必须考虑到驱动的可移植性,因此不管你用的是单核的还是多核的 SOC,都将其当做多核 SOC 来编写驱动程序。

信号量

信号量有一个信号量值,相当于一个房子有 10 把钥匙,这 10 把钥匙就相当于信号量值为10。因此,可以通过信号量来控制访问共享资源的访问数量,如果要想进房间,那就要先获取一把钥匙,信号量值减 1,直到 10 把钥匙都被拿走,信号量值为 0,这个时候就不允许任何人进入房间了,因为没钥匙了。如果有人从房间出来,那他要归还他所持有的那把钥匙,信号量值加 1,此时有 1 把钥匙了,那么可以允许进去一个人。相当于通过信号量控制访问资源的线程数。

在初始化的时候将信号量值设置的大于 1,那么这个信号量就是计数型信号量,计数信号量不能用于互斥访问,因为它允许多个线程同时访问共享资源。如果要互斥的访问共享资源那么信号量的值就不能大于 1,此时的信号量就是一个二值信号量。

信号量API,前两者和自旋锁一样的含义

image-20251024231407341

信号量会提高处理器的使用效率,因为它不是自旋等待而是休眠等待。但是,信号量的开销要比自旋锁大,因为信号量使线程进入休眠状态以后会切换线程,切换线程就会有开销。总结一下信号量的特点:

①、因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。

②、因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。

③、如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。

互斥体

将信号量的值设置为 1 就可以使用信号量进行互斥访问了,虽然可以通过信号量实现互斥,但是 Linux 提供了一个比信号量更专业的机制来进行互斥,它就是互斥体—mutex。互斥访问表示一次只有一个线程可以访问共享资源,不能递归申请互斥体。在我们编写 Linux 驱动的时候遇到需要互斥访问且耗时较长的地方建议使用 mutex。

互斥体API,前两者和自旋锁一样的含义

image-20251024231710467

在使用 mutex 之前要先定义一个 mutex 变量。在使用 mutex 的时候要注意如下几点:

①、mutex 可以导致休眠,而中断上下文是绝对不允许休眠的,因为会导致系统崩溃。因此不能在中断中使用 mutex,中断中只能使用自旋锁。

②、和信号量一样,mutex 保护的临界区可以调用引起阻塞的 API 函数,因为不会让别的线程死等而是休眠,所以拿到mutex的线程醒来就可以继续执行

③、因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并且 mutex 和自旋锁一样不能递归上锁和解锁,否则死锁。

总结

对比项 原子操作 atomic 自旋锁 spinlock 信号量 semaphore 互斥锁 mutex
保护对象 单个变量 一段代码临界区 一段代码 / 同步事件 一段代码临界区
获取不到锁行为 无锁概念,硬件原子执行 自旋忙等待,占用 CPU,不休眠 休眠阻塞,让出 CPU 休眠阻塞,让出 CPU
能否在中断使用 ✅ 可以 ✅ 可以 ❌ 不可以 ❌ 不可以
临界区能否睡眠阻塞 无临界区 ❌ 严禁睡眠,睡眠必死锁 ✅ 可以随便睡 ✅ 可以随便睡
是否禁止内核抢占 不禁止 获取锁自动禁止抢占 不禁止 不禁止
CPU 开销 最低,最快 高(忙等待浪费 CPU) 低(休眠让出 CPU) 低(休眠让出 CPU)
有无线程归属 无归属 无归属 无归属,任意线程可解锁 有归属,必须持有者本人解锁
能否递归上锁 无递归概念 不建议递归,递归自旋死等 ✅ 可以递归 ❌ 严禁递归,递归必死锁
主要用途 计数器、标志位、状态位 短临界区、中断、线程↔中断互斥 线程同步、生产者消费者、二值互斥 线程之间纯互斥保护、长临界区

Linux定时器

Linux 内核中有大量的函数需要时间管理,比如周期性的调度程序、延时程序、定时器。硬件定时器提供时钟源,时钟源的频率可以设置, 设置好以后就周期性的产生定时中断,系统使用定时中断来计时。

节拍率HZ

系统节拍率是可以设置的,单位是 Hz,我们在编译 Linux 内核的时候可以通过图形化界面设置系统节拍率,设置好之后可以在内核根目录下面的.config里面看到CONFIG_HZ的值就是我们设置的频率。Linux内核会定义一个宏HZ,使用CONFIG_HZ来设置HZ的值。

高节拍率和低节拍率的优缺点:

①、高节拍率会提高系统时间精度,如果采用 100Hz 的节拍率,时间精度就是 10ms,采用1000Hz 的话时间精度就是 1ms,精度提高了 10 倍。高精度时钟的好处有很多,对于那些对时间要求严格的函数来说,能够以更高的精度运行,时间测量也更加准确。

②、高节拍率会导致中断的产生更加频繁,频繁的中断会加剧系统的负担,1000Hz 和 100Hz的系统节拍率相比,系统要花费 10 倍的“精力”去处理中断。中断服务函数占用处理器的时间增加,但是现在的处理器性能都很强大,所以采用 1000Hz 的系统节拍率并不会增加太大的负载压力,所以要根据自己的实际情况,选择合适的系统节拍率。

滴答计算器jiffies

Linux 内核使用全局变量 jiffies 来记录系统从启动以来的系统节拍数,有jiffies_64jiffies, 其实是同一个东西,jiffies_64 用于 64 位系统,而 jiffies 用于 32 位系统。当我们访问 jiffies 的时候其实访问的是 jiffies_64 的低 32 位,在 32 位的系统上读取 jiffies 的值,在 64 位的系统上 jiffesjiffies_64表示同一个变量,因此也可以直接读取 jiffies 的值。所以不管是 32 位的系统还是 64 位系统,都可以使用 jiffies。系统启动的时候会将 jiffies 初始化为0。

不管是 32 位还是 64 位的 jiffies,都有溢出的风险,溢出以后会重新从 0 开始计数,相当于绕回来了,因此有些资料也将这个现象也叫做绕回。假如 HZ 为最大值 1000 的时候,32 位的 jiffies 只需要 49.7 天就发生了绕回,对于 64 位的 jiffies 来说大概需要5.8 亿年才能绕回,因此 jiffies_64 的绕回忽略不计。

判断超时API

image-20260419154310474

单位转换API

image-20260419154432386

延时API

image-20260419155236816

软件定时器

Linux软件定时器使用很简单,只需要提供超时时间(相当于定时值)和定时处理函数即可,当超时时间到了以后设置的定时处理函数就会执行,不需要做一大堆的寄存器初始化工作。在使用的时候要注意一点,软件定时器并不是周期性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器。

1
2
3
4
5
6
7
8
9
struct timer_list {		/*软件定时器结构体,是基于软中断的*/
struct list_head entry; /*挂载进内核的链表*/
unsigned long expires; /* 定时器超时时间,单位是节拍数jiffies */
struct tvec_base *base;
void (*function)(unsigned long); /* 定时处理函数,是基于软中断实现的 */
unsigned long data; /* 要传递给 function 函数的参数 */
int slack;
};
/*硬件定时器的中断频率由HZ决定,jiffies是计数硬件定时器中断次数的,time_list是软件定时器,内部有expires(基于jiffies的值完成设置),function(到期后执行的回调函数),entry(用于挂载进内核的链表)。所有软件定时器timer_list共用一个硬件定时器,然后内核通过entry来排队调度这些软件定时器,到点就执行内部的回调函数*/

常用API

1
2
3
4
5
6
timer_list timer;	//声明定时器
init_timer((struct timer_list *timer); //初始化定时器
add_timer((struct timer_list *timer); //注册并且激活定时器
del_timer((struct timer_list *timer); //删除定时器
del_timer_sync((struct timer_list *timer); //在多处理器设备上,可能另外一个处理器会调用这个定时器的回调,直接删除会崩溃。这个函数可以等待其他处理器全部使用完定时器再删除,注意不能用在中断上下文里面,因为这个函数是阻塞等待的
mod_timer(struct timer_list *timer, unsigned long expires); //修改定时器的定时值,没有激活的话还会激活定时器,就是add_timer的效果

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/*设备结构体*/
struct timeled {
...
struct timer_list timer; /*定时器*/
int time_led;
};
struct timeled timeled;
static int timeled_open(struct inode *inode,struct file *filp){
filp->private_data = &timeled;
return 0;
}
static int timeled_release(struct inode *inode,struct file *filp){
return 0;
}

static void timer_led(unsigned long arg){ //定时器回调函数
struct timeled *dev = (struct timeled *)arg; //传入的设备结构体
static int flag = LED_OFF;
flag = !flag;
gpio_set_value(dev->time_led,flag);
mod_timer(&dev->timer,jiffies + msecs_to_jiffies(500));
}

/*操作集*/
static struct file_operations timeled_fops = {
.owner = THIS_MODULE,
.release = timeled_release,
.open = timeled_open,
};

/*驱动入口函数*/
static int __init timeled_init(void){
/*字符设备基本流程*/
...
init_timer(&timeled.timer); //初始化定时器
timeled.timer.expires = jiffies + msecs_to_jiffies(500); //设置定时值
timeled.timer.function = timer_led; //设置回调函数
timeled.timer.data = (unsigned long)&timeled; //传给回调函数的参数,
add_timer(&timeled.timer); //注册并启动定时器
return 0;
...
}

/*驱动出口函数*/
static void __exit timeled_exit(void){
...
del_timer_sync(&timeled.timer); //同步等待删除定时器
...

}
...

Linux中断

Linux 内核提供了完善的中断框架,我们只需要申请中断,然后注册中断处理函数即可,不需要一系列复杂的寄存器配置

调度器只能调度进程而不能调度中断,所以中断里面不允许执行会导致休眠或者阻塞的任务,否则调度器不知道接下来给谁安排进入CPU工作,调度异常,就会导致出错卡死

中断API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*申请中断,第一个参数是申请的中断号,第二个是中断回调函数,第三个是下表中的标志,第四个是使用共享中断的时候传入用来区分的设备参数*/
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev_id);
irqreturn_t (*irq_handler_t)(int irq, void *dev_id);// 中断处理函数类型定义
void free_irq(unsigned int irq, void *dev_id);// 释放中断
void enable_irq(unsigned int irq);// 使能指定中断线
void disable_irq(unsigned int irq);// 阻塞禁用指定中断(等待中断处理函数执行完)
void disable_irq_nosync(unsigned int irq);// 立即禁用指定中断(不等待,直接关闭)
void local_irq_enable(void);// 开启本地CPU全局中断总开关
void local_irq_disable(void);// 关闭本地CPU全局中断总开关
void local_irq_save(unsigned long flags);// 保存中断状态+关闭本地全局中断
void local_irq_restore(unsigned long flags);// 恢复本地全局中断状态(配合save使用)
/*下面两个有所区别,第一个使用时必须在设备树里面设置interrupts,这个时候使用request_irq就无需指明标志,因为会自己从里面解析,而第二个就是单纯的把gpio转为中断号,无法解析到interrupts,且必须使用gpio子系统才能使用*/
unsigned int irq_of_parse_and_map(struct device_node *node, int index);// 设备树解析从interupts属性中提取到对应的中断号
int gpio_to_irq(unsigned int gpio);// GPIO引脚转对应中断号

image-20260419214324370

上半部下半部

中断会打断CPU运行的所有任务,所以要快速响应快速停止。为了处理一些耗时的任务,中断就有上半部和下半部之分,上半部使用硬中断来快速处理和接收,下半部使用软中断做一些稍微耗时一点的逻辑,因为软中断会在CPU空闲时执行,不会打断CPU也不会屏蔽其它硬件中断。而tasklet就是基于软中断的下半部机制,是简化版的封装。但是tasklet由于也是中断的一种,而在中断的上下文里面是不允许执行延时或者阻塞等会引起CPU睡眠的任务,所以又有了一种新的下半部机制叫工作队列,它会把工作加入一个工作队列里面,然后内核里面的工作者线程就会执行这里面的工作,因为它不是基于软中断的,而是将工作任务交给一个内核工作者线程执行,是进程上下文所以工作内部可以执行延时或者阻塞的任务。

两者的回调函数也有所不同,tasklet回调函数传入data,工作队列则只允许传入需要执行的工作work,两者如果想要拿到定义的dev设备,对于tasklet来说可以把dev地址传入回调函数,而工作队列回调函数因为无法传参,所以一般是将work定义在dev内部,再通过container_of拿到父结构体dev。之所以有这样子的差异是因为,tasklet比较早本身也只是一个小工具,不知道自己属于哪个设备,当时也还没有出现container_of函数,只能这样子传参。而工作队列出来的时候已经有container_of了,并且设计的时候就明确work是设备结构体自带的一部分,所以就不需要额外添加一个参数了

tasklet

1
2
3
4
5
struct tasklet_struct tasklet;
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data); //初始化tasklet
void tasklet_schedule(struct tasklet_struct *t); //在中断回调函数里面调用该函数就可以进入tasklet的处理函数
void tesklet_function(unsigned long data){ //tasklet处理函数,内部不允许执行耗时等待的任务
}

工作队列

1
2
3
4
5
struct work_struct work;
INIT_WORK(work, work_function); //初始化work
bool schedule_work(struct work_struct *work); //在中断回调函数里面调用该函数就可以进入工作队列的处理函数
void work_function(struct work_struct *work){ //工作队列处理函数,内部可以执行耗时等待的任务
}

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
gpio1: gpio@0209c000 {
compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
reg = <0x0209c000 0x4000>; // 寄存器地址
interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>; // 控制器给GIC的中断
gpio-controller; // 是GPIO控制器
#gpio-cells = <2>; // 使用GPIO需2个参数
interrupt-controller; // 是中断控制器
#interrupt-cells = <2>; // 使用中断需2个参数
};

mykey{
#address-cells = <1>;
#size-cells = <1>;
compatible = "atkalpha-key";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_key>;
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>;
interrupt-parent = <&gpio1>; /*设置中断控制器*/
interrupts = <18 IRQ_TYPE_EDGE_BOTH>; /*设置中断引脚,双边沿触发*/
status = "okay";
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
... //头文件

struct keyio {
int gpio_key;
int irq_key;
irqreturn_t (*handler)(int,void*);
int key_num;
char name[10];
atomic_t keyvalue;
atomic_t releasevalue;
};

/*设备结构体*/
struct mykey {
... //设备节点参数
struct keyio keyio;
struct timer_list timer;
//struct tasklet_struct tasklet;
struct work_struct work;
};

struct mykey mykey;

static irqreturn_t irqkey_handler(int irq,void * dev){ //中断回调函数
struct mykey *mydev = (struct mykey *)dev;
//tasklet_schedule(&mydev->tasklet);
schedule_work(&mydev->work);
return IRQ_HANDLED;
}

static void timer_function(unsigned long arg){ //定时器回调函数
struct mykey *dev = (struct mykey *)arg;
... //省略原子操作部分
}

// static void tasklet_function(unsigned long data){ //tasklet回调函数,内部不能执行耗时任务,所以使用定时器
// struct mykey *dev = (struct mykey *)data;
// dev->timer.data = (unsigned long)dev;
// mod_timer(&dev->timer,jiffies + msecs_to_jiffies(20));
// }

static void work_function(struct work_struct *work){
struct mykey *dev = container_of(work,struct mykey,work);
mdelay(20); //工作队列里面可以执行耗时任务
... //省略原子操作部分
}

... //操作集定义部分

/*驱动入口函数*/
static int __init mykey_init(void){
... //注册设备获取节点

mykey->keyio.irq_key = gpio_to_irq(mykey->keyio.gpio_key); //根据gpio获取中断号
//mykey->keyio.irq_key = irq_of_parse_and_map(mykey->nd,mykey->keyio.key_num); //根据设备树中断节点获取中断号
mykey->keyio.handler = irqkey_handler; //传入中断回调函数
request_irq(dev->keyio.irq_key, dev->keyio.handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, dev->keyio.name,dev);//申请中断号

//tasklet_init(&mykey->tasklet,tasklet_function,(unsigned long)dev); //初始化tasklet
INIT_WORK(&mykey.work,work_function); //初始化工作队列

init_timer(&mykey->timer); //初始化定时器
mykey->timer.function = timer_function; //传入定时器回调函数
... //错误处理部分
}

/*驱动出口函数*/
static void __exit mykey_exit(void){
del_timer_sync(&mykey.timer); //删除定时器
free_irq(mykey.keyio.irq_key,&mykey); //释放中断号
... //设备注销部分
}

... //签名部分

阻塞和非阻塞IO

当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO 就会将应用程序对应的线程挂起,直到设备资源可以获取为止。对于非阻塞 IO,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。

阻塞访问就是,当应用程序read/write访问设备资源的时候,访问不到时会进入休眠,直到可以获取时再醒来

非阻塞访问就是,当应用程序read/write访问设备资源的时候,访问不到,立刻返回进行下一次访问

非阻塞访问会非常占用CPU,那为什么还要有呢。是因为阻塞访问的话只能用于读取一个设备,比如要获取多个资源的话,读取第一个的时候就进入睡眠了。后面的设备就只能等待了,而非阻塞就可以同时访问多个设备,因为它会立即返回。而且可能非阻塞使用方式也有问题,一般使用非阻塞就是while循环然后一直read,这样子肯定占CPU,正确的方法是使用轮询

阻塞和非阻塞的打开方式有所区别

1
2
3
fd = open("/dev/xxx_dev", O_RDWR); /* 阻塞方式打开 */
fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式打开 */
/*需要注意的是,这里面的阻塞非阻塞标志位是给驱动看的,目的是让驱动能够根据标志位来切换模式而不是系统自动给你切换。但是如果你的驱动里面压根没有写非阻塞方式,你就算加了也没有用*/

等待队列

阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将CPU 资源让出来。当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完成唤醒工作。Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作

如果我们要在驱动中使用等待队列,必须创建并初始化一个等待队列头。等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个等待队列项,当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。因为只有添加到等待队列头中以后进程才能进入休眠态,当设备可以访问以后再将进程对应的等待队列项从等待队列头中移除。在实际使用中只需要创建等待队列头,等待队列项只需要调用wait_event/wait_event_interruptible等函数就会自动创建。这两种API内部逻辑大概就是,先创建一个等待队列项,然后添加进入等待队列头,并将进程运行状态改成TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE,然后使用调度器放弃CPU进入休眠,当wait_event唤醒时,再将进程从等待队列移出去,并把状态改回运行态。

等待队列API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct __wait_queue_head head; //等待队列头
init_waitqueue_head(&head); //初始化等待队列头
DECLARE_WAIT_QUEUE_HEAD(head); //一次性完成定义与初始化等待队列头

//等待队列项API,已经不建议自己使用,因为等待队列项这些都已经封装在wait_event等函数里面了
struct __wait_queue body; //等待队列项
DECLARE_WAITQUEUE(body, current); //定义并且初始化等待队列项
add_wait_queue(&head, &body); //把等待队列项添加到等待队列头
remove_wait_queue(&head, &body); //移除等待队列项

wait_event(head, condition); //等待某一个等待队列头被唤醒,把进程设置为TASK_UNINTERRUPTIBLE
wait_event_timeout(head, condition, timeout); //与wait_event类似,但是可以添加超时时间
wait_event_interruptible(head, condition); //等待某一个等待队列头被唤醒,把进程设置为TASK_INTERRUPTIBLE,就是可以被信号打断
wait_event_interruptible_timeout(head, condition, timeout);//与wait_evenr_interruptible类似,但是可以添加超时时间

wake_up(&head); //唤醒等待队列头里面的所有等待队列项,包括处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE
wake_up_interruptible(&head); //只可以唤醒处于TASK_INTERRUPTIBLE的等待队列项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// struct wait_queue_head_t wqh;	//定义等待队列头
// init_waitqueue_head(%wqh); //初始化等待队列头
DECLARE_WAIT_QUEUE_HEAD(wqh); //定义并初始化等待队列头

/*设备结构体*/
struct mykey {
...
};

struct mykey mykey;

static irqreturn_t irqkey_handler(int irq,void * dev){
struct mykey *mydev = (struct mykey *)dev;
schedule_work(&mydev->work); //工作队列
return IRQ_HANDLED;
}

static void work_function(struct work_struct *work){
struct mykey *dev = container_of(work,struct mykey,work);
dev->timer.data = (unsigned long)dev;
mod_timer(&dev->timer,jiffies + msecs_to_jiffies(20)); //定时
}

static void timer_function(unsigned long arg){ //定时器回调函数
struct mykey *dev = (struct mykey *)arg;
if(gpio_get_value(dev->keyio.gpio_key) == 0){
atomic_set(&dev->keyio.keyvalue,KEYVALUE);
}else{
atomic_set(&dev->keyio.keyvalue,KEYNONE);
atomic_set(&dev->keyio.releasevalue,1);
wake_up(&wqh); //唤醒等待队列头里面的进程
}
}

...

static ssize_t mykey_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt){
struct mykey *dev = filp->private_data;
int ret;
int keyvalue;
int releasevalue;

/*运行到这里会判断条件,不满足则进入休眠,直到被唤醒,然后会再次判断条件,如果还是不满足则继续休眠,满足则向下运行*/
ret = wait_event_interruptible(wqh,atomic_read(&dev->keyio.releasevalue));

...//读取逻辑
}

轮询

当使用非阻塞打开文件设备的时候,有以下三种方式实现不占CPU且可以同时访问多个文件描述符。首先就是以下三种都可以监听多个文件描述符,然后会阻塞等待监听的描述符中是否有可访问资源,当有可访问资源的时候就会退出阻塞,然后程序继续执行read/write,这样子实现了不阻塞read/write,且不会一直占用CPU,因为这三种方法会休眠等待,它们的实现方式也是基于等待队列的。至于为什么不直接阻塞read而是阻塞这三种,是因为它们可以同时监听多个文件描述符而read只能读一个,从而实现多设备同时监听、不占 CPU、read /write的IO非阻塞。所以之所以叫轮询,其实是指select/poll里面会不断循环判断文件描述符

特点 select poll epoll
最大监听数 受限制(1024) 无限制 无限制
效率 低(每次要遍历全部) 低(每次要遍历全部) 高(只返回就绪的)
内存拷贝 每次都要拷贝 fd 集合 每次都要拷贝 只需一次
使用难度 一般 一般 稍复杂
适用场景 少量 fd、嵌入式 少量 fd、嵌入式 高并发、海量连接服务器

之所以epoll快,就是因为select/poll不会告诉你谁就绪了,所以还需要自己判断所有文件描述符,但是epoll就可以告诉你谁就绪了。嵌入式基本上用不到epoll。当应用程序里面使用了select/poll的时候,驱动里面就会调用fops操作集里面的poll,在这个函数里面就可以调用poll_wait把当前进程添加进入等待队列里面

image-20260420221826619

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fd_set *readfds;	//读描述符集合
void FD_ZERO(fd_set *set); //把fd_set每个位清空
void FD_SET(int fd, fd_set *set); //把某一个位置1,也就是添加一个文件描述符
void FD_CLR(int fd, fd_set *set); //把某个位清零,也就是删除一个文件描述符
int FD_ISSET(int fd, fd_set *set); //判断某个文件描述符是在哪一个fd_set里面
struct timeval {
long tv_sec; //秒
long tv_usec; //微秒
}
struct timeval *timeout;
select(int nfds,fd_set *readfds,fd_set *writefds, fd_set *execptfds,struct timeval *timeout); //每一次轮询完就会改变fd_set参数,会把没就绪的删掉,就绪的留在里面,是根据驱动里面的poll操作里面的mask来判断文件描述符是属于哪一个描述符集合的,再次轮询需要重新设置和添加。第一个是序号最大的文件描述符+1,因为select从0遍历,有读监视,写监视,异常监视超时时间设置NULL就是完全阻塞等待,


struct pollfd {
int fd; //监听的文件描述符
short events; //想要监听的事件,可或上多种
short revents; //poll完后如果就绪,会使用mask填充这个参数
}
poll(struct pollfd *fds,nfds_t nfds,int timeout); //监听的文件描述符数组,数组大小,等待时间。poll返回的是就绪的文件描述符的个数,这个时候就需要循环判断所有文件描述符的revents位来判断谁就绪了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
static void timer_function(unsigned long arg){
struct mykey *dev = (struct mykey *)arg;
if(gpio_get_value(dev->keyio.gpio_key) == 0){
atomic_set(&dev->keyio.keyvalue,KEYVALUE);
}else{
atomic_set(&dev->keyio.keyvalue,KEYNONE);
atomic_set(&dev->keyio.releasevalue,1);
wake_up(&wq); //唤醒等待队列里面的进程
}
}
...

static ssize_t key_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt){
...
if (filp->f_flags & O_NONBLOCK) { //非阻塞访问,判断O_NONBLOCK标准来选择
if(atomic_read(&dev->releasekey) == 0)
return -EAGAIN;
} else { //阻塞访问
ret = wait_event_interruptible(dev->r_wait,atomic_read(&dev->releasekey));
atomic_read(&dev->releasekey));
}
...
}
...
unsigned int key_poll(struct file *filp, struct poll_table_struct *wait)
{
unsigned int mask = 0;
struct key_dev *dev = filp->private_data;

/*把当前进程挂到内核的poll_table等待队列挂载管理列表,然后向下执行,如果mask返回值是0,poll_table就会把当前进程添加到等待队列dev->r_wait上,然后进程进入休眠,直到wake_up唤醒,就会再次执行poll从上到下再来一遍,如果mask不为0,就说明资源可以访问了,内核就会把进程从 poll_table 移除,然后select/poll返回,应用程序就可以调用read读取*/
poll_wait(filp, &dev->r_wait, wait);

if (atomic_read(&dev->keyvalue) != 0) { //判断是否有数据可读(返回 POLLIN | POLLRDNORM 表示可读)
mask |= POLLIN | POLLRDNORM;
}

return mask; // 3. 返回状态掩码
}
...

异步通知

除了上述两种应用程序主动访问设备,还有驱动程序主动通知应用程序设备可以访问。这个时候使用的是信号,例如Ctrl+C的时候其实就是内核向进程发送了一个SIGINT信号,或者终端输入kill -9也是向进程发送了一个SIGKILL信号来终止进程,可以说信号就是类似于中断一样的东西,可以让驱动在设备资源可访问时给进程发消息通知它可以读写了

内核的信号有非常的种,常用于驱动通知应用程序的是SIGIO。首先用户在应用程序里面需要使用signal函数给SIGIO信号指定一个回调函数sign_function,然后是固定的流程,先fcntl(fd,F_SETOWN,getpid())告诉fd对应的驱动程序,信号要发送给这个进程,然后fcntl(fd,F_SETFL,flags | FASYNC)获取当前文件描述符标志位再或上FASYNC,这样子就不会干扰其它标志位,这一步会把上一步的进程加入异步通知表,这样子应用程序部分就完成了。

在驱动里面,首先需要定义一个异步通知表指针,然后还需要往fops里面添加一个.fasync操作函数,当应用程序给文件描述符标志位或上FASYNC的时候就会调用这个操作,这个操作内部就会调用fasync_helper函数把当前进程添加到异步通知表里面(这就是应用程序里面或上FASYNC之后可以把进程添加到异步通知表的原理)。然后在·需要发送信号的地方调用kill_fasync函数,就会给异步通知表里面的进程发送信号,应用程序里面的回调函数就会触发,然后在这个回调函数里面读取设备资源就行了。当关闭设备的时候还需要主动调用一次fops里面的.fasync,其实就是再次使用fasync_helper(-1,filp,0,&dev->fasync_mykey),这样子就会把当前进程从异步通知表里面去除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int fd;
const char *filename;
int databuf;
...
static void sign_function(void){ //信号回调函数,内部读取设备资源
read(fd,&databuf,sizeof(databuf));
printf("keyvalue = %d\n",databuf);
}

int main(int argc, char const *argv[]){
filename = argv[1];
open(filename,O_RDONLY);

signal(SIGIO,sign_function); //设置SIGIO信号回调函数,异步通知基本上使用的就是SIGIO信号
fcntl(fd,F_SETOWN,getpid()); //告诉fd对应的驱动程序,信号要发送给这个进程
int flags = fcntl(fd,F_GETFL); //获取fd标志位
fcntl(fd,F_SETFL,flags | FASYNC); //开启异步通知,会把上述进程加入异步通知表

while(1){
sleep(2);
}
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct mykey {
...
struct fasync_struct *fasync_mykey; //异步通知链表指针,用于存放所有要异步通知的进程
};

static void timer_function(unsigned long arg){
struct mykey *dev = (struct mykey *)arg;
if(gpio_get_value(dev->keyio.gpio_key) == 0){
atomic_set(&dev->keyio.keyvalue,KEYVALUE);
}else{
atomic_set(&dev->keyio.keyvalue,KEYNONE);
atomic_set(&dev->keyio.releasevalue,1);
kill_fasync(&dev->fasync_mykey,SIGIO,POLL_IN); //发送信号通知异步通知表里面的进程,然后应用程序就会调用回调函数
}
}

static int mykey_fasync(int fd, struct file *filp, int on) //异步通知操作函数,应用程序设置FASYNC时会调用这个来添加到异步通知表
{
struct mykey *dev = filp->private_data;
return fasync_helper(fd, filp, on, &dev->fasync_mykey); //把当前进程添加到异步通知表
}

static int mykey_release(struct inode *inode,struct file *filp){
return mykey_fasync(-1, filp, 0); //关闭文件描述符时,把当前进程从异步通知表中去除
}

/*操作集*/
static struct file_operations key_fops = {
.owner = THIS_MODULE,
...
.release = mykey_release,
.fasync = mykey_fasync,
};

Platform

在实际开发过程中,设备和驱动的分离是很重要的,可以避免大量重复代码。为此引出了总线(bus)、驱动(driver)和设备(device)模型,比如 I2C、SPI、USB 等总线。但是在 SOC 中有些外设是没有总线这个概念的,为了解决此问题,Linux 提出了 platform 这个虚拟总线,相应的就有 platform_driver 和 platform_device

platform总线已经在内核中注册好了,直接使用就可以。

platform_device设备,内核启动解析设备树,无专用总线(I2C/SPI)的普通外设节点,内核会自动创建 platform_device 并挂载到 platform 总线

我们只需要编写platform驱动就行了。platform驱动首先需要在驱动模块加载的时候注册进入platform总线,可以在原本的init/exit框架里面使用platform_driver_register完成注册,也可以使用module_platform_driver一次性完成。然后还需要实现of_match_table,probe和remove。of_match_table是用来匹配platform设备的,是通过设备树里面的compatible属性来匹配的,匹配成功之后就会执行probe函数,然后就是在probe函数里面实现设备操作集和创建设备节点,因为platform驱动和设备已经匹配成功了,probe 参数 dev 即为内核解析生成的 platform 设备,这样子就可以直接访问到设备而不需要再去主动找设备树节点。remove是卸载驱动的时候执行,完成设备号和设备节点的销毁等等。

这一套驱动-总线-设备模型和普通的方法比起来,原来不管有没有设备只要insmod加载就会创建设备节点,且驱动主动寻找设备树节点导致绑死一个设备树节点,无法通过compatible属性支持不同厂商或不同版本的硬件。而platform就会在驱动和设备都匹配的时候再创建设备节点,并且可以通过of_match_table匹配多种compatible,支持不同厂商或不同版本的硬件,实现了驱动和设备的解耦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
...//头文件

#define GPIOLED_NAME "platformdtbled"
#define GPIOLED_CNT 1
#define LED_ON 1
#define LED_OFF 0

/*设备结构体*/
struct gpioled {
...//成员参数
};

struct gpioled gpioled;

...//fops操作集

static int led_probe(struct platform_device *dev){ //platform驱动和设备匹配成功后执行
int ret = 0;
printk("OK!\n");

... //字符设备注册的标准流程

gpioled.nd = dev->dev.of_node; //关键点,驱动和设备匹配成功后,不需要使用of函数寻找设备树节点,probe里面的dev参数就是设备树,直接使用即可

gpioled.gpio_led = of_get_named_gpio(gpioled.nd,"led-gpios",0);
if(gpioled.gpio_led < 0){
ret = -EINVAL;
goto fail_get_gpio;
}

...//错误处理
}

static int led_remove(struct platform_device *dev){ //卸载驱动的时候执行
... //销毁部分
return 0;
}

static const struct of_device_id led_table[] = { //用于驱动和设备匹配的compatible表,最后一个必须是空结构体
{ .compatible = "imx6ull-atk-gpio-led" },
{ .compatible = "gpio-leds" },
{ /*sentinel*/ },
};

MODULE_DEVICE_TABLE(of,led_table); //有了这个的话就算不主动加载驱动模块,platform设备也会根据这个表自动寻找并加载匹配驱动模块,但是一般用不上,因为把驱动编译成模块是便于开发的,生产的时候就会把驱动编译到内核里面了,也就没有用了

static struct platform_driver platformdtbled = {
.driver = {
.name = "imx6ull-led", //传统匹配方式,也就是没有设备树需要自己实现platform设备时,platform设备的name与这个进行匹配
.of_match_table = led_table, //有设备树时,使用这个表匹配
},
.probe = led_probe,
.remove = led_remove,
};

module_platform_driver(platformdtbled); //使用总线模型时,可以直接使用该函数,相当于下面注释部分的功能

// static int __init gpioled_init(void){
// return platform_driver_register(&platformdtbled);
// }

// /*驱动出口函数*/
// static void __exit gpioled_exit(void){
// platform_driver_unregister(&platformdtbled);
// }

// module_init(gpioled_init);
// module_exit(gpioled_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Gnosis");

misc

misc驱动就是简化版的字符设备驱动,设备少的时候,不想占用和管理主设备号可以使用misc,一般和platform搭配使用。misc主设备号是10,内部自动完成设备注册,创建设备类并挂载到专用的misc_class(/sys/class/misc)下面,以及设备节点的添加。

image-20260422103019947

misc已经预定了一些次设备号,也可以自己定义次设备号,只要没有被使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
...//头文件

#define MISCBEEP_NAME "miscbeep"
#define MISCBEEP_MINOR 144 //只需要指定次设备号
#define BEEP_ON 1
#define BEEP_OFF 0

/*设备结构体*/
struct beep {
struct device_node *nd;
int gpio_beep;
};

struct beep beep;

...//字符设备操作集

static struct miscdevice misc_beep = { //miscdevice是misc设备结构体,内部需要指明次设备号,设备节点名称(/dev/name),字符设备操作集
.minor = MISCBEEP_MINOR,
.name = MISCBEEP_NAME,
.fops = &miscbeep_fops,
};

static int miscbeep_probe(struct platform_device *dev){
printk("probe\n");
/*platform驱动和设备匹配成功后,在里面注册misc设备,misc_register函数可以完成正常字符设备的设备号注册,设备类的创建和设备节点添加功能*/
misc_register(&misc_beep);

...
return 0;
}

static int miscbeep_remove(struct platform_device *dev){
...
misc_deregister(&misc_beep); //可以完成设备类的注销和设备节点的删除
printk("remove\n");
return 0;
}

static const struct of_device_id misc_beep_match[] = {
{.compatible = "imx6ull-atk-gpio-beep"},
{},
};

MODULE_DEVICE_TABLE(of,misc_beep_match);

static struct platform_driver miscbeep = {
.driver = {
.name = "imx6ull-beep",
.of_match_table = misc_beep_match,
},
.probe = miscbeep_probe,
.remove = miscbeep_remove,
};

module_platform_driver(miscbeep);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Gnosis");

input

input子系统分为驱动层,核心层,事件层,这种结构体现了驱动的分层,各司其职。

input核心层会向内核注册一个字符设备input,主设备号为13,并且创建input类,到时候所有创建的输入设备都会挂载在这个类下面

input_dev是核心层给驱动层提供的input设备结构体。驱动层首先需要使用input_allocate_device分配一个input_dev结构体指针,需要填充这个结构体如设备名称、设备输入类型和设备输入事件是什么。然后使用input_register_device把设备给核心层,核心层会分配次设备号,把该设备挂载到input_class(/sys/class/input内核自己创建的input类,所有input设备会在这个类下面),再根据输入设备类型匹配handler(绝大多数匹配的都是evdev_handler,这个是通用的,会把数据原封不动的传递给用户)。这个handler就是事件层处理函数,负责创建/dev/input/eventX设备节点,将接收到的输入事件传递给用户。驱动层里面会使用input_event和input_sync来给核心层发送输入数据包,核心层根据对应的类型传递给handler,handler事件层就可以把数据给放在/dev/input/eventX给用户读取。用户在应用程序里面可以通过input_event结构体读取数据。

image-20260422193254377

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
....
#define INPUTKEY_NAME "inputkey"
...
struct inputkey {
struct input_dev *inputdev; //input_dev,用于注册一个input设备
};

struct inputkey inputkey;
...
static void timer_function(unsigned long arg){
struct inputkey *dev = (struct inputkey *)arg;
if(gpio_get_value(dev->keyio.gpio_key) == 0){
input_report_key(inputkey.inputdev,BTN_0,1); //封装的input_event函数,专门给key事件使用的,用于把数据发送给核心层
input_sync(inputkey.inputdev); //上报一个同步事件,表示发送完成
}else{
/*当发送输入数据时,事件处理层的handler就会将接收到的输入数据放在`/dev/input/eventX`给用户读取。用户在应用程序里面可以通过input_event结构体读取数据*/
input_report_key(inputkey.inputdev,BTN_0,0);
input_sync(inputkey.inputdev);
}
}
...
static int inputkey_probe(struct platform_device *dev){

inputkey.inputdev = input_allocate_device(); //申请一个input_dev设备
inputkey.inputdev->name = INPUTKEY_NAME; //分配设备名字

/*********第一种设置事件和事件值的方法***********/
__set_bit(EV_KEY, inputdev->evbit); //设置输入事件类型为KEY事件
__set_bit(EV_REP, inputdev->evbit); //设置输入事件可连续输入
__set_bit(KEY_0, inputdev->keybit); //设置按键码为BTN_0

///*********第二种设置事件和事件值的方法***********/
//keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) |
//BIT_MASK(EV_REP);
//keyinputdev.inputdev->keybit[BIT_WORD(KEY_0)] |=
//BIT_MASK(KEY_0);

///*********第三种设置事件和事件值的方法***********/
//keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) |
//BIT_MASK(EV_REP);
//input_set_capability(keyinputdev.inputdev, EV_KEY, KEY_0);

ret = input_register_device(inputkey.inputdev); //注册输入设备,核心层就会分配设备号,并且挂载到input类下面,并且匹配对应的handler
...
return ret;
}

static int inputkey_remove(struct platform_device *dev){
...
return 0;
}

...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
...
struct input_event {
struct timeval time; //事件发生的事件
__u16 type; //输入事件类型
__u16 code; //按键码,KEY_0,KEY_1等等
__s32 value; //按键值
};
int main(int argc, char const *argv[]){
...
static struct input_event databuf;
...
while(1){
retvaluel = read(fd,&databuf,sizeof(databuf));
if(retvaluel < 0){
}else{
switch(databuf.type){
case EV_KEY:
printf("key %d %s\n",databuf.code,databuf.value?"press":"release");
break;
case EV_REL:
break;
default:
break;
}
}
}
return 0;
}

RTC

RTC 也就是实时时钟,用于记录当前系统时间,是一个标准的字符设备驱动,应用程序通过 open、release、read、write 、ioctl等函数完成对 RTC 设备的操作

Linux把RTC设备抽象为结构体rtc_device,该结构体下面有统一的rtc操作集rtc_class_ops。Linux内核有一个标准的platform框架的RTC驱动,在fops操作集里面调用了rtc_class_ops里面的接口函数,所以需要Soc厂商编写设备驱动完善rtc_class_ops。Soc厂商根据自己的板子编写操作RTC设备寄存器的函数用于填充rtc_class_ops结构体,在probe函数里面调用rtc_device_register传入RTC设备结构体和rtc_class_ops结构体,会注册一个rtc_device结构体,并且在/dev创建rtc设备节点。这样子厂商RTC驱动和Linux的RTC标准驱动就连接上了,用户操作这个设备节点,rtc标准驱动就会调用rtc_device下面的操作集来访问RTC设备

image-20260422210343784

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct rtc_class_ops {
int (*open)(struct device *);
void (*release)(struct device *);
int (*ioctl)(struct device *, unsigned int, unsigned long);
int (*read_time)(struct device *, struct rtc_time *);
int (*set_time)(struct device *, struct rtc_time *);
int (*read_alarm)(struct device *, struct rtc_wkalrm *);
int (*set_alarm)(struct device *, struct rtc_wkalrm *);
int (*proc)(struct device *, struct seq_file *);
int (*set_mmss64)(struct device *, time64_t secs);
int (*set_mmss)(struct device *, unsigned long secs);
int (*read_callback)(struct device *, int data);
int (*alarm_irq_enable)(struct device *, unsigned int enabled);
};

一般板子里面已经编写好了rtc设备驱动,直接使用即可

1
2
3
date	//查看时间
date -s "2019-08-31 18:13:00" //设置时间
hwclock -w //将当前系统时间写入到 RTC 里面

I2C

I2C和platform一样,只不过platform是虚拟总线,而i2c是真实存在的总线。Linux的I2C驱动有三个部分,i2c适配器驱动,i2c设备驱动,i2c核心层。

i2c适配器驱动

i2c适配器驱动一般是芯片厂商根据自己的i2c外设编写,比如NXP的适配器驱动是基于platform的。Linux使用i2c_adapter结构体表示一个适配器,而适配器驱动的工作就是编写访问芯片i2c寄存器的方法,然后赋值给i2c_adapter->i2c_algorithm,i2c_algorithm 就是 I2C 适配器与 IIC 设备进行通信的方法。然后会调用i2c_add_adapter把适配器注册到核心层里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static struct i2c_algorithm i2c_imx_algo = {	//把I2C适配器与I2C设备进行通信的方法赋值给i2c_algorithm 
.master_xfer = i2c_imx_xfer,
.functionality = i2c_imx_func,
};

static int i2c_imx_probe(struct platform_device *pdev)
{
...
/* Setup i2c_imx driver structure */
strlcpy(i2c_imx->adapter.name, pdev->name, sizeof(i2c_imx->adapter.name));
i2c_imx->adapter.owner = THIS_MODULE;
i2c_imx->adapter.algo = &i2c_imx_algo; //将i2c_algorithm赋值给i2c_adapter,这样子适配器就有了访问i2c设备的方法
i2c_imx->adapter.dev.parent = &pdev->dev;
i2c_imx->adapter.nr = pdev->id;
i2c_imx->adapter.dev.of_node = pdev->dev.of_node;
i2c_imx->base = base;
...

/* Add I2C adapter */
//i2c_add_adapter(&i2c_imx->adapter); //使用动态总线号把适配器注册到核心层
ret = i2c_add_numbered_adapter(&i2c_imx->adapter); //使用静态总线号把适配器注册到核心层
...
}

i2c核心层驱动

因为i2c_add_adapter是核心层的函数,在这个函数里面就会调用i2c_register_adapter,设置适配器名字类型和总线类型并注册进入内核,i2c_bus_type就是i2c总线的结构体,这样子就可以在 /sys/bus/i2c/devices/看到适配器i2c-x了。而且在核心层里面,会有统一定义的i2c数据传输函数i2c_transfer,并且进行二次封装实现发送和接收函数,而在i2c_transfer内部就会调用适配器的master_xfer函数,这样子就能根据厂商写的适配器驱动操作i2c设备了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int i2c_add_adapter(struct i2c_adapter *adapter){
...
return i2c_register_adapter(adapter);
}

static int i2c_register_adapter(struct i2c_adapter *adap){
...
dev_set_name(&adap->dev, "i2c-%d", adap->nr);//设置适配器名字
adap->dev.bus = &i2c_bus_type; //设置适配器为i2c总线
adap->dev.type = &i2c_adapter_type; //设置适配器类型为i2c适配器
res = device_register(&adap->dev); //把适配器注册到内核
...
}

int __i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num){ //内部调用适配器里面的master_xfer
ret = adap->algo->master_xfer(adap, msgs, num);
}
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num){ //数据传输函数
ret = __i2c_transfer(adap, msgs, num);
}
int i2c_master_send(const struct i2c_client *client, const char *buf, int count){ //标准发送函数
ret = i2c_transfer(adap, &msg, 1);
}

int i2c_master_recv(const struct i2c_client *client, char *buf, int count){ //标准接收函数
ret = i2c_transfer(adap, &msg, 1);
}

i2c设备驱动

这个是需要自己编写的驱动,调用核心层提供的收发函数完成与i2c设备通信。需要使用i2c_driver驱动框架来匹配设备树里面的i2c设备,匹配成功之后调用probe函数,并且在/sys/bus/i2c/devices/看到设备0-001e(使用i2c-0,从机地址001e)参数client就是Linux分配给设备的结构体,Linux用这个结构体描述i2c设备,核心层会自动把client挂载到i2c总线上。这样子当用户读取设备数据的时候,会通过client调用i2c_smbus_read_byte_data,该函数内部就会调用i2c_transfer,i2c_transfer调用适配器的master_xfer函数,master_xfer又连接到厂商写的适配器操作函数,这样子就连通起来了

1
2
3
4
5
6
7
8
9
10
11
&i2c1 {
clock-frequency = <100000>; //设置i2c传输速率
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";

ap3216c@1e { //@后面就是从机地址
compatible = "alientek,ap3216c";
reg = <0x1e>; //从机设备地址
};
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct bus_type i2c_bus_type = {
.name = "i2c",
.match = i2c_device_match, //.match就是I2C总线的设备和驱动匹配函数,也就是比较of_device_id和设备节点的compatible来完成匹配,成功之后就会分配一个client给设备
.probe = i2c_device_probe,
.remove = i2c_device_remove,
.shutdown = i2c_device_shutdown,
};

struct i2c_client {
unsigned short flags;
unsigned short addr;
char name[I2C_NAME_SIZE];
struct i2c_adapter *adapter; //i2c适配器
struct device dev;
int irq;
struct list_head detected;
#if IS_ENABLED(CONFIG_I2C_SLAVE)
i2c_slave_cb_t slave_cb;
#endif
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
...	//头文件

...//设备寄存器地址

#define AP3216C_NAME "ap3216c"
#define AP3216C_CNT 1

struct ap3216c_dev {
struct i2c_client *client; //用于接收Linux分配给设备的i2c_client结构体

};

struct ap3216c_dev ap3216c_dev;

static unsigned char ap3216c_readdata(struct ap3216c_dev *dev){
buf[i] = i2c_smbus_read_byte_data(client,IRDATALOW + i); //内部调用的就是i2c_transfer
}

static unsigned char ap3216c_writedata(struct ap3216c_dev *dev){
i2c_smbus_write_byte_data(client,SYSCONFIG,0x04); //内部调用的就是i2c_transfer
}

...//字符设备操作集


static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id){
... //字符设备注册流程

ap3216c_dev.client = client; //把client赋值给dev设备

... //错误处理
}

static int ap3216c_remove(struct i2c_client *client){
...
}

static const struct i2c_device_id ap3216c_i2c_id[] = { //传统匹配方式ID列表
{ "atk,ap3216c", 0 },
{ }
};

static const struct of_device_id ap3216c_of_match_table[] = { //设备树匹配表
{ .compatible = "atk,ap3216c", },
{ },
};

static struct i2c_driver ap3216c_driver = { //i2c_driver和platform_driver一样,是i2c设备驱动框架
.driver = {
.owner = THIS_MODULE,
.name = "ap3216c",
.of_match_table = ap3216c_of_match_table,
},
.probe = ap3216c_probe,
.remove = ap3216c_remove,
.id_table = ap3216c_i2c_id,
};

module_i2c_driver(ap3216c_driver); //和module_platform_driver一样的作用,会把从机驱动注册到总线,可以在/sys/bus/i2c/drivers看到

...//开发者信息

SPI

spi主机控制器

Linux下的SPI驱动分为spi_master主机控制器设备和spi_driver从机驱动。spi_master结构体就类似于iic的适配器,该结构体下面有transfer函数,这个transfer函数就是厂商需要根据自己的芯片spi来编写的与spi设备通信的时序方法。主机驱动首先要使用spi_alloc_master申请一个spi_master,编写好spi_master之后需要使用spi_register_masterspi总线注册这个spi主机,这个时候就可以在/sys/bus/spi/devices/看到这个主机设备了。

1
2
3
4
5
6
7
8
9
10
11
struct spi_master {
....
(*transfer)(struct spi_device *spi,struct spi_message *mesg); //需要厂商根据自己芯片的spi完成通信时序
...
}
struct spi_master *spi_alloc_master(struct device *dev, unsigned size); //申请一个spi_master指针
void spi_master_put(struct spi_master *master); //释放spi_master指针

int spi_register_master(struct spi_master *master)//注册spi_master,此操作会把spi_master注册进入spi总线,可以在/sys/bus/spi/devices/看到
void spi_unregister_master(struct spi_master *master); //注销设备

spi设备驱动

然后就是spi_driver设备驱动,和i2c的i2c_driver一样的驱动框架,调用module_spi_driver就会把设备驱动注册到spi总线,可以在/sys/bus/spi/drivers看到设备驱动,驱动和设备匹配成功后会调用probe函数,参数spi_device就是spi设备结构体,这时候就可以在/sys/bus/spi/devices看到设备。其中spi总线结构体就是spi_bus_type,里面的.match就是根据of_device_id和设备树节点的compatible用来匹配spi设备和spi设备驱动的。这样子主机和从机都有了,接下来就是通信方法。spi总线使用一个spi_transfer结构体由于存放需要收发的数据,然后会把spi_transfer放到一个spi_message结构体队列中,而主机驱动里面的transfer函数的参数就是spi_devicespi_message,当使用spi_sync发送数据的时候就会调用spi_master->transfer遍历spi_message列表里面的spi_transfer,根据厂商自己定义的时序方法来完成数据的收发

1
2
3
4
5
6
7
8
9
10
11
12
13
&ecspi3 {
fsl,spi-num-chipselects = <1>; //设置片选数量为1
cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>; //片选线
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_ecspi3>;
status = "okay";

spidev: icm20608@0 { //从机设备,@后面不是从机地址了,spi使用片选号来选择设备
compatible = "alientek,icm20608";
spi-max-frequency = <8000000>; //设置spi传输速率,spi的从机可以设置不同传输频率,但是i2c不可以,所以i2c的频率是写在控制器里面的
reg = <0>; //片选号
};
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct bus_type spi_bus_type = {
.name = "spi",
.dev_groups = spi_dev_groups,
.match = spi_match_device, //设备树节点和驱动匹配方法,也是对比of_device_id和compatible
.uevent = spi_uevent,
};

struct spi_transfer {
...
const void *tx_buf; //发数据
void *rx_buf; //收数据
unsigned len; //数据长度,因为是同步全双工,收发长度一样
...
};

struct spi_message {
struct list_head transfers; // spi_transfer队列
...
void (*complete)(void *context); //设置异步传输时需要设置这个回调函数,异步传输完成后调用这个回调函数
...
struct spi_device *spi; //从机设备
}
void spi_message_init(struct spi_message *m); //初始化spi_message
void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m); //把spi_transfer加入spi_meaasge队列
int spi_sync(struct spi_device *spi, struct spi_message *message); //同步传输,也就是会等待数据传输完成
int spi_async(struct spi_device *spi, struct spi_message *message); //异步传输
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
...//spi从机寄存器地址

#define MISCICM20608_NAME "icm20608"
#define MISCICM20608_MINOR 144

/*设备结构体*/
struct icm20608_dev {
...
struct spi_device *spi;
};

struct icm20608_dev icm20608_dev;

static void icm20608_readdata(struct icm20608_dev *dev, u8 reg,u8 *buf, int len){
struct spi_device *spi = (struct spi_device*)dev->spi;
...
spi_write_then_read(spi, &tx, sizeof(tx), buf, len); //同下
}

static void icm20608_writedata(struct icm20608_dev *dev, u8 reg, u8 buf){
struct spi_device *spi = (struct spi_device*)dev->spi; //这里相当于获取到了spi_device从机设备
...//设置数据和寄存器地址
/*里面会调用spi_sync/spi_async发送,这个时候就会调用spi_master->transfer遍历spi_message列表里面的spi_transfer*/
spi_write(spi, tx, sizeof(tx));
}

...//这一次使用的是misc,这里是操作集

static int icm20608_probe(struct spi_device *spi){ //驱动和设备匹配成功执行,参数就是spi从机设备
...
icm20608_dev.spi = spi;
...
}

static int icm20608_remove(struct spi_device *spi){
...
}

static const struct spi_device_id icm20608_spi_id[] = { //原始匹配表
{ "atk,icm20608", 0 },
{ }
};

static const struct of_device_id icm20608_of_match_table[] = { //设备树匹配表
{ .compatible = "atk,icm20608"},
{ },
};

static struct spi_driver icm20608_driver = {
.driver = {
.owner = THIS_MODULE,
.name = "icm20608",
.of_match_table = icm20608_of_match_table,
},
.probe = icm20608_probe,
.remove = icm20608_remove,
.id_table = icm20608_spi_id,
};

module_spi_driver(icm20608_driver); //注册设备驱动到spi总线,可以在/sys/bus/spi/drivers看到设备驱动

...//模块信息
0%