返回列表 发新帖我要提问本帖赏金: 110.00元(功能说明)

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

[复制链接]
30578|7
 楼主| yuyy1989 发表于 2024-12-7 12:43 | 显示全部楼层 |阅读模式
本帖最后由 yuyy1989 于 2024-12-7 12:46 编辑

@21小跑堂 #申请原创#
用到的硬件,STM32F103核心板USB引脚已引出,SPI通讯的SD卡模块
微信截图_20241206211518.png
打开STM32CubeMX配置SPI和USB
微信截图_20241206212001.png 微信截图_20241206212013.png
在USB_DEVICE这里把USB缓存改为512,STM32F103C8T6的USB和CAN共享512字节的SRAM

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

微信截图_20241206212137.png
接下来实现SPI读写SD卡
实现SPI读写方法
  1. void SD_SPI_CSSetLev(uint8_t lev)
  2. {
  3.     if(lev == 0)
  4.         LL_GPIO_ResetOutputPin(SPI1_CS_GPIO_Port,SPI1_CS_Pin);
  5.     else
  6.         LL_GPIO_SetOutputPin(SPI1_CS_GPIO_Port,SPI1_CS_Pin);
  7. }
  8. uint8_t SD_SPI_WriteByte(uint8_t data)
  9. {
  10.     uint8_t rev = 0;
  11.     while(!LL_SPI_IsActiveFlag_TXE(SPI1));
  12.     LL_SPI_TransmitData8(SPI1,data);
  13.     while(!LL_SPI_IsActiveFlag_RXNE(SPI1));
  14.     rev = LL_SPI_ReceiveData8(SPI1);
  15.     return rev;
  16. }
  17. uint8_t SD_SPI_ReadByte(void)
  18. {
  19.     return SD_SPI_WriteByte(0xFF);
  20. }
  21. void SD_SPI_WriteDatas(uint8_t *datas, uint16_t len)
  22. {
  23.     while(len > 0)
  24.     {
  25.         SD_SPI_WriteByte(*datas);
  26.         datas += 1;
  27.         len -= 1;
  28.     }
  29. }
  30. void SD_SPI_ReadDatas(uint8_t *datas, uint16_t len)
  31. {
  32.     while(len > 0)
  33.     {
  34.         *datas = SD_SPI_ReadByte();
  35.         datas += 1;
  36.         len -= 1;
  37.     }
  38. }


通电后需要等待SD卡电压稳定后再进行后续通讯
微信截图_20241206213432.png
通过SPI向SD卡发送的命令格式如下
微信截图_20241206213632.png
涉及到的部分命令码如下
微信截图_20241206214109.png
几种应答的数据格式:
    R1,1字节,R1b与R1格式相同,但可选地添加了忙信号。忙信号令牌可以是任意数量的字节。零值表示卡正忙。非零值表示卡已准备好下一个命令。
微信截图_20241206214325.png
    R2,2字节
微信截图_20241206214423.png
    R3,5字节,用于获取OCR寄存器的数据。高8位为R1令牌,低4字节为OCR寄存器的数据。
微信截图_20241206214613.png
    R7,共5字节,主要用于获取SD卡工作电压信息,高8位为R1令牌。
微信截图_20241206214654.png
在SPI模式中,只有CMD0和CMD8需要CRC,其他命令的CRC无效直接填0xFF即可
封装命令发送方法
  1. /**
  2. * [url=home.php?mod=space&uid=247401]@brief[/url]  SD卡命令
  3. */
  4. #define SD_CMD0_GO_IDLE_STATE           0   //复位所有的卡到idle状态
  5. #define SD_CMD1_SEND_OP_COND            1   //发送主机支持的电压操作范围
  6. #define SD_CMD8_SEND_IF_COND            8   //发送SD卡接口条件,包含主机支持的电压信息,并询问卡是否支持
  7. #define SD_CMD9_SEND_CSD                9   //要求卡发送其CSD寄存器内容
  8. #define SD_CMD10_SEND_CID               10  //要求卡发送其CID寄存器内容
  9. #define SD_CMD12_STOP_TRANSMISSION      12  //强制卡停止传输,可用于多块读写时表示结束
  10. #define SD_CMD13_SEND_STATUS            13  //要求卡发送状态寄存器内容
  11. #define SD_CMD16_SET_BLOCKLEN           16  //对于标准SD卡设置块命令长度,SDHC卡固定512
  12. #define SD_CMD17_READ_SINGLE_BLOCK      17  //对于标准SD卡读取指定长度的块,SDHC卡固定读取512
  13. #define SD_CMD18_READ_MULT_BLOCK        18  //连续从SD卡读取数据块,直到被CMD12打断
  14. #define SD_CMD24_WRITE_SINGLE_BLOCK     24  //对于标准SD卡设置写入CMD16设置长度的数据,SDHC卡固定512
  15. #define SD_CMD25_WRITE_MULT_BLOCK       25  //连续向SD卡写入数据,直到被CMD12打断
  16. #define SD_CMD27_PROG_CSD               27  //对CSD的可编程位进行编程
  17. #define SD_CMD30_SEND_WRITE_PROT        30  //要求卡发送写保护状态
  18. #define SD_CMD32_SD_ERASE_GRP_START     32  //设置擦除的起始块地址
  19. #define SD_CMD33_SD_ERASE_GRP_END       33  //设置擦除的结束块地址
  20. #define SD_CMD38_ERASE                  38  //擦除选定的块
  21. #define SD_CMD58_READ_OCR               58  //要求返回OCR寄存器的值
  22. #define SD_CMD55_APP_CMD                55  //发送ACMD前发送此命令,返回0x01
  23. #define SD_ACMD23_SE_SET_BLOCK_COUNT    23  //设置预擦除块数
  24. #define SD_ACMD41_SD_SEND_OP_COND       41  //返回0x00

  25. typedef enum {
  26.     /**
  27.     * [url=home.php?mod=space&uid=247401]@brief[/url]  SD 响应及错误标志
  28.     */
  29.     SD_RESPONSE_NO_ERROR      = (0x00),
  30.     SD_IN_IDLE_STATE          = (0x01),
  31.     SD_ERASE_RESET            = (0x02),
  32.     SD_ILLEGAL_COMMAND        = (0x04),
  33.     SD_COM_CRC_ERROR          = (0x08),
  34.     SD_ERASE_SEQUENCE_ERROR   = (0x10),
  35.     SD_ADDRESS_ERROR          = (0x20),
  36.     SD_PARAMETER_ERROR        = (0x40),
  37.     SD_RESPONSE_FAILURE       = (0xFF),

  38.     /**
  39.     * @brief  数据响应类型
  40.     */
  41.     SD_DATA_OK                = (0x05),
  42.     SD_DATA_CRC_ERROR         = (0x0B),
  43.     SD_DATA_WRITE_ERROR       = (0x0D),
  44.     SD_DATA_OTHER_ERROR       = (0xFF)
  45. } SD_Error;

  46. enum {
  47.     SD_RESPONSE_TYPE_NONE = 0,
  48.     SD_RESPONSE_TYPE_R1,
  49.     SD_RESPONSE_TYPE_R1b,
  50.     SD_RESPONSE_TYPE_R2,
  51.     SD_RESPONSE_TYPE_R3,
  52.     SD_RESPONSE_TYPE_R7,
  53. };
  54. uint8_t SD_GetCMResponseType(uint8_t cmd)
  55. {
  56.     switch(cmd)
  57.     {
  58.         case SD_CMD0_GO_IDLE_STATE:
  59.         case SD_CMD1_SEND_OP_COND:
  60.         case SD_CMD9_SEND_CSD:
  61.         case SD_CMD10_SEND_CID:
  62.         case SD_CMD16_SET_BLOCKLEN:
  63.         case SD_CMD17_READ_SINGLE_BLOCK:
  64.         case SD_CMD18_READ_MULT_BLOCK:
  65.         case SD_CMD24_WRITE_SINGLE_BLOCK:
  66.         case SD_CMD25_WRITE_MULT_BLOCK:
  67.         case SD_CMD27_PROG_CSD:
  68.         case SD_CMD32_SD_ERASE_GRP_START:
  69.         case SD_CMD33_SD_ERASE_GRP_END:
  70.         case SD_CMD55_APP_CMD:
  71.         case SD_ACMD41_SD_SEND_OP_COND:
  72.             return SD_RESPONSE_TYPE_R1;
  73.             break;
  74.         case SD_CMD12_STOP_TRANSMISSION:
  75.             return SD_RESPONSE_TYPE_R1b;
  76.             break;
  77.         case SD_CMD13_SEND_STATUS:
  78.             return SD_RESPONSE_TYPE_R2;
  79.             break;
  80.         case SD_CMD58_READ_OCR:
  81.             return SD_RESPONSE_TYPE_R3;
  82.             break;
  83.         case SD_CMD8_SEND_IF_COND:
  84.             return SD_RESPONSE_TYPE_R7;
  85.             break;
  86.         default:
  87.             break;
  88.     }
  89.     return SD_RESPONSE_TYPE_NONE;
  90. }

  91. void SD_SendCmd(uint8_t cmd, uint32_t arg, uint8_t crc,uint8_t *res,uint8_t release_cs)
  92. {
  93.     uint8_t datas[6];
  94.     uint16_t i = 0xFFF;
  95.     SD_SPI_CSSetLev(0);
  96.     datas[0] = (cmd | 0x40); /*!< Construct byte 1 */
  97.     datas[1] = (uint8_t)(arg >> 24); /*!< Construct byte 2 */
  98.     datas[2] = (uint8_t)(arg >> 16); /*!< Construct byte 3 */
  99.     datas[3] = (uint8_t)(arg >> 8); /*!< Construct byte 4 */
  100.     datas[4] = (uint8_t)(arg); /*!< Construct byte 5 */
  101.     datas[5] = (crc); /*!< Construct CRC: byte 6 */
  102.     SD_SPI_WriteDatas(datas,6);
  103.     do
  104.     {
  105.         res[0] = SD_SPI_ReadByte();
  106.         if(i == 0)
  107.         {
  108.             res[0]= SD_RESPONSE_FAILURE;
  109.             return;
  110.         }
  111.         i -= 1;
  112.     }while(res[0]&0x80);
  113.     if(SD_GetCMResponseType(cmd) == SD_RESPONSE_TYPE_R2)
  114.     {
  115.         res[1]= SD_SPI_ReadByte();
  116.     }
  117.     else if(SD_GetCMResponseType(cmd) == SD_RESPONSE_TYPE_R3 || SD_GetCMResponseType(cmd) == SD_RESPONSE_TYPE_R7)
  118.     {
  119.         i = 1;
  120.         while(i<5)
  121.         {
  122.             res[i] = SD_SPI_ReadByte();
  123.             i += 1;
  124.         }
  125.     }
  126.     if(release_cs == 0)
  127.         return;
  128.     SD_SPI_CSSetLev(1);
  129.     SD_SPI_WriteByte(0xFF);
  130. }

SD卡初始化时,拉低CS并发送CMD0使SD卡进入SPI模式,之后通过CMD8判断SD卡版本,如果是2.0版本再通过CMD58判断是不是SDHC,完整流程如图
微信截图_20241207101852.png
完成初始化流程后读取CSD寄存器,里面有SD卡大小的信息,CSD结构如图
微信截图_20241207102515.png
根据C_SIZE就能计算出SD卡大小

微信截图_20241207102526.png
我这里为了方便只判断是否符合SDHC,有兴趣的可以在此基础上实现完整的初始化过程,代码如下
  1. uint32_t sdcard_block_num = 0;
  2. /**
  3. * @brief  初始化 SD/SD 卡
  4. * @param  None
  5. * @retval  SD 响应:
  6. *         - SD_RESPONSE_FAILURE: 初始化失败
  7. *         - SD_RESPONSE_NO_ERROR: 初始化成功
  8. */
  9. uint8_t SD_Init(void)
  10. {
  11.     uint8_t i;
  12.     uint8_t res[5];
  13.     uint8_t csd[16];
  14.     SD_SPI_CSSetLev(1);
  15.     for (i = 0; i < 10; i++)
  16.     {
  17.         SD_SPI_WriteByte(0xFF);
  18.     }
  19.     //进入空闲模式
  20.     SD_SendCmd(SD_CMD0_GO_IDLE_STATE, 0, 0x95,res,1);
  21.     if(res[0] != SD_IN_IDLE_STATE)
  22.         return SD_RESPONSE_FAILURE;

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

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

  28.     i = 200;
  29.     do
  30.     {
  31.         SD_SendCmd(SD_CMD55_APP_CMD, 0, 0xFF,res,1);
  32.         if(res[0] != SD_IN_IDLE_STATE)
  33.             return SD_RESPONSE_FAILURE;
  34.         SD_SendCmd(SD_ACMD41_SD_SEND_OP_COND, 0x40000000, 0xFF,res,1);

  35.         if (i == 0)
  36.             return SD_RESPONSE_FAILURE;
  37.         i-=1;
  38.     } while (res[0] != SD_RESPONSE_NO_ERROR);
  39.     i = 200;
  40.     do
  41.     {
  42.         SD_SendCmd(SD_CMD58_READ_OCR, 0, 0xFF,res,1);
  43.         if (i == 0)
  44.             return SD_RESPONSE_FAILURE;
  45.         i-=1;
  46.     } while (res[0] != SD_RESPONSE_NO_ERROR);
  47.     if((res[1]&0x40) == 0)
  48.         return SD_RESPONSE_FAILURE;

  49.     //读CSD
  50.     SD_SendCmd(SD_CMD9_SEND_CSD, 0, 0xFF,res,0);
  51.     if(res[0] == SD_RESPONSE_NO_ERROR)
  52.     {
  53.         SD_SPI_ReadBlockDatas(csd,16);
  54.     }
  55.     sdcard_block_num = (csd[9] + ((uint16_t)csd[8] << 8) + 1)*1024;
  56.     SD_SPI_CSSetLev(1);
  57.     SD_SPI_WriteByte(0xFF);
  58.     return SD_RESPONSE_NO_ERROR;
  59. }
需要注意的是,在SD卡初始化过程中SPI时钟频率配置在400kHz以下,保证卡能够稳定地进入工作状态,初始化完成后再切换为高频率,所以在main中应该这样调用
  1.   LL_GPIO_SetOutputPin(LED_GPIO_Port, LED_Pin);
  2.   LL_SPI_SetBaudRatePrescaler(SPI1,LL_SPI_BAUDRATEPRESCALER_DIV256);
  3.   if(SD_Init() == SD_RESPONSE_NO_ERROR)
  4.     LL_GPIO_ResetOutputPin(LED_GPIO_Port, LED_Pin);
  5.   LL_SPI_SetBaudRatePrescaler(SPI1,LL_SPI_BAUDRATEPRESCALER_DIV4);
SD初始化完成后就可以对数据进行读写了,SD卡的读写都是基于块进行的,对于SDHC来说每块的长度固定为512,读数据可以单块或多块读
单块读取:
    1.发送CMD17,等待回应
    2.等待读取到0xFE,
    3.连续读512的数据和2字节的CRC
微信截图_20241207103404.png
多块读取:
    1.发送CMD17,等待回应
    2.等待读取到0xFE,
    3.连续读512的数据和2字节的CRC
    4.重复2~3
    5.读取完数据后发送CMD12结束
微信截图_20241207103903.png
单块写入:
    1.发送CMD24,等待回应
    2.发送0xFE
    3.发送512字节的数据和2字节CRC
    4.读取回应,回应的低5位如果为0x05说明数据发送成功,之后SD卡进入忙碌状态会将MISO拉低,这期间不能对SD卡进行操作

微信截图_20241207104140.png
多块写入:
    1.发送CMD25,等待回应
    2.发送0xFC
    3.发送512字节的数据和2字节CRC
    4.读取回应,回应的低5位如果为0x05说明数据发送成功,之后SD卡进入忙碌状态会将MISO拉低,这期间不能对SD卡进行操作
    5.重复2~4
    6.发送完数据后发送0xFD结束写入
微信截图_20241207105137.png
代码实现如下
  1. uint8_t SD_SPI_ReadBlockDatas(uint8_t *datas, uint16_t len)
  2. {
  3.     uint16_t i = 0xFFF;
  4.     while(SD_SPI_ReadByte() != 0xFE)
  5.     {
  6.         if(i == 0)
  7.             return SD_RESPONSE_FAILURE;
  8.         i -= 1;
  9.     }
  10.    
  11.     SD_SPI_ReadDatas(datas,len);
  12.     SD_SPI_ReadByte();
  13.     SD_SPI_ReadByte();
  14.     return SD_RESPONSE_NO_ERROR;
  15. }

  16. uint8_t SD_SPI_WriteBlockDatas(uint8_t flag,uint8_t *datas, uint16_t len)
  17. {
  18.     uint16_t i = 64;
  19.     SD_SPI_WriteByte(0xFF);
  20.     SD_SPI_WriteByte(flag);
  21.     if(flag == 0xFE || flag == 0xFC)
  22.     {
  23.         SD_SPI_WriteDatas(datas,len);
  24.         SD_SPI_WriteByte(0xFF);
  25.         SD_SPI_WriteByte(0xFF);
  26.         while((SD_SPI_ReadByte()&0x1F) != SD_DATA_OK)
  27.         {
  28.             if(i == 0)
  29.                 return SD_RESPONSE_FAILURE;
  30.             i -= 1;
  31.         }
  32.     }
  33.     while(SD_SPI_ReadByte() != 0xFF);
  34.     return SD_RESPONSE_NO_ERROR;   
  35. }
  36. uint8_t SD_ReadBlocks(uint32_t block_addr,uint8_t *data,uint32_t block_num)
  37. {
  38.     uint8_t res;
  39.     if(sdcard_block_num == 0 || data == NULL || block_num == 0)
  40.         return SD_RESPONSE_FAILURE;
  41.     if(block_num == 1)
  42.     {
  43.         SD_SendCmd(SD_CMD17_READ_SINGLE_BLOCK, block_addr, 0xFF,&res,0);
  44.         if(res != SD_RESPONSE_NO_ERROR)
  45.             return SD_RESPONSE_FAILURE;
  46.         SD_SPI_ReadBlockDatas(data,512);
  47.     }
  48.     else
  49.     {
  50.         SD_SendCmd(SD_CMD18_READ_MULT_BLOCK, block_addr, 0xFF,&res,0);
  51.         if(res != SD_RESPONSE_NO_ERROR)
  52.             return SD_RESPONSE_FAILURE;
  53.         while(block_num > 0 && res == SD_RESPONSE_NO_ERROR)
  54.         {
  55.             res = SD_SPI_ReadBlockDatas(data,512);
  56.             data += 512;
  57.             block_num -= 1;
  58.         }
  59.         SD_SendCmd(SD_CMD12_STOP_TRANSMISSION, 0, 0xFF,&res,0);
  60.     }
  61.    
  62.     SD_SPI_CSSetLev(1);
  63.     SD_SPI_WriteByte(0xFF);
  64.     return SD_RESPONSE_NO_ERROR;
  65. }

  66. uint8_t SD_WriteBlocks(uint32_t block_addr,uint8_t *data,uint32_t block_num)
  67. {
  68.     uint8_t res;
  69.     if(sdcard_block_num == 0 || data == NULL || block_num == 0)
  70.         return SD_RESPONSE_FAILURE;
  71.     if(block_num == 1)
  72.     {
  73.         SD_SendCmd(SD_CMD24_WRITE_SINGLE_BLOCK, block_addr, 0xFF,&res,0);
  74.         if(res != SD_RESPONSE_NO_ERROR)
  75.             return SD_RESPONSE_FAILURE;
  76.         SD_SPI_WriteBlockDatas(0xFE,data,512);
  77.     }
  78.     else
  79.     {
  80.         SD_SendCmd(SD_CMD25_WRITE_MULT_BLOCK, block_addr, 0xFF,&res,0);
  81.         if(res != SD_RESPONSE_NO_ERROR)
  82.             return SD_RESPONSE_FAILURE;
  83.         while(block_num > 0 && res == SD_RESPONSE_NO_ERROR)
  84.         {
  85.             res = SD_SPI_WriteBlockDatas(0xFC,data,512);
  86.             data += 512;
  87.             block_num -= 1;
  88.         }
  89.         SD_SPI_WriteBlockDatas(0xFD,NULL,0);
  90.     }
  91.     SD_SPI_CSSetLev(1);
  92.     SD_SPI_WriteByte(0xFF);
  93.     return SD_RESPONSE_NO_ERROR;
  94. }
接下来修改USB代码,让电脑可以正确识别SD卡容量并读写SD卡
打开usbd_storage_if.c这个文件,找到STORAGE_GetCapacity_FS这个方法,这个方法告诉电脑存储设备的块大小与块数量,修改如下
  1. int8_t STORAGE_GetCapacity_FS(uint8_t lun, uint32_t *block_num, uint16_t *block_size)
  2. {
  3.   /* USER CODE BEGIN 3 */
  4.   *block_num  = sdcard_block_num;
  5.   *block_size = STORAGE_BLK_SIZ;
  6.   return (USBD_OK);
  7.   /* USER CODE END 3 */
  8. }
找到STORAGE_Read_FS这个方法,这个方法在电脑读取时会被调用,修改如下
  1. int8_t STORAGE_Read_FS(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len)
  2. {
  3.   /* USER CODE BEGIN 6 */
  4.     SD_ReadBlocks(blk_addr,buf,blk_len);
  5.   return (USBD_OK);
  6.   /* USER CODE END 6 */
  7. }
找到STORAGE_Write_FS这个方法,这个方法在电脑读写入数据时会被调用,修改如下
  1. int8_t STORAGE_Write_FS(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len)
  2. {
  3.   /* USER CODE BEGIN 7 */
  4.     SD_WriteBlocks(blk_addr,buf,blk_len);
  5.   return (USBD_OK);
  6.   /* USER CODE END 7 */
  7. }
最后在main中将USB初始化方法放到SD卡初始化方法之后
微信截图_20241207124544.png
将SD卡模块与核心板连接好,编译烧录后一个简单的读卡器就做好了,运行效果如下


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


打赏榜单

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

评论

使用STM32F103通过SPI和SD卡模块通信,完成SD读卡器的实现。作者设计过程详细,相关知识点介绍较为完整,关键代码展示较好,实现较佳。  发表于 2024-12-11 10:50
zeshoufx 发表于 2024-12-17 09:07 | 显示全部楼层
谢谢分享,,,你这个b站视频怎么链接到21ic的,,,
 楼主| yuyy1989 发表于 2024-12-17 16:18 | 显示全部楼层
zeshoufx 发表于 2024-12-17 09:07
谢谢分享,,,你这个b站视频怎么链接到21ic的,,,

微信截图_20241217161722.png
点视频把B站视频链接粘进去



zeshoufx 发表于 2024-12-17 16:41 | 显示全部楼层
yuyy1989 发表于 2024-12-17 16:18
点视频把B站视频链接粘进去

好的,,学习了,,谢谢你
Amazingxixixi 发表于 2024-12-27 17:09 | 显示全部楼层
非常不错啊
yangjiaxu 发表于 2024-12-31 13:25 | 显示全部楼层
速度的话,可以用DMA就会提速,而且还可以根据TF卡的特性,优化一下文件管理系统就好了
申小林一号 发表于 2024-12-31 14:49 | 显示全部楼层
感谢分享,学习一下
您需要登录后才可以回帖 登录 | 注册

本版积分规则

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

168

主题

826

帖子

10

粉丝
快速回复 在线客服 返回列表 返回顶部