嵌入式Linux之我行——S3C2440上MMC/SD卡驱动实例开发讲解(一)嵌入式Linux之我行,主要讲述和总结了本人在学习嵌入式linux中的每个步骤。一为总结经验,二希望能给想入门嵌入式Linux的朋友提供方便。如有错误之处,谢请指正。一、开发环境- 主 机:VMWare--Fedora 9
- 开发板:Mini2440--64MB Nand, Kernel:2.6.30.4
- 编译器:arm-linux-gcc-4.3.2
二、MMC/SD介绍及SDI主机控制器 首先我们来理清几个概念:- MMC:(Multi Media Card)由西门子公司和首推CF的SanDisk于1997年推出的多媒体记忆卡标准。
- SD:(Secure Digital Memory Card)由日本松下、东芝及美国SanDisk公司于1999年8月共同开发研制的新一代记忆卡标准,已完全兼容MMC标准。
- SDIO:(Secure Digital Input and Output Card)安全数字输入输出卡。SDIO是在SD标准上定义了一种外设接口,通过SD的I/O接脚来连接外围设备,并且通过SD上的 I/O数据接位与这些外围设备进行数据传输。是目前较热门的技术,如下图中的一些设备:GPS、相机、Wi-Fi、调频广播、条形码读卡器、蓝牙等。
- 工作模式:工作模式是针对主机控制器来说的。也就是说,S3C2440中的SDI控制器可以在符合MMC的标准下工作,或者可以在符合SD的标准下工作,或者可以在符合SDIO的标准下工作。故就分别简称为:MMC模式、SD模式和SDIO模式。
- 传输模式:传输模式也是针对主机控制器来说的,指控制器与卡之间数据的传输模式,或者说是总线类型。S3C2440中的SDI控制器可支持SPI、1位和4位的三种传输模式(总线类型)。那么什么又是SPI呢?请参考这里:SPI协议简介;至于1位和4位又是什么意思呢?他们是指传输数据总线的线宽,具体参考数据手册。
下面使用表格列出了MMC、SD、SDIO的电气特性及性能和不同工作模式下支持的传输模式情况:
那么,我们现在怎样让主机控制器在我们所要求的工作模式和传输模式上工作呢?很简单,就是对主机控制器的各个寄存器进行相应的配置即可。下面来简单介绍一下SDI主机控制器的结构和各寄存器的用途。S3C2440内的SDI主机控制器结构图如下:
如上图所示,SDI主机控制器是使用1个串行时钟线与5条数据线同步进行信息移位和采样。传输频率通过设定SDIPRE寄存器的相应位的设定来控制,可以修改频率来调节波特率数据寄存器的值。各主要寄存器介绍,对于具体的寄存器位的设置就参考数据手册:
- SDICON:控制寄存器,完成SD卡基础配置,包括大小端,中断允许,模式选择,时钟使能等。
- SDIPRE:波特率预定标器寄存器,对SDCLK的配置。
- SDICmdArg:指令参数寄存器,指令的参数存放在这里。
- SDICCON:控制指令形式的寄存器,配置SPI还是SDI指令,指令的反馈长度,是否等待反馈,是否运行指令,指令的索引等。
- SDICmdSta:指令状态寄存器,指令是否超时,传送,结束,CRC是否正确等。
- SDIRSP0-3:反映SD的状态。
- SDIDTimer:设置超时时间。
- SDIBSize:模块大小寄存器。
- SDIDatCon:数据控制寄存器,配置是几线传输,数据发送方向,数据传送方式等。
- SDIDatSta:数据状态寄存器,数据是否发送完,CRC效验,超时等。
- SDIFSTA:FIFO状态寄存器,DMA传输是否判断FIFO。
- SDIIntMsk:中断屏蔽寄存器。
- SDIDAT:SDI数据寄存器。
SDI主机控制器在SD/MMC工作模式下的设置步骤:(注意:因为SD模式兼容MMC模式,所以我们只需了解SD模式的即可,而SDIO的工作模式则是针对SDIO设备的,所以这里就不讨论了)
- 设置SDICON寄存器来配置适当的时钟及中断使能;
- 设置SDIPRE寄存器适当的值;
- 等待74个SDCLK时钟以初始化卡;
- 命令操作步骤:
a. 写命令参数32位到SDICmdArg寄存器;
b. 设置命令类型并通过设置SDICCON寄存器开始命令传输;
c. 当SDICSTA寄存器的特殊标志被置位,确认命令操作完成;
d. 如果命令类型相应,标志是RspFin,否则标志是CmdSend;
e. 通过对相应位写1,清除SDICmdSta的标志。 - 数据操作步骤:
a. 写数据超时时间到SDIDTimer寄存器;
b. 写模块大小到SDIBSize寄存器(通常是0x80字节);
c. 确定模块模式、总线线宽、DMA等且通过设置SDIDatCon寄存器开始数据传输;
d. 发送数据->写数据到SDIDAT寄存器,当发送FIFO有效(TFDET置位),或一半(TFHalf置位),或空(TFEmpty置位);
e. 接收数据->从数据寄存器SDIDAT读数据,当接收FIFO有效(RFDET置位),或满(RFFull置位),或一半(RFHalf置位),或准备最后数据(RFLast置位);
f. 当SDIDatSta寄存器的DatFin标志置位,确认数据操作完成;
g. 通过对相应位写1,清除SDIDatSta的标志。
三、MMC/SD协议 这里我并不是要讨论MMC/SD的整个通信协议(详细的协议请看MMC/SD规范),而是遵循MMC/SD协议了解一下MMC/SD在被驱动的过程中卡所处的各种阶段和状态。根据协议,MMC/SD卡的驱动被分为:卡识别阶段和数据传输阶段。在卡识别阶段通过命令使MMC/SD处于:空闲(idle)、准备(ready)、识别(ident)、等待(stby)、不活动(ina)几种不同的状态;而在数据传输阶段通过命令使MMC/SD处于:发送(data)、传输(tran)、接收(rcv)、程序(prg)、断开连接(dis)几种不同的状态。所以可以总结MMC/SD在工作的整个过程中分为两个阶段和十种状态。下面使用图形来描述一下在两个阶段中这十种状态之间的转换关系。卡识别阶段,如下图:
数据传输阶段,如下图:
四、MMC/SD设备驱动在Linux中的结构层次 我们翻开MMC/SD设备驱动代码在Linux源码中的位置/linux-2.6.30.4/drivers/mmc/,乍一看,分别有card、core和host三个文件夹,那哪一个文件才是我们要找的驱动代码文件啊?答案是他们都是。不是吧,听起来有些可怕,三个文件夹下有多少代码啊。呵呵,还是先放下对庞大而又神秘代码的恐惧感吧,因为在实际驱动开发中,其实只需要在host文件夹下实现你具体的MMC/SD设备驱动部分代码,现在你的心情是不是要好点了。具体的MMC/SD设备是什么意思呢?他包括RAM芯片中的SDI控制器(支持对MMC/SD卡的控制,俗称MMC/SD主机控制器)和SDI控制器与MMC/SD卡的硬件接口电路。 那为什么刚才又说card、core和host都是MMC/SD设备的驱动呢?这就好比我们建房子,建房子首先要的是什么,地皮对吧, 有了地皮然后要到政府部门备案,备案后才能开始建,是这样的吧。在Linux中MMC/SD卡的记忆体都当作块设备。那么,我们这里的card层就是要把操作的数据以块设备的处理方式写到记忆体上或从记忆体上读取,就好比是在地皮上填沙石、挖地基等;core层则是将数据以何种格式,何种方式在MMC/SD主机控制器与MMC/SD卡的记忆体(即块设备)之间进行传递,这种格式、方式被称之为规范或协议,就好比到政府部门备案,备案就会要求你的房子应该按照怎样的行业标准进行建造;最后只剩下host层了,上面也讲到了,host层下的代码就是你要动手实现的具体MMC/SD设备驱动了,就好比现在地皮买好挖好了,建房的标准也定好了,剩下的就需要人开始动工了。 那么,
card、core和host这三层的关系,我们用一幅图来进行描述,图如下:
从这幅图中的关系可以看出,整个MMC/SD模块中最重要的部分是Core核心层,他提供了一系列的接口函数,对上提供了将主机驱动注册到系统,给应用程序提供设备访问接口,对下提供了对主机控制器控制的方法及块设备请求的支持。对于主机控制器的操作就是对相关寄存器进行读写,而对于MMC/SD设备的请求处理则比较复杂。那么在主机驱动层中的一个请求处理是怎么通过核心层提交到块设备请求层的呢? 在网上找到一副图来说明他们之间的关联和处理流程,如下图:
命令、数据发送流程如下图: 其中,黑 {MOD}粗线部分为命令发送或者数据发送都要经过的流程,橙 {MOD}方框部分判断所有类型的请求是否完成。 下面我们就来具体实例分析一个MMC/SD卡设备驱动程序。 五、实例分析MMC/SD卡设备驱动程序- Mini2440开发板的MMC/SD硬件接口电路原路图如下:
从电路原理图上可以看出,SD分别使用S3C2440的复用IO端口GPE7-10作为4根数据信号线、使用GPE6作命令信号线、使用GPE5作时钟信号线,使用复用端口GPG8的外部中断功能来作SD卡的插拔检测,使用GPH8端口来判断SD卡是否写有保护。
- MMC/SD卡驱动程序的重要数据结构,该结果位于Core核心层,主要用于核心层与主机驱动层的数据交换处理。定义在/include/linux/mmc/host.h中:
struct mmc_host
{
struct device *parent;
struct device class_dev;
int index;
const struct mmc_host_ops *ops;
unsigned int f_min;
unsigned int f_max;
u32 ocr_avail;
#define MMC_VDD_165_195 0x00000080 /* VDD voltage 1.65 - 1.95 */
#define MMC_VDD_20_21 0x00000100 /* VDD voltage 2.0 ~ 2.1 */
#define MMC_VDD_21_22 0x00000200 /* VDD voltage 2.1 ~ 2.2 */
#define MMC_VDD_22_23 0x00000400 /* VDD voltage 2.2 ~ 2.3 */
#define MMC_VDD_23_24 0x00000800 /* VDD voltage 2.3 ~ 2.4 */
#define MMC_VDD_24_25 0x00001000 /* VDD voltage 2.4 ~ 2.5 */
#define MMC_VDD_25_26 0x00002000 /* VDD voltage 2.5 ~ 2.6 */
#define MMC_VDD_26_27 0x00004000 /* VDD voltage 2.6 ~ 2.7 */
#define MMC_VDD_27_28 0x00008000 /* VDD voltage 2.7 ~ 2.8 */
#define MMC_VDD_28_29 0x00010000 /* VDD voltage 2.8 ~ 2.9 */
#define MMC_VDD_29_30 0x00020000 /* VDD voltage 2.9 ~ 3.0 */
#define MMC_VDD_30_31 0x00040000 /* VDD voltage 3.0 ~ 3.1 */
#define MMC_VDD_31_32 0x00080000 /* VDD voltage 3.1 ~ 3.2 */
#define MMC_VDD_32_33 0x00100000 /* VDD voltage 3.2 ~ 3.3 */
#define MMC_VDD_33_34 0x00200000 /* VDD voltage 3.3 ~ 3.4 */
#define MMC_VDD_34_35 0x00400000 /* VDD voltage 3.4 ~ 3.5 */
#define MMC_VDD_35_36 0x00800000 /* VDD voltage 3.5 ~ 3.6 */
unsigned long caps; /* Host capabilities */
#define MMC_CAP_4_BIT_DATA (1 << 0)/* Can the host do 4 bit transfers */
#define MMC_CAP_MMC_HIGHSPEED (1 << 1)/* Can do MMC high-speed timing */
#define MMC_CAP_SD_HIGHSPEED (1 << 2)/* Can do SD high-speed timing */
#define MMC_CAP_SDIO_IRQ (1 << 3)/* Can signal pending SDIO IRQs */
#define MMC_CAP_SPI (1 << 4)/* Talks only SPI protocols */
#define MMC_CAP_NEEDS_POLL (1 << 5)/* Needs polling for card-detection */
#define MMC_CAP_8_BIT_DATA (1 << 6)/* Can the host do 8 bit transfers */
/* host specific block data */
unsigned int max_seg_size; /* see blk_queue_max_segment_size */
unsigned short max_hw_segs; /* see blk_queue_max_hw_segments */
unsigned short max_phys_segs; /* see blk_queue_max_phys_segments */
unsigned short unused;
unsigned int max_req_size; /* maximum number of bytes in one req */
unsigned int max_blk_size; /* maximum size of one mmc block */
unsigned int max_blk_count; /* maximum number of blocks in one req */
/* private data */
spinlock_t lock; /* lock for claim and bus ops */
struct mmc_ios ios; /* current io bus settings */
u32 ocr; /* the current OCR setting */
/* group bitfields together to minimize padding */
unsigned int use_spi_crc:1;
unsigned int claimed:1; /* host exclusively claimed */
unsigned int bus_dead:1; /* bus has been released */
#ifdef CONFIG_MMC_DEBUG
unsigned int removed:1; /* host is being removed */
#endif
struct mmc_card *card; /* device attached to this host */
wait_queue_head_t wq;
struct delayed_work detect;
const struct mmc_bus_ops *bus_ops; /* current bus driver */
unsigned int bus_refs; /* reference counter */
unsigned int sdio_irqs;
struct task_struct *sdio_irq_thread;
atomic_t sdio_irq_thread_abort;
#ifdef CONFIG_LEDS_TRIGGERS
struct led_trigger *led; /* activity led */
#endif
struct dentry *debugfs_root;
unsigned long private[0] ____cacheline_aligned;
};
- MMC/SD卡驱动程序的头文件中一些变量的定义,这些变量在驱动中都会用到。先不用看这些变量将用做什么,等驱动中用到时自然就明白了。代码如下:
#define S3CMCI_DMA 0
enum s3cmci_waitfor
{
COMPLETION_NONE,
COMPLETION_FINALIZE,
COMPLETION_CMDSENT,
COMPLETION_RSPFIN,
COMPLETION_XFERFINISH,
COMPLETION_XFERFINISH_RSPFIN,
};
struct s3cmci_host
{
struct platform_device *pdev;
struct s3c24xx_mci_pdata *pdata;
struct mmc_host *mmc;
struct resource *mem;
struct clk *clk;
void __iomem *base;
int irq;
int irq_cd;
int dma;
unsigned long clk_rate;
unsigned long clk_div;
unsigned long real_rate;
u8 prescaler;
unsigned sdiimsk;
unsigned sdidata;
int dodma;
int dmatogo;
struct mmc_request *mrq;
int cmd_is_stop;
spinlock_t complete_lock;
enum s3cmci_waitfor complete_what;
int dma_complete;
u32 pio_sgptr;
u32 pio_bytes;
u32 pio_count;
u32 *pio_ptr;
#define XFER_NONE 0
#define XFER_READ 1
#define XFER_WRITE 2
u32 pio_active;
int bus_width;
char dbgmsg_cmd[301];
char dbgmsg_dat[301];
char *status;
unsigned int ccnt, dcnt;
struct tasklet_struct pio_tasklet;
#ifdef CONFIG_CPU_FREQ
struct notifier_block freq_transition;
#endif
};
- MMC/SD卡驱动程序的加载与卸载部分:
在Linux中,MMC/SD设备是被作为平台设备添加到系统的。可以查看内核代码:/arch/arm/plat-s3c24xx/devs.c中为MMC/SD主机控制器SDI定义了平台设备和平台设备资源,然后在/arch/arm/mach-s3c2440/mach-smdk2440.c中的系统初始化的时候添加到系统中。如下: //平台设备资源
static struct resource s3c_sdi_resource[] = {
[0] = {
.start = S3C24XX_PA_SDI,
.end = S3C24XX_PA_SDI + S3C24XX_SZ_SDI - 1,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = IRQ_SDI,
.end = IRQ_SDI,
.flags = IORESOURCE_IRQ,
}
};
//定义SDI平台设备
struct platform_device s3c_device_sdi = {
.name = "s3c2410-sdi",
.id = -1,
.num_resources = ARRAY_SIZE(s3c_sdi_resource),
.resource = s3c_sdi_resource,
};
EXPORT_SYMBOL(s3c_device_sdi);
//添加SDI平台设备到平台设备列表
static struct platform_device *smdk2440_devices[] __initdata = {
&s3c_device_usb,
&s3c_device_sdi,
&s3c_device_lcd,
&s3c_device_wdt,
&s3c_device_rtc,
&s3c_device_dm9000,
.
.
.
};
//平台设备添加到系统
static void __init smdk2440_machine_init(void)
{
.
.
.
platform_add_devices(smdk2440_devices, ARRAY_SIZE(smdk2440_devices));
smdk_machine_init();
}
所以,MMC/SD设备驱动程序的加载和卸载部分很简单,就是注册和注销平台设备,代码如下: //加载
static int __init s3cmci_init(void)
{
platform_driver_register(&s3cmci_driver);
return 0;
}
//卸载
static void __exit s3cmci_exit(void)
{
platform_driver_unregister(&s3cmci_driver);
}
//平台设备操作结构体
static struct platform_driver s3cmci_driver = {
.driver.name = "s3c2410-sdi",//名称和平台设备定义中的对应
.driver.owner = THIS_MODULE,
.probe = s3cmci_probe,//平台设备探测接口函数
.remove = __devexit_p(s3cmci_remove),//__devexit_p的作用以前将过
.shutdown = s3cmci_shutdown,
.suspend = s3cmci_suspend,
.resume = s3cmci_resume,
};
- 平台探测函数s3cmci_probe的讲解:
static int __devinit s3cmci_probe(struct platform_device *pdev)
{
//该结构体定义在头文件中,现在实例一个名为host的结构体指针为结构体中的成员赋值做准备
struct s3cmci_host *host;
//实例一个名为mmc的结构体指针,用于与Core核心层中的mmc_host结构体指针相关联
struct mmc_host *mmc;
int ret;
//初始化一个名为complete_lock的自旋锁以备后用,该自旋锁的定义在s3cmci_host结构体中
spin_lock_init(&host->complete_lock);
//初始化一个名为pio_tasklet的tasklet,用于实现中断的底半部机制,底半部服务函数为pio_tasklet,
//将host结构体变量作为服务函数的参数。注意:这里tasklet的变量名与服务函数名称同名了(这是可以的)。
tasklet_init(&host->pio_tasklet, pio_tasklet, (unsigned long) host);
//分配mmc_host结构体指针的内存空间大小,该函数在host.c中实现,这里要注意一点,为什么参数
//是s3cmci_host结构体的大小,到host.c中看,实际这里分配的是mmc_host加s3cmci_host的大小。
mmc = mmc_alloc_host(sizeof(struct s3cmci_host), &pdev->dev);
if (!mmc)
{
ret = -ENOMEM;
goto probe_out;
}
//调用mmc_priv函数将mmc_host和s3cmci_host结构体的对象关联起来,mmc_priv定义在host.h中
host = mmc_priv(mmc);
//下面就开始初始化s3cmci_host结构体的各成员
host->mmc = mmc;
host->pdev = pdev;
host->pdata = pdev->dev.platform_data;
//SDI主机控制器的中断屏蔽寄存器和数据寄存器,他们定义在mach-s3c2410/include/mach/regs-sdi.h中
host->sdiimsk = S3C2440_SDIIMSK;
host->sdidata = S3C2440_SDIDATA;
//complete_what定义在s3cmci_host结构体中,用来记录请求处理所处的当前状态,这里初始化为
//COMPLETION_NONE即无状态,定义在头文件的s3cmci_waitfor中,里面枚举了6种状态。
host->complete_what = COMPLETION_NONE;
//pio_active定义在s3cmci_host结构体中,用来标记请求处理数据在FIFO方式下的数据方向是读还是写
host->pio_active = XFER_NONE;
//dodma和dma方便用于标记是否要使用DMA数据传输方式和DMA通道资源,0表示不使用DMA功能
host->dodma = 0;
host->dma = S3CMCI_DMA;
//从SDI平台设备资源中获取SDI的IO端口资源,该资源在plat-s3c24xx/devs.c的s3c_sdi_resource中指定的
host->mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!host->mem)
{
dev_err(&pdev->dev, "failed to get io memory region resouce.
");
ret = -ENOENT;
goto probe_free_host;
}
//申请SDI的IO端口资源所占用的IO空间(要注意理解IO空间和内存空间的区别)
host->mem = request_mem_region(host->mem->start, RESSIZE(host->mem), pdev->name);
if (!host->mem)
{
dev_err(&pdev->dev, "failed to request io memory region.
");
ret = -ENOENT;
goto probe_free_host;
}
//将SDI的IO端口占用的这段IO空间映射到内存的虚拟地址,ioremap定义在io.h中。
//注意:IO空间要映射后才能使用,以后对虚拟地址的操作就是对IO空间的操作。
host->base = ioremap(host->mem->start, RESSIZE(host->mem));
if (!host->base) {
dev_err(&pdev->dev, "failed to ioremap() io memory region.
");
ret = -EINVAL;
goto probe_free_mem_region;
}
//同样从SDI平台设备资源中获取SDI的中断号
host->irq = platform_get_irq(pdev, 0);
if (host->irq == 0)
{
dev_err(&pdev->dev, "failed to get interrupt resouce.
");
ret = -EINVAL;
goto probe_iounmap;
}
//申请SDI的中断服务,服务函数为s3cmci_irq,主要参数为host
if (request_irq(host->irq, s3cmci_irq, 0, DRIVER_NAME, host))
{
dev_err(&pdev->dev, "failed to request mci interrupt.
");
ret = -ENOENT;
goto probe_iounmap;
}
//在SDI未准备好之前先屏蔽SDI的中断功能
disable_irq(host->irq);
//根据开发板原理图分别设置GPG8、GPH8端口为SD卡插入拔出的检测和有无写保护的检查,
//注意:其实有没有写保护就是检查SD卡侧面有个移动按钮的开关,MMC卡无此功能
host->pdata->gpio_detect = S3C2410_GPG8;
host->pdata->gpio_wprotect = S3C2410_GPH8;
//获取GPG8复用端口中断功能的中断号
host->irq_cd = s3c2410_gpio_getirq(host->pdata->gpio_detect);
//GPG8是复用端口,要使用中断功能则要配置成中断功能,GPG8对应的中断功能是外部中断EINT16,这个数据手册上有讲到
s3c2410_gpio_cfgpin(S3C2410_GPG8, S3C2410_GPG8_EINT16);
//申请SDI的卡检测中断服务,服务函数为s3cmci_irq_cd,主要参数也为host
if (request_irq(host->irq_cd, s3cmci_irq_cd, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, DRIVER_NAME, host))
{
dev_err(&pdev->dev, "can't get card detect irq.
");
ret = -ENOENT;
goto probe_free_irq;
}
//获取DMA通道并申请DMA中断,S3C2440中DMA的使用在后续的文章中再了解
if (s3c2410_dma_request(S3CMCI_DMA, &s3cmci_dma_client, NULL) < 0)
{
dev_err(&pdev->dev, "unable to get DMA channel.
");
ret = -EBUSY;
goto probe_free_irq_cd;
}
//从平台时钟队列