FPGA 之 SOPC 系列(七)NIOS II 高级技术

2020-12-30 10:54:26 浏览数 (1)

大侠好,欢迎来到FPGA技术江湖,江湖偌大,相见即是缘分。大侠可以关注FPGA技术江湖,在“闯荡江湖”、"行侠仗义"栏里获取其他感兴趣的资源,或者一起煮酒言欢。

今天给大侠带来今天带来FPGA 之 SOPC 系列第七篇,NIOS II 高级技术,希望对各位大侠的学习有参考价值,话不多说,上货。

本篇是有关SOPC的深入设计,帮助读者掌握如何定制用户指令。定制用户逻辑外设和定制用户指令是使用Nios II嵌入式软核处理器的SOPC系统的重要特性,用户还可以通过定制用户逻辑外设和定制用户指令来实现各种应用要求,同时介绍了Nios II C语言至硬件加速编译器(C2H)。

以下为本篇的目录简介:

7.1 定制基于Avalon的用户外设

7.2 定制Nios II用户指令

7.3 Nios II C语言至硬件加速编译器(C2H)简介

7.1 定制基于Avalon的用户外设

NIOS II是一个建立在FPGA上的嵌入式软核处理器,除了可以根据需要任意添加已经提供的外设外,用户还可以通过定制用户逻辑外设和定制用户指令来实现各种应用要求。

用户定制SOPC Builder元件的开发流程

(1)指定硬件功能

(2)指定微处理器访问和控制该硬件的应用程序接口

(3)定义一个AVALON接口:提供正确的控制机制、足够的吞吐性能

(4)采用VHDL或Verilog编写硬件设计

(5)单独测试硬件设计

(6)编写C头文件,定义寄存器映射

(7)使用元件编辑器将硬件和软件文件打包成一个元件

(8)例化元件为SOPC系统的一个模块

(9)使用NIOSII处理器测试元件的寄存器级访问

(10)编写元件的驱动程序

(11)反复改进元件的设计:硬件、软件、元件更新

(12)编译完整的包含一个或多个该元件的SOPC系统

(13)执行系统级的验证,若必要,进行反复设计

(14)完成元件设计,发布共享元件

定制用户外设简介

下图为带Avalon Slave端口的典型元件组成框图:

带Avalon Slave端口的典型元件组成框图

在这里我们将详细的通过一个控制LED灯亮度的元件来介绍基于AVALON总线的用户外设的过程。

LED灯亮度原理:

PWM输出一个占空比可调的方波。当一个周期11个时钟,高电平输出7个时钟时的PWM输出波形如下图所示。

PWM输出波形

PWM设计说明:

本实例的PWM是按下列要求设计的:

1.任务逻辑按一个简单时钟进行同步操作。

2.可以使用微控制器(Nios II)来设置PWM的周期和占空比的值。因此要提供一个可对PWM寄存器进行读写的接口和控制逻辑。

3.定义寄存器来存储PWM周期和占空比的值。

4.微控制器可以通过控制寄存器的禁止位来关闭PWM输出。

元件内部还包括使能控制寄存器、周期设定寄存器以及占空比设置寄存器。设计中我们将各寄存器映射成AVALON SLAVE端口地址空间内一个单独的偏移地址。每个寄存器都可以进行读写访问,软件可以读回寄存器中的当前值,寄存器及偏移地址设定如下:

构建一个符合AVALON-MM slave 接口规范的可以实现我们功能的时序逻辑,在这里,我们利用VERILOG语言来编写。在程序中会涉及到AVALON信号,我们把这些要用到的信号陈列如下。

代码如下:

代码语言:javascript复制
module led_control(
    clk,
    reset_n,
    chipselect,
    address,
    write,
    writedata,
    read,
    byteenable,
    readdata,
    led_out
    );
  input  clk;
  input  reset_n;
  input  chipselect;
  input  [1:0]address;
  input  write;
  input  [31:0]  writedata;
  input  read;
  input  [3:0]  byteenable;
  output  [31:0]  readdata;
  output  led_out;
   
  reg    [31:0]  clock_divide_reg;
  reg    [31:0]  duty_cycle_reg;
  reg    control_reg;
  reg    duty_cycle_reg_selected;
  reg    clock_divide_reg_selected;
  reg    control_reg_selected;
  reg    [31:0]  led_counter;
  reg    [31:0]  readdata;
  reg    led_out;
  wire  led_enable;
  
  //地址译码
  always  @  (address)
    begin
            clock_divide_reg_selected<=0;
            duty_cycle_reg_selected<=0;
            control_reg_selected<=0;
        case(address)
            2'b00:clock_divide_reg_selected<=1;
            2'b01:duty_cycle_reg_selected<=1;
            2'b10:control_reg_selected<=1;
        default:
            begin
              clock_divide_reg_selected<=0;
              duty_cycle_reg_selected<=0;
              control_reg_selected<=0;
            end
        endcase
     end
  //写led输出周期的时钟数寄存器
  always @ (posedge clk or negedge reset_n)
  begin
    if(reset_n==1'b0)
      clock_divide_reg=0;
    else
    begin
      if(write & chipselect & clock_divide_reg_selected)
        begin
          if(byteenable[0])
            clock_divide_reg[7:0]=writedata[7:0];
          if(byteenable[1])
            clock_divide_reg[15:8]=writedata[15:8];
          if(byteenable[2])
            clock_divide_reg[23:16]=writedata[23:16];
          if(byteenable[3])
            clock_divide_reg[31:24]=writedata[31:24];
        end
    end
  end
//写led周期占空比寄存器
always @ (posedge clk or negedge reset_n)
begin
  if(reset_n==1'b0)
    duty_cycle_reg=0;
  else
  begin
    if(write & chipselect & duty_cycle_reg_selected)
      begin
        if(byteenable[0])
          duty_cycle_reg[7:0]=writedata[7:0];
        if(byteenable[1])
          duty_cycle_reg[15:8]=writedata[15:8];
        if(byteenable[2])
          duty_cycle_reg[23:16]=writedata[23:16];
        if(byteenable[3])
          duty_cycle_reg[31:24]=writedata[31:24];
      end
  end
end
//写控制寄存器
always @ (posedge clk or negedge reset_n)
begin
  if(reset_n==1'b0)
    control_reg=0;
  else
  begin
    if(write & chipselect & control_reg_selected)
      begin
        if(byteenable[0])
          control_reg=writedata[0];
      end
  end
end
//读寄存器
always @ (address or read or clock_divide_reg or duty_cycle_reg or control_reg or chipselect)
begin
  if (read & chipselect)
    case(address)
      2'b00:readdata<=clock_divide_reg;
      2'b01:readdata<=duty_cycle_reg;
      2'b10:readdata<=control_reg;
      default:readdata=32'h8888;
    endcase
end
//控制寄存器
assign led_enable=control_reg;
//led功能部分
always @(posedge clk or negedge reset_n)
begin
  if(reset_n==1'b0)
    led_counter=0;
  else
  begin
    if(led_enable)
    begin
      if(led_counter>=clock_divide_reg)
        led_counter<=0;
      else
        led_counter<=led_counter 1;
    end
    else
      led_counter<=0;
  end
end
always @(posedge clk or negedge reset_n)
begin
  if(reset_n==1'b0)
    led_out<=1'b0;
  else
  begin
    if(led_enable)
    begin
      if(led_counter<=duty_cycle_reg)
        led_out<=1'b1;
      else
        led_out<=1'b0;
    end
    else
      led_out<=1'b0;
  end
end
 
endmodule

在工程里添加好这个.v文件后,命名为led_control.v并将其存放到工程目录下就OK

接下来我们开始添加这个元件了,我们在Q2里打开sopc builder,进入后,点击file->new component 。

点击后,如下图,我们点击ADD,添加进来我们建立的那个.V文件。加入后,稍微等一下,系统开始对它的端口进行分析了,当出现NOERROR时,说明就OK了。关闭它就行。点击NEXT,我们可以看到,我们的那些端口信号都出现了。

根据功能要求来配置这些信号,其中,INTERFACE是AVALON接口类型了。SIGNAL TYPE指的是各个AVALON接口类型下的信号类型。好在系统已经分析好了,只有一个LED_OUT需要改动,因为它的朝向不是avalon模块了,改完后如下。

改完后点击NEXT,找到slave addressing把它改成native,意思就是地址对齐的选项,选择为静态地址对齐,其他的地方默认。其他的选项说明一下这个TIMIING部分,led的avalon slave端口与avalon slave端口时钟信号同步,读写的建立时间保持时间为0,因为读、写寄存器仅需要一个时钟周期,所以读写为0等待不需要延时。点击NEXT,在GROUP建立一个名称,叫MYIP,那么这个元件就放到这个组里了。

点击FINISH,后点击YES,就会生成一个led_control_hw.tcl脚本文件,回到sopc builder界面,在左侧中的myip中可以找到led_control元件了。

添加路径,这个路径设置是为了让SOPC BUILDER可以找到led_control.v的位置。不然的话,下次进入SOPC BUILDER时,这个元件可能无效。操作是在tool->options,点击右侧的ip search path,接下来的操作一目了然。

  • 自动地址分配
  • 分配中断
  • 管脚分配
  • 编译

编译好后查看一下system.h的变化情况,我们可以发现,多出来一个ledcontrol部分了下面给出测试代码:

代码语言:javascript复制
#include<unistd.h>
#include"system.h"
//
typedef struct{
                    volatile unsigned int divi;
                    volatile unsigned int duty;
                    volatile unsigned int enable;
                }LED_CONTROL;   
int main()
{
    int dir=1;
    //
    LED_CONTROL *led_control=(LED_CONTROL*)LED_CONTROL_BASE;
    
    //
    led_control->divi=1000;
    led_control->duty=0;
    led_control->enable=1;
    
    while(1){
        if(dir>0){
            if(led_control->duty<led_control->divi)
                led_control->duty =100;
            else
                dir=0;
            }
            else{
                if(led_control->duty>0)
                    led_control->duty-=100;
                else
                    dir=1;
            }
            usleep(100000);
        }   
        
        return 0;
}           

7.2 定制Nios II用户指令

用户定制指令:将一个包含多条标准指令的指令序列减少为硬件实现的一条指令

1)NIOSII处理器配置向导提供了图形化界面添加封装用户定制指令;

2)NIOSII支持256条定制指令;

3)NIOSII IDE在system.h中为每条定制指令产生一个宏,用户在应用程序中通过调用宏访问定制指令。

Nios II定制指令综述

组合逻辑指令结构框图如下图所示:

组合逻辑指令结构框图

多周期指令结构框图如下图所示:

多周期指令结构框图

扩展指令结构框图如下图所示:

扩展指令结构框图

带内部寄存器的乘加指令结构框图如下图所示:

带内部寄存器的乘加指令结构框图

定制指令实现方式

定制指令支持多种设计文件,包括:Verilog HDL, VHDL, EDIF netlist file, Quartus II Block Design File (.bdf), 和Verilog Quartus Mapping File (.vqm)。

具体实现方法有:

1.导入HDL文件实现定制指令;

2.通过DSP Builder实现定制指令加速模块;

3.直接使用SOPC Builder中自带的定制指令。

简易步骤如下:

(1)打开NIOSII CPU的定制指令设置页;

(2)添加用户定制指令设计文件;

(3)发布用户定制指令

(4)将定制指令加入系统,完成定制指令添加。

7.3 Nios II C语言至硬件加速编译器(C2H)简介

  • C2H是能够提升对时间性能要求较高的ANSIC函数的工具,它将这些函数转换为FPGA中的硬件加速器。
  • C2H支持标准ANSI C代码,可加速实现多种应用程序,提高其运行效率,包括访问片内、外部存储器和外设等。
  • C2H帮助Nios II用户以最少的资源占用来达到提高系统性能的目的。

Nios II C2H编译器设计流程非常简单,编写好应用程序后,用户需要做的工作首先是分析软件代码,确定出现性能瓶颈的函数,然后在Nios II IDE中高亮显示所需的函数,右键单击加速便可以生成自动链接至软件流程的硬件加速器。

FPGA 之 SOPC 系列第七篇就到这里结束,下一篇将带来第八篇,程序固化相关内容。各位大侠,明天见!

END

后续会持续更新,带来Vivado、 ISE、Quartus II 、candence等安装相关设计教程,学习资源、项目资源、好文推荐等,希望大侠持续关注。

大侠们,江湖偌大,继续闯荡,愿一切安好,有缘再见!

0 人点赞