原文地址:http://blog.chinaunix.net/u1/59291/showart_461605.html
在學習有關I/O總線的內容時,最好先看看相關的知識:從PC總線到ARM的內部總線
I/O 端口和 I/O 內存
每種外設都是通過讀寫寄存器來進行控制。
在硬件層,內存區和 I/O 區域沒有概念上的區別: 它們都是通過向在地址總線和控制總線發出電平信號來進行訪問,再通過數據總線讀寫數據。
因爲外設要與I/O總線匹配,而大部分流行的 I/O 總線是基於個人計算機模型(主要是 x86 家族:它爲讀和寫 I/O 端口提供了獨立的線路和特殊的 CPU 指令),所以即便那些沒有單獨I/O 端口地址空間的處理器,在訪問外設時也要模擬成讀寫I/O端口。這一功能通常由外圍芯片組(PC 中的南北橋)或 CPU 中的附加電路實現(嵌入式中的方法) 。
Linux 在所有的計算機平臺上實現了 I/O 端口。但不是所有的設備都將寄存器映射到 I/O 端口。雖然ISA設備普遍使用 I/O 端口,但大部分 PCI 設備則把寄存器映射到某個內存地址區,這種 I/O 內存方法通常是首選的。因爲它無需使用特殊的處理器指令,CPU 核訪問內存更有效率,且編譯器在訪問內存時在寄存器分配和尋址模式的選擇上有更多自由。
I/O 寄存器和常規內存
在進入這部分學習的時候,首先要理解一個概念:side effect,書中譯爲邊際效應,第二版譯爲副作用。我覺得不管它是怎麼被翻譯的,都不可能精準表達原作者的意思,所以我個人認爲記住side effect就好。下面來講講side effect的含義。我先貼出兩個網上已有的兩種說法(
在這裏謝謝兩位高人的分享):
結合以上兩種說法和自己看《Linux設備驅動程序(第3版)》的理解,我個人認爲可以這樣解釋:
side effect 是指:訪問I/O寄存器時,不僅僅會像訪問普通內存一樣影響存儲單元的值,更重要的是它可能改變CPU的I/O端口電平、輸出時序或CPU對I/O端口電平的反應等等,從而實現CPU的控制功能。CPU在電路中的意義就是實現其side effect 。 |
I/O 寄存器和 RAM 的主要不同就是 I/O 寄存器操作有side effect, 而內存操作沒有。
因爲存儲單元的訪問速度對 CPU 性能至關重要,編譯器會對源代碼進行優化,主要是: 使用高速緩存保存數值 和 重新編排讀/寫指令順序。但對I/O 寄存器操作來說,這些優化可能造成致命錯誤。因此,驅動程序必須確保在操作I/O 寄存器時,不使用高速緩存,且不能重新編排讀/寫指令順序。
解決方法:
硬件緩存問題:只要把底層硬件配置(自動地或者通過 Linux 初始化代碼)成當訪問 I/O 區域時(不管內存還是端口)禁止硬件緩存即可。
硬件指令重新排序問題:在硬件(或其他處理器)必須以一個特定順序執行的操作之間設置內存屏障(memory barrier)。
Linux 提供以下宏來解決所有可能的排序問題:
#include <linux/kernel.h> void barrier(void) /*告知編譯器插入一個內存屏障但是對硬件沒有影響。編譯後的代碼會將當前CPU 寄存器中所有修改過的數值保存到內存中, 並當需要時重新讀取它們。可阻止在屏障前後的編譯器優化,但硬件能完成自己的重新排序。其實<linux/kernel.h> 中並沒有這個函數,因爲它是在kernel.h包含的頭文件compiler.h中定義的*/ #include <linux/compiler.h> # define barrier() __memory_barrier()
#include <asm/system.h> void rmb(void); /*保證任何出現於屏障前的讀在執行任何後續的讀之前完成*/ void wmb(void); /*保證任何出現於屏障前的寫在執行任何後續的寫之前完成*/ void mb(void); /*保證任何出現於屏障前的讀寫操作在執行任何後續的讀寫操作之前完成*/ void read_barrier_depends(void); /*一種特殊的、弱些的讀屏障形式。rmb 阻止屏障前後的所有讀指令的重新排序,read_barrier_depends 只阻止依賴於其他讀指令返回的數據的讀指令的重新排序。區別微小, 且不在所有體系中存在。除非你確切地理解它們的差別, 並確信完整的讀屏障會增加系統開銷,否則應當始終使用 rmb。*/ /*以上指令是barrier的超集*/
void smp_rmb(void); void smp_read_barrier_depends(void); void smp_wmb(void); void smp_mb(void); /*僅當內核爲 SMP 系統編譯時插入硬件屏障; 否則, 它們都擴展爲一個簡單的屏障調用。*/
|
典型的應用:
writel(dev->registers.addr, io_destination_address); writel(dev->registers.size, io_size); writel(dev->registers.operation, DEV_READ); wmb();/*類似一條分界線,上面的寫操作必然會在下面的寫操作前完成,但是上面的三個寫操作的排序無法保證*/ writel(dev->registers.control, DEV_GO);
|
內存屏障影響性能,所以應當只在確實需要它們的地方使用。不同的類型對性能的影響也不同,因此要儘可能地使用需要的特定類型。值得注意的是大部分處理同步的內核原語,例如自旋鎖和atomic_t,也可作爲內存屏障使用。
某些體系允許賦值和內存屏障組合,以提高效率。它們定義如下:
#define set_mb(var, value) do {var = value; mb();} while 0 /*以下宏定義在ARM體系中不存在*/ #define set_wmb(var, value) do {var = value; wmb();} while 0 #define set_rmb(var, value) do {var = value; rmb();} while 0
|
使用do...while 結構來構造宏是標準 C 的慣用方法,它保證了擴展後的宏可在所有上下文環境中被作爲一個正常的 C 語句執行。
使用 I/O 端口
I/O 端口是驅動用來和許多設備之間的通訊方式。
I/O 端口分配
在尚未取得端口的獨佔訪問前,不應對端口進行操作。內核提供了一個註冊用的接口,允許驅動程序聲明它需要的端口:
#include <linux/ioport.h> struct resource *request_region(unsigned long first, unsigned long n, const char *name);/*告訴內核:要使用從 first 開始的 n 個端口,name 參數爲設備名。若分配成功返回非 NULL,否則將無法使用需要的端口。*/ /*所有的的端口分配顯示在 /proc/ioports 中。若不能分配到需要的端口,則可以到這裏看看誰先用了。*/
/*當用完 I/O 端口集(可能在模塊卸載時), 應當將它們返回給系統*/ void release_region(unsigned long start, unsigned long n);
int check_region(unsigned long first, unsigned long n); /*檢查一個給定的 I/O 端口集是否可用,若不可用, 返回值是一個負錯誤碼。不推薦使用*/
|
操作 I/O 端口
在驅動程序註冊I/O 端口後,就可以讀/寫這些端口。大部分硬件會把8、16和32位端口區分開,不能像訪問系統內存那樣混淆使用。驅動必須調用不同的函數來存取不同大小的端口。
只支持內存映射的 I/O 寄存器的計算機體系通過重新映射I/O端口到內存地址來僞裝端口I/O。爲了提高移植性,內核向驅動隱藏了這些細節。Linux 內核頭文件(體系依賴的頭文件 <asm/io.h> ) 定義了下列內聯函數(有的體系是宏,有的不存在)來訪問 I/O 端口:
unsigned inb(unsigned port); void outb(unsigned char byte, unsigned port); /*讀/寫字節端口( 8 位寬 )。port 參數某些平臺定義爲 unsigned long ,有些爲 unsigned short 。 inb 的返回類型也體系而不同。*/
unsigned inw(unsigned port); void outw(unsigned short word, unsigned port); /*訪問 16位 端口( 一個字寬 )*/
unsigned inl(unsigned port); void outl(unsigned longword, unsigned port); /*訪問 32位 端口。 longword 聲明有的平臺爲 unsigned long ,有的爲 unsigned int。*/
|
在用戶空間訪問 I/O 端口
以上函數主要提供給設備驅動使用,但它們也可在用戶空間使用,至少在 PC上可以。 GNU C 庫在 <sys/io.h> 中定義了它們。如果在用戶空間代碼中使用必須滿足以下條件:
(1)程序必須使用 -O 選項編譯來強制擴展內聯函數。
(2)必須用ioperm 和 iopl 系統調用(#include <sys/perm.h>) 來獲得對端口 I/O 操作的權限。ioperm 爲獲取單獨端口操作權限,而 iopl 爲整個 I/O 空間的操作權限。 (x86 特有的)
(3)程序以 root 來調用 ioperm 和 iopl,或是其父進程必須以 root 獲得端口操作權限。(x86 特有的)
若平臺沒有 ioperm 和 iopl 系統調用,用戶空間可以仍然通過使用 /dev/prot 設備文件訪問 I/O 端口。注意:這個文件的定義是體系相關的,並且I/O 端口必須先被註冊。
串操作
除了一次傳輸一個數據的I/O操作,一些處理器實現了一次傳輸一個數據序列的特殊指令,序列中的數據單位可以是字節、字或雙字,這是所謂的串操作指令。它們完成任務比一個 C 語言循環更快。下列宏定義實現了串I/O,它們有的通過單個機器指令實現;但如果目標處理器沒有進行串 I/O 的指令,則通過執行一個緊湊的循環實現。 有的體系的原型如下:
void insb(unsigned port, void *addr, unsigned long count); void outsb(unsigned port, void *addr, unsigned long count);
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);
|
使用時注意: 它們直接將字節流從端口中讀取或寫入。當端口和主機系統有不同的字節序時,會導致不可預期的結果。 使用 inw 讀取端口應在必要時自行轉換字節序,以匹配主機字節序。
暫停式 I/O
爲了匹配低速外設的速度,有時若 I/O 指令後面還緊跟着另一個類似的I/O指令,就必須在 I/O 指令後面插入一個小延時。在這種情況下,可以使用暫停式的I/O函數代替通常的I/O函數,它們的名字以 _p 結尾,如 inb_p、outb_p等等。 這些函數定義被大部分體系支持,儘管它們常常被擴展爲與非暫停式I/O 同樣的代碼。因爲如果體系使用一個合理的現代外設總線,就沒有必要額外暫停。細節可參考平臺的 asm 子目錄的 io.h 文件。以下是include/asm-arm/io.h中的宏定義:
#define outb_p(val,port) outb((val),(port)) #define outw_p(val,port) outw((val),(port)) #define outl_p(val,port) outl((val),(port)) #define inb_p(port) inb((port)) #define inw_p(port) inw((port)) #define inl_p(port) inl((port))
#define outsb_p(port,from,len) outsb(port,from,len) #define outsw_p(port,from,len) outsw(port,from,len) #define outsl_p(port,from,len) outsl(port,from,len) #define insb_p(port,to,len) insb(port,to,len) #define insw_p(port,to,len) insw(port,to,len) #define insl_p(port,to,len) insl(port,to,len)
|
由此可見,由於ARM使用內部總線,就沒有必要額外暫停,所以暫停式的I/O函數被擴展爲與非暫停式I/O 同樣的代碼。
平臺相關性
由於自身的特性,I/O 指令與處理器密切相關的,非常難以隱藏系統間的不同。所以大部分的關於端口 I/O 的源碼是平臺依賴的。以下是x86和ARM所使用函數的總結:
IA-32 (x86) x86_64 這個體系支持所有的以上描述的函數,端口號是 unsigned short 類型。
ARM 端口映射到內存,支持所有函數。串操作 用C語言實現。端口是 unsigned int 類型。
使用 I/O 內存
除了 x86上普遍使用的I/O 端口外,和設備通訊另一種主要機制是通過使用映射到內存的寄存器或設備內存,統稱爲 I/O 內存。因爲寄存器和內存之間的區別對軟件是透明的。I/O 內存僅僅是類似 RAM 的一個區域,處理器通過總線訪問這個區域,以實現設備的訪問。
根據平臺和總線的不同,I/O 內存可以就是否通過頁表訪問分類。若通過頁表訪問,內核必須首先安排物理地址使其對設備驅動程序可見,在進行任何 I/O 之前必須調用 ioremap。若不通過頁表,I/O 內存區域就類似I/O 端口,可以使用適當形式的函數訪問它們。因爲“side effect”的影響,
不管是否需要 ioremap ,都不鼓勵直接使用 I/O 內存的指針。而使用專用的 I/O 內存操作函數,不僅在所有平臺上是安全,而且對直接使用指針操作 I/O 內存的情況進行了優化。
I/O 內存分配和映射
I/O 內存區域使用前必須先分配,函數接口在 <linux/ioport.h> 定義:
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);/* 從 start 開始,分配一個 len 字節的內存區域。成功返回一個非NULL指針,否則返回NULL。所有的 I/O 內存分配情況都 /proc/iomem 中列出。*/
/*I/O內存區域在不再需要時應當釋放*/ void release_mem_region(unsigned long start, unsigned long len);
/*一箇舊的檢查 I/O 內存區可用性的函數,不推薦使用*/ int check_mem_region(unsigned long start, unsigned long len);
|
然後必須設置一個映射,由 ioremap 函數實現,此函數專門用來爲I/O 內存區域分配虛擬地址。經過ioremap 之後,設備驅動即可訪問任意的 I/O 內存地址。注意:ioremap 返回的地址不應當直接引用;應使用內核提供的 accessor 函數。以下爲函數定義:
#include <asm/io.h> void *ioremap(unsigned long phys_addr, unsigned long size); void *ioremap_nocache(unsigned long phys_addr, unsigned long size);/*如果控制寄存器也在該區域,應使用的非緩存版本,以實現side effect。*/ void iounmap(void * addr);
|
訪問I/O 內存
訪問I/O 內存的正確方式是通過一系列專用於此目的的函數(在 <asm/io.h> 中定義的):
/*I/O 內存讀函數*/ unsigned int ioread8(void *addr); unsigned int ioread16(void *addr); unsigned int ioread32(void *addr); /*addr 是從 ioremap 獲得的地址(可能包含一個整型偏移量), 返回值是從給定 I/O 內存讀取的值*/
/*對應的I/O 內存寫函數*/ void iowrite8(u8 value, void *addr); void iowrite16(u16 value, void *addr); void iowrite32(u32 value, void *addr);
/*讀和寫一系列值到一個給定的 I/O 內存地址,從給定的 buf 讀或寫 count 個值到給定的 addr */ void ioread8_rep(void *addr, void *buf, unsigned long count); void ioread16_rep(void *addr, void *buf, unsigned long count); void ioread32_rep(void *addr, void *buf, unsigned long count); void iowrite8_rep(void *addr, const void *buf, unsigned long count); void iowrite16_rep(void *addr, const void *buf, unsigned long count); void iowrite32_rep(void *addr, const void *buf, unsigned long count);
/*需要操作一塊 I/O 地址,使用一下函數*/ void memset_io(void *addr, u8 value, unsigned int count); void memcpy_fromio(void *dest, void *source, unsigned int count); void memcpy_toio(void *dest, void *source, unsigned int count);
/*舊函數接口,仍可工作, 但不推薦。*/ unsigned readb(address); unsigned readw(address); unsigned readl(address); void writeb(unsigned value, address); void writew(unsigned value, address); void writel(unsigned value, address);
|
像 I/O 內存一樣使用端口
一些硬件有一個有趣的特性:一些版本使用 I/O 端口,而其他的使用 I/O 內存。爲了統一編程接口,使驅動程序易於編寫,2.6 內核提供了一個ioport_map函數:
void *ioport_map(unsigned long port, unsigned int count);/*重映射 count 個I/O 端口,使其看起來像 I/O 內存。,此後,驅動程序可以在返回的地址上使用 ioread8 和同類函數。其在編程時消除了I/O 端口和I/O 內存的區別。
/*這個映射應當在它不再被使用時撤銷:*/ void ioport_unmap(void *addr);
/*注意:I/O 端口仍然必須在重映射前使用 request_region 分配I/O 端口。ARM9不支持這兩個函數!*/
|
上面是基於《Linux設備驅動程序(第3版)》的介紹,以下分析 ARM9的s3c2440A的linux驅動接口。
ARM9的linux驅動接口
s3c24x0處理器是使用I/O內存的,也就是說:他們的外設接口是通過讀寫相應的寄存器實現的,這些寄存器和內存是使用單一的地址空間,並使用和讀寫內存一樣的指令。所以推薦使用I/O內存的相關指令。
但這並不表示I/O端口的指令在s3c24x0中不可用。但是隻要你注意其源碼,你就會發現:其實I/O端口的指令只是一個外殼,內部還是使用和I/O內存一樣的代碼。以下列出一些:
I/O端口
#define outb(v,p) __raw_writeb(v,__io(p)) #define outw(v,p) __raw_writew((__force __u16) / cpu_to_le16(v),__io(p)) #define outl(v,p) __raw_writel((__force __u32) / cpu_to_le32(v),__io(p))
#define inb(p) ({ __u8 __v = __raw_readb(__io(p)); __v; }) #define inw(p) ({ __u16 __v = le16_to_cpu((__force __le16) / __raw_readw(__io(p))); __v; }) #define inl(p) ({ __u32 __v = le32_to_cpu((__force __le32) / __raw_readl(__io(p))); __v; })
|
I/O內存
#define ioread8(p) ({ unsigned int __v = __raw_readb(p); __v; }) #define ioread16(p) ({ unsigned int __v = le16_to_cpu(__raw_readw(p)); __v; }) #define ioread32(p) ({ unsigned int __v = le32_to_cpu(__raw_readl(p)); __v; })
#define iowrite8(v,p) __raw_writeb(v, p) #define iowrite16(v,p) __raw_writew(cpu_to_le16(v), p) #define iowrite32(v,p) __raw_writel(cpu_to_le32(v), p)
|
我對I/O端口的指令和I/O內存的指令都寫了相應的驅動程序,都通過了測試。在這裏值得注意的有4點:
(1)所有的讀寫指令所賦的地址必須都是虛擬地址,你有兩種選擇:使用內核已經定義好的地址,如 S3C2440_GPJCON等等,這些都是內核定義好的虛擬地址,有興趣的可以看源碼。還有一種方法就是使用自己用ioremap映射的虛擬地址。絕對不能使用實際的物理地址,否則會因爲內核無法處理地址而出現oops。
(2)在使用I/O指令時,可以不使用request_region和request_mem_region,而直接使用outb、ioread等指令。因爲request的功能只是告訴內核端口被誰佔用了,如再次request,內核會制止。
(3)在使用I/O指令時,所賦的地址數據有時必須通過強制類型轉換爲 unsigned long ,不然會有警告(具體原因請看Linux設備驅動程序學習(7)-內核的數據類型) 。雖然你的程序可能也可以使用,但是最好還是不要有警告爲妙。
(4)在include/asm-arm/arch-s3c2410/hardware.h中定義了很多io口的操作函數,有需要可以在驅動中直接使用,很方便。
實驗源碼:
IO_port.tar.gz
IO_port_test.tar.gz
IO_mem.tar.gz
IO_mem_test.tar.gz
兩個模塊都實現了阻塞型獨享設備的訪問控制,並通知內核不支持llseek。具體的測試在IO_port中。
測試現象如下:
[Tekkaman2440@SBC2440V4]#cd /lib/modules/ [Tekkaman2440@SBC2440V4]#insmod IO_port.ko [Tekkaman2440@SBC2440V4]#insmod IO_mem.ko [Tekkaman2440@SBC2440V4]#cat /proc/devices Character devices: 1 mem 2 pty 3 ttyp 4 /dev/vc/0 4 tty 4 ttyS 5 /dev/tty 5 /dev/console 5 /dev/ptmx 7 vcs 10 misc 13 input 14 sound 81 video4linux 89 i2c 90 mtd 116 alsa 128 ptm 136 pts 153 spi 180 usb 189 usb_device 204 s3c2410_serial 251 IO_mem 252 IO_port 253 usb_endpoint 254 rtc
Block devices: 1 ramdisk 256 rfd 7 loop 31 mtdblock 93 nftl 96 inftl 179 mmc [Tekkaman2440@SBC2440V4]#mknod -m 666 /dev/IO_port c 252 0 [Tekkaman2440@SBC2440V4]#mknod -m 666 /dev/IO_mem c 251 0 [Tekkaman2440@SBC2440V4]#cd /tmp/ [Tekkaman2440@SBC2440V4]#./IO_mem_test io_addr : c485e0d0 IO_mem: the module can not lseek! please input the command :1 IO_mem: ioctl 1 ok! please input the command :8 IO_mem: ioctl STATUS ok!current_status=0X1 please input the command :3 IO_mem: ioctl 3 ok! please input the command :q [Tekkaman2440@SBC2440V4]#./IO_porttest_sleep & [Tekkaman2440@SBC2440V4]#./IO_porttest_sleep & [Tekkaman2440@SBC2440V4]#./IO_porttest_sleep & [Tekkaman2440@SBC2440V4]#./IO_port_test IO_port: the module can not lseek! please input the command :1 IO_port: ioctl 1 ok! please input the command :8 IO_port: ioctl STATUS ok!current_status=0X1 please input the command :3 IO_port: ioctl 3 ok! please input the command :8 IO_port: ioctl STATUS ok! current_status=0X3 please input the command :q [1] Done ./IO_porttest_sleep [Tekkaman2440@SBC2440V4]#ps PID Uid VSZ Stat Command 1 root 1744 S init 2 root SW< [kthreadd] 3 root SWN [ksoftirqd/0] 4 root SW< [watchdog/0] 5 root SW< [events/0] 6 root SW< [khelper] 61 root SW< [kblockd/0] 62 root SW< [ksuspend_usbd] 65 root SW< [khubd] 67 root SW< [kseriod] 79 root SW [pdflush] 80 root SW [pdflush] 81 root SW< [kswapd0] 82 root SW< [aio/0] 709 root SW< [mtdblockd] 710 root SW< [nftld] 711 root SW< [inftld] 712 root SW< [rfdd] 746 root SW< [kpsmoused] 755 root SW< [kmmcd] 773 root SW< [rpciod/0] 782 root 1752 S -sh 783 root 1744 S init 785 root 1744 S init 787 root 1744 S init 790 root 1744 S init 843 root 1336 S ./IO_porttest_sleep 844 root 1336 S ./IO_porttest_sleep 846 root 1744 R ps [Tekkaman2440@SBC2440V4]#ps PID Uid VSZ Stat Command 1 root 1744 S init 2 root SW< [kthreadd] 3 root SWN [ksoftirqd/0] 4 root SW< [watchdog/0] 5 root SW< [events/0] 6 root SW< [khelper] 61 root SW< [kblockd/0] 62 root SW< [ksuspend_usbd] 65 root SW< [khubd] 67 root SW< [kseriod] 79 root SW [pdflush] 80 root SW [pdflush] 81 root SW< [kswapd0] 82 root SW< [aio/0] 709 root SW< [mtdblockd] 710 root SW< [nftld] 711 root SW< [inftld] 712 root SW< [rfdd] 746 root SW< [kpsmoused] 755 root SW< [kmmcd] 773 root SW< [rpciod/0] 782 root 1752 S -sh 783 root 1744 S init 785 root 1744 S init 787 root 1744 S init 790 root 1744 S init 847 root 1744 R ps [3] + Done ./IO_porttest_sleep [2] + Done ./IO_porttest_sleep
|
程序是針對2440的,若是用2410只需要改改測試的io口就好了!