Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写

本文以STM32F1系列的单片机为例,详细讲解Marvell公司的88W8686/88W8782/88W8801 WiFi模块驱动程序的编写。编写程序时为了代码简短起见,直接用寄存器操作,不使用STM32库函数。IDE采用Keil uVision5。为了存储下WiFi模块庞大的固件,以及方便lwip的移植,请尽量采用较大SRAM和Flash容量的单片机(如High-density或XL-density系列的),这里笔者用的是STMF103RET6(引脚数64,SRAM容量64KB,Flash容量512KB)。因为Connectivity line系列的STM32F105/107单片机没有SDIO接口,所以请不要使用这两种单片机来测试。STM32F103C8T6容量太小,虽然Flash程序存储空间有64KB,但SRAM运行内存只有20KB,不方便lwip协议栈的移植,所以最好也不要使用(当然这种情况改用uip协议栈也行,uip在SRAM容量才1KB的ATMega16单片机上都能运行)。

88W8686已经是比较老的芯片了,其数据手册(datasheet)的发布时间是2007年2月20日。淘宝网上可以买到芯片组(Chip Set)为88W8686的WM-G-MR-09模块,价格比较贵,85元一个。其固件(firmware)及Fedora Linux下的驱动程序可直接在Marvell的官方网站上下载到,压缩包名称为SD-8686-LINUX26-SYSKT-9.70.3.p24-26409.P45-GPL.zip,里面有两个固件:helper_sd.bin和sd8686.bin,最后修改日期都是2008年2月29日。

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第1张图片

卖家世讯电子提供了STM32103RET6驱动该网卡的程序。但该程序可靠性很差,代码既乱又复杂而且很难看懂,扫描热点时经常出现problem fetching packet from firmware, rewhile的错误,连接热点时有时候会出现认证失败的错误type=0x888e!,一连接失败就直接重启单片机,而且与WPA2-PSK认证有关的代码被封装到了wap_wpa2_lib.lib文件中,不开放源代码。这也是笔者写本教程的原因:自己编写出高可靠性的驱动程序!

较新的88W8782和88W8801模块价格更便宜,大概20~30块钱一个,支持创建AP模式的热点,安卓手机可以不打补丁直接连接,但很多寄存器的地址和88W8686不一样。其固件和Linuxl驱动程序无法在Marvell的官方网站上下载到,但可在淘宝的卖家那里获得,还可以获得数据手册PDF文档。数据手册的发布时间分别为2011年4月6日和2013年8月19日。压缩包的名称分别为SD-UAPSTA-8782-FC13-MMC-14.69.12.p35-M2614336_B0-GPL_new.zip和SD-UAPSTA-8801-FC18-MMC-14.76.36.p61-C3X14090_B0-GPL.zip。每个模块只有一个固件,分别是sd8782_uapsta.bin和sd8801_uapsta.bin,最后修改日期分别为2012年8月16日和2015年2月25日。

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第2张图片 Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第3张图片

本文主要讲解88W8686模块,同时也会顺带说明如何在88W8782/8801这两款芯片上运行所写的程序。

这些WiFi模块都是SD I/O卡,而我们平常在手机里面插的内存卡属于SD memory卡。SDIO card和SD memory card都可以用STM32单片机的SDIO接口来操作,但它们所支持的命令不一样。后者支持很多命令,比如复位命令CMD0、写数据块命令CMD24~25、读数据块命令CMD17~18等等。但前者就只支持CMD0、3、5、7、15、52、53这几个命令,并且CMD0不是复位命令,而是从SDIO模式切换到SPI模式的命令。并且,两者的初始化时序也不一样。

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第4张图片

在SD卡的官方网站https://www.sdcard.org/上可以下载到SD memory卡和SDIO卡的文档。

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第5张图片

其中,Part1_Physical_Layer_Simplified_Specification_Ver6.00是SD memory卡的文档,PartE1_SDIO_Simplified_Specification_Ver3.00是SDIO卡的文档。PartE7_Wireless_LAN_Simplified_Addendum_Ver1.10虽然是介绍SDIO WiFi卡的文档,但和本文所讲的WiFi模块没有任何关系,因此不必下载。

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第6张图片


接下来,笔者将参考Part E1文档,讲解SDIO WiFi卡的初始化方法。

完整的代码请参考:http://blog.csdn.net/zlk1214/article/details/75410647(Marvell 88W8686 WiFi模块创建或连接热点,并使用lwip建立http服务器)

工程的建立:在Keil中新建一个STM32F103RE的工程,并勾选上启动代码CMSIS/CORE和Device/Startup:

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第7张图片

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第8张图片

如果要使用STM32的库函数,则还需要勾选Device/StdPeriph Drivers中的项目(本文不使用库函数,因此不必勾选):

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第9张图片

工程建好后,Keil已经帮我们自动添加好了启动文件startup_stm32f10x_hd.s,其中hd是指high-density。

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第10张图片

在其中创建main.c文件,以及存放WiFi驱动程序的文件WiFi.h和WiFi.c。


以下是88W8686 WiFi模块与STM32单片机的连线。

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第11张图片


SDIO_CLK接PC12,SDIO_CMD接PD2,数据线D0~D3接PC8~11。

在笔者所用的的开发板上,VCC3V3引脚不是直接连接到电源的,而是通过一个场效应管接到PB12上的。当开发板外接了5V的电源插头,并且PB12为低电平时,WiFi模块才通电工作。下载程序时,PB12输出高阻态,此时WiFi模块断电。每次单片机复位时,WiFi模块也就跟着自动复位。自己焊的板子可以用一个PNP三极管来代替场效应管Q1。

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第12张图片

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第13张图片


【程序1】Marvell 88W8686 WiFi模块的初始化代码:http://blog.csdn.net/zlk1214/article/details/73693167

[cpp]  view plain  copy
  1. RCC->AHBENR = RCC_AHBENR_SDIOEN;  
  2. RCC->APB1ENR = RCC_APB1ENR_TIM6EN;  
  3. RCC->APB2ENR = RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_IOPDEN| RCC_APB2ENR_USART1EN;  
  4.   
  5. GPIOA->CRH = 0x000004b0;  
  6. GPIOB->CRH = 0x00030000; // PB12为WiFi模块电源开关, PB12=0时打开WiFi模块  
  7. GPIOC->CRH = 0x000bbbbb;  
  8. GPIOD->CRL = 0x00000b00;  
  9.   
  10. USART1->BRR = 0x271;  
  11. USART1->CR1 = USART_CR1_UE | USART_CR1_TE;  
在main函数中,首先打开SDIO、GPIOA~D、定时器TIM6和串口USART1的时钟,需要先打开时钟才能使用这些STM32外设。这些外设分别在AHB、APB1和APB2总线上。AHB和APB2的时钟频率为72MHz,而APB1的频率为36MHz。STM32外接的外部高速晶振HSE只有8MHz,这些频率都是RCC上的PLL倍频器产生的。
接下来配置PA~PD的I/O口。CRL负责Px0~Px7,CRH负责Px8~Px15。每个十六进制数位配置一个端口,最低位为Px8或Px0,最高位为Px15或Px7。

对于USART1串口,发送端口USART1_TX为PA9,设置为复用推挽输出,速度为50MHz:b,接收端口USART1_RX为PA10,设置为浮空输入:4。

PB12为外接的电源开关,最开始为高阻态,WiFi模块为断电状态。当设置CRH为3(推挽输出,速度为50MHz)后,因为GPIOB->ODR为0,所以PB12输出低电平,WiFi模块通电。

PC8~11为SDIO数据端口D0~D3,PC12为SDIO时钟引脚,PD2为SDIO命令端口。这些都应该设置为复用推挽输出50MHz,因此都设为b。

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第14张图片

由STM32F103的参考手册(Reference manual)的9.1.11 GPIO configurations for device peripherals中的表格可知,所有的SDIO引脚都应该设为复用推挽输出。

推挽和开漏输出的区别:推挽输出可以输出低电平(ODR=0)和高电平(ODR=1)。开漏输出可以输出低电平(ODR=0),但不能输出高电平,当ODR=1时输出高阻态,高阻态相当于断开了端口与电源的连接。

GPIO_CRH/CRL配置方法(加粗的表示常用):

每1位16进制数表示一个I/O端口。
1为10MHz推挽输出(推挽输出适合直接驱动)(复用为9)
2为2MHz推挽输出(复用为a)
3为50MHz推挽输出(复用为b)

5为10MHz开漏输出(开漏输出适合接三极管基极)(复用为d)
6为2MHz开漏输出(复用为e)
7为50MHz开漏输出(复用为f)

0为模拟输入/输出
4为浮空输入
8为带上/下拉电阻的输入
(ODR=0为下拉,1为上拉),上拉输入表示IDR的默认值为1,下拉输入表示IDR的默认值为0

当使用带上下拉电阻的输入模式时,该端口对应的ODR位的值就表示默认的输入电平。当该端口悬空时,IDR=ODR。否则IDR就等于输入的电平值。

一般情况下,PNP型三极管的发射极都是接的+5V,如果基极通过电阻接到单片机的I/O口上并配置为开漏输出,则当ODR=0时,三极管饱和导通,发射极与基极间的电压为0.7V,基极电阻两端的电压为4.3V;当ODR=1时输出高阻态,相当于基极直接悬空,三极管截止。所以开漏输出特别适合接三极管的基极。如果配置为推挽输出,那么当ODR=1时,I/O口输出的是3.3V的高电平,5V-3.3V=1.7V高于导通电压0.7V,因此三极管还是导通的,且基极电阻两端的电压为1V。

所以,如果WiFi模块的电源上接的是PNP三极管,那么只有将PB12配置为开漏输出(7)后,才能通过ODR寄存器控制WiFi模块电源的通断。


接着配置串口USART1,BRR为波特率。USART1是在APB2总线上的外设,该总线的时钟为72MHz,也就是72000000Hz。欲设置的波特率为115200,72000000 ÷ 115200 = 625 = 0x271,因此,BRR=0x271。CR1为控制寄存器,UE表示启动该外设,TE表示允许发送。

为了在程序中使用printf函数向串口输出信息,需要引入stdio.h头文件,然后实现fputc函数。printf函数使用的是C语言标准输出流stdout,因此fp=stdout。ch为要输出的每个字符。若输出的是换行符\n,为了正确换行,需要先输出一个回车符\r组成\r\n。向USART1的DR寄存器写入数据前,必须先等待SR寄存器中的TXE位(发送缓冲区空)变为1。写入数据后,串口外设将自动发送数据。最后函数必须返回ch的原有内容。

[cpp]  view plain  copy
  1. #include   
  2.   
  3. int fputc(int ch, FILE *fp)  
  4. {  
  5.     if (fp == stdout)  
  6.     {  
  7.         if (ch == '\n')  
  8.         {  
  9.             while ((USART1->SR & USART_SR_TXE) == 0);  
  10.             USART1->DR = '\r';  
  11.         }  
  12.         while ((USART1->SR & USART_SR_TXE) == 0);  
  13.         USART1->DR = ch;  
  14.     }  
  15.     return ch;  
  16. }  

此外还需在项目属性里勾选上“Use MicroLIB”选项。

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第15张图片


J-Link下载器配置:在Debug选项卡中选择J-Link作为调试工具,并在设置对话框里勾选上Reset and Run复选框,以便下载完成后程序能自动开始运行。

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第16张图片


STM32 SDIO外设的初始化

串口初始化完毕后便开始执行Card_Init函数:SDIO卡初始化函数。

[cpp]  view plain  copy
  1. printf("Initialization begins...\n");  
  2. SDIO->POWER = SDIO_POWER_PWRCTRL;  
  3. SDIO->CLKCR = SDIO_CLKCR_CLKEN | 178; // 初始化时最高允许的频率: 72MHz/(178+2)=400kHz  
  4. delay(5); // 延时可防止CMD5重发  
首先将SDIO_POWER寄存器中的PWRCTRL位置位,启动SDIO外设。以下是STM32F1参考手册中的SDIO_POWER寄存器的说明:

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第17张图片

只有当该寄存器的第1~0位同时为1时才能启动该外设。

在STM32F10x.h头文件中,有如下的定义:

[cpp]  view plain  copy
  1. /******************  Bit definition for SDIO_POWER register  ******************/  
  2. #define  SDIO_POWER_PWRCTRL                  ((uint8_t)0x03)               /*!< PWRCTRL[1:0] bits (Power supply control bits) */  
  3. #define  SDIO_POWER_PWRCTRL_0                ((uint8_t)0x01)               /*!< Bit 0 */  
  4. #define  SDIO_POWER_PWRCTRL_1                ((uint8_t)0x02)               /*!< Bit 1 */  
SDIO_POWER_PWRCTRL表示PWRCTRL的全部位,即第1~0位。SDIO_POWER_PWRCTRL_0表示第0位,SDIO_POWER_PWRCTRL_1表示第1位。


接着配置SDIO_CLKCR寄存器。该寄存器的CLKEN位决定是否启用时钟引脚SDIO_CK,即是否向PC12引脚输出时钟信号。CLKDIV为时钟分频系数。

PC12引脚上的频率为:SDIO外设的频率 ÷ (CLKDIV + 2)

因为SDIO是AHB总线上的外设,所以SDIO外设的频率等于AHB总线的频率(记为HCLK),为72MHz。

程序中配置的是CLKDIV=178,因此分频后,在PC12引脚上输出的时钟频率就是400kHz。这是SD卡在初始化时所允许的最高频率。只有当SDIO总线上挂接的所有SD卡都初始化完毕了之后,这一频率才允许提高。

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第18张图片


然后调用delay函数延时5毫秒。延时的目的是上电后使器件做好准备,降低CMD5命令重发的可能性,但这不能完全防止CMD5重发。delay函数的实现如下:

[cpp]  view plain  copy
  1. // 延时n毫秒  
  2. void delay(uint16_t n)  
  3. {  
  4.     TIM6->ARR = 10 * n - 1; // nms  
  5.     TIM6->PSC = 7199; // 72MHz/7200=10kHz  
  6.     TIM6->CR1 = TIM_CR1_URS | TIM_CR1_OPM; // UG不置位UIF, 非循环模式  
  7.     TIM6->EGR = TIM_EGR_UG; // 保存设置  
  8.     TIM6->CR1 |= TIM_CR1_CEN; // 开始计时  
  9.     while ((TIM6->SR & TIM_SR_UIF) == 0); // 等待计时完毕  
  10.     TIM6->SR &= ~TIM_SR_UIF; // 清除溢出标志  
  11. }  
该函数使用了STM32中的基本定时器TIM6。ARR为计数量,这里n=5,ARR=49,PSC为分频系数,PSC=7199为7200分频。这里要注意的是,虽然TIM6是APB1总线上的外设,但提供的时钟却是2×36MHz=72MHz(见下图),所以分频后是10kHz。OPM=1表示定时器溢出后自动关闭,即只计时一次。UG=1表示使ARR和PSC中的值立即生效。URS=1表示执行UG=1时不将溢出标志UIF置位。接着CEN置1开始计时,用while循环等待溢出标志UIF置位,起到延时的目的。当CNT的值从49跳变到0的瞬间,UIF置位,CEN自动清零关闭定时器,跳出while循环。然后清除UIF标志位,以便于下次延时。

CEN刚置位时,CNT=0。经过1/(10kHz)=0.1ms后,CNT从0跳变到1。经过4.9ms后,CNT从48跳变到49。经过5ms后,CNT刚好从49跳变到0。

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第19张图片


SDIO卡的初始化流程见Part E1文档的图3-2。首先以空参数(ARG=0)发送一个CMD5命令,检查有无回应。若有回应,则设置参数ARG后再次发送CMD5,检查回应中的MP(Memory Present)位后决定之后的流程。

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第20张图片

在STM32 SDIO外设中,使用SDIO_CMD寄存器发送命令,使用SDIO_ARG寄存器设置命令参数。

在SDIO_CMD寄存器中,CMDINDEX决定命令号,CPSMEN=1时发送命令(该位不会自动清零,只要写完寄存器后该位为1,就发送命令)。WAITRESP=00时不等待回应,WAITRESP=01时等待48位的短回应,WAITRESP=11时等待136位的的长回应。回应的内容保存在RESP1~4寄存器中。

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第21张图片

[cpp]  view plain  copy
  1. /* 发送CMD5: IO_SEND_OP_COND */  
  2. SDIO->CMD = SDIO_CMD_CPSMEN | SDIO_CMD_WAITRESP_0 | 5;  
  3. while (SDIO->STA & SDIO_STA_CMDACT);  
  4. Card_CheckCommand(); // 为了保险起见还是要检查一下是否要重发命令  
  5. if (SDIO->STA & SDIO_STA_CMDREND)  
  6. {  
  7.     SDIO->ICR = SDIO_ICR_CMDRENDC;  
  8.     Card_ShowShortResponse();  
  9. }  
因为上电后ARG寄存器的默认值为0,所以这里没有写SDIO->ARG=0;。这里以空参数发送CMD5,只将WAITRESP的第0位置1,等待短回应。SDIO_STA_CMDACT=1表示命令正在发送。由于之前延时5ms并不能100%保证命令不会出现超时,所以调用Card_CheckCommand函数检查一下等待回应是否超时。如果超时就重发命令。若收到了回应,则SDIO_STA_CMDREND自动置1,对SDIO_ICR_CMDRENDC写1清除该位,然后调用Card_ShowShortResponse函数显示命令的回应内容。

[cpp]  view plain  copy
  1. // 检查命令是否收到了回应, 若没收到则重发命令  
  2. void Card_CheckCommand(void)  
  3. {  
  4.     while (SDIO->STA & SDIO_STA_CTIMEOUT)  
  5.     {  
  6.         SDIO->ICR = SDIO_ICR_CTIMEOUTC; // 清除标志  
  7.         SDIO->CMD = SDIO->CMD; // 重发  
  8.         printf("Timeout! Resend CMD%d\n", SDIO->CMD & SDIO_CMD_CMDINDEX);  
  9.         while (SDIO->STA & SDIO_STA_CMDACT);  
  10.     }  
  11. }  
命令重发检查: 若经过了64个SDIO_CK时钟周期(64/400000s=0.16ms)后仍没有收到回应,SDIO_STA_CTIMEOUT位自动置1表明超时。此时执行循环体内的语句。先对SDIO_ICR_CTIMEOUTC写1清除该位,然后将CMD寄存器中的内容再次送入CMD寄存器,写完之后CPSMEN仍为1,ARG=0,自动重发CMD5命令,等待命令发送完毕后再次检查是否超时。若收到了回应,CTIMEOUT=0跳出循环。

[cpp]  view plain  copy
  1. void Card_ShowShortResponse(void)  
  2. {  
  3.     printf("Command response received: CMD%d, RESP_%08x\n", SDIO->RESPCMD, SDIO->RESP1);  
  4. }  
Card_ShowShortResponse函数用于显示短回应的内容,包括短回应的回应命令号和32位的回应内容。

运行结果如下:

[plain]  view plain  copy
  1. Initialization begins...  
  2. Command response received: CMD63, RESP_90ff8000  

CMD5命令的回应格式是R4格式,长度为48位。其中第45~40位为回应命令号,保存在RESPCMD寄存器中,第39~8位为32位的回应内容,保存在RESP1寄存器中。

如下图,从C开始到I/O OCR为RESP1,Reserved为RESPCMD(始终等于全1,也就是63)。

因为RESP1=0x90ff8000,所以C=1,功能(Function)数为1,MP=0(是否有内存区域),S18A=0(是否已接受1.8V低电压),OCR寄存器的值为0xff8000。

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第22张图片
OCR寄存器的第23~15位为1,表明该SDIO卡支持2.7~3.6V的电压。
Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第23张图片

值得注意的是,该程序假定SDIO总线上只接了WiFi模块,没有接其他任何东西,包括SD内存卡。所以不发送Part E1文档图3-2中的用于SD卡的CMD0复位命令和CMD8命令。

因为CMD5的命令回应中,功能数NF=1>0,且OCR寄存器的值有效,所以需要再次发送CMD5,且这次参数ARG为主机设置的电压范围。因为我们不请求1.8V低电压模式,所以ARG中S18R=0。

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第24张图片

[cpp]  view plain  copy
  1. /* 设置参数VDD Voltage Window: 3.2~3.4V, 并再次发送CMD5 */  
  2. SDIO->ARG = 0x300000;  
  3. SDIO->CMD = SDIO->CMD;  
  4. while (SDIO->STA & SDIO_STA_CMDACT);  
  5. if (SDIO->STA & SDIO_STA_CMDREND)  
  6. {  
  7.     SDIO->ICR = SDIO_ICR_CMDRENDC;  
  8.     Card_ShowShortResponse();  
  9.     if (SDIO->RESP1 & _BV(31))  
  10.     {  
  11.         // Card is ready to operate after initialization  
  12.         printf("Number of I/O Functions: %d\n", (SDIO->RESP1 >> 28) & 7);  
  13.         printf("Memory Present: %d\n", (SDIO->RESP1 & _BV(27)) != 0);  
  14.     }  
  15. }  
程序中的_BV(31)表示第31位,即检查RESP1寄存器中的第31位(C位)是否为1。
[cpp]  view plain  copy
  1. #define _BV(n) (1u << (n))  
C=1(图3-2中对应IO=1)表明卡已经准备好了。此时程序向串口输出NF和MP的值。

[plain]  view plain  copy
  1. Command response received: CMD63, RESP_90300000  
  2. Number of I/O Functions: 1  
  3. Memory Present: 0  

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第25张图片

接下来,发送CMD3命令,获取WiFi模块的RCA相对地址,并保存到全局变量rca中。

[cpp]  view plain  copy
  1. uint16_t rca;  
  2.   
  3. /* 获取Wi-Fi模块地址 (CMD3: SEND_RELATIVE_ADDR, Ask the card to publish a new relative address (RCA)) */  
  4. SDIO->ARG = 0;  
  5. SDIO->CMD = SDIO_CMD_CPSMEN | SDIO_CMD_WAITRESP_0 | 3;  
  6. while (SDIO->STA & SDIO_STA_CMDACT);  
  7. if (SDIO->STA & SDIO_STA_CMDREND)  
  8. {  
  9.     SDIO->ICR = SDIO_ICR_CMDRENDC;  
  10.     rca = SDIO->RESP1 >> 16;  
  11.     printf("Relative card address: 0x%04x\n", rca);  
  12. }  
由SD内存卡的文档Part1可知,CMD3的参数为0:

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第26张图片

在SDIO卡中CMD3的回应格式为R6,其高16位为RCA相对卡地址:

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第27张图片

获得相对地址后,发送CMD7选中WiFi模块,其参数ARG的高16位为欲选中模块的RCA地址,其余位为0。

[cpp]  view plain  copy
  1. /* 选中Wi-Fi模块 (CMD7: SELECT/DESELECT_CARD) */  
  2. SDIO->ARG = rca << 16;  
  3. SDIO->CMD = SDIO_CMD_CPSMEN | SDIO_CMD_WAITRESP_0 | 7;  
  4. while (SDIO->STA & SDIO_STA_CMDACT);  
  5. if (SDIO->STA & SDIO_STA_CMDREND)  
  6. {  
  7.     SDIO->ICR = SDIO_ICR_CMDRENDC;  
  8.     printf("Card selected! status=0x%08x\n", SDIO->RESP1);  
  9. }  
程序输出:

[plain]  view plain  copy
  1. Relative card address: 0x0001  
  2. Card selected! status=0x00001e00  

到此,WiFi模块已初始化完毕,现在可提升SDIO总线的时钟频率。频率不要设得太高(如24MHz),否则即便是不用库函数,用寄存器操作,发送/接收数据时也很容易忙不过来导致Underrun/Overrun的错误,除非使用DMA。

[cpp]  view plain  copy
  1. // 提高时钟频率  
  2. SDIO->CLKCR = (SDIO->CLKCR & ~SDIO_CLKCR_CLKDIV) | 70; // 72MHz/(70+2)=1MHz  
  3. SDIO->DTIMER = 1000000; // 当频率为1MHz时, 超时时间为1秒  
DTIMER寄存器表示SDIO接口在数据端口上发送或接收数据时的最大超时时间。

SDIO接口开机后的默认数据宽度为1,只使用D0(PC8)这一根数据线。为了同时使用D0~D3四根数据线,需要修改WiFi模块中的卡公共控制寄存器(CCCR寄存器,见Part E1文档的6.9节),将地址为0x07的总线接口控制寄存器(Bus Interface Control)中的Bus Width位改为10。

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第28张图片

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第29张图片

[cpp]  view plain  copy
  1. /* 选择总线宽度 (Wide Bus Selection) */  
  2. // For an SDIO card a write to the CCCR using CMD52 is used to select bus width. (See 4.5 Bus Width)  
  3. // CMD52: IO_RW_DIRECT, CCCR: Card Common Control Registers  
  4. Card_Write(0, 0x07, Card_Read(0, 0x07) | 0x02); // Bus Width: 4-bit bus  
  5. SDIO->CLKCR |= SDIO_CLKCR_WIDBUS_0;  

SDIO_CLKCR寄存器中的WIDBUS位控制的是SDIO外设的数据宽度,当WIDBUS=01时采用四位数据线模式。


这里涉及到SDIO卡内寄存器的读取(Card_Read)和写入(Card_Write),这是通过发送CMD52命令实现的。

[cpp]  view plain  copy
  1. void Card_SendCMD52(uint8_t is_write, uint8_t read_after_write, uint8_t function_number, uint32_t register_address, uint8_t data_to_write)  
  2. {  
  3.     SDIO->ARG = (is_write << 31) | (function_number << 28) | (read_after_write << 27) | (register_address << 9) | data_to_write;  
  4.     SDIO->CMD = SDIO_CMD_CPSMEN | SDIO_CMD_WAITRESP_0 | 52;  
  5.     while (SDIO->STA & SDIO_STA_CMDACT);  
  6. }  

CMD52命令参数的格式如下图所示:

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第30张图片
其中,R/W flag决定是读寄存器还是写寄存器,Function Number为寄存器所在的功能区,RAW flag表示写寄存器后是否自动读取寄存器的实际内容,Stuff为始终为0的填充位,Register Address为寄存器地址,Write Data为要写入的数据,读寄存器时用0填充。

[cpp]  view plain  copy
  1. // 读寄存器  
  2. uint8_t Card_Read(uint8_t func_num, uint32_t addr)  
  3. {  
  4.     Card_SendCMD52(0, 0, func_num, addr, 0);  
  5.     if (SDIO->STA & SDIO_STA_CMDREND)  
  6.     {  
  7.         SDIO->ICR = SDIO_ICR_CMDRENDC;  
  8.         return SDIO->RESP1 & 0xff;  
  9.     }  
  10.     else  
  11.     {  
  12.         printf("Card_Read failed, SDIO->STA=0x%08x!\n", SDIO->STA);  
  13.         return 0;  
  14.     }  
  15. }  
  16.   
  17. // 写寄存器, 返回写入后寄存器的实际内容  
  18. uint8_t Card_Write(uint8_t func_num, uint32_t addr, uint8_t value)  
  19. {  
  20.     Card_SendCMD52(1, 1, func_num, addr, value);  
  21.     if (SDIO->STA & SDIO_STA_CMDREND)  
  22.     {  
  23.         SDIO->ICR = SDIO_ICR_CMDRENDC;  
  24.         return SDIO->RESP1 & 0xff;  
  25.     }  
  26.     else  
  27.     {  
  28.         printf("Card_Write failed, SDIO->STA=0x%08x!\n", SDIO->STA);  
  29.         return 0;  
  30.     }  
  31. }  
写寄存器时,RAW=1,自动返回写入后寄存器中的实际内容。

CMD52命令的回应格式为R5,其中不仅包含了8位的寄存器内容,还包括卡状态信息Response Flags,所以RESP1必须要与上0xff滤掉卡状态信息位。

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第31张图片

CCCR寄存器和以后要讲的FBR寄存器和CIS卡信息结构,都位于0号功能区(Function 0)中的公共I/O区域(CIA)中。

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第32张图片


接下来我们来讨论一下,如果SDIO总线上同时接了WiFi模块和SD内存卡,该怎么初始化。

笔者建议大家最好不要将WiFi模块和SD卡同时接到STM32的SDIO接口上,因为这样会产生很多兼容性问题。如果必须要同时使用WiFi模块和SD内存卡,例如要用WiFi做一个HTTP服务器浏览保存在SD卡上的网页,或是要实现FTP协议读写SD卡,则最好WiFi模块单独使用SDIO接口,SD卡使用SPI接口。

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第33张图片

SD内存卡的常规初始化顺序是:CMD0 -> CMD8 -> ACMD41 -> CMD2 -> CMD3 -> CMD7。

当SDIO总线上直接并联多张SD内存卡时,CMD8、ACMD41、CMD2、CMD3都会产生碰撞,导致CRC校验错误(SDIO_STA_CCRCFAIL置位),甚至主机端完全无法接收命令,导致超时(SDIO_STA_CTIMEOUT置位)。更严重的是,用于分配RCA地址的CMD3命令无法屏蔽,若第一张SD卡已经分配好了RCA地址(RCA1),现在要再次发送CMD3命令给第二张SD卡分配RCA地址,但第一张卡仍会响应CMD3,最终第一张卡的RCA地址被更换,RCA1失效。为了解决这个问题,需要改变硬件电路。命令引脚SDIO_CMD和SDIO_D0~SDIO_D3直接连接在一起,但每个卡槽的时钟引脚CLKn则需要通过一个与门(74HC08)连接在一起,并在各与门的输入端增加一个片选I/O口CSn,其逻辑式为CSn & SDIO_CK = CLKn,其中SDIO_CK为单片机上的PC12引脚。初始化时,仅使一个CSn片选引脚有效(高电平),其他的卡槽都为无效(低电平),封锁住相应的时钟,这样就避免了碰撞。当所有的SD内存卡的RCA地址都已分配好,且各不相同时,再打开所有的CSn时钟,使用CMD7命令选择要操作的卡。


【示例程序】STM32F407单片机SDIO接口上插入多张SD卡并进行通信:http://blog.csdn.net/zlk1214/article/details/76651382

STM32F407单片机开机时默认的时钟为16MHz,而SDIO外设的时钟为48MHz,所以必须打开PLL倍频器后才能使用SDIO外设,否则将无法发送命令,程序卡死在SDIO_STA_CMDACT上。

[cpp]  view plain  copy
  1. // 开机时默认的时钟: SYSCLK=HCLK=16MHz, PCLK1=PCLK2=16MHz    
  2. // SDIO的时钟是48MHz, 而系统时钟只有16MHz, 所以必须打开PLL倍频器    
  3. RCC->CR |= RCC_CR_PLLON;    
  4. while ((RCC->CR & RCC_CR_PLLRDY) == 0);    


若SDIO总线上只有1张SD卡和1个WiFi模块,问题将变得简单很多。我们知道,SD I/O卡的初始化顺序是:

CMD5 -> CMD5 -> CMD3 -> CMD7

其中只有CMD3会和SD内存卡产生碰撞。因此我们只需要先将SD I/O卡初始化,然后禁用掉SD I/O卡的CMD3命令,再初始化SD内存卡,问题就解决了。

当一张卡被CMD7命令选中时,将不能用CMD3来更换被选中卡的RCA地址。因此我们可以用CMD7来禁用CMD3。这样我们就可以在WiFi和SD卡的时钟都打开的情况下,成功完成初始化操作。但一定要在CMD引脚上接与门,因为SD卡在进行数据传输时(如CMD17命令读取扇区内容),WiFi模块的存在会导致数据传输出错(SDIO_STA_DCRCFAIL置位),并且在WiFi模块下载固件时使用CMD7命令切换到SD卡读取数据也会导致异常(WIFI_CARDSTATUS寄存器的值由0x0d变为0x05,导致固件无法继续下载)。


由Part E1文档中的初始化流程图可知,用于初始化SD内存卡的CMD0和CMD8应放在用于初始化SD I/O卡的CMD5命令的前面。

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第34张图片

由于WiFi模块中MP(Memory Present)=0,所以不应在WiFi模块的CMD3命令前发送ACMD41和CMD2,应该改在CMD3后面发送。

Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写_第35张图片

最终的初始化顺序为:

CMD0 -> CMD8 -> CMD5 -> CMD5 -> CMD3 -> CMD7 -> ACMD41 -> CMD2 -> CMD3

其中加粗的部分为WiFi模块的初始化命令。


【程序2】Marvell 88W8686 WiFi模块与SD内存卡同时插在SDIO总线上的初始化代码:http://blog.csdn.net/zlk1214/article/details/76768218

程序中定义了两个存放RCA地址的全局变量,第一个用于存放SD内存卡的RCA地址,第二个用于存放WiFi模块的RCA地址。若没有插入SD卡,则rca_sd=0。

[cpp]  view plain  copy
  1. uint16_t rca_sd, rca_wifi;  
程序不允许不插入WiFi模块,若没有插入WiFi模块,程序将卡死在Card_CheckCommand函数中不停地重发CMD5命令,初始化永远无法完成。

程序中新增了一个Card_Select函数,专门用于发送CMD7命令,选择要操作的卡。

[cpp]  view plain  copy
  1. void Card_Select(uint16_t rca)  
  2. {  
  3.     SDIO->ARG = rca << 16;  
  4.     SDIO->CMD = SDIO_CMD_CPSMEN | SDIO_CMD_WAITRESP_0 | 7;  
  5.     while (SDIO->STA & SDIO_STA_CMDACT);  
  6.     if (SDIO->STA & SDIO_STA_CMDREND)  
  7.     {  
  8.         SDIO->ICR = SDIO_ICR_CMDRENDC;  
  9.         printf("Card 0x%04x selected! status=0x%08x\n", rca, SDIO->RESP1);  
  10.     }  
  11.     else if (SDIO->STA & SDIO_STA_CTIMEOUT)  
  12.     {  
  13.         SDIO->ICR = SDIO_ICR_CTIMEOUTC;  
  14.         printf("Card not selected! CMD7 timeout!\n");  
  15.     }  
  16. }  

若参数rca为0,则取消选中所有的卡,CMD7命令将不会产生回应,进入的分支是SDIO_STA_CTIMEOUT。

注意:当rca!=0时,CMD7命令也有可能超时,所以最好加上命令超时重发的代码。

SD卡和SD I/O卡设置总线宽度的命令是不一样的。前者用的是ACMD6命令,后者用的是CMD52命令。

[cpp]  view plain  copy
  1. /* 发送CMD52, 设置WiFi模块的总线宽度 */  
  2. Card_Write(0, 0x07, Card_Read(0, 0x07) | 0x02); // Bus Width: 4-bit bus  
  3.   
  4. /* 发送ACMD6, 设置SD内存卡的总线宽度 */  
  5. if (rca_sd)  
  6. {  
  7.     Card_Select(rca_sd); // 使用CMD7命令选中SD内存卡  
  8.     SDIO->ARG = rca_sd << 16; // 这回CMD55是发给SD内存卡的  
  9.     SDIO->CMD = SDIO_CMD_CPSMEN | SDIO_CMD_WAITRESP_0 | 55;  
  10.     while (SDIO->STA & SDIO_STA_CMDACT);  
  11.     SDIO->ICR = SDIO_ICR_CMDRENDC;  
  12.     SDIO->ARG = 2; // ACMD6的参数: 4-bit bus  
  13.     SDIO->CMD = SDIO_CMD_CPSMEN | SDIO_CMD_WAITRESP_0 | 6; // SET_BUS_WIDTH  
  14.     while (SDIO->STA & SDIO_STA_CMDACT);  
  15.     if (SDIO->STA & SDIO_STA_CMDREND)  
  16.     {  
  17.         SDIO->ICR = SDIO_ICR_CMDRENDC;  
  18.         printf("Memory bus width is changed! status=0x%08x\n", SDIO->RESP1);  
  19.     }  
  20. }  
  21.   
  22. SDIO->CLKCR |= SDIO_CLKCR_WIDBUS_0; // 将STM32上的SDIO外设设为4位数据宽度  
  23. Card_Select(rca_wifi); // 选中WiFi模块  

你可能感兴趣的:(Marvell 88W8686/88W8782/88W8801 WiFi模块驱动程序的编写)