ZYNQ学习笔记-字符设备驱动开发

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

描述驱动操作的数据结构

include/linux/fs.h 中定义了一个结构体,集合了内核驱动操作函数集合。

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
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long,
unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
u64);
ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *,
u64);
} __randomize_layout;

file_operation中常用的成员:

ower拥有该结构体模块的指针,一般设置为THIS_MODULE。

llseek用于修改文件当前的读写位置。

read对应c库中的读取函数。

write对应c库中的写入函数。

poll轮训函数,由于查询设备是否可以进行非阻塞的读写。

unlocked_ioctl提供设备控制功能,对应应用中的ioctl函数。

compat_ioctl与上面函数功能一样,区别在于在 64 位系统上, 32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl。

mmap用于将设备内存映射到用户空间中,用于直接操作缓存(比如LCD设备的显存)。

open对应c库中的open函数。

release对应c库中的close函数。

fsync由于处理待刷新的数据,用于将缓存数据刷新到磁盘。

fasync与上面功能相似,但是是异步操作。

编写驱动时,根据需要实现部分函数即可。

字符设备开发步骤

驱动加载和卸载

Linux驱动有两种方式运行,第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),在 Linux 内核启动以后使用“insmod命令加载驱动模块,使用rmmod删除模块。

当执行加载和卸载命令时会分别执行两个函数,在驱动代码中使用以下方式注册者两个函数。

1
2
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 /* 驱动入口函数 */
static int __init xxx_init(void)
{
/* 入口函数具体内容 */
return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
/* 出口函数具体内容 */
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);

其中__init__exit修饰实际上是c语言的attribute关键字,用来将函数放在特点区域,使用完就从内存卸载。

驱动编译完成后是.ko后缀的文件,这时可以使用两个命令来加载驱动。首先是insmod

1
insmod drv.ko

这种只会单纯的加载驱动,不会去管依赖关系。如果drv.ko依赖first.ko,那么必须先加载first。如果想要解决依赖问题,需要使用modprobe命令,modprobe 命令主要智能在提供了模块的依赖性分析、错误检查、错误报告等功能,推荐使用 modprobe 命令来加载驱动。 modprobe 命令默认会去/lib/modules/目录中查找模块 ,一般自己制作的根文件系统中是不会有这个目录的,所以需要自己手动创建。

1
2
rmmod drv.ko # 卸载驱动
modprobe -r drv.ko # 卸载驱动

modprobe 命令可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没有被其他模块所使用,否则就不能使用 modprobe 来卸载驱动模块。所以对于模块的卸载,还是推荐使用 rmmod 命令。

字符设备的注册与注销

在上面提到的加载和卸载函数中一般会运行设备的注册和注销函数。函数原型如下:

1
2
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)

register_chrdev 函数用于注册字符设备 :

major: 主设备号, Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分,关于设备号后面会详细讲解。
name:设备名字,指向一串字符串。
fops: 结构体 file_operations 类型指针,指向设备的操作函数集合变量。

unregister_chrdev 函数用于注销字符设备,此函数有两个参数,这两个参数含义如下:
major: 要注销的设备对应的主设备号。
name: 要注销的设备对应的设备名。

设备的具体操作

file_operations结构体中包含了设备的具体操作,每个驱动都需要定义一个,并在register_chrdev函数中进行注册。

一个基本的设备应该具备以下操作:

  1. 能够进行打开和关闭操作,这对应着结构体中的open和release函数。
  2. 能够进行读写操作,这对应着结构体中的read和write函数。

具体步骤是实现这些函数并赋值到file_operations结构体中。

设备的LICENSE和作者信息

LICENSE是必须添加的,作者信息可以不添加。使用以下两个函数添加:

1
2
MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息

例子:

1
2
MODULE_LICENSE("GPL");
MODULE_AUTHOR("LO_StacNet");

Linux设备号

Linux每个驱动都有一个设备号,由主设备号和次设备号注组成(合并成一个dev_t数据类型,高位为主设备号,低位为次设备号)。dev_t定义在include/linux/types.h :

1
2
3
 typedef __u32 __kernel_dev_t;
...
typedef __kernel_dev_t dev_t;

__u32定义在include/uapi/asm-generic/int-ll64.h

1
typedef unsigned int __u32;

dev_t是一个32位数,高12位为主设备号,低20位为次设备号。

include/linux/kdev_t.h 中定义了关于设备号的操作函数:

1
2
3
4
5
#define MINORBITS 20 //次设备号位数
#define MINORMASK ((1U << MINORBITS) - 1) //次设备号掩码
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) //从dev_t中获取主设备号
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) //从dev_t中获取次设备号
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) //使用主设备号和次设备号组合成设备号

设备号可以有两种分配方式:

  1. 静态分配。由开发者自己设一个静态值,注意与已经使用的设备号区分。使用cat /proc/devices 查看已经使用的设备号。
  2. 动态分配。申请一个驱动号,卸载时释放。

动态分配使用以下函数:

1
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

函数 alloc_chrdev_region 用于申请设备号,此函数有 4 个参数:
dev:保存申请到的设备号。

baseminor: 次设备号起始地址, alloc_chrdev_region 可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址并逐次递增。一般 baseminor 为 0,也就是说次设备号从 0 开始。
count: 要申请的设备号数量。
name:设备名字。

1
void unregister_chrdev_region(dev_t from, unsigned count)

销毁字符设备时要释放设备号。

from:要释放的设备号。
count: 表示从 from 开始, 要释放的设备号数量。

应用操作函数

通过c库的文件操作函数可以操作字符驱动,一般使用open、read、write、close函数。

1
int open(const char *pathname, int flags)

pathname:要打开的设备或者文件名。
flags: 文件打开模式,以下三种模式必选其一:

O_RDONLY-只读;O_WRONLY-只写;O_RDWR读写;还有其他模式,可以百度一下。

返回值:如果文件打开成功的话返回文件的文件描述符。

1
ssize_t read(int fd, void *buf, size_t count)

fd:要读取的文件描述符,读取文件之前要先用 open 函数打开文件, open 函数打开文件成功以后会得到文件描述符。
buf: 数据读取到此 buf 中。
count: 要读取的数据长度,也就是字节数。
返回值: 读取成功的话返回读取到的字节数;如果返回 0 表示读取到了文件末尾;如果返回负值,表示读取失败。

1
ssize_t write(int fd, const void *buf, size_t count);

fd:要进行写操作的文件描述符,写文件之前要先用 open 函数打开文件, open 函数打开文件成功以后会得到文件描述符。
buf: 要写入的数据。
count: 要写入的数据长度,也就是字节数。
返回值: 写入成功的话返回写入的字节数;如果返回 0 表示没有写入任何数据;

1
int close(int fd);

fd:要关闭的文件描述符。

返回值: 0 表示关闭成功,负值表示关闭失败。在 Ubuntu 中输入“ man 2 close”命令即可查看 close 函数的详细内容。

LED驱动的开发

现在终于进行到了点灯环节。首先我们需要了解几个概念。

基本的驱动原理

LED灯的基本驱动原理大家应该都很熟悉了,就是操作外设的寄存器。因此我们需要拿到寄存器地址。但是在Linux中不能直接操作地址,需要一些转换。

地址映射与虚拟地址

Linux中有一个MMU(Memory Manage Unit 内存管理单元),老版本linux要求处理器必须要有MMU单元,但是新版本Linux已经支持无MMU的处理器了。但是实际上仍然基于MMU运行。MMU主要完成以下功能:

  1. 将虚拟地址映射到物理地址。
  2. 内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。

其中对我们使用影响最大的就是第一点,这也是所谓虚拟内存的来源。它可以把DDR(我使用的是512M)映射到32位处理器4G的内存地址中,至于多出来的空间怎么用的,比较复杂,这里就不说了(我也不知道)。

Linux内核启动时会初始化MMU,设置好内存映射,之后访问的都是虚拟地址。如果没有开启MMU,可以直接对物理寄存器地址进行读写,但如果开启了MMU,就必须获取对应虚拟地址才能进行操作。这里就涉及到两个函数了:

  • ioremap函数
1
2
3
4
5
6
#define ioremap(cookie,size) __arm_ioremap((cookie), (size), MT_DEVICE)

void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype)
{
return arch_ioremap_caller(phys_addr, size, mtype, __builtin_return_address(0));
}

ioremap 是个宏,有两个参数: cookie 和 size ,真正起作用的函数有三个参数。

phys_addr:要映射给的物理起始地址。
size:要映射的内存空间大小。
mtype: ioremap 的类型,可以选择 MT_DEVICEMT_DEVICE_NONSHAREDMT_DEVICE_CACHEDMT_DEVICE_WC, ioremap 函数选择 MT_DEVICE。
返回值: __iomem 类型的指针,指向映射后的虚拟空间首地址。

假如要获取ZYNQ 的 APER_CLK_CTRL 寄存器对应的虚拟地址 :

1
2
3
#define APER_CLK_CTRL 0xF800012C
static void __iomem *aper_clk_ctrl_addr;
aper_clk_ctrl_addr = ioremap(APER_CLK_CTRL, 4);

宏定义 APER_CLK_CTRL 是寄存器物理地址, aper_clk_ctrl_addr 是该物理地址映射后的虚拟地址。对于 ZYNQ 来说一个寄存器是 4 字节(32 位)的,因此映射的内存长度为 4。映射完成以后直接对 aper_clk_ctrl_addr 进行读写操作即可。

  • iounmap函数
1
void iounmap (volatile void __iomem *addr)

卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射 。

要取消掉 APER_CLK_CTRL 寄存器的地址映射 :

1
iounmap(aper_clk_ctrl_addr);

IO内存访问函数

当外部寄存器或内存映射到内存空间时,称为 I/O 内存 。使用 ioremap 函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux 内核不建议这么做,而是推荐操作函数来进行读写操作。

  • 读操作函数
1
2
3
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)

readb、 readw 和 readl 这三个函数分别对应 8bit、 16bit 和 32bit 读操作,参数 addr 就是要读取写内存地址,返回值就是读取到的数据。

  • 写操作函数
1
2
3
void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)

writeb、 writew 和 writel 这三个函数分别对应 8bit、 16bit 和 32bit 写操作,参数 value是要写入的数值, addr 是要写入的地址。

GPIO的寄存器

终于到属性的寄存器操作环节了,这里主要来源是官方的数据手册,就不细说了。

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
1 /***************************************************************
2 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
3 文件名 : led.c
4 作者 : 邓涛
5 版本 : V1.0
6 描述 : ZYNQ LED 驱动文件。
7 其他 : 无
8 论坛 : www.openedv.com
9 日志 : 初版 V1.0 2019/1/30 邓涛创建
10 ***************************************************************/
11
12 #include <linux/types.h>
13 #include <linux/kernel.h>
14 #include <linux/delay.h>
15 #include <linux/ide.h>
16 #include <linux/init.h>
17 #include <linux/module.h>
18 #include <linux/errno.h>
19 #include <linux/gpio.h>
20 #include <asm/mach/map.h>
21 #include <asm/uaccess.h>
22 #include <asm/io.h>
23
24 #define LED_MAJOR 200 /* 主设备号 */
25 #define LED_NAME "led" /* 设备名字 */
26
27 /*
28 * GPIO 相关寄存器地址定义
29 */
30 #define ZYNQ_GPIO_REG_BASE 0xE000A000
31 #define DATA_OFFSET 0x00000040
32 #define DIRM_OFFSET 0x00000204
33 #define OUTEN_OFFSET 0x00000208
34 #define INTDIS_OFFSET 0x00000214
35 #define APER_CLK_CTRL 0xF800012C
36
37 /* 映射后的寄存器虚拟地址指针 */
38 static void __iomem *data_addr;
39 static void __iomem *dirm_addr;
40 static void __iomem *outen_addr;
41 static void __iomem *intdis_addr;
42 static void __iomem *aper_clk_ctrl_addr;
43
44
45 /*
46 * @description : 打开设备
47 * @param – inode : 传递给驱动的 inode
48 * @param – filp : 设备文件, file 结构体有个叫做 private_data 的成员变量
49 * 一般在 open 的时候将 private_data 指向设备结构体。
50 * @return : 0 成功;其他 失败
51 */
52 static int led_open(struct inode *inode, struct file *filp)
53 {
54 return 0;
55 }
56
57 /*
58 * @description : 从设备读取数据
59 * @param – filp : 要打开的设备文件(文件描述符)
60 * @param – buf : 返回给用户空间的数据缓冲区
61 * @param – cnt : 要读取的数据长度
62 * @param – offt : 相对于文件首地址的偏移
63 * @return : 读取的字节数,如果为负值,表示读取失败
64 */
65 static ssize_t led_read(struct file *filp, char __user *buf,
66 size_t cnt, loff_t *offt)
67 {
68 return 0;
69 }
70
71 /*
72 * @description : 向设备写数据
73 * @param – filp : 设备文件,表示打开的文件描述符
74 * @param – buf : 要写给设备写入的数据
75 * @param – cnt : 要写入的数据长度
76 * @param – offt : 相对于文件首地址的偏移
77 * @return : 写入的字节数,如果为负值,表示写入失败
78 */
79 static ssize_t led_write(struct file *filp, const char __user *buf,
80 size_t cnt, loff_t *offt)
81 {
82 int ret;
83 int val;
84 char kern_buf[1];
85
86 ret = copy_from_user(kern_buf, buf, cnt);// 得到应用层传递过来的数据
87 if(0 > ret) {
88 printk(KERN_ERR "kernel write failed!\r\n");
89 return -EFAULT;
90 }
91
92 val = readl(data_addr);
93 if (0 == kern_buf[0])
94 val &= ~(0x1U << 7); // 如果传递过来的数据是 0 则关闭 led
95 else if (1 == kern_buf[0])
96 val |= (0x1U << 7); // 如果传递过来的数据是 1 则点亮 led
97
98 writel(val, data_addr);
99 return 0;
100 }
101
102 /*
103 * @description : 关闭/释放设备
104 * @param – filp : 要关闭的设备文件(文件描述符)
105 * @return : 0 成功;其他 失败
106 */
107 static int led_release(struct inode *inode, struct file *filp)
108 {
109 return 0;
110 }
111
112 /* 设备操作函数 */
113 static struct file_operations led_fops = {
114 .owner = THIS_MODULE,
115 .open = led_open,
116 .read = led_read,
117 .write = led_write,
118 .release = led_release,
119 };
120
121 static int __init led_init(void)
122 {
123 u32 val;
124 int ret;
125
126 /* 1.寄存器地址映射 */
127 data_addr = ioremap(ZYNQ_GPIO_REG_BASE + DATA_OFFSET, 4);
128 dirm_addr = ioremap(ZYNQ_GPIO_REG_BASE + DIRM_OFFSET, 4);
129 outen_addr = ioremap(ZYNQ_GPIO_REG_BASE + OUTEN_OFFSET, 4);
130 intdis_addr = ioremap(ZYNQ_GPIO_REG_BASE + INTDIS_OFFSET, 4);
131 aper_clk_ctrl_addr = ioremap(APER_CLK_CTRL, 4);
132
133 /* 2.使能 GPIO 时钟 */
134 val = readl(aper_clk_ctrl_addr);
135 val |= (0x1U << 22);
136 writel(val, aper_clk_ctrl_addr);
137
138 /* 3.关闭中断功能 */
139 val |= (0x1U << 7);
140 writel(val, intdis_addr);
141
142 /* 4.设置 GPIO 为输出功能 */
143 val = readl(dirm_addr);
144 val |= (0x1U << 7);
145 writel(val, dirm_addr);
146
147 /* 5.使能 GPIO 输出功能 */
148 val = readl(outen_addr);
149 val |= (0x1U << 7);
150 writel(val, outen_addr);
151
152 /* 6.默认关闭 LED */
153 val = readl(data_addr);
154 val &= ~(0x1U << 7);
155 writel(val, data_addr);
156
157 /* 7.注册字符设备驱动 */
158 ret = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
159 if(0 > ret){
160 printk(KERN_ERR "Register LED driver failed!\r\n");
161 return ret;
162 }
163
164 return 0;
165 }
166
167 static void __exit led_exit(void)
168 {
169 /* 1.卸载设备 */
170 unregister_chrdev(LED_MAJOR, LED_NAME);
171
172 /* 2.取消内存映射 */
173 iounmap(data_addr);
174 iounmap(dirm_addr);
175 iounmap(outen_addr);
176 iounmap(intdis_addr);
177 iounmap(aper_clk_ctrl_addr);
178 }
179
180 /* 驱动模块入口和出口函数注册 */
181 module_init(led_init);
182 module_exit(led_exit);
183
184 MODULE_AUTHOR("DengTao <773904075@qq.com>");
185 MODULE_DESCRIPTION("Alientek ZYNQ GPIO LED Driver");
186 MODULE_LICENSE("GPL");

第 2425 行, 定义了两个宏,设备名字和设备的主设备号。
第 30
35 行,本实验要用到的寄存器宏定义。
第 3842 行,经过内存映射以后的寄存器地址指针。
第 52
55 行, led_open 函数,为空函数,可以自行在此函数中添加相关内容,一般在此函数中将设备结构体作为参数 filp 的私有数据(filp->private_data)。
第 6569 行, led_read 函数,为空函数,如果想在应用程序中读取 LED 的状态,那么就可以在此函数中添加相应的代码,比如读取 MIO 的 DATA 寄存器的值,然后返回给应用程序。
第 79
100 行, led_write 函数,实现对 LED 灯的开关操作,当应用程序调用 write 函数
向 led 设备写数据的时候此函数就会执行。首先通过函数 copy_from_user 获取应用程序发送过来的操作信息(打开还是关闭 LED),最后根据应用程序的操作信息来控制寄存器打开或关闭LED 灯。
第 107110 行, led_release 函数,为空函数,可以自行在此函数中添加相关内容,一般关闭设备的时候会释放掉 led_open 函数中添加的私有数据。
第 113
119 行,设备文件操作结构体 led_fops 的定义和初始化。
第 121165 行,驱动入口函数 led_init,此函数实现了 LED 的初始化工作, 127131 行通过 ioremap 函数获取物理寄存器地址映射后的虚拟地址,得到寄存器对应的虚拟地址以后就可以完成相关初始化工作了。比如使能 GPIO 时钟、 关闭 MIO7 的中断功能、配置并使能 MIO7 的输出功能等。最后,最重要的一步!使用 register_chrdev 函数注册 led 这个字符设备。
第 167178 行,驱动出口函数 led_exit,首先使用函数 unregister_chrdev 注销 led 这
个字符设备,然后调用 iounmap 函数取消内存映射,因为设备已经被卸载,也就意味用不到了,必须要取消映射;需要注意的是这两顺序不要反了,不能在设备没有卸载的情况下,你就把人家的内存映射给取消了,这是不合理的!
第 181
182 行,使用 module_init 和 module_exit 这两个函数指定 led 设备驱动加载和卸载函数。
第 184~186 行,添加模块 LICENSE、 作者信息以及模块描述信息。

对应的应用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
48
49
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

/*
* @description : main 主程序
* @param - argc : argv 数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
int fd, ret;
unsigned char buf[1];

if(3 != argc) {
printf("Usage:\n"
"\t./ledApp /dev/led 1 @ close LED\n"
"\t./ledApp /dev/led 0 @ open LED\n"
);
return -1;
}

/* 打开设备 */
fd = open(argv[1], O_RDWR);
if(0 > fd) {
printf("file %s open failed!\r\n", argv[1]);
return -1;
}

/* 将字符串转换为 int 型数据 */
buf[0] = atoi(argv[2]);

/* 向驱动写入数据 */
ret = write(fd, buf, sizeof(buf));
if(0 > ret){
printf("LED Control Failed!\r\n");
close(fd);
return -1;
}

/* 关闭设备 */
close(fd);
return 0;
}

编译和运行

首先是编译驱动代码。驱动文件依赖了内核的的大量源码,不能直接通过交叉工具链编译。因此首先需要下载linux源码,由于petalinux本身没有包含内核文件,因此需要我们手动下载linux内核。使用以下命令下载:

1
git clone --depth 1 https://github.com/Xilinx/linux-xlnx.git

或者,在Petalinux编译后,在build/tmp/work-shared/plnx_arm/kernel-source/中可以找到代码,将其复制到一个易于寻找的目录下。输入:

1
2
make ARCH=arm xilinx_zynq_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j12

等待编译完成。

如果整编内核,可以放在内核目录下的drivers/char下,并在该目录的Makefile中添加obj-m += demo_driver.o。这样在编译内核时可以编译该驱动。

如果只是单独编译,使用make modules

make modules 指令为编译内核模块指令,该指令的功能是编译内核中所有配置为模块的程序得到模块ko文件,make modules 命令只能在内核源码顶层目录下执行。如果想单独编译一个模块,使用M=参数。M=DIR,程序会自动跳转到所指定的DIR目录中查找模块源码,编译生成ko文件。

1
make  M=DIR modules

基于此,我们可以编写一个makefile:

1
2
3
4
5
6
7
8
KDIR := /home/lo/kernel-source #内核源码目录

obj-m := led.o

all:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C $(KDIR) M=`pwd` modules
clean:
make -C $(KDIR) M=`pwd` clean

之后使用make命令即可。

获得.ko文件后复制到开发板,如果有网络,可以使用以下方式进行复制:

1
scp led.ko root@192.168.1.133:/lib/modules/4.14.0-xilinx

输入命令加载驱动:

1
insmod led.ko

之后可以在/proc/devices中找到文件。

或者:

1
2
depmod # 生成.dep文件
modprobe led.ko

然后需要在/dev目录下创建节点:

1
mknod /dev/led c 200 0

mknod是创建节点命令,/dev/led是要创建的节点文件,c表示
这是个字符设备,200是设备的主设备号,0是设备的次设备号。创建完成以后就会存
在/dev/led 这个文件

编译APP。

1
arm-linux-gnueabihf-gcc ledApp.c -o ledApp

复制到开发板运行:

1
2
./ledApp /dev/led 1 //点亮 LED 灯
./ledApp /dev/led 0 //关闭 LED 灯

当然如果使用petalinux工具,可以更简单的添加驱动,但那样编译时间长,不适合调试。

新字符设备

以上字符设备的注册方法使用的是老版本的API(register_chrdev,unregister_chrdev),现在都推荐使用新的API函数(cdev系列函数),并有一套比较标准的创建方法。

新的字符设备注册方法

在 Linux 中使用 cdev 结构体表示一个字符设备, cdev 结构体在include/linux/cdev.h文件中的定义如下:

1
2
3
4
5
6
7
8
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};

这你需要关注两个成员,ops这就是要实现的file_operations,dev设备号。与老API不同,这里传递参数使用一个结构体传输。

1
struct cdev test_cdev;
  • cdev_init函数
1
void cdev_init(struct cdev *cdev, const struct file_operations *fops)

初始化结构体,用来设定一些成员的初始值。

1
2
3
4
5
6
7
8
struct cdev testcdev;
/* 设备操作函数 */
static struct file_operations test_fops = {
.owner = THIS_MODULE,
/* 其他具体的初始项 */
};
testcdev.owner = THIS_MODULE;
cdev_init(&testcdev, &test_fops); /* 初始化 cdev 结构体变量 */
  • cdev_add函数

该函数就是实际的注册函数。

1
int cdev_add(struct cdev *p, dev_t dev, unsigned count)

参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所使用的设备号,参数 count 是要添加的设备数量。

1
cdev_add(&testcdev, devid, 1);
  • cdev_del函数

该函数就是实际的注销函数。

1
void cdev_del(struct cdev *p)

示例:

1
cdev_del(&testcdev); /* 删除 cdev */

使用文件的私有数据

之前,我们使用变量来存储驱动的设备号,状态以及其他数据。这样其实不专业,对于一个设备的所有属性信息我们最好将其做成一个结构体,在open中传到私有数据中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct test_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};

struct test_dev testdev;

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

之后在 write、 read、 close 等函数中直接读取private_data 即可得到设备结构体 。

自动创建设备节点

我们往常加载驱动后还需要手动mknod创建设备节点,在驱动中实现自动创建设备节点的功能以后,使用 modprobe 加载驱动模块成功的话就会自动在/dev 目录下创建对应的设备文件。

自动功能由udev实现,这是一个应用程序,构建文件系统时会构建一个建议版本mdev。它会检测系统中硬件设备状态,自动创建和删除设备文件。这样可以在安装驱动时自动创建节点,也可以管理热拔插。

  • 创建类和删除类

自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添加自动创建设备节点相关代码。在include/linux/device.h 中可以看到:

1
2
3
4
5
6
7
8
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})

struct class *__class_create(struct module *owner, const char *name,
struct lock_class_key *key)

宏展开后:

1
struct class *class_create (struct module *owner, const char *name)

该代码实现了类的创建。参数 owner 一般为 THIS_MODULE,参数 name 是类名字。返回值是个指向结构体 class 的指针,也就是创建的类。

卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy,函数原型如下:

1
void class_destroy(struct class *cls);
  • 设备创建和删除

除了创建类以外,还需要在这个类下创建一个设备。

1
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char*fmt, ...)

device_create 是个可变参数函数,参数 class 就是设备要创建哪个类下面;参数 parent是父设备,一般为 NULL,也就是没有父设备;参数 devt 是设备号;参数 drvdata 是设备可能会使用的一些数据,一般为 NULL;参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx 这个设备文件。返回值就是创建好的设备。

卸载驱动的时候需要删除掉创建的设备:

1
void device_destroy(struct class *class, dev_t devt)

参数 classs 是要删除的设备所处的类,参数 devt 是要删除的设备号 。

使用新API的代码示例

在上面代码的基础上,这里只列出不同部分:

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
...
#define NEWCHRLED_CNT 1 /* 设备号个数 */
#define NEWCHRLED_NAME "newchrled" /* 名字 */
...
/* newchrled 设备结构体 */
struct newchrled_dev {
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
static struct newchrled_dev newchrled; /* led 设备 */

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

...
static int __init led_init(void)
{
u32 val;
int ret;
...
/* 7.注册字符设备驱动 */
/* 创建设备号 */
if (newchrled.major) {
newchrled.devid = MKDEV(newchrled.major, 0);
ret = register_chrdev_region(newchrled.devid, NEWCHRLED_CNT,CHRLED_NAME);
if (ret)
goto out1;
} else {
ret = alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT,NEWCHRLED_NAME);
if (ret)
goto out1;

newchrled.major = MAJOR(newchrled.devid);
newchrled.minor = MINOR(newchrled.devid);
}

printk("newcheled major=%d,minor=%d\r\n",newchrled.major,newchrled.minor);

/* 初始化 cdev */
newchrled.cdev.owner = THIS_MODULE;
cdev_init(&newchrled.cdev, &newchrled_fops);

/* 添加一个 cdev */
ret = cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);
if (ret)
goto out2;

/* 创建类 */
newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
if (IS_ERR(newchrled.class)) {
ret = PTR_ERR(newchrled.class);
goto out3;
}

/* 创建设备 */
newchrled.device = device_create(newchrled.class, NULL,
newchrled.devid, NULL, NEWCHRLED_NAME);
if (IS_ERR(newchrled.device)) {
ret = PTR_ERR(newchrled.device);
goto out4;
}

return 0;

out4:
class_destroy(newchrled.class);
out3:
cdev_del(&newchrled.cdev);
out2:
unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);
out1:
led_iounmap();

return ret;
}
static void __exit led_exit(void)
{
/* 注销设备 */
device_destroy(newchrled.class, newchrled.devid);

/* 注销类 */
class_destroy(newchrled.class);

/* 删除 cdev */
cdev_del(&newchrled.cdev);

/* 注销设备号 */
unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);

/* 取消地址映射 */
led_iounmap();
}

...

驱动中的倒退式处理方法 :出现错误后使用goto跳转来恢复之前的操作。

编译:

1
2
3
4
5
6
7
8
9
KERN_DIR := 内核地址

obj-m := newchrled.o

all:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C $(KERN_DIR) M=`pwd` modules

clean:
make -C $(KERN_DIR) M=`pwd` clean

使用make命令编译。

加载驱动:

1
2
3
Depmod //第一次加载驱动的时候需要运行此命令
modprobe newchrled.ko //加载驱动
rmmod newchrled.ko //卸载驱动

基于设备树的驱动开发

基于设备树的开发,就是将硬件信息写到设备树中,驱动只需要通过函数获取设备树信息,就不需要将寄存器地址之类的写到驱动中。

添加设备树

想设备树根节点中添加以下节点:

1
2
3
4
5
6
7
8
9
10
11
12
led {
compatible = "zynq,led";
status = "okay";
default-state = "on";

reg = <0xE000A040 0x4
0xE000A204 0x4
0xE000A208 0x4
0xE000A214 0x4
0xF800012C 0x4
>;
};

这里主要是添加了5个相关寄存器地址。

添加到Linux系统中,可以进入到/proc/device-tree/目录中查看是否有“ 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

/* 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; /* 设备节点 */
};

static struct dtsled_dev dtsled; /* led 设备 */

static inline void led_ioremap(void)
{
data_addr = of_iomap(dtsled.nd, 0);
dirm_addr = of_iomap(dtsled.nd, 1);
outen_addr = of_iomap(dtsled.nd, 2);
intdis_addr = of_iomap(dtsled.nd, 3);
aper_clk_ctrl_addr = of_iomap(dtsled.nd, 4);
}

static int __init led_init(void)
{
const char *str;
u32 val;
int ret;

/* 1.获取 led 设备节点 */
dtsled.nd = of_find_node_by_path("/led");
if(NULL == dtsled.nd) {
printk(KERN_ERR "led node can not found!\r\n");
return -EINVAL;
}

/* 2.读取 status 属性 */
ret = of_property_read_string(dtsled.nd, "status", &str);
if(!ret) {
if (strcmp(str, "okay"))
return -EINVAL;
}

/* 2、获取 compatible 属性值并进行匹配 */
ret = of_property_read_string(dtsled.nd, "compatible", &str);
if(0 > ret)
return -EINVAL;

if (strcmp(str, "alientek,led"))
return -EINVAL;

printk(KERN_ERR "led device matching successful!\r\n");
/* 4.寄存器地址映射 */
led_ioremap();
/* 5.使能 GPIO 时钟 */
/* 6.关闭中断功能 */
/* 7.设置 GPIO 为输出功能 */
/* 8.使能 GPIO 输出功能 */
/* 9.初始化 LED 的默认状态 */
val = readl(data_addr);

ret = of_property_read_string(dtsled.nd, "default-state", &str);
if(!ret) {
if (!strcmp(str, "on"))
val |= (0x1U << 7);
else
val &= ~(0x1U << 7);
} else
val &= ~(0x1U << 7);

writel(val, data_addr);
/* 10.注册字符设备驱动 */

}

使用 of_iomap 函数替换之前使用 ioremap 函数来实现物理地址到虚拟地址的映射,它能够直接解析给定节点的 reg 属性,并将 reg 属性中存放的物理地址和长度进行映射,使用不同的下标依次对 reg 数组中记录的不同组“物理地址-长度” 地址空间进行映射。

其他函数的介绍

printk函数

在linux内核送使用printk进行输出而不是printf。printk 可以根据日志级别对消息进行分类,一共有 8 个消息级别,这 8 个消息级别定义在文件include/linux/kern_levels.h里面 :

1
2
3
4
5
6
7
8
9
10
#define KERN_SOH "\001" /* ASCII Start Of Header */
#define KERN_SOH_ASCII '\001'
#define KERN_EMERG KERN_SOH "0" /* system is unusable */
#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */
#define KERN_CRIT KERN_SOH "2" /* critical conditions */
#define KERN_ERR KERN_SOH "3" /* error conditions */
#define KERN_WARNING KERN_SOH "4" /* warning conditions */
#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */
#define KERN_INFO KERN_SOH "6" /* informational */
#define KERN_DEBUG KERN_SOH "7" /* debug-level messages */

一共定义了 8 个级别,其中 0 的优先级最高, 7 的优先级最低。如果要设置消息级别,参考如下示例:

1
printk(KERN_EMERG "gsmi: Log Shutdown Reason\n");//消息级别为KERN_EMERG

默认级别可以设置(CONFIG_MESSAGE_LOGLEVEL_DEFAULT ),默认为4。同时还有CONSOLE_LOGLEVEL_DEFAULT 控制哪些消息可以显示在控制台上,默认为 7,意味着只有优先级高于 7 的消息才能显示在控制台上 。

copy_to_user

内核空间的数据和用户空间的数据是分开的,想要向用户传参,需要调用函数:

1
static inline long copy_to_user(void __user *to, const void *from, unsigned long n)

参数 to 表示目的,参数 from 表示源,参数 n 表示要复制的数据长度。如果复制成功,返回值为 0,如果复制失败则返回负数。