天天看點

Linux的IO端口和IO記憶體

CPU對外設端口實體位址的編址方式有兩種:一種是IO映射方式,另一種是記憶體映射方式。

 Linux将基于IO映射方式的和記憶體映射方式的IO端口統稱為IO區域(IO region)。IO region仍然是一種IO資源,是以它仍然可以用resource結構類型來描述。

  Linux管理IO region:

  1) request_region()

  把一個給定區間的IO端口配置設定給一個IO裝置。

  2) check_region()

  檢查一個給定區間的IO端口是否空閑,或者其中一些是否已經配置設定給某個IO裝置。

  3) release_region()

  釋放以前配置設定給一個IO裝置的給定區間的IO端口。

  Linux中可以通過以下輔助函數來通路IO端口:

  inb(),inw(),inl(),outb(),outw(),outl()

  “b”“w”“l”分别代表8位,16位,32位。

 對IO記憶體資源的通路

注:其實對IO記憶體和IO端口的通路的不同函數,隻是對同一函數的不同調用而已,定義在include/linux/ioport.h:

 #define request_region(start,n,name)    __request_region(&ioport_resource, (start), (n), (name))

 #define request_mem_region(start,n,name) __request_region(&iomem_resource, (start), (n), (name))

 #define rename_region(region, newname) do { (region)->name = (newname); } while (0)

 extern struct resource * __request_region(struct resource *,

                                       resource_size_t start,

                                         resource_size_t n, const char *name);

  1) request_mem_region()

  請求配置設定指定的IO記憶體資源。

  2) check_mem_region()

  檢查指定的IO記憶體資源是否已被占用。

  3) release_mem_region()

  釋放指定的IO記憶體資源。

  其中傳給函數的start address參數是記憶體區的實體位址(以上函數參數表已省略)。

  驅動開發人員可以将記憶體映射方式的IO端口和外設記憶體統一看作是IO記憶體資源。

  ioremap()用來将IO資源的實體位址映射到核心虛位址空間(3GB - 4GB)中,參數addr是指向核心虛位址的指針。

  Linux中可以通過以下輔助函數來通路IO記憶體資源:

  readb(),readw(),readl(),writeb(),writew(),writel()。

  Linux在kernel/resource.c檔案中定義了全局變量ioport_resource和iomem_resource,來分别描述基于IO映射方式的整個IO端口空間和基于記憶體映射方式的IO記憶體資源空間(包括IO端口和外設記憶體)。

1)關于IO與記憶體空間:

    在X86處理器中存在着I/O空間的概念,I/O空間是相對于記憶體空間而言的,它通過特定的指令in、out來通路。端口号辨別了外設的寄存器位址。Intel文法的in、out指令格式為:

    IN 累加器, {端口号│DX}

    OUT {端口号│DX},累加器

    目前,大多數嵌入式微控制器如ARM、PowerPC等中并不提供I/O空間,而僅存在記憶體空間。記憶體空間可以直接通過位址、指針來通路,程式和程式運作中使用的變量和其他資料都存在于記憶體空間中。

    即便是在X86處理器中,雖然提供了I/O空間,如果由我們自己設計電路闆,外設仍然可以隻挂接在記憶體空間。此時,CPU可以像通路一個記憶體單元那樣通路外設I/O端口,而不需要設立專門的I/O指令。是以,記憶體空間是必須的,而I/O空間是可選的。

(2)inb和outb:

在Linux裝置驅動中,宜使用Linux核心提供的函數來通路定位于I/O空間的端口,這些函數包括:

· 讀寫位元組端口(8位寬)

unsigned inb(unsigned port);

void outb(unsigned char byte, unsigned port);

· 讀寫字端口(16位寬)

unsigned inw(unsigned port);

void outw(unsigned short word, unsigned port);

· 讀寫長字端口(32位寬)

unsigned inl(unsigned port);

void outl(unsigned longword, unsigned port);

· 讀寫一串位元組

void insb(unsigned port, void *addr, unsigned long count);

void outsb(unsigned port, void *addr, unsigned long count);

· insb()從端口port開始讀count個位元組端口,并将讀取結果寫入addr指向的記憶體;outsb()将addr指向的記憶體的count個位元組連續地寫入port開始的端口。

· 讀寫一串字

void insw(unsigned port, void *addr, unsigned long count);

void outsw(unsigned port, void *addr, unsigned long count);

· 讀寫一串長字

void insl(unsigned port, void *addr, unsigned long count);

void outsl(unsigned port, void *addr, unsigned long count);

上述各函數中I/O端口号port的類型高度依賴于具體的硬體平台,是以,隻是寫出了unsigned

(3)readb和writeb:

在裝置的實體位址被映射到虛拟位址之後,盡管可以直接通過指針通路這些位址,但是工程師宜使用Linux核心的如下一組函數來完成裝置記憶體映射的虛拟位址的讀寫,這些函數包括:

· 讀I/O記憶體

unsigned int ioread8(void *addr);

unsigned int ioread16(void *addr);

unsigned int ioread32(void *addr);

與上述函數對應的較早版本的函數為(這些函數在Linux 2.6中仍然被支援):

unsigned readb(address);

unsigned readw(address);

unsigned readl(address);

· 寫I/O記憶體

void iowrite8(u8 value, void *addr);

void iowrite16(u16 value, void *addr);

void iowrite32(u32 value, void *addr);

與上述函數對應的較早版本的函數為(這些函數在Linux 2.6中仍然被支援):

void writeb(unsigned value, address);

void writew(unsigned value, address);

void writel(unsigned value, address);

(4)把I/O端口映射到“記憶體空間”:

void *ioport_map(unsigned long port, unsigned int count);

通過這個函數,可以把port開始的count個連續的I/O端口重映射為一段“記憶體空間”。然後就可以在其傳回的位址上像通路I/O記憶體一樣通路這些I/O端口。當不再需要這種映射時,需要調用下面的函數來撤消:

void ioport_unmap(void *addr);

實際上,分析ioport_map()的源代碼可發現,所謂的映射到記憶體空間行為實際上是給開發人員制造的一個“假象”,并沒有映射到核心虛拟位址,僅僅是為了讓工程師可使用統一的I/O記憶體通路接口通路I/O端口。

 ----------------------------------------------------------------------------------

Linux對I/O端口資源的管理

    幾乎每一種外設都是通過讀寫裝置上的寄存器來進行的。外設寄存器也稱為“I/O端口”,通常包括:控制寄存器、狀态寄存器和資料寄存器三大類,而且一個外設的寄存器通常被連續地編址。CPU對外設IO端口實體位址的編址方式有兩種:一種是I/O映射方式(I/O-mapped),另一種是記憶體映射方式(Memory-mapped)。而具體采用哪一種則取決于CPU的體系結構。

    有些體系結構的CPU(如,PowerPC、m68k等)通常隻實作一個實體位址空間(RAM)。在這種情況下,外設 I/O端口的實體位址就被映射到CPU的單一實體位址空間中,而成為記憶體的一部分。此時,CPU可以象通路一個記憶體單元那樣通路外設I/O端口,而不需要設立專門的外設I/O指令。這就是所謂的“記憶體映射方式”(Memory-mapped)。

    而另外一些體系結構的CPU(典型地如X86)則為外設專門實作了一個單獨地位址空間,稱為“I/O位址空間”或者“I/O端口空間”。這是一個與CPU地RAM實體位址空間不同的位址空間,所有外設的I/O端口均在這一空間中進行編址。CPU通過設立專門的I/O指令(如X86的IN和OUT指令)來通路這一空間中的位址單元(也即 I/O端口)。這就是所謂的“I/O映射方式”(I/O-mapped)。與RAM實體位址空間相比,I/O位址空間通常都比較小,如x86 CPU的I/O空間就隻有64KB(0-0xffff)。這是“I/O映射方式”的一個主要缺點。

 Linux将基于I/O映射方式的或記憶體映射方式的I/O端口通稱為“I/O區域”(I/O region)。在讨論對I/O區域的管理之前,我們首先來分析一下Linux是如何實作“I/O資源”這一抽象概念的。

Linux對I/O資源的描述

--------------------------------------  

    Linux設計了一個通用的資料結構resource來描述各種I/O資源(如:I/O端口、外設記憶體、DMA和IRQ等)。該結構定義在include/linux/ioport.h頭檔案中:

struct resource {

    resource_size_t start;

    resource_size_t end;

    const char      *name;

    unsigned long   flags;

    struct resource *parent, *sibling, *child;

};

Linux對I/O資源的管理

--------------------------------------    

    Linux是以一種倒置的樹形結構來管理每一類I/O資源(如:I/O端口、外設記憶體、DMA和IRQ)的。每一類I/O資源都對應有一顆倒置的資源樹,樹中的每一個節點都是一個resource結構,而樹的根結點root則描述了該類資源的整個資源空間。

    基于上述這個思想,Linux在kernel/resource.c檔案中實作了對資源的申請、釋放及查找等操作。

    資源的申請:    request_resource()

    資源的釋放:    release_resource()

    尋找可用資源: --find_resource()

    配置設定資源:      allocate_resource()

管理I/O Region資源

--------------------------------------    

    Linux将基于I/O映射方式的I/O端口和基于記憶體映射方式的I/O端口資源統稱為“I/O區域”(I/O Region)。I/O Region仍然是一種I/O資源,是以它仍然可以用resource結構類型來描述。下面我們就來看看Linux是如何管理I/O Region的。

    I/O Region的配置設定: __request_region()

    I/O Region的釋放: __release_region()

    檢查指定的I/O Region是否已被占用: __check_region()

管理I/O端口資源

--------------------------------------      

    我們都知道,采用I/O映射方式的X86處理器為外設實作了一個單獨的位址空間,也即“I/O空間”(I/O Space)或稱為“I/O端口空間”,其大小是64KB(0x0000-0xffff)。Linux在其所支援的所有平台上都實作了“I/O端口空間” 這一概念。

    由于I/O空間非常小,是以即使外設總線有一個單獨的I/O端口空間,卻也不是所有的外設都将其I/O端口(指寄存器)映射到“I/O端口空間”中。比如,大多數PCI卡都通過記憶體映射方式來将其I/O端口或外設記憶體映射到CPU的RAM實體位址空間中。而老式的ISA卡通常将其I/O端口映射到I/O端口空間中。

    Linux是基于“I/O Region”這一概念來實作對I/O端口資源(I/O-mapped 或 Memory-mapped)的管理的。

 資源根節點的定義:  

    Linux在kernel/resource.c檔案中定義了全局變量ioport_resource和iomem_resource,來分别描述基于I/O映射方式的整個I/O端口空間和基于記憶體映射方式的I/O記憶體資源空間(包括I/O端口和外設記憶體)。其定義如下:

struct resource ioport_resource = {

    .name   = "PCI IO",

    .start -= 0,

    .end    = IO_SPACE_LIMIT,

    .flags -= IORESOURCE_IO,

};

struct resource iomem_resource = {

    .name   = "PCI mem",

    .start -= 0,

    .end    = -1,

    .flags -= IORESOURCE_MEM,

};

    其中,宏IO_SPACE_LIMIT表示整個I/O空間的大小,對于X86平台而言,它是0xffff(定義在include/asm-i386/io.h頭檔案中)。顯然,I/O記憶體空間的大小是4GB。

    對I/O端口空間的操作:

    基于I/O Region的操作函數__xxx_region(),Linux在頭檔案include/linux/ioport.h中定義了三個對I/O端口空間進行操作的接口:

    request_region()    請求在I/O端口空間中配置設定指定範圍的I/O端口資源。

    check_region()      檢查I/O端口空間中的指定I/O端口資源是否已被占用。       

    release_region()    釋放I/O端口空間中的指定I/O端口資源。

    對I/O記憶體資源的操作:  

    基于I/O Region的操作函數__xxx_region(),Linux在頭檔案include/linux/ioport.h中定義了三個對I/O記憶體資源進行操作的接口:

    request_mem_region()    請求配置設定指定的I/O記憶體資源。

    check_mem_region()      檢查指定的I/O記憶體資源是否已被占用。

    release_mem_region()    釋放指定的I/O記憶體資源。

通路I/O端口空間(I/O-mapped)

--------------------------------------  

    在驅動程式請求了I/O端口空間中的端口資源後,它就可以通過CPU的IO指令來讀寫這些I/O端口了。在讀寫I/O端口時要注意的一點就是,大多數平台都區分8位、16位和32位的端口,也即要注意I/O端口的寬度。

    inb() outb() inw() outw() inl() outl()

    unsigned char inb(unsigned port);

    port參數指定I/O端口空間中的端口位址。在大多數平台上(如x86)它都是unsigned short類型的,其它的一些平台上則是unsigned int類型的。顯然,端口位址的類型是由I/O端口空間的大小來決定的。

    對I/O端口的字元串操作

    除了上述這些“單發”(single-shot)的I/O操作外,某些CPU也支援對某個I/O端口進行連續的讀寫操作,也即對單個I/O端口讀或寫一系列位元組、字或32位整數,這就是所謂的“字元串I/O指令”(String Instruction)。這種指令在速度上顯然要比用循環來實作同樣的功能要快得多。

    insb() outsb() insw() outw() insl() outsl()

Pausing I/O  

    在一些平台上(典型地如X86),對于老式總線(如ISA)上的慢速外設來說,如果CPU讀寫其I/O端口的速度太快,那就可能會發生丢失資料的現象。對于這個問題的解決方法就是在兩次連續的I/O操作之間插入一段微小的時延,以便等待慢速外設。這就是所謂的“Pausing I/O”。

    對于Pausing I/O,Linux也在io.h頭檔案中定義了它的I/O讀寫函數,而且都以XXX_p命名,比如:inb_p()、outb_p()等等。

通路I/O記憶體資源(I/O Memory)

--------------------------------------    

    盡管I/O端口空間曾一度在x86平台上被廣泛使用,但是由于它非常小,是以大多數現代總線的裝置都以記憶體映射方式(Memory-mapped)來映射它的I/O端口(指I/O寄存器)和外設記憶體。基于記憶體映射方式的I/O端口稱為"I/O記憶體"資源(I/O Memory)。因為基于記憶體映射方式的I/O和外設記憶體在硬體實作上的差異對于軟體來說是完全透明的,是以驅動程式開發人員可以将記憶體映射方式的I/O端口和外設記憶體統一看作是“I/O記憶體”資源。

    I/O記憶體資源是在CPU的單一記憶體實體位址空間内進行編址的,也即它和系統RAM同處在一個實體位址空間内。是以通過CPU的訪内指令就可以通路I/O記憶體資源。

    一般來說,在系統運作時,外設的I/O記憶體資源的實體位址是已知的,這可以通過系統固件(如BIOS)在啟動時配置設定得到,或者通過裝置的硬連線(hardwired)得到。比如,PCI卡的I/O記憶體資源的實體位址通路I/O記憶體資源,而必須将它們映射到核心虛位址空間内(通過頁表),然後才能根據映射所得到的核心虛位址範圍,通過訪内指令通路這些I/O記憶體資源。實體位址就是在系統啟動時由PCI BIOS配置設定并寫到PCI卡的配置空間中的BAR中的。而ISA卡的I/O記憶體資源的實體位址則是通過裝置硬連線映射到640KB-1MB範圍之内的。但是CPU通常并沒有為這些已知的外設I/O記憶體資源的實體位址預定義虛拟位址範圍,因為它們是在系統啟動後才已知的(某種意義上講是動态的),是以驅動程式并不能直接通過

    映射I/O記憶體資源

    Linux在io.h頭檔案中聲明了函數ioremap(),用來将I/O記憶體資源的實體位址映射到核心虛位址空間(3GB-4GB)中

    對于X86體系結構ioremap()定義在/usr/src/linux-2.6.21.5/include/asm-i386/io.h中

    讀寫I/O記憶體資源

    在将I/O記憶體資源的實體位址映射成核心虛位址後,理論上講我們就可以象讀寫RAM那樣直接讀寫I/O記憶體資源了。但是,由于在某些平台上,對 I/O記憶體和系統記憶體有不同的通路處理,是以為了確定跨平台的相容性,Linux實作了一系列讀寫I/O記憶體資源的函數,這些函數在不同的平台上有不同的實作。但在x86平台上,讀寫I/O記憶體與讀寫RAM無任何差别。如下所示(include/asm-i386/io.h):

    readb() readw() readl()

    writeb() writew() writel()

    memset_io() memcpy_fromio() memcpytoio()

顯然,在x86平台上通路I/O記憶體資源與通路系統主存RAM是無差别的。但是為了保證驅動程式的跨平台的可移植性,我們應該使用上面的函數來通路I/O記憶體資源,而不應該通過指向核心虛位址的指針來通路。

繼續閱讀