Linux如何访问mmio空间,一文读懂Linux下如何访问I/O端口和I/O内存
雖然訪問I/O端口非常簡單,但是檢測哪些I/O端口已經分配給I/O設備可能就不這么簡單了,對基于ISA總線的系統來說更是如此。通常,I/O設備驅動程序為了探測硬件設備,需要盲目地向某一I/O端口寫入數據;但是,如果其他硬件設備已經使用這個端口,那么系統就會崩潰。為了防止這種情況的發生,內核必須使用“資源”來記錄分配給每個硬件設備的I/O端口。資源表示某個實體的一部分,這部分被互斥地分配給設備驅動程序。在這里,資源表示I/O端口地址的一個范圍。每個資源對應的信息存放在resource數據結構中:本文引用地址:http://www.eepw.com.cn/article/201712/373670.htm
1.struct?resource?{
2.?resource_size_t?start;//?資源范圍的開始
3.?resource_size_t?end;//?資源范圍的結束
4.?const?char?*name;?//資源擁有者的名字
5.?unsigned?long?flags;//?各種標志
6.?struct?resource?*parent,?*sibling,?*child;//?指向資源樹中父親,兄弟和孩子的指針
};
所有的同種資源都插入到一個樹型數據結構(父親、兄弟和孩子)中;例如,表示I/O端口地址范圍的所有資源都包括在一個根節點為ioport_resource的樹中。節點的孩子被收集在一個鏈表中,其第一個元素由child指向。sibling字段指向鏈表中的下一個節點。
為什么使用樹?例如,考慮一下IDE硬盤接口所使用的I/O端口地址-比如說從0xf000?到?0xf00f。那么,start字段為0xf000?且end?字段為0xf00f的這樣一個資源包含在樹中,控制器的常規名字存放在name字段中。但是,IDE設備驅動程序需要記住另外的信息,也就是IDE鏈主盤使用0xf000?到0xf007的子范圍,從盤使用0xf008?到0xf00f的子范圍。為了做到這點,設備驅動程序把兩個子范圍對應的孩子插入到從0xf000?到0xf00f的整個范圍對應的資源下。
一般來說,樹中的每個節點肯定相當于父節點對應范圍的一個子范圍。I/O端口資源樹(ioport_resource)的根節點跨越了整個I/O地址空間(從端口0到65535)。
任何設備驅動程序都可以使用下面三個函數,傳遞給它們的參數為資源樹的根節點和要插入的新資源數據結構的地址:
request_resource(?)?//把一個給定范圍分配給一個I/O設備。
allocate_resource(?)?//在資源樹中尋找一個給定大小和排列方式的可用范圍;若存在,將這個范圍分配給一個I/O設備(主要由PCI設備驅動程序使用,可以使用任意的端口號和主板上的內存地址對其進行配置)。
release_resource(?)?//釋放以前分配給I/O設備的給定范圍。
內核也為以上函數定義了一些應用于I/O端口的快捷函數:request_region(?)分配I/O端口的給定范圍,release_region(?)釋放以前分配給I/O端口的范圍。當前分配給I/O設備的所有I/O地址的樹都可以從/proc/ioports文件中獲得。
2、內存映射方式
將IO端口映射為內存進行訪問,在設備打開或驅動模塊被加載時,申請IO端口區域并使用ioport_map()映射到內存,之后使用IO內存的函數進行端口訪問,最后,在設備關閉或驅動模塊被卸載時釋放IO端口并釋放映射。
映射函數的原型為:
void?*ioport_map(unsigned?long?port,?unsigned?int?count);
通過這個函數,可以把port開始的count個連續的I/O端口重映射為一段“內存空間”。然后就可以在其返回的地址上像訪問I/O內存一樣訪問這些I/O端口。但請注意,在進行映射前,還必須通過request_region(?)分配I/O端口。
當不再需要這種映射時,需要調用下面的函數來撤消:
void?ioport_unmap(void?*addr);
在設備的物理地址被映射到虛擬地址之后,盡管可以直接通過指針訪問這些地址,但是宜使用Linux內核的如下一組函數來完成訪問I/O內存:
讀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);
流程如下:
六、Linux下訪問IO內存
IO內存的訪問方法是:首先調用request_mem_region()申請資源,接著將寄存器地址通過ioremap()映射到內核空間的虛擬地址,之后就可以Linux設備訪問編程接口訪問這些寄存器了,訪問完成后,使用ioremap()對申請的虛擬地址進行釋放,并釋放release_mem_region()申請的IO內存資源。
struct?resource?*requset_mem_region(unsigned?long?start,?unsigned?long?len,char?*name);
這個函數從內核申請len個內存地址(在3G~4G之間的虛地址),而這里的start為I/O物理地址,name為設備的名稱。注意,。如果分配成功,則返回非NULL,否則,返回NULL。
另外,可以通過/proc/iomem查看系統給各種設備的內存范圍。
要釋放所申請的I/O內存,應當使用release_mem_region()函數:
void?release_mem_region(unsigned?long?start,?unsigned?long?len)
申請一組I/O內存后,?調用ioremap()函數:
void?*?ioremap(unsigned?long?phys_addr,?unsigned?long?size,?unsigned?long?flags);
其中三個參數的含義為:
phys_addr:與requset_mem_region函數中參數start相同的I/O物理地址;
size:要映射的空間的大小;
flags:要映射的IO空間的和權限有關的標志;
功能:將一個I/O地址空間映射到內核的虛擬地址空間上(通過release_mem_region()申請到的)
流程如下:
七、ioremap和ioport_map
下面具體看一下ioport_map和ioport_umap的源碼:
void?__iomem?*ioport_map(unsigned?long?port,?unsigned?int?nr)
{
1.
if?(port?>?PIO_MASK)
2.
return?NULL;
3.
return?(void?__iomem?*)?(unsigned?long)?(port?+?PIO_OFFSET);
4.
}
5.
6.
void?ioport_unmap(void?__iomem?*addr)
7.
{
8.
/*?Nothing?to?do?*/
ioport_map僅僅是將port加上PIO_OFFSET(64k),而ioport_unmap則什么都不做。這樣portio的64k空間就被映射到虛擬地址的64k~128k之間,而ioremap返回的虛擬地址則肯定在3G之上。ioport_map函數的目的是試圖提供與ioremap一致的虛擬地址空間。分析ioport_map()的源代碼可發現,所謂的映射到內存空間行為實際上是給開發人員制造的一個“假象”,并沒有映射到內核虛擬地址,僅僅是為了讓工程師可使用統一的I/O內存訪問接口ioread8/iowrite8(......)訪問I/O端口。
最后來看一下ioread8的源碼,其實現也就是對虛擬地址進行了判斷,以區分IO端口和IO內存,然后分別使用inb/outb和readb/writeb來讀寫。
unsigned?int?fastcall?ioread8(void?__iomem?*addr)
{
IO_COND(addr,?return?inb(port),?return?readb(addr));
}
#define?VERIFY_PIO(port)?BUG_ON((port?&?~PIO_MASK)?!=?PIO_OFFSET)
#define?IO_COND(addr,?is_pio,?is_mmio)?do?{?\
unsigned?long?port?=?(unsigned?long?__force)addr;?\
if?(port?
VERIFY_PIO(port);?\
port?&=?PIO_MASK;?\
is_pio;?\
}?else?{?\
is_mmio;?\
}?\
}?while?(0)
展開:
unsigned?int?fastcall?ioread8(void?__iomem?*addr)
{
unsigned?long?port?=?(unsigned?long?__force)addr;
if(?port?
BUG_ON(?(port?&?~PIO_MASK)?!=?PIO_OFFSET?);
port?&=?PIO_MASK;
return?inb(port);
}else{
return?readb(addr);
}
}
八、總結
外設IO寄存器地址獨立編址的CPU,這時應該稱外設IO寄存器為IO端口,訪問IO寄存器可通過ioport_map將其映射到虛擬地址空間,但實際上這是給開發人員制造的一個“假象”,并沒有映射到內核虛擬地址,僅僅是為了可以使用和IO內存一樣的接口訪問IO寄存器;也可以直接使用in/out指令訪問IO寄存器。
例如:Intel?x86平臺普通使用了名為內存映射(MMIO)的技術,該技術是PCI規范的一部分,IO設備端口被映射到內存空間,映射后,CPU訪問IO端口就如同訪?問內存一樣。
外設IO寄存器地址統一編址的CPU,這時應該稱外設IO寄存器為IO內存,訪問IO寄存器可通過ioremap將其映射到虛擬地址空間,然后再使用read/write接口訪問。
總結
以上是生活随笔為你收集整理的Linux如何访问mmio空间,一文读懂Linux下如何访问I/O端口和I/O内存的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Mac下运行git报错xcrun: er
- 下一篇: Unlicensed ARC sessi