发新帖本帖赏金 110.00元(功能说明)我要提问
返回列表
打印
[STM32F1]

【每周分享】用STM32F103核心板+SD卡模块实现简单读卡器

[复制链接]
18764|9
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
本帖最后由 yuyy1989 于 2024-12-7 12:46 编辑

@21小跑堂 #申请原创#
用到的硬件,STM32F103核心板USB引脚已引出,SPI通讯的SD卡模块

打开STM32CubeMX配置SPI和USB

在USB_DEVICE这里把USB缓存改为512,STM32F103C8T6的USB和CAN共享512字节的SRAM


生成工程后直接编译烧录后可以看到这样一个U盘,由于相关方法还没有实现,这时并不能打开这个U盘


接下来实现SPI读写SD卡
实现SPI读写方法
void SD_SPI_CSSetLev(uint8_t lev)
{
    if(lev == 0)
        LL_GPIO_ResetOutputPin(SPI1_CS_GPIO_Port,SPI1_CS_Pin);
    else
        LL_GPIO_SetOutputPin(SPI1_CS_GPIO_Port,SPI1_CS_Pin);
}
uint8_t SD_SPI_WriteByte(uint8_t data)
{
    uint8_t rev = 0;
    while(!LL_SPI_IsActiveFlag_TXE(SPI1));
    LL_SPI_TransmitData8(SPI1,data);
    while(!LL_SPI_IsActiveFlag_RXNE(SPI1));
    rev = LL_SPI_ReceiveData8(SPI1);
    return rev;
}
uint8_t SD_SPI_ReadByte(void)
{
    return SD_SPI_WriteByte(0xFF);
}
void SD_SPI_WriteDatas(uint8_t *datas, uint16_t len)
{
    while(len > 0)
    {
        SD_SPI_WriteByte(*datas);
        datas += 1;
        len -= 1;
    }
}
void SD_SPI_ReadDatas(uint8_t *datas, uint16_t len)
{
    while(len > 0)
    {
        *datas = SD_SPI_ReadByte();
        datas += 1;
        len -= 1;
    }
}


通电后需要等待SD卡电压稳定后再进行后续通讯

通过SPI向SD卡发送的命令格式如下

涉及到的部分命令码如下

几种应答的数据格式:
    R1,1字节,R1b与R1格式相同,但可选地添加了忙信号。忙信号令牌可以是任意数量的字节。零值表示卡正忙。非零值表示卡已准备好下一个命令。

    R2,2字节

    R3,5字节,用于获取OCR寄存器的数据。高8位为R1令牌,低4字节为OCR寄存器的数据。

    R7,共5字节,主要用于获取SD卡工作电压信息,高8位为R1令牌。

在SPI模式中,只有CMD0和CMD8需要CRC,其他命令的CRC无效直接填0xFF即可
封装命令发送方法
/**
* [url=home.php?mod=space&uid=247401]@brief[/url]  SD卡命令
*/
#define SD_CMD0_GO_IDLE_STATE           0   //复位所有的卡到idle状态
#define SD_CMD1_SEND_OP_COND            1   //发送主机支持的电压操作范围
#define SD_CMD8_SEND_IF_COND            8   //发送SD卡接口条件,包含主机支持的电压信息,并询问卡是否支持
#define SD_CMD9_SEND_CSD                9   //要求卡发送其CSD寄存器内容
#define SD_CMD10_SEND_CID               10  //要求卡发送其CID寄存器内容
#define SD_CMD12_STOP_TRANSMISSION      12  //强制卡停止传输,可用于多块读写时表示结束
#define SD_CMD13_SEND_STATUS            13  //要求卡发送状态寄存器内容
#define SD_CMD16_SET_BLOCKLEN           16  //对于标准SD卡设置块命令长度,SDHC卡固定512
#define SD_CMD17_READ_SINGLE_BLOCK      17  //对于标准SD卡读取指定长度的块,SDHC卡固定读取512
#define SD_CMD18_READ_MULT_BLOCK        18  //连续从SD卡读取数据块,直到被CMD12打断
#define SD_CMD24_WRITE_SINGLE_BLOCK     24  //对于标准SD卡设置写入CMD16设置长度的数据,SDHC卡固定512
#define SD_CMD25_WRITE_MULT_BLOCK       25  //连续向SD卡写入数据,直到被CMD12打断
#define SD_CMD27_PROG_CSD               27  //对CSD的可编程位进行编程
#define SD_CMD30_SEND_WRITE_PROT        30  //要求卡发送写保护状态
#define SD_CMD32_SD_ERASE_GRP_START     32  //设置擦除的起始块地址
#define SD_CMD33_SD_ERASE_GRP_END       33  //设置擦除的结束块地址
#define SD_CMD38_ERASE                  38  //擦除选定的块
#define SD_CMD58_READ_OCR               58  //要求返回OCR寄存器的值
#define SD_CMD55_APP_CMD                55  //发送ACMD前发送此命令,返回0x01
#define SD_ACMD23_SE_SET_BLOCK_COUNT    23  //设置预擦除块数
#define SD_ACMD41_SD_SEND_OP_COND       41  //返回0x00

typedef enum {
    /**
    * [url=home.php?mod=space&uid=247401]@brief[/url]  SD 响应及错误标志
    */
    SD_RESPONSE_NO_ERROR      = (0x00),
    SD_IN_IDLE_STATE          = (0x01),
    SD_ERASE_RESET            = (0x02),
    SD_ILLEGAL_COMMAND        = (0x04),
    SD_COM_CRC_ERROR          = (0x08),
    SD_ERASE_SEQUENCE_ERROR   = (0x10),
    SD_ADDRESS_ERROR          = (0x20),
    SD_PARAMETER_ERROR        = (0x40),
    SD_RESPONSE_FAILURE       = (0xFF),

    /**
    * @brief  数据响应类型
    */
    SD_DATA_OK                = (0x05),
    SD_DATA_CRC_ERROR         = (0x0B),
    SD_DATA_WRITE_ERROR       = (0x0D),
    SD_DATA_OTHER_ERROR       = (0xFF)
} SD_Error;

enum {
    SD_RESPONSE_TYPE_NONE = 0,
    SD_RESPONSE_TYPE_R1,
    SD_RESPONSE_TYPE_R1b,
    SD_RESPONSE_TYPE_R2,
    SD_RESPONSE_TYPE_R3,
    SD_RESPONSE_TYPE_R7,
};
uint8_t SD_GetCMResponseType(uint8_t cmd)
{
    switch(cmd)
    {
        case SD_CMD0_GO_IDLE_STATE:
        case SD_CMD1_SEND_OP_COND:
        case SD_CMD9_SEND_CSD:
        case SD_CMD10_SEND_CID:
        case SD_CMD16_SET_BLOCKLEN:
        case SD_CMD17_READ_SINGLE_BLOCK:
        case SD_CMD18_READ_MULT_BLOCK:
        case SD_CMD24_WRITE_SINGLE_BLOCK:
        case SD_CMD25_WRITE_MULT_BLOCK:
        case SD_CMD27_PROG_CSD:
        case SD_CMD32_SD_ERASE_GRP_START:
        case SD_CMD33_SD_ERASE_GRP_END:
        case SD_CMD55_APP_CMD:
        case SD_ACMD41_SD_SEND_OP_COND:
            return SD_RESPONSE_TYPE_R1;
            break;
        case SD_CMD12_STOP_TRANSMISSION:
            return SD_RESPONSE_TYPE_R1b;
            break;
        case SD_CMD13_SEND_STATUS:
            return SD_RESPONSE_TYPE_R2;
            break;
        case SD_CMD58_READ_OCR:
            return SD_RESPONSE_TYPE_R3;
            break;
        case SD_CMD8_SEND_IF_COND:
            return SD_RESPONSE_TYPE_R7;
            break;
        default:
            break;
    }
    return SD_RESPONSE_TYPE_NONE;
}

void SD_SendCmd(uint8_t cmd, uint32_t arg, uint8_t crc,uint8_t *res,uint8_t release_cs)
{
    uint8_t datas[6];
    uint16_t i = 0xFFF;
    SD_SPI_CSSetLev(0);
    datas[0] = (cmd | 0x40); /*!< Construct byte 1 */
    datas[1] = (uint8_t)(arg >> 24); /*!< Construct byte 2 */
    datas[2] = (uint8_t)(arg >> 16); /*!< Construct byte 3 */
    datas[3] = (uint8_t)(arg >> 8); /*!< Construct byte 4 */
    datas[4] = (uint8_t)(arg); /*!< Construct byte 5 */
    datas[5] = (crc); /*!< Construct CRC: byte 6 */
    SD_SPI_WriteDatas(datas,6);
    do
    {
        res[0] = SD_SPI_ReadByte();
        if(i == 0)
        {
            res[0]= SD_RESPONSE_FAILURE;
            return;
        }
        i -= 1;
    }while(res[0]&0x80);
    if(SD_GetCMResponseType(cmd) == SD_RESPONSE_TYPE_R2)
    {
        res[1]= SD_SPI_ReadByte();
    }
    else if(SD_GetCMResponseType(cmd) == SD_RESPONSE_TYPE_R3 || SD_GetCMResponseType(cmd) == SD_RESPONSE_TYPE_R7)
    {
        i = 1;
        while(i<5)
        {
            res[i] = SD_SPI_ReadByte();
            i += 1;
        }
    }
    if(release_cs == 0)
        return;
    SD_SPI_CSSetLev(1);
    SD_SPI_WriteByte(0xFF);
}

SD卡初始化时,拉低CS并发送CMD0使SD卡进入SPI模式,之后通过CMD8判断SD卡版本,如果是2.0版本再通过CMD58判断是不是SDHC,完整流程如图

完成初始化流程后读取CSD寄存器,里面有SD卡大小的信息,CSD结构如图

根据C_SIZE就能计算出SD卡大小


我这里为了方便只判断是否符合SDHC,有兴趣的可以在此基础上实现完整的初始化过程,代码如下
uint32_t sdcard_block_num = 0;
/**
* @brief  初始化 SD/SD 卡
* @param  None
* @retval  SD 响应:
*         - SD_RESPONSE_FAILURE: 初始化失败
*         - SD_RESPONSE_NO_ERROR: 初始化成功
*/
uint8_t SD_Init(void)
{
    uint8_t i;
    uint8_t res[5];
    uint8_t csd[16];
    SD_SPI_CSSetLev(1);
    for (i = 0; i < 10; i++)
    {
        SD_SPI_WriteByte(0xFF);
    }
    //进入空闲模式
    SD_SendCmd(SD_CMD0_GO_IDLE_STATE, 0, 0x95,res,1);
    if(res[0] != SD_IN_IDLE_STATE)
        return SD_RESPONSE_FAILURE;

    SD_SendCmd(SD_CMD8_SEND_IF_COND, 0x1AA, 0x87,res,1);
    if(res[0] != SD_IN_IDLE_STATE)
        return SD_RESPONSE_FAILURE;

    if (res[3]!=0x01 || res[4]!=0xAA)
        return SD_RESPONSE_FAILURE;

    i = 200;
    do
    {
        SD_SendCmd(SD_CMD55_APP_CMD, 0, 0xFF,res,1);
        if(res[0] != SD_IN_IDLE_STATE)
            return SD_RESPONSE_FAILURE;
        SD_SendCmd(SD_ACMD41_SD_SEND_OP_COND, 0x40000000, 0xFF,res,1);

        if (i == 0)
            return SD_RESPONSE_FAILURE;
        i-=1;
    } while (res[0] != SD_RESPONSE_NO_ERROR);
    i = 200;
    do
    {
        SD_SendCmd(SD_CMD58_READ_OCR, 0, 0xFF,res,1);
        if (i == 0)
            return SD_RESPONSE_FAILURE;
        i-=1;
    } while (res[0] != SD_RESPONSE_NO_ERROR);
    if((res[1]&0x40) == 0)
        return SD_RESPONSE_FAILURE;

    //读CSD
    SD_SendCmd(SD_CMD9_SEND_CSD, 0, 0xFF,res,0);
    if(res[0] == SD_RESPONSE_NO_ERROR)
    {
        SD_SPI_ReadBlockDatas(csd,16);
    }
    sdcard_block_num = (csd[9] + ((uint16_t)csd[8] << 8) + 1)*1024;
    SD_SPI_CSSetLev(1);
    SD_SPI_WriteByte(0xFF);
    return SD_RESPONSE_NO_ERROR;
}
需要注意的是,在SD卡初始化过程中SPI时钟频率配置在400kHz以下,保证卡能够稳定地进入工作状态,初始化完成后再切换为高频率,所以在main中应该这样调用
  LL_GPIO_SetOutputPin(LED_GPIO_Port, LED_Pin);
  LL_SPI_SetBaudRatePrescaler(SPI1,LL_SPI_BAUDRATEPRESCALER_DIV256);
  if(SD_Init() == SD_RESPONSE_NO_ERROR)
    LL_GPIO_ResetOutputPin(LED_GPIO_Port, LED_Pin);
  LL_SPI_SetBaudRatePrescaler(SPI1,LL_SPI_BAUDRATEPRESCALER_DIV4);
SD初始化完成后就可以对数据进行读写了,SD卡的读写都是基于块进行的,对于SDHC来说每块的长度固定为512,读数据可以单块或多块读
单块读取:
    1.发送CMD17,等待回应
    2.等待读取到0xFE,
    3.连续读512的数据和2字节的CRC

多块读取:
    1.发送CMD17,等待回应
    2.等待读取到0xFE,
    3.连续读512的数据和2字节的CRC
    4.重复2~3
    5.读取完数据后发送CMD12结束

单块写入:
    1.发送CMD24,等待回应
    2.发送0xFE
    3.发送512字节的数据和2字节CRC
    4.读取回应,回应的低5位如果为0x05说明数据发送成功,之后SD卡进入忙碌状态会将MISO拉低,这期间不能对SD卡进行操作


多块写入:
    1.发送CMD25,等待回应
    2.发送0xFC
    3.发送512字节的数据和2字节CRC
    4.读取回应,回应的低5位如果为0x05说明数据发送成功,之后SD卡进入忙碌状态会将MISO拉低,这期间不能对SD卡进行操作
    5.重复2~4
    6.发送完数据后发送0xFD结束写入

代码实现如下
uint8_t SD_SPI_ReadBlockDatas(uint8_t *datas, uint16_t len)
{
    uint16_t i = 0xFFF;
    while(SD_SPI_ReadByte() != 0xFE)
    {
        if(i == 0)
            return SD_RESPONSE_FAILURE;
        i -= 1;
    }
   
    SD_SPI_ReadDatas(datas,len);
    SD_SPI_ReadByte();
    SD_SPI_ReadByte();
    return SD_RESPONSE_NO_ERROR;
}

uint8_t SD_SPI_WriteBlockDatas(uint8_t flag,uint8_t *datas, uint16_t len)
{
    uint16_t i = 64;
    SD_SPI_WriteByte(0xFF);
    SD_SPI_WriteByte(flag);
    if(flag == 0xFE || flag == 0xFC)
    {
        SD_SPI_WriteDatas(datas,len);
        SD_SPI_WriteByte(0xFF);
        SD_SPI_WriteByte(0xFF);
        while((SD_SPI_ReadByte()&0x1F) != SD_DATA_OK)
        {
            if(i == 0)
                return SD_RESPONSE_FAILURE;
            i -= 1;
        }
    }
    while(SD_SPI_ReadByte() != 0xFF);
    return SD_RESPONSE_NO_ERROR;   
}
uint8_t SD_ReadBlocks(uint32_t block_addr,uint8_t *data,uint32_t block_num)
{
    uint8_t res;
    if(sdcard_block_num == 0 || data == NULL || block_num == 0)
        return SD_RESPONSE_FAILURE;
    if(block_num == 1)
    {
        SD_SendCmd(SD_CMD17_READ_SINGLE_BLOCK, block_addr, 0xFF,&res,0);
        if(res != SD_RESPONSE_NO_ERROR)
            return SD_RESPONSE_FAILURE;
        SD_SPI_ReadBlockDatas(data,512);
    }
    else
    {
        SD_SendCmd(SD_CMD18_READ_MULT_BLOCK, block_addr, 0xFF,&res,0);
        if(res != SD_RESPONSE_NO_ERROR)
            return SD_RESPONSE_FAILURE;
        while(block_num > 0 && res == SD_RESPONSE_NO_ERROR)
        {
            res = SD_SPI_ReadBlockDatas(data,512);
            data += 512;
            block_num -= 1;
        }
        SD_SendCmd(SD_CMD12_STOP_TRANSMISSION, 0, 0xFF,&res,0);
    }
   
    SD_SPI_CSSetLev(1);
    SD_SPI_WriteByte(0xFF);
    return SD_RESPONSE_NO_ERROR;
}

uint8_t SD_WriteBlocks(uint32_t block_addr,uint8_t *data,uint32_t block_num)
{
    uint8_t res;
    if(sdcard_block_num == 0 || data == NULL || block_num == 0)
        return SD_RESPONSE_FAILURE;
    if(block_num == 1)
    {
        SD_SendCmd(SD_CMD24_WRITE_SINGLE_BLOCK, block_addr, 0xFF,&res,0);
        if(res != SD_RESPONSE_NO_ERROR)
            return SD_RESPONSE_FAILURE;
        SD_SPI_WriteBlockDatas(0xFE,data,512);
    }
    else
    {
        SD_SendCmd(SD_CMD25_WRITE_MULT_BLOCK, block_addr, 0xFF,&res,0);
        if(res != SD_RESPONSE_NO_ERROR)
            return SD_RESPONSE_FAILURE;
        while(block_num > 0 && res == SD_RESPONSE_NO_ERROR)
        {
            res = SD_SPI_WriteBlockDatas(0xFC,data,512);
            data += 512;
            block_num -= 1;
        }
        SD_SPI_WriteBlockDatas(0xFD,NULL,0);
    }
    SD_SPI_CSSetLev(1);
    SD_SPI_WriteByte(0xFF);
    return SD_RESPONSE_NO_ERROR;
}
接下来修改USB代码,让电脑可以正确识别SD卡容量并读写SD卡
打开usbd_storage_if.c这个文件,找到STORAGE_GetCapacity_FS这个方法,这个方法告诉电脑存储设备的块大小与块数量,修改如下
int8_t STORAGE_GetCapacity_FS(uint8_t lun, uint32_t *block_num, uint16_t *block_size)
{
  /* USER CODE BEGIN 3 */
  *block_num  = sdcard_block_num;
  *block_size = STORAGE_BLK_SIZ;
  return (USBD_OK);
  /* USER CODE END 3 */
}
找到STORAGE_Read_FS这个方法,这个方法在电脑读取时会被调用,修改如下
int8_t STORAGE_Read_FS(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len)
{
  /* USER CODE BEGIN 6 */
    SD_ReadBlocks(blk_addr,buf,blk_len);
  return (USBD_OK);
  /* USER CODE END 6 */
}
找到STORAGE_Write_FS这个方法,这个方法在电脑读写入数据时会被调用,修改如下
int8_t STORAGE_Write_FS(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len)
{
  /* USER CODE BEGIN 7 */
    SD_WriteBlocks(blk_addr,buf,blk_len);
  return (USBD_OK);
  /* USER CODE END 7 */
}
最后在main中将USB初始化方法放到SD卡初始化方法之后

将SD卡模块与核心板连接好,编译烧录后一个简单的读卡器就做好了,运行效果如下


以上就是一个简单的读卡器实现过程,能够正常的对SD卡进行读写,就是速度有点慢,有兴趣的伙伴可以在此基础上进行扩展


使用特权

评论回复

打赏榜单

21小跑堂 打赏了 110.00 元 2024-12-11
理由:恭喜通过原创审核!期待您更多的原创作品~~

评论
21小跑堂 2024-12-11 10:50 回复TA
使用STM32F103通过SPI和SD卡模块通信,完成SD读卡器的实现。作者设计过程详细,相关知识点介绍较为完整,关键代码展示较好,实现较佳。 
沙发
zeshoufx| | 2024-12-17 09:07 | 只看该作者
谢谢分享,,,你这个b站视频怎么链接到21ic的,,,

使用特权

评论回复
板凳
yuyy1989|  楼主 | 2024-12-17 16:18 | 只看该作者
zeshoufx 发表于 2024-12-17 09:07
谢谢分享,,,你这个b站视频怎么链接到21ic的,,,


点视频把B站视频链接粘进去



使用特权

评论回复
地板
zeshoufx| | 2024-12-17 16:41 | 只看该作者
yuyy1989 发表于 2024-12-17 16:18
点视频把B站视频链接粘进去

好的,,学习了,,谢谢你

使用特权

评论回复
5
Amazingxixixi| | 2024-12-27 17:09 | 只看该作者
非常不错啊

使用特权

评论回复
6
yangjiaxu| | 2024-12-31 13:25 | 只看该作者
速度的话,可以用DMA就会提速,而且还可以根据TF卡的特性,优化一下文件管理系统就好了

使用特权

评论回复
7
申小林一号| | 2024-12-31 14:49 | 只看该作者
感谢分享,学习一下

使用特权

评论回复
8
丙丁先生| | 2025-1-5 09:04 | 只看该作者
感谢分享,如果可以代码压缩包

使用特权

评论回复
9
丙丁先生| | 2025-1-13 11:32 | 只看该作者
感谢分享

使用特权

评论回复
发新帖 本帖赏金 110.00元(功能说明)我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

认证:同飞软件研发工程师
简介:制冷系统单片机软件开发,使用PID控制温度

151

主题

732

帖子

7

粉丝