文章目录
- 1 6UL的USDHC简介
- 1.1 USDHC Block Diagram
- 1.2 USDHC支持的模式
- 1.3 外部信号
- 1.4 Data Buffer
- 1.5 ADMA
- 1.5.1 ADMA Engine
- 1.5.2 ADMA2 Descriptor格式
- 1.6 Register
- 2 SD协议简介
- 2.1 SD总线拓扑
- 2.2 SD总线协议
- 2.3 SD卡功能描述
- 3 QEMU SD卡读写demo
- 3.1 USDHC层
- 3.1.1 usdhc控制器初始化
- 3.1.2 usdhc发送命令
- 3.1.3 usdhc接收响应
- 3.1.4 usdhc读取一个block
- 3.1.5 usdhc写入一个block
- 3.2 SD协议层
- 3.2.1 sd_card.h
- 3.2.2 SD卡初始化
- 3.2.3 SD卡读写
- 3.3 测试函数
- 4 移植FATFS
- 4.1 FATFS初始化
- 4.2 FATFS读写函数
- 4.3 其他函数实现
- 4.4 测试
- 4.4.1 创建测试文件系统
- 4.4.2 测试函数
1 6UL的USDHC简介
6UL USDHC(Ultra Secured Digital Host Controller)是一个很强大的IP。它支持SD/SDIO/MMC协议。
USDHC的IP很复杂,光寄存器就有30多个,每一个寄存器都是十几个控制位。这里为了简化demo,只支持了QEMU上的SD标准卡的读写,很多USDHC的功能都没用到,因此本节着重用到的一些功能做介绍。USDHC IP详细的文档参考6UL Reference Mannual,Chapter 58
Ultra Secured Digital Host Controller (uSDHC)
1.1 USDHC Block Diagram
- CLK(input):外部输入时钟
- CLK(output):外部输出时钟,主要用于SD卡的SCK。
- CMD(input)/(output):input是卡输入到USDHC的CMD信号,output是USDHC到卡的CMD信号。
- DATA[7:0]:同样的,input是外部卡输入到USDHC,output是USDHC输出到卡,用的是同一个引脚,只是在IP内部是分input,output。
- DLL:用于输入信号延时。当输入信号从SD卡到USDHC内部时,是有延时存在的,而此时如果用USDHC的CLK去采样,会导致采样的数据出现偏差,因此DLL会对USDHC内部的CLK进行delay去适应CMD,DATA线的延时。有一些卡有外部DQS,那可以使用外部DQS。
- Interrupt Generator:USDHC中产生中断的模块,当有中断发生后,Interrupt会产生IRQ信号给CPU。
- Register Bank:CPU使用IPS Bus访问,软件访问USDHC的接口。
- PIO,Buffer Control,TX/RX Buffer, SRAM, 这一部分都是跟USDHC内部的数据输入输出有关,对于软件来说是透明的。
- DMA:USDHC内部有多种DMA用于搬运数据
- CMD_CTRL,DATA_CTRL,CLK_CTRL:通过寄存器里的配置产生CMD,CLK和DATA,然后输出到SD总线上。
1.2 USDHC支持的模式
USDHC对于SD卡的支持如下:
- SD 1-bit
- SD 4-bit
- Identification Mode (up to 400 kHz)
- SD/SDIO full speed mode (up to 25 MHz)
- SD/SDIO high speed mode (up to 50 MHz)
- SD/SDIO UHS-I mode (up to 208 MHz in SDR mode, up to 50 MHz in DDR mode)
demo中使用了SD-4bit, SD/SDIO full speed mode。
1.3 外部信号
demo中用到了SD1_CLK, SD1_CMD, SD1_DATA0/1/2/3,这些pin的iomux默认ALT0就是USDHC1的引脚,因此在代码中不需要去配这些pin的iomux。
1.4 Data Buffer
USDHC用了一个可以配置的data buffer来传输SD bus的数据。 这个data buffer是在SD卡和CPU之间的一个临时性buffer。 读写watermark level都是可配置的,大小从1到128 word(512 bytes)。每一笔读写的burst length也可以配置,最大31个word。
USDHC 提供了三种访问data buffer的方式:
- CPU 轮询
- 外部DMA
- 内部DMA:包括Simple DMA和ADMA。
demo中使用了第三种的ADMA的方式来进行数据读写。
1.5 ADMA
1.5.1 ADMA Engine
USDHC内部DMA实现了一个DMA和AHB主机。
在SD Host Controller标准中,定义了一种叫ADMA的传输算法。对于以前的simple dma来说,一旦一个page传输完成,就会产生一个中断给CPU,然后CPU需要重新去写DMA的系统地址。
ADMA定义了一种可编程的ADMA descriptor标。主机驱动能自动计算出下一个page的地址而不需要在传输完一个page后再去重新写DMA的寄存器。这就减少了SD控制机像CPU中断的次数,提高了吞吐。
USDHC 实现了两种ADMA:
- ADMA1: 只支持4K对齐的内存地址。
- ADMA2:没有对齐的限制。
两种ADMA的descirptor表是不同的,文中使用了ADMA2。
ADMA能够识别不同的descriptor,如果"END"标志位被设立了,那么在ADMA执行完该descriptor后就会停止。
1.5.2 ADMA2 Descriptor格式
ADMA2包含以下几种descriptor:
- Valid/Invalid descriptor.
- Nop descriptor.
- Rsv descriptor.
- Set data length & address descriptor.
- Link descriptor.
- Interrupt flag & End flag
ADMA2 descriptor的格式如下图,每一个descriptor占用64个bit:
- [63:32]存放着改描述符指向的内存地址
- [31:16]存放该描述符传输块的长度
- [5:4]是该描述符的属性,00为NOP描述符,01位Rsv描述符,10为传输描述符,11为Link描述符。
- [2]为中断使能位,当该描述符传输完成后,是否要产生DMA中断。
- [1]为END标识符,标识该描述符为最后一个描述符
- [0]为Valid标识符,标识该描述符是否是一个有效的描述符
ADMA2描述符的数据结构如下图: - System address register存着描述符表的地址,即第0个描述符的地址
- 如果该描述符不是link描述符,那么硬件会顺序的往下取描述符
- 如果该描述符是link描述符,那么硬件下一个会去查找到link的那个描述符。
USDHC会根据当前描述符去做数据搬运操作。
1.6 Register
USDHC的寄存器非常多而且demo中只用到了一部分寄存器,这里不一一介绍了,在第三节中通过代码来介绍所使用到的寄存器。
2 SD协议简介
[注:本节节选自SD2.0标准协议完整版]
SD卡协议也非常长,这里只截取了部分重要的概念,同样的没办法列出所有SD协议的细节,在第三节中通过代码来介绍所使用的SD卡协议。
2.1 SD总线拓扑
SD 总线包含下面的信号:
CLK: 时钟信号
CMD: 双向命令/响应信号
DAT0-DAT3: 双向数据信号
Vdd,Vss1,Vss2: 电源和地信号
SD 卡总线有一个主(应用),多个从(卡),同步的星型拓扑结构(图3-2)。时钟,电源和
地信号是所有卡都有的。命令(CMD)和数据(DAT0-3)信号是根据每张卡的,提供连续地点对点连接到所有卡。
在初始化时,处理命令会单独发送到每个卡,允许应用程序检测卡以及分配逻辑地址给物理卡槽。数据总是单独发送(接收)到(从)每张卡。但是,为了简化卡的堆栈操作,在初始化过程结束后,所有的命令都是同时发送到所有卡。地址信息包含在命令包中。
SD 总线允许数据线的动态配置。上电后,SD 卡默认只使用DAT0 来传输数据。初始化之后,主机可以改变总线宽度(使用的数据线数目)。这个功能允许硬件成本和系统性能之间的简单交换。
2.2 SD总线协议
SD 总线的通信是基于命令和数据流的。由一个起始位开始,由一个停止位终止。
● 命令(Command):命令就是一个标记,用于发起一个操作。由主机发送到单个卡(寻址命
令)或者所有卡(广播命令)。命令在CMD 线上是连续传输的。
● 响应(Response):响应是一个标记,从寻址的卡或者所有卡(同步)发送给主机,作为向
前接收到的命令的回答。响应也是在CMD 线上连续传输的。
● 数据(Data):数据可以从主机到卡,也可以从卡到主机。通过数据线传输。
卡片寻址通过使用会话地址来实现,会话地址会在初始化阶段分配给卡。命令,响应和数据块的结构在第4 章中描述。SD 总线上的基本交互是命令/响应交互(表格3-4)。这种总线交互直接在命令或者响应的结构里面传输他们的信息。此外,一些操作还有数据内容。
SD 卡发送或接收的数据在块(block)中完成。数据块以CRC 位来保证成功。目前有单块或多块操作。注意:多块操作模式在快速写操作时更好一点。多块传输以命令线上的结束命令为结束标志。主机端可以配置单线还是多线传输。
块写操作使用简单的busy 来表示DAT0 数据线上的持续写操作,不管使用几线传输。
2.3 SD卡功能描述
主机和卡之间的交互都是由主机控制的。主机发送两种命令:广播命令,寻址(点对点)
命令。
● 广播命令
广播命令的目的是所有的卡。部分命令需要响应。
● 寻址(点对点)命令
寻址命令是发送给对应地址的卡的,并且会引起这张卡的响应。命令流程的简介图,[图4-1]是卡识别模式,[图4-3]是数据传输模式。命令列举在命令表格中[表4-19 ~ 表4-28]。当前状态,收到命令和后续状态之间的关系在[表4-29]中。后面的章节中,会首先描述各种卡的操作模式。之后,定义了控制时钟信号的限制。所有SD 卡的命令以及对应的响应,状态转换,错误条件以及时序都在后续章节
SD 卡系统(host &card)定义了两种操作模式:
● 卡识别模式
在复位后,查找总线上的新卡的时候,主机会处于“卡识别模式”。卡在复位后会处于
识别模式,直到收到SEND_RCA(CMD3)命令.
● 数据传输模式
当RCA 第一次发布后,卡会处于“数据传输模式”。主机会在总线上所有的卡都被识别后进入这个模式。
卡状态(Card state) | 操作模式(Operation mode) |
无效状态(Inactive State) | 无效模式(Inactive) |
空闲状态(Idle State) 准备状态(Ready State) 识别状态(Identification State) | 卡识别模式(Card identification mode) |
待机状态(Stand-by State) 传输状态(Transfer State) 发送数据状态(Sending-data State) 接收数据状态(Receive-data State) 编程状态(Programming State) 断开连接状态(Disconnect State) | 数据传输模式(Data transfer mode) |
在卡识别模式下,主机会复位所有处于“卡识别模式”的卡,确认工作电压范围,识别
卡,并且要求他们发布相对卡地址(Relative Card Address)。这个操作是通过卡各自的CMD线完成的。卡识别模式下,所有数据通信都只通过数据线完成。
总线激活后, 主机启动卡的初始化和识别进程( 见图-2) 。初始化进程以命令
SD_SEND_OP_COND(ACMD41)作为开始,通过设置操作条件和OCR 的HCS 位来进行。HCS(HighCapacity Support)位为1,表示主机支持高容量SD 卡。为0 表示不支持。
CMD8 扩展了ACMD41 的功能;参数里的HCS 位以及响应里的CCS(Card Capacity Status)位。HCS 会被不回应CMD8 的卡忽视掉。然而,如果卡不回应CMD8,主机应该设置HCS 为0。
标准容量卡会忽略HCS。如果HCS 设置为0,那么高容量SD 卡永远都不会返回ready 状态(保持busy 位为0)。卡通过OCR 的busy 位来通知主机ACMD41 的初始化完成了。设置busy 位为0 表示卡仍然在初始化。设置busy 位为1,表示已经完成初始化。主机会重复发送ACMD41,直到busy 为被设置为1。
卡片只在第一个ACMD41 的命令时,检查操作条件和OCR 里面的HCS 位。当重复ACMD41的时候,除了CMD0,主机不应该再发其他命令。
如果卡响应了CMD8,那么ACMD41 的响应就包括了CCS 字段信息。当卡返回“ready”的时候,CCS 是有效的(busy 位设置为1)。
CCS=1 表示卡是高容量SD 卡;CCS=0 表示卡是普通SD 卡。
在系统中,主机遵照相同的初始化顺序来初始化所有的新卡。不兼容的卡会进入
“Inactive”状态。主机接着就会发送命令ALL_SEND_CID(CMD2)给每一个卡,来得到他们的CID 号。未识别的卡(处于Ready 状态的)发送自己的CID 作为响应。当卡发送了CID 之后,
它就进入“Identification”状态。之后主机发送SEND_RELATIVE_ADDR(CMD3)命令,通知卡发布一个新的相对地址(RCA),这个地址比CID 短,用于作为将来数据传输模式的地址。一旦收到RCA,卡就会变为“Stand-by”状态。这时,如果主机想要分配另一个RCA 号,它可以再发送一个CMD3,通知卡重新发布一个RCA 号。最后一个产生的RCA 才是有效的。主机会重复识别进程,为系统中的每个卡循环发送“CMD2”和“CMD3”。
3 QEMU SD卡读写demo
源码链接 整个SD卡驱动代码结构树如下
include
imx_usdhc.h
sd_card.h
device
sd_card.c
driver
imx_usdhc.c
imx_usdhc.h定义了usdhc控制器,sd_card.h抽象了sd_card的操作。
3.1 USDHC层
include/imx_usdhc.h
#ifndef __IMX_USDHC__
#define __IMX_USDHC__
#include <stdint.h>
#include <stdbool.h>
#include <imx_uart.h>
#include <sd_card.h>
typedef struct imx_usdhc_tag
{
volatile uint32_t dma_sys_addr; //<00h
volatile uint32_t blk_att; //<04h
volatile uint32_t cmd_arg; //<08h
volatile uint32_t cmd_xfr_type; //<0Ch
volatile uint32_t cmd_rsp0; //<10h
volatile uint32_t cmd_rsp1; //<14h
volatile uint32_t cmd_rsp2; //<18h
volatile uint32_t cmd_rsp3; //<1Ch
volatile uint32_t data_buff_acc_port; //<20H
volatile uint32_t pres_state; //<24h
volatile uint32_t prot_ctrl; //<28h
volatile uint32_t sys_ctrl; //<2Ch
volatile uint32_t int_status; //<30h
volatile uint32_t int_status_en; //<34h
volatile uint32_t int_singal_en; //<38h
volatile uint32_t autocmd12_err_status; //<3Ch
volatile uint32_t host_ctrl_cap; //<40h
volatile uint32_t wtmk_lvl; //<44h
volatile uint32_t mix_ctrl; //<48h
volatile uint32_t reserve1; //<4Ch
volatile uint32_t force_event; //<50h
volatile uint32_t adma_error_status; //<54h
volatile uint32_t adma_sys_addr; //<58h
volatile uint32_t reserve2; //<5Ch
volatile uint32_t dll_ctrl; //<60h
volatile uint32_t dll_status; //<64h
volatile uint32_t clk_tune_ctrl_status; //<68h
volatile uint32_t reserve3; //<6Ch
volatile uint32_t reserve4[20]; //<70h-BFh
volatile uint32_t vend_spec; //<C0h
volatile uint32_t mmc_boot; //<C4h
volatile uint32_t vend_spec2; //<C8h
volatile uint32_t tuning_ctrl; //<CCh
} imx_usdhc_t;
首先根据USDHC的register memory map定义了usdhc控制器的结构体imx_usdhc_t, 这部分很简单,单纯的体力活。然后就是根据register memory map里的每个bit编写相应的XXX_SHIFT和XXX_MASK。同样的是体力活,这里就列出了几个bit,全部的代码有几百行,可以直接看github里的代码。
include/imx_usdhc.h
......
#define USDHC_BLKATT_BLKSIZE_SHIFT 0UL
#define USDHC_BLKATT_BLKSIZE_MASK (0xFFFUL << USDHC_BLKATT_BLKSIZE_SHIFT)
#define USDHC_BLKATT_BLKCNT_SHIFT 16UL
#define USDHC_BLKATT_BLKCNT_MASK (0xFFFFUL << USDHC_BLKATT_BLKCNT_SHIFT)
#define USDHC_CMD_XFRTYPE_CMDINX_SHIFT 24UL
#define USDHC_CMD_XFRTYPE_CMDINX_MASK (0x3FUL << USDHC_CMD_XFRTYPE_CMDINX_SHIFT)
#define USDHC_CMD_XFRTYPE_CMDTYPE_SHIFT 22UL
#define USDHC_CMD_XFRTYPE_CMDTYPE_MASK (0x3UL << USDHC_CMD_XFRTYPE_CMDTYPE_SHIFT)
.....
然后我们根据ADMA2 描述符那节所定义的格式定义出ADMA的描述符adma_bd_t;
include/imx_usdhc.h
typedef struct {
uint8_t att;
uint8_t reserved;
uint16_t len;
uint32_t addr;
} __attribute__((packed)) adma_bd_t;
3.1.1 usdhc控制器初始化
首先在头文件里先声明初始化函数ushdc_init。
imx_usdhc.h
extern bool usdhc_init(void *);
初始化的流程如下
初始化USDHC
软件复位USDHC
等待软件复位完成
设置初始化位宽为1bit
设置USDHC数据模式为小端模式
关闭DLL
选择DMA为ADMA2
设置卡识别模式时钟
使能USDHC发送80个clock
接着在c文件里实现该函数
imx_usdhc.c
bool usdhc_init(void *host)
{
imx_usdhc_t *usdhc = (imx_usdhc_t *)host;
USDHC_TRACE("%s: usdhc:%x\n", __func__, usdhc);
//STUB: iomux config here
//STUB: clock config here
usdhc->sys_ctrl |= USDHC_SYS_CTRL_RSTA_MASK;
while ((usdhc->sys_ctrl & USDHC_SYS_CTRL_RSTA_MASK) != 0)
;
usdhc_set_data_width(usdhc, USDHC_DWT_1BIT);
usdhc_set_endian_mode(usdhc, USDHC_EMODE_LITTLE_ENDIAN);
USDHC_TRACE("%s: disable usdhc dll\n", __func__);
usdhc->dll_ctrl &= ~USDHC_DLL_CTRL_DLL_CTRL_ENABLE_MASK;
USDHC_TRACE("%s: select adma2\n", __func__);
usdhc->prot_ctrl &= ~USDHC_PROT_CTRL_DMASEL_MASK;
usdhc->prot_ctrl |= (2 << USDHC_PROT_CTRL_DMASEL_SHIFT) & USDHC_PROT_CTRL_DMASEL_MASK;
usdhc_set_clock(usdhc, IDENTIFICATION_FREQ);
usdhc_initialization_active(usdhc);
return true;
}
代码中首先复位了USDHC控制器。
usdhc->sys_ctrl |= USDHC_SYS_CTRL_RSTA_MASK;
while ((usdhc->sys_ctrl & USDHC_SYS_CTRL_RSTA_MASK) != 0)
;
根据RSTA的定义,复位USDHC首先往RSTA中写入1,然后等待RSTA为0即可
接着调用了usdhc_set_data_width(usdhc, USDHC_DWT_1BIT); 设置USDHC数据位宽为1bit。usdhc_set_data_width的实现在同一个文件中。代码实现很简单,仅仅只是设置了uSDHCx_PROT_CTRL的DTW位。
imx_usdhc.c
void usdhc_set_data_width(void *host, uint8_t dtw)
{
imx_usdhc_t *usdhc = (imx_usdhc_t *)host;
usdhc->prot_ctrl &= ~USDHC_PROT_CTRL_DTW_MASK;
usdhc->prot_ctrl |= (dtw << USDHC_PROT_CTRL_DTW_SHIFT) & USDHC_PROT_CTRL_DTW_MASK;
}
根据DTW位的定义,定义宏如下:
imx_usdhc.h
#define USDHC_DWT_1BIT 0
#define USDHC_DWT_4BIT 1
#define USDHC_DWT_8BIT 2
然后根据流程,调用usdhc_set_endian_mode(usdhc, USDHC_EMODE_LITTLE_ENDIAN); 设置USDHC的字节序为小端模式。usdhc_set_endian_mode实现如下
imx_usdhc.c
void usdhc_set_endian_mode(void *host, uint8_t emode)
{
imx_usdhc_t *usdhc = (imx_usdhc_t *)host;
usdhc->prot_ctrl &= ~USDHC_PROT_CTRL_EMODE_MASK;
usdhc->prot_ctrl |= (emode << USDHC_PROT_CTRL_EMODE_SHIFT) & USDHC_PROT_CTRL_EMODE_MASK;
}
imx_usdhc.h
#define USDHC_EMODE_BIG_ENDIAN 0
#define USDHC_EMODE_HALF_WORD_BIG_ENDIAN 1
#define USDHC_EMODE_LITTLE_ENDIAN 2
usdhc->dll_ctrl &= ~USDHC_DLL_CTRL_DLL_CTRL_ENABLE_MASK;
清了DLL_CTRL寄存器的enable位,关闭DLL。
usdhc->prot_ctrl &= ~USDHC_PROT_CTRL_DMASEL_MASK;
usdhc->prot_ctrl |= (2 << USDHC_PROT_CTRL_DMASEL_SHIFT) & USDHC_PROT_CTRL_DMASEL_MASK;
这两行代码选择了ADMA2作为数据传输的方式,起定义如下:
然后调用了usdhc_set_clock(usdhc, IDENTIFICATION_FREQ); 将clock设置为识别模式的clock。这里注意的是,这里的usdhc_set_clock并没有改变clock的实际频率,在QEMU上对于timing的仿真是没有什么意义的,因为都是纯软件的。但在真实的芯片上需要去改变clock频率,否则过高的clock会使SD卡在初始化的时候失败。
最后调用了usdhc_initialization_active 去激活SD总线。
imx_usdhc.c
void usdhc_initialization_active(void *host)
{
imx_usdhc_t *usdhc = (imx_usdhc_t *)host;
usdhc->sys_ctrl |= USDHC_SYS_CTRL_INITA_MASK;
while ((usdhc->sys_ctrl & USDHC_SYS_CTRL_INITA_MASK) != 0)
;
}
3.1.2 usdhc发送命令
usdhc中发送命令需要操作以下寄存器:
- Command Argument (uSDHC1_CMD_ARG)
- Command Transfer Type (uSDHC1_CMD_XFR_TYP)
- Interrupt Status (uSDHC1_INT_STATUS)
- Interrupt Status Enable (uSDHC1_INT_STATUS_EN)
- Mixer Control (uSDHC1_MIX_CTRL)
- Protocol Control (uSDHC1_PROT_CTRL)
先贴出代码,然后一一解释
imx_usdhc.c
bool usdhc_send_command(void *host, uint8_t cmd_idx, uint32_t arg)
{
usdhc_cmd_t cmd;
imx_usdhc_t *usdhc = (imx_usdhc_t *)host;
uint32_t temp;
bool ret = true;
USDHC_TRACE("%s: cmd:%d\n", __func__, cmd_idx);
if (cmd_idx == CMD0) {
usdhc_create_cmd(&cmd, CMD0, 0, READ, RESPONSE_NONE,
false, false, false, false);
} else if (cmd_idx == CMD55) {
usdhc_create_cmd(&cmd, CMD55, 0, READ, RESPONSE_48,
false, true, true, false);
} else if (cmd_idx == ACMD41) {
usdhc_create_cmd(&cmd, ACMD41, arg, READ, RESPONSE_48,
false, false, false, false);
} else if (cmd_idx == CMD2) {
usdhc_create_cmd(&cmd, CMD2, 0, READ, RESPONSE_136,
false, true, false, false);
} else if (cmd_idx == CMD3) {
usdhc_create_cmd(&cmd, CMD3, arg, READ, RESPONSE_48,
false, true, true, false);
} else if (cmd_idx == CMD7) {
usdhc_create_cmd(&cmd, CMD7, arg, READ, RESPONSE_48_CHECK_BUSY,
false, true, true, false);
} else if (cmd_idx == CMD16) {
usdhc_create_cmd(&cmd, CMD16, arg, READ, RESPONSE_48,
false, true, true, false);
} else if (cmd_idx == CMD18) {
usdhc_create_cmd(&cmd, CMD18, arg, READ, RESPONSE_48,
true, true, true, true);
} else if (cmd_idx == CMD25) {
usdhc_create_cmd(&cmd, CMD25, arg, WRITE, RESPONSE_48,
true, true, true, true);
}
usdhc->int_status = 0x117f01ff; /*clear interrupt status register*/
usdhc->int_status_en |= 0x007f013f; /*enable all the interrupt status*/
if (cmd.dma_en == true) {
usdhc->int_status_en |= USDHC_INT_STATUS_EN_DINTSEN_MASK;
}
/* wait cmd line free */
while ((usdhc->pres_state & USDHC_PRES_STATE_CIHB_MASK) != 0);
/* wait data line free */
if (cmd.dat_pres == true) {
while ((usdhc->pres_state & USDHC_PRES_STATE_CDIHB_MASK) != 0);
}
/* write command arugment in the Command Argument Register */
usdhc->cmd_arg = cmd.arg;
if (cmd.dma_en == false) {
usdhc->prot_ctrl &= ~USDHC_PROT_CTRL_DMASEL_MASK;
} else {
usdhc->prot_ctrl |= (2 << USDHC_PROT_CTRL_DMASEL_SHIFT) & USDHC_PROT_CTRL_DMASEL_MASK;
}
temp = usdhc->mix_ctrl & 0xFFFFFFC0;
if (cmd.dma_en == true)
temp |= USDHC_MIX_CTRL_DMAEN_MASK;
else
temp &= ~USDHC_MIX_CTRL_DMAEN_MASK;
if (cmd.blk_cnt_en_chk == true)
temp |= USDHC_MIX_CTRL_BCEN_MASK;
else
temp &= ~USDHC_MIX_CTRL_BCEN_MASK;
if (cmd.auto_cmd12_en == true)
temp |= USDHC_MIX_CTRL_AC12EN_MASK;
else
temp &= ~USDHC_MIX_CTRL_AC12EN_MASK;
if (cmd.ddr_en == true)
temp |= USDHC_MIX_CTRL_DDREN_MASK;
else
temp &= ~USDHC_MIX_CTRL_DDREN_MASK;
if (cmd.xfer_type == READ)
temp |= USDHC_MIX_CTRL_DTDSEL_MASK;
else
temp &= ~USDHC_MIX_CTRL_DTDSEL_MASK;
if (cmd.blk_type == MULTIPLE_BLK)
temp |= USDHC_MIX_CTRL_MSBSEL_MASK;
else
temp &= ~USDHC_MIX_CTRL_MSBSEL_MASK;
usdhc->mix_ctrl = temp;
temp = usdhc->cmd_xfr_type;
temp &= ~USDHC_CMD_XFRTYPE_RSPTYPE_MASK;
switch (cmd.resp_format) {
case RESPONSE_NONE:
default:
temp |= (0 << USDHC_CMD_XFRTYPE_RSPTYPE_SHIFT) & USDHC_CMD_XFRTYPE_RSPTYPE_MASK;
break;
case RESPONSE_136:
temp |= (1 << USDHC_CMD_XFRTYPE_RSPTYPE_SHIFT) & USDHC_CMD_XFRTYPE_RSPTYPE_MASK;
break;
case RESPONSE_48:
temp |= (2 << USDHC_CMD_XFRTYPE_RSPTYPE_SHIFT) & USDHC_CMD_XFRTYPE_RSPTYPE_MASK;
break;
case RESPONSE_48_CHECK_BUSY:
temp |= (3 << USDHC_CMD_XFRTYPE_RSPTYPE_SHIFT) & USDHC_CMD_XFRTYPE_RSPTYPE_MASK;
break;
}
if (cmd.crc_chk == true)
temp |= USDHC_CMD_XFRTYPE_CCCEN_MASK;
else
temp &= ~USDHC_CMD_XFRTYPE_CCCEN_MASK;
if (cmd.cmdidx_chk == true)
temp |= USDHC_CMD_XFRTYPE_CICEN_MASK;
else
temp &= ~USDHC_CMD_XFRTYPE_CICEN_MASK;
if (cmd.dat_pres == true)
temp |= USDHC_CMD_XFRTYPE_DPSEL_MASK;
else
temp &= ~USDHC_CMD_XFRTYPE_DPSEL_MASK;
temp &= ~USDHC_CMD_XFRTYPE_CMDINX_MASK;
temp |= (cmd.cmd << USDHC_CMD_XFRTYPE_CMDINX_SHIFT) & USDHC_CMD_XFRTYPE_CMDINX_MASK;
usdhc->cmd_xfr_type = temp;
if (cmd.dma_en == false) {
/* DMAE|CIE|CEBE|CCE|CTOE|CC */
USDHC_TRACE("%s wait DMAE|CIE|CEBE|CCE|CTOE|CC\n", __func__);
while ((usdhc->int_status & 0x100F0001) == 0);
} else {
USDHC_TRACE("%s wait DMAE|DEBE|DCE|DTOE|CIE|CEBE|CCE|CTOE|TC\n", __func__);
/* DMAE|DEBE|DCE|DTOE|CIE|CEBE|CCE|CTOE|TC */
while((usdhc->int_status & 0x107F0002) == 0);
}
/* mask all the signals */
usdhc->int_singal_en = 0;
/* check CCE or CTOE error */
if ((usdhc->int_status & USDHC_INT_STATUS_CCE_MASK) != 0) {
ret = false;
USDHC_TRACE("%s Command CRC error\n", __func__);
goto cleanup;
}
if ((usdhc->int_status & USDHC_INT_STATUS_CTOE_MASK) != 0) {
ret = false;
USDHC_TRACE("%s Command Timeout error\n", __func__);
goto cleanup;
}
cleanup:
USDHC_TRACE("%s, ret:%d\n", __func__, ret);
return ret;
}
输入的参数第一个是cmd的id,第二个是cmd的argument。代码的第一步是根据传入的cmd的id来创建一个cmd。cmd的结构体是由我们自己定义的,这个并非硬件spec定义的。主要定义了一些标志位用于操作寄存器。先来看一看usdhc_cmd_t的定义。
imx_usdhc.h
#define WRITE 0
#define READ 1
#define RESPONSE_NONE 0
#define RESPONSE_136 1
#define RESPONSE_48 2
#define RESPONSE_48_CHECK_BUSY 3
#define SINGLE_BLK 0
#define MULTIPLE_BLK 1
typedef struct usdhc_cmd_tag{
uint32_t cmd;
uint32_t arg;
uint8_t xfer_type;
uint8_t resp_format;
bool dat_pres;
bool crc_chk;
bool cmdidx_chk;
bool blk_cnt_en_chk;
uint8_t blk_type;
bool dma_en;
bool auto_cmd12_en;
bool ddr_en;
} usdhc_cmd_t;
- cmd是cmd的idx
- arg记录的是发送cmd所需要的的argument
- xfer_type有READ和WRITE,这一个flag用于选择uSDHCx_MIX_CTRL中的DTDSEL位。
- resp_format这个flag标识cmd response的格式。用来设置uSDHCx_CMD_XFR_TYP中的RSPTYP位
- dat_pres用于标识该cmd是否有数据传输。用于设置uSDHCx_CMD_XFR_TYP中的DPSEL位。
- crc_chk标识是否需要对cmd的crc进行检查,用于设置uSDHCx_CMD_XFR_TYP中的CCCEN位。
- cmdidx_chk标识是否对cmd的id进行check,用于设置uSDHCx_CMD_XFR_TYP中的CICEN位。
- blk_cnt_en_chk用于设置uSDHCx_MIX_CTRL的BCEN位
- blk_type用于设置uSDHCx_MIX_CTRL的MSBSEL位
- dma_en用于设置uSDHCx_MIX_CTRL中的DMAEN位。
- auto_cmd12_en用于设置uSDHCx_MIX_CTRL的AC12EN位。当该位设置后,在传输多个block的时候,当一个block传输完成后,硬件会自动发一个CMD12命令。
- ddr_en 用于设置uSDHCx_MIX_CTRL中的DDR_EN位。
usdhc_create_cmd 的实现很简单,就是将传入的参数赋值到usdhc_cmd_t中的各个字段。
imx_usdhc.c
static void usdhc_create_cmd(usdhc_cmd_t *cmd,
uint32_t idx,
uint32_t arg,
uint8_t xfer_type,
uint8_t format,
bool data_pres,
bool crc_chk,
bool cmd_idx_chk,
bool dma_en)
{
cmd->cmd = idx;
cmd->arg = arg;
cmd->xfer_type = xfer_type;
cmd->resp_format = format;
cmd->dat_pres = data_pres;
cmd->crc_chk = crc_chk;
cmd->cmdidx_chk = cmd_idx_chk;
if (dma_en) {
cmd->blk_cnt_en_chk = true;
cmd->blk_type = MULTIPLE_BLK;
cmd->auto_cmd12_en = true;
}
else {
cmd->blk_cnt_en_chk = false;
cmd->blk_type = SINGLE_BLK;
cmd->auto_cmd12_en = false;
}
cmd->dma_en = dma_en;
cmd->ddr_en = false;
}
demo中使用了ADMA2,所以当dma_en被设置了之后,AC12EN也需要被设置,不然的话在多个块传输的时候会失败。
有了该函数后,我们就可以看看每一个cmd的配置了。
- CMD0: 没有参数,没有response,所有的check都关闭。不需要使能DMA
if (cmd_idx == CMD0) {
usdhc_create_cmd(&cmd, CMD0, 0, READ, RESPONSE_NONE,
false, false, false, false);
命令索引 | 类型 | 参数 | 应答 | 缩写 | 命令说明 |
CMD0 | bc | 00000000 | - | GO_IDLE_STATE | 复位设备至idle状态 |
- CMD55: CMD55的地址0,在没有分配RCA之前,使用0地址进行访问。RESPONSE格式为48bit的响应。打开crc_chk和cmd_idx_chk。没有数据传输,故data_pres和dma_en不使能。
} else if (cmd_idx == CMD55) {
usdhc_create_cmd(&cmd, CMD55, 0, READ, RESPONSE_48,
false, true, true, false);
命令索引 | 类型 | 参数 | 应答 | 缩写 | 命令说明 |
CMD55 | ac | [31:16]RCA [15:0]填充位 | R1 | APP_CMD | 告诉卡,下个命令是特定应用命令,而不是标准命令。 |
- ACMD41:
else if (cmd_idx == ACMD41) {
usdhc_create_cmd(&cmd, ACMD41, arg, READ, RESPONSE_48,
false, false, false, false);
命令索引 | 类型 | 参数 | 应答 | 缩写 | 命令说明 |
ACMD41 | bcr | [31]保留位 [30]HCS(OCR30) [29:24]保留位 [23:0]VddVdd 电压(OCR[23:0]) | R3 | SD_SEND_OP_COND | 发送卡的支持信息(HCS),并要求卡通过命令线返回OCR 寄存器内容。当卡收到SEND_IF_COND 时,HCS 是有效的。保留位设为0。CCS 位对应OCR[30] |
- CMD2: 用于获取CID寄存器,CID寄存器返回136bit的响应,参数为0。
else if (cmd_idx == CMD2) {
usdhc_create_cmd(&cmd, CMD2, 0, READ, RESPONSE_136,
false, true, false, false);
CMD2获取CID
命令索引 | 类型 | 参数 | 应答 | 缩写 | 命令说明 |
CMD2 | bc | [31:0] 填充位 | R2 | ALL_SEND_CID | 请求设备在CMD线发送其CID编号 |
- CMD3:获取RCA,起响应是48位的,参数为SD卡的RCA,发送完命令后SD卡会返回SD卡自身的RCA给
else if (cmd_idx == CMD3) {
usdhc_create_cmd(&cmd, CMD3, arg, READ, RESPONSE_48,
false, true, true, false);
命令索引 | 类型 | 参数 | 应答 | 缩写 | 命令说明 |
CMD3 | ac | [31:16] RCA [15:0] 填充位 | R1 | SET_RELATIVE_ADDR | 分配相对地址到设备 |
- CMD7 设置transfer状态。选中卡,参数是卡的RCA,响应为48bit的响应。
} else if (cmd_idx == CMD7) {
usdhc_create_cmd(&cmd, CMD7, arg, READ, RESPONSE_48_CHECK_BUSY,
false, true, true, false);
命令索引 | 类型 | 参数 | 应答 | 缩写 | 命令说明 |
CMD7 | ac | [31:16] RCA [15:0] 填充位 | R2 | SELECT/DESELECT_C ARD | 在stand-by和transfer状态之间或program- ming和disconnect状态之间切换设备的命令。两种情况下,设备以其自己的相对地址被选定并以其他地址被取消选定;地址0取消所有设备的选定。 |
- CMD16: CMD16用于设置SD卡的位宽,参数是设置的block长度,响应为48bit的响应,没有数据传输,故dma_en和data_pres都没有设置
else if (cmd_idx == CMD16) {
usdhc_create_cmd(&cmd, CMD16, arg, READ, RESPONSE_48,
false, true, true, false);
命令索引 | 类型 | 参数 | 应答 | 缩写 | 命令说明 |
CMD16 | ac | [31:0]块长度 | R1 | SET_BLOCKLEN | 对于标准SD 卡来说,这个命令会设置所有块命令的长度(字节)。默认的块长度是512Byte。只有当CSD 允许部分块读取操作,设置的长度才对存储访问命令有效。对于高容量SD 卡来说,CMD16 设置的块长度对于读写命令来说没有硬性,因为块长度是固定的512Byte。这个命令只对加锁/解锁命令有效。不管哪种,只要块长度设置大于512Byte,就报错BLOCK_LEN_ERROR。 |
- CMD18: 读数据。参数为读数据块的起始块地址,48bit的响应。有数据传输,所以data_pres和dma_en都设上
} else if (cmd_idx == CMD18) {
usdhc_create_cmd(&cmd, CMD18, arg, READ, RESPONSE_48,
true, true, true, true);
命令索引 | 类型 | 参数 | 应答 | 缩写 | 命令说明 |
CMD18 | adtc | [31:0] 数据地址1 | R1 | READ_MULTIPLE_ BLOCK | 从设备向主机连续传输数据块,直至被停止命令中断,或所要求传输的块数。 |
- CMD25: 写数据。参数为写数据块的起始块地址,48bit的响应。有数据传输,所以data_pres和dma_en都设上。数据方向为输出。
} else if (cmd_idx == CMD25) {
usdhc_create_cmd(&cmd, CMD25, arg, WRITE, RESPONSE_48,
true, true, true, true);
}
命令索引 | 类型 | 参数 | 应答 | 缩写 | 命令说明 |
CMD25 | adtc | [31:0] 数据地址1 | R1 | WRITE_MULTIPLE_ BLOCK | 连续写数据块直到STOP_TRANSMISSION 命令被发送。块长度和WRITE_BLOCK 一致。 |
到这cmd的就能根据cmd的索引创建好了。接下来就是根据不同的配置来配置以下寄存器
- Command Argument (uSDHC1_CMD_ARG)
- Command Transfer Type (uSDHC1_CMD_XFR_TYP)
- Interrupt Status (uSDHC1_INT_STATUS)
- Interrupt Status Enable (uSDHC1_INT_STATUS_EN)
- Mixer Control (uSDHC1_MIX_CTRL)
- Protocol Control (uSDHC1_PROT_CTRL)
usdhc->int_status = 0x117f01ff; /*clear interrupt status register*/
usdhc->int_status_en |= 0x007f013f; /*enable all the interrupt status*/
if (cmd.dma_en == true) {
usdhc->int_status_en |= USDHC_INT_STATUS_EN_DINTSEN_MASK;
}
/* wait cmd line free */
while ((usdhc->pres_state & USDHC_PRES_STATE_CIHB_MASK) != 0);
/* wait data line free */
if (cmd.dat_pres == true) {
while ((usdhc->pres_state & USDHC_PRES_STATE_CDIHB_MASK) != 0);
}
/* write command arugment in the Command Argument Register */
usdhc->cmd_arg = cmd.arg;
if (cmd.dma_en == false) {
usdhc->prot_ctrl &= ~USDHC_PROT_CTRL_DMASEL_MASK;
} else {
usdhc->prot_ctrl |= (2 << USDHC_PROT_CTRL_DMASEL_SHIFT) & USDHC_PROT_CTRL_DMASEL_MASK;
}
temp = usdhc->mix_ctrl & 0xFFFFFFC0;
usdhc->mix_ctrl = temp;
......
temp &= ~USDHC_CMD_XFRTYPE_CMDINX_MASK;
temp |= (cmd.cmd << USDHC_CMD_XFRTYPE_CMDINX_SHIFT) & USDHC_CMD_XFRTYPE_CMDINX_MASK;
usdhc->cmd_xfr_type = temp;
当Command Transfer Type一旦写入命令,USDHC就会将数据发送到SD总线上。而后只要等待相应的中断状态标志位即可。
if (cmd.dma_en == false) {
/* DMAE|CIE|CEBE|CCE|CTOE|CC */
USDHC_TRACE("%s wait DMAE|CIE|CEBE|CCE|CTOE|CC\n", __func__);
while ((usdhc->int_status & 0x100F0001) == 0);
} else {
USDHC_TRACE("%s wait DMAE|DEBE|DCE|DTOE|CIE|CEBE|CCE|CTOE|TC\n", __func__);
/* DMAE|DEBE|DCE|DTOE|CIE|CEBE|CCE|CTOE|TC */
while((usdhc->int_status & 0x107F0002) == 0);
}
/* mask all the signals */
usdhc->int_singal_en = 0;
当有cmd crc错误和timeout错误的时候,函数返回失败
/* check CCE or CTOE error */
if ((usdhc->int_status & USDHC_INT_STATUS_CCE_MASK) != 0) {
ret = false;
USDHC_TRACE("%s Command CRC error\n", __func__);
goto cleanup;
}
if ((usdhc->int_status & USDHC_INT_STATUS_CTOE_MASK) != 0) {
ret = false;
USDHC_TRACE("%s Command Timeout error\n", __func__);
goto cleanup;
}
3.1.3 usdhc接收响应
响应的结构体很简单,定义在sd_card.h中,一共有4个word的响应和1个byte的响应格式标志符。
sd_card.h
typedef struct sd_resp_tag {
uint32_t rsp0;
uint32_t rsp1;
uint32_t rsp2;
uint32_t rsp3;
uint8_t resp_format;
} sd_resp_t;
获取响应的函数实现如下,只需要去读Command Response0/1/2/3四个寄存器即可。
imx_usdhc.c
void usdhc_get_response(void *host, sd_resp_t *rsp)
{
imx_usdhc_t *usdhc = (imx_usdhc_t *)host;
rsp->rsp0 = usdhc->cmd_rsp0;
rsp->rsp1 = usdhc->cmd_rsp1;
rsp->rsp2 = usdhc->cmd_rsp2;
rsp->rsp3 = usdhc->cmd_rsp3;
}
3.1.4 usdhc读取一个block
控制usdhc的步骤只需两步:
- 设置好ADMA描述符表, 这里因为只有传输一个block,所以该描述符表只有一个描述符。其地址设置为目标内存地址,长度为512 byte,即一个block,属性设为TRANS,VALID, END。ADMA完成这一个描述符后就结束传输。
imx_usdhc.c
bool usdhc_read_block(void *host, uint8_t *dst, uint32_t blk_idx)
{
adma_bd_t adma_bd;
imx_usdhc_t *usdhc = (imx_usdhc_t *)host;
USDHC_TRACE("%s dst:%x, blk_idx:0x%x\n", __func__, dst, blk_idx);
adma_bd.addr = (uint32_t)dst;
adma_bd.len = 512;
adma_bd.att = 0x20 | 0x02 | 0x01;
- 然后打开所有中断标志位,配置ADMA System Address寄存器为描述符表的地址,设置好传输的block个数和block的大小,以及wtmk_lvl的大小。
imx_usdhc.c
usdhc->int_status = 0x117f01ff; /* clear all the interrupt */
usdhc->adma_sys_addr = (uint32_t)&adma_bd;
usdhc->blk_att = (1 << USDHC_BLKATT_BLKCNT_SHIFT) | 512;
usdhc->wtmk_lvl = 0x00000080;
- 发送CMD18,USDHC就会发出一个CMD18的传输到SD总线上。
imx_usdhc.c
return usdhc_send_command(usdhc, CMD18, blk_idx);
}
3.1.5 usdhc写入一个block
USDHC写入一个block的过程和读取一个block过程相似,唯一的不同就是发送的命令不同。读取使用CMD18,写入使用CMD25即可。
imx_usdhc.c
bool usdhc_write_block(void *host, uint8_t *src, uint32_t blk_idx)
{
adma_bd_t adma_bd;
imx_usdhc_t *usdhc = (imx_usdhc_t *)host;
USDHC_TRACE("%s src:%x, blk_idx:0x%x\n", __func__, src, blk_idx);
adma_bd.addr = (uint32_t)src;
adma_bd.len = 512;
adma_bd.att = 0x20 | 0x02 | 0x01;
usdhc->int_status = 0x117f01ff;
usdhc->adma_sys_addr = (uint32_t)&adma_bd;
usdhc->blk_att = (1 << USDHC_BLKATT_BLKCNT_SHIFT) | 512;
usdhc->wtmk_lvl = 0x00000080;
return usdhc_send_command(usdhc, CMD25, blk_idx);
}
3.2 SD协议层
3.2.1 sd_card.h
#ifndef __SDCARD_H__
#define __SDCARD_H__
#include <stdint.h>
#include <stdbool.h>
typedef struct sdcard_tag{
void *host;
uint32_t rca;
uint32_t ocr;
uint32_t cid[4];
char product_name[6];
uint8_t major;
uint8_t minor;
bool (*host_init)(void *);
bool (*send_cmd)(void *, uint8_t, uint32_t);
void (*get_resp)(void *, sd_resp_t *);
bool (*read_block)(void *, uint8_t *, uint32_t);
bool (*write_block)(void *, uint8_t *, uint32_t);
} sdcard_t;
#endif
首先我们定义sdcard的结构体sdcard_t。
- host为sd卡对应的控制器,本例中对应了usdhc。
- rca保存着这张卡的地址。
- ocr保存这张卡的ocr寄存器的值。
- cid保存这张卡的cid寄存器的值。
- product_name记录这张卡的产品名称。
- major和minor记录这张卡的版本号。
- host_init指向了控制器的初始化函数,用来初始化sd卡对应的控制器。
- send_cmd指向了控制器的发送命令函数。
- get_resp指向控制器的读响应函数。
- read_block指向控制器的读block函数
- write_block指向控制器的写block函数
然后就是声明三个sd卡的API。
extern uint32_t sdcard_init(sdcard_t *);
extern uint32_t sdcard_read_block(sdcard_t *, uint8_t *, uint32_t);
extern uint32_t sdcard_write_block(sdcard_t *, uint8_t *, uint32_t);
3.2.2 SD卡初始化
sd_card.c
sd卡初始化分两步:
- 调用sdcard->host_init初始化host控制器
- 调用sdcard_device_init初始化SD卡设备
uint32_t sdcard_init(sdcard_t *sdcard)
{
uint32_t ret = SDCARD_SUCCESS;
SDCARD_TRACE("%s entry\n", __func__);
if ((sdcard == NULL) || (sdcard->host_init == NULL)) {
ret = SDCARD_PARAM_NULL;
goto cleanup;
}
if (sdcard->host_init(sdcard->host) == false) {
ret = SDCARD_HOST_INIT_FAILURE;
goto cleanup;
}
ret= sdcard_device_init(sdcard);
cleanup:
SDCARD_TRACE("%s ret:%d\n", __func__, ret);
return ret;
}
```c
static uint32_t sdcard_device_init(sdcard_t *sdcard)
{
uint32_t status = SDCARD_SUCCESS;
SDCARD_TRACE("%s entry\n", __func__);
#define SD_INIT_SEQUENCE(func) if ((status = func(sdcard)) != SDCARD_SUCCESS) return status
SD_INIT_SEQUENCE(sdcard_go_idle_cmd0);
SD_INIT_SEQUENCE(sdcard_send_cmd55);
SD_INIT_SEQUENCE(sdcard_get_ocr_acmd41);
SD_INIT_SEQUENCE(sdcard_get_cid_cmd2);
SD_INIT_SEQUENCE(sdcard_set_rca_cmd3);
SD_INIT_SEQUENCE(sdcard_select_card_cmd7);
SD_INIT_SEQUENCE(sdcard_set_blk_len_cmd16);
return status;
}
sdcard_device_init函数实现了SD卡的初始化流程:
- 调用sdcard_go_idle_cmd0复位SD卡,该函数就是调用host的send_cmd函数发出cmd0。
static uint32_t sdcard_go_idle_cmd0(sdcard_t *sdcard)
{
return sdcard->send_cmd(sdcard->host, CMD0, 0) == true ?
SDCARD_SUCCESS : SDCARD_SEND_COMMADN_FAILURE;
}
- 调用sdcard_send_cmd55和sdcard_get_ocr_acmd41获取卡的OCR寄存器,在demo中并没有根据获得的OCR进行电压处理。在实际的芯片上是要根据卡得到的OCR进行电压的适配,比如切换IO的电压,这个过程叫做电压检测。因为是纯软件模拟,但并没有模拟IO口电压这类模拟电路,因此这离仅读回OCR寄存器,不作任何处理。
static uint32_t sdcard_send_cmd55(sdcard_t *sdcard)
{
bool res = true;
uint32_t ret = SDCARD_SUCCESS;
sd_resp_t resp;
res = sdcard->send_cmd(sdcard->host, CMD55, 0);
if (res != true) {
ret = SDCARD_SEND_COMMADN_FAILURE;
goto cleanup;
}
sdcard->get_resp(sdcard->host, &resp);
sdcard_dump_response(&resp);
ret = SDCARD_SUCCESS;
cleanup:
SDCARD_TRACE("%s ret:%d\n", __func__, ret);
return ret;
}
static uint32_t sdcard_get_ocr_acmd41(sdcard_t *sdcard)
{
bool res = true;
uint32_t ret = SDCARD_SUCCESS;
sd_resp_t resp;
res = sdcard->send_cmd(sdcard->host, ACMD41, 0xff800000);
if (res != true) {
ret = SDCARD_SEND_COMMADN_FAILURE;
goto cleanup;
}
sdcard->get_resp(sdcard->host, &resp);
sdcard_dump_response(&resp);
ret = SDCARD_SUCCESS;
cleanup:
SDCARD_TRACE("%s ret:%d\n", __func__, ret);
return ret;
}
- 调用sdcard_get_cid_cmd2获取卡的CID寄存器,获取响应后解析出产品名称并记录打印下来。
static uint32_t sdcard_get_cid_cmd2(sdcard_t *sdcard)
{
bool res = true;
uint32_t ret = SDCARD_SUCCESS;
sd_resp_t resp;
res = sdcard->send_cmd(sdcard->host, CMD2, 0);
if (res != true) {
ret = SDCARD_SEND_COMMADN_FAILURE;
goto cleanup;
}
sdcard->get_resp(sdcard->host, &resp);
sdcard_dump_response(&resp);
ret = SDCARD_SUCCESS;
sdcard->cid[0] = resp.rsp0;
sdcard->cid[1] = resp.rsp1;
sdcard->cid[2] = resp.rsp2;
sdcard->cid[3] = resp.rsp3;
sdcard->product_name[5] = 0;
sdcard->product_name[4] = sdcard->cid[2] & (0xff);
sdcard->product_name[3] = (sdcard->cid[2] & (0xff << 8)) >> 8;
sdcard->product_name[2] = (sdcard->cid[2] & (0xff << 16)) >> 16;
sdcard->product_name[1] = (sdcard->cid[2] & (0xff << 24)) >> 24;
sdcard->product_name[0] = sdcard->cid[3] & (0xff);
SDCARD_TRACE("%s: sd card product name:%s\n", __func__, sdcard->product_name);
cleanup:
SDCARD_TRACE("%s ret:%d\n", __func__, ret);
return ret;
}
- 调用sdcard_set_rca_cmd3获取卡的RCA并且记录下来。
static uint32_t sdcard_set_rca_cmd3(sdcard_t *sdcard)
{
bool res = true;
uint32_t ret = SDCARD_SUCCESS;
sd_resp_t resp;
res = sdcard->send_cmd(sdcard->host, CMD3, 0);
if (res != true) {
ret = SDCARD_SEND_COMMADN_FAILURE;
goto cleanup;
}
sdcard->get_resp(sdcard->host, &resp);
sdcard->rca = resp.rsp0;
sdcard_dump_response(&resp);
ret = SDCARD_SUCCESS;
cleanup:
SDCARD_TRACE("%s ret:%d\n", __func__, ret);
return ret;
}
- 调用sdcard_select_card_cmd7使卡进入传输模式,即选中该SD卡
static uint32_t sdcard_select_card_cmd7(sdcard_t *sdcard)
{
bool res = true;
uint32_t ret = SDCARD_SUCCESS;
sd_resp_t resp;
res = sdcard->send_cmd(sdcard->host, CMD7, sdcard->rca);
if (res != true) {
ret = SDCARD_SEND_COMMADN_FAILURE;
goto cleanup;
}
sdcard->get_resp(sdcard->host, &resp);
sdcard_dump_response(&resp);
ret = SDCARD_SUCCESS;
cleanup:
SDCARD_TRACE("%s ret:%d\n", __func__, ret);
return ret;
}
- 调用sdcard_set_blk_len_cmd16设置卡的block大小
static uint32_t sdcard_set_blk_len_cmd16(sdcard_t *sdcard)
{
bool res = true;
uint32_t ret = SDCARD_SUCCESS;
sd_resp_t resp;
res = sdcard->send_cmd(sdcard->host, CMD16, 512);
if (res != true) {
ret = SDCARD_SEND_COMMADN_FAILURE;
goto cleanup;
}
sdcard->get_resp(sdcard->host, &resp);
sdcard_dump_response(&resp);
ret = SDCARD_SUCCESS;
cleanup:
SDCARD_TRACE("%s ret:%d\n", __func__, ret);
return ret;
}
至此SD卡的初始化完成了,其过程有点类似于USB的枚举,但是比起USB的枚举还是简单的多。
3.2.3 SD卡读写
SD卡初始化完成后,读写就非常简单了。在sd_card.c中仅仅只是调用了host的read_block和write_block函数。
uint32_t sdcard_read_block(sdcard_t *sdcard, uint8_t *dst, uint32_t blk_idx)
{
return sdcard->read_block(sdcard->host, dst, blk_idx) == true ?
SDCARD_SUCCESS : SDCARD_SEND_COMMADN_FAILURE;
}
uint32_t sdcard_write_block(sdcard_t *sdcard, uint8_t *src, uint32_t blk_idx)
{
return sdcard->write_block(sdcard->host, src, blk_idx) == true ?
SDCARD_SUCCESS : SDCARD_SEND_COMMADN_FAILURE;
}
3.3 测试函数
测试函数很简单,首先定义了sdcard_t对象,然后将usdhc host端的函数赋给sdcard各个接口,这样sdcard_t就对应上了usdhc的这个host。
entry.c
static void test_sdcard()
{
sdcard_t sdcard;
imx_usdhc_t *usdhc = (imx_usdhc_t *)0x02190000;
uint8_t buf[512];
uint8_t buf2[512];
uint32_t i;
sdcard.host = usdhc;
sdcard.rca = 0x45670000;
sdcard.host_init = usdhc_init;
sdcard.send_cmd = usdhc_send_command;
sdcard.get_resp = usdhc_get_response;
sdcard.read_block = usdhc_read_block;
sdcard.write_block = usdhc_write_block;
```c
然后将测试buf前16个byte打印出来
```c
printf("\ninit buf as 0\n");
for (i = 0; i < 16; i++) {
printf("%x ", buf[i]);
}
printf("\n");
接着初始化sd卡,然后从卡上读取一个block出来到buf中,并将读出的数据打印出来。并将buf2的数据在buf的数据加上1
sdcard_init(&sdcard);
sdcard_read_block(&sdcard, buf, 0);
printf("\nread sdcard before write\n");
for (i = 0; i < 16; i++) {
printf("%x ", buf[i]);
buf2[i] = buf[i] + 1;
}
printf("\n");
将处理过的buf2写入sd卡的第0个block。然后再将其读出到buf中并打印出来,可以看到sd卡前16个byte已经加了1了。
printf("write sdcard by add 1\n");
sdcard_write_block(&sdcard, buf2, 0);
sdcard_read_block(&sdcard, buf, 0);
printf("\nread sdcard after write\n");
for (i = 0; i < 16; i++) {
printf("%x ", buf[i]);
}
printf("\n");
while(1);
}
测试使用了一个test.img的文件作为测试文件,以其中一次测试为例。在测试程序运行前,前16个byte的数据是
00000000: 0405 0607 0809 0a0b 0c0d 0e0f 1011 1213 ................
在运行完程序后,前16个byte的数据皆增加了1。
00000000: 0506 0708 090a 0b0c 0d0e 0f10 1112 1314 ................
以下就是test_sdcard运行的完整log,注意需要把#define SDCARD_DEBUG定义在头文件中打开SD协议层的打印。
hello imx6ul bare metal:00000000
init buf as 0
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
sdcard_init entry
sdcard_device_init entry
sdcard_dump_response: rsp0:00000120
sdcard_dump_response: rsp1:00000000
sdcard_dump_response: rsp2:00000000
sdcard_dump_response: rsp3:00000000
sdcard_send_cmd55 ret:0
sdcard_dump_response: rsp0:80ffff00
sdcard_dump_response: rsp1:00000000
sdcard_dump_response: rsp2:00000000
sdcard_dump_response: rsp3:00000000
sdcard_get_ocr_acmd41 ret:0
sdcard_dump_response: rsp0:beef0062
sdcard_dump_response: rsp1:2101dead
sdcard_dump_response: rsp2:51454d55
sdcard_dump_response: rsp3:00aa5859
sdcard_get_cid_cmd2: sd card product name:YQEMU
sdcard_get_cid_cmd2 ret:0
sdcard_dump_response: rsp0:45670500
sdcard_dump_response: rsp1:00000000
sdcard_dump_response: rsp2:00000000
sdcard_dump_response: rsp3:00000000
sdcard_set_rca_cmd3 ret:0
sdcard_dump_response: rsp0:00000700
sdcard_dump_response: rsp1:00000000
sdcard_dump_response: rsp2:00000000
sdcard_dump_response: rsp3:00000000
sdcard_select_card_cmd7 ret:0
sdcard_dump_response: rsp0:00000900
sdcard_dump_response: rsp1:00000000
sdcard_dump_response: rsp2:00000000
sdcard_dump_response: rsp3:00000000
sdcard_set_blk_len_cmd16 ret:0
sdcard_init ret:0
read sdcard before write
00000007 00000008 00000009 0000000a 0000000b 0000000c 0000000d 0000000e 0000000f 00000010 00000011 00000012 00000013 00000014 00000015 00000016
write sdcard by add 1
read sdcard after write
00000008 00000009 0000000a 0000000b 0000000c 0000000d 0000000e 0000000f 00000010 00000011 00000012 00000013 00000014 00000015 00000016 00000017
4 移植FATFS
本节移植最新的FATFS到系统上。
首先下载最新的FATFS:
FatFs R0.14 然后把FATFS目录下的c文件放到fatfs目录,将头文件放在include目录下如下:
4.1 FATFS初始化
移植FATFS,我们只需要重写diskio.c即可。
首先实现disk_initialize函数。
diskio.c
#define DEV_SD 0
static sdcard_t s_sdcard;
static bool s_is_sdcard_init;
DSTATUS disk_initialize (
BYTE pdrv
)
{
DSTATUS stat;
uint32_t result;
s_is_sdcard_init = false;
switch(pdrv) {
case DEV_SD:
result = disk_init_sdcard();
break;
default:
stat = STA_NODISK;
break;
}
if (result != SDCARD_SUCCESS) {
stat = STA_NOINIT;
} else {
s_is_sdcard_init = true;
stat = RES_OK;
}
DSIKIO_TRACE("%s stat:%x\n", __func__, stat);
return stat;
}
代码很简单,demo中只支持一个SD标准卡设备,当pdrv不是SD卡的时候,返回STA_NODISK。当pdrv是SD卡的时候,调用disk_init_sdcard初始化SD。
static uint32_t disk_init_sdcard()
{
imx_usdhc_t *usdhc = (imx_usdhc_t *)0x02190000;
DSIKIO_TRACE("%s entry\n", __func__);
s_sdcard.host = usdhc;
s_sdcard.rca = 0x45670000;
s_sdcard.host_init = usdhc_init;
s_sdcard.send_cmd = usdhc_send_command;
s_sdcard.get_resp = usdhc_get_response;
s_sdcard.read_block = usdhc_read_block;
s_sdcard.write_block = usdhc_write_block;
return sdcard_init(&s_sdcard);
}
disk_init_sdcard的代码与上一节的测试程序相似,将SD卡和usdhc绑定起来,然后调用sdcard_init初始化卡即可。
4.2 FATFS读写函数
disk_read 的实现也很简单,最终就是调用sdcard_read去调用驱动读block上的数据。这里需要注意的是传入sdcard_read的地址是sector * 512。这是由于qemu上的SD卡是标准卡。数据地址在标准卡中是以字节为单位的,而高容量卡中,是以块(512byte)为单位的。
DRESULT disk_read (
BYTE pdrv,
BYTE *buff,
LBA_t sector,
UINT count
)
{
DSTATUS ret;
uint32_t res = SDCARD_SUCCESS;
if (pdrv > 0) {
ret = STA_NODISK;
goto cleanup;
}
DSIKIO_TRACE("%s buff:%x, sector:%x, count:%x\n", __func__,
buff, sector, count);
while ((count > 0) || (res != SDCARD_SUCCESS)) {
res = sdcard_read_block(&s_sdcard, buff, sector * 512);
count--;
sector++;
}
ret = (res == SDCARD_SUCCESS) ? RES_OK : RES_PARERR;
cleanup:
DSIKIO_TRACE("%s ret:%x, buff[0]:%x, buff[1]:%x\n", __func__, ret, buff[0], buff[1]);
return ret;
}
disk_write的实现与disk_read类似,最终调用sdcard_write_block去写SD卡。
#if FF_FS_READONLY == 0
DRESULT disk_write (
BYTE pdrv,
const BYTE *buff,
LBA_t sector,
UINT count
)
{
DSTATUS ret;
uint32_t res = SDCARD_SUCCESS;
if (pdrv > 0) {
ret = STA_NODISK;
goto cleanup;
}
DSIKIO_TRACE("%s buff:%x, sector:%x, count:%x\n", __func__,
buff, sector, count);
while ((count > 0) || (res != SDCARD_SUCCESS)) {
res = sdcard_write_block(&s_sdcard, buff, sector * 512);
count--;
sector++;
}
ret = (res == SDCARD_SUCCESS) ? RES_OK : RES_PARERR;
cleanup:
DSIKIO_TRACE("%s ret:%x, buff[0]:%x, buff[1]:%x\n", __func__, ret, buff[0], buff[1]);
return ret;
}
#endif
4.3 其他函数实现
DSTATUS disk_status (
BYTE pdrv
)
{
DSTATUS stat;
switch(pdrv) {
case DEV_SD:
stat = s_is_sdcard_init == true ? RES_OK : STA_NOINIT;
break;
default:
stat = STA_NODISK;
break;
}
return stat;
}
DRESULT disk_ioctl (
BYTE pdrv,
BYTE cmd,
void *buff
)
{
return RES_OK;
}
至此,FATFS的移植就完成了,很简单,实现这5个函数即可。
4.4 测试
4.4.1 创建测试文件系统
创建一个128M的FAT32文件系统文件
~/6ul_study/6ul_bare_metal$ mkfs.msdos -F 32 -C testfs.img 131072
mkfs.fat 4.1 (2017-01-24)
挂载到/mnt/sdcard下,添加aa.txt,往aa.txt中写入"aa.txt:hello fatfs!"。然后umount掉,这样一个测试的文件系统就创建了。包含一个aa.txt的FAT32文件系统。
~/6ul_study/6ul_bare_metal$sudo mount -t msdos -o loop testfs.img /mnt/sdcard/
~/6ul_study/6ul_bare_metal$sudo vim /mnt/sdcard/aa.txt
~/6ul_study/6ul_bare_metal$ ls /mnt/sdcard/
aa.txt
~/6ul_study/6ul_bare_metal$ sudo umount /mnt/sdcard
~/6ul_study/6ul_bare_metal$ fdisk -l testfs.img
Disk testfs.img: 128 MiB, 134217728 bytes, 262144 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x00000000
4.4.2 测试函数
挂载SD卡上的文件系统到fs,然后dump出 fs的信息。
entry.c
static void test_fatfs()
{
FATFS fs;
FIL file;
char buf[64];
char *test_str = "test fatfs string";
uint32_t len = 0;
FRESULT res = 0;
printf("%s entry\n", __func__);
res = f_mount(&fs,"0:",1);
printf("%s f_mount res:%d\n", __func__, res);
然后打开文件aa.txt,读出内容到buf中,并打印。
res = f_open(&file, "aa.txt", FA_READ);
printf("%s f_open res:%d\n", __func__, res);
f_read(&file, buf, 64, &len);
printf("%s read content: buf:%s\n", __func__, buf);
f_close(&file);
然后打开一个不存在的文件bb.txt,以FA_CREATE_NEW | FA_WRITE方式打开,将char *test_str = “test fatfs string”;写入到bb.txt中
res = f_open(&file, "bb.txt", FA_CREATE_NEW | FA_WRITE);
f_write(&file, test_str, 64, &len);
f_close(&file);
最后再重新打开bb.txt读取文件内容到buf中打印出来
res = f_open(&file, "bb.txt", FA_READ);
printf("%s f_open res:%d\n", __func__, res);
f_read(&file, buf, 64, &len);
printf("%s read content: buf:%s\n", __func__, buf);
f_close(&file);
while(1);
}
运行log:
:~/6ul_study/6ul_bare_metal$ make fatfs
hello imx6ul bare metal:00000000
test_fatfs entry
test_fatfs f_mount res:0
===========dump_fatfs=========
fs_type: 00000003
pdrv: 00000000
csize: 00000001
n_fats: 00000002
wflag: 00000000
fsi_flag: 00000000
id: 00000001
n_rootdir: 00000000
last_clst:00000006
free_clst:0003f01b
n_fatent:0003f020
fsize:000007e1
volbase:00000000
fatbase:00000020
dirbase:00000002
winsect:00000001
======================
test_fatfs f_open res:0
test_fatfs read content: buf:aa.txt:hello fatfs!
test_fatfs f_open res:0
test_fatfs read content: buf:test fatfs string
最后重新挂载一下testfs.img,可以看到在该文件系统下存在了bb.txt
~/6ul_study/6ul_bare_metal$ sudo mount -t msdos -o loop testfs.img /mnt/sdcard/
~/6ul_study/6ul_bare_metal$ ls /mnt/sdcard/
aa.txt bb.txt