嵌入式 Linux LED 驱动开发实验

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

提示:这里可以添加本文要记录的大概内容:

会对着手册操作点亮LED对应的寄存器,裸机部分有说。


提示:以下是本篇文章正文内容,下面案例可供参考

一、点亮、熄灭LED

1、物理地址与虚拟地址

物理地址就是寄存器地址,Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚拟 地 址 。 比 如 I.MX6ULL 的 GPIO1_IO03 引 脚 的 复 用 寄 存 器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 的地址为 0X020E0068。如果没有开启 MMU 的话直接向 0X020E0068 这个寄存器地址写入数据就可以配置 GPIO1_IO03 的复用功能。现在开启了 MMU,并且设置了内存映射,因此就不能直接向 0X020E0068 这个地址写入数据了。我们必须得到 0X020E0068 这个物理地址在 Linux 系统里面对应的虚拟地址,这里就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数: ioremap 和 iounmap。
1、 ioremap 函数
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,真正起作用的是函数__arm_ioremap,此函数有三个参数和一个返回值,这些参数和返回值的含义如下:

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

假如我们要获取 I.MX6ULL 的 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 寄存器对应的虚拟地址,使用如下代码即可:

#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
static void __iomem* SW_MUX_GPIO1_IO03;
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);

宏 SW_MUX_GPIO1_IO03_BASE 是寄存器物理地址, SW_MUX_GPIO1_IO03 是映射后的虚拟地址。对于 I.MX6ULL 来说一个寄存器是 4 字节(32 位)的,因此映射的内存长度为 4。映射完成以后直接对 SW_MUX_GPIO1_IO03 进行读写操作即可。
2、 iounmap 函数
卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射, iounmap 函数原型如下:

void iounmap (volatile void __iomem *addr)

iounmap 只有一个参数 addr,此参数就是要取消映射的虚拟地址空间首地址。假如我们现在要取消掉 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 寄存器的地址映射,使用如下代码即可:

iounmap(SW_MUX_GPIO1_IO03);

第一个参数就是物理地址其实大小,第二个参数就是要转化的字节数量。0X01010101,开始10个地址进行转换,va=ioremap(0X01010101, 10)

2、I/O 内存访问函数

这里说的 I/O 是输入/输出的意思,并不是我们学习单片机的时候讲的 GPIO 引脚。这里涉及到两个概念: I/O 端口和 I/O 内存。当外部寄存器或内存映射到 IO 空间时,称为 I/O 端口。当外部寄存器或内存映射到内存空间时,称为 I/O 内存。但是对于 ARM 来说没有 I/O 空间这个概念,因此 ARM 体系下只有 I/O 内存(可以直接理解为内存)。使用 ioremap 函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux 内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。
1、读操作函数
读操作函数有如下几个:

1 u8 readb(const volatile void __iomem *addr)
2 u16 readw(const volatile void __iomem *addr)
3 u32 readl(const volatile void __iomem *addr)

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

1 void writeb(u8 value, volatile void __iomem *addr)
2 void writew(u16 value, volatile void __iomem *addr)
3 void writel(u32 value, volatile void __iomem *addr)

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

3、代码梳理

1、寄存器物理地址

/*寄存器物理地址*/
#define CCM_CCGR1_BASE				(0X020C406C)	
#define SW_MUX_GPIO1_IO03_BASE		(0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE		(0X020E02F4)
#define GPIO1_DR_BASE				(0X0209C000)
#define GPIO1_GDIR_BASE				(0X0209C004)

2、将物理地址映射到虚拟地址

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

3、在led_init()对led初始化

1、寄存器地址映射

假如我们要获取 I.MX6ULL 的 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 寄存器对应的虚拟地址,使用如下代码即可:

#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
static void __iomem* SW_MUX_GPIO1_IO03;
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);

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

/* 1、寄存器地址映射 */
  	IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
	SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
  	SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
	GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
	GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
2、使能GPIO1时钟
/* 2、使能GPIO1时钟 */
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26);	/* 清楚以前的设置 */
val |= (3 << 26);	/* 设置新值 */
writel(val, IMX6U_CCM_CCGR1);
3、设置复用,设置电气属性
/* 3、设置GPIO1_IO03的复用功能,将其复用为
 *    GPIO1_IO03,最后设置IO属性。
 */
writel(0x5, SW_MUX_GPIO1_IO03);

/*寄存器SW_PAD_GPIO1_IO03设置IO属性
 *bit 16:0 HYS关闭
 *bit [15:14]: 00 默认下拉
 *bit [13]: 0 kepper功能
 *bit [12]: 1 pull/keeper使能
 *bit [11]: 0 关闭开路输出
 *bit [7:6]: 10 速度100Mhz
 *bit [5:3]: 110 R0/6驱动能力
 *bit [0]: 0 低转换率
 */
writel(0x10B0, SW_PAD_GPIO1_IO03);


4、设置GPIO1_IO03为输出功能
/* 4、设置GPIO1_IO03为输出功能 */
val = readl(GPIO1_GDIR);
// val &= ~(1 << 3);	/* 清除以前的设置 */
val |= (1 << 3);	/* 设置为输出 */
writel(val, GPIO1_GDIR);
5、开启LED
/* 5、默认开启LED */
val = readl(GPIO1_DR);
val &= ~(1 << 3);	
writel(val, GPIO1_DR);

4、注销字符驱动

iounmap 只有一个参数 addr,此参数就是要取消映射的虚拟地址空间首地址。假如我们现在要取消掉 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 寄存器的地址映射,使用如下代码即可:

iounmap(SW_MUX_GPIO1_IO03);
	int val = 0;
	/* 关闭LED  , 关闭一定要在取消映射前*/
	val = readl(GPIO1_DR);
	val |= (1 << 3);	
	writel(val, GPIO1_DR);

有物理地址对应的虚拟地址映射,就一定要在注销字符驱动模块里取消映射

/* 取消映射 */
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);

4、编译烧录

虚拟机下

make
sudo cp led.ko /home/zero/linux/nfs/rootfs/lib/modules/4.1.15/

xshell

depmod
modprobe // 加载驱动,灯亮
rmmod // 卸载驱动,灯灭

完整led.c代码

#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>


#define LED_MAJOR		200		/* 主设备号 */
#define LED_NAME		"led" 	/* 设备名字 */


static int led_open(struct inode *innode,struct file *filp)
{
	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 *file,const char __user *buf,size_t cnt,loff_t *offt)
{
	return 0;
}
static int led_release (struct inode *inode,struct file *filp)
{
	return 0;
}


/*定义结构体变量并初始化
	语法:
	struct 结构体类型 变量名 = {
		.成员1 = 初始值1,
		.成员2 = 初始值2 ,
		...
	};

*/
static struct file_operations led_fops={
	.owner = THIS_MODULE,
	.open  = led_open,
	.read  = led_read,
	.write = led_read,
	.release = led_release,
};

static int __init led_init(void)
{
	/*注册字符驱动 原型 :
	 * static inline int register_chrdev(unsigned int major, const char *name,
									const struct file_operations *fops)  
	* 归根到底还是结构体变量与结构体指针(指向结构体变量的指针) 之间的关系
	0:fops = &led_fops将结构体变量的地址存放在一个指针变量中,那么指针变量就指向该结构体变量。
	1:结构体变量	led_fops							
	2:结构体指针	fops							
	3:参数要求是传入一个	struct file_operations 类型的指针fops
	4:将指针变量	led_fops取地址& 得到&led_fops,
	5:赋值,fops = &led_fops,即满足要求 (结构体变量的起始地址(取地址)就是结构体变量的指针)
	6:引用,
			结构体变量引用结构体成员用.	即  led_fops.read	led_fops.write	
			指针变量引用结构体成员用->	即  fops->read	    fops->write	
	*/
	int ret =0;
	ret =  register_chrdev(LED_MAJOR, LED_NAME,&led_fops); 	//调用函数,不用把函数类型带上。有返回值							
	printk("led_init\r\n");
	return 0;
}
static void __exit led_exit(void) // void类型函数没有返回值
{
	/*卸载字符驱动*/
	// static inline void unregister_chrdev(unsigned int major, const char *name)
	unregister_chrdev(LED_MAJOR,LED_NAME); //调用函数,不用把函数类型带上。

	printk("led_exit\r\n");
}



module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zero");




二、完善驱动与APP编写

copy_to_user()这个函数的完整形态为

unsigned long copy_to_user(void *to, const void *from, unsigned long n);
这个函数的作用是将内核空间的数据复制到用户空间。其中
to:目标地址(用户空间)
from:源地址(内核空间)
n:将要拷贝数据的字节数
返回:成功返回0,失败返回没有拷贝成功的数据字节数

copy_from_user()这个函数的完整形态为

unsigned long copy_from_user(void *to, const void *from, unsigned long n);
这个函数的作用是将用户空间的数据复制到内核空间。其中
to:目标地址(内核空间)
from:源地址(用户空间)
n:将要拷贝数据的字节数
返回:成功返回0,失败返回没有拷贝成功的数据字节数

使用这个两个函数就可以实现内核空间和用户空间的数据交互了
完整代码:

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"


#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 这个测试程序:

arm-linux-gnueabihf-gcc ledApp.c -o ledApp
sudo cp ledApp /home/zero/linux/nfs/rootfs/lib/modules/4.1.15/

编译成功以后就会生成 ledApp 这个应用程序。

  • 27
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值