一、项目要求
利用ADC制作一个数字电压表
-
旋转电位计可以产生0-3.3V的电压;
-
利用板上的串行ADC对电压进行转换;
-
将电压值在板上的OLED屏幕上显示出来。
二、设计思路及功能模块
本次使用的硬件为基于Lattice XO2-4000HC FPGA核心板及电子森林综合训练底板,开发环境为Diamond 3.10,验证环境为QuestaSim-64 2021.1,使用Verilog编程语言。
本项目分为如下几个功能模块:
- ADC驱动及数码管显示模块:adc_driver2seg;
- OLED驱动模块:oled_driver_adc;
- 项目顶层模块:proj_top。
在ADC驱动及数码管显示模块下分为:
- ADC驱动模块;
- 数码管显示模块(额外完成);
- adc_driver2seg顶层模块。
在OLED驱动模块下分为:
- oled_cmd_RAM;
- oled_char_RAM;
- oled_driver_adc顶层模块。
总系统RTL如下图所示:
三、各模块代码及简要说明
3.1 ADC驱动模块
ADC驱动模块原先参考电子森林:简易电压表设计中关于ADC081S101采样的程序,该程序主要思想是通过计数器编写一个ADC采样时钟(SCLK),根据计数器的值编写对应的操作。而后改为更好的FSM形式的驱动,该驱动的SCLK可以直接使用系统时钟,不必损失转换速度;还可根据实际ADC芯片的工作时序进行状态调整;且可通过adc_eoc信号对片选位作出自动控制,以精确控制ADC的转换流程。
module adc_driver #(
parameter ADC_WIDTH = 8
)(
input sys_clk,
input rst_n,
input sdo,
output sclk,
output reg adc_csn,
output reg [ADC_WIDTH-1:0] adc_data
);
localparam IDLE = 2'b00;
localparam HOLD = 2'b01;
localparam CONVERT = 2'b10;
localparam FINISH = 2'b11;
wire adc_eoc;
reg [ADC_WIDTH-1:0] adc_data_reg;
reg [1:0] state;
reg [1:0] CNT_0;
reg [2:0] CNT_data;
assign sclk = sys_clk;
assign adc_eoc = (state == FINISH);
always @(posedge sys_clk or negedge rst_n) begin
if(!rst_n)
adc_csn <= 1'b1;
else if(adc_eoc)
adc_csn <= 1'b1;
else
adc_csn <= 1'b0;
end
always @(posedge sys_clk or negedge rst_n) begin
if(!rst_n) begin
state <= IDLE;
adc_data <= 8'b0;
adc_data_reg <= 8'b0;
CNT_0 <= 2'b10;
CNT_data <= 3'b111;
end
else
case(state)
IDLE: begin
state <= HOLD;
end
HOLD: begin
CNT_0 <= CNT_0 - 1'b1;
if(CNT_0 == 0) begin
state <= CONVERT;
CNT_0 <= 2'b10;
end
else
state <= HOLD;
end
CONVERT: begin
CNT_data <= CNT_data - 1'b1;
adc_data_reg <= {adc_data_reg[ADC_WIDTH-2:0], sdo};
if(CNT_data == 0) begin
state <= FINISH;
CNT_data <= 3'b111;
end
else
state <= CONVERT;
end
FINISH: begin
adc_data <= adc_data_reg;
state <= IDLE;
end
default: state <= IDLE;
endcase
end
endmodule
3.2 数码管显示模块
3.2.1 二进制转BCD码模块
参考电子森林:简易电压表设计的代码并做了改进,将if
判断操作写成function
便于多次调用,改进后的代码如下:
module bin2bcd #(
parameter ADC_WIDTH = 8
)(
input clk,
input rst_n,
input [ADC_WIDTH-1:0] adc_data,
output reg [19:0] bcd_code
);
wire [15:0] bin_code;
reg [35:0] shift_reg; // 16*2+4=36
assign bin_code = adc_data * 16'd129; // MAX 32895(1000_0000_0111_1111)
// BCD码各位数据作满5加3操作
function [3:0] fout(input [3:0] fin);
fout = (fin > 4) ? (fin + 2'b11) : fin;
endfunction
always @(*) begin
shift_reg = bin_code;
if(!rst_n)
bcd_code = 'b0;
else begin
repeat(16) begin
shift_reg[19:16] = fout(shift_reg[19:16]);
shift_reg[23:20] = fout(shift_reg[23:20]);
shift_reg[27:24] = fout(shift_reg[27:24]);
shift_reg[31:28] = fout(shift_reg[31:28]);
shift_reg[35:32] = fout(shift_reg[35:32]);
shift_reg = shift_reg << 1;
end
bcd_code = shift_reg[35:16];
end
end
endmodule
3.2.2 数码管驱动模块
参考电子森林:数码管显示代码并做了改进,为提高通用性,将共阴/阳极与小数点亮灭的控制写入模块的参数(parameter
)中,本项目采用共阴极数码管。
module seg_display #(
parameter seg_dig = 1'b0, // 1'b0:共阴极;1'b1:共阳极
parameter seg_dp = 1'b1
)(
input clk,
input rst_n,
input [3:0] seg_data,
output reg [8:0] seg_led // DIG、DP、G、F、E、D、C、B、A
);
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
seg_led <= 9'h3f; // 0
else if(!seg_dig)
case(seg_data)
0: seg_led <= {seg_dig, seg_dp, 7'h3f}; // 0
1: seg_led <= {seg_dig, seg_dp, 7'h06}; // 1
2: seg_led <= {seg_dig, seg_dp, 7'h5b}; // 2
3: seg_led <= {seg_dig, seg_dp, 7'h4f}; // 3
4: seg_led <= {seg_dig, seg_dp, 7'h66}; // 4
5: seg_led <= {seg_dig, seg_dp, 7'h6d}; // 5
6: seg_led <= {seg_dig, seg_dp, 7'h7d}; // 6
7: seg_led <= {seg_dig, seg_dp, 7'h07}; // 7
8: seg_led <= {seg_dig, seg_dp, 7'h7f}; // 8
9: seg_led <= {seg_dig, seg_dp, 7'h6f}; // 9
10:seg_led <= {seg_dig, seg_dp, 7'h77}; // A
11:seg_led <= {seg_dig, seg_dp, 7'h7c}; // B
12:seg_led <= {seg_dig, seg_dp, 7'h39}; // C
13:seg_led <= {seg_dig, seg_dp, 7'h5e}; // D
14:seg_led <= {seg_dig, seg_dp, 7'h79}; // E
15:seg_led <= {seg_dig, seg_dp, 7'h71}; // F
default: seg_led <= {seg_dig, seg_dp, 7'h3f};
endcase
else
case(seg_data)
0: seg_led <= {seg_dig, seg_dp, 7'hc0}; // 0
1: seg_led <= {seg_dig, seg_dp, 7'hf9}; // 1
2: seg_led <= {seg_dig, seg_dp, 7'ha4}; // 2
3: seg_led <= {seg_dig, seg_dp, 7'hb0}; // 3
4: seg_led <= {seg_dig, seg_dp, 7'h99}; // 4
5: seg_led <= {seg_dig, seg_dp, 7'h92}; // 5
6: seg_led <= {seg_dig, seg_dp, 7'h82}; // 6
7: seg_led <= {seg_dig, seg_dp, 7'hf8}; // 7
8: seg_led <= {seg_dig, seg_dp, 7'h80}; // 8
9: seg_led <= {seg_dig, seg_dp, 7'h90}; // 9
10:seg_led <= {seg_dig, seg_dp, 7'h88}; // A
11:seg_led <= {seg_dig, seg_dp, 7'h83}; // B
12:seg_led <= {seg_dig, seg_dp, 7'hc6}; // C
13:seg_led <= {seg_dig, seg_dp, 7'ha1}; // D
14:seg_led <= {seg_dig, seg_dp, 7'h86}; // E
15:seg_led <= {seg_dig, seg_dp, 7'h8e}; // F
default: seg_led <= {seg_dig, seg_dp, 7'hc0};
endcase
end
endmodule
3.2.3 adc2seg顶层模块
将ADC转换的8位数字量(adc_data)输入bin2bcd模块,输出8位BCD码(bcd_code),高4位输入seg_display模块,并点亮小数点,低4位同理,但不点亮小数点。输出端的8位信号oled_display_digital(即转换后的BCD码,个位+一位小数)输入至OLED显示模块。
module adc2seg #(
parameter ADC_WIDTH = 8,
parameter seg_dig = 1'b0,
parameter seg_dp = 2'b10
)(
input clk,
input rst_n,
input [ADC_WIDTH-1:0] adc_data,
output [7:0] oled_display_digital,
output [8:0] digital_1,
output [8:0] digital_2
);
wire [19:0] bcd_code;
bin2bcd #(
.ADC_WIDTH(ADC_WIDTH)
) ins1(
.clk(clk),
.rst_n(rst_n),
.adc_data(adc_data),
.bcd_code(bcd_code)
);
seg_display #(
.seg_dig(seg_dig),
.seg_dp(seg_dp[1])
) seg_1(
.clk(clk),
.rst_n(rst_n),
.seg_data(bcd_code[19:16]),
.seg_led(digital_1)
);
seg_display #(
.seg_dig(seg_dig),
.seg_dp(seg_dp[0])
) seg_2(
.clk(clk),
.rst_n(rst_n),
.seg_data(bcd_code[15:12]),
.seg_led(digital_2)
);
assign oled_display_digital = bcd_code[19:12];
endmodule
3.3 adc_driver2seg顶层模块
顶层模块将前述ADC驱动与数码管显示模块组合即可。为将8位数字量在8个LED上显示,额外增加了一个8位leds
信号,对ADC转换后的数据取反后输出即可(1灭0亮)。
module adc_driver2seg #(
parameter ADC_WIDTH = 8,
parameter seg_dig = 1'b0,
parameter seg_dp = 2'b10
)(
input sys_clk,
input rst_n,
input sdo,
output sclk,
output adc_csn,
output [ADC_WIDTH-1:0] leds,
output [7:0] oled_display_digital,
output [8:0] digital_1,
output [8:0] digital_2
);
wire [ADC_WIDTH-1:0] adc_data;
assign leds = ~adc_data;
adc_driver #(
.ADC_WIDTH(ADC_WIDTH)
) adc_driver(
.sys_clk(sys_clk),
.rst_n(rst_n),
.sdo(sdo),
.sclk(sclk),
.adc_csn(adc_csn),
.adc_data(adc_data)
);
adc2seg #(
.ADC_WIDTH(ADC_WIDTH),
.seg_dig(seg_dig),
.seg_dp(seg_dp)
) segment_displayer(
.clk(sys_clk),
.rst_n(rst_n),
.adc_data(adc_data),
.oled_display_digital(oled_display_digital),
.digital_1(digital_1),
.digital_2(digital_2)
);
endmodule
3.4 OLED驱动模块
控制OLED需要5个信号:
- oled_csn:使能,低有效;
- oled_rst:复位,低有效;
- oled_dcn:数据/指令控制,高为输入数据,低为输入指令;
- oled_clk;
- oled_data:数据/指令输入。
参考电子森林:OLED驱动说明及Verilog代码实例的OLED驱动模块并作出改进。首先将整个模块拆分为OLED驱动、命令RAM(oled_cmd_RAM,存放初始化命令,准确的说应该是ROM)、字库RAM(oled_char_RAM,存放显示的字符编码)。
oled_cmd_RAM如下:
module oled_cmd_RAM #(
parameter RAM_WIDTH = 8,
parameter RAM_DEPTH = 32,
parameter ADDR_WIDTH = 5
)(
input clk,
input rst_n,
input re,
input [ADDR_WIDTH-1:0] addr,
output reg [RAM_WIDTH-1:0] data
);
reg [RAM_WIDTH-1:0] Mem[RAM_DEPTH-1:0];
always @(posedge rst_n) begin
Mem[ 0] = 8'hae; // 关闭屏幕
Mem[ 1] = 8'h81;
Mem[ 2] = 8'hff; // 设置对比度:256级
Mem[ 3] = 8'ha6; // 设置显示模式为1亮0灭
Mem[ 4] = 8'h20;
Mem[ 5] = 8'h02; // 设置页寻址模式
Mem[ 6] = 8'h00; // 设置起始列地址低位
Mem[ 7] = 8'h10; // 设置起始列地址高位
Mem[ 8] = 8'h40; // 设置GDDRAM起始行: (01)xxxxxx -> 40为第0行
Mem[ 9] = 8'ha1; // 设置COL0映射到SEG0*
Mem[10] = 8'hc8; // 设置COM扫描方向:从COM0至COM[N-1]*
Mem[11] = 8'ha8;
Mem[12] = 8'h1F; // 设置复用率:即选通的COM行数为1F*
Mem[13] = 8'hd3;
Mem[14] = 8'h00; // 设置垂直显示偏移:00~3F
Mem[15] = 8'hd5;
Mem[16] = 8'h80; // 设置显示时钟分频数和fosc
Mem[17] = 8'hd9;
Mem[18] = 8'h1f; // 设置预充电周期
Mem[19] = 8'hda;
Mem[20] = 8'h02; // 设置COM硬件配置:禁止左右反置、使用序列COM引脚配置*
Mem[21] = 8'hdb;
Mem[22] = 8'h40; // 设置VCOMH输出的高电平
Mem[23] = 8'h8d;
Mem[24] = 8'ha4; // 设置显示模式为正常模式,此时屏幕输出GDDRAM中的显示数据
Mem[25] = 8'haf; // 开启屏幕
end
always @(posedge clk) begin
if(!re)
data <= Mem[addr];
else
data <= 8'b0;
end
endmodule
oled_char_RAM如下:
module oled_char_RAM #(
parameter RAM_WIDTH = 40,
parameter RAM_DEPTH = 256,
parameter ADDR_WIDTH = 8
)(
input clk,
input rst_n,
input re,
input [ADDR_WIDTH-1:0] addr,
output reg [RAM_WIDTH-1:0] data
);
reg [RAM_WIDTH-1:0] Mem[RAM_DEPTH-1:0];
always @(posedge rst_n) begin
Mem[ 0] = {8'h3E, 8'h51, 8'h49, 8'h45, 8'h3E}; // 48 0
Mem[ 1] = {8'h00, 8'h42, 8'h7F, 8'h40, 8'h00}; // 49 1
Mem[ 2] = {8'h42, 8'h61, 8'h51, 8'h49, 8'h46}; // 50 2
Mem[ 3] = {8'h21, 8'h41, 8'h45, 8'h4B, 8'h31}; // 51 3
Mem[ 4] = {8'h18, 8'h14, 8'h12, 8'h7F, 8'h10}; // 52 4
Mem[ 5] = {8'h27, 8'h45, 8'h45, 8'h45, 8'h39}; // 53 5
Mem[ 6] = {8'h3C, 8'h4A, 8'h49, 8'h49, 8'h30}; // 54 6
Mem[ 7] = {8'h01, 8'h71, 8'h09, 8'h05, 8'h03}; // 55 7
Mem[ 8] = {8'h36, 8'h49, 8'h49, 8'h49, 8'h36}; // 56 8
Mem[ 9] = {8'h06, 8'h49, 8'h49, 8'h29, 8'h1E}; // 57 9
Mem[ 10] = {8'h7C, 8'h12, 8'h11, 8'h12, 8'h7C}; // 65 A
Mem[ 11] = {8'h7F, 8'h49, 8'h49, 8'h49, 8'h36}; // 66 B
Mem[ 12] = {8'h3E, 8'h41, 8'h41, 8'h41, 8'h22}; // 67 C
Mem[ 13] = {8'h7F, 8'h41, 8'h41, 8'h22, 8'h1C}; // 68 D
Mem[ 14] = {8'h7F, 8'h49, 8'h49, 8'h49, 8'h41}; // 69 E
Mem[ 15] = {8'h7F, 8'h09, 8'h09, 8'h09, 8'h01}; // 70 F
Mem[ 32] = {8'h00, 8'h00, 8'h00, 8'h00, 8'h00}; // 32 sp
Mem[ 33] = {8'h00, 8'h00, 8'h2f, 8'h00, 8'h00}; // 33 !
Mem[ 34] = {8'h00, 8'h07, 8'h00, 8'h07, 8'h00}; // 34
Mem[ 35] = {8'h14, 8'h7f, 8'h14, 8'h7f, 8'h14}; // 35 #
Mem[ 36] = {8'h24, 8'h2a, 8'h7f, 8'h2a, 8'h12}; // 36 $
Mem[ 37] = {8'h62, 8'h64, 8'h08, 8'h13, 8'h23}; // 37 %
Mem[ 38] = {8'h36, 8'h49, 8'h55, 8'h22, 8'h50}; // 38 &
Mem[ 39] = {8'h00, 8'h05, 8'h03, 8'h00, 8'h00}; // 39 '
Mem[ 40] = {8'h00, 8'h1c, 8'h22, 8'h41, 8'h00}; // 40 (
Mem[ 41] = {8'h00, 8'h41, 8'h22, 8'h1c, 8'h00}; // 41 )
Mem[ 42] = {8'h14, 8'h08, 8'h3E, 8'h08, 8'h14}; // 42 *
Mem[ 43] = {8'h08, 8'h08, 8'h3E, 8'h08, 8'h08}; // 43 +
Mem[ 44] = {8'h00, 8'h00, 8'hA0, 8'h60, 8'h00}; // 44 ,
Mem[ 45] = {8'h08, 8'h08, 8'h08, 8'h08, 8'h08}; // 45 -
Mem[ 46] = {8'h00, 8'h60, 8'h60, 8'h00, 8'h00}; // 46 .
Mem[ 47] = {8'h20, 8'h10, 8'h08, 8'h04, 8'h02}; // 47 /
Mem[ 48] = {8'h3E, 8'h51, 8'h49, 8'h45, 8'h3E}; // 48 0
Mem[ 49] = {8'h00, 8'h42, 8'h7F, 8'h40, 8'h00}; // 49 1
Mem[ 50] = {8'h42, 8'h61, 8'h51, 8'h49, 8'h46}; // 50 2
Mem[ 51] = {8'h21, 8'h41, 8'h45, 8'h4B, 8'h31}; // 51 3
Mem[ 52] = {8'h18, 8'h14, 8'h12, 8'h7F, 8'h10}; // 52 4
Mem[ 53] = {8'h27, 8'h45, 8'h45, 8'h45, 8'h39}; // 53 5
Mem[ 54] = {8'h3C, 8'h4A, 8'h49, 8'h49, 8'h30}; // 54 6
Mem[ 55] = {8'h01, 8'h71, 8'h09, 8'h05, 8'h03}; // 55 7
Mem[ 56] = {8'h36, 8'h49, 8'h49, 8'h49, 8'h36}; // 56 8
Mem[ 57] = {8'h06, 8'h49, 8'h49, 8'h29, 8'h1E}; // 57 9
Mem[ 58] = {8'h00, 8'h36, 8'h36, 8'h00, 8'h00}; // 58 :
Mem[ 59] = {8'h00, 8'h56, 8'h36, 8'h00, 8'h00}; // 59 ;
Mem[ 60] = {8'h08, 8'h14, 8'h22, 8'h41, 8'h00}; // 60 <
Mem[ 61] = {8'h14, 8'h14, 8'h14, 8'h14, 8'h14}; // 61 =
Mem[ 62] = {8'h00, 8'h41, 8'h22, 8'h14, 8'h08}; // 62 >
Mem[ 63] = {8'h02, 8'h01, 8'h51, 8'h09, 8'h06}; // 63 ?
Mem[ 64] = {8'h32, 8'h49, 8'h59, 8'h51, 8'h3E}; // 64 @
Mem[ 65] = {8'h7C, 8'h12, 8'h11, 8'h12, 8'h7C}; // 65 A
Mem[ 66] = {8'h7F, 8'h49, 8'h49, 8'h49, 8'h36}; // 66 B
Mem[ 67] = {8'h3E, 8'h41, 8'h41, 8'h41, 8'h22}; // 67 C
Mem[ 68] = {8'h7F, 8'h41, 8'h41, 8'h22, 8'h1C}; // 68 D
Mem[ 69] = {8'h7F, 8'h49, 8'h49, 8'h49, 8'h41}; // 69 E
Mem[ 70] = {8'h7F, 8'h09, 8'h09, 8'h09, 8'h01}; // 70 F
Mem[ 71] = {8'h3E, 8'h41, 8'h49, 8'h49, 8'h7A}; // 71 G
Mem[ 72] = {8'h7F, 8'h08, 8'h08, 8'h08, 8'h7F}; // 72 H
Mem[ 73] = {8'h00, 8'h41, 8'h7F, 8'h41, 8'h00}; // 73 I
Mem[ 74] = {8'h20, 8'h40, 8'h41, 8'h3F, 8'h01}; // 74 J
Mem[ 75] = {8'h7F, 8'h08, 8'h14, 8'h22, 8'h41}; // 75 K
Mem[ 76] = {8'h7F, 8'h40, 8'h40, 8'h40, 8'h40}; // 76 L
Mem[ 77] = {8'h7F, 8'h02, 8'h0C, 8'h02, 8'h7F}; // 77 M
Mem[ 78] = {8'h7F, 8'h04, 8'h08, 8'h10, 8'h7F}; // 78 N
Mem[ 79] = {8'h3E, 8'h41, 8'h41, 8'h41, 8'h3E}; // 79 O
Mem[ 80] = {8'h7F, 8'h09, 8'h09, 8'h09, 8'h06}; // 80 P
Mem[ 81] = {8'h3E, 8'h41, 8'h51, 8'h21, 8'h5E}; // 81 Q
Mem[ 82] = {8'h7F, 8'h09, 8'h19, 8'h29, 8'h46}; // 82 R
Mem[ 83] = {8'h46, 8'h49, 8'h49, 8'h49, 8'h31}; // 83 S
Mem[ 84] = {8'h01, 8'h01, 8'h7F, 8'h01, 8'h01}; // 84 T
Mem[ 85] = {8'h3F, 8'h40, 8'h40, 8'h40, 8'h3F}; // 85 U
Mem[ 86] = {8'h1F, 8'h20, 8'h40, 8'h20, 8'h1F}; // 86 V
Mem[ 87] = {8'h3F, 8'h40, 8'h38, 8'h40, 8'h3F}; // 87 W
Mem[ 88] = {8'h63, 8'h14, 8'h08, 8'h14, 8'h63}; // 88 X
Mem[ 89] = {8'h07, 8'h08, 8'h70, 8'h08, 8'h07}; // 89 Y
Mem[ 90] = {8'h61, 8'h51, 8'h49, 8'h45, 8'h43}; // 90 Z
Mem[ 91] = {8'h00, 8'h7F, 8'h41, 8'h41, 8'h00}; // 91 [
Mem[ 92] = {8'h55, 8'h2A, 8'h55, 8'h2A, 8'h55}; // 92 .
Mem[ 93] = {8'h00, 8'h41, 8'h41, 8'h7F, 8'h00}; // 93 ]
Mem[ 94] = {8'h04, 8'h02, 8'h01, 8'h02, 8'h04}; // 94 ^
Mem[ 95] = {8'h40, 8'h40, 8'h40, 8'h40, 8'h40}; // 95 _
Mem[ 96] = {8'h00, 8'h01, 8'h02, 8'h04, 8'h00}; // 96 '
Mem[ 97] = {8'h20, 8'h54, 8'h54, 8'h54, 8'h78}; // 97 a
Mem[ 98] = {8'h7F, 8'h48, 8'h44, 8'h44, 8'h38}; // 98 b
Mem[ 99] = {8'h38, 8'h44, 8'h44, 8'h44, 8'h20}; // 99 c
Mem[100] = {8'h38, 8'h44, 8'h44, 8'h48, 8'h7F}; // 100 d
Mem[101] = {8'h38, 8'h54, 8'h54, 8'h54, 8'h18}; // 101 e
Mem[102] = {8'h08, 8'h7E, 8'h09, 8'h01, 8'h02}; // 102 f
Mem[103] = {8'h18, 8'hA4, 8'hA4, 8'hA4, 8'h7C}; // 103 g
Mem[104] = {8'h7F, 8'h08, 8'h04, 8'h04, 8'h78}; // 104 h
Mem[105] = {8'h00, 8'h44, 8'h7D, 8'h40, 8'h00}; // 105 i
Mem[106] = {8'h40, 8'h80, 8'h84, 8'h7D, 8'h00}; // 106 j
Mem[107] = {8'h7F, 8'h10, 8'h28, 8'h44, 8'h00}; // 107 k
Mem[108] = {8'h00, 8'h41, 8'h7F, 8'h40, 8'h00}; // 108 l
Mem[109] = {8'h7C, 8'h04, 8'h18, 8'h04, 8'h78}; // 109 m
Mem[110] = {8'h7C, 8'h08, 8'h04, 8'h04, 8'h78}; // 110 n
Mem[111] = {8'h38, 8'h44, 8'h44, 8'h44, 8'h38}; // 111 o
Mem[112] = {8'hFC, 8'h24, 8'h24, 8'h24, 8'h18}; // 112 p
Mem[113] = {8'h18, 8'h24, 8'h24, 8'h18, 8'hFC}; // 113 q
Mem[114] = {8'h7C, 8'h08, 8'h04, 8'h04, 8'h08}; // 114 r
Mem[115] = {8'h48, 8'h54, 8'h54, 8'h54, 8'h20}; // 115 s
Mem[116] = {8'h04, 8'h3F, 8'h44, 8'h40, 8'h20}; // 116 t
Mem[117] = {8'h3C, 8'h40, 8'h40, 8'h20, 8'h7C}; // 117 u
Mem[118] = {8'h1C, 8'h20, 8'h40, 8'h20, 8'h1C}; // 118 v
Mem[119] = {8'h3C, 8'h40, 8'h30, 8'h40, 8'h3C}; // 119 w
Mem[120] = {8'h44, 8'h28, 8'h10, 8'h28, 8'h44}; // 120 x
Mem[121] = {8'h1C, 8'hA0, 8'hA0, 8'hA0, 8'h7C}; // 121 y
Mem[122] = {8'h44, 8'h64, 8'h54, 8'h4C, 8'h44}; // 122 z
end
always @(posedge clk) begin
if(re)
data <= Mem[addr];
else
data <= 40'b0;
end
endmodule
之后,oled_driver_adc顶层模块则参考原程序的FSM,并修改要显示的字符串及数据(包括出现的位置):
module oled_driver_adc #(
parameter CMD_WIDTH = 8, // LCD命令宽度
parameter CMD_DEPTH = 5'd25, // LCD初始化的命令的数量
parameter CHAR_WIDTH = 40, // 一个文字的数据宽度
parameter CHAR_DEPTH = 7'd123 // 文字库数量
)(
input sys_clk,
input rst_n,
input [7:0] oled_display_digital, // 两位ADC数据(一位小数)
output reg oled_csn, //OLCD液晶屏使能
output reg oled_rst, //OLCD液晶屏复位
output reg oled_dcn, //OLCD数据指令控制
output reg oled_clk, //OLCD时钟信号
output reg oled_data //OLCD数据信号
);
localparam IDLE = 3'b0, MAIN = 3'b1, INIT = 3'b10;
localparam SCAN = 3'b11, WRITE = 3'b100, DELAY = 3'b101;
localparam HIGH = 1'b1, LOW = 1'b0;
localparam DATA = 1'b1, CMD = 1'b0;
wire [CMD_WIDTH-1:0] cmd_out; // cmd_RAM输出的8位命令
wire [CHAR_WIDTH-1:0] char_out; // data_RAM输出的40位文字
reg [7:0] wr_reg;
reg [7:0] ypage, xpage_high, xpage_low; // 位置
reg [(8*21-1):0] char; // 字符串
reg [4:0] char_num; // 文字个数 最多16
reg [4:0] cmd_addr;
reg [7:0] char_addr;
reg [2:0] cnt_main;
reg [2:0] cnt_init;
reg [3:0] cnt_scan;
reg [4:0] cnt_write;
reg [14:0]num_delay, cnt_delay;
reg [2:0] state, state_last;
oled_cmd_RAM #(
.RAM_WIDTH(CMD_WIDTH),
.RAM_DEPTH(CMD_DEPTH),
.ADDR_WIDTH(5)
) CMD_RAM(
.clk(sys_clk),
.rst_n(rst_n),
.re(oled_dcn),
.addr(cmd_addr),
.data(cmd_out)
);
oled_char_RAM #(
.RAM_WIDTH(CHAR_WIDTH),
.RAM_DEPTH(CHAR_DEPTH),
.ADDR_WIDTH(8)
) CHAR_RAM(
.clk(sys_clk),
.rst_n(rst_n),
.re(oled_dcn),
.addr(char_addr),
.data(char_out)
);
always @(posedge sys_clk or negedge rst_n) begin
if(!rst_n) begin
cnt_main <= 1'b0;
cnt_init <= 1'b0;
cnt_scan <= 1'b0;
cnt_write <= 1'b0;
wr_reg <= 1'b0;
ypage <= 1'b0;
xpage_high <= 1'b0;
xpage_low <= 1'b0;
char <= 1'b0;
char_num <= 1'b0;
cmd_addr <= 1'b0;
char_addr <= 1'b0;
num_delay <= 15'd5;
cnt_delay <= 1'b0;
oled_csn <= HIGH;
oled_rst <= HIGH;
oled_dcn <= CMD;
oled_clk <= HIGH;
oled_data <= LOW;
state <= IDLE;
state_last <= IDLE;
end
else begin
case(state)
IDLE: begin
cnt_main <= 1'b0;
cnt_init <= 1'b0;
cnt_scan <= 1'b0;
cnt_write <= 1'b0;
wr_reg <= 1'b0;
ypage <= 1'b0;
xpage_high <= 1'b0;
xpage_low <= 1'b0;
char <= 1'b0;
char_num <= 1'b0;
cmd_addr <= 1'b0;
char_addr <= 1'b0;
num_delay <= 15'd5;
cnt_delay <= 1'b0;
oled_csn <= HIGH;
oled_rst <= HIGH;
oled_dcn <= CMD;
oled_clk <= HIGH;
oled_data <= LOW;
state <= MAIN;
state_last <= MAIN;
end
MAIN: begin
if(cnt_main >= 3'd6)
cnt_main <= 3'd5;
else
cnt_main <= cnt_main + 1'b1;
case(cnt_main) //MAIN状态
3'd0: begin state <= INIT; end
3'd1: begin ypage <= 8'hb0; xpage_high <= 8'h10; xpage_low <= 8'h00; char_num <= 5'd16; char <= "ADC DATA DISPLAY";state <= SCAN; end
3'd2: begin ypage <= 8'hb1; xpage_high <= 8'h10; xpage_low <= 8'h00; char_num <= 5'd16; char <= "VOLTAGE: . V ";state <= SCAN; end
3'd3: begin ypage <= 8'hb2; xpage_high <= 8'h10; xpage_low <= 8'h00; char_num <= 5'd16; char <= "AUTHOR: ZIRU PAN";state <= SCAN; end
3'd4: begin ypage <= 8'hb3; xpage_high <= 8'h10; xpage_low <= 8'h00; char_num <= 5'd16; char <= " ";state <= SCAN; end
3'd5: begin ypage <= 8'hb1; xpage_high <= 8'h15; xpage_low <= 8'h00; char_num <= 5'd1 ; char <= oled_display_digital[7:4]; state <= SCAN; end
3'd6: begin ypage <= 8'hb1; xpage_high <= 8'h16; xpage_low <= 8'h00; char_num <= 5'd1 ; char <= oled_display_digital[3:0]; state <= SCAN; end
default: state <= IDLE;
endcase
end
INIT: begin //初始化状态
case(cnt_init)
5'd0: begin
oled_rst <= LOW;
cnt_init <= cnt_init + 1'b1;
end //复位有效
5'd1: begin
num_delay <= 15'd25000;
state <= DELAY;
state_last <= INIT;
cnt_init <= cnt_init + 1'b1;
end //延时大于3us
5'd2: begin
oled_rst <= HIGH;
cnt_init <= cnt_init + 1'b1;
end //复位恢复
5'd3: begin
num_delay <= 15'd25000;
state <= DELAY;
state_last <= INIT;
cnt_init <= cnt_init + 1'b1;
end //延时大于220us
5'd4: begin
if(cmd_addr >= CMD_DEPTH) begin
cmd_addr <= 1'b0;
cnt_init <= cnt_init + 1'b1;
end
else begin
cmd_addr <= cmd_addr + 1'b1;
num_delay <= 15'd5;
oled_dcn <= CMD;
wr_reg <= cmd_out;
state <= WRITE;
state_last <= INIT;
end
end
5'd5: begin
cnt_init <= 1'b0;
state <= MAIN;
end
default: state <= IDLE;
endcase
end
SCAN: begin
if(cnt_scan == 4'd12) begin
if(char_num)
cnt_scan <= 4'd3;
else
cnt_scan <= cnt_scan + 1'b1;
end
else if(cnt_scan == 4'd13)
cnt_scan <= 4'd0;
else
cnt_scan <= cnt_scan + 1'b1;
case(cnt_scan)
4'd0: begin oled_dcn <= CMD; wr_reg <= ypage; state <= WRITE; state_last <= SCAN; end //定位列页地址
4'd1: begin oled_dcn <= CMD; wr_reg <= xpage_low; state <= WRITE; state_last <= SCAN; end //定位行地址低位
4'd2: begin oled_dcn <= CMD; wr_reg <= xpage_high; state <= WRITE; state_last <= SCAN; end //定位行地址高位
4'd3: begin char_num <= char_num - 1'b1; end
4'd4: begin char_addr <= char[(char_num*8)+:8]; end
4'd5: begin oled_dcn <= DATA; wr_reg <= 8'h00; state <= WRITE; state_last <= SCAN; end //5*8点阵变成8*8
4'd6: begin oled_dcn <= DATA; wr_reg <= 8'h00; state <= WRITE; state_last <= SCAN; end //5*8点阵变成8*8
4'd7: begin oled_dcn <= DATA; wr_reg <= 8'h00; state <= WRITE; state_last <= SCAN; end //5*8点阵变成8*8
4'd8: begin oled_dcn <= DATA; wr_reg <= char_out[39:32]; state <= WRITE; state_last <= SCAN; end
4'd9: begin oled_dcn <= DATA; wr_reg <= char_out[31:24]; state <= WRITE; state_last <= SCAN; end
4'd10:begin oled_dcn <= DATA; wr_reg <= char_out[23:16]; state <= WRITE; state_last <= SCAN; end
4'd11:begin oled_dcn <= DATA; wr_reg <= char_out[15:8] ; state <= WRITE; state_last <= SCAN; end
4'd12:begin oled_dcn <= DATA; wr_reg <= char_out[7:0] ; state <= WRITE; state_last <= SCAN; end
4'd13:begin state <= MAIN; end
default: state <= IDLE;
endcase
end
WRITE: begin //WRITE状态,将数据按照SPI时序发送给屏幕
if(cnt_write >= 5'd17)
cnt_write <= 5'd0;
else
cnt_write <= cnt_write + 1'b1;
case(cnt_write)
5'd0: begin oled_csn <= LOW; end //9位数据最高位为命令数据控制位
5'd1: begin oled_clk <= LOW; oled_data <= wr_reg[7]; end //先发高位数据
5'd2: begin oled_clk <= HIGH; end
5'd3: begin oled_clk <= LOW; oled_data <= wr_reg[6]; end
5'd4: begin oled_clk <= HIGH; end
5'd5: begin oled_clk <= LOW; oled_data <= wr_reg[5]; end
5'd6: begin oled_clk <= HIGH; end
5'd7: begin oled_clk <= LOW; oled_data <= wr_reg[4]; end
5'd8: begin oled_clk <= HIGH; end
5'd9: begin oled_clk <= LOW; oled_data <= wr_reg[3]; end
5'd10:begin oled_clk <= HIGH; end
5'd11:begin oled_clk <= LOW; oled_data <= wr_reg[2]; end
5'd12:begin oled_clk <= HIGH; end
5'd13:begin oled_clk <= LOW; oled_data <= wr_reg[1]; end
5'd14:begin oled_clk <= HIGH; end
5'd15:begin oled_clk <= LOW; oled_data <= wr_reg[0]; end //后发低位数据
5'd16:begin oled_clk <= HIGH; end
5'd17:begin oled_csn <= HIGH; state <= DELAY; end
default: state <= IDLE;
endcase
end
DELAY: begin
if(cnt_delay >= num_delay) begin
cnt_delay <= 15'd0;
state <= state_last;
end
else
cnt_delay <= cnt_delay + 1'b1;
end
default: state <= IDLE;
endcase
end
end
endmodule
3.5 项目顶层模块proj_top
顶层模块将上述adc_driver2seg
模块、oled_driver_adc驱动组合,预留了7个参数。代码如下:
module proj_top #(
parameter ADC_WIDTH = 8,
parameter seg_dig = 1'b0,
parameter seg_dp = 2'b10,
parameter CMD_WIDTH = 8, // LCD命令宽度
parameter CMD_DEPTH = 5'd26, // LCD初始化的命令的数量
parameter CHAR_WIDTH = 40, // 一个文字的数据宽度
parameter CHAR_DEPTH = 7'd123 // 文字库数量
)(
input sys_clk,
input rst_n,
input sdo,
input sw,
output sclk,
output adc_csn,
output [ADC_WIDTH-1:0] leds, // LED灯
output [8:0] digital_1, // 数码管个位
output [8:0] digital_2, // 数码管小数位
output oled_csn, //OLCD液晶屏使能
output oled_rst, //OLCD液晶屏复位
output oled_dcn, //OLCD数据指令控制
output oled_clk, //OLCD时钟信号
output oled_data, //OLCD数据信号
output [2:0] RGB_led_1, //RGB三色灯
output [2:0] RGB_led_2 //RGB三色灯
);
wire [7:0] oled_display_digital;
assign RGB_led_1 = {3{~sw}}; //RGB三色灯灭
assign RGB_led_2 = {3{~sw}}; //RGB三色灯灭
adc_driver2seg #(
.ADC_WIDTH(ADC_WIDTH),
.seg_dig(seg_dig),
.seg_dp(seg_dp)
) adc_driver2seg(
.sys_clk(sys_clk),
.rst_n(rst_n),
.sdo(sdo),
.sclk(sclk),
.adc_csn(adc_csn),
.leds(leds),
.oled_display_digital(oled_display_digital),
.digital_1(digital_1),
.digital_2(digital_2)
);
oled_driver_adc #(
.CMD_WIDTH(CMD_WIDTH),
.CMD_DEPTH(CMD_DEPTH),
.CHAR_WIDTH(CHAR_WIDTH),
.CHAR_DEPTH(CHAR_DEPTH)
) oled_driver_adc(
.sys_clk(sys_clk),
.rst_n(rst_n),
.oled_display_digital(oled_display_digital),
.oled_csn(oled_csn),
.oled_rst(oled_rst),
.oled_dcn(oled_dcn),
.oled_clk(oled_clk),
.oled_data(oled_data)
);
endmodule
注:不知为何,不对三色RGB灯进行任何连接在实际调试中两个灯却常亮,因此代码中改为用一个常闭开关控制两个三色RGB灯熄灭。
四、资源使用情况
五、项目总结
- 所要求的功能均能正常实现;整个项目工程按功能模块编写,清晰明了;
- 不仅完成了项目的所有要求,还实现了将电压值显示在数码管及8个LED灯上;
- 经过这次项目的锻炼,我对通过FPGA驱动ADC这种外设有了深入了解,对于OLED屏幕(及SSD1306)的驱动有了较为细致的分析和研究,这对于我的FPGA开发十分有益。
六、更多说明
- 详细的项目说明参见基于Lattice XO2-4000HC FPGA核心板及电子森林综合训练底板的ADC数字电压表及OLED显示设计(Verilog);
- 关于本项目中ADC驱动模块的说明参见基于Lattice XO2-4000HC FPGA核心板ADS7868驱动模块及波形分析(Verilog);
- 关于本项目中OLED驱动模块及工作方式(结合代码)参见基于Lattice XO2-4000HC FPGA核心板的SSD1306 OLED12832驱动芯片指令及工作方式详述(Verilog);
- 源代码及.JED文件参见附件压缩包,包括一份文件说明README;
- 作者的CSDN主页,欢迎访问。