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

Gretel ·
更新时间:2024-11-11
· 544 次阅读

本章介绍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 <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 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 #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协议栈移植。

更多文章: 《STM32之CubeL4(四)— SD/MMC + SDIO + HAL》 《IOT-OS之RT-Thread(十四)— AT命令集 + ESP8266 WiFi模块》 《IOT-OS之RT-Thread(十二)— 驱动分层与主从分离思想》
作者:StreamAI



IoT sdio rt-thread 对象

需要 登录 后方可回复, 如果你还没有账号请 注册新账号