Linux内核源码-存储驱动之 QSPI Flash
传输方式 DIO/QIO/DOUT/QPI
- QPI模式(Quad Peripheral Interface),所有阶段都通过4线传输。与之相对的是SPI。
- SPI模式:
- 纯种SPI(MISO/MOSI两个数据线)
- DOUT 全称 Dual I/O,命令字和地址字均为单线,仅在数据阶段为双线。
- QOUT 全称 Quad I/O,命令字和地址字均为单线,仅在数据阶段为双线。
- DIO 全称 Dual I/O (DQ0,DQ1两个数据线),首个命令字为单线传输,后面地址和数据为双线。
- QIO 全称 Quad I/O (DQ0~DQ3四个数据线),首个命令字为单线传输,后面地址和数据为四线。
以上的阶段指的是 Command-Address-Data三阶段,Command只占用一个字节。
常见FLASH芯片详解
MT25QU512
芯片手册:https://www.mouser.com/datasheet/2/671/mict_s_a0004996076_1-2290890.pdf
国产芯片:IS25WP512M QPI https://www.mouser.cn/datasheet/2/198/25LP_WP512M-1221196.pdf
QPI(Quad Peripheral Interface) 采用DQ0传输指令,地址和数据则通过DQ0~3传输。
MT25QU512ABB8E12-0SIT
我用的那款连线属于8E系,使用Qual SPI模式(复用功能略有不同,参考手册Figure5即可):
- S# 接上拉电阻+SPI片选引脚
- C 时钟线
- DQ0
- DQ1
- DQ2/W# 写保护
- DQ3/HOLD#
- RESET# 复位,QIO-SPI模式下不可用。
FLASH内部架构框图
- 控制线连接到控制逻辑Control logic,用于整体的状态操作,类似于FPGA的全局开关Flag。全程状态寄存器用于指示闪存模块Memory就绪等状态。
- DQ连接到I/O移位寄存器(这个是SPI原理),然后根据DQ内容执行以下分支:
- 如果DQ是地址,那么通过地址寄存器和计数器用于寻址,映射到闪存模块Memory上的X,Y二维地址矩阵。
- 如果DQ是数据,那么则Buffer模块作为缓冲,用于从闪存模块Memory储存上读写数据后置于移位寄存器,这涉及串并转换。
- 如果DQ是OTP,则对应到"64 OTP bytes"模块,执行对应的指令。OTP(全称ONE-TIME PROGRAMMABLE)这种只能写入一次的空间常用于放置加密密钥或者唯一ID。
- 如果是状态指令,可对两个8位的状态寄存器进行读写。状态寄存器TOP BP3~BP0可以组合指示FLASH只读保护区
注: DQ0~DQ3 是双向复用SPI数据线DIO/QIO,用于传输FLASH的指令数据地址信息。以下是各个DQ线的传输方向:
其中Memory的储存映射分布为
E系列是Sector父扇区(64KB) - Subsector子扇区32KB - Subsector孙扇区4KB,然后才是对孙扇区每个字节的寻址
也就是:必须找到孙扇区编号后,才能寻址。
关于JEDEC标准
寄存器
Nonvolatile Configuration Register
这里关键可以配置
- 协议模式,如Quad/Dual
- 地址模式4-byte或3-byte address mode
- XIP速度模式
- 时钟周期(Number of dummy clock cycles),周期数必须和时钟频率一致,否则储存器读取到的数据不正确
I/O protocol
Quad IO 协议,是指令-地址-数据 4-4-4 模式
Dual IO协议,是指令-地址-数据 2-2-2模式
第二条提到:一般来说,模式前的数字代表这部分使用了多少DQ线,如2-4-4代表 传输command部分时使用2根DQ,传输Address部分时使用4根DQ,传输Data部分时使用4根DQ。
时序:
当然我们不需要那么多种模式,我们使用Quad SPI的时候,只需要关注4-4-4, 4-0-4, 4-0-0, 4-4-0这几种即可。
3/4字节地址模式
另外还需要特别关注的是 3-Bytes 和 4-Bytes地址模式的区别。
- 4Bytes地址模式会直接使用Address寄存器,地址寻址为A[31:0]
- 3Bytes地址模式,仅仅支持128MB储存,它有个Extened Address寄存器,低3位表示储存所在的段序号,高5位表示地址,组成地址寻址A[31:24]。
内部配置寄存器(Internal Configuration Register)
内部配置寄存器Internal Configuration 由易失和非易失组成(Nonvolatile configuration和Volatile configuration + enhanced volatile configuration)
非易失寄存器提供的配置项:
- 4/3字节地址模式
- 3字节地址模式所选的128MB段,
- Quad I/O 协议开关
- Dual I/O 协议开关
- DTR(Double transfer rate) 协议开关
- 电流驱动力
- XIP模式:Fast/Dual/Quad等等
- 时钟周期(Number of dummy clock cycles),周期数必须和时钟频率一致,否则储存器读取到的数据不正确
注:DTR是双边沿触发的意思,比与之相对的STR单边沿触发快一倍。
易失寄存器提供的功能配置项:
- 时钟周期(Number of dummy clock cycles),周期数必须和时钟频率一致,否则储存器读取到的数据不正确
- XIP开关
- Wrap 填充 用于N字节对齐(连续不需对齐/64B/32B/16B对齐),也和字节顺序有关,效果如下表
增强易失寄存器提供的功能配置项: - Quad I/O协议开关
- Dual I/O协议开关
- DTR 协议开关
- Reset/Hold 开关位
- 电流驱动能力
其中时钟周期配置根据这个表
其作用在地址和数据之间的间隔
安全寄存器
Security Register可配置同时锁住第n个扇区(0 ~ 15) 并设置密码
写入操作/PROGRAM
由于FLASH介质问题,而且Flash芯片没有像EMMC那么高端,写入操作和常见的磁盘并不一样。所以写入的命令也不叫Write,而是叫PROGRAM
- 读取STATUS寄存器查看BUSY位,只有芯片不BUSY的时候才能继续下面的操作
- 发送Write Enable指令
- 发送PROGRAM指令
- FLASH芯片会自动回到Write Disabled状态
常用指令
Device ID Data
这部分一般是JEDEC规范+厂家自己的规范,这个ID用于识别厂商型号等等,底层Bootload到UBoot再到Kernel,都会使用这个ID来匹配启用对应的从设备FLASH驱动。
CRC
多项式:
用于对比读/写的数据正确性。
STATUS Table
更多具体请查阅数据手册,在此仅抛砖引玉。
Linux驱动部分
SPI驱动
请见我的另一篇文章:Linux内核之SPI协议
配置驱动
我们这里选用 Qual SPI来操作Flash。
ZYNQMP 使用 QSPI 的官方指南文档:https://xilinx-wiki.atlassian.net/wiki/spaces/A/pages/18841754/Zynqmp+QSPI+Driver
设备树配置大概是这样:
这里会用到两个关键驱动 compatible = "xlnx,zynqmp-qspi-1.0";
和 compatible = "n25q512a", "jedec,spi-nor";
QSPI控制器驱动源码
这部分的驱动只处理字节流
drivers/spi/spi-zynqmp-gqspi.c
关键probe在下图1358行,它使用了spi_controller结构体(SoC片上的SPI控制器实例)
主要的操作是:
- 分配spi_controller控制器实例
- 配置platform驱动私有数据pdev
- 设备树匹配和处理
- plarform驱动的资源ioremap (寄存器映射)
- 时钟配置
- 电源管理pm_runtime_开头的API
- SPI模式配置(相位、极性、Dual/Quad模式、速率、片选等)
- QSPI硬件控制器初始化
- 中断处理
- DMA掩码配置
- fops指针赋值(zynqmp_qspi_mem_ops、zynqmp_qspi_setup_op)
zynqmp_qspi_mem_ops
仅仅是开启SoC片上的QSPI控制器而已。zynqmp_qspi_mem_ops
里面只有一个.exec_op成员,指向了zynqmp_qspi_exec_op
,主要是初始化QSPI传输所需的底层实现,主要是QSPI协议的实现,如Command-Addr-Data模式配置,opcode、将TX/RX BUF和SOC QSPI寄存器上的收发寄存器交互等等。
- devm_spi_register_controller
- 启动pm_runtime
Tips: 大量使用了devm_
开头的函数,这是Linux内核提供给开发者的设备相关的自动管理API,其生命周期(堆栈内存)会根据设备模型自动维护。详见:Devres - Managed Device Resource
NOR FLASH 驱动源码
内核NOR框架文档:SPI NOR framework
驱动源码位于 drivers/mtd/spi-nor/core.c
probe函数体
static int spi_nor_probe(struct spi_mem *spimem)
{
struct spi_nor_flash_parameter *params;
struct spi_device *spi = spimem->spi;
struct flash_platform_data *data = dev_get_platdata(&spi->dev);
struct spi_nor *nor;
/*
* Enable all caps by default. The core will mask them after
* checking what's really supported using spi_mem_supports_op().
*/
const struct spi_nor_hwcaps hwcaps = { .mask = SNOR_HWCAPS_ALL };
char *flash_name;
int ret;
nor = devm_kzalloc(&spi->dev, sizeof(*nor), GFP_KERNEL);
if (!nor)
return -ENOMEM;
nor->spimem = spimem;
nor->dev = &spi->dev;
spi_nor_set_flash_node(nor, spi->dev.of_node);
if (nor->spimem)
init_completion(&nor->spimem->request_completion);
spi_mem_set_drvdata(spimem, nor);
if (data && data->name)
nor->mtd.name = data->name;
if (!nor->mtd.name)
nor->mtd.name = spi_mem_get_name(spimem);
/*
* For some (historical?) reason many platforms provide two different
* names in flash_platform_data: "name" and "type". Quite often name is
* set to "m25p80" and then "type" provides a real chip name.
* If that's the case, respect "type" and ignore a "name".
*/
if (data && data->type)
flash_name = data->type;
else if (!strcmp(spi->modalias, "spi-nor"))
flash_name = NULL; /* auto-detect */
else
flash_name = spi->modalias;
ret = spi_nor_scan(nor, flash_name, &hwcaps);
if (ret)
return ret;
spi_nor_debugfs_register(nor);
params = spi_nor_get_params(nor, 0);
/*
* None of the existing parts have > 512B pages, but let's play safe
* and add this logic so that if anyone ever adds support for such
* a NOR we don't end up with buffer overflows.
*/
if (params->page_size > PAGE_SIZE) {
nor->bouncebuf_size = params->page_size;
devm_kfree(nor->dev, nor->bouncebuf);
nor->bouncebuf = devm_kmalloc(nor->dev,
nor->bouncebuf_size,
GFP_KERNEL);
if (!nor->bouncebuf)
return -ENOMEM;
}
ret = spi_nor_create_read_dirmap(nor);
if (ret)
return ret;
ret = spi_nor_create_write_dirmap(nor);
if (ret)
return ret;
return mtd_device_register(&nor->mtd, data ? data->parts : NULL,
data ? data->nr_parts : 0);
}
主要操作有:
- 配置支持的特性
- 分配内存并赋值
- 私有数据
- name处理
- 通过spi_nor_scan扫描总线
- debugfs注册
- 从FLASH硬件获取参数
- 创建读写所需的dirmap
- 注册mtd设备
。。。未完待续