本文总结BCM43438的初始化流程,初始化成功的标志是连上WiFi。

参考WICED SDK、NuttX,以及Linux的源码,它们的流程略有不同。Linux的驱动面面俱到;WICED的驱动虽只针对物联网设备,却写得相当冗杂;nuttx的驱动则相当简洁恰好能用。

  • WICED可从Cypress官网上下载,注册个账号就好了。这份软件架构简直是一切以WICED为中心,有bootloader、dct(Device Configuration Table)、OTA(Over The Air)等自创的元素,如果用它支持的平台如stm32的话,那可以非常迅速地出成果,然而如果要移植到别的CPU架构的话它并不太友好。。
  • NuttX可从bitbucket/nuttx上git clone一份,或者从SourceForge下载代码包。SourceForge上面的7.24及以前的驱动有问题,我在bitbucket上提交了这份commit。bitbucket在墙内访问相当不稳定。。
  • Linux可以直接访问Bootlin的Linux Cross Reference。在Linux内核中,博通SDIO接口的网卡驱动是brcmfmac,归类到brcm80211,这是博通官方提供的驱动,需要non-free的固件。b43的驱动是社区通过一些逆向工程实现的,只支持老版的PCIe网卡。。

初始化过程纯粹就是面向过程的代码,因此以下通过罗列它们的流程来进行分析。

固件这玩意儿想必是厂商喜闻乐见的东西——在你编写的程序中安插黑盒子,而你又无可奈何。

SDIO卡简述

BCM43438是一个SDIO卡。SDIO卡SD卡虽然都用了SDIO接口,但是二者不可混为一谈。

  • SD卡是SD memory card,驱动通用;
  • SDIO卡就是为IO而准备的,为此使用了扩展的SDIO规范
  • 如果卡中既有存储,又有IO,则被称为combo卡。

SDIO卡具有IO空间,通过CMD52CMD53这两条的指令读写其中的数据:

  • CMD52,单字节IO读写,数据只通过CMD引脚传输;
  • CMD53,多字节IO读写,数据通过DATA[3:0]传输,非常适合传输大批量数据

IO空间里有多个Function,一个Function就是一块独立的IO空间:

sdio function
sdio function

  • 第0个Function叫CIA(common IO area),其中包含了多个寄存器组,基本上只在初始化时候才会用到:
    • CCCR(Card Common Control Registers),功能诸如开关其他Function、开关中断等等;
    • FBR(Function Basic Registers),其中有设置该Function的block size的寄存器,还有指向下面CIS区域的指针;
    • CIS(Card Information Structure),除了SDIO specification里面有介绍外,没找到其他资料,而且WICED和nuttx里代码里面也没有用到。。
  • 另外最多支持7个Function,里面的寄存器由厂商自己定义。BCM43438有两个Function:
    • Function1叫Backplane,来源于Sonics Silicon Backplane(SSB),是可以访问整个SOC的地址空间的总线。不过只有老版的网卡使用SSB总线,新版网卡(包括我们的BCM43438)则使用AXI总线,二者的接口并不同,只是名称沿用了下来。
    • Function2叫WLAN,与固件交互时候使用,传输控制命令以及数据。

IO空间的编号、寄存器地址编码到CMD52、CMD53命令中,从而进行读写操作。

cmd52,53
cmd52,53

BCM43438-bringup

博通家SDIO接口的网卡初始化流程比较通用,下面罗列出要做的事情,WICED、nuttx、Linux里的执行顺序略有不同,说明初始化流程并不是唯一的。

  • Linux中涉及到的bcmfmac里的文件有:chip.csdio.cfirmware.c
  • WICED里主要就是WICED/WWD/internal/bus_protocols/SDIO/ wwd_bus_protocol.c
  • Nuttx则主要是drivers/wireless/ieee80211/ bcmf_sdio.c

枚举SDIO卡

它们的流程都一样:

  • 发送CMD5,没回应就按照SD存储卡初始化流程走,若有回应则是SDIO卡;
  • 发送CMD3,让卡返回一个自己生成的RCA(Relative Card Address)
  • 将刚才的RCA作为参数,发送CMD7,从而选中那张卡;

使能Function

  • WICED:WICED/WWD/internal/bus_protocols/SDIO/wwd_bus_protocol.c: wwd_bus_init()
  • Nuttx:drivers/wireless/ieee80211/bcmf_sdio.c: bcmf_probe()和bcmf_businitialize()
  • Linux:drivers/net/wireless/broadcom/brcm80211/brcmfmac/bcmsdh.c: brcmf_sdiod_probe(),被sdio_driver的probe函数调用。

首先初始化Function0、1的寄存器。一部分通用寄存器在SDIO specification里讲述,其他的寄存器没有资料。

  • 使能Backplane即F1(写F0的CCCR_IOEN寄存器)。
  • 设置4位总线宽度(写F0的CCCR_BICTL寄存器)。另外主机也要设置为4bit,并可以提高时钟速度。
  • 设置各个Function的block size(写F0的CCCR_FNx_BLKSIZE0、1、2,每个Function各两个寄存器)。它都设为64了,虽然数据手册上说Function0的最大只有32。。。
  • 使能Function1、2的中断(写F0的CCCR_INTEN寄存器)。
  • 在Function1中,使能“ALP(Active Low-Power)”时钟(写F1的CHIPCLKCSR寄存器,BCM定义的)。

使能了F1之后可以随时读F1的0x18000000地址处读到设备ID号,在Linux的brcmfmac的驱动中,该地址处的结构体为struct chipregs,第一个元素是chipid。而WICED和nuttx的驱动里面没发现这种结构体——它们都用的是一大堆宏定义的寄存器地址。不同的模块chipid可见Linux内核的drivers/net/wireless/broadcom/brcm80211/include/ brcm_hw_ids.h,其中BCM43438的ID是十进制的43430

0x18000000这个地址在brcmfmac里被命名为SI_ENUM_BASE,在老版SSB驱动里被命名为SSB_ENUM_BASE。brcmfmac里SSB总线、AXI总线分别实现了,体现了bcm网卡内部总线的发展历史。。。

总线 chipid最高8位 接口前缀 头文件
SSB 0 brcmf_chip_sb_ linux/ssb/ssb_regs.h
AXI 1 brcmf_chip_ai_ linux/bcma/bcma_regs.h

传输固件

固件有三种:

  • 两三百kb的大固件
  • 一两kb的nvram
  • 几kb的clm blob,这不是必需品

在这个阶段首先传前两个固件。大固件从F1的RAM首地址开始放,nvram放在RAM末尾倒数4个字节之前,RAM最后4个字节是编码之后的nvram大小。

fwlayout
fwlayout

nvram的配置

nvram是一个字符串数组,目的在于调试时候设置一些参数,它们最终要烧写到BCM43438的OTP(One Time Programable)存储器里面的。BCM固件收到nvram之后会与OTP空间对比,同名的参数配置以OTP为准。因此实际操作中为了保持一些灵活性,大家都不会把nvram里面所有的东西都写入OTP里面。详细的参数设置可以参考Cypress的这份文档:AN214807:OTP Programming and NVRAM Development in SDIO Mode。

  • WICED:在platforms/XXXXXXXXXX/wifi_nvram_image.h中定义的数组。
  • NuttX:在configs目录下板级配置中定义的数组,如configs/photon/src/stm32_wlan_firmware.c
  • Linux:通用固件nvram_ap6212a.txt

      #AP6212A_NVRAM_V1.0.1_20160606
      # 2.4 GHz, 20 MHz BW mode
    
      # The following parameter values are just placeholders, need to be updated.
      manfid=0x2d0
      prodid=0x0726
      vendid=0x14e4
      devid=0x43e2
      boardtype=0x0726
      ...
    

其中需要留意的参数有:

  • macaddr,设置MAC地址。其实现在买到的AP6xxx模块的OTP里面都已经有这个参数了,除非直接订购的是BCM43438芯片。
  • xtalfreq,设置晶振的频率,单位为kHz。有些板子用的是37.4M晶振,有些用的是26M晶振。一定要设对,否则后面PLL时钟起不来

需要特别注意的是,最终下载到芯片里的东西必然是WICED、nuttx里面的那种风格的数组,每一个xxxx=xxxx的字符串键值对之后都有一个0字符,而不是Linux的配置文件那种风格。Linux的nvram.txt是经过处理才下载的,详见drivers/net/wireless/broadcom/brcm80211/brcmfmac/ firmware.c。它使用状态机进行词法分析,滤除注释,然后brcmf_fw_add_defaults(),添加0字符。

下载固件之前

首先需要disable芯片的Cores,包括Cortex M3的核,还有内存控制器的核。

另外BCM43438需要取消SRAM bank 3的remap。Linux写在drivers/net/wireless/broadcom/brcm80211/brcmfmac/ chip.c:brcmf_chip_cm3_set_passive()处;WICED则是WICED/WWD/internal/chips/xxxxx/ wwd_chip_specific_functions.c: wwd_chip_specific_socsram_init()。nuttx目前只是单纯在下载固件之前判断了一下。

下载固件

根据不同的ID可以选择不同的固件。

  • Linux:在brcmfmac/sdio.c里定义一堆固件名称,从而可以在brcmf_fw_get_firmwares()处从文件系统里request_firmware。sdio.c中传入callback函数brcmf_sdio_firmware_callback(),随后brcmf_sdio_firmware_callback(),传输大固件以及nvram。
  • WICED:层层调用到WICED/WWD/internal/bus_protocols/ wwd_bus_common.c: download_resource(),然后使用 host_platform_resource_read_direct() 或者host_platform_resource_read_indirect()函数,从片内flash或者片外flash里面读取固件,然后wwd_bus_transfer_bytes()下载到BCM芯片内。因为WICED运行时的平台单一,所以它并不考虑通过不同ID号选择不同的固件。
  • nuttx:在drivers/wireless/ieee80211/bcmf_sdio.c: bcmf_chipinitialize()里通过ID号switch case一个芯片,随后drivers/wireless/ieee80211/bcmf_core.c: bcmf_core_upload_firmware(),然后bcmf_upload_binary()。目前nuttx的固件直接写为一个巨大的uint8_t数组。。。比如photon的固件,在configs/photon/src/ stm32_wlan_firmware.c里。
等待固件初始化时钟
  • Linux:brcmfmac/sdio.c:brcmf_sdio_htclk(),有个循环;
  • WICED:WICED/WWD/internal/bus_protocols/SDIO/wwd_bus_protocol.c: wwd_bus_sdio_download_firmware(),下载了firmware和nvram之后有个循环;
  • NuttX:drivers/wireless/ieee80211/bcmf_sdio.c: bcmf_sdio_bus_sleep(false),这有个循环。下完固件,设置中断的时候才调用它。

做法就是循环读F1的CHIPCLKCSR寄存器的第7位,命名为High Throughput Clock Available。这一步经常会不过关,原因可能是:

  • 大固件不对,比方说将BCM43438-A0的固件下到了A1的芯片上面了。
  • nvram不对,可能直接将Linux的哪个txt下进去了,可能是xtalfreq不对,可能是别的东西。。。
  • 板子问题,晶振不起振,等等。
  • 等待的时间不够长,可能板子焊的不太好,要多等一会儿才能available。

如果这一步过关了,说明这个固件或许可以用,后面或许就轻松了不少。。

然后收尾
  • 使能F2、F2的中断
  • 初始化save-restore功能。并不是所有固件都支持sr功能。
    • WICED:WICED/WWD/internal/chips/xxxxx/ wwd_chip_specific_functions.c: wwd_chip_specific_init()
    • Linux:brcmf_sdio_firmware_callback()之后brcmf_sdio_sr_init()
    • Nuttx:我照着WICED的代码在bcmf_sdio.c里面添加了bcmf_sdio_sr_init()。之前虽然没有,但是据说也能用。。

与固件交互进行初始化

与固件交互,用的是Function2即WLAN,Linux、nuttx里将该功能注释为“frame transfer”。在Function2传输的数据要符合BCM定义的一套协议,分为两层:

  • 上层:bdc、cdc
    • bdc用于传输数据包,接入IP协议栈的底部。
    • cdc用于传输控制命令,有ioctl和iovar之分。控制命令包括开关WiFi、查询或设置MAC地址等等。也可以说属于带外传输的范畴。
  • 下层:sdpcm,用于包装bdc、cdc。

具体实现部分下一篇博客将讨论。这里仅讨论初始化阶段需要做的东西。

这阶段就是发送一些ioctl、iovar去查询一些信息,以及设置一些东西。
ioctl是一些整数指令号,可以附带一些参数;
iovar是字符串命令,也可以附带一些参数。

初始化参数
  • 查询该固件的版本。发送名为"ver"的iovar,返回一个字符串。比方说它会返回”wl0: Jun 19 2016 22:40:09 version 7.45.45.17 (r644353) FWID 01-dbaba83”
  • 查询MAC地址。发送名为"cur_etheraddr"的iovar,返回6字节的MAC地址(返回的buffer不一定只有6字节,其他的都没用)
  • 关闭所谓的TX glomming:"bus:txglom"的iovar
  • 开启APSTA模式:"apsta"的iovar
  • 设置A-MPDU的参数。amsdu、ampdu的概念可参考这篇博客ampdu_为前缀的一系列iovar
  • 设置国家和地区:"country"的iovar
  • 注册事件,使用"event_msgs",参数是一个一百多位的位图,每一位代表一个事件,置一则代表主机打算相应这个事件。Linux在brcmfmac/fweh.h里定义为enum brcmf_fweh_event_code,nuttx在bcmf_ioctl.h里定义为enum wl_event_num_t,WICED在WICED/WWD/include/wwd_events.h里定义为enum wwd_event_num_t
  • WiFi up,发送WLC_UP的ioctl,编号是2。ifup这个命令在Linux和nuttx里都有,if的全称应该是interface,比方说ifup wlan0;它底层就是发送了WLC_UP指令。

另外需要注意roaming(漫游)的问题。bcmf固件有自动漫游功能,通过"roam_off"的iovar来开关。不应该关闭;否则,如果现在连接的AP并不是信号最强的话,每隔一段时间就会自行断开当前连接,给上层一个DEAUTH事件,然后连接信号最强者。Linux的brcmfmac驱动有个参数名叫brcmf_roamoff,默认不roam_off。而nuttx则默认roam_off,上层却缺乏断线重连的逻辑,因此相关的初始化应该去掉。

连接WiFi

就是join一个Access Point(AP),一个AP一般有一个名字叫SSID,以及密码(当然也有隐藏的WiFi和开放的WiFi)。对于Linux和nuttx来说决定要连接WiFi的是应用程序而不是内核,需要调用ioctl来陷入内核从而进行操作。

  • Linux:drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c:brcmf_cfg80211_connect()
  • WICED:WICED/WWD/internal/wwd_wifi_join()
  • NuttX:drivers/wireless/ieee80211/bcmf_driver.c:bcmf_wl_set_ssid(),它在ioctl(SIOCSIWESSID)时候被调用。

流程如下:

  • 设置wireless mode为infra或adhoc,使用ioclt为WLC_SET_INFRA(WICED & nuttx)或BRCMF_C_SET_INFRA(Linux);
  • 设置WPA授权方式的版本号,使用iovar"bsscfg:sup_wpa"
  • 设置授权方式,如WPA、WPA2。使用ioctl为WLC_SET_WPA_AUTH
  • 设置加密方式,如WEP、TKIP、CCMP(其核心算法是AES)等。使用ioctl为WLC_SET_WSECWLC_SET_AUTH
  • 设置WiFi密码,使用ioctl为WLC_SET_WSEC_PMK,参数就是密码字符串;
  • 设置WiFi名,使用ioctl为WLC_SET_SSID(WICED & nuttx)或BRCMF_C_SET_SSID(Linux),参数是WiFi SSID字符串。
  • 随后就等待路由器的授权信息。。。

连接上路由器,并不代表获得了IP地址。IP可以静态设置,也可以动态分配(DHCP)。但这些已经不属于MAC层的范畴了,正常的操作系统都会配备相应的应用程序来完成这些功能。

错误

有时候BCM芯片会发一个很大的东西回来,是其内核寄存器的dump。Linux内核中对此命名为struct brcmf_trap_info,并输出log信息。WICED和nuttx都假装没有这种问题。dump之后WiFi芯片将不会响应任何命令。。。

这一般说明固件不配套,可能是发送了不合法的命令,也可能发了几条命令之后片子发现自己不能执行下去了,然后就dump,卡死。这时就要多试几个固件了。。。

板子试验

只讨论WICED和nuttx的板子。

  • WICED开发板选用BCM94343WWCD1-EVB,主控是STM32F411VEH6,WiFi芯片是BCM4343W。板子带有一片SPI flash,BCM的固件就存放在那里。
  • Redbear提供了WICED 6.1的补丁,直接复制到SDK目录下面就行了。
  • Photon和Redbear都可以跑nuttx,Redbear需要稍作移植,主要是填充BCM43438的固件,毕竟43362和43438的驱动相当通用。

跑WICED

安装了WICED SDK之后,在43xxx_Wi-Fi目录下面make。这份SDK提供了make程序从而不必使用系统的make程序。可以直接make它自带的例程,例程都放在apps目录的子目录下面:

apps/
├── demo
│   ├── aliyun_mns
│   ├── apollo
│   ├── appliance
│   └── ...
├── snip
│   ├── scan
│   ├── https_server
│   ├── tcp_client
│   └── ...
├── test
│   └── ...
├── waf
│   ├── bootloader
│   └── ...
└── wwd
    ├── ping
    ├── scan
    └── ...

make后接的target这样设置:<例程目录>.<例程名称>-<板子名称>。比方说要跑apps/snip/scan里面的例程:

  • ./make snip.scan-BCM94343WWCD1 # 对于BCM94343W开发板
  • ./make snip.scan-RB_DUO # 打了补丁之后可用的Redbear开发板

值得一试的例程除了scan,还有tcp_client,后者需要在主机上跑一个Python程序,板子连接主机之后不断发送hello数据。

跑nuttx

目前nuttx有photon开发板的现成配置。nuttx的编译方式是,内核和apps目录分开,在内核目录下面用tools/configure.sh来配置板子。比方说photon:

  • ./tools/configure.sh photon/wlan

然后更改.config文件中的CONFIG_NSH_WAPI_SSID为WiFi名,CONFIG_NSH_WAPI_PASSPHRASE为WiFi密码。其他按照默认配置,编译出来的nuttx.bin可用直接用photon的bootloader进行烧写、启动。

  • sudo dfu-util -d 2b04:d006 -a 0 -s 0x08020000 -D nuttx.bin

复位之后,板子会自动连上WiFi。在路由器中找板子的ip地址,然后通过telnet来访问板子的nsh。

$ telnet 192.168.2.102
Trying 192.168.2.102...
Connected to 192.168.2.102.
Escape character is '^]'

NuttShell (NSH) NuttX-7.24
nsh>

对于Redbear,则可以依葫芦画瓢地增加板级配置文件,也能达到这个效果。

photon
photon