大家好,我是良许
在嵌入式开发中,存储器的选择往往决定了产品的成本和性能。
作为一名从事嵌入式开发多年的程序员,我在项目中经常需要为设备选择合适的存储方案。
SD 卡和 TF 卡作为两种最常见的可移动存储介质,它们的应用场景各有千秋。
今天就来和大家聊聊这两种存储卡在实际项目中的应用,以及我在开发过程中积累的一些经验。
1. SD 卡和 TF 卡的基本概念
1.1 什么是 SD 卡
SD 卡(Secure Digital Card)是一种基于半导体闪存的存储卡,由松下、东芝和 SanDisk 公司于 1999 年联合开发。
SD 卡的标准尺寸为 32mm×24mm×2.1mm,相对来说体积较大,但接口稳定性好,适合需要频繁插拔的应用场景。
在我早期做汽车电子项目时,车载导航系统普遍使用 SD 卡来存储地图数据。
这是因为 SD 卡的物理尺寸较大,插拔时不容易损坏,而且当时 SD 卡的容量已经能够满足地图存储的需求。
SD 卡支持 SPI 和 SDIO 两种通信协议,其中 SDIO 协议的传输速度更快,可以达到 104MB/s 甚至更高。
1.2 什么是 TF 卡
TF 卡(TransFlash Card),后来被 SD 协会收编后改名为 microSD 卡,是一种超小型的存储卡。
它的尺寸仅为 15mm×11mm×1mm,是目前最小的存储卡格式之一。
TF 卡虽然体积小,但功能和 SD 卡完全相同,只是物理尺寸不同而已。
在我目前的项目中,几乎所有的便携式设备都采用 TF 卡作为存储方案。
比如我们为客户开发的一款工业相机,就使用了 TF 卡来存储拍摄的图像数据。
TF 卡的小巧体积使得设备可以做得更加紧凑,这在空间受限的嵌入式系统中是非常重要的优势。
1.3 两者的主要区别
从技术角度来看,SD 卡和 TF 卡在电气特性和通信协议上基本相同,主要区别在于物理尺寸。
SD 卡更大更厚,接触面积大,插拔时的机械强度更好。
TF 卡则更小更薄,适合空间受限的应用。
在实际开发中,我们可以通过转接卡将 TF 卡转换为 SD 卡使用,但反过来就不行了。
另外一个重要区别是成本。
由于 TF 卡的生产工艺更复杂,相同容量和速度等级的 TF 卡通常比 SD 卡贵一些。
但在批量采购时,这个价格差异会缩小。
我在给客户做成本分析时,通常会综合考虑存储卡本身的价格、卡座的价格以及 PCB 板的空间成本。
2. SD 卡和 TF 卡在嵌入式系统中的应用
2.1 数据存储应用
在嵌入式系统中,SD 卡和 TF 卡最基本的应用就是数据存储。
我在做过的项目中,有很多设备需要记录运行日志、传感器数据或者用户配置信息。
比如我们开发的一款环境监测设备,需要每隔 10 秒钟记录一次温度、湿度、PM2.5 等数据,一天下来就会产生大量的数据。
使用 TF 卡存储这些数据,不仅成本低廉,而且可以方便地将数据导出到电脑进行分析。
在 STM32 平台上实现 SD 卡的基本读写操作,使用 HAL 库可以这样做:
#include "stm32f4xx_hal.h"
#include "fatfs.h"
// SD卡句柄
SD_HandleTypeDef hsd;
// 初始化SD卡
HAL_StatusTypeDef SD_Init(void)
{
hsd.Instance = SDIO;
hsd.Init.ClockEdge = SDIO_CLOCK_EDGE_RISING;
hsd.Init.ClockBypass = SDIO_CLOCK_BYPASS_DISABLE;
hsd.Init.ClockPowerSave = SDIO_CLOCK_POWER_SAVE_DISABLE;
hsd.Init.BusWide = SDIO_BUS_WIDE_1B;
hsd.Init.HardwareFlowControl = SDIO_HARDWARE_FLOW_CONTROL_DISABLE;
hsd.Init.ClockDiv = 0;
if (HAL_SD_Init(&hsd) != HAL_OK)
{
return HAL_ERROR;
}
// 配置为4位总线宽度以提高速度
if (HAL_SD_ConfigWideBusOperation(&hsd, SDIO_BUS_WIDE_4B) != HAL_OK)
{
return HAL_ERROR;
}
return HAL_OK;
}
// 写入数据到SD卡
HAL_StatusTypeDef SD_WriteData(uint32_t blockAddr, uint8_t *pData, uint32_t numBlocks)
{
HAL_StatusTypeDef status;
status = HAL_SD_WriteBlocks(&hsd, pData, blockAddr, numBlocks, HAL_MAX_DELAY);
if (status == HAL_OK)
{
// 等待写入完成
while(HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER)
{
}
}
return status;
}
// 从SD卡读取数据
HAL_StatusTypeDef SD_ReadData(uint32_t blockAddr, uint8_t *pData, uint32_t numBlocks)
{
HAL_StatusTypeDef status;
status = HAL_SD_ReadBlocks(&hsd, pData, blockAddr, numBlocks, HAL_MAX_DELAY);
if (status == HAL_OK)
{
// 等待读取完成
while(HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER)
{
}
}
return status;
}
这段代码展示了如何在 STM32 上初始化 SD 卡并进行基本的读写操作。
在实际项目中,我通常会在此基础上集成 FatFS 文件系统,这样就可以像操作普通文件一样操作 SD 卡了。
2.2 固件升级应用
在我做过的很多项目中,SD 卡和 TF 卡被用作固件升级的介质。
这种方式特别适合那些部署在野外或者难以通过网络升级的设备。
用户只需要将新的固件文件拷贝到存储卡中,插入设备,设备启动时会自动检测并完成升级。
我在一个工业控制器项目中实现过这样的功能。
设备启动时,Bootloader 会检查 SD 卡中是否存在特定名称的固件文件。
如果存在,就会读取这个文件并烧写到 Flash 中,然后跳转到新的应用程序。
这个过程的关键代码如下:
#include "stm32f4xx_hal.h"
#include "fatfs.h"
#define FIRMWARE_FILE_NAME "firmware.bin"
#define APP_START_ADDRESS 0x08010000 // 应用程序起始地址
// 从SD卡升级固件
HAL_StatusTypeDef UpdateFirmwareFromSD(void)
{
FIL file;
FRESULT fres;
UINT bytesRead;
uint8_t buffer[1024];
uint32_t flashAddress = APP_START_ADDRESS;
// 打开固件文件
fres = f_open(&file, FIRMWARE_FILE_NAME, FA_READ);
if (fres != FR_OK)
{
return HAL_ERROR; // 文件不存在或打开失败
}
// 解锁Flash
HAL_FLASH_Unlock();
// 擦除应用程序区域
FLASH_EraseInitTypeDef eraseInit;
uint32_t sectorError;
eraseInit.TypeErase = FLASH_TYPEERASE_SECTORS;
eraseInit.Sector = FLASH_SECTOR_4; // 根据实际情况调整
eraseInit.NbSectors = 4; // 擦除4个扇区
eraseInit.VoltageRange = FLASH_VOLTAGE_RANGE_3;
if (HAL_FLASHEx_Erase(&eraseInit, §orError) != HAL_OK)
{
HAL_FLASH_Lock();
f_close(&file);
return HAL_ERROR;
}
// 读取文件并写入Flash
while (1)
{
fres = f_read(&file, buffer, sizeof(buffer), &bytesRead);
if (fres != FR_OK || bytesRead == 0)
{
break;
}
// 将数据写入Flash
for (uint32_t i = 0; i < bytesRead; i += 4)
{
uint32_t data = *(uint32_t *)(buffer + i);
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, flashAddress, data) != HAL_OK)
{
HAL_FLASH_Lock();
f_close(&file);
return HAL_ERROR;
}
flashAddress += 4;
}
}
// 锁定Flash
HAL_FLASH_Lock();
f_close(&file);
// 删除固件文件,避免重复升级
f_unlink(FIRMWARE_FILE_NAME);
return HAL_OK;
}
这个固件升级方案在我们的产品中运行得非常稳定。
客户反馈说这种升级方式比通过串口或者网络升级要可靠得多,特别是在网络环境不好的工业现场。
2.3 多媒体应用
在音视频相关的嵌入式项目中,SD 卡和 TF 卡的应用更是不可或缺。
我参与过一个行车记录仪项目,使用 TF 卡来存储录制的视频。
这类应用对存储卡的写入速度要求很高,因为视频数据是连续产生的,如果写入速度跟不上,就会导致丢帧。
在选择存储卡时,我们需要特别注意卡的速度等级。
SD 协会定义了多种速度等级标准,包括 Class 2/4/6/10,UHS-I/II/III 等。
对于 1080P 视频录制,至少需要 Class 10 或者 UHS-I U1 等级的卡。
在我们的项目中,为了保证 4K 视频的流畅录制,我们要求客户使用 UHS-I U3 或更高等级的 TF 卡。
在代码实现上,我们使用了 DMA 来提高数据传输效率:
// 使用DMA写入视频数据
HAL_StatusTypeDef SD_WriteDMA(uint32_t blockAddr, uint8_t *pData, uint32_t numBlocks)
{
HAL_StatusTypeDef status;
// 启动DMA传输
status = HAL_SD_WriteBlocks_DMA(&hsd, pData, blockAddr, numBlocks);
return status;
}
// DMA传输完成回调函数
void HAL_SD_TxCpltCallback(SD_HandleTypeDef *hsd)
{
// 设置标志位,通知应用层写入完成
sdWriteComplete = 1;
}
// 在主循环中处理视频数据
void ProcessVideoData(void)
{
static uint8_t videoBuffer[VIDEO_BUFFER_SIZE];
static uint32_t currentBlock = 0;
// 从摄像头获取视频数据
if (GetVideoFrame(videoBuffer, VIDEO_BUFFER_SIZE) == HAL_OK)
{
// 等待上一次写入完成
while (sdWriteComplete == 0)
{
// 可以在这里处理其他任务
}
sdWriteComplete = 0;
// 启动DMA写入
SD_WriteDMA(currentBlock, videoBuffer, VIDEO_BUFFER_SIZE / 512);
currentBlock += VIDEO_BUFFER_SIZE / 512;
}
}
使用 DMA 可以大大减轻 CPU 的负担,让 CPU 有更多时间处理视频编码等计算密集型任务。
在我们的行车记录仪项目中,使用 DMA 后 CPU 占用率从 85% 降低到了 60% 左右。
2.4 数据采集应用
在工业数据采集系统中,SD 卡和 TF 卡也扮演着重要角色。
我曾经为一家制造企业开发过一套生产线监控系统,需要实时采集多个传感器的数据并存储到 TF 卡中。
这个项目的挑战在于数据量大、采样频率高,而且需要保证数据不丢失。
为了解决这个问题,我采用了双缓冲机制。
系统使用两块内存缓冲区,一块用于接收传感器数据,另一块用于写入 TF 卡。
当一块缓冲区写满后,立即切换到另一块,同时将写满的缓冲区数据写入 TF 卡。
这样可以保证数据采集的连续性。
#define BUFFER_SIZE 4096
// 双缓冲区
uint8_t dataBuffer1[BUFFER_SIZE];
uint8_t dataBuffer2[BUFFER_SIZE];
uint8_t *currentBuffer;
uint8_t *writeBuffer;
uint32_t bufferIndex = 0;
// 初始化双缓冲
void InitDoubleBuffer(void)
{
currentBuffer = dataBuffer1;
writeBuffer = dataBuffer2;
bufferIndex = 0;
}
// 数据采集回调函数(在定时器中断中调用)
void DataAcquisitionCallback(void)
{
uint16_t sensorData;
// 读取传感器数据
sensorData = ReadSensorData();
// 将数据存入当前缓冲区
currentBuffer[bufferIndex++] = (sensorData >> 8) & 0xFF;
currentBuffer[bufferIndex++] = sensorData & 0xFF;
// 如果缓冲区满了,切换缓冲区
if (bufferIndex >= BUFFER_SIZE)
{
// 交换缓冲区指针
uint8_t *temp = currentBuffer;
currentBuffer = writeBuffer;
writeBuffer = temp;
bufferIndex = 0;
// 设置标志,通知主循环写入SD卡
bufferReadyFlag = 1;
}
}
// 在主循环中写入SD卡
void MainLoop(void)
{
static uint32_t fileBlock = 0;
while (1)
{
if (bufferReadyFlag)
{
bufferReadyFlag = 0;
// 将数据写入SD卡
SD_WriteData(fileBlock, writeBuffer, BUFFER_SIZE / 512);
fileBlock += BUFFER_SIZE / 512;
}
// 处理其他任务
}
}
这个双缓冲方案在我们的项目中表现很好,即使在采样频率达到 10kHz 的情况下,也能保证数据不丢失。
客户对这个方案非常满意,后来又追加了几套同样的系统。
3. SD 卡和 TF 卡的选型建议
3.1 容量选择
在选择存储卡容量时,我通常会根据应用的具体需求来决定。
对于日志记录类应用,一般 4GB 到 16GB 就足够了。
但对于视频录制或者大量图像存储的应用,可能需要 32GB 甚至更大的容量。
需要注意的是,并不是容量越大越好。
在我的经验中,容量过大的存储卡在格式化和文件系统维护时会花费更多时间。
而且,如果使用 FAT32 文件系统,单个文件的大小限制是 4GB,这在长时间视频录制时需要特别注意。
对于需要存储超过 4GB 单个文件的应用,建议使用 exFAT 文件系统。
3.2 速度等级选择
速度等级的选择直接影响系统的性能。
在我做过的项目中,如果只是存储日志或者配置文件,Class 4 的卡就够用了。
但如果是视频录制或者高速数据采集,就必须选择 Class 10 或者 UHS 等级的卡。
这里有一个实际的例子。
我们在开发一款工业相机时,最初使用的是 Class 10 的 TF 卡。
在测试中发现,当连续拍摄高分辨率图片时,偶尔会出现保存失败的情况。
后来更换为 UHS-I U3 等级的卡后,问题就完全解决了。
所以在选型时,一定要根据实际的数据传输速率来选择合适的速度等级,并且留有一定的余量。
3.3 品牌和可靠性
在嵌入式产品中,存储卡的可靠性至关重要。
我在项目中一般会选择 SanDisk、Samsung、Kingston 等知名品牌的产品。
虽然价格可能贵一些,但质量和售后服务有保障。
我曾经遇到过一个教训。
在一个项目中,为了降低成本,客户坚持使用某个不知名品牌的 TF 卡。
结果产品上市后,陆续收到用户反馈说数据丢失。
后来排查发现,是 TF 卡的质量问题导致的。
最终不得不召回产品更换存储卡,损失远远超过了当初节省的成本。
所以我现在在给客户做方案时,都会强调存储卡质量的重要性。
3.4 工业级 vs 消费级
对于工业应用,我强烈建议使用工业级的存储卡。
工业级存储卡在温度范围、抗震性、使用寿命等方面都比消费级产品要好得多。
虽然价格可能是消费级产品的 2 到 3 倍,但在恶劣环境下的可靠性是值得的。
在我参与的一个户外监控项目中,设备需要在-40°C 到 85°C 的温度范围内工作。
我们使用的是 SanDisk 的工业级 TF 卡,经过两年的实际运行,故障率几乎为零。
而同期使用消费级 TF 卡的竞品,在极端温度下频繁出现问题。
4. 使用中的注意事项
4.1 文件系统的选择
在嵌入式系统中使用 SD 卡或 TF 卡,通常需要配合文件系统使用。
最常用的是 FAT32 和 exFAT。
FAT32 兼容性好,几乎所有设备都支持,但有单个文件 4GB 的限制。
exFAT 没有这个限制,但不是所有设备都支持。
在我的项目中,如果不需要存储超过 4GB 的单个文件,我都会选择 FAT32。
因为 FAT32 的实现更简单,占用的资源更少。
在 STM32 这样的 MCU 上,使用 FatFS 库可以很方便地实现 FAT32 文件系统:
#include "ff.h"
FATFS fs; // 文件系统对象
FIL file; // 文件对象
FRESULT fres; // 操作结果
// 挂载文件系统
void MountFileSystem(void)
{
fres = f_mount(&fs, "0:", 1);
if (fres != FR_OK)
{
// 挂载失败,可能需要格式化
printf("Mount failed, error code: %d\n", fres);
}
}
// 写入日志文件
void WriteLog(const char *logMessage)
{
UINT bytesWritten;
// 打开文件,如果不存在则创建
fres = f_open(&file, "log.txt", FA_OPEN_APPEND | FA_WRITE);
if (fres == FR_OK)
{
// 写入时间戳
char timestamp[32];
sprintf(timestamp, "[%lu] ", HAL_GetTick());
f_write(&file, timestamp, strlen(timestamp), &bytesWritten);
// 写入日志内容
f_write(&file, logMessage, strlen(logMessage), &bytesWritten);
f_write(&file, "\n", 1, &bytesWritten);
// 关闭文件
f_close(&file);
}
}
4.2 数据完整性保护
在嵌入式系统中,突然断电是常见的情况。
如果在写入 SD 卡时突然断电,可能会导致数据损坏甚至整个文件系统损坏。
为了避免这个问题,我在项目中通常会采取以下措施:
首先,尽量减少文件的打开关闭次数。
频繁打开关闭文件会增加文件系统损坏的风险。
其次,在关键数据写入后,调用 f_sync()函数强制将缓冲区数据写入存储卡。
最后,可以考虑实现一个简单的日志系统,记录每次写入操作,这样即使发生数据损坏,也可以通过日志恢复。
// 安全写入数据
HAL_StatusTypeDef SafeWriteData(const char *filename, uint8_t *data, uint32_t size)
{
FIL file;
FRESULT fres;
UINT bytesWritten;
// 打开文件
fres = f_open(&file, filename, FA_CREATE_ALWAYS | FA_WRITE);
if (fres != FR_OK)
{
return HAL_ERROR;
}
// 写入数据
fres = f_write(&file, data, size, &bytesWritten);
if (fres != FR_OK || bytesWritten != size)
{
f_close(&file);
return HAL_ERROR;
}
// 强制同步,确保数据写入存储卡
fres = f_sync(&file);
if (fres != FR_OK)
{
f_close(&file);
return HAL_ERROR;
}
// 关闭文件
f_close(&file);
return HAL_OK;
}
4.3 热插拔处理
在某些应用中,用户可能需要在设备运行时插拔存储卡。
这就需要我们的程序能够检测存储卡的插入和移除,并做出相应的处理。
大多数 SD 卡座都有一个检测引脚,可以用来检测卡的插入状态。
在我的项目中,我通常会使用 GPIO 中断来检测存储卡的插拔:
// 卡检测引脚的GPIO中断回调
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == SD_DETECT_PIN)
{
// 延时去抖动
HAL_Delay(50);
if (HAL_GPIO_ReadPin(SD_DETECT_PORT, SD_DETECT_PIN) == GPIO_PIN_RESET)
{
// 卡插入
cardInserted = 1;
// 重新挂载文件系统
f_mount(&fs, "0:", 1);
}
else
{
// 卡移除
cardInserted = 0;
// 卸载文件系统
f_mount(NULL, "0:", 0);
}
}
}
// 在写入前检查卡是否存在
HAL_StatusTypeDef WriteDataToCard(uint8_t *data, uint32_t size)
{
if (!cardInserted)
{
return HAL_ERROR; // 卡未插入
}
// 执行写入操作
return SafeWriteData("data.bin", data, size);
}
4.4 性能优化
在实际应用中,SD 卡的读写性能可能会成为系统的瓶颈。
我在项目中总结了一些优化技巧:
第一,使用块读写而不是字节读写。
SD 卡的最小读写单位是 512 字节的块,使用块读写可以大大提高效率。
第二,尽量使用连续的存储空间。
碎片化的文件会降低读写速度。
在我的一个项目中,定期整理存储卡可以将写入速度提升 30% 左右。
第三,合理设置 FatFS 的扇区大小和缓冲区大小。
在 ffconf.h 配置文件中,可以调整这些参数来优化性能:
// ffconf.h 中的关键配置
#define FF_MAX_SS 4096 // 最大扇区大小,增大可以提高性能
#define FF_MIN_SS 512 // 最小扇区大小
#define FF_USE_LFN 2 // 长文件名支持
#define FF_FS_LOCK 4 // 文件锁定功能
第四,对于大文件的写入,可以考虑使用 f_expand()函数预分配空间,这样可以减少文件系统的开销:
// 预分配文件空间
FRESULT PreAllocateFile(const char *filename, FSIZE_t size)
{
FIL file;
FRESULT fres;
// 创建文件
fres = f_open(&file, filename, FA_CREATE_ALWAYS | FA_WRITE);
if (fres != FR_OK)
{
return fres;
}
// 预分配空间
fres = f_expand(&file, size, 1);
if (fres != FR_OK)
{
f_close(&file);
return fres;
}
f_close(&file);
return FR_OK;
}
5. 总结
SD 卡和 TF 卡在嵌入式系统中的应用非常广泛,从简单的数据存储到复杂的多媒体应用,它们都能胜任。
在我多年的嵌入式开发经验中,选择合适的存储方案、正确地使用存储卡、做好数据保护和性能优化,是保证产品稳定运行的关键。
对于初学者来说,建议先从简单的文件读写开始,逐步掌握文件系统的使用。
对于有经验的开发者,则需要更多地关注可靠性和性能优化。
无论是哪个阶段,都要记住一点:存储卡虽然看起来简单,但在实际应用中有很多细节需要注意。
只有充分理解和掌握这些细节,才能开发出稳定可靠的产品。
希望这篇文章能够帮助大家更好地理解和应用 SD 卡和 TF 卡。
如果你在项目中遇到相关问题,欢迎交流讨论。
作为一名深耕嵌入式领域的程序员,我深知技术交流的重要性,也愿意将自己的经验分享给更多的同行。
更多编程学习资源