4412开发板学习之Linux驱动开发(五):4412MMU及GPIO操作(点灯)

物理地址与虚拟地址

与传统MCU的对比

传统的MCU直接操作寄存器,主要原因是本身的主频并不太高
而现代的CPU由于速度远快于存储设备所以通过Cache、内存来进行缓冲,解决速度不匹配的问题
在这里插入图片描述

4412中的物理地址

  • CPU内部寄存器register
  • eMMC
  • TF卡

MMU内存管理单元

MMU是中央处理器中用来管理虚拟存储器、物理存储器的控制线路,同时负责虚拟地址映射为物理地址
为解决代码需要的内存大于物理内存的问题
基本思路:数据和堆栈的大小总和可以超过物理存储器的大小,操作系统把当前使用的部分留在内存中,其它的保留在硬盘上
地址范围

  • 32位0-2^32
  • 64位0-2^64

页和页帧

  • 虚拟地址空间被划分为页的单位,相应的物理地址空间被划分为页帧,这就是分页机制。页的大小必须一样,页帧的大小也必须一样

一台32位机器最大可运行4G大小的程序,前提是这台机器首先要有4G的存储空间

存储器分类

  • ROM
  • RAM
  • Cache
  • SFR:特殊寄存器

4412中的存储器映射

在这里插入图片描述
iROM

  • 存储三星的一段固化代码,用于启动Uboot

iRAM

  • iROM是不能运行程序的,三星固化的代码在iRAM中运行

i-Cache

  • CPU和内存之间的通信

SFR region

  • 特殊寄存器,GPIO、中断、总线等等

Memory of Dynamic Memory Controller

  • 动态内存控制器,
    Linux内核中函数ioremap,返回的地址会在这个区间之内(0x4000000-0x00000000)。
    1G 内存的话是0x40000000-0x80000000
    内存是有物理地址的

物理地址和虚拟地址

虚拟地址指的是一种通信技术
虚拟地址的返回和内存的物理地址会重合

其他的地址概念

Base Address

  • 某一类寄存器的其实地址,一个寄存器加上偏移地址,就是这个寄存器的物理地址

Offset

  • 偏移地址

总线的地址

  • 和内存地址虚拟地址毫无关系,是总线用于区分设备的

GPIO操作

GPIO初始化

查看GPIO部分是否被编译进内核:ls drivers/gpio/*.o
在这里插入图片描述
有.o文件就说明已经被编译
在gpio-exynos4.c最下面一行

core_initcall(exynos4_gpiolib_init);

在linux初始化过程中会调用的函数
初始化函数调用exynos4_gpiolib_init,该函数中重要的一句

chip = exynos4_gpio_common_4bit;

exynos4_gpio_common_4bit结构体部分

static struct s3c_gpio_chip exynos4_gpio_common_4bit[] = {
#endif
	{
		.base	= S5P_VA_GPIO1,
		.eint_offset = 0x0,
		.group	= 0,
		.chip	= {
			.base	= EXYNOS4_GPA0(0),
			.ngpio	= EXYNOS4_GPIO_A0_NR,
			.label	= "GPA0",
		},
	}, {
		.base	= (S5P_VA_GPIO1 + 0x20),
		.eint_offset = 0x4,
		.group	= 1,
		.chip	= {
			.base	= EXYNOS4_GPA1(0),
			.ngpio	= EXYNOS4_GPIO_A1_NR,
			.label	= "GPA1",
		},
	}, {
		.base	= (S5P_VA_GPIO1 + 0x40),
		.eint_offset = 0x8,
		.group	= 2,
		.chip	= {
			.base	= EXYNOS4_GPB(0),
			.ngpio	= EXYNOS4_GPIO_B_NR,
			.label	= "GPB",
		},
	}, {
		.base   = (S5P_VA_GPIO1 + 0x60),
		.eint_offset = 0xC,
		.group	= 3,
		.chip	= {
			.base	= EXYNOS4_GPC0(0),
			.ngpio	= EXYNOS4_GPIO_C0_NR,
			.label	= "GPC0",
		},
	}, 

结构体中S5P_VA_XXXX的基地址定义,VA一般用来代表虚拟地址,PV用来代表物理地址
在这里插入图片描述

{
	.base   = (S5P_VA_GPIO2 + 0x100),//基地址和偏移地址相加
	.eint_offset = 0x20,//和中断相关
	.group	= 22,//Linux内核处理中的分组
	.chip	= {
		.base	= EXYNOS4_GPL2(0),//宏定义 EXYNOS4_GPL2(0)赋值给初始化函数
		.ngpio	= EXYNOS4_GPIO_L2_NR,//表示这一小组有几个GPIO
		.label	= "GPL2",//程序员需要关心的标志
},

EXYNOS4_GPL2(0)的分析

#define EXYNOS4_GPL2(_nr)	(EXYNOS4_GPIO_L2_START + (_nr))

我们再看一下EXYNOS4_GPIO_L2_STAR是什么

EXYNOS4_GPIO_L2_START		= EXYNOS4_GPIO_NEXT(EXYNOS4_GPIO_L1),

EXYNOS4_GPIO_NEXT的宏定义

#define EXYNOS4_GPIO_NEXT(__gpio) ((__gpio##_START) + (__gpio##_NR) + CONFIG_S3C_GPIO_SPACE + 1)

GPIO的数量

GPIO数量:EXYNOS4_GPIO_L2_NR
也可以通过手册来查找

S5P_VA_GPIO2

#define S5P_VA_GPIO1		S5P_VA_GPIO
#define S5P_VA_GPIO2		S3C_ADDR(0x02240000)
#define S5P_VA_GPIO3		S3C_ADDR(0x02280000)
#define S5P_VA_GPIO4		S3C_ADDR(0x022C0000)

四个GPIO bank/part

虚拟地址与物理地址的映射

#define S3C_ADDR(x)	((void __iomem __force *)S3C_ADDR_BASE + (x))
#define S3C_ADDR_BASE	0xF6000000

这是给分配的虚拟地址
虚拟地址与物理地址的映射

static struct map_desc exynos4_iodesc[] __initdata 
{
	.virtual	= (unsigned long)S5P_VA_GPIO2,//表示虚拟地址
	.pfn		= __phys_to_pfn(EXYNOS4_PA_GPIO2),//表示物理地址
	.length		= SZ_4K,//表示映射宽度
	.type		= MT_DEVICE,
}
#define EXYNOS4_PA_GPIO2		0x11000000//物理地址

GPIO初始化流程

平台文件分别定义好物理地址和虚拟地址
物理地址和虚拟地址之间映射
在初始化中,引入了程序员需要使用的GPIO宏定义,并将宏定义装入chip结构体中
通过调用gpio-cfg.h中的s3c_gpio_cfgpin函数,用来给GPIO做配置

LED驱动程序

用到的头文件

Linux中申请GPIO的头文件

  • #include <linux/gpio.h>

三星平台的GPIO配置函数头文件

  • arch/arm/plat-samsung/include/plat/gpio-cfg.h
  • #include <plat/gpio-cfg.h>

三星平台EXYNOS系列平台,GPIO配置参数宏定义头文件

  • arch/arm/plat-samsung/include/plat/gpio-cfg.h

  • #include <plat/gpio-cfg.h>

  • #include <mach/gpio.h>
    三星平台4412平台,GPIO宏定义头文件

  • arch/arm/mach-exynos/include/mach/gpio-exynos4.h

  • #include <mach/gpio-exynos4.h>

  • 包括4412处理器所有的GPIO的宏定义

管脚调用、赋值以及配置

LinuxGPIO申请函数和赋值函数

  • gpio_request
  • gpio_set_value

三星平台配置GPIO函数

  • s3c_gpio_cfgpin

GPIO配置输出模式的宏变量

  • S3C_GPIO_OUTPUT

点灯实验

硬件连接

在这里插入图片描述
在核心板原理图中查找芯片引脚
在这里插入图片描述
所以我们要操作GPL2_0,当它输出高定平的时候LED亮,当它输出低电平的时候LED灭。

生成设备节点的代码

#include <linux/init.h>
#include <linux/module.h>
/*driver register*/
#include <linux/platform_device.h>

/*注册杂项设备头文件*/
#include <linux/miscdevice.h>
/*注册设备节点的文件结构体*/
#include <linux/fs.h>

/*Linux中申请GPIO的头文件*/
#include <linux/gpio.h>
/*三星平台的GPIO配置函数头文件*/
/*GPIO配置参数宏定义头文件*/
#include <plat/gpio-cfg.h>
#include <mach/gpio.h>
/*三星平台4412平台,GPIO宏定义头文件*/
#include <mach/gpio-exynos4.h>

#define DRIVER_NAME "hello_ctl"
#define DEVICE_NAME "hello_ctl_dev"//设备名
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("GYY");

static int hello_open(struct inode * pinode , struct file * pfile )
{
	printk(KERN_EMERG "Hello OPEN !!\n");
	return 0;
}

static int hello_release(struct inode * pinode, struct file * pfile)
{
	printk(KERN_EMERG "Hello RELEASE !!\n");
	return 0;
}
/*GPIO控制函数,cmd为电平(0或1),arg为IO口的标号(此处仅为1)*/
static long hello_ioctl(struct file * pfile, unsigned int cmd, unsigned long arg)
{
	printk("cmd is %d ,arg is %d\n",cmd,arg);
	if(cmd > 1)
	{
		printk(KERN_EMERG "cmd is 0 or 1 \n");
		return 0;
	}
	if(arg > 1)
	{
		printk(KERN_EMERG "arg is only 1 \n");
		return 0;
	}
	gpio_set_value(EXYNOS4_GPL2(0),cmd);
	return 0;
}

static struct file_operations hello_ops = {
	.owner = THIS_MODULE,
	.open = hello_open,
	.release = hello_release,
	.unlocked_ioctl = hello_ioctl,
	
};


static struct miscdevice hello_dev = {
	.minor = MISC_DYNAMIC_MINOR,//自动分配设备号
	.name = DEVICE_NAME,//设备名
	.fops = &hello_ops,
};

static int hello_probe (struct platform_device *pdv){
	
	int ret;
	printk(KERN_EMERG "\tinitialized\n");
	/*申请设备*/
	ret = gpio_request(EXYNOS4_GPL2(0),"LEDs");
	if(ret < 0)//申请是失败
	{
		printk(KERN_EMERG "gpio_request EXYNOS4_GPL2(0) failed\n");
		return ret;
	}
	//初始化GPL2_0为输出
	s3c_gpio_cfgpin(EXYNOS4_GPL2(0),S3C_GPIO_OUTPUT);
	//输出低电平,灭灯
	gpio_set_value(EXYNOS4_GPL2(0),0);
	/*生成设备节点*/
	misc_register(&hello_dev);
	return 0;
}

static int hello_remove (struct platform_device *pdv){
	
	printk(KERN_EMERG "\tremove\n");
	misc_deregister(&hello_dev);
	return 0;
}

static void hello_shutdown (struct platform_device *pdv){
	
	
}

static int hello_suspend (struct platform_device *pdv,pm_message_t state){
	
	return 0;
}

static int hello_resume (struct platform_device *pdv){
	
	return 0;
}


struct platform_driver hello_driver = {
	.probe = hello_probe,
	.remove = hello_remove,
	.shutdown = hello_shutdown,
	.suspend = hello_suspend,
	.resume = hello_resume,
	.driver = {
		.name = DRIVER_NAME,
		.owner = THIS_MODULE,
	}
};


static int hello_init(void)
{
	int DriverState;
	printk(KERN_EMERG "HELLO WORLD enter!\n");
	DriverState=platform_driver_register(&hello_driver);
	
	printk(KERN_EMERG "\t%d\n",DriverState);
	return 0;
}

static void hello_exit(void)
{
	printk(KERN_EMERG "HELLO WORLD exit!\n");
	platform_driver_unregister(&hello_driver);
}

module_init(hello_init);
module_exit(hello_exit);

生成设备节点代码分析

在上面的代码中我们生成了设备节点“hello_ctl_dev”,并且我们写了底层的操作,当我们生成设备节点成功时,会进入probe(初始化)函数,然后在probe函数中申请GPIO设备,初始化GPIO,并且将GPIO置0,本IO口在Uboot中是置一的,所以说执行后应当灯灭。

应用程序代码

#include <stdio.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>


int main()
{
	int fd;
	char *hello_node = "/dev/hello_ctl_dev";
	
	/*O_RDWR只读打开,O_NDELAY非阻塞方式打开*/
	if((fd = open(hello_node,O_RDWR|O_NDELAY)) < 0)
	{
		printf("APP open %s failed\n",hello_node);
	}
	else
	{
		printf("APP open %s success\n",hello_node);
		ioctl(fd,1,1);//灯亮
		sleep(3);//延时3秒
		ioctl(fd,0,1);//灯灭
		sleep(3);//延时3秒
		ioctl(fd,1,1);//灯亮
		sleep(3);//延时3秒
		ioctl(fd,0,1);//灯灭
	}
	close(fd);
}

应用程序代码分析

这个应用程序代码和上次的应用程序代码没有什么不同,就是打开文件,然后操作IO,最后关闭文件。
如果执行成功的话,灯会以3秒的间隔闪烁

实验效果

生成设备节点
在这里插入图片描述
此时LED熄灭
查看设备节点
在这里插入图片描述
在这里插入图片描述
生成设备节点成功
执行应用程序
在这里插入图片描述
可以看到LED闪烁
实验成功

总结

以前在单片机上点灯就是最简单的操作,但是上了ARM+Linux,发现点灯也并不容易,学了这么久了终于把灯点着了
学习驱动的过程中很大一部分时间是用于熟悉库函数的使用
写驱动一般性的方法都是先了解和掌握对应驱动相关的库函数
在掌握驱动库函数的基础上,掌握Linux架构,驱动就自然写出来或者很容易就移植成功

发布了123 篇原创文章 · 获赞 598 · 访问量 34万+

猜你喜欢

转载自blog.csdn.net/a568713197/article/details/89735042