1. 程式人生 > >從零開始之驅動發開、linux驅動(三十一、framebuffer中對mmap使用)

從零開始之驅動發開、linux驅動(三十一、framebuffer中對mmap使用)

前面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;
}