目录
CPLD需要实现一个SPI-Slave接口,把从MCU传来的数据对SRAM写操作。或者把SRAM读取的数据传输到MCU。如何把这个功能从设计到具体代码实现,做了简单的计划。
1)学习Verilog或者SystemVerilog的基本语法。
找了本电子书《数字逻辑基础与Verilog设计》开始学习。了解一下数字电子的基本概念。电子书看得累,需要买一本方便翻阅。
优快云上找了篇SystemVerilog的教程。因为FPGA游戏卡实现都是基本的逻辑功能,不涉及数字算法,信号处理,了解常用的Verilog语法就可以满足实现要求。
另外还需要找一本数字设计方法和仿真验证方面的书。了解一些系统设计的思路和技巧。再买本《Verilog HDL高级数字设计》看看。
2)VSCode Verilog/SystemVerilog代码开发和阅读环境配置
VSCode安装了几个插件:
- Verilog-HDL/SystemVerilog/Bluespec SystemVerilog
- SystemVerilog - Language Support
- Vim插件
这个个插件是方便阅读Verilog和SystemVerilog代码,语法高亮,模块或者结构体查找跳转方便。
另外安装了iverilog。这样可以在VSCode下编译verilog代码,语法检查。
GVim配合ctags,verilog_systemverilog.vim也可以方便编辑阅读代码。
3)找一下verilog的代码阅读分析一下。
在githut上下了几个spi实现的代码。因AG256S100 CPLD芯片与EPM240芯片管脚兼容,所以从intel网站下了相关的资料,其中有一篇讲述SPI-to-I2S实现,并提供源代码。另外就是N8 Pro mapper的实现代码。
githut上的东西也是良莠不齐,所以还是先看一些质量高一点的代码。
SPI-to-I2S这个例子简单实现了SPI-Slave的数据的收发处理,数据的格式定义,命令字的处理。简单buffer的缓存控制。既有文档也有代码实现还有testbench。比较适合初学者学习。
N8 Pro mapper的项目规模也比较小,除去各个mapper的实现,大概不到2000行的代码,用SystemVerilog实现。
N8 Pro游戏卡的几个功能也很有特点,比如in-system menu(游戏中弹出选项菜单),随时存档保存和游戏恢复,cheat功能。我想做的游戏卡与N8相似。N8 Pro的一些思路可以作为学习参考。
通过代码阅读,分析,了解一下verilog是如何设计一个系统。
首先是从N8 Pro的SPI接口设计,实现开始分析其功能。
SPI的总线接口网上资料比较多,应用开发的话随便找一篇看看就基本有个大致的了解。
1. module pi_io (spi接口模块)
pi.sv是该模块的代码实现。
pi_io(peripheral interface io), 该模块实现了spi数据的收发处理。
1.1 模块输入
input clk, | 系统时钟50MHz |
input spi_clk, | spi时钟,MCU输入 |
input spi_ss, | spi ss,MCU输入 |
input spi_mosi, | spi master out/ slave in,MCU输入 |
input [7:0]dati, | 数据输入,准备通过spi_miso输出到MCU。 数据输入的来源: 1)如果是mem_req(包括prg_ram, chr_ram,sram),则连接dma的数据输出pi_di。 2)如果是fifo,则连接base_io 模块的数据输出,pi_di_bio。 3)如果是cfg(系统配置区),则连接配置的数据输出,pi_di_cfg。 4)如果是sst(save state stuff),则连接sst_do(sst的数据输出) wire [7:0]pi_di = dma.mem_req ? dma.pi_di : pi.map.ce_fifo ? pi_di_bio : pi.map.ce_cfg ? pi_di_cfg : pi.map.ce_sst ? sst_do : 8'hff;
|
1.2 模块输出
output spi_miso | spi slave输出线 |
output PiBus pi | PiBus总线数据结构,定义了从MCU spi接口接收过来的数据处理之后的输出接口。内容包括:8bit数据,32bit地址,读/写模式,内存读写准备就绪act标记,PiMap(该数据结构定义访问哪块内存区域的位标记)。 |
1.3 SPI输入的数据格式定义
SPI接口只是定义了一套数据传输的方式,并没有定义通信协议或者应答机制,slave也无法知道到底要收发多少数据。
所以用户需要根据实际应用预先定义一套应答机制和传输数据的格式,方便对数据的处理。
spi-slave接收数据的格式定义如下:
命令字 | 读写地址 | 数据 |
8bit | 32bit | 8bit的倍数 |
命令字有两种,读命令和写命令
读写地址中的bit[24:23]定义是读写哪片内存(PRG-RAM/CHR-RAM/SRAM/System Registers)
bit[21-16]定义了读写System Registers中的哪一块(64KB的system寄存器/或者64KB的FIFO区)。
作者把各个内存块做了一个memory mapper,确定了每块RAM区的起始地址,大小。在具体实现了,32bit地址就是指向这些内存区。
注:在用Quartus综合代码的时候发现,虽然FIFO区和system registers区定义了64KB,但Cyclone IV EP4CE6F17C8的Block RAM(M9K)并没有这么多RAM资源,其实综合的时候只分配了2KB。
spi-slave判断数据接收结束是根据spi-ss,如果为高电平,接收结束。准备接收下一个命令字。
1.4 PiBus数据结构
由pi_io模块处理的数据填写到该数据结构,交由相连接的下一个模块进行处理。
内容包括8bit的数据,地址,读写标记,内存可以开始读写操作标记act。PiMap是定义了读写那块内存。
typedef struct{
bit [7:0]dato;
bit [31:0]addr;
bit we;//write mode
bit oe;//read mode
bit act;//memory read or write during act=1 pulse
PiMap map;
}PiBus;
1.5 PiMap数据结构
PiMap是定义各个标记位,确定读写那块内存。
typedef struct{
bit ce_prg;
bit ce_chr;
bit ce_srm;
bit ce_sys;
//all below located in ce_sys area
bit ce_cfg;
bit ce_ggc;
bit ce_sst;
bit ce_fifo;
}PiMap;
1.6 module pi_io_map
连接pi_io模块的pi数据输入,输出map信息。
把输入pi数据的读写,地址信息进行解析,填写map信息(确定内存片选)之后输出。
//********************************************************************************* pi map
module pi_io_map(
input PiBus pi,
output PiMap map
);
wire pi_exec = pi.oe | pi.we;
wire [1:0]pi_dst = pi.addr[24:23];
assign map.ce_prg = pi_dst == 0 & pi_exec;//8M prg ram
assign map.ce_chr = pi_dst == 1 & pi_exec;//8M chr ram
assign map.ce_srm = pi_dst == 2 & pi_exec;//8M battery ram
assign map.ce_sys = pi_dst == 3 & pi_exec & pi.addr[21:16] == 0;//64K 0x1800000 system registers
assign map.ce_fifo = pi_dst == 3 & pi_exec & pi.addr[21:16] == 1;//64K 0x1810000 fifo. do not use next 64k
//******** system registers
assign map.ce_ggc = map.ce_sys & pi.addr[15:5] == 0;//32B 0x1800000 cheat codes
assign map.ce_cfg = map.ce_sys & pi.addr[15:4] == 2;//16B 0x1800020 mapper configuration
assign map.ce_sst = map.ce_sys & pi.addr[15:13] == 1;//8K 0x1802000 save state data
endmodule
2. SPI数据处理流程(pi_io模块)代码分析
2.1 spi mosi输入接收移位操作
根据spi_clk,每个上升沿接收1bit数据,把原来的sin[6:0]左移1位,刚收到的存入最低位。
always @(posedge spi_clk)
begin
sin[7:0] <= {sin[6:0], spi_mosi};
end
2.2 spi miso输出
assign spi_miso = !spi_ss ? sout[7] : 1'bz;
spi_ss低电平的时候,从scout[7]输出1位,否则输出高阻。
2.3 spi_ss高电平处理
复位命令字,bit和byte计数器,wr_ok,exec标记。sout置为ff。
if(spi_ss)
begin
cmd[7:0] <= 8'h00;
sout[7:0] <= 8'hff;
bit_ctr[2:0] <= 3'd0;
byte_ctr[3:0] <= 4'd0;
pi.act <= 0;
wr_ok <= 0;
exec <= 0;
end
else
begin
2.4 命令字读取和解析处理
- 每个spi_clk收到1bit数据之后,bit_ctr计数加1,
- 当bit_ctr==7(因为bit_ctr是3bit计数器,到7之后再加1就为0了,准备下一个word接收),表示命令字已经接收完毕,!exec表示地址还没有接收完成。
- byte_ctr寄存器为0,是命令字word,1是addr[7:0],依次类推。当地址接收完毕,exec置1.
- 每接收一个word,byte加1(??)。
- 解析命令字,如果是写,并且exec为1(表示数据字段接收开始),没1个word接收完,存入pi.dato,wr_ok置1(可以读写)。在下一个spi_clk,act置1,进行写内存操作。过5个spi_clk,act清0,下一个clk,地址加1,准备写一个word。
- 如果是读,地址收完的下一个clk,act置1,读内存一个word(从dati输入到rd_buffer缓存)。读写完毕act清0,地址加1,rd_buffer写入sout,再一个clk通过下面的代码输出到miso。
assign spi_miso = !spi_ss ? sout[7] : 1'bz;
注:读写的bit计数的每个操作步骤需要时序仿真验证,并考虑SRAM芯片的读写时序。
bit_ctr <= bit_ctr + 1;
if(bit_ctr == 7 & !exec)
begin
if(byte_ctr[3:0] == 4'd0)cmd[7:0] <= sin[7:0];
if(byte_ctr[3:0] == 4'd1)pi.addr[7:0] <= sin[7:0];
if(byte_ctr[3:0] == 4'd2)pi.addr[15:8] <= sin[7:0];
if(byte_ctr[3:0] == 4'd3)pi.addr[23:16] <= sin[7:0];
if(byte_ctr[3:0] == 4'd4)pi.addr[31:24] <= sin[7:0];
if(byte_ctr[3:0] == 4'd4)exec <= 1;
byte_ctr <= byte_ctr + 1;
end
if(cmd[7:0] == CMD_MEM_WR & exec)
begin
if(bit_ctr == 7)pi.dato[7:0] <= sin[7:0];
if(bit_ctr == 7)wr_ok <= 1;
if(bit_ctr == 0 & wr_ok)pi.act <= 1;
if(bit_ctr == 5 & wr_ok)pi.act <= 0;
if(bit_ctr == 6 & wr_ok)pi.addr <= pi.addr + 1;
end
if(cmd[7:0] == CMD_MEM_RD & exec)
begin
if(bit_ctr == 1)pi.act <= 1;
if(bit_ctr == 5)rd_buff[7:0] <= dati[7:0];
if(bit_ctr == 5)pi.act <= 0;//should not release on last cycle. otherwise spi clocked thing may not work properly
if(bit_ctr == 6)pi.addr <= pi.addr + 1;
if(bit_ctr == 7)sout[7:0] <= rd_buff[7:0];
if(bit_ctr != 7)sout[7:0] <= {sout[6:0], 1'b1};
end