admin 管理员组

文章数量: 887021

一、概述

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) 定义了两类卡: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密钥不正确,后果只是移动站收不到广播帧而已,没有其他影响。

本文标签: 模块 代码 Marvell WiFi