ZYNQ学习笔记-字符设备驱动开发
字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的 LED、按键、 IIC、 SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
描述驱动操作的数据结构
在include/linux/fs.h 中定义了一个结构体,集合了内核驱动操作函数集合。
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删除模块。
当执行加载和卸载命令时会分别执行两个函数,在驱动代码中使用以下方式注册者两个函数。
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
/* 驱动入口函数 */
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:
insmod drv.ko
这种只会单纯的加载驱动,不会去管依赖关系。如果drv.ko依赖first.ko,那么必须先加载first。如果想要解决依赖问题,需要使用modprobe命令,modprobe 命令主要智能在提供了模块的依赖性分析、错误检查、错误报告等功能,推荐使用 modprobe 命令来加载驱动。 modprobe 命令默认会去/lib/modules/
rmmod drv.ko # 卸载驱动
modprobe -r drv.ko # 卸载驱动
modprobe 命令可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没有被其他模块所使用,否则就不能使用 modprobe 来卸载驱动模块。所以对于模块的卸载,还是推荐使用 rmmod 命令。
字符设备的注册与注销
在上面提到的加载和卸载函数中一般会运行设备的注册和注销函数。函数原型如下:
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函数中进行注册。
一个基本的设备应该具备以下操作:
- 能够进行打开和关闭操作,这对应着结构体中的open和release函数。
- 能够进行读写操作,这对应着结构体中的read和write函数。
具体步骤是实现这些函数并赋值到file_operations结构体中。
设备的LICENSE和作者信息
LICENSE是必须添加的,作者信息可以不添加。使用以下两个函数添加:
MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息
例子:
MODULE_LICENSE("GPL");
MODULE_AUTHOR("LO_StacNet");
Linux设备号
Linux每个驱动都有一个设备号,由主设备号和次设备号注组成(合并成一个dev_t数据类型,高位为主设备号,低位为次设备号)。dev_t定义在include/linux/types.h :
typedef __u32 __kernel_dev_t;
...
typedef __kernel_dev_t dev_t;
__u32定义在include/uapi/asm-generic/int-ll64.h :
typedef unsigned int __u32;
dev_t是一个32位数,高12位为主设备号,低20位为次设备号。
include/linux/kdev_t.h 中定义了关于设备号的操作函数:
#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)) //使用主设备号和次设备号组合成设备号
设备号可以有两种分配方式:
- 静态分配。由开发者自己设一个静态值,注意与已经使用的设备号区分。使用
cat /proc/devices查看已经使用的设备号。 - 动态分配。申请一个驱动号,卸载时释放。
动态分配使用以下函数:
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:设备名字。
void unregister_chrdev_region(dev_t from, unsigned count)
销毁字符设备时要释放设备号。
from:要释放的设备号。
count: 表示从 from 开始, 要释放的设备号数量。
应用操作函数
通过c库的文件操作函数可以操作字符驱动,一般使用open、read、write、close函数。
int open(const char *pathname, int flags)
pathname:要打开的设备或者文件名。
flags: 文件打开模式,以下三种模式必选其一:
O_RDONLY-只读;O_WRONLY-只写;O_RDWR读写;还有其他模式,可以百度一下。
返回值:如果文件打开成功的话返回文件的文件描述符。
ssize_t read(int fd, void *buf, size_t count)
fd:要读取的文件描述符,读取文件之前要先用 open 函数打开文件, open 函数打开文件成功以后会得到文件描述符。
buf: 数据读取到此 buf 中。
count: 要读取的数据长度,也就是字节数。
返回值: 读取成功的话返回读取到的字节数;如果返回 0 表示读取到了文件末尾;如果返回负值,表示读取失败。
ssize_t write(int fd, const void *buf, size_t count);
fd:要进行写操作的文件描述符,写文件之前要先用 open 函数打开文件, open 函数打开文件成功以后会得到文件描述符。
buf: 要写入的数据。
count: 要写入的数据长度,也就是字节数。
返回值: 写入成功的话返回写入的字节数;如果返回 0 表示没有写入任何数据;
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主要完成以下功能:
- 将虚拟地址映射到物理地址。
- 内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。
其中对我们使用影响最大的就是第一点,这也是所谓虚拟内存的来源。它可以把DDR(我使用的是512M)映射到32位处理器4G的内存地址中,至于多出来的空间怎么用的,比较复杂,这里就不说了(我也不知道)。
Linux内核启动时会初始化MMU,设置好内存映射,之后访问的都是虚拟地址。如果没有开启MMU,可以直接对物理寄存器地址进行读写,但如果开启了MMU,就必须获取对应虚拟地址才能进行操作。这里就涉及到两个函数了:
- ioremap函数
#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_DEVICE、 MT_DEVICE_NONSHARED、 MT_DEVICE_CACHED和 MT_DEVICE_WC, ioremap 函数选择 MT_DEVICE。
返回值: __iomem 类型的指针,指向映射后的虚拟空间首地址。
假如要获取ZYNQ 的 APER_CLK_CTRL 寄存器对应的虚拟地址 :
#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函数
void iounmap (volatile void __iomem *addr)
卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射 。
要取消掉 APER_CLK_CTRL 寄存器的地址映射 :
iounmap(aper_clk_ctrl_addr);
IO内存访问函数
当外部寄存器或内存映射到内存空间时,称为 I/O 内存 。使用 ioremap 函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux 内核不建议这么做,而是推荐操作函数来进行读写操作。
- 读操作函数
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)
readb、 readw 和 readl 这三个函数分别对应 8bit、 16bit 和 32bit 读操作,参数 addr 就是要读取写内存地址,返回值就是读取到的数据。
- 写操作函数
void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)
writeb、 writew 和 writel 这三个函数分别对应 8bit、 16bit 和 32bit 写操作,参数 value是要写入的数值, addr 是要写入的地址。
GPIO的寄存器
终于到属性的寄存器操作环节了,这里主要来源是官方的数据手册,就不细说了。
LED驱动示例代码
这里就直接使用正点原子的代码示例吧。
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");
第 24~25 行, 定义了两个宏,设备名字和设备的主设备号。
第 30~35 行,本实验要用到的寄存器宏定义。
第 38~42 行,经过内存映射以后的寄存器地址指针。
第 52~55 行, led_open 函数,为空函数,可以自行在此函数中添加相关内容,一般在此函数中将设备结构体作为参数 filp 的私有数据(filp->private_data)。
第 65~69 行, led_read 函数,为空函数,如果想在应用程序中读取 LED 的状态,那么就可以在此函数中添加相应的代码,比如读取 MIO 的 DATA 寄存器的值,然后返回给应用程序。
第 79~100 行, led_write 函数,实现对 LED 灯的开关操作,当应用程序调用 write 函数
向 led 设备写数据的时候此函数就会执行。首先通过函数 copy_from_user 获取应用程序发送过来的操作信息(打开还是关闭 LED),最后根据应用程序的操作信息来控制寄存器打开或关闭LED 灯。
第 107~110 行, led_release 函数,为空函数,可以自行在此函数中添加相关内容,一般关闭设备的时候会释放掉 led_open 函数中添加的私有数据。
第 113~119 行,设备文件操作结构体 led_fops 的定义和初始化。
第 121~165 行,驱动入口函数 led_init,此函数实现了 LED 的初始化工作, 127~131 行通过 ioremap 函数获取物理寄存器地址映射后的虚拟地址,得到寄存器对应的虚拟地址以后就可以完成相关初始化工作了。比如使能 GPIO 时钟、 关闭 MIO7 的中断功能、配置并使能 MIO7 的输出功能等。最后,最重要的一步!使用 register_chrdev 函数注册 led 这个字符设备。
第 167~178 行,驱动出口函数 led_exit,首先使用函数 unregister_chrdev 注销 led 这
个字符设备,然后调用 iounmap 函数取消内存映射,因为设备已经被卸载,也就意味用不到了,必须要取消映射;需要注意的是这两顺序不要反了,不能在设备没有卸载的情况下,你就把人家的内存映射给取消了,这是不合理的!
第 181~182 行,使用 module_init 和 module_exit 这两个函数指定 led 设备驱动加载和卸载函数。
第 184~186 行,添加模块 LICENSE、 作者信息以及模块描述信息。
对应的应用APP代码:
#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内核。使用以下命令下载:
git clone --depth 1 https://github.com/Xilinx/linux-xlnx.git
或者,在Petalinux编译后,在build/tmp/work-shared/plnx_arm/kernel-source/中可以找到代码,将其复制到一个易于寻找的目录下。输入:
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文件。
make M=DIR modules
基于此,我们可以编写一个makefile:
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文件后复制到开发板,如果有网络,可以使用以下方式进行复制:
scp led.ko root@192.168.1.133:/lib/modules/4.14.0-xilinx
输入命令加载驱动:
insmod led.ko
之后可以在/proc/devices中找到文件。
或者:
depmod # 生成.dep文件
modprobe led.ko
然后需要在/dev目录下创建节点:
mknod /dev/led c 200 0
mknod是创建节点命令,/dev/led是要创建的节点文件,c表示
这是个字符设备,200是设备的主设备号,0是设备的次设备号。创建完成以后就会存
在/dev/led 这个文件
编译APP。
arm-linux-gnueabihf-gcc ledApp.c -o ledApp
复制到开发板运行:
./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文件中的定义如下:
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不同,这里传递参数使用一个结构体传输。
struct cdev test_cdev;
- cdev_init函数
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
初始化结构体,用来设定一些成员的初始值。
struct cdev testcdev;
/* 设备操作函数 */
static struct file_operations test_fops = {
.owner = THIS_MODULE,
/* 其他具体的初始项 */
};
testcdev.owner = THIS_MODULE;
cdev_init(&testcdev, &test_fops); /* 初始化 cdev 结构体变量 */
- cdev_add函数
该函数就是实际的注册函数。
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所使用的设备号,参数 count 是要添加的设备数量。
cdev_add(&testcdev, devid, 1);
- cdev_del函数
该函数就是实际的注销函数。
void cdev_del(struct cdev *p)
示例:
cdev_del(&testcdev); /* 删除 cdev */
使用文件的私有数据
之前,我们使用变量来存储驱动的设备号,状态以及其他数据。这样其实不专业,对于一个设备的所有属性信息我们最好将其做成一个结构体,在open中传到私有数据中。
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 中可以看到:
#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)
宏展开后:
struct class *class_create (struct module *owner, const char *name)
该代码实现了类的创建。参数 owner 一般为 THIS_MODULE,参数 name 是类名字。返回值是个指向结构体 class 的指针,也就是创建的类。
卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy,函数原型如下:
void class_destroy(struct class *cls);
- 设备创建和删除
除了创建类以外,还需要在这个类下创建一个设备。
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 这个设备文件。返回值就是创建好的设备。
卸载驱动的时候需要删除掉创建的设备:
void device_destroy(struct class *class, dev_t devt)
参数 classs 是要删除的设备所处的类,参数 devt 是要删除的设备号 。
使用新API的代码示例
在上面代码的基础上,这里只列出不同部分:
...
#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跳转来恢复之前的操作。
编译:
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命令编译。
加载驱动:
Depmod //第一次加载驱动的时候需要运行此命令
modprobe newchrled.ko //加载驱动
rmmod newchrled.ko //卸载驱动
基于设备树的驱动开发
基于设备树的开发,就是将硬件信息写到设备树中,驱动只需要通过函数获取设备树信息,就不需要将寄存器地址之类的写到驱动中。
添加设备树
想设备树根节点中添加以下节点:
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”这个节点。
驱动编写
驱动在之前的基础上进行设置,给出不同的部分:
/* 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里面 :
#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 的优先级最低。如果要设置消息级别,参考如下示例:
printk(KERN_EMERG "gsmi: Log Shutdown Reason\n");//消息级别为KERN_EMERG
默认级别可以设置(CONFIG_MESSAGE_LOGLEVEL_DEFAULT ),默认为4。同时还有CONSOLE_LOGLEVEL_DEFAULT 控制哪些消息可以显示在控制台上,默认为 7,意味着只有优先级高于 7 的消息才能显示在控制台上 。
copy_to_user
内核空间的数据和用户空间的数据是分开的,想要向用户传参,需要调用函数:
static inline long copy_to_user(void __user *to, const void *from, unsigned long n)
参数 to 表示目的,参数 from 表示源,参数 n 表示要复制的数据长度。如果复制成功,返回值为 0,如果复制失败则返回负数。

浙公网安备 33010602011771号