三、数据断电存储,工程必备
7.FLASH读写断电存储
这个例程主要是说明怎么设计FPGA时序逻辑来实现FLASH的页面数据擦除、读写等,例程本身有着非常深刻地学习价值,大家不妨回想一些嵌入式工作中的常见开发场景:1.做单片机STM32开发,KEIL编译过后下载直接通过ST-LINK,J-LINK等把bin文件烧写到单片机的内部FLASH里,断电重启后STM32即运行刚才烧写过的程序;2.同样地我们也可以把一些希望断电存储的大批量数据保存到STM32内部FLASH里,用到后面程序段未使用的扇区空间即可;3.STM32外挂一颗FLASH,把需要显示的图像二进制数据预先存入FLASH里,上电后再把数据读出,重新绘制到显示屏的指定坐标上即可;4.FPGA外挂一颗QSPI-FLASH,通过下载器把mcs文件烧写到FLASH里,开机上电后FPGA即读出FLASH的配置,再按照该配置去重新综合其内部的电路来达到预期设计等,所以毫不夸张地说掌握FLASH的读写在嵌入式开发中是一项必备技能。
这里和朋友们简单讨论一些题外话,对于很多工程项目因为拆机不便,都会有着远程更新程序的现实性需求,例如上位机对STM32的IAP串口程序升级;4G网络远程升级ARM LIUNX等,虽然具体的升级接口方式随着硬件变化而变化,但其实核心内容不变依旧是进入Bootloader,通过各类通信协议接收到bin文件的二级制数据,逐一写入FLASH的对应扇区内,最后从Bootloader跳转到Application。
大家有没有想过对FPGA而言,本身只是时序逻辑,那么它怎样才能实现远程升级呢?这是一个很有意思也很有价值的问题,现实项目中如果需要对FPGA远程设计,一般需要通过上位机或者ARM端的协助,上位机可以通过常见的LAN、USB2.0、RS232等不同的外设接口把bin文件打报数据发送给FPGA,FPGA接收后按照时序逻辑去把数据写入外挂QSPI-FLASH的对应页面(此时FPGA已上电从QSPI-FLASH中读出电路配置内容,按照程序给定的时序逻辑工作),也可以预先把FPGA的bin文件预先拷入U盘里,ARM或STM32通过移植FATS32,把U盘里的bin文件读出并打报数据扔给FPGA,同样的FPGA再去写入数据到外挂的QSPI-FLASH里,这个例程先暂且不去讨论具体FPGA远程升级的实现方案,而是提供一套擦除、读写FPGA外挂的镁光QSPI-FLASH的设计方法,如图1所示是豌豆开发板板载的一颗镁光N25Q128芯片,利用这颗QSPI-FLASH芯片,既可以支持mcs文件烧写即完成程序的断电保存,也可以实现其他大批量数据的存储记忆。
图1 豌豆开发板Artix7上N25Q128电路
谈过FLASH的实战背景意义后,在这个例程里,笔者想详细地向大家介绍FPGA驱动IC设计的大体方法,只有当理解并掌握一般性方法后,再去做其他相关IC驱动研发时才能举一反三、灵活自如。但可能正应了那句老话,一万个读者就有一万个哈姆雷特,这里只是笔者的多年工作积累供参考,并没有像数学考试一样一个标准的答案,仁者见仁智者见智,可能不是最优解,但确实是合理解。
这里我们还是先来仔细阅读N25Q128的芯片手册,在提取程序设计中的用到关键信息后,再来和朋友们探讨FPGA去驱动IC芯片的一般性方法!本例程的代码是从转产项目中摘录的也具有很大参考价值,需要特殊说明的是QSPI-FLASH的引脚应该接到Artix7芯片的固定IO上,否则无法用下载器下载mcs文件。
写到这里,笔者不由自主地想和大家分享一些亲身研发经历,也是这个例程代码的原型,当时项目开发需要实现Artix7断电存储一些数据,XILINX官方手册也推荐镁光的外扩QSPI-FLASH芯片,PCB投板并焊接回来,却发现QSPI-FLASH芯片不管怎么驱动都好像不工作,就不断地用读取ID号做测试始终没任何反应,因为物料是立创上面买的没有怀疑,所以花了大量精力去排查其他地方,索性买来一块低端外挂FLASH的Spartan6开发板,经过测试读取ID号成功了,但型号是W25Q128,所以仍旧无法确定具体原因,当时感到束手无策,仔细核对了两个FLASH的芯片手册差别不大,但相同的逻辑代码放在Artix7板上依旧不行,最后没有办法直接把Artix7板上的N25Q128芯片用烙铁拆了,也把Spartan6开发板的W25Q128拆了换上镁光的N25Q128芯片再试,结果再试居然好了!于是矛头直指Artix7芯片,当然现在看来确实走了很多弯路,因为过于相信XILINX下的ILA在线调试工具,实际没有用示波器或逻辑分析仪去实际测量SPI总线协议上的信号,虽然在ILA下观察SPI总线的时钟一直作为输出在线观察有波形产生,但该引脚事实上没有任何输出只是被ILA调试欺骗,最后查阅了XILINX的官方手册证明是连接该引脚的CCLK_0是特殊引脚,即使在.xdc文件里定义,也不能被直接驱动,需要参考XILINX手册用特殊的原语去驱动该引脚!
如图2所示是芯片手册上对N25Q128的引脚说明,大家可以对照图1的电路来看,除了VCC和GND两个引脚外,SO8W 封装的N25Q128芯片,还有D0-D3,SCK时钟信号和CS#片选信号六个引脚,朋友们可以注意到对于这颗QSPI-FLASH芯片有extended spi、dual spi、quad spi三种读写模式,即单线SPI、双线SPI、四线SPI通信,其中单线SPI的一根数据线只用来发送,另一根数据线只用来接收,即为全双工;双线 SPI的两根线都具有收发功能,但在同一时刻只能是发送或者是接收,即为半双工;而四线 SPI与双线 SPI相类似,只是数据线数量上有所区别,也为半双工。
其实笔者对单线SPI、双线SPI、四线SPI通信模式下驱动N25Q128芯片都有尝试过,平心而论单线SPI则更具有广泛地实用性,因为一方面在双线SPI、四线SPI通信中均为半双工,即引脚需要定义成inout类型这也增加了程序设计的复杂性,另一方面芯片手册对双线SPI、四线SPI通信做了详细说明,不难发现在这两种模式下的命令发送和数据接收都有一些很容易大意的地方。
当然在这里也有朋友会反问,如果用双线SPI、四线SPI通信不也提高了数据读写速度吗?答案是肯定会有所提高,但大家想一想在整个设计,首先我们对SPI总线协议使用25Mhz的时钟,对现实项目中读写FLASH数据的需求已经完全可以满足了;其次对FLASH的读写频率不会很频繁,不管是远程更新程序还是断电记忆数据;再次在项目设计上,往往FPGA是作为计算、接口、采集后端的,而ARM作为显示、工控、通信的前端,这时还需要通过各种总线、协议等把数据报文发送给FPGA,FPGA才会触发读写FLASH操作,而通常情况下25Mhz的SPI通信相对于Modbus 485总线、CAN总线等通信来说,速度方面已经足够用了,即读写FLASH速度通常情况下要快于报文接收的速度,所以在这里再去纠结双线SPI、四线SPI通信来提升FLASH读写速度,实际上现实意义并不大。
图2 N25Q128引脚的说明
如图3所示,是芯片手册对N25Q128存储空间的详细描述,大家可以看到N25Q128标明的是128Mb空间,即128*1024*1024=134217728位,也就是手册中所写的16777216字节,对FLASH以及下一个例程的SD卡里都有扇区的概念,手册说明了一颗N25Q128芯片含有256个扇区,每个扇区是64K字节,即256*64*1024=16777216字节;含有4096个子扇区,每个子扇区是4K字节,即4096*4*1024=16777216字节;含有65536页,每页是256字节,即65536*256=16777216字节,所以要搞清楚这些基本概念,对于FLASH擦除、读写最小的操作是以页为单位,当然也能以扇区和子扇区为单位,在这个例程设计中我们以页为操作的基本单位。
图3 N25Q128的存储空间示意图
如下图4所示是N25Q128芯片的操作指令表格,笔者把程序上常用的指令用红圈标注了出来。在表格中非常详细地说明了Command具体内容,Code对应指令,是否支持Extended、Dual I/O、Quad I/O即单线SPI、双线SPI、四线SPI通信和注意事项,这里因为程序设计中选择了单线SPI,所以我们更加关注Extended栏中的项目。大家注意到表格下面的Notes,只需要看2、4、8三条即可,其中有的指令需要发送1字节的指令信息后再发送3字节的地址信息,有的指令则直接发送1字节的指令信息即可,有的指令还需要在发送前再发送write enable指令,下面我们结合芯片手册上每种操作指令的时序逻辑图,来为大家展开更加细致的说明,这些信息也关系到程序设计。
图4 N25Q128芯片的操作指令表格
笔者在这里按照芯片手册的排版顺序,为大家介绍程序设计上需要用到指令的时序逻辑,如图5到图10所示,分别是SPI单线模式下N25Q128芯片的读取状态寄存器指令、读取ID指令、页读取指令、页写入指令、写使能和写禁止指令、页擦除指令时序图。
图5 SPI单线模式下N25Q128芯片的读取状态寄存器指令时序图
图6 SPI单线模式下N25Q128芯片的读取ID指令时序图
图7 SPI单线模式下N25Q128芯片的页读取指令时序图
图8 SPI单线模式下N25Q128芯片的页写入指令时序图
图9 SPI单线模式下N25Q128芯片的写使能和写禁止指令时序图
图10 SPI单线模式下N25Q128芯片的页擦除指令时序图
如图11所示是N25Q128芯片的状态寄存器的说明,这里特别应该去注意的是,在页写入和页擦除操作后,需要轮询地发送读状态寄存器指令以确定N25Q128是否执行完成、准备就绪,当读到一字节的状态寄存器最低位是0时,即代表芯片准备就绪可以执行后续的其他操作,否则当读到最低位是1则代表尚未准备就绪,这时再发送其他操作指令则会被视为无效操作,感兴趣的同学可以在看看芯片手册对其他寄存器的描述,N25Q128芯片确实有不止一个寄存器,例如有Status Register、Nonvolatile Configuration Register、Enhanced Volatile Configuration Register、Flag Status Register等,对这些寄存器的读写也会产生不同的效果,但在本例程中就用到最常见的一个即可。
图11 N25Q128芯片的状态寄存器(Status Register)的说明
如表1所示,是笔者总结的N25Q128芯片驱动指令列表,在这个列表中注明了指令的具体内容和编号,和在各个指令下FPGA与FLASH通信时是否需要发送指令和地址内容、是否需要在此后继续读写数据等,在这里也定义了一个空指令CMD_NULL便于程序设计。(“√”代表该指令下通信需要发送命令、地址、写数据或者接收数据等)搞清楚整个表格的内容,对于flash_driver模块的状态机设计很有帮助。
表1 N25Q128芯片驱动指令列表
详细阅读过N25Q128芯片手册并提取出关键信息后,就如前面所说笔者想通过这个例程和朋友们一起去探讨使用FPGA驱动一颗IC芯片的一般性程序设计方法。这个例程中,我们去实现如下功能:通过按下豌豆开发板上的按键,触发一次读写FLASH的过程,为了验证相关的操作指令,我们依次去进行如下操作:读取ID、写禁止、写使能、写擦除、读取状态寄存器、写使能、页写入、读取状态寄存器、页读取,为了方便测试,这里选择在FLASH的页地址(每页是256字节)0下的,依次写入0-255的256个字节数据,然后再去读取页地址0下的256个数据通过串口打印到上位机。
这时朋友们可以想一想应该怎么去实现整个代码设计,当然FPGA设计本身具有很大的灵活性,可能每个人都有不同的看法,笔者在这里给大家分享一种一般性的驱动IC的方法,也是从多年工作经验中总结归纳而来的,比如在这个例程中,通过对例程功能的描述,对于操作FLASH则需要一个状态机去实现指令之间的跳转;通过对芯片手册的分析,表3-3也详细总结了不同指令下的数据发送和接收数量上不完全相同;通过对协议总线的学习,SPI底层还需要驱动逻辑的支持才能正常工作,这样一想就会感觉整个设计起来会很复杂、很困难,那么我们应该如何去化繁为简呢?
通过模块划分的思想,我们可以把很多复杂烦琐的设计做得简单明了,一般性地我们可以把FPGA对IC驱动细划分成驱动层、控制层、逻辑层,比如在本例程中可以对应地划分成flash_driver、flash_control、qspi_flash_rw_top三个层,这样就可以把很多东西简化。驱动层实现对FLASH不同指令下的SPI总线上数据收发的时序逻辑;控制层则把驱动层例化到本模块,根据工程需求实现操作FLASH指令之间的跳转;逻辑层再把控制层例化过来,简单修改就可以满足整个代码逻辑上对FLASH的控制,这样就很大程度上减少编码的复杂度,相反如果所有的驱动层、控制层、逻辑层的时序逻辑都放到一个模块实现,那么肯定很复杂易出错,而且也不易排查问题。
其次这里还有个小技巧,细心的同学会发现几乎大部分IC芯片都有支持读取ID操作,用读取ID来作为验证驱动层时序逻辑设计是否正确,往往在实际项目中被证明是最为简单快捷的验证方法。
如上表1所示,对于flash_driver驱动模块,需要用到FLASH操作指令如下:读取ID(9F);写禁止(04);写使能(06);页编程(02);页读取(03);页擦除(20);读取状态寄存器(05),对于这些指令操作,可以把它归纳成几个步骤:发送CMD命令、发送ADDR地址、读取数据和写入数据,针对到具体指令操作有些步骤是跳过的,但不失一般性我们把flash_driver模块划分成6个状态的状态机,即IDLE、CMD_SEND、ADDR_SEND、RD_DATA、WR_DATA、DONE。
flash_driver驱动模块根据上游flash_control控制模块发来的具体指令做状态机的跳转即可,如表2所示是flash_driver模块信号列表,设计非常直观明了给出详细代码供参考,对照代码和说明很容易就可以动手还原出来,这里强调几个设计上容易忽略的地方:
1. 对于FLASH的SPI时钟的驱动设计,直接在本模块从50Mhz时钟分频到25Mhz,但这里需要通过XILINX原语驱动CCLK_0引脚,CCLK_0引脚是特殊引脚,即使在xdc文件中定义也不生效;
2. 这个模块中笔者使用的是单线SPI读写,感兴趣的同学可以尝试双线SPI和四线SPI读写,从而结合具体工程练习,提高了FPGA的设计能力;
3. 市面上各种教程对于SPI总线的设计,其实很多是有些瑕疵的地方,笔者在项目当中之前也参考别的代码SPI设计,存在一些实际问题,在这个例程中也详细分享给大家,确实很多细节真的需要自己静下心来思考才能体会到。
通过第4个例程“ 串行DAC输出模拟电压控制LED亮度的学习实践 ”,朋友们也都了解到SPI是有极性和相位一说的,即在上升沿和下降沿时接收或者发送数据,当然接收和发送数据通过两根独立的数据线,即mosi和miso实现全双工通信,对于SPI时序逻辑笔者见过主流的几种设计方式:1. 从上游模块产生一个SPI总线时钟和一个相对偏移180度时钟,再用这两个时钟的上升沿分别做数据的收发时钟参考,这样猛地一看貌似没有问题,但实际增加了代码的复杂度,同时因为两个时钟存在相位差,在和上游模块进行数据交互的时候,人为引入了跨时钟域的问题;2. 直接用always模块的posedge clk和negedge clk两个时钟去收发数据,因为FPGA里的D触发器默认是用时钟上升沿,虽然语法上支持用时钟下降沿的方法当从底层配置上来说不太友好;3. 通过时序逻辑产生一个时钟,再通过时序逻辑去向mosi总线上发送数据,因为时序逻辑是存在一拍的延时误差,这样做在分频大的情况下,比如50Mhz分频到6.25Mhz时,SPI的mosi和miso总线的建立和保持时间是足够的,但在50Mhz分频到25Mhz时,也是不可行的,所以笔者在flash_driver模块中直接去产生SPI总线时钟,并用组合逻辑从SPI总线上收发数据,这样从代码层面上就合理规避了可能出现的问题和人为增加的设计复杂度,如图12所示是本模块详细代码设计。
表2 flash_driver模块信号列表
图12 FLASH驱动模块的代码设计
在完成FLASH驱动模块的编写后,我们就需要再动手编写FLASH控制模块,前面也提起过在控制模块中例化驱动模块,再用一个状态机完成各个指令之间的切换,依次去进行如下操作:读取ID、写禁止、写使能、写擦除、读取状态寄存器、写使能、页写入、读取状态寄存器、页读取,即对应程序设计中的状态机:IDLE、RD_DEVICE_ID、WR_DISABLE、WR_ENABLE_FIRST、SECTOR_ERASE、RD_STATUS_REGISTER_FIRST、WR_ENABLE_SECOND、PAGE_PROGRAM、RD_STATUS_REGISTER_SECOND、RD_FLASH_DATA,如表3所示是flash_ control模块的信号列表,其中flash_control_en信号由按键产生。
表3 flash_ control模块信号列表
如图13所示是FLASH控制模块的代码设计,和上个例程读写EEPROM类似,这里也需要一个FIFO用来缓存从FLASH的0地址页内读出的256字节数据,本模块中例化了FLASH驱动模块,通过向下游模块发送flash_driver_en使能和flash_cmd命令和FLASH驱动模块数据交互,状态机的跳转按照前面芯片手册提取出的信息设计即可,没有太多复杂的东西。
图13 FLASH控制模块的代码设计
如图14所示是FLASH读写顶层文件的例化,这里只需要把几个模块相关信号例化到一起即可,如图15所示是串口助手读取FLASH页的数据实验截图,用户去按下豌豆开发板上的按键,就会触发FLASH控制模块的一系列指令操作,通过先擦除再写入后读取的方式,即首先擦除0地址页内的数据,接着从0-255依次写入0地址页内256字节数据,最后从0地址页内读取256字节的数据打印到串口助手上显示。
图14 FLASH读写顶层文件的例化
图15 串口助手读取FLASH页的数据
源工程代码下载链接(含datasheet):
链接:/s/1R_Bsakz545CfCZgco-vzwg
提取码:cdug