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

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
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標識符,標識該描述符是否是一個有效的描述符
    ADMA2描述符格式
    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的步驟只需兩步:

  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 <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
發佈了22 篇原創文章 · 獲贊 54 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章