字符设备驱动-pinctrl子系统

1 pinctrl和gpio subsystem引入#

上一节引入gpio了子系统:

linux内核驱动-gpio子系统 - fuzidage - 博客园 (cnblogs.com)

字符设备驱动-gpio子系统 | Hexo (fuzidage.github.io)

Linux 驱动讲究驱动分离与分层,pinctrl 和 gpio 子系统就是驱动分离与分层思想下的产物。
pinctrl顾名思义就是引脚控制,用来配置比如引脚mux复用信息,引脚电器属性(比如上/下拉、速度、驱动能力等)信息。
gpio顾名思义就是控制gpio的输入输出,以及高低电平。不过,大多数的芯片并没有单独的IOMUX模块,引脚的复用、配置等,而是在GPIO模块内部实现的。
image

2 pinctrl子系统原理介绍#

2.1 pinctrl子系统#

2.1.1 pinctrl子系统软件架构#

pinctrl子系统源码路径是linux/drivers/pinctrl:
image

image

1
2
3
4
其他驱动层(client):具体到使用系统pin资源的设备驱动程序
pinctrl核心层(core):内核抽象出来,向下为SoC pin controler drvier提供底层通信接口的能力,
向上为其他驱动提供了控制pin的能力,比如pin复用、配置引脚的电气特性,同时也为GPIO子系统提供pin操作。
pin控制器驱动层(pinctrl-driver): 提供了操作pin的具体方法。

pinctrl-driver主要为pinctrl-core提供pin的操作能力。把系统所有的pin以及对于pin的控制接口实例化成pinctrl_desc,并将pinctrl_desc注册到pinctrl-core中去。

2.1.2 Pinctrl重要概念#

Documentation\devicetree\bindings\pinctrl\pinctrl-bindings.txt有介绍重要相关概念:

1. pin controller:
芯片手册里你找不到 pin controller,它是一个软件上的概念。对应 IOMUX──用来复用引脚,还可以配置引脚(比如上下拉电阻等)。
2. client device:
“客户设备”,客户是指Pinctrl系统的客户,即使用Pinctrl系统的设备,使用引脚的设备
image

    1. pin state
      对于一个"client device",如UART设备,它有多个“状态”default、sleep等,那么对应的引脚也有这些状态。

    比如,默认状态下,UART设备正常工作,那么所用的引脚就要复用为UART功能;
    休眠状态下,为了省电,可以把这些引脚复用为GPIO功能;或者直接把它们配置输出高电平。

    上图pinctrl-names定义2种状态:default,sleep
    第0种状态用到的引脚在pinctrl-0中定义,它是state_0_node_a,位于pincontroller节点中。
    第1种状态用到的引脚在Pinctrl-1中定义,它是state_1_node_a,位于pincontroller节点中。

    当UART设备处于default状态时,pinctrl子系统会自动根据上述信息将所用引脚复用为uart0功能。
    当UART设备处于sleep状态时,pinctrl子系统会自动根据上述信息将所用引脚配置为高电平。

    1. groups和function
      一个设备会用到一个或多个引脚,这些引脚可以归纳为一组(group)
      这些引脚可以复用为某个功能:function,如I2C功能,SPI功能,GPIO功能等。当然:一个设备可以用到多组引脚,比如A1、A2两组引脚,A1组复用为F1功能,A2组复用为F2功能:
      image
    1. Generic pin multiplexing node和Generic pin configuration node
      下图左边pin controller节点中,有子节点或孙节点,它们是给client device使用的。
      可用来描述复用信息:哪组(group)引脚复用为哪个功能(function)
      配置信息:哪组(group)引脚配置为哪个设置功能(setting),如上拉、下拉等;

image

2.1.3 pinctrl 子系统注册流程#

以imx6ull为例,drivers/pinctrl/freescale/pinctrl-imx6ul.c
image

驱动的入口是 arch_initcall 中声明的函数,类似于我们经常写的 module_init是动态ko加载,arch_initcall是编译进入内核镜像。可以看到是利用platform_device框架来写的。
image
根据 compatible设备树的compatible 字段进行匹配,匹配成功执行 probe 函数,调用imx_pinctrl_probe_dt解析dts中的pinctrl描述信息。调用pinctrl_register或者devm_pinctrl_register注册pinctl子系统。

3 soc pinctrl主要结构体(controller)#

3.0 数据结构关系图#

总框图记录数据结构之间的关联。
1876680-20240315174913445-1187095793

1
2
3
4
5
6
7
8
9
10
//pincontroller数据结构
drivers\pinctrl\core.h
include\linux\pinctrl\pinctrl.h
include\linux\pinctrl\pinmux.h
include\linux\pinctrl\pinconf.h
//client数据结构
drivers\pinctrl\core.h
include\linux\pinctrl\devinfo.h
include\linux\device.h
include\linux\pinctrl\machine.h

3.1 pinctrl_dev#

pinctrl_dev 是 pinctrl 子系统的根源结构体。

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 pinctrl_dev {
struct list_head node;
struct pinctrl_desc *desc; //提供具体操作方法和抽象包括pincrtl_ops函数,pinmux操作函数和pin的描述等
struct radix_tree_root pin_desc_tree;
#ifdef CONFIG_GENERIC_PINCTRL_GROUPS
struct radix_tree_root pin_group_tree;//存储group的描述信息
unsigned int num_groups;
#endif
#ifdef CONFIG_GENERIC_PINMUX_FUNCTIONS
struct radix_tree_root pin_function_tree;//存储function的描述信息
unsigned int num_functions;
#endif
struct list_head gpio_ranges;//链接gpio range 2 pin range相关的信息
struct device *dev;
struct module *owner;
void *driver_data;
struct pinctrl *p;//每个pinctrl都描述着一组gpio的复用和状态配置,
//如果这个pinctrl_dev是一个通过iic连接的,那么使用这个pinctrl_dev
//就需要配置其占用的gpio为iic功能,那么就要用这个来描述
struct pinctrl_state *hog_default;
struct pinctrl_state *hog_sleep;
struct mutex mutex;
#ifdef CONFIG_DEBUG_FS
struct dentry *device_root;
#endif
}

/driver/pinctrl/core.c中注册pinctrl时将soc中所有的pinctrl_dev挂载到pinctrl_dev_list链表中方便查询使用。

1
2
struct pinctrl_dev *pinctrl_register(struct pinctrl_desc *pctldesc,
struct device *dev, void *driver_data);

3.2 pinctrl_desc#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct pinctrl_desc {
const char *name;
const struct pinctrl_pin_desc *pins; //描述一个pin控制器的引脚,
unsigned int npins; //描述该控制器有多少个引脚
const struct pinctrl_ops *pctlops; //引脚操作函数,有描述引脚,获取引脚等
const struct pinmux_ops *pmxops; //引脚复用相关的操作函数
const struct pinconf_ops *confops; //引脚配置相关的操作函数
struct module *owner;
#ifdef CONFIG_GENERIC_PINCONF
unsigned int num_custom_params;
const struct pinconf_generic_params *custom_params;
const struct pin_config_item *custom_conf_items;
#endif
};

image
image

三个ops:

  1. group操作接口对应数据结构struct pinctrl_ops,包含get_groups_countget_group_nameget_group_pins等接口;

  2. mux操作接口对应的数据结构为struct pinmux_ops,包含pin requestfreeset_muxget_functions_countget_function_groups等;

  3. function操作接口对应的数据结构为struct pinconf_ops,包含pin_config_setpin_config_getpin_config_group_getpin_config_group_set等接口;

3.2.1 pinctrl_pin_desc#

1
2
3
4
5
struct pinctrl_pin_desc {
unsigned number;//引脚序号
const char *name;//引脚名
void *drv_data;
};

pinctrl_pin_desc来描述一个引脚.

3.2.2 pin_desc#

1
2
3
4
5
6
7
8
9
10
11
12
13
struct pin_desc {
struct pinctrl_dev *pctldev;
const char *name;
bool dynamic_name;
void *drv_data;
/* These fields only added when supporting pinmux drivers */
#ifdef CONFIG_PINMUX
unsigned mux_usecount;
const char *mux_owner;
const struct pinctrl_setting_mux *mux_setting;
const char *gpio_owner;
#endif
};

记录引脚的使用计数、引脚当前所属的function、group信息(该数据结构主要是pinctrl子系统用于判断一个引脚是否被多次配置不同的复用情况使用(pin_request、pin_free

3.2.3 三个ops#

3.2.3.1 pinctrl_ops#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct pinctrl_ops {
//获取组数
int (*get_groups_count) ();
//获取组名
const char *(*get_group_name) ();
//获取某组的引脚
int (*get_group_pins) ();
//用以debugfs提供每个引脚的信息
void (*pin_dbg_show) ();
//解析设备树节点,转换成pinctrl_map,重点
int (*dt_node_to_map) ();
//释放map
void (*dt_free_map) ();
};
  • 来取出某组的引脚:get_groups_count、get_group_pins
  • 处理设备树中pin controller中的某个节点创建映射:dt_node_to_map,把device_node转换为一系列的pinctrl_map

3.2.3.2 pinmux_ops#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct pinconf_ops {
#ifdef CONFIG_GENERIC_PINCONF
bool is_generic;
#endif
//获取单个引脚配置
int (*pin_config_get) ();
//配置单个引脚
int (*pin_config_set) ();
//获取某组引脚配置
int (*pin_config_group_get) ();
//配置某组引脚
int (*pin_config_group_set) ();
//用以debugfs修改pin配置信息
int (*pin_config_dbg_parse_modify) ();
//用以debugfs提供pin配置信息
void (*pin_config_dbg_show) ();
//用以debugfs提供group配置信息
void (*pin_config_group_dbg_show) ();
//用以debugfs解析并显示pin的配置
void (*pin_config_config_dbg_show) (s);
};

3.2.3.3 pinconf_ops#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct pinconf_ops {
#ifdef CONFIG_GENERIC_PINCONF
bool is_generic;
#endif
//获取单个引脚配置
int (*pin_config_get) ();
//配置单个引脚
int (*pin_config_set) ();
//获取某组引脚配置
int (*pin_config_group_get) ();
//配置某组引脚
int (*pin_config_group_set) ();
//用以debugfs修改pin配置信息
int (*pin_config_dbg_parse_modify) ();
//用以debugfs提供pin配置信息
void (*pin_config_dbg_show) ();
//用以debugfs提供group配置信息
void (*pin_config_group_dbg_show) ();
//用以debugfs解析并显示pin的配置
void (*pin_config_config_dbg_show) (s);
};

4 board pinctrl相关结构体(使用者client)#

4.0 数据结构关系#

前面Soc pin描述相关的数据结构,已经搭建了该soc所支持的pinfunctiongroup以及相关操作接口等信息;而board pin描述相关的数据结构则描述一块board所使用到的function及相关的group
1876680-20240316150501629-222504717

4.1 pinctrl_map#

1
2
3
4
5
6
7
8
9
10
11
12
struct pinctrl_map {
const char *dev_name;//设备名称
const char *name;//该pinctrl_map对应的状态(default、idle、sleep等
enum pinctrl_map_type type;//pinctrl_map的类型,包括mux group、config group、config pins等
const char *ctrl_dev_name;//pinctrl device的名称,根据该名称可获取到soc pin controller对应的pinctrl device
union {
struct pinctrl_map_mux mux;//引脚复用的内容,该数据结构中包含function名称、group名称,
//通过function、group就可以确定进行引脚复用的引脚id与引脚复用值等信息;
struct pinctrl_map_configs configs;//引脚配置相关的内容,包括group或者pin的名称,
//以及该group、pin的配置信息,实现引脚配置操作。
} data;
};

该数据结构可以理解为一个function类型:

4.2 dev_pin_info#

device结构体中有一个dev_pin_info,用来保存设备的pinctrl信息:
image

1
2
3
4
5
6
7
8
9
10
struct dev_pin_info {    //该device对应引脚的配置与复用信息
struct pinctrl *p;//该设备支持引脚配置类型(包括支持的pinctrl状态,
//每一种pinctrl状态下的引脚复用以及引脚配置信息)
struct pinctrl_state *default_state;
struct pinctrl_state *init_state;
#ifdef CONFIG_PM
struct pinctrl_state *sleep_state;
struct pinctrl_state *idle_state;
#endif
};

4.2.1 pinctrl#

一个设备的所有引脚配置相关的信息:

1
2
3
4
5
6
7
8
struct pinctrl {
struct list_head node;
struct device *dev;
struct list_head states;
struct pinctrl_state *state;//states下链接了该设备支持的所有引脚配置状态
struct list_head dt_maps;
struct kref users;
};
4.2.1.1 pinctrl_state#
1
2
3
4
5
struct pinctrl_state {
struct list_head node;
const char *name;
struct list_head settings;
};

image

5 pinctrl实例化示例#

5.1 pin controller示例#

打开imx6ull的dtsi找到pin controller控制器节点,对比IMX6ULL参考手册可知:imx6ull一共3个IOMUX控制器
image

5.2 client示例#

evk公板为例:
image
imx6ull pinmux dts配置描述:
我们看到:

1
MX6UL_PAD_UART1_RTS_B__GPIO1_IO19       0x17059 /* SD1 CD */

它是表示什么意思呢?配置了哪些信息呢?
image

UART1_RTS_B复用成GPIO1_IO19:
image

此宏定义在imx_6ull_pinfunc.h, 后面跟着 5 个数字,也就是这个宏定义的具体值,如下所示:

1
0x0090 0x031C 0x0000 0x5 0x0

这 5 个值的含义如下所示:

1
<mux_reg conf_reg input_reg mux_mode input_val>

0x0090表示UART1_RTS_B的iomux寄存器
0x031C表示UART1_RTS_B的电器属性寄存器
0x0000表示input寄存器
0x5表示复用成模式5
0x0表示input寄存器设置值为0x0

注意还少了一项电器属性的值啊?别急定义在dts中,0x17059就刚好是电器属性的值。

5.2.1 client如何使用pinctrl#

前面讲过client包含device结构体,每个device结构体里都有一个dev_pin_info结构体,用来保存设备的pinctrl信息。platform_device匹配driver会执行probe,probe前会进行pinctrl处理,处理函数为pinctrl_bind_pins

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
platform_device_register
platform_device_add
device_add
bus_probe_device;
device_initial_probe
__device_attach
bus_for_each_drv(dev->bus, NULL, &data, __device_attach_driver);
// 对于plarform_bus_type下的每一个driver, 调用__device_attach_driver

__device_attach_driver
driver_match_device(drv, dev);
drv->bus->match(dev, drv)// 调用platform_bus_type.match
driver_probe_device
really_probe
/* If using pinctrl, bind pins now before probing */
pinctrl_bind_pins
drv->probe //执行driver中的probe函数

5.2.1.1 pinctrl_bind_pins过程#

  • 构造pinctrl
    • 通过pinctrl_ops.dt_node_to_map将设备树节点转换成一系列pinctrl_map
    • pinctrl_map转换成pinctrl_setting,放入settings链表,记录在pinctrl_state
  • 选择state,遍历settings链表,进行pinctrl的mux和config
    image

函数调用过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pinctrl_bind_pins
/* 分配dev_pin_info结构体 */
devm_kzalloc(dev, sizeof(*(dev->pins)), GFP_KERNEL);
/* 获取pinctrl */
devm_pinctrl_get(dev);
pinctrl_get
/* 构建pinctrl */
create_pinctrl(dev);
/* 分配pinctrl */
p = kzalloc(sizeof(*p), GFP_KERNEL);
/* 设备树节点转换为pinctrl_map */
pinctrl_dt_to_map(p);
/* 每个pinctrl_map,又被转换为一个pinctrl_setting,添加到setting链表 */
for_each_maps(maps_node, i, map) {
ret = add_setting(p, map);
}

节点转换为pinctrl_map过程

点击查看代码
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
pinctrl_dt_to_map(p);
for (state = 0; ; state++) {
propname = kasprintf(GFP_KERNEL, "pinctrl-%d", state);
/* 取出pinctrl-%d节点属性 */
prop = of_find_property(np, propname, &size);
list = prop->value;
size /= sizeof(*list);
/* 对pinctrl-%d中的每一个phandle进行pinctrl_map转换 */
/* 例如pinctrl-0 = <&state_0_node_a &state_0_node_b>有两个phandle */
for (config = 0; config < size; config++) {
/* 根据phandle找到对应节点 */
np_config = of_find_node_by_phandle(phandle);
/* Parse the node */
dt_to_map_one_config(p, statename, np_config);
/* 调用Pincontroller中dt_node_to_map函数,构造pinctrl_map */
ops->dt_node_to_map()
/* 将pinctrl_map添加到maps链表 */
dt_remember_or_free_map
pinctrl_register_map
list_add_tail
}
}

/* fy00的dt_node_to_map函数 */
mc_pctrl_dt_node_to_map()
/* 取出每一个子节点 */
for_each_child_of_node(np_config, np) {
mc_pctrl_dt_subnode_to_map
/* 获取设备树pinmux属性 */
pins = of_find_property(node, "pinmux", NULL);
num_pins = pins->length / sizeof(u32);
for (i = 0; i < num_pins; i++) {
of_property_read_u32_index(node, "pinmux",i, &pinfunc);
/* 解析设备树,将pinmux属性中每一个成员记录在pin和func中 */
pin = MC_GET_PIN_NO(pinfunc);
func = MC_GET_PIN_FUNC(pinfunc);
/* 设置复用的pinctrl_map */
mc_pctrl_dt_node_to_map_func();
(*map)[*num_maps].type = PIN_MAP_TYPE_MUX_GROUP;
(*map)[*num_maps].data.mux.group = grp->name;
(*map)[*num_maps].data.mux.function = mc_gpio_functions[fnum];
(*num_maps)++;
if (has_config) {
/* 设置配置pinctrl_map,fy00没有配置pinctrl_map */
pinctrl_utils_add_map_configs
(*map)[*num_maps].type = type;
(*map)[*num_maps].data.configs.group_or_pin = group;
(*map)[*num_maps].data.configs.configs = dup_configs;
(*map)[*num_maps].data.configs.num_configs = num_configs;
(*num_maps)++;
}
}
}

pinctrl_map转换为pinctrl_setting过程

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
for_each_maps() {
add_setting();
find_state();
if (!state)
/* 第一次添加state到states链表 */
create_state(p, map->name);
list_add_tail(&state->node, &p->states);
/* 将map的name和tpye赋值给setting */
setting->type = map->type;
setting->dev_name = map->dev_name;
switch (map->type) {
/* MUX类型 */
case PIN_MAP_TYPE_MUX_GROUP:
pinmux_map_to_setting(map, setting);
/* 将pinctrl_map中的function字符串转换为序号,赋值给setting*/
pinmux_func_name_to_selector(pctldev, map->data.mux.function);
setting->data.mux.func = ret;
/* 将pinctrl_map中的group字符串转换为序号,赋值给setting */
ret = pinctrl_get_group_selector(pctldev, group);
setting->data.mux.group = ret;
/* CONFIGS类型 */
case PIN_MAP_TYPE_CONFIGS_PIN:
case PIN_MAP_TYPE_CONFIGS_GROUP:
pinconf_map_to_setting(map, setting);
/* 从pinctrl_map取出pin或group赋值给setting */
setting->data.configs.group_or_pin = pin;
/* 将pinctrl_map的configs赋值给setting */
setting->data.configs.num_configs = map->data.configs.num_configs;
setting->data.configs.configs = map->data.configs.configs;
}
/* 添加到settings链表 */
list_add_tail(&setting->node, &state->settings);
}

pin脚的复用和配置过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pinctrl_bind_pins
/* 寻找state */
pinctrl_lookup_state
/* 选择state */
pinctrl_select_state
pinctrl_commit_state
/* 遍历settings链表 */
list_for_each_entry(setting, &state->settings, node) {
switch (setting->type) {
case PIN_MAP_TYPE_MUX_GROUP:
/* 设置复用 */
pinmux_enable_setting(setting);
ops->set_mux(...);
case PIN_MAP_TYPE_CONFIGS_PIN:
case PIN_MAP_TYPE_CONFIGS_GROUP:
/* 设置配置 */
pinconf_apply_setting(setting);
ops->pin_config_group_set(...);
}

字符设备驱动-gpio子系统

1 gpio 子系统引入#

如果 pinctrl 子系统将一个 PIN 复用为 GPIO 的话,那么接下来要用到 gpio 子系统了。gpio 子系统顾名思义,就是用于初始化 GPIO 并且提供相应的 API 函数,比如设置 GPIO为输入输出,设置读取 GPIO 的值等。

gpio 子系统的主要目的就是方便驱动开发者使用 gpio,驱动开发者在设备树中添加 gpio 相关信息,然后就可以在驱动程序中使用 gpio 子系统提供的 API函数来操作 GPIO,Linux 内核向驱动开发者屏蔽掉了 GPIO 的设置过程,极大的方便了驱动开发者使用 GPIO。

2 gpio子系统架构#

Linux的GPIO子系统驱动框架由三个主要部分组成:① GPIO控制器驱动程序、②gpio lib驱动程序 ③GPIO字符设备驱动程序:
image-20240817202346143
使用gpiochip_add/gpiochip_add_data向系统注册gpio_chip, 这些都是半导体原厂要做的,设备商只需要使用即可。

2.0 gpio控制器源码分析#

drivers/gpio/gpio-mxc.c 就是 I.MX6ULL的 GPIO 控制器驱动文件,在此文件中有如下所示of_device_id 匹配表:
image
对照imx6ull.dtsi的gpio控制器可以看到能匹配:
image

打开drivers/gpio/gpio-mxc.c
image
probe函数内容如下:

点击查看代码
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123

static int mxc_gpio_probe(struct platform_device *pdev){
struct device_node *np = pdev->dev.of_node;
struct mxc_gpio_port *port;
struct resource *iores;
int irq_base = 0;
int err;

mxc_gpio_get_hw(pdev);

port = devm_kzalloc(&pdev->dev, sizeof(*port), GFP_KERNEL);
if (!port)
return -ENOMEM;

iores = platform_get_resource(pdev, IORESOURCE_MEM, 0);
port->base = devm_ioremap_resource(&pdev->dev, iores);
if (IS_ERR(port->base))
return PTR_ERR(port->base);

port->irq_high = platform_get_irq(pdev, 1);
port->irq = platform_get_irq(pdev, 0);
if (port->irq < 0)
return port->irq;

/* the controller clock is optional */
port->clk = devm_clk_get(&pdev->dev, NULL);
if (IS_ERR(port->clk))
port->clk = NULL;

err = clk_prepare_enable(port->clk);
if (err) {
dev_err(&pdev->dev, "Unable to enable clock.\n");
return err;
}

pm_runtime_set_active(&pdev->dev);
pm_runtime_enable(&pdev->dev);
err = pm_runtime_get_sync(&pdev->dev);
if (err < 0)
goto out_pm_dis;

/* disable the interrupt and clear the status */
writel(0, port->base + GPIO_IMR);
writel(~0, port->base + GPIO_ISR);

if (mxc_gpio_hwtype == IMX21_GPIO) {
/*
* Setup one handler for all GPIO interrupts. Actually setting
* the handler is needed only once, but doing it for every port
* is more robust and easier.
*/
irq_set_chained_handler(port->irq, mx2_gpio_irq_handler);
} else {
/* setup one handler for each entry */
irq_set_chained_handler_and_data(port->irq,
mx3_gpio_irq_handler, port);
if (port->irq_high > 0)
/* setup handler for GPIO 16 to 31 */
irq_set_chained_handler_and_data(port->irq_high,
mx3_gpio_irq_handler,
port);
}

err = bgpio_init(&port->gc, &pdev->dev, 4,
port->base + GPIO_PSR,
port->base + GPIO_DR, NULL,
port->base + GPIO_GDIR, NULL,
BGPIOF_READ_OUTPUT_REG_SET);
if (err)
goto out_bgio;

if (of_property_read_bool(np, "gpio_ranges"))
port->gpio_ranges = true;
else
port->gpio_ranges = false;

port->gc.request = mxc_gpio_request;
port->gc.free = mxc_gpio_free;
port->gc.parent = &pdev->dev;
port->gc.to_irq = mxc_gpio_to_irq;
port->gc.base = (pdev->id < 0) ? of_alias_get_id(np, "gpio") * 32 :
pdev->id * 32;

err = devm_gpiochip_add_data(&pdev->dev, &port->gc, port);
if (err)
goto out_bgio;

irq_base = irq_alloc_descs(-1, 0, 32, numa_node_id());
if (irq_base < 0) {
err = irq_base;
goto out_bgio;
}

port->domain = irq_domain_add_legacy(np, 32, irq_base, 0,
&irq_domain_simple_ops, NULL);
if (!port->domain) {
err = -ENODEV;
goto out_irqdesc_free;
}

/* gpio-mxc can be a generic irq chip */
err = mxc_gpio_init_gc(port, irq_base, &pdev->dev);
if (err < 0)
goto out_irqdomain_remove;

list_add_tail(&port->node, &mxc_gpio_ports);

platform_set_drvdata(pdev, port);
pm_runtime_put(&pdev->dev);

return 0;

out_pm_dis:
pm_runtime_disable(&pdev->dev);
clk_disable_unprepare(port->clk);
out_irqdomain_remove:
irq_domain_remove(port->domain);
out_irqdesc_free:
irq_free_descs(irq_base, 32);
out_bgio:
dev_info(&pdev->dev, "%s failed with errno %d\n", __func__, err);
return err;
}

2.0.1 probe分析#

里面定义了一个很重要的结构体mxc_gpio_port 就是对 I.MX6ULL GPIO 的抽象。mxc_gpio_port 结构体定义如下:
image
mxc_gpio_probe又会继续调用mxc_gpio_get_hw获取gpio的硬件相关数据,也就是gpio组,gpio1.gpio2等。

2.0.1.0 mxc_gpio_get_hw#

image

我们imx6ull gpio控制器类型就是imx35系列。因此选用imx35_gpio_hwdata,如下:可以看出这些成员不就是对应寄存器的偏移量吗?

image
image

2.0.1.1 get resource and ioremap#

比如我们probe中通过platform_get_resource 获取gpio1基地址为0X0209,C000,那么就可以通过配置mxc_gpio_hwdata结构体成员来配置寄存器。

image

然后调用 devm_ioremap_resource 函数进行内存映射,得到 0x0209C000 在 Linux 内核中的虚拟地址。
然后platform_get_irq 函数获取中断号,分为获取高 16 位 GPIO 的中断号,和获取低 16 位 GPIO 中断号。

操作 GPIO1 的 IMRISR 这两个寄存器,关闭 GPIO1 所有 IO 中断,并且清除状态寄存器:
image

设置对应 GPIO 的中断服务函数,不管是高 16 位还是低 16 位,中断服务函数都是 mx3_gpio_irq_handler:
image

2.0.1.2 bgpio_init#

image

image-20240817211118974

调用bgpio_init 函数主 要 任 务 就 是 初 始 化 port->gc, (gc就是gpio_chip)。顾名思义bgpio_init就是basic gpio init,里 面 有 三 个 setup 函 数 :

  1. bgpio_setup_io
  2. bgpio_setup_accessors
  3. bgpio_setup_direction。这三个函数就是初始化 port->gc 中的各种有关GPIO 的操作,比如输出,输入等等。

image-20240817213544044

2.0.1.3 devm_gpiochip_add_data#

调用devm_gpiochip_add_data注册这个port。gpio控制器就成功注册给了gpio子系统。
image

至此,port->gc既有了对 GPIO 的操作函数,又有了 I.MX6ULL 有关 GPIO的寄存器,那么只要得到 port 就可以对 I.MX6ULL 的 GPIO 进行操作。

2.1 gpio子系统数据结构#

2.1.1 gpio_device#

每个GPIO Controller用一个gpio_device来表示:

  1. 每组gpio引脚对应一个gpio_desc和一个gpio_chip
  2. gpio引脚的操作函数,都放在gpio_chip成员函数中。

image-20240818143429583

2.1.2 gpio_chip#

image-20240818140515596

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
struct gpio_chip {
const char *label;
struct gpio_device *gpiodev;
struct device *parent;
struct module *owner;
int (*request)(struct gpio_chip *chip,
unsigned offset);//请求一个 GPIO 引脚
void (*free)(struct gpio_chip *chip,
unsigned offset);//释放一个之前请求的 GPIO 引脚
int (*get_direction)(struct gpio_chip *chip,
unsigned offset);//获取方向
int (*direction_input)(struct gpio_chip *chip,
unsigned offset);//输入模式
int (*direction_output)(struct gpio_chip *chip,
unsigned offset, int value);//输出模式,并且set gpio val
int (*get)(struct gpio_chip *chip,
unsigned offset);//读取 GPIO 引脚的值
void (*set)(struct gpio_chip *chip,
unsigned offset, int value);//设置gpio 引脚值
void (*set_multiple)(struct gpio_chip *chip,
unsigned long *mask,
unsigned long *bits);
int (*set_debounce)(struct gpio_chip *chip,
unsigned offset,
unsigned debounce);//设置 GPIO 引脚的去抖动时间
int (*set_single_ended)(struct gpio_chip *chip,
unsigned offset,
enum single_ended_mode mode);
int (*to_irq)(struct gpio_chip *chip,
unsigned offset);
void (*dbg_show)(struct seq_file *s,
struct gpio_chip *chip);//调试目的,显示 GPIO 引脚的状态
int base;//chip的基地址
u16 ngpio;//GPIO 引脚数量
const char *const *names;
bool can_sleep;
bool irq_not_threaded;
#if IS_ENABLED(CONFIG_GPIO_GENERIC)// bgpio使能
unsigned long (*read_reg)(void __iomem *reg);
void (*write_reg)(void __iomem *reg, unsigned long data);
unsigned long (*pin2mask)(struct gpio_chip *gc, unsigned int pin);
void __iomem *reg_dat;
void __iomem *reg_set;
void __iomem *reg_clr;
void __iomem *reg_dir;
int bgpio_bits;
spinlock_t bgpio_lock;
unsigned long bgpio_data;
unsigned long bgpio_dir;
#endif
#ifdef CONFIG_GPIOLIB_IRQCHIP
struct irq_chip *irqchip;
struct irq_domain *irqdomain;
unsigned int irq_base;
irq_flow_handler_t irq_handler;
unsigned int irq_default_type;
int irq_parent;
bool irq_need_valid_mask;
unsigned long *irq_valid_mask;
struct lock_class_key *lock_key;
#endif

#if defined(CONFIG_OF_GPIO)
struct device_node *of_node;
int of_gpio_n_cells;//dts描述几个cells构成
int (*of_xlate)(struct gpio_chip *gc,
const struct of_phandle_args *gpiospec, u32 *flags);//设备树中 GPIO 引脚的转换
#endif

2.1.3 gpio_desc#

gpio_device中有一个gpio_desc数组,每一引脚有一项gpio_desc

image-20240818145531275

3 gpio子系统api#

3.1使用整数的GPIO传统方式#

3.1.1 请求和配置#

1
2
3
4
5
6
7
8
9
10
11
12
13
// 请求和释放GPIO
static int gpio_request(unsigned gpio, const char* label);
void gpio_free(unsigned gpio);

// 判断gpio释放可用
static bool gpio_is_valid(unsigned gpio);

// 设置gpio为输入还是输出
static int gpio_direction_input(unsigned gpio);
static int gpio_direction_output(unsigned gpio, int value);

// 设置gpio的去抖动时间,其中debounce以ms为单位
static int gpio_set_debounce(unsigned gpio, unsigned debounce);

3.1.2 读取和设置值#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 当gpio没有连接到I2C或SPI等慢速总线上,不会导致睡眠,可以在原子上下文中使用
static int gpio_get_value(unsigned gpio);
void gpio_set_value(unsigned gpio, int value); // value为bool值,0表示低电平,非0高电平

// 可以用gpio_can_sleep()判断gpio线是否可能睡眠
bool gpio_cansleep(unsigned gpio);

// 当gpio没有连接到I2C或SPI等慢速总线上,不会导致睡眠,可以在原子上下文中使用
static int gpio_get_value_cansleep(unsigned gpio);
void gpio_set_value_cansleep(unsigned gpio, int value);

// 当gpio映射到irq时,使用方式如下gpio_to_irq,返回irq号,接下来可以使用request_irq申请irq
int gpio_to_irq(unsigned gpio);
int irq_to_gpio(int irq)

3.1.3 gpiochip操作#

1
2
3
4
5
6
7
8
9
10
11
static inline int gpiochip_add(struct gpio_chip *chip)//注册一个gpio_chip结构体,它描述了一组GPIO引脚及其操作函数。
void gpiochip_remove(struct gpio_chip *chip)//移除之前注册的gpio_chip。

gpiochip_line_config//配置gpio_chip中的特定引脚。
gpiochip_request_own//请求对gpio_chip中的引脚的所有权。
gpiochip_request_unown//释放对gpio_chip中的引脚的所有权。
gpiochip_set//为gpio_chip中的多个引脚设置值。
gpiochip_clear//清除gpio_chip中的多个引脚的值。

gpiochip_set_direction//为gpio_chip中的多个引脚设置方向。
gpiochip_get_direction//获取gpio_chip中引脚的方向。

3.2 基于描述符的GPIO方式#

1
2
3
4
5
6
7
8
9
10
foo_device {
compatible = "acme,foo";
[...];
led-gpios = <&gpio 15 GPIO_ACTIVE_HIGH> //红色
<&gpio 16 GPIO_ACTIVE_HIGH> //绿色
<&gpio 17 GPIO_ACTIVE_HIGH> //蓝色
;
power-gpios = <&gpio 1 GPIO_ACTIVE_LOW>;
reset-gpios = <&gpio 1 GPIO_ACTIVE_LOW>;
};

在代码中获取GPIO的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//获取gpio的代码
sruct gpio_desc * gpiod_get_index(struct device *dev, char *con_id,
enum gpiod_flags flags);
sruct gpio_desc * gpiod_get(struct device *dev, char *con_id,
enum gpiod_flags flags);
void gpiod_put(sruct gpio_desc *desc);

//使用示例:
struct gpio_desc *red, *green, *blue, *power, *reset;
red = gpiod_get_index(dev, "led", 0, GPIO_OUT_HIGH);
green = gpiod_get_index(dev, "led", 1, GPIO_OUT_HIGH);
blue = gpiod_get_index(dev, "led", 2, GPIO_OUT_HIGH);

power = gpiod_get(dev, "power", GPIO_OUT_HIGH);
reset = gpiod_get(dev, "reset", GPIO_OUT_HIGH);

其他类似的功能函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int gpiod_direction_input(struct gpio_desc *desc);
int gpiod_direction_output(struct gpio_desc *desc, int value);

int gpiod_set_debounce(struct gpio_desc *desc, unsigned debounce);
// 输出逻辑1
// 在Active-High的情况下它会输出高电平
// 在Active-Low的情况下它会输出低电平
void gpiod_set_value(struct gpio_desc *desc, int val);

void gpiod_set_value_cansleep(struct gpio_desc *desc, int val);

int gpiod_get_value(struct gpio_desc *desc);
int gpiod_get_value_cansleep(struct gpio_desc *desc);

// 二者之间相互转换
struct gpio_desc *gpio_to_desc(unsigned gpio);
int desc_to_gpio(struct gpio_desc *desc);

image-20240818143109206

无论是传统GPIO控制还是基于描述符的gpio方式,都是调用底层控制器gpio_chip的操作函数。

3.3 和设备树相关GPIO接口#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
gpio1: gpio1 {
gpio-controller;
#gpio-cells = <2>;
};

gpio2 : gpio2 {
gpio-controller;
#gpio-cells = <1>;
};
foo-device {
cs-gpios = <&gpio1 17 0>
<&gpio1 2>
<&gpio1 17 0>;
reset-gpio = <&gpio1 30 0>;
cs-gpios = <&gpio2 10>;
};

// 在传统使用gpio的方式中,需要获取gpio编号,获取方式如下:
int n_gpios = of_get_named_gpio_count(dev.of_node, "cs-gpios");
int first_gpio = of_get_named_gpio(dev.of_node, "cs-gpios");

4 基于sysfs操作gpio#

声明GPIO口:

1
2
echo 256 > /sys/class/gpio/export  #/sys/class/gpio会生成gpio256目录
echo 256 > /sys/class/gpio/unexport

image
image

方向:

1
2
3
echo "in" > direction    #输入方向
echo "out" > direction #输出方向
cat direction

val:

1
2
3
echo 1 > value
echo 0 > value
cat value

edge:
表示中断的触发方式,edge文件有如下四个值:"none", "rising","falling","both"

1
2
3
4
5
none:#表示引脚为输入,不是中断引脚
rising:#表示引脚为中断输入,上升沿触发
falling:#表示引脚为中断输入,下降沿触发
both:#表示引脚为中断输入,边沿触发
echo "both" > /sys/class/gpio/gpioN/edge

5 gpio子系统示例#

nxp官方evk公板imx6ull-14x14-evk.dts为例:
image
重新定义了iomuxc引脚控制器,evk设备默认default状态对应的pins为pinctrl_hog_1, 配置3个引脚(UART1_RTS_B, GPIO1_IO05, GPIO1_IO09)信息。看起来是要做成sd卡的热插拔功能。
我们再找到描述sd的设备树节点:usdhc1usdhc2:

1
2
3
4
5
6
7
8
9
10
11
&usdhc1 {
pinctrl-names = "default", "state_100mhz", "state_200mhz";
pinctrl-0 = <&pinctrl_usdhc1>;
pinctrl-1 = <&pinctrl_usdhc1_100mhz>;
pinctrl-2 = <&pinctrl_usdhc1_200mhz>;
cd-gpios = <&gpio1 19 GPIO_ACTIVE_LOW>;
keep-power-in-suspend;
enable-sdio-wakeup;
vmmc-supply = <&reg_sd1_vmmc>;
status = "okay";
};

iomuxc控制器节点下有usdhc1要用的pins信息,包括pinctrl_hog_1pinctrl_usdhc1pinctrl_usdhc1_100mhz节点。

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
&iomuxc {
pinctrl_usdhc1: usdhc1grp {
fsl,pins = <
MX6UL_PAD_SD1_CMD__USDHC1_CMD 0x17059
MX6UL_PAD_SD1_CLK__USDHC1_CLK 0x10071
MX6UL_PAD_SD1_DATA0__USDHC1_DATA0 0x17059
MX6UL_PAD_SD1_DATA1__USDHC1_DATA1 0x17059
MX6UL_PAD_SD1_DATA2__USDHC1_DATA2 0x17059
MX6UL_PAD_SD1_DATA3__USDHC1_DATA3 0x17059
>;
};

pinctrl_usdhc1_100mhz: usdhc1grp100mhz {
fsl,pins = <
MX6UL_PAD_SD1_CMD__USDHC1_CMD 0x170b9
MX6UL_PAD_SD1_CLK__USDHC1_CLK 0x100b9
MX6UL_PAD_SD1_DATA0__USDHC1_DATA0 0x170b9
MX6UL_PAD_SD1_DATA1__USDHC1_DATA1 0x170b9
MX6UL_PAD_SD1_DATA2__USDHC1_DATA2 0x170b9
MX6UL_PAD_SD1_DATA3__USDHC1_DATA3 0x170b9
>;
};

pinctrl_usdhc1_200mhz: usdhc1grp200mhz {
fsl,pins = <
MX6UL_PAD_SD1_CMD__USDHC1_CMD 0x170f9
MX6UL_PAD_SD1_CLK__USDHC1_CLK 0x100f9
MX6UL_PAD_SD1_DATA0__USDHC1_DATA0 0x170f9
MX6UL_PAD_SD1_DATA1__USDHC1_DATA1 0x170f9
MX6UL_PAD_SD1_DATA2__USDHC1_DATA2 0x170f9
MX6UL_PAD_SD1_DATA3__USDHC1_DATA3 0x170f9
>;
};
}

5.1 gpio控制器dts描述#

前面讲的其实都还是pinctrl的内容,usdhc1有一个属性cd-gpios。这时就需要用到gpio控制器了,以imx6ull为例,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
28
29
30
31
32
33
34
Linux-4.9.88/Documentation/devicetree/bindings/gpio$ ls -l fsl-imx-gpio.txt

* Freescale i.MX/MXC GPIO controller

Required properties:
- compatible : Should be "fsl,<soc>-gpio"
- reg : Address and length of the register set for the device
- interrupts : Should be the port interrupt shared by all 32 pins, if
one number. If two numbers, the first one is the interrupt shared
by low 16 pins and the second one is for high 16 pins.
- gpio-controller : Marks the device node as a gpio controller.
- #gpio-cells : Should be two. The first cell is the pin number and
the second cell is used to specify the gpio polarity:
0 = active high
1 = active low
- interrupt-controller: Marks the device node as an interrupt controller.
- #interrupt-cells : Should be 2. The first cell is the GPIO number.
The second cell bits[3:0] is used to specify trigger type and level flags:
1 = low-to-high edge triggered.
2 = high-to-low edge triggered.
4 = active high level-sensitive.
8 = active low level-sensitive.

Example:

gpio0: gpio@73f84000 {
compatible = "fsl,imx51-gpio", "fsl,imx35-gpio";
reg = <0x73f84000 0x4000>;
interrupts = <50 51>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};

打开具体的imx6ull.dtsi:
image
打开芯片参考书册,刚好对应gpio1控制器:
image

5.2 gpio控制器使用者#

回到usdhc1的属性cd-gpios

1
cd-gpios = <&gpio1 19 GPIO_ACTIVE_LOW>;

表述使用gpio1控制器,由于该控制器的gpio-cells为2, 根据描述:

1
2
3
4
#gpio-cells : Should be two.  The first cell is the pin number and
the second cell is used to specify the gpio polarity:
0 = active high
1 = active low

19表示pin numberGPIO_ACTIVE_LOW是一个宏定义:可以看到为1,也就是低电平有效
image

5.2.1 使用者操作流程#

1
2
3
4
5
1. of_find_node_by_path获取使用该gpio的节点
2. of_get_named_gpio,获取gpio编号
3. gpio_request,申请gpio
4. gpio_direction_set/gpio_direction_get
5. gpio_val_set

5.2.1.1 dts自定义gpio控制器使用者(demo1,gpio_led)#

编写一个gpioled节点:

image
同时修改iomuxc节点,因为用到了GPIO1_IO03,要设置该pin脚为gpio
image

点击查看代码
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define GPIOLED_CNT 1
#define GPIOLED_NAME "gpioled"
#define LEDOFF 0
#define LEDON 1

struct gpioled_dev{
dev_t devid;
struct cdev cdev;
struct class *class;
struct device *device;
int major;
int minor;
struct device_node *nd;
int led_gpio;
};

struct gpioled_dev gpioled;

static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &gpioled;
return 0;
}

static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}

static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue;
unsigned char databuf[1];
unsigned char ledstat;
struct gpioled_dev *dev = filp->private_data;

retvalue = copy_from_user(databuf, buf, cnt);
if(retvalue < 0) {
printk("kernel write failed!\r\n");
return -EFAULT;
}

ledstat = databuf[0];

if(ledstat == LEDON) {
gpio_set_value(dev->led_gpio, 0);
} else if(ledstat == LEDOFF) {
gpio_set_value(dev->led_gpio, 1);
}
return 0;
}

static int led_release(struct inode *inode, struct file *filp)
{
return 0;
}

static struct file_operations gpioled_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};

static int __init led_init(void)
{
int ret = 0;

gpioled.nd = of_find_node_by_path("/gpioled");
if(gpioled.nd == NULL) {
printk("gpioled node not find!\r\n");
return -EINVAL;
} else {
printk("gpioled node find!\r\n");
}

gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio", 0);
if(gpioled.led_gpio < 0) {
printk("can't get led-gpio");
return -EINVAL;
}
printk("led-gpio num = %d\r\n", gpioled.led_gpio);

ret = gpio_direction_output(gpioled.led_gpio, 1);
if(ret < 0) {
printk("can't set gpio!\r\n");
}

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);
}
printk("gpioled major=%d,minor=%d\r\n",gpioled.major, gpioled.minor);

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);
if (IS_ERR(gpioled.class)) {
return PTR_ERR(gpioled.class);
}

gpioled.device = device_create(gpioled.class, NULL, gpioled.devid, NULL, GPIOLED_NAME);
if (IS_ERR(gpioled.device)) {
return PTR_ERR(gpioled.device);
}
return 0;
}

static void __exit led_exit(void)
{
cdev_del(&gpioled.cdev);
unregister_chrdev_region(gpioled.devid, GPIOLED_CNT);

device_destroy(gpioled.class, gpioled.devid);
class_destroy(gpioled.class);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

5.2.1.2 dts自定义gpio控制器使用者(demo2,beep)#

evk公板的蜂鸣器。BEEP使用了SNVS_TAMPER1这个PIN,打开imx6ull-alientek-emmc.dtsSNVS_TAMPER1属于iomuxc_snvs这个pin controller
image
iomuxc节点的imx6ul-evk子节点下创建一个名为“pinctrl_beep”的子节点:

1
2
3
4
5
pinctrl_beep: beepgrp {
fsl,pins = <
MX6ULL_PAD_SNVS_TAMPER1__GPIO5_IO01 0x10B0 /* beep */
>;
};

根节点“/”下创建BEEP节点:可以看到用到了引脚控制器的pinctrl_beep节点。同时使用gpio子系统,beep-gpio属性用到gpio5控制器。

1
2
3
4
5
6
7
8
9
beep {
#address-cells = <1>;
#size-cells = <1>;
compatible = "evk-beep";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_beep>;
beep-gpio = <&gpio5 1 GPIO_ACTIVE_HIGH>;
status = "okay";
};

蜂鸣器使用的 PIN 为 SNVS_TAMPER1,因此先检查 PIN 为SNVS_TAMPER1 这个 PIN 有没有被其他的 pinctrl 节点使用,如果有使用的话就要屏蔽掉,然后再检查 GPIO5_IO01 这个 GPIO 有没有被其他外设使用,如果有的话也要屏蔽掉。

输入“make dtbs”命令重新编译设备树,然后使用新编译出来的 imx6ull-alientek-emmc.dtb 文件启动 Linux 系统,进入“/proc/device-tree”目录中 查看“beep”节点是否存在:
image

点击查看代码
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define BEEP_CNT 1 /* 设备号个数 */
#define BEEP_NAME "beep" /* 名字 */
#define BEEPOFF 0 /* 关蜂鸣器 */
#define BEEPON 1 /* 开蜂鸣器 */

struct beep_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct device_node *nd; /* 设备节点 */
int beep_gpio; /* beep所使用的GPIO编号 */
};

struct beep_dev beep; /* beep设备 */

static int beep_open(struct inode *inode, struct file *filp)
{
filp->private_data = &beep; /* 设置私有数据 */
return 0;
}

static ssize_t beep_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue;
unsigned char databuf[1];
unsigned char beepstat;
struct beep_dev *dev = filp->private_data;

retvalue = copy_from_user(databuf, buf, cnt);
if(retvalue < 0) {
printk("kernel write failed!\r\n");
return -EFAULT;
}

beepstat = databuf[0]; /* 获取状态值 */

if(beepstat == BEEPON) {
gpio_set_value(dev->beep_gpio, 0); /* 打开蜂鸣器 */
} else if(beepstat == BEEPOFF) {
gpio_set_value(dev->beep_gpio, 1); /* 关闭蜂鸣器 */
}
return 0;
}

static int beep_release(struct inode *inode, struct file *filp)
{
return 0;
}

static struct file_operations beep_fops = {
.owner = THIS_MODULE,
.open = beep_open,
.write = beep_write,
.release = beep_release,
};

static int __init beep_init(void)
{
int ret = 0;

/* 1、获取设备节点:beep */
beep.nd = of_find_node_by_path("/beep");
if(beep.nd == NULL) {
printk("beep node not find!\r\n");
return -EINVAL;
} else {
printk("beep node find!\r\n");
}

/* 2、 获取设备树中的gpio属性,得到BEEP所使用的BEEP编号 */
beep.beep_gpio = of_get_named_gpio(beep.nd, "beep-gpio", 0);
if(beep.beep_gpio < 0) {
printk("can't get beep-gpio");
return -EINVAL;
}
printk("led-gpio num = %d\r\n", beep.beep_gpio);

/* 3、设置GPIO5_IO01为输出,并且输出高电平,默认关闭BEEP */
ret = gpio_direction_output(beep.beep_gpio, 1);
if(ret < 0) {
printk("can't set gpio!\r\n");
}

/* 1、创建设备号 */
if (beep.major) { /* 定义了设备号 */
beep.devid = MKDEV(beep.major, 0);
register_chrdev_region(beep.devid, BEEP_CNT, BEEP_NAME);
} else { /* 没有定义设备号 */
alloc_chrdev_region(&beep.devid, 0, BEEP_CNT, BEEP_NAME); /* 申请设备号 */
beep.major = MAJOR(beep.devid); /* 获取分配号的主设备号 */
beep.minor = MINOR(beep.devid); /* 获取分配号的次设备号 */
}
printk("beep major=%d,minor=%d\r\n",beep.major, beep.minor);

beep.cdev.owner = THIS_MODULE;
cdev_init(&beep.cdev, &beep_fops);
cdev_add(&beep.cdev, beep.devid, BEEP_CNT);

beep.class = class_create(THIS_MODULE, BEEP_NAME);
if (IS_ERR(beep.class)) {
return PTR_ERR(beep.class);
}

beep.device = device_create(beep.class, NULL, beep.devid, NULL, BEEP_NAME);
if (IS_ERR(beep.device)) {
return PTR_ERR(beep.device);
}
return 0;
}

static void __exit beep_exit(void)
{
cdev_del(&beep.cdev);/* 删除cdev */
unregister_chrdev_region(beep.devid, BEEP_CNT); /* 注销设备号 */

device_destroy(beep.class, beep.devid);
class_destroy(beep.class);
}

module_init(beep_init);
module_exit(beep_exit);
MODULE_LICENSE("GPL");

核心代码分析:

1
2
3
4
1. of_find_node_by_path("/beep");//找到设备节点
2. of_get_named_gpio(beep.nd, "beep-gpio", 0);//获取beep-gpio这个引脚编号
3. gpio_direction_output(beep.beep_gpio, 1);//请求gpio并且配成输出
4. gpio_set_value(dev->beep_gpio, 0);//设置高低电平

注意这里并没有使用pinctrl, pinctrl子系统是内核启动时就对pinctrl(也叫iomuxc)控制器进行了配置,进行了IOMUX配置。因此SNVS_TAMPER1会复用成gpio模式。
image

按键作为输入时:

static int keyio_init(void)
{
    keydev.nd = of_find_node_by_path("/key");
    if (keydev.nd== NULL) {
        return -EINVAL;
    }

    keydev.key_gpio = of_get_named_gpio(keydev.nd ,"key-gpio", 0);
    if (keydev.key_gpio < 0) {
        printk("can't get key0\r\n");
        return -EINVAL;
    }
    printk("key_gpio=%d\r\n", keydev.key_gpio);

    /* 初始化key所使用的IO */
    gpio_request(keydev.key_gpio, "key0");	/* 请求IO */
    gpio_direction_input(keydev.key_gpio);	/* 设置为输入 */
    return 0;
}

字符设备驱动-用户态构造IP寄存器结构体和读写寄存器

1 用户态定义寄存器结构#

以键盘keyscan为例,定义一个IP寄存器描述头文件,IOCRREG, IOCWREG定义了两个ioctl命令,用来读写寄存器。struct msg用来存放寄存器地址和值。
image
image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* Keyscan register: addr + offset + mask */
#define keyscan_top_keyscan_config1 0x0
#define keyscan_top_keyscan_config2 0x4
#define keyscan_top_keyscan_config3 0x8
#define keyscan_top_keyscan_config4 0xc
......
#define keyscan_top_reg_fifo_count 0x20
#define keyscan_top_reg_fifo_count_OFFSET 0
#define keyscan_top_reg_fifo_count_MASK 0xf
#define keyscan_top_reg_fifo_not_empty 0x20
#define keyscan_top_reg_fifo_not_empty_OFFSET 4
#define keyscan_top_reg_fifo_not_empty_MASK 0x10
struct msg {
long unsigned int addr;
unsigned int data;
};
#define IOC_MAGIC 'k'
#define IOCRREG _IOR(IOC_MAGIC, 1, struct msg)
#define IOCWREG _IOW(IOC_MAGIC, 2, struct msg)

2 驱动代码#

image

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
static long keyscan_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct msg m;
memset(&m, 0, sizeof(m));
void __iomem* base = dev->base;
switch (cmd) {
case IOCRREG:
if (copy_from_user(&m, (struct msg __user *)arg, sizeof(m)))
return -EFAULT;
m.data = readl(base + m.addr);
printk(KERN_DEBUG "base_addr:0x%lx, offset:0x%lx, read data: 0x%x \n", base, m.addr, m.data);
if (copy_to_user((struct msg __user *)arg, &m, sizeof(m)))
return -EFAULT;
break;
case IOCWREG:
if (copy_from_user(&m, (struct msg __user *)arg, sizeof(m)))
return -EFAULT;
printk(KERN_DEBUG "base_addr:0x%lx, offset:0x%lx, write data: 0x%x \n", base, m.addr, m.data);
writel(m.data, base + m.addr);
break;
default:
return -EINVAL;
}
return 0;
}

驱动先定义IP base addr,然后透过ioctl进行arg参数接收,确定msg中的addrdata, 接收cmd调用writel, readl进行读写。

3 用户态代码#

image

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
static inline void clrsetbits_32(long unsigned int addr, unsigned int clear, unsigned int set) {
int ret;
struct msg m;
memset(&m, 0, sizeof(m));
m.addr = addr;
ret = ioctl(fd, IOCRREG, &m);
if (ret) {
perror("ioctl: read error!");
return;
}
m.data = (m.data & (~clear)) | set;
ret = ioctl(fd, IOCWREG, &m);
if (ret) {
perror("ioctl: write error");
return;
}
return;
}
static unsigned int read_reg(long unsigned int addr) {
int ret;
struct msg m;
memset(&m, 0, sizeof(m));
m.addr = addr;
ret = ioctl(fd, IOCRREG, &m);
if (ret) {
perror("ioctl: read reg error!");
return -1;
}
return m.data;
}

首先实现基础读写函数进行寄存器读写,reg_read函数传入addr即可得到val, clrsetbits_32需要先读,在写入val.

1
2
3
4
5
6
7
8
#define KEYSCAN_MASK(REG_NAME) keyscan_top_##REG_NAME##_MASK
#define KEYSCAN_OFFSET(REG_NAME) keyscan_top_##REG_NAME##_OFFSET
#define KEYSCAN_SET(REG_NAME, VAL) \
clrsetbits_32(keyscan_top_##REG_NAME, KEYSCAN_MASK(REG_NAME), \
(VAL) << KEYSCAN_OFFSET(REG_NAME))
#define KEYSCAN_GET(REG_NAME) \
((read_reg(keyscan_top_##REG_NAME) & KEYSCAN_MASK(REG_NAME)) >> \
KEYSCAN_OFFSET(REG_NAME))

image

例如,当调用

1
2
3
4
KEYSCAN_GET(reg_fifo_count); 
//表示
read_reg(keycan_top_reg_fifo_count) &
keycan_top_reg_fifo_count_MASK >> keycan_top_reg_fifo_count_OFFSET);

reg_fifo表示IP的某一个寄存器:

count表示位域,因此对OFFSET定义成位域在该寄存器的偏移量(count位域是bit[3:0])定义为0,因此MASK定义为0xf.
②同理not_empty也是一个位域,bit[4]OFFSET定义成4,MASK定义成0x10,来屏蔽除bit[4]的其他bit.

KEYSCAN_GET(reg_fifo_count);最终就获取到了reg_fifo寄存器的count位域的内容。
KEYSCAN_GET(reg_fifo_not_empty);最终就获取到了reg_fifo寄存器的not_empty位域的内容

又例如,当调用

1
2
3
4
5
KEYSCAN_SET(reg_enable, 1);
//表示
clrsetbits_32(keycan_top_reg_enable,
keycan_top_reg_enable_MASK,
1 << keycan_top_reg_enable_OFFSET);

image

1
2
3
4
5
6
7
8
9
#define keyscan_top_reg_row_mask			0x0
#define keyscan_top_reg_row_mask_OFFSET 0
#define keyscan_top_reg_row_mask_MASK 0xff
#define keyscan_top_reg_col_mask 0x0
#define keyscan_top_reg_col_mask_OFFSET 8
#define keyscan_top_reg_col_mask_MASK 0xff00
#define keyscan_top_reg_enable 0x00
#define keyscan_top_reg_enable_OFFSET 16
#define keyscan_top_reg_enable_MASK 0x10000

可以看到offset定义为16,mask定义为0x10000,用来屏蔽除bit[16]的其他位。clrsetbits_32会先读出该寄存器,然后对该位set1, mask掉其他位,再次写入该寄存器。

字符设备驱动-ioctl命令详解

1 引入ioctl#

一个字符设备驱动通常会实现设备打开、关闭、读、写等功能,在一些需要细分的情境下,如果需要扩展新的功能增添命令,通常以增设 ioctl() 命令的方式实现。
对于ioctl这个系统调用接口,Linux的创始人在2.0版本之前并没有进行添加,仅有write和read两个接口,但是后来发现当需要去控制文件的某些操作的时候,很显然这两个接口根本不够用。所以才有了这个万能控制接口ioctl,但是作为Linux的创始人Linus本人一直排斥该接口,因为这个ioctl接口的在内核中的使用相当于对应用层开设了一个能够直接交互的窗口,很影响内核整体的权限控制,不过由于目前还暂时没有更好可以替代的方法,所以还是继续保留了这个接口的使用。
image

2 用户空间 ioctl#

image

1
2
3
4
5
int ioctl(int fd, int request, …);
ret = ioctl(fd, MYCMD);
if (ret == -1) {
printf("ioctl: %s\n", strerror(errno));
}

函数功能:
1.向硬件设备发送控制命令
2.还可以和硬件设备进行读或者写操作
参数:
fd:文件描述符
request:给硬件设备发送的控制命令
arg:保存的就是用户缓冲区的首地址
返回值:执行成功返回0,执行失败返回-1, ioctl 最常见的 errorno 值为 ENOTTYerror not a typewriter)表示命令找不到。

3 内核空间 ioctl#

image

1
2
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);

4 IOCTL的命令构成#

ioctl命令就是用户和驱动约定的一种协议, 理论上可以为任意 int 型数据,可以为 0、1、2、3……,但是为了确保该 “协议” 的唯一性,ioctl 命令应该使用更科学严谨的方法赋值,在linux中,提供了一种 ioctl 命令的统一格式,将 32 位 int 型数据划分为四个位段,如下图所示:
image

1
2
3
4
5
6
7
8
1. dir(direction),ioctl 命令访问模式(数据传输方向),占据 2 bit,
可以为 _IOC_NONE、_IOC_READ、_IOC_WRITE、_IOC_READ | _IOC_WRITE,
分别指示了四种访问模式:无数据、读数据、写数据、读写数据;
2. size,涉及到 ioctl 函数第三个参数 arg ,占据14bit,指定了 arg 的数据类型及长度;
3. type(device type),设备类型,占据 8 bit,可以为任意 char 型字符,
例如‘a’、’b’、’c’ 等等,其主要作用是使 ioctl 命令有唯一的设备标识;
4. nr(number),命令编号/序数,占据 8 bit,可以为任意 unsigned char 型数据,
取值范围 0~255,如果定义了多个 ioctl 命令,通常从 0 开始编号递增

通常而言,为了方便会使用宏 _IOC() 衍生的接口来直接定义 ioctl 命令:

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
75
76
77
78
79
80
81
82
//ioctl.h
#define _IOC_NRBITS 8
#define _IOC_TYPEBITS 8
/*
* Let any architecture override either of the following before
* including this file.
*/
#ifndef _IOC_SIZEBITS
# define _IOC_SIZEBITS 14
#endif

#ifndef _IOC_DIRBITS
# define _IOC_DIRBITS 2
#endif

#define _IOC_NRMASK ((1 << _IOC_NRBITS)-1)
#define _IOC_TYPEMASK ((1 << _IOC_TYPEBITS)-1)
#define _IOC_SIZEMASK ((1 << _IOC_SIZEBITS)-1)
#define _IOC_DIRMASK ((1 << _IOC_DIRBITS)-1)

#define _IOC_NRSHIFT 0
#define _IOC_TYPESHIFT (_IOC_NRSHIFT+_IOC_NRBITS)
#define _IOC_SIZESHIFT (_IOC_TYPESHIFT+_IOC_TYPEBITS)
#define _IOC_DIRSHIFT (_IOC_SIZESHIFT+_IOC_SIZEBITS)

/*
* Direction bits, which any architecture can choose to override
* before including this file.
*
* NOTE: _IOC_WRITE means userland is writing and kernel is
* reading. _IOC_READ means userland is reading and kernel is writing.
*/

#ifndef _IOC_NONE
# define _IOC_NONE 0U
#endif

#ifndef _IOC_WRITE
# define _IOC_WRITE 1U
#endif

#ifndef _IOC_READ
# define _IOC_READ 2U
#endif

#define _IOC(dir,type,nr,size) \
(((dir) << _IOC_DIRSHIFT) | \
((type) << _IOC_TYPESHIFT) | \
((nr) << _IOC_NRSHIFT) | \
((size) << _IOC_SIZESHIFT))

#ifndef __KERNEL__
#define _IOC_TYPECHECK(t) (sizeof(t))
#endif

/*
* Used to create numbers.
*
* NOTE: _IOW means userland is writing and kernel is reading. _IOR
* means userland is reading and kernel is writing.
*/
#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)))
#define _IOR_BAD(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size))
#define _IOW_BAD(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
#define _IOWR_BAD(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))

/* 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)

/* ...and for the drivers/sound files... */

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

image
image
除了_IO/_IOR/_IOW/_IOW等命令外,还支持反向解析 ioctl 命令的宏接口:主要就是利用Mask看是否4个位段是否越界,如果越界说明cmd构造的不合法:

1
2
3
4
#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)

举个例子比如image,展开命令成一个unsigned int的 cmd整数为:(字符D的ascii码为68)

1
0<<30 | 0<<16 | 68<<8 | 0x7<<0

5 ioctl系统调用过程详解#

5.1 app示例#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define IOC_MAGIC 'c'
#define IOCINIT _IO(IOC_MAGIC, 0)
#define IOCRREG _IOR(IOC_MAGIC, 1, int)
#define IOCWREG _IOW(IOC_MAGIC, 2, int)//定义3个cmd
#define IOC_MAXNR 3
struct msg {
int addr;
unsigned int data;
};

fd = open("/dev/ioctl-test", O_RDWR);
ioctl(fd, IOCINIT);
/* 往寄存器0x01写入数据0xef */
memset(&my_msg, 0, sizeof(my_msg));
my_msg.addr = 0x01;
my_msg.data = 0xef;
ioctl(fd, IOCWREG, &my_msg);
/* 读寄存器0x01 */
memset(&my_msg, 0, sizeof(my_msg));
my_msg.addr = 0x01;
ret = ioctl(fd, IOCRREG, &my_msg);

构造了IOCINIT IOCRREG IOCWREG3个命令。

5.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
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
#define IOC_MAGIC 'c'
#define IOCINIT _IO(IOC_MAGIC, 0)
#define IOCRREG _IOR(IOC_MAGIC, 1, int)
#define IOCWREG _IOW(IOC_MAGIC, 2, int)//定义3个cmd
#define IOC_MAXNR 3
struct msg {
int addr;
unsigned int data;
};
static long test_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
int ret;
struct msg my_msg;
/* 检查设备类型 */
if (_IOC_TYPE(cmd) != IOC_MAGIC) {
pr_err("[%s] command type [%c] error!\n", __func__, _IOC_TYPE(cmd));
return -ENOTTY;
}
/* 检查序数 */
if (_IOC_NR(cmd) > IOC_MAXNR) {
pr_err("[%s] command numer [%d] exceeded!\n", __func__, _IOC_NR(cmd));
return -ENOTTY;
}
/* 检查访问模式 */
if (_IOC_DIR(cmd) & _IOC_READ)
#if (LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 0))
ret= !access_ok(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd));
#else
ret= !access_ok((void __user *)arg, _IOC_SIZE(cmd));
#endif
else if (_IOC_DIR(cmd) & _IOC_WRITE)
#if (LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 0))
ret= !access_ok(VERIFY_READ, (void __user *)arg, _IOC_SIZE(cmd));
#else
ret= !access_ok((void __user *)arg, _IOC_SIZE(cmd));
#endif
if (ret)
return -EFAULT;
switch(cmd) {
/* 初始化设备 */
case IOCINIT:
break;
/* 读寄存器 */
case IOCRREG:
ret = copy_from_user(&msg, (struct msg __user *)arg, sizeof(my_msg));
if (ret)
return -EFAULT;
msg->data = read_reg(msg->addr);
ret = copy_to_user((struct msg __user *)arg, &msg, sizeof(my_msg));
if (ret)
return -EFAULT;
break;
/* 写寄存器 */
case IOCWREG:
ret = copy_from_user(&msg, (struct msg __user *)arg, sizeof(my_msg));
if (ret)
return -EFAULT;
write_reg(msg->addr, msg->data);
break;
default:
return -ENOTTY;
}
return 0;
}

image
首先定好3个命令,通过arg传入要写入的地址和数据or 要读的地址。然后检查type是否为‘c’, 检查命令号是否超过最大值3,检查方向是读还是写,利用access_ok判断用户地址是否可以访问。
image
最后将用户地址arg的数据透过copy_from_usercopy_to_user进行拷贝。然后进行寄存器读写。

5.3 ioctl过程详解#

在系统调用中,是通过SWI(Software Interrupt)的方式陷入内核态的, 首先通过软中断方式切换到内核态,ioctl的系统调用位于arch/arm/include/asm/unistd.h

5.3.1 sys_ioctl#

1
#define __NR_ioctl	(__NR_SYSCALL_BASE+ 54)

arch/arm/kernel/calls.S

1
/* 55 */	CALL(sys_ioctl);

调用sys_ioctl()

1
2
/include/linux.h
asmlinkage long sys_ioctl(unsigned int fd, unsigned int cmd, unsigned long arg);

然后调用SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)

1
2
3
4
/include/linux/syscalls.h
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINEx(x, sname, ...) __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
#define __SYSCALL_DEFINEx(x, name, ...) asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__))

SYSCALL_DEFINE3
SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)就是sys_ioctl的定义:
image
fget_light() 以及 security_file_ioctl() 就是检验可操作安全性,所以sys_ioctl更多是调用更深一层接口 do_vfs_ioctl()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg){
struct file *filp;
int error = -EBADF;
int fput_needed;

filp = fget_light(fd, &fput_needed);//由fd得带filp指针
if (!filp)
goto out;
error = security_file_ioctl(filp, cmd, arg);
if (error)
goto out_fput;
error = do_vfs_ioctl(filp, fd, cmd, arg);
out_fput:
fput_light(filp, fput_needed);
out:
return error;
}

5.3.1.1 do_vfs_ioctl#

image

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
int do_vfs_ioctl(struct file *filp, unsigned int fd, unsigned int cmd,
unsigned long arg) {
int error = 0;
int __user *argp = (int __user *)arg;
struct inode *inode = filp->f_path.dentry->d_inode;

switch (cmd) {
case FIOCLEX:
set_close_on_exec(fd, 1);
break;

case FIONCLEX:
set_close_on_exec(fd, 0);
break;

case FIONBIO:
error = ioctl_fionbio(filp, argp);
break;

case FIOASYNC:
error = ioctl_fioasync(fd, filp, argp);
break;

case FIOQSIZE:
if (S_ISDIR(inode->i_mode) || S_ISREG(inode->i_mode) ||
S_ISLNK(inode->i_mode)) {
loff_t res = inode_get_bytes(inode);
error = copy_to_user(argp, &res, sizeof(res)) ?
-EFAULT : 0;
} else
error = -ENOTTY;
break;

case FIFREEZE:
error = ioctl_fsfreeze(filp);
break;

case FITHAW:
error = ioctl_fsthaw(filp);
break;

case FS_IOC_FIEMAP:
return ioctl_fiemap(filp, arg);

case FIGETBSZ:
return put_user(inode->i_sb->s_blocksize, argp);

default:
if (S_ISREG(inode->i_mode))//是否为常规文件若是常规文件
error = file_ioctl(filp, cmd, arg);
else
error = vfs_ioctl(filp, cmd, arg);//调用vfs_ioctl
break;
}
return error;
}
5.3.1.1.1 vfs_ioctl#

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static long vfs_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg)
{
int error = -ENOTTY;

if (!filp->f_op || !filp->f_op->unlocked_ioctl)
goto out;
unlocked_ioctl
error = filp->f_op->unlocked_ioctl(filp, cmd, arg);//调用unlocked_ioctl()
if (error == -ENOIOCTLCMD)
error = -EINVAL;
out:
return error;
}

最终调用对应驱动人员自己fops的unlocked_ioctl函数。

字符设备驱动-misc杂项设备

1 引入misc device#

1.1 传统cdev方式#

char_drv.c
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
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <linux/cdev.h>
#include <linux/device.h>
static int led_major;
struct cdev cdev;
static int led_drv_open(struct inode *inode, struct file *file)
{
return 0;
}
static ssize_t led_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
return 0;
}
static struct file_operations led_drv_fops = {
.owner = THIS_MODULE,
.open = led_drv_open,
.write = led_drv_write,
};
static void led_setup_cdev(void)
{
int err, devno = MKDEV(led_major, 0);//index 为从设备号
cdev_init(&cdev, &led_drv_fops);
cdev.owner = THIS_MODULE;
cdev.ops = &led_drv_fops;
err = cdev_add(&cdev, devno, 1);//devno 为第一个设备号,1为数量
if (err)
printk(KERN_NOTICE "Error %d adding", err);
}
static int led_drv_init(void)
{
int result;
dev_t devno;
struct class *led_class;
struct device *dev;
devno=MKDEV(led_major,0);
if(led_major)//静态申请设备号
result=register_chrdev_region(devno,1,"led1_dev");
else{
result = alloc_chrdev_region(&devno,0,1,"led1_dev");//动态申请设备号
led_major = MAJOR(devno);
}
if(result<0) {
printk (KERN_WARNING "hello: can't get major number %d\n", led_major);
return result;
}

led_setup_cdev();
led_class = class_create(THIS_MODULE, "led_class");
dev = device_create(led_class, NULL, devno, NULL, "%s", "led_dev");
if (IS_ERR(dev)) {
dev_err(dev, "device create failed error code(%ld)\n", PTR_ERR(dev));
return PTR_ERR(dev);
}
return 0;
}
static void led_drv_exit(void)
{
device_destroy(led_class, dev); /* remove the device */
class_destroy(led_class); /* remove the device class */
cdev_del(&cdev);
unregister_chrdev_region(MKDEV(led_major,0),1);
}
module_init(led_drv_init);
module_exit(led_drv_exit);
MODULE_LICENSE("GPL");
总结流程:
1
2
3
4
5
6
A:创建设备号。MKDEV(major_no,0),其值为一个整数。因为linux中使用设备号来关联相应的设备和设备对于的驱动程序。
B:注册设备号。register_chrdev_region(devno,1,"led1_dev")
或者alloc_chrdev_region(&devno,0,1,"led1_dev");//动态申请设备号
C:初始化并关联file_operations结构体。 cdev_init(&cdev, &led_drv_fops);
D:添加字符设备到内核。int cdev_add(struct cdev *p, dev_t dev, unsigned count),
E:移除字符设备及设备号。cdev_del(&cdev); unregister_chrdev_region(MKDEV(led_major,0),1);

kdev_t.h
image

上面涉及到的API可以在函数linux/fs/char_dev.c中找到定义。

1.2 misc device方式#

使用misc_register,在加载模块时会自动创建设备节点,为主设备号为10的字符设备。使用misc_deregister,在卸载模块时会自动删除设备节点。因此无需调用cdev这一套框架流程,无需调用class_createdevice_create操作。misc_register时会自行调用了 class_create(), device_create() 因此/sys/class/misc类会被创建, /dev/下的设备节点也会自动创建。
/proc/misc记录了系统中所有加载的misc设备:
image

点击查看代码
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
#include <linux/miscdevice.h>
#include <linux/delay.h>
#include <asm/irq.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/delay.h>
#include <linux/moduleparam.h>
#include <linux/slab.h>
#include <linux/errno.h>
#include <linux/ioctl.h>
#include <linux/cdev.h>
#include <linux/string.h>
#include <linux/list.h>
#include <linux/pci.h>
#include <asm/uaccess.h>
#include <asm/atomic.h>
#include <asm/unistd.h>
struct led_dev {
struct miscdevice miscdev;
void *data;
}
struct led_dev my_led;
static int leds_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg){
return 0;
}
static int leds_open(struct inode *inode, struct file *filp){
//filp->private_data = &my_led;
return 0;
}
static int leds_release(struct inode *inode, struct file *filp){
return 0;
}
static ssize_t leds_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos){
return 1;
}

static struct file_operations leds_fops ={
.owner = THIS_MODULE,
.read = leds_read,
.ioctl = leds_ioctl,
.open = leds_open,
.release = leds_release
};
static int __init dev_init(void){
struct miscdevice *miscdev = &my_led.miscdev;
miscdev->minor = MISC_DYNAMIC_MINOR,
miscdev->name = "misc_leds",
miscdev->fops = &leds_fops,
miscdev->parent = NULL;
int ret = misc_register(miscdev);
return ret;
}
static void __exit dev_exit(void){
misc_deregister(&my_led.miscdev);
}
module_init(dev_init);
module_exit(dev_exit);
MODULE_LICENSE("GPL");

2 misc杂项设备解析#

源代码位置driver/char/misc.c,主设备号固定为10,所有的miscdevice设备形成了一个链表,对设备访问时内核根据次设备号查找对应的miscdevice设备,然后调用其file_operations结构中注册的文件操作接口进行操作。

2.1 misc_init过程#

image
misc子系统的初始化是利用subsys_initcall进行子系统初始化,首先创建/proc/misc条目,对应cat /proc/misc可以看到所有misc设备信息,cat /proc/misc于是就会调用misc_seq_ops中的misc_seq_show函数,可以看到刚好为misc设备的次设备号和名字信息。
image
主设备号固定为10,调用class_create创建/sys/class/misc, 调用register_chrdev注册字符设备,添加file_operations。(register_chrdev如果传入主设备号,则静态注册,否则动态注册返回主设备号)
image
image
image

2.2 misc设备注册misc_register过程#

image

MISC_DYNAMIC_MINOR = 255,使用者调用misc_register时一般会次设备号传入MISC_DYNAMIC_MINOR,那么会自动分配次设备号;否则遍历misc_list链表,看这个次设备号以前有没有被用过,如果次设备号已被占有则退出返回-EBUSY。
得到这个次设备号后set_bit(i, misc_minors);设置位图中相应位为1。device_create_with_groups等同于device_create创建设备节点。
最后将list节点添加到misc_list链表中。
cat /sys/class可以看到所有驱动中调用class_creat()函数的模块,cat /sys/class/misc则可以看到所有misc杂项驱动模块。ls /dev/*可以看到对应的设备节点
image
image
image

2.3 misc设备卸载过程#

image
misc_list链表中删除节点list, 然后删除设备节点。释放位图相应位清0,以便次设备号留给下一个模块使用。

2.4 misc设备打开过程#

image

  1. 当用户调用open("/dev/xxx")时,由于misc设备主设备号都为10,那么会统一进入到misc_open,那么会根据次设备号来区分不同的misc设备,首先iminor(inode)取出次设备号,i_rdev是对应具体misc设备的设备号dev_t
    image
  2. 然后遍历misc_list链表,找到与minor次设备号相匹配的misc device,找到后将file_operations(简称fops)暂存到new_fops。如果匹配不到,则请求加载这个次设备号对应的模块。request_module表示让linux系统的用户空间调用/sbin/modprobe函数加载名为char-major-%d-%d的模块。
    image
    匹配成功后file->private_data = c;表示将链表中匹配出的miscdevice作为file->private_data(后面会介绍作用)
1
2
3
4
5
6
/*
* Place the miscdevice in the file's
* private_data so it can be used by the
* file operations, including f_op->open below
*/
file->private_data = c;

最后将暂存的new_fops赋值给file->f_op,调用具体的misc模块的fops:

1
file->f_op->open(inode, file);

image

3 如何从fops中获取模块设备信息#

3.1 引入#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct xxx_dev {
struct miscdevice miscdev;
void *data;
};

static long dwa_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct xxx_dev *m = container_of(filp->private_data, struct xxx_dev, miscdev);
...
return 0;
}
static int __init xxx_init(void) {
struct xxx_dev *m;
...
}

3.1.1 方法1:(对于misc设备)#

可以看到如果我们想要重file_oprations获取设备入口,可以通过如下方式:

1
struct xxx_dev *m = container_of(filp->private_data, struct xxx_dev, miscdev);

前面2.4讲过了匹配成功后file->private_data = c;表示将链表中匹配出的miscdevice作为file->private_data.

3.1.2 方法2:(对于cdev设备)#

1
2
3
4
5
6
7
8
9
10
struct xxx_dev {
struct cdev cdev;
void *data;
};
int xxx_open(struct inode *inode, struct file *file)
{
int ret = 0;
struct xxx_dev *m;
m = container_of(inode->i_cdev, struct xxx_dev, cdev);
}

inodei_cdev指向的即为cdev结构体。调用container_of即可获取设备信息。

3.1.3方法3:xxx_open中保存设备信息#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct xxx_dev {
struct cdev cdev;
void *data;
struct resource* res;
};
struct xxx_dev *m;
static long keyscan_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
struct xxx_dev *m = file->private_data;
uint32_t res_size = (uint32_t)resource_size(m->res);
}
int xxx_open(struct inode *inode, struct file *file)
{
file->private_data = m;
}

file->private_data = m保存设备信息。

字符设备驱动-mmap驱动应用实例

1 mmap驱动要做的事情#

  1. 确定物理地址
  2. 确定属性:是否使用 cache、 buffer
  3. 建立映射关系

参考 Linux 驱动源文件代码:
image
image
我们要验证mmap功能,在驱动程序中申请一个 8K 的 buffer,让 APP 通过 mmap 能直接访问。

2 mmap驱动代码示例分析#

linux内核中常用的内存申请方式:

函数名 说明
kmalloc 分配到的内存物理地址是连续的
kzalloc 分配到的内存物理地址是连续的,内容清 0
vmalloc 分配到的内存物理地址不保证是连续的
vzalloc vzalloc 分配到的内存物理地址不保证是连续的,内容清 0

我们在 mmap 时应该使用 kmalloc 或 kzalloc,这样得到的内存物理地址是连续的,mmap后 APP 才可以使用同一个基地址去访问这块内存。 (如果物理地址不连续,就要执行多次 mmap 了)

点击查看代码
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <asm/pgtable.h>
#include <linux/mm.h>
#include <linux/slab.h>

static int major = 0;
static char *kernel_buf;
static struct class *hello_class;
static int bufsiz = 1024*8;

#define MIN(a, b) (a < b ? a : b)

static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset){
int err;
err = copy_to_user(buf, kernel_buf, MIN(bufsiz, size));
return MIN(bufsiz, size);
}
static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
int err;
err = copy_from_user(kernel_buf, buf, MIN(1024, size));
return MIN(1024, size);
}
static int hello_drv_mmap(struct file *file, struct vm_area_struct *vma)
{
/* 获得物理地址 */
unsigned long phy = virt_to_phys(kernel_buf);
/* 设置属性: cache, buffer */
vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);
/* map */
if (remap_pfn_range(vma, vma->vm_start, phy >> PAGE_SHIFT,
vma->vm_end - vma->vm_start, vma->vm_page_prot)) {
printk("mmap remap_pfn_range failed\n");
return -ENOBUFS;
}
return 0;
}

static int hello_drv_open (struct inode *node, struct file *file){
return 0;
}
static int hello_drv_close (struct inode *node, struct file *file){
return 0;
}
static struct file_operations hello_drv = {
.owner = THIS_MODULE,
.open = hello_drv_open,
.read = hello_drv_read,
.write = hello_drv_write,
.release = hello_drv_close,
.mmap = hello_drv_mmap,
};

static int __init hello_init(void)
{
int err;
kernel_buf = kmalloc(bufsiz, GFP_KERNEL);
strcpy(kernel_buf, "old");
major = register_chrdev(0, "hello", &hello_drv);
hello_class = class_create(THIS_MODULE, "hello_class");
err = PTR_ERR(hello_class);
if (IS_ERR(hello_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "hello");
return -1;
}
device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello");
return 0;
}

static void __exit hello_exit(void)
{
device_destroy(hello_class, MKDEV(major, 0));
class_destroy(hello_class);
unregister_chrdev(major, "hello");
kfree(kernel_buf);
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

分析:init时,驱动使用kmalloc分配8K空间(物理地址连续), 初始化为”old“字符串。实现read,write函数。mmap函数中:

1
2
3
4
5
6
/* 获得物理地址 */
unsigned long phy = virt_to_phys(kernel_buf);
/* 设置属性: cache, buffer */
vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);
/*映射*/
remap_pfn_range(vma, vma->vm_start, phy >> PAGE_SHIFT, vma->vm_end - vma->vm_start, vma->vm_page_prot);

image

pgprot_writecombine设置属性为Non-cached buffered (NCB)

1
2
3
4
5
6
7
#include <asm/pgtable.h>
#define pgprot_noncached(prot) \
__pgprot_modify(prot, PTE_ATTRINDX_MASK, PTE_ATTRINDX(MT_DEVICE_nGnRnE) | PTE_PXN | PTE_UXN)
#define pgprot_writecombine(prot) \
__pgprot_modify(prot, PTE_ATTRINDX_MASK, PTE_ATTRINDX(MT_NORMAL_NC) | PTE_PXN | PTE_UXN)
#define pgprot_device(prot) \
__pgprot_modify(prot, PTE_ATTRINDX_MASK, PTE_ATTRINDX(MT_DEVICE_nGnRE) | PTE_PXN | PTE_UXN)

image
image

注意:remap_pfn_range 中,pfn 的意思是“ Page Frame Number”。在 Linux 中,整个物理地址空间可以分为第 0 页、第 1 页、第 2 页,诸如此类,这就是 pfn。假设每页大小是 4K,那么给定物理地址 phy,它的 pfn = phy / 4096 = phy >> 12。内核的 page 一般是 4K,但是也可以配置内核修改 page的大小。所以为了通用, pfn = phy >> PAGE_SHIFT

1
2
3
#include <linux/mm.h>
int remap_pfn_range(struct vm_area_struct *, unsigned long addr,
unsigned long pfn, unsigned long size, pgprot_t);

如果我们的buf不是用kmalloc, 而是vmalloc,那么需要映射多次,每次映射一个page 4k.(MMU过程中内存以page为单位作为连续内存单元)
image

3 mmap应用代码示例与分析#

点击查看代码
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
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

/*
* ./hello_drv_test
*/
int main(int argc, char **argv){
int fd;
char *buf;
int len;
char str[1024];
/* 1. 打开文件 */
fd = open("/dev/hello", O_RDWR);
if (fd == -1)
{
printf("can not open file /dev/hello\n");
return -1;
}
/* 2. mmap
* MAP_SHARED : 多个APP都调用mmap映射同一块内存时, 对内存的修改大家都可以看到。
* 就是说多个APP、驱动程序实际上访问的都是同一块内存
* MAP_PRIVATE : 创建一个copy on write的私有映射。
* 当APP对该内存进行修改时,其他程序是看不到这些修改的。
* 就是当APP写内存时, 内核会先创建一个拷贝给这个APP,
* 这个拷贝是这个APP私有的, 其他APP、驱动无法访问。
*/
buf = mmap(NULL, 1024*8, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (buf == MAP_FAILED){
printf("can not mmap file /dev/hello\n");
return -1;
}

printf("mmap address = 0x%x\n", buf);
printf("buf origin data = %s\n", buf); /* old */

/* 3. write */
strcpy(buf, "new");

/* 4. read & compare */
/* 对于MAP_SHARED映射: str = "new"
* 对于MAP_PRIVATE映射: str = "old"
*/
read(fd, str, 1024);
if (strcmp(buf, str) == 0)
{
/* 对于MAP_SHARED映射,APP写的数据驱动可见
* APP和驱动访问的是同一个内存块
*/
printf("compare ok!\n");
} else {
/* 对于MAP_PRIVATE映射,APP写数据时, 是写入原来内存块的"拷贝"
*/
printf("compare err!\n");
printf("str = %s!\n", str); /* old */
printf("buf = %s!\n", buf); /* new */
}

while (1){
sleep(10); /* cat /proc/pid/maps */
}

munmap(buf, 1024*8);
close(fd);
return 0;
}

3.1 共享映射与私有映射#

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* MAP_SHARED : 多个APP都调用mmap映射同一块内存时, 对内存的修改大家都可以看到。
* 就是说多个APP、驱动程序实际上访问的都是同一块内存
* MAP_PRIVATE : 创建一个copy on write的私有映射。
* 当APP对该内存进行修改时,其他程序是看不到这些修改的。
* 就是当APP写内存时, 内核会先创建一个拷贝给这个APP,
* 这个拷贝是这个APP私有的, 其他APP、驱动无法访问。
*/

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(void *addr, size_t length);

3.1.1 copy on write#

image

① mmap时使用MAP_PRIVATE
②③ 当写入mmap内存时,会copy这块内存
④写入新数据,会将数据写入新copy的内存
⑤读数据还是从旧的那块映射内存去读,因此这时会与buf中的数据不一样

根据上面的mmap应用示例来分析和验证MAP_SHAREDMAP_PRIVATE的差异:
image
先用MAP_PRIVATE,执行测试程序:
image
再用MAP_SHARED,执行测试程序:
image

字符设备驱动-mmap机制

1 引入mmap#

应用程序和驱动程序之间传递数据时,可以通过 read、write 函数进行, 用户态和内核态的数据交互一般用copy_from_user,copy_to_user。这种方式在数据量比较小时没什么问题;但是数据量比较大时效率就太低了。比如更新 LCD 显示时,如果每次都让 APP 传递一帧数据给内核,假设 LCD 采用1024x600x32 bpp 的格式,一帧数据就有1024x600x32/8=2.3MB 左右,而且一般为了显示动态画面,LDC输出fps要求是60fps or 30 fps,那么一秒数据量为30x2.3 = 70M左右,显然copy_from_usercopy_to_user的方式不再适合。
改进的方法就是让程序可以直接读写驱动程序中的 buffer,这可以通过mmap 实现(memory map),把内核的 buffer 映射到用户态,让 APP 在用户态直接读写。
image
image

1.1 内存映射现象#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int a;
int main(int argc, char **argv)
{
if (argc != 2) {
printf("Usage: %s <number>\n", argv[0]);
return -1;
}
a = strtol(argv[1], NULL, 0);
printf("a's address = 0x%lx, a's value = %d\n", &a, a);
while (1) {
sleep(10);
}
return 0;
}

在 PC 上如下编译(必须静态编译):

1
gcc -o test test.c -staitc

分别后台执行 test 程序 2 次。最后执行 ps,可以看到这 2 个程序同时存在,这 2 个程序里 a 变量的地址相同,但是值不同。
image

1
2
2 个程序同时运行,它们的变量a的地址都是一样的:0x6bc3a0;
2 个程序同时运行,它们的变量a的值是不一样的,一个是 111,另一个是 123。

1.1.1 引入MMU#

来分析一下:
2个程序同时在内存中运行,它们的值不一样,所以变量 a 的物理内存地址肯定不同(2个变量存放不是同一个地方);
但是打印出来的变量 a 的地址却是一样的。怎么回事?
这里要引入虚拟地址的概念:CPU 发出的地址是虚拟地址,它经过MMU(Memory Manage Unit,内存管理单元)映射到物理地址上,对于不同进程的同一个虚拟地址,MMU 会把它们映射到不同的物理地址。
总结:虽然虚拟地址一样,但物理地址不一样,这个是mmu的功劳,将同一虚拟地址映射到不同物理地址。
image

1
2
当前运行的是 app1 时,MMU 会把 CPU 发出的虚拟地址 addr 映射为物理地址paddr1,用 paddr1 去访问内存。
当前运行的是 app2 时,MMU 会把 CPU 发出的虚拟地址 addr 映射为物理地址paddr2,用 paddr2 去访问内存。

1.1.2 查看进程地址空间#

MMU 负责把虚拟地址映射为物理地址,虚拟地址映射到哪个物理地址去?可以执行ps命令查看进程 ID,然后执行“cat /proc/[PID]/maps”得到虚拟地址空间映射关系。
image

1
2
3
4
5
6
7
8
00400000-004b6000 r-xp 00000000 08:04 2228541                            /home/book/ftp/a.out
006b6000-006bc000 rw-p 000b6000 08:04 2228541 /home/book/ftp/a.out
006bc000-006bd000 rw-p 00000000 00:00 0
021c8000-021eb000 rw-p 00000000 00:00 0 [heap]
7ffe18738000-7ffe18759000 rw-p 00000000 00:00 0 [stack]
7ffe187f9000-7ffe187fc000 r--p 00000000 00:00 0 [vvar]
7ffe187fc000-7ffe187fd000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]

第一行地址范围显示权限为可读可执行,表示该程序代码段(.text)
第二行地址范围显示权限为可读可写, 表示该程序的数据段(.data)
第三行地址范围显示权限为可读可写, 表示该程序的数据段(.data)刚才变量a地址就在这段地址范围内
第四行地址范围是堆空间(.heap段),malloc的内存就会处于这段
第5行地址范围是栈空间(.stack段),局部变量处于这段
p表示private, s表示share, 再来看一个使用动态库的进程,比如bash进程:
image

2 mmap内核态的描述#

2.1 进程结构体(task_struct)和进程地址空间(mm_struct)#

每一个 APP对应了很多虚拟地址空间,比如栈空间,堆空间,数据段,代码段等,也叫做进程地址空间(mm_strcut)
image

因此在内核里都有一个 tast_struct,这个结构体中保存有内存信息:mm_struct。而虚拟地址、物理地址的映射关系保存在页目录表中:
image

1
2
3
4
5
6
7
8
9
10
1. 每个 APP 在内核中都有一个 task_struct 结构体,它用来描述一个进程;
2. 每个 APP 都要占据内存,在 task_struct 中用 mm_struct 来管理进程占用的内存;
3. 内存有虚拟地址、物理地址,mm_struct 中用 mmap 来描述虚拟地址,
用 pgd 来描述对应的物理地址。(注意:pgd,Page Global Directory,页目录)
4. 每个 APP 都有一系列的 VMA:virtual memory,即mmap会指向vm_area_struct,
比如 APP 含有代码段、数据段、BSS 段、栈等等,还有共享库。这些单元会保存在内存里,
它们的地址空间不同,权限不同(代码段是只读的可运行的、数据段可读可写),内核用一系列的 vm_area_struct 来描述它们。
6. vm_area_struct 中的 vm_start、vm_end 是虚拟地址。
7. vm_area_struct 中虚拟地址如何映射到物理地址去? 每一个 APP 的虚拟地址可能相同,
物理地址不相同,这些对应关系保存在 pgd 中。

2.1.1 vm_area_struct虚拟内存区域#

每个进程有一个task_struct和一个mm_struct, 其中mm_struct中的mmap对应vm_area_struct虚拟内存区域:

image-20240816222839173

可以看到mm_struct每一段都对应一块vm_area_struct

3 页表映射#

image

页表是存在ddr中的一段连续地址空间,页表里面存放了要映射的物理地址集合,页表分为很多个页表项。
ARM 架构支持一级页表映射,也就是说 MMU 根据 CPU 发来的虚拟地址可以找到第 1 个页表,从第 1 个页表里就可以知道这个虚拟地址对应的物理地址。一级页表里地址映射的最小单位是 1M。
ARM 架构还支持二级页表映射,也就是说 MMU 根据 CPU 发来的虚拟地址先找到第 1 个页表,从第 1 个页表里就可以知道第 2 级页表在哪里;再取出第 2 级页表,从第 2 个页表里才能确定这个虚拟地址对应的物理地址。二级页表地址映射的最小单位有 4K、1K,Linux 使用 4K。
一级页表项里的内容,决定了它是指向一块物理内存,还是指问二级页表,一个页表项格式如下图:
image

3.1 一级页表映射#

arm32系统中,一个页表项占4个byte, 32bit,它分为一级页表项和二级页表项,通过bit[1:0]区分,一级页表项保存有物理地址,用bit[31:20] 共12位表示段基地址,有1M物理内存。比如cpu发出虚拟地址0x12345678, MMU通过bit[31:20]发现为0x123,也就是从第0x123个页表项中找到Section Base Address, 比如第0个页表项中物理及地址为0x8000,0000, 那么第0x123个页表项目物理基地址就是0x123 * 1M + 0x8000,0000,也就是0x9230,0000, 因为每一个一级页表项物理内存大小为1M.
段内偏移是 0x45678,那么最终通过一级页表映射最终映射到物理地址就为0x0x9230,0000 + 0x45678,也就是0x9234,5678
Section Base Address的数量为多少呢?一共12bit,也就是4096个,每一个1级页表项大小为1M, 因此总共可表示4G。对于 32 位的系统,虚拟地址空间有 4G,4G/1M=4096。所以一级页表要映射整个 4G 空间的话,刚好需要 4096 个页表项。
所以 CPU 要访问虚拟地址 0x12345678 时,实际上访问的是 0x81045678 的物理地址。
image

3.2 二级页表映射#

一级页表项每项有1M空间, 一级页表映射时是吧虚拟地址的1M映射到物理地址的1M连续空间,但有时我们的程序没有那么大,显然用1M太浪费空间。 那么引入二级页表映射来映射更小的块,对于二级页表,每一个页可以是1K, 4K,64K, Linux系统一般使用4K, 对应Small Page, 64K对应的是大页(Large Page), 1K对应的是Tine Page(一般很少用)。
二级页表映射过程:
首先设置好一级页表、二级页表,并且把一级页表的首地址告诉 MMU,比如0x8000,0000
二级页表首先也是要经过一级页表映射,用bit[31:20] 共12位表示段基地址,找到对应的一级页表项比如0x123项,通过这一项里面的bit[1:0]发现它是一个二级页表项(注意不再是取出1M的物理地址),然后根据二级页表项的bit[19:12]这8位得到二级页表是得到索引0x45,表示为第0x45个二级页表项。从这个二级页表项中取出里面的物理地址,比如为addr。
二级页表格式如下:
image

可以看到里面含有64k(Large Page) 4k(Small Page)1K(Tine) 物理空间的基地址 page base addr,假设从第0x45个二级页表项取出的物理地址为0x8188,9000。然后offset=0x678, 那么它跟 vaddr[11:0] 组 合 得 到 物 理 地 址 : 0x8188,9000 + 0x678 = 0x8188,9678,所以 CPU 要访问虚拟地址 0x1234,5678 时,实际上访问的是0x8188,9678的物理地址, 根据bit[1:0]得到映射的大小为4K(linux Small Page)。假如这里不使用二级页表映射,理论去计算对应物理地址则会是0x8180,0000往后1M内存,显然浪费了。
image

4 mmap函数调用过程#

从上面内存映射的过程可以知道,要给 APP 新开劈一块虚拟内存,并且让它指向某块内核buffer,我们要做这些事:

  1. 得到一个 vm_area_struct,它表示 APP 的一块虚拟内存空间:
    很 幸 运 , APP 调 用 mmap 系 统 函 数 时 , 内 核 就 帮 我 们 构 造 了 一 个vm_area_stuct 结构体。里面含有虚拟地址的地址范围、权限,属性。
  2. 确定物理地址:
    你想映射某个内核 buffer,你需要得到它的物理地址,这得由你提供。
  3. vm_area_struct和物理地址建立映射关系

比如APP 里调用 mmap 时,导致的内核相关函数调用过程如下:
image

1
2
3
4
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(void *addr, size_t length);

可以传入一个addr虚拟地址,需要用户自己malloc。也可以将addr设置为NULL, 让linux内核帮你产生一段内存映射,返回虚拟地址给你。
内核得到可用的虚拟地址后会分配一个vm_area_struct, 用来描述一块虚拟地址空间,里面有这块虚拟地址空间的起始地址、结束地址、权限信息。最后会调用驱动里面的mmap函数,参数为刚刚分配的vm_area_sruct
那么需要再驱动程序实现mmap函数,主要包括:

  1. 提供物理地址
  2. 设置属性,cache,buffer
  3. vm_area_stuct和物理地址建立映射

4.1 vm_area_struct描述#

image

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
/*
* This struct describes a virtual memory area. There is one of these
* per VM-area/task. A VM area is any part of the process virtual memory
* space that has a special rule for the page-fault handlers (ie a shared
* library, the executable area etc).
*/
struct vm_area_struct {
/* The first cache line has the info for VMA tree walking. */

unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next, *vm_prev;
struct rb_node vm_rb;
/*
* Largest free memory gap in bytes to the left of this VMA.
* Either between this VMA and vma->vm_prev, or between one of the
* VMAs below us in the VMA rbtree and its ->vm_prev. This helps
* get_unmapped_area find a free area of the right size.
*/
unsigned long rb_subtree_gap;
/* Second cache line starts here. */
struct mm_struct *vm_mm; /* The address space we belong to. */
/*
* Access permissions of this VMA.
* See vmf_insert_mixed_prot() for discussion.
*/
pgprot_t vm_page_prot;
unsigned long vm_flags; /* Flags, see mm.h. */

它表示的是一块连续的虚拟地址空间区域,给进程使用的,地址空间范围是0~3G,对应的物理页面都可以是不连续的.
主要成员有起始地址、结束地址、权限信息,属性信息。
vm_flags:可读,可写,可执行权限,私有,共享等权限

常用vm_flags访问权限的取值说明:

1
2
3
4
5
6
7
8
VM_READ:可读
VM_WRITE:可写
VM_EXEC:可执行
VM_SHARD:可多进程之间共享
VM_IO:可映射至设备 IO 空间
VM_RESERVED:内存区域不可被换出
VM_SEQ_READ:内存区域可能被顺序访问
VM_RAND_READ:内存区域可能被随机访问

vm_pgoff:是否使用cache? 是否使用buffer?

4.2 引入cache和buffer#

使用 mmap 时,需要有cache、 buffer的知识。下图是 CPU 和内存之间的关系,有 cache、 buffer(写缓冲器)。 Cache 是一块高速内存;写缓冲器相当于一个 FIFO,可以把多个写操作集合起来一次写入内存。
image

4.2.1 引入时间局部性和空间局部性#

当程序运行时有“局部性原理”,这又分为时间局部性、空间局部性。举个例子:

1
2
3
4
5
int i;
int a = 0;
for (i = 0; i < 100; i++) {
a++;
}

时间局部性:
a++在很短的时间内被重复写了100次,与此同时i也被访问了100次,像这种在某个时间点访问了存储器的特定位置,反复地访问这个位置被称为“时间局部性”
空间局部性:
访问变量a的同时也访问了它周围临近变量i, 像这种访问了存储器的特定位置,很可能在不久的将来访问它附近的位置被称作”空间局部性“, 那么为什么不用buffer or cache把它一次性访问完呢?
根据“局部性原理”,引入 cachebuffer

4.2.1.1 cache miss和cache hit#

读数据:

  1. 要读取内存指定addr处的数据时,先看看cache中有没有addr的数据,如果有则直接从cache返回数据,这一过程叫做cache命中(cache hit)
  2. 假如cache中没有该addr的数据,触发cache缺失 (cache miss), 那么会从addr读一段连续数据进去,注意:它不是仅仅读入一个数据,而是读入一行数据(cache line)。
  3. 那么CPU 短时间内很可能会再次用到甚至多次用到这个 addr 的数据或者周围临近的数据,那么就可以直接从cache快速的获取数据。这样弥补了时间和空间上的”局部性“

写数据:

  1. CPU 写数据时,可以直接写内存,这很慢;也可以先把数据写入 cache,这很快。
  2. cache 中的数据终究是要写入内存的啊,这有 2 种写策略:
    2.1 写通(write through):
    ◆ 数据要同时写入 cache 和内存,所以 cache 和内存中的数据保持一致,但是它的效率很低。能改进吗?可以!使用“写缓冲器”:cache 大哥,你把数据给我就可以了,我来慢慢写,保证帮你写完。
    ◆ 有些写缓冲器有“写合并”的功能,比如 CPU 执行了 4 条写指令:写第 0、 1、 2、 3 个字节,每次写 1 字节;写缓冲器会把这 4 个写操作合并成一个写操作:写 word。对于内存来说,这没什么差别,但是对于硬件寄存器,这就有可能导致问题。
    ◆ 所以对于寄存器操作,不会启动 buffer 功能;对于内存操作,比如 LCD 的显存,可以启用 buffer 功能(cpu直接用write buffer进行操作frame buffer内存)
    2.2 写回(write back):
    ◆ 新数据只是写入 cache,不会立刻写入内存, cache 和内存中的数据并不一致。
    ◆ 新数据写入 cache 时,这一行 cache 被标为“脏” (dirty);当cache 不够用时,才需要把脏的数据写入内存。

对内存或者变量进行写操作可以使用写回功能,可以大幅提高效率。但是要注意 cache 和内存中的数据很可能不一致。这在很多时间要小心处理:比如 CPU 产生了新数据, DMA 把数据从内存搬到网卡,这时候就要 CPU 执行命令先把新数据从 cache 刷到内存。反过来也是一样的, DMA 从网卡得过了新数据存在内存里, CPU 读数据之前先把 cache中的数据丢弃。下图举例说明哪些硬件可以用或者不能用cache:
image

是否使用 cache、是否使用 buffer,就有 4 种组合(位于arch\arm\include\asm\pgtable-2level.h

image

以s3c2440芯片为例,上面 4 种组合对应下表中的各项:

是否启用 cache 是否启用 buffer 说明
0 0 Non-cached, non-buffered (NCNB)读、写都直达外设硬件
0 1 Non-cached buffered (NCB)读、写都直达外设硬件;写操作通过 buffer 实现, CPU 不等待操作完成, CPU 会马上执行下一条指令
1 0 Cached, write-through mode (WT),写通 ①读:cache hit时从 cahce 读数据; cache miss 时已入一行数据到 cache;②写:通过 buffer 实现, CPU 不等待写操作完成, CPU 会马上执行下一条指令
1 1 Cached, write-back mode (WB),写回 ①读: cache hit 时从 cahce 读数据;cache miss时已入一行数据到 cache;②写:通过 buffer 实现, cache hit 时新数据不会到达硬件,而是在 cahce 中被标为 “脏”; cache miss 时,通过 buffer写入硬件, CPU 不等待写操作完成, CPU 会马上执行下一条指令

◼ 第 1 种是不使用 cache 也不使用 buffer,读写时都直达硬件,这适合寄存器的读写。
◼ 第 2 种是不使用 cache 但是使用 buffer,写数据时会用 buffer 进行优化,可能会有“写合并”,这适合显存的操作。因为对显存很少有读操作,基本都是写操作,而写操作即使被“合并”也没有关系。
◼ 第 3 种是使用 cache 不使用 buffer,就是“ write through”,适用于只读设备:在读数据时用 cache 加速,基本不需要写。
◼ 第 4 种是既使用 cache 又使用 buffer,适合一般的内存读写

字符设备驱动-9-中断子系统-中断线程化-threaded_irq

1 threaded_irq引入#

工作队列用起来挺简单,但是它有一个缺点:工作队列中有多个 work,前一个 work 没处理完会影响后面的 work执行,导致后面的work没法快速响应。那么可以再内核自己创建一个线程来单独处理,不跟别的 work 凑在一块了。比如在 Linux 系统中,对于存储设备比如 SD/TF 卡,它的驱动程序就是这样做的,它有自己的内核线程。用kthread_creat创建内核线程。
对于中断处理,还有另一种方法:threaded irq,线程化的中断处理。中断的处理仍然可以认为分为上半部、下半部。上半部用来处理紧急的事情,下半部用一个内核线程来处理,这个内核线程专用于这个中断。

2 threaded_irq使用#

1异常中断引入

字符设备驱动-9-中断子系统-中断引入 | Hexo (fuzidage.github.io)前面已经提到了threaded_irq

  1. 你可以只提供 thread_fn,内核会提供默认的上半部处理函数irq_default_primary_handler,该函数只是返回一个IRQ_WAKE_THREAD。发生中断时,系统会立刻调用 handler 函数,然后唤醒某个内核线程,内核线程再来执行thread_fn 函数。
  2. 你也可以既提供handler函数,也提供thread_fn函数。等硬件中断到来,先执行handler函数,handler函数中返回IRQ_WAKE_THREAD去唤醒中断线程函数thread_fn

image

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
extern int __must_check
devm_request_threaded_irq(struct device *dev, unsigned int irq,
irq_handler_t handler, irq_handler_t thread_fn,
unsigned long irqflags, const char *devname,
void *dev_id);
//include\linux\interrupt.h
static inline int __must_check
devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id)
{
return devm_request_threaded_irq(dev, irq, handler, NULL, irqflags,
devname, dev_id);
}
extern int __must_check
request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn,
unsigned long flags, const char *name, void *dev);

static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
extern void free_irq(unsigned int, void *);

3 threaded_irq实例#

3.1 驱动源码编写#

驱动代码
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
#include <linux/module.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/fcntl.h>
#include <linux/timer.h>
#include <linux/workqueue.h>
#include <asm/current.h>

struct gpio_key{
int gpio;
struct gpio_desc *gpiod;
int flag;
int irq;
struct timer_list key_timer;
struct tasklet_struct tasklet;
struct work_struct work;
} ;
static struct gpio_key *gpio_keys_100ask;
static int major = 0;
static struct class *gpio_key_class;

#define BUF_LEN 128
static int g_keys[BUF_LEN];
static int r, w;

struct fasync_struct *button_fasync;
#define NEXT_POS(x) ((x+1) % BUF_LEN)
static int is_key_buf_empty(void){
return (r == w);
}

static int is_key_buf_full(void){
return (r == NEXT_POS(w));
}
static void put_key(int key){
if (!is_key_buf_full()){
g_keys[w] = key;
w = NEXT_POS(w);
}
}
static int get_key(void){
int key = 0;
if (!is_key_buf_empty()){
key = g_keys[r];
r = NEXT_POS(r);
}
return key;
}

static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);
static void key_timer_expire(unsigned long data){
struct gpio_key *gpio_key = data;
int val;
int key;
val = gpiod_get_value(gpio_key->gpiod);

printk("key_timer_expire key %d %d\n", gpio_key->gpio, val);
key = (gpio_key->gpio << 8) | val;
put_key(key);
wake_up_interruptible(&gpio_key_wait);
kill_fasync(&button_fasync, SIGIO, POLL_IN);
}

static void key_tasklet_func(unsigned long data){
struct gpio_key *gpio_key = data;
int val;
int key;

val = gpiod_get_value(gpio_key->gpiod);
printk("key_tasklet_func key %d %d\n", gpio_key->gpio, val);
}
static void key_work_func(struct work_struct *work){
struct gpio_key *gpio_key = container_of(work, struct gpio_key, work);
int val;

val = gpiod_get_value(gpio_key->gpiod);
printk("key_work_func: the process is %s pid %d\n",current->comm, current->pid);
printk("key_work_func key %d %d\n", gpio_key->gpio, val);
}
static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset){
int err;
int key;
if (is_key_buf_empty() && (file->f_flags & O_NONBLOCK))
return -EAGAIN;
wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());
key = get_key();
err = copy_to_user(buf, &key, 4);
return 4;
}
static unsigned int gpio_key_drv_poll(struct file *fp, poll_table * wait){
poll_wait(fp, &gpio_key_wait, wait);
return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM;
}

static int gpio_key_drv_fasync(int fd, struct file *file, int on){
if (fasync_helper(fd, file, on, &button_fasync) >= 0)
return 0;
else
return -EIO;
}
static struct file_operations gpio_key_drv = {
.owner = THIS_MODULE,
.read = gpio_key_drv_read,
.poll = gpio_key_drv_poll,
.fasync = gpio_key_drv_fasync,
};
static irqreturn_t gpio_key_isr(int irq, void *dev_id){
struct gpio_key *gpio_key = dev_id;
//printk("gpio_key_isr key %d irq happened\n", gpio_key->gpio);
tasklet_schedule(&gpio_key->tasklet);
mod_timer(&gpio_key->key_timer, jiffies + HZ/50);
schedule_work(&gpio_key->work);
return IRQ_WAKE_THREAD;
}

static irqreturn_t gpio_key_thread_func(int irq, void *data){
struct gpio_key *gpio_key = data;
int val;

val = gpiod_get_value(gpio_key->gpiod);
printk("gpio_key_thread_func: the process is %s pid %d\n",current->comm, current->pid);
printk("gpio_key_thread_func key %d %d\n", gpio_key->gpio, val);
return IRQ_HANDLED;
}

static int gpio_key_probe(struct platform_device *pdev){
int err;
struct device_node *node = pdev->dev.of_node;
int count;
int i;
enum of_gpio_flags flag;

count = of_gpio_count(node);
if (!count){
printk("%s %s line %d, there isn't any gpio available\n",
__FILE__, __FUNCTION__, __LINE__);
return -1;
}

gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
for (i = 0; i < count; i++){
gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag);
if (gpio_keys_100ask[i].gpio < 0){
printk("%s %s line %d, of_get_gpio_flags fail\n",
__FILE__, __FUNCTION__, __LINE__);
return -1;
}
gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;
gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio);
setup_timer(&gpio_keys_100ask[i].key_timer, key_timer_expire, &gpio_keys_100ask[i]);
gpio_keys_100ask[i].key_timer.expires = ~0;
add_timer(&gpio_keys_100ask[i].key_timer);
tasklet_init(&gpio_keys_100ask[i].tasklet, key_tasklet_func, &gpio_keys_100ask[i]);
INIT_WORK(&gpio_keys_100ask[i].work, key_work_func);
}

for (i = 0; i < count; i++){
//err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr
//, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING
//, "100ask_gpio_key", &gpio_keys_100ask[i]);
err = request_threaded_irq(gpio_keys_100ask[i].irq, gpio_key_isr
, gpio_key_thread_func,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING
, "100ask_gpio_key", &gpio_keys_100ask[i]);
}

major = register_chrdev(0, "100ask_gpio_key", &gpio_key_drv);
gpio_key_class = class_create(THIS_MODULE, "100ask_gpio_key_class");
if (IS_ERR(gpio_key_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "100ask_gpio_key");
return PTR_ERR(gpio_key_class);
}
device_create(gpio_key_class, NULL, MKDEV(major, 0), NULL, "100ask_gpio_key");
return 0;
}

static int gpio_key_remove(struct platform_device *pdev){
struct device_node *node = pdev->dev.of_node;
int count;
int i;
device_destroy(gpio_key_class, MKDEV(major, 0));
class_destroy(gpio_key_class);
unregister_chrdev(major, "100ask_gpio_key");
count = of_gpio_count(node);
for (i = 0; i < count; i++){
free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);
del_timer(&gpio_keys_100ask[i].key_timer);
tasklet_kill(&gpio_keys_100ask[i].tasklet);
}
kfree(gpio_keys_100ask);
return 0;
}
static const struct of_device_id ask100_keys[] = {
{ .compatible = "100ask,gpio_key" },
{ },
};
static struct platform_driver gpio_keys_driver = {
.probe = gpio_key_probe,
.remove = gpio_key_remove,
.driver = {
.name = "100ask_gpio_key",
.of_match_table = ask100_keys,
},
};
static int __init gpio_key_init(void){
int err;
err = platform_driver_register(&gpio_keys_driver);
return err;
}

static void __exit gpio_key_exit(void){
platform_driver_unregister(&gpio_keys_driver);
}
module_init(gpio_key_init);
module_exit(gpio_key_exit);
MODULE_LICENSE("GPL");

3.2 app代码编写#

app代码
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
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
#include <signal.h>
static int fd;

/*
* ./button_test /dev/100ask_button0
*
*/
int main(int argc, char **argv){
int val;
struct pollfd fds[1];
int timeout_ms = 5000;
int ret;
int flags;
int i;
if (argc != 2) {
printf("Usage: %s <dev>\n", argv[0]);
return -1;
}
fd = open(argv[1], O_RDWR | O_NONBLOCK);
if (fd == -1){
printf("can not open file %s\n", argv[1]);
return -1;
}
for (i = 0; i < 10; i++) {
if (read(fd, &val, 4) == 4)
printf("get button: 0x%x\n", val);
else
printf("get button: -1\n");
}
flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
while (1){
if (read(fd, &val, 4) == 4)
printf("get button: 0x%x\n", val);
else
printf("while get button: -1\n");
}
close(fd);
return 0;
}

3.3 驱动代码解析#

image
为每个按键注册中断服务

image
硬件中断上半部irq中做完重要事情如:清中断,然后返回IRQ_WAKE_THREAD

image
返回后,内核线程开始调度gpio_key_thread_func,中断线程化的处理函数gpio_key_thread_func做完后返回IRQ_HANDLED;

image
最后卸载驱动时取消irq注册.

4 threaded_irq内核机制#

前面中断相关结构体

字符设备驱动-9-中断子系统-中断结构体 | Hexo (fuzidage.github.io)讲过struct irq_desc结构:
image

1
2
1. 当发生中断时,handler函数被调用,如果返回IRQ_HANDLED,表示中断处理完毕,如果返回IRQ_WAKE_THREAD表示要唤醒thread_fn.
2. 内核线程唤醒后,执行thread_fn

4.1 request_threaded_irq过程#

点击查看代码
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
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id){
struct irqaction *action;
struct irq_desc *desc;
int retval;
if (irq == IRQ_NOTCONNECTED)
return -ENOTCONN;
if (((irqflags & IRQF_SHARED) && !dev_id) ||
(!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) ||
((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND)))
return -EINVAL;

desc = irq_to_desc(irq); //1
if (!desc)
return -EINVAL;

if (!irq_settings_can_request(desc) ||
WARN_ON(irq_settings_is_per_cpu_devid(desc)))
return -EINVAL;

if (!handler) {
if (!thread_fn)
return -EINVAL;
handler = irq_default_primary_handler;
}

action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;

action->handler = handler; //2
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;

retval = irq_chip_pm_get(&desc->irq_data);
if (retval < 0) {
kfree(action);
return retval;
}

chip_bus_lock(desc);
retval = __setup_irq(irq, desc, action); //3
chip_bus_sync_unlock(desc);

if (retval) {
irq_chip_pm_put(&desc->irq_data);
kfree(action->secondary);
kfree(action);
}

#ifdef CONFIG_DEBUG_SHIRQ_FIXME
if (!retval && (irqflags & IRQF_SHARED)) {
/*
* It's a shared IRQ -- the driver ought to be prepared for it
* to happen immediately, so let's make sure....
* We disable the irq to make sure that a 'real' IRQ doesn't
* run in parallel with our fake.
*/
unsigned long flags;

disable_irq(irq);
local_irq_save(flags);
handler(irq, dev_id);
local_irq_restore(flags);
enable_irq(irq);
}
#endif
return retval;
}

image

  1. 首先根据irq num获取到struct irq_desc信息。

  2. 然后分配、设置一个 irqaction 结构体。设置中断相关参数

  3. 然后进入__setup_irq__setup_irq 函数核心代码如下:

4.1.1 __setup_irq#

4.1.1.1 setup_irq_thread#

1
2
3
4
5
6
7
8
9
10
11
12
if (new->thread_fn && !nested) {
ret = setup_irq_thread(new, irq, false);
//setup_irq_thread函数核心代码如下:
if (!secondary) {
t = kthread_create(irq_thread, new, "irq/%d-%s", irq,
new->name);
} else {
t = kthread_create(irq_thread, new, "irq/%d-s-%s", irq,
new->name);
param.sched_priority -= 1;
}
new->thread = t;

image
image
①可以看到创建了irq_thread这个内核线程。线程名字为“irq/pid-中断名字”

kthread_create()只是创建一个内核线程,但并没有启动,需要调用wake_up_process()来启动线程,所以内核又帮我们定义了一个宏kthread_run来帮我们搞定. 来看kthread相关API:

image-20240811233308139

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define kthread_create(threadfn, data, namefmt, arg...) \
kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg)
struct task_struct *kthread_create_on_cpu(int (*threadfn)(void *data),
void *data,
unsigned int cpu,
const char *namefmt);
/**
* kthread_run - create and wake a thread.
* @threadfn: the function to run until signal_pending(current).
* @data: data ptr for @threadfn.
* @namefmt: printf-style name for the thread.
*
* Description: Convenient wrapper for kthread_create() followed by
* wake_up_process(). Returns the kthread or ERR_PTR(-ENOMEM).
*/
#define kthread_run(threadfn, data, namefmt, ...) \
({ \
struct task_struct *__k \
= kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
if (!IS_ERR(__k)) \
wake_up_process(__k); \
__k; \
})
int kthread_stop(struct task_struct *k);

②然后将创建内核线程返回的task_strcut给到irqaction.(new->trhead = t;这句)
我们知道irqaction就包含了thread_fnhandler

4.2 中断后处理thread_fn是怎么被执行的#

4.2.1 上半部硬件中断的调用过程#

无论是中断的上半部的handler, 还是后处理的thread_fn前面的字符设备驱动-9-中断子系统-GICv2架构解析 | Hexo (fuzidage.github.io)

设备驱动-10.中断子系统-5 armv7 GIC架构解析 - fuzidage - 博客园 (cnblogs.com)有引入介绍。

当中断产生时,用gdb看看gic驱动框架调用关系:

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
Breakpoint 1, gpio_keys_gpio_isr (irq=200, dev_id=0x863e6930) at drivers/input/keybo
ard/gpio_keys.c:393
393 {
(gdb) bt
#0 gpio_keys_gpio_isr (irq=200, dev_id=0x863e6930) at drivers/input/keyboard/gpio_k
eys.c:393
#1 0x80270528 in __handle_irq_event_percpu (desc=0x8616e300, flags=0x86517edc) at ke
rnel/irq/handle.c:145
#2 0x802705cc in handle_irq_event_percpu (desc=0x8616e300) at kernel/irq/handle.c:18
5
#3 0x80270640 in handle_irq_event (desc=0x8616e300) at kernel/irq/handle.c:202
#4 0x802738e8 in handle_level_irq (desc=0x8616e300) at kernel/irq/chip.c:518
#5 0x8026f7f8 in generic_handle_irq_desc (desc=<optimized out>) at ./include/linux/i
rqdesc.h:150
#6 generic_handle_irq (irq=<optimized out>) at kernel/irq/irqdesc.c:590
#7 0x805005e0 in mxc_gpio_irq_handler (port=0xc8, irq_stat=2252237104) at drivers/gp
io/gpio-mxc.c:274
#8 0x805006fc in mx3_gpio_irq_handler (desc=<optimized out>) at drivers/gpio/gpio-mx
c.c:291
#9 0x8026f7f8 in generic_handle_irq_desc (desc=<optimized out>) at ./include/linux/i
rqdesc.h:150
#10 generic_handle_irq (irq=<optimized out>) at kernel/irq/irqdesc.c:590
#11 0x8026fd0c in __handle_domain_irq (domain=0x86006000, hwirq=32, lookup=true, regs
=0x86517fb0) at kernel/irq/irqdesc.c:627
#12 0x80201484 in handle_domain_irq (regs=<optimized out>, hwirq=<optimized out>, dom
ain=<optimized out>) at ./include/linux/irqdesc.h:168
#13 gic_handle_irq (regs=0xc8) at drivers/irqchip/irq-gic.c:364
#14 0x8020b704 in __irq_usr () at arch/arm/kernel/entry-armv.S:464

image

从打印可以看出注册的上半部硬件中断handler的调用流程。

4.2.2 thread_fn的调用过程#

来看gpio_keys_gpio_isr是如何一层层调用上来的。从__handle_irq_event_percpu开始分析:(它在kernel\irq\handle.c中)
image
执行上半部提供的的handler函数。判断上半部返回值如果是IRQ_WAKE_THREAD,就调用__irq_wake_thread唤醒中断线程处理函数。

如果上半部返回值是IRQ_HANDLED,表示该中断无需线程化处理,直接退出。

4.2.2.1 __irq_wake_thread分析#

它在kernel\irq\handle.c中:

1
2
3
4
5
void __irq_wake_thread(struct irq_desc *desc, struct irqaction *action){
......
atomic_inc(&desc->threads_active);
wake_up_process(action->thread);
}

唤醒的是谁,就是action->thread,也就是对应前面kthread_create出来的irq_thread,再次贴图如下:
image

4.2.2.2 irq_thread函数分析#

kernel\irq\manage.c,平时irq_thread是处于休眠idle状态,不占用cpu资源。

当被唤醒后,irq_thread进入唤醒状态调用handler_fn,也就是最终使用者预先设定的action->thread_fn

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
/*
* Interrupt handler thread
*/
static int irq_thread(void *data){
struct callback_head on_exit_work;
struct irqaction *action = data;
struct irq_desc *desc = irq_to_desc(action->irq);
irqreturn_t (*handler_fn)(struct irq_desc *desc,
struct irqaction *action);

if (force_irqthreads && test_bit(IRQTF_FORCED_THREAD,
&action->thread_flags))
handler_fn = irq_forced_thread_fn;
else
handler_fn = irq_thread_fn;

init_task_work(&on_exit_work, irq_thread_dtor);
task_work_add(current, &on_exit_work, false);
irq_thread_check_affinity(desc, action);
while (!irq_wait_for_interrupt(action)) {
irqreturn_t action_ret;
irq_thread_check_affinity(desc, action);
action_ret = handler_fn(desc, action);//调用irq_thread_fn
if (action_ret == IRQ_HANDLED)
atomic_inc(&desc->threads_handled);
if (action_ret == IRQ_WAKE_THREAD)
irq_wake_secondary(desc, action);
wake_threads_waitq(desc);
}
/*
* Interrupts explicitly requested as threaded interrupts want to be
* preemtible - many of them need to sleep and wait for slow busses to
* complete.
*/
static irqreturn_t irq_thread_fn(struct irq_desc *desc,
struct irqaction *action){
irqreturn_t ret;
ret = action->thread_fn(action->irq, action->dev_id);//调用使用者预先设定的action->thread_fn
irq_finalize_oneshot(desc, action);
return ret;
}

image
image

字符设备驱动-9-中断子系统-中断线程化-workqueue

1工作队列workqueue引入#

定时器、 tasklet,它们都是在中断上下文中执行(softirq中完成的),它们无法休眠。那么如果一旦中断要处理耗时复杂的操作,就会显得很卡。那么使用内核线程来处理这些耗时的工作,那就可以解决系统卡顿的问题。
Linux内核中工作队列workqueue就是线程化处理的一种方式,“工作队列”(workqueue), 它是内核自带的内核线程。要使用“工作队列”,只需要把“工作”放入“工作队列"中,对应的内核线程就会取出 “工作”,执行里面的函数。

工作队列的应用场合:
要做的事情比较耗时,甚至可能需要休眠,那么可以使用工作队列。
缺点:多个工作(函数)是在某个内核线程中依序执行的,前面函数执行很慢,就会影响到后面的函数。
在多 CPU 的系统下,一个工作队列可以有多个内核线程,可以在一定程度上缓解这个问题。

工队队列的源码机制在Linux-4.9.88\kernel\workqueue.c,头文件在Linux-4.9.88\include\linux\workqueue.h

1.1 work_struct描述#

1
2
3
4
5
6
7
8
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};

image
image
表示一个work结构,一个任务或者叫做一个工作,里面的.func表示是要执行的任务函数,类型定义为:

1
typedef void (*work_func_t)(struct work_struct *work);

1.1.1 定义一个work#

1
2
3
4
5
6
7
8
#define DECLARE_WORK(n, f) \
struct work_struct n = __WORK_INITIALIZER(n, f)
#define DECLARE_DELAYED_WORK(n, f) \
struct delayed_work n = __DELAYED_WORK_INITIALIZER(n, f, 0)

//第 1 个宏是用来定义一个 work_struct 结构体,要指定它的函数。
//第 2 个宏用来定义一个 delayed_work 结构体,也要指定它的函数。所以“delayed”
// ,意思就是说要让它运行时,可以指定:某段时间之后你再执行

image
定义一个work为n, 并且初始化函数f.

如果代码中定义好了一个work_struct结构体,那么可以用INIT_WORK函数来初始化:
image
image

1
2
#define INIT_WORK(_work, _func)						\
__INIT_WORK((_work), (_func), 0)

1.1.2 使用work#

初始化完work后,调用schedule_work即可调度工作队列进行处理当前任务。
调用 schedule_work 时,就会把work_struct 结构体放入队列system_wq中,并唤醒对应的内核线程。内核线程就会从队列里把 work_struct 结构体取出来,执行里面的函数。
image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* schedule_work - put work task in global workqueue
* @work: job to be done
*
* Returns %false if @work was already on the kernel-global workqueue and
* %true otherwise.
*
* This puts a job in the kernel-global workqueue if it was not already
* queued and leaves it in the same position on the kernel-global
* workqueue otherwise.
*/
static inline bool schedule_work(struct work_struct *work)
{
return queue_work(system_wq, work);
}

可以看到system_wq是内核自带的队列,结构属性为struct workqueue_struct
image
如果不想用内核自带的system_wq来调度我们的work, 那么可以调用create_workqueue函数自行创建工队队列。然后用queue_work函数使能.

1.1.3 工作队列相关函数#

函数名 作用
create_workqueue 在 Linux 系统中已经有了现成的 system_wq 等工作队列,你当然也可以自己调用 create_workqueue 创建工作队列,对于 SMP 系统,这个工作队列会有多个内核线程与它对应,创建工作队列时,内核会帮这个工作队列创建多个内核线程
create_singlethread_workqueue 如果想只有一个内核线程与工作队列对应,可以用本函数创建工作队列,创建工作队列时,内核会帮这个工作队列创建一个内核线程
destroy_workqueue 销毁工作队列
schedule_work 调度执行一个具体的 work,执行的 work 将会被挂入 Linux 系统提供的工作队列
schedule_delayed_work 延迟一定时间去执行一个具体的任务,功能与 schedule_work 类似,多了一个延迟时间
queue_work 跟 schedule_work 类似,schedule_work 是在系统默认的工作队列上执行一个work,queue_work 需要自己指定工作队列
queue_delayed_work 跟 schedule_delayed_work 类似,schedule_delayed_work 是在系统默认的工作队列上执行一个 work,queue_delayed_work 需要自己指定工作队列
flush_work 等待一个 work 执行完毕,如果这个 work 已经被放入队列,那么本函数等它执行完毕,并且返回 true;如果这个 work 已经执行完华才调用本函数,那么直接返回 false
flush_delayed_work 等待一个 delayed_work 执行完毕,如果这个 delayed_work 已经被放入队列,那么本函数等它执行完毕,并且返回 true;如果这个 delayed_work 已经执行完华才调用本函数,那么直接返回 false

2 编写代码及解析#

2.1 workqueue用例驱动源码#

驱动代码
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
#include <linux/module.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/fcntl.h>
#include <linux/timer.h>
#include <linux/workqueue.h>
#include <asm/current.h>

struct gpio_key{
int gpio;
struct gpio_desc *gpiod;
int flag;
int irq;
struct timer_list key_timer;
struct tasklet_struct tasklet;
struct work_struct work;
} ;

static struct gpio_key *gpio_keys_100ask;
static int major = 0;
static struct class *gpio_key_class;

#define BUF_LEN 128
static int g_keys[BUF_LEN];
static int r, w;
struct fasync_struct *button_fasync;
#define NEXT_POS(x) ((x+1) % BUF_LEN)

static int is_key_buf_empty(void){
return (r == w);
}
static int is_key_buf_full(void){
return (r == NEXT_POS(w));
}
static void put_key(int key){
if (!is_key_buf_full()){
g_keys[w] = key;
w = NEXT_POS(w);
}
}

static int get_key(void){
int key = 0;
if (!is_key_buf_empty()){
key = g_keys[r];
r = NEXT_POS(r);
}
return key;
}

static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);
static void key_timer_expire(unsigned long data){
struct gpio_key *gpio_key = data;
int val;
int key;
val = gpiod_get_value(gpio_key->gpiod);
printk("key_timer_expire key %d %d\n", gpio_key->gpio, val);
key = (gpio_key->gpio << 8) | val;
put_key(key);
wake_up_interruptible(&gpio_key_wait);
kill_fasync(&button_fasync, SIGIO, POLL_IN);
}

static void key_tasklet_func(unsigned long data){
struct gpio_key *gpio_key = data;
int val;
int key;

val = gpiod_get_value(gpio_key->gpiod);
printk("key_tasklet_func key %d %d\n", gpio_key->gpio, val);
}

static void key_work_func(struct work_struct *work){
struct gpio_key *gpio_key = container_of(work, struct gpio_key, work);
int val;
val = gpiod_get_value(gpio_key->gpiod);
printk("key_work_func: the process is %s pid %d\n",current->comm, current->pid);
printk("key_work_func key %d %d\n", gpio_key->gpio, val);
}

static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset){
int err;
int key;
if (is_key_buf_empty() && (file->f_flags & O_NONBLOCK))
return -EAGAIN;
wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());
key = get_key();
err = copy_to_user(buf, &key, 4);
return 4;
}

static unsigned int gpio_key_drv_poll(struct file *fp, poll_table * wait){
poll_wait(fp, &gpio_key_wait, wait);
return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM;
}

static int gpio_key_drv_fasync(int fd, struct file *file, int on){
if (fasync_helper(fd, file, on, &button_fasync) >= 0)
return 0;
else
return -EIO;
}

static struct file_operations gpio_key_drv = {
.owner = THIS_MODULE,
.read = gpio_key_drv_read,
.poll = gpio_key_drv_poll,
.fasync = gpio_key_drv_fasync,
};
static irqreturn_t gpio_key_isr(int irq, void *dev_id){
struct gpio_key *gpio_key = dev_id;
//printk("gpio_key_isr key %d irq happened\n", gpio_key->gpio);
tasklet_schedule(&gpio_key->tasklet);
mod_timer(&gpio_key->key_timer, jiffies + HZ/50);
schedule_work(&gpio_key->work);
return IRQ_WAKE_THREAD;
}

static irqreturn_t gpio_key_thread_func(int irq, void *data){
struct gpio_key *gpio_key = data;
int val;

val = gpiod_get_value(gpio_key->gpiod);
printk("gpio_key_thread_func: the process is %s pid %d\n",current->comm, current->pid);
printk("gpio_key_thread_func key %d %d\n", gpio_key->gpio, val);
return IRQ_HANDLED;
}


static int gpio_key_probe(struct platform_device *pdev){
int err;
struct device_node *node = pdev->dev.of_node;
int count;
int i;
enum of_gpio_flags flag;

count = of_gpio_count(node);
if (!count){
printk("%s %s line %d, there isn't any gpio available\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}
gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
for (i = 0; i < count; i++){
gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag);
if (gpio_keys_100ask[i].gpio < 0){
printk("%s %s line %d, of_get_gpio_flags fail\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}
gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;
gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio);
setup_timer(&gpio_keys_100ask[i].key_timer, key_timer_expire, &gpio_keys_100ask[i]);
gpio_keys_100ask[i].key_timer.expires = ~0;
add_timer(&gpio_keys_100ask[i].key_timer);
tasklet_init(&gpio_keys_100ask[i].tasklet, key_tasklet_func, &gpio_keys_100ask[i]);
INIT_WORK(&gpio_keys_100ask[i].work, key_work_func);
}
for (i = 0; i < count; i++){
/*err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"100ask_gpio_key", &gpio_keys_100ask[i]);*/
err = request_threaded_irq(gpio_keys_100ask[i].irq
, gpio_key_isr, gpio_key_thread_func
, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING
, "100ask_gpio_key", &gpio_keys_100ask[i]);
}

major = register_chrdev(0, "100ask_gpio_key", &gpio_key_drv);
gpio_key_class = class_create(THIS_MODULE, "100ask_gpio_key_class");
if (IS_ERR(gpio_key_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "100ask_gpio_key");
return PTR_ERR(gpio_key_class);
}
device_create(gpio_key_class, NULL, MKDEV(major, 0), NULL, "100ask_gpio_key");
return 0;

}
static int gpio_key_remove(struct platform_device *pdev){
struct device_node *node = pdev->dev.of_node;
int count;
int i;
device_destroy(gpio_key_class, MKDEV(major, 0));
class_destroy(gpio_key_class);
unregister_chrdev(major, "100ask_gpio_key");
count = of_gpio_count(node);
for (i = 0; i < count; i++){
free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);
del_timer(&gpio_keys_100ask[i].key_timer);
tasklet_kill(&gpio_keys_100ask[i].tasklet);
}
kfree(gpio_keys_100ask);
return 0;
}
static const struct of_device_id ask100_keys[] = {
{ .compatible = "100ask,gpio_key" },
{ },
};
static struct platform_driver gpio_keys_driver = {
.probe = gpio_key_probe,
.remove = gpio_key_remove,
.driver = {
.name = "100ask_gpio_key",
.of_match_table = ask100_keys,
},
};

static int __init gpio_key_init(void){
return platform_driver_register(&gpio_keys_driver);
}
static void __exit gpio_key_exit(void){
platform_driver_unregister(&gpio_keys_driver);
}
module_init(gpio_key_init);
module_exit(gpio_key_exit);
MODULE_LICENSE("GPL");

2.2 分析#

image
image
为每一个按键都建立一个work_struct,并且初始化work。

1
INIT_WORK(&gpio_keys_100ask[i].work, key_work_func);

key_work_func是work里面函数,参数为该work自身。该函数只是简单打印该work的自身属性(work名字,work进程id),然后输出按键值。通过container_of找到父亲结构体gpio_key
注意:current是Linux内核自带的一个变量,外部驱动要引用它只需要包含头文件:

1
#include <asm/current.h>

image
image

中断到来后,这时候上半部完成清中断等一些列重要操作,使能workqueue工作队列,调用函数schedule_work
image
内核从系统工作队列system_wq从取出该work,执行里面的函数(key_work_func)

可以看到current信息:pid为428,内核线程名字为[kworker/0:1]
image
image

3 工作队列内部机制原理#

3.1 Linux 2.x 的工作队列创建过程#

代码在kernel\workqueue.c中:

1
2
3
4
5
6
init_workqueues//函数主体如下
keventd_wq = create_workqueue("events");
__create_workqueue((name), 0, 0)
for_each_possible_cpu(cpu) {
err = create_workqueue_thread(cwq, cpu);
p = kthread_create(worker_thread, cwq, fmt, wq->name, cpu);

image
image

会先分配一个workqueue结构创建一个system_wq工作队列,为每一个 CPU,都创建一个名为“events/X”的内核线程,X 从 0 开始。在创建 workqueue 的同时创建内核线程。

3.2 Linux 4.x 的工作队列创建过程#

Linux4.x 中,内核线程和工作队列是分开创建的。先创建内核线程,在 kernel\workqueue.c
对每一个cpu,都会创建2个work_pool结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
init_workqueues //函数主体如下:
/* initialize CPU pools */
for_each_possible_cpu(cpu) {
for_each_cpu_worker_pool(pool, cpu) {
/* 对每一个 CPU 都创建 2 个 worker_pool 结构体,它是含有 ID 的 */
/* 一个 worker_pool 对应普通优先级的 work,第 2 个对应高优先级的 work */
}
/* create the initial worker */
for_each_online_cpu(cpu) {
for_each_cpu_worker_pool(pool, cpu) {
/* 对每一个 CPU 的每一个 worker_pool,创建一个 worker */
/* 每一个 worker 对应一个内核线程 */
BUG_ON(!create_worker(pool));
}
}

image
create_worker 函数代码如下:
image

创建好内核线程后,再创建 workqueue:这里workqueue会和普通优先级的work_pool建立联系,以后给workqueue添加work的时候会放入work_pool中,执行对应work的时候唤醒相对应的work线程,比如kwork/0:1
image

image

3.3 schedule_work#

schedule_work 会将 work 添加到默认的工作队列也就是 system_wq 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static inline bool schedule_work(struct work_struct *work)
{
return queue_work(system_wq, work);
}
static inline bool queue_work(struct workqueue_struct *wq,
struct work_struct *work)
{
return queue_work_on(WORK_CPU_UNBOUND, wq, work);
}//queue_work_on ,增加一个参数 WORK_CPU_UNBOUND,这个参数并不是指将当前
//work 绑定到 unbound 类型的 worker_pool 中,只是说明调用者并不指定将当前
//work 绑定到哪个 cpu 上,由系统来分配 cpu.当然,调用者也可以直接使用
//queue_work_on 接口,通过第一个参数来指定当前 work 绑定的 cpu。

bool queue_work_on(int cpu, struct workqueue_struct *wq,
struct work_struct *work)
{
...
if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(work))) {
__queue_work(cpu, wq, work);
ret = true;
}
...
return ret;
}

3.3.1 __queue_work#

继续调用__queue_work.

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
static void __queue_work(int cpu, struct workqueue_struct *wq,
struct work_struct *work)
{
...
//获取 cpu 相关参数
if (req_cpu == WORK_CPU_UNBOUND)
cpu = wq_select_unbound_cpu(raw_smp_processor_id());
...

//检查当前的 work 是不是在这之前被添加到其他 worker_pool 中,
//如果是,就让它继续在原本的 worker_pool 上运行
last_pool = get_work_pool(work);
if (last_pool && last_pool != pwq->pool) {
struct worker *worker;

spin_lock(&last_pool->lock);

worker = find_worker_executing_work(last_pool, work);

if (worker && worker->current_pwq->wq == wq) {
pwq = worker->current_pwq;
} else {
/* meh... not running there, queue here */
spin_unlock(&last_pool->lock);
spin_lock(&pwq->pool->lock);
}
} else {
spin_lock(&pwq->pool->lock);
}

//如果超过 pwq 支持的最大的 work 数量,将work添加到 pwq->delayed_works 中
//,否则就添加到 pwq->pool->worklist 中。
if (likely(pwq->nr_active < pwq->max_active)) {
trace_workqueue_activate_work(work);
pwq->nr_active++;
worklist = &pwq->pool->worklist;
if (list_empty(worklist))
pwq->pool->watchdog_ts = jiffies;
} else {
work_flags |= WORK_STRUCT_DELAYED;
worklist = &pwq->delayed_works;
}
//添加 work 到队列中。
insert_work(pwq, work, worklist, work_flags);
}

主要由3个部分组成:

  1. 获取 cpu 参数
  2. 检查冲突
  3. 添加 work 到队列insert_work

3.3.1.1 insert_work#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void insert_work(struct pool_workqueue *pwq, struct work_struct *work,
struct list_head *head, unsigned int extra_flags)
{
struct worker_pool *pool = pwq->pool;
//设置 work 的 pwq 和 flag。
set_work_pwq(work, pwq, extra_flags);
//将 work 添加到 worklist 链表中
list_add_tail(&work->entry, head);
//为 pwq 添加引用计数
get_pwq(pwq);
//添加内存屏障,防止 cpu 将指令乱序排列
smp_mb();

//唤醒 worker 对应的内核线程
if (__need_more_worker(pool))
wake_up_worker(pool);
}

简单地说,就是将 work 插入到worker_pool->worklist中。
添加完之后,就会唤醒 worker_pool 中第一个处于idle状态worker->task内核线程,work 就会进入到待处理状态。

3.4 worker_thread调度#

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
static int worker_thread(void *__worker)
{
struct worker *worker = __worker;
struct worker_pool *pool = worker->pool;


worker->task->flags |= PF_WQ_WORKER;
woke_up:
spin_lock_irq(&pool->lock);
//在必要的时候删除 worker,退出当前线程。
if (unlikely(worker->flags & WORKER_DIE)) {
spin_unlock_irq(&pool->lock);
WARN_ON_ONCE(!list_empty(&worker->entry));
worker->task->flags &= ~PF_WQ_WORKER;

set_task_comm(worker->task, "kworker/dying");
ida_simple_remove(&pool->worker_ida, worker->id);
worker_detach_from_pool(worker, pool);
kfree(worker);
return 0;
}

worker_leave_idle(worker);
recheck:
//管理 worker 线程
if (!need_more_worker(pool))
goto sleep;

if (unlikely(!may_start_working(pool)) && manage_workers(worker))
goto recheck;

//执行 work
do {
struct work_struct *work =
list_first_entry(&pool->worklist,
struct work_struct, entry);

pool->watchdog_ts = jiffies;

if (likely(!(*work_data_bits(work) & WORK_STRUCT_LINKED))) {
process_one_work(worker, work);
if (unlikely(!list_empty(&worker->scheduled)))
process_scheduled_works(worker);
} else {
move_linked_works(work, &worker->scheduled, NULL);
process_scheduled_works(worker);
}
} while (keep_working(pool));

worker_set_flags(worker, WORKER_PREP);
sleep:
//处理完成,陷入睡眠
worker_enter_idle(worker);
__set_current_state(TASK_IDLE);
spin_unlock_irq(&pool->lock);
schedule();
goto woke_up;
}

worker_thread函数主要包括以下的几个主要部分:

  1. 管理 worker
  2. 执行 work
    1
    2
    3
    4
    5
    6
    7
    8
    9
    2.1 从当前worker_pool->worklist 中的链表元素取出work
    2.2 move_linked_works 将会在执行前将 work 添加到 worker->scheduled 链表中
    ,该接口和 list_add_tail 不同的是,这个接口会先删除链表中存在的节点并重新添加,
    保证不会重复添加,且始终添加到最后一个节点。
    2.3 process_scheduled_works 函数正式执行 work,该函数会遍历 worker->scheduled 链表,
    执行每一个 work,执行之前会做一些必要的检查,比如在同一个 cpu 上,
    一个 worker 不能在多个 worker 线程中被并发执行(这里的并发执行指的是同时加入到 schedule 链表),
    是否需要唤醒其它的 worker 来协助执行(碰到 cpu 消耗型的work 需要这么做),
    执行 work 的方式就是调用 work->func
    当执行完worker_pool->worklist 中所有的work之后,当前线程就会陷入睡眠.

3.5 linux5.1.x版本的workqueue bug#

在多核cpu调度时,使用workqueue会小概率出现WARNING: CPU: x PID: xx at linux_5.10/kernel/workqueue.c:1796 worker_enter_idle
call trace提示,然后cpu进入idel休眠状态。
image

由于如果在 WORKER_NOT_RUNNING 检查时和下面的 nr_running 增量之间被unbind_workers()抢占,我们可能会破坏 nr_running 重置并在新的未绑定池上留下意外的 pool->nr_running == 1

为了 防止这样的竞态产生,linux内核patch参考:
https://lore.kernel.org/lkml/20220114081544.899493450@linuxfoundation.org/
image
image

字符设备驱动-9-中断子系统-中断下半部-tasklet

1 tasklet引入#

设备驱动-10.中断子系统-1异常中断引入 - fuzidage - 博客园 (cnblogs.com)

字符设备驱动-9-中断子系统-中断引入 | Hexo (fuzidage.github.io)

介绍了硬件中断和软件中断,硬件中断有gpio中断,网卡,外部电路IP引起的中断,而软件中断则有定时器,tasklet这些为软件中断。cpu会先处理硬件中断,然后处理软件中断。

简单说可以认为内核中有一个数组softirq[], 里面有很多项,某一项对应timer,某一项表示tasklet, 每一项中都有一个.action函数,当内核处理完某一个硬件中断之后,处理软件中断时会找到对应的项,找到.action函数执行。

对于tasklet软件中断,它的.action函数对应tasklet_action,这个函数会从链表里取出每一个tasklet结构,执行里面的.func函数。

内核处理硬件中断的过程叫做中断上半部,处理软件中断的过程叫做中断下半部。上半部执行过程中,中断是禁止的,这里防止中断嵌套,也就是说来了更紧急的硬件中断,也要等这个上半部分处理完,上半部一般处理重要紧急事情。下半部执行过程中,中断是使能的,因此在下半部的处理过程它是可以响应其他中断。因此如果有不紧急但是耗时的事情放在下半部来处理,比如用tasklet。
image

2 tasklet是如何从从上半部调度到下半部分#

如何使能或者说调度tasklet呢?它其实就是把一个tasklet放入到内核tasklet链表中,假如硬件中断服务函数func_A(), 软件中断中也就是下半部放入tasklet中断的服务函数为func_B(), 那么可以想象看A和B被调度的次数因该是多对1的关系,应为软件中断由于是使能中断的,因此func_A()对应的硬件又可能会产生硬件中断。

3 tasklet使用#

3.1 初始化tasklet#

3.1.0 tasklet结构体#

image

1
2
3
4
5
6
7
struct tasklet_struct {
struct tasklet_struct *next;//tasklet链表
unsigned long state;//RUN表示正在运行,SCHED表示已被调度
atomic_t count;//0表示处于激活状态,不为0表示该tasklet禁止,不允许执行
void (*func)(unsigned long);//处理函数,等效softirq的action函数
unsigned long data;
};

\include\linux\interrupt.h
state 有 2 位:
image

1
2
3
4
5
◼ bit0 表示 TASKLET_STATE_SCHED
等于 1 时表示已经执行了 tasklet_schedule 把该 tasklet 放入队列了;(等于1表示放入队列就绪了)
tasklet_schedule 会判断该位,如果已经等于 1 那么它就不会再次把tasklet 放入队列。
◼ bit1 表示 TASKLET_STATE_RUN
等于 1 时,表示正在运行 tasklet 中的 func 函数;函数执行完后内核会把该位清 0

count 表示该 tasklet 是否使能:

1
等于 0 表示使能了,非 0 表示被禁止了。对于 count 非 0 的 tasklet,里面的 func 函数不会被执行

3.1.1 tasklet链表#

1
2
3
4
5
6
7
8
struct tasklet_head {                            
struct tasklet_struct *head;
struct tasklet_struct **tail;
};

static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec); //kernel/softirq.c
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);
//对应softirq的TASKLET_SOFTIRQ和HI_SOFTIRQ, 优先级分别为6和0

tasklet执行过程 TASKLET_SOFTIRQ对应执行函数为tasklet_actionHI_SOFTIRQtasklet_hi_action

3.1.2 tasklet_init#

image

1
extern void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);

3.1.3 DECLARE_TASKLET#

DECLARE_TASKLETDECLARE_TASKLET_DISABLED可以定义一个tasklet结构体。

1
2
//使用 DECLARE_TASKLET 定义的 tasklet 结构体,它是使能的;
//使用 DECLARE_TASKLET_DISABLED 定义的 tasklet 结构体,它是禁止的;使用之前要先调用 tasklet_enable 使能它。

3.2 使能/禁止 tasklet#

1
2
static inline void tasklet_enable(struct tasklet_struct *t);
static inline void tasklet_disable(struct tasklet_struct *t);

image

3.3 调度tasklet#

1
static inline void tasklet_schedule(struct tasklet_struct *t);

image
tasklet 放入链表,并且设置它的 TASKLET_STATE_SCHED 状态为1。tasklet_schedule 只是把 tasklet 放入内核队列,它的 func 函数会在软件中断的执行过程中被调用。

3.4 执行tasklet#

对于 TASKLET_SOFTIRQ 类型的 softirq,其handler是 tasklet_action,可以看到软中断执行,硬件中断是使能的。执行对应的func函数。

image-20240810224518977

3.5 tasklet_kill#

1
void tasklet_kill(struct tasklet_struct *t)

image

1
2
//如果一个 tasklet 未被调度, tasklet_kill 会把它的TASKLET_STATE_SCHED 状态清 0;
//如果一个 tasklet 已被调度,tasklet_kill 会等待它执行完华,再把它的TASKLET_STATE_SCHED 状态清 0。

通常在卸载驱动程序时调用 tasklet_kill

4 tasklet代码示例解析#

4.1 tasklet使用驱动源码#

驱动代码
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
#include <linux/module.h>
#include <linux/poll.h>

#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/fcntl.h>
#include <linux/timer.h>
struct gpio_key{
int gpio;
struct gpio_desc *gpiod;
int flag;
int irq;
struct timer_list key_timer;
struct tasklet_struct tasklet;
} ;
static struct gpio_key *gpio_keys_100ask;
static int major = 0;
static struct class *gpio_key_class;

#define BUF_LEN 128
static int g_keys[BUF_LEN];
static int r, w;
struct fasync_struct *button_fasync;

#define NEXT_POS(x) ((x+1) % BUF_LEN)

static int is_key_buf_empty(void){
return (r == w);
}

static int is_key_buf_full(void){
return (r == NEXT_POS(w));
}

static void put_key(int key){
if (!is_key_buf_full()){
g_keys[w] = key;
w = NEXT_POS(w);
}
}

static int get_key(void){
int key = 0;
if (!is_key_buf_empty()){
key = g_keys[r];
r = NEXT_POS(r);
}
return key;
}

static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);

static void key_timer_expire(unsigned long data){
/* data ==> gpio */
struct gpio_key *gpio_key = data;
int val;
int key;

val = gpiod_get_value(gpio_key->gpiod);

printk("key_timer_expire key %d %d\n", gpio_key->gpio, val);
key = (gpio_key->gpio << 8) | val;
put_key(key);
wake_up_interruptible(&gpio_key_wait);
kill_fasync(&button_fasync, SIGIO, POLL_IN);
}

static void key_tasklet_func(unsigned long data){
/* data ==> gpio */
struct gpio_key *gpio_key = data;
int val;
int key;

val = gpiod_get_value(gpio_key->gpiod);
printk("key_tasklet_func key %d %d\n", gpio_key->gpio, val);
}

static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset){
int err;
int key;
if (is_key_buf_empty() && (file->f_flags & O_NONBLOCK))
return -EAGAIN;
wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());
key = get_key();
err = copy_to_user(buf, &key, 4);
return 4;
}

static unsigned int gpio_key_drv_poll(struct file *fp, poll_table * wait){
poll_wait(fp, &gpio_key_wait, wait);
return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM;
}

static int gpio_key_drv_fasync(int fd, struct file *file, int on){
if (fasync_helper(fd, file, on, &button_fasync) >= 0)
return 0;
else
return -EIO;
}

static struct file_operations gpio_key_drv = {
.owner = THIS_MODULE,
.read = gpio_key_drv_read,
.poll = gpio_key_drv_poll,
.fasync = gpio_key_drv_fasync,
};
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
struct gpio_key *gpio_key = dev_id;
tasklet_schedule(&gpio_key->tasklet);
mod_timer(&gpio_key->key_timer, jiffies + HZ/50);
return IRQ_HANDLED;
}

static int gpio_key_probe(struct platform_device *pdev){
int err;
struct device_node *node = pdev->dev.of_node;
int count;
int i;
enum of_gpio_flags flag;

count = of_gpio_count(node);
if (!count){
printk("%s %s line %d, there isn't any gpio available\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}

gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
for (i = 0; i < count; i++){
gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag);
if (gpio_keys_100ask[i].gpio < 0){
printk("%s %s line %d, of_get_gpio_flags fail\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}
gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;
gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio);

setup_timer(&gpio_keys_100ask[i].key_timer, key_timer_expire, &gpio_keys_100ask[i]);
gpio_keys_100ask[i].key_timer.expires = ~0;
add_timer(&gpio_keys_100ask[i].key_timer);

tasklet_init(&gpio_keys_100ask[i].tasklet, key_tasklet_func, &gpio_keys_100ask[i]);
}

for (i = 0; i < count; i++){
err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr
, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "100ask_gpio_key", &gpio_keys_100ask[i]);
}

major = register_chrdev(0, "100ask_gpio_key", &gpio_key_drv);
gpio_key_class = class_create(THIS_MODULE, "100ask_gpio_key_class");
if (IS_ERR(gpio_key_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "100ask_gpio_key");
return PTR_ERR(gpio_key_class);
}
device_create(gpio_key_class, NULL, MKDEV(major, 0), NULL, "100ask_gpio_key");
return 0;
}
static int gpio_key_remove(struct platform_device *pdev){
struct device_node *node = pdev->dev.of_node;
int count;
int i;

device_destroy(gpio_key_class, MKDEV(major, 0));
class_destroy(gpio_key_class);
unregister_chrdev(major, "100ask_gpio_key");
count = of_gpio_count(node);
for (i = 0; i < count; i++){
free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);
del_timer(&gpio_keys_100ask[i].key_timer);
tasklet_kill(&gpio_keys_100ask[i].tasklet);
}
kfree(gpio_keys_100ask);
return 0;
}
static const struct of_device_id ask100_keys[] = {
{ .compatible = "100ask,gpio_key" },
{ },
};
static struct platform_driver gpio_keys_driver = {
.probe = gpio_key_probe,
.remove = gpio_key_remove,
.driver = {
.name = "100ask_gpio_key",
.of_match_table = ask100_keys,
},
};
static int __init gpio_key_init(void){
return platform_driver_register(&gpio_keys_driver);
}

static void __exit gpio_key_exit(void){
platform_driver_unregister(&gpio_keys_driver);
}
module_init(gpio_key_init);
module_exit(gpio_key_exit);
MODULE_LICENSE("GPL");
用户态测试代码
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

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
#include <signal.h>
static int fd;
/*
* ./button_test /dev/100ask_button0
*
*/
int main(int argc, char **argv){
int val;
struct pollfd fds[1];
int timeout_ms = 5000;
int ret;
int flags;
int i;

if (argc != 2) {
printf("Usage: %s <dev>\n", argv[0]);
return -1;
}
fd = open(argv[1], O_RDWR | O_NONBLOCK);
if (fd == -1){
printf("can not open file %s\n", argv[1]);
return -1;
}

for (i = 0; i < 10; i++) {
if (read(fd, &val, 4) == 4)
printf("get button: 0x%x\n", val);
else
printf("get button: -1\n");
}
flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
while (1){
if (read(fd, &val, 4) == 4)
printf("get button: 0x%x\n", val);
else
printf("while get button: -1\n");
}
close(fd);
return 0;
}

4.2 分析#

probe函数中为每一个gpio按键都创建一个tasklet
image
image
当按键按下,中断服务执行,那么此时需要调度tasklet去完成中断下半部的事情,每当一个按键按下,就会执行一次tasklet里面的函数,也就是key_tasklet_func
image
image
根据不同的gpio带来不同的data,这里key_tasklet_func下半部分只是简单打印出对应哪一个gpio,输出什么电平。
最终驱动卸载时调用tasklet_kill
image

5 tasklet内部机制剖析#

5.1 tasklet_action的执行过程#

tasklet属于TASKLET_SOFTIRQ软件中断,入口函数为tasklet_action,这在内核 kernel\softirq.c 中设置:
image
image

当驱动程序调用 tasklet_schedule 时,会设置 tasklet 的stateTASKLET_STATE_SCHED,并把它放入内核tasklet链表:
image
触发TASKLET_SOFTIRQ 软件中断,会调用 tasklet_action 函数,遍历tasklet 链表,进行状态判断后执行 .func
函数,从队列中删除 tasklet
可以看出:

1
2
3
1.tasklet_schedule 调度 tasklet 时,其中的函数并不会立刻执行,而只是把tasklet 放入队列
2.调用一次 tasklet_schedule,只会导致 tasklet 的函数被执行一次;如果 tasklet 的函数尚未执行
,多次调用 tasklet_schedule 也是无效的,只会放入队列一次,TASKLET_STATE_SCHED状态会自行判断。

最终tasklet中的func执行要看tasklet_action的过程分析:
image
首先从链表中取出每一项tasklet, 读取count, 如果等于0表示有使能该tasklet, 清除TASKLET_STATE_SCHED位,并且执行t->func,执行完该task中的func后从链表取出并且删除掉。