Linux字符驱动框架学习笔记01(ZYNQ-7010)

Linux字符驱动框架学习笔记01(ZYNQ-7010)

发现最近随便转载的很多啊,未经授权禁止转载!抄袭!!否则转载者死全家!!另外这是我的笔记,不是教程,难免会有错误,不具有很高的参考性,望周知。

以此笔记记录我在驱动学习过程中的知识点,仅此而已。

模块基本框架:

1
2
3
4
5
module_init(newchrled_init);
module_exit(newchrled_exit);
MODULE_AUTHOR("verylk");
MODULE_DESCRIPTION("ZYNQ New Char Driver");
MODULE_LICENSE("GPL");

头文件:

1
2
3
4
5
6
7
8
9
10
11
12
#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 <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/cdev.h>

宏定义:

1
2
#define NEWCHRLED_CNT    1    /* 设备号个数 */
#define NEWCHRLED_NAME    "newchrled" /* 名字 */

GPIO寄存器地址:

1
2
3
4
5
6
#define ZYNQ_GPIO_REG_BASE    0xE000A000
#define DATA_OFFSET 0x00000040
#define DIRM_OFFSET 0x00000204
#define OUTEN_OFFSET 0x00000208
#define INTDIS_OFFSET 0x00000214
#define APER_CLK_CTRL 0xF800012C

地址映射,虚拟地址到物理地址映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void __iomem *data_addr;
static void __iomem *dirm_addr;
static void __iomem *outen_addr;
static void __iomem *intdis_addr;
static void __iomem *aper_clk_ctrl_addr;

static inline void led_ioremap(void)//使用内联函数,以空间换时间,减小压栈和出栈时间,提高执行效率
{
data_addr = ioremap(ZYNQ_GPIO_REG_BASE + DATA_OFFSET, 4);
dirm_addr = ioremap(ZYNQ_GPIO_REG_BASE + DIRM_OFFSET, 4);
outen_addr = ioremap(ZYNQ_GPIO_REG_BASE + OUTEN_OFFSET, 4);
intdis_addr = ioremap(ZYNQ_GPIO_REG_BASE + INTDIS_OFFSET, 4);
aper_clk_ctrl_addr = ioremap(APER_CLK_CTRL, 4);
}
static inline void led_iounmap(void)
{
iounmap(data_addr);
iounmap(dirm_addr);
iounmap(outen_addr);
iounmap(intdis_addr);
iounmap(aper_clk_ctrl_addr);
}

字符设备的结构体:

1
2
3
4
5
6
7
8
9
10
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 设备 */

设备操作函数:

1
2
3
4
5
6
7
8
/* 设备操作函数 */
static struct file_operations newchrled_fops = {
.owner = THIS_MODULE,
.open = led_open, //需要实现这几个函数
.read = led_read,
.write = led_write,
.release = led_release,
};

操作函数实现,函数参数可以参考内核中现有的驱动,这里主要实现open和write函数:

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
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &newchrled; /* 设置私有数据,私有数据指向设备结构体 */
return 0;
}

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

static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int ret;
int val;
char kern_buf[1];

ret = copy_from_user(kern_buf, buf, cnt); // 得到应用层传递过来的数据
if(0 > ret) {
printk(KERN_ERR "kernel write failed!\r\n");
return -EFAULT;
}

val = readl(data_addr);
if (0 == kern_buf[0])
val &= ~(0x1U << 7); // 如果传递过来的数据是 0 则关闭 led
else if (1 == kern_buf[0])
val |= (0x1U << 7); // 如果传递过来的数据是 1 则点亮 led

writel(val, data_addr);
return 0;
}

static int led_release(struct inode *inode, struct file *filp)
{
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
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
static int __init led_init(void)
{
u32 val;
int ret;

/* 1.寄存器地址映射 */
led_ioremap();

/* 2.使能 GPIO 时钟 */
val = readl(aper_clk_ctrl_addr);
val |= (0x1U << 22);
writel(val, aper_clk_ctrl_addr);

/* 3.关闭中断功能 */
val |= (0x1U << 7);
writel(val, intdis_addr);

/* 4.设置 GPIO 为输出功能 */
val = readl(dirm_addr);
val |= (0x1U << 7);
writel(val, dirm_addr);

/* 5.使能 GPIO 输出功能 */
val = readl(outen_addr);
val |= (0x1U << 7);
writel(val, outen_addr);

/* 6.默认关闭 LED */
val = readl(data_addr);
val &= ~(0x1U << 7);
writel(val, data_addr);

/* 7.注册字符设备驱动 */
/* 创建设备号 */
if (newchrled.major) {
newchrled.devid = MKDEV(newchrled.major, 0);

ret = register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_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();
}

至此,驱动编写完毕。

编写驱动编译makefile文件:

1
2
3
4
5
6
7
8
9
KERN_DIR := /home/ubuntu/Zynq-Pfj/ACZ702_Linux_Base/linux/kernel
#需要修改为对应内核源码路径
obj-m := newchrled.o
#需要修改为.c文件对应的.o文件,.c文件make会自动查找
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
make -j12 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-

编译没有错误会生成.ko文件:

1
2
3
4
5
6
7
8
9
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C /home/ubuntu/Zynq-Pfj/ACZ702_Linux_Base/linux/kernel M=`pwd` modules
make[1]: warning: jobserver unavailable: using -j1. Add '+' to parent make rule.
make[1]: Entering directory '/home/ubuntu/Zynq-Pfj/ACZ702_Linux_Base/linux/kernel'
CC [M] /home/ubuntu/Zynq-Pfj/3_newchrled/newchrled.o
Building modules, stage 2.
MODPOST 1 modules
CC /home/ubuntu/Zynq-Pfj/3_newchrled/newchrled.mod.o
LD [M] /home/ubuntu/Zynq-Pfj/3_newchrled/newchrled.ko
make[1]: Leaving directory '/home/ubuntu/Zynq-Pfj/ACZ702_Linux_Base/linux/kernel'

复制.ko文件到文件系统中:

1
sudo cp newchrled.ko ~/zynq/linux/nfs/lib/modules/4.14.0-xilinx

连接ZYNQ开发板,在开发板上执行命令:

1
2
3
root@ACZ702_System:~# depmod
root@ACZ702_System:~# modprobe newchrled.ko
newcheled major=245,minor=0

查看/dev目录下是否自动创建了newchrled设备:

1
2
root@ACZ702_System:~# ls /dev | grep newchrled
newchrled

打开LED,测试newchrled设备是否正常:

1
2
echo 1 > /dev/newchrled
#不要尝试,会死机

这样直接向设备写字符会导致死机,这里需要写一个APP,让APP去向newchrled写入数据:

APP.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#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;
}

交叉编译后,执行:

1
root@ACZ702_System:~# ./APP /dev/newchrled 1

发现LED灯亮了,至此驱动编写完成。

坚持原创技术分享,您的支持是我前进的动力!