本帖最后由 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卡进行读写,就是速度有点慢,有兴趣的伙伴可以在此基础上进行扩展
|
此文章已获得独家原创/原创奖标签,著作权归21ic所有,未经允许禁止转载。
打赏榜单
21小跑堂 打赏了 110.00 元 2024-12-11 理由:恭喜通过原创审核!期待您更多的原创作品~~
共2人点赞
|
使用STM32F103通过SPI和SD卡模块通信,完成SD读卡器的实现。作者设计过程详细,相关知识点介绍较为完整,关键代码展示较好,实现较佳。