SIS 900 是一個可以用來實作 10/100 網絡卡的控制芯片。它提供了對 PCI mastermode , MII, 802.3x 流量控制等各種標準的支持。這篇文章將告訴大家,如何寫一個 Linux 的網絡驅動程序,它將比大家想象中簡單很多。這篇文章將以 Linux 2.4 版為對象, 2.2 版提供的界面略有不同,但差別并不太大,讀完本文后再讀 2.2 版的程序碼應該不會有太大困難才是。 本文所參考的驅動程序是在 2.4.3 版中 drivers/net/sis900.c 這個檔案。你可以在 http://xxx.xxx.xxx.xxx/linux-2.4.3/drivers/net/sis900.c 找到它。
作者 : wyclearcase/" target="_blank" >cc
SIS 900 是一個可以用來實作 10/100 網絡卡的控制芯片。它提供了對 PCI mastermode , MII, 802.3x 流量控制等各種標準的支持。這篇文章將告訴大家,如何寫一個 Linux 的網絡驅動程序,它將比大家想象中簡單很多。這篇文章將以 Linux 2.4 版為對象, 2.2 版提供的界面略有不同,但差別并不太大,讀完本文后再讀 2.2 版的程序碼應該不會有太大困難才是。 本文所參考的驅動程序是在 2.4.3 版中 drivers/net/sis900.c 這個檔案。你可以在 http://xxx.xxx.xxx.xxx/linux-2.4.3/drivers/net/sis900.c 找到它。
如果你能有一份硬件的 databook 在手邊,讀起驅動程序的碼可能會更簡單。 SIS900的 databook 可以直接在 http://www.sis.com.tw/ftp/Databook/900/sis900.exe下載。
PCI 驅動程序
對一個 PCI 驅動程序而言, Linux 提供了很完整的支持,大部份的 PCI 資訊都由內建的程序讀出。對個別的驅動程序而言直接使用就可以了。所以在這個部份,唯一要做的事只是告知 PCI 子系統一個新的驅動程序己經被加入系統之中了。
在檔案的最末端,你會看到下面的程序,
static struct pci_driver sis900_pci_driver = {
name: SIS900_MODULE_NAME,
id_table: sis900_pci_tbl,
probe: sis900_probe,
remove: sis900_remove,
};
static int __init sis900_init_module(void)
{
printk(KERN_INFO "%s", version);
return pci_module_init(&sis900_pci_driver);
}
static void __exit sis900_cleanup_module(void)
{
pci_unregister_driver(&sis900_pci_driver);
}
pci_module_init 是用來向 PCI 子系統注冊一個 PCI 驅動程序。根據 id_table 中所提供的資料, PCI 子系統會在發現符合驅動程序要求的裝置時使用它。那 PCI 子系統如何做到這件事呢 ? 我們先看一下 id_table 的內容就很清楚了。
static struct pci_device_id sis900_pci_tbl [] __devinitdata = {
{PCI_VENDOR_ID_SI, PCI_DEVICE_ID_SI_900,
PCI_ANY_ID, PCI_ANY_ID, 0, 0, SIS_900},
{PCI_VENDOR_ID_SI, PCI_DEVICE_ID_SI_7016,
PCI_ANY_ID, PCI_ANY_ID, 0, 0, SIS_7016},
};
MODULE_DEVICE_TABLE (pci, sis900_pci_tbl);
看懂了嗎 ? 嗯,我想你懂了。不過我還是解釋一下。前面四個分別是
初始化
好了,那其它的部份呢 ? 還記意 sis900_pci_driver 中其它的二個項目 probe 和remove 嗎 ? 它們是用來初始化和移除一個驅動程序的呼叫。你可以把它們想成驅動程序對象的 constructor 和 destructor 。在 probe 中,你應該由硬件中把一些將來可能會用到的資訊準備好。由于這是一個 PCI 驅動程序,你不必特意去檢查裝置是否真的存在。但如果你的驅動程序只支持某些特定的硬件,或是你想要檢查系統中是否有一些特別的硬件存在,你可以在這里做。例如在這個驅動程序中,對不同版本的硬件,我們用不同的方法去讀它的 MAC 地址。
pci_read_config_byte(pci_dev, PCI_CLASS_REVISION, &revision);
if (revision == SIS630E_900_REV || revision == SIS630EA1_900_REV)
ret = sis630e_get_mac_addr(pci_dev, net_dev);
else if (revision == SIS630S_900_REV)
ret = sis630e_get_mac_addr(pci_dev, net_dev);
else
ret = sis900_get_mac_addr(pci_dev, net_dev);
對于 SIS630E SIS630EA1 和 SIS630S 這些整合式芯片而言,其 MAC 地址被儲存在 APC CMOS RAM 之中。但對其它獨立的芯片而言則是存在網絡卡的 EEPROM 之上。
為了不要讓這篇文章像流水帳一般,我不仔細的說明 probe 的過程。大家自己揣摸一下吧 !
在 probe 中還有一段比較和后文有關的程序碼
net_dev->open = &sis900_open;
net_dev->hard_start_xmit = &sis900_start_xmit;
net_dev->stop = &sis900_close;
net_dev->get_stats = &sis900_get_stats;
net_dev->set_config = &sis900_set_config;
net_dev->set_multicast_list = &set_rx_mode;
net_dev->do_ioctl = &mii_ioctl;
net_dev->tx_timeout = sis900_tx_timeout;
net_dev->watchdog_timeo = TX_TIMEOUT;
我想這很清楚,我們透過 net_dev 這個結構告訴 Linux 網絡子系統如何來操作這個裝置。當你使用 ifconfig 這個 R 令時,系統會使用 sis900_open 打開這個驅動程序,并使用 set_config 來說定裝置的參數,如 IP address 。當有資料需要被傳送時, sis900_start_xmit 被用來將資料送入裝置之中。接下來,我們就一一的檢視這些函數。
初始化裝置
sis900_open(struct net_device *net_dev);
這個函數會在我們使用 ifconfig 將一網絡裝置激活時被呼叫。當驅動程序被插入系統之后,通常并不會馬上開始接收或傳送封包。一般來說,在 probe 的階段,我們只是單純的判斷裝置是否存在。實際激活硬件的動作在這里才會被實際執行。
以 SIS900 為例,在其硬件中只有一個大約 2K 的緩沖區。也就是說在裝置上只有一個封包的緩沖區。當一個封包被傳送后,裝置必須產生一個中斷要求操作系統將下一個封包傳入。如果由中斷到中斷驅動程序被執行需要 5ms 的時間,那一秒至多我們可以送出 200 個封包。也就是說網絡傳送是不可能大于 400K/s ,這對于一般的情況下是不太可能接受的事。
SIS900 雖然在裝置上只有很小的緩沖區,但它可以透過 PCI master 模式直接控制主機板上的記憶體。事實上,它使用下面的方式來傳送資料。
javascript:window.open(this.src);" style="CURSOR: pointer" onload="return imgzoom(this,550)">
你必須在記憶體中分配一組串接成環狀串行的緩沖區,然后將 TXDP 指向緩沖區的第一個地址。 SIS900 會在第一個緩沖區傳送完后自動的由第二個緩沖區取資料,并更新記憶中的資料將己傳送完緩沖區的 OWN 位清除。當 CPU 將緩沖區串行設定完成后,這個動作可以在完全沒有 CPU 的介入下完成。所以硬件不必等待作業系統將新的資料送入,而可以連續的送出多個封包。操作系統只要能來的及讓環狀串行不會進入空的狀態就可以了。
同樣的,我們也需要一個接收緩沖區,使用進來的封包不至因操作系統來不及處理而遺失。在 sis900_open 中, sis900_init_rx_ring 和 sis900_init_tx_ring 就是用來負處初始化這二個串行。
在初始化串行之后,我們便可以要求 SIS900 開始接收封包。下面二行程序碼便是用來做這件事。
outl((RxSOVR|RxORN|RxERR|RxOK|TxURN|TxERR|TxIDLE), ioaddr + imr);
outl(RxENA, ioaddr + cr);
outl(IE, ioaddr + ier);
第一行設定硬件在下列情況發出一個系統中斷,
在這個函數的最后,我們安裝一個每秒執行五次的 timer 。在它的處理函數 sis900_timer 中,我們會檢查目前的連結狀態,這包括了連結的種類 (10/100)和連接的狀態 ( 網絡卡是否直的被接到網絡上去 ) 。
如果各位用過 Window 2000 ,另人印象最深刻的是當你將網絡線拔出時, GUI 會自動警言網絡己經中斷。其實 Linux 也可以做到這件事,只是你需要一個比較好的圖形界面就是了。
傳送一個封包的 descriptor 給網絡卡
sis900_start_xmit(struct sk_buff *skb, struct net_device *net_dev);
這個函數是用來將一個由 skb 描述的網絡資料緩沖區送進傳送緩沖區中準備傳送。其中最重要的程序碼為
sis_priv->tx_ring[entry].bufptr = virt_to_bus(skb->data);
sis_priv->tx_ring[entry].cmdsts = (OWN | skb->len);
outl(TxENA, ioaddr + cr);
SIS900 會使用 DMA 由緩沖區中取得封包的資料。由于緩沖區的數目有限,我們必須在緩沖區用完的時后告訴上層的網絡協定不要再往下送資料了。在這里我們用下面的程序來做這件事。
if (++sis_priv->cur_tx - sis_priv->dirty_tx < NUM_TX_DESC) {
netif_start_queue(net_dev);
} else {
sis_priv->tx_full = 1;
netif_stop_queue(net_dev);
}
netif_start_queue 用來告訴上層網絡協定這個驅動程序還有空的緩沖區可用,請把下一個封包送進來。 netif_stop_queue 則是用來告訴上層網絡協定所有的封包都用完了,請不要再送。
接收一個或多個封包
int sis900_rx(struct net_device *net_dev);
這個函式在會在有封包進入系統時被呼叫,因為可能有多于一個的封包在緩沖區之中。這個函數會逐一檢查所有的緩沖區,直到遇到一個空的緩沖區為止。
當我們發現一個有資料的緩沖區時,我們需要做二件事。首先是告知上層網絡協定有一個新的封包進入系統,這件事由下面的程序完成
skb = sis_priv->rx_skbuff[entry];
skb_put(skb, rx_size);
skb->protocol = eth_type_trans(skb, net_dev);
netif_rx(skb);
前三行根據封包的內容更新 skbuff 中的檔頭。最后一行則是正式通知上層處理封包。
請注意 Linux 為了增加處理效能,在 netif_rx 并不會真的做完整接收封包的動作,而只是將這個封包記下來。真實的動作是在 bottom half 中才去處理。因為如此,原先儲存封包的緩沖區暫時不能再被使用,我們必須重新分配一個新的緩沖區供下一個封包使用。下面的程序碼是用來取得一個新的緩沖區。
if ((skb = dev_alloc_skb(RX_BUF_SIZE)) == NULL) {
sis_priv->rx_skbuff[entry] = NULL;
sis_priv->rx_ring[entry].cmdsts = 0;
sis_priv->rx_ring[entry].bufptr = 0;
sis_priv->stats.rx_dropped++;
break;
}
skb->dev = net_dev;
sis_priv->rx_skbuff[entry] = skb;
sis_priv->rx_ring[entry].cmdsts = RX_BUF_SIZE;
sis_priv->rx_ring[entry].bufptr = virt_to_bus(skb->tail);
sis_priv->dirty_rx++;
這個函數其馀的部份其實只是用來記錄一些統計資料而己。
傳送下一個封包
void sis900_finish_xmit (struct net_device *net_dev);
這個函數用來處理傳送中斷。在收到一個 TX 中斷,表示有一個或多數緩沖區中的資料己經傳送完成。我們可以把原先的緩沖區釋出來供其它的封包使用,并且用下面的程序告訴上層協定可以送新的封包下來了。
if (sis_priv->tx_full && netif_queue_stopped(net_dev) &&
sis_priv->cur_tx - sis_priv->dirty_tx < NUM_TX_DESC - 4) {
sis_priv->tx_full = 0;
netif_wake_queue (net_dev);
}
netif_wake_queue() 會使得上層協定開始傳送新的資料下來。
改變裝置的設定
int sis900_set_config(struct net_device *dev, struct ifmap *map);
處理由 ifconfig 送來的命令,在驅動程序中我們通常只處理 media type的改變。這個函數會根據 ifconfig 送來的值改變 MII 控制器的 media tyep ,你可以使用
# ifconfig eth0 media 10basT
將目前的輸出入界面強迫改到 10basT 。對于某些自動媒體檢測做的有問題的switch 而言這可能是必要的設定,但一般而言默認的 auto 是最好的設定。硬件會自動決定要使用那一個界面,使用者完全不必擔心,當實體層的設定改變 ( 例如將網絡線插到不同的地方 ) ,硬件會自動偵測并改變設定。
void set_rx_mode(struct net_device *net_dev);
改變目前封包過濾器的模式。當你使用
# ifconfig eth0 promisc
# ifconfig eth0 multicast
等命令時會被呼叫。一般而言,驅動程序的默認值是只接受目的地址和網絡卡的 MAC address 相同的封包。你可以透過 ifconfig 命令控制驅動程序接受其它種類的封包。
結語
好了 ! 我己經解析完整個網絡卡的驅動程序了。當你了解這個驅動程序后,再去了解其它的驅動程序變成一件很簡單的事情。大部份網絡驅動程序的架構其實都很類似。事實上, Linux 早期的網絡卡驅動程序幾乎是由同一個人完成的。而后來的驅動程序也幾乎都以這些驅動程序為藍本,所以看起來都很類似。你要不要也試著再去讀另一個網絡驅動程序的源代碼呢 ? 也許 你會開始抱怨怎幺寫驅動程序這幺神秘的東西怎幺變得如此簡單了 !
多馀的一節
這一節多馀的,你不想看就算了 :-) 為了證明網絡驅動程序之間有多類似我再簡略的 trace Intel eepro100 的驅程程序給大家看。不羅唆,馬上開始。
初始化
static struct pci_device_id eepro100_pci_tbl[] __devinitdata = {
{ PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82557,
PCI_ANY_ID, PCI_ANY_ID, },
{ PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82559ER,
PCI_ANY_ID, PCI_ANY_ID, },
{ PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_ID1029,
PCI_ANY_ID, PCI_ANY_ID, },
{ PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_ID1030,
PCI_ANY_ID, PCI_ANY_ID, },
{ PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82820FW_4,
PCI_ANY_ID, PCI_ANY_ID, },
};
MODULE_DEVICE_TABLE(pci, eepro100_pci_tbl);
tatic struct pci_driver eepro100_driver = {
name: "eepro100",
id_table: eepro100_pci_tbl,
probe: eepro100_init_one,
remove: eepro100_remove_one,
#ifdef CONFIG_EEPRO100_PM
suspend: eepro100_suspend,
resume: eepro100_resume,
#endif
};
return pci_module_init(&eepro100_driver);
嗯 ! 一切都不出意類之外,是吧 !
初始化裝置
eepro100_init_one()
這個看起來比 SIS900 的復雜多了。不過幾個關鑒的函數還是一樣,只是它的程序碼看起比較亂。 BSD 的人喜歡說 Linux 的程序碼太亂 ! 嗯,好象不承認不行 :-) 不過我說它亂的很可愛,行了吧 !
傳送封包
speedo_start_xmit(struct sk_buff *skb, struct net_device *dev)
這個函數相似到我不必做任何講解,也不必有任何文件你就可以知道它在做些什幺事了 ! 程序碼幾乎到了一行對一行的程度 ( 夸張了一點 ! 不過很接近事實。我信相 SIS900 的 driver 是很整個程序 copy 過去再修改的 )
中斷處理
void speedo_interrupt(int irq, void *dev_instance, struct pt_regs *regs);
這個函數,我再喜歡 Linux 也不得不抱怨一下了。 Donald Becker 先生,能麻煩程序寫的好看一點好嗎 ?
基本上,它把 sis900_rx 的內容直接放在中斷處理函數之中。不過我想分開還是會清楚一些。
speedo_tx_buffer_gc 基本上就是 sis900_finish_xmit 。下面的程序是不是很眼熟呢 ?
dirty_tx = sp->dirty_tx;
while ((int)(sp->cur_tx - dirty_tx) > 0) {
int entry = dirty_tx % TX_RING_SIZE;
int status = le32_to_cpu(sp->tx_ring[entry].status);
}
連變數名字都很像呢 !
不過 eepro100 的驅動程序沒有實作 set_config 的界面,所以你不能用ifconfig 來改變 media type 。不過 eepro100 提供了由模塊命令列選項改變的功 能,當然它是不及 set_config 來的方便就是了。
還要再來一個嗎 ? 你自己去做吧 !!!