移植到vivado上,使用zybo开发板。
文章目录
文件目录
core
include
memery
寄存器组使用双口ram
双口ram看起来没有问题,可以直接移植。
ddr改用axi_hp总线
见后文
移植到zybo
注意需要改成sv文件,以及编码格式改成gbk。
先试一下core
综合成功:
需要加个顶层文件,我们使用原来的reindeer工程进行修改:
暂时先测试core能否运行,所以先不增加外设,只使用core和memery。memery之后添加。
添加memery
mem_addr地址范围
先看一下mem_addr这个信号是什么涵义:
mem_addr(或ext_dram_mem_addr)范围是0起始,每次增加1。也就是说,sdram使用的是相对地址,与cpu总线地址无关。计算公式为:
mem_addr = (pc addr - 0x80000000)/4;
注意,mem_addr对应的是32位数据地址。
代码中如下:
此外,在上一篇提到了:16位bus的sdram地址一次增加2字节,因此sdram的访问地址和pc地址有这样的关系:(/tb_RV/uut/sdram_controller_i/mem_addr
)
sdram addr = (pc addr - 0x80000000)/4 * 2;
这里说一下如何同时使用SRAM与SDRAM
代码中没有使用SRAM:
如果更改配置文件,那么可以自动将SRAM添加进来。此时,SRAM与SDRAM公用mem_addr地址,此时SDRAM起始地址不是0。但是源代码这里没有处理,不知道是怎么回事,
这里应该减去SRAM的地址:
不然SDRAM前一部分空间就没有用上了。
dram_rw_buffer是干嘛用的
写数据时直接写入SDRAM,并且只有一个时钟延迟(首先写入寄存器,然后再写入SDRAM)。读数据则直接连接SDRAM接口。
buf_mem是256大小的数组,里面保存的是{读写位,地址}
。
write_addr表示buf_mem写入地址(从0开始递增),其值表示core发起了多少次读写请求并且未被处理。
read_addr表示buf_mem读出地址(从0开始递增),其值表示SDRAM待处理的地址。
因此可以看出,buf_mem是一个读取请求队列。每来一个读写请求,write_addr+1,随后状态变化为WAIT,pending信号置高。直到收到dram_ack信号,pending信号置零。
外部SDRAM访问控制:
buf_mem:
个人认为这块代码有问题,第一没有必要256的缓存,几个就够了,第二不用缓存应该也可以,反正就是请求时就挂起呗。猜测作者当初是准备实现一个更高级的缓存机制的,例如
1、读SDRAM阻塞core,写SDRAM缓存,不阻塞core,此时需要缓存写数据;
2、一级缓存,这个就比较复杂了,简单来说就是写不缓存,直接写入SDRAM与一级缓存,读数据时首先在一级缓存中查找,如果不存在,阻塞core,然后读取SDRAM。
sdram_controller
这个时我们移植时,更改DDR时需要更改的模块。
主要的信号线:
可以看到接口分为三部分:reset,mem接口(连接MCU),avalon接口(连接片外SDRAM),我们要替换掉avalon接口。
先看看原来的模块干了些啥
mem_cs置高后就开始访问SDRAM,读取完毕后,将两个16位数据拼接成32位返回,同时ctl_mem_ack拉高。
写数据:
读数据:
替换接口
sdram_av_byteenable_n信号(注意,在代码里面,这个信号低电平有效,因此上面的写数据时,sdram_av_byteenable_n=0b00):
转自:https://wenku.baidu.com/view/4fe27c6077232f60ddcca1b8.html
mem_byteenable则是高电平有效,符合上表内容。
其他信号比较明显,这里就不写了。
zybo使用AXI访问内存
这里使用AXI_FULL协议访问,使用黑金的代码,这里只需要会用就行了。
//
// Company: ALINX黑金
// Engineer: 老梅
//
// Create Date: 2016/11/17 10:27:06
// Design Name:
// Module Name: mem_test
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module mem_test
#(
parameter MEM_DATA_BITS = 64,
parameter ADDR_BITS = 32
)
(
input rst, /*复位*/
input mem_clk, /*接口时钟*/
output reg rd_burst_req, /*读请求*/
output reg wr_burst_req, /*写请求*/
output reg[9:0] rd_burst_len, /*读数据长度*/
output reg[9:0] wr_burst_len, /*写数据长度*/
output reg[ADDR_BITS - 1:0] rd_burst_addr, /*读首地址*/
output reg[ADDR_BITS - 1:0] wr_burst_addr, /*写首地址*/
input rd_burst_data_valid, /*读出数据有效*/
input wr_burst_data_req, /*写数据信号*/
input[MEM_DATA_BITS - 1:0] rd_burst_data, /*读出的数据*/
output[MEM_DATA_BITS - 1:0] wr_burst_data, /*写入的数据*/
input rd_burst_finish, /*读完成*/
input wr_burst_finish, /*写完成*/
output reg error
);
parameter IDLE = 3'd0;
parameter MEM_READ = 3'd1;
parameter MEM_WRITE = 3'd2;
parameter BURST_LEN = 128;
(*mark_debug="true"*)reg[2:0] state;
(*mark_debug="true"*)reg[7:0] wr_cnt;
reg[MEM_DATA_BITS - 1:0] wr_burst_data_reg;
assign wr_burst_data = wr_burst_data_reg;
(*mark_debug="true"*)reg[7:0] rd_cnt;
reg[31:0] write_read_len;
//assign error = (state == MEM_READ) && rd_burst_data_valid && (rd_burst_data != {(MEM_DATA_BITS/8){rd_cnt}});
always@(posedge mem_clk or posedge rst)
begin
if(rst)
error <= 1'b0;
else if(state == MEM_READ && rd_burst_data_valid && rd_burst_data != {(MEM_DATA_BITS/8){rd_cnt}})
error <= 1'b1;
end
//一次写burst
always@(posedge mem_clk or posedge rst)
begin
if(rst)
begin
wr_burst_data_reg <= {MEM_DATA_BITS{1'b0}};
wr_cnt <= 8'd0;
end
else if(state == MEM_WRITE)
begin
if(wr_burst_data_req/*写数据信号*/)
begin
wr_burst_data_reg <= {(MEM_DATA_BITS/8){wr_cnt}};
wr_cnt <= wr_cnt + 8'd1;
end
else if(wr_burst_finish/*写完成*/)
wr_cnt <= 8'd0;
end
end
//一次读burst
always@(posedge mem_clk or posedge rst)
begin
if(rst)
begin
rd_cnt <= 8'd0;
end
else if(state == MEM_READ)
begin
if(rd_burst_data_valid/*读出数据有效*/)
begin
rd_cnt <= rd_cnt + 8'd1;
end
else if(rd_burst_finish)
rd_cnt <= 8'd0;
end
else
rd_cnt <= 8'd0;
end
//状态机
always@(posedge mem_clk or posedge rst)
begin
if(rst)
begin
state <= IDLE;
wr_burst_req <= 1'b0;
rd_burst_req <= 1'b0;
rd_burst_len <= BURST_LEN;
wr_burst_len <= BURST_LEN;
rd_burst_addr <= 0;
wr_burst_addr <= 0;
write_read_len <= 32'd0;
end
else
begin
case(state)
IDLE:
begin
state <= MEM_WRITE;
wr_burst_req <= 1'b1;
wr_burst_len <= BURST_LEN;
wr_burst_addr <='h2000000; //首地址为0x2000000<<3 = 0x10000000
write_read_len <= 32'd0;
end
MEM_WRITE:
begin
if(wr_burst_finish)
begin
state <= MEM_READ;
wr_burst_req <= 1'b0;
rd_burst_req <= 1'b1;
rd_burst_len <= BURST_LEN;
rd_burst_addr <= wr_burst_addr;
write_read_len <= write_read_len + BURST_LEN;
end
end
MEM_READ:
begin
if(rd_burst_finish)
begin
if(write_read_len == 32'h2000000)
begin
rd_burst_req <= 1'b0;
state <= IDLE;
end
else
begin
state <= MEM_WRITE;
wr_burst_req <= 1'b1;
wr_burst_len <= BURST_LEN;
rd_burst_req <= 1'b0;
wr_burst_addr <= wr_burst_addr + BURST_LEN;//加128*64 bit
end
end
end
default:
state <= IDLE;
endcase
end
end
endmodule
现在我们要改成一次读写32位数据,当然未来可以一次读取多一点,做一个一级缓存,毕竟AXI_FULL总线还是比较好用的。现在先看看一次读写32位数据,并且根据reindeer的mem接口进行归一化。
AXI_FULL Master
发现上面的代码没有把WSTRB引出来,而且位宽什么的都不好改,看来只能自己写一个AXI_MASTER module了。
参考:https://www.cnblogs.com/milianke/p/15145563.html
注意一下这几个信号的位宽
新的axi_ddr_controller module
要实现的功能:
- 一次读写32位数据
- 写数据可以设置字节有效位
- mem_addr地址范围是0 ~ 8MB/4,需要转换为vivado给AXI_HP分配的新地址
- 读写成功后的ACK信号
mem interface
因为现在可以直接访问32位数据了,所以这里改成21位地址
调试了两天我就想说,怎么官方的代码也是有问题的,value信号和data信号差一个周期,我也是醉了。看来啥都得自己写。
测试了一下是OK的。
复位信号和启动信号和阻塞信号
关注一下CPU复位信号和启动信号,之后用硬核启动cpu,而不是上电自动运行。此外还可以阻塞cpu运行。
start信号
目前没有看到直接阻塞流水线的信号,dram_rw_pending可以起到阻塞流水线作用,但不保证整个流水线阻塞。有待验证。
顶层文件
看一下step_cyc10做了什么
晶振输入信号倍频到需要的时钟信号。
然后是SDRAM部分
然后是外设接口
现在重点看一下boot loader和MCU部分。
boot loader
使用UART进行load,并且控制CPU启动:
debug_coprocessor_wrapper控制boot loader 的整个流程:首先在debug_coprocessor_wrapper启动,下载程序,下载结束后输出cpu_start信号,即开始运行CPU。
注意一下,何时开始boot loader是由串口的TX数据控制的:
因为仿真的过程没有load code过程,因此在时钟准备好后,CPU自动从DEFAULT_START_ADDR
启动
在CPU运行的过程中,debug模块监视TX信息并控制状态(这个没有测试,只是猜测)。
MCU
PulseRain_Reindeer_MCU PulseRain_Reindeer_MCU_i (
.clk (clk_100MHz),
.reset_n ((~cpu_reset) & pll_locked), //active low
.sync_reset (1'b0),
.INTx ({ADXL345_INT2, int0}),
.ocd_read_enable (ocd_read_enable),
.ocd_write_enable (ocd_write_enable),
.ocd_rw_addr (ocd_rw_addr),
.ocd_write_word (ocd_write_word),
.ocd_mem_enable_out (ocd_mem_enable_out),
//assign ocd_mem_word_out = mem_read_data;
.ocd_mem_word_out (ocd_mem_word_out),
.ocd_reg_read_addr (5'd2),
.ocd_reg_we (cpu_start),
.ocd_reg_write_addr (5'd2),
.ocd_reg_write_data (`DEFAULT_STACK_ADDR),
.RXD (RXD),
.TXD (uart_tx_cpu),
.GPIO_IN (gpio_in),
.GPIO_OUT(gpio_out),
.sda_in (ADXL345_SDA),
.scl_in (ADXL345_SCL),
.sda_out (sda_out),
.scl_out (scl_out),
.start (actual_cpu_start),
.start_address (actual_start_addr),
.processor_paused (processor_paused),
.dram_ack (dram_ack),
.dram_mem_read_data (dram_mem_read_data),
.dram_mem_addr (mcu_dram_mem_addr),
.dram_mem_read_en (dram_mem_read_en),
.dram_mem_write_en (mcu_dram_mem_write_en),
.dram_mem_byte_enable (mcu_dram_mem_byte_enable),
.dram_mem_write_data (mcu_dram_mem_write_data),
.peek_pc (),
.peek_ir (),
.peek_mem_write_en (),
.peek_mem_write_data (),
.peek_mem_addr ());
这部分比较简单,主要就是reset、start、OCD、外设、dram
完整的移植代码
约束文件
原工程的约束文件在Reindeer_Step-1.1.2\build\par\constraints\step_cyc10.sdc
里面,主要是对SDRAM的约束,由于zybo使用片上ddr,并且由zynq提供主时钟,因此不需要约束。
load代码和运行
我在这里主要测试了一下依次执行的指令是否正确,个人认为,取值都是对的话,那肯定cpu运行没问题啦,细节没有测试,之后慢慢研究。
上图对比,实际运行和仿真一致。
接下来准备给这个riscv软核加个协处理器接口。