与固件交互时,数据包往下传到SDIO网卡之前,还需要对它进行两层包装,简单说:IP协议栈数据包包装为bdc数据包,控制命令则包装为cdc数据包;bdc、cdc在Linux中统称为bcdc;bcdc再进一步包装为sdpcm数据包,进而通过SDIO发送给网卡。而博通只有SDIO的网卡才多包一层sdpcm,USB、PCIE接口的网卡都没有这样做。

Linux、nuttx、WICED它们在sdpcm这一层都用了一个线程,专门包装发送、接收解析这些sdpcm数据包。单线程+中断的模型未必不可以,但是若接收数据包时处于SDIO中断上下文,而接收的时候也得利用SDIO的中断去收数据,这就是自己嵌套自己的不愉快的事情了。于是在RTOS上,该线程便使用信号量去指示是否有事情发生,在Linux上则直接使用work queue去处理这种事情。

bcdc层

cdc用于发送、接收命令,比如查询固件版本号、查询设置MAC地址、开关连接WiFi、注册事件等等;这些功能一般都调用ioctl去执行,属于带外数据
bdc上面则直接接的是IP协议栈,上层就是socket的read、write,属于带内数据

cdc协议

控制命令分为ioctliovar两种;二者都带有cdc头部,其数据结构如下:

struct bcmf_cdc_header {
    uint32_t cmd;    /* 命令编号 */
    uint32_t len;    /* 命令长度 */
    uint32_t flags;  /* 标志位 */
    uint32_t status; /* 返回状态 */
};

发送ioctl,将命令编号填入cmd字段,cdc头之后紧接着数据。

  • linux里的cmd编号以BRCMF_C_为前缀,见fwil.h
  • nuttx和WICED的cmd宏定义命名都是一样的,以WLC_为前缀。nuttx见drivers/wireless/ieee80211/ bcmf_ioctl.h第723行之后,WICED则在WICED/WWD/include/ wwd_wlioctl.h第899行之后。

iovar是cmd为WLC_SET_VARWLC_GET_VAR的ioctl,cdc头之后跟着一个字符串,然后再跟着数据。这些字符串,nuttx和WICED都将其定义为一大堆以IOVAR_STR_开头的宏定义;而Linux则是直接写的字符串,散落在cfg80211.c、common.c等文件里面

bdc协议

bdc头只有四个字节:

struct bcmf_bdc_header {
  uint8_t flags;       /* bdc标志位 */
  uint8_t priority;    /* 优先级 */
  uint8_t flags2;      /* 网卡模式 */
  uint8_t data_offset; /* bdc头部之后跳过的4字节个数 */
};
  • flags字段填0x20是bdc协议的标志位
  • 优先级其实不用管
  • flags2是网卡此时工作模式,有station、access point、P2P这三种取值;正常情况下连WiFi就是station,开热点就是access point。
  • data_offset这个字段说的是4字节的倍数。之所以是“4”据说是因为bdc头部长度为4,为了考虑扩展和对齐,bdc头后面会跳过data_offset个bdc头大小的数据

bdc层还包装了事件帧,共有一百多个事件,包括扫描到WiFi、路由器的授权信号、接入点的游走(roaming)等等;这些事件就是之前用cdc注册的事件,却交给bdc去处理。。。

sdpcm数据结构

在bcdc包前面补上一个sdpcm头即可。对于这个头,Linux中有一段这样的注释(横屏看)

/**
* brcmfmac sdio bus specific header
* This is the lowest layer header wrapped on the packets transmitted between
* host and WiFi dongle which contains information needed for SDIO core and
* firmware
*
* It consists of 3 parts: hardware header, hardware extension header and
* software header
* hardware header (frame tag) - 4 bytes
* Byte 0~1: Frame length
* Byte 2~3: Checksum, bit-wise inverse of frame length
* hardware extension header - 8 bytes
* Tx glom mode only, N/A for Rx or normal Tx
* Byte 0~1: Packet length excluding hw frame tag
* Byte 2: Reserved
* Byte 3: Frame flags, bit 0: last frame indication
* Byte 4~5: Reserved
* Byte 6~7: Tail padding length
* software header - 8 bytes
* Byte 0: Rx/Tx sequence number
* Byte 1: 4 MSB Channel number, 4 LSB arbitrary flag
* Byte 2: Length of next data frame, reserved for Tx
* Byte 3: Data offset
* Byte 4: Flow control bits, reserved for Tx
* Byte 5: Maximum Sequence number allowed by firmware for Tx, N/A for Tx packet
* Byte 6~7: Reserved
*/

Linux里填充sdpcm头时就是一个个字节地数的。。。详见brcmf_sdio_hdpack()。正常发送bdc、cdc时候并不需要“hardware extension header”,所以就是两部分:4字节的大小和8字节的software header。

在WICED中将这翻译为如下的数据结构:

typedef struct
{
    uint8_t sequence;
    uint8_t channel_and_flags;
    uint8_t next_length;
    uint8_t header_length;
    uint8_t wireless_flow_control;
    uint8_t bus_data_credit;
    uint8_t _reserved[2];
} sdpcm_sw_header_t;

typedef struct
{
    uint16_t           frametag[2];
    sdpcm_sw_header_t  sw_header;
} sdpcm_header_t;

typedef struct
{
    wwd_buffer_header_t    buffer_header;
    sdpcm_header_t         sdpcm_header;
} sdpcm_common_header_t;

可以看到WICED跟Linux一样将sdpcm头拆成两部分:4字节的大小和8字节的software header。不过WICED自己添加了一个wwd_buffer_header_t,这是WICED自己的头,发送之前会去掉。随后,它定义了cdc、bdc的专用数据头:

typedef struct
{
    sdpcm_common_header_t  common;
    sdpcm_cdc_header_t     cdc_header;
} sdpcm_control_header_t;

typedef struct
{
    sdpcm_common_header_t  common;
    uint8_t                _padding[2];
    sdpcm_bdc_header_t     bdc_header;
} sdpcm_data_header_t;

可见它认为bdc数据头是要多加两个字节的padding的。

nuttx则更简单:

struct bcmf_sdpcm_header {
    uint16_t size;
    uint16_t checksum;
    uint8_t  sequence;
    uint8_t  channel;
    uint8_t  next_length;
    uint8_t  data_offset;
    uint8_t  flow_control;
    uint8_t  credit;
    uint16_t padding;
};

注意,nuttx的padding还是属于前面software header的最后的两字节,而且它发送bdc、cdc都用这个头,不像WICED发送bdc时候又再多补了两字节的padding。不过BCM芯片返回的bdc数据包的sdpcm头的确比cdc的sdpcm头多两个字节;但是只要数据段寻址时候用的是header_length(data_offset)字段,而不是简单粗暴地用sizeof,就不会出问题。

以nuttx的数据结构为例,header里的字段如下:

  • size,这个包的长度,包含sdpcm头。
  • checksum,size逐位取反。。。
  • sequence,每发一个包时候递增1;每发一个包,芯片都会返回一个包,它们sequence编号相同。
  • channel,有control、data、event三种通道,其中event代表了异步事件,就是之前用cdc注册的一百多种事件之一。
  • next_length,在三份代码里面都直接置零而不去利用;或许说明BCM一开始设计这个头的时候,考虑将多个bdc、cdc包聚合(aggregate)为一个sdpcm包,有点类似A-MSDU和A-MPDU的想法,但是后来就直接简单地一个sdpcm包里只放一个bcdc包了。
  • data_offset,也可以解释为这个头部的长度。
  • flow_control,没用
  • credit,解释为max sequence,上面的sequence字段不能超过它。BCM芯片每返回一个包就更新一次credit。
  • padding,置零。(WICED里发送bdc包时候后面会在此基础上另外多增加两字节的padding。。。)

分层实现

  • nuttx严格分层了:drivers/wireless/ieee80211/目录下的bcmf_cdc.c、bcmf_bdc.c、bcmf_sdpcm.c
  • Linux中bdc、cdc集中为drivers/net/wireless/broadcom/brcm80211/brcmfmac/ bcdc.c,sdpcm则是sdio.c
  • WICED将bcdc、sdpcm都搅在一起:WICED/WWD/internal/ wwd_sdpcm.c

bcdc层跟sdpcm层的接口,三份代码都无一例外使用了函数指针。如果说Linux里是为了兼顾PCIE和USB接口的网卡的话,nuttx和WICED估计就是单纯地依葫芦画瓢吧。。。

零拷贝缓冲区

发送的数据都要写入驱动的buffer。驱动中的子层每一层都会对它进行包装——添加一个头部。如果不涉及数据的切分,为了避免重复性的拷贝,最好的方法是这些子层分别引用同一个buffer的不同部分。显然,最底层需要最大的缓冲区。

因此,无论是nuttx还是wiced,它们发送数据的话都是“向驱动申请缓冲区,填写缓冲区,最后发送”这种模式,而不是自己随便申请一个缓冲区,写完之后丢给驱动。

本层模块向低一层模块申请一块包含本层头部与数据区的buffer,然后给高一层的模块返回减去本层头部的那部分。最底层模块才真正malloc一个缓冲区。

nuttx

发送数据或命令都是阻塞的,bdc、cdc都会等到收到回应才返回。

发送接收线程:bcmf_sdio.c里有一个bcmf_sdio_thread(),循环地等待一个名叫thread_signal的信号量,发送数据、接收数据,都会发出信号量从而触发其行动。

以发送一个iovar命令为例。cdc层给调用sdpcm层发送thread_signal之后,等待一个名为control_timeout的信号量;由中断程序触发bcmf_sdio_thread()去接收数据后,该线程发出control_timeout信号量,进而促使cdc层返回数据。

cdc
cdc

WICED

WICED的线程是wwd_thread.c:wwd_thread_func(),整个流程处理方式与nuttx很类似,只是做了一些冗余的操作。

  • 添加了WICED自己的头部,发送之前去掉。
  • 申请了sdpcm缓冲区之后填数据,数据前面为对齐而留白,然而最终发送之前又将整段数据往前面搬了。。。或许它是为了防止别的进程使用的内存中该缓冲区之前的数据的越界而践踏我的数据。

Linux

Linux用的是work queue机制,由worker_thread内核线程统一执行。brcmfmac的workqueue是brcmf_sdio_dataworker()。除此之外整个流程其实相当相似,只是考虑到多核、编译器优化、乱序、大小字节序等情况而用了一些晦涩的接口,加了不少锁以及一些存储屏障,并且将函数分的更细致了,到处都是层层调用。