1. SDIO驱动开发概述
在嵌入式系统开发中,SD卡作为最常用的存储介质之一,其驱动实现是系统开发的基础环节。树莓派平台通过SDIO(Secure Digital Input Output)接口与SD卡通信,本文将深入解析SDIO驱动的裸机实现细节。
SD卡协议支持多种工作模式,从低速的SPI模式到高速的4-bit SD模式。在树莓派裸机开发中,我们需要直接操作BCM2835/2837芯片组的EMMC控制器寄存器,完成从GPIO配置、卡初始化到数据读写的完整流程。
2. SD卡协议核心要点
2.1 工作模式对比
SD协议定义了三种主要工作模式:
| 模式 | 典型应用场景 | 理论速率 | 数据线数量 |
|---|---|---|---|
| SPI模式 | 低成本MCU | 0-25Mbps | 1 |
| SD 1-bit模式 | 兼容模式 | 0-25Mbps | 1 |
| SD 4-bit模式 | 高速传输 | 0-104Mbps | 4 |
在树莓派开发中,我们优先使用4-bit SD模式以获得最佳性能。模式切换需要通过特定命令序列完成。
2.2 关键命令解析
SD协议命令由6字节组成,格式如下:
code复制[Start(0)] + [传输方向] + [命令号] + [参数(4字节)] + [CRC]
常用命令及其作用:
| 命令 | 代码 | 功能说明 |
|---|---|---|
| CMD0 | 0x00 | 卡复位,进入IDLE状态 |
| CMD8 | 0x08 | 检查卡支持的电压范围 |
| CMD17 | 0x11 | 读取单个数据块 |
| CMD24 | 0x18 | 写入单个数据块 |
| CMD55 | 0x37 | 应用命令前缀 |
| ACMD41 | 0x29 | 初始化卡并设置工作条件 |
2.3 初始化流程详解
完整的SD卡初始化包含以下关键步骤:
- 硬件复位:发送CMD0使卡进入IDLE状态
- 电压检测:通过CMD8确认卡支持的工作电压
- 初始化过程:发送CMD55+ACMD41组合命令
- 身份识别:
- CMD2获取CID(卡识别号)
- CMD3获取RCA(相对卡地址)
- 模式切换:
- CMD7选择卡进入传输状态
- ACMD6切换至4-bit总线模式
在代码实现中,每个命令发送后都需要检查响应和超时,确保操作成功。
3. 博通平台硬件实现
3.1 EMMC控制器架构
BCM2835的EMMC控制器包含以下功能单元:
- 命令引擎:处理命令发送和响应接收
- 数据引擎:管理数据FIFO和DMA传输
- 时钟控制:可编程时钟分频器
- 中断系统:支持11种中断类型
控制器寄存器组位于0x3F300000(树莓派3外设地址空间),共包含63个32位寄存器。
3.2 GPIO配置要点
SDIO功能使用GPIO48-53引脚,需要配置为ALT3功能:
c复制// 配置GPIO48-53为SD功能(ALT3)
r = *GPFSEL4;
r |= (7<<(8*3)) | (7<<(9*3)); // GPIO48-49
*GPFSEL4 = r;
r = *GPFSEL5;
r |= (7<<(0*3)) | (7<<(1*3)) | (7<<(2*3)) | (7<<(3*3)); // GPIO50-53
*GPFSEL5 = r;
同时需要配置引脚的上拉电阻,确保信号稳定性:
c复制// 启用GPIO48-53上拉
*GPPUD = 2; // 上拉使能
wait_cycles(150);
*GPPUDCLK1 = (1<<16)|(1<<17)|(1<<18)|(1<<19)|(1<<20)|(1<<21);
wait_cycles(150);
*GPPUD = 0; // 清除控制
*GPPUDCLK1 = 0; // 移除时钟
3.3 时钟配置策略
SD卡初始化需要分阶段调整时钟频率:
- 初始阶段:400KHz低速时钟,确保稳定通信
- 识别阶段:保持400KHz直到获取RCA
- 传输阶段:提升至25MHz全速运行
时钟分频计算公式:
code复制实际频率 = 核心时钟 / (分频系数 * 2)
其中分频系数写入EMMC_CLOCK寄存器的CDIV字段。
4. 驱动实现关键代码解析
4.1 命令发送函数
c复制int sd_cmd(unsigned int code, unsigned int arg) {
// 检查命令是否需要APP前缀
if(code & CMD_NEED_APP) {
sd_cmd(CMD_APP_CMD | (sd_rca?CMD_RSPNS_48:0), sd_rca);
if(sd_err) return 0;
code &= ~CMD_NEED_APP;
}
// 等待命令通道就绪
if(sd_status(SR_CMD_INHIBIT)) {
sd_err = SD_TIMEOUT;
return 0;
}
// 发送命令
*EMMC_INTERRUPT = *EMMC_INTERRUPT; // 清除中断
*EMMC_ARG1 = arg; // 设置参数
*EMMC_CMDTM = code; // 发送命令(自动触发)
// 特殊命令额外延时
if(code == CMD_SEND_OP_COND) wait_msec(1000);
else if(code == CMD_SEND_IF_COND || code == CMD_APP_CMD) wait_msec(100);
// 等待命令完成
if((r = sd_int(INT_CMD_DONE))) {
sd_err = r;
return 0;
}
// 解析响应
r = *EMMC_RESP0;
// ...响应处理逻辑...
}
4.2 数据块读取实现
c复制int sd_readblock(unsigned int lba, unsigned char *buffer, unsigned int num) {
// 参数检查
if(num < 1) num = 1;
// 等待数据通道就绪
if(sd_status(SR_DAT_INHIBIT)) {
sd_err = SD_TIMEOUT;
return 0;
}
// 设置块大小和数量
*EMMC_BLKSIZECNT = (num << 16) | 512;
// 发送读取命令
if(sd_scr[0] & SCR_SUPP_CCS) { // 高速卡支持
sd_cmd(num == 1 ? CMD_READ_SINGLE : CMD_READ_MULTI, lba);
} else { // 标准卡
sd_cmd(CMD_READ_SINGLE, (lba + c) * 512);
}
// 读取数据
unsigned int *buf = (unsigned int *)buffer;
for(int d = 0; d < 128; d++) {
buf[d] = *EMMC_DATA; // 每次读取4字节
}
// 多块读取需要发送停止命令
if(num > 1 && !(sd_scr[0] & SCR_SUPP_SET_BLKCNT)) {
sd_cmd(CMD_STOP_TRANS, 0);
}
return num * 512;
}
5. 开发调试经验分享
5.1 常见问题排查
-
卡初始化失败:
- 检查GPIO配置是否正确设置为ALT3功能
- 确认上拉电阻已启用
- 验证初始时钟是否设置为400KHz
-
数据读写错误:
- 检查EMMC_BLKSIZECNT寄存器配置
- 确认DMA缓冲区地址对齐到32字节边界
- 验证时钟频率是否在卡支持的范围内
-
中断未触发:
- 检查EMMC_INT_EN寄存器中断使能位
- 确认已清除EMMC_INTERRUPT状态位
- 验证命令超时值设置是否合理
5.2 性能优化技巧
-
批量传输:对于连续扇区读取,使用CMD18多块读取命令,减少命令开销。
-
DMA配置:启用DMA传输可显著提升大块数据读写效率:
c复制*EMMC_CONTROL1 |= C1_DMA_ENABLE; *EMMC_DMA_ADDR = (uint32_t)buffer; -
时钟调整:在初始化完成后,可尝试逐步提高时钟频率,找到卡的稳定工作极限。
-
缓冲区对齐:确保数据缓冲区32字节对齐,避免额外的内存拷贝。
6. 文件系统层实现
虽然裸机开发中文件系统并非必需,但了解FAT32基础实现有助于调试:
-
MBR解析:首个扇区包含分区表
c复制struct MBR { uint8_t boot_code[446]; struct { uint8_t status; uint8_t chs_start[3]; uint8_t type; uint8_t chs_end[3]; uint32_t lba_start; uint32_t sector_count; } partitions[4]; uint16_t signature; }; -
FAT32结构:
- 保留扇区:包含BPB(BIOS参数块)
- FAT表:记录簇分配情况
- 数据区:实际文件存储区域
-
目录遍历:通过读取根目录项获取文件信息
c复制struct DirEntry { char name[8]; char ext[3]; uint8_t attr; // ...其他字段... uint16_t cluster_high; uint16_t cluster_low; uint32_t file_size; };
在实际项目中,可根据需求选择实现完整文件系统或直接按扇区访问。对于启动加载器等场景,直接LBA访问通常更为简单可靠。