IOT-OS之RT-Thread(十五)--- SDIO设备对象管理 + AP6181(BCM43362) WiFi模块

本章介绍SDIO Wi-Fi模块的驱动实现过程,对SDIO不熟悉的可以先参阅博客:SD/MMC + SDIO,对RT-Thread驱动分层与主从分离思想不熟悉的,可以先参阅博客:驱动分层与主从分离思想

一、AP6181 Wi-Fi模块简介

1.1 AP6181 硬件接口

Pandora开发板为我们提供了一个板载Wi-Fi 芯片 — AP6181,我们不需要再外接扩展模块(比如 ESP8266)即可实现连接 Wi-Fi 并访问外部网络的功能。我们先看下Pandora开发板原理图中 Wi-Fi 模块 AP6181 的硬件接线原理图:
AP6181硬件接线原理图
正基公司的 AP6181 Wi-Fi 模组具有以下特点:

  • 符合IEEE 802.11 b/g/n标准,可以实现单通道高达72.2Mbps 的传输速度(IEEE 802.11n 标准);
  • 支持标准接口SDIO v2.0(时钟频率高速模式可达50MHz,数据线位宽支持4位或1位模式);
  • 集成ARM Cortex-M3 (带有片上存储器)以运行 IEEE802.11 固件(用于Wi-Fi 数据帧的处理);

AP6181 Wi-Fi 模组内部实际封装的是Broadcom 43362 芯片,接下来看看 BCM43362 芯片内部都有哪些模块(图片取自BCM43362_datasheet):
BCM43362系统框图
从上面 BCM43362 系统框图可以看出,内部是集成了ARM处理器及RAM/ROM存储空间的,用于运行 Wi-Fi 固件,管理 Wi-Fi MAC/PHY/RADIO层的无线链路信道资源,完成 Wi-Fi 数据帧 与 以太网数据帧 之间的转换等功能。

AP6181 / BCM43362 模组外接引脚如下:

  • SDIO(Secure Digital Input Output):包括DATA[0:3]、CMD、CLK 共6个引脚,支持SDIO V2.0总线标准;
  • WiFi INT / WL_HOST_WAKE:WLAN wake-up Host,当接收到数据帧后,产生中断信号,唤醒主机Host接收并处理该数据帧;
  • WL_REG_ON:Internal regulators power enable/disable,我觉得跟BCM43362芯片引脚 WL_RST_N / POR(WLAN Reset / Power-On Reset,该引脚持续拉低则BCM 43362进入Power-down状态,给该引脚一个低电平脉冲则BCM 43362进入Reset 状态) 功能相似;
  • Coexistence interface:相近频率的无线设备(比如蓝牙)通过该接口与BCM43362连接,可以共享BCM43362的无线介质(MAC/PHY/RADIO层及其后的ANT等资源),我们暂时不需要蓝牙功能,该引脚可忽略;
  • WL_BT_ANT:向外与板载丝印天线相连,向内通过 T/R Switch 与WLAN Tx/Rx 相连,AP6181把BCM43362与T/R Switch封装到一起了;
  • System Clock / XTAL:外接晶振XTAL_IN / XTAL_OUT,为AP6181或BCM43362提供系统时钟;
  • Sleep Clock / LPO:睡眠时钟或者低功耗时钟输入,Pandora开发板并未使用该引脚;
  • VBAT / VDDIO / LDO:Power supply,为AP6181或BCM43362提供电源支持。

从AP6181 模块的接线图可以看出,我们需要重点关注的是SDIO、WiFi INT、WL_REG_ON这三组共8个引脚,其余的引脚Pandora 开发板上已经帮我们接好了。SDIO引脚的定义在博客:SD/MMC + SDIO中已经有过介绍,WiFi INT引脚需要绑定自定义的中断处理函数,WL_REG_ON引脚是内部稳压电源的使能引脚,在WLAN模块正常工作时需要将其拉高,对该引脚的拉高时间有什么要求吗?我们看下AP6181 / BCM43362 WLAN模块的启动时序图:
WLAN Boot-Up Sequence
从上图可以看出,WL_REG_ON引脚需要在 VBAT / VDDIO 上电2个睡眠时钟周期(32.768KHZ)后,1.5ms之内完成电平拉高,我们可以在 AP6181 驱动代码中设置WL_REG_ON引脚的拉高时机(比如0.1ms ~ 1ms)与动作。

1.2 AP6181 驱动层级

AP6181 模块不像 ESP8266 模块那样内部集成了 WLAN驱动与TCP/IP协议栈,甚至AP6181 为了节省成本,模块内可能就没有可供存放WLAN驱动代码的ROM区域。

AP6181 模块内部有ARM处理器和RAM 内存区域,在工作时也需要运行WLAN固件程序以处理WLAN数据帧,这就需要开发者在初始化该模块时,将主控端Flash中保存的WLAN固件代码传送到AP6181 模块内(RAM内存区域)。这样做虽然增加了点主控端的驱动代码和ROM占用空间,但也有三个明显的好处:

  • 省去了大部分ROM空间,降低了模块的成本;
  • 不需要在模块出厂时单独为其烧录固件代码,减少了生产环节;
  • 固件代码便于维护和升级,只需要更新主控端Flash内的固件文件,模块初始化时自动会将新的固件代码传输到模块内。

由于Nand Flash成本比Nor Flash(可以在芯片内执行代码,而不需先拷贝到RAM内存中)更低,且在主控端Flash中更新固件代码更灵活方便,因此这种固件加载方式在设备驱动开发中很常见。

AP6181 模块是基于SDIO 总线协议进行通信的,因此模块与主控端最底层应该分别是SDIO Card controller与Host controller。SDIO Card内有一个CSA(Code Storage Area)可以用来存放WLAN固件代码(由AP6181 芯片供应商提供),SDIO Host controller上层则分别是SDIO Bus Driver和SDIO Card Driver。AP6181 模块提供的是Wi-Fi 网络访问服务,因此这里的SDIO Card Driver 也就是 WLAN Driver(由AP6181 芯片供应商提供公版驱动,开发者再根据需要调整或移植)。当 WLAN Driver 适配完成,接下来的Wi-Fi 管理与网络服务就可以交给操作系统了,Pandora 开发板上的 AP6181 模块WLAN驱动及TCP/IP协议栈的层级关系图如下:
AP6181 驱动层级图
RT-Thread为方便我们管理Wi-Fi 设备,提供了一个WLAN管理框架,相当于WLAN 设备无关层,可以向上提供统一的访问接口。当我们更换 Wi-Fi 模块时,只需要修改相应的适配代码,不需要修改上层的应用程序。

二 SDIO设备对象管理

前篇博客:SD/MMC + SDIO已经简单介绍过SDIO协议的三个部分:SDIO Host controller、SDIO Bus protocol、SDIO Card。上面已经简单介绍过本文需要用到的 SDIO Card — AP6181 模块,这里重点介绍主机端的SDIO Host controller 与 SDIO Bus protocol,包括SDIO Card Driver — WLAN Driver。

前篇博客:驱动分层与主从分离思想介绍了RT-Thread I/O 设备驱动管理的分层思想和一主多从工作模式的主从分离思想,也提到了Linux的总线设备驱动模型。SDIO驱动也可以看出明显的分层结构(只列出了SDIO相关的驱动文件,省去了MMC/SD相关的驱动文件):

// I/O 设备管理层
rt-thread-4.0.1\include\rtdef.h
rt-thread-4.0.1\src\device.c

// 设备驱动框架层
rt-thread-4.0.1\components\drivers\include\drivers\sdio.h
rt-thread-4.0.1\components\drivers\sdio\sdio.c
rt-thread-4.0.1\components\drivers\include\drivers\mmcsd_card.h
rt-thread-4.0.1\components\drivers\include\drivers\mmcsd_core.h
rt-thread-4.0.1\components\drivers\include\drivers\mmcsd_cmd.h
rt-thread-4.0.1\components\drivers\sdio\mmcsd_core.c
rt-thread-4.0.1\components\drivers\include\drivers\mmcsd_host.h

// 设备驱动层
libraries\HAL_Drivers\drv_sdio.h
libraries\HAL_Drivers\drv_sdio.c

SDIO的设备驱动框架层还可以细分为SDIO Card Layer、Core Layer、Host Layer,但这种分层没有清晰的展示出Host与Card的主从分离思想。上面并没有展示SDIO Card Driver — WLAN Driver 的相关文件,我也不知道应该将其放到设备驱动框架层还是设备驱动层。

如果按照主从分离思想看,SDIO协议可以分为主机Host 与卡设备Card,I/O Card可能支持的功能模块比较多(1个I/O Card最多可以包含7个功能模块),驱动种类与数量自然也比较多。如果我们把 I/O Card和 function device driver 看作一体,驱动代码的复用性就比较低。如果我们我们借鉴Linux的总线设备驱动模型(如下图所示),将 I/O Card和 function device driver 也分开,就可以根据需要灵活匹配 Driver 与 Function Device,不仅实现了Host — Card 的主从分离,也实现了 Function Device — Function Driver 的设备驱动分离,符合编写代码的高内聚、低耦合原则。
Linux总线设备驱动模型
一个总线Bus 分别管理一个设备链表device_list 和一个驱动链表 driver_list,当新注册一个I/O Card/Device 或 Function device Driver 时,探测已有的 driver_list 或 device_list 并尝试与自己匹配(调用Bus提供的match接口函数)。当Driver 与 Device 完成匹配后,就可以调用Driver提供的probe接口函数完成Device设备的初始化,后面就可以通过Driver提供的API 来访问Device设备,实现相应功能的扩展服务。

2.1 SDIO Bus Driver

2.1.1 Host 数据结构描述

SDIO Bus要给出Command、Response、Data三部分分别是如何描述、配置和传输的,而这三部分的传输是受主机Host 控制的,在介绍Command/Response与Data的描述结构前,先介绍下主机Host 是如何描述的:

// rt-thread-4.0.1\components\drivers\include\drivers\mmcsd_host.h

struct rt_mmcsd_host {
	struct rt_mmcsd_card *card;
	const struct rt_mmcsd_host_ops *ops;
	rt_uint32_t  freq_min;
	rt_uint32_t  freq_max;
	struct rt_mmcsd_io_cfg io_cfg;
	rt_uint32_t  valid_ocr;	/* current valid OCR */
#define VDD_165_195		(1 << 7)	/* VDD voltage 1.65 - 1.95 */
#define VDD_20_21		(1 << 8)	/* VDD voltage 2.0 ~ 2.1 */
#define VDD_21_22		(1 << 9)	/* VDD voltage 2.1 ~ 2.2 */
#define VDD_22_23		(1 << 10)	/* VDD voltage 2.2 ~ 2.3 */
#define VDD_23_24		(1 << 11)	/* VDD voltage 2.3 ~ 2.4 */
#define VDD_24_25		(1 << 12)	/* VDD voltage 2.4 ~ 2.5 */
#define VDD_25_26		(1 << 13)	/* VDD voltage 2.5 ~ 2.6 */
#define VDD_26_27		(1 << 14)	/* VDD voltage 2.6 ~ 2.7 */
#define VDD_27_28		(1 << 15)	/* VDD voltage 2.7 ~ 2.8 */
#define VDD_28_29		(1 << 16)	/* VDD voltage 2.8 ~ 2.9 */
#define VDD_29_30		(1 << 17)	/* VDD voltage 2.9 ~ 3.0 */
#define VDD_30_31		(1 << 18)	/* VDD voltage 3.0 ~ 3.1 */
#define VDD_31_32		(1 << 19)	/* VDD voltage 3.1 ~ 3.2 */
#define VDD_32_33		(1 << 20)	/* VDD voltage 3.2 ~ 3.3 */
#define VDD_33_34		(1 << 21)	/* VDD voltage 3.3 ~ 3.4 */
#define VDD_34_35		(1 << 22)	/* VDD voltage 3.4 ~ 3.5 */
#define VDD_35_36		(1 << 23)	/* VDD voltage 3.5 ~ 3.6 */
	rt_uint32_t  flags; /* define device capabilities */
#define MMCSD_BUSWIDTH_4	(1 << 0)
#define MMCSD_BUSWIDTH_8	(1 << 1)
#define MMCSD_MUTBLKWRITE	(1 << 2)
#define MMCSD_HOST_IS_SPI	(1 << 3)
#define controller_is_spi(host)	(host->flags & MMCSD_HOST_IS_SPI)
#define MMCSD_SUP_SDIO_IRQ	(1 << 4)	/* support signal pending SDIO IRQs */
#define MMCSD_SUP_HIGHSPEED	(1 << 5)	/* support high speed */

	rt_uint32_t	max_seg_size;	/* maximum size of one dma segment */
	rt_uint32_t	max_dma_segs;	/* maximum number of dma segments in one request */
	rt_uint32_t	max_blk_size;   /* maximum block size */
	rt_uint32_t	max_blk_count;  /* maximum block count */

	rt_uint32_t   spi_use_crc;
	struct rt_mutex  bus_lock;
	struct rt_semaphore  sem_ack;

	rt_uint32_t       sdio_irq_num;
	struct rt_semaphore    *sdio_irq_sem;
	struct rt_thread     *sdio_irq_thread;

	void *private_data;
};

struct rt_mmcsd_io_cfg {
	rt_uint32_t	clock;			/* clock rate */
	rt_uint16_t	vdd;			/* vdd stores the bit number of the selected voltage range from below. */

	rt_uint8_t	bus_mode;		/* command output mode */
#define MMCSD_BUSMODE_OPENDRAIN	1
#define MMCSD_BUSMODE_PUSHPULL	2

	rt_uint8_t	chip_select;		/* SPI chip select */
#define MMCSD_CS_IGNORE		0
#define MMCSD_CS_HIGH		1
#define MMCSD_CS_LOW		2

	rt_uint8_t	power_mode;		/* power supply mode */
#define MMCSD_POWER_OFF		0
#define MMCSD_POWER_UP		1
#define MMCSD_POWER_ON		2

	rt_uint8_t	bus_width;		/* data bus width */
#define MMCSD_BUS_WIDTH_1		0
#define MMCSD_BUS_WIDTH_4		2
#define MMCSD_BUS_WIDTH_8		3
};

结构体rt_mmcsd_host 描述了SDIO主机Host 应具有的属性和方法(SDIO这个名字出现较晚,很多地方仍用旧名MMC、SDMMC或MMCSD等),包括要访问的卡设备指针 *card、Host驱动层应实现的操作函数集合指针 *ops、Input/Output配置结构io_cfg(包括时钟频率、电源模式、总线模式及位宽等)、主机Host支持的有效工作电压valid_ocr、传输数据的DMA/Block配置、同步命令/响应的mutex/semaphore对象等。

最后几个成员变量是用于管理卡设备的中断处理过程的,sdio_irq_num为绑定到Host 的中断处理函数的数量,sdio_irq_sem 则用于通知线程 sdio_irq_thread 有中断被触发,sdio_irq_thread 是执行注册到Host 的中断处理函数的线程,当获得信号量sdio_irq_thread 后开始执行用户绑定的中断处理函数。

卡设备描述rt_mmcsd_card我们在下文介绍,这里先看主机Host 需要下驱动层实现并向其注册的操作函数有哪些:

// rt-thread-4.0.1\components\drivers\include\drivers\mmcsd_host.h

struct rt_mmcsd_host_ops {
	void (*request)(struct rt_mmcsd_host *host, struct rt_mmcsd_req *req);
	void (*set_iocfg)(struct rt_mmcsd_host *host, struct rt_mmcsd_io_cfg *io_cfg);
	rt_int32_t (*get_card_status)(struct rt_mmcsd_host *host);
	void (*enable_sdio_irq)(struct rt_mmcsd_host *host, rt_int32_t en);
};

操作函数集合rt_mmcsd_host_ops中包含的接口函数功能介绍如下:

  • request 方法:用于Host 向 Card 发送Command / Data Request;
  • set_iocfg 方法:用于配置Input/Output 结构io_cfg,包括时钟频率、电源模式、总线模式及位宽等;
  • get_card_status 方法:获取卡设备的状态信息,尝试探测卡设备;
  • enable_sdio_irq 方法:使能/禁用 SDIO 的中断请求功能。

2.1.2 rt_mmcsd_req 数据结构描述

接口函数request 用于Host 向 Card 发送Command / Data Request,参数rt_mmcsd_req 是如何描述的呢?

// rt-thread-4.0.1\components\drivers\include\drivers\mmcsd_core.h

struct rt_mmcsd_req {
	struct rt_mmcsd_data  *data;
	struct rt_mmcsd_cmd   *cmd;
	struct rt_mmcsd_cmd   *stop;
};

struct rt_mmcsd_data {
	rt_uint32_t  blksize;
	rt_uint32_t  blks;
	rt_uint32_t  *buf;
	rt_int32_t  err;
	rt_uint32_t  flags;
#define DATA_DIR_WRITE	(1 << 0)
#define DATA_DIR_READ	(1 << 1)
#define DATA_STREAM	(1 << 2)

	unsigned int		bytes_xfered;

	struct rt_mmcsd_cmd	*stop;		/* stop command */
	struct rt_mmcsd_req	*mrq;		/* associated request */

	rt_uint32_t  timeout_ns;
	rt_uint32_t  timeout_clks;
};

struct rt_mmcsd_cmd {
	rt_uint32_t  cmd_code;
	rt_uint32_t  arg;
	rt_uint32_t  resp[4];
	rt_uint32_t  flags;
/*rsponse types 
 *bits:0~3
 */
#define RESP_MASK	(0xF)
#define RESP_NONE	(0)
#define RESP_R1		(1 << 0)
#define RESP_R1B	(2 << 0)
#define RESP_R2		(3 << 0)
#define RESP_R3		(4 << 0)
#define RESP_R4		(5 << 0)
#define RESP_R6		(6 << 0)
#define RESP_R7		(7 << 0)
#define RESP_R5		(8 << 0)	/*SDIO command response type*/
/*command types 
 *bits:4~5
 */
#define CMD_MASK	(3 << 4)		/* command type */
#define CMD_AC		(0 << 4)
#define CMD_ADTC	(1 << 4)
#define CMD_BC		(2 << 4)
#define CMD_BCR		(3 << 4)

#define resp_type(cmd)	((cmd)->flags & RESP_MASK)

/*spi rsponse types 
 *bits:6~8
 */
#define RESP_SPI_MASK	(0x7 << 6)
#define RESP_SPI_R1	(1 << 6)
#define RESP_SPI_R1B	(2 << 6)
#define RESP_SPI_R2	(3 << 6)
#define RESP_SPI_R3	(4 << 6)
#define RESP_SPI_R4	(5 << 6)
#define RESP_SPI_R5	(6 << 6)
#define RESP_SPI_R7	(7 << 6)

#define spi_resp_type(cmd)	((cmd)->flags & RESP_SPI_MASK)
/*
 * These are the command types.
 */
#define cmd_type(cmd)	((cmd)->flags & CMD_MASK)
	
	rt_int32_t  retries;	/* max number of retries */
	rt_int32_t  err;

	struct rt_mmcsd_data *data;
	struct rt_mmcsd_req	*mrq;		/* associated request */
};

结构体rt_mmcsd_req包含了数据指针 *data、命令指针 *cmd、停止命令指针 *stop三部分,SDIO总线上传输的主要就是Data、Command/Response两部分,附加一个停止命令方便判断数据流或多个数据块的传输结束。

结构体rt_mmcsd_data包括了数据块大小与数量、缓冲区首地址与字节数、读写标志(包括数据流还是数据块的标志)、停止传输命令、与该数据对象相关的请求、超时等信息。

结构体rt_mmcsd_cmd包括了命令编码(比如CMD5)、命令参数、响应数据、命令类型与响应类型标志(命令/响应在博客:SD/MMC + SDIO中有详细介绍)、允许该命令尝试发送的最大次数、与该命令对象相关的数据、与该命令对象相关的请求等信息。

2.1.3 SDIO Bus 接口函数及初始化过程

SDIO Bus有了上面主机Host 和请求 Request 的数据结构描述,再加上 Host 驱动层提供的方法集合 rt_mmcsd_host_ops (下文介绍Host 驱动层时,会介绍其如何实现并注册给 Host 的),就可以对外提供一些访问接口,方便开发者配置管理这些结构体,并通过 SDIO Bus 访问 Card device。SDIO Core Layer为我们提供的访问接口如下:

// rt-thread-4.0.1\components\drivers\include\drivers\mmcsd_core.h

int mmcsd_wait_cd_changed(rt_int32_t timeout);
void mmcsd_host_lock(struct rt_mmcsd_host *host);
void mmcsd_host_unlock(struct rt_mmcsd_host *host);
void mmcsd_req_complete(struct rt_mmcsd_host *host);
void mmcsd_send_request(struct rt_mmcsd_host *host, struct rt_mmcsd_req *req);
rt_int32_t mmcsd_send_cmd(struct rt_mmcsd_host *host, struct rt_mmcsd_cmd *cmd, int retries);
rt_int32_t mmcsd_go_idle(struct rt_mmcsd_host *host);
rt_int32_t mmcsd_spi_read_ocr(struct rt_mmcsd_host *host, rt_int32_t high_capacity, rt_uint32_t *ocr);
rt_int32_t mmcsd_all_get_cid(struct rt_mmcsd_host *host, rt_uint32_t *cid);
rt_int32_t mmcsd_get_cid(struct rt_mmcsd_host *host, rt_uint32_t *cid);
rt_int32_t mmcsd_get_csd(struct rt_mmcsd_card *card, rt_uint32_t *csd);
rt_int32_t mmcsd_select_card(struct rt_mmcsd_card *card);
rt_int32_t mmcsd_deselect_cards(struct rt_mmcsd_card *host);
rt_int32_t mmcsd_spi_use_crc(struct rt_mmcsd_host *host, rt_int32_t use_crc);
void mmcsd_set_chip_select(struct rt_mmcsd_host *host, rt_int32_t mode);
void mmcsd_set_clock(struct rt_mmcsd_host *host, rt_uint32_t clk);
void mmcsd_set_bus_mode(struct rt_mmcsd_host *host, rt_uint32_t mode);
void mmcsd_set_bus_width(struct rt_mmcsd_host *host, rt_uint32_t width);
void mmcsd_set_data_timeout(struct rt_mmcsd_data *data, const struct rt_mmcsd_card *card);
rt_uint32_t mmcsd_select_voltage(struct rt_mmcsd_host *host, rt_uint32_t ocr);
void mmcsd_change(struct rt_mmcsd_host *host);
void mmcsd_detect(void *param);
struct rt_mmcsd_host *mmcsd_alloc_host(void);
void mmcsd_free_host(struct rt_mmcsd_host *host);
int rt_mmcsd_core_init(void);

// SD memory card API
int rt_mmcsd_blk_init(void);
rt_int32_t rt_mmcsd_blk_probe(struct rt_mmcsd_card *card);
void rt_mmcsd_blk_remove(struct rt_mmcsd_card *card);

SDIO Core Layer提供的访问接口及涉及到的结构体描述就构成了SDIO Bus Driver 部分,这里我们重点关注SDIO Card的SD 4-bit通信模式,先忽略跟SPI 模式和SD memory Card相关的接口函数与涉及到的结构体变量。

先从SDIO Core Layer 的初始化过程开始看:

// rt-thread-4.0.1\components\drivers\sdio\mmcsd_core.c\

int rt_mmcsd_core_init(void)
{
    rt_err_t ret;

    /* initialize detect SD cart thread */
    /* initialize mailbox and create detect SD card thread */
    ret = rt_mb_init(&mmcsd_detect_mb, "mmcsdmb",
        &mmcsd_detect_mb_pool[0], sizeof(mmcsd_detect_mb_pool) / sizeof(mmcsd_detect_mb_pool[0]),
        RT_IPC_FLAG_FIFO);
    RT_ASSERT(ret == RT_EOK);

   ret = rt_mb_init(&mmcsd_hotpluge_mb, "mmcsdhotplugmb",
        &mmcsd_hotpluge_mb_pool[0], sizeof(mmcsd_hotpluge_mb_pool) / sizeof(mmcsd_hotpluge_mb_pool[0]),
        RT_IPC_FLAG_FIFO);
    RT_ASSERT(ret == RT_EOK);
     ret = rt_thread_init(&mmcsd_detect_thread, "mmcsd_detect", mmcsd_detect, RT_NULL, 
                 &mmcsd_stack[0], RT_MMCSD_STACK_SIZE, RT_MMCSD_THREAD_PREORITY, 20);
    if (ret == RT_EOK) 
    {
        rt_thread_startup(&mmcsd_detect_thread);
    }

    rt_sdio_init();

	return 0;
}
INIT_PREV_EXPORT(rt_mmcsd_core_init);

函数rt_mmcsd_core_init 会被自动初始化组件自动调用,既然不必自己调用,我们看看在初始化过程中都做了什么。首先初始化了一个SDIO Card探测邮箱mmcsd_detect_mb和一个探测线程mmcsd_detect_thread,当探测到SDIO Card新增时,会调用相应的初始化函数完成新增SDIO Card的初始化。

SDIO Card是支持热插拔的,函数rt_mmcsd_core_init 也为热插拔功能初始化了一个邮箱mmcsd_hotpluge_mb ,通过定时轮询或中断触发方式检测到 SDIO Card 发生了热插拔事件,则会通过该邮箱通知主机Host 做出相应的处理。

接下来看SDIO Card探测线程都做了什么?

// rt-thread-4.0.1\components\drivers\sdio\mmcsd_core.c

void mmcsd_detect(void *param)
{
    struct rt_mmcsd_host *host;
    rt_uint32_t  ocr;
    rt_int32_t  err;

    while (1) 
    {
        if (rt_mb_recv(&mmcsd_detect_mb, (rt_ubase_t *)&host, RT_WAITING_FOREVER) == RT_EOK)
        {
            if (host->card == RT_NULL)
            {
                mmcsd_host_lock(host);
                mmcsd_power_up(host);
                mmcsd_go_idle(host);

                mmcsd_send_if_cond(host, host->valid_ocr);

                err = sdio_io_send_op_cond(host, 0, &ocr);
                if (!err)
                {
                    if (init_sdio(host, ocr))
                        mmcsd_power_off(host);
                    mmcsd_host_unlock(host);
                    continue;
                }
                /* detect SD card */
                ......
                /* detect mmc card */
                ......
                mmcsd_host_unlock(host);
            }
            else
            {
            	/* card removed */
            	......
            	rt_mb_send(&mmcsd_hotpluge_mb, (rt_uint32_t)host);
            }
        }
    }
}

void mmcsd_change(struct rt_mmcsd_host *host)
{
    rt_mb_send(&mmcsd_detect_mb, (rt_uint32_t)host);
}

SDIO Card探测线程内部是一个死循环,等待接收来自探测邮箱mmcsd_detect_mb的消息,当该邮箱接收到消息后会调用SDIO Card的初始化函数,完成卡设备的初始化,卡设备的初始化流程已经在博客:SD/MMC + SDIO)中详细介绍过了。向探测邮箱mmcsd_detect_mb发送消息的函数是mmcsd_change,该函数应由开发者在新增或移除SDIO Card时调用,以便线程mmcsd_detect 能及时处理SDIO Card 的新增或移除。

如果读者熟悉其它的组件初始化函数,不难发现函数 rt_mmcsd_core_init 中并没有为主机Host 初始化一个对象,倒是提供了一个接口函数 mmcsd_alloc_host,从名字就能看出,该函数为我们创建并初始化一个 Host 对象,包括释放 Host 对象的接口函数 mmcsd_free_host,这两个函数需要我们在 Host 驱动层中主动调用。

从前面Linux总线设备驱动框架可以看出,SDIO Bus Driver 还应提供 match 方法,用来匹配 device 与 driver。RT-Thread将 match 方法放到了 SDIO Card device端,我们也稍后在下文介绍 match 方法。

2.2 SDIO Card Device & Driver

2.2.1 SDIO Card 数据结构描述

我们先从卡设备的数据结构描述开始入手:

// rt-thread-4.0.1\components\drivers\include\drivers\mmcsd_card.h

struct rt_mmcsd_card {
	struct rt_mmcsd_host *host;
	rt_uint32_t	rca;		/* card addr */
	rt_uint32_t	resp_cid[4];	/* card CID register */
	rt_uint32_t	resp_csd[4];	/* card CSD register */
	rt_uint32_t	resp_scr[2];	/* card SCR register */

	rt_uint16_t	tacc_clks;	/* data access time by ns */
	rt_uint32_t	tacc_ns;	/* data access time by clk cycles */
	rt_uint32_t	max_data_rate;	/* max data transfer rate */
	rt_uint32_t	card_capacity;	/* card capacity, unit:KB */
	rt_uint32_t	card_blksize;	/* card block size */
	rt_uint32_t	erase_size;	/* erase size in sectors */
	rt_uint16_t	card_type;
#define CARD_TYPE_MMC                   0 /* MMC card */
#define CARD_TYPE_SD                    1 /* SD card */
#define CARD_TYPE_SDIO                  2 /* SDIO card */
#define CARD_TYPE_SDIO_COMBO            3 /* SD combo (IO+mem) card */

	rt_uint16_t flags;
#define CARD_FLAG_HIGHSPEED  (1 << 0)   /* SDIO bus speed 50MHz */
#define CARD_FLAG_SDHC       (1 << 1)   /* SDHC card */
#define CARD_FLAG_SDXC       (1 << 2)   /* SDXC card */

	struct rt_sd_scr	scr;
	struct rt_mmcsd_csd	csd;
	rt_uint32_t     hs_max_data_rate;  /* max data transfer rate in high speed mode */

	rt_uint8_t      sdio_function_num;	/* totol number of SDIO functions */
	struct rt_sdio_cccr    cccr;  /* common card info */
	struct rt_sdio_cis     cis;  /* common tuple info */
	struct rt_sdio_function	*sdio_function[SDIO_MAX_FUNCTIONS + 1]; /* SDIO functions (devices) */
};

#define SDIO_MAX_FUNCTIONS		7

结构体rt_mmcsd_card 则包含了对应主机Host 对象的指针、卡设备的RCA/CID/CSD/SCR寄存器的值、数据访问时间和传输速率、卡容量/块大小/卡类型、卡速度与容量标志、高速模式下的最大数据传输速率、SDIO Card支持的功能设备数量及对象指针、CCCR(Card Common Control Registers) / CIS(Card Information Structure)等(卡设备的寄存器信息参考博客:SD/MMC + SDIO)。

一个 I/O Card 最多可以有 7 个 function device,比如我们常见的 Wi-Fi / BT二合一模块就是一个卡上有两个功能设备。对于 I/O Card 来说,我们通过 SDIO Bus 与卡设备通信,实际访问的是卡设备内包含的 Function device(SD memory card没有function device)。从具有多种 I/O 功能的SDIO卡内部CIA(Common I/O Area)映射图可以看出,CCCR卡通用控制寄存器相当于function device 0,所以sdio function devices数组成员数量为(SDIO_MAX_FUNCTIONS + 1)。

SD memory相关的比如CID/CSD/SCR寄存器不是本文关注点,我们重点关注跟 I/O Card相关的比如 rt_sdio_function 和 CCCR/CIS 等结构体的代码描述如下:

// rt-thread-4.0.1\components\drivers\include\drivers\mmcsd_card.h

/* SDIO function devices */
struct rt_sdio_function {
	struct rt_mmcsd_card		*card;		/* the card this device belongs to */
	rt_sdio_irq_handler_t	*irq_handler;	/* IRQ callback */
	rt_uint8_t		num;		/* function number */

	rt_uint8_t		func_code;   /*  Standard SDIO Function interface code  */
	rt_uint16_t		manufacturer;		/* manufacturer id */
	rt_uint16_t		product;		/* product id */

	rt_uint32_t		max_blk_size;	/* maximum block size */
	rt_uint32_t		cur_blk_size;	/* current block size */

	rt_uint32_t		enable_timeout_val; /* max enable timeout in msec */

	struct rt_sdio_function_tuple *tuples;
    
    void            *priv;
};

typedef void (rt_sdio_irq_handler_t)(struct rt_sdio_function *);

/* SDIO function CIS tuple (unknown to the core) */
struct rt_sdio_function_tuple {
	struct rt_sdio_function_tuple *next;
	rt_uint8_t code;
	rt_uint8_t size;
	rt_uint8_t *data;
};

struct rt_sdio_cccr {
	rt_uint8_t		sdio_version;
	rt_uint8_t		sd_version;
	rt_uint8_t		direct_cmd:1,     /*  Card Supports Direct Commands during data transfer
	                                               only SD mode, not used for SPI mode */
				multi_block:1,    /*  Card Supports Multi-Block */
				read_wait:1,      /*  Card Supports Read Wait
				                       only SD mode, not used for SPI mode */
				suspend_resume:1, /*  Card supports Suspend/Resume
				                       only SD mode, not used for SPI mode */
				s4mi:1,            /* generate interrupts during a 4-bit 
				                      multi-block data transfer */
				e4mi:1,            /*  Enable the multi-block IRQ during 
				                       4-bit transfer for the SDIO card */
				low_speed:1,      /*  Card  is  a  Low-Speed  card */
				low_speed_4:1;    /*  4-bit support for Low-Speed cards */

	rt_uint8_t		bus_width:1,     /* Support SDIO bus width, 1:4bit, 0:1bit */
				cd_disable:1,    /*  Connect[0]/Disconnect[1] the 10K-90K ohm pull-up 
				                     resistor on CD/DAT[3] (pin 1) of the card */
				power_ctrl:1,    /* Support Master Power Control */
				high_speed:1;    /* Support High-Speed  */
				
				
};

struct rt_sdio_cis {
	rt_uint16_t		manufacturer;
	rt_uint16_t		product;
	rt_uint16_t		func0_blk_size;
	rt_uint32_t		max_tran_speed;
};

SDIO卡的每个功能设备rt_sdio_function都包含所在卡设备的指针、中断回调函数指针(通过函数 sdio_attach_irq 绑定,在线程 host->sdio_irq_thread 中被调用)、功能设备数量、功能接口码、制造商ID与产品ID、最大块大小与当前块大小、功能元组等。结构体rt_sdio_function_tuple 被组织成一条单向链表,也即一个卡设备上的多个功能设备按链表形式组织管理。

结构体rt_sdio_function 中的功能编号、制造商ID与产品ID三个成员信息实际上是 function device 与 function driver 之间相互匹配的依据,二者这三个成员信息一致的话,说明是Device与Driver是相互匹配的。

2.2.2 SDIO Driver 数据结构描述

前面的Linux总线设备驱动模型提到,SDIO Driver至少应提供一个 probe 接口,用于探测并初始化卡设备,我们看看RT-Thread 为SDIO 提供的驱动接口有哪些:

// rt-thread-4.0.1\components\drivers\include\drivers\sdio.h

struct rt_sdio_driver
{
    char *name;
    rt_int32_t (*probe)(struct rt_mmcsd_card *card);
    rt_int32_t (*remove)(struct rt_mmcsd_card *card);
    struct rt_sdio_device_id *id;
};

struct rt_sdio_device_id
{
    rt_uint8_t   func_code;
    rt_uint16_t  manufacturer;
    rt_uint16_t  product;
};

结构体rt_sdio_driver 包含的接口函数只有两个:probe 用于探测并初始化卡设备;remove 用于移除并释放卡设备资源,这两个接口函数需要function device驱动(本文中指的是AP6181 SDIO设备的WLAN驱动) 实现并注册。除了两个就接口函数指针,rt_sdio_driver 还包括驱动名和设备ID,驱动名便于标识驱动,设备ID 则是为了方便匹配要驱动的设备。

2.2.3 SDIO Card接口函数及初始化过程

SD I/O Card 重点是 Input/Output,前面有了Core Layer 提供的接口函数做基础,SDIO Card在这些接口函数基础上根据自己的规范再进行封装处理,为我们更方便的访问 I/O function device 提供的新的接口函数如下:

// rt-thread-4.0.1\components\drivers\include\drivers\sdio.h

rt_int32_t sdio_io_send_op_cond(struct rt_mmcsd_host *host,
                                rt_uint32_t           ocr,
                                rt_uint32_t          *cmd5_resp);
rt_int32_t sdio_io_rw_direct(struct rt_mmcsd_card *card,
                             rt_int32_t            rw,
                             rt_uint32_t           fn,
                             rt_uint32_t           reg_addr,
                             rt_uint8_t           *pdata,
                             rt_uint8_t            raw);
rt_int32_t sdio_io_rw_extended(struct rt_mmcsd_card *card,
                               rt_int32_t            rw,
                               rt_uint32_t           fn,
                               rt_uint32_t           addr,
                               rt_int32_t            op_code,
                               rt_uint8_t           *buf,
                               rt_uint32_t           blocks,
                               rt_uint32_t           blksize);
rt_int32_t sdio_io_rw_extended_block(struct rt_sdio_function *func,
                              rt_int32_t               rw,
                              rt_uint32_t              addr,
                              rt_int32_t               op_code,
                              rt_uint8_t              *buf,
                              rt_uint32_t              len);
rt_uint8_t sdio_io_readb(struct rt_sdio_function *func, 
                         rt_uint32_t              reg,
                         rt_int32_t              *err);
rt_int32_t sdio_io_writeb(struct rt_sdio_function *func, 
                          rt_uint32_t              reg,
                          rt_uint8_t               data);
rt_uint16_t sdio_io_readw(struct rt_sdio_function *func,
                          rt_uint32_t              addr,
                          rt_int32_t              *err);
rt_int32_t sdio_io_writew(struct rt_sdio_function *func,
                          rt_uint16_t              data,
                          rt_uint32_t              addr);
rt_uint32_t sdio_io_readl(struct rt_sdio_function *func,
                          rt_uint32_t              addr,
                          rt_int32_t              *err);
rt_int32_t sdio_io_writel(struct rt_sdio_function *func,
                          rt_uint32_t              data,
                          rt_uint32_t              addr);
rt_int32_t sdio_io_read_multi_fifo_b(struct rt_sdio_function *func, 
                                     rt_uint32_t              addr,
                                     rt_uint8_t              *buf,
                                     rt_uint32_t              len);
rt_int32_t sdio_io_write_multi_fifo_b(struct rt_sdio_function *func, 
                                      rt_uint32_t              addr,
                                      rt_uint8_t              *buf,
                                      rt_uint32_t              len);
rt_int32_t sdio_io_read_multi_incr_b(struct rt_sdio_function *func, 
                                     rt_uint32_t              addr,
                                     rt_uint8_t              *buf,
                                     rt_uint32_t              len);
rt_int32_t sdio_io_write_multi_incr_b(struct rt_sdio_function *func, 
                                      rt_uint32_t              addr,
                                      rt_uint8_t              *buf,
                                      rt_uint32_t              len); 
rt_int32_t init_sdio(struct rt_mmcsd_host *host, rt_uint32_t ocr);
rt_int32_t sdio_attach_irq(struct rt_sdio_function *func,
                           rt_sdio_irq_handler_t   *handler);
rt_int32_t sdio_detach_irq(struct rt_sdio_function *func);
void sdio_irq_wakeup(struct rt_mmcsd_host *host);
rt_int32_t sdio_enable_func(struct rt_sdio_function *func);
rt_int32_t sdio_disable_func(struct rt_sdio_function *func);
void sdio_set_drvdata(struct rt_sdio_function *func, void *data);
void* sdio_get_drvdata(struct rt_sdio_function *func);
rt_int32_t sdio_set_block_size(struct rt_sdio_function *func,
                               rt_uint32_t              blksize);
rt_int32_t sdio_register_driver(struct rt_sdio_driver *driver);
rt_int32_t sdio_unregister_driver(struct rt_sdio_driver *driver);
void rt_sdio_init(void);

前面大部分都是跟 I/O 相关的,我们依然从SDIO的初始化过程开始看:

// rt-thread-4.0.1\components\drivers\sdio\sdio.c

rt_int32_t init_sdio(struct rt_mmcsd_host *host, rt_uint32_t ocr)
{
    rt_int32_t err;
    rt_uint32_t  current_ocr;
	......
    current_ocr = mmcsd_select_voltage(host, ocr);
    ......
    err = sdio_init_card(host, current_ocr);
    if (err)
        goto remove_card;

    return 0;

remove_card:
    rt_free(host->card);
    host->card = RT_NULL;
err:
    LOG_E("init SDIO card failed");
    return err;
}

SDIO设备的初始化过程在博客:SD/MMC + SDIO中以流程图的形式介绍过了,这里是代码实现过程。

SDIO初始化函数init_sdio 被前面介绍的SDIO 探测线程mmcsd_detect 调用,在函数 init_sdio 被调用之前,先执行了CMD0或CMD52 I/O Reset命令(函数mmcsd_go_idle)、CMD8命令(函数mmcsd_send_if_cond)和CMD5命令(函数sdio_io_send_op_cond),Host 与 Card 协商出有效的工作电压,在函数 init_sdio 中选择前面协商出的有效工作电压,开始执行 SDIO 卡设备初始化过程(函数 sdio_init_card)。

// rt-thread-4.0.1\components\drivers\sdio\sdio.c

static rt_int32_t sdio_init_card(struct rt_mmcsd_host *host, rt_uint32_t ocr)
{
    rt_int32_t err = 0;
    rt_int32_t i, function_num;
    rt_uint32_t  cmd5_resp;
    struct rt_mmcsd_card *card;

    err = sdio_io_send_op_cond(host, ocr, &cmd5_resp);
    ......
    if (controller_is_spi(host)) 
    ......
    function_num = (cmd5_resp & 0x70000000) >> 28;
    card = rt_malloc(sizeof(struct rt_mmcsd_card));
    ......
    rt_memset(card, 0, sizeof(struct rt_mmcsd_card));

    card->card_type = CARD_TYPE_SDIO;
    card->sdio_function_num = function_num;
    card->host = host;
    host->card = card;

    card->sdio_function[0] = rt_malloc(sizeof(struct rt_sdio_function));
    if (!card->sdio_function[0])
    {
        LOG_E("malloc sdio_func0 failed");
        err = -RT_ENOMEM;
        goto err1;
    }
    rt_memset(card->sdio_function[0], 0, sizeof(struct rt_sdio_function));
    card->sdio_function[0]->card = card;
    card->sdio_function[0]->num = 0;

    if (!controller_is_spi(host)) 
    {
        err = mmcsd_get_card_addr(host, &card->rca);
        if (err)
            goto err2;
        mmcsd_set_bus_mode(host, MMCSD_BUSMODE_PUSHPULL);
    }

    if (!controller_is_spi(host)) 
    {
        err = mmcsd_select_card(card);
        if (err)
            goto err2;
    }

    err = sdio_read_cccr(card);
    if (err)
        goto err2;

    err = sdio_read_cis(card->sdio_function[0]);
    if (err)
        goto err2;

    err = sdio_set_highspeed(card);
    if (err)
        goto err2;

    if (card->flags & CARD_FLAG_HIGHSPEED) 
        mmcsd_set_clock(host, 50000000); 
    else 
        mmcsd_set_clock(host, card->cis.max_tran_speed);

    err = sdio_set_bus_wide(card);
    if (err)
        goto err2;

    for (i = 1; i < function_num + 1; i++) 
    {
        err = sdio_initialize_function(card, i);
        if (err)
            goto err3;
    }

    /* register sdio card */
    err = sdio_register_card(card);
    if (err)
        goto err3;

    return 0;

err3:
    if (host->card)
    {
        for (i = 1; i < host->card->sdio_function_num + 1; i++)
        {
            if (host->card->sdio_function[i])
            {
                sdio_free_cis(host->card->sdio_function[i]);
                rt_free(host->card->sdio_function[i]);
                host->card->sdio_function[i] = RT_NULL;
                rt_free(host->card);
                host->card = RT_NULL;
                break;
            }
        }
    }
err2:
    if (host->card && host->card->sdio_function[0])
    {
        sdio_free_cis(host->card->sdio_function[0]);
        rt_free(host->card->sdio_function[0]);
        host->card->sdio_function[0] = RT_NULL;
    }
err1:
    if (host->card)
        rt_free(host->card);
err:
    LOG_E("error %d while initialising SDIO card", err);    
    return err;
}

SDIO卡设备初始化函数 sdio_init_card 中,完成结构体 rt_mmcsd_card 成员变量的初始化配置,发送CMD3命令(函数mmcsd_get_card_addr)获得卡设备的RCA(Relative Card Address),发送CMD7命令(函数mmcsd_select_card)选中卡设备,读取SDIO Card的CCCR与CIS信息,配置时钟频率与总线位宽,开始执行 SDIO function device 初始化过程(函数sdio_initialize_function)。

// rt-thread-4.0.1\components\drivers\sdio\sdio.c

static rt_int32_t sdio_initialize_function(struct rt_mmcsd_card *card,
                                           rt_uint32_t           func_num)
{
    rt_int32_t ret;
    struct rt_sdio_function *func;
    RT_ASSERT(func_num <= SDIO_MAX_FUNCTIONS);

    func = rt_malloc(sizeof(struct rt_sdio_function));
    if (!func)
    {
        LOG_E("malloc rt_sdio_function failed");
        ret = -RT_ENOMEM;
        goto err;
    }
    rt_memset(func, 0, sizeof(struct rt_sdio_function));

    func->card = card;
    func->num = func_num;

    ret = sdio_read_fbr(func);
    if (ret)
        goto err1;

    ret = sdio_read_cis(func);
    if (ret)
        goto err1;

    card->sdio_function[func_num] = func;

    return 0;

err1:
    sdio_free_cis(func);
    rt_free(func);
    card->sdio_function[func_num] = RT_NULL;
err:
    return ret;
}

SDIO function device初始化过程主要是完成每个功能结构体 rt_sdio_function 成员变量的初始化配置,然后读取 SDIO function device的FBR(Function Basic Registers)和CIS信息。

完成了SDIO Card 与function device 的初始化过程,接下来看card device 与 driver 的注册和匹配过程。

2.2.4 SDIO Card与Driver的注册和匹配过程

SDIO Card 初始化函数 sdio_init_card 在完成 function device 的初始化后,开始执行卡设备的注册过程,该过程的实现代码如下:

// rt-thread-4.0.1\components\drivers\sdio\sdio.c

static rt_int32_t sdio_register_card(struct rt_mmcsd_card *card)
{
    struct sdio_card *sc;
    struct sdio_driver *sd;
    rt_list_t *l;

    sc = rt_malloc(sizeof(struct sdio_card));
    ......
    sc->card = card;
    rt_list_insert_after(&sdio_cards, &sc->list);

    if (rt_list_isempty(&sdio_drivers))
        goto out;

    for (l = (&sdio_drivers)->next; l != &sdio_drivers; l = l->next)
    {
        sd = (struct sdio_driver *)rt_list_entry(l, struct sdio_driver, list);
        if (sdio_match_card(card, sd->drv->id))
            sd->drv->probe(card);
    }

out:
    return 0;
}

static rt_list_t sdio_cards = RT_LIST_OBJECT_INIT(sdio_cards);
static rt_list_t sdio_drivers = RT_LIST_OBJECT_INIT(sdio_drivers);

struct sdio_card
{
    struct rt_mmcsd_card *card;
    rt_list_t  list;
};

struct sdio_driver
{
    struct rt_sdio_driver *drv;
    rt_list_t  list;
};

SDIO Card 注册过程实际上是将新增的卡设备对象 sdio_card 插入到RT-Thread管理的一个链表中,并没有将其注册到 I/O 设备管理层,因此通过命令 list_device 是看不到这里注册的卡设备的。

前面Linux总线设备驱动框架中提到,当有Device或Driver新注册时,会调用Driver 提供的 match 方法,尝试完成 Device 与 Driver 的匹配,接下来看 Device 与 Driver 的匹配过程是如何实现的:

// rt-thread-4.0.1\components\drivers\sdio\sdio.c

rt_inline rt_int32_t sdio_match_card(struct rt_mmcsd_card           *card,
                                     const struct rt_sdio_device_id *id)
{
    rt_uint8_t num = 1;
    
    if ((id->manufacturer != SDIO_ANY_MAN_ID) && 
        (id->manufacturer != card->cis.manufacturer))
        return 0;
    
    while (num <= card->sdio_function_num)
    {
        if ((id->product != SDIO_ANY_PROD_ID) && 
            (id->product == card->sdio_function[num]->product))
            return 1;
        num++;
    }

    return 0;
}


static struct rt_mmcsd_card *sdio_match_driver(struct rt_sdio_device_id *id)
{
    rt_list_t *l;
    struct sdio_card *sc;
    struct rt_mmcsd_card *card;

    for (l = (&sdio_cards)->next; l != &sdio_cards; l = l->next)
    {
        sc = (struct sdio_card *)rt_list_entry(l, struct sdio_card, list);
        card = sc->card;

        if (sdio_match_card(card, id))
            return card;
    }

    return RT_NULL;
}

从上面的两个match函数代码可以看出,Device与Driver的匹配是通过 rt_sdio_device_id 比对实现的。当有新的卡设备注册时,会遍历驱动链表,看是否有能与新注册的卡设备相匹配的驱动。当有新驱动注册时,则会遍历卡设备链表,看是否有能与新注册的驱动相匹配的功能设备。待Device 与 Driver 完成匹配后,会调用Driver 提供的 probe 接口函数,完成Device 的探测和初始化。

知道了卡设备的注册实现过程,应该能想到驱动的注册实现过程:

// rt-thread-4.0.1\components\drivers\sdio\sdio.c

rt_int32_t sdio_register_driver(struct rt_sdio_driver *driver)
{
    struct sdio_driver *sd;
    struct rt_mmcsd_card *card;

    sd = rt_malloc(sizeof(struct sdio_driver));
    if (sd == RT_NULL)
    {
        LOG_E("malloc sdio driver failed");
        return -RT_ENOMEM;
    }

    sd->drv = driver;
    rt_list_insert_after(&sdio_drivers, &sd->list);

    if (!rt_list_isempty(&sdio_cards))
    {
        card = sdio_match_driver(driver->id);
        if (card != RT_NULL)
            return driver->probe(card);
    }

    return -RT_EEMPTY;
}

驱动 rt_sdio_driver 的注册,包括要求的两个接口函数 probe和remove 的实现,都需要功能设备的驱动代码完成了,本文中指的是AP6181 Wi-Fi 驱动代码。但Pandora开发板提供的源码中并没有AP6181 WLAN驱动的源码,而是以库文件的形式提供的,我们没法看到 AP6181 WLAN驱动对接口函数 probe和remove 的实现及注册过程了。

2.3 SDIO Host Driver Layer

SDIO Host controller 驱动层是在前面介绍的SDIO Core Layer的下层,该层负责实现SDIO Bus Driver 需要的操作函数集合 rt_mmcsd_host_ops,并将其注册到 SDIO Core Layer 用来支持Core Layer API 的实现。

2.3.1 SDIO硬件设备数据结构描述

在介绍该层实现并注册接口函数 rt_mmcsd_host_ops 之前,先看看SDIO Host controller 驱动层是如何描述SDIO 设备的:

// libraries\HAL_Drivers\drv_sdio.c

struct rthw_sdio
{
    struct rt_mmcsd_host *host;
    struct stm32_sdio_des sdio_des;
    struct rt_event event;
    struct rt_mutex mutex;
    struct sdio_pkg *pkg;
};

struct sdio_pkg
{
    struct rt_mmcsd_cmd *cmd;
    void *buff;
    rt_uint32_t flag;
};

// libraries\HAL_Drivers\drv_sdio.h

struct stm32_sdio_des
{
    struct stm32_sdio *hw_sdio;
    dma_txconfig txconfig;
    dma_rxconfig rxconfig;
    sdio_clk_get clk_get;
};

typedef rt_err_t (*dma_txconfig)(rt_uint32_t *src, rt_uint32_t *dst, int size);
typedef rt_err_t (*dma_rxconfig)(rt_uint32_t *src, rt_uint32_t *dst, int size);
typedef rt_uint32_t (*sdio_clk_get)(struct stm32_sdio *hw_sdio);

struct stm32_sdio
{
    volatile rt_uint32_t power;
    volatile rt_uint32_t clkcr;
    volatile rt_uint32_t arg;
    volatile rt_uint32_t cmd;
    ......
    volatile rt_uint32_t fifo;
};

结构体rthw_sdio 包含了主机Host 对象的指针 *host、SDIO设备描述结构体 sdio_des(包括SDIO Host controller 寄存器、DMA Tx/Rx 配置函数指针、获取SDIO时钟频率函数指针等成员)、用于同步命令/响应的事件集与互斥量对象、SDIO数据包结构体指针 *pkg(包含SDIO 命令结构体指针 *cmd、传输数据缓冲区首地址 *buff 及其必要的标识位信息)等成员。结构体 stm32_sdio 成员既然要储存SDIO Host controller 寄存器的值,就需要使用 volatile 关键字来修饰,实际内容等同于 SDCARD_INSTANCE_TYPE。

2.3.2 SDIO硬件驱动初始化过程

接下来看SDIO Host controller 驱动层是如何实现并注册函数集 rt_mmcsd_host_ops 的,先从该层的初始化过程看起:

// libraries\HAL_Drivers\drv_sdio.c

int rt_hw_sdio_init(void)
{
    struct stm32_sdio_des sdio_des;
    SD_HandleTypeDef hsd;
    hsd.Instance = SDCARD_INSTANCE;
    {
        rt_uint32_t tmpreg = 0x00U;
		......
		/* enable DMA clock && Delay after an RCC peripheral clock enabling*/
        SET_BIT(RCC->AHB1ENR, sdio_config.dma_rx.dma_rcc);
        tmpreg = READ_BIT(RCC->AHB1ENR, sdio_config.dma_rx.dma_rcc);
        UNUSED(tmpreg); /* To avoid compiler warnings */
    }
    HAL_NVIC_SetPriority(SDIO_IRQn, 2, 0);
    HAL_NVIC_EnableIRQ(SDIO_IRQn);
    HAL_SD_MspInit(&hsd);

    sdio_des.clk_get = stm32_sdio_clock_get;
    sdio_des.hw_sdio = (struct stm32_sdio *)SDCARD_INSTANCE;
    sdio_des.rxconfig = DMA_RxConfig;
    sdio_des.txconfig = DMA_TxConfig;

    host = sdio_host_create(&sdio_des);
    if (host == RT_NULL)
    {
        LOG_E("host create fail");
        return -1;
    }

    return 0;
}
INIT_DEVICE_EXPORT(rt_hw_sdio_init);

/**
  * @brief  This function get stm32 sdio clock.
  * @param  hw_sdio: stm32_sdio
  * @retval PCLK2Freq
  */
static rt_uint32_t stm32_sdio_clock_get(struct stm32_sdio *hw_sdio)
{
    return HAL_RCC_GetPCLK2Freq();
}

static rt_err_t DMA_TxConfig(rt_uint32_t *src, rt_uint32_t *dst, int Size)
{
    SD_LowLevel_DMA_TxConfig((uint32_t *)src, (uint32_t *)dst, Size / 4);
    return RT_EOK;
}

static rt_err_t DMA_RxConfig(rt_uint32_t *src, rt_uint32_t *dst, int Size)
{
    SD_LowLevel_DMA_RxConfig((uint32_t *)src, (uint32_t *)dst, Size / 4);
    return RT_EOK;
}

SDIO硬件驱动层设备的初始化可以分为三部分:一是完成SDIO 接口引脚的初始化(通过CubeMX 生成的函数HAL_SD_MspInit 实现)和SDIO 外设中断使能(通过函数HAL_NVIC_EnableIRQ 实现);二是完成结构体 stm32_sdio_des 的成员初始化配置(三个函数指针和一个HAL标准库提供的SDIO 寄存器结构体);三是创建 Host 对象(通过函数 sdio_host_create 实现)。函数 rt_hw_sdio_init 也被自动初始化组件调用,不需要开发者再主动调用了。

SDIO硬件接口引脚的初始化在下文介绍CubeMX配置时可以看到,结构体 stm32_sdio_des 中DMA Tx/Rx 配置函数主要完成SDIO DMA Tx/Rx 通道的参数配置,这里就不展开介绍了。获取时钟频率的函数 stm32_sdio_clock_get 值得我们注意,不同芯片使用的SDIO时钟源不同,该函数的实现也有差异,比如我们使用的STM32L475芯片中SDIO的时钟源就不一定是PCLK2,具体使用哪个时钟源在下文介绍 CubeMX 时钟树时可以看到。

下面继续看Host 对象的创建过程:

// libraries\HAL_Drivers\drv_sdio.c

/**
  * @brief  This function create mmcsd host.
  * @param  sdio_des  stm32_sdio_des
  * @retval rt_mmcsd_host
  */
struct rt_mmcsd_host *sdio_host_create(struct stm32_sdio_des *sdio_des)
{
    struct rt_mmcsd_host *host;
    struct rthw_sdio *sdio = RT_NULL;

    if ((sdio_des == RT_NULL) || (sdio_des->txconfig == RT_NULL) || (sdio_des->rxconfig == RT_NULL))
        return RT_NULL;

    sdio = rt_malloc(sizeof(struct rthw_sdio));
    if (sdio == RT_NULL)
        return RT_NULL;
    rt_memset(sdio, 0, sizeof(struct rthw_sdio));

    host = mmcsd_alloc_host();
    if (host == RT_NULL)
    {
        LOG_E("L:%d F:%s mmcsd alloc host fail");
        rt_free(sdio);
        return RT_NULL;
    }

    rt_memcpy(&sdio->sdio_des, sdio_des, sizeof(struct stm32_sdio_des));
    sdio->sdio_des.hw_sdio = (sdio_des->hw_sdio == RT_NULL ? (struct stm32_sdio *)SDIO_BASE_ADDRESS : sdio_des->hw_sdio);
    sdio->sdio_des.clk_get = (sdio_des->clk_get == RT_NULL ? stm32_sdio_clk_get : sdio_des->clk_get);

    rt_event_init(&sdio->event, "sdio", RT_IPC_FLAG_FIFO);
    rt_mutex_init(&sdio->mutex, "sdio", RT_IPC_FLAG_FIFO);

    /* set host defautl attributes */
    host->ops = &ops;
    host->freq_min = 400 * 1000;
    host->freq_max = SDIO_MAX_FREQ;
    host->valid_ocr = 0X00FFFF80;/* The voltage range supported is 1.65v-3.6v */
#ifndef SDIO_USING_1_BIT
    host->flags = MMCSD_BUSWIDTH_4 | MMCSD_MUTBLKWRITE | MMCSD_SUP_SDIO_IRQ;
#else
    host->flags = MMCSD_MUTBLKWRITE | MMCSD_SUP_SDIO_IRQ;
#endif
    host->max_seg_size = SDIO_BUFF_SIZE;
    host->max_dma_segs = 1;
    host->max_blk_size = 512;
    host->max_blk_count = 512;

    /* link up host and sdio */
    sdio->host = host;
    host->private_data = sdio;

    rthw_sdio_irq_update(host, 1);

    /* ready to change */
    mmcsd_change(host);

    return host;
}

static const struct rt_mmcsd_host_ops ops =
{
    rthw_sdio_request,
    rthw_sdio_iocfg,
    rthw_sd_delect,
    rthw_sdio_irq_update,
};

在函数 sdio_host_create 中完成了 rt_mmcsd_host 成员的初始化配置,包括SDIO硬件设备驱动层实现的函数集合 rt_mmcsd_host_ops 的注册。有了该层向Host 注册的函数集合 rt_mmcsd_host_ops,SDIO Core Layer 提供的接口函数才能真是使用。

完成rt_mmcsd_host 成员的初始化配置后,通过函数 rthw_sdio_irq_update 使能SDIO 中断,并通知Core Layer 有新增卡设备。函数mmcsd_change 会向探测邮箱 mmcsd_detect_mb 发送刚完成配置的 Host 对象指针,探测线程 mmcsd_detect 则会接收到来自探测邮箱的 Host 对象指针,并开始SDIO卡设备的探测和初始化过程(前面已经介绍过了)。

三、SDIO 驱动配置

前面已经介绍了RT-Thread的SDIO设备对象是如何管理的,下面看看Pandora 开发板上的 AP6181 SDIO模块驱动如何配置。

3.1 SDIO CubeMX配置

AP6181 Wi-Fi 模块与Pandora开发板直接的硬件接线原理图在前面已经介绍过了,我们需要重点关注的分别是SDIO接口的6个引脚、WiFi INT引脚和WL_REG_ON引脚,后两个引脚可以通过PIN设备对象提供的接口函数来配置,我们只需要在CubeMX内配置SDIO接口的6个引脚即可,配置图示如下:

SDIO引脚配置
需要注意的是,SDIO的CMD引脚和D0 ~ D3 引脚需要配置为上拉模式,因为传输的命令/响应或者数据的开始位都是低电平信号,若按默认的悬空配置,可能会产生干扰信号,让SDIO设备不能稳定工作。

配置完SDIO总线引脚,接下来配置SDIO的时钟源和时钟频率,配置图示如下:
SDIO时钟树配置
从上图可以看出,SDMMC使用的并不是PCLK2时钟,而是PLLSAI1Q时钟。SDMMC的高速模式最高支持50MHZ,Pandora开发板提供的外部高速晶振频率为8MHZ,无法通过倍频分频得到50MHZ,最接近的可以达到48MHZ,因此这里将PLLSAI1Q的时钟频率配置到48MHZ(通过配置倍频系数N和分频系数R的值实现)。当然也可以选择PLLQ时钟,为了达到STM32L475支持的最高频率80MHZ,PLLCLK时钟的倍频与分频系数已确定,PLLQ与PLLCLK共用倍频系数N,因此我们只能将PLLQ时钟配置到40MHZ,时钟频率不如PLLSAI1Q,因此我们选择SDMMC的时钟源为PLLSAI1Q。配置完SDMMC的时钟后,就可以通过GENERATE CODE生成代码了。

3.2 SDIO时钟配置

还记得前面介绍CPU架构与BSP移植过程的博客中提到的,需要将CubeMX生成的SystemClock_Config函数代码复制到 board.c 中。这里新增了SDIO时钟配置,所以需要更新board.c 中函数SystemClock_Config 的代码,也即直接将CubeMX生成的SystemClock_Config函数代码(在源文件.\board\CubeMX_Config\Src\main.c中)复制到board.c 中即可。函数SystemClock_Config中新增的关于SDMMC时钟的代码如下:

// projects\stm32l475_wifi_sample\board\board.c

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  ......
  /* Initializes the CPU, AHB and APB busses clocks */
  ......
  /* Initializes the CPU, AHB and APB busses clocks */
  ......
  PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_SDMMC1;
  PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2;
  PeriphClkInit.Sdmmc1ClockSelection = RCC_SDMMC1CLKSOURCE_PLLSAI1;
  PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE;
  PeriphClkInit.PLLSAI1.PLLSAI1M = 1;
  PeriphClkInit.PLLSAI1.PLLSAI1N = 12;
  PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7;
  PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2;
  PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2;
  PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_48M2CLK;
  if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
  {
    Error_Handler();
  }
  /* Configure the main internal regulator output voltage */
  ......
}

前面介绍SDIO硬件设备驱动层在初始化结构体 stm32_sdio_des 时,需要提供一个获取SDIO时钟频率的函数指针,RT-Thread提供的默认函数是获取PCL2的时钟频率,但这里我们使用的是PLLSAI1Q时钟,我们应该如何获取PLLSAI1Q时钟频率呢?我们查看HAL标准库中与RCC相关的文件,从中找到跟获取RCC频率相关的函数声明如下:

// libraries\STM32L4xx_HAL\STM32L4xx_HAL_Driver\Src\stm32l4xx_hal_rcc.c
/**
  * @brief  Return the PCLK2 frequency.
  * @note   Each time PCLK2 changes, this function must be called to update the
  *         right PCLK2 value. Otherwise, any configuration based on this function will be incorrect.
  * @retval PCLK2 frequency in Hz
  */
uint32_t HAL_RCC_GetPCLK2Freq(void);

// libraries\STM32L4xx_HAL\STM32L4xx_HAL_Driver\Src\stm32l4xx_hal_rcc_ex.c
/**
  * @brief  Return the peripheral clock frequency for peripherals with clock source from PLLSAIs
  * @note   Return 0 if peripheral clock identifier not managed by this API
  * @param  PeriphClk  Peripheral clock identifier
  *         This parameter can be one of the following values:
  *            @arg @ref RCC_PERIPHCLK_RTC  RTC peripheral clock
  *            @arg @ref RCC_PERIPHCLK_ADC  ADC peripheral clock
  *            @arg @ref RCC_PERIPHCLK_I2C1  I2C1 peripheral clock
  *            @arg @ref RCC_PERIPHCLK_I2C2  I2C2 peripheral clock
  *            @arg @ref RCC_PERIPHCLK_I2C3  I2C3 peripheral clock
  *            @arg @ref RCC_PERIPHCLK_LPTIM1  LPTIM1 peripheral clock
  *            @arg @ref RCC_PERIPHCLK_LPTIM2  LPTIM2 peripheral clock
  *            @arg @ref RCC_PERIPHCLK_LPUART1  LPUART1 peripheral clock
  *            @arg @ref RCC_PERIPHCLK_RNG  RNG peripheral clock
  *            @arg @ref RCC_PERIPHCLK_SAI1  SAI1 peripheral clock (only for devices with SAI1)
  *            @arg @ref RCC_PERIPHCLK_SDMMC1  SDMMC1 peripheral clock
  *            @arg @ref RCC_PERIPHCLK_USART1  USART1 peripheral clock
  *            @arg @ref RCC_PERIPHCLK_USART2  USART1 peripheral clock
  *            @arg @ref RCC_PERIPHCLK_USART3  USART1 peripheral clock
  * @retval Frequency in Hz
  */
uint32_t HAL_RCCEx_GetPeriphCLKFreq(uint32_t PeriphClk);

从注释代码可知,函数HAL_RCCEx_GetPeriphCLKFreq 可以获得PLLSAIs 时钟的频率,我们使用的外设是SDMMC1,所以传入该函数的参数为 RCC_PERIPHCLK_SDMMC1,修改获取SDMMC时钟频率的函数代码如下:

// libraries\HAL_Drivers\drv_sdio.c

/**
  * @brief  This function get stm32 sdio clock.
  * @param  hw_sdio: stm32_sdio
  * @retval PLLSAI1QFreq
  */
static rt_uint32_t stm32_sdio_clock_get(struct stm32_sdio *hw_sdio)
{
    return HAL_RCCEx_GetPeriphCLKFreq(RCC_PERIPHCLK_SDMMC1);
}

3.3 WL_REG_ON引脚配置

在本文开头已经介绍过WL_REG_ON 引脚的作用,从WLAN Boot-Up时序图可以看出,WL_REG_ON 引脚应该SDIO 初始化前被拉高,我们将该引脚拉高的操作放到函数rt_hw_sdio_init 的最前面。由于WL_REG_ON 引脚并非所有SDIO 设备共有的,可能只是部分设备(比如AP6181)特有的,因此我们使用一个条件宏将WL_REG_ON 引脚拉高的操作包含在内。函数rt_hw_sdio_init 中新增代码如下:

// libraries\HAL_Drivers\drv_sdio.c

int rt_hw_sdio_init(void)
{
#ifdef BSP_USING_WIFI

    #include <drivers/pin.h>
    #define WIFI_REG_ON_PIN   GET_PIN(D, 1)
    rt_pin_mode(WIFI_REG_ON_PIN, PIN_MODE_OUTPUT);
    rt_pin_write(WIFI_REG_ON_PIN, PIN_LOW);
    rt_thread_mdelay(1);
    rt_pin_write(WIFI_REG_ON_PIN, PIN_HIGH);

#endif

    struct stm32_sdio_des sdio_des;
    ......
}
INIT_DEVICE_EXPORT(rt_hw_sdio_init);

从WLAN Boot-Up时序图得知,WL_REG_ON 引脚的拉高时间为 2个睡眠时钟周期(32.768KHZ)后,1.5ms之内,这里设置 1ms后拉高。可能读者发现不延迟等待直接拉高也可以,系统从上电启动开始,运行到此处应该已经够2个睡眠时钟周期了,所以也是可以正常运行的。

3.4 配置SDIO编译选项

配置好SDIO总线引脚及时钟后,要想使用RT-Thread提供的SDIO驱动代码,还需要定义宏BSP_USING_SDIO与RT_USING_SDIO,我们依然在Kconfig中配置SDIO外设编译选项,新增SDIO编译选项配置代码如下:

// projects\stm32l475_wifi_sample\board\Kconfig

menu "Hardware Drivers Config"
......
menu "On-chip Peripheral Drivers"
    ......
    config BSP_USING_SDIO
            bool "Enable SDIO"
            select RT_USING_SDIO
            default n

endmenu
......

保存配置,在env环境执行menuconfig命令,启用刚配置的 Enable SDIO 编译选项,配置图示如下:
启用SDIO编译选项
保存配置后,RT-Thread SDIO驱动就可以正常使用了,AP6181 Wi-Fi 模组的SDIO 驱动部分也配置完成了。下一篇博客将介绍 AP6181 WLAN驱动及WLAN框架部分,包括上层的LwIP协议栈移植。

更多文章:

发布了65 篇原创文章 · 获赞 35 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/m0_37621078/article/details/105097567