KEIL 5.38的ARM-CM3/4 ARM汇编设计学习笔记11 - STM32的SDIO学习3 - Read Card Status and SD Status

一、前情回顾

上期做到了SD Memory Card的Card Identification。但是这只是将卡成功转入了DATA TRANSFER MODE,对卡进行更多的读写擦的操作都需要更多的卡的信息。所以这次需要实现读取卡的状态和SD状态,及Card Status和SD Status。

二、目标

  1. 读取Card Status。
  2. 读取CSD信息,获得卡的容量和最大支持的块容量。
  3. 读取SD Status,并整理都回来的数据。

三、技术方案和潜在问题

3.1 实现方案

本实验还是采用HX32F4的开发板做实验。
在这里插入图片描述

3.1.1 读取Card Status

读取Card Status主要是要解析响应是R1或R1b的值。很多指令其实都有返回R1,但是也可以通过发送CMD13读取当前的最新的Card Status。

3.1.2 读取CSD并计算卡的容量和块的字节数

CSD信息可以在Card Identification Mode中发送COM2这一步和在Data Transfer Mode中发送COM10得到的R2响应中获得。但是要注意返回的字节序。RESP1存放的是[127:96],RESP4存放的是[31:1],如下图引用所示。

这里是引用《RM0090 Rev 19》第1064页

读取到了数据CSD以后,用指令把这些位提取出来。再根据公式算出相应的最大支持的块容量和按最大块容量算出的块的数量。

这里是引用 《Physical Layer Simplified Specification Version 9.10 》第229(255)页

里面涉及到的C_SIZE、MULT、 BLOCKNR、C_SIZE_MULT、READ_BL_LEN和BLOCK_LEN都在下面的这个表里找到。

这里是引用《Physical Layer Simplified Specification Version 9.10 》第226(252)页

这里是引用《Physical Layer Simplified Specification Version 9.10 》第226(252)页

3.1.3 读取SD Status

根据手册《Physical Layer Simplified Specification Version 9.10》,SD Status是通过数据总线从卡发给主机,作为ACMD13的响应。共有512位。

这里是引用《Physical Layer Simplified Specification Version 9.10 》第125(151)页

但是其实只有[512:312]位是有意义的。其他的[311:0]是厂家预留用的。所以我们只需要读200位,就是25个字节就可以了。由于FIFO是字寄存器,所以我们做了个Floor [25 / 4 ] = 7个字的缓冲。换句话说,我们就是读7 * 32 = 224个位。后面就不读了。

这里是引用《Physical Layer Simplified Specification Version 9.10 》第125(151)页

但是这里有个问题,就是MCU的SDIO_Adapter的FIFO,都是32位的。但是SD内存卡的数据其实是按字节发送的。并且,它是先发送最高字节。于是就出现了这样一个问题。卡发送的是假如是下面的数据
0x80, 0x00, 0x00, 0x00
表示的是[511:511]位是1,其他的[510:480]都是0。但是我们直接读寄存器,会得到0x80这样的32位数据。所以要把接收到的数据按照字节调整顺序,再存到我们的缓冲区的最高字地址。依次降地址存入数据。

虽然前面已经把内存卡的块设置了,但是MCU的SDIO Adapter也要设置每次数据传输的块的字节数和每次要传输的字节。并且,要传输的字节数必须是设置的块的字节数的整数倍。这个SD Status Register一共是512位,其实就是64个字节,16个字。参考STM32F407的数据发送的时序,其实是命令相应到了的时候第一个数据就已经发出来了。当然,这个数据肯定是在管子里。由于有ARM流水线存在,且内核取指令的I-Bus的速度肯定是远快过SDIO的速度的,所以这个时序不会造成数据丢失的。

这里是引用RM0090 - STM32F407数据手册第1020页

为了方便整理数据,并且考虑数据量不大,笔者采用了轮询的方式读出数据,不涉及到中断和DMA了。那么流程就是:

  1. 配置并启动DPSM
  2. 发送命令ACMD13
  3. 循环读取SDIO_FIFO并存到内存中
  4. 读了7个字以后发送CMD12打断传输
  5. 关闭DPSM

3.2 Thumb-2 ARM汇编器小技巧

3.2.1位提取指令ubfx

在Thumb-2汇编中,对于把若干连续的位从一个数中提取出来有个比较方便的指令,就是ubfx。具体的参考手册。

3.2.2 汇编结构体

汇编中也有结构体,但是不能像C中方便的给各个域取名字。详细情况参考《ARM DUI 0068B》2.10和、7.3.2和7.3.3,以及这次的代码实现。

汇编结构体里面的各个域的大小如果可以遵守C语言的字地址、半字地址和字节地址大小安排,未来如果再在C下面也按照这个顺序定义一个结构体类型,那么在C下面是可以用结构体指针安全访问这个汇编结构体的。

3.2.3 位和字节序调整指令rbitrevrev16revsh

调整一个字中的字节序、位序这种问题,往往被作为C语言的考题,甚至是面试题。非常让人烦恼。但是在Thumb-2汇编中有rbitrevrev16revsh这四个指令,可以很方便的解决这些问题。

有关Thumb-2的指令的一些手册,我已经收录在下载中。欢迎访问下载。

3.3 潜在问题

3.3.1有关SDIO的PLL48CK和APB2时钟分频

多次测试发现了一个问题,就是如果将高频外设总线APB2开到最小分频(APB2设置为2分频,当前VCO输出是288MHz,main PLL输出被选为SYSCLK,主频是144MHz),如果把PLLCFGR中的Q值设置成6,即输出48MHz,会发现SDIO Adapter不工作。换成更低的理论分频数也不工作,换更高的只有96MHz可以稳定工作,144MHz也不行。 这个PLL48CK实际上是main PLL的第二路输出。但是这个时钟信号难以确认。因为main PLL的第二路输出是不能通过MCO输出到芯片引脚,就更不可能用示波器直接确认波形。由于笔者目前在调试SDIO口,所以也没有用其他的外设去测试这个主频是不是有问题。

经过多次测试,倒是找到了一个解决办法,就是把高频外设总线APB2的分频数调到4分频或者更高,就可以将PLL48CK设置到48MHz,且一切工作都正常了。因为这个总线貌似就是要求达到这个频率,多了少了都不行。笔者就没有再继续深究。就先使用这个解决方案,把这个事情作为设计经验来使用。

当然,原因还是很耐人寻味的。笔者猜测可能是跟板子的供电有关。毕竟这个板子目前只是通过DEBUG线有点供电。并没有尝试用个负载能力强一点的电源带带试试。后面可以找个条件测试验证一下。

3.3.2有关SDIO_CLKCN下面的时钟分频

按照手册上说的,最好是上来就给个400kHz以下的时钟。等到了Data Transfer Mode,再把速度提起来。

这里是引用《Physical Layer Simplified Specification Version 9.10 》第41(67)页

下图是个之前测试的数据。可以看到,进入Data Transfer Mode以后,可以用CMD9可以再一次把CSD读出来。把传输速度相关的位提取出来。

在这里插入图片描述
速度位就是RESP1的低8位字节,也就是[103:96]位,如下所示

这里是引用《Physical Layer Simplified Specification Version 9.10 》第226(252)页

速度的含义如下所示。可以看到,这些卡的速度值都是0x32,也就是25MBit/s。

这里是引用《Physical Layer Simplified Specification Version 9.10 》第227(253)页

这样,根据这个数据,虽然SDIO_Adapter的PLL48CK的频率是48MHz,但是卡的最大读写速度是25MBit/s,所以必须启动分频器,将PLL48CK进行2分频。但是如果吧PLL48CK的主频强制分频成24MHz,也是不行。因为根据测试,24MHz的时候,这个SDIO_Adapter根本就不工作。看来,不仅仅是USB,这个SDIO也是必须在48MHz上工作。

3.3.3 SDIO data timer register (SDIO_DTIMER)的设置

经测试,这个寄存器的值设定为0xffff是可以正常工作的。当然,也可以设置成更大的值。但是设置成更小的,比如0xfff就不行。中间其他的值没有研究过。

四、实现

4.1 SDIO_Memory_Card.h文件调整

该文件调整为如下所示。

#ifndef _SDIO_Memory_CARD_H_
#define _SDIO_Memory_CARD_H_

#include "stdint.h"
typedef enum {
	waitRsp_noRsp    = 0,
	waitRsp_shortRsp = 1,
	waitRsp_longRsp  = 3,
}WaitRspKind;

typedef struct {
	void  (*init)(void);
	void  (*card_identification)(void);
	void* (*read_SD_status)(void);
	uint32_t (*erase_block)(uint32_t);
}SDIO_Memory_Card_Def;

extern const SDIO_Memory_Card_Def SDIO_Memory_Card;
#endif

void* (*read_SD_status)(void)函数指针用于执行读取SD Status Register。由于目前没有想好到底读出来的哪些数据是有用的,所以就干脆把这个数据块的地址用void * 指向一下就返回了。这个不影响调试和试验验证。未来可能会有修改。

4.2 SDIO_Memory_Card.s文件调整

在前面的基础上,增加了数据段的设计和新的寄存器命名。

get peripherals.s
	
rRCC   		rn r8
rSDIO  		rn r9
rGPIOC 		rn r10
rGPIOD 		rn r11
rCard_Info	rn r8
SDIO_GPIO_SPEED	equ	GPIOx_OSPEEDR_VERYHIGH
	 
	 area user_data, data
	 align 4 
     map 0, rCard_Info
StartofDataCardInfo field 0
card_rca	 	field 4		 
card_block_num  field 4	
card_block_size	field 2
EndofDataCardInfo field 0
Size_Card_Info  equ	EndofDataCardInfo - StartofDataCardInfo
card_info_data	space 	Size_Card_Info
	
	 align 4	
SD_status_DataSize equ 7
SD_status	space SD_status_DataSize

rCard_Info用于标定card status内容的结构体。
利用map ... field ...定义了一个汇编结构体,这个结构体的用法参考《ARM DUI 0068B》。
目前考虑可能是笔者对KEIL的ARM Linker还不太熟悉,导致如果自己随意定义段名,最后反而会发生互相覆盖。但是都叫做一个名字就好了。所以笔者这里就把所有的数据段上定义的段都叫做user_data,用area user_data, datal来声明。

4.3 卡识别函数 void *card_identification(void)

这个函数的行为稍微调整了一下。现在这个函数的行为流程如下所示。

  1. CMD0。这个我感觉其实是没有什么用的,但是既然手册里都写了要有,笔者就姑且加上去了。测试看不出这句有什么用。
  2. CMD8, ACMD41广播,ACMD41设置电平,CMD2广播获得CID,CMD3获得RCA。保存RCA。
  3. 总线提速。设置SDIO_CLKCR,将SDIO_CLK的分频设置成0,这样就是2分频。其实也可以干脆把分频器旁路掉,就是置位SDIO_CLKCR_BYPASS位。
  4. CMD9加参数RCA获得CSD
  5. 根据前面提到的手册中的内容解析CSD的内容,并存到相应的域中。
  6. CMD7加参数RCA选中卡。
  7. ACMD6设置卡的工作数据位宽是4位(参数为2_10)
  8. CMD16设置卡的工作块大小为512。这个要先参考一下卡最大支持的块大小是多少。主要是STM32F407最大就是512。所以笔者就也设置成512了。
  9. CMD13再确认一下状态,并把状态值装入r0。哪天想用来当返回值可以直接用。
     align  4
card_identification proc
	 push   {r4 - r11, lr}
	 ldr    rSDIO, =SDIO_BaseAddr
	 ldr    rCard_Info, =card_info_data
	 mov    r0, #SDIO_CMD_CPSMEN
	 mov    r1, #0
	 str    r0, [rSDIO, #SDIO_CMD]
wait_for_cmd_send
	 ldr 	r0, [rSDIO, #SDIO_STA]
	 tst	r0, #SDIO_STA_CMDSENT
	 beq	wait_for_cmd_send
	 mov	r0, #0x7ff
	 str	r0, [rSDIO, #SDIO_ICR]
	 str	r1, [rSDIO, #SDIO_CMD]
	 mov    r0, #8:or:SDIO_CMD_WAITRESP_Short:or:SDIO_CMD_CPSMEN
	 mov    r1, #1:shl:8
	 bl     send_cmd
	 mov    r0, #55:or:SDIO_CMD_WAITRESP_Short:or:SDIO_CMD_CPSMEN
	 mov    r1, #0
	 bl     send_cmd
	 mov    r0, #41:or:SDIO_CMD_WAITRESP_Short:or:SDIO_CMD_CPSMEN
	 mov    r1, #0
	 bl     send_cmd
	 ldr    r4, [rSDIO, #SDIO_RESP1]
set_voltage
	 mov    r0, #55:or:SDIO_CMD_WAITRESP_Short:or:SDIO_CMD_CPSMEN
	 mov    r1, #0
	 bl     send_cmd
	 mov    r0, #41:or:SDIO_CMD_WAITRESP_Short:or:SDIO_CMD_CPSMEN
	 mov    r1, r4
	 bl     send_cmd
	 ldr    r0, [rSDIO, #SDIO_RESP1]
	 tst    r0, #1:shl:31
	 beq    set_voltage
	 
Ask_the_Card_to_Publish_CID
	 mov    r0, #2:or:SDIO_CMD_WAITRESP_Long:or:SDIO_CMD_CPSMEN
	 mov    r1, #0
	 bl     send_cmd
	 
Ask_the_Card_to_Publish_card_rca	
	 mov    r0, #3:or:SDIO_CMD_WAITRESP_Short:or:SDIO_CMD_CPSMEN
	 mov    r1, #0
	 bl     send_cmd
	 ldr    r0, [rSDIO, #SDIO_RESP1]	 
	 bfc    r0, #0, #16
	 str    r0, card_rca
	 
; Now the SD Card is in Data Transfer Mode.
; Change the frequency from 400kHz to 48MHz.
; Bypass the frequency divider.
	 ldr    r0, [rSDIO, #SDIO_CLKCR]
	 orr	r0, #SDIO_CLKCR_BYPASS
	 str    r0, [rSDIO, #SDIO_CLKCR]
	 
; Get the CSD via CMD9
	 mov    r0, #9:or:SDIO_CMD_WAITRESP_Long:or:SDIO_CMD_CPSMEN
	 ldr    r1, card_rca
	 bl     send_cmd
	 
; Calculate the block size and save it to card_block_size.
	 ldr    r4, [rSDIO, #SDIO_RESP2]	
	 ldr    r5, [rSDIO, #SDIO_RESP3]	
	 ubfx   r2, r4, #16,#4
	 strh   r2, card_block_size
	 ubfx   r0, r4, #0, #10
	 ubfx   r1, r5, #30, #2
	 orr    r2, r1, r0, lsl #2 ; C_SIZE
	 add    r2, #1             ; C_SIZE + 1
	 ubfx   r3, r5, #15, #3	   ; C_SIZE_MULT
	 mov    r0, #1
	 add    r3, #2
	 lsl    r3, r0, r3;  MULT
	 mul    r0, r2, r3
	 str    r0, card_block_num	 
	 
; Select the card in RCA. Coz this is the only card, just select it.
	 mov    r0, #7:or:SDIO_CMD_WAITRESP_Short:or:SDIO_CMD_CPSMEN
	 ldr    r1, card_rca
	 bl     send_cmd	 
; Set the bus width to 4-bit.
	 mov    r0, #55:or:SDIO_CMD_WAITRESP_Short:or:SDIO_CMD_CPSMEN
	 ldr    r1, card_rca
	 bl     send_cmd
	 mov    r0, #6:or:SDIO_CMD_WAITRESP_Short:or:SDIO_CMD_CPSMEN
	 mov    r1, #2_10	 
	 bl     send_cmd
; Set the block size of the card to 512 bytes.	
	 mov    r1, #512
	 mov    r0, #16:or:SDIO_CMD_WAITRESP_Short:or:SDIO_CMD_CPSMEN
	 bl     send_cmd	 
; Read the card status. 
; Maybe someday I will use the response as a return value.
	 mov    r0, #13:or:SDIO_CMD_WAITRESP_Short:or:SDIO_CMD_CPSMEN
	 ldr    r1, card_rca
	 bl     send_cmd	
	 ldr    r0, [rSDIO, #SDIO_RESP1]
	 pop    {r4 - r11, lr}
	 bx     lr
	 endp

有关的注释都在代码中了,就不再解释了。
通过上面的代码,笔者读取了CSD,解析了数据,并把数据存放到了SRAM上。这里会发现有一些读取和存储指令,后面的地址常量是不带#或=的。这些是汇编结构体的用法。详细情况参考详细情况参考《ARM DUI 0068B》2.10和、7.3.2和7.3.3。

4.4 void* (*read_SD_status)(void)函数的实现

读取SD状态这个函数的设计流程就是

  1. 把SDIO_Adapter的块大小设置为6(即64),再把DLEN也设置成64。笔者没有尝试过把块大小设置为32或更小的2的指数,有兴趣的读者可以去试试。
  2. 准备好接收缓冲区,通过把缓冲区的地址加载到相应的寄存器
  3. 设置DTIMER为0xffff。这个延时是测出来的
  4. 启动DPSM
  5. 发ACMD13
  6. 轮询收数据,用rev转化数据,把数据存入缓冲
  7. 关闭DPSM
  8. 加载缓冲区首地址,装入r0作为返回值
  9. 收工
; read_SD_status function
	 align  4
read_SD_status proc
	 push   {r4 - r11, lr}
	 ldr    rSDIO, =SDIO_BaseAddr
	 ldr    rCard_Info, =card_info_data  	
	 ldr    r4, =SD_status
	 mov    r5, #SD_status_DataSize * 4 - 4
	 
; Read the SD Status Register via ACOM13
; Set the block size of SDIO_Adapter to 64 bytes.
; So as to read the SD Status Register.
	 mov    r0, #64
	 str    r0, [rSDIO, #SDIO_DLEN]
	 mov    r0, #(6:shl:4):or:SDIO_DCTRL_DTDIR_Card2Controller
	 str    r0, [rSDIO, #SDIO_DCTRL]
	 mov    r0, #0xffff
	 str    r0, [rSDIO, #SDIO_DTIMER]
	 ldr    r0, [rSDIO, #SDIO_DCTRL]
	 orr    r0, #SDIO_DCTRL_DTDIR_Dten
	 str    r0, [rSDIO, #SDIO_DCTRL]	
	 
	 mov    r0, #55:or:SDIO_CMD_WAITRESP_Short:or:SDIO_CMD_CPSMEN
	 ldr    r1, card_rca
	 bl     send_cmd
	 mov    r0, #13:or:SDIO_CMD_WAITRESP_Short:or:SDIO_CMD_CPSMEN
	 mov    r1, #0	 
	 bl     send_cmd	 
wait_for_SD_Status_Data
	 ldr    r0, [rSDIO, #SDIO_STA]
	 tst    r0, #SDIO_STA_RXDAVL
	 beq    wait_for_SD_Status_Data
	 ldr    r1, [rSDIO, #SDIO_FIFO]
	 rev    r1, r1
	 str    r1, [r4, r5]
	 cbz    r5, Read_SD_Status_Data_Completed
	 sub    r5, #4
	 b      wait_for_SD_Status_Data
Read_SD_Status_Data_Completed
	 blt    wait_for_SD_Status_Data	 
	 mov    r0, #12:or:SDIO_CMD_WAITRESP_Short:or:SDIO_CMD_CPSMEN
	 bl     send_cmd
	 ldr    r1, [rSDIO, #SDIO_DCTRL]
	 bic    r1, #SDIO_DCTRL_DTDIR_Dten
	 str    r1, [rSDIO, #SDIO_DCTRL]
	 mov    r0, r4
	 
	 pop    {r4 - r11, lr}
	 bx     lr
	 endp		 
     

这个发送函数也顺便再谈一下。就是在里面加了个发完指令收到响应后关闭CPSM的操作。

align  4
erase_block proc
	 push   {r4 - r11, lr}
	 nop
	 pop    {r4 - r11, lr}
	 bx     lr
     endp
; 	 Priviate Function Name: Send_cmd
;	 Args:  r0 - cmd, r1 - arg
;	 实现这个函数

	 align  4		 
send_cmd	proc
	 push   {r4 - r11, lr}
	 ldr    rSDIO, =SDIO_BaseAddr
	 ldr    rCard_Info, =card_info_data
	 mov    r2, #0x7f
	 str    r2, [rSDIO, #SDIO_ICR]
	 str    r1, [rSDIO, #SDIO_ARG]
	 str    r0, [rSDIO, #SDIO_CMD]
wait_for_response
	 ldr    r1, [rSDIO, #SDIO_STA]
	 tst    r1, #SDIO_STA_CMDREND:or:SDIO_STA_DCRCFAIL:or:SDIO_STA_CTIMEOUT
	 beq    wait_for_response
	 bic	r0, #SDIO_CMD_CPSMEN
	 str    r0, [rSDIO, #SDIO_CMD]	 
	 pop    {r4 - r11, lr}	
	 bx     lr
	 endp
	 

五、测试结果

5.1 编译,链接,上调试,插卡,运行!

read_SD_status过程返回之前下断,可以得到下面的结果。

在这里插入图片描述
笔者读取SD_status缓冲区的最后一个字到r0,可以看到这个值是0x80000000。从内存里也可以看到这个数值的位置。他的端序也是大端序了。这个值对一下表,发现它的意思是最高位是1,即数据线位宽为4位。这里可以尝试调整一下。如果设置为数据位宽为1位,会得到这个地方是0。所以说明配置和运行都是成功的。

这里是引用

六、结论

上面的实验设计发现了若干设计难点和要克服的MCU陷阱。尤其是时钟配置在这种应用中其实是尤为重要的。这些实验设计最终实现了STM32F407通过汇编语句访问内存卡控制器、解析从命令线和数据线返回的值并确认了这些值的正确性。笔者有以下的体会:

  1. 不能简单的按照手册把所有的工作频率直接拉满了事。要根据外设的运行情况进行调整和调试才能确定。
  2. MCU不一定是完美的,甚至是有缺陷的。如果不能规避这个MCU,可以尝试用技术手段规避这个缺陷。

下次要实现SD卡的擦、写和读操作。这样关于SD内存卡的测试就可以齐活了。

  • 30
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值