本文以STM32F1系列的单片机为例,详细讲解Marvell公司的88W8686 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单片机上都能运行)。
【完整的程序】Marvell 88W8686 WiFi模块(WM-G-MR-09)创建或连接热点,并使用lwip2.0.3建立http服务器(20180706版)(支持WEP,WPA-PSK和WPA2-PSK):https://blog.csdn.net/ZLK1214/article/details/80941657
Marvell 88W8801 WiFi模块连接路由器,并使用lwip2.0.3建立http服务器(20180729版)(支持WPA-PSK和WPA2-PSK):https://blog.csdn.net/ZLK1214/article/details/81275584
88W8686已经是比较老的芯片了,其数据手册(datasheet)的发布时间是2007年2月20日。淘宝网上可以买到芯片组(Chip Set)为88W8686的WM-G-MR-09模块,价格比较贵,85元一个。不过只买芯片本身的话就很便宜,88W8686的裸芯片价格大约是12元左右一个。该WiFi模块通电后需要装入固件才能正常运行。模块有两种操作接口:G-SPI接口和SDIO接口,并支持SDIO接口的SPI模式。
SDIO接口(包括SDIO接口的SPI模式)的固件(firmware)及Linux下的驱动程序可直接在Marvell的官方网站上下载到,压缩包名称为SD-8686-LINUX26-SYSKT-9.70.3.p24-26409.P45-GPL.zip,里面有两个固件:helper_sd.bin和sd8686.bin,最后修改日期都是2008年2月29日。其中_sd表示这两个固件用于SDIO接口(包括SDIO接口的SPI模式)。
G-SPI接口的固件和Linux驱动在官网上是下载不到的,但CSDN上可以下载到:https://blog.csdn.net/xiaolei05/article/details/8526013。固件文件名称为helper_gspi.bin和gspi8686.bin。这里要注意的是,G-SPI接口和SDIO接口的SPI模式虽然都是SPI,但是它们的操作方法却是完全不同的,而且引脚的顺序也不一样。笔者推荐使用后者,因为这种模式跟SDIO的操作方法非常接近。如果使用G-SPI接口的话,不仅要更换固件,修改大量的代码不说,还要在模块的后面焊接上两个100kΩ的贴片电阻。
卖家世讯电子提供了STM32103RET6驱动该网卡的程序。但该程序可靠性很差,代码既乱又复杂而且很难看懂,扫描热点时经常出现problem fetching packet from firmware, rewhile的错误,连接热点时有时候会出现认证失败的错误type=0x888e!,一连接失败就直接重启单片机,而且与WPA2-PSK认证(EAPOL)有关的代码被封装到了wap_wpa2_lib.lib文件中,不开放源代码。这也是笔者写本教程的原因:自己编写出高可靠性的驱动程序!
卖家创思通信提供的WPA版本的程序,处理认证的代码也是被封装到了STM32F10xR.LIB里面,看不到源代码。这个文件名具有很大的欺骗性,看着名字很容易以为里面是封装的STM32F1的库函数实现,其实里面还封装了很多很多EAPOL认证处理函数,比如Ox0000008E函数(看上去是一个指针地址,其实第一个字符是大写字母O,是一个合法的C语言函数名),wpa_supplicant_event函数,以及wpa_msg函数等等。
较新的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日。其中_uapsta意思是:micro(μ) access point / station。该模块只支持SDIO接口和USB接口,支持SDIO接口的SPI模式。
这些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模式的命令。并且,两者的初始化时序也不一样。以下是88W8686的SDIO接口支持的SDIO命令。
在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模块没有任何关系,因此不必下载。
在SD的官方网站上只能下载到阉割版的文档,里面没有SDIO的引脚连线方法以及SDIO协议的时序图,完整版的文档才有这些。完整版的文档是标明了Confidential的(机密文档),需要交几万块钱成为会员后才能获得。不过呢,CSDN上是可以免费下载到完整版的文档的。
接下来,笔者将参考Part E1文档,讲解SDIO WiFi卡的初始化方法。
先来给大家讲讲如何使用Keil 5快速创建一个STM32单片机的工程。先点击Project菜单的New uVersion Project命令,在Keil中新建一个STM32F103RE的工程,并勾选上启动代码CMSIS/CORE和Device/Startup:
如果要使用STM32的库函数,则还需要勾选Device/StdPeriph Drivers中的项目:
库函数根本就不需要去ST的官网上下载,下载下来还要配置半天。直接在keil 5里面勾选上就可以用了。
工程建好后,Keil已经帮我们自动添加好了启动文件startup_stm32f10x_hd.s,其中hd是指high-density。STM32F103RE单片机属于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。
单片机复位后首先会运行程序中的main函数(位于main.c)。main函数调用了WiFi.c中的WiFi_Init函数,WiFi_Init函数调用了WiFi_LowLevel.c中的WiFi_LowLevel_Init函数。WiFi_LowLevel_Init函数又调用了WiFi_LowLevel_GPIOInit函数和WiFi_LowLevel_SDIOInit函数。代码如下:
RCC->APB2ENR = RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN;
GPIOA->CRH = (GPIOA->CRH & 0xfffff00f) | 0x4b0; // 串口发送引脚PA9设为复用推挽输出(b), 串口接收引脚PA10设为浮空输入(4)
USART1->BRR = SystemCoreClock / 115200;
USART1->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
printf("STM32F103RE SDIO 88W8686\n");
rtc_init();
WiFi_Init();
/* 初始化WiFi模块 */
void WiFi_Init(void)
{
// 初始化底层寄存器
WiFi_LowLevel_Init();
// .....
}
void WiFi_LowLevel_Init(void)
{
//.....
WiFi_LowLevel_GPIOInit();
WiFi_LowLevel_SDIOInit();
//....
}
/* 初始化WiFi模块有关的所有GPIO引脚 */
static void WiFi_LowLevel_GPIOInit(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_IOPDEN;
// WiFi模块的电源引脚是通过场效应管(相当于PNP三极管)接到VCC上的
// 基极接的是单片机的PB12, 发射极接的是电源VCC, 集电极接的是WiFi模块的VCC, 基极必须串联一个限流电阻
// 单片机复位时PB12输出高阻态, 三极管不导通, WiFi模块不通电
// 现将PB12设为输出低电平, 三极管导通, WiFi模块上电 (这起到了复位的效果)
GPIOB->CRH = (GPIOB->CRH & 0xfff0ffff) | 0x30000; // PB12设为推挽输出(3), 并立即输出默认的低电平
// SDIO相关引脚
GPIOC->CRH = (GPIOC->CRH & 0xfff00000) | 0xbbbbb; // PC8~11: SDIO_D0~3, PC12: SDIO_CK, 设为复用推挽输出(b)
GPIOD->CRL = (GPIOD->CRL & 0xfffff0ff) | 0xb00; // PD2: SDIO_CMD, 设为复用推挽输出
}
这些代码首先打开了GPIOA~D和串口USART1的时钟,需要先打开时钟才能使用这些STM32外设(数字电路需要时钟驱动才能正常工作)。这些外设都在APB2总线上。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。
由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型三极管的发射极接的是+3.3V,如果基极通过电阻接到单片机的I/O口上并配置为开漏输出,则当ODR=0时,三极管饱和导通,发射极与基极间的电压为0.7V,基极电阻两端的电压为2.6V;当ODR=1时输出高阻态,相当于基极直接悬空,三极管截止。
接着配置串口USART1,BRR为波特率。USART1是在APB2总线上的外设,该总线的时钟为72MHz,也就是72000000Hz。欲设置的波特率为115200,72000000 ÷ 115200 = 625 = 0x271,因此,BRR=0x271。CR1为控制寄存器,UE表示启动该外设,TE表示允许发送,RE表示允许接收。变量SystemCoreClock是定义在system_stm32f10x.c中的一个全局变量,其值刚好是72000000,所以用该变量的值除以115200后赋给BRR寄存器就行了。
为了在程序中使用printf函数向串口输出信息,需要引入stdio.h头文件,然后实现fputc函数。printf函数使用的是C语言标准输出流stdout,因此fp=stdout。ch为要输出的每个字符。若输出的是换行符\n,为了正确换行,需要先输出一个回车符\r组成\r\n。向USART1的DR寄存器写入数据前,必须先等待SR寄存器中的TXE位(发送缓冲区空)变为1。写入数据后,串口外设将自动发送数据。最后函数必须返回ch的原有内容。
#include
int fputc(int ch, FILE *fp)
{
if (fp == stdout)
{
if (ch == '\n')
{
while ((USART1->SR & USART_SR_TXE) == 0); // 等待前一字符发送完毕
USART1->DR = '\r';
}
while ((USART1->SR & USART_SR_TXE) == 0);
USART1->DR = ch;
}
return ch;
}
使用printf函数(以及后面要用到的malloc内存分配函数)前,还需在项目属性里勾选上“Use MicroLIB”选项。
J-Link下载器配置:在Debug选项卡中选择J-Link作为调试工具,并在设置对话框里勾选上Reset and Run复选框,以便下载完成后程序能自动开始运行。
J-Link有一个缺点,就是有时候会掉固件导致无法使用。笔者建议使用ST官方的调试工具ST-Link,配置时同样要勾选Reset and Run,这个调试工具使用起来很方便,不会出现掉固件的情况。如果下载程序时提示错误,只需按住开发板的复位键不放,点击屏幕上的下载按钮后再松开就行。
程序中rtc_init()函数是用来初始化STM32的RTC实时时钟外设的,用于实现delay函数和lwip协议栈需要的sys_now函数,这个函数后面讲lwip移植的时候再来详细说明。
STM32 SDIO外设的初始化
I/O口初始化完毕后便开始执行WiFi_LowLevel_SDIOInit函数初始化SDIO卡。
// SDIO外设拥有两个时钟: SDIOCLK=HCLK=72MHz(分频后用于产生SDIO_CK=PC12引脚时钟), AHB bus clock=HCLK/2=36MHz(用于访问寄存器)
RCC->AHBENR |= RCC_AHBENR_SDIOEN;
#ifdef WIFI_USEDMA
RCC->AHBENR |= RCC_AHBENR_DMA2EN;
#endif
SDIO->POWER = SDIO_POWER_PWRCTRL; // 打开SDIO外设
SDIO->CLKCR = SDIO_CLKCR_CLKEN | 178; // 初始化时最高允许的频率: 72MHz/(178+2)=400kHz
SDIO->DCTRL = SDIO_DCTRL_SDIOEN; // 设为SDIO模式
#ifdef WIFI_USEDMA
SDIO->DCTRL |= SDIO_DCTRL_DMAEN; // 设为DMA传输模式
#endif
// 不需要发送CMD0, 因为SD I/O card的初始化命令是CMD52
// An I/O only card or the I/O portion of a combo card is NOT reset by CMD0. (See 4.4 Reset for SDIO)
delay(10); // 延时可防止CMD5重发
打开SDIO外设时钟后,首先将SDIO_POWER寄存器中的PWRCTRL位置位,启动SDIO外设。以下是STM32F1参考手册中的SDIO_POWER寄存器的说明:
只有当该寄存器的第1~0位同时为1时才能启动该外设。
在STM32F10x.h头文件中,有如下的定义:
/****************** Bit definition for SDIO_POWER register ******************/
#define SDIO_POWER_PWRCTRL ((uint8_t)0x03) /*!< PWRCTRL[1:0] bits (Power supply control bits) */
#define SDIO_POWER_PWRCTRL_0 ((uint8_t)0x01) /*!< Bit 0 */
#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_DCTRL_SDIOEN位置位的目的是为了后面使用SDIO中断,SDIO卡的SDIO_D1引脚用于产生中断。
接着配置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函数延时10毫秒。延时的目的是上电后使器件做好准备,降低CMD5命令重发的可能性,但这不能完全防止CMD5重发。delay函数是用STM32的RTC实时时钟实现的,精度不是很高,这里实际延时的时间为10~11毫秒。如果想要实现精确延时,最好使用TIMx定时器。
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: IO_SEND_OP_COND */
SDIO->ARG = 0;
SDIO->CMD = SDIO_CMD_CPSMEN | SDIO_CMD_WAITRESP_0 | 5; // 接收短回应
WiFi_LowLevel_WaitForResponse(__func__);
printf("RESPCMD%d, RESP1_%08x\n", SDIO->RESPCMD, SDIO->RESP1);
这里以空参数发送CMD5,只将WAITRESP的第0位置1,等待短回应。写SDIO->CMD寄存器后SDIO就开始发送命令,WiFi_LowLevel_WaitForResponse函数用于等待命令回应,如果回应超时或出现CRC校验错误,该函数会自动重发命令,直到正确收到了命令回应。
/* 等待SDIO命令回应 */
static void WiFi_LowLevel_WaitForResponse(const char *msg_title)
{
uint8_t first = 1;
do
{
if (!first)
SDIO->CMD = SDIO->CMD; // 重发命令
else
first = 0;
while (SDIO->STA & SDIO_STA_CMDACT); // 等待命令发送完毕
WiFi_LowLevel_CheckError(msg_title);
} while ((SDIO->STA & SDIO_STA_CMDREND) == 0); // 如果没有收到回应, 则重试
SDIO->ICR = SDIO_ICR_CMDRENDC;
}
WiFi_LowLevel_CheckError函数的作用是输出错误信息,然后通过ICR寄存器清除错误标志位。
SDIO_STA_CMDACT=1表示命令正在发送。由于之前延时10ms并不能100%保证命令不会出现超时,所以该函数会检查回应是否超时,如果超时就重发命令。若收到了回应,则SDIO_STA_CMDREND自动置1,对SDIO_ICR_CMDRENDC写1清除该位,然后调用printf函数显示命令的回应内容,包括短回应的回应命令号和32位的回应内容。
若经过了64个SDIO_CK时钟周期(64/400000s=0.16ms)后仍没有收到回应,SDIO_STA_CTIMEOUT位自动置1表明超时,CMDREND位不会置位,满足do-while循环条件,再次执行循环体内的语句,重复上述过程。只有正确收到了命令回应,循环才会跳出。
运行结果如下:
STM32F103RE SDIO 88W8686
RESPCMD63, RESP1_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。
OCR寄存器的第23~15位为1,表明该SDIO卡支持2.7~3.6V的电压。
88W8686不支持CMD0和CMD8,所以不用发送Part E1文档图3-2中的用于SD卡的CMD0复位命令和CMD8命令。
因为CMD5的命令回应中,功能数NF=1>0,且OCR寄存器的值有效,所以需要再次发送CMD5,且这次参数ARG为主机设置的电压范围。因为我们不请求1.8V低电压模式,所以ARG中S18R=0。
/* 设置参数VDD Voltage Window: 3.2~3.4V, 并再次发送CMD5 */
SDIO->ARG = 0x300000;
SDIO->CMD = SDIO->CMD;
WiFi_LowLevel_WaitForResponse(__func__);
printf("RESPCMD%d, RESP1_%08x\n", SDIO->RESPCMD, SDIO->RESP1);
if (SDIO->RESP1 & _BV(31))
{
// Card is ready to operate after initialization
sdio_func_num = (SDIO->RESP1 >> 28) & 7;
printf("Number of I/O Functions: %d\n", sdio_func_num);
printf("Memory Present: %d\n", (SDIO->RESP1 & _BV(27)) != 0);
}
程序中的_BV(31)表示第31位,即检查RESP1寄存器中的第31位(C位)是否为1。
#define _BV(n) (1u << (n))
C=1(图3-2中对应IO=1)表明卡已经准备好了。此时程序向串口输出NF和MP的值。
RESPCMD63, RESP1_90300000
Number of I/O Functions: 1
Memory Present: 0
接下来,发送CMD3命令,获取WiFi模块的RCA相对地址,并保存到全局变量sdio_rca中。
static uint16_t sdio_rca; // RCA相对地址: 虽然SDIO标准规定SDIO接口上可以接多张SD卡, 但是STM32的SDIO接口只能接一张卡 (芯片手册上有说明)
/* 获取WiFi模块地址 (CMD3: SEND_RELATIVE_ADDR, Ask the card to publish a new relative address (RCA)) */
SDIO->ARG = 0;
SDIO->CMD = SDIO_CMD_CPSMEN | SDIO_CMD_WAITRESP_0 | 3;
WiFi_LowLevel_WaitForResponse(__func__);
sdio_rca = SDIO->RESP1 >> 16;
printf("Relative Card Address: 0x%04x\n", sdio_rca);
由SD内存卡的文档Part1可知,CMD3的参数为0:
在SDIO卡中CMD3的回应格式为R6,其高16位为RCA相对卡地址:
获得相对地址后,发送CMD7选中WiFi模块,其参数ARG的高16位为欲选中模块的RCA地址,其余位为0。
/* 选中WiFi模块 (CMD7: SELECT/DESELECT_CARD) */
SDIO->ARG = sdio_rca << 16;
SDIO->CMD = SDIO_CMD_CPSMEN | SDIO_CMD_WAITRESP_0 | 7;
WiFi_LowLevel_WaitForResponse(__func__);
printf("Card selected! RESP1_%08x\n", SDIO->RESP1);
程序输出:
Relative Card Address: 0x0001
Card selected! RESP1_00001e00
到此,WiFi模块已初始化完毕,现在可提升SDIO总线的时钟频率。频率不要设得太高(如24MHz),否则即便是不用库函数,用寄存器操作,发送/接收数据时也很容易忙不过来导致Underrun/Overrun的错误,除非使用DMA。DTIMER寄存器表示SDIO接口在数据端口上发送或接收数据时的最大超时时间。
/* 提高时钟频率, 并设置数据超时时间为0.1s */
#ifdef WIFI_HIGHSPEED
SDIO->CLKCR = (SDIO->CLKCR & ~SDIO_CLKCR_CLKDIV) | 1; // 72MHz/(1+2)=24MHz
SDIO->DTIMER = 2400000;
printf("SDIO Clock: 24MHz\n");
#else
SDIO->CLKCR = (SDIO->CLKCR & ~SDIO_CLKCR_CLKDIV) | 70; // 72MHz/(70+2)=1MHz
SDIO->DTIMER = 100000;
printf("SDIO Clock: 1MHz\n");
#endif
SDIO接口开机后的默认数据宽度为1,只使用D0(PC8)这一根数据线。为了同时使用D0~D3四根数据线,需要修改WiFi模块中的卡公共控制寄存器(CCCR寄存器,见Part E1文档的6.9节),将地址为0x07的总线接口控制寄存器(Bus Interface Control)中的Bus Width位改为10。
/* SDIO外设的总线宽度设为4位 */
SDIO->CLKCR |= SDIO_CLKCR_WIDBUS_0;
WiFi_LowLevel_WriteReg(0, SDIO_CCCR_BUSIFCTRL, WiFi_LowLevel_ReadReg(0, SDIO_CCCR_BUSIFCTRL) | SDIO_CCCR_BUSIFCTRL_BUSWID_4Bit);
SDIO_CLKCR寄存器中的WIDBUS位控制的是SDIO外设的数据宽度,当WIDBUS=01时采用四位数据线模式。
这里涉及到SDIO卡内寄存器的读取(Card_Read)和写入(Card_Write),这是通过发送CMD52命令实现的。
static void WiFi_LowLevel_SendCMD52(uint8_t func, uint32_t addr, uint8_t data, uint32_t flags)
{
SDIO->ARG = (func << 28) | (addr << 9) | data | flags;
SDIO->CMD = SDIO_CMD_CPSMEN | SDIO_CMD_WAITRESP_0 | 52;
}
CMD52命令参数的格式如下图所示:
其中,R/W flag决定是读寄存器还是写寄存器,Function Number为寄存器所在的功能区,RAW flag表示写寄存器后是否自动读取寄存器的实际内容,Stuff为始终为0的填充位,Register Address为寄存器地址,Write Data为要写入的数据,读寄存器时用0填充。
#define CMD52_WRITE _BV(31)
#define CMD52_READAFTERWRITE _BV(27)
/* 读SDIO寄存器 */
uint8_t WiFi_LowLevel_ReadReg(uint8_t func, uint32_t addr)
{
WiFi_LowLevel_SendCMD52(func, addr, 0, 0);
WiFi_LowLevel_WaitForResponse(__func__);
return SDIO->RESP1 & 0xff;
}
/* 写寄存器, 返回写入后寄存器的实际内容 */
uint8_t WiFi_LowLevel_WriteReg(uint8_t func, uint32_t addr, uint8_t value)
{
WiFi_LowLevel_SendCMD52(func, addr, value, CMD52_WRITE | CMD52_READAFTERWRITE);
WiFi_LowLevel_WaitForResponse(__func__);
return SDIO->RESP1 & 0xff;
}
写寄存器时,RAW=1,自动返回写入后寄存器中的实际内容。
CMD52命令的回应格式为R5,其中不仅包含了8位的寄存器内容,还包括卡状态信息Response Flags,所以RESP1必须要与上0xff滤掉卡状态信息位。
CCCR寄存器和以后要讲的FBR寄存器和CIS卡信息结构,都位于0号功能区(Function 0)中的公共I/O区域(CIA)中。