本项目基于Lattice XO2-4000HC核心板,并使用基于小脚丫FPGA的综合技能训练平台,使用Verilog编程,Lattice Diamond 3.10开发环境,实现了如下功能:
⚠️ 注意:
该项目将记录在作者的GitHub上,待补充。
综合训练底板及其与GPIO引脚的电路原理图如下图所示,详细电路图参见STEP-MXO2V2.2原理图。
adc_driver2seg.v
文件下的 adc_driver2seg
模块
adc_driver.v
文件下的 adc_driver
模块
根据基于Lattice的XO2-4000HC FPGA的核心模块的综合训练底板原理图及硬件手册的电路图,底板自带一串行ADC(ADS7868),通过可调电阻获得0~3.3V电压,其ADC输出端(ADC_SDO)、时钟(ADC_CLK)及片选(ADC_CS#,低电平有效)与FPGA的IO口连接,因此需要ADC驱动模块完成对ADC数据的采样。
参考TI:ADS7868数据手册,其各引脚功能如下图所示。
简要说明一下SDO端口,该端口每在SCLK时钟下降沿输出转换结果,从MSB至LSB,对于ADS7868其分辨率为8位(即8位数字量);当片选信号拉低后会产生连续4个0,在转换结束后SDO端口呈变为高阻态(Hi-Z)。
着重关注ADS7868的时序图:
两种驱动模块代码及时序波形的分析比较参见《基于Lattice XO2-4000HC FPGA核心板ADS7868驱动模块及波形分析(Verilog)》,在此仅给出状态机形式的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
adc2seg.v
文件下的 adc2seg
模块
adc2seg
模块由bin2bcd
及两个seg_display
子模块组成,顶层模块adc2seg
在本章最后一小节详述。
数码管显示模块负责将ADC转换后的8位数字量以一位小数的形式(例如0.5V、3.3V等)显示在数码管上。因此,需要将ADC量化后的数字量( N N N)与模拟量( V i n V_{in} Vin)间做换算:
由于
N = 256 × V i n V r e f V i n = N × V r e f 256 , V r e f = 3.3 V N = 256 \times \frac{V_{in}}{V_{ref}}\\ V_{in}=N\times \frac{V_{ref}}{256},\quad V_{ref}=3.3V N=256×VrefVinVin=N×256Vref,Vref=3.3V
因此有
V i n = N × 0.0129 V_{in}=N\times0.0129 Vin=N×0.0129
所以需要将转换后的数字量*0.0129(此时计算得到的结果在FPGA中仍为二进制),并先将结果转换成BCD码,然后以个位+一位小数形式显示在数码管上。程序实现上,先将数字量乘以129(小数点右移4位),最后的BCD码结果舍弃后4位(小数点左移4位)即可。
adc2seg.v
文件下的 bin2bcd
模块
将二进制数转换成BCD码的流程可参考CSDN:FPGA Verilog实现二进制转BCD码,采用左移加三的算法(以243为例),计算流程如下图所示:
参考电子森林:简易电压表设计的代码并做了改进,将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]);
// 上述五句与下方注释代码等价,若采用下方代码,可注释掉function
// if (shift_reg[19:16] >= 5)
// shift_reg[19:16] = shift_reg[19:16] + 2'b11;
// if (shift_reg[23:20] >= 5)
// shift_reg[23:20] = shift_reg[23:20] + 2'b11;
// if (shift_reg[27:24] >= 5)
// shift_reg[27:24] = shift_reg[27:24] + 2'b11;
// if (shift_reg[31:28] >= 5)
// shift_reg[31:28] = shift_reg[31:28] + 2'b11;
// if (shift_reg[35:32] >= 5)
// shift_reg[35:32] = shift_reg[35:32] + 2'b11;
shift_reg = shift_reg << 1;
end
bcd_code = shift_reg[35:16];
end
end
endmodule
开始的 bin_code = adc_data * 16'd129
将ADC数字量与129相乘(小数点右移4位),最后转换的BCD码结果bcd_code = shift_reg[35:16]
舍弃了低16位,即小数点左移4位。
adc2seg.v
文件下的 seg_display
模块
XO2-4000HC的数码管局部电路如下图所示,控制一位数码管需要9个信号,从高至低分别为{DIG、DP、G、F、E、D、C、B、A}
,其中DIG
控制共阴极/共阳极(DIG = 1'b0
为共阴极,反之共阳极),DP
控制小数点,其余信号参见下图。
参考电子森林:数码管显示代码并做了改进,为提高通用性,将共阴/阳极与小数点亮灭的控制写入模块的参数(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
将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
顶层模块将前述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
oled_driver_adc.v
文件下的 oled_driver_adc
模块
控制OLED需要5个信号:
参考电子森林:OLED驱动说明及Verilog代码实例的OLED驱动模块并作出改进。首先将整个模块拆分为OLED驱动、命令RAM(oled_cmd_RAM,存放初始化命令,准确的说应该是ROM)、字库RAM(oled_char_RAM,存放显示的字符编码)。
SSD1306芯片手册参见百度云:SSD1306 英文手册,提取码csdn(由于CSDN存在相同资源但是不免费)与百度文库:SSD1306 中文手册。
oled_cmd_RAM.v
文件下的 oled_cmd_RAM
模块
类似一个只读ROM,根据输入的addr
读取相应的指令编码,有关SSD1306指令解释参见CSDN:SSD1306 OLED驱动芯片 详细介绍及芯片手册。
此外,可参考OLED显示模块驱动原理及应用一文,该文对SS1306的工作原理和指令进行了说明。
该模块代码如下:
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.v
文件下的 oled_char_RAM
模块
类似一个只读ROM,根据输入的addr
(字符与地址ASCII码对应)读取相应的字符编码(5*8大小),该模块代码如下:
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
注:字符编码写入方式另做文章讨论。
该顶层模块的主体FSM与原参考代码相同,在调用OLED的指令编码与字符编码时存在差异。代码如下:
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
OLED驱动状态(state
)分为IDLE
、MAIN
、INIT
、SCAN
、WRITE
、DELAY
,进入MAIN
后跳转至INIT
完成复位、指令加载(WRITE
中完成写入)等初始化操作,根据state_last
跳回MAIN
,再在MAIN
中获取要显示的字符串,并在SCAN
中不断扫描所需要的字的编码,在WRITE
中写入至OLED中。
⚠️ 注意:
MAIN
中所要显示的字符串预留了两个数字显示的位置(“ . V”),第四行全为空格是由于作者实际调试中当未指定第四行显示内容后会出现杂点,但不排除可以通过初始化阶段指令等方式解决;INIT
中5’d4时cmd_addr <= cmd_addr + 1'b1; oled_dcn <= CMD; wr_reg <= cmd_out;
:由于cmd_addr
与oled_cmd_RAM
地址输入连接,cmd_out
与oled_cmd_RAM
指令输出连接,因此当oled_dcn <= CMD
时,在下个时钟上升沿时,cmd_out
完成指令的输出,并赋给wr_reg
,继而进入WRITE
写入至OLED;SCAN
负责完成对字符串中字的编码的读取:4’d3处char_num
记录所需写入字的数量,为一个倒数计数器;4’d4处char_addr
与字符的关系为ASCII码对应,因此倒着(字符串右侧开始)取一个八位即对应该字符在oled_char_RAM
的地址,char_out
与字符编码输出连接,由于每个字符由5列8位编码组成,因此将char_out
(40位)分5次经wr_reg
写入OLED中;proj_top.v
文件下的 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灯熄灭。
整体RTL如下图所示:
⚠️ 注意:顶层模块的输入、输出引脚与FPGA的GPIO连接需要根据小脚丫STEP FPGA Training V2.0硬件手册的管脚分配图,在Diamond软件中的“Spreadsheet View”进行分配,之后才可进行后续编译。
Diamond开发环境及使用参见Lattice Diamond使用案例。
由于作者的STEP-MXO2小脚丫采用STM32F072作为下载器的版本,下载方式是将生成的JED文件复制到U盘里,即可完成下载。详细介绍参见STEP-MXO2-C快速上手指南及STEP-MXO2-C。
将顶层项目的.JED文件下载至FPGA内,FPGA即刻开始运行。注意先将跳线帽连接左侧R_ADJ与J3端口。调节左侧电位器旋钮,可得到不同电压值。以下为调试效果:
注:显示的电压范围为0~3.2V。
[1] 基于小脚丫FPGA的综合技能训练平台
[2] TI:ADS7868数据手册.pdf
[3] 电子森林:简易电压表设计
[4] 电子森林:数码管显示
[5] 电子森林:OLED驱动说明及Verilog代码实例
[6] CSDN:SSD1306 OLED驱动芯片 详细介绍
[7] OLED显示模块驱动原理及应用
[8] 百度云:SSD1306 英文手册,提取码csdn(由于CSDN存在相同资源但是不免费);
[9] 百度文库:SSD1306 中文手册
[10] CSDN:FPGA Verilog实现二进制转BCD码
[11] Lattice Diamond使用案例
[12] STEP-MXO2-C快速上手指南
[13] STEP-MXO2-C
[14] STEP-MXO2V2.2原理图.pdf
[15] 基于Lattice的XO2-4000HC FPGA的核心模块的综合训练底板原理图及硬件手册.pdf
[16] 小脚丫STEP FPGA Training V2.0硬件手册.pdf
[17] 基于Lattice XO2-4000HC的ADC数字电压表及OLED显示设计