linux设备树

设备树是什么

设备树就是一个用来描述硬件的接口 相互的子节点关系 在写驱动程序的时候 要通过特定的接口 来找到这和设备的硬件信息 写出驱动

dts语法

先上一个设备树的代码

1
2
3
4
5
6
7
8
9
10
11
12
alphaled{
#address-cell=<1>;
#size-cell=<1>;
compatible="atkalpha-led";
status="okay";
reg=<
0X020C406C 0X04 /* CCM_CCGR1_BASE */
0X020E0068 0X04 /* SW_MUX_GPIO1_IO03_BASE */
0X020E02F4 0X04 /* SW_PAD_GPIO1_IO03_BASE */
0X0209C000 0X04 /* GPIO1_DR_BASE */
0X0209C004 0X04>;
};

这就是一个简单的设备树块 他描述了led这个硬件的结构 我们可以在驱动程序中 利用一系列的接口来找到他的信息

#address-cell #size-cell 这个是告诉 reg 地址单元的数量为1大小单元的数量为1 也对应了reg里每个都为1

compatible 这个在总线里用的比较多 就是匹配驱动和设备 这个到时候说

alphaled 这个就是可以利用这个来找到硬件的节点 然后进行匹配

内核解析

内核在启动的时候 会调用 start_kernel() 函数 这个函数会调用解析设备树的函数 然后会在设备的 /proc/devicetree 来体现
实例

特殊节点

可以发现在这里有两个比较特殊的接口分别是

  1. aliases:这个是用来定义别名访问节点的
  2. chosen:uboot的bootargs 就是往这里传递的

常见操作

在下一节写代码上

LED驱动

硬件原理图

image.png
可以看出led是连在gpio1_3 上的 我们需要做的步骤时

  1. 使能时钟GPIO1 的时钟由 CCM_CCGR1 的 bit27 和 bit26 这两个位控制 地址为
  2. 设置 GPIO1_IO03 的复用功能 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 找到地址和复用信息
    IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03
    可以看出她想要gpio1_3 就要设置成5
  3. 配置 GPIO1_IO03 IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03
  4. 设置 GPIO 就是每个寄存器都有各自的功能本实验中 GPIO1_IO03 是作为输出功能的,因此 GPIO1_GDIR 的 bit3 要设置为 1,表示输出。

代码

led驱动代码
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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
#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 <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : dtsled.c
作者 : 左忠凯
版本 : V1.0
描述 : LED驱动文件。
其他 : 无
论坛 : www.openedv.com
日志 : 初版V1.0 2019/7/9 左忠凯创建
***************************************************************/
#define DTSLED_CNT 1 /* 设备号个数 */
#define DTSLED_NAME "dtsled" /* 名字 */
#define LEDOFF 0 /* 关灯 */
#define LEDON 1 /* 开灯 */

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

/* dtsled设备结构体 */
struct dtsled_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct device_node *nd; /* 设备节点 */
};

struct dtsled_dev dtsled; /* led设备 */

/*
* @description : LED打开/关闭
* @param - sta : LEDON(0) 打开LED,LEDOFF(1) 关闭LED
* @return : 无
*/
void led_switch(u8 sta)
{
u32 val = 0;
if(sta == LEDON) {
val = readl(GPIO1_DR);
val &= ~(1 << 3);
writel(val, GPIO1_DR);
}else if(sta == LEDOFF) {
val = readl(GPIO1_DR);
val|= (1 << 3);
writel(val, GPIO1_DR);
}
}

/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &dtsled; /* 设置私有数据 */
return 0;
}

/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}

/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
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;

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

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

if(ledstat == LEDON) {
led_switch(LEDON); /* 打开LED灯 */
} else if(ledstat == LEDOFF) {
led_switch(LEDOFF); /* 关闭LED灯 */
}
return 0;
}

/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int led_release(struct inode *inode, struct file *filp)
{
return 0;
}

/* 设备操作函数 */
static struct file_operations dtsled_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};

/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static int __init led_init(void)
{
u32 val = 0;
int ret;
u32 regdata[14];
const char *str;
struct property *proper;

/* 获取设备树中的属性数据 */
/* 1、获取设备节点:alphaled */
dtsled.nd = of_find_node_by_path("/alphaled");
if(dtsled.nd == NULL) {
printk("alphaled node nost find!\r\n");
return -EINVAL;
} else {
printk("alphaled node find!\r\n");
}

/* 2、获取compatible属性内容 */
proper = of_find_property(dtsled.nd, "compatible", NULL);
if(proper == NULL) {
printk("compatible property find failed\r\n");
} else {
printk("compatible = %s\r\n", (char*)proper->value);
}

/* 3、获取status属性内容 */
ret = of_property_read_string(dtsled.nd, "status", &str);
if(ret < 0){
printk("status read failed!\r\n");
} else {
printk("status = %s\r\n",str);
}

/* 4、获取reg属性内容 */
ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 10);
if(ret < 0) {
printk("reg property read failed!\r\n");
} else {
u8 i = 0;
printk("reg data:\r\n");
for(i = 0; i < 10; i++)
printk("%#X ", regdata[i]);
printk("\r\n");
}

/* 初始化LED */
#if 0
/* 1、寄存器地址映射 */
IMX6U_CCM_CCGR1 = ioremap(regdata[0], regdata[1]);
SW_MUX_GPIO1_IO03 = ioremap(regdata[2], regdata[3]);
SW_PAD_GPIO1_IO03 = ioremap(regdata[4], regdata[5]);
GPIO1_DR = ioremap(regdata[6], regdata[7]);
GPIO1_GDIR = ioremap(regdata[8], regdata[9]);
#else
IMX6U_CCM_CCGR1 = of_iomap(dtsled.nd, 0);
SW_MUX_GPIO1_IO03 = of_iomap(dtsled.nd, 1);
SW_PAD_GPIO1_IO03 = of_iomap(dtsled.nd, 2);
GPIO1_DR = of_iomap(dtsled.nd, 3);
GPIO1_GDIR = of_iomap(dtsled.nd, 4);
#endif

/* 2、使能GPIO1时钟 */
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26); /* 清楚以前的设置 */
val |= (3 << 26); /* 设置新值 */
writel(val, IMX6U_CCM_CCGR1);

/* 3、设置GPIO1_IO03的复用功能,将其复用为
* GPIO1_IO03,最后设置IO属性。
*/
writel(5, SW_MUX_GPIO1_IO03);

/*寄存器SW_PAD_GPIO1_IO03设置IO属性
*bit 16:0 HYS关闭
*bit [15:14]: 00 默认下拉
*bit [13]: 0 kepper功能
*bit [12]: 1 pull/keeper使能
*bit [11]: 0 关闭开路输出
*bit [7:6]: 10 速度100Mhz
*bit [5:3]: 110 R0/6驱动能力
*bit [0]: 0 低转换率
*/
writel(0x10B0, SW_PAD_GPIO1_IO03);

/* 4、设置GPIO1_IO03为输出功能 */
val = readl(GPIO1_GDIR);
val &= ~(1 << 3); /* 清除以前的设置 */
val |= (1 << 3); /* 设置为输出 */
writel(val, GPIO1_GDIR);

/* 5、默认关闭LED */
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);

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

/* 2、初始化cdev */
dtsled.cdev.owner = THIS_MODULE;
cdev_init(&dtsled.cdev, &dtsled_fops);

/* 3、添加一个cdev */
cdev_add(&dtsled.cdev, dtsled.devid, DTSLED_CNT);

/* 4、创建类 */
dtsled.class = class_create(THIS_MODULE, DTSLED_NAME);
if (IS_ERR(dtsled.class)) {
return PTR_ERR(dtsled.class);
}

/* 5、创建设备 */
dtsled. dtsled.class = device_create(dtsled.class, NULL, dtsled.devid, NULL, DTSLED_NAME);
if (IS_ERR(dtsled.device)) {
return PTR_ERR(dtsled.device);
}

return 0;
}

/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit led_exit(void)
{
/* 取消映射 */
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);

/* 注销字符设备驱动 */
cdev_del(&dtsled.cdev);/* 删除cdev */
unregister_chrdev_region(dtsled.devid, DTSLED_CNT); /* 注销设备号 */

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

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

解释一下这个代码 其实前面也没啥好解释的 主要就时候后面这一段

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
if (dtsled.major) {		/*  定义了设备号 */
dtsled.devid = MKDEV(dtsled.major, 0);
register_chrdev_region(dtsled.devid, DTSLED_CNT, DTSLED_NAME);
} else { /* 没有定义设备号 */
alloc_chrdev_region(&dtsled.devid, 0, DTSLED_CNT, DTSLED_NAME); /* 申请设备号 */
dtsled.major = MAJOR(dtsled.devid); /* 获取分配号的主设备号 */
dtsled.minor = MINOR(dtsled.devid); /* 获取分配号的次设备号 */
}
printk("dtsled major=%d,minor=%d\r\n",dtsled.major, dtsled.minor);

/* 2、初始化cdev */
dtsled.cdev.owner = THIS_MODULE;
cdev_init(&dtsled.cdev, &dtsled_fops);

/* 3、添加一个cdev */
cdev_add(&dtsled.cdev, dtsled.devid, DTSLED_CNT);

/* 4、创建类 */
dtsled.class = class_create(THIS_MODULE, DTSLED_NAME);
if (IS_ERR(dtsled.class)) {
return PTR_ERR(dtsled.class);
}

/* 5、创建设备 */
dtsled. dtsled.class = device_create(dtsled.class, NULL, dtsled.devid, NULL, DTSLED_NAME);
if (IS_ERR(dtsled.device)) {
return PTR_ERR(dtsled.device);
}

return 0;

这段代码的作用就是 在/dev文档里生出来一个设备节点文件 通过调用这个文件 来联系驱动 进而驱使硬件
其实这个过程哥们也不太明白这里卖弄的cdev和clss 但是我明白这就是创建一个节点的过程 通过它来 链接硬件的ops 然后 create直接创建对应的文件 然后就可以直接操作 。

pinctrl 和 gpio 子系统实验

pinctrl

刚才我们编写led的程序 又臭又长 主要是由这些代码

1
2
3
4
5
IMX6U_CCM_CCGR1 = of_iomap(dtsled.nd, 0);
SW_MUX_GPIO1_IO03 = of_iomap(dtsled.nd, 1);
SW_PAD_GPIO1_IO03 = of_iomap(dtsled.nd, 2);
GPIO1_DR = of_iomap(dtsled.nd, 3);
GPIO1_GDIR = of_iomap(dtsled.nd, 4);

写pin 的方式就是直接操作相应的寄存器,但是这种配置方式比较繁琐、而且容易出问题(比如 pin 功能冲突)我们就引入了 pinxtrl 的概念 主要就是为了 解决在驱动文件里直接配置寄存器的办法
举个例子pinctrl 是怎么工作的
打开 .dts文件 里面有

1
2
3
4
5
6
7
8
 &iomuxc {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_hog_1>;
imx6ul-evk {
pinctrl_hog_1: hoggrp-1 {
fsl,pins = <
MX6UL_PAD_UART1_RTS_B__GPIO1_IO19
}

MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 代表一个宏名 可以去文件中找到 对应这个 #define MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x0090 0x031C 0x0000 0x5 0x0
这就是配置这个io复用 以及电器属性 的配置 用这个fsl,pin 来配置 就可以直接配置好接口 不用在驱动文件里配置
那么问题又来了 我只是配置好了 那在驱动里 哥们要写这个寄存器的配置啊 咋办 别急 看下一集

gpio

我们可以在设备树加上 例如 cd-gpios = <&gpio1 19 GPIO_ACTIVE_LOW>; “&gpio1”表示 CD 引脚所使用的 IO 属于 GPIO1 组,“19”
表示 GPIO1 组的第 19 号 IO,“GPIO_ACTIVE_LOW”表示低电平有效,如果改为“GPIO_ACTIVE_HIGH”就表示高电平有效。然后我们就可以通过 函数的接口配置 往寄存器中读写数据 我就贴几个常见的吧 算了 一会实例编程就有了 (偷懒 )

实例编程

来吧 我们实战一下
我们首先修改

  1. pinctrl:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
     pinctrl_led: ledgrp {
    fsl,pins = <
    MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0 /* LED0 */
    >;
    };
    2. gpio:
    ```c
    gpioled {
    #address-cells = <1>;
    #size-cells = <1>;
    compatible = "atkalpha-gpioled";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_led>;
    led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
    status = "okay";
    };
    3.编程 其实跟之前差不多
    就是在结构体处 加上int led_gpio; 在改变led状态的时候 gpio_set_value(dev->led_gpio, 0);
    读取led_gpiogpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio", 0); 就可以了

并发与竞争

这都是很老生常谈的问题了 就不用多说了 咱们主要来研究解决竞争的几种方法

原子操作

咱们先来说说正常操作的隐患
比如我的a程序写了 a=3 其实真是a=3? 他中间要有很多过程 比如

1
2
3
ldr r0, =0X30000000 /* 变量 a 地址 */
ldr r1, = 3 /* 要写入的值 */
str r1, [r0] /* 将 3 写入到 a 变量中 */

这个时候如果时间片用完了 轮到了别的程序 就会把这个结果覆盖掉,Linux 内核定义了叫做 atomic_t 的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量,它的用法
atomic_t a; //定义 a
atomic_t b = ATOMIC_INIT(0);

函数 描述
ATOMIC_INIT(int i) 定义原子变量的时候对其初始化。
int atomic_read(atomic_t *v) 读取 v 的值,并且返回。

懒得写了 到时候自己查吧

现在的问题是 我们要保护的东西可不止是整形 有可能是一段资源 那咋办 到了上锁这步了

自旋锁

自旋锁的“自旋”也就是“原地打转”的意思,“原地打转”的目的是为了等待自旋锁可以用,可以访问共享资源 适合段时间占有锁 代码一会贴 先来看看 长见应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
DEFINE_SPINLOCK(lock) /* 定义并初始化一个锁 */

/* 线程 A */
void functionA (){
unsigned long flags; /* 中断状态 */
spin_lock_irqsave(&lock, flags) /* 获取锁 */
/* 临界区 */
spin_unlock_irqrestore(&lock, flags) /* 释放锁 */
}

/* 中断服务函数 */
void irq() {
spin_lock(&lock) /* 获取锁 */
/* 临界区 */
spin_unlock(&lock) /* 释放锁 */
}

信号量

相比于自旋锁,信号量可以使线程进入休眠状态,比如 A 与 B、C 合租了一套房子,这个房子只有一个厕所,一次只能一个人使用。某一天早上 A 去上厕所了,过了一会 B 也想用厕所,因为 A 在厕所里面,所以 B 只能等到 A 用来了才能进去。B 要么就一直在厕所门口等着,等 A 出来,这个时候就相当于自旋锁。B 也可以告诉 A,让 A 出来以后通知他一下,然后 B 继续回房间睡觉,这个时候相当于信号量 而且信号量可以规定多个进程共享资源

1
2
3
4
5
6

struct semaphore sem; /* 定义信号量 */
sema_init(&sem, 1); /* 初始化信号量 */
down(&sem); /* 申请信号量 */
/* 临界区 */
up(&sem); /* 释放信号量 */

互斥体

将信号量的值设置为 1 就可以使用信号量进行互斥访问了,虽然可以通过信号量实现互斥,但是 Linux 提供了一个比信号量更专业的机制来进行互斥,它就是互斥体—mutex

1
2
3
4
5
struct mutex lock; /* 定义一个互斥体 */
mutex_init(&lock); /* 初始化互斥体 */
mutex_lock(&lock); /* 上锁 */
/* 临界区 */
mutex_unlock(&lock); /* 解锁 */

并发竞争实验

原子变量

就是定义了一个一原子变量 程序打开时判断是否为0 不为零减一 为零就返回错误 以上个led代码为例 我们要想使用led 可以在结构提处加上
atomic_t lock; 在init上初始化变量atomic_set(&gpioled.lock, 1); 关闭区驱动释放变量atomic_inc(&dev->lock);
在打开文件是 使用

1
2
3
4
 if (!atomic_dec_and_test(&gpioled.lock)) {
atomic_inc(&gpioled.lock);/* 小于 0 的话就加 1,使其原子变量等于 0 */
return -EBUSY; /* LED 被使用,返回忙 */
}

自旋锁

涛声依旧还是在之前的代码上进行修改
在结构体处加上int dev_stats; spinlock_t lock; 代表锁 设备状态,0,设备未使用;>0,设备已经被使用 init spin_lock_init(&gpioled.lock);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// open 函数
spin_lock_irqsave(&gpioled.lock, flags); /* 上锁 */
if (gpioled.dev_stats) { /* 如果设备被使用了 */
spin_unlock_irqrestore(&gpioled.lock, flags); /* 解锁 */
return -EBUSY;
}
gpioled.dev_stats++; /* 如果设备没有打开,那么就标记已经打开了 */
spin_unlock_irqrestore(&gpioled.lock, flags);/* 解锁 */

return 0;
//release
spin_lock_irqsave(&dev->lock, flags); /* 上锁 */
if (dev->dev_stats) {
dev->dev_stats--;
}
spin_unlock_irqrestore(&dev->lock, flags);/* 解锁 */

return 0;

信号量

struct semaphore sem; 结构提出加上信号量函数
sema_init(&gpioled.sem, 1); init 初始化

1
2
3
4
5
6
//open
if (down_interruptible(&gpioled.sem)) {
return -ERESTARTSYS;
}
//release
up(&dev->sem);

按键输入实验

直接贴代码吧 感觉和led一样 就是换成了输入 然后在里面加了一个原子变量

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/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 <l
#include <linux/semaphore.h>inux/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>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : key.c
作者 : 左忠凯
版本 : V1.0
描述 : Linux按键输入驱动实验
其他 : 无
论坛 : www.openedv.com
日志 : 初版V1.0 2019/7/18 左忠凯创建
***************************************************************/
#define KEY_CNT 1 /* 设备号个数 */
#define KEY_NAME "key" /* 名字 */

/* 定义按键值 */
#define KEY0VALUE 0XF0 /* 按键值 */
#define INVAKEY 0X00 /* 无效的按键值 */

/* key设备结构体 */
struct key_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct device_node *nd; /* 设备节点 */
int key_gpio; /* key所使用的GPIO编号 */
atomic_t keyvalue; /* 按键值 */
};

struct key_dev keydev; /* key设备 */

/*
* @description : 初始化按键IO,open函数打开驱动的时候
* 初始化按键所使用的GPIO引脚。
* @param : 无
* @return : 无
*/
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;
}

/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int key_open(struct inode *inode, struct file *filp)
{
int ret = 0;
filp->private_data = &keydev; /* 设置私有数据 */

ret = keyio_init(); /* 初始化按键IO */
if (ret < 0) {
return ret;
}

return 0;
}

/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t key_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int ret = 0;
int value;
struct key_dev *dev = filp->private_data;

if (gpio_get_value(dev->key_gpio) == 0) { /* key0按下 */
while(!gpio_get_value(dev->key_gpio)); /* 等待按键释放 */
atomic_set(&dev->keyvalue, KEY0VALUE);
} else {
atomic_set(&dev->keyvalue, INVAKEY); /* 无效的按键值 */
}

value = atomic_read(&dev->keyvalue);
ret = copy_to_user(buf, &value, sizeof(value));
return ret;
}

/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t key_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}

/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int key_release(struct inode *inode, struct file *filp)
{
return 0;
}

/* 设备操作函数 */
static struct file_operations key_fops = {
.owner = THIS_MODULE,
.open = key_open,
.read = key_read,
.write = key_write,
.release = key_release,
};

/*
* @description : 驱动入口函数
* @param : 无
* @return : 无
*/
static int __init mykey_init(void)
{
/* 初始化原子变量 */
atomic_set(&keydev.keyvalue, INVAKEY);

/* 注册字符设备驱动 */
/* 1、创建设备号 */
if (keydev.major) { /* 定义了设备号 */
keydev.devid = MKDEV(keydev.major, 0);
register_chrdev_region(keydev.devid, KEY_CNT, KEY_NAME);
} else { /* 没有定义设备号 */
alloc_chrdev_region(&keydev.devid, 0, KEY_CNT, KEY_NAME); /* 申请设备号 */
keydev.major = MAJOR(keydev.devid); /* 获取分配号的主设备号 */
keydev.minor = MINOR(keydev.devid); /* 获取分配号的次设备号 */
}

/* 2、初始化cdev */
keydev.cdev.owner = THIS_MODULE;
cdev_init(&keydev.cdev, &key_fops);

/* 3、添加一个cdev */
cdev_add(&keydev.cdev, keydev.devid, KEY_CNT);

/* 4、创建类 */
keydev.class = class_create(THIS_MODULE, KEY_NAME);
if (IS_ERR(keydev.class)) {
return PTR_ERR(keydev.class);
}

/* 5、创建设备 */
keydev.device = device_create(keydev.class, NULL, keydev.devid, NULL, KEY_NAME);
if (IS_ERR(keydev.device)) {
return PTR_ERR(keydev.device);
}

return 0;
}

/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit mykey_exit(void)
{
/* 注销字符设备驱动 */
gpio_free(keydev.key_gpio);
cdev_del(&keydev.cdev);/* 删除cdev */
unregister_chrdev_region(keydev.devid, KEY_CNT); /* 注销设备号 */

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

module_init(mykey_init);
module_exit(mykey_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");

Linux 内核定时器实验

这代码又臭又长 我贴个思路吧

计时器的思路

首先定义一个定时器 struct timer_list timer;
然后定义一个函数 以及初始化函数

1
2
3
4
5
6
7
8
9
10
11
12
13
 void function(unsigned long arg){
mod_timer(&dev->timertest, jiffies + msecs_to_jiffies(2000));
}
17 void init(void)
{
init_timer(&timer); /* 初始化定时器 */

timer.function = function; /* 设置定时处理函数 */
timer.expires=jffies + msecs_to_jiffies(2000);/* 超时时间 2 秒 */
timer.data = (unsigned long)&dev; /* 将设备结构体作为参数 */

add_timer(&timer); /* 启动定时器 */
}

这样就完成了一个周期定时器的创建

控制函数

首先定义好

1
2
3
#define CLOSE_CMD (_IO(0XEF, 0x1)) /* 关闭定时器 */
#define OPEN_CMD (_IO(0XEF, 0x2)) /* 打开定时器 */
#define SETPERIOD_CMD (_IO(0XEF, 0x3))

然后再 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
 struct timer_dev *dev = (struct timer_dev *)filp->private_data;
int timerperiod;
unsigned long flags;

switch (cmd) {
case CLOSE_CMD: /* 关闭定时器 */
del_timer_sync(&dev->timer);
break;
case OPEN_CMD: /* 打开定时器 */
spin_lock_irqsave(&dev->lock, flags);
timerperiod = dev->timeperiod;
spin_unlock_irqrestore(&dev->lock, flags);
mod_timer(&dev->timer, jiffies +
msecs_to_jiffies(timerperiod));
break;
case SETPERIOD_CMD: /* 设置定时器周期 */
spin_lock_irqsave(&dev->lock, flags);
dev->timeperiod = arg;
spin_unlock_irqrestore(&dev->lock, flags);
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(arg));
break;
default:
break;
}
ret

中断实验

中断流程

就是我们每一个引脚区域 都对应着不同的中断号 我们可以利用函数 多这些中短号注册函数 在定义的状态发生改变时会通知注册的函数 达到终端启用函数的效果
例如 我们的gpio5节点 的设备树为

1
2
3
4
5
6
7
8
9
10
  gpio5: gpio@020ac000 {
compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
reg = <0x020ac000 0x4000>;
interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};

在这里interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,<GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;描述了gpio5 对应了两个中断号 74和75 interrupt-controller 表明了 gpio5 节点也是个中断控制器,用于控制 gpio5 所有 IO的中断。
而使用该节点的具体设备
1
2
3
4
5
6
7
  fxls8471@1e {
compatible = "fsl,fxls8471";
reg = <0x1e>;
position = <0>;
interrupt-parent = <&gpio5>;
interrupts = <0 8>;
};

interrupt-parent 属性设置中断控制器,这里使用 gpio5 作为中断控制器。interrupts 设置中断信息,0 表示 GPIO5_IO00,8 表示低电平触发。

中断相关的函数接口

获取中断号

irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号

编程实例

先来说一下想要的流程 就是现在init 函数里注册中断 按键按下 中断开启 启动定时器 目的是为了消抖 然后把读取出来的结果传递给应用程序

设备树修改

就是在key的设备书里 加上 8 interrupt-parent = <&gpio1>;9 interrupts = <18 IRQ_TYPE_EDGE_BOTH>; /* FALLING RISING */

驱动程序编写

代码 我就挑着写吧

定义结构体

在终端里定义两个结构体
irq_keydesc 和 imx6uirq_dev

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct irq_keydesc {
int gpio; /* gpio */
int irqnum; /* 中断号 */
unsigned char value; /* 按键对应的键值 */
char name[10]; /* 名字 */
irqreturn_t (*handler)(int, void *); /* 中断服务函数 */
};

/* imx6uirq设备结构体 */
struct imx6uirq_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct device_node *nd; /* 设备节点 */
atomic_t keyvalue; /* 有效的按键键值 */
atomic_t releasekey; /* 标记是否完成一次完成的按键,包括按下和释放 */
struct timer_list timer;/* 定义一个定时器*/
struct irq_keydesc irqkeydesc[KEY_NUM]; /* 按键描述数组 */
unsigned char curkeynum; /* 当前的按键号 */
};

注册中断

首先获取节点 nd 然后找到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
35
36
37
38
39
40
41
42
 	unsigned char i = 0;
int ret = 0;

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

/* 提取GPIO */
for (i = 0; i < KEY_NUM; i++) {
imx6uirq.irqkeydesc[i].gpio = of_get_named_gpio(imx6uirq.nd ,"key-gpio", i);
if (imx6uirq.irqkeydesc[i].gpio < 0) {
printk("can't get key%d\r\n", i);
}
}

/* 初始化key所使用的IO,并且设置成中断模式 */
for (i = 0; i < KEY_NUM; i++) {
memset(imx6uirq.irqkeydesc[i].name, 0, sizeof(imx6uirq.irqkeydesc[i].name)); /* 缓冲区清零 */
sprintf(imx6uirq.irqkeydesc[i].name, "KEY%d", i); /* 组合名字 */
gpio_request(imx6uirq.irqkeydesc[i].gpio, imx6uirq.irqkeydesc[i].name);
gpio_direction_input(imx6uirq.irqkeydesc[i].gpio);
imx6uirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(imx6uirq.nd, i);
#if 0
imx6uirq.irqkeydesc[i].irqnum = gpio_to_irq(imx6uirq.irqkeydesc[i].gpio);
#endif
printk("key%d:gpio=%d, irqnum=%d\r\n",i, imx6uirq.irqkeydesc[i].gpio,
imx6uirq.irqkeydesc[i].irqnum);
}
/* 申请中断 */
imx6uirq.irqkeydesc[0].handler = key0_handler;
imx6uirq.irqkeydesc[0].value = KEY0VALUE;

for (i = 0; i < KEY_NUM; i++) {
ret = request_irq(imx6uirq.irqkeydesc[i].irqnum, imx6uirq.irqkeydesc[i].handler,
IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, imx6uirq.irqkeydesc[i].name, &imx6uirq);
if(ret < 0){
printk("irq %d request failed!\r\n", imx6uirq.irqkeydesc[i].irqnum);
return -EFAULT;
}
}

写计时器函数 和中断函数

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

static irqreturn_t key0_handler(int irq, void *dev_id)
{
struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;

dev->curkeynum = 0;
dev->timer.data = (volatile long)dev_id;
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10)); /* 10ms定时 */
return IRQ_RETVAL(IRQ_HANDLED);
}

/* @description : 定时器服务函数,用于按键消抖,定时器到了以后
* 再次读取按键值,如果按键还是处于按下状态就表示按键有效。
* @param - arg : 设备结构变量
* @return : 无
*/
void timer_function(unsigned long arg)
{
unsigned char value;
unsigned char num;
struct irq_keydesc *keydesc;
struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;

num = dev->curkeynum;
keydesc = &dev->irqkeydesc[num];

value = gpio_get_value(keydesc->gpio); /* 读取IO值 */
if(value == 0){ /* 按下按键 */
atomic_set(&dev->keyvalue, keydesc->value);
}
else{ /* 按键松开 */
atomic_set(&dev->keyvalue, 0x80 | keydesc->value);
atomic_set(&dev->releasekey, 1); /* 标记松开按键,即完成一次完整的按键过程 */
}
}

platform 设备驱动

作用

如果我在三个平台 A、B 和 C 都使用了i2c 去传递MPU6050 这个 I2C接口的六轴传感器 按之前的逻辑 我们一定会在三个平台都在写好 ioc驱动 然后到mpu 再写一次 这样会造成大量的代码 我们采用总线的这个思路 把 每个平台的 I2C 控制器都提供一个统一的接口
,每个设备通过统一的 I2C接口驱动来访问,这样就可以大大简化驱动文件
就类似于
pCZwcTg.png
变成:
pCZw560.png
他的代码逻辑是这样 :
首先在init 函数里调用 platform_driver_register
然后这个函数里面会哦有一个结构体 platform_driver led_driver 他是里面可以和设备树挂钩 of_device_id led_of_match 寻找设备树 匹配上之后会调用 led_probe 来注册led驱动 其实在这个函数里体现不出来总线的作用 我们一会举一个i2c总线的例子就明白了

i2c驱动

首先捋一下代码流程然后说代码
i2c_add_driver(&ap3216c_driver); 类似于 platform_driver_register 然后带哦用寻找设备树的结构体 最后在prope函数中 static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id) 直接把client 放在ap3216cdev.private_data = client; 这样就完成了 匹配 能够普直接使用 i2c 传输了 省的写太多冗余代码
代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
#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_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/i2c.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include "ap3216creg.h"
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : ap3216c.c
作者 : 左忠凯
版本 : V1.0
描述 : AP3216C驱动程序
其他 : 无
论坛 : www.openedv.com
日志 : 初版V1.0 2019/9/2 左忠凯创建
***************************************************************/
#define AP3216C_CNT 1
#define AP3216C_NAME "ap3216c"

struct ap3216c_dev {
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
struct device_node *nd; /* 设备节点 */
int major; /* 主设备号 */
void *private_data; /* 私有数据 */
unsigned short ir, als, ps; /* 三个光传感器数据 */
};

static struct ap3216c_dev ap3216cdev;

/*
* @description : 从ap3216c读取多个寄存器数据
* @param - dev: ap3216c设备
* @param - reg: 要读取的寄存器首地址
* @param - val: 读取到的数据
* @param - len: 要读取的数据长度
* @return : 操作结果
*/
static int ap3216c_read_regs(struct ap3216c_dev *dev, u8 reg, void *val, int len)
{
int ret;
struct i2c_msg msg[2];
struct i2c_client *client = (struct i2c_client *)dev->private_data;

/* msg[0]为发送要读取的首地址 */
msg[0].addr = client->addr; /* ap3216c地址 */
msg[0].flags = 0; /* 标记为发送数据 */
msg[0].buf = &reg; /* 读取的首地址 */
msg[0].len = 1; /* reg长度*/

/* msg[1]读取数据 */
msg[1].addr = client->addr; /* ap3216c地址 */
msg[1].flags = I2C_M_RD; /* 标记为读取数据*/
msg[1].buf = val; /* 读取数据缓冲区 */
msg[1].len = len; /* 要读取的数据长度*/

ret = i2c_transfer(client->adapter, msg, 2);
if(ret == 2) {
ret = 0;
} else {
printk("i2c rd failed=%d reg=%06x len=%d\n",ret, reg, len);
ret = -EREMOTEIO;
}
return ret;
}

/*
* @description : 向ap3216c多个寄存器写入数据
* @param - dev: ap3216c设备
* @param - reg: 要写入的寄存器首地址
* @param - val: 要写入的数据缓冲区
* @param - len: 要写入的数据长度
* @return : 操作结果
*/
static s32 ap3216c_write_regs(struct ap3216c_dev *dev, u8 reg, u8 *buf, u8 len)
{
u8 b[256];
struct i2c_msg msg;
struct i2c_client *client = (struct i2c_client *)dev->private_data;

b[0] = reg; /* 寄存器首地址 */
memcpy(&b[1],buf,len); /* 将要写入的数据拷贝到数组b里面 */

msg.addr = client->addr; /* ap3216c地址 */
msg.flags = 0; /* 标记为写数据 */

msg.buf = b; /* 要写入的数据缓冲区 */
msg.len = len + 1; /* 要写入的数据长度 */

return i2c_transfer(client->adapter, &msg, 1);
}

/*
* @description : 读取ap3216c指定寄存器值,读取一个寄存器
* @param - dev: ap3216c设备
* @param - reg: 要读取的寄存器
* @return : 读取到的寄存器值
*/
static unsigned char ap3216c_read_reg(struct ap3216c_dev *dev, u8 reg)
{
u8 data = 0;

ap3216c_read_regs(dev, reg, &data, 1);
return data;

#if 0
struct i2c_client *client = (struct i2c_client *)dev->private_data;
return i2c_smbus_read_byte_data(client, reg);
#endif
}

/*
* @description : 向ap3216c指定寄存器写入指定的值,写一个寄存器
* @param - dev: ap3216c设备
* @param - reg: 要写的寄存器
* @param - data: 要写入的值
* @return : 无
*/
static void ap3216c_write_reg(struct ap3216c_dev *dev, u8 reg, u8 data)
{
u8 buf = 0;
buf = data;
ap3216c_write_regs(dev, reg, &buf, 1);
}

/*
* @description : 读取AP3216C的数据,读取原始数据,包括ALS,PS和IR, 注意!
* : 如果同时打开ALS,IR+PS的话两次数据读取的时间间隔要大于112.5ms
* @param - ir : ir数据
* @param - ps : ps数据
* @param - ps : als数据
* @return : 无。
*/
void ap3216c_readdata(struct ap3216c_dev *dev)
{
unsigned char i =0;
unsigned char buf[6];

/* 循环读取所有传感器数据 */
for(i = 0; i < 6; i++)
{
buf[i] = ap3216c_read_reg(dev, AP3216C_IRDATALOW + i);
}

if(buf[0] & 0X80) /* IR_OF位为1,则数据无效 */
dev->ir = 0;
else /* 读取IR传感器的数据 */
dev->ir = ((unsigned short)buf[1] << 2) | (buf[0] & 0X03);

dev->als = ((unsigned short)buf[3] << 8) | buf[2]; /* 读取ALS传感器的数据 */

if(buf[4] & 0x40) /* IR_OF位为1,则数据无效 */
dev->ps = 0;
else /* 读取PS传感器的数据 */
dev->ps = ((unsigned short)(buf[5] & 0X3F) << 4) | (buf[4] & 0X0F);
}

/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int ap3216c_open(struct inode *inode, struct file *filp)
{
filp->private_data = &ap3216cdev;

/* 初始化AP3216C */
ap3216c_write_reg(&ap3216cdev, AP3216C_SYSTEMCONG, 0x04); /* 复位AP3216C */
mdelay(50); /* AP3216C复位最少10ms */
ap3216c_write_reg(&ap3216cdev, AP3216C_SYSTEMCONG, 0X03); /* 开启ALS、PS+IR */
return 0;
}

/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t ap3216c_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
short data[3];
long err = 0;

struct ap3216c_dev *dev = (struct ap3216c_dev *)filp->private_data;

ap3216c_readdata(dev);

data[0] = dev->ir;
data[1] = dev->als;
data[2] = dev->ps;
err = copy_to_user(buf, data, sizeof(data));
return 0;
}

/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int ap3216c_release(struct inode *inode, struct file *filp)
{
return 0;
}

/* AP3216C操作函数 */
static const struct file_operations ap3216c_ops = {
.owner = THIS_MODULE,
.open = ap3216c_open,
.read = ap3216c_read,
.release = ap3216c_release,
};

/*
* @description : i2c驱动的probe函数,当驱动与
* 设备匹配以后此函数就会执行
* @param - client : i2c设备
* @param - id : i2c设备ID
* @return : 0,成功;其他负值,失败
*/
static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
/* 1、构建设备号 */
if (ap3216cdev.major) {
ap3216cdev.devid = MKDEV(ap3216cdev.major, 0);
register_chrdev_region(ap3216cdev.devid, AP3216C_CNT, AP3216C_NAME);
} else {
alloc_chrdev_region(&ap3216cdev.devid, 0, AP3216C_CNT, AP3216C_NAME);
ap3216cdev.major = MAJOR(ap3216cdev.devid);
}

/* 2、注册设备 */
cdev_init(&ap3216cdev.cdev, &ap3216c_ops);
cdev_add(&ap3216cdev.cdev, ap3216cdev.devid, AP3216C_CNT);

/* 3、创建类 */
ap3216cdev.class = class_create(THIS_MODULE, AP3216C_NAME);
if (IS_ERR(ap3216cdev.class)) {
return PTR_ERR(ap3216cdev.class);
}

/* 4、创建设备 */
ap3216cdev.device = device_create(ap3216cdev.class, NULL, ap3216cdev.devid, NULL, AP3216C_NAME);
if (IS_ERR(ap3216cdev.device)) {
return PTR_ERR(ap3216cdev.device);
}

ap3216cdev.private_data = client;

return 0;
}

/*
* @description : i2c驱动的remove函数,移除i2c驱动的时候此函数会执行
* @param - client : i2c设备
* @return : 0,成功;其他负值,失败
*/
static int ap3216c_remove(struct i2c_client *client)
{
/* 删除设备 */
cdev_del(&ap3216cdev.cdev);
unregister_chrdev_region(ap3216cdev.devid, AP3216C_CNT);

/* 注销掉类和设备 */
device_destroy(ap3216cdev.class, ap3216cdev.devid);
class_destroy(ap3216cdev.class);
return 0;
}

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

/* 设备树匹配列表 */
static const struct of_device_id ap3216c_of_match[] = {
{ .compatible = "alientek,ap3216c" },
{ /* Sentinel */ }
};

/* i2c驱动结构体 */
static struct i2c_driver ap3216c_driver = {
.probe = ap3216c_probe,
.remove = ap3216c_remove,
.driver = {
.owner = THIS_MODULE,
.name = "ap3216c",
.of_match_table = ap3216c_of_match,
},
.id_table = ap3216c_id,
};

/*
* @description : 驱动入口函数
* @param : 无
* @return : 无
*/
static int __init ap3216c_init(void)
{
int ret = 0;

ret = i2c_add_driver(&ap3216c_driver);
return ret;
}

/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit ap3216c_exit(void)
{
i2c_del_driver(&ap3216c_driver);
}

/* module_i2c_driver(ap3216c_driver) */

module_init(ap3216c_init);
module_exit(ap3216c_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");




MISC 驱动实验

是什么

杂项驱动 就是你在一些写之前的驱动的时候 你有一部分的代码是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (ap3216cdev.major) {
ap3216cdev.devid = MKDEV(ap3216cdev.major, 0);
register_chrdev_region(ap3216cdev.devid, AP3216C_CNT, AP3216C_NAME);
} else {
alloc_chrdev_region(&ap3216cdev.devid, 0, AP3216C_CNT, AP3216C_NAME);
ap3216cdev.major = MAJOR(ap3216cdev.devid);
}

/* 2、注册设备 */
cdev_init(&ap3216cdev.cdev, &ap3216c_ops);
cdev_add(&ap3216cdev.cdev, ap3216cdev.devid, AP3216C_CNT);

/* 3、创建类 */
ap3216cdev.class = class_create(THIS_MODULE, AP3216C_NAME);
if (IS_ERR(ap3216cdev.class)) {
return PTR_ERR(ap3216cdev.class);
}

/* 4、创建设备 */
ap3216cdev.device = device_create(ap3216cdev.class, NULL, ap3216cdev.devid, NULL, AP3216C_NAME);
if (IS_ERR(ap3216cdev.device)) {
return PTR_ERR(ap3216cdev.device);
}

这个过程就像是裹脚布 又臭又长 哦我们可以把一些有各自性质的进行封装 然后为他们规定好主设备号 然后只需要调用对应的 api 就可以完成注册
比如说 beep 就可以用之前的杂项 只需要在对应的prope函数里 ret = misc_register(&beep_miscdev); 就可以完成驱动的注册

input子系统

input 就是输入的意思,因此 input 子系统就是管理输入的子系统,和 pinctrl、gpio 子系统一样,都是 Linux 内核针对某一类设备而创建的框架。比如按键输入、键盘、鼠标、触摸屏等等这些都属于输入设备,不同的输入设备所代表的含义不同,按键和键盘就是代表按键信息,鼠标和触摸屏代表坐标信息 就是把之前的 misc 杂项 具体为某个输入的设备 唯一有不一样的就是 你要告诉你的系统 你注册的是什么驱动

代码流程

首先要在你的结构体上定义input struct input_dev *inputdev; 然后按键被按下input_report_key(dev->inputdev, keydesc->value, 1);/*1,按下*/

1
2
3
4
5
6
7
8
9
keyinputdev.inputdev = input_allocate_device();
keyinputdev.inputdev->name = KEYINPUT_NAME;
keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) |
BIT_MASK(EV_REP);
input_set_capability(keyinputdev.inputdev, EV_KEY, KEY_0);

/* 注册输入设备 */
ret = input_register_device(keyinputdev.inputdev);

申请定义和注册

音频驱动

音频是我们最常用到的功能,音频也是 linux 和安卓的重点应用场合。I.MX6ULL 带有 SAI接口,正点原子的 I.MX6ULL ALPHA 开发板通过此接口外接了一个 WM8960 音频 DAC 芯片,本章我们就来学习一下如何使能 WM8960 驱动,并且通过 WM8960 芯片来完成音乐播放与录音
首先要把模拟信号转化成数字信号 所以需要 adc和dac 音频一般利用的协议时 i2s
ADCDAT:ADC 数据输出引脚,采集到的音频数据转换为数字信号以后通过此引脚传输给
主控制器。
ADCLRC:ADC 数据对齐时钟,也就是帧时钟(LRCK),用于切换左右声道数据,此信号
的频率就是采样率。此引脚可以配置为 GPIO 功能,配置为 GPIO 以后 ADC 就会使用 DACLRC
引脚作为帧时钟。
DACDAT:DAC 数据输入引脚,主控器通过此引脚将数字信号输入给 WM8960 的 DAC。
DACLRC:DAC 数据对齐时钟,功能和 ADCLRC 一样,都是帧时钟(LRCK),用于切换左
右声道数据,此信号的频率等于采样率。
BCLK:位时钟,用于同步。
MCLK:主时钟,WM8960 工作的时候还需要一路主时钟,此时钟由 I.MX6ULL 提供,
MCLK 频率等于采样率的 256 或 384 倍,因此大家在 WM8960 的数据手册里面常看到
MCLK=256fs 或 MCLK=384fs。
我们的audio电路图如图:
pCeqq2j.png
主要关注两点

  1. SAI 接口一共用到了 6 根数据线,这 6 根数据线用于 I.MX6ULL 与 WM8960 之间的音
    频数据收发。
  2. WM8960 在使用的时候需要进行配置,配置接口为 I2C,

修改设备树

wm8960 i2c 接口设备

1
2
3
4
5
6
7
codec: wm8960@1a {
compatible = "wlf,wm8960";
reg = <0x1a>;
clocks = <&clks IMX6UL_CLK_SAI2>;
clock-names = "mclk";
wlf,shared-lrclk;
};

把这个放到对应的 I2C2 节点下,

SAI 音频接口设备树

1
2
3
4
5
6
7
8
9
10
&sai2 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_sai2
&pinctrl_sai2_hp_det_b>;
assigned-clocks = <&clks IMX6UL_CLK_SAI2_SEL>,
<&clks IMX6UL_CLK_SAI2>;
assigned-clock-parents = <&clks IMX6UL_CLK_PLL4_AUDIO_DIV>;
assigned-clock-rates = <0>, <12288000>;
status = "okay";
};

然后一直对应音频需要的配置就可以了 这里就不罗嗦了

CAN

CAN 的全称为 Controller Area Network,也就是控制局域网络,
CAN 的特点主要有一下几点:

  1. 多主控制
  2. 系统的柔软性与总线相连的单元没有类似于“地址”的信息。因此在总线上增加单元时,连接在总线上
  3. 通信速度快,距离远
  4. 具有错误检测、错误通知和错误恢复功能
  5. 连接节点多

CAN 电气属性

CAN 总线使用两根线来连接各个单元:CAN_H 和 CAN_L,CAN 控制器通过判断这两根
线上的电位差来得到总线电平,CAN 总线电平分为显性电平和隐性电平两种。显性电平表示逻
辑“0”,此时 CAN_H 电平比 CAN_L 高,分别为 3.5V 和 1.5V,电位差为 2V。隐形电平表示
逻辑“1”,此时 CAN_H 和 CAN_L 电压都为 2.5V 左右,电位差为 0V。CAN 总线就通过显性
和隐形电平的变化来将具体的数据发送出去

CAN 协议

我就说一下他的每个帧都是用来干什么的
pCexrVI.png
起始位: 0
识别码:就是每个设备都有自己的编号
控制码 6位 剩下的都是纠错用的码

修改设备树

首先要配置pinctrl 吧引脚配置成can

1
2
3
4
5
6
pinctrl_flexcan1: flexcan1grp{
fsl,pins = <
MX6UL_PAD_UART3_RTS_B__FLEXCAN1_RX 0x1b020
MX6UL_PAD_UART3_CTS_B__FLEXCAN1_TX 0x1b020
>;
};

示例代码:

1
2
3
4
5
6
 &flexcan1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_flexcan1>;
xceiver-supply = <&reg_can_3v3>;//性指定 CAN 收发器的电压为 3.3V。
status = "okay";
};

需要注意的是 can总线在linux中以网卡的形式存在

块设备驱动实验

块设备是针对存储设备的,比如 SD 卡、EMMC、NAND Flash、Nor Flash、SPI Flash、机械硬盘、固态硬盘等linux 内 核 使 用 block_device 表 示 块 设 备 , block_device 为 一 个 结 构 体 , 定 义 在include/linux/fs.h 文件中,在这里有一个重要的结构体struct gendisk *bd_disk;block_device 来表示一个具体的块设备对象,比如一个硬盘或者分区,如果是硬盘的话 bd_disk 就指向通用磁盘结构 gendisk。
驱动书写流程:
首先 写结构体 块设备的结构题

1
2
3
4
5
6
7
8
9
struct ramdisk_dev{
int major; /* 主设备号 */
unsigned char *ramdiskbuf; /* ramdisk内存空间,用于模拟块设备 */
spinlock_t lock; /* 自旋锁 */
struct gendisk *gendisk; /* gendisk */
struct request_queue *queue;/* 请求队列 */

};

那么在init函数里 我们要做的就是初始化这几个结构体里的数据 还有把去操作及放在ramdisk.gendisk 这个结构体里

init
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
static int __init ramdisk_init(void)
{
int ret = 0;
printk("ramdisk init\r\n");

/* 1、申请用于ramdisk内存 */
ramdisk.ramdiskbuf = kzalloc(RAMDISK_SIZE, GFP_KERNEL);
if(ramdisk.ramdiskbuf == NULL) {
ret = -EINVAL;
goto ram_fail;
}

/* 2、初始化自旋锁 */
spin_lock_init(&ramdisk.lock);

/* 3、注册块设备 */
ramdisk.major = register_blkdev(0, RAMDISK_NAME); /* 由系统自动分配主设备号 */
if(ramdisk.major < 0) {
goto register_blkdev_fail;
}
printk("ramdisk major = %d\r\n", ramdisk.major);

/* 4、分配并初始化gendisk */
ramdisk.gendisk = alloc_disk(RADMISK_MINOR);
if(!ramdisk.gendisk) {
ret = -EINVAL;
goto gendisk_alloc_fail;
}

/* 5、分配并初始化请求队列 */
ramdisk.queue = blk_init_queue(ramdisk_request_fn, &ramdisk.lock);
if(!ramdisk.queue) {
ret = EINVAL;
goto blk_init_fail;
}

/* 6、添加(注册)disk */
ramdisk.gendisk->major = ramdisk.major; /* 主设备号 */
ramdisk.gendisk->first_minor = 0; /* 第一个次设备号(起始次设备号) */
ramdisk.gendisk->fops = &ramdisk_fops; /* 操作函数 */
ramdisk.gendisk->private_data = &ramdisk; /* 私有数据 */
ramdisk.gendisk->queue = ramdisk.queue; /* 请求队列 */
sprintf(ramdisk.gendisk->disk_name, RAMDISK_NAME); /* 名字 */
set_capacity(ramdisk.gendisk, RAMDISK_SIZE/512); /* 设备容量(单位为扇区) */
add_disk(ramdisk.gendisk);

return 0;

blk_init_fail:
put_disk(ramdisk.gendisk);
//del_gendisk(ramdisk.gendisk);
gendisk_alloc_fail:
unregister_blkdev(ramdisk.major, RAMDISK_NAME);
register_blkdev_fail:
kfree(ramdisk.ramdiskbuf); /* 释放内存 */
ram_fail:
return ret;
}

初始化好之后 写入操作集
示例代码:

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

int ramdisk_open(struct block_device *dev, fmode_t mode)
{
printk("ramdisk open\r\n");
return 0;
}

/*
* @description : 释放块设备
* @param - disk : gendisk
* @param - mode : 模式
* @return : 0 成功;其他 失败
*/
void ramdisk_release(struct gendisk *disk, fmode_t mode)
{
printk("ramdisk release\r\n");
}

/*
* @description : 获取磁盘信息
* @param - dev : 块设备
* @param - geo : 模式
* @return : 0 成功;其他 失败
*/
int ramdisk_getgeo(struct block_device *dev, struct hd_geometry *geo)
{
/* 这是相对于机械硬盘的概念 */
geo->heads = 2; /* 磁头 */
geo->cylinders = 32; /* 柱面 */
geo->sectors = RAMDISK_SIZE / (2 * 32 *512); /* 一个磁道上的扇区数量 */
return 0;
}

还有比较重要的函数 是传输和处理队列的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
 */
static void ramdisk_transfer(struct request *req)
{
unsigned long start = blk_rq_pos(req) << 9; /* blk_rq_pos获取到的是扇区地址,左移9位转换为字节地址 */
unsigned long len = blk_rq_cur_bytes(req); /* 大小 */

/* bio中的数据缓冲区
* 读:从磁盘读取到的数据存放到buffer中
* 写:buffer保存这要写入磁盘的数据
*/
void *buffer = bio_data(req->bio);

if(rq_data_dir(req) == READ) /* 读数据 */
memcpy(buffer, ramdisk.ramdiskbuf + start, len);
else if(rq_data_dir(req) == WRITE) /* 写数据 */
memcpy(ramdisk.ramdiskbuf + start, buffer, len);

}

/*
* @description : 请求处理函数
* @param-q : 请求队列
* @return : 无
*/
void ramdisk_request_fn(struct request_queue *q)
{
int err = 0;
struct request *req;

/* 循环处理请求队列中的每个请求 */
req = blk_fetch_request(q);
while(req != NULL) {

/* 针对请求做具体的传输处理 */
ramdisk_transfer(req);

/* 判断是否为最后一个请求,如果不是的话就获取下一个请求
* 循环处理完请求队列中的所有请求。
*/
if (!__blk_end_request_cur(req, err))
req = blk_fetch_request(q);
}
}

Linux 网络驱动实验

1