Quartus创建自定义IP核 - LED控制IP核

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/tq384998430/article/details/84988888

一、前言

之前使用Quartus II的Qsys工具软件创建了一个SOPC系统,包含了NIOS II处理器、ROM、RAM、JTAG_UART等IP核,虽然Qsys工具已经提供了非常丰富的IP库,但是有些特殊功能的IP核在Library中是没有的,例如我要控制一个LED屏幕,如果使用单片机控制的话直接使用单片机的IO口控制时序刷新LED屏即可,如果想要使用SOPC,可以在系统上添加一个PIO模块,然后使用C语言和单片机一样控制LED屏即可。但是如果LED屏幕很大,需要控制的点数很多的话,会占用太多NIOS CPU的时间,甚至会导致LED屏幕刷新速度过慢,显示效果差。这时候我们就想了,如果有一个专门控制LED屏幕的芯片就好了,我们的SOPC系统连接到这个LED屏控制芯片,控制LED屏幕刷新的任务交过这个芯片就行了,我们用程序控制芯片来显示就行了............嗯?不对,FPGA不就是可以创造一个芯片么?我们完全可以在FPGA中创建一个这样的LED屏的控制芯片,然后在FPGA内部将这个“芯片”集成到我们的SOPC系统中去就行了?

没错,我这就是这篇文章的目的所在,而且Quartus软件的Qsys工具早就已经有这个功能了:创建自定义的IP核。之前创建SOPC系统的时候我们发现NIOS II核和RAM、ROM、JTAG等外设的连接是通过“data master”和“instruction master”总线连接的,而“instruction master”总线是指令总线,都是连接到可执行程序的存储器(On_chip RAM、On_chip ROM、SDRAM、DDR2 RAM)或者JTAG调试模块的,其他外设一般不会连接“instruction master”总线的,但是“data master”总线是会连接到所有外设的,这个总线将所有外设连接到NIOS II CPU寻址空间上去的,使得CPU可以寻址到挂载这个总线上的器件,并向器件中写入或者读取数据。如下图所示,“data master”数据总线在CPU端的名字叫做“Avalon Memery Mapped Master”,在器件端的名字叫做“Avalon Memery Mapped Slave”。

到这里我们就知道了,想要将我们自己创建的IP核的能够被NIOS II控制到话,需要将器件挂载到NIOS II的地址总线上去,然后将数据总线连接到NIOS II的数据总线,以及一些控制信号线。其实在NIOS II系统“Avalon Memery Mapped Master”就是传统的8086系统中的地址总线和数据总线。

二、设计IP核

上面我们说过了要能够融合进NIOS II的系统中去,需要创建一个和“Avalon Memery Mapped Master”总线匹配的连接端口,这个端口就是“Avalon Memery Mapped Slave”了。我们先不考虑Avalon总线的时序,我们先创建一个自己的IP核,哪怕这个核没有挂载到NIOS II的总线系统上去,只是一个简单的控制LED闪烁的功能。

创建一个Quartus工程,添加顶层模块,创建一个SOPC系统......这句操作之前的文章有,就不再说了,现在我已经创建好了一个SOPC系统了,如下图:

然后我们再创建一个Verilog文件,保存叫做“led_ip.v“,这个文件就是作为我们要创建的IP核的功能描述文件,在这个module的端口中添加信号:clk、rst_n、avs_address、avs_write、avs_writedata、led,其中包括了输入时钟和复位信号“Avalon Memery Mapped Slave”总线接口led输出端口。然后在程序中添加一个简单的LED计数器功能用于查看运行状态,程序如下:

然后在Qsys软件界面的菜单栏操作,File -- New Component :

Parameters设置先略过,简单的模块不需要设计Parameters,下面设置Signals参数,Signals参数里面是根据Verilog文件中的参数来的,需要正确选择Interface和Signal Type,如图所示:

最后进入到Interface设置,按照下图设置:

在下面可以看到一个Write Waveforms栏:

这就是我们在自己的IP核中要实现的“Avalon Memery Mapped Slave”总线的时序简图,暂且不管。

保存这个设计,然后在Qsys主界面中我们可以看见我们设计的IP核在IP Library中了:

我们就按照添加其他外设一样的操作来添加我们的led_controller:

可以看到这里面什么都没有,但是左边有连接示意图:

这个看起来和System Library库里面的器件很像了啊,时钟、复位、总线、export信号都有,点击Finish完成,然后连接时钟、复位和总线到之前的系统中去,选择Assign Base Address分配地址,最后效果如下图:

然后再Generate生成SOPC系统,进去Quartus重新编译并下载到FPGA中去,发现4个LED灯累加计数,说明添加的自定义IP核已经在工作了。

下面可以进行读写控制了,我做的这个简单的IP核没有读取功能,只有写入操作,所以IP核只需要控制写入的时序就行了。具体时序上面那张时序图已经画得很清楚了,在clk的上升沿,如果write信号为高电平,则表示写操作,这时候address表示的就是操作地址,writedata就是要写的数据。我们设计led_controller核的时候设定了其地址线的宽度为2bit,难道说这个器件只有两根线连接到NIOS II的地址总线上去么?显然不是,我们在配置SOPC系统的时候执行了Assign Base Address操作,然后可以看到led_controller模块被分配了起始地址0x6000,终止地址0x600F,表示地址0x6000到0x600F都是led_controller模块的。“Avalon Memery Mapped Master”发出读写信号时,如果地址总线上的地址在0x6000和0x600F之间的话,就会选通led_controller模块,但是怎么解释led_controller的2bit地址呢?我没有找到哪里有说明,但是我的直觉就是地址总线的最低两位就是led_controller定义的2bit地址,那么我么写led_controller模块的时候0x6000到0x6003这四个地址是有效的,那么Qsys为什么不分配0x6000到0x6003而是分配0x6000到0x600F给led_controller模块呢?好像在New Component过程中有配置,以后可以再试试,这倒是无所谓。具体程序如下图,程序中加了一个reg变量“register1”表示寄存器1,Avalon总线可以通过write操作将数据写入到“register1”寄存器中,我定义操作地址的最低两位为2b'00时修改register1为50000000,为2b'01时修改......

在Qsys中右击led_controller模块,电机Edit,在Files中再一次Analysis Synthesis Files,Finish之后重新Generate生成新的SOPC系统,然后到Quartus中重新编译下载......

三、设计C程序

SOPC系统已经设计好了,开始C语言编程:

#include "sys/alt_stdio.h"
#include "io.h"

int main()
{ 
  alt_putstr("Hello from Nios II!\n");
//  while(1)
  {
	  IOWR(0x6000, 0, 10000000);
	  IOWR(0x6000, 1, 10000000);
	  IOWR(0x6000, 2, 10000000);
	  IOWR(0x6000, 3, 1000000);
  }
  /* Event loop never exits. */
  while (1);

  return 0;
}

 然后进行单步调试,注意,单步调试会暂停NIOS II CPU,但是不会暂停我们的led_controller,因为这是硬件模块控制LED的,不是程序控制LED的,执行完之后发现LED的累加计数频率按照预想的进行,说明我猜对了。

调试的时候遇到一个问题,就是执行最后一句设置之后,LED的累加频率变得较高,这时候退出Debug模式会卡主,且无法正常退出Debug,我在猜是不是由于我使用的几根LED线跟单片机的运行配置或者调试有关的线,比如PIN_76的Special Function是nCEO,PIN_75的Special Function是INIT_DONE,如果nCEO或者INIT_DONE一直在跳变不知道是不是会影响FPGA的运行或者调试,留一个疑问在这!

之后我又实验了一下指针式的操作,程序如下:

#include "sys/alt_stdio.h"
#include "io.h"

int main()
{ 
  alt_putstr("Hello from Nios II!\n");
//  while(1)
  {
	  *(int *)(0x6000) = 10000000;
	  *(int *)(0x6001) = 10000000;
	  *(int *)(0x6002) = 10000000;
	  *(int *)(0x6003) = 1000000;
  }
  /* Event loop never exits. */
  while (1);

  return 0;
}

但是单步调试发现LED的闪烁并没有反应,着实是不开心,难道不是地址总线的最低两位对应着led_controller模块定义的两根地址线么?又想到既然led_controller模块的寻址空间是0x6000到0x600Fm,也就是4位地址线,那我再试试高两位:

#include "sys/alt_stdio.h"
#include "io.h"

int main()
{ 
  alt_putstr("Hello from Nios II!\n");
//  while(1)
  {
	  *(int *)(0x6000 | 3) = 10000000;
	  *(int *)(0x6004 | 3) = 10000000;
	  *(int *)(0x6008 | 3) = 10000000;
	  *(int *)(0x600C | 3) = 1000000;
  }
  /* Event loop never exits. */
  while (1);

  return 0;
}

程序里面控制2、3位,然后将最低位写1,运行发现可以控制LED的累加频率,说明模块的两根地址线对应的是地址总线的2、3两位,而不是最低两位。

但是这个问题很奇怪,于是我修改了一下led_controller IP核,将address的宽度改成4bits,然后重新编译led_controller IP核,删除原来的led_controller器件并重新添加新的led_controller,然后重新Assign Base Address,发现地址分配变成了下图:

首地址还是0x6000,结束地址为0x603F,地址空间好像总是比我定义的地址空间长度要长两位。

捋一捋思绪,突然一激灵,想起来了!!!因为NIOS II系统的数据/地址总线宽度为32位!我定义的IP核的地址线有4根,数据总线是固定的32位,那么IP核的寻址范围为 2^4 * 4 = 64 个字节的数据,也就是0x00到0x3F,这就和Qsys分配的地址正好对应起来了。由于我的IP核的地址线寻址的都是32位数据,那么地址信号的最低两位数据是无效的,我们使用IOWR函数(宏定义)的时候其实就是屏蔽了最低两位的,IOWR函数的第二个参数是偏移量Offset,这个偏移量是以WORD(字)为单位偏移的,而不是BYTE(字节)。

 

 

 

 

 

猜你喜欢

转载自blog.csdn.net/tq384998430/article/details/84988888