Linux裝置驅動程式–與硬體通訊

來源:互聯網
上載者:User

/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, 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 記憶體地區使用前必須先分配,函數介面在 定義:

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_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中。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.