2020.2.18 留
肺炎当道,被堵在家回不去学校,莫得器材,莫得设备,干不了活,只有手边一块放假顺手捎回来的 FPGA 开发板。在家颓过了整个春节,手机电脑都快玩吐了也不能一直浪费时间,那就搞搞这块板子呗,为假期结束后要做的东西提前准备好改造素材也好。然后就瞄上了 Kevin 大佬的 SDRAM 教程。12 号晚开工,中间翘了两天,直到 18 号下午正式调通。东西虽然简单,但是整体的梳理还是要做一下的,故有此一文。(顺便提高以后可能存在的复习效率)
目录
一、项目整体介绍
要做的事情很简单,PC 端利用串口助手通过 UART 向 FPGA 板子发送数据包,FPGA 接收后提取出对 SDRAM 操作命令及操作数据,即将数据写入 SDRAM 和将数据从 SDRAM 读出。数据从 SDRAM 被读出后,通过串口回发到 PC端。项目的系统设计框图如下所示(下图中 Cmd_encode 有误,应为 Cmd_decode,不是编码而是解码):
二、系统各模块设计结构梳理
2.1 串口收发模块
2.1.1 UART_RX模块设计
串口接收时序图设计如上图。下面说明设计思路:
- rx 为 FPGA 的串口接收端,传递 PC 端过来的数据。空闲时为高电平,PC 端要发送数据过来时会将该线拉低表示起始位,接着依次发送 8bit 数据(最低位优先)。无校验位(完整的 RS232 协议中有一位的奇偶校验位)。
- rx_t ~ rx_ttt 都是对 rx 信号的延拍。打两拍作用是跨时钟域处理(PC 端与 FPGA 端使用的明显不是同一个时钟域),打三拍作用是捕获 rx 信号的下降沿(起始位)。这里附上其他人关于跨时钟域处理的学习总结: FPGA基础学习(3) – 跨时钟域处理方法
- rx_flag 是串口接收端识别出起始位后,拉高表示接收模块开始工作的标志信号。在接收 8bit 数据完毕后重新拉低。
- baud_cnt 为波特率计数器,FPGA 使用时钟为 50MHz,所以串口发送 1bit 的数据占用的时钟周期数计算方式为:
(1 / 9600)* 10^9 / 20 = 5208 ,这里 9600 为波特率,20 为 20ns。也可记为 50 * 10^6 / 9600,50 * 10^6 即 50MHz。
波特率计数器计满自动清零。 - bit_flag 为 baud_cnt 的中间值(此时的数据最稳定),用于作为数据抓取标志。
- bit_cnt 用于统计一次通信累计接收的数据位数,计数到 8 表示当前数据接收完毕后清零。
- rx_data 通过移位拼接实现串转并。
- po_flag 为串口数据接收完成,数据可用的标识。
Modelsim 软件仿真结果如下图:
2.1.2 UART_TX模块设计
串口发送时序图设计如上图。下面说明设计思路:
- tx_trig 为串口发送触发信号,对应串口接收模块中的 po_flag 。
- tx_data 是需要通过串口发送出去的数据。这里额外注意 tx_trig 为高时的数据。
- tx_data_reg 是新定义的一个寄存器,用来锁存 tx_data 的值(防止发送期间因 tx_data 变化而导致数据混乱)。
- tx_flag 为串口发送模块处于工作状态的标志信号,对应串口接收模块中的 rx_flag。
- baud_cnt、bit_flag、bit_cnt 则与串口接收模块中的用途相同(这里的 bit_flag 之所以放在 baud_cnt 计满溢出的位置而非中间位置只是为了写代码时方便一些)。
- rs232_tx 为串口发送信号,只在 baud_cnt 为 0 且 tx_flag 为高的时候才发送起始位。
Modelsim 软件仿真结果如下图:
2.2 命令解析模块
- uart_flag 为 UART_RX 模块给出的 po_flag 信号输入,uart_data 为 UART_RX 模块给出的 rx_data 。
- rec_num 为接收数据计数器,计满 4 个清零(该项目默认一帧数据一次写入4个)。注意,该计数器不对 8‘haa 读命令进行计入。
- cmd_reg 为命令寄存器,默认一帧数据中的第一个数据为 SDRAM 控制器的操作命令。—— 8‘h55 为写,8’haa 为读。
- wr_trig 为给 SDRAM 的写触发信号。等4个数据全部写入 wfifo 后,再让 SDRAM 控制器到 wfifo 这里读出来。
- rd_trig 为给 SDRAM 的读触发信号,当解析出 8’haa 读命令时给出触发脉冲。
- wfifo_wr_en 为 wfifo 的写使能信号。命令解析模块解析出 8’h55 写命令后,将接下来的 4 个数据判断为要写入 wfifo 的数据。
- wr_trig、rd_trig 、wfifo_wr_en 这三者实际上都可看作是不同条件下对 uart_flag 的筛分。
Modelsim 软件仿真结果如下图:
2.3 读写FIFO(wfifo、rfifo)
2.3.1 wfifo时序设计及要点
上半部分对接命令解析模块,对接 SDRAM 的主要是下半部分。这也是尤为需要注意的地方:在命令解析模块向 SDRAM 给出写触发脉冲( wr_trig )后,SDRAM 给出 wfifo 的读使能信号,从 wfifo 里读出要写入 SDRAM 的数据。所以,wfifo 的读使能信号在 wfifo 数据未被全部读出时必须保持高电平,全部读出后才能拉低。
2.3.2 rfifo时序设计及要点
图中的 rfifo_empty 信号为 rfifo 的空信号,实际应该取反,即为空时为高电平,内部有数据时该信号拉低。当命令解析模块解析出 8’haa 读命令时,给 SDRAM 控制器一个读触发脉冲。SDRAM 收到后,将 rfifo 的写使能信号拉高,并把读出来的数据写入 rfifo(上图中的红框部分)。由此,rfifo 内部不再为空, rfifo_empty 信号拉低,UART_TX 模块给出 rfifo 的读使能,将从 rfifo 读出的数据做并转串处理后发送。
以下的部分为 SDRAM 控制器设计,这就需要根据自身的所用芯片的不同,参考芯片的数据手册进行修改了。Kevin 大佬在视频教程中所用的是地址位为 12 位的 SDRAM,而我板子上的 SDRAM 是 13 位的地址,就不能直接用,得自己看着改。
2.4 SDRAM控制器 —— 初始化
时间已经从手册上查出并标注在了时序图的对应位置。
初始化操作完成后,对外输出一个高电平表示初始化操作已完成。
2.5 SDRAM控制器 —— 刷新
自动刷新的频率根据手册要求来决定。如博主使用的 SDRAM 芯片要求在 64ms 内刷新完 8192 行,这就要求 64 * 1000 / 8192 = 7.8us 要有一次刷新。
同时,在这里引入仲裁机制。顾名思义,在判断出 SDRAM 已完成初始化的情况下,为确保刷新、读、写的操作循环有序进行,引入仲裁机制。这里对刷新模块引出刷新请求信号,即在刷新模块内部进行 7.8us 的周期计时,计满拉高刷新请求信号,传递至仲裁状态机给出刷新使能后,再由刷新模块对 SDRAM 执行刷新操作。(这里先不考虑读、写模块)刷新操作执行完成后,拉高刷新完成信号,反馈给仲裁状态机,确保仲裁状态机退出刷新标记状态,以便进行下一次仲裁。
2.6 SDRAM控制器 —— 写
接收到写触发信号后,写模块对外拉高写请求信号,经仲裁状态机判断后给出写使能(默认处理优先级顺序为刷新、写、读),写模块执行写操作。写操作完成后,同样拉高写完成信号,反馈到仲裁状态机。由于写模块整个操作流程涉及到的命令较多,故使用状态机进行设计。
在这里还有两点细节需要注意:
- 可能存在写满了一行,但仍未写完的情况,此时需要Precharge关闭现有行,然后回跳到行激活状态,激活新行后继续执行写操作。
- 可能存在正在执行写操作时刷新模块发出刷新请求的情况,此时继续保持写入直到此次突发写完再关闭现有行,退出写状态,并重新发送写请求等待仲裁状态机响应。
Modelsim 软件仿真如下,几张图依次对应开始写入、写入中途刷新、写完一行换行、写完数据的情况:
这是代码里设定的状态及其对应值:
2.7 SDRAM控制器 —— 读
读与写设计思路大体相同(代码可以看作是类似写的镜像),最需要注意的部分就在时序图上:读模块的 read 命令、Precharge 命令的给出与数据读出的时序关系。