Linux設(shè)備驅(qū)動程序--與硬件通信
I/O 端口和 I/O 內(nèi)存
每種外設(shè)都是通過讀寫寄存器來進(jìn)行控制。
在硬件層,內(nèi)存區(qū)和 I/O 區(qū)域沒有概念上的區(qū)別: 它們都是通過向在地址總線和控制總線發(fā)出電平信號來進(jìn)行訪問,再通過數(shù)據(jù)總線讀寫數(shù)據(jù)。
因為外設(shè)要與I\O總線匹配,而大部分流行的 I/O 總線是基于個人計算機(jī)模型(主要是 x86 家族:它為讀和寫 I/O 端口提供了獨立的線路和特殊的 CPU 指令),所以即便那些沒有單獨I/O 端口地址空間的處理器,在訪問外設(shè)時也要模擬成讀寫I\O端口。這一功能通常由外圍芯片組(PC 中的南北橋)或 CPU 中的附加電路實現(xiàn)(嵌入式中的方法) 。
Linux 在所有的計算機(jī)平臺上實現(xiàn)了 I/O 端口。但不是所有的設(shè)備都將寄存器映射到
I/O 端口。雖然ISA設(shè)備普遍使用 I/O 端口,但大部分 PCI 設(shè)備則把寄存器映射到某個內(nèi)存地址區(qū),這種 I/O
內(nèi)存方法通常是首選的。因為它無需使用特殊的處理器指令,CPU
核訪問內(nèi)存更有效率,且編譯器在訪問內(nèi)存時在寄存器分配和尋址模式的選擇上有更多自由。
I/O 寄存器和常規(guī)內(nèi)存
在進(jìn)入這部分學(xué)習(xí)的時候,首先要理解一個概念:side
effect,書中譯為邊際效應(yīng),第二版譯為副作用。我覺得不管它是怎么被翻譯的,都不可能精準(zhǔn)表達(dá)原作者的意思,所以我個人認(rèn)為記住side
effect就好。下面來講講side effect的含義。我先貼出兩個網(wǎng)上已有的兩種說法(在這里謝謝兩位高人的分享):
第一種說法:
3. side effect(譯為邊際效應(yīng)或副作用):是指讀取某個地址時可能導(dǎo)致該地址內(nèi)容發(fā)生變化,比如,有些設(shè)備的中斷狀態(tài)寄存器只要一讀取,便自動清零。I/O寄存器的操作具有side effect,因此,不能對其操作不能使用cpu緩存。
原文網(wǎng)址:
http://qinbh.blog.sohu.com/62733495.html
第二種說法:
說一下我的理解:I/O端口與實際外部設(shè)備相關(guān)聯(lián),通過訪問I/O端口控制外部設(shè)備,“邊際效應(yīng)”是指控制設(shè)備(讀取或?qū)懭耄┥?,訪問I/O口的
主要目的就是邊際效應(yīng),不像訪問普通的內(nèi)存,只是在一個位置存儲或讀取一個數(shù)值,沒有別的含義了。我是基于arm平臺理解的,在《linux設(shè)備驅(qū)動程
序》第二版中的說法是“副作用”,不是“邊際效應(yīng)”。
原文網(wǎng)址:
linux.chinaunix.net/bbs/viewthread.php?tid=890636&page=1#pid6312646">http://linux.chinaunix.net/bbs/viewthread.php?tid=890636&page=1#pid6312646
結(jié)合以上兩種說法和自己看《Linux設(shè)備驅(qū)動程序(第3版)》的理解,我個人認(rèn)為可以這樣解釋:
side effect
是指:訪問I/O寄存器時,不僅僅會像訪問普通內(nèi)存一樣影響存儲單元的值,更重要的是它可能改變CPU的I/O端口電平、輸出時序或CPU對I/O端口電
平的反應(yīng)等等,從而實現(xiàn)CPU的控制功能。CPU在電路中的意義就是實現(xiàn)其side effect 。
I/O 寄存器和 RAM 的主要不同就是 I/O 寄存器操作有side effect, 而內(nèi)存操作沒有。
因為存儲單元的訪問速度對 CPU 性能至關(guān)重要,編譯器會對源代碼進(jìn)行優(yōu)化,主要是: 使用高速緩存保存數(shù)值 和 重新編排讀/寫指令順序。但對I/O 寄存器操作來說,這些優(yōu)化可能造成致命錯誤。因此,驅(qū)動程序必須確保在操作I/O 寄存器時,不使用高速緩存,且不能重新編排讀/寫指令順序。
解決方法:
硬件緩存問題:只要把底層硬件配置(自動地或者通過 Linux 初始化代碼)成當(dāng)訪問 I/O 區(qū)域時(不管內(nèi)存還是端口)禁止硬件緩存即可。
硬件指令重新排序問題:在硬件(或其他處理器)必須以一個特定順序執(zhí)行的操作之間設(shè)置內(nèi)存屏障(memory barrier)。
#p#Linux 提供以下宏來解決所有可能的排序問題:
#include linux/kernel.h>
void barrier(void) /*告知編譯器插入一個內(nèi)存屏障但是對硬件沒有影響。編譯后的代碼會將當(dāng)前CPU 寄存器中所有修改過的數(shù)值保存到內(nèi)存中, 并當(dāng)需要時重新讀取它們??勺柚乖谄琳锨昂蟮木幾g器優(yōu)化,但硬件能完成自己的重新排序。其實linux/kernel.h> 中并沒有這個函數(shù),因為它是在kernel.h包含的頭文件compiler.h中定義的*/
#include linux/compiler.h>
# define barrier() __memory_barrier()
#include asm/system.h>
void rmb(void); /*保證任何出現(xiàn)于屏障前的讀在執(zhí)行任何后續(xù)的讀之前完成*/
void wmb(void); /*保證任何出現(xiàn)于屏障前的寫在執(zhí)行任何后續(xù)的寫之前完成*/
void mb(void); /*保證任何出現(xiàn)于屏障前的讀寫操作在執(zhí)行任何后續(xù)的讀寫操作之前完成*/
void read_barrier_depends(void); /*
一種特殊的、弱些的讀屏障形式。rmb 阻止屏障前后的所有讀指令的重新排序,read_barrier_depends
只阻止依賴于其他讀指令返回的數(shù)據(jù)的讀指令的重新排序。區(qū)別微小, 且不在所有體系中存在。除非你確切地理解它們的差別,
并確信完整的讀屏障會增加系統(tǒng)開銷,否則應(yīng)當(dāng)始終使用 rmb。*/
/*以上指令是barrier的超集*/
void smp_rmb(void);
void smp_read_barrier_depends(void);
void smp_wmb(void);
void smp_mb(void);
/*僅當(dāng)內(nèi)核為 SMP 系統(tǒng)編譯時插入硬件屏障; 否則, 它們都擴(kuò)展為一個簡單的屏障調(diào)用。*/
典型的應(yīng)用:
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);
內(nèi)存屏障影響性能,所以應(yīng)當(dāng)只在確實需要它們的地方使用。不同的類型對性能的影響也不同,因此要盡可能地使用需要的特定類型。值得注意的是大部分處理同步的內(nèi)核原語,例如自旋鎖和atomic_t,也可作為內(nèi)存屏障使用。
某些體系允許賦值和內(nèi)存屏障組合,以提高效率。它們定義如下:
#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 結(jié)構(gòu)來構(gòu)造宏是標(biāo)準(zhǔn) C 的慣用方法,它保證了擴(kuò)展后的宏可在所有上下文環(huán)境中被作為一個正常的 C 語句執(zhí)行。
使用 I/O 端口
I/O 端口是驅(qū)動用來和許多設(shè)備之間的通訊方式。
I/O 端口分配
在尚未取得端口的獨占訪問前,不應(yīng)對端口進(jìn)行操作。內(nèi)核提供了一個注冊用的接口,允許驅(qū)動程序聲明它需要的端口:
#include linux/ioport.h>
struct resource *request_region(unsigned long first, unsigned long n, const char *name);/*告訴內(nèi)核:要使用從 first 開始的 n 個端口,name 參數(shù)為設(shè)備名。若分配成功返回非 NULL,否則將無法使用需要的端口。*/
/*所有的的端口分配顯示在 /proc/ioports 中。若不能分配到需要的端口,則可以到這里看看誰先用了。*/
/*當(dāng)用完 I/O 端口集(可能在模塊卸載時), 應(yīng)當(dāng)將它們返回給系統(tǒng)*/
void release_region(unsigned long start, unsigned long n);
int check_region(unsigned long first, unsigned long n);
/*檢查一個給定的 I/O 端口集是否可用,若不可用, 返回值是一個負(fù)錯誤碼。不推薦使用*/
操作 I/O 端口
在驅(qū)動程序注冊I/O 端口后,就可以讀/寫這些端口。大部分硬件會把8、16和32位端口區(qū)分開,不能像訪問系統(tǒng)內(nèi)存那樣混淆使用。驅(qū)動必須調(diào)用不同的函數(shù)來存取不同大小的端口。
只支持內(nèi)存映射的 I/O 寄存器的計算機(jī)體系通過重新映射I/O端口到內(nèi)存地址來偽裝端口I/O。為了提高移植性,內(nèi)核向驅(qū)動隱藏了這些細(xì)節(jié)。Linux 內(nèi)核頭文件(體系依賴的頭文件 ) 定義了下列內(nèi)聯(lián)函數(shù)(有的體系是宏,有的不存在)來訪問 I/O 端口:
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
/*讀/寫字節(jié)端口( 8 位寬 )。port 參數(shù)某些平臺定義為 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 端口
以上函數(shù)主要提供給設(shè)備驅(qū)動使用,但它們也可在用戶空間使用,至少在 PC上可以。 GNU C 庫在 中定義了它們。如果在用戶空間代碼中使用必須滿足以下條件:
(1)程序必須使用 -O 選項編譯來強(qiáng)制擴(kuò)展內(nèi)聯(lián)函數(shù)。
(2)必須用ioperm 和 iopl 系統(tǒng)調(diào)用(#include ) 來獲得對端口 I/O 操作的權(quán)限。ioperm 為獲取單獨端口操作權(quán)限,而 iopl 為整個 I/O 空間的操作權(quán)限。 (x86 特有的)
(3)程序以 root 來調(diào)用 ioperm 和 iopl,或是其父進(jìn)程必須以 root 獲得端口操作權(quán)限。(x86 特有的)
若平臺沒有 ioperm 和 iopl 系統(tǒng)調(diào)用,用戶空間可以仍然通過使用 /dev/prot 設(shè)備文件訪問 I/O 端口。注意:這個文件的定義是體系相關(guān)的,并且I/O 端口必須先被注冊。
串操作
除了一次傳輸一個數(shù)據(jù)的I/O操作,一些處理器實現(xiàn)了一次傳輸一個數(shù)據(jù)序列的特殊指令,序列中的數(shù)據(jù)單位可以是字節(jié)、字或雙字,這是所謂的串操作指
令。它們完成任務(wù)比一個 C 語言循環(huán)更快。下列宏定義實現(xiàn)了串I/O,它們有的通過單個機(jī)器指令實現(xiàn);但如果目標(biāo)處理器沒有進(jìn)行串 I/O
的指令,則通過執(zhí)行一個緊湊的循環(huán)實現(xiàn)。 有的體系的原型如下:
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);
使用時注意: 它們直接將字節(jié)流從端口中讀取或?qū)懭搿.?dāng)端口和主機(jī)系統(tǒng)有不同的字節(jié)序時,會導(dǎo)致不可預(yù)期的結(jié)果。 使用 inw 讀取端口應(yīng)在必要時自行轉(zhuǎn)換字節(jié)序,以匹配主機(jī)字節(jié)序。
暫停式 I/O
為了匹配低速外設(shè)的速度,有時若 I/O 指令后面還緊跟著另一個類似的I/O指令,就必須在 I/O 指令后面插入一個小延時。在
這種情況下,可以使用暫停式的I/O函數(shù)代替通常的I/O函數(shù),它們的名字以 _p 結(jié)尾,如 inb_p、outb_p等等。
這些函數(shù)定義被大部分體系支持,盡管它們常常被擴(kuò)展為與非暫停式I/O
同樣的代碼。因為如果體系使用一個合理的現(xiàn)代外設(shè)總線,就沒有必要額外暫停。細(xì)節(jié)可參考平臺的 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使用內(nèi)部總線,就沒有必要額外暫停,所以暫停式的I/O函數(shù)被擴(kuò)展為與非暫停式I/O 同樣的代碼。
#p#平臺相關(guān)性
由于自身的特性,I/O 指令與處理器密切相關(guān)的,非常難以隱藏系統(tǒng)間的不同。所以大部分的關(guān)于端口 I/O 的源碼是平臺依賴的。以下是x86和arm所使用函數(shù)的總結(jié):
IA-32 (x86)
x86_64
這個體系支持所有的以上描述的函數(shù),端口號是 unsigned short 類型。
arm
端口映射到內(nèi)存,支持所有函數(shù)。串操作 用C語言實現(xiàn)。端口是 unsigned int 類型。
使用 I/O 內(nèi)存
除了 x86上普遍使用的I/O
端口外,和設(shè)備通訊另一種主要機(jī)制是通過使用映射到內(nèi)存的寄存器或設(shè)備內(nèi)存,統(tǒng)稱為 I/O 內(nèi)存。因為寄存器和內(nèi)存之間的區(qū)別對軟件是透明的。I/O
內(nèi)存僅僅是類似 RAM 的一個區(qū)域,處理器通過總線訪問這個區(qū)域,以實現(xiàn)設(shè)備的訪問。
根據(jù)平臺和總線的不同,I/O
內(nèi)存可以就是否通過頁表訪問分類。若通過頁表訪問,內(nèi)核必須首先安排物理地址使其對設(shè)備驅(qū)動程序可見,在進(jìn)行任何 I/O 之前必須調(diào)用
ioremap。若不通過頁表,I/O 內(nèi)存區(qū)域就類似I/O 端口,可以使用適當(dāng)形式的函數(shù)訪問它們。因為“side effect”的影響,不管是否需要 ioremap ,都不鼓勵直接使用 I/O 內(nèi)存的指針。而使用專用的 I/O 內(nèi)存操作函數(shù),不僅在所有平臺上是安全,而且對直接使用指針操作 I/O 內(nèi)存的情況進(jìn)行了優(yōu)化。
I/O 內(nèi)存分配和映射
I/O 內(nèi)存區(qū)域使用前必須先分配,函數(shù)接口在 定義:
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);/* 從 start 開始,分配一個 len 字節(jié)的內(nèi)存區(qū)域。成功返回一個非NULL指針,否則返回NULL。所有的 I/O 內(nèi)存分配情況都 /proc/iomem 中列出。*/
/*I/O內(nèi)存區(qū)域在不再需要時應(yīng)當(dāng)釋放*/
void release_mem_region(unsigned long start, unsigned long len);
/*一個舊的檢查 I/O 內(nèi)存區(qū)可用性的函數(shù),不推薦使用*/
int check_mem_region(unsigned long start, unsigned long len);
然后必須設(shè)置一個映射,由 ioremap 函數(shù)實現(xiàn),此函數(shù)專門用來為I/O
內(nèi)存區(qū)域分配虛擬地址。經(jīng)過ioremap 之后,設(shè)備驅(qū)動即可訪問任意的 I/O 內(nèi)存地址。注意:ioremap
返回的地址不應(yīng)當(dāng)直接引用;應(yīng)使用內(nèi)核提供的 accessor 函數(shù)。以下為函數(shù)定義:
#include asm/io.h>
void *ioremap(unsigned long phys_addr, unsigned long size);
void *ioremap_nocache(unsigned long phys_addr, unsigned long size);/*如果控制寄存器也在該區(qū)域,應(yīng)使用的非緩存版本,以實現(xiàn)side effect。*/
void iounmap(void * addr);
訪問I/O 內(nèi)存
訪問I/O 內(nèi)存的正確方式是通過一系列專用于此目的的函數(shù)(在 中定義的):
/*I/O 內(nèi)存讀函數(shù)*/
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
/*addr 是從 ioremap 獲得的地址(可能包含一個整型偏移量), 返回值是從給定 I/O 內(nèi)存讀取的值*/
/*對應(yīng)的I/O 內(nèi)存寫函數(shù)*/
void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);
/*讀和寫一系列值到一個給定的 I/O 內(nèi)存地址,從給定的 buf 讀或?qū)?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 地址,使用一下函數(shù)*/
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);
/*舊函數(shù)接口,仍可工作, 但不推薦。*/
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 內(nèi)存一樣使用端口
一些硬件有一個有趣的特性:一些版本使用 I/O 端口,而其他的使用 I/O 內(nèi)存。為了統(tǒng)一編程接口,使驅(qū)動程序易于編寫,2.6 內(nèi)核提供了一個ioport_map函數(shù):
void *ioport_map(unsigned long port, unsigned int count);/*重映射 count 個I/O 端口,使其看起來像 I/O 內(nèi)存。,此后,驅(qū)動程序可以在返回的地址上使用 ioread8 和同類函數(shù)。其在編程時消除了I/O 端口和I/O 內(nèi)存的區(qū)別。
/*這個映射應(yīng)當(dāng)在它不再被使用時撤銷:*/
void ioport_unmap(void *addr);
/*注意:I/O 端口仍然必須在重映射前使用 request_region 分配I/O 端口。arm9不支持這兩個函數(shù)!*/
上面是基于《Linux設(shè)備驅(qū)動程序(第3版)》的介紹,以下分析 arm9的s3c2440A的linux驅(qū)動接口。
arm9的linux驅(qū)動接口
s3c24x0處理器是使用I/O內(nèi)存的,也就是說:他們的外設(shè)接口是通過讀寫相應(yīng)的寄存器實現(xiàn)的,這些寄存器和內(nèi)存是使用單一的地址空間,并使用和讀寫內(nèi)存一樣的指令。所以推薦使用I/O內(nèi)存的相關(guān)指令。
但這并不表示I/O端口的指令在s3c24x0中不可用。但是只要你注意其源碼,你就會發(fā)現(xiàn):其實I/O端口的指令只是一個外殼,內(nèi)部還是使用和I/O內(nèi)存一樣的代碼。以下列出一些:
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內(nèi)存
#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內(nèi)存的指令都寫了相應(yīng)的驅(qū)動程序,都通過了測試。在這里值得注意的有4點:
(1)所有的讀寫指令所賦的地址必須都是虛擬地址,你有兩種選擇:使用內(nèi)核已經(jīng)定
義好的地址,如
S3C2440_GPJCON等等,這些都是內(nèi)核定義好的虛擬地址,有興趣的可以看源碼。還有一種方法就是使用自己用ioremap映射的虛擬地址。絕對
不能使用實際的物理地址,否則會因為內(nèi)核無法處理地址而出現(xiàn)oops。
(2)在使用I/O指令時,可以不使用request_region和request_mem_region,而直接使用outb、ioread等指令。因為request的功能只是告訴內(nèi)核端口被誰占用了,如再次request,內(nèi)核會制止。
(3)在使用I/O指令時,所賦的地址數(shù)據(jù)有時必須通過強(qiáng)制類型轉(zhuǎn)換為 unsigned long ,不然會有警告(具體原因請看
Linux設(shè)備驅(qū)動程序?qū)W習(xí)(7)-內(nèi)核的數(shù)據(jù)類型
) 。雖然你的程序可能也可以使用,但是最好還是不要有警告為妙。
(4)在include\asm-arm\arch-s3c2410\hardware.h中定義了很多io口的操作函數(shù),有需要可以在驅(qū)動中直接使用,很方便。
實驗源碼:
IO_port.tar.gz
IO_port_test.tar.gz
IO_mem.tar.gz
IO_mem_test.tar.gz
兩個模塊都實現(xiàn)了阻塞型獨享設(shè)備的訪問控制,并通知內(nèi)核不支持llseek。具體的測試在IO_port中。
測試現(xiàn)象如下:
[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
【編輯推薦】