SD NAND 的 SDIO在STM32上的应用详解(下篇)

七.sdio外设结构体其实前面关于sdio寄存器的讲解已经比较详细了,这里再借助于关于sdio结构体再进行总结一遍。
标准库函数对 sdio 外设建立了三个初始化结构体,分别为 sdio 初始化结构体sdio_inittypedef、sdio 命令初始化结构体 sdio_cmdinittypedef 和 sdio 数据初始化结
构体 sdio_datainittypedef。这些结构体成员用于设置 sdio 工作环境参数,并由 sdio 相应初始化配置函数或功能函数调用,这些参数将会被写入到 sdio 相应的寄存器,达到配置 sdio 工作环境的目的。
至于为什么需要一个命令结构体与数据结构体,就是为了方便我们配置sdio关于寄存器位,因为发送命令或者数据需要很多参数配置。
1.sdio初始化结构体sdio 初始化结构体用于配置 sdio 基本工作环境,比如时钟分频、时钟沿、数据宽度等等。它被 sdio_init 函数使用。
1) sdio_clockedge:主时钟 sdioclk 产生 clk 引脚时钟有效沿选择,可选上升沿或下降沿。
2) sdio_clockbypass:时钟分频旁路使用,可选使能或禁用,如果使能旁路,sdioclk (72mhz )直接驱动 clk 线输出时钟(不满足最高25hz的要求),如果禁用,使用 sdio_clkcr 寄存器的 clkdiv 位值分频 sdioclk,然后输出到 clk 线。一般选择禁用时钟分频旁路。
3) sdio_clockpowersave:节能模式选择,可选使能或禁用,它设定 sdio_clkcr 寄存器的 pwrsav 位的值。如果使能节能模式,clk 线只有在总线激活时才有时钟输出;如果禁用节能模式,始终使能 clk 线输出时钟。
4) sdio_buswide:数据线宽度选择,可选 1 位数据总线、4 位数据总线或 8 为数据总线,系统默认使用 1 位数据总线,操作 sd 卡时在数据传输模式下一般选择 4 位数据总线。它设定 sdio_clkcr 寄存器的 widbus 位的值。
5) sdio_hardwareflowcontrol:硬件流控制选择,可选使能或禁用,它设定sdio_clkcr 寄存器的 hwfc_en 位的值。硬件流控制功能可以避免 fifo 发送上溢和下溢错误。
6) sdio_clockdiv:时钟分频系数,它设定 sdio_clkcr 寄存器的 clkdiv 位的值,设置 sdioclk 与 clk 线输出时钟分频系数:
clk 线时钟频率=sdioclk/([clkdiv+2])。
2.sdio命令初始化结构体
1) sdio_argument:作为命令的一部分发送到卡的命令参数,它设定 sdio 参数寄存器(sdio_arg)的值。
(2) sdio_cmdindex:命令号选择,它设定 sdio 命令寄存器(sdio_cmd)的 cmdindex位的值。
(3) sdio_response:响应类型,sdio 定义两个响应类型:长响应和短响应。根据命令号选择对应的响应类型。sdio 定义了四个 32 位的 sdio 响应寄存器(sdio_respx,x=1…4),短响应只用sdio_resp1,长响应使用4个(sdio_respx,x=1…4)。
1)命令响应寄存器
2)sdio响应寄存器1~4
4) sdio_wait:等待类型选择,有三种状态可选,一种是无等待状态,超时检测功能启动,一种是等待中断,另外一种是等待传输完成。
5) sdio_cpsm:命令路径状态机控制,可选使能或禁用 cpsm。它设定 sdio_cmd 寄存器的 cpsmen 位的值
只要我们使能的了命令状态机,则下面发送命令和接收响应的过程中的状态转换就不用我们管了
当我们要发送命令,我们只需要配置这个命令初始化结构体的成员,然后调用下图这个函数,则我们配置的参数写入对应的寄存器位中。
3.sdio数据初始化结构体
1) sdio_datatimeout:设置数据传输以卡总线时钟周期表示的超时周期,它设定 sdio数据定时器寄存器(sdio_dtimer)的值。在 dpsm 进入 wait_r 或繁忙状态后开始递减,直到 0 还处于以上两种状态则将超时状态标志置 1(详情前面的数据通道小节)。
2) sdio_datalength:设置传输数据长度。
3) sdio_datablocksize:设置数据块大小,有多种尺寸可选,不同命令要求的数据块可能不同。
4) sdio_transferdir:数据传输方向,可选从主机到卡的写操作,或从卡到主机的读操作。
5) sdio_transfermode:数据传输模式,可选数据块或数据流模式。对于 sd 卡操作使用数据块类型。
6) sdio_dpsm:数据路径状态机控制,可选使能或禁用 dpsm。它设定 sdio_dctrl寄存器的 dten 位的值。要实现数据传输都必须使能 sdio_dpsm。
与命令一样使能了数据路径状态机,就不用高那么多麻烦的状态转换了
八.sd卡读写测试实验我们平时使用的sd 卡都是已经包含有文件系统的,一般不会使用本实验的操作方式读写 sd 卡,但是对学习sd卡的驱动原理非常重要!!!
本实验是进行 sd卡最底层的数据读写操作,直接使用 sdio 对 sd 卡进行读写,会损坏 sd 卡的文件系统,导致数据丢失,所以做这个实验之前需要备份sd卡数据。
主要是学习sd卡的卡识别过程,以及数据传输工过程,其实就是完全依照前面的两个流程图来实现代码的。
卡识别模式流程图
数据传输流程图
1.硬件设计原理图:
实物图:
我这里用的是cs创世的贴片式sd卡,也称之为sd nand , 内部存储单元架构为slc,适合存代码。直接上板时相比于拔插式sd卡在抗震和抗pin氧化方面更有优势,对于缩小整板体积也有一定帮助。
详情请参考:雷龙官网
2.代码讲解先看主函数:
sd_terst函数:
我们主要讲解的就是sd卡的初始化
sd_init()函数:
/**
 * 函数名:sd_init
 * 描述:初始化sd卡,使卡处于就绪状态(准备传输数据)
 * 输入:无
 * 输出:-sd_error sd卡错误代码
 * 成功时则为 sd_ok
 * 调用:外部调用
 */sd_error sd_init(void){
/*重置sd_error状态*/
sd_error errorstatus = sd_ok;
nvic_configuration();
/* sdio 外设底层引脚初始化 */
gpio_configuration();
/*对sdio的所有寄存器进行复位*/
sdio_deinit();
/*上电并进行卡识别流程,确认卡的操作电压*/
errorstatus = sd_poweron(); 
/*如果上电,识别不成功,返回“响应超时”错误 */
if (errorstatus != sd_ok)
{
/*!< cmd response timeout (wait for cmdsent flag) */
return(errorstatus);
}
/*卡识别成功,进行卡初始化*/
errorstatus = sd_initializecards(); 
if (errorstatus != sd_ok) //失败返回
{
/*!< cmd response timeout (wait for cmdsent flag) */
return(errorstatus);
}
/* 配置sdio外设
 * 上电识别,卡初始化都完成后,进入数据传输模式,提高读写速度
 */
/* sdioclk = hclk, sdio_ck = hclk/(2 + sdio_transfer_clk_div) */
sdio_initstructure.sdio_clockdiv = sdio_transfer_clk_div;
/*上升沿采集数据 */
sdio_initstructure.sdio_clockedge = sdio_clockedge_rising;
/* bypass模式使能的话,sdio_ck不经过sdio_clockdiv分频 */
sdio_initstructure.sdio_clockbypass = sdio_clockbypass_disable;
/* 若开启此功能,在总线空闲时关闭sd_clk时钟 */
sdio_initstructure.sdio_clockpowersave = sdio_clockpowersave_disable;
/* 暂时配置成1bit模式 */
sdio_initstructure.sdio_buswide = sdio_buswide_1b;
/* 硬件流,若开启,在fifo不能进行发送和接收数据时,数据传输暂停 */
sdio_initstructure.sdio_hardwareflowcontrol = sdio_hardwareflowcontrol_disable;
sdio_init(&sdio_initstructure);
if (errorstatus == sd_ok)
{
/* 用来读取csd/cid寄存器 */
errorstatus = sd_getcardinfo(&sdcardinfo);
}
if (errorstatus == sd_ok)
{
/* 通过cmd7,rca选择要操作的卡 */
errorstatus = sd_selectdeselect((uint32_t) (sdcardinfo.rca < 31) == 1) ? 1 : 0);
count++; /* 计算循环次数 */
}
if (count >= sd_max_volt_trial)  /* 循环检测超过一定次数还没上电 */
{
errorstatus = sd_invalid_voltrange;  /* sdio不支持card的供电电压 */
return(errorstatus);
}
/*检查卡返回信息中的hcs位*/
/* 判断ocr中的ccs位 ,如果是sdsc卡则不执行下面的语句 */
if (response &= sd_high_capacity) 
{
cardtype = sdio_high_capacity_sd_card; /* 把卡类型从初始化的sdsc型改为sdhc型 */
}
}/* else mmc card */
return(errorstatus); }
1.配置sdio初始化结构体**
配置 sdio_initstructure 结构体变量成员并调用 sdio_init 库函数完成 sdio 外设的基本配置,注意此处的 sdio 时钟分频,由于处于卡识别阶段,其时钟不能超过 400khz。
2.发送cmd0命令:要sd卡回到空闲状态
那些检测标志全是来源与下图:
3.发送cmd8: 用来识别不同版本的卡和检测卡是否能在主机提供的电压下工作。
如果发送cmd8无响应:
1.电压不匹配的 2.0 以上 sd 卡
2.1.0 的 sd 卡
3.不是 sd 卡
如果发送cmd8有响应:
电压匹配的 2.0 以上 sd 卡(就是我们即将要使用的sd卡)
4.使用 acmd41 命令判断卡的具体类型。因为是 a 类命令,所以在发送 acmd41之前必须先发送 cmd55,cmd55 命令的响应类型的 r1。如果 cmd55 命令都没有响应说明是 mmc 卡或不可用卡。在正确发送 cmd55 之后就可以送acmd41,并根据响应判断卡类型,acmd41 的响应号为 r3,cmdresp3error 函数用于检测命令正确发送并带有超时检测功能,但并不具备响应内容接收功能,需要在判定命令正确发送之后调用 sdio_getresponse 函数才能获取响应的内容。
实际上,在有响应时,sdio 外设会自动把响应存放在 sdio_respx 寄存器中,sdio_getresponse 函数只是根据形参返回对应响应寄存器的值。通过判定响应内容值即可确定 sd 卡类型。
总结:执行 sd_poweron 函数无错误后就已经确定了 sd 卡类型,并说明卡和主机电压是匹配的,sd 卡处于卡识别模式下的准备状态。退出 sd_poweron 函数返回sd_init 函数,执行接下来代码。
执行 sd_poweron 函数没有错误后:sd 卡处于卡识别模式下的准备状态
sd_initializecards()函数:
/*
 * 函数名:sd_initializecards
 * 描述:初始化所有的卡或者单个卡进入就绪状态
 * 输入:无
 * 输出:-sd_error sd卡错误代码
 * 成功时则为 sd_ok
 * 调用:在 sd_init() 调用,在调用power_on()上电卡识别完毕后,调用此函数进行卡初始化
 */sd_error sd_initializecards(void){
sd_error errorstatus = sd_ok;
uint16_t rca = 0x01;
if (sdio_getpowerstate() == sdio_powerstate_off)
{
errorstatus = sd_request_not_applicable;
return(errorstatus);
}
/* 判断卡的类型 */
if (sdio_secure_digital_io_card != cardtype)
{
/* send cmd2 all_send_cid 
 * 响应:r2,对应cid寄存器
 */
sdio_cmdinitstructure.sdio_argument = 0x0;
sdio_cmdinitstructure.sdio_cmdindex = sd_cmd_all_send_cid;
sdio_cmdinitstructure.sdio_response = sdio_response_long;
sdio_cmdinitstructure.sdio_wait = sdio_wait_no;
sdio_cmdinitstructure.sdio_cpsm = sdio_cpsm_enable;
sdio_sendcommand(&sdio_cmdinitstructure);
errorstatus = cmdresp2error();
if (sd_ok != errorstatus)
{
return(errorstatus);
}
/* 将返回的cid信息存储起来 */
cid_tab[0] = sdio_getresponse(sdio_resp1);
cid_tab[1] = sdio_getresponse(sdio_resp2);
cid_tab[2] = sdio_getresponse(sdio_resp3);
cid_tab[3] = sdio_getresponse(sdio_resp4);
}/********************************************************************************************************/
if ( (sdio_std_capacity_sd_card_v1_1 == cardtype) 
 ||(sdio_std_capacity_sd_card_v2_0 == cardtype) 
 ||(sdio_secure_digital_io_combo_card == cardtype)
 ||(sdio_high_capacity_sd_card == cardtype) )  /* 使用的是2.0的卡 */
{
/* send cmd3 set_rel_addr with argument 0 
 * sd card publishes its rca.
 * 响应:r6,对应rca寄存器
 */
sdio_cmdinitstructure.sdio_argument = 0x00;
sdio_cmdinitstructure.sdio_cmdindex = sd_cmd_set_rel_addr;
sdio_cmdinitstructure.sdio_response = sdio_response_short;
sdio_cmdinitstructure.sdio_wait = sdio_wait_no;
sdio_cmdinitstructure.sdio_cpsm = sdio_cpsm_enable;
sdio_sendcommand(&sdio_cmdinitstructure);
/* 把接收到的卡相对地址存起来 */
errorstatus = cmdresp6error(sd_cmd_set_rel_addr, &rca);
if (sd_ok != errorstatus)
{
return(errorstatus);
}
}/********************************************************************************************************/
if (sdio_secure_digital_io_card != cardtype)
{
rca = rca;
/* send cmd9 send_csd with argument as card's rca 
 * 响应:r2对应寄存器csd(card-specific data)
 */
sdio_cmdinitstructure.sdio_argument = (uint32_t)(rca cmd38。如果发送顺序不对,sd 卡会设置 erase_seq_error 位到状态寄存器
2) sd_erase 函数发送 cmd32 指令用于设定擦除块开始地址,在执行无错误后发送cmd33 设置擦除块的结束地址。
3) 发送擦除命令 cmd38,使得 sd 卡进行擦除操作。sd 卡擦除操作由 sd 卡内部控制完成,不同卡擦除后是 0xff 还是 0x00 由厂家决定。擦除操作需要花费一定时间,这段时间不能对 sd 卡进行其他操作。
4) 通过 iscardprogramming 函数可以检测 sd 卡是否处于编程状态(即卡内部的擦写状态),需要确保 sd 卡擦除完成才退出 sd_erase 函数。iscardprogramming 函数先通过发送cmd13 命令 sd 卡发送它的状态寄存器内容,并对响应内容进行分析得出当前 sd 卡的状态以及可能发送的错误。
数据写入操作
sd_writeblock 函数用于向指定的目标地址写入一个块的数据,它有三个形参,分别为指向待写入数据的首地址的指针变量、目标写入地址和块大小。块大小一般都设置为512 字节。sd_writeblock 写入函数的执行流程如下:
1) sd_writeblock 函数开始时将 sdio 数据控制寄存器 (sdio_dctrl)清零,复位之前的传输设置。
2) 对 sd 卡进行数据读写之前,都必须发送 cmd16 指定块的大小,对于标准卡,要写入blocksize 长度字节的块;对于 sdhc 卡,写入固定为 512 字节的块。接下来就可以发送块写入命令 cmd24 通知 sd 卡要进行数据写入操作,并指定待写入数据的目标地址。
3) 利用 sdio_datainittypedef 结构体类型变量配置数据传输的超时、块数量、数据块大小、数据传输方向等参数并使用 sdio_dataconfig 函数完成数据传输环境配置。
4) 调用 sdio_itconfig 函数使能 sdio 数据结束传输结束中断,传输结束时,会跳转到sdio 的中断服务函数运行。
5)sd_dma_txconfig 函数,配置使能 sdio 数据向 sd 卡的数据传输的dma 请求,为使 sdio 发送 dma 请求,需要调用
sdio_dmacmd 函数使能。对于高容量的 sd 卡要求块大小必须为 512 字节,sdio 外设会自动生成 dma 发送请求,将指定数据使用 dma 传输写入到 sd 卡内。
普通模式需要自己去处理那些溢出什么的太麻烦了,用dma传输数据就好了
dma外设配置(不清楚的参考:dma外设详解):
写入操作等待函数
sd_waitwriteoperation 函数用于检测和等待数据写入完成,在调用数据写入函数之后一般都需要调用,sd_waitwriteoperation 函数适用于单块及多块写入函数。
sdio 中断服务函数
在进行数据传输操作时都会使能相关标志中断,用于跟踪传输进程和错误检测。
sd_processirqsrc 函数首先判断全局变量 stopcondition 变量是否为 1,该全局变量在sdio 的多块读写函数中被置 1(前面分析的单块读写函数中 stopcondition 均为 0),因为根据 sd 卡的要求,多块读写命令由 cmd12 结束,sd 卡在接收到该命令时才停止多块的传输,此处正是根据 stopcondition 的情况控制是否发送 cmd12 命令,它发送命令时直接采用往寄存器写入命令和参数的方式。
调用库函数 sd_dmaendoftransferstatus 一直检测 dma 的传输完成标志,当 dma 传输结束时,该函数会返回 set 值。另外,while 循环中的判断条件使用的transferend 和 transfererror 是全局变量,它们会在 sdio 的中断服务函数根据传输情况被设置,传输结束后,根据 transfererror 的值来确认是否正确传输,若不正确则直接返回错
误代码。sd_waitwriteoperation 函数最后是清除相关标志位并返回错误。
数据读取操作
同向 sd 卡写入数据类似,从 sd 卡读取数据可分为单块读取和多块读取。这里仅介绍单块读操作函数,多块读操作类似。
这一部分自己看代码吧,操作差不多,已经人麻了太多了。
还有多块读取与多块写入,其实是一样的,只不过传输结束需要发送cmd12来结束传输。
总结:代码太多了,但是核心的东西已经讲完了,自己去看代码悟一下,其实前面的理论部分懂了,代码部分是完全按照理论来走的,只不过多了一点点细节,就这样咯,那些边边角角留给你们。
3.实验结果【本文转载自csdn,作者:rivencode】

英特尔披露10纳米Ice Lake架构处理器细节
关于固体绝缘环网柜的种类剖析
餐饮行业音视频监控系统的结构组成及功能实现
拷贝10G视频不到30秒,aigo国民好物移动固态硬盘S7 Pro开箱
分立器件在3D打印机控制板中的应用
SD NAND 的 SDIO在STM32上的应用详解(下篇)
RFID移动读写器自动配置怎样来实现
倒计时!华东磁性元器件产业链峰会议程出炉
英特尔新的 Loihi 神经形态芯片模仿活体大脑
流程图设计器简介
离子液体在锂电领域的应用研究
窄带物联网和射频前端器件的机遇和挑战
交错并联全桥LLC电路的仿真验证
中小尺寸面板龙头,Notch屏需求量持续攀升
氢燃料电池原理
美国人文与科学学院发布最新入选院士名单,百度总裁张亚勤入选
科鑫光电发布圆形中空LED屏
基于DensePose的姿势转换系统,仅根据一张输入图像和目标姿势
区块链和智慧城市之间怎样相联系起来
基于Zigbee/Sub-GHz技术的智能电表设计