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的含義。我先貼出兩個網上已有的兩種說法(在這裡謝謝兩位高人的分享):
第一種說法:
3. side effect(譯為邊際效應或副作用):是指讀取某個地址時可能導致該地址內容發生變化,比如,有些設備的中斷狀態寄存器只要一讀取,便自動清零。I/O寄存器的操作具有side effect,因此,不能對其操作不能使用cpu緩存。
原文網址:
http://qinbh.blog.sohu.com/62733495.html
第二種說法:
說一下我的理解:I/O端口與實際外部設備相關聯,通過訪問I/O端口控制外部設備,“邊際效應”是指控制設備(讀取或寫入)生效,訪問I/O口的
主要目的就是邊際效應,不像訪問普通的內存,只是在一個位置存儲或讀取一個數值,沒有別的含義了。我是基於arm平台理解的,在《linux設備驅動程
序》第二版中的說法是“副作用”,不是“邊際效應”。
原文網址:
http://linux.chinaunix.net/bbs/viewthread.php?tid=890636&page=1#pid6312646
結合以上兩種說法和自己看《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 內核頭文件(體系依賴的頭文件 ) 定義了下列內聯函數(有的體系是宏,有的不存在)來訪問 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 庫在 中定義了它們。如果在用戶空間代碼中使用必須滿足以下條件:
(1)程序必須使用 -O 選項編譯來強制擴展內聯函數。
(2)必須用ioperm 和 iopl 系統調用(#include ) 來獲得對端口 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, uns
igned 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 內存區域使用前必須先分配,函數接口在 定義:
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 內存的正確方式是通過一系列專用於此目的的函數(在 中定義的):
/*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_r
eadw(__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口就好了