本章介绍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 Wi-Fi 模组具有以下特点:
AP6181 Wi-Fi 模组内部实际封装的是Broadcom 43362 芯片,接下来看看 BCM43362 芯片内部都有哪些模块(图片取自BCM43362_datasheet):
从上面 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模块的启动时序图:
从上图可以看出,WL_REG_ON引脚需要在 VBAT / VDDIO 上电2个睡眠时钟周期(32.768KHZ)后,1.5ms之内完成电平拉高,我们可以在 AP6181 驱动代码中设置WL_REG_ON引脚的拉高时机(比如0.1ms ~ 1ms)与动作。
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协议栈的层级关系图如下:
RT-Thread为方便我们管理Wi-Fi 设备,提供了一个WLAN管理框架,相当于WLAN 设备无关层,可以向上提供统一的访问接口。当我们更换 Wi-Fi 模块时,只需要修改相应的适配代码,不需要修改上层的应用程序。
前篇博客: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 的设备驱动分离,符合编写代码的高内聚、低耦合原则。
一个总线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设备,实现相应功能的扩展服务。
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 <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 <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 <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 card)
{
for (i = 1; i 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 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 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 LayerSDIO 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的CMD引脚和D0 ~ D3 引脚需要配置为上拉模式,因为传输的命令/响应或者数据的开始位都是低电平信号,若按默认的悬空配置,可能会产生干扰信号,让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生成代码了。
还记得前面介绍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
#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 编译选项,配置图示如下:
保存配置后,RT-Thread SDIO驱动就可以正常使用了,AP6181 Wi-Fi 模组的SDIO 驱动部分也配置完成了。下一篇博客将介绍 AP6181 WLAN驱动及WLAN框架部分,包括上层的LwIP协议栈移植。