1. 项目概述
在嵌入式开发领域,存储扩展一直是个经久不衰的话题。nRF52832作为Nordic Semiconductor推出的经典低功耗蓝牙SoC,凭借其优异的射频性能和ARM Cortex-M4内核,被广泛应用于物联网设备中。但这款芯片内部Flash容量有限(通常512KB),当需要存储日志、配置或音视频数据时,外接SD卡就成了性价比最高的选择。
我最近在一个智能穿戴项目中,就遇到了需要将运动数据定期写入SD卡的需求。本以为用FatFS这类成熟文件系统应该很简单,实际调试时却踩了不少坑:从SPI时序不稳定导致的写入错误,到电源管理不当引发的文件损坏,再到多任务环境下的互斥问题。本文将把这些实战经验系统梳理,手把手教你实现稳定可靠的SD卡文件系统操作。
2. 硬件设计要点
2.1 硬件连接方案
nRF52832与SD卡的典型连接采用SPI模式,相比SDIO模式节省引脚且更易实现。以下是推荐连接方式:
| nRF52832引脚 | SD卡引脚 | 备注 |
|---|---|---|
| P0.xx (SCK) | CLK | 建议加33Ω串联电阻 |
| P0.xx (MOSI) | DI | 主出从入 |
| P0.xx (MISO) | DO | 主入从出 |
| P0.xx (CS) | CS | 片选,需上拉10kΩ |
| VDD (3.3V) | VCC | 需加100μF+0.1μF去耦 |
| GND | GND | 共地 |
注意:SD卡槽的检测引脚(CD)和写保护(WP)可根据需求接入,但务必确保上拉电阻可靠
2.2 电源管理设计
SD卡在写入时峰值电流可达100mA,必须做好电源设计:
- 使用LDO而非DCDC供电,避免纹波干扰(如TPS70933)
- 在VCC引脚就近放置100μF钽电容+0.1μF陶瓷电容组合
- 插入TVS二极管防护ESD事件(如SMAJ5.0A)
c复制// 示例:nRF52的GPIO初始化
#define SD_CS_PIN 5
#define SD_MOSI_PIN 6
#define SD_MISO_PIN 7
#define SD_SCK_PIN 8
void spi_init() {
nrf_gpio_cfg_output(SD_CS_PIN);
nrf_gpio_pin_set(SD_CS_PIN); // CS初始高电平
nrf_spi_pins_set(NRF_SPI0, SD_SCK_PIN, SD_MOSI_PIN, SD_MISO_PIN);
nrf_spi_frequency_set(NRF_SPI0, NRF_SPI_FREQ_4M); // 初始低速
nrf_spi_configure(NRF_SPI0, NRF_SPI_MODE_0, NRF_SPI_BIT_ORDER_MSB_FIRST);
}
3. 软件栈构建
3.1 FatFS移植要点
使用Chan's FatFS R0.14b版本,修改diskio.c关键函数:
c复制DRESULT disk_write (
BYTE pdrv, // 物理驱动器号
const BYTE* buff, // 写入数据缓冲区
LBA_t sector, // 起始扇区
UINT count // 扇区数
) {
sd_cs_low();
res = sd_write_blocks(buff, sector, count); // 自定义SPI写函数
sd_cs_high();
if(res == SD_OK) return RES_OK;
else {
log_error("Write failed at sector %lu", sector);
return RES_ERROR;
}
}
关键适配点:
- 实现disk_initialize、disk_status等5个接口函数
- 修改ffconf.h配置:
#define _FS_REENTRANT 1用于RTOS环境#define _USE_MKFS 1允许格式化#define _CODE_PAGE 936支持中文文件名
3.2 存储性能优化
通过实测对比不同配置下的写入速度:
| 配置项 | 速度(KB/s) | 稳定性 |
|---|---|---|
| SPI 4MHz + 512B块 | 42.5 | ★★★★ |
| SPI 8MHz + 1KB块 | 78.3 | ★★★☆ |
| SPI 16MHz + 4KB块 | 128.7 | ★★☆☆ |
建议方案:
- 默认使用4MHz+512B组合
- 大数据写入时临时切换到8MHz+1KB模式
- 避免长时间16MHz操作(发热影响稳定性)
4. 关键问题解决方案
4.1 电源瞬断处理
突发断电可能导致文件系统损坏,解决方案:
- 采用"预写日志"模式:
c复制void write_with_logging(const char* path, void* data, size_t len) {
FIL fp;
f_open(&fp, "LOG.TMP", FA_WRITE | FA_CREATE_ALWAYS);
f_write(&fp, data, len, NULL);
f_sync(&fp); // 强制刷入物理设备
if(check_file_ok("LOG.TMP")) {
f_rename("LOG.TMP", path); // 原子操作
}
f_close(&fp);
}
- 每次启动时检查.fsck文件:
c复制if(f_stat("_FSCHECK.TXT", &filinfo) == FR_OK) {
// 上次异常断电,需要修复
scan_disk();
}
4.2 多任务访问同步
在FreeRTOS环境中需添加互斥锁:
c复制SemaphoreHandle_t fs_mutex;
void safe_write() {
if(xSemaphoreTake(fs_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
/* 文件操作代码 */
xSemaphoreGive(fs_mutex);
}
}
5. 实测案例:运动数据记录仪
5.1 存储格式设计
采用CSV+二进制混合存储:
code复制2023-08-01.csv # 文本头记录设备信息
2023-08-01.dat # 二进制加速度数据(100Hz采样)
5.2 功耗优化技巧
- 批量写入:缓存满1KB再触发写操作
- 休眠管理:
c复制void sd_power_down() {
f_sync(&fp); // 确保数据落盘
sd_send_cmd(CMD15, 0); // 发送SD卡休眠命令
set_sd_power(0); // 关闭电源
}
6. 调试经验分享
6.1 常见错误码排查
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| FR_DISK_ERR (-1) | SPI时序不稳定 | 降低时钟频率,检查走线 |
| FR_NOT_READY (-2) | 卡未初始化 | 重新插拔,检查电源 |
| FR_NO_FILE (-3) | 路径错误 | 使用f_chdir设置当前目录 |
| FR_NO_FILESYSTEM (-13) | 卡未格式化 | 执行f_mkfs创建FAT32文件系统 |
6.2 逻辑分析仪抓包技巧
使用Saleae逻辑分析仪捕获SPI信号时:
- 设置SCK下降沿采样
- 添加SD协议解码器
- 重点关注CMD24(写单块)响应:
- 正常流程:CMD24→0x00→数据令牌(0xFE)→512B数据→2B CRC
- 异常情况:可能收到0x05(写错误)或0x0B(地址错误)
7. 进阶优化方向
对于需要更高可靠性的场景:
- 磨损均衡:记录各区块写入次数,动态分配冷区
c复制uint32_t wear_level[1024]; // 记录每个擦除块的写入次数
void select_block() {
// 选择写入次数最少的块
}
- 坏块管理:建立替换块映射表
- 掉电保护:增加超级电容维持5ms供电
通过本文介绍的方法,我在最终产品中实现了日均10万次写入的稳定运行。关键点在于:严格遵循硬件设计规范、合理配置FatFS参数、以及完善的异常处理机制。