1. 适用范围
本文档为实现Nuc970平台的SD驱动总结,提供一些SylixOS SD驱动移植方法的参考。
2. 原理概述
2.1 控制器类型
SD控制器有两种类型,分为SD标准控制器(SDHCI)和SD非标准控制器。
SylixOS Base代码中实现了SDHCI的驱动,但Nuc970的SD控制器是非标准控制器,其功能都需要在BSP中单独实现。
2.2 命令、应答、数据
SD传输过程中会有命令、应答和数据三个概念存在。
命令和应答都是在CMD线上传输的,数据在DAT线上进行传输。
2.2.1 命令
命令有四种类型:
广播无应答命令(bc) 此命令SD总线上的各个设备都会接收到。
广播有应答命令(bcr) 此命令当SD总线上各个设备的CMD线都是独立时,都会接收到,并且各自独立的在CMD线上产生应答。
点对点无数据命令(ac) 此命令不会在DAT线上产生数据。
点对点有数据命令(adtc) 此命令会在DAT线上产生数据。
有的命令是可以带参数的,在SD标准协议中定义了如图 2-1所示的命令,标准协议对每条命令是否可以带参数、所带参数的格式、应答类型以及功能都进行了详细地定义。
图 2-1 SD标准协议中定义的CMD
2.2.2 应答
应答有五种类型:
(1)R1和R1b
R1和R1b是最常见的应答类型,如图 2-2所示。R1b较R1不同之处为应答产生后R1b会在DAT线上产生busy标志。
图 2-2 R1和R1b应答
(2)R2
R2应答如图 2-3所示,用于读取SD卡中的CID和CSD寄存器,CID和CSD寄存器记录了SD卡的相关信息。
图 2-3 R2应答
(3)R3
R3应答如图 2-4所示,用于读取SD卡中的OCR寄存器,OCR寄存器记录了SD卡电压支持情况。
图 2-4 R3应答
(4)R6
R6应答如图 2-5所示,用于获取SD卡中的RCA,RCA寄存器用于存储SD卡识别后被设置的16bits卡地址。
图 2-5 R6应答
(5)R7
R7应答如图 2-6所示,用于获取SD卡接口状态,比如电压是否处于可兼容状态。
图 2-6 R7应答
2.2.3 数据
SD的数据传输分为单块传输和多块传输两种。
单块读取的流程如图 2-7所示。
图 2-7单块读取流程
多块读取的流程如图 2-8所示。
图 2-8多块读取流程
2.3 SD卡识别流程
SD卡在被插入SD卡槽后,系统会发送一系列的命令对SD卡进行初始化、获取电压支持等操作。当SD卡被成功识别后,系统会根据SD卡的CID和CSD读取出如所示的SD卡信息,如图 2-9所示。
图 2-9 SD卡信息
SD卡基本识别流程如图 2-10所示,识别之后的SD卡会在/dev/blk下创建设备节点sdcard-0。
图 2-10 SD卡识别流程
2.4 文件系统挂载
SD识别之后,系统会读取SD卡中的首扇区,其中定义了根目录入口簇号、FAT表占用扇区数、文件系统信息扇区号等信息,SD协议栈会根据这些信息读取到目录节点、文件节点等信息,并完成文件系统的挂载。
3. 技术实现
3.1 驱动框架
3.1.1 SD总线适配器创建
SD驱动需要首先创建SD总线适配器,如程序清单 3-1所示,创建适配器时需要注册总线的操作集。
程序清单 3-1 SD总线适配器创建
_G_sdfunc.SDFUNC_pfuncMasterXfer = __sdTransfer; _G_sdfunc.SDFUNC_pfuncMasterCtl = __sdIoCtl; iRet = API_SdAdapterCreate(__SDHOST_NAME,&_G_sdfunc); if (iRet != ERROR_NONE) { printk("__err2\n"); goto __err2; }
3.1.2 热插拔检测
Nuc970的SD控制器在拔插卡之后会产生中断,所以直接在中断中判断SD卡的状态,并通知系统创建SD存储设备节点或移除SD存储设备节点。
3.1.3 SD中断处理
SD中断处理逻辑如图 3-1所示,中断处理主要包括了以下几点内容:
- 传输CRC校验出错
- 应答超时出错
- DMA传输结束置位
- 热插拔状态检测
图 3-1 SD中断处理流程
3.2 通信流程
通信流程如图 3-2所示。命令是否发送完成和应答是否产生,Nuc970都只提供了可轮询检查的状态位CO和RI;当有数据进行发送时,同时需要对DO位进行置位;当有数据需要读取时,同时需要对DI位进行置位。读写操作由DMA完成,DMA传输结束后会产生中断。
图 3-2通信流程
3.3 代码实现
Nuc970的SD驱动以内核模块的形式提供,整体的代码结构如图 3-3所示。
图 3-3 SD驱动文件结构
3.3.1 发送命令
发送命令的基本代码实现如程序清单 3-2所示。
程序清单 3-2发送命令__mciSendCmd
INT __mciSendCmd (PSDIO_DAT psdio) { …… __SD_FUNCTION_ENABLE(); uiRegCtl = readl(REG_SDH_CTL)& (~((BIT_CTL_CMD_CODE_MASK)| (BIT_CTL_EN_MASK))); if (psdio->SDIO_bData) { /* * 有数据发送 */ …… psdio->SDIO_eCompleteWhat = COMPLETION_XFERFINISH_RSPFIN; } else if(SD_CMD_TEST_RSP((psdio->SDIO_psdcmd), SD_RSP_PRESENT)) { /* * 只有应答,没有数据发送 */ …… psdio->SDIO_eCompleteWhat = COMPLETION_RSPFIN; } else { /* * 只发送命令 */ psdio->SDIO_eCompleteWhat = COMPLETION_CMDSENT; } writel(psdio->SDIO_psdcmd->SDCMD_uiArg, REG_SDH_CMD); /* 将参数填入命令寄存器 */ uiRegCtl |= (psdio->SDIO_psdcmd->SDCMD_uiOpcode << 8) | /* 将Opcode填入寄存器 */ (BIT_CTL_CO_EN); psdio->SDIO_ucEvent |= SD_EVENT_CMD_OUT; /* 添加发送命令事件 */ writel(uiRegCtl, REG_SDH_CTL); __mciWakeupQueue(psdio->SDIO_hSdioSync); /* 通知事件处理线程 */ return(ERROR_NONE); }
发送命令之后会唤醒等待事件线程,如程序清单 3-3所示,在此线程中会轮询相关状态位,判断发送和应答状态。
程序清单 3-3事件处理线程
#define __WAIT_COMPLETE(ulMsk, x) while (1) { if (!(readl(REG_SDH_CTL) & ulMsk)) { x = 1; break; } } static PVOID __mciSdioThread (PVOID pvArg) { …… for (;;) { __mciIoWaitQueue(psdio->SDIO_hSdioSync); /* 等待事件处理消息 */ …… if (ucEvent & SD_EVENT_CMD_OUT) { __WAIT_COMPLETE(BIT_CTL_CO_EN, ucCompleted); /* 等待命令发送完成 */ } …… if (ucCompleted) { __sdCompletedCommand(psdio, ucEvent); /* 通知事件处理完成 */ } } return(LW_NULL); }
3.3.2 接收应答
接收应答需要在调用发送命令之后,只是对于需要产生应答的命令,在发送时同时需要设置应答接收类型和事件,代码实现如程序清单 3-4所示。
程序清单 3-4发送命令时设置应答
INT __mciSendCmd (PSDIO_DAT psdio) { …… __SD_FUNCTION_ENABLE(); …… if (psdio->SDIO_bData) { /* * 需要应答 */ psdio->SDIO_ucEvent |= SD_EVENT_RSP_IN; psdio->SDIO_eCompleteWhat = COMPLETION_XFERFINISH_RSPFIN; } else if(SD_CMD_TEST_RSP((psdio->SDIO_psdcmd), SD_RSP_PRESENT)) { /* * 需要应答 */ if ((psdio->SDIO_psdcmd->SDCMD_uiFlag & SD_RSP_R2) == SD_RSP_R2) { /* * 应答类型为R2 */ uiRegCtl |= BIT_CTL_R2_EN; psdio->SDIO_ucEvent |= SD_EVENT_RSP2_IN; } else { /* * 应答类型为R1 */ uiRegCtl |= BIT_CTL_RI_EN; psdio->SDIO_ucEvent |= SD_EVENT_RSP_IN; } psdio->SDIO_eCompleteWhat = COMPLETION_RSPFIN; } else { psdio->SDIO_eCompleteWhat = COMPLETION_CMDSENT; } …… /* 发送命令 */ __mciWakeupQueue(psdio->SDIO_hSdioSync); /* 通知事件处理线程 */ return(ERROR_NONE); }
同样,在事件处理线程中需要对应答状态进行轮询,如程序清单 3-5所示。
程序清单 3-5事件处理线程处理应答
static PVOID __mciSdioThread (PVOID pvArg) { …… for (;;) { __mciIoWaitQueue(psdio->SDIO_hSdioSync); /* 等待事件处理消息 */ …… if (ucEvent & SD_EVENT_RSP_IN) { /* 应答类型R1 */ …… while(1) { if (!(readl(REG_SDH_CTL) & BIT_CTL_RI_EN)) { /* 等待应答 */ ucCompleted =1; break; } if (readl(REG_SDH_INTSTS) & BIT_INTSTS_RITO_IF) { /* 传输超时 */ …… break; } } } if (ucEvent & SD_EVENT_RSP2_IN) { /* 应答类型R2 */ __WAIT_COMPLETE(BIT_CTL_R2_EN, ucCompleted); } …… if (ucCompleted) { __sdCompletedCommand(psdio, ucEvent); /* 通知事件处理完成 */ } } return(LW_NULL); }
当应答状态位被置位后,就可以调用如程序清单 3-6所示代码,读取出应答的内容。
程序清单 3-6应答产生后调用
static VOID __sdCompletedCommand(PSDIO_DAT psdio, UINT8 ucEvent) { …… UINT32 uiIntSts = readl(REG_SDH_INTSTS); if (uiIntSts & BIT_INTSTS_RITO_IF) { writel(BIT_INTSTS_RITO_IF, REG_SDH_INTSTS); …… } if (psdio->SDIO_eCompleteWhat == COMPLETION_RSPFIN || psdio->SDIO_eCompleteWhat == COMPLETION_XFERFINISH_RSPFIN) { if (ucEvent & SD_EVENT_RSP_IN) { psdio->SDIO_psdcmd->SDCMD_uiResp[0] = (readl(REG_SDH_RESP0) << 8) | (readl(REG_SDH_RESP1) & 0xff); psdio->SDIO_psdcmd->SDCMD_uiResp[1] = 0; psdio->SDIO_psdcmd->SDCMD_uiResp[2] = 0; psdio->SDIO_psdcmd->SDCMD_uiResp[3] = 0; } else if (ucEvent & SD_EVENT_RSP2_IN) { pucPtr = (UINT8 *)REG_SDH_FB0; for (i = 0, j = 0; j < 5; i += 4, j++) { uiTmp[j] = (*(pucPtr + i) << 24) | (*(pucPtr + i + 1) << 16) | (*(pucPtr + i + 2) << 8) | (*(pucPtr + i + 3)); } for (i = 0; i < 4; i++) { psdio->SDIO_psdcmd->SDCMD_uiResp[i] = ((uiTmp[i] & 0x00ffffff) << 8) | ((uiTmp[i + 1] & 0xff000000) >> 24); } } } …… psdio->SDIO_eCompleteWhat = COMPLETION_NONE; __mciRequestDone(psdio); }
3.3.3 读取和写入数据
在数据读写之前需要配置DMA传输的块大小,如程序清单 3-7所示。
程序清单 3-7数据传输前准备
static INT __sendDataPrepare (PSDIO_DAT psdio) { …… /* * 设置块传输的块大小 */ uiBlkSiz = psdio->SDIO_psddata->SDDAT_uiBlkSize; uiBlkLen = psdio->SDIO_psddata->SDDAT_uiBlkNum; writel(uiBlkSiz - 1, REG_SDH_BLEN); if ((uiBlkSiz > 512) || (uiBlkLen >= 256)) { printk("ERROR: don‘t support read/write 256 blocks in on CMD\n"); } else { uiRegCtl = readl(REG_SDH_CTL) & ~0x00ff0000; uiRegCtl |= (uiBlkLen << 16); writel(uiRegCtl, REG_SDH_CTL); } /* * 设置DMA传输基址 */ writel((UINT32)psdio->SDIO_puiBuf, REG_SDH_DMASA); return(ERROR_NONE); }
4. 总结
本文章结合Nuc970说明了SylixOS中非标SD驱动移植的方法,标准SD驱动实现方法可能和本文介绍内容差异较大。