自定义数码管IP核,并让NiosⅡ SBT for Eclipse自动抓取驱动文件
19 Dec 2018 11474字 39分 次 Nios Ⅱ FPGA打赏作者 CC BY 4.0 (除特别声明或转载文章外)
1.前言
在Platform Designer(原Qsys)中创建自定义六位七段独立数码管IP核并让NiosⅡ SBT for Eclipse自动抓取驱动文件。
- 开发环境:Quartus Prime Standard Edition 18.1
- 系统版本:Windows 10 Pro x64 1809
2.框架
编写IP核首先需要编写HDL形成硬件电路将数码管与NiosⅡ处理器连接起来并通信,然后还需要编写基于C语言的类似STM32中的板级支持包的驱动库函数,可以在SOPC开发中直接应用库函数。不然只能用IO口模拟的方式编程,这样就失去了自定义IP核的意义。
2.1.硬件框架
先上一个RTL图看下。
可以看到硬件电路由三部分组成。
首先是“digital_tube_avalon_slaver”,NiosⅡ处理器直接与这个模块相连并通过Avalon总线通信。所以这里需要添加符合Avalon总线协议的接口,如下表:
名称 | 宽度 | 方向 | 说明 |
---|---|---|---|
address | 2 | input | 地址总线 |
chipselect | 1 | input | 片选 |
write_n | 1 | input | 读/写使能 |
writedata | 32 | input | 要写入数码管的数据 |
readdata | 32 | output | 要读进Nios系统中的数据(这里用不到) |
这里边省略了时钟和复位信号。
-
address:宽度不一定是2位,一般是用于选择writedata信号写出去的是什么含义。因为写数据总线(writedata)只有一个,但有时候想写命令有时候想写数据等等,这时通过address的变化就可以区分出writedata到底要往外设写命令还是数据还是别的什么。例如address为0时writedata上要写给外设命令;address为1时writedata上要写给外设数据。
-
chipselect:使能信号,高电平选中。
-
write_n:用来控制这个模块是往出写还是往进读。由于这是个数码管IP,只可能往出写,所以在这跟片选信号几乎一样。
-
writedata:这个信号的宽度是固定的:32bit,用不到的位置零就好;不够用的话……我也不知道咋办。它负责把数码管要显示的数字输出出去。
-
readdata:这个信号的宽度是固定的:32bit,读数据,这里用不到。
这里需要计算一下:要显示六位的数字,那最大就是999999,转换为二进制就是1111_0100_0010_0011_1111,可以看到是20位宽度,所以writedata只用到了低20位。
然后就是自定义的接口:
名称 | 宽度 | 方向 | 说明 |
---|---|---|---|
display_enble | 1 | output | 显示使能 |
display_num | 20 | output | 需要数码管显示的数(二进制) |
很好理解,不说。
然后是二进制转BCD码模块。因为我们希望代码里给的是十进制(二进制)数,数码管直接就能把对应的十进制数字打印出来,但是数码管是一位一位的,只认BCD码。所以这里要用到一个二进制转BCD码模块。输入为20位二进制码,输出为24位的BCD码。这里为了便于查看六位数码管各自的通道所以把24位拆成六个4位,本质上一样。
最后就是数码管显示模块。它会把BCD码转化为对应的数码管显示信号输出给数码管,完成六位十进制数字的显示。
2.2.软件框架
上图:
可以看出驱动文件由寄存器头文件、函数头文件和函数源文件组成。熟悉单片机的应该对后面两个文件很熟悉了。这里只说寄存器头文件。
寄存器头文件就是将直接操作寄存器的函数封装成操作数码管的函数,方便后续的调用。
3.硬件设计
开始写代码了。按照之前介绍的三个模块分别设计。
首先是Avalon总线通信模块:
module digital_tube_avalon_slaver(
input clk, //时钟信号
input rst_n, //复位信号,低电平有效
//Avalon-MM总线
input chipselect, //片选信号,高电平有效
input write_n, //写选通,低电平有效
input [1:0] address, //地址总线
input [31:0] writedata, //写数据总线
//output irq, //中断请求信号,高电平有效
output reg [31:0] readdata, //读数据总线,这里边用不到
output reg display_enable, //数码管显示位使能信号,高电平有效
output reg [19:0] display_num
);
//-------------------------------------------------
//参数设置
//寄存器定义
// address = 00 数码管显示数据寄存器,bit31-20:保留;bit19-0:999999以内十进制数字
// address = 01 数码管显示控制寄存器,bit31-1:保留;bit0:显示
//-------------------------------------------------
//寄存器写入操作
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
begin
display_num <= 20'h00000;
display_enable <= 1'b0;
end
else if(chipselect && !write_n)
begin
case(address)
2'b00: display_num <= writedata[19:0];
2'b01: display_enable <= 1'b1;
default:display_num<=20'b0;
endcase
end
end
//-------------------------------------------------
//寄存器读操作,写了也没用
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
readdata <= 32'd0;
else if(chipselect && write_n)
begin
case(address)
2'b00: readdata <= {12'd0,display_num[19:0]};
2'b01: readdata <= {31'd0,display_enable};
default: readdata <= 32'd0;
endcase
end
end
endmodule
这个模块通过Avalon总线把NiosⅡ发出的数据转换成了后面电路可以识别的20位的二进制码和使能信号。
然后是二进制码转BCD码模块:
Emmmmmmmmmm
太长了,去我GitHub看源码吧:https://github.com/Verdvana/BIN2BCD
原理就是加3移位法:将20位二进制码向左移位20次,每次移位都要判断[43:40],[39:36],[35:32],[31:28],[27:24],[23:10]这6个4位数是否大于等于0101,如果是则当场左移一位。这儿这个“当场”就要用到阻塞赋值了。完成之后这六个4位数就是要显示的六位数的BCD码。
最后就是数码管显示模块:
module digital_tube_seg7(
input clk,
input rst_n,
input display_enable,
input [23:0] display_num,
output reg [6:0] hex0,
output reg [6:0] hex1,
output reg [6:0] hex2,
output reg [6:0] hex3,
output reg [6:0] hex4,
output reg [6:0] hex5
);
//-------------------------------------------------
//参数定义
parameter NUM0 = 7'b1000000,//0,
NUM1 = 7'b1111001,//1,
NUM2 = 7'b0100100,//2,
NUM3 = 7'b0110000,//3,
NUM4 = 7'b0011001,//4,
NUM5 = 7'b0010010,//5,
NUM6 = 7'b0000010,//6,
NUM7 = 7'b1111000,//7,
NUM8 = 7'b0000000,//8,
NUM9 = 7'b0011000,//9,
NULL = 7'b1111111;
always @(posedge clk or negedge rst_n)
if(!rst_n)
begin
hex5 <= NUM0;
hex4 <= NUM0;
hex3 <= NUM0;
hex2 <= NUM0;
hex1 <= NUM0;
hex0 <= NUM0;
end
else
begin
case(display_num[23:20])
4'h0: hex5 <= NUM0;
4'h1: hex5 <= NUM1;
4'h2: hex5 <= NUM2;
4'h3: hex5 <= NUM3;
4'h4: hex5 <= NUM4;
4'h5: hex5 <= NUM5;
4'h6: hex5 <= NUM6;
4'h7: hex5 <= NUM7;
4'h8: hex5 <= NUM8;
4'h9: hex5 <= NUM9;
default:hex5 <= NULL ;
endcase
case(display_num[19:16])
4'h0: hex4 <= NUM0;
4'h1: hex4 <= NUM1;
4'h2: hex4 <= NUM2;
4'h3: hex4 <= NUM3;
4'h4: hex4 <= NUM4;
4'h5: hex4 <= NUM5;
4'h6: hex4 <= NUM6;
4'h7: hex4 <= NUM7;
4'h8: hex4 <= NUM8;
4'h9: hex4 <= NUM9;
default: hex4 <= NULL;
endcase
case(display_num[15:12])
4'h0: hex3 <= NUM0;
4'h1: hex3 <= NUM1;
4'h2: hex3 <= NUM2;
4'h3: hex3 <= NUM3;
4'h4: hex3 <= NUM4;
4'h5: hex3 <= NUM5;
4'h6: hex3 <= NUM6;
4'h7: hex3 <= NUM7;
4'h8: hex3 <= NUM8;
4'h9: hex3 <= NUM9;
default: hex3 <= NULL;
endcase
case(display_num[11:8])
4'h0: hex2 <= NUM0;
4'h1: hex2 <= NUM1;
4'h2: hex2 <= NUM2;
4'h3: hex2 <= NUM3;
4'h4: hex2 <= NUM4;
4'h5: hex2 <= NUM5;
4'h6: hex2 <= NUM6;
4'h7: hex2 <= NUM7;
4'h8: hex2 <= NUM8;
4'h9: hex2 <= NUM9;
default: hex2 <= NULL;
endcase
case(display_num[7:4])
4'h0: hex1 <= NUM0;
4'h1: hex1 <= NUM1;
4'h2: hex1 <= NUM2;
4'h3: hex1 <= NUM3;
4'h4: hex1 <= NUM4;
4'h5: hex1 <= NUM5;
4'h6: hex1 <= NUM6;
4'h7: hex1 <= NUM7;
4'h8: hex1 <= NUM8;
4'h9: hex1 <= NUM9;
default: hex1 <= NULL;
endcase
case(display_num[3:0])
4'h0: hex0 <= NUM0;
4'h1: hex0 <= NUM1;
4'h2: hex0 <= NUM2;
4'h3: hex0 <= NUM3;
4'h4: hex0 <= NUM4;
4'h5: hex0 <= NUM5;
4'h6: hex0 <= NUM6;
4'h7: hex0 <= NUM7;
4'h8: hex0 <= NUM8;
4'h9: hex0 <= NUM9;
default: hex0 <= NULL;
endcase
end
endmodule
和其他所有数码管显示模块原理都一样,就是个数字转换。这里是共阳极七段数码管。
最后用顶层文件把他们组合在一起:
module digital_tube_controller(
input clk, //时钟信号,25MHz
input rst_n, //复位信号,低电平有效
//Avalon-MM总线
input chipselect, //片选信号,高电平有效
input write_n, //写选通,低电平有效
input address, //地址总线
input[31:0] writedata, //写数据总线
//output irq, //中断请求信号,高电平有效
output[31:0] readdata, //读数据总线
//数码管接口
output [6:0] hex0,
output [6:0] hex1,
output [6:0] hex2,
output [6:0] hex3,
output [6:0] hex4,
output [6:0] hex5
);
//-------------------------------------------------
//Avalon-MM总线接口模块
wire display_enable;
wire [19:0] display_num;
wire [3:0] one;
wire [3:0] ten;
wire [3:0] hun;
wire [3:0] tho;
wire [3:0] tth;
wire [3:0] trl;
digital_tube_avalon_slaver uut_digital_tube_avalon_slaver(
.clk(clk), //时钟信号
.rst_n(rst_n), //复位信号,低电平有效
//Avalon-MM总线
.chipselect(chipselect), //片选信号,高电平有效
.write_n(write_n), //写选通,低电平有效
.address(address), //地址总线
.writedata(writedata), //写数据总线
//output irq, //中断请求信号,高电平有效
.readdata(readdata), //读数据总线
//数码管控制信号
.display_enable(display_enable),//数码管显示位使能信号,高电平有效
.display_num(display_num) //数码管显示数据,[15:12]--数码管千位,[11:8]--数码管百位,[7:4]--数码管十位,[3:0]--数码管个位
);
//-------------------------------------------------
//数码管显示驱动模块
digital_tube_seg7 uut_seg7(
.clk(clk), //时钟信号
.rst_n(rst_n), //复位信号,低电平有效
//数码管控制信号
.display_enable(display_enable), //数码管显示位使能信号,高电平有效
.display_num({trl,tth,tho,hun,ten,one}), //数码管显示数据,[15:12]--数码管千位,[11:8]--数码管百位,[7:4]--数码管十位,[3:0]--数码管个位
//数码接口
.hex5 (hex5),
.hex4 (hex4),
.hex3 (hex3),
.hex2 (hex2),
.hex1 (hex1),
.hex0 (hex0)
);
bintobcd uut_bin2bcd(
.clk(clk),
.rst_n(rst_n),
.bin(display_num), //20位二进制码
.one(one), //个位
.ten(ten), //十位
.hun(hun), //百位
.tho(tho), //千位
.tth(tth), //万位
.trl(trl) //兆位
);
endmodule
到此整个电路就搭建好了,综合之后会形成图1那个RTL图。
4.IP核制作
打开Quartus Prime,打开Platform Designer,直接点击IP核目录下Project里的“New Component…”。
在Component Type选项卡里填写IP核信息。
里边的英文单词很简单,也都是字面意思,连我这个六级还没过的渣渣都能看懂(啥时候侥幸过了我就回来删掉这句),所以直接按自己的设定修改就行。
在Files选项卡里点击“Add File…”,将刚写的四个*.v文件塞进去,然后将顶层文件设置为“Top-level File”。点击“Analyze Synthesis Files”编译,然后会发现有错误(我这儿是改好的所以没有),后面会修改。
错误发生在Signal&Interfaces选项卡里。里面缺少复位(reset)和管道(conduit)信号。在左边一栏的“add signal”中添加这两个类,然后把相应的信号拖进对应的类里。并且修改“Signal Tpye”值(图片里灰色斜体)。