1)实验平台:正点原子开拓者FPGA 开发板
2)摘自《开拓者 Nios II开发指南》关注官方微信号公众号,获取更多资料:正点原子
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/index.html
http://weixin.qq.com/r/hEhUTLbEdesKrfIv9x2W (二维码自动识别)
第九章自定义IP核—数码管
NiosII 是一个嵌入式软核处理器,除了可以根据需要任意添加已经提第九章的各种外设外,用
户还可以通过定制自定义 IP 核的方式来满足各种应用需求。定制 IP 核是使用 NiosII 嵌入式软
核处理器的一个重要特征。定制的 IP 核能够以“硬件加速器”的形式实现各种各样用户要求
的功能。本章我们通过自定义数码管 IP 核来学习如何自定义 IP 核。本章包括以下几个部分:
9.1 简介
9.2 实验任务
9.3 硬件设计
9.4 软件设计
9.5 下载验证
简介
自定义 IP 核之前我们先来看一下在系统中 Nios II 是如何与各种外设进行交互的,如下图
所示。
图 9.1.1 Nios II与外设的交互
在该图中,我们可以看到 Nios II 处理器通过使用 Avalon-MM 总线接口操作片上外设的控
制和状态寄存器与外设进行交互。Avalon-MM 总线接口的主端连到 Nios II 和 DMA,从端与
内部各个 IP 核相连接。IP 核通过其它 Avalon 总线接口与内部 IP 和外设进行交互。如 Scatter
gather DMA 通过 Avalon-ST 接口发送和接收数据。PLL 通过 Avalon Clock Sink 接口获得了一
个输入时钟并产生两个输出时钟等。可见如果自定义 IP 核需要了解 Avalon 总线。
一、
Avalon 总线接口规范
在 Altera FPGA 中 Avalon 允许你简单地连接组建来简化系统设计,Avalon 接口适用于高速数据流,读写寄存器,存储器,以及控制片外设备。这些标准接口在 Qsys 中有效地设计到
组件中。你可以在你定制的组件中使用这些标准化的接口来增强你设计的兼容性。在 Avalon
口规范中,定义了以下七个接口:
(1)Avalon Clock Interface:发送和接受时钟的接口。所有 Avalon 接口都是同步的。
(2)Avalon reset Interface:复位的接口。
(3)Avalon Memory Mapped Interface (Avalon-MM):基于地址读写典型的主从连接关系的接
口。
(4)Avalon Interrupt Interface:允许组件到信号事件与其他组件的接口。
(5)Avalon Streaming Interface (Avalon-ST):支持单向数据流,包括复用流、数据包、DSP
数据。
(6)Avalon Tri-State Conduit Interface (Avalon-TC):支持连接到片外外围设备的接口。多重
外围设备可以通过信号复用(多路传输)来分享引脚,减少使用 FPGA 引脚数和 PCB 上的导
线。
(7)Avalon Conduit Interface:适应不能适合任何其余 Avalon 类型的个别信号或信号组的接
口类型。你可以连接在 Qsys 系统里面的 conduit 接口,或者你可以输出它们,以连接到设计中
的其他模块,或者 FPGA 的引脚。
1. Avalon Clock
首先我们介绍的是 Avalon Clock 接口,Avalon Clock 接口定义了一个时钟或多个时钟用于
一个组件。组件可以有时钟输入,时钟输出,或两者都有。例如,相位锁相环(PLL),它一
个时钟输入和多个时钟输出,如下图所示。
图 9.1.2 PLL 的时钟输入与输出
从该图中我们可以看出,Clock Sink 是输入信号,Clock Source 是输出信号,下面我们就
对这两个信号分别进行介绍:首先我们介绍的是 Clock Sink 输入信号。Clock Sink 为其他接口
和内部逻辑提供一个同步时钟。Clock Sink 信号类型,如下表所示。
图 9.1.3 Clock Sink 信号类型
看完了 Clock Sink 信号类型,我们再来看下 Clock Sink 信号属性,如下表所示。
图 9.1.4 Clock Sink 的信号属性
说完了 Clock Sink,接着我们再来看下 Clock Source,Clock Source 类型,如下表所示。
图 9.1.5 Clock Source 信号类型
看完了 Clock Source 信号类型,我们再来看下 Clock Source 信号属性,如下表所示。
图 9.1.6 Clock Source信号属性
2. Avalon Reset
说完了 AvalonClock,接下来我们再来看看 Avalon Reset,Reset Sink 信号类型,如下表所
示。
图 9.1.7 Reset Sink 信号类型
看完了 Reset Sink 信号类型,我们再来看下 Reset Sink 信号属性,如下表所示。
图 9.1.8 Reset Sink信号属性
说完了 Reset Sink,接着我们再来看下 Reset Source,Reset Source 信号类型,如下表所示。
图 9.1.9 Reset Source信号类型
看完了 Reset Source 信号类型,我们再来看下 Reset Source 信号属性,如下表所示。
图 9.1.10 Reset Source信号属性
3. Avalon-MM Interfaces
Avalon-MM Interfaces 是 Avalon Memory-Mapped Interfaces 的简称,即 Avalon 存储器映射
接口。Avalon-MM 接口是一种交换式总线,具有良好的数据交换特性和很高的总线带宽。由于
Avalon-MM 接口是针对 Qsys 设计的,所以 Avalon-MM 接口具有结构简单,采用全同步时序,
以及可以灵活地配置等特点,其运行时钟、总线位宽、各个接口位宽以及各个外设之间的互联
特性等都可以灵活地配置。
一个 Avalon-MM 外设可以包含任意的信号类型,这取决于它与外设逻辑接口的需求,但
外设的每个信号都要指定一个有效的 Avalon-MM 信号类型,以确定该信号的作用。Avalon-MM
信号类型可以分为从端口和主端口信号两类,这取决于 Avalon-MM 端口是主端口还是从端口。
对于某些信号类型,主端口和从端口中可能都包含,但由于端口类型不同、这些信号的行为可
能有所不同。每个单独的主端口或从端口使用的信号类型由外设的设计决定。Avalon-MM 主端口或从端口的每个信号都准确地对应与一种 Avalon-MM 信号类型。对于每种信号类型,
Avalon-MM 端口都只能具有一个信号类型。Avalon-MM 接口信号可配置,对于特定的 Avalon
MM 外设,并不是所有 Avalon-MM 信号都必须用到,外设设计者可以根据需要只使用必须的
信号类型,从而降低系统的复杂性。例如,一个只用于输出的 16 位的通用 I/O 外设,如下图
所示。
图 9.1.11 只用于输出的 16 位通用 I/O 外设
从该图中我们可以看出,这个简单的 Avalon-MM 外设只写信号和写数据信号,没有用到
读信号和读数据信号。
Avalon-MM 总线的传输方式是一种主从式的传输方式,即由一个主控端外设发起并控制
传输过程,而从属端外设响应经由总线模块发来的信号完成整个传输。我们看看 Avalon-MM
接口信号类型,Avalon-MM 接口信号类型,如下表所示。
图 9.1.12 Avalon-MM接口信号类型
Avalon-MM 信号属性有很多,下面我们介绍一下接口相关的属性和相关时序:
图 9.1.13 Avalon-MM信号属性
下面我们再来看下 Avalon-MM 传输时序,
Avalon-MM 的传输定义为外设(peripheral)与 Avalon-MM 总线模块间的数据传输,分为
主(Mster)端传输和从(Slave)端传输两类,每类传输又分为基本(fundamental)传输、流
水线(pipelined)传输、突发(burst)传输等等。所有的 Avalon-MM 传输都基于基本传输,其
它传输形式都是在该传输模式下加以改进或增加某些特性以适应不同需求。一个 Master 端传
输和一个对应的 Slave 端传输即可完成两个外设通过总线模块进行的一次数据传输,但 Master
端传输与 Slave 端传输的模式并不要求一致,两端传输模式可以随意搭配。同种类型的 Master
端传输与 Slave 端传输在时序上基本是一致的,其区别仅在于 Master 端传输是由 Master 端外
设驱动总线模块,而 Slave 端传输是由总线模块驱动 Slave 端外设。下面我们就以 Avalon-MM
从端写基本传输时序为例进行讲解,如下图所示。
图 9.1.14 Avalon-MM 从端写基本传输时序图
从该图中我们可以看出,当时钟信号 clk 为上升沿,且写使能信号 write 为高时,地址信
号 address 和写数据信号 writedata 才有效。
4. Avalon Conduit
说完了 Avalon-MM,接下来我们再来看看 AvalonConduit,Avalon Conduit 接口用于驱动
片外外设信号,如驱动 SDRAM 的地址、数据和控制信号。信号类型如下表所示。
图 9.1.15 Avalon Conduit信号类型
实验任务
本章的实验任务是自定义 IP 核实现动态数码管显示
硬件设计
了解了 Avalon 总线,现在我们就可以为我们的外设定制 IP 核了。我们以定制数码管外设
的 IP 为例,介绍 IP 核的制作流程。由于我们是初次制作 IP 核,对 IP 核的制作步骤并不了解,
所以,在开始制作 IP 核之前,我们先来讲一讲 IP 核的制作流程,一个典型 IP 核的制作流程
主要分为以下六个步骤:
1) 规划 IP 核的硬件功能;
2) 定义一个恰当的 Avalon 接口;
3) 使用硬件描述语言描述硬件逻辑;
4) 使用 IP 核编辑器封装硬件逻辑,完成 IP 核定制;
5) 编写用于描述寄存器的 C 头文件和 IP 核的驱动 C 文件;
6) 让 Nios SBT for Eclipse 自动抓取 IP 核的 HAL
知道了 IP 核的制作流程,接下来我们可以跟着制作流程一步步往下走,定制数码管 IP 核。
(1) 规划 IP 核的硬件功能
首先我们的第一步是规划 IP 核的硬件功能,如何规划数码管 IP 核的硬件功能呢?因为
Avalon 总线是对寄存器进行操作,寄存器内的数值控制外设的状态。所以我们针对数码管的数
值显示和显示控制设置数据寄存器和控制寄存器,具体如下:
图 9.3.1 寄存器描述
(2) 定义一个恰当的 Avalon 接口
规划完了数码管 IP 核的硬件功能,如果我们没有记错的话,那么便会进入第二步也就是:
给我们的数码管外设定义一个恰当的 Avalon 接口。下面我们给出数码管所用的接口信号,如
下表所示。
图 9.3.2 数码管所用的接口信号
sel 为数码管的位选端,segled 为段选端,所以选用 Conduit 接口。
(3) 使用硬件描述语言描述硬件逻辑
定义完了 Avalon 接口,接下来我们就可以进入第三步,使用硬件描述语言描述硬件逻辑。
在开始描述 LEDIP 核的硬件逻辑之前,我们先来看下一个典型的 IP 核的硬件逻辑。一个典型
的 IP 核的硬件逻辑由以下三个功能模块组成:
◆ 接口文件:作为顶层模块,定义总线接口信号;
◆ 寄存器文件:完成该 IP 核与外部信号进行通信,有了寄存器文件,用户就可以通过 Avalon
接口采用基地址+地址偏移量的方式来访问组件内部各寄存器。
◆ 硬件逻辑文件:实现 IP 核的硬件功能;
知道了这三个功能模块后,接下来我们就来编写这个三个功能模块,首先我们需要在
Quartus 软件的安装路径的 ip 文件夹下新建一个文件夹用来存放我们的 IP 核文件,这里我们
创建的文件夹名字是 my_ip,然后我们在 my_ip 文件夹中创建了一个新文件夹,这里我们取名
为 segled。现在我们这该文件夹下创建三个文件:分别为 Avalon 接口文件也可以称为顶层文
件 segled_controller.v、硬件逻辑文件 segled_logic.v、寄存器文件 segled_register.v。创建这三个
文件有很多方法,这里我们推荐使用 Quartus 来创建,我们可以创建一个以 segled_controller.v
为顶层文件的 Quartus 工程,在 Quartus 工程中我们可以编写代码并编译检查代码中的错误,
代码通过编译后将.v 文件复制到 my_ipsegled 文件夹中即可。
这三个文件的 RTL 连接如下图所示:
图 9.3.3 RTL连接图
首先我们给出顶层文件 segled_controller.v,该文件代码如下:
1 module segled_controller(
2 //module clock
3 input clk , // 时钟信号
4 input rst_n , // 复位信号(低有效)
5
6 //Avalon-MM interface
7 input [ 1:0] avs_address , // Avalon 地址总线
8 input avs_write , // Avalon 写请求信
9 input [31:0] avs_writedata, // Avalon 写数据总线
10
11 //seg_led interface
12 output [5:0] sel , // 数码管位选端(选择的数码管)
13 output [7:0] seg_led // 数码管段选端(数码管数值显示的段)
14 );
15
16 //wire define
17 wire [19:0] data ; // 6 个数码管要显示的数值
18 wire [ 5:0] point; // 小数点显示的位置,从高(左)到低(右),高电平有效
19 wire sign ; // 显示符号位(高电平显示“-”号)
20 wire en ; // 数码管使能信号
21
22 //*****************************************************
23 //** main code
24 //*****************************************************
25
26 //寄存器文件
27 segled_register u_segled_register(
28 //module clock
29 .clk (clk ), // 时钟信号
30 .rst_n (rst_n ), // 复位信号(低有效)
31 //Avalon-MM interface
32 .avs_address (avs_address ), //Avalon 地址总线
33 .avs_write (avs_write ), //Avalon 写请求信
34 .avs_writedata (avs_writedata), //Avalon 写数据总线
35 //user interface
36 .data (data ), // 6 个数码管要显示的数值
37 .point (point ), // 小数点显示的位置,高电平有效
38 .sign (sign ), // 显示符号位(高电平显示“-”号)
39 .en (en ), // 数码管使能信号
40 );
41
42 //硬件逻辑文件
43 segled_logic u_segled_logic(
44 //module clock
45 .clk (clk ), // 时钟信号
46 .rst_n (rst_n ), // 复位信号(低有效)
47 //user interface
48 .data (data ), // 6 个数码管要显示的数值
49 .point (point ), // 小数点显示的位置,高电平有效
50 .sign (sign ), // 显示符号位(高电平显示“-”号)
51 .en (en ), // 数码管使能信号
52 //seg_led interface
53 .sel (sel ), // 数码管位选端
54 .seg_led (seg_led ) // 数码管段选端
55 );
56
57 endmodule
从该代码中我们可以看出,该文件主要是作为顶层模块,用于连接硬件逻辑文件和寄存器
文件,代码中没有编写任何的逻辑功能。下面我们给出的是硬件逻辑文件 segled_logic.v,该文
件代码如下:
1 module segled_logic(
2 //module clock
3 input clk , // 时钟信号
4 input rst_n , // 复位信号(低有效)
5
6 //seg_led interface
7 output reg [5:0] sel , // 数码管位选端(选择的数码管)
8 output reg [7:0] seg_led, // 数码管段选端(数码管数值显示的段)
9
10 //user interface
11 input [19:0] data , // 6 个数码管要显示的数值
12 input [ 5:0] point , // 小数点显示的位置,从左到右,高电平有效
13 input sign , // 显示符号位(高电平显示“-”号)
14 input en // 数码管使能信号
15 );
16
17 //parameter define
18 localparam MAX_NUM = 13'd5000 ; // 1ms 计数值
19 localparam CLK_DIVIDE = 4'd10 ; // 时钟分频
20
21 //reg define
22 reg [12:0] cnt0 ; // 1ms 计数
23 reg flag ; // 1ms 计满标志信号
24 reg [2:0] cnt ; // 切换显示数码管用
25 reg [3:0] num1 ; // 送给要显示的数码管,要亮的灯
26 reg point1 ; // 要显示的小数点
27 reg [23:0] num ; // 24 位 bcd 码用寄存器
28 reg [ 3:0] clk_cnt ; // 时钟计数
29 reg dri_clk ; // 驱动数码管操作的驱动时钟
30
31 //wire define
32 wire [3:0] data0 ; // 十万位数
33 wire [3:0] data1 ; // 万位数
34 wire [3:0] data2 ; // 千位数
35 wire [3:0] data3 ; // 百位数
36 wire [3:0] data4 ; // 十位数
37 wire [3:0] data5 ; // 个位数
38
39 //*****************************************************
40 //** main code
41 //*****************************************************
42
43 assign data5 = data[19:0] / 17'd100000; // 十万位数
44 assign data4 = data[19:0] / 14'd10000 % 4'd10; // 万位数
45 assign data3 = data[19:0] / 10'd1000 % 4'd10 ; // 千位数
46 assign data2 = data[19:0] / 7'd100 % 4'd10 ; // 百位数
47 assign data1 = data[19:0] / 4'd10 % 4'd10 ; // 十位数
48 assign data0 = data[19:0] % 4'd10; // 个位数
49
50 //生成数码管的驱动时钟用于驱动数码管的操作
51 always @(posedge clk or negedge rst_n) begin
52 if(!rst_n) begin
53 dri_clk <= 1'b1;
54 clk_cnt <= 4'd0;
55 end
56 else if(clk_cnt == CLK_DIVIDE/2 - 1'd1) begin
57 clk_cnt <= 4'd0;
58 dri_clk <= ~dri_clk;
59 end
60 else
61 clk_cnt <= clk_cnt + 1'b1;
62 end
63
64 //将 20 位 2 进制数转换为 8421bcd 码
65 always @ (posedge dri_clk or negedge rst_n) begin
66 if (!rst_n)
67 num <= 24'b0;
68 else begin
69 if (data5 || point[5]) begin
70 num[23:20] <= data5;
71 num[19:16] <= data4;
72 num[15:12] <= data3;
73 num[11:8] <= data2;
74 num[ 7:4] <= data1;
75 num[ 3:0] <= data0;
76 end
77 else begin
78 if (data4 || point[4]) begin
79 num[19:0] <= {data4,data3,data2,data1,data0};
80 if(sign)
81 num[23:20] <= 4'd11;
82 else
83 num[23:20] <= 4'd10;
84 end
85 else begin
86 if (data3 || point[3]) begin
87 num[15: 0] <= {data3,data2,data1,data0};
88 num[23:20] <= 4'd10;
89 if(sign)
90 num[19:16] <= 4'd11;
91 else
92 num[19:16] <= 4'd10;
93 end
94 else begin
95 if (data2 || point[2]) begin
96 num[11: 0] <= {data2,data1,data0};
97 num[23:16] <= {2{4'd10}};
98 if(sign)
99 num[15:12] <= 4'd11;
100 else
101 num[15:12] <= 4'd10;
102 end
103 else begin
104 if (data1 || point[1]) begin
105 num[ 7: 0] <= {data1,data0};
106 num[23:12] <= {3{4'd10}};
107 if(sign)
108 num[11:8] <= 4'd11;
109 else
110 num[11:8] <= 4'd10;
111 end
112 else begin
113 num[3:0] <= data0;
114 if(sign)
115 num[23:4] <= {{4{4'd10}},4'd11};
116 else
117 num[23:4] <= {5{4'd10}};
118 end
119 end
120 end
121 end
122 end
123 end
124 end
125
126 //计数 1ms
127 always @ (posedge dri_clk or negedge rst_n) begin
128 if (rst_n == 1'b0) begin
129 flag <= 1'b0;
130 cnt0 <= 13'b0;
131 end
132 else if (cnt0 < MAX_NUM - 1'b1) begin
133 flag <= 1'b0;
134 cnt0 <= cnt0 + 1'b1;
135 end
136 else begin
137 flag <= 1'b1;
138 cnt0 <= 13'b0;
139 end
140 end
141
142 //计数器,用来计数 6 个状态(因为有 6 个灯)
143 always @ (posedge dri_clk or negedge rst_n) begin
144 if (rst_n == 1'b0)
145 cnt <= 3'b0;
146 else if(flag) begin
147 if(cnt < 3'd5)
148 cnt <= cnt + 1'b1;
149 else
150 cnt <= 3'b0;
151 end
152 end
153
154 //6 个数码管轮流显示,完成刷新( 从右到左)
155 always @ (posedge dri_clk or negedge rst_n) begin
156 if(!rst_n) begin
157 sel <= 6'b000000;
158 num1 <= 4'b0;
159 end
160 else begin
161 if(en) begin
162 case (cnt)
163 3'd0: begin
164 sel <= 6'b111110;
165 num1 <= num[3:0] ;
166 point1 <= ~point[0] ;
167 end
168 3'd1: begin
169 sel <= 6'b111101;
170 num1 <= num[7:4] ;
171 point1 <= ~point[1] ;
172 end
173 3'd2: begin
174 sel <= 6'b111011;
175 num1 <= num[11:8];
176 point1 <= ~point[2] ;
177 end
178 3'd3: begin
179 sel <= 6'b110111;
180 num1 <= num[15:12];
181 point1 <= ~point[3] ;
182 end
183 3'd4: begin
184 sel <= 6'b101111;
185 num1 <= num[19:16];
186 point1 <= ~point[4];
187 end
188 3'd5: begin
189 sel <= 6'b011111;
190 num1 <= num[23:20];
191 point1 <= ~point[5];
192 end
193 default: begin
194 sel <= 6'b000000;
195 num1 <= 4'b0;
196 point1 <= 1'b1;
197 end
198 endcase
199 end
200 else
201 sel <= 6'b111111;
202 end
203 end
204
205 //数码管显示数据
206 always @ (posedge dri_clk or negedge rst_n) begin
207 if (!rst_n)
208 seg_led <= 7'h40;
209 else begin
210 case (num1)
211 4'd0 : seg_led <= {point1,7'b1000000};
212 4'd1 : seg_led <= {point1,7'b1111001};
213 4'd2 : seg_led <= {point1,7'b0100100};
214 4'd3 : seg_led <= {point1,7'b0110000};
215 4'd4 : seg_led <= {point1,7'b0011001};
216 4'd5 : seg_led <= {point1,7'b0010010};
217 4'd6 : seg_led <= {point1,7'b0000010};
218 4'd7 : seg_led <= {point1,7'b1111000};
219 4'd8 : seg_led <= {point1,7'b0000000};
220 4'd9 : seg_led <= {point1,7'b0010000};
221 4'd10: seg_led <= 8'b11111111;
222 4'd11: seg_led <= 8'b10111111;
223 default : seg_led <= {point1,7'b1000000};
224 endcase
225 end
226 end
227
228 endmodule
该代码与我们的数码管动态显示实验中的数码管驱动代码相同,只是更改了文件名和模块
名,对于此代码有不理解的地方可参考动态数码管显示实验。下面我们给出的是寄存器文件
segled_register.v,该文件代码如下:
1 module segled_register(
2 input clk , // 时钟信号
3 input rst_n , // 复位信号(低有效)
4
5 //Avalon-MM interface
6 input [ 1:0] avs_address , //Avalon 地址总线
7 input avs_write , //Avalon 写请求信
8 input [31:0] avs_writedata, //Avalon 写数据总线
9
10 //user interface
11 output reg [19:0] data , // 6 个数码管要显示的数值
12 output reg [ 5:0] point , // 小数点显示的位置,从左到右,高电平有效
13 output reg sign , // 显示符号位(高电平显示“-”号)
14 output reg en // 数码管使能信号
15 );
16
17 //*****************************************************
18 //** main code
19 //*****************************************************
20
21 //用于给数码管数据寄存器进行赋值
22 always @(posedge clk or negedge rst_n) begin
23 if(!rst_n)
24 data <= 20'd0;
25 else if((avs_write) && (avs_address == 2'b00))
26 data <= avs_writedata[19:0]; // 数码管显示的数据
27 end
28
29 //用于给数码管控制小数点显示寄存器进行赋值
30 always @(posedge clk or negedge rst_n) begin
31 if(!rst_n)
32 point <= 6'd0;
33 else if((avs_write) && (avs_address == 2'b01))
34 point <= avs_writedata[5:0]; // 小数点显示的位置,从左到右,高电平有效
35 end
36
37 //用于给数码管控制符号寄存器进行赋值
38 always @(posedge clk or negedge rst_n) begin
39 if(!rst_n)
40 sign <= 1'b0;
41 else if((avs_write) && (avs_address == 2'b10))
42 sign <= avs_writedata[0]; // 显示符号位(高电平显示“-”号)3
43 end
44
45 //用于给数码管使能寄存器进行赋值
46 always @(posedge clk or negedge rst_n) begin
47 if(!rst_n)
48 en <= 1'b0;
49 else if((avs_write) && (avs_address == 2'b11))
50 en <= avs_writedata[0]; // 数码管使能信号
51 end
52
53 endmodule
代码第 6~8 行定义了 Avalon 的地址总线、写请求线和数据总线的接口。第 11~14 行为 4
个寄存器的接口。从第 22 行开始的语句通过写信号 ava_write 和地址信号 ava_address 给相应
地址的寄存器写入数据线 avs_writedata 的数据。
(4) 使用 IP 核编辑器封装硬件逻辑,完成 IP 核定制
当我们创建好了描述数码管 IP 核的三个模块后,我们就可以使用 Qsys 中的组件编辑器
将它们封装成一个 IP 核。我们打开 Qsys 软件,选择 Qsys 软件菜单栏中的【File】→【New
Component…】将会出现如下图所示页面。
图 9.3.4 Component Editor 页面图
在组件编辑器页面中我们给创建的IP核填写基本信息,填写完毕后,我们便可以点击【Next】
进入下一个页面,如下图所示。
图 9.3.5 添加完硬件文件后的窗口
在该图中,我们可以点击【+】按钮打开添加文件对话框,将路径指向 segled 文件夹所在
的目录,最后选中三个.v 文件添加即可,要注意的是默认第一栏为顶层文件,如果不是,双击
Attributes 栏下的 no attributes 修改为顶层文件即可。添加完硬件文件后,Synthesis Files 栏中可
看到刚添加的 3 个文件。下面我们点击【Analyze Synthesis Files】按钮来分析这三个文件,分
析完毕后将会出现如下图所示页面。
图 9.3.6 硬件描述文件分析完毕结果图
从该图中我们可以看到,我们的代码没有错误,如果代码中存在错误,这里分析就不会通
过。我们点击【Close】即可,这时我们会发现最下面的提示窗口中出现了错误,如下图所示:
图 9.3.7 错误提示
这个错误我们会在后面的步骤中将它解决掉,这里先不用管。接下来我们继续点击【Next】
进入下一个页面,如下图所示。
图 9.3.8 参数页面图
参数标签能够用于在 Qsys 系统中指定配置组件的实例的参数。如我们开拓者开发板上使
用的是 6 位数码管,所以该 IP 核用于 6 位数码管的驱动,如果我们想把该自定义 IP 核用于任
意位数码管,可以定义一个参数,当用于不同位数码管,修改该参数即可,如同 SDRAM 控制
器的参数设置,这样就不用再重新添加 IP 核组件。接下来我们点击【Next】进入下一个页面,
如下图所示。
图 9.3.9 设置信号页面图
我们需要修改信号的总线接口 Interface 和信号类型 Signal Type,如上图所示。我们发现底
部的错误减少了。接下来我们点击【Next】进入下一个页面,如下图所示。
图 9.3.10 设置接口页面图
从该图中我们在 avalon slave 一栏中修改了 Name 和 Associated 两个选项,这时错误消失
便消失了。下面我们便可以点击【Finish】按钮,出现如下图所示页面。
图 9.3.11 保存
这时我们可以看到 Qsys 软件将我们创建的 segled_controller_hw.tcl 文件保存到了 F:
Qsysqsys_segledpar 路径下面。之所以保存在这里,是因为我们在创建 IP 核之前已经创建了
Quartus 工程和 Qsys 系统,如果没有创建 Quartus 工程直接在 Qsys 软件中创建 IP 核,默认路
径将会是 Quartus 的安装路径,这里需要注意的是,如果默认路径是 Quartus 的安装路径,那
么创建出来的 segled_controller_hw.tcl 文件我们还需要修改其中的路径,否则我们在使用时会
出现错误。这里读者可以自行尝试。我们点击【Yes,Save】按钮就完成 IP 核的定制,这里需
要我们注意的是,为了方便 IP 的管理,我们需要将 segled_controller_hw.tcl 文件复制到我们先
前创建的放置三个.v 文件的路径中,即 D:quartus13ipmy_ipsegled 中。
(5) 编写用于描述寄存器的 C 头文件和 IP 核的驱动 C 文件
完成 IP 核定制,我们就可以在 Qsys 软件的 IP 核库中看到我们添加的 IP 核了,也就是
说,我们已经可以使用该 IP 核了,不过,由于我们没有给该 IP 核添加寄存器头文件和底层驱
动文件,我们使用起来是极其不方便的,为了以后的使用方便,下面我们就需要给该 IP 核添
加寄存器头文件和底层驱动文件,首先我们需要做的是在my_ipsegled 文件夹中创建两个新文
件夹,一个为 HAL 文件夹,另一个为 inc 文件夹。如图 9.3.12 所示。
图 9.3.12 segled IP核文件夹
我们需要在my_ipsegledinc 文件夹下创建一个 segled_controller_regs.h 文件,还需要在
my_ipsegledHALinc 文件夹创建一个 segled_controller.h 文件。创建好了文件以后,接下来我
们要分别对这两个 C 文件进行编写代码,在编写代码时我们可以参考 altera 为我们已经提供好
的 IP 核,比如,可以参考 altera_avalon_pio_regs.h 文件来写我们的 segled_controller_regs.h 文
件,这里我们直接给出已经编写好的代码,首先我们给出的是 segled_controller_regs.h 文件的
代码,如下所示:
1 #ifndef __SEGLED_CONTROLLER_REGS_H__
2 #define __SEGLED_CONTROLLER_REGS_H__
3
4 #include <io.h>
5
6 //Data register
7 #define IOWR_AVALON_SEGLED_DATA(base,data) IOWR(base, 0, data)
8 //Control register1
9 #define IOWR_AVALON_SEGLED_DOT(base,data) IOWR(base, 1, data)
10 //Control register2
11 #define IOWR_AVALON_SEGLED_SIGN(base,data) IOWR(base, 2, data)
12 //Control register3
13 #define IOWR_AVALON_SEGLED_EN(base,data) IOWR(base, 3, data)
14
15 #endif
从该代码中,我们定义了四个宏定义,分别用于写数据寄存器、控制小数点显示的控制寄
存器 1、控制符号位显示的控制寄存器 2 和控制是否显示的控制寄存器 3。接下来我们给出的
是 segled_controller.h 文件的代码,如下所示:
1 #ifndef __SEGLED_CONTROLLER_H__
2 #define __SEGLED_CONTROLLER_H__
3
4 #include "system.h"
5 #include "alt_types.h"
6 #include "segled_controller_regs.h"
7
8 #ifdef __cplusplus
9 extern "C"
10 {
11 #endif /* __cplusplus */
12
13
14 /* function station */
15
16 #ifdef __cplusplus
17 }
18 #endif /* __cplusplus */
19
20 #endif /* __SEGLED_CONTROLLER_H__ */
我们可以在第 12~15 行声明函数,由于我们这里没有额外定义函数,就没有声明。
(6) 让 Nios SBT for Eclipse 自动抓取 IP 核的 HAL
编写完了寄存器头文件和驱动底层头文件,也就到了 LED IP 核定制的最后一步,创建
_sw.tcl 文件,_sw.tcl 文件同_hw.tcl 文件一样都是利用 Tcl 语言编写的,由于_sw.tcl 内容比较
少,并且 Tcl 语言也是比较容易看懂的,所以我们完全没有必要再专门去学习 Tcl 语言,当然,
如果有对 Tcl 语言感兴趣的朋友可以另外查找关于 Tcl 语言资料进行学习。这里我们就不再进
一步介绍了。
下面我们直接给出已经编写好的 segled_controller_sw.tcl 代码,如代码 5.7 所示。
1 #
2 # segled_controller.tcl
3 #
4
5 # Create a new driver
6 create_driver segled_controller
7
8 # Associate it with some hardware known as "segled_controller"
9 set_sw_property hw_class_name segled_controller
10
11 # The version of this driver
12 set_sw_property version 13.1
13
14 # This driver may be incompatible with versions of hardware less
15 # than specified below. Updates to hardware and device drivers
16 # rendering the driver incompatible with older versions of
17 # hardware are noted with this property assignment.
18 set_sw_property min_compatible_hw_version 1.0
19
20 # Initialize the driver in alt_sys_init()
21 set_sw_property auto_initialize false
22
23
24 #
25 # Source file listings...
26 #
27
28
29 # Include files
30 add_sw_property include_source HAL/inc/segled_controller.h
31 add_sw_property include_source inc/segled_controller_regs.h
32
33 # This driver supports HAL & UCOSII BSP (OS) types
34 add_sw_property supported_bsp_type HAL
35 add_sw_property supported_bsp_type UCOSII
36 add_sw_property supported_bsp_type BML
37
38 # End of file
这里我们需要说明的是,我们参考的是 altera_avalon_pio_sw.tcl 文件中的代码来编写的,
在该代码中,比较重要的也就两处,第一处是第 21 行代码,该代码是取消用来将我们的数码
管 IP 核添加至 alt_sys_init()函数中进行自动初始化。第二处是第 30 和 31 行,该代码是用来关
联我们编写的寄存器头文件和底层驱动文件。
下面我们来看一下 Qsys 的硬件框架,如下图所示
图 9.3.13 数码管 IP 核的硬件框架图
添加的数码管 IP 核我们不需要任何配置,clk 为 50Mhz,RAM 为 20480Bytes,ROM 为
10240Bytes。
对硬件工程进行例化的顶层代码,如下所示:
1 module top_segled(
2 //module clock
3 input sys_clk, // 时钟信号
4 input sys_rst_n, // 复位信号(低有效)
5
6 //segled interface
7 output wire [5:0] sel, // 数码管位选端
8 output wire [7:0] seg_led // 数码管段选端
9 );
10
11 //*****************************************************
12 //** main code
13 //*****************************************************
14
15 //例化数码管模块
16 segled u_segled (
17 .clk_clk (sys_clk ), // 时钟信号
18 .reset_reset_n (sys_rst_n), // 复位信号(低有效)
19 .segled_sel (sel ), // 数码管位选端
20 .segled_seg_led (seg_led ) // 数码管段选端
21 );
22
23 endmodule
其中管脚分配如下:
表9.3.1EPCS IP核实验管脚分配
软件设计
讲完了硬件框架,接下来我们来看一下本实验的软件工程代码,如下:
1 #include <stdio.h> // 标准输入输出头文件
2 #include <unistd.h> // 延迟函数头文件
3 #include "alt_types.h" // 数据类型头文件
4 #include "segled_controller.h" // 数码管驱动文件
5
6 //---------------------------------------------------------------------------
7 //-- 名称 : main()
8 //-- 功能 : 程序入口
9 //-- 输入参数 : 无
10 //-- 输出参数 : 无
11 //---------------------------------------------------------------------------
12
13 int main() {
14
15 alt_u32 counter;
16
17 IOWR_AVALON_SEGLED_DOT(SEGLED_CONTROLLER_BASE,04); // 8 进制,点亮右边第 3 个小数点
18 IOWR_AVALON_SEGLED_EN(SEGLED_CONTROLLER_BASE,1);
19 IOWR_AVALON_SEGLED_SIGN(SEGLED_CONTROLLER_BASE,1); // 显示符号位
20 for(counter=200;counter>0;counter--){ // 从 200 开始递减
21 IOWR_AVALON_SEGLED_DATA(SEGLED_CONTROLLER_BASE,counter);
22 usleep(10000);
23 }
24
25 IOWR_AVALON_SEGLED_SIGN(SEGLED_CONTROLLER_BASE,0); // 关闭显示符号位
26 for(counter=0;counter<=99999;counter++){ // 从 f 开始递增
27 IOWR_AVALON_SEGLED_DATA(SEGLED_CONTROLLER_BASE,counter);
28 usleep(10000);
29 if(counter==99999)
30 counter=0;
31 }
32 return 0;
33 }
SEGLED_CONTROLLER_BASE 是数码管 IP 的寄存器起始地址,可在 system.h 中找到。
代码中第一个 for 的功能是从-200 递减到 0,第二个 for 的功能是从 0 递增到 99999。
下载验证
讲完了软件工程,接下来我们就将该实验下载至我们的开拓者开发板进行验证,首先我们
需要在 Quartus II 软件中将 qsys_segled.sof 文件下载至我们的开拓者开发板,qsys_segled.sof 下
载完成后,我们还需要在 Eclipse 软件中将 segled.elf 文件下载至我们的开拓者开发板,下载完
成以后,我们的 C 程序将会在我们的开拓者开发板上执行,在开发板上的显示结果如下:
图 9.5.1 实验结果
实验结果与设计的相符,自定义数码管 IP 核实验完成。