導航:首頁 > 編程系統 > linux設備驅動層

linux設備驅動層

發布時間:2023-08-29 03:58:00

linux設備驅動程序如何與硬體通信

LINUX設備驅動程序是怎麼樣和硬體通信的?下面將由我帶大家來解答這個疑問吧,希望對大家有所收獲!

LINUX設備驅動程序與硬體設備之間的通信

設備驅動程序是軟體概念和硬體電路之間的一個抽象層,因此兩方面都要討論。到目前為止,我們已經討論詳細討論了軟體概念上的一些細節,現在討論另一方面,介紹驅動程序在Linux上如何在保持可移植性的前提下訪問I/O埠和I/O內存。

我們在需要示例的場合會使用簡單的數字I/O埠來講解I/O指令,並使用普通的幀緩沖區顯存來講解內存映射I/O。

I/O埠和I/O內存

計算機對每種外設都是通過讀寫它的寄存器進行控制的。大部分外設都有幾個寄存器,不管是在內存地址空間還是在I/O地址空間,這些寄存器的訪問地址都是連續的。

I/O埠就是I/O埠,設備會把寄存器映射到I/O埠,不管處理器是否具有獨立的I/O埠地址空間。即使沒有在訪問外設時也要模擬成讀寫I/O埠。

I/O內存是設備把寄存器映射到某個內存地址區段(如PCI設備)。這種I/O內存通常是首先方案,它不需要特殊的處理器指令,而且CPU核心訪問內存更有效率。

I/O寄存器和常規內存

盡管硬體寄存器和內存非常相似,但程序員在訪問I/O寄存器的時候必須注意避免由於CPU或編譯器不恰當的優化而改變預期的I/O動作。

I/O寄存器和RAM最主要的區別就是I/O操作具有邊際效應,而內存操作則沒有:由於內存沒有邊際效應,所以可以用多種 方法 進行優化,如使用高速緩存保存數值、重新排序讀/寫指令等。

編譯器能夠將數值緩存在CPU寄存器中而不寫入內存,即使儲存數據,讀寫操作也都能在高速緩存中進行而不用訪問物理RAM。無論是在編譯器一級或是硬體一級,指令的重新排序都有可能發生:一個指令序列如果以不同於程序文本中的次序運行常常能執行得更快。

在對常規內存進行這些優化的時候,優化過程是透明的,而且效果良好,但是對I/O操作來說這些優化很可能造成致命的錯誤,這是因為受到邊際效應的干擾,而這卻是驅動程序訪問I/O寄存器的主要目的。處理器無法預料某些 其它 進程(在另一個處理器上運行,或在在某個I/O控制器中發生的操作)是否會依賴於內存訪問的順序。編譯器或CPU可能會自作聰明地重新排序所要求的操作,結果會發生奇怪的錯誤,並且很難調度。因此,驅動程序必須確保不使用高速緩沖,並且在訪問寄存器時不發生讀或寫指令的重新排序。

由硬體自身引起的問題很解決:只要把底層硬體配置成(可以是自動的或是由Linux初始化代碼完成)在訪問I/O區域(不管是內存還是埠)時禁止硬體緩存即可。

由編譯器優化和硬體重新排序引起的問題的解決辦法是:對硬體(或其他處理器)必須以特定順序的操作之間設置內存屏障(memory barrier)。Linux提供了4個宏來解決所有可能的排序問題:

#include <linux/kernel.h>

void barrier(void)

這個函數通知編譯器插入一個內存屏障,但對硬體沒有影響。編譯後的代碼會把當前CPU寄存器中的所有修改過的數值保存到內存中,需要這些數據的時候再重新讀出來。對barrier的調用可避免在屏障前後的編譯器優化,但硬體完成自己的重新排序。

#include <asm/system.h>

void rmb(void);

void read_barrier_depends(void);

void wmb(void);

void mb(void);

這些函數在已編譯的指令流中插入硬體內存屏障;具體實現方法是平台相關的。rmb(讀內存屏障)保證了屏障之前的讀操作一定會在後來的讀操作之前完成。wmb保證寫操作不會亂序,mb指令保證了兩者都不會。這些函數都是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操作,也能作為內存屏障使用。同時還需要注意,某些外設匯流排(比如PCI匯流排)存在自身的高速緩存問題,我們將在後面的章節中討論相關問題。

在某些體系架構上,允許把賦值語句和內存屏障進行合並以提高效率。內核提供了幾個執行這種合並的宏,在默認情況下,這些宏的定義如下:

#define set_mb(var, value) do {var = value; mb();} while 0

#define set_wmb(var, value) do {var = value; wmb();} while 0

#define set_rmb(var, value) do {var = value; rmb();} while 0

在適當的地方,<asm/system.h>中定義的這些宏可以利用體系架構特有的指令更快的完成任務。注意只有小部分體系架構定義了set_rmb宏。

使用I/O埠

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,如果失敗返回NULL。

所有分配的埠可從/proc/ioports中找到。如果我們無法分配到我們要的埠集合,則可以查看這個文件哪個驅動程序已經分配了這些埠。

如果不再使用這些埠,則用下面函數返回這些埠給系統:

void release_region(unsigned long start, unsigned long n);

下面函數允許驅動程序檢查給定的I/O埠是否可用:

int check_region(unsigned long first, unsigned long n);//不可用返回負的錯誤代碼

我們不贊成用這個函數,因為它返回成功並不能確保分配能夠成功,因為檢查和其後的分配並不是原子操作。我們應該始終使用request_region,因為這個函數執行了必要的鎖定,以確保分配過程以安全原子的方式完成。

操作I/O埠

當驅動程序請求了需要使用的I/O埠范圍後,必須讀取和/或寫入這些埠。為此,大多數硬體都會把8位、16位、32位區分開來。它們不能像訪問系統內存那樣混淆使用。

因此,C語言程序必須調用不同的函數訪問大小不同的埠。那些只支持映射的I/O寄存器的計算機體系架構通過把I/O埠地址重新映射到內存地址來偽裝埠I/O,並且為了易於移植,內核對驅動程序隱藏了這些細節。Linux內核頭文件中(在與體系架構相關的頭文件<asm/io.h>中)定義了如下一些訪問I/O埠的內聯函數:

unsigned inb(unsigned port);

void outb(unsigned char byte, unsigned port);

位元組讀寫埠。

unsigned inw(unsigned port);

void outw(unsigned short word, unsigned port);

訪問16位埠

unsigned inl(unsigned port);

void outl(unsigned longword, unsigned port);

訪問32位埠

在用戶空間訪問I/O埠

上面這些函數主要是提供給設備驅動程序使用的,但它們也可以用戶空間使用,至少在PC類計算機上可以使用。GNU的C庫在<sys/io.h>中定義了這些函數。如果要要用戶空間使用inb及相關函數,則必須滿足正下面這些條件:

編譯程序時必須帶有-O選項來強制內聯函數的展開。

必須用ioperm(獲取單個埠的許可權)或iopl(獲取整個I/O空間)系統調用來獲取對埠進行I/O操作的許可權。這兩個函數都是x86平台特有的。

必須以root身份運行該程序才能調用ioperm或iopl。或者進程的祖先進程之一已經以root身份獲取對埠的訪問。

如果宿主平台沒有以上兩個系統調用,則用戶空間程序仍然可以使用/dev/port設備文件訪問I/O埠。不過要注意,該設備文件的含義與平台密切相關,並且除PC平台以處,它幾乎沒有什麼用處。

串操作

以上的I/O操作都是一次傳輸一個數據,作為補充,有些處理器實現了一次傳輸一個數據序列的特殊指令,序列中的數據單位可以是位元組、字、雙字。這些指令稱為串操作指令,它們執行這些任務時比一個C語言編寫的循環語句快得多。下面列出的宏實現了串I/O:

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

void outsb(unsigned port, void *addr, unsigned long count);從內存addr開始連續讀/寫count數目的位元組。只對單一埠port讀取或寫入數據

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

void outsw(unsigned port, void *addr, unsigned long count);對一個16位埠讀寫16位數據

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

void outsl(unsigned port, void *addr, unsigned long count);對一個32位埠讀寫32位數據

在使用串I/O操作函數時,需要銘記的是:它們直接將位元組流從埠中讀取或寫入。因此,當埠和主機系統具有不同的位元組序時,將導致不可預期的結果。使用inw讀取埠將在必要時交換位元組,以便確保讀入的值匹配於主機的位元組序。然而,串函數不會完成這種交換。

暫停式I/O

在處理器試圖從匯流排上快速傳輸數據時,某些平台(特別是i386)就會出現問題。當處理器時鍾比外設時鍾(如ISA)快時就會出現問題,並且在設備板上特別慢時表現出來。為了防止出現丟失數據的情況,可以使用暫停式的I/O函數來取代通常的I/O函數,這些暫停式的I/O函數很像前面介紹的那些I/O函數,不同之處是它們的名字用_p結尾,如inb_p、outb_p等等。在linux支持的大多數平台上都定義了這些函數,不過它們常常擴展為非暫停式I/O同樣的代碼,因為如果不使用過時的外設匯流排就不需要額外的暫停。

平台相關性

I/O指令是與處理器密切相關的。因為它們的工作涉及到處理器移入移出數據的細節,所以隱藏平台間的差異非常困難。因此,大部分與I/O埠相關的源代碼都與平台相關。

回顧前面函數列表可以看到有一處不兼容的地方,即數據類型。函數的參數根據各平台體系架構上的不同要相應地使用不同的數據類型。例如,port參數在x86平台上(處理器只支持64KB的I/O空間)上定義為unsigned short,但在其他平台上定義為unsigned long,在這些平台上,埠是與內存在同一地址空間內的一些特定區域。

感興趣的讀者可以從io.h文件獲得更多信息,除了本章介紹的函數,一些與體系架構相關的函數有時也由該文件定義。

值得注意的是,x86家族之外的處理器都不為埠提供獨立的地址空間。

I/O操作在各個平台上執行的細節在對應平台的編程手冊中有詳細的敘述;也可以從web上下載這些手冊的PDF文件。

I/O埠示例

演示設備驅動程序的埠I/O的示例代碼運行於通用的數字I/O埠上,這種埠在大多數計算機平台上都能找到。

數字I/O埠最常見的一種形式是一個位元組寬度的I/O區域,它或者映射到內存,或者映射到埠。當把數字寫入到輸出區域時,輸出引腳上的電平信號隨著寫入的各位而發生相應變化。從輸入區域讀取到的數據則是輸入引腳各位當前的邏輯電平值。

這類I/O埠的具體實現和軟體介面是因系統而異的。大多數情況下,I/O引腳由兩個I/O區域控制的:一個區域中可以選擇用於輸入和輸出的引腳,另一個區域中可以讀寫實際的邏輯電平。不過有時情況簡單些,每個位不是輸入就是輸出(不過這種情況下就不能稱為“通用I/O"了);在所有個人計算機上都能找到的並口就是這樣的非通用的I/O埠。

並口簡介

並口的最小配置由3個8位埠組成。第一個埠是一個雙向的數據寄存器,它直接連接到物理連接器的2~9號引腳上。第二個埠是一個只讀的狀態寄存器;當並口連接列印機時,該寄存器 報告 列印機狀態,如是否是線、缺紙、正忙等等。第三個埠是一個只用於輸出的控制寄存器,它的作用之一是控制是否啟用中斷。

如下所示:並口的引腳

示例驅動程序

while(count--) {

outb(*(ptr++), port);

wmb();

}

使用I/O內存

除了x86上普遍使的I/O埠之外,和設備通信的另一種主要機制是通過使用映射到內存的寄存器或設備內存,這兩種都稱為I/O內存,因為寄存器和內存的差別對軟體是透明的。

I/O內存僅僅是類似RAM的一個區域,在那裡處理器可以通過匯流排訪問設備。這種內存有很多用途,比如存放視頻數據或乙太網數據包,也可以用來實現類似I/O埠的設備寄存器(也就是說,對它們的讀寫也存在邊際效應)。

根據計算機平台和所使用匯流排的不同,i/o內存可能是,也可能不是通過頁表訪問的。如果訪問是經由頁表進行的,內核必須首先安排物理地址使其對設備驅動程序可見(這通常意味著在進行任何I/O之前必須先調用ioremap)。如果訪問無需頁表,那麼I/O內存區域就非常類似於I/O埠,可以使用適當形式的函數讀取它們。

不管訪問I/O內存是否需要調用ioremap,都不鼓勵直接使用指向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得到。

不再使用已分配的內存區域時,使用如下介面釋放:

void release_mem_region(unsigned long start, unsigned long len);

下面函數用來檢查給定的I/O內存區域是否可用的老函數:

int check_mem_region(unsigned long start, unsigned long len);//這個函數和check_region一樣不安全,應避免使用

分配內存之後我們還必須確保該I/O內存對內存而言是可訪問的。獲取I/O內存並不意味著可引用對應的指針;在許多系統上,I/O內存根本不能通過這種方式直接訪問。因此,我們必須由ioremap函數建立映射,ioremap專用於為I/O內存區域分配虛擬地址。

我們根據以下定義來調用ioremap函數:

#include <asm/io.h>

void *ioremap(unsigned long phys_addr, unsigned long size);

void *ioremap_nocache(unsigned long phys_addr, unsigned long size);在大多數計算機平台上,該函數和ioremap相同:當所有I/O內存已屬於非緩存地址時,就沒有必要實現ioremap的獨立的,非緩沖版本。

void iounmap(void *addr);

記住,由ioremap返回的地址不應該直接引用,而應該使用內核提供的accessor函數。

訪問I/O內存

在某些平台上我們可以將ioremap的返回值直接當作指針使用。但是,這種使用不具有可移植性,訪問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內存地址處讀/寫一系列值,則可使用上述函數的重復版本:

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);

上述函數從給定的buf向給定的addr讀取或寫入count個值。count以被寫入數據的大小為單位。

上面函數均在給定的addr處執行所有的I/O操作,如果我們要在一塊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);

上述函數和C函數庫的對應函數功能一致。

像I/O內存一樣使用I/O埠

某些硬體具有一種有趣的特性:某些版本使用I/O埠,而其他版本則使用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埠。

為I/O內存重用short

前面介紹的short示例模塊訪問的是I/O埠,它也可以訪問I/O內存。為此必須在載入時通知它使用I/O內存,另外還要修改base地址以使其指向I/O區域。

下例是在MIPS開發板上點亮調試用的LED:

mips.root# ./short_load use_mem=1 base = 0xb7ffffc0

mips.root# echo -n 7 > /dev/short0

下面代碼是short寫入內存區域時使用的循環:

while(count--) {

iowrite8(*ptr++, address);

wmb();

}

1MB地址空間之下的ISA內存

最廣為人知的I/O內存區之一就是個人計算機上的ISA內存段。它的內存范圍在64KB(0xA0000)到1MB(0x100000)之間,因此它正好出現在常規系統RAM的中間。這種地址看上去有點奇怪,因為這個設計決策是20世紀80年代早期作出的,在當時看來沒有人會用到640KB以上的內存。

❷ 如何學習Linux設備驅動

通常,內核的升級對從事linux應用程序開發的人員來說影響較小,因為系統調用基本保持兼容,影響比較大的是驅動開發人員。每次內核的更新都可能導致許多內核函數原型上的變化,其中既有內核本身提供的函數,也有硬體平台代碼提供的函數,後者變化的更加頻繁。這一點從許多經典書籍就可驗證,當你按照手裡的經典著作,如:Alessandro的《linux設備驅動程序》,編寫驅動時,發現並不能夠成功的在你的linux平台上編譯通過、或不能正常執行,原因就在於你用的內核和書里的不一致。
本文從兩個方面去解釋這個問題,一方面是如何寫好linux設備驅動,另一方面是如何應對不斷升級的內核。
如何寫好Linux設備驅動
Linux設備驅動是linux內核的一部分,是用來屏蔽硬體細節,為上層提供標准介面的一種技術手段。為了能夠編寫出質量比較高的驅動程序,要求工程師必須具備以下幾個方面的知識:
● 熟悉處理器的性能
如:處理器的體系結構、匯編語言、工作模式、異常處理等。對於初學者來說,在還不熟悉驅動編寫方法的情況下,可以先不把重心放在這一項上,因為可能因為它的枯燥、抽象而影響到你對設備驅動的興趣。隨著你不斷地熟悉驅動的編寫,你會很自然的意識到此項的重要性。
● 掌握驅動目標的硬體工作原理及通訊協議
如:串口控制器、顯卡控制器、硬體編解碼、存儲卡控制器、I2C通訊、SPI通訊、USB通訊、SDIO通訊、I2S通訊、PCI通訊等。編寫設備驅動的前提就是需要了解設備的操作方法,所以這些內容的重要程度不言而喻。但不是說要把所有設備的操作方法都熟悉了以後才可以寫驅動,你只需要了解你要驅動的硬體就可以了。
● 掌握硬體的控制方法
如:中斷、輪詢、DMA 等,通常一個硬體控制器會有多種控制方法,你需要根據系統性能的需要合理的選擇操作方法。初學階段以實現功能為目的,掌握的順序應該是,輪詢->中斷->DMA。隨著學習的深入,需要綜合考慮系統的性能需求,採取合適的方法。
● 良好的GNU C語言編程基礎
如:C語言的指針、結構體、內存操作、鏈表、隊列、棧、C和匯編混合編程等。這些編程語法是編寫設備驅動的基礎,無論對於初學者還是有經驗者都非常重要。
● 良好的linux操作系統概念
如:多進程、多線程、進程調度、進程搶占、進程上下文、虛擬內存、原子操作、阻塞、睡眠、同步等概念及它們之間的關系。這些概念及方法在設備驅動里的使用是linux設備驅動區別單片機編程的最大特點,只有理解了它們才會編寫出高質量的驅動。
● 掌握linux內核中設備驅動的編寫介面
如:字元設備的cdev、塊設備的gendisk、網路設備的net_device,以及基於這些基本介面的framebuffer設備的fb_info、mtd設備的mtd_info、tty設備的tty_driver、usb設備的usb_driver、mmc設備的mmc_host等。
Linux內核為設備驅動編寫者提供了標準的介面,驅動編寫者無需精通內核的各個部分,只需要明確內核提供給我們的介面,並實現此介面就可以了。內核提供的介面採用的是面向對象的思路,即把目標設備抽象成一個對象,通常利用一個結構體來描述這個對象。驅動工程師的任務就是實現這個對象。這個結構體中會包含設備的屬性(用變數表示)和操作方法(用函數指針表示)。如:字元設備的cdev
struct cdev {
struct kobject kobj;
struct mole *owner;
const struct file_operations *ops; // 操作方法結合,其它項都是屬性
struct list_head list;
dev_t dev;
unsigned int count;
};
開始階段可以以模仿為主,即套用一些固定的模板、參考常式。
如何應對不斷升級的內核
內核升級對驅動的影響主要體現在,(1)驅動介面定義的變化;(2)內核的一些功能函數的名稱、參數、頭文件、宏定義的變化;(3)平台代碼關於硬體操作方面封裝的一些函數的變化;(4)設備模型的影響。
● 驅動介面定義的變化
如:2.4內核中字元設備驅動的注冊介面是:
int register_chrdev(unsigned int major, const char * name, struct file_operations *fops)
而2.6內核中已經不建議使用這種方法了,改為:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
這種介面定義及注冊方法帶來的變化,發生的並不頻繁。解決方案是:參考內核中的代碼。這種介面定義及注冊方法在內核中非常容易找到,如:字元設備驅動的注冊方法及介面定義可以參照內核driver/char/目錄下的很多實例。
● 內核的一些功能函數的名稱、參數、頭文件、宏定義的變化
如:中斷注冊函數的格式及參數在2.4內核、2.6內核低版本和高版本之間都存在差別,在2.6.8中,中斷注冊函數的定義為:
int request_irq(unsigned int irq, irqreturn_t (*handler)(int, void *, struct pt_regs *),unsigned long irq_flags, const char * devname, void *dev_id)
irq_flags的取值主要為下面的某一種或組合: SA_INTERRUPT、SA_SAMPLE_RANDOM、SA_SHIRQ
在2.6.26中,中斷注冊函數的定義為:
int request_irq(unsigned int irq, irq_handler_t handler,unsigned long irqflags, const char *devname, void *dev_id)
typedef irqreturn_t (*irq_handler_t)(int, void *); irq_flags的取值主要為下面的某一種或組合:(功能和2.6.8的對應)IRQF_DISABLED、IRQF_SAMPLE_RANDOM、IRQF_SHARED
當出現這些問題時,編譯過程中,編譯器會給我們比較明確的錯誤提示,根據這些提示你可以判斷出是否是缺少頭文件問題、是否是函數參數定義有誤等。解決問題的最好辦法還是到你的目標內核中找信息。此時找問題的方法可以藉助於搜索,如:你可以在新的內核中搜索request_irq,看新內核中的驅動是如何使用它的,這種方法非常有效。
● 平台代碼關於硬體操作方面封裝的一些函數的變化
內核中,硬體平台相關的代碼在內核更新過程中變化比較頻繁,和我們的設備驅動也是息息相關,所以在針對一個新內核編寫設備驅動前,一定要熟悉你的平台代碼的結構。有時平台雖然提供了內核要求的介面函數,但使用起來功能卻並不完善。下面還是先舉個例子說明平台代碼更新對設備驅動的影響。
如:在linux-2.6.8內核中,調用set_irq_type(IRQ_EINT0,IRQT_FALLING);去設置S3C2410的IRQ_EINT0的中斷觸發信號類型,你會發現不會有什麼效果。跟蹤代碼發現內核的set_irq_type函數需要平台提供一個針對硬體平台的實現函數
static struct irqchip s3c_irqext_chip = {
.mask = s3c_irqext_mask,
.unmask = s3c_irqext_unmask,
.ack = s3c_irqext_ack,
.type = s3c_irqext_type
};
s3c_irqext_type就是linux內核需要的實現函數,而s3c_irqext_type在2.6.8中的實現為: static int s3c_irqext_type(unsigned int irq, unsigned int type)
{
irqdbf("s3c_irqext_type: called for irq %d, type %d\n", irq, type);
return 0;
}
原來並沒有實現。而在較高版本的內核,如2.6.26內核中,這個函數是實現了的。所以你一定要小心。當平台函數不好用時,一定要查查原因,或者直接操作硬體寄存器來達到目的。
● 2.6內核設備模型對驅動的影響
在2.6內核中寫設備驅動和在2.4內核中有著很大的不同,主要就是在設備驅動中融入了比設備驅動本身結構還復雜、還難以理解的設備模型。初學驅動時你可以不理會設備模型,但你會發現內核里的驅動代碼基本上都是融入了設備模型的了。所以很多時候你不得不面對現實,還是要弄懂它,並且它也的注冊方法也會隨著內核的升級而發生變化。解決此類問題的最好方法還是參考目標內核驅動代碼。

❸ linux設備驅動好學么需要什麼基礎

你好,首先我要說明我是ARM嵌入式開發工程師,偏驅動向
驅動難,沒有3年以上的工作經驗 都沒專什麼料子的
要寫驅動屬
你要很熟悉linux C 而且注重字元串和指針處理
然後你就學數字電路 要多懂
其次要學會看datasheet
這些學得差不多了就進入了內核,為什麼要知道內核(只需要知道內核的工作原理機制還有版本變動),因為內核跟驅動是不分家的,你寫了驅動就知道
然後就是進入主菜,寫驅動,看書時必須的,我看驅動的書不下10本,而且盡量消化,隨後你就嘗試寫某些驅動,只要你會一個類型的就會這一類型的驅動,同類驅動是萬變不離其宗 不要想著全部驅動都學會,要學精!像NV realtek這些厲害的工程師 一輩子就寫兩個驅動(當然,所有驅動中為網卡驅動和顯卡驅動最難最容易出錯!同時也是最厲害的人去寫的)
驅動!路漫漫其修遠兮!

❹ linux驅動程序結構框架及工作原理分別是什麼

一、Linux device driver 的概念x0dx0ax0dx0a系統調用是操作系統內核和應用程序之間的介面,設備驅動程序是操作系統內核和機器硬體之間的介面。設備驅動程序為應用程序屏蔽了硬體的細節,這樣在應用程序看來,硬體設備只是一個設備文件,應用程序可以象操作普通文件一樣對硬體設備進行操作。設備驅動程序是內核的一部分,它完成以下的功能:x0dx0ax0dx0a1、對設備初始化和釋放;x0dx0ax0dx0a2、把數據從內核傳送到硬體和從硬體讀取數據;x0dx0ax0dx0a3、讀取應用程序傳送給設備文件的數據和回送應用程序請求的數據;x0dx0ax0dx0a4、檢測和處理設備出現的錯誤。x0dx0ax0dx0a在Linux操作系統下有三類主要的設備文件類型,一是字元設備,二是塊設備,三是網路設備。字元設備和塊設備的主要區別是:在對字元設備發出讀/寫請求時,實際的硬體I/O一般就緊接著發生了,塊設備則不然,它利用一塊系統內存作緩沖區,當用戶進程對設備請求能滿足用戶的要求,就返回請求的數據,如果不能,就調用請求函數來進行實際的I/O操作。塊設備是主要針對磁碟等慢速設備設計的,以免耗費過多的CPU時間來等待。x0dx0ax0dx0a已經提到,用戶進程是通過設備文件來與實際的硬體打交道。每個設備文件都都有其文件屬性(c/b),表示是字元設備還是塊設備?另外每個文件都有兩個設備號,第一個是主設備號,標識驅動程序,第二個是從設備號,標識使用同一個設備驅動程序的不同的硬體設備,比如有兩個軟盤,就可以用從設備號來區分他們。設備文件的的主設備號必須與設備驅動程序在登記時申請的主設備號一致,否則用戶進程將無法訪問到驅動程序。x0dx0ax0dx0a最後必須提到的是,在用戶進程調用驅動程序時,系統進入核心態,這時不再是搶先式調度。也就是說,系統必須在你的驅動程序的子函數返回後才能進行其他的工作。如果你的驅動程序陷入死循環,不幸的是你只有重新啟動機器了,然後就是漫長的fsck。x0dx0ax0dx0a二、實例剖析x0dx0ax0dx0a我們來寫一個最簡單的字元設備驅動程序。雖然它什麼也不做,但是通過它可以了解Linux的設備驅動程序的工作原理。把下面的C代碼輸入機器,你就會獲得一個真正的設備驅動程序。x0dx0ax0dx0a由於用戶進程是通過設備文件同硬體打交道,對設備文件的操作方式不外乎就是一些系統調用,如 open,read,write,close?, 注意,不是fopen, fread,但是如何把系統調用和驅動程序關聯起來呢?這需要了解一個非常關鍵的數據結構:x0dx0ax0dx0aSTruct file_operatiONs {x0dx0ax0dx0aint (*seek) (struct inode * ,struct file *, off_t ,int);x0dx0ax0dx0aint (*read) (struct inode * ,struct file *, char ,int);x0dx0ax0dx0aint (*write) (struct inode * ,struct file *, off_t ,int);x0dx0ax0dx0aint (*readdir) (struct inode * ,struct file *, struct dirent * ,int);x0dx0ax0dx0aint (*select) (struct inode * ,struct file *, int ,select_table *);x0dx0ax0dx0aint (*ioctl) (struct inode * ,struct file *, unsined int ,unsigned long);x0dx0ax0dx0aint (*mmap) (struct inode * ,struct file *, struct vm_area_struct *);x0dx0ax0dx0aint (*open) (struct inode * ,struct file *);x0dx0ax0dx0aint (*release) (struct inode * ,struct file *);x0dx0ax0dx0aint (*fsync) (struct inode * ,struct file *);x0dx0ax0dx0aint (*fasync) (struct inode * ,struct file *,int);x0dx0ax0dx0aint (*check_media_change) (struct inode * ,struct file *);x0dx0ax0dx0aint (*revalidate) (dev_t dev);x0dx0ax0dx0a}x0dx0ax0dx0a這個結構的每一個成員的名字都對應著一個系統調用。用戶進程利用系統調用在對設備文件進行諸如read/write操作時,系統調用通過設備文件的主設備號找到相應的設備驅動程序,然後讀取這個數據結構相應的函數指針,接著把控制權交給該函數。這是linux的設備驅動程序工作的基本原理。既然是這樣,則編寫設備驅動程序的主要工作就是編寫子函數,並填充file_operations的各個域。x0dx0ax0dx0a下面就開始寫子程序。x0dx0ax0dx0a#include 基本的類型定義x0dx0ax0dx0a#include 文件系統使用相關的頭文件x0dx0ax0dx0a#include x0dx0ax0dx0a#include x0dx0ax0dx0a#include x0dx0ax0dx0aunsigned int test_major = 0;x0dx0ax0dx0astatic int read_test(struct inode *inode,struct file *file,char *buf,int count)x0dx0ax0dx0a{x0dx0ax0dx0aint left; 用戶空間和內核空間x0dx0ax0dx0aif (verify_area(VERIFY_WRITE,buf,count) == -EFAULT )x0dx0ax0dx0areturn -EFAULT;x0dx0ax0dx0afor(left = count ; left > 0 ; left--)x0dx0ax0dx0a{x0dx0ax0dx0a__put_user(1,buf,1);x0dx0ax0dx0abuf++;x0dx0ax0dx0a}x0dx0ax0dx0areturn count;x0dx0ax0dx0a}x0dx0ax0dx0a這個函數是為read調用准備的。當調用read時,read_test()被調用,它把用戶的緩沖區全部寫1。buf 是read調用的一個參數。它是用戶進程空間的一個地址。但是在read_test被調用時,系統進入核心態。所以不能使用buf這個地址,必須用__put_user(),這是kernel提供的一個函數,用於向用戶傳送數據。另外還有很多類似功能的函數。請參考,在向用戶空間拷貝數據之前,必須驗證buf是否可用。這就用到函數verify_area。為了驗證BUF是否可以用。x0dx0ax0dx0astatic int write_test(struct inode *inode,struct file *file,const char *buf,int count)x0dx0ax0dx0a{x0dx0ax0dx0areturn count;x0dx0ax0dx0a}x0dx0ax0dx0astatic int open_test(struct inode *inode,struct file *file )x0dx0ax0dx0a{x0dx0ax0dx0aMOD_INC_USE_COUNT; 模塊計數加以,表示當前內核有個設備載入內核當中去x0dx0ax0dx0areturn 0;x0dx0ax0dx0a}x0dx0ax0dx0astatic void release_test(struct inode *inode,struct file *file )x0dx0ax0dx0a{x0dx0ax0dx0aMOD_DEC_USE_COUNT;x0dx0ax0dx0a}x0dx0ax0dx0a這幾個函數都是空操作。實際調用發生時什麼也不做,他們僅僅為下面的結構提供函數指針。x0dx0ax0dx0astruct file_operations test_fops = {?x0dx0ax0dx0aread_test,x0dx0ax0dx0awrite_test,x0dx0ax0dx0aopen_test,x0dx0ax0dx0arelease_test,x0dx0ax0dx0a};x0dx0ax0dx0a設備驅動程序的主體可以說是寫好了。現在要把驅動程序嵌入內核。驅動程序可以按照兩種方式編譯。一種是編譯進kernel,另一種是編譯成模塊(moles),如果編譯進內核的話,會增加內核的大小,還要改動內核的源文件,而且不能動態的卸載,不利於調試,所以推薦使用模塊方式。x0dx0ax0dx0aint init_mole(void)x0dx0ax0dx0a{x0dx0ax0dx0aint result;x0dx0ax0dx0aresult = register_chrdev(0, "test", &test_fops); 對設備操作的整個介面x0dx0ax0dx0aif (result < 0) {x0dx0ax0dx0aprintk(KERN_INFO "test: can't get major number\n");x0dx0ax0dx0areturn result;x0dx0ax0dx0a}x0dx0ax0dx0aif (test_major == 0) test_major = result; /* dynamic */x0dx0ax0dx0areturn 0;x0dx0ax0dx0a}x0dx0ax0dx0a在用insmod命令將編譯好的模塊調入內存時,init_mole 函數被調用。在這里,init_mole只做了一件事,就是向系統的字元設備表登記了一個字元設備。register_chrdev需要三個參數,參數一是希望獲得的設備號,如果是零的話,系統將選擇一個沒有被佔用的設備號返回。參數二是設備文件名,參數三用來登記驅動程序實際執行操作的函數的指針。x0dx0ax0dx0a如果登記成功,返回設備的主設備號,不成功,返回一個負值。x0dx0ax0dx0avoid cleanup_mole(void)x0dx0ax0dx0a{x0dx0ax0dx0aunregister_chrdev(test_major,"test");x0dx0ax0dx0a}x0dx0ax0dx0a在用rmmod卸載模塊時,cleanup_mole函數被調用,它釋放字元設備test在系統字元設備表中佔有的表項。x0dx0ax0dx0a一個極其簡單的字元設備可以說寫好了,文件名就叫test.c吧。x0dx0ax0dx0a下面編譯 :x0dx0ax0dx0a$ gcc -O2 -DMODULE -D__KERNEL__ -c test.c _c表示輸出制定名,自動生成.o文件x0dx0ax0dx0a得到文件test.o就是一個設備驅動程序。x0dx0ax0dx0a如果設備驅動程序有多個文件,把每個文件按上面的命令行編譯,然後x0dx0ax0dx0ald ?-r ?file1.o ?file2.o ?-o ?molename。x0dx0ax0dx0a驅動程序已經編譯好了,現在把它安裝到系統中去。x0dx0ax0dx0a$ insmod ?_f ?test.ox0dx0ax0dx0a如果安裝成功,在/proc/devices文件中就可以看到設備test,並可以看到它的主設備號。要卸載的話,運行 :x0dx0ax0dx0a$ rmmod testx0dx0ax0dx0a下一步要創建設備文件。x0dx0ax0dx0amknod /dev/test c major minorx0dx0ax0dx0ac 是指字元設備,major是主設備號,就是在/proc/devices里看到的。x0dx0ax0dx0a用shell命令x0dx0ax0dx0a$ cat /proc/devicesx0dx0ax0dx0a就可以獲得主設備號,可以把上面的命令行加入你的shell script中去。x0dx0ax0dx0aminor是從設備號,設置成0就可以了。x0dx0ax0dx0a我們現在可以通過設備文件來訪問我們的驅動程序。寫一個小小的測試程序。x0dx0ax0dx0a#include x0dx0ax0dx0a#include x0dx0ax0dx0a#include x0dx0ax0dx0a#include x0dx0ax0dx0amain()x0dx0ax0dx0a{x0dx0ax0dx0aint testdev;x0dx0ax0dx0aint i;x0dx0ax0dx0achar buf[10];x0dx0ax0dx0atestdev = open("/dev/test",O_RDWR);x0dx0ax0dx0aif ( testdev == -1 )x0dx0ax0dx0a{x0dx0ax0dx0aprintf("Cann't open file \n");x0dx0ax0dx0aexit(0);x0dx0ax0dx0a}x0dx0ax0dx0aread(testdev,buf,10);x0dx0ax0dx0afor (i = 0; i < 10;i++)x0dx0ax0dx0aprintf("%d\n",buf[i]);x0dx0ax0dx0aclose(testdev);x0dx0ax0dx0a}x0dx0ax0dx0a編譯運行,看看是不是列印出全1 x0dx0ax0dx0a以上只是一個簡單的演示。真正實用的驅動程序要復雜的多,要處理如中斷,DMA,I/O port等問題。這些才是真正的難點。上述給出了一個簡單的字元設備驅動編寫的框架和原理,更為復雜的編寫需要去認真研究LINUX內核的運行機制和具體的設備運行的機制等等。希望大家好好掌握LINUX設備驅動程序編寫的方法。

❺ Linux驅動與設備節點簡介 & Android內核與Linux內核的區別

驅動是內核的一部分,作為直接訪問物理硬體的一個軟體層,用於應用程序與物理硬體設備通信。內核包含多種驅動,如WIFI、USB、Audio、藍牙、相機、顯示驅動。

(1)設備驅動程序三類:字元設備驅動程序、塊設備驅動程序、網路設備驅動程序;

(2)對應Linux三類設備:字元設備、塊設備、網路設備;

(3)常見字元設備:滑鼠、鍵盤、串口、控制台等;

(4)常見塊設備:各種硬碟、flash磁碟、RAM磁碟等;

(5)網路設備(網路介面):eth0、eth1,註:網路設備沒有設備節點,應用程序通過Socket訪問網路設備。由於網路設備面向報文,較難實現相關read、write等文件讀寫函數,所以驅動的實現也與字元設備和塊設備不同。

Linux使用對文件一樣的管理方式來管理設備,所有設備都以文件的形式存放在/dev目錄下,系統中的每個字元設備或者塊設備都必須為其創建一個設備文件,它包含了該設備的設備類型(塊設備或字元設滾橋備)、設備號(主設備號和次設備號)以及設備訪問控制屬性等。設備節點通過 mknod 命令創建,也可以由Udev用戶工具軟體在系統啟動後根據/sys目錄下每個設備的實際信息創建,使用後一種方式可以為每個設備動態分配設備號。

Linux中設備節點通過「mknod」命令創建,創建時需要指定主設備號和次設備號,即指定對應的驅動程序和對應的物理設備(訪問設備節點時就相當於通過其設備號訪問驅動程序進而間接訪問到物理設備)。主設備號用來區分不同種類的設備,而次設備號用來區分同一類大舉猛型的多個設備。對於常用設備,Linux有約定俗成的編號,如硬碟的主設備號是3

理解:應用程序通過訪問設備節點讀取主設備號和次設備號,通過主設答枯備號找對應的驅動,通過次設備號對應到具體物理設備。註:1個驅動對應一類設備,並用唯一主設備號標識。

Linux支持的各種設備的主設備號定義在include/linux/major.h文件中,已經在官方注冊的主設備號和次設備號在Documentation/devices.txt文件中。

Android系統最底層是Linux,並且在中間加上了一個Dalvik / ART的Java虛擬機,從表面層看是Android運行庫。每個Android應用都運行在自己的進程上,享有Dalvik / ART虛擬機為它分配的專有實例,並支持多個虛擬機在同一設備上高效運行,虛擬機執行的是專有格式的可執行文件(.dex) - 該格式經過優化,以將內存好用降到最低。

Android內核和Linux內核的差別主要體現在如下11個方面:

❻ Linux網路設備驅動的結構

Linux網路設備驅動程序的體系結構從上到下可以劃分為4層,依次為網路協議介面層、網路設備介面層、提供實際功能的設備驅動功能層以及網路設備與媒介層,這4層的作用如下所示。
1)網路協議介面層向網路層協議提供統一的數據包收發介面,不論上層協議是ARP,還是IP,都通過dev_queue_xmit() 函數發送數據,並通過netif rx ()函數接收數據。這一層的存在使得上層協議獨立於具體的設備。
2)網路設備介面層向協議介面層提供統一的用於描述具體網路設備屬性和操作的結構體net device,該結構體是設備驅動功能層中各函數的容器。實際上,網路設備介面層從宏觀上規劃了具體操作硬體的設備驅動功能層的結構。
3)設備驅動功能層的各函數是網路設備介面層net_device數據結構的具體成員,是驅使網路設備硬體完成相應動作的程序,它通過hard_start_ xmit ()函數啟動發送操作,並通過網路設備上的中斷觸發接收操作。
4)網路設備與媒介層是完成數據包發送和接收的物理實體,包括網路適配器和具體的傳輸媒介,網路適配器被設備驅動功能層中的函數在物理上驅動。對於Linux系統而言,網路設備和媒介都可以是虛擬的。

❼ 《Linux設備驅動程序》(十六)-中斷處理

設備與處理器之間的工作通常來說是非同步,設備數據要傳遞給處理器通常來說有以下幾種方法:輪詢、等待和中斷。

讓CPU進行輪詢等待總是不能讓人滿意,所以通常都採用中斷的形式,讓設備來通知CPU讀取數據。

2.6內核的函數參數與現在的參數有所區別,這里都主要介紹概念,具體實現方法需要結合具體的內核版本。

request_irq函數申請中斷,返回0表示申請成功,其他返回值表示申請失敗,其具體參數解釋如下:

flags 掩碼可以使用以下幾個:

快速和慢速處理常式 :現代內核中基本沒有這兩個概念了,使用SA_INTERRUPT位後,當中斷被執行時,當前處理器的其他中斷都將被禁止。通常不要使用SA_INTERRUPT標志位,除非自己明確知道會發生什麼。

共享中斷 :使用共享中斷時,一方面要使用SA_SHIRQ位,另一個是request_irq中的dev_id必須是唯一的,不能為NULL。這個限制的原因是:內核為每個中斷維護了一個共享處理常式的列表,常式中的dev_id各不相同,就像設備簽名。如果dev_id相同,在卸載的時候引起混淆(卸載了另一個中斷),當中斷到達時會產生內核OOP消息。

共享中斷需要滿足以下一個條件才能申請成功:

當不需要使用該中斷時,需要使用free_irq釋放中斷。

通常我們會在模塊載入的時候申請安裝中斷處理常式,但書中建議:在設備第一次打開的時候安裝,在設備最後一次關閉的時候卸載。

如果要查看中斷觸發的次數,可以查看 /proc/interrupts 和 /proc/stat。

書中講述了如何自動檢測中斷號,在嵌入式開發中通常都是查看原理圖和datasheet來直接確定。

自動檢測的原理如下:驅動程序通知設備產生中斷,然後查看哪些中斷信號線被觸發了。Linux提供了以下方法來進行探測:

探測工作耗時較長,建議在模塊載入的時候做。

中斷處理函數和普通函數其實差不多,唯一的區別是其運行的中斷上下文中,在這個上下文中有以下注意事項:

中斷處理函數典型用法如下:

中斷處理函數的參數和返回值含義如下:

返回值主要有兩個:IRQ_NONE和IRQ_HANDLED。

對於中斷我們是可以進行開啟和關閉的,Linux中提供了以下函數操作單個中斷的開關:

該方法可以在所有處理器上禁止或啟用中斷。

需要注意的是:

如果要關閉當前處理器上所有的中斷,則可以調用以下方法:

local_irq_save 會將中斷狀態保持到flags中,然後禁用處理器上的中斷;如果明確知道中斷沒有在其他地方被禁用,則可以使用local_irq_disable,否則請使用local_irq_save。

locat_irq_restore 會根據上面獲取到flags來恢復中斷;local_irq_enable 會無條件打開所有中斷。

在中斷中需要做一些工作,如果工作內容太多,必然導致中斷處理所需的時間過長;而中斷處理又要求能夠盡快完成,這樣才不會影響正常的系統調度,這兩個之間就產生了矛盾。

現在很多操作系統將中斷分為兩個部分來處理上面的矛盾:頂半部和底半部。

頂半部就是我們用request_irq來注冊的中斷處理函數,這個函數要求能夠盡快結束,同時在其中調度底半部,讓底半部在之後來進行後續的耗時工作。

頂半部就不再說明了,就是上面的中斷處理函數,只是要求能夠盡快處理完成並返回,不要處理耗時工作。

底半部通常使用tasklet或者工作隊列來實現。

tasklet的特點和注意事項:

工作隊列的特點和注意事項:

❽ linux主機側與設備側USB驅動

USB採用樹形拓撲結構,主機側和設備側的USB控制器分別稱為主機控制器((Host Controller)和USB設備控制器(UDC),每條匯流排上只有一個主機控制器,負責協調主機和設備間的通信,而設備不能主動向主機發送任何消息。

在Linux系統中,USB驅動可以從兩個角度去觀察,一個角度是主機側,一個角度是設備側。從上圖主機側去看,在Linux驅動中,處於USB驅動最底層的是USB主機控制器硬體,在其上運行的是USB主機控制器驅動,在主機控制器上的為USB核心層,再上層為USB設備驅動層(插入主機上的U盤、滑鼠、USB轉串口等設備驅動)。因此,在主機側的層次結構中,要實現的USB驅動包括兩類:USB主機控制器驅動和USB設備驅動,前者控制插入其中的USB設備,後者控制USB設備如何與主機通信。Linux內核中的USB核心負責USB驅動管理和協議處理的主要工作。主機控制器驅動和設備驅動之間的USB核心非常重要,其功能包括:通過定義一些數據結構、宏和功能函數,向上為設備驅動提供編程介面,向下為USB主機控制器驅動提供編程介面;維護整個系統的USB設備信息;完成設備熱插拔控制、匯流排數據傳輸控制等。

閱讀全文

與linux設備驅動層相關的資料

熱點內容
微信小程序報表控制項 瀏覽:722
ipad添加不了qq郵箱賬號和密碼 瀏覽:503
matlab中文文件名 瀏覽:236
excel是啥類型文件 瀏覽:471
安慶培訓編程怎麼選 瀏覽:817
vs2010mfc工具箱 瀏覽:402
刪除文件重置win10怎麼恢復 瀏覽:180
新裝win10有線連不上 瀏覽:785
資料庫商品名唯一怎麼標注 瀏覽:214
電信5g網路什麼時候出 瀏覽:626
手機如何把視頻打包到文件夾裡面 瀏覽:248
excel數據轉vcf文件 瀏覽:585
編程題如何左右翻轉 瀏覽:920
wp10微信安裝不了 瀏覽:793
微軟資料庫密碼怎麼改 瀏覽:627
我們是貴族什麼app可以看 瀏覽:65
哪個app認字免費 瀏覽:964
少兒編程線上教育如何 瀏覽:137
ps3升級檢測不到優盤升級文件 瀏覽:845
vs連接資料庫顯示數據 瀏覽:867

友情鏈接