从零开始之驱动发开、linux驱动(三十一、framebuffer中对mmap使用)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_16777851/article/details/83716640

前面framebuffer章节我们了解了通过write函数来对fremebbuffer中的显存写数据的方式。

在开始分析mmap之前我们再次回顾一下fb_write函数


static ssize_t
fb_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
	unsigned long p = *ppos;                          //显存起始地址的偏移
	struct inode *inode = file->f_path.dentry->d_inode;
	int fbidx = iminor(inode);                        //得到次设备号
	struct fb_info *info = registered_fb[fbidx];      //得到设备信息
	u32 *buffer, *src;
	u32 __iomem *dst;
	int c, i, cnt = 0, err = 0;
	unsigned long total_size;

	if (!info || !info->screen_base)        /* 参数有效性判断 */
		return -ENODEV;

	if (info->state != FBINFO_STATE_RUNNING)
		return -EPERM;

    /* 设备信息中的fb_ops中如果有定义fb_wrute函数,则调用具体驱动中的,不使用通用的(三星的都输通用的,所以这里是没定义的) */
	if (info->fbops->fb_write)
		return info->fbops->fb_write(info, buf, count, ppos);
	
	total_size = info->screen_size;        /* 显存大小 */

	if (total_size == 0)
		total_size = info->fix.smem_len;    /* 显存为0,则使用fb帧长度作为显存大小 */

	if (p > total_size)                    /* 偏移不能超过显存大小 */
		return -EFBIG;

	if (count > total_size) {               /* 要写的长度大于显存显存长度,则只写显存大小内容 */
		err = -EFBIG;
		count = total_size;
	}

	if (count + p > total_size) {           /* 偏移+要写的字节,不能超过显存大小 */
		if (!err)
			err = -ENOSPC;

		count = total_size - p;            /* 超出显存大小,则把超出的丢弃 */
	}

    /* 要写的内容大于1页(0x1000),则申请一页空间,否则申请需要的字节数
       (这里主要作为中间,用来把用户空间拷贝过来的数据做个暂存) */
	buffer = kmalloc((count > PAGE_SIZE) ? PAGE_SIZE : count,
			 GFP_KERNEL);
	if (!buffer)
		return -ENOMEM;

    /* 显存基址+偏移 ---> 真正数据要写的显存开始地址 */
	dst = (u32 __iomem *) (info->screen_base + p);

	if (info->fbops->fb_sync)            /* 是都需要同步,三星没这个函数 */
		info->fbops->fb_sync(info);


    /* 要写的大小大于一页,则通过buffer 多次把用户空间的数据拷贝到内核空间,之后再通过
        fb_writel和fb_writeb函数写入显存(要写的内容小于1页,那一次就可以写完了)
     */
	while (count) {
		c = (count > PAGE_SIZE) ? PAGE_SIZE : count;        
		src = buffer;

		if (copy_from_user(src, buf, c)) {
			err = -EFAULT;
			break;
		}

		for (i = c >> 2; i--; )
			fb_writel(*src++, dst++);

		if (c & 3) {
			u8 *src8 = (u8 *) src;
			u8 __iomem *dst8 = (u8 __iomem *) dst;

			for (i = c & 3; i--; )
				fb_writeb(*src8++, dst8++);

			dst = (u32 __iomem *) dst8;
		}

		*ppos += c;
		buf += c;
		cnt += c;
		count -= c;
	}

    /* 释放掉暂存数据的空间 */
	kfree(buffer);

    /* 返回成功写入的数据字节数 */
	return (cnt) ? cnt : err;
}

这里我们看到了,数据的传输需要两个过程

1.从用户空间拷贝数据到内核空间copy_from_user

2.把从用户空间拷贝的数据写入显存中fb_writel

这对fb设备特别是分辨率很高,数据量非常大的情况,效率是非常低的。

而前两节我们学过的mmap就非常好的解决了这个问题。

原理以及说过,这里我们就直接看函数实现。


static int
fb_mmap(struct file *file, struct vm_area_struct * vma)
{
	struct fb_info *info = file_fb_info(file);
	struct fb_ops *fb;
	unsigned long mmio_pgoff;
	unsigned long start;
	u32 len;

	if (!info)
		return -ENODEV;
	fb = info->fbops;
	if (!fb)
		return -ENODEV;
	mutex_lock(&info->mm_lock);
	if (fb->fb_mmap) {
		int res;
		res = fb->fb_mmap(info, vma);        /* 具体驱动中有定义,则优先用驱动里面的 */
		mutex_unlock(&info->mm_lock);
		return res;
	}

	/*
	 * Ugh. This can be either the frame buffer mapping, or
	 * if pgoff points past it, the mmio mapping.
	 */
	start = info->fix.smem_start;            /* 显存起始地址(这个是物理地址) */
	len = info->fix.smem_len;                /* 显存大小 */
    /* 计算显存起始地址对应的那个物理页 */
	mmio_pgoff = PAGE_ALIGN((start & ~PAGE_MASK) + len) >> PAGE_SHIFT;
    /* 用户传过来的比实际物理页小,则有问题(因为实际的已经是从显存起始地址开始了) */
	if (vma->vm_pgoff >= mmio_pgoff) {
		if (info->var.accel_flags) {
			mutex_unlock(&info->mm_lock);
			return -EINVAL;
		}

		vma->vm_pgoff -= mmio_pgoff;
		start = info->fix.mmio_start;
		len = info->fix.mmio_len;
	}
	mutex_unlock(&info->mm_lock);

    /* 得到页的访问模式权限(读/写/可执行/私有) */
	vma->vm_page_prot = vm_get_page_prot(vma->vm_flags);
	fb_pgprotect(file, vma, start);       /* 页额访问权限增加一种,不使用cache使用write buffer*/

    /* vma时用户空间的一块分配的空间,statr是要映射的物理地址,len是要映射的长度 */
    /* 即把下面物理地址起始的一块空间映射带用户空间 */
	return vm_iomap_memory(vma, start, len);     /* 映射页io */
}

增加访问权限

static inline void fb_pgprotect(struct file *file, struct vm_area_struct *vma,
				unsigned long off)
{
	vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);   /* 在原来的权限基础上增加write buffer功能 */
}


#define pgprot_writecombine(prot) \
	__pgprot_modify(prot, L_PTE_MT_MASK, L_PTE_MT_BUFFERABLE)


#define __pgprot_modify(prot,mask,bits)		\
	__pgprot((pgprot_val(prot) & ~(mask)) | (bits))

/* 强制类型转换 */
#define pte_val(x)      ((x).pte)
#define pmd_val(x)      ((x).pmd)
#define pgd_val(x)	((x).pgd[0])
#define pgprot_val(x)   ((x).pgprot)

#define __pte(x)        ((pte_t) { (x) } )
#define __pmd(x)        ((pmd_t) { (x) } )
#define __pgprot(x)     ((pgprot_t) { (x) } )

/**
 * vm_iomap_memory - remap memory to userspace
 * @vma: user vma to map to
 * @start: start of area
 * @len: size of area
 *
 * This is a simplified io_remap_pfn_range() for common driver use. The
 * driver just needs to give us the physical memory range to be mapped,
 * we'll figure out the rest from the vma information.
 *
 * NOTE! Some drivers might want to tweak vma->vm_page_prot first to get
 * whatever write-combining details or similar.
 */
int vm_iomap_memory(struct vm_area_struct *vma, phys_addr_t start, unsigned long len)
{
	unsigned long vm_len, pfn, pages;

	/* Check that the physical memory area passed in looks valid 检查不要越界 */
	if (start + len < start)
		return -EINVAL;
	/*
	 * You *really* shouldn't map things that aren't page-aligned,
	 * but we've historically allowed it because IO memory might
	 * just have smaller alignment.
	 */
	len += start & ~PAGE_MASK;        /* 得到结束地址 */
	pfn = start >> PAGE_SHIFT;        /* 得到显存物理页号 */
	pages = (len + ~PAGE_MASK) >> PAGE_SHIFT;    /* 得到显存页数 */
	if (pfn + pages < pfn)
		return -EINVAL;

	/* We start the mapping 'vm_pgoff' pages into the area */
	if (vma->vm_pgoff > pages)        /* 用户空间传过来的起始页的偏移必须在范围内 */
		return -EINVAL;
	pfn += vma->vm_pgoff;            /* 原始基物理页号+用户空间传过来偏移页数 = 显存起始物理页号 */
	pages -= vma->vm_pgoff;          /* 显存页数 - 用户空传来的偏移页数 = 剩下最多可以映射的页数 */

	/* Can we fit all of the mapping? */
	vm_len = vma->vm_end - vma->vm_start;        /* 用户空间映射大小 */
	if (vm_len >> PAGE_SHIFT > pages)            /* 用户空间要求映射的页不能大于实际显存可映射的页 */
		return -EINVAL;

	/* Ok, let it rip */
    /* 把用户空间虚拟地址vma->vm_start开始的vm_len长度映射到物理内存pfn页开始的内存 */
	return io_remap_pfn_range(vma, vma->vm_start, pfn, vm_len, vma->vm_page_prot);
}



#define io_remap_pfn_range remap_pfn_range

可见使用了mmap对lcd的显存进行了映射以后,除了这里增加了对页表的一次操作以外。对显存写数据可以直接使用写地址方式操作。

和write相比没有数据的拷贝拷贝,这样可以大大的提高运行效率。

下面简单举例使用mmap来映射,并把lcd显示器的背景刷成红色。

#include <stdio.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdlib.h>
 
int main(int argc, char *argv[])
{
    int fd = -1; 
    int i;
    unsigned int *mmaped = NULL;
 
 
 
    fd = open("/dev/fb0", O_RDWR);
    if (fd < 0) {
        fprintf(stderr, "open fb0 fail\n");
        exit(-1);
    }   
 
 
    /* 将文件映射至进程的地址空间 */
    mmaped = (unsigned int *)mmap(NULL, 1024*600*4, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); 
    if (mmaped == (unsigned int*)-1) {
        fprintf(stderr, "mmap fail\n");
        close(fd);
    
        return -1; 
    }   

    /* 映射完后, 关闭文件也可以操纵内存 */
    close(fd);

    /* 刷全屏红色背景 */
    for(i = 0; i < (1024*600); i++)
        mmaped[i] = 0x00ff0000;

    /* 同步mmap映射的文件从内存写到硬盘文件中 */
    msync(mmaped, 1024*600*4, MS_SYNC);


    return 0;
}
          

猜你喜欢

转载自blog.csdn.net/qq_16777851/article/details/83716640
今日推荐