CS创世SD NAND在君正平台和RK平台的应用
各位工程师,你们好,我是alan,今天就瑞芯微平台和君正平台下的linux系统中关于SD NAND的使用做一些经验的分享,如有不正,请联系我们批评指正;
采用的开发板是RK3568和x2600e,ubuntu版本是20.04,交叉编译工具链是aarch64-linux-gnu-和mips-linux-gnu-;
下面将从五个板块来进行介绍,分别是操作SD NAND的常用命令、SD底层协议简要介绍、对SD NAND进行读写操作的三大方式、SD的驱动框架介绍以及SD NAND启动,前三个板块没有瑞芯微和君正平台之分,只要是跑linux系统,差别不大,第四块以RK平台为例,第五块同时以君正平台和RK平台为例。
一:操作SD NAND的常用命令
1.查看SD设备:lsblk或fdisk -l,设备节点一般为/dev/mmcblkX或/dev/sdX,eg:mmcblk0;
2.挂载分区:mount 设备分区 挂载点 eg:mount /dev/mmcblk0p1 /mnt/sdcard
3.卸载分区:umount 设备分区/挂载点 eg:umount /dev/mmcblk0p1 或 umount /mnt/sdcard
4.分区管理:fdisk 设备节点eg:fdisk /dev/mmcblk0
进入交互页面后常用命令:
p 打印分区表
n 创建新分区
d 删除分区
t 更改分区类型
w 将更改写入磁盘并退出
q 不保存更改退出
m 显示帮助菜单
l 列出已知的分区类型
v 验证分区表
g 创建新的空GPT分区表
o 创建新的空DOS分区表
在进行分区管理前请务必备份重要数据,因为在更改生效后会丢失原来数据;
5.格式化分区:
格式化为FAT32:mkfs.vfat 设备分区;eg: mkfs.vfat /dev/mmcblk0p1
格式化为ext4:mkfs.ext4 设备分区;eg:mkfs.ext4 /dev/mmcblk0p2
6.简单读写:cp,cat,echo等命令
eg:cp /usr/data/1.txt /mnt/sdcard/2.txt
cat /mnt/sdcard/test.c
echo "测试内容" > /mnt/sdcard/test.txt
二:SD底层协议简要介绍(由于SD NAND和SD卡遵守相同协议,并且标准协议中使用SD卡来描述,因此以下用词使用SD卡代替SD NAND)
此处是为下面介绍读写方式准备协议的理论知识,仅做简要介绍以及提醒一些需要注意的点,完整的协议内容较多,详细请参考SD2.0协议标准完整版(参考附件);
SD驱动中最重要的部分就是初始化,这里描述一下初始化流程,(需要区别四类卡,SDHC卡,SDSC卡,SD1.X卡,mmc卡)先是给SD卡上电,通常这一步在将卡插入卡槽就会自动完成,然后是发送CMD0进行软复位,进入空闲模式,再发送CMD8,主机询问SD卡是否支持电压范围,(SD1.X和mmc卡不会对CMD8产生响应,只有SDHC和SDSC卡会对CMD8回复R1响应);如果不响应再区分SD1.X和mmc卡,发送ACMD41(先发送CMD55告诉SD卡接下来发送的是应用命令),如果不响应就说明是mmc卡(此时发送CMD1激活mmc卡,mmc卡响应后即完成mmc卡的初始化),如果回复R1相应就说明是V1.X卡;
收到R1响应再区分SDHC和SDSC,然后发送ACMD41,主机告诉SD卡支持高容量,根据返回的R3响应中的OCR寄存器来判断是标准还是高容量,只有当busy位置1时CCS位才有效,CCS位为1是高容量V2.0卡(即SDHC卡),CCS位为0是标准V2.0卡(即SDSC卡),至此区分出了V1.X,标准V2.0,高容量V2.0卡,这三种卡接下来的步骤一致:
发送CMD2获取CID寄存器的值,SD卡会回复R2响应,再发送CMD3,SD卡获得相对地址,流程图如下图所示:
然后是数据传输模式,初始化完成后进行数据传输就相对简单了,只需要发送对应的命令即可,流程图如下:
下面附上常用命令以及响应的图:
最后提醒几个需要注意的点:
第一是上电之后有一个时间段叫“供电上升时间”,这个是电压上升到操作的总线电平以及等到能发送第一条命令的时间,这个时间需要在1ms,74个时钟周期以及供电上升时间这三者中取最大值,其实这点在驱动源码中也有体现,在mmc_power_up函数(drivers/mmc/core/core.c)如下图所示,同时在协议的6.4.1章节也有说明,这个时间如果没有等待而直接开始发送命令进行初始化,可能也能通过初始化,但是在后面的数据传输阶段就有概率会出现问题了,如果没有注意到这个供电上升时间,那么其实出现了异常是很难定位问题的;
第二是SD卡的两种数据包格式,分别是常规数据和宽位数据,常规数据是指普通的8bit字节数据,发送规则是先发低字节再发高字节,每个字节是先发高位后发低位;而宽位数据一般指SD卡存储寄存器,规则是先发高位,后发低位, 这点在解析SD卡寄存器时需要格外注意,否则就会发现解析的数据明显不符规范;
三:对SD NAND进行读写操作的三大方式
SD NAND作为一种存储设备,不外乎就是读和写,同时这也是最重要的,熟悉读写方式对于使用SD NAND开发非常有帮助,因此在这一块会详细介绍;
第一:使用dd命令:
1. 使用前先挂载分区的文件系统(常用于写入普通文件)
sudo dd if=输入文件 of=输出文件 bs=块大小 status=状态信息 其他选项参数
eg:sudo dd if=/mnt/sdcard/test.doc of=backup.doc bs=1M count=1
//将/mnt/sdcard/test.doc的前1M字节写入backup.doc;
2. 使用前不挂载分区的文件系统(常用于写入镜像文件,备份或擦除整个SD NAND等)
sudo dd if=输入设备 of=输出设备 bs=块大小 status=状态信息 其他选项参数
eg:sudo dd if=sd_back_up.img of=/dev/mmcblk0 bs=4M status=progress
//将sd_back_up.img以4MB的块大小写入/dev/mmcblk0,同时显示进度和速度
sudo dd if=/dev/mmcblk1p1 of=/root/zboot.img bs=4M status=progress
//将mmcblk1p1设备中的所有数据读取到/root/zboot.img文件下
如果要对读取的数据进行限制,只读取部分数据,那么使用参数skip或者count
sudo dd if=/dev/sdX of=/path/to/output.img bs=4M skip=10 status=progress
//skip:跳过前10个块再开始读取数据
sudo dd if=/dev/sdX of=/path/to/output.img bs=4M count=20 status=progress
//count:只读取20个块的数据
也可以结合skip和count,实现从特定位置读取指定数量的数据
;
!!!注意:若使用前不挂载分区,那么在写入之前请备份SD NAND内重要数据,因为不挂载是绕过文件系统对原始存储设备直接操作,会完全忽略文件系统结构,很可能会覆盖已有数据,甚至可能会损坏文件系统,严重的话只能重新格式化;
如果是先挂载了分区,那么dd命令会通过文件系统进行操作,相对安全;
第二:使用块设备的标准接口,通过文件io或标准io进行读写,无论是挂载文件系统还是不挂载文件系统,都能使用文件io或标准io对SD NAND进行操作,区别是前者通过文件系统较为安全且效率稍低而后者直接操作硬件存储设备效率更高同时对于数据的写入需要更加注意防止覆盖重要数据,下面分别提供两个示例;
第一个示例:使用标准io(fopen,fwrite,fread,fseek,fclose等)在已挂载文件系统的SD NAND上进行读写,总所周知,在linux里流传着一句话,那就是“一切皆文件”,在这种情况下操作挂载点的文件其实与操作一般文件基本无异;示例实现的功能是在SD卡挂载目录/mnt/sdcard/目录下创建test_data.txt文件,然后再往里面写入4次0~255,然后再将数据读取出来验证是否写入成功并打印测试结果;
编译命令是aarch64-linux-gnu-gcc test2.c -o test2,编译成功后使用sftp root@ip将test2发送至开发板运行测试,运行结果以及程序见下面两张图;
第二个示例:使用文件io(open,write,read,lseek,close)对未挂载文件系统的SD NAND直接操作;实现的功能是从SD NAND的第1000块开始写入512字节数据,数据内容为0~255,0~255,写入后再把数据读取出来比较是否成功并打印结果;
编译命令是:aarch64-linux-gnu-gcc test.c -o test,编译成功后使用sftp root@ip将test2发送至开发板运行测试,运行结果和程序也如下面两张图所示
第三:使用ioctl产生系统调用,陷入内核进行处理,前两种方式都容易实现,而且不需要关注底层发送的命令,而ioctl可以对SD NAND进行精细的命令控制来约束NAND的行为,并且在ioctl使用过程中还有一些需要注意的点,如果想要对SD NAND进行最为底层的命令操作,那么ioctl必定是是首选,因此着重介绍这种方式;
使用ioctl来对SD NAND发送命令,其中最为重要的就是填充struct mmc_ioc_cmd结构体,结构体的详细定义位于kernel/include/uapi/linux/mmc/ioctl.h,其中关于flags即命令标志位掩码的定义位于/include/linux/mmc/core.h文件中的struct mmc_command结构体中,下面对struct mmc_ioc_cmd结构体的各成员作详细介绍,如下图所示:
介绍完struct mmc_ioc_cmd结构体后细心的小伙伴会发现了这里面几乎所有的参数都好填充,唯独flags,命令标志位掩码的定义在core.h中,如下图所示:
那么在编写ioctl的程序时,填充flags需要用到core.h里面的内容,总所周知,linux里面的隔离是很严重的,在应用层调用内核层的定义,这是不允许的,编译会报错,之前刚开发的时候因为这个编译报错还折腾了不少时间,最终的解决办法就是把这些标志位的宏定义复制到自己写的应用程序中,这样就没有报错了,下面提供一份程序
程序较长,文件名是init.c(见附件),编译命令是:aarch64-linux-gnu-gcc init.c -o init,编译成功后使用sftp root@ip(若有adb功能也可使用adb push)将可执行程序init发送至开发板进行运行测试,文件中包含ioctl使用的详细解释,下面三张图是运行结果截图(前两张)以及使用逻辑分析仪抓取的命令(第三张图),波形文件名是:init.TLW,见附件,两者都与程序一致,运行过程中没有报错,这就是使用ioctl向SD NAND发送底层命令的详细过程;
关于ioctl的使用,有下面几点需要格外注意,这些都是小编亲身走过的坑,所谓前人栽树,后人乘凉, 也算方便大家“乘凉”了哈哈;
第一是如果进行的是写操作,例如发送CMD24,CMD25等命令,那么struct mmc_ioc_cmd结构体的write_flag必须要赋值为非零值,如果是0值,那么很容易导致发送命令失败,得到“errno=110 (Connection timed out)”的错误信息,这个错误信息相对常见,意为连接超时,分析函数调用链可知:在__mmc_blk_ioctl_cmd(drivers/mmc/core/block.c)函数中有
根据write_flag的值会进一步增加MMC_DATA_WRITE或MMC_DATA_READ的标志再传到驱动中,因此发送写命令时请务必给write_flag赋非零值;
第二是在SD卡初始化完成后,开始数据传输前,需要发送ACMD6定义数据总线的宽度,分析源码可知:在mmc_sd_init_card(drivers/mmc/core/sd.c)函数中有如下图所示部分
SD和主机通常都是同时支持4位数据总线宽度的,在不修改SD驱动的情况下,主机控制器是4位总线宽度,所以如果不发送ACMD6,那么卡会保持默认的1位总线宽度,此时主机和NAND的总线宽度不一致,就会出现“errno=84 (Invalid or incomplete multibyte or wide character)”的错误信息,含义是无效或不完整的多字节或宽字符,解决办法有两种,一种是修改驱动源码,将主机控制器的总线宽度固定为1位,此时可以不发送ACMD6,主机和 SD NAND都使用1位数据总线通信,第二种是发送ACMD6通知卡改变总线宽度为4位,两者都用4位总线传输数据,ACMD6的参数说明如下图,很明显要第二种方式更简单,因此如果遇到errno=84的错误码,检查一下主机和SD NAND的总线宽度是否一致;
第三是频率问题,如果使用ioctl发送cmd时,检查了命令和各项参数都没有错,也没犯上面两种错误,但是运行程序发现就是会报各种错,例如下图(运行的是示例程序init)
那么很大概率就是频率太高,SD NAND接受不了,此时需要降频,如下图
降低频率后再次运行,运行结果见下图
只要多加注意以上三点,相信使用ioctl就没有什么大问题;
四:SD NAND的驱动框架介绍(以瑞芯微平台的RK3568驱动源码为例)
第一:MMC/SD驱动在linux中的结构层次
通常在linux系统中,MMC/SD设备都是被抽象成块设备来处理,在kernel的顶层目录下的drivers/mmc目录下通常有三个文件夹分别是core、card和host,有些驱动会将core和card合并成一个core,例如RK3568就是只有core和host,这个驱动框架就是以RK平台的SD驱动来介绍的,下面解释三个文件夹的作用;
1.card层: 要把操作的数据以块设备的处理方式写到存储设备上或从存储设备上读取;因为SD NAND属于块设备,那么必然要提供块设备的驱动程序,这部分就是解决了一个问题,即如何将你的SD NAND实现为块设备的。
2.core 层:则是将数据以何种格式,何种方式在 MMC/SD主机控制器与MMC/SD卡的记 忆体(即块设备)之间进行传递,这种格式、方式被称之为规范或协议.
这部分完成了不同协议和规范的实现,抽离出不同SD主机控制器的共性,并为HOST 层的驱动提供了接口函数
3.host 层: 是这个文件夹属于 Linux 内核中 MMC/SD 子系统 的 硬件驱动层,直接负责与 MMC/SD 主机控制器(Host Controller) 的硬件交互。它的核心作用是向上提供统一的接口供 Core 层 调用(如发送命令、读写数据),向下为不同厂商的 Host 控制器提供驱动实现,将上层(Core 层)的协议请求转换为具体的寄存器操作、时钟控制、DMA 传输等硬件行为。
core层根据协议规范来构造各种命令,那么命令是怎么发送给SD NAND呢?通过主机控制器。
主机控制器通过设置SD需要的gpio资源,注册中断资源,使能控制器等等,然后再向上面的核心层增加一个host,这样核心层就能调用具体的硬件操作函数来和SD卡通信了;
card和core是封装好的共性以及规范,通常是不需要修改的,而host层是直接与硬件打交道,需要控制底层寄存器的,不同的host控制器硬件资源也不一样,因此驱动SD NAND,host层才是应该需要修改开发的,MMC/SD驱动在linux中的结构层次见下图:
第二:SD驱动中核心的数据结构举例(SD NAND和SD卡在驱动中使用的数据结构和调用的函数是一致的,并且有不少数据结构命名或函数功能注释翻译过来用SD卡描述更贴切,因此下面描述用词使用SD卡代替SD NAND,不再赘述)
1.struct mmc_host
功能:表示一个MMC/SD卡主机控制器,它是驱动程序和内核MMC子系统之间的主要接口;
重要成员:
const struct mmc_host_ops *ops:包含操作该主机的各种函数指针,包括发送命令,设置时钟和电源,请求操作等,用于和硬件交互
struct device class_dev:代表该主机控制器的设备对象,可用于设备模型的注册和管理
unsigned int f_min:主机控制器支持的最小时钟频率
unsigned int f_max:主机控制器支持的最大时钟频率,对于SD卡的操作频率非常重要
struct mmc_card *card:指向插入该主机的SD卡设备
struct mmc_ios ios:包含了当前IO的状态信息,例如时钟频率,电压范围,电源模式等
const struct mmc_bus_ops *bus_ops:指向struct mmc_bus_ops结构体,定义了和MMC卡通信的操作集,例如读写,卡检测等;
u32 ocr_avail:存储MMC主机可用的操作条件寄存器OCR的值,
u32 caps:表示MMC主机的能力和特性,通过不同的位来标记主机的各种模式;
2. struct mmc_card
功能:表示一个插入到MMC主机控制器的SD卡设备
重要成员:
struct mmc_host *host:指向SD卡所连接的主机控制器
unsigned int rca:相对卡地址,是SD卡的重要标识符,用于在总线上唯一标识该卡
unsigned int type:卡类型,例如MMC,SD,SDIO等,用于区分不同的设备类型
u32 ocr:操作条件寄存器,包含了SD卡的操作条件信息,例如支持的电压范围,电源模式等;
struct mmc_cid cid:包含卡的CID信息,例如制造商id,产品名称等;
3. struct mmc_ios
功能:包含SD主机控制器的IO状态信息
重要成员:
unsigned int clock:当前的时钟频率,驱动可以根据需要调整此时钟频率,来满足不同操作的需求;
unsigned char power_mode:电源模式,例如MMC_POWER_OFF,MMC_POWER_UP,用于控制SD卡的电源状态
unsigned char bus_width:总线宽度,可以选择1线,4线,8线,根据SD卡的能力和操作需求进行调整
4. struct mmc_host_ops
功能:包含了操作MMC主机控制器的一系列函数指针,是驱动程序与硬件交互的接口;
重要成员举例:
void (*request)(struct mmc_host *host, struct mmc_request *req):函数指针,用于将一个操作请求添加到主机控制器的请求队列中
void (*set_ios)(struct mmc_host *host, struct mmc_ios *ios):用于设置主机控制器的IO状态,例如调整时钟频率,电源模式和总线宽度等等;
int (*start_signal_voltage_switch)(struct mmc_host *host, struct mmc_ios *ios):在需要切换SD卡的操作电压时调用此函数;
int (*get_ro)(struct mmc_host *host):检查MMC/SD卡是否被写保护了;
int (*get_cd)(struct mmc_host *host):检查SD卡的插入和拔出;
5. struct mmc_request
功能:表示一个对SD卡的操作请求,通常包括命令,数据传输等信息;
重要成员:
struct mmc_command *cmd:指向要执行的命令;
struct mmc_data *data:指向要传输的数据对象,用于数据块的读写操作;
第三:host层函数调用关系解析
这里主要做了两件事,其一是mmc_alloc_host分配一个mmc_host,其二是mmc_add_host添加一个mmc_host;
第四:卡的检测
我们用的SD卡只是一张卡,要操作卡还得通过主机控制器才行,因此会有struct mmc_card,struct mmc_host之分,截至这里再回忆一下 dw_mci_init_slot做的事情,大概就是准备一个 mmc_host 结构,然后添加一个主控制器设备到内核,最后又调用了一下 mmc_rescan 来检测是不是有卡插入了,有人会问如果此时卡没有插入呢,那么不就是白白调用一次mmc_rescan,那下次卡插入又是怎么检测到的呢,很明显,这种不能确定触发时间的完全未知的动作,肯定是需要通过中断机制来处理,检测到卡插入,触发中断,扫描卡,开始初始化等流程,检测到卡拔出触发中断,清理各类资源,释放空间;
第五:扫描流程
struct mmc_card 结构里面包含了一个 struct device 结构, mmc_alloc_card 不但申请了内存,而且还填充了 struct device 中的几个成员,尤其 card->dev.bus = &mmc_bus_type; 这一句要重点对待,mmc_bus_type的定义如下图
申请一个 mmc_card 结构,并简单初始化后, mmc_init_card 的使命就完成了,然后再调用 mmc_add_card 将这个 card 设备添加到内核。 mmc_add_card 其实很简单,就是调用 device_add 将 card->dev 添加到内核当中去;
device_add 里面,设备对应的总线会拿着你这个设备和挂在这个总线上的所有驱动程序去匹配( match ),此时会调用 match 函数(如下图),如果匹配到了就会调用总线的 probe 函数或驱动的 probe 函数;
所以match永远不会失败,匹配成功就会执行mmc_bus_probe(定义见下图)
追踪到这里,probe由调用了 drv->probe() ,这就需要知道drv的定义了,struct mmc_driver*drv=to_mmc_driver(dev->driver);match 函数总是返回 1 ,说明挂在这条总线上的 driver 都有可能跑到这里来了,事实的确也是这样的,不过好在挂在这条总线上的 driver 只有一个,定义见下图:
因此跳转到mmc_bllk_probe函数执行,函数定义如下:
mmc_blk_probe 是 MMC 块设备驱动的核心探测函数,负责将 MMC/SD 卡注册为块设备,使其能够被系统访问,看到这里已经把core,host目录下的文件都牵扯进来了,慢慢再捋一下就能看出从host到core的联系了;
第五:数据的读写
在驱动中,向SD卡发送cmd都是通过请求队列来完成的,所有的cmd都会被封装成请求,然后由内核的I/O调度器统一排序,合并或拆分,再下发给硬件驱动,这样避免了驱动直接操作硬件,而是通过标准接口解耦,增强了安全性和可维护性,所以分析cmd的发送首先要定位到request请求队列,关键函数调用流程如下图:
顺便附上上图中提到的两个ops操作集合的赋值:
分析到这里,整个SD驱动框架做的事情说简单一些其实就干了两件事,一是卡的扫描检测,而是数据的读写,不过这个过程中涉及到的数据结构和函数调用确实相对复杂,捋清楚驱动框架还是需要静下心来花费时间的;
五:SD NAND启动
相同的板卡厂商一般SD NAND启动的流程固定,大致讲一下流程,首先是要制作SD启动卡,即将启动镜像烧录进SD NAND,一般板卡厂商都会有专门的制作工具,只需按照使用方法来制作启动卡即可,如果没有制作工具,那么一般使用dd命令将系统镜像写入卡内(写入时可能有地址参数要求,需要咨询板卡厂商的技术支持),然后是选择启动方式,一般是有几个boot引脚,启动时系统会根据boot引脚的电平组合来选择启动方式,有些开发板可能在选择SD NAND启动时还需要配置其他内核参数,只需咨询对应的技术即可,最后就是验证系统是否能正常从SD启动,数据读写是否正常;
首先是君正平台,开发板是x2600e,SD NAND是采用CS256G-AOW;
第一:制作SD启动卡
君正的烧录工具支持将启动镜像烧录到SD NAND,先编译准备好启动镜像,然后按照下图顺序进行制作启动卡;
第二:选择启动方式
查询芯片手册得知启动方式的选择依据如下图
再查看原理图如下:
可知目前的电平组合是001,即选择的是SFC@PD06_3.3V(默认从SPI NAND FLASH启动),现在需要改为从SD启动,那么就要将BOOT_SET0即PD14接低电平,使得电平组合为000,然后就是查看PCB,找到PD14的gpio,再修改硬件将其接地即可;
第三:验证SD启动
通过上图可知,系统成功从SD NAND启动,然后使用一些简单的读写命令验证是否正常即可,一般而言只要能正常启动系统读写就没问题,因为启动过程中本身就已经包含了对SD进行读写。
然后就是瑞芯微平台,开发板是RK3568,SD NAND采用CS256G-AOW,也采取相同步骤;
第一:制作SD启动卡
首先编译准备好镜像文件,并将其拷贝到windows端(可以将镜像放置共享目录下),然后打开瑞芯微的制作工具SDDiskTool,按照下图所示进行配置:
正常来说制作过程是2~3分钟,但是也遇到过十分钟左右的,这个可能因SD NAND而异,但是只要没报错,就耐心等待制作完成;
第二:选择启动方式
由于咨询技术支持得到的答复是RK对于启动引导不开源,原理也是靠硬件选boot脚,然后在芯片手册上也没找到有关boot引脚配置的部分,所以说明在RK上我们不需要修改硬件,额外配置boot引脚来选择启动方式,后面在rockchip-common.h文件中找到了关于启动方式的优先顺序,如下图:
因此只要启动时在卡槽检测到SD NAND,那么就会优先从SD启动;
第三:验证SD启动
从上面两张图中可以相互印证,系统成功从SD启动;
启动卡的分区情况如下图:
从mmcblk1p1至mmcblk1p6共6个分区,依次是uboot,misc,boot,recovery,backup和rootfs,前面5个分区一般存储特定数据,不建议用户将数据写入,而mmcblk1p6是根文件系统分区,存储所有系统文件、应用程序、用户数据等,进行用户操作时就是基于这个分区,因此进行读写测试或者存储用户数据建议在此分区进行。
最后分享一些使用的软件:
串口工具:Windows下:MobaXterm;Ubuntu下:Minicom
分析内核源码:sourceinsight4
逻辑分析仪:ATK-Logic 或Acute TravelLogic Analyzer
除了软件,硬件上的逻辑分析仪,示波器,万用表也可辅助调试验证。
附件:
亲爱的卡友们,欢迎光临雷龙官网,如果看完文章之后还是有疑惑或不懂的地方,请联系我们,自己去理解或猜答案是件很累的事,请把最麻烦的事情交给我们来处理,术业有专攻,闻道有先后,深圳市雷龙发展专注存储行业13年,专业提供小容量存储解决方案。