從零開始之驅動發開、linux驅動(三十一、framebuffer中對mmap使用)
阿新 • • 發佈:2018-11-19
前面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;
}