通俗易懂:mmap与硬件寄存器的关系

分析应用程序获取按键操作的流程

分析应用程序控制LED灯的操作流程

 

按键:按键操作,触发中断,读取硬件寄存器,获取按键状态,唤醒休眠的进程,read操作调用copy_to_user将内核的缓冲区数据拷贝到用户缓冲区

总结:第一次拷贝:从硬件寄存器读数读到内核缓冲区

      第二次拷贝:从内核缓冲区到用户缓冲区

 

灯:第一次拷贝:用户缓冲区到内核缓冲区,

    第二次拷贝:从内核缓冲区到硬件寄存器

总结:以上两个驱动都要经历两次的数据拷贝,如果数据量非常小,基本对系统的性能没有影响,如果进行大数据量的处理,比如视频数据采集卡,还要经过这两次拷贝,无形之中降低了系统的性能。

 

简单点来说:我们都知道,我们都是基于linux操作系统下编程的,凡是都要经过内核这一层,无论进行什么操作,都要经过内核,如果应用程序想要访问物理硬件,并不是直接访问,比如有一个应用程序想要读取硬件上的信息,那么硬件并不是把数据直接给应用程序,而是先把数据被内核,然后内核再把数据转交给应用程序,也就是说,内核起到了中转的作用,那么有些时候,这样做的话,就会降低系统的性能,为什么这样说呢?我打个比方,我们如果外接了个摄像头之类的视频器件,像视频这么大的数据量,如果应用程序想要读取外部视频这个硬件的话,如果按照以往的方式,就会先把几百兆的视频数据先交给内核,然后内核在转交给应用程序,这样的话如果视频是2m的数据,这样一搞,就会产生4m的数据。系统明线降低了性能,所以,有些时候,需要让应用程序直接访问操作硬件,那么这就是本帖将要解决的问题,我们将使用mmap/select/poll函数把物理地址映射到进程内地址空间内,让应用程序直接访问硬件。此时内核有一个机制:mmap,mmap实现的原理就是将硬件的相关内容映射到当前用户进程的某一个虚拟地址空间里去,以后应用程序访问这个虚拟地址就是在操作对应的物理地址信息。

 

回顾以前UNIX的api编程:

void *gpio_base;

int fd=open("a.txt,O_RDWR");

gpio_base = mmap(NULL,映射的大小,读写权限,fd,0);

这样在用户空间就得到了一个虚拟地址gpio_base,以后在用户空间操作这个虚拟地址就是在操作这个文件

 

问:应用程序调用mmap,底层内核驱动是否有对应的mmap接口?

答:在struct file_operations里有

 

问:底层驱动的mmap作何事?

答:以前驱动在操作硬件寄存器都是将物理地址直接映射到内核的某一个虚拟地址(3g-4g),现在使    用mmap就是将这个物理地址映射到当前进程的某一个虚拟地址(0-3G),所以底层的mmap接口就    是完成物理地址和虚拟地址的映射,建立页表。

 

当应用程序调用mmap系统调用时,内核在当前进程的虚拟地址空间里找到一个内存区域来存放mmap操作的地址信息,并且为这块特殊的内存区域创建一个结构体来描述mmap使用的虚拟地址信息(起始地址,结束地址,读写访问权限等),那么底层的mmap接口的第二个参数为这个结构体指针,底层mmap只需通过这个指针得到对应进程的虚拟地址然后和物理地址建立映射关系即可。

 

struct vm_area_struct {

unsigned long vm_start;//应用程序调用mmap的返回值保存在这个字段

unsigned long vm_end;//结束地址

pgprot_t vm_page_prot;//读写权限

。。。

}

 

底层驱动mmap:

1.首获取物理地址

2.通过mmap的第二个形参获取用户进程的虚拟地址

  vma.vm_start

3.目的就是建立1,2两个地址的映射

  调用remap_pfn_range函数建立映射即可

  

例子:改造led驱动,用mmap接口来实现led驱动

 

______________________________________________________________________________________

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

 

//定义硬件私有结构体

struct led_hw_priv {

char *name;

int productid;

};

 

//定义驱动私有结构体

struct led_sf_priv {

struct cdev led_cdev;

unsigned long phy_addr;

int major;

struct class *cls;

struct led_hw_priv *pled_hw;

};

 

//初始化硬件相关的信息

static struct led_hw_priv led_info = {

.name = "Tarena",

.productid = 0x654321

};

 

//初始化硬件资源结构

static struct resource led_res[] = {

[0] = {

.start = 0xe0200000, //页对齐

.end = 0xe0200000 + 8 - 1,

.flags = IORESOURCE_MEM

}

};

 

static void led_release(struct device *dev)

{

printk("%s\n", __FUNCTION__);

}

 

//初始化platform_device

static struct platform_device led_dev = {

.name = "myled",

.id = -1,

.dev = {

.release = led_release,

.platform_data = &led_info

},

.num_resources = ARRAY_SIZE(led_res),

.resource = led_res

};

 

static int led_open(struct inode *inode,struct file *file)

{

struct led_sf_priv *pled = container_of(inode->i_cdev, struct led_sf_priv, led_cdev);

 

file->private_data = pled;

 

printk("Device Name is %s, Device ProductId = %#x\n",pled->pled_hw->name,pled->pled_hw->productid);

return 0;

}

 

static int led_mmap(struct file *file,struct vm_area_struct *vma)

{

struct led_sf_priv *pled = file->private_data;

int vmasize = vma->vm_end - vma->vm_start;

 

//修改属性,保证数据的一致性,关闭cache

vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);

   remap_pfn_range(vma, vma->vm_start, pled->phy_addr >> 12, vmasize,vma->vm_page_prot);

return 0;

}

 

static struct file_operations led_fops = {

.owner = THIS_MODULE,

.open = led_open,

.mmap = led_mmap

};

 

static int led_probe(struct platform_device *pdev)

{

struct resource *res = NULL;

dev_t dev_id;

struct led_sf_priv *pled = kzalloc(sizeof(struct led_sf_priv), GFP_KERNEL);

if (pled->major) {

dev_id = MKDEV(pled->major, 0);

register_chrdev_region(dev_id, 1, "led");

} else {

alloc_chrdev_region(&dev_id, 0, 1, "led");

pled->major = MAJOR(dev_id);

}

cdev_init(&pled->led_cdev, &led_fops);

cdev_add(&pled->led_cdev, dev_id, 1);

pled->cls = class_create(THIS_MODULE, "led");

device_create(pled->cls, NULL, dev_id, NULL, "myled");

 

pled->pled_hw = pdev->dev.platform_data;

res = platform_get_resource(pdev, IORESOURCE_MEM,0);

pled->phy_addr = res->start;

 

dev_set_drvdata(&pdev->dev, pled);

return 0;

}

 

static int led_remove(struct platform_device *pdev)

{

struct led_sf_priv *pled = dev_get_drvdata(&pdev->dev);

 

dev_t dev_id = MKDEV(pled->major, 0);

device_destroy(pled->cls, dev_id);

class_destroy(pled->cls);

cdev_del(&pled->led_cdev);

unregister_chrdev_region(dev_id, 1);

kfree(pled);

return 0;

}

 

//初始化platform_driver

static struct platform_driver led_drv = {

.driver = {

.name = "myled",

.owner = THIS_MODULE,

},

.probe = led_probe,

.remove = led_remove

};

 

static int led_init(void)

{

//注册platform_device

platform_device_register(&led_dev);

//注册platform_driver

platform_driver_register(&led_drv);

return 0;

}

 

static void led_exit(void)

{

platform_device_unregister(&led_dev);

platform_driver_unregister(&led_drv);

}

 

module_init(led_init);

module_exit(led_exit);

MODULE_LICENSE("GPL v2");

 

______________________________________________________________________________________

以上代码就是将led灯所在接口的寄存器的地址映射到进程空间的虚拟地址的过程,下面开始编写app

______________________________________________________________________________________

#include

#include

#include

#include

#include

#include

 

int main(int argc, char *argv[])

{

int fd;

unsigned char *gpio_base;

unsigned long *gpiocon;

unsigned long *gpiodat;

 

if (argc != 2) {

printf("usage:\n%s \n", argv[0]);

return -1;

}

 

fd = open("/dev/myled", O_RDWR);

if (fd < 0) {

printf("open led failed.\n");

return -1;

}

 

gpio_base = mmap(NULL, 0x1000, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);

//现在gpio_base这个虚拟地址就是寄存器所在的地址

gpiocon = (unsigned long *)(gpio_base + 0x80);

gpiodat = (unsigned long *)(gpio_base + 0x84);

 

//配置GPIO为输出

*gpiocon &= ~(0xf << 12);

*gpiocon |= (1 << 12);

 

if (strcmp(argv[1], "on") == 0)

*gpiodat |= (1 << 3);//开灯操作

else

*gpiodat  &= ~(1 << 3);//关灯

 

close(fd);

munmap(gpio_base, 0x1000);//这一步当然就是解除映射啦\(^o^)/~

 

return 0;

}

 

______________________________________________________________________________________

 

现在的手机的其实就是ARM处理器加外部各种模块为核心实现的,比如GPS模块,蓝牙模块等等。比如GPS模块和s5pv210通信的话,是采用串口通信,wifi模块和s5pv210通信是采用网络通信,也就是网络套接字。我们驱动程序做的就是利用内核提供的底层接口函数来操作这些模块。然后应用程序调用驱动程序提供的函数来实现工作。现在我们以安卓手机为例,但凡手机都是嵌入式设备。比如说一个安卓手机上面安装了一款应用软件,比如这个应用程序是QQ2013这个软件,你想想看,QQ是不是支持WIFI功能?在搜寻附近好友的时候,是不是得要启动手机内置的GPS模块?在用户要聊天打字时,是不是要操作按键?等等,其实以上小编讲的那些都是应用程序操作多个硬件设备的典型例子,现在如果说有这样一个应用软件,要同时操作GPS(通过串口),按键,和wifi网络(通过socket)

首先是不是得先要打开设备?所以先用open函数打开GPS的设备文件和其他驱动程序的设备文件:

int fd1 = open("/dev/GPS",O_RDWR);//打开GPS定位模块的设备文件来访问GPS驱动程序

int fd2 = open("/dev/button",O_RDWR);//打开wifi模块的设备文件

int fd3 = socket(...);//打开其他模块

打开完设备之后,接下来读取三个设备的数据,你想怎样读取?现在有三种方案:

方案1:

主进程:

while(1){

read(fd1,buf,size);//读取GPS卫星传来的定位数据

read(fd2,buf,size);//读取键盘按键数据

read(fd3,buf,sise);//读取wifi网络数据包

}

以上的方案是采用轮询方式依次读取数据,大家想想看,如果这个时候cpu执行到读取GPS的时候,来了一个wifi网络数据包,这个时候是不是就会因为没有读取到wifi数据而发生丢包?所以这种方案在实际开发显然是不行的,不然的话,你用qq聊天,可能有时候就会发生在使用腾讯提供的搜寻周围好友服务的时候,别人正好发qq消息给你,而这个时候,cpu正在执行读取GPS数据而来不及读取网络数据包,就会照成你没有接受到别人发的qq消息。所以这样是不行的。

 

方案2:

针对方案1这种串行实现的问题,可以为每一个设备的读取操作创建一个子进程或者线程来轮询的读取操作,来实现并行。虽然能够解决并行问题,但随着设备数量的增加,无形会浪费很多的系统资源

 

方案3:下一节中出现------>>>>使用selec/poll这个函数来实现IO多路监听。而这个函数就是下节的内容。

 

因为使用selec/poll来实现同时操作多路数据(GPS,WIFI,蓝牙等等)就不会发生因为各种问题而导致的丢失数据的可能。。。。

你可能感兴趣的:(通俗易懂:mmap与硬件寄存器的关系)