为了账号安全,请及时绑定邮箱和手机立即绑定

SD和TF卡的应用

标签:
C++

大家好,我是良许

在嵌入式开发中,存储器的选择往往决定了产品的成本和性能。

作为一名从事嵌入式开发多年的程序员,我在项目中经常需要为设备选择合适的存储方案。

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, &sectorError) != 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卡。

如果你在项目中遇到相关问题,欢迎交流讨论。

作为一名深耕嵌入式领域的程序员,我深知技术交流的重要性,也愿意将自己的经验分享给更多的同行。

更多编程学习资源

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
Linux系统工程师
手记
粉丝
102
获赞与收藏
285

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消