nRF52832 SPI驱动Micro SD卡移植实战:从STM32到Nordic的避坑指南
在嵌入式开发中,存储扩展是常见需求,而Micro SD卡因其体积小、容量大、价格低廉成为首选方案。本文将分享如何将正点原子STM32平台的SD卡驱动移植到nRF52832平台,重点解析SPI模式下的关键差异点和实际移植中遇到的"坑"。
1. 硬件平台差异与基础配置
nRF52832与STM32在SPI外设设计上存在显著差异,这些差异直接影响SD卡驱动的移植工作:
时钟配置对比
| 参数 | nRF52832 | STM32F1系列典型配置 |
|---|---|---|
| 最大时钟频率 | 8MHz(SPIM模式) | 18MHz |
| 时钟极性 | 模式3(CPOL=1, CPHA=1) | 模式3 |
| 分频控制 | 预定义频率等级 | 自由分频 |
| DMA支持 | EasyDMA(需使用SPIM实例) | 标准DMA控制器 |
nRF52832的SPI初始化需要特别注意:
nrf_drv_spi_config_t spi_config = NRF_DRV_SPI_DEFAULT_CONFIG; spi_config.sck_pin = 29; // SCK引脚 spi_config.mosi_pin = 25; // MOSI引脚 spi_config.miso_pin = 28; // MISO引脚 spi_config.ss_pin = NRF_DRV_SPI_PIN_NOT_USED; // 手动控制CS spi_config.frequency = NRF_DRV_SPI_FREQ_4M; spi_config.mode = NRF_DRV_SPI_MODE_3; // SD卡必需的模式关键提示:nRF52832的SPI0与TWI0共享资源,如果同时使用I2C和SPI,必须选择不同的外设实例。
引脚控制特殊要求:
- CS引脚需要手动控制,不能依赖硬件NSS
- 上电时需要发送至少74个时钟脉冲(发送空字节0xFF)
- 切换时钟频率需要先uninit再重新初始化
2. 移植过程中的典型问题与解决方案
2.1 容量读取异常问题
在测试8GB SD卡时,读取到的容量显示为3290MB,远小于实际容量。这个问题源于CSD寄存器解析逻辑:
// 修正后的容量计算逻辑(针对SDHC/SDXC卡) if((csd[0]&0xC0)==0x40) { // SDHC/SDXC卡 csize = csd[9] + ((u32)csd[8] << 8) + 1; Capacity = (u32)csize << 10; // 注意使用32位运算 } else { // 标准容量卡 // ...原有计算逻辑 }问题根源:
- 变量类型未统一使用32位导致运算溢出
- 未考虑SDXC卡的特殊处理(容量≥32GB)
- CSD版本判断条件不够严谨
2.2 SPI时序兼容性问题
SD卡对SPI时序有严格要求,移植时发现以下关键点:
模式3必须严格配置:
- CPOL=1(空闲时时钟高电平)
- CPHA=1(第二个边沿采样)
速度切换时机:
- 初始化阶段必须使用低速(≤400kHz)
- 初始化完成后才能切换到高速模式
- 需要动态重配置SPI外设:
void spi_set_speed(u32 speed) { nrf_drv_spi_uninit(&spi); // 必须先解除初始化 if(speed > 400000) { spi_config.frequency = NRF_DRV_SPI_FREQ_4M; } else { spi_config.frequency = NRF_DRV_SPI_FREQ_250K; } APP_ERROR_CHECK(nrf_drv_spi_init(&spi, &spi_config, spi_event_handler, NULL)); }2.3 多卡兼容性处理
不同品牌/容量的SD卡可能存在以下差异:
初始化流程差异:
- SDv1.x卡:需要CMD1初始化
- SDv2卡:需要CMD8+CMD55+CMD41组合
- MMC卡:需要CMD1初始化
超时处理优化:
u8 SD_GetResponse(u8 Response) { u32 Count = 100000; // 增大超时计数范围 while((SpiFlash_ReadOneByte()!=Response) && Count--) { __NOP(); // 插入空指令确保时序 } return (Count==0) ? MSD_RESPONSE_FAILURE : MSD_RESPONSE_NO_ERROR; }3. 完整工程关键代码解析
3.1 驱动层适配代码
SPI读写基础函数:
// 带超时检测的字节写入 void SD_WriteByte(u8 data, u32 timeout) { spi_tx_buf[0] = data; spi_xfer_done = false; APP_ERROR_CHECK(nrf_drv_spi_transfer(&spi, spi_tx_buf, 1, spi_rx_buf, 1)); u32 start = nrfx_get_us(); while(!spi_xfer_done) { if((nrfx_get_us() - start) > timeout) { break; // 超时处理 } } } // 多字节连续读取 void SD_ReadMultiBytes(u8 *buf, u16 len) { memset(spi_tx_buf, 0xFF, len); // 发送0xFF触发时钟 APP_ERROR_CHECK(nrf_drv_spi_transfer(&spi, spi_tx_buf, len, buf, len)); while(!spi_xfer_done); }3.2 命令发送优化实现
u8 SD_SendCmd(u8 cmd, u32 arg, u8 crc) { u8 frame[6]; frame[0] = cmd | 0x40; frame[1] = (u8)(arg >> 24); frame[2] = (u8)(arg >> 16); frame[3] = (u8)(arg >> 8); frame[4] = (u8)arg; frame[5] = crc; // 使用DMA传输提高效率 spi_xfer_done = false; APP_ERROR_CHECK(nrf_drv_spi_transfer(&spi, frame, 6, spi_rx_buf, 0)); u32 timeout = 10000; // 10ms超时 while(!spi_xfer_done && timeout--); // 等待响应 u8 retry = 20; u8 r1; do { r1 = SpiFlash_ReadOneByte(); } while((r1&0x80) && retry--); return r1; }4. 性能优化与实测数据
通过优化SPI传输效率,我们得到以下实测数据:
不同时钟频率下的读写速度:
| 时钟频率 | 读取速度 (KB/s) | 写入速度 (KB/s) | 稳定性 |
|---|---|---|---|
| 250kHz | 45.2 | 38.7 | ★★★★★ |
| 1MHz | 152.4 | 128.6 | ★★★★☆ |
| 4MHz | 412.8 | 387.5 | ★★★☆☆ |
| 8MHz | 不稳定 | 不稳定 | ★★☆☆☆ |
实际测试建议:4MHz时钟在大多数情况下提供最佳性价比,若需要更高速度可尝试6MHz但需严格验证信号完整性
优化技巧:
- 使用乒乓缓冲区减少传输间隔
- 合理设置SPI FIFO阈值
- 关键路径禁用中断
- 采用DMA传输解放CPU资源
// DMA传输示例(需使用SPIM实例) nrfx_spim_xfer_desc_t xfer = NRFX_SPIM_XFER_TRX(tx_buf, tx_len, rx_buf, rx_len); nrfx_spim_xfer(&spim, &xfer, 0);移植完成后工程实测可稳定支持以下操作:
- 创建/删除文件
- 连续读写测试(>1MB数据)
- 长时间稳定性测试(72小时连续运行)
- 多种品牌SD卡兼容(SanDisk、Kingston、Samsung等)
在完成移植后,发现nRF52832的SPI驱动在4MHz时钟下即可满足大多数应用需求,过高频率反而可能导致信号完整性问题。实际项目中建议根据PCB布局和线长谨慎选择最高工作频率。