本文以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日。
卖家世讯电子提供了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日。
本文主要讲解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模式的命令。并且,两者的初始化时序也不一样。
在SD卡的官方网站https://www.sdcard.org/上可以下载到SD memory卡和SDIO卡的文档。
其中,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模块没有任何关系,因此不必下载。
接下来,笔者将参考Part E1文档,讲解SDIO WiFi卡的初始化方法。
完整的代码请参考:http://blog.csdn.net/zlk1214/article/details/75410647(Marvell 88W8686 WiFi模块创建或连接热点,并使用lwip建立http服务器)
工程的建立:在Keil中新建一个STM32F103RE的工程,并勾选上启动代码CMSIS/CORE和Device/Startup:
如果要使用STM32的库函数,则还需要勾选Device/StdPeriph Drivers中的项目(本文不使用库函数,因此不必勾选):
工程建好后,Keil已经帮我们自动添加好了启动文件startup_stm32f10x_hd.s,其中hd是指high-density。
在其中创建main.c文件,以及存放WiFi驱动程序的文件WiFi.h和WiFi.c。
以下是88W8686 WiFi模块与STM32单片机的连线。
SDIO_CLK接PC12,SDIO_CMD接PD2,数据线D0~D3接PC8~11。
在笔者所用的的开发板上,VCC3V3引脚不是直接连接到电源的,而是通过一个场效应管接到PB12上的。当开发板外接了5V的电源插头,并且PB12为低电平时,WiFi模块才通电工作。下载程序时,PB12输出高阻态,此时WiFi模块断电。每次单片机复位时,WiFi模块也就跟着自动复位。自己焊的板子可以用一个PNP三极管来代替场效应管Q1。
【程序1】Marvell 88W8686 WiFi模块的初始化代码:http://blog.csdn.net/zlk1214/article/details/73693167
对于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。
由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的原有内容。
此外还需在项目属性里勾选上“Use MicroLIB”选项。
J-Link下载器配置:在Debug选项卡中选择J-Link作为调试工具,并在设置对话框里勾选上Reset and Run复选框,以便下载完成后程序能自动开始运行。
STM32 SDIO外设的初始化
串口初始化完毕后便开始执行Card_Init函数:SDIO卡初始化函数。
只有当该寄存器的第1~0位同时为1时才能启动该外设。
在STM32F10x.h头文件中,有如下的定义:
接着配置SDIO_CLKCR寄存器。该寄存器的CLKEN位决定是否启用时钟引脚SDIO_CK,即是否向PC12引脚输出时钟信号。CLKDIV为时钟分频系数。
PC12引脚上的频率为:SDIO外设的频率 ÷ (CLKDIV + 2)
因为SDIO是AHB总线上的外设,所以SDIO外设的频率等于AHB总线的频率(记为HCLK),为72MHz。
程序中配置的是CLKDIV=178,因此分频后,在PC12引脚上输出的时钟频率就是400kHz。这是SD卡在初始化时所允许的最高频率。只有当SDIO总线上挂接的所有SD卡都初始化完毕了之后,这一频率才允许提高。
然后调用delay函数延时5毫秒。延时的目的是上电后使器件做好准备,降低CMD5命令重发的可能性,但这不能完全防止CMD5重发。delay函数的实现如下:
CEN刚置位时,CNT=0。经过1/(10kHz)=0.1ms后,CNT从0跳变到1。经过4.9ms后,CNT从48跳变到49。经过5ms后,CNT刚好从49跳变到0。
SDIO卡的初始化流程见Part E1文档的图3-2。首先以空参数(ARG=0)发送一个CMD5命令,检查有无回应。若有回应,则设置参数ARG后再次发送CMD5,检查回应中的MP(Memory Present)位后决定之后的流程。
在STM32 SDIO外设中,使用SDIO_CMD寄存器发送命令,使用SDIO_ARG寄存器设置命令参数。
在SDIO_CMD寄存器中,CMDINDEX决定命令号,CPSMEN=1时发送命令(该位不会自动清零,只要写完寄存器后该位为1,就发送命令)。WAITRESP=00时不等待回应,WAITRESP=01时等待48位的短回应,WAITRESP=11时等待136位的的长回应。回应的内容保存在RESP1~4寄存器中。
运行结果如下:
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。
OCR寄存器的第23~15位为1,表明该SDIO卡支持2.7~3.6V的电压。
值得注意的是,该程序假定SDIO总线上只接了WiFi模块,没有接其他任何东西,包括SD内存卡。所以不发送Part E1文档图3-2中的用于SD卡的CMD0复位命令和CMD8命令。
因为CMD5的命令回应中,功能数NF=1>0,且OCR寄存器的值有效,所以需要再次发送CMD5,且这次参数ARG为主机设置的电压范围。因为我们不请求1.8V低电压模式,所以ARG中S18R=0。
接下来,发送CMD3命令,获取WiFi模块的RCA相对地址,并保存到全局变量rca中。
在SDIO卡中CMD3的回应格式为R6,其高16位为RCA相对卡地址:
获得相对地址后,发送CMD7选中WiFi模块,其参数ARG的高16位为欲选中模块的RCA地址,其余位为0。
到此,WiFi模块已初始化完毕,现在可提升SDIO总线的时钟频率。频率不要设得太高(如24MHz),否则即便是不用库函数,用寄存器操作,发送/接收数据时也很容易忙不过来导致Underrun/Overrun的错误,除非使用DMA。
SDIO接口开机后的默认数据宽度为1,只使用D0(PC8)这一根数据线。为了同时使用D0~D3四根数据线,需要修改WiFi模块中的卡公共控制寄存器(CCCR寄存器,见Part E1文档的6.9节),将地址为0x07的总线接口控制寄存器(Bus Interface Control)中的Bus Width位改为10。
SDIO_CLKCR寄存器中的WIDBUS位控制的是SDIO外设的数据宽度,当WIDBUS=01时采用四位数据线模式。
这里涉及到SDIO卡内寄存器的读取(Card_Read)和写入(Card_Write),这是通过发送CMD52命令实现的。
CMD52命令参数的格式如下图所示:
其中,R/W flag决定是读寄存器还是写寄存器,Function Number为寄存器所在的功能区,RAW flag表示写寄存器后是否自动读取寄存器的实际内容,Stuff为始终为0的填充位,Register Address为寄存器地址,Write Data为要写入的数据,读寄存器时用0填充。
CMD52命令的回应格式为R5,其中不仅包含了8位的寄存器内容,还包括卡状态信息Response Flags,所以RESP1必须要与上0xff滤掉卡状态信息位。
CCCR寄存器和以后要讲的FBR寄存器和CIS卡信息结构,都位于0号功能区(Function 0)中的公共I/O区域(CIA)中。
接下来我们来讨论一下,如果SDIO总线上同时接了WiFi模块和SD内存卡,该怎么初始化。
笔者建议大家最好不要将WiFi模块和SD卡同时接到STM32的SDIO接口上,因为这样会产生很多兼容性问题。如果必须要同时使用WiFi模块和SD内存卡,例如要用WiFi做一个HTTP服务器浏览保存在SD卡上的网页,或是要实现FTP协议读写SD卡,则最好WiFi模块单独使用SDIO接口,SD卡使用SPI接口。
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上。
若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命令的前面。
由于WiFi模块中MP(Memory Present)=0,所以不应在WiFi模块的CMD3命令前发送ACMD41和CMD2,应该改在CMD3后面发送。
最终的初始化顺序为:
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。
程序中新增了一个Card_Select函数,专门用于发送CMD7命令,选择要操作的卡。
若参数rca为0,则取消选中所有的卡,CMD7命令将不会产生回应,进入的分支是SDIO_STA_CTIMEOUT。
注意:当rca!=0时,CMD7命令也有可能超时,所以最好加上命令超时重发的代码。
SD卡和SD I/O卡设置总线宽度的命令是不一样的。前者用的是ACMD6命令,后者用的是CMD52命令。