內核中與驅動相關的內存操作之十七(DMA)

1.DMA的宏觀理論:

    DMA,即Direct Memory Access,直接內存訪問.主要是考慮到RAM和外設之間拷貝大量數據時提升性能的一種硬件策略.數據交互,需要一個數據的源地址和目的地址,類似memcpy()函數.DMA也不能例外,它也必須提供源、目的寄存器,只不過其內部是靠硬件實現的從而達到更高效的數據交互目的而已.這裏可以假想DMA是一種數據傳輸過程中的"橋樑",DMA的源寄存器存放的就是我們要拷貝的數據的開始位置地址,DMA的目的寄存器存放的我們要拷貝數據的目的地址.這樣硬件DMA就幫助我們高效完成了數據的傳輸.

    下面是DMA操作相關的僞代碼:

static void DmaCpy(char *src,char *dest)
{
	DmaSrcReg = src;
	DmaDestReg = dest;
	CfgDmaRegs();
}
    比如,我們通過DMA來播放音頻文檔,其中音頻數據源存放在數組ArrayWav,我們要實現播放就需要把這音頻數組裏面的音頻數據丟到IISFIFO裏面去.如下:

static void PlayWav(char *wav)
{
    ... ...;
    DmaCpy(wav,iisfiforegs);
    ... ...;
}
    由於DMA是硬件實現的,而且不佔用CPU資源,因此,其效率是很高的.一般CPU都提供了幾路DMA通道,例如S3C2440就提供了四路DMA Channel.


2.內核中DMA相關:

    DMA屬於系統級別的資源,它只是提供一種設備I/O和RAM的硬件數據交互的手段,這裏需求注意的是,它不屬於類似字符設備、塊設備、網絡設備這樣的外設.這樣的外設一般都有一定的功能暴露給用戶空間去操作,而DMA都是在默默無聞在內核空間協助完成外設到RAM的數據交互.它相當於CPU的一個助手,它是因爲CPU本身的性能而存在,而不是因爲用戶的功能而存在的.

    也就是說,DMA本身不會有像網卡、串口一樣有"常規驅動",它的存在,只是給需要用到DMA傳輸數據的外設提供了一種更高效的手段而已.當然,具體的外設驅動可以選擇不用DMA的.

    

    2-1.DMA和Cache的一致性:

    如果驅動編程中需要用到DMA進行數據交互的話,必須注意DMA和Cache的一致性問題.Cache,即CPU內置的緩存,其功能類似外設RAM,但是Cache被CPU訪問更快,它緩存的是最近CPU訪問過的指令或數據.DMA傳輸數據的過程中CPU是不知道的,如果DMA的目的地址和Cache有重疊的時候,Cache裏面的數據內容發生變化了,而CPU並不知道,它還是把Cache裏面的數據當作RAM中的數據.這樣一來,同樣一個數據,可能即存在於Cache中,也可能存在於主存RAM中.如果二者並不形成同步邏輯的話,將會引發驅動無法使用.


    2-2.DMA緩衝:

    內存中與外設交互數據的一塊區域叫做"DMA緩衝區",即具有DMA能力的內存區域.比如ISA設備,其最前16M的內存區域是具有DMA能力的.DMA緩衝區是內核中實現DMA數據傳輸有着重要的地位--無論是通過DMA發數據還是通過DMA收數據,DMA緩衝都是必須的"中轉環節".

    

    2-2-1.通過DMA往外發數據的流程:

    我們需要通過DMA往外傳輸數據的時候,大體流程如下:

申請DMA緩衝區
-->
把目標數據(可以是用戶空間的數據通過mmap映射下來的)存放到DMA緩衝區
-->
把目標DMA緩衝區的地址轉換爲設備在總線上的地址
-->
把設備的總線地址告訴DMA,並開啓DMA控制器使能.DMA控制器便開始自動傳輸
-->
DMA傳輸完畢後,發出一箇中斷告訴CPU.

    2-2-2.通過DMA接收數據的流程:

    設備到主控的數據通過DMA傳輸過程(同步):

1. 當一個進程調用 read, 驅動方法分配一個 DMA 緩衝並引導硬件來傳輸它的數據到那個緩衝. 這個進程被置爲睡眠.

2. 硬件寫數據到這個 DMA 緩衝並且在它完成時引發一箇中斷.

3. 中斷處理獲得輸入數據, 確認中斷, 並且喚醒進程, 它現在可以讀數據了.
    此機制只用到了單一的DMA緩衝.

    設備到主控的數據通過DMA傳輸過程(異步):

1. 硬件引發一箇中斷來宣告新數據已經到達.

2. 中斷處理分配一個DMA緩衝並且告知硬件在哪裏傳輸數據.

3. 外設寫數據到緩衝並且引發另一箇中斷當完成時.

4. 處理者分派新數據, 喚醒任何相關的進程, 並且負責雜務.

    此機制用到了DMA緩衝隊列.

    例如網卡常常期望見到一個在內存中和處理器共享的環形緩衝(常常被稱爲一個 DMA 的緩衝); 每個到來的報文被放置在環中下一個可用的緩衝, 並且發出一箇中斷. 驅動接着傳遞網絡本文到內核其他部分並且在環中放置一個新 DMA 緩衝.

    在所有這些情況中的處理的步驟都強調,有效的 DMA 處理依賴中斷報告.雖然可能實現 DMA 使用一個輪詢驅動,它不可能有意義,因爲一個輪詢驅動可能浪費 DMA 提供的性能益處超過更容易的處理器驅動的I/O.


    2-3.分配DMA緩存:

    當我們在驅動中需要用到DMA來實現DMA數據的傳輸時,可以通過函數kmalloc()或get_free_pages()並設置GFP_DMA標誌來獲取一塊具有DMA能力的內存緩衝區.更可讀更明瞭的API如下:

static unsigned long dma_mem_alloc(int size)
{
	int order = get_order(size);
	return __get_dma_pages(GFP_KERNEL, order);
}    

    [附:]有時候我們獲取比較大的內存區域(如幾MB)或者比較小的內存區域(如遠小於128KB)時,大的容易導致內存獲取失敗,小的容易產生內存碎片.因此,當內核無法返回請求數量的內存或者當你需要多於 128 KB時,一個替代返回 -ENOMEM 的做法是在啓動時分配內存或者保留物理 RAM 的頂部給你的緩衝. 保留 RAM 的頂部是通過在啓動時傳遞一個 mem= 參數給內核實現的. 例如, 如果你有 256 MB, 參數 mem=255M 使內核不使用頂部的1MByte. 你的模塊可能後來使用下列代碼來獲得對這個內存的存取:

dmabuf = ioremap (0xFF00000 /* 255M */, 0x100000 /* 1M */); 

    基於DMA的硬件使用的是總線地址.而我們申請的DMA緩存地址是內核需要的虛擬地址.因此,需要藉助下面兩個API完成兩者的轉換:

unsigned long virt_to_bus(void *address)
void *bus_to_virt(unsigned long address)


    2-4.DMA映射:

    我們通過上述可以知道,使用DMA來傳輸數據包括下面幾個方面:考慮Cache的一致性、分配DMA緩衝區、地址轉換.其中,這些步驟均可由DAM映射來完成.

    DMA映射的的主要工作是:

1).分配一塊DMA緩衝;
2).爲此DMA緩衝產生設備可訪問的地址;
3).保證此DMA緩衝Cache的一致性.    

    DMA映射方式主要有三種:一致性DMA映射、流DMA映射、SGDMA映射.

   2-4-1.一致性DMA映射: 

    分配一個DMA一致性的內存區域:

void *dma_alloc_coherent(struct device *dev,size_t size,dma_addr_t *handle,gfp_t gfp);
    返回的就是申請到的DMA緩衝區的虛擬地址;參數handle返回的是DMA緩衝區的總線地址.此外,此函數還保證了該DMA緩衝區Cache的一致性.

    釋放一個DMA一致性的內存區域:

void dma_free_coherent(struct device *dev,size_t size,void *cpu_addr,dma_addr_t handle);
    2-4-2.流DMA映射:

    流DMA映射,處理對象爲"單個已經分配好的緩衝(比如uboot傳遞的時候,256M內存傳遞mem = 250MB,保留出最高端的5M通過ioremap來使用這塊預留內存緩衝)".使用下面的函數來建立流DMA映射:

dma_addr_t dma_map_single(struct device *dev,void *buffer,size_t size,enum dma_data_direction direction);
    如果成功,返回的是總線地址,否則返回NULL.第二個參數即爲已經分配好的緩衝,第三個參數爲緩衝大小,第四個參數爲方向,可以是DMA_TO_DEVICE、DMA_FROM_DEVICE、DMA_BIDIRECTIONAL和DMA_NONE.

    解除流DMA映射:    

void dma_unmap_single(struct device *dev,dma_addr_t dma_addr,size_t size,enum dma_data_direction direction);
    流式DMA映射可能存在競爭的情況,可以通過下面的函數獲取DMA緩衝區的擁有權:

void dma_sync_single_for_cpu(struct device *dev,dma_handle_t bus_addr,size_t size,enum dma_data_direction direction);
    訪問完DMA緩衝區後,應該將其返給設備:
void dma_sync_single_for_device(stryct devuce *devmdna_handle_t bus_addr,size_t size,enum dma_data_direction direction);
    2-4-3.SGDMA映射:

    SGDMA映射屬於流式DMA,只不過流式DMA是單個緩衝,而SGDMA是隊列緩衝.如果一個設備需要較大的DMA緩衝區並支持SG模式的情況下,申請多個不連續的、相對較小的DMA緩衝區通常是防止申請太大的連續的物理空間的一種有效的策略.

    建立SGDMA映射通過下面的API:

int dma_map_sg(struct device *dev,struct scatterlist *sg,int nets,enum dma_data_direction direction);
    參數nets爲散列表(scatterlist)入口的數量,參數sg爲DMA緩衝隊列的頭,其組織最基本的單元爲scatterlist,第4個參數爲方向.函數返回值爲DMA緩衝區的數據,它可能小於nets.既然有了DMA緩衝區,那麼,我們需要的總線地址呢?我們知道,通過上述的API產生的DMA緩衝被組織在sg參數裏面,其類似是scatterlist,裏面就包含了DMA數據傳輸相關的所有信息,當然,總線地址也包含其中.如下:

struct scatterlist {
	unsigned long	page_link;
	unsigned int	offset;		/* buffer offset		 */
	dma_addr_t	dma_address;	/* dma address			 */
	unsigned int	length;		/* length			 */
};

    其中,成員dma_address就是總線地址.成員length標記了scatterlist對應的緩衝區的長度.可以通過下面兩個內核API返回這兩成員:

dma_addr_t sg_dma_address(struct scatterlist *sg);
unsigned int sg_dma_len(struct scatterlist *sg);
    釋放SGDMA映射:
void dma_unmap_sg(struct device *dev,struct scatterlit *list,int nents,enum dma_data_direction direction);
    SGDMA也屬於流DMA,存在着競爭的情況,因此在訪問之前,需要先獲取相應的DMA緩衝區的所有權:
void dma_sync_sg_for_cpu(struct device *dev,struct scatterlist *sg,int nents,enum dma_data_direction direction);
    訪問結束後,通過下列函數將所有權返回設備:
void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg,int nents, enum dma_data_direction direction);

3.設備使用DMA傳輸數據的流程:

    設備驅動要使用DMA,先向系統申請DMA通道,通過下面API實現:

int request_dma(unsigned int dmanr,const char *device_id);
     參數dmanr是DMA通道編號,參數device_id往往傳入設備結構體struct device.

    使用完DMA通道後,通過下面函數釋放該通道:

void free_dma(unsigned int dmanr);
    因此,在LINUX設備驅動中設備要使用DMA來傳輸數據的時候,流程如下:

request_dma()並初始化DMA控制器;
-->
申請DMA緩衝區;
-->
進行DMA傳輸;
-->
若全能了對應的中斷,進行DMA傳輸後的中斷處理;
-->
釋放DMA緩衝區
-->
free_dma()
  
4.DMA控制器的一個示例:

    下面以DMA控制器8237爲例看DMAC(DMA控制器端)的操作:

/* 8237 DMA 控制器 */
#define IO_DMA1_BASE 0x00	/* 8 位從 DMA,0~3 通道 */
#define IO_DMA2_BASE 0xC0	/* 16 位主 DMA,4~7 通道 */


/* DMA 控制器 1 控制寄存器 */
#define DMA1_CMD_REG 0x08	/* 命令寄存器 (w) */
#define DMA1_STAT_REG 0x08	/* 狀態寄存器 (r) */
#define DMA1_REQ_REG 0x09	/* 請求寄存器 (w) */

#define	DMA1_MASK_REG	0x0A		/* 單通道屏蔽 (w) */
#define	DMA1_MODE_REG	0x0B		/* 模式寄存器 (w) */
#define	DMA1_CLEAR_FF_REG	0x0C	/* 清除 flip-flop (w) */
#define	DMA1_TEMP_REG	0x0D	/* 臨時寄存器 (r) */
#define	DMA1_RESET_REG	0x0D	/* 主清除 (w) */
#define	DMA1_CLR_MASK_REG	0x0E	/* 清除屏蔽 */
#define	DMA1_MASK_ALL_REG	0x0F	/* 所有通道屏蔽 */
/* DMA 控制器 2 控制寄存器 */	
#define DMA2_CMD_REG	0xD0	/* 命令寄存器 (w) */
...				/*類似於 DMA 控制器 1*/

/* DMA0~7 通道地址寄存器 */
#define DMA_ADDR_0 	0x00
                ... 
/* DMA0~7 通道計數寄存器 */ 
#define DMA_CNT_0	0x01
                ... 
/* DMA0~7 通道頁寄存器 */ 
#define DMA_PAGE_0	0x87 
... 
#define DMA_MODE_READ	0x44
#define DMA_MODE_WRITE	0x48
...

/*使能 DMA 通道*/
static _ _inline_ _ void enable_dma(unsigned int dmanr)
{
	if (dmanr <= 3)
		dma_outb(dmanr, DMA1_MASK_REG);
	else
		dma_outb(dmanr &3, DMA2_MASK_REG);
}

/*禁止 DMA 通道*/
static _ _inline_ _ void disable_dma(unsigned int dmanr)
{
	if (dmanr <= 3)
		dma_outb(dmanr | 4, DMA1_MASK_REG);
	else
		dma_outb((dmanr &3) | 4, DMA2_MASK_REG);
}

/* 設置傳輸尺寸(通道 0~3 最大 64KB, 通道 5~7 最大 128KB */
static _ _inline_ _ void set_dma_count(unsigned int dmanr, unsigned int count)
{
	count--;
	if (dmanr <= 3)
	{
		dma_outb(count &0xff, ((dmanr &3) << 1) + 1+IO_DMA1_BASE);
		dma_outb((count >> 8) &0xff, ((dmanr &3) << 1) + 1+IO_DMA1_BASE);
	}
	else
	{
		dma_outb((count >> 1) &0xff, ((dmanr &3) << 2) + 2+IO_DMA2_BASE);
		dma_outb((count >> 9) &0xff, ((dmanr &3) << 2) + 2+IO_DMA2_BASE);
	}
}

/* 設置傳輸地址和頁位 */
static _ _inline_ _ void set_dma_addr(unsigned int dmanr, unsigned int a)
{
	set_dma_page(dmanr, a >> 16);
	if (dmanr <= 3)
	{
		dma_outb(a &0xff, ((dmanr &3) << 1) + IO_DMA1_BASE);
		dma_outb((a >> 8) &0xff, ((dmanr &3) << 1) + IO_DMA1_BASE);
	}
	else
	{
		dma_outb((a >> 1) &0xff, ((dmanr &3) << 2) + IO_DMA2_BASE);
		dma_outb((a >> 9) &0xff, ((dmanr &3) << 2) + IO_DMA2_BASE);
	}
}

/* 設置頁寄存器位,當已知 DMA 當前地址寄存器的低 16 位時,連續傳輸 */
static _ _inline_ _ void set_dma_page(unsigned int dmanr, char pagenr)
{
	switch (dmanr)
	{
		case 0:
			dma_outb(pagenr, DMA_PAGE_0);
		break;

		case 1:
			dma_outb(pagenr, DMA_PAGE_1);
		break;

		case 2:
			dma_outb(pagenr, DMA_PAGE_2);
		break;

		case 3:
			dma_outb(pagenr, DMA_PAGE_3);
		break;

		case 5:
			dma_outb(pagenr &0xfe, DMA_PAGE_5);
		break;

		case 6:
			dma_outb(pagenr &0xfe, DMA_PAGE_6);
		break;

		case 7:
			dma_outb(pagenr &0xfe, DMA_PAGE_7);
		break;	
	}
}

/*清除 DMA flip-flop*/
static _ _inline_ _ void clear_dma_ff(unsigned int dmanr)
{
if (dmanr <= 3)
	dma_outb(0, DMA1_CLEAR_FF_REG);
else
	dma_outb(0, DMA2_CLEAR_FF_REG);
}

/*設置某通道的 DMA 模式*/
static _ _inline_ _ void set_dma_mode(unsigned int dmanr, char mode)
{
	if (dmanr <= 3)
		dma_outb(mode | dmanr, DMA1_MODE_REG);
	else
dma_outb(mode | (dmanr &3), DMA2_MODE_REG);
}
    可以看到,DMAC端主要是對直接對相碰寄存器的賦值,只不過需要一些特定的內核函數(如dma_outb())完成地址的轉換.


5.一個利用DMAC的外設:

    一個外設如果要用到DMA來實現數據的傳輸,無非涉及到兩個方面:外設通過DMA到RAM和RAM通過DMA到外設.從硬件上講,當RAM通過DMA到外設傳輸數據時,把數據的大小長度賦值(這個值由於是我們要發送給外設的,當然是可知的)給DMAC的一個計數寄存器,當配置DMAC計數寄存器爲0時中斷的話,即數據傳輸長度計數爲0時觸發中斷;當外設通過DMA傳輸數據到RAM時,這時候我們並不知道外設過來的數據長度,此需要第三方策略(具體可google/百度搜索"DMA 接收數據").

    假設設備 xxx 使用了 DMA,DMA 相關的信息應該被添加到設備結構體內.在模塊加載函數或打開函數中,DMA 通道和中斷應該被申請,而 DMA 本身也應被初始化.以設備xxx使用上述的DMAC 8237全例.示例代碼如下:

/*xxx 設備結構體*/
typedef struct
{
	...
	void *dma_buffer; //DMA 緩衝區
	/*當前 DMA 的相關信息*/
	struct
	{
		unsigned int direction; //方向
		unsigned int length;	//尺寸
		void *target;		//目標
		unsigned long start_time; //開始時間
	}current_dma;

	unsigned char dma;//DMA 通道
}xxx_device;
    在這裏具體的設備xxx_device需要用到DMA傳輸數據應該封裝一些具有DAM信息成員結構體.
static int xxx_open(...)
{
	...
	/*安裝中斷服務程序 */
	if ((retval = request_irq(dev->irq, &xxx_interrupt, 0, dev->name,dev)))
	{
		printk(KERN_ERR "%s: could not allocate IRQ%d\n", dev->name,dev->irq);
		return retval;
	}
	/*申請 DMA*/
	if ((retval = request_dma(dev->dma, dev->name))) 
	{
		free_irq(dev->irq, dev);
		printk(KERN_ERR "%s: could not allocate DMA%d channel\n", ...);
		return retval;
	}
	/*申請 DMA 緩衝區*/
	dev->dma_buffer = (void *) dma_mem_alloc(DMA_BUFFER_SIZE);
	if (!dev->dma_buffer)
	{
		printk(KERN_ERR "%s:could not allocate DMA buffer\n",dev->name);
		free_dma(dev->dma);
		free_irq(dev->irq, dev);
		return -ENOMEM;
	}
	/*初始化 DMA*/
	init_dma();
	...
}
    在設備xxx_device打開的時候,我們應該申請好DMA緩衝區和硬件DMA通道,並且安裝xxx_device中斷.

/*內存到外設*/
static int mem_to_xxx(const byte *buf,int len)
{
	...
	dev->current_dma.direction = 1; /*DMA 方向*/
	dev->current_dma.start_time = jiffies; /*記錄 DMA 開始時間*/

	memcpy(dev->dma_buffer, buf, len); /*複製要發送的數據到 DMA 緩衝區*/
	target = isa_virt_to_bus(dev->dma_buffer);/*假設 xxx 掛接在 ISA 總線*/

	/*進行一次 DMA 寫操作*/
	flags=claim_dma_lock();
	disable_dma(dev->dma); /*禁止 DMA*/
	clear_dma_ff(dev->dma); /*清除 DMA flip-flop*/
	set_dma_mode(dev->dma, 0x48);
	set_dma_addr(dev->dma, target); /*設置 DMA 地址*/
	set_dma_count(dev->dma, len); /*設置 DMA 長度*/
	outb_control(dev->x_ctrl | DMAE | TCEN, dev);/*讓設備接收 DMA*/
	enable_dma(dev->dma);
	release_dma_lock(flags);
	printk(KERN_DEBUG "%s: DMA transfer started\n", dev->name);
	...
}

    可以看到,內存到外設的過程中,涉及到了虛擬地址到總線地址的轉換,並且內存到外設數據長度是可知的,直接賦值給DMAC的計數寄存器即可,所以,當DMAC的計數寄存器爲0時,便觸發了DMAC中斷.

/*外設到內存*/
static void xxx_to_mem(const char *buf, int len,char * target)
{
	...
	/*記錄 DMA 信息*/
	dev->current_dma.target = target;
	dev->current_dma.direction = 0;
	dev->current_dma.start_time = jiffies;
	dev->current_dma.length = len;

	/*進行一次 DMA 讀操作*/
	outb_control(dev->x_ctrl | DIR | TCEN | DMAE, dev);
	flags = claim_dma_lock();
	disable_dma(dev->dma);
	clear_dma_ff(dev->dma);
	set_dma_mode(dev->dma, 0x04); /* I/O->mem */
	set_dma_addr(dev->dma, isa_virt_to_bus(target));
	set_dma_count(dev->dma, len);
	enable_dma(dev->dma);
	release_dma_lock(flags);
	...
}
    從外設通過DMA到RAM時,如果外設的數據長度是固定的,我們當然可以直接寫進DMAC的計數寄存器,這樣當接收到預期的數據長度時,便觸發DMA中斷.如CCIC外接SENSOR,一般SENSOR以YUV格式輸出一幀圖像時,大小爲640X480X2個字節.筆者曾處理過JZ4775平臺的CCIC外接SENSOR,由於某DATA腳虛焊導致了DMAC收不到足夠的數據而無法引發中斷.如果接收到的數據大小不確定時,就得想第三方策略了.

/*設備中斷處理*/
static irqreturn_t xxx_interrupt(int irq, void *dev_id, struct pt_regs *reg_ptr)
{
	...
	do
	{
	/* DMA 傳輸完成?*/
		if (int_type==DMA_DONE)
		{
			outb_control(dev->x_ctrl &~(DMAE | TCEN | DIR), dev);
			if (dev->current_dma.direction)
			{
				/*內存->I/O*/
				...
			}
		else
		/*I/O->內存*/
			{
				memcpy(dev->current_dma.target, dev->dma_buffer, dev->current_dma.length);
			}
		}
		else if(int_type==RECV_DATA) /*收到數據*/
		{
			xxx_to_mem(...);/*通過 DMA 讀接收到的數據到內存*/
		}
		...
	}
	...
}
    這裏應該說是DAMC中斷而不是xxx_device中斷更準確一些?


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章