imx6ull-qemu 裸机教程2:USDHC SD卡

文章目录

  • 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

imx6ull-qemu 裸机教程2:USDHC SD卡_第1张图片

  • 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 外部信号

imx6ull-qemu 裸机教程2:USDHC SD卡_第2张图片
demo中用到了SD1_CLK, SD1_CMD, SD1_DATA0/1/2/3,这些pin的iomux默认ALT0就是USDHC1的引脚,因此在代码中不需要去配这些pin的iomux。

1.4 Data Buffer

imx6ull-qemu 裸机教程2:USDHC SD卡_第3张图片
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:

  1. ADMA1: 只支持4K对齐的内存地址。
  2. 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标识符,标识该描述符是否是一个有效的描述符
    imx6ull-qemu 裸机教程2:USDHC SD卡_第4张图片
    ADMA2描述符的数据结构如下图:
  • System address register存着描述符表的地址,即第0个描述符的地址
  • 如果该描述符不是link描述符,那么硬件会顺序的往下取描述符
  • 如果该描述符是link描述符,那么硬件下一个会去查找到link的那个描述符。

USDHC会根据当前描述符去做数据搬运操作。
imx6ull-qemu 裸机教程2:USDHC SD卡_第5张图片

1.6 Register

USDHC的寄存器非常多而且demo中只用到了一部分寄存器,这里不一一介绍了,在第三节中通过代码来介绍所使用到的寄存器。

2 SD协议简介

[注:本节节选自SD2.0标准协议完整版]
SD卡协议也非常长,这里只截取了部分重要的概念,同样的没办法列出所有SD协议的细节,在第三节中通过代码来介绍所使用的SD卡协议。

2.1 SD总线拓扑

imx6ull-qemu 裸机教程2:USDHC SD卡_第6张图片
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):数据可以从主机到卡,也可以从卡到主机。通过数据线传输。
imx6ull-qemu 裸机教程2:USDHC SD卡_第7张图片
卡片寻址通过使用会话地址来实现,会话地址会在初始化阶段分配给卡。命令,响应和数据块的结构在第4 章中描述。SD 总线上的基本交互是命令/响应交互(表格3-4)。这种总线交互直接在命令或者响应的结构里面传输他们的信息。此外,一些操作还有数据内容。
SD 卡发送或接收的数据在块(block)中完成。数据块以CRC 位来保证成功。目前有单块或多块操作。注意:多块操作模式在快速写操作时更好一点。多块传输以命令线上的结束命令为结束标志。主机端可以配置单线还是多线传输。
imx6ull-qemu 裸机教程2:USDHC SD卡_第8张图片
块写操作使用简单的busy 来表示DAT0 数据线上的持续写操作,不管使用几线传输。imx6ull-qemu 裸机教程2:USDHC SD卡_第9张图片

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线完成的。卡识别模式下,所有数据通信都只通过数据线完成。

imx6ull-qemu 裸机教程2:USDHC SD卡_第10张图片

总线激活后, 主机启动卡的初始化和识别进程( 见图-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 
#include 
#include 
#include 

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;            //
    volatile uint32_t mmc_boot;             //
    volatile uint32_t vend_spec2;           //
    volatile uint32_t tuning_ctrl;          //
} 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即可

imx6ull-qemu 裸机教程2:USDHC SD卡_第11张图片
接着调用了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位的定义,定义宏如下:imx6ull-qemu 裸机教程2:USDHC SD卡_第12张图片
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;
}

imx6ull-qemu 裸机教程2:USDHC SD卡_第13张图片
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作为数据传输的方式,起定义如下:
imx6ull-qemu 裸机教程2:USDHC SD卡_第14张图片
然后调用了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)
        ;
}

imx6ull-qemu 裸机教程2:USDHC SD卡_第15张图片

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位。
    imx6ull-qemu 裸机教程2:USDHC SD卡_第16张图片

  • resp_format这个flag标识cmd response的格式。用来设置uSDHCx_CMD_XFR_TYP中的RSPTYP位
    在这里插入图片描述

  • dat_pres用于标识该cmd是否有数据传输。用于设置uSDHCx_CMD_XFR_TYP中的DPSEL位。
    imx6ull-qemu 裸机教程2:USDHC SD卡_第17张图片

  • crc_chk标识是否需要对cmd的crc进行检查,用于设置uSDHCx_CMD_XFR_TYP中的CCCEN位。
    imx6ull-qemu 裸机教程2:USDHC SD卡_第18张图片

  • cmdidx_chk标识是否对cmd的id进行check,用于设置uSDHCx_CMD_XFR_TYP中的CICEN位。
    imx6ull-qemu 裸机教程2:USDHC SD卡_第19张图片

  • blk_cnt_en_chk用于设置uSDHCx_MIX_CTRL的BCEN位imx6ull-qemu 裸机教程2:USDHC SD卡_第20张图片

  • blk_type用于设置uSDHCx_MIX_CTRL的MSBSEL位
    imx6ull-qemu 裸机教程2:USDHC SD卡_第21张图片

  • dma_en用于设置uSDHCx_MIX_CTRL中的DMAEN位。
    imx6ull-qemu 裸机教程2:USDHC SD卡_第22张图片

  • auto_cmd12_en用于设置uSDHCx_MIX_CTRL的AC12EN位。当该位设置后,在传输多个block的时候,当一个block传输完成后,硬件会自动发一个CMD12命令。 imx6ull-qemu 裸机教程2:USDHC SD卡_第23张图片

  • 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状态

imx6ull-qemu 裸机教程2:USDHC SD卡_第24张图片

  • 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]

imx6ull-qemu 裸机教程2:USDHC SD卡_第25张图片

  • 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编号

imx6ull-qemu 裸机教程2:USDHC SD卡_第26张图片

  • 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 分配相对地址到设备

imx6ull-qemu 裸机教程2:USDHC SD卡_第27张图片

  • 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取消所有设备的选定。

imx6ull-qemu 裸机教程2:USDHC SD卡_第28张图片

  • 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 从设备向主机连续传输数据块,直至被停止命令中断,或所要求传输的块数。

这里写图片描述

imx6ull-qemu 裸机教程2:USDHC SD卡_第29张图片

  • 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的步骤只需两步:

  1. 设置好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;
  1. 然后打开所有中断标志位,配置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;
  1. 发送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 
#include 
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目录下如下:
imx6ull-qemu 裸机教程2:USDHC SD卡_第30张图片

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

你可能感兴趣的:(QEMU,ARM,C)