1. 程式人生 > >STM32驅動Marvell 88W8686 WiFi模組程式碼說明(20180129版)

STM32驅動Marvell 88W8686 WiFi模組程式碼說明(20180129版)

一、概述

88W8686是Marvell公司2007年推出的一款SDIO Wi-Fi晶片,使用簡單的SPI或SDIO協議就可以與微控制器連線起來,操作方便,具有建立無密碼或帶有WEP密碼的Ad-Hoc熱點的功能,以及連線無密碼或帶有WEP、WPA/WPA2密碼的路由器的功能。不過有一點要注意,安卓手機預設是不能連線Ad-Hoc型的熱點的(Win10電腦好像也不行),必須要打補丁才行。也就是說,安卓手機預設是無法連線88W8686建立的熱點的,這種情況下最好讓安卓手機開一個普通熱點(就是路由器建立的那種非Ad-Hoc熱點),讓Wi-Fi模組自己去連線。

WM-G-MR-9是環旭電子生產的基於Marvell 88W8686的一款Wi-Fi晶片,引腳數遠少於88W8686,同樣也支援SDIO和SPI介面,使用起來和88W8686是一樣的。不過,淘寶網上關於該晶片的搜尋結果極少,甚至光是晶片就要20~40元一個,有一家還賣到了175元一個!淘寶網上12塊錢左右就可以買到88W8686晶片,但是帶該晶片的模組卻寥寥無幾,帶WM-G-MR-9晶片的模組也很貴,要85元,所以建議實際做產品的時候為了降低成本,直接用88W8686貼片晶片,自己畫一個PCB板子和微控制器連線起來,不要用人家封裝好的模組。

Marvell公司近年來還推出了88W8782和88W8801晶片,功能也比88W8686更強大,在淘寶網上無論是裸晶片還是帶晶片的模組都要便宜很多,一個模組20塊錢就能買到。但是由於缺少文件和資料,筆者目前只成功實現了韌體下載,未能用程式實現其他的功能。

SD協會 (www.sdcard.org) 定義了兩類卡:SD卡和SD I/O卡。SD卡就是我們平時插在手機裡面那種用來儲存資料的記憶體卡,而SD I/O卡(簡稱SDIO卡)則是一種通過SD協議與主機通訊的裝置。有些型別的SDIO卡還擁有和SD記憶體卡一樣的儲存功能,這樣的卡稱為Combo卡。兩種卡都使用SD協議與主機通訊,在主機不支援SD協議的情況下都可以用SPI方式來代替,但兩種卡的初始化過程和收發資料所用的命令是不一樣的,SD卡支援的命令SDIO卡不一定支援,SDIO卡也有一些特有的命令。88W8686是一種不帶儲存功能的SDIO卡。在SD協會的官網上可以下載到SD卡和SDIO卡通訊協議的詳細文件。講SD卡的文件的名稱是“Part 1 Physical Layer Simplified Specification”,講SDIO卡的則是“Part E1 SDIO Simplified Specification”。值得注意的是,網站上還有一個名叫“Part E7 Wireless LAN Simplified Addendum”的文件,講的是一種帶Wi-Fi功能的SDIO卡,不過這種卡和88W8686晶片沒有任何關係,文件裡面講的內容根本就不適合88W8686晶片。

微控制器上電後,需要先初始化Wi-Fi模組的SDIO通訊介面,完成SDIO卡的識別過程,接著將晶片韌體下載到晶片內的CPU裡執行,然後傳送Wi-Fi命令掃描熱點,與其中一個熱點建立關聯並認證或者自己建立一個熱點,這時才能在資料鏈路層上收發資料幀。此外,我們還需要一個網路協議棧,在資料鏈路層的基礎上實現網路層的網際路由,以及運輸層的TCP和UDP協議,這樣才能使用套接字與網路中的其他計算機或裝置進行通訊。這裡我們選用的是lwip協議棧。

二、SDIO通訊介面的初始化過程

微控制器啟動後首先會執行main函式,main函式初始化完USART1串列埠後,呼叫了rtc_init函式和Wi-Fi模組的初始化函式WiFi_Init。rtc_init函式的作用是初始化STM32的RTC時鐘,讓RTC開始走時,使common.c中的sys_now函式可以獲取到精確到毫秒的系統時間,並使delay函式可以實現毫秒級的延時。程式進入WiFi_Init函式後便會呼叫WiFi_LowLevel_Init函式,初始化Wi-Fi模組執行時需要使用的STM32底層外設。該函式先初始化STM32微控制器SDIO外設對應的GPIO引腳,將SDIO的時鐘引腳PC12、命令引腳PD2以及4個數據引腳PC8~11全部設為複用推輓輸出模式,然後將Wi-Fi模組的電源控制引腳PB12設為推輓輸出並輸出預設的低電平,使串聯在Wi-Fi模組VCC電源引腳上的三極體導通,Wi-Fi模組通電工作。GPIO初始化完畢後便開始初始化SDIO外設。程式先在RCC上開啟SDIO外設的時鐘,然後啟動SDIO外設,設定好SDIO時鐘輸出引腳的頻率,並將SDIO外設設為SDIO模式,接下來就進入了SDIO卡的識別過程。

筆者所用的Wi-Fi開發板上的Wi-Fi模組只引出了電源引腳和SDIO通訊引腳,並沒有把晶片的復位引腳引出來,所以為了使微控制器復位後模組也能跟著復位,Wi-Fi模組的電源引腳VCC並不是直接連線到電源上的,而是中間串聯了一個場效電晶體,該場效電晶體可以用PNP三極體代替,控制埠為微控制器的PB12。微控制器復位後PB12是沒有輸出的(高阻態),此時三極體截止,Wi-Fi模組不通電。只要讓PB12輸出低電平,三極體就能導通,Wi-Fi模組就能通電工作,這間接起到了復位的效果。

Wi-Fi模組復位非常重要。如果模組不能復位,那麼微控制器接下來下載韌體1時傳送的資料就會被模組視為無效的資料而丟棄,然後程式卡死在下載韌體2之前的while迴圈等待語句裡面。

三、SDIO卡的識別過程

根據SDIO標準規範(Part E1),SDIO卡識別過程中SDIO的時鐘頻率最高不能超過400kHz。STM32F1系列微控制器的SDIO外設使用兩個時鐘,第一個是SDIOCLK=HCLK=72MHz,該時鐘經過分頻後用於產生SDIO_CK(PC12)引腳時鐘,另一個是AHB bus clock=HCLK/2=36MHz,主要用於SDIO外設的暫存器訪問。SDIO_CK引腳的輸出頻率等於SDIOCLK÷(分頻係數+2),為了輸出400kHz的時鐘,分頻係數應該設定為72÷0.4-2=178。這裡順便說一下,賣家給的程式裡面就是因為儲存微控制器執行頻率的變數host->clk_rate沒有正確賦值,所以才會在串列埠上顯示錯誤的頻率值(500多kHz),我估計是他在把STM32F2的程式移植到F1的過程中,忘了把120MHz改成72MHz才導致的,因為F2微控制器的最高時鐘頻率是120MHz,而且淘寶網上也有賣家在賣用F205微控制器做的Wi-Fi開發板。把該變數的值改成72000000後,串列埠上輸出的就是正確的400kHz初始化頻率和24000kHz的執行頻率。

識別一張SDIO卡需要傳送4個命令:兩個CMD5、CMD3和CMD7。CMD5用於器件電壓匹配,其迴應告訴了我們該SDIO卡的資訊,包括電壓適用範圍,功能區數量,以及該卡是否為帶有儲存功能的Combo卡。88W8686擁有兩個功能區:Function 0~1,88W8801擁有四個功能區:Function 0~3,都屬於不帶儲存功能的普通SDIO卡。所有的SDIO卡都有0號功能區,並且所有的功能區都有相應的CIS(Card Information Structure)資訊,以及很多暫存器。CIS資訊包含了產品的詳細資訊,例如產品序列號和製造商的資訊。CMD3用於獲取SDIO卡的RCA地址,後續操作都用這個地址來代表這張卡。雖然SD標準規定了SDIO匯流排可以掛接多張SD卡,但是STM32晶片手冊(Datasheet)上明確說明了STM32微控制器的SDIO介面只支援一張卡,RCA地址不能起到“軟體片選”的作用。如果要同時使用Wi-Fi模組和SD記憶體卡,則其中一張卡必須改用SPI介面。CMD7用於選中指定RCA地址的卡。選中Wi-Fi模組後,卡識別過程就結束了,就可以提高SDIO的時鐘頻率了。SDIO最高支援的頻率為25MHz,這遠遠大於串列埠Wi-Fi模組的頻率。程式中提供了兩種時鐘頻率供大家選擇,較低的頻率是1MHz,較高的頻率是24MHz,如果WiFi.h中定義了WIFI_HIGHSPEED巨集,那麼就選擇了24MHz的頻率。

最後,程式還把SDIO資料匯流排的寬度設成了4位模式,SDIO外設的資料寬度必須和SDIO裝置暫存器裡面的設定保持一致,否則收發資料的時候就會出現STBITERR錯誤。SDIO裝置暫存器的讀寫是通過傳送CMD52命令實現的。命令的引數包含了暫存器的地址和欲寫入的值,以及其他的引數,命令的迴應資訊則包含了讀出的暫存器的值。0號功能區的暫存器資訊可以在SDIO Part E1文件中找到,1號功能區的暫存器資訊則位於88W8686 Host Interface Registers PDF文件中。

上述過程完畢後,程式還顯示了每個功能區的CIS資訊,其中就有一個產品資訊字串,88W8686晶片對應的內容是“Marvell 802.11 SDIO ID: 0B”,88W8801晶片對應的內容是“Marvell 802.11 SDIO ID: 48”。賣家給的程式裡面就是通過這個字串來判斷Wi-Fi模組的型號是不是88W8686的,如果不是的話就提示錯誤資訊“Cann't find support modules!”並終止程式的執行。串列埠上輸出的幾個“error!”是在處理CIS資訊時輸出的。

四、韌體的下載

Wi-Fi模組內部有一個CPU,要想讓該CPU正常工作就必須要往裡面寫入程式。Marvell公司已經把這個程式開發好了,並給我們提供了該程式的二進位制資料,這段資料稱為韌體。韌體資料的傳輸是通過CMD53命令往1號功能區指定的暫存器地址wifi_port傳送資料完成的。傳送韌體資料前必須先啟用1號功能區,並且資料塊的大小必須設定為32位元組。該功能區中的IOPORT0~2暫存器告訴了我們wifi_port的地址,以後都是在這個地址上收發Wi-Fi命令和資料。

88W8686晶片有兩個韌體。第一個韌體稱為helper韌體,第二個韌體需要在第一個韌體的協助下下載,然後才能啟動執行。我們可以把這兩個韌體的資料存放到STM32微控制器的Flash區域裡面。把helper韌體的內容儲存到helper_sd.c的const unsigned char firmware_helper_sd[2516]變數中,其中const表示變數內容是儲存到Flash裡面而不是SRAM裡面的,把韌體2的內容儲存到sd8686.c的const unsigned char firmware_sd8686[122916]變數中,然後把這兩個檔案新增到工程裡面,在要使用的c檔案中宣告一下就行了。但是這樣做會大大增加程式下載到STM32微控制器的時間。因此,筆者專門編寫了一個程式,把韌體資料預先寫入到Flash儲存空間的末尾,也就是每次燒寫程式不會被Keil擦除的地方。這樣除錯燒寫的時候就不用再每次都重新燒寫韌體內容了,達到大幅度減少燒寫時間的目的。WiFi.h中定義了一個WIFI_FIRMWAREAREA_ADDR巨集,其值為 0x08061000,這就是儲存在Flash中的韌體的地址。如果註釋掉這個巨集,則必須把helper_sd.c和sd8686.c新增到工程中。為了保證Flash中儲存的韌體內容是完好無損的,程式中加入了CRC校驗,每次下載韌體之前都要先檢驗一下韌體資料是否正確。如果整個資料連同CRC校驗碼一起送入CRC校驗器計算後得出的結果為0,那麼就說明韌體內容是正確無誤的。這裡要說明的是,儲存韌體資料的程式會將淘寶賣家提供的開發板程式的配置資料覆蓋掉,導致賣家提供的程式無法正常執行,建立或關聯熱點時會提示Could not find best network。這時只需要根據賣家提供的使用手冊所說的,長按恢復出廠設定按鍵就行了,這個操作也會破壞儲存在Flash中的韌體資料。

下載helper韌體時,每次都必須傳送兩個資料塊的內容,也就是64位元組。其中,前4位元組表示本次傳送的有效資料大小,相當於一個uint32_t型的整型變數,後60位元組為韌體資料。每傳送一次CMD53,都要等待模組給出確認之後,才能傳送下一個CMD53。所有的helper韌體資料傳送完畢之後,還必須要傳送一個前4位元組為0的空資料包,通知模組helper韌體已經發送完畢,可以啟動。

下載完helper韌體就開始下載第二個韌體。每次傳送的有效資料大小curr由helper韌體在SQREADBASEADDR0~1暫存器中給出。如果curr為奇數,則表明上一次傳送的內容有誤,必須重新發送。如果curr為偶數,則必須傳送一個CMD53命令,並攜帶大於或等於curr位元組的資料。模組規定每次只能使用一個CMD53命令,可以用多位元組模式(DTMODE=1),也可以用塊傳輸模式(DTMODE=0)。使用塊傳輸模式時,傳送的資料量必須為塊大小的整數倍。例如,當curr=16時,可以用CMD53多位元組模式傳送16位元組的資料,也可以用塊傳輸模式傳送1個數據塊的資料,即32位元組資料。當SDIO_CK的時鐘頻率大於16MHz左右時,只能使用塊傳輸模式,不能使用多位元組模式。韌體2下載完畢後,等待SCRATCHPAD4_0~1暫存器的值變為0xfedc,出現該值表明韌體2啟動成功。

WiFi_LowLevel_WriteData函式用於通過CMD53命令傳送資料,其原型為:

uint8_t WiFi_LowLevel_WriteData(uint8_t func, uint32_t addr, const void *data, uint32_t size, uint32_t bufsize);

其中,func為暫存器所在的功能區,addr為暫存器的地址,data為要傳送的資料,size為資料的大小,bufsize為資料所在的緩衝區的大小。Wi-Fi模組規定,所有的資料必須由一條CMD53命令一次性發送完畢,而且CMD53引數中的資料大小必須為4的倍數。所以,該函式會根據功能區的塊大小決定是採用塊傳輸模式還是多位元組模式,並動態調整size的值。如果調整後的size值大於bufsize,就會在串列埠中輸出一條警告資訊,提示緩衝區溢位。傳送CMD53命令後,程式將資料送入SDIO->FIFO暫存器中,由SDIO外設傳送出去。SDIO外設還支援通過DMA方式傳送資料,使用的DMA通道是DMA2_Channel4。當SDIO的時鐘頻率比較高時,為了保證資料傳送成功,必須使用DMA方式傳送,畢竟CPU程式碼的執行速度是有限的,可能跟不上SDIO傳送資料的速率。是否使用DMA是由WiFi.h中的WIFI_USEDMA巨集決定的。

傳送一次CMD53命令和資料後,必須等待模組確認,然後才能繼續傳送資料。可使用CARDSTATUS暫存器或INTSTATUS暫存器的Download Ready位檢查資料是否被確認。如果DNLDRDY=1,則可以繼續傳送資料。傳送下一條CMD53命令後,CARDSTATUS中的DNLDRDY位會被自動清除,但是INTSTATUS暫存器中的DNLDRDY不能自動清除,需要手動清除。另外,使用INTSTATUS暫存器之前,必須要開啟SDIO裝置中斷。

開啟SDIO裝置中斷的方法是:先將SDIO外設設為SDIO模式,然後寫0號功能區中的INTEN暫存器,將IENM位和IEN1位置1,最後把1號功能區的INTMASK暫存器設為0x0f。INTMASK不能設為其他值,包括0x01,否則就不能產生SDIO中斷。因為程式沒有在STM32的NVIC中啟用SDIO中斷,所以SDIO中斷產生時並不會執行中斷服務函式,而只是簡單地將中斷標誌位SDIO_STA_SDIOIT置位,通知程式中斷已經產生了。

SDIO->DTIMER暫存器表示資料傳送完畢後收到迴應的超時時間,以及接收資料時最長等待的時間。如果發生超時,SDIO_STA_DTIMEOUT位將會置1。該暫存器使用的時間單位為1/f秒,f為SDIO_CK引腳輸出的時鐘頻率。如果頻率為24MHz,DTIMER= 2400000,則超時時間為0.1秒。

五、Wi-Fi命令的傳送和迴應的接收

韌體下載完畢並啟動後,就可以傳送Wi-Fi命令,讓Wi-Fi模組執行與Wi-Fi有關的操作了,比如掃描熱點、連線熱點等等。Wi-Fi命令也是通過CMD53命令傳送的,韌體下載完畢後程序就將塊大小設為了256位元組。命令幀傳送完畢後會在幾毫秒內收到確認,經過幾十毫秒到幾百毫秒的時間後會收到命令迴應幀。收到確認時,Download Ready位會置位。收到命令迴應幀時,Upload Ready位會置位,此時可以傳送CMD53命令接收回應幀的內容。CARDSTATUS暫存器和INTSTATUS暫存器都含有這兩位,但CARDSTATUS暫存器不穩定,標誌位的狀態容易丟失,所以程式中檢查的是INTSTATUS暫存器。Wi-Fi命令傳送後通常都能收到確認,但不一定能收到命令迴應幀,收不到迴應幀意味著命令執行失敗了。模組規定,傳送完一個Wi-Fi命令後,必須要收到迴應,才能傳送下一個Wi-Fi命令,迴應幀超時除外。

程式中專門建立了一個命令傳送緩衝區wifi_tx_command,這是一個WiFi_TxBuffer型別的結構體變數,該結構體擁有如下成員變數:buffer、callback、arg、busy、ready、retry、start_time和timeout。其中,buffer用於存放待發送的命令內容,callback是命令收到迴應或執行失敗後呼叫的回撥函式,呼叫時會傳入arg引數,busy表示緩衝區是否被佔用,ready表示命令是否已收到確認,retry表示剩餘重傳次數,start_time表示命令傳送的時間,timeout表示命令迴應超時時間。回撥函式共有3個引數:arg、data和status。data通常為收到的資料,status為狀態碼。傳送命令可使用WiFi_SendCommand函式,其原型為:

void WiFi_SendCommand(uint16_t code, const void *data, uint16_t size, WiFi_Callback callback, void *arg, uint32_t timeout, uint8_t max_retry);

其中,code為命令編碼,常用的命令編碼位於WiFi_CommandList列舉定義中。data為含有頭部的命令內容,size為data的大小。callback是命令執行成功或失敗後要呼叫的回撥函式,傳入的引數為arg。timeout是命令迴應的超時時間,max_retry是命令的最大重試次數。

除了韌體資料外,通過CMD53傳送的資料都含有幀頭資訊WiFi_SDIOFrameHeader。幀頭含有兩個欄位,一個是length,另一個是type,每個欄位都是16位的。length表示CMD53命令傳送的資料中含有的有效資料的長度,也就是幀的長度。幀長度可以為任意非零整數,但CMD53傳送的資料長度必須為4的倍數,而且一個完整的幀必須在一個CMD53命令內傳送完畢。type為幀的型別。幀的型別只有三種:命令幀、資料幀和事件幀。事件幀只能由模組傳送給主機,通常用於通知Wi-Fi掉線等網路事件。

命令幀除了幀頭外,還有命令頭部WiFi_CommandHeader,其中包含了命令編碼、命令幀去掉幀頭後的大小,序號和執行結果碼。命令迴應幀的命令編碼等於命令幀的編碼加上0x8000。命令頭部之後就是實際的命令引數,有定長的引數,也有不定長的引數,有些不定長的引數是可選引數。不定長的引數一般都是TLV (Tag Length Value)結構。TLV也有頭部,含有type和length欄位。type為TLV的型別,length為TLV資料欄位的大小。TLV共有兩種型別,一種是IEEE型別的TLV,其幀頭每個欄位為1位元組,另一種是Marvell型別(MrvlIE)的TLV,其幀頭每個欄位為2位元組,可使用WiFi_TranslateTLV函式將IEEE型別的TLV轉換為MrvlIE型別的TLV。

Wi-Fi模組支援的命令及其詳細格式,以及迴應幀的格式可以在韌體API手冊(WLAN Subsystem Firmware API Specification)中的Host Commands一節中查到。WiFi.h標頭檔案中包含了常用命令的格式和封裝好的函式,名稱以WiFi_Cmd_開頭的結構體可以同時表示命令幀和迴應幀,名稱以WiFi_CmdRequest_開頭的結構體只表示命令幀,名稱以WiFi_CmdResponse_開頭的結構體只表示命令迴應幀。為了能在32位微控制器上使結構體的各成員地址與實際資料的各欄位一一對應,所有這樣的結構體都加了__packed關鍵字,防止結構被編譯器優化,lwip協議棧的arch/cc.h中也有該關鍵字的巨集定義PACK_STRUCT_BEGIN。

命令傳送成功並收到迴應時,Upload Ready位會置位,同時會將STM32 SDIO外設的SDIOIT中斷標誌位置位。程式在主函式中一檢測到SDIOIT位為1後,就呼叫WiFi_Input函式接收回應並呼叫應用程式設定的回撥函式,同時通過SDIO->ICR暫存器清除中斷標誌位。如果沒有SDIOIT中斷髮生,則呼叫WiFi_CheckTimeout函式檢查是否有命令幀和資料幀超時,如果超時就重傳相應的幀。

命令迴應幀的接收是使用CMD53命令完成的,幀的大小從1號功能區的SCRATCHPAD4_0~1暫存器中讀取,收到的命令迴應幀是儲存在wifi_rx變數裡面的。由於Wi-Fi模組規定整個命令幀必須要在一個CMD53命令內接收完畢,所以wifi_rx緩衝區必須要開得足夠大。特別是在執行掃描熱點命令的時候,如果周圍的熱點比較多,那麼返回的迴應幀資料量是很大的。淘寶賣家世訊電子(現已下架)給的程式裡面就是因為掃描熱點的時候只開了1024位元組的接收緩衝區,而收到的資料往往超過了1300位元組,所以才會顯示“problem fetching packet from firmware”和“@@@@@@@@@@@@@############################################# re while”的錯誤。

WiFi_LowLevel_ReadData函式用於通過CMD53命令接收資料,其原型為:

uint8_t WiFi_LowLevel_ReadData(uint8_t func, uint32_t addr, void *data, uint32_t size, uint32_t bufsize);

引數列表和WiFi_LowLevel_WriteData完全一樣,唯一不同的是,bufsize必須嚴格大於函式中調整後的size的值,否則函式就會執行失敗。

收到命令迴應並不一定代表命令執行成功。命令迴應幀的頭部有一個result欄位,取值範圍為0~2。0表示執行成功,1表示執行失敗,2表示Wi-Fi模組不支援此命令。例如,88W8686就不支援手冊裡面的CMD_802_11_CRYPTO命令,如果硬要執行該命令,那麼收到的迴應裡面會發現result=2。

六、乙太網資料幀的傳送和接收

在資料鏈路層上收發資料幀前,必須執行CMD_MAC_CONTROL命令,開啟乙太網資料的傳送和接收功能。不執行該命令的話就只能接收資料,無法傳送資料。如果要使用WEP加密方式收發資料,則還必須用該命令開啟WEP功能。執行命令時指定WIFI_MACCTRL_ETHERNET2選項的作用是去掉收到的資料幀中SNAP頭部欄位的內容。

資料幀也有幀頭和資料頭部。傳送的資料幀的資料頭部WiFi_DataTx包含了資料幀目的MAC地址、資料幀大小以及優先順序等資訊,以及資料鏈路層上的重試次數,預設的重試次數為2。接收的資料幀的資料頭部WiFi_DataRx則包含了幀大小、接收速率、信噪比以及優先順序等資訊。收到資料幀時會呼叫WiFi_PacketHandler函式,該函式會呼叫ethernetif_input函式通知lwip網絡卡收到了新資料。

ethernetif.c提供了網絡卡驅動程式與lwip協議棧之間的介面。收到資料時會呼叫ethernetif_input函式,進而呼叫low_level_input函式,在該函式中會將網絡卡收到的資料傳遞給lwip,經過lwip協議棧以及高層應用的處理後,回到資料鏈路層,產生要傳送的資料,並呼叫low_level_output函式將資料傳送出去。WiFi_SendPacket就是傳送資料幀的函式。

程式中專門建立了一個數據傳送緩衝區wifi_tx_packet.buffer。WiFi_GetPacketBuffer函式用於獲取該緩衝區的地址,該函式保證了使用傳送緩衝區前緩衝區未被佔用。

七、命令通道和資料通道的同步、確認和超時重傳

Wi-Fi模組執行命令需要幾十到幾百毫秒的時間,而資料幀只需要幾毫秒就能傳送出去,命令執行期間可能會收到很多很多資料幀。因此,傳送命令後用while迴圈輪詢等待命令迴應,在裸機環境下是不現實的。要是把收到命令迴應前收到的資料幀全部丟棄掉,又會導致資料傳送端不必要的重傳,降低了網路的吞吐量,同時也會大大延長最終收到命令迴應的時間。所以,命令通道和資料通道必須要採取同步措施。

程式規定,呼叫命令幀傳送函式WiFi_SendCommand前必須保證命令緩衝區未被佔用,檢查命令緩衝區是否被佔用可使用WiFi_IsCommandBusy函式。資料幀傳送函式WiFi_SendPacket可以在任意時刻呼叫。執行新的CMD53命令傳送資料之前,必須要等待上一個CMD53命令傳送的資料收到Download Ready的確認。WiFi_WaitForLastTask函式負責等待確認。如果資料幀確認超時,則重傳之前的資料幀,如果命令幀確認超時則無需重傳。

程式還規定,程式指定的回撥函式在任何情況下都必須保證最終能夠被呼叫到。因為回撥函式中通常都含有釋放記憶體的語句,不呼叫回撥函式的話就會導致記憶體洩露。

命令幀和資料幀傳送成功後,就會把當前系統時間儲存到start_time成員變數裡面,並把busy置1。命令幀傳送成功後還會把ready設為0。資料幀緩衝區不使用ready成員變數。命令幀收到確認後,會將ready置1,收到命令迴應時會把busy清0並呼叫回撥函式,通知應用程式命令執行成功。如果命令幀傳送後超過應用程式指定的超時時間timeout還沒收到迴應幀,則會重新發送命令。如果資料幀傳送出去後在指定的超時時間內未收到確認,則會重新發送資料幀。如果超過了最大的重試次數,則呼叫回撥函式,通知應用程式命令執行失敗或資料幀傳送失敗。

使用CARDSTATUS暫存器和INTSTATUS暫存器都可以檢查Download/Upload Ready位的狀態,但CARDSTATUS暫存器非常不穩定,執行CMD53讀寫命令都會導致Upload Ready狀態位清零。如果傳送資料幀時剛好有資料幀進來,那麼Upload Ready位就會在置位的瞬間被CMD53寫命令給清除掉,從而導致程式不知道有新資料幀駐留在模組內而一直不會去讀取。並且,只要資料留在Wi-Fi模組的緩衝區內一直不去讀取,那麼Wi-Fi模組就永遠也不會去接收新的資料幀,程式就會卡死。Download Ready位更不穩定,當命令收到迴應時,Upload Ready位置位的同時Download Ready位也會自動清除掉。而INTSTATUS暫存器則不同,裡面的標誌位都需要手動清除才行。使用INTSTATUS暫存器來檢查通道的狀態可以提高程式執行的穩定性和可靠性。所以為了防止程式編寫過程中意外使用到不穩定的CARDSTATUS暫存器,在WiFi.h中,筆者把CARDSTATUS暫存器的所有位定義都註釋掉了。

INTSTATUS暫存器中的位是在CARDSTATUS暫存器位從0跳變到1時(上升邊沿)置位的,清除標誌位時,為了防止清除掉新產生的中斷,不需要清除的位必須寫1。另外,收到的資料幀在被主機讀取之前,Wi-Fi模組不會去接收新的資料幀,自然就不會產生新的Upload Ready中斷,而讀完資料幀的瞬間有可能又收到新的資料幀,所以一定要先清除中斷標誌位,再發送CMD53命令讀取資料。

WiFi_Wait函式可以用來在指定的超時時間內等待指定的標誌位置位,並清除這些標誌位。如果超時時間為0,則永久等待。該函式的返回值為0表明超時。

WiFi_SendCommand函式的前三個引數為code、data和size,其中data含有幀頭和命令頭部。當size!=0時,data中只有命令內容是有效資料,函式根據code和size的值填充幀頭和命令頭部併發送命令。當size=0且data!=NULL時,data的所有資料都是有效資料,code引數將被忽略,該函式直接傳送data資料。當size=0且data=NULL時,code引數將被忽略,函式重傳儲存在wifi_tx_command.buffer中命令幀內容。

WiFi_SendPacket函式的前兩個引數為data和size,其中data不含幀頭和資料頭部。當size!=0時該函式生成幀頭和資料頭部併發送新資料幀。當size=0時data引數將被忽略,該函式重傳儲存在wifi_tx_packet.buffer中的資料幀內容。

八、WEP加密和WPA/WPA2認證

Wi-Fi模組配置WEP加密非常簡單。只需使用CMD_MAC_CONTROL命令開啟WEP選項,然後傳送CMD_802_11_SET_WEP命令設定WEP金鑰,最後在建立或連線熱點的時候給cap_info成員新增WIFI_CAPABILITY_PRIVACY選項就可以了。

WPA和WPA2方式就比較複雜了。執行CMD_MAC_CONTROL命令打開發送和接收時不需要WEP選項,接下來關聯熱點的時候,除了給cap_info加PRIVACY選項外,還必須要新增一個TLV。WPA認證方式下需要新增MrvlIETypes_VendorParamSet_t型別的TLV,WPA2方式則新增的是MrvlIETypes_RsnParamSet_t型別的TLV,這兩個TLV的具體內容是在掃描熱點的時候獲得的。Ad-Hoc模式下不能使用WPA和WPA2方式,只能使用WEP加密方式。只有無線基礎模式(Infrastructure Mode)下才能使用WPA/WPA2。與熱點建立關聯後,WPA方式下還需要經過PTK四次握手和GTK兩次握手,WPA2方式下則只需要經過PTK四次握手,握手是通過收發EAPOL握手資料幀進行的。握手完畢後必須把PTK和GTK通過CMD_802_11_KEY_MATERIAL命令發給韌體,韌體才能正確地加密傳送的資料包並解密收到的資料包。PTK金鑰用於收發單播幀和傳送廣播幀,GTK金鑰用於接收廣播幀。

lwip協議棧無法處理EAPOL握手幀,所以我們必須自己編寫EAPOL幀處理函式。WiFi_Input函式收到新資料幀後,先判斷乙太網幀的type/length欄位是否等於大端序的0x888e,如果等於,則送入WiFi_EAPOLProcess函式處理,不交給lwip協議棧。EAPOL幀的格式可以參閱802.11i-2004.pdf中的“8.5.2 EAPOL-Key frames”一節的內容。程式通過key_info欄位來判斷收到的EAPOL幀是屬於哪一次握手的,key_info的低三位代表加密型別是TKIP還是AES。

PTK四次握手的過程是,路由器通過第一次握手幀把ANonce發給Wi-Fi模組,模組收到後先生成SNonce,然後根據PSK金鑰、路由器和模組的MAC地址以及ANonce和SNonce生成PTK,把SNonce放到待發送的第二次握手幀中,然後把PTK分成KCK、KEK和TK三部分,用其中的KCK對待發送的幀的EAPOL部分計算MIC校驗值並放到資料幀的MIC欄位中,計算前MIC欄位全部為0。路由器收到模組發來的第二次握手幀後,會根據幀裡面含有的SNonce生成相同內容的PTK,然後用PTK中的KCK檢驗MIC,檢驗不通過就會丟棄,等於沒有收到該幀。如果檢驗通過,路由器就會發送第三次握手幀,其中也含有MIC校驗值。模組收到後,必須檢驗MIC的值是否正確,若不正確應丟棄該幀。認證型別為WPA時可不理會幀中的key_data資料,認證型別為WPA2時,需要用KEK解密key_data,提取出其中的GTK金鑰。把PTK的TK部分和GTK(如果有的話)發給韌體後,就可以傳送第四次握手包作出迴應,裡面有SNonce和MIC校驗值,完成PTK四次握手。

認證型別為WPA時還需進行GTK兩次握手,路由器先給模組傳送第一次握手幀,裡面含有用KEK加密的GTK金鑰,明文的GNonce以及MIC校驗碼,模組收到後先檢驗MIC,提取出其中的GTK發給韌體,然後給路由器傳送第二次握手幀作為迴應,裡面含有SNonce和MIC校驗值,完成GTK兩次握手。另外,無論是使用WPA還是WPA2,路由器每隔一定的時間(例如一天)都會進行一次GTK兩次握手,更新所有移動站的GTK金鑰。程式提取出GTK之後,必須要和PTK的TK部分一起發給韌體,不能只發GTK,否則CMD_802_11_KEY_MATERIAL命令將不能執行成功。

PSK金鑰是通過pbkdf2_hmac_sha1演算法生成的,執行演算法時需要傳入熱點名稱SSID和密碼,由WiFi_SetWPA函式的引數提供。WPA/WPA2認證用到的演算法函式都在WPA.c檔案中,所有的演算法都只有一個輸出。pbkdf2_hmac_sha1演算法共有四個輸入:password、salt、c和dkLen。其中password為密碼,salt為鹽,c為迭代次數,dkLen為輸出內容的長度。計算PSK時,password就是路由器密碼,salt是熱點名稱SSID,c為4096,輸出的PSK的長度dkLen為32位元組。pbkdf2_hmac_sha1演算法是在hmac_sha1演算法的基礎上實現的,hmac_sha1演算法又是在sha1演算法的基礎上實現的。lwip協議棧已經給我們提供了sha1演算法的實現。hmac演算法共有五個輸入:key、message、hash、blockSize和outputSize。其中key為金鑰,message為訊息內容,hash為一個單輸入的函式,blockSize為hash函式的分塊長度,outputSize為hash函式的輸出長度。當hash=sha1時blockSize=64,outputSize=20,演算法被稱為hmac_sha1。當hash=md5時blockSize=64,outputSize=16,演算法被稱為hmac_md5。lwip也提供了md5演算法函式。

路由器和移動站(Wi-Fi模組)雙方都是利用32位元組的PSK,雙方的MAC地址和ANonce、SNonce用PRF演算法生成PTK的。ANonce是路由器生成的32位元組的隨機數,SNonce是移動站生成的32位元組的隨機數。SNonce理論上隨便怎麼生成都可以,但按照802.11標準,最好採用PRF演算法生成。PRF演算法是在hmac_sha1演算法的基礎上實現的,共有四個輸入:K、A、B和N。K是金鑰,A是一個字串,B是一些資料,N是輸出的位元組數。PRF演算法常用PRF-n(K, A, B)表示式來表示,其中n=8N。生成SNonce的方法為PRF-256(Random number, "Init Counter", Local MAC Address || Time),雙豎線表示把兩段資料拼在一起。生成PTK的方法為PRF-512(PMK, "Pairwise key expansion", MAC1||MAC2||Nonce1||Nonce2)。PMK就是PSK,MAC1和MAC2為雙方的MAC地址,MAC1必須小於MAC2。Nonce1和Nonce2為雙方產生的隨機數ANonce和SNonce,但Nonce1必須小於Nonce2。

生成的PTK共有64位元組。前16位元組為KCK,用於EAPOL握手幀的MIC校驗,接下來的16位元組是KEK,用於對GTK金鑰封包進行加解密,再接下來的16位元組是TK,用於普通資料包的加解密。剩餘的16位元組只在TKIP加密方式下使用,前8位元組為TKIPTxMICKey,用於路由器傳送普通資料幀時進行MIC校驗,後8位元組為TKIPRxMICKey,用於路由器接收普通資料幀時進行MIC校驗。當加密方式為TKIP時,程式需要把PTK的TK、PRxMICKey和TKIPTxMICKey組合在一起共32位元組發給韌體,注意中間是RxKey,最後是TxKey,這和生成時的順序是不同的。當加密方式為AES時,只需把16位元組的TK發給韌體使用。

生成和檢驗MIC時,若加密方式為TKIP,則採用的演算法是hmac_md5。若加密方式為AES,則採用的演算法是hmac_sha1。計算前先將key_mic欄位清零,然後用KCK金鑰從type/length=0x888e欄位後面的version欄位(表示WPA版本)開始計算,一直到整個幀結束。MIC的長度為16位元組。

GTK金鑰是封裝在WPA2認證型別下的PTK第三次握手幀的key_data欄位中,以及WPA/WPA2認證型別下的GTK第一次握手幀的key_data欄位中的,用於移動站接收廣播幀(當然也包括多播幀)。當認證型別為WPA時,key_data解密後就是GTK。當認證型別為WPA2時,key_data解密後是一些KDE (Key Data Encapsulation)結構的資料,一個KDE含有kde_type、length、OUI、data_type和data欄位,GTK就位於其中一個KDE的data欄位中,該KDE的kde_type為0xdd,data_type為1。解密演算法是由加密型別確定的。當加密型別為TKIP時,解密演算法為ARC4(也叫RC4),GTK的長度為32位元組,傳送給韌體時需要將中間8位元組和最後8位元組內容交換位置。lwip已給我們提供了ARC4演算法的函式,ARC4演算法有兩個輸入:金鑰和資料。使用ARC4演算法解密GTK封包時,需要把EAPOL幀中的16位元組的key_iv欄位提取出來,與KEK金鑰拼在一起作為演算法的金鑰,先對一個256位元組的空陣列解密並丟棄,然後再解密key_data資料。當加密型別為AES時,解密演算法為AES Key Unwrap,GTK的長度為16位元組。AES Key Unwrap演算法也有金鑰和資料這兩個輸入,該演算法是在AES演算法的基礎上實現的。但lwip沒有提供AES演算法的函式,於是筆者採用了GitHub上下載的tiny-AES-c庫,該演算法的輸入也是金鑰和資料。

在正常使用過程中移動站只要使用了錯誤的PTK金鑰傳送資料,路由器就會立即強制移動站下線。路由器傳送了多個EAPOL握手幀而移動站都沒有迴應的話,最終也會強制下線。路由器每傳送一次握手幀,其中的隨機數都會改變一次。即使移動站傳送了EAPOL握手幀,但如果其中的MIC校驗值不正確,也會被路由器視為沒有傳送。如果安裝的GTK金鑰不正確,後果只是移動站收不到廣播幀而已,沒有其他影響。