ARM Linux 驱动开发篇--嵌入式 Linux LED 驱动开发实验(2)--Linux 下 LED 灯驱动开发代码编写-- Ubuntu20.04

🎬 渡水无言个人主页渡水无言

专栏传送门linux专栏

⭐️流水不争先,争的是滔滔不绝

📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生

| 省级优秀毕业生获得者 | csdn新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生

在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连

目录

前言

一、硬件原理图分析

二、实验程序编写

2.1、寄存器原理设置(看过裸机程序可以忽略)

2.2地址映射

2.3向虚拟地址写数据函数

2.4、程序编写

[2.5编写测试 APP](#2.5编写测试 APP)

三、运行测试

3.1、编译驱动程序和测试APP

3.1.1编写驱动程序

[3.1.2编译测试 APP](#3.1.2编译测试 APP)

3.2、运行测试

总结


前言

上一期博客介绍了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 驱动开发实验的代码的编写及测试。

相关推荐
草莓熊Lotso2 小时前
Linux 磁盘基础:从物理结构到 CHS/LBA 寻址,吃透数据存储底层逻辑
linux·运维·服务器·c++·人工智能
嵌入式-老费2 小时前
Linux Camera驱动开发(fpga vs soc)
驱动开发·fpga开发
叠叠乐2 小时前
EasyTier 免费自建自用5$每个月的服务器
linux·运维·bash
yueyin1234562 小时前
Linux下MySQL的简单使用
linux·mysql·adb
鸠摩智首席音效师11 小时前
如何在 Linux 中将文件复制到多个目录 ?
linux·运维·服务器
香蕉你个不拿拿^11 小时前
Linux进程地址空间解析
linux·运维·服务器
人间打气筒(Ada)11 小时前
Linux学习~日志文件参考
linux·运维·服务器·学习·日志·log·问题修复
xuhe212 小时前
Claude Code配合Astro + GitHub Pages:为 sharelatex-ce 打造现代化的开源项目宣传页
linux·git·docker·github·浏览器·overleaf
charlie11451419112 小时前
RK3568跑Arch Linux全路程指南(以正点原子的RK3568开发板为例子)
linux·嵌入式·rootfs·教程·环境配置·嵌入式linux·工程实践