Verilog自制NIOS2外设,Avalon总线上的HelloWorld

嘿嘿嘿,开新坑

一开始想写一个SDRAM控制器,和FTDI一些芯片的通信,可是看了很多文章介绍,用Verilog写却又不知如何下手。SDRAM控制部分知道了,控制器是用来读写的,总得有个内部接口来调用吧。有很多的FPGA设计也是一样,我设计出来了怎么知道他是可以用的呢?我总不能使用几个LED和数码管来表示吧?写状态机来测试的话可能比设计电路的本身更复杂更枯燥。

这个时候有个CPU来测试就太好了

Nios2是Altera的32bit哈弗架构软核处理器,在Cyclone IV上一个基本内核综合后占了4.5K左右的资源,剩下的资源相对来说还是很充足的。Nios2各方面的资料支持很足,设计也很标准,各种完善的IP核,C语言开发环境也是自带的,自动化的工具应有尽有,生怕你不会用。完全没有必要为此而自制一个CPU,大费周折,我们的主要矛盾不是自制CPU,也不是自制总线协议,而是一个足够简单的总线和总线主机:CPU。一些IP核学习自己写才是目标。

总线

说起总线我就想起了,今年下半年学习的微机原理与接口技术,8086/8088。这套总线足够简单嘛都是,那个时候才分清了总线和IO,CPU和外设的关系。学单片机配置的各种寄存器的不都是挂在总线上可以寻址的外设嘛! x86就是特殊搞了个I/O总线,用inout指令访问,Nios和ARM Cortex-M单片机差不多嘛,都是和内存统一寻址。Nios2用的是Avalon总线,名字莫名其妙,其实这个总线非常简单。

这次只用到下面几个必要的信号

  • clock(1bit) 时钟信号,8086的总线不是靠时钟边沿,而是靠等待一个总线周期
  • resetn(1bit) 下降沿复位信号
  • writedata(16bit) 写数据
  • readdata(16bit) 读数据
  • read(1bit)读信号
  • write(1bit)写信号
  • byteenable(2bit)字节使能信号
  • chipselect(1bit)片选信号

address地址是可选的,根据自己定制的寄存器数量而定,只有一个的话chipselect选中外设就可以了。irq中断请求不需要,放在以后的文章里面讲。

8086中的data数据线是双向的为的是节省IO,FPGA里面的总线用inout做双向端口的话只会更浪费资源,设计起来也会复杂,所以readdata和writedata分开了。

byteenable看起来很奇怪,32bit的总线有的时候难免操作1个byte的数据,bytenenable是告诉外设要操作哪一byte数据。byteenable的位宽和writedata的位宽是相关的,如果writedata的位宽为32bit,byteenable就应该是4bit。注意:'byteenable’只对写操作有效。

byteenable[1:0]write mask
010x00ff
100xff00
110xffff

接下来干什么

接下来做一个16bit寄存器,写进去的数据读出来是取过反的,实验一个所谓的硬件取反。为了方便我们把它接到低电平驱动的一排LED上,方便观察。

自制外设的代码

// 16位寄存器
module reg16(clock, reset_n, D, byteenable, Q);
    input clock, reset_n;
    input [1:0]byteenable;
    input [15:0]D;
    output reg[15:0]Q;

    always@(posedge clock) begin
      if(!reset_n)
        Q <= 16'h0;
      else begin
      	// byteenable只是判断哪个字节是否锁存
        if(byteenable[0]) Q[7:0] <= D[7:0];
        if(byteenable[1]) Q[15:8] <= D[15:8];
      end
    end
endmodule

module LEDS(
	// Input
    clk,
    reset_n,

    // address,
    byteenable,
    read,
    write,
    writedata,
    chipselect,

    // Output
    readdata,
    Q_export
);
 
input clk, reset_n, read, write, chipselect;

// input address;
input [1:0]byteenable;
input [15:0]writedata;
output [15:0] readdata;
output [15:0] Q_export;

wire [1:0] local_byteenable;
wire [15:0]from_reg;

// 片选且写入,否则的话local_byteenable为0,不改变寄存器值
assign local_byteenable = (chipselect & write) ? byteenable: 2'b00;

reg16 U1(.clock(clk), .reset_n(reset_n), .D(writedata), .byteenable(local_byteenable), .Q(from_reg));

// 取反输出
assign Q_export = ~from_reg;
assign readdata = Q_export;
endmodule

Qsys配置新组件

配置一个基本的Nios有很多,就不在此举出了。

  1. Qsys配置新组件,随便起个名字
    在这里插入图片描述
  2. 选择文件,和顶层入口在这里插入图片描述
  3. 信号设置在这里插入图片描述
    设置后的信号如上图,需要设计的也就Q_export,设置为conduit_end导出IO。
  4. interface设置需要两步
  • Avalon slave复位关联信号
    在这里插入图片描述
  • 设置Read wait为0, 内部寄存器的读取只需要1个时钟周期, 不需要等待在这里插入图片描述

连接到Nios内核

在这里插入图片描述

  • 地址自动分配
  • 复位信号自动连接
  • 从机只需要连接数据总线data master
  • condiuit_end就是设置导出的信号,双击设置导出名称,导出到一组LED观察

配置顶层连接

module	hello
(
	input			clk,
	input[3:0]		key,
	
	inout[15:0]		sdram_dq,
	output[12:0]	sdram_addr,
	output[1:0]		sdram_dqm,
	output			sdram_we_n,
	output			sdram_cas_n,
	output			sdram_ras_n,
	output			sdram_cs_n,
	output[1:0]		sdram_ba,
	output			sdram_clk,
	output			sdram_cke,
	input			epcs_data0,
	output			epcs_dclk,
	output			epcs_sce,
	output			epcs_sdo,
	output[3:0]			leds
);
wire clk_qsys;
pll pll_inst	 
(
	.inclk0		(clk),
	.c0			(clk_qsys),
	.c1			(sdram_clk)
);
qsys qsys_inst
(
	.clk_clk			(clk_qsys),	
	.reset_reset_n		(key[3]),
	
	.sdram_addr			(sdram_addr),
	.sdram_ba			(sdram_ba),
	.sdram_cas_n		(sdram_cas_n),
	.sdram_cke			(sdram_cke), 
	.sdram_cs_n			(sdram_cs_n),
	.sdram_dq			(sdram_dq),
	.sdram_dqm			(sdram_dqm),	 
	.sdram_ras_n		(sdram_ras_n),
	.sdram_we_n			(sdram_we_n),
	
	.epcs_data0			(epcs_data0),	
	.epcs_dclk			(epcs_dclk),
	.epcs_sce			(epcs_sce),
	.epcs_sdo			(epcs_sdo),
	
	.to_led_export		(leds)
);
endmodule

太寒碜了,板子只有4个LED,能看就行,按理说,低电平亮的LED,寄存器写1取反后就亮了。记得锁相环给CPU一个时钟。

来吧,eclipse

打开Nios II开发环境,配置一个Hello World的C工程。很多文章有讲,就不多说了。记得BSP更新生成库文件。

#include <stdio.h>
#include <stdint.h>
#include "system.h"
#include "io.h"

typedef struct {
	uint16_t led;
} LEDS;

int main()
{
  printf("Hello from Nios II!\n");
  LEDS *leds = LEDS_0_BASE;
  uint16_t i, val;
  for(i = 0; i <  0xffff; ++i) {
//	  IOWR_16DIRECT(leds, 0, i);
//	  val = IORD_16DIRECT(leds, 0);
	  leds->led = i;
	  val = leds->led;
	  printf("zpwm: 0x%x\n", val);
	  usleep(200000);
  }

  while(1);
  return 0;
}

没用?这就对了

如果把指针读写换成代码中注释掉的宏就可以了!

IOWR_16DIRECT(leds, 0, i);
val = IORD_16DIRECT(leds, 0);

之前说,Nios和STM32之类的单片差不多,为什么人家是一样的?难道是骗人的?有的同学就会说没有加volatile。可是STM32的外设头文件也没加啊,而且加了也没用!难道Nios单独用了IO指令,和x86一样也是IOMemory分开的吗?外设里也没有涉及到IO/M#信号呀?

这个问题就是经常提及的内存一致性问题,问题是为了提高数据访问速度的D-Cache数据缓存造成的,循环中指针命中了缓存,D-Cache完全由硬件电路负责,和编译器是没有关系的,编译器也很无奈。stm32没有D-Cache,所以不存在这个问题,绕过D-Cache,Nios有两种办法:

  • 引入IO指令绕过D-Cache
  • 31bit1绕过D-Cache,会导致寻址范围降为2Gbit

内存一致性问题

D-Cache是为了加速数据访问引入的,对程序是完全透明,而volatile只能防止变量优化称寄存器,D-Cache满天飞的时代这个关键字已经失去它原本的意义了,快要从标准中剔除了。Cortex-M3 M4都是没有D-Cache只有I-Cache,具体可以看stm32的内核和总线架构。Cortex-M7则出现了这个问题,stm32f7出现的问题,跨界内核有了AXI总线和L1 Cache这些高级玩意,自然不能幸免,和这个还不太一样,主要是两个问题,都是DMA一起用时出现的:

  • RAM里的数据想DMA发送,可是D-Cache还没同步到RAM
  • DMA数据更新到了RAM,可是读取的还是D-Cache

stm32f7里的的MPU设置了D-Cache的地址范围,外设区间被禁用了,所以上面的问题也不会出现。

新姿势以后接着讲

Avalone细说就是一本几十页的手册,目前能用就好,以后慢慢来

参考文献
Making Qsys Components.pdf

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值