Linux 下wifi 驅動開發(三)—— SDIO介面WiFi驅動淺析
SDIO-Wifi模組是基於SDIO介面的符合wifi無線網路標準的嵌入式模組,內建無線網路協議IEEE802.11協議棧以及TCP/IP協議棧,能夠實現使用者主平臺數據通過SDIO口到無線網路之間的轉換。SDIO具有傳輸資料快,相容SD、MMC介面等特點。
對於SDIO介面的wifi,首先,它是一個sdio的卡的裝置,然後具備了wifi的功能,所以,註冊的時候還是先以sdio的卡的裝置去註冊的。然後檢測到卡之後就要驅動他的wifi功能了,顯然,他是用sdio的協議,通過發命令和資料來控制的。下面先簡單回顧一下SDIO的相關知識:
一、SDIO相關基礎知識解析
1、SDIO介面
SDIO 故名思義,就是 SD 的 I/O 介面(interface)的意思,不過這樣解釋可能還有點抽像。更具體的說明,SD 本來是記憶卡的標準,但是現在也可以把 SD 拿來插上一些外圍介面使用,這樣的技術便是 SDIO。
所以 SDIO 本身是一種相當單純的技術,透過 SD 的 I/O 接腳來連線外部外圍,並且透過 SD 上的 I/O 資料接位與這些外圍傳輸資料,而且 SD 協會會員也推出很完整的 SDIO stack 驅動程式,使得 SDIO 外圍(我們稱為SDIO 卡)的開發與應用變得相當熱門。
現在已經有非常多的手機或是手持裝置都支援 SDIO 的功能(SD 標準原本就是針對 mobile device 而制定),而且許多 SDIO 外圍也都被開發出來,讓手機外接外圍更加容易,並且開發上更有彈性(不需要內建外圍)。目前常見的 SDIO 外圍(SDIO 卡)有:
· Wi-Fi card(無線網路卡)
· CMOS sensor card(照相模組)
· GPS card
· GSM/GPRS modem card
· Bluetooth card
SDIO 的應用將是未來嵌入式系統最重要的介面技術之一,並且也會取代目前 GPIO 式的 SPI 介面。
2、SDIO匯流排
SDIO匯流排 和 USB匯流排 類似,SDIO也有兩端,其中一端是HOST端,另一端是device端。所有的通訊都是由HOST端 傳送 命令 開始的,Device端只要能解析命令,就可以相互通訊。
CLK訊號:HOST給DEVICE的 時鐘訊號,每個時鐘週期傳輸一個命令。
CMD訊號:雙向 的訊號,用於傳送 命令 和 反應。
DAT0-DAT3 訊號:四條用於傳送的資料線。
VDD訊號:電源訊號。
VSS1,VSS2:電源地訊號。
3、SDIO熱插拔原理
方法:設定一個 定時器檢查 或 插拔中斷檢測
硬體:假如GPG10(EINT18)用於SD卡檢測
GPG10 為高電平 即沒有插入SD卡
GPG10為低電平 即插入了SD卡
4、SDIO命令
SDIO總線上都是HOST端發起請求,然後DEVICE端迴應請求。sdio命令由6個位元組組成。
a -- Command:用於開始傳輸的命令,是由HOST端發往DEVICE端的。其中命令是通過CMD訊號線傳送的。
b -- Response:迴應是DEVICE返回的HOST的命令,作為Command的迴應。也是通過CMD線傳送的。
c -- Data:資料是雙向的傳送的。可以設定為1線模式,也可以設定為4線模式。資料是通過DAT0-DAT3訊號線傳輸的。
SDIO的每次操作都是由HOST在CMD線上發起一個CMD,對於有的CMD,DEVICE需要返回Response,有的則不需要。
對於讀命令,首先HOST會向DEVICE傳送命令,緊接著DEVICE會返回一個握手訊號,此時,當HOST收到迴應的握手訊號後,會將資料放在4位的資料線上,在傳送資料的同時會跟隨著CRC校驗碼。當整個讀傳送完畢後,HOST會再次傳送一個命令,通知DEVICE操作完畢,DEVICE同時會返回一個響應。
對於寫命令,首先HOST會向DEVICE傳送命令,緊接著DEVICE會返回一個握手訊號,此時,當HOST收到迴應的握手訊號後,會將資料放在4位的資料線上,在傳送資料的同時會跟隨著CRC校驗碼。當整個寫傳送完畢後,HOST會再次傳送一個命令,通知DEVICE操作完畢,DEVICE同時會返回一個響應。
二、SDIO介面驅動
前面講到,SDIO介面的wifi,首先,它是一個sdio的卡的裝置,然後具備了wifi的功能,所以SDIO介面的WiFi驅動就是在wifi驅動外面套上了一個SDIO驅動的外殼,SDIO驅動仍然符合裝置驅動的分層與分離思想:
裝置驅動層(wifi 裝置)
|
核心層(向上向下提供介面)
|
主機驅動層 (實現SDIO驅動)
下面先分析SDIO介面驅動的實現,看幾個重要的資料結構(用於核心層與主機驅動層 的資料交換處理)。
[ /include/linux/mmc/host.h ]
struct mmc_host 用來描述卡控制器
struct mmc_card 用來描述卡
struct mmc_driver 用來描述 mmc 卡驅動
struct sdio_func 用來描述 功能裝置
struct mmc_host_ops 用來描述卡控制器操作介面函式功能,用於從 主機控制器層向 core 層註冊操作函式,從而將core 層與具體的主機控制器隔離。也就是說 core 要操作主機控制器,就用這個 ops 當中給的函式指標操作,不能直接呼叫具體主控制器的函式。
1、編寫Host層驅動
這裡參考的是S3C24XX的HOST驅動程式 /drivers/mmc/host/s3cmci.c
static struct platform_driver s3cmci_driver = {
.driver = {
.name = "s3c-sdi", //名稱和平臺裝置定義中的對應
.owner = THIS_MODULE,
.pm = s3cmci_pm_ops,
},
.id_table = s3cmci_driver_ids,
.probe = s3cmci_probe, //平臺裝置探測介面函式
.remove = __devexit_p(s3cmci_remove),
.shutdown = s3cmci_shutdown,
};
s3cmci_probe(struct platform_device *pdev)
{
//....
struct mmc_host *mmc;
mmc = mmc_alloc_host(sizeof(struct s3cmci_host), &pdev->dev); //分配mmc_host結構體
//.....
}
/*註冊中斷處理函式s3cmci_irq,來處理資料收發過程引起的各種中斷*/
request_irq(host->irq, s3cmci_irq, 0, DRIVER_NAME, host) //註冊中斷處理函式s3cmci_irq
/*註冊中斷處理s3cmci_irq_cd函式,來處理熱撥插引起的中斷,中斷觸發的形式為上升沿、下降沿觸發*/
request_irq(host->irq_cd, s3cmci_irq_cd,IRQF_TRIGGER_RISING |IRQF_TRIGGER_FALLING, DRIVER_NAME, host)
mmc_add_host(mmc); //initialise host hardware //向MMC core註冊host驅動
----> device_add(&host->class_dev); //新增裝置到mmc_bus_type總線上的裝置連結串列中
----> mmc_start_host(host); //啟動mmc host
/*MMC drivers should call this when they detect a card has been inserted or removed.檢測sd卡是否插上或移除*/
---->mmc_detect_change(host, 0);
/*Schedule delayed work in the MMC work queue.排程延時工作佇列*/
mmc_schedule_delayed_work(&host->detect, delay);
搜尋host->detected得到以下資訊:
[/drivers/mmc/core/host.c]
NIT_DELAYED_WORK(&host->detect, mmc_rescan);
mmc_rescan(struct work_struct *work)
---->mmc_bus_put(host);//card 從bus上移除時,釋放它佔有的匯流排空間
/*判斷當前mmc host控制器是否被佔用,當前mmc控制器如果被佔用,那麼 host->claimed = 1;否則為0
*如果為1,那麼會在while(1)迴圈中呼叫schedule切換出自己,當佔用mmc控制器的操作完成之後,執行 *mmc_release_host()的時候,會啟用登記到等待佇列&host->wq中的其他 程式獲得mmc主控制器的使用權
*/
mmc_claim_host(host);
mmc_rescan_try_freq(host, max(freqs[i], host->f_min);
static int mmc_rescan_try_freq(struct mmc_host *host, unsigned freq)
{
…
/* Order's important: probe SDIO, then SD, then MMC */
if (!mmc_attach_sdio(host))
return 0;
if (!mmc_attach_sd(host))
return 0;
if (!mmc_attach_mmc(host))
return 0;
….
}
mmc_attach_sdio(struct mmc_host *host) //匹配sdio介面卡
--->mmc_attach_bus(host, &mmc_sdio_ops);
/*當card與總線上的驅動匹配,就初始化card*/
mmc_sdio_init_card(host, host->ocr, NULL, 0);
--->card = mmc_alloc_card(host, NULL);//分配一個card結構體
mmc_set_bus_mode(host, MMC_BUSMODE_PUSHPULL); //設定mmc_bus的工作模式
struct sdio_func *sdio_func[SDIO_MAX_FUNCS]; //SDIO functions (devices)
sdio_init_func(host->card, i + 1);
--->func = sdio_alloc_func(card); //分配struct sdio_fun(sdio功能裝置)結構體
mmc_io_rw_direct();
card->sdio_func[fn - 1] = func;
mmc_add_card(host->card); //將具體的sdio裝置掛載到mmc_bus_types 匯流排
sdio_add_func(host->card->sdio_func[i]); //將sdio功能裝置掛載到sdio_bus_types匯流排
這裡一系列函式呼叫在前面的SD驅動蚊帳中已經闡述過了,不再詳細闡述
2、SDIO裝置的熱插拔
當插拔SDIO裝置,會觸發中斷通知到CPU,然後執行卡檢測中斷處理函式在這個中斷服務函式中,mmc_detect_change->mmc_schedule_delayed_work(&host->detect,delay), INIT_DELAYED_WORK(&host->detect, mmc_rescan)會排程mmc_rescan函式延時排程工作佇列,這樣也會觸發SDIO裝置的初始化流程,檢測到有效的SDIO裝置後,會將它註冊到系統中去。
static irqreturn_t s3cmci_irq_cd(int irq, void *dev_id)
{
struct s3cmci_host *host = (struct s3cmci_host *)dev_id;
........
mmc_detect_change(host->mmc, msecs_to_jiffies(500));
return IRQ_HANDLED;
}
三、wifi 驅動部分解析
wifi驅動的通用的軟體架構
1. 分為兩部分,上面為主機端驅動,下面是我們之前所說的firmware
2. 其中韌體部分的主要工作是:因為天線接受和傳送回來的都是802.11幀的幀,而主機接受和傳送出來的資料都必須是802.3的幀,所以必須由firmware來負責802.3的幀和802.11幀之間的轉換
3. 當天線收到資料,並被firmware處理好後會放在一個buffer裡,併產生一箇中斷,主機在收到中斷後就去讀這個buffer。
SDIO裝置的驅動由sdio_driver結構體定義,sdio_driver其實是driver的封裝。通過sdio_register_driver函式將SDIO裝置驅動載入進核心,其實就是掛載到sdio_bus_type總線上去。
1、裝置驅動的註冊與匹配
[Drivers/net/wireless/libertas/if_sdio.c]
/* SDIO function device driver*/
struct sdio_driver {
char *name; //裝置名
const struct sdio_device_id *id_table; //裝置驅動ID
int (*probe)(struct sdio_func *, const struct sdio_device_id *);//匹配函式
void (*remove)(struct sdio_func *);
struct device_driver drv;
};
下面是具體函式的填充:
/*if_sdio.c*/
static struct sdio_driver if_sdio_driver = {
.name = "libertas_sdio",
.id_table = if_sdio_ids, //用於裝置與驅動的匹配
.probe = if_sdio_probe,
.remove = if_sdio_remove,
.drv = {
.pm = &if_sdio_pm_ops,
}
};
設備註冊函式
/**
* sdio_register_driver - register a function driver
* @drv: SDIO function driver
*/
int sdio_register_driver(struct sdio_driver *drv)
{
drv->drv.name = drv->name;
drv->drv.bus = &sdio_bus_type; //設定driver的bus為sdio_bus_type
return driver_register(&drv->drv);
}
匯流排函式
static struct bus_type sdio_bus_type = {
.name = "sdio",
.dev_attrs = sdio_dev_attrs,
.match = sdio_bus_match,
.uevent = sdio_bus_uevent,
.probe = sdio_bus_probe,
.remove = sdio_bus_remove,
.pm = SDIO_PM_OPS_PTR,
};
注意:裝置或者驅動註冊到系統中的過程中,都會呼叫相應bus上的匹配函式來進行匹配合適的驅動或者裝置,對於sdio裝置的匹配是由sdio_bus_match和sdio_bus_probe函式來完成。
static int sdio_bus_match(struct device *dev, struct device_driver *drv)
{
struct sdio_func *func = dev_to_sdio_func(dev);
struct sdio_driver *sdrv = to_sdio_driver(drv);
if (sdio_match_device(func, sdrv))
return 1;
return 0;
}
static const struct sdio_device_id *sdio_match_device(struct sdio_func *func,
struct sdio_driver *sdrv)
{
const struct sdio_device_id *ids;
ids = sdrv->id_table;
if (sdio_match_one(func, ids))
return ids;
}
由以上匹配過程來看,通過匹配id_table 和 sdio_driver裝置驅動中id,來匹配合適的驅動或裝置。最終會呼叫.probe函式,來完成相關操作。
2、If_sdio_probe函式
當檢測到sdio卡插入了之後就會呼叫If_sdio_probe,而當卡被移除後就會呼叫If_sdio_remove。
下面先看下If_sdio_probet函式,if_sdio_prob 函式 主要做了兩件事
static struct sdio_driver if_sdio_driver = {
.name = "libertas_sdio",
.id_table = if_sdio_ids, //用於裝置和驅動的匹配
.probe = if_sdio_probe,
.remove = if_sdio_remove,
.drv = {
.pm = &if_sdio_pm_ops,
},
};
1 //定義一個 if_sdio card的結構體
struct if_sdio_card *card;
struct if_sdio_packet *packet; //sdio 包的結構體
struct mmc_host *host = func->card->host;
// 查詢是否有指定的功能暫存器在mmc
//_sdio_card中
for (i = 0;i < func->card->num_info;i++) {
if (sscanf(func->card->info[i],
"802.11 SDIO ID: %x", &model) == 1)
//在這裡進行片選 選擇到我們使用的marvell 8686 的裝置
case MODEL_8686:
card->scratch_reg = IF_SDIO_SCRATCH;
//建立sdio 的工作佇列
card->workqueue = create_workqueue("libertas_sdio");
//呼叫下面的函式
INIT_WORK(&card->packet_worker, if_sdio_host_to_card_worker);
//主機到卡的工作佇列
static void if_sdio_host_to_card_worker(struct work_struct *work)
/* Check if we support this card 選擇我們所支援的卡的型別*/
//賦值為sd8686_helper.bin sd8686.bin
/*fw_table 中的 MODEL_8686, "sd8686_helper.bin", "sd8686.bin" },?/
for (i = 0; i < ARRAY_SIZE(fw_table); i++) {
if (card->model == fw_table[i].model)
break;
}
{ MODEL_8688, "libertas/sd8688_helper.bin", "libertas/sd8688.bin" },
//申請一個host
sdio_claim_host(func);
//使能sdio 的功能 暫存器
ret = sdio_enable_func(func);
if (ret)
goto release;
2//申請 sdio 的中斷 當有資料 ,命令 或者是事件 的時間執行中斷
ret = sdio_claim_irq(func, if_sdio_interrupt);
ret = if_sdio_card_to_host(card); //從無線網絡卡接收到資料 或者說是上報資料
ret = if_sdio_handle_data(card, card->buffer + 4, chunk - 4); //接收資料的處理
ret = if_sdio_handle_cmd(card, card->buffer + 4, chunk - 4); //處理申請的命令中斷
ret = if_sdio_handle_event(card, card->buffer + 4, chunk - 4);//處理申請的事件中斷
//新增網路結構體 分配裝置並註冊
priv = lbs_add_card(card, &func->dev);
//分配Ethernet裝置並註冊
wdev = lbs_cfg_alloc(dmdev);
//802無線網的具體的操作函式
wdev->wiphy = wiphy_new(&lbs_cfg80211_ops, sizeof(struct lbs_private));
//分配網路裝置是整個網路部分操作的
//的核心結構體
dev = alloc_netdev(0, "wlan%d", ether_setup); //例項化wlan0的屬性
dev->ieee80211_ptr = wdev;
dev->ml_priv = priv;
//設定裝置的實體地址
SET_NETDEV_DEV(dev, dmdev);
wdev->netdev = dev;
priv->dev = dev;
//初始化網路裝置 ops. 看門狗
dev->netdev_ops = &lbs_netdev_ops; //網路裝置的具體的操作函式
dev->watchdog_timeo = 5 * HZ;
dev->ethtool_ops = &lbs_ethtool_ops;
dev->flags |= IFF_BROADCAST | IFF_MULTICAST; //廣播或者多播
//啟動一個核心執行緒來管理這個網路裝置的資料傳送,事件的處理(卡的拔出)和一些命令的處理
priv->main_thread = kthread_run(lbs_thread, dev, "lbs_main");
//初始化相關的工作佇列
priv->work_thread = create_singlethread_workqueue("lbs_worker");
INIT_WORK(&priv->mcast_work, lbs_set_mcast_worker);
priv->wol_criteria = EHS_REMOVE_WAKEUP;
priv->wol_gpio = 0xff;
priv->wol_gap = 20;
priv->ehs_remove_supported = true;
//設定私有變數
//設定主機發送資料到卡
priv->hw_host_to_card = if_sdio_host_to_card;
priv->enter_deep_sleep = if_sdio_enter_deep_sleep;
priv->exit_deep_sleep = if_sdio_exit_deep_sleep;
priv->reset_deep_sleep_wakeup = if_sdio_reset_deep_sleep_wakeup;
sdio_claim_host(func);
//啟動卡裝置
ret = lbs_start_card(priv);
if (lbs_cfg_register(priv))
ret = register_netdev(priv->dev);
err = register_netdevice(dev);
//具體的wifi裝置驅動功能
//網路裝置操作的具體函式
static const struct net_device_ops lbs_netdev_ops = {
.ndo_open = lbs_dev_open, //開啟
.ndo_stop = lbs_eth_stop, //停止
.ndo_start_xmit = lbs_hard_start_xmit, //開始傳送資料
.ndo_set_mac_address = lbs_set_mac_address, //設定mac地址
.ndo_tx_timeout = lbs_tx_timeout, //傳送超時
.ndo_set_multicast_list = lbs_set_multicast_list, //多播地址
.ndo_change_mtu = eth_change_mtu, //最大傳輸單元
.ndo_validate_addr = eth_validate_addr, //判斷地址的有效性
3、資料的接收,通過中斷的方式來解決
網路裝置接收資料的主要方法是由中斷引發裝置的中斷處理函式,中斷處理函式判斷中斷的型別,如果為接收中斷,則讀取接收到的資料,分配sk_buff資料結構和資料緩衝區,並將接收的資料複製到資料快取區,並呼叫netif_rx()函式將sk_buff傳遞給上層協議。
搜尋if_sdio_interrupt,可知道它是在if_sdio.c檔案中if_sdio_probe()函式中sdio_claim_irq(func, if_sdio_interrupt) ,func->irq_handler = if_sdio_interrupt。當s3cmci_irq中斷處理函式的S3C2410_SDIIMSK_SDIOIRQ 中斷被觸發時將呼叫if_sdio_interrupt()函式,進行接收資料。
static void if_sdio_interrupt(struct sdio_func *func)
ret = if_sdio_card_to_host(card); //從無線網絡卡接收到資料 或者說是上報資料
//讀取埠上的資料 ,放到card的buffer中
ret = sdio_readsb(card->func, card->buffer, card->ioport, chunk);
1.在這裡一方面處理中斷 還有2
switch (type) { //處理cmd data event的請求
case MVMS_CMD:
ret = if_sdio_handle_cmd(card, card->buffer + 4, chunk - 4); //處理申請的命令中斷
if (ret)
goto out;
break;
case MVMS_DAT:
ret = if_sdio_handle_data(card, card->buffer + 4, chunk - 4);//處理申請的資料中斷
if (ret)
goto out;
break;
case MVMS_EVENT:
ret = if_sdio_handle_event(card, card->buffer + 4, chunk - 4);//處理申請的事件中斷
//讀取包的過程
lbs_process_rxed_packet(card->priv, skb);
//如果是中斷 ,就把skb這個包提交給協議層,這個函式是
//協議層提供的 netif_rx(skb)
if (in_interrupt())
netif_rx(skb); //提交給協議層
2//讀取埠上的資料 ,放到card的buffer中
ret = sdio_readsb(card->func, card->buffer, card->ioport, chunk);
//讀取地址,目的地址,數量 等
int sdio_readsb(struct sdio_func *func, void *dst, unsigned int addr, int count)
return sdio_io_rw_ext_helper(func, 0, addr, 0, dst, count);
ret = mmc_io_rw_extended(func->card, write,func->num, addr, incr_addr, buf,blocks, func->cur_blksize);
cmd.arg = write ? 0x80000000 : 0x00000000;
//wait for request
mmc_wait_for_req(card->host, &mrq);
開始應答
mmc_start_request(host, mrq);
wait_for_completion(&complete);
host->ops->request(host, mrq);
4、資料傳送
//IP層通過dev_queue_xmit()將資料交給網路裝置協議介面層,網路介面層通過netdevice中的註冊函式的資料傳送函式
int dev_queue_xmit(struct sk_buff *skb)
if (!netif_tx_queue_stopped(txq)) {
__this_cpu_inc(xmit_recursion);
//裝置硬體開始傳送
rc = dev_hard_start_xmit(skb, dev, txq);
//呼叫wifi網路中的ops
rc = ops->ndo_start_xmit(skb, dev);
dev->netdev_ops = &lbs_netdev_ops; //裝置的操作函式
//處理sdio firware資料和核心的資料main_thread 主執行緒
priv->main_thread = kthread_run(lbs_thread, dev, "lbs_main");
//呼叫host_to_card 即if_sdio_card_to_host函式。
int ret = priv->hw_host_to_card(priv, MVMS_DAT,priv->tx_pending_buf,priv->tx_pending_len);
為什麼是if_sdio_to_host呢 ?因為在prob函式中定義了這一個
//設定主機發送資料到卡
priv->hw_host_to_card = if_sdio_host_to_card;
static int if_sdio_host_to_card(struct lbs_private *priv,u8 type, u8 *buf, u16 nb)
//把buf中的資料 copy到sdio 包中,在對sdio 的包進行處理
memcpy(packet->buffer + 4, buf, nb);
//建立工作佇列
queue_work(card->workqueue, &card->packet_worker);
//初始化佇列
INIT_WORK(&card->packet_worker, if_sdio_host_to_card_worker);
//sdio的寫資料
ret = sdio_writesb(card->func, card->ioport, packet->buffer, packet->nb);
//mmc寫擴充套件口
ret = mmc_io_rw_extended(func->card, write,func->num, addr, incr_addr, buf,blocks, func->cur_blksize);
//wait for request
mmc_wait_for_req(card->host, &mrq);
mrq->done_data = &complete;
mrq->done = mmc_wait_done;
mmc_start_request(host, mrq);
//完成等待 寫資料結束
wait_for_completion(&complete);
host->ops->request(host, mrq);
//到底結束 傳送資料
5、移除函式
當sdio卡拔除時,驅動會呼叫該函式,完成相應操作。如釋放佔有的資源,禁止func功能函式,釋放host。
if_sdio_remove(struct sdio_func *func)
---->lbs_stop_card(card->priv);
lbs_remove_card(card->priv);
---->kthread_stop(priv->main_thread); //終止核心執行緒
lbs_free_adapter(priv);
lbs_cfg_free(priv);
free_netdev(dev);
flush_workqueue(card->workqueue); //重新整理工作佇列
destroy_workqueue(card->workqueue);
sdio_claim_host(func);
sdio_release_irq(func);
sdio_disable_func(func);
sdio_release_host(func);