
🎬 渡水无言 :个人主页渡水无言
❄专栏传送门 :linux专栏
⭐️流水不争先,争的是滔滔不绝
📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生
| 省级优秀毕业生获得者 | csdn新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生
在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连

目录
[2.5编写测试 APP](#2.5编写测试 APP)
[3.1.2编译测试 APP](#3.1.2编译测试 APP)
前言
上一期博客介绍了linux开发下的led灯驱动的原理,这一期博客便开始进行代码的编写并测试。
一、硬件原理图分析
首先咱们看看硬件原理情况,原理图如下:
从图中可以看出,LED0 接到了 GPIO_3 上,GPIO_3 就是 GPIO1_IO03,当 GPIO1_IO03 输出低电平 (0) 的时候发光二极管 LED0 就会导通点亮,当 GPIO1_IO03 输出高电平 (1) 的时候发光二极管 LED0 不会导通,因此 LED0 也就不会点亮。所以 LED0 的亮灭取决于 GPIO1_IO03 的输出电平,输出 0 就亮,输出 1 就灭。
二、实验程序编写
通过之前的裸机实验咱们可以知道需要对 GPIO1_IO03 做如下设置:
2.1、寄存器原理设置(看过裸机程序可以忽略)
1 、使能 GPIO1 时钟
GPIO1 的时钟由 CCM_CCGR1 的 bit27 和 bit26 这两个位控制,将这两个位都设置位 11 即
可。
2 、设置 GPIO1_IO03 的复用功能
找到 GPIO1_IO03 的复用寄存器"IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03"的地址为
0X020E0068,然后设置此寄存器,将 GPIO1_IO03 这个 IO 复用为 GPIO 功能,也就是 ALT5。
3 、配置 GPIO1_IO03
找到 GPIO1_IO03 的配置寄存器"IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03"的地址为
0X020E02F4,根据实际使用情况,配置此寄存器。
4 、设置 GPIO
我们已经将 GPIO1_IO03 复用为了 GPIO 功能,所以我们需要配置 GPIO。找到 GPIO3 对应的 GPIO 组寄存器地址,在《IMX6ULL 参考手册》的 1357 页,如图 8.3.1 所示:

本实验中 GPIO1_IO03 是作为输出功能的,因此 GPIO1_GDIR 的 bit3 要设置为1,表示输出。
5 、控制 GPIO 的输出电平
经过前面几步,GPIO1_IO03 已经配置好了,只需要向 GPIO1_DR 寄存器的 bit3 写入 0 即可控制 GPIO1_IO03 输出低电平,打开 LED,向 bit3 写入 1 可控制 GPIO1_IO03 输出高电平, 关闭 LED。
2.2地址映射
在上一期的博客中我们知道在linux里面操作的都是虚拟地址,所以需要先要用地址映射得到这个物理地址对应的虚拟地址。
获得物理物理地址对应的虚拟地址使用ioremap函数。
第一个参数就是物理地址起始地址,第二个参数就是要转化的字节数量。0X01010101,开始10个地址进行转换,比如:va=ioremap(0X01010101, 10). 知道这些以后可以开始写linux下led的驱动代码了。
2.3向虚拟地址写数据函数
writel()函数是 Linux 内核中专门用于向内存映射的 I/O 地址(MMIO)写入 32 位数据的函数。
函数原型:void writel(u32 value, volatile void __iomem *addr)
参数 value 是要写入的数值,addr 是要写入的地址。
2.4、程序编写
新建名为"2_led"文件夹,然后在 2_led 文件夹里面创建 VSCode 工程,工作区命名为"led"。 工程创建好以后新建 led.c 文件,此文件就是 led 的驱动文件,在 led.c 里面输入如下内容:
1 #include <linux/types.h>
2 #include <linux/kernel.h>
3 #include <linux/delay.h>
4 #include <linux/ide.h>
5 #include <linux/init.h>
6 #include <linux/module.h>
7 #include <linux/errno.h>
8 #include <linux/gpio.h>
9 #include <asm/mach/map.h>
10 #include <asm/uaccess.h>
11 #include <asm/io.h>
12 /***************************************************************
13 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
14 文件名 : led.c
15 版本 : V1.0
16 描述 : LED驱动文件。
17 其他 : 无
18 ***************************************************************/
19 #define LED_MAJOR 200 /* 主设备号 */
20 #define LED_NAME "led" /* 设备名字 */
21
22 #define LEDOFF 0 /* 关灯 */
23 #define LEDON 1 /* 开灯 */
24
25 /* 寄存器物理地址 */
26 #define CCM_CCGR1_BASE (0X020C406C)
27 #define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
28 #define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
29 #define GPIO1_DR_BASE (0X0209C000)
30 #define GPIO1_GDIR_BASE (0X0209C004)
31
32 /* 映射后的寄存器虚拟地址指针 */
33 static void __iomem *IMX6U_CCM_CCGR1;
34 static void __iomem *SW_MUX_GPIO1_IO03;
35 static void __iomem *SW_PAD_GPIO1_IO03;
36 static void __iomem *GPIO1_DR;
37 static void __iomem *GPIO1_GDIR;
38
39 /*
40 * @description : LED打开/关闭
41 * @param - sta : LEDON(0) 打开LED,LEDOFF(1) 关闭LED
42 * @return : 无
43 */
44 void led_switch(u8 sta)
45 {
46 u32 val = 0;
47 if(sta == LEDON) {
48 val = readl(GPIO1_DR);
49 val &= ~(1 << 3);
50 writel(val, GPIO1_DR);
51 }else if(sta == LEDOFF) {
52 val = readl(GPIO1_DR);
53 val|= (1 << 3);
54 writel(val, GPIO1_DR);
55 }
56 }
57
58 /*
59 * @description : 打开设备
60 * @param - inode : 传递给驱动的inode
61 * @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
62 * 一般在open的时候将private_data指向设备结构体。
63 * @return : 0 成功;其他 失败
64 */
65 static int led_open(struct inode *inode, struct file *filp)
66 {
67 return 0;
68 }
69
70 /*
71 * @description : 从设备读取数据
72 * @param - filp : 要打开的设备文件(文件描述符)
73 * @param - buf : 返回给用户空间的数据缓冲区
74 * @param - cnt : 要读取的数据长度
75 * @param - offt : 相对于文件首地址的偏移
76 * @return : 读取的字节数,如果为负值,表示读取失败
77 */
78 static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
79 {
80 return 0;
81 }
82
83 /*
84 * @description : 向设备写数据
85 * @param - filp : 设备文件,表示打开的文件描述符
86 * @param - buf : 要写给设备写入的数据
87 * @param - cnt : 要写入的数据长度
88 * @param - offt : 相对于文件首地址的偏移
89 * @return : 写入的字节数,如果为负值,表示写入失败
90 */
91 static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
92 {
93 int retvalue;
94 unsigned char databuf[1];
95 unsigned char ledstat;
96
97 retvalue = copy_from_user(databuf, buf, cnt);
98 if(retvalue < 0) {
99 printk("kernel write failed!\r\n");
100 return -EFAULT;
101 }
102
103 ledstat = databuf[0]; /* 获取状态值 */
104
105 if(ledstat == LEDON) {
106 led_switch(LEDON); /* 打开LED灯 */
107 } else if(ledstat == LEDOFF) {
108 led_switch(LEDOFF); /* 关闭LED灯 */
109 }
110 return 0;
111 }
112
113 /*
114 * @description : 关闭/释放设备
115 * @param - filp : 要关闭的设备文件(文件描述符)
116 * @return : 0 成功;其他 失败
117 */
118 static int led_release(struct inode *inode, struct file *filp)
119 {
120 return 0;
121 }
122
123 /* 设备操作函数 */
124 static struct file_operations led_fops = {
125 .owner = THIS_MODULE,
126 .open = led_open,
127 .read = led_read,
128 .write = led_write,
129 .release = led_release,
130 };
131
132 /*
133 * @description : 驱动入口函数
134 * @param : 无
135 * @return : 无
136 */
137 static int __init led_init(void)
138 {
139 int retvalue = 0;
140 u32 val = 0;
141
142 /* 初始化LED */
143 /* 1、寄存器地址映射 */
144 IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
145 SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
146 SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
147 GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
148 GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
149
150 /* 2、使能GPIO1时钟 */
151 val = readl(IMX6U_CCM_CCGR1);
152 val &= ~(3 << 26); /* 清楚以前的设置 */
153 val |= (3 << 26); /* 设置新值 */
154 writel(val, IMX6U_CCM_CCGR1);
155
156 /* 3、设置GPIO1_IO03的复用功能,将其复用为
157 * GPIO1_IO03,最后设置IO属性。
158 */
159 writel(5, SW_MUX_GPIO1_IO03);
160
161 /*寄存器SW_PAD_GPIO1_IO03设置IO属性
162 *bit 16:0 HYS关闭
163 *bit [15:14]: 00 默认下拉
164 *bit [13]: 0 kepper功能
165 *bit [12]: 1 pull/keeper使能
166 *bit [11]: 0 关闭开路输出
167 *bit [7:6]: 10 速度100Mhz
168 *bit [5:3]: 110 R0/6驱动能力
169 *bit [0]: 0 低转换率
170 */
171 writel(0x10B0, SW_PAD_GPIO1_IO03);
172
173 /* 4、设置GPIO1_IO03为输出功能 */
174 val = readl(GPIO1_GDIR);
175 val &= ~(1 << 3); /* 清除以前的设置 */
176 val |= (1 << 3); /* 设置为输出 */
177 writel(val, GPIO1_GDIR);
178
179 /* 5、默认关闭LED */
180 val = readl(GPIO1_DR);
181 val |= (1 << 3);
182 writel(val, GPIO1_DR);
183
184 /* 6、注册字符设备驱动 */
185 retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
186 if(retvalue < 0){
187 printk("register chrdev failed!\r\n");
188 return -EIO;
189 }
190 return 0;
191 }
192
193 /*
194 * @description : 驱动出口函数
195 * @param : 无
196 * @return : 无
197 */
198 static void __exit led_exit(void)
199 {
200 /* 取消映射 */
201 iounmap(IMX6U_CCM_CCGR1);
202 iounmap(SW_MUX_GPIO1_IO03);
203 iounmap(SW_PAD_GPIO1_IO03);
204 iounmap(GPIO1_DR);
205 iounmap(GPIO1_GDIR);
206
207 /* 注销字符设备驱动 */
208 unregister_chrdev(LED_MAJOR, LED_NAME);
209 }
210
211 module_init(led_init);
212 module_exit(led_exit);
213 MODULE_LICENSE("GPL");
214 MODULE_AUTHOR("duan");
19~23页 定义了一些宏,包括主设备号、设备名字、LED开/关宏。
第26~30行,本实验要用到的寄存器宏定义(物理地址)。
第33~37行,经过内存映射以后的寄存器虚拟地址指针。
第44~56行,led switch函数,用于控制开发板上的LED灯亮灭,当参数sta为LEDON(1)的时候打开LED灯,sta为LEDOFF(O)的时候关闭LED灯。
第65~68行,led_open函数,为空函数,可以自行在此函数中添加相关内容,一般在此函数中将设备结构体作为参数filp的私有数据(filp->private_data)。
第78~81行,led_read函数,为空函数,如果想在应用程序中读取LED的状态,那么就可以在此函数中添加相应的代码,比如读取GPIO1DR寄存器的值,然后返回给应用程序。
第91~111行,led_write函数,实现对LED灯的开关操作,当应用程序调用write函数向ked设备写数据的时候此函数就会执行。首先通过函数copy_from_usr获取应用程序发送过来的操作信息(打开还是关闭LED),最后根据应用程序的操作信息来打开或关闭LED灯。
第118~121行,led_release函数,为空函数,可以自行在此函数中添加相关内容,一般关闭设备的时候会释放掉led_open函数中添加的私有数据。
第124~130行,设备文件操作结构体led_fops 的定义和初始化。
第137~191行,驱动入口函数led_init.此函数实现了LED的初始化工作。
144~148行通过ioremap函数获取物理寄存器地址映射后的虚拟地址,得到寄存器对应的虚拟地址以后就可以完成相关初始化工作了。(这就是为什么前边要介绍地址映射的概念)
之后便可以使能GPIO1时钟、设置GPIO1_1O03复用功能、配置GPIO1_1O03的属性等等。
最后,最重要的一步!使用register_chrdev函数注册led这个字符设备。
简单来说,这是将我们编写的 LED 驱动程序注册到 Linux 内核中,让内核识别并管理这个字符设备 ,让内核知道:
有一个主设备号为 LED_MAJOR(200)、名字为 LED_NAME("led") 的字符设备;
这个设备的操作逻辑(打开、读写、关闭)由 led_fops 结构体(代码 124-130 行)定义。
第198~209行,驱动出口函数led_exit,首先使用函数iounmap取消内存映射,最后使用函数unregister_chrdev 注销led这个字符设备。
第211~212行,使用module_init 和module_exit这两个函数指定led设备驱动加载和卸载函数。
第213~214行,添加LICENSE和作者信息。
2.5编写测试****APP
编写测试 APP,led 驱动加载成功以后手动创建/dev/led 节点,应用 APP 通过操作/dev/led
文件来完成对 LED 设备的控制。向/dev/led 文件写 0 表示关闭 LED 灯,写 1 表示打开 LED 灯。
新建 ledApp.c 文件,在里面输入如下内容:
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : ledApp.c
版本 : V1.0
描述 : chrdevbase驱测试APP。
其他 : 无
使用方法 :./ledtest /dev/led 0 关闭LED
./ledtest /dev/led 1 打开LED
***************************************************************/
#define LEDOFF 0
#define LEDON 1
/*
* @description : main主程序
* @param - argc : argv数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
unsigned char databuf[1];
if(argc != 3){
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
/* 打开led驱动 */
fd = open(filename, O_RDWR);
if(fd < 0){
printf("file %s open failed!\r\n", argv[1]);
return -1;
}
databuf[0] = atoi(argv[2]); /* 要执行的操作:打开或关闭 */
/* 向/dev/led文件写入数据 */
retvalue = write(fd, databuf, sizeof(databuf));
if(retvalue < 0){
printf("LED Control Failed!\r\n");
close(fd);
return -1;
}
retvalue = close(fd); /* 关闭文件 */
if(retvalue < 0){
printf("file %s close failed!\r\n", argv[1]);
return -1;
}
return 0;
}
ledApp.c 的内容还是很简单的,就是对 led 的驱动文件进行最基本的打开、关闭、写操作等。
三、运行测试
3.1、编译驱动程序和测试APP
3.1.1编写驱动程序
这个文件上几期博客实验的makefile文件基本一样,把obj-m 变量的值改为 led.o,代码如下:
KERNELDIR := /home/duan/linux/linux-imx-rel_imx_4.1.15_2.1.1_ga_alientek_v2.2
CURRENT_PATH := $(shell pwd)
obj-m := led.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
输入如下命令编译出驱动模块文件:
make -j32
编译成功以后就会生成一个名为" led.ko "的驱动模块文件。
3.1.2编译测试****APP
输入如下命令编译测试 ledApp.c 这个测试程序:
arm-linux-gnueabihf-gcc ledApp.c -o ledApp
编译成功以后就会生成 ledApp 这个应用程序。
3.2、运行测试
再把led.ko文件以及ledApp文件复制到 rootfs/lib/modules/4.1.15 目录中,命令如下:
sudo cp led.ko /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f
sudo cp ledApp /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f
拷贝完之后,便输入如下命令加载 led.ko 驱动文件:
modprobe led.ko
输入如下命令查看当前系统中有没有 chrdevbase 这个设备:
cat /proc/devices

驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。输入如下命令创建/dev/chrdevbase 这个设备节点文件:
mknod /dev/led c 200 0
这个文件,可以使用"ls /dev/led -l"命令查看,结果如下图所示:
ls /dev/led-l

驱动节点创建成功以后就可以使用 ledApp 软件来测试驱动是否工作正常,输入如下命令打开 LED 灯:
./ledApp /dev/led 1
关闭灯:
./ledApp /dev/led 0
然后观察开发板上的红色 LED 灯是否对应点亮或关闭,博主这里的是正常的。
如果要卸载驱动的话输入如下命令即可:
rmmod led.ko
总结
这一期博客完成了嵌入式 Linux LED 驱动开发实验的代码的编写及测试。