第6章 串行設備驅動
串口是被許多技術和應用廣泛使用的基本通信通道。通用異步收發器(Universal Asynchronous Receiver Transmitter,UART)常用來實現串行通信。在PC兼容機硬件上,UART是Super I/O芯片組的一部分,如圖6.1所示。
圖 6.1. PC串口連接圖
儘管RS-232串口是常見的串行硬件,內核的串行子系統還是用通用化的方式組織在一起,以服務不同的用戶。你將在如下場合接觸到串行子系統:
<!--[if !supportLists]-->· <!--[endif]-->通過RS-232串行鏈路運行終端會話。
<!--[if !supportLists]-->· <!--[endif]-->通過撥號、小區蜂窩或軟件modem連接到Internet。
<!--[if !supportLists]-->· <!--[endif]-->和觸摸控制器、智能卡、藍牙芯片,或紅外dongle<!--[if !supportAnnotations]-->[MS1]<!--[endif]--> 等使用串行傳輸方式的設備接口。
<!--[if !supportLists]-->· <!--[endif]-->使用USB到串口的轉換器模擬串口。
<!--[if !supportLists]-->· <!--[endif]-->通過RS-485通信,RS-485在RS-232的基礎上支持多個節點,傳輸距離更遠,抗噪性能更強。
在本章中,讓我們看看內核如何組織串行子系統。我們將利用Linux蜂窩電話作爲例子學習底層的UART驅動,利用串行觸摸控制器做例子,去發現其高級線路規程的實現細節。
PC機上常見的UART是美國國家半導體有限公司(National Semiconductor)的16550,或者是其他廠家的兼容芯片,因此你將會在代碼或文檔中看到建議參考“16550類型UART”。8250芯片是16550之前使用的,因此PC上Linux的UART驅動被命名爲8250.c。
層次結構
正如你所看到的,串行子系統的用戶多種多樣。因此促使內核開發者使用如下的構建模塊去構造分層次的串行結構:
<!--[if !supportLists]-->1. <!--[endif]-->底層的驅動關注UART或其它底層串行硬件的特徵。
<!--[if !supportLists]-->2. <!--[endif]-->tty驅動層提供和底層驅動的接口。tty驅動將上層驅動和形形色色的硬件進行了隔離。
<!--[if !supportLists]-->3. <!--[endif]-->線路規程加工用於和tty驅動交換的數據。線路規程勾勒串行層的行爲,有助於複用底層的代碼來支持不同的技術。
爲了幫助用戶實現驅動,串行子系統也提供了一些內核API,它們描繪出了子系統中各層的共性。
圖6.2展現了不同層次之間的聯繫。N_TTY,N_IRDA,和N_PPP這3種不同的線路規程分別爲串行子系統提供了終端、紅外和撥號網絡的支持。圖6.3顯示了串行子系統與內核源文件的映射關係。
圖 6.2. 串行子系統層間連接關係圖
圖 6.3. 串行子系統到內核源文件的映射關係
讓我們用一個例子來說明層次化的串行結構的好處。假設你在沒有串口的筆記本電腦上使用USB串口適配器以獲得串口功能。可能的場景就是你將筆記本電腦作爲主機端,使用kgdb(kgdb將在第21章“調試設備驅動”中討論)調試某個嵌入式目標設備的內核,就像圖6.4中所展示的。
圖6.4. 使用USB到串口轉接頭
正如在圖6.3中所看到的,在你的筆記本電腦上,你首先需要一個合適的USB物理層驅動(和UART驅動相對應的USB部分)。在內核的USB子系統drivers/usb/中提供了此驅動。其次,在USB物理層之上你需要tty驅動。usbserial驅動(drivers/usb/serial/usb-serial.c)爲核心層,它實現了基於USB串口轉換器的通用tty。usbserial驅動和與設備相關的tty函數一起組成了tty層。其中tty函數由適配器驅動註冊(例如,如果你使用Keyspan適配器,其驅動爲drivers/usb/serial/keyspan.c)。最後,但不僅僅是這些,你需要N_TTY線路規程以處理終端I/O。
tty驅動將底層的USB的內部特性與線路規程及高層進行了隔離。實際上,線路規程仍然認爲其運行在傳統的UART上。其原因是因爲tty驅動從USB請求塊(USB Request Blocks,將在第11章“通用串行總線”中討論)中獲取數據,並且將數據封裝成N_TTY線路規程期望的格式。層次化的結構簡化了實現——所有從線路規程上傳的數據能夠不作改變的複用。
前述的例子使用了專用的tty驅動和通用的線路規程,相反的用法也很常見。第16章中討論的紅外棧,“無線Linux”,採用的就是通用的tty驅動和被稱爲N_IRDA的專用的線路規程。
正如你在圖6.2和圖6.3中所看到的,雖然UART驅動是字符驅動,但它們並不像我們在前面章節中見到的通常的字符驅動那樣,將接口直接暴露給內核系統調用。相反,UART驅動(像下一章中討論的鍵盤驅動)提供服務給另一內核層:tty層。I/O系統調用的旅程首先從頂層的線路規程開始,通過tty層,止於UART驅動層。
在本章的剩下章節中,讓我們近距離接觸串行層的不同驅動組件。我們從底含UART驅動的串行棧的底層開始,行至中間的tty驅動,再向頂層的線路規程驅動進軍。
UART驅動
UART驅動圍繞三個關鍵的數據結構展開。這三個數據結構都定義於include/linux/ serial_core.h中:
<!--[if !supportLists]-->1. <!--[endif]-->特定UART相關的(per-UART)驅動結構,uart_driver:
struct uart_driver {
struct module *owner; /* 擁有此結構體的模塊 */
const char *driver_name; /* 名稱 */
const char *dev_name; /* /dev 節點名稱,例如ttyS */
/* ... */
int major; /* 主設備號 */
int minor; /* 次設備號 */
/* ... */
struct tty_driver *tty_driver; /* tty 驅動 */
};
結構體中每個域的註釋解釋了其作用。owner域的作用和前面章節中討論的file_operations結構中owner域相同。
<!--[if !supportLists]-->2. <!--[endif]-->uart_port結構。UART驅動擁有的每個端口,都存在uart_port結構的一個實例。
struct uart_port {
spinlock_t lock; /* 端口鎖 */
unsigned int iobase; /* in/out[bwl]*/
unsigned char __iomem *membase; /* read/write[bwl]*/
unsigned int irq; /* 中斷號 */
unsigned int uartclk; /* base uart clock */
unsigned char fifosize; /* 傳輸fifo大小*/
unsigned char x_char; /* xon/xoff 流控 */
/* ... */
};
<!--[if !supportLists]-->3. <!--[endif]-->uart_ops結構。這個結構是每個UART驅動必須支持的物理硬件上可完成的操作的入口函數的集合。
struct uart_ops {
uint (*tx_empty)(struct uart_port *); /* Is TX FIFO empty? */
void (*set_mctrl)(struct uart_port *,
unsigned int mctrl); /* Set modem control params */
uint (*get_mctrl)(struct uart_port *); /* Get modem control params */
void (*stop_tx)(struct uart_port *); /* Stop xmission */
void (*start_tx)(struct uart_port *); /* Start xmission */
/* ... */
void (*shutdown)(struct uart_port *); /* Disable the port */
void (*set_termios)(struct uart_port *,
struct termios *new,
struct termios *old); /* Set terminal interface
params */
/* ... */
void (*config_port)(struct uart_port *,
int); /* Configure UART port */
/* ... */
};
UART驅動爲了將自身和內核聯繫起來,必須完成兩個重要的步驟:
1.
通過調用uart_register_driver(struct uart_driver *)向串口核心層註冊。
2.
調用uart_add_one_port(struct uart_driver *, struct uart_port *)註冊其支持的每個端口。如果你的串口硬件支持熱插拔,探測到設備存在後,從入口點向內核註冊。第10章“PCI”的清單10.4中的CardBus Modem驅動,就是串口設備熱插拔的例子。需要注意的是一些驅動使用封裝的註冊函數serial8250_register_port(struct uart_port *),在其內部調用了uart_add_one_port()。
這些數據結構和註冊函數組成了UART驅動的最基本的共同點。瞭解了這些結構和例程後,讓我們來開發一個UART驅動實例。
設備例子:手機
考慮圍繞嵌入式片上系統構建的Linux手機。此片上系統有兩個集成的UART,但如圖6.5所示,都已經被佔用。其中一個用於和調制解調器通信,另一個用於和藍牙芯片組接口。由於沒有空閒的UART用於調試,所以此電話使用了兩個USB到串口的轉換芯片,一個提供給PC主機作爲調試終端,另一個作爲備用端口。正如你在本章前面所看到的,USB到串口的轉換器使你在PC上通過USB可以連接串口設備,在第11章中,我們將會作更詳細的討論。
圖6.5. Linux蜂窩電話上的USB_UART端口.
兩個USB到串口的轉換芯片的串口一側和片上系統通過CPLD(見第18章《嵌入式Linux》中的“CPLD/FPGA”相關章節)連接在一起。CPLD通過創建了兩個虛擬的UART(或USB_UART),它提供了三個寄存器接口以訪問每個USB/串口轉換器,如表6.1所,這些寄存器爲狀態寄存器、讀數據寄存器和寫數據寄存器。爲了寫一個字符到USB_UART,程序需要循環檢測狀態寄存器,當芯片內部的發送FIFO有空間時,狀態寄存器位被置位,就可以向寫數據寄存器寫入字節了。爲了讀取一個字符,程序將等待直至狀態寄存器的相應位顯示接收FIFO中有數據,然後從讀數據寄存器讀取數據。
表 6.1. USB_UART上的寄存器分佈
寄存器名
描述
相對USB_UART內存基址偏移
UU_STATUS_REGISTER
檢查發送FIFO是否滿或接收FIFO是否空的比特位
0x0
UU_READ_DATA_REGISTER
從USB_UART讀一個字符
0x1
UU_WRITE_DATA_REGISTER
寫一個字符至USB_UART
0x2
在PC一端,使用相應的Linux usbserial驅動(例如,如果在蜂窩電話上使用的是FT232AM芯片,其驅動爲drivers/usb/serial/ftdi_sio.c)以創建並管理對應於USB/串口端口的/dev/ttyUSBX設備節點。你可以運行基於這些設備節點的終端仿真器,例如minicom,以獲得控制檯或調試終端。在蜂窩電話一端,我們必須爲USB_UART實現UART驅動。此驅動創建並管理負責通信的/dev/ttyUUX節點。
圖6.5中的蜂窩電話充當藍牙設備到GSM網絡,以及Internet的智能網關。例如,此電話能夠將你的基於藍牙的血壓監控器上的數據傳輸至你的健康護理提供者在Internet上的服務器上。在和你的基於藍牙的心率監控器通信過程監測到異常時,它也能夠向醫生報警。第13章《音頻設備驅動》中所使用的藍牙MP3播放器,以及第16章中使用的喂藥棒都是可通過Linux蜂窩電話連接Internet的設備例子。
清單6.1實現了USB_UART驅動。它是作爲一個平臺(platform)設備驅動實現的。platform可看作一種僞總線,通常用於將集成進片上系統的的輕量級設備和Linux設備模型連接在一起。platform由如下部分組成:
platform設備。使用和特定結構的安裝程序platform_device_register()或者其簡化版本platform_device_register_simple()添加platform設備。你也可以用platform_add_devices()一次添加多個platform設備。定義於include/linux/platform_device.h的platform_device結構體代表了一個platform設備:
struct platform_device {
const char *name; /* 設備名稱 */
u32 id; /* 此域用於註冊一個platform設備的多個實例。
在本例中,兩個USB_UART有不同的ID。 */
struct device dev; /* 包括一個release() 方法和一個平臺數據 */
/* ... */
};
platform驅動。platform驅動使用platform_driver_register()將自身註冊進平臺中。結構體platform_driver亦定義於include/linux/platform_device.h中,代表了platform驅動:
struct platform_driver {
int (*probe)(struct platform_device *); /*Probe 方法*/
int (*remove)(struct platform_device *);/*Remove 方法*/
/* ... */
/* The name field in the following structure should match
the name field in the associated platform_device
structure */
struct device_driver driver;
};
關於platform設備和platform驅動更詳細的文檔可參考Documentation/driver-model/platform.txt。爲了討論簡單,我們的例子驅動註冊了platform設備和platform驅動。
在初始化過程中,首先USB_UART驅動用uart_register_driver()向串口核心層註冊自身。初始化成功後,在/proc/tty/drivers中你將會發現以usb_uart開始的新行。其次,驅動用platform_device_register_simple()註冊兩個platform設備(每個USB_UART一個)。正如前面提到的,platform設備通常在電路板引導過程中註冊。隨後,驅動用platform_driver_register()註冊platform驅動入口點(probe(), remove(), suspend(), and resume())。USB_UARTplatform驅動和前面的platform設備聯繫在一起,並有一個相應的名字(usb_uart)。在以上步驟完成後,你將會發現在sysfs下出現兩個新目錄,每一個和相應的USB_UART端口相對應:/sys/devices/platform/usb_uart.0/ 和 /sys/devices/platform/usb_uart.1/
因爲Linux設備層開始檢測到和註冊的USB_UART platform設備相匹配的platform驅動,它調用屬於platform驅動的probe()入口點[<!--[if !supportFootnotes]-->[1]<!--[endif]-->](usb_uart_probe()),每個USB_UART一次。probe入口點用uart_add_one_port()添加相應的USB_UART端口。以上步驟會觸發config_port()入口點(前面討論的uart_ops結構的一部分)的調用:聲明並映射USB_UART寄存器空間。如果所有的USB_UART端口都被成功的添加,串口核心層會發送如下內核消息:
ttyUU0 at MMIO 0xe8000000 (irq = 3) is a USB_UART
ttyUU1 at MMIO 0xe9000000 (irq = 4) is a USB_UART
但是,直到某個應用程序打開USB_UART端口,纔會佔用中斷號。當應用程序關閉USB_UART端口,中斷號被釋放。表6.2追溯了驅動代碼中聲明和釋放內存區域與中斷號的整個過程。
Table 6.2. Claiming and Freeing Memory and IRQ Resources
Module Insert
usb_uart_init()
uart_register_driver()
usb_uart_probe()
uart_add_one_port()
usb_uart_config_port()
request_mem_region()
Module Unload
usb_uart_exit()
usb_unregister_driver()
usb_uart_remove()
uart_remove_one_port()
usb_uart_release_port()
release_mem_region()
Open /dev/ttyUUX
usb_uart_startup()
request_irq()
Close /dev/ttyUUX
usb_uart_shutdown()
free_irq()
在發送過程中,驅動收集和UART端口相關的循環緩衝區中的待發送數據。在UART驅動的start_tx()的入口函數usb_uart_start_tx()可以看到,數據存放於port->info->xmit.buf[port->info->xmit.tail]。
在接收過程中,驅動使用tty_insert_flip_char()和tty_flip_buffer_push()將從USB_UART收到的數據推出至tty驅動。這些在收中斷處理例程usb_uart_rxint()中完成的。讀者可在讀完下一節“TTY驅動”後,再重讀此例程。
清單6.1中的註釋解釋了驅動入口函數和其操作的目的。在uart_ops中留下了一些入口函數未作實現,以避免讓讀者陷入過多的細節。
清單6.1. Linux蜂窩電話的USB_UART驅動
Code View:
#include <linux/console.h>
#include <linux/platform_device.h>
#include <linux/tty.h>
#include <linux/tty_flip.h>
#include <linux/serial_core.h>
#include <linux/serial.h>
#include <asm/irq.h>
#include <asm/io.h>
#define USB_UART_MAJOR 200 /* You've to get this assigned */
#define USB_UART_MINOR_START 70 /* Start minor numbering here */
#define USB_UART_PORTS 2 /* The phone has 2 USB_UARTs */
#define PORT_USB_UART 30 /* UART type. Add this to
include/linux/serial_core.h */
/* Each USB_UART has a 3-byte register set consisting of
UU_STATUS_REGISTER at offset 0, UU_READ_DATA_REGISTER at
offset 1, and UU_WRITE_DATA_REGISTER at offset 2 as shown
in Table 6.1 */
#define USB_UART1_BASE 0xe8000000 /* Memory base for USB_UART1 */
#define USB_UART2_BASE 0xe9000000 /* Memory base for USB_UART2 */
#define USB_UART_REGISTER_SPACE 0x3
/* Semantics of bits in the status register */
#define USB_UART_TX_FULL 0x20 /* TX FIFO is full */
#define USB_UART_RX_EMPTY 0x10 /* TX FIFO is empty */
#define USB_UART_STATUS 0x0F /* Parity/frame/overruns? */
#define USB_UART1_IRQ 3 /* USB_UART1 IRQ */
#define USB_UART2_IRQ 4 /* USB_UART2 IRQ */
#define USB_UART_FIFO_SIZE 32 /* FIFO size */
#define USB_UART_CLK_FREQ 16000000
static struct uart_port usb_uart_port[]; /* Defined later on */
/* Write a character to the USB_UART port */
static void
usb_uart_putc(struct uart_port *port, unsigned char c)
{
/* Wait until there is space in the TX FIFO of the USB_UART.
Sense this by looking at the USB_UART_TX_FULL bit in the
status register */
while (__raw_readb(port->membase) & USB_UART_TX_FULL);
/* Write the character to the data port*/
__raw_writeb(c, (port->membase+1));
}
/* Read a character from the USB_UART */
static unsigned char
usb_uart_getc(struct uart_port *port)
{
/* Wait until data is available in the RX_FIFO */
while (__raw_readb(port->membase) & USB_UART_RX_EMPTY);
/* Obtain the data */
return(__raw_readb(port->membase+2));
}
/* Obtain USB_UART status */
static unsigned char
usb_uart_status(struct uart_port *port)
{
return(__raw_readb(port->membase) & USB_UART_STATUS);
}
/*
* Claim the memory region attached to USB_UART port. Called
* when the driver adds a USB_UART port via uart_add_one_port().
*/
static int
usb_uart_request_port(struct uart_port *port)
{
if (!request_mem_region(port->mapbase, USB_UART_REGISTER_SPACE,
"usb_uart")) {
return -EBUSY;
}
return 0;
}
/* Release the memory region attached to a USB_UART port.
* Called when the driver removes a USB_UART port via
* uart_remove_one_port().
*/
static void
usb_uart_release_port(struct uart_port *port)
{
release_mem_region(port->mapbase, USB_UART_REGISTER_SPACE);
}
/*
* Configure USB_UART. Called when the driver adds a USB_UART port.
*/
static void
usb_uart_config_port(struct uart_port *port, int flags)
{
if (flags & UART_CONFIG_TYPE && usb_uart_request_port(port) == 0)
{
port->type = PORT_USB_UART;
}
}
/* Receive interrupt handler */
static irqreturn_t
usb_uart_rxint(int irq, void *dev_id)
{
struct uart_port *port = (struct uart_port *) dev_id;
struct tty_struct *tty = port->info->tty;
unsigned int status, data;
/* ... */
do {
/* ... */
/* Read data */
data = usb_uart_getc(port);
/* Normal, overrun, parity, frame error? */
status = usb_uart_status(port);
/* Dispatch to the tty layer */
tty_insert_flip_char(tty, data, status);
/* ... */
} while (more_chars_to_be_read()); /* More chars */
/* ... */
tty_flip_buffer_push(tty);
return IRQ_HANDLED;
}
/* Called when an application opens a USB_UART */
static int
usb_uart_startup(struct uart_port *port)
{
int retval = 0;
/* ... */
/* Request IRQ */
if ((retval = request_irq(port->irq, usb_uart_rxint, 0,
"usb_uart", (void *)port))) {
return retval;
}
/* ... */
return retval;
}
/* Called when an application closes a USB_UART */
static void
usb_uart_shutdown(struct uart_port *port)
{
/* ... */
/* Free IRQ */
free_irq(port->irq, port);
/* Disable interrupts by writing to appropriate
registers */
/* ... */
}
/* Set UART type to USB_UART */
static const char *
usb_uart_type(struct uart_port *port)
{
return port->type == PORT_USB_UART ? "USB_UART" : NULL;
}
/* Start transmitting bytes */
static void
usb_uart_start_tx(struct uart_port *port)
{
while (1) {
/* Get the data from the UART circular buffer and
write it to the USB_UART's WRITE_DATA register */
usb_uart_putc(port,
port->info->xmit.buf[port->info->xmit.tail]);
/* Adjust the tail of the UART buffer */
port->info->xmit.tail = (port->info->xmit.tail + 1) &
(UART_XMIT_SIZE - 1);
/* Statistics */
port->icount.tx++;
/* Finish if no more data available in the UART buffer */
if (uart_circ_empty(&port->info->xmit)) break;
}
/* ... */
}
/* The UART operations structure */
static struct uart_ops usb_uart_ops = {
.start_tx = usb_uart_start_tx, /* Start transmitting */
.startup = usb_uart_startup, /* App opens USB_UART */
.shutdown = usb_uart_shutdown, /* App closes USB_UART */
.type = usb_uart_type, /* Set UART type */
.config_port = usb_uart_config_port, /* Configure when driver
adds a USB_UART port */
.request_port = usb_uart_request_port,/* Claim resources
associated with a
USB_UART port */
.release_port = usb_uart_release_port,/* Release resources
associated with a
USB_UART port */
#if 0 /* Left unimplemented for the USB_UART */
.tx_empty = usb_uart_tx_empty, /* Transmitter busy? */
.set_mctrl = usb_uart_set_mctrl, /* Set modem control */
.get_mctrl = usb_uart_get_mctrl, /* Get modem control
.stop_tx = usb_uart_stop_tx, /* Stop transmission */
.stop_rx = usb_uart_stop_rx, /* Stop reception */
.enable_ms = usb_uart_enable_ms, /* Enable modem status
signals */
.set_termios = usb_uart_set_termios, /* Set termios */
#endif
};
static struct uart_driver usb_uart_reg = {
.owner = THIS_MODULE, /* Owner */
.driver_name = "usb_uart", /* Driver name */
.dev_name = "ttyUU", /* Node name */
.major = USB_UART_MAJOR, /* Major number */
.minor = USB_UART_MINOR_START, /* Minor number start */
.nr = USB_UART_PORTS, /* Number of UART ports */
.cons = &usb_uart_console, /* Pointer to the console
structure. Discussed in Chapter
12, "Video Drivers" */
};
/* Called when the platform driver is unregistered */
static int
usb_uart_remove(struct platform_device *dev)
{
platform_set_drvdata(dev, NULL);
/* Remove the USB_UART port from the serial core */
uart_remove_one_port(&usb_uart_reg, &usb_uart_port[dev->id]);
return 0;
}
/* Suspend power management event */
static int
usb_uart_suspend(struct platform_device *dev, pm_message_t state)
{
uart_suspend_port(&usb_uart_reg, &usb_uart_port[dev->id]);
return 0;
}
/* Resume after a previous suspend */
static int
usb_uart_resume(struct platform_device *dev)
{
uart_resume_port(&usb_uart_reg, &usb_uart_port[dev->id]);
return 0;
}
/* Parameters of each supported USB_UART port */
static struct uart_port usb_uart_port[] = {
{
.mapbase = (unsigned int) USB_UART1_BASE,
.iotype = UPIO_MEM, /* Memory mapped */
.irq = USB_UART1_IRQ, /* IRQ */
.uartclk = USB_UART_CLK_FREQ, /* Clock HZ */
.fifosize = USB_UART_FIFO_SIZE, /* Size of the FIFO */
.ops = &usb_uart_ops, /* UART operations */
.flags = UPF_BOOT_AUTOCONF, /* UART port flag */
.line = 0, /* UART port number */
},
{
.mapbase = (unsigned int)USB_UART2_BASE,
.iotype = UPIO_MEM, /* Memory mapped */
.irq = USB_UART2_IRQ, /* IRQ */
.uartclk = USB_UART_CLK_FREQ, /* CLock HZ */
.fifosize = USB_UART_FIFO_SIZE, /* Size of the FIFO */
.ops = &usb_uart_ops, /* UART operations */
.flags = UPF_BOOT_AUTOCONF, /* UART port flag */
.line = 1, /* UART port number */
}
};
/* Platform driver probe */
static int __init
usb_uart_probe(struct platform_device *dev)
{
/* ... */
/* Add a USB_UART port. This function also registers this device
with the tty layer and triggers invocation of the config_port()
entry point */
uart_add_one_port(&usb_uart_reg, &usb_uart_port[dev->id]);
platform_set_drvdata(dev, &usb_uart_port[dev->id]);
return 0;
}
struct platform_device *usb_uart_plat_device1; /* Platform device
for USB_UART 1 */
struct platform_device *usb_uart_plat_device2; /* Platform device
for USB_UART 2 */
static struct platform_driver usb_uart_driver = {
.probe = usb_uart_probe, /* Probe method */
.remove = __exit_p(usb_uart_remove), /* Detach method */
.suspend = usb_uart_suspend, /* Power suspend */
.resume = usb_uart_resume, /* Resume after a suspend */
.driver = {
.name = "usb_uart", /* Driver name */
},
};
/* Driver Initialization */
static int __init
usb_uart_init(void)
{
int retval;
/* Register the USB_UART driver with the serial core */
if ((retval = uart_register_driver(&usb_uart_reg))) {
return retval;
}
/* Register platform device for USB_UART 1. Usually called
during architecture-specific setup */
usb_uart_plat_device1 =
platform_device_register_simple("usb_uart", 0, NULL, 0);
if (IS_ERR(usb_uart_plat_device1)) {
uart_unregister_driver(&usb_uart_reg);
return PTR_ERR(usb_uart_plat_device1);
}
/* Register platform device for USB_UART 2. Usually called
during architecture-specific setup */
usb_uart_plat_device2 =
platform_device_register_simple("usb_uart", 1, NULL, 0);
if (IS_ERR(usb_uart_plat_device2)) {
uart_unregister_driver(&usb_uart_reg);
platform_device_unregister(usb_uart_plat_device1);
return PTR_ERR(usb_uart_plat_device2);
}
/* Announce a matching driver for the platform
devices registered above */
if ((retval = platform_driver_register(&usb_uart_driver))) {
uart_unregister_driver(&usb_uart_reg);
platform_device_unregister(usb_uart_plat_device1);
platform_device_unregister(usb_uart_plat_device2);
}
return 0;
}
/* Driver Exit */
static void __exit
usb_uart_exit(void)
{
/* The order of unregistration is important. Unregistering the
UART driver before the platform driver will crash the system */
/* Unregister the platform driver */
platform_driver_unregister(&usb_uart_driver);
/* Unregister the platform devices */
platform_device_unregister(usb_uart_plat_device1);
platform_device_unregister(usb_uart_plat_device2);
/* Unregister the USB_UART driver */
uart_unregister_driver(&usb_uart_reg);
}
module_init(usb_uart_init);
module_exit(usb_uart_exit);
RS-485
與RS-232不同,RS-485並不是標準的PC接口,但在嵌入式領域,你可能會碰到計算機和控制系統之間爲了可靠通信而使用RS-485的情況。RS-485使用差分信號,因此其傳輸距離可達數千英尺;而RS-232的傳輸距離僅爲數十英尺。在處理器一端,RS-485接口是半雙工的UART操作。因此從發送FIFO發送數據至電纜之前,UART設備驅動需要禁止接收器,使能發送器,這一操作可以通過設置相應GPIO引腳的電平來實現。而爲了從電纜上獲取數據並傳送至接收FIFO,UART驅動需要完成相反的操作。
你必須在串口層中恰當的地方使能/禁止RS-485的接收器/發送器。如果你太早地禁止了發送器,它可能沒有足夠的時間清空發送FIFO中最後的幾個字節數據,這將導致發送數據丟失。相反的,如果你太早地禁止了發送器,你就會阻止此段時間的數據接收,這將導致接收數據丟失。
RS-485支持多節點,因此如果你有多個設備和總線相連,高層的協議必須實現恰當的尋址機制。RS-485不支持使用RTS(Request To Send)和CTS(Clear To Send)的硬流控。
TTY驅動
讓我們開始看看tty驅動的核心結構體和註冊函數。有三個結構體非常重要:
定義於include/linux/tty.h中的tty_struct結構體。此結構體包含了和打開的tty相關的所有的狀態信息。其結構體成員衆多,下面列出了一些重要的成員:
struct tty_struct {
int magic; /* Magic marker */
struct tty_driver *driver; /* tty驅動指針 */
struct tty_ldisc ldisc; /* Attached Line
discipline */
/* ... */
struct tty_flip_buffer flip; /* Flip Buffer. See
below. */
/* ... */
wait_queue_head_t write_wait; /* See the section
"Line Disciplines" */
wait_queue_head_t read_wait; /* See the section
"Line Disciplines" */
/* ... */
};
tty_struct結構體中的tty_flip_buffer結構體。這是數據收集和處理機制的中樞:
struct tty_flip_buffer {
/* ... */
struct semaphore pty_sem; /* Serialize */
char *char_buf_ptr; /* Pointer to the flip
buffer */
/* ... */
unsigned char char_buf[2*TTY_FLIPBUF_SIZE]; /* The flip
buffer */
/* ... */
};
底層的串行驅動將flip緩衝區的一半用於數據收集,線路規程則使用另一半來進行數據處理。數據處理伴隨着串行驅動和線路規程所使用的緩衝區指針的移動而持續進行。從drivers/char/tty_io.c文件的flush_to_ldisc()函數中可看到flip的確切行爲。
在最近的內核中,tty_flip_buffer結構體有些改動,該結構體目前由緩衝區頭部(tty_bufhead)和緩衝區鏈表(tty_buffer)組成:
struct tty_bufhead {
/* ... */
struct semaphore pty_sem; /* Serialize */
struct tty_buffer *head, tail, free; /* See below */
/* ... */
};
struct tty_buffer {
struct tty_buffer *next;
char *char_buf_ptr; /* Pointer to the flip buffer */
/* ... */
unsigned long data[0]; /* The flip buffer, memory for
which is dynamically
allocated */
};
定義於include/linux/tty_driver.h文件中的tty_driver結構體。它規定了tty驅動和高層的編程接口:
struct tty_driver {
int magic; /* Magic number */
/* ... */
int major; /* Major number */
int minor_start; /* Start of minor number */
/* ... */
/* Interface routines between a tty driver and higher
layers */
int (*open)(struct tty_struct *tty, struct file *filp);
void (*close)(struct tty_struct *tty, struct file *filp);
int (*write)(struct tty_struct *tty,
const unsigned char *buf, int count);
void (*put_char)(struct tty_struct *tty,
unsigned char ch);
/* ... */
};
像UART驅動一樣,tty驅動也需要完成兩個步驟以向內核註冊自身:
1.
調用tty_register_driver(struct tty_driver *tty_d)向tty核心註冊自身。
2.
調用
tty_register_device(struct tty_driver *tty_d,
unsigned device_index,
struct device *device)
註冊它支持的每個單獨的tty。
本章不會給出tty驅動的實例,在Linux內核中有一些通用的tty驅動:
第16章討論的藍牙模擬串口,就是用tty驅動的形式實現的。此驅動(drivers/net/bluetooth/rfcomm/tty.c)在初始化階段調用tty_register_driver(),在處理每個到來的藍牙連接時調用tty_register_device()。
在Linux桌面上,爲了使用系統控制檯,如果你工作在字符模式下,將需要虛擬終端(virtual terminals,VTs)服務,如果你處在圖形模式下,將需要僞終端(pseudo terminals,PTYs)服務。虛擬終端和僞終端都是用tty驅動實現的,分別位於drivers/char/vt.c和drivers/char/pty.c。
傳統的UART使用的tty驅動位於drivers/serial/serial_core.c。
USB到串口轉換器的tty驅動位於drivers/usb/serial/usb-serial.c。
線路規程
線路規程提供了一套靈活的機制,使得用戶運行不同的應用時,使用相同的串行驅動。底層的物理驅動和tty驅動完成從硬件上收發數據,而線路規程則負責處理這些數據,並在內核空間和用戶空間之間進行數據的傳遞。
串行子系統支持17種標準的線路規程。當你打開串口時系統會綁定默認的線路規程N_TTY,它實現了終端I/O處理。N_TTY負責加工從鍵盤接收到的字符。根據用戶需要,它完成控制字符到“新起一行”的映射,進行小寫字符至大寫字符的轉換,將tab、echo字符傳遞給關聯的虛擬終端。N_TTY也支持原始編輯模式,此時,它將所有前述的處理都交給用戶程序。下一章《輸入設備驅動》中的圖7.3展示了鍵盤子系統如何和N_TTY相關聯。前一節“TTY驅動”中列出的tty驅動例子默認就是使用N_TTY。
線路規程也實現通過串行傳輸協議的網絡接口。PPP(N_PPP)和SLIP(N_SLIP)子系統中的線路規程完成將包組幀、分配相應的網絡數據結構並將數據傳送至相應的網絡協議棧的工作。其它的線路規程還包括紅外數據(N_IRDA)和藍牙主機控制接口(N_HCI)。
設備例子:觸摸控制器
本節通過實現一個簡單的串行觸摸屏控制器的線路規程,來深入線路規程的內幕。圖6.6顯示了觸摸控制器和嵌入式掌上電腦的連接。由於觸摸控制器的有限狀態機(FSM)能夠很好地描述串行層提供的接口和功用,因此將可根據它實現線路規程。
圖 6.6. PC-derivative上觸摸控制器連接圖
<!--[if !vml]--><!--[endif]-->
Open與Close
爲了創建線路規程,需要定義tty_ldisc結構體,並向內核註冊指定的入口函數集。清單6.2包括了實現了以上功能的觸摸控制器實例的部分代碼。
清單 6.2. 線路規程操作
Code View:
struct tty_ldisc n_touch_ldisc = {
TTY_LDISC_MAGIC, /* Magic */
"n_tch", /* 線路規程名 */
N_TCH, /* 線路規程ID號 */
n_touch_open, /* 打開線路規程 */
n_touch_close, /* 關閉線路規程 */
n_touch_flush_buffer, /* Flush the line discipline's read
buffer */
n_touch_chars_in_buffer, /* Get the number of processed characters in
the line discipline's read buffer */
n_touch_read, /* Called when data is requested
from user space */
n_touch_write, /* Write 方法 */
n_touch_ioctl, /* I/O 控制命令 */
NULL, /* We don't have a set_termios
routine */
n_touch_poll, /* 輪詢 */
n_touch_receive_buf, /* 由底層的驅動調用,
用於向用戶空間傳送數據 */
n_touch_receive_room, /* Returns the room left in the line
discipline's read buffer */
n_touch_write_wakeup /* Called when the low-level device
driver is ready to transmit more
data */
};
/* ... */
if ((err = tty_register_ldisc(N_TCH, &n_touch_ldisc))) {
return err;
}
在清單6.2中,n_tch是線路規程名,N_TCH是線路規程的ID號。你需要在include/linux/tty.h中定義其值(此頭文件中包括所有線路規程的定義)。在/proc/tty/ldiscs可發現正使用的線路規程。
線路規程從tty的flip緩衝區對應的部分收集、處理數據,然後拷貝處理後的數據至本地讀緩衝區。對於N_TCH,n_touch_receive_room()返回讀緩衝區中的剩餘內存數,n_touch_chars_in_buffer()返回讀緩衝區中已經處理過的、準備送至用戶空間的字符個數。由於N_TCH是隻讀設備, n_touch_write() 和 n_touch_write_wakeup()將不進行任何操作。n_touch_open()用於爲線路規程的主要數據結構分配內存,可參照清單6.3。
清單6.3. 打開線路規程
Code View:
/* Private structure used to implement the Finite State Machine
(FSM) for the touch controller. The controller and the processor
communicate using a specific protocol that the FSM implements */
struct n_touch {
int current_state; /* Finite State Machine */
spinlock_t touch_lock; /* Spinlock */
struct tty_struct *tty; /* Associated tty */
/* Statistics and other housekeeping */
/* ... */
} *n_tch;
/* Device open() */
static int
n_touch_open(struct tty_struct *tty)
{
/* Allocate memory for n_tch */
if (!(n_tch = kmalloc(sizeof(struct n_touch), GFP_KERNEL))) {
return -ENOMEM;
}
memset(n_tch, 0, sizeof(struct n_touch));
tty->disc_data = n_tch; /* Other entry points now
have direct access to n_tch */
/* Allocate the line discipline's local read buffer
used for copying data out of the tty flip buffer */
tty->read_buf = kmalloc(BUFFER_SIZE, GFP_KERNEL);
if (!tty->read_buf) return -ENOMEM;
/* Clear the read buffer */
memset(tty->read_buf, 0, BUFFER_SIZE);
/* Initialize lock */
spin_lock_init(&ntch->touch_lock);
/* Initialize other necessary tty fields.
See drivers/char/n_tty.c for an example */
/* ... */
return 0;
}
當打開連有觸摸控制器的串口時,你可能想將N_TCH而非N_TTY設置爲默認的線路規程,“改變線路規程”一節中介紹的方法可以實現從用戶空間改變線路規程的目的。
讀數據過程
對於中斷驅動的設備,讀取數據的過程通常由一前一後兩個線程組成:
由於發起讀數據請求而從用戶空間發起的頂層線程;
由中斷處理程序(接收來自設備的數據)喚醒的底層線程。
圖6.7顯示了這兩個與讀數據流程相關的線程。中斷處理程序將receive_buf()(在我們的例子中是n_touch_receive_buf())函數當作任務進行排隊。通過設置tty->low_latency可重載這一行爲。
圖 6.7. 線路規程讀數據過程
<!--[if !vml]--><!--[endif]-->
在觸摸控制器的數據手冊詳細描述了觸摸控制器和處理器之間的專用通信協議,而驅動則用前面討論過的有限狀態機來實現此協議。清單6.4中將有限狀態機作爲receive_buf()入口點n_touch_receive_buf()的一部分。
清單 6.4. n_touch_receive_buf() 方法
Code View:
static void
n_touch_receive_buf(struct tty_struct *tty,
const unsigned char *cp, char *fp, int count)
{
/* Work on the data in the line discipline's half of
the flip buffer pointed to by cp */
/* ... */
/* Implement the Finite State Machine to interpret commands/data
arriving from the touch controller and put the processed data
into the local read buffer */
....................................................................................
/* Datasheet-dependent Code Region */
switch (tty->disc_data->current_state) {
case RESET:
/* Issue a reset command to the controller */
tty->driver->write(tty, 0, mode_stream_command,
sizeof(mode_stream_command));
tty->disc_data->current_state = STREAM_DATA;
/* ... */
break;
case STREAM_DATA:
/* ... */
break;
case PARSING:
/* ... */
tty->disc_data->current_state = PARSED;
break;
case PARSED:
/* ... */
}
....................................................................................
if (tty->disc_data->current_state == PARSED) {
/* If you have a parsed packet, copy the collected coordinate
and direction information into the local read buffer */
spin_lock_irqsave(&tty->disc_data->touch_lock, flags);
for (i=0; i < PACKET_SIZE; i++) {
tty->disc_data->read_buf[tty->disc_data->read_head] =
tty->disc_data->current_pkt[i];
tty->disc_data->read_head =
(tty->disc_data->read_head + 1) & (BUFFER_SIZE - 1);
tty->disc_data->read_cnt++;
}
spin_lock_irqrestore(&tty->disc_data->touch_lock, flags);
/* ... */ /* See Listing 6.5 */
}
}
n_touch_receive_buf() 處理從串行驅動來的數據。它和觸摸控制器進行一系列命令/響應的交互,將接收的座標和壓下/釋放信息放入線路規程讀緩衝區。對讀緩衝區的訪問必須藉助自旋鎖的加鎖能力來依次進行,如圖6.7所示,該自旋鎖被ldisc.receive_buf() 和 ldisc.read() 線程(在我們的例子中分別是n_touch_receive_buf() 和 n_touch_read())同時使用。如清單6.4,n_touch_receive_buf()通過直接調用串行驅動的write()入口函數將命令分發給觸摸控制器。
n_touch_receive_buf()需要做如下操作:
如果沒有數據可獲得,圖6.7中的頂層的read()線程會將調用進程置爲休眠狀態。因此n_touch_receive_buf()必須將其喚醒,使其讀取剛處理過的數據。
如果線路規程耗盡了讀緩衝空間,n_touch_receive_buf()必須要求串行驅動中止從設備接收數據。當它將數據搬移至用戶空間並釋放讀緩衝區中的內存後,ldisc.read()負責重新開啓數據接收。串行驅動利用軟件或硬件流控機制完成數據接收的中止和重啓。
清單6.5實現了上面的操作。
清單 6.5. 喚醒讀線程和中止串行驅動
/* n_touch_receive_buf() continued.. */
/* Wake up any threads waiting for data */
if (waitqueue_active(&tty->read_wait) &&
(tty->read_cnt >= tty->minimum_to_wake))
wake_up_interruptible(&tty->read_wait);
}
/* If we are running out of buffer space, request the
serial driver to throttle incoming data */
if (n_touch_receive_room(tty) < TOUCH_THROTTLE_THRESHOLD) {
tty->driver.throttle(tty);
}
/* ... */
等待隊列tty->read_wait用於在ldisc.read()和ldisc.receive_buf()線程之間實現同步。當ldisc.read()未發現可讀的數據時,將調用進程加入等待隊列,當有數據可讀時,ldisc.receive_buf()喚醒ldisc.read()線程。因此,n_touch_read()完成如下操作:
<!--[if !supportLists]-->· <!--[endif]-->當仍然無可讀數據時,將調用進程放入read_wait隊列中使其休眠。當數據到來時由n_touch_receive_buf()喚醒此進程。
<!--[if !supportLists]-->· <!--[endif]-->若數據可獲得,從本地讀緩衝區(tty->read_buf[tty->read_tail])中收集數據,並分發至用戶空間。
<!--[if !supportLists]-->· <!--[endif]-->若串行驅動被中止,並且在讀操作後讀緩衝區中又有了足夠的可用空間,請求串行驅動重啓。
網絡線路規程通常分配sk_buff(第15章“網絡接口卡”中將討論到的基本的Linux網絡數據結構),並用它作讀緩衝區。由於網絡線路規程的receive_buf()將接收的拷貝至sk_buff並將其直接傳送至相應的協議棧,因此它沒有read()函數。
寫數據過程
線路規程的write()入口函數需要完成一些數據傳送至底層驅動之前必要的後處理工作。
如果底層驅動不能接受線路規程提供的所有數據,線路規程會將請求發送數據的線程置於休眠狀態。當驅動準備好接受更多的數據時,驅動的中斷處理例程將線路規程喚醒。爲了達成此目的,驅動調用由線路規程註冊的write_wakeup()方法。類似於前面章節中討論過的read_wait實現同步,此處通過等待隊列tty->write_wait來實現相應的同步。
很多網絡線路規程沒有write()方法。協議實現時直接將數據幀向下傳送給串行設備驅動。然而,這些線路規程通常仍然有write_wakeup()入口點,以響應串行驅動的傳輸更多數據的請求。
因爲觸摸控制器是隻讀設備,N_TCH也沒有write()方法。正如在清單6.4中所見,當需要發送命令幀給控制器時,接收路徑中的例程直接和底層的UART驅動交互。
I/O 控制
像第5章《字符設備驅動》中討論的那樣,用戶程序可以通過調用ioctl()向設備發送命令。當一個應用程序打開一個串口設備時,它通常可向其發出三類ioctl:
<!--[if !supportLists]-->· <!--[endif]-->串行驅動設備支持的命令,例如設置modem信息的TIOCMSET。
<!--[if !supportLists]-->· <!--[endif]-->tty驅動支持的命令,例如改變關聯的線路規程的TIOCSETD。
<!--[if !supportLists]-->· <!--[endif]-->關聯的線路規程支持的命令,例如在N_TCH例子中,復位觸摸控制器的命令。
N_TCH中實現的ioctl()基本是標準的。支持的命令依賴於觸摸控制器的數據手冊中描述的協議。
其他操作
另一個線路規程操作是flush_buffer(),用於清理讀緩衝區中未處理的數據。當線路規程關閉時,也會調用flush_buffer()。它喚醒所有等待數據的讀線程,其操作如下:
if (tty->link->packet){
wake_up_interruptible(&tty->disc_data->read_wait);
}
還有一個入口函數(N_TCH不支持)爲set_termios()。N_TTY線路規程支持set_termios()接口,用於進行和線路規程數據處理相關的配置。例如你可以用set_termios()將線路規程置爲原始模式或加工模式。一些觸摸控制器特定的配置(如改變波特率、校驗和停止位)由底層設備驅動的set_termios()方法提供。
其他的入口函數例如poll()都是一些標準入口函數,如果需要可以參閱第五章。
你可以將線路規程編譯進內核或編譯爲模塊動態加載。如果你選擇編譯爲模塊,就必須提供模塊初始化或釋放需要調用的函數。前者通常使用和init()類似的方法,後者需要清空私有的數據結構,並註銷線路規程。註銷線路規程的操作爲:
tty_unregister_ldisc(N_TCH);
驅動串行觸摸控制器更簡單的途徑是利用內核輸入子系統提供的服務和內嵌的serport線路規程。在後續章節中我們將看到其技術細節。
修改線路規程
當用戶空間程序打開和觸摸控制器相連的串口時,N_TCH將被綁定到底層的串行驅動。但有時,用戶空間的應用程序可能需要給設備綁定其他的線路規程。例如,你可能想編寫程序清空觸摸控制器接收的所有原始數據而不處理它。清單6.6打開觸摸控制器,並修改線路規程爲N_TTY並清空所有接收的數據。
清單 6.6. 從用戶空間修改線路規程
fd = open("/dev/ttySX", O_RDONLY | O_NOCTTY);
/* At this point, N_TCH is attached to /dev/ttySX, the serial port used
by the touch controller. Switch to N_TTY */
ldisc = N_TTY;
ioctl(fd, TIOCSETD, &ldisc);
/* Set termios to raw mode and dump the data coming in */
/* ... */
ioctl()的TIOCSETD命令關閉當前的線路規程並打開新選定的線路規程。
查看源代碼
串行核心位於drivers/serial/,tty實現和底層的驅動分散在內核源碼樹中。例如,驅動文件可參考圖6.3,位於四個不同的目錄中:drivers/serial/,drivers/char/,drivers/usb/serial/和 drivers/net/irda/。drivers/serial/目錄包含UART驅動,而在2.4版本的內核中無此目錄,UART相關的代碼過去通常分散在drivers/char/ 和 arch/your-arch/目錄中。現在的代碼劃分較之以前更有邏輯性,因爲UART驅動不是串行層的唯一訪問者,USB到串口轉換器以及紅外dongle等設備也需要和串行核心交互。
drivers/serial/imx.c是一個實際的、底層的UART驅動,它驅動的是飛思卡爾(Freescale)i.MX系列嵌入式控制器的UART。
Linux支持的線路規程列表可以參照include/linux/tty.h文件。如果想了解網絡線路規程,可以閱讀相應的源文件:PPP(drivers/net/ppp_async.c),藍牙(drivers/bluetooth/hci_ldisc.c), 紅外(drivers/net/irda/irtty-sir.c)和SLIP(drivers/net/slip.c)。
表6.3包括本章中用到的主要數據結構的概述和它們在源碼樹中被定義的位置。表6.4列出了本章用到的主要內核編程接口和其定義位置。
表 6.3. 數據結構概述
數據結構
位置
描述
uart_driver
include/linux/serial_core.h
代表底層的UART驅動。
uart_port
include/linux/serial_core.h
代表一個UART端口。
uart_ops
include/linux/serial_core.h
UART驅動支持的入口函數。
platform_device
include/linux/platform_device.h
代表platform設備。
platform_driver
include/linux/platform_device.h
代表一個platform驅動。
tty_struct
include/linux/tty.h
tty的狀態信息。
tty_bufhead, tty_buffer
include/linux/tty.h
這兩個結構體實現了和tty關聯的flip緩衝區。
tty_driver
include/linux/tty_driver.h
tty驅動和高層之間的編程接口。
tty_ldisc
include/linux/tty_ldisc.h
線路規程支持的入口函數。
表 6.4. 內核編程接口概述
內核接口
位置
描述
uart_register_driver()
drivers/serial/sderial_core.c
向串行核心註冊UART驅動。
uart_add_one_port()
drivers/serial/sderial_core.c
註冊UART驅動支持的UART端口。
uart_unregister_driver()
drivers/serial/sderial_core.c
從串行核心移除UART驅動。
platform_device register()
platform_device_register_simple()
platform_add_devices()
drivers/base/platform.c
註冊platform設備。
platform_device_unregister()
drivers/base/platform.c
卸載platform設備。
platform_driver_register()/
platform_driver_unregister()
drivers/base/platform.c
註冊/卸載platform驅動。
tty_insert_flip_char()
include/linux/tty_flip.h
向tty flip緩衝區添加一個字符。
tty_flip_buffer_push()
drivers/char/tty_io.c
排隊一個將flip緩衝區推向線路規程的請求。
tty_register_driver()
drivers/char/tty_io.c
向串行核心註冊tty驅動。
tty_unregister_driver()
drivers/char/tty_io.c
從串行核心註銷tty驅動。
tty_register_ldisc()
drivers/char/tty_io.c
通過註冊指定的入口函數,創建線路規程。
tty_unregister_ldisc()
drivers/char/tty_io.c
從串行核心移除線路規程。
一些串行數據的傳送場景較爲複雜。正如你在圖6.3中所見,你可能需要混用和匹配多個不同的串行層模塊。某些情況下,數據傳送可能需要穿越多個線路規程來完成。例如,設置一個使用藍牙建立的撥號連接就需要數據在HCI線路規程和PPP線路規程中傳輸。你可以嘗試建立此連接,並使用內核調試器單步跟蹤此代碼流。
<!--[if !supportFootnotes]-->
--------------------------------------------------------------------------------
<!--[endif]-->
<!--[if !supportFootnotes]-->[1]<!--[endif]--> 平臺設備通常不支持熱插拔。對平臺設備的probe()方法的調用不同於在後面章節中將學到的熱插拔設備(如PCMCIA,PCI,和USB),它只是爲了使驅動入口點的結構相同,以保持Linux設備模型一致、連續性。
<!--[if !supportAnnotations]-->
--------------------------------------------------------------------------------
<!--[endif]-->
<!--[if !supportAnnotations]-->
<!--[endif]--><!--[if !supportAnnotations]--><!--[endif]-->
<!--[if !supportAnnotations]-->[MS1]<!--[endif]-->此詞可不譯,是非常流行的說法,至今無準確的中文翻譯,其含義基本只可以意會。
<!--[if !supportAnnotations]-->
<!--[endif]-->