传统的MCU直接操作寄存器,主要原因是本身的主频并不太高
而现代的CPU由于速度远快于存储设备所以通过Cache、内存来进行缓冲,解决速度不匹配的问题
MMU是中央处理器中用来管理虚拟存储器、物理存储器的控制线路,同时负责虚拟地址映射为物理地址
为解决代码需要的内存大于物理内存的问题
基本思路:数据和堆栈的大小总和可以超过物理存储器的大小,操作系统把当前使用的部分留在内存中,其它的保留在硬盘上
地址范围
页和页帧
一台32位机器最大可运行4G大小的程序,前提是这台机器首先要有4G的存储空间
iRAM
i-Cache
SFR region
Memory of Dynamic Memory Controller
虚拟地址指的是一种通信技术
虚拟地址的返回和内存的物理地址会重合
Base Address
Offset
总线的地址
查看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",//程序员需要关心的标志
},
#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数量:EXYNOS4_GPIO_L2_NR
也可以通过手册来查找
#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宏定义,并将宏定义装入chip结构体中
通过调用gpio-cfg.h中的s3c_gpio_cfgpin函数,用来给GPIO做配置
Linux中申请GPIO的头文件
三星平台的GPIO配置函数头文件
三星平台EXYNOS系列平台,GPIO配置参数宏定义头文件
arch/arm/plat-samsung/include/plat/gpio-cfg.h
#include
#include
三星平台4412平台,GPIO宏定义头文件
arch/arm/mach-exynos/include/mach/gpio-exynos4.h
#include
包括4412处理器所有的GPIO的宏定义
LinuxGPIO申请函数和赋值函数
三星平台配置GPIO函数
GPIO配置输出模式的宏变量
在核心板原理图中查找芯片引脚
所以我们要操作GPL2_0,当它输出高定平的时候LED亮,当它输出低电平的时候LED灭。
#include
#include
/*driver register*/
#include
/*注册杂项设备头文件*/
#include
/*注册设备节点的文件结构体*/
#include
/*Linux中申请GPIO的头文件*/
#include
/*三星平台的GPIO配置函数头文件*/
/*GPIO配置参数宏定义头文件*/
#include
#include
/*三星平台4412平台,GPIO宏定义头文件*/
#include
#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
#include
#include
#include
#include
#include
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架构,驱动就自然写出来或者很容易就移植成功