1. 前言
前言部分对全文无关紧要,可直接跳过。
距离1月9日发布的上一篇博客已经过去大约一个月的时间,期间旅游、过年以及各种事项接踵而至,并且更要命的是,矩阵4x4键盘不论如何调试,都会出问题。根据指导人所说的修改方案,即加上上拉电阻,问题依旧存在——状态机的跳转不如所愿,不该出现数据的信号出现了数据等——起初,我以为是我自己的问题,拼命找bug,代码反复修改多次,无疾而终,这便花去了大概五六天的时间;之后用梅雪松(梅哥)的代码加上SignalTap进行调试观察,出人意料的是,他的输出,比如多按几下按键S6,输出的数据会跳变到4或者7,再跳变成6,或者干脆不跳变了,直接是4或者7。比照他的代码,修改,润色,效果依然不理想。这里并不是说梅哥的代码有问题,我相信他的代码肯定是经过自己层层修改并验证过的,只是自己庶竭驽钝。看到配件箱中有个红外键盘,想了想,两者应该能达到同样的效果,并且在我看来,红外键盘比矩阵4x4键盘至少有如下两个好处:
- 拿来即用,且在一定范围内可以持续使用,省去接线等不必要的繁琐工作;
- 红外键盘按键有21个,比4x4的矩阵键盘多出5个按键,虽然说多出来的不一定能够用到,但是多一些总该是有点好处的;
出于对时间流逝的感到不安、焦虑,又出于想要尽快得到按键正确输出的目的,最终,决定弃用矩阵键盘,改用红外键盘。
这也是为什么上一篇博客分明是与矩阵键盘相关,而此篇博客又叙述红外键盘之内容的原因。
2. 键盘键值定义
虽说换了个按键终端,但是主题还是没有变——依旧是针对DDS的频率控制字fwd
以及相位控制字pwd
,进行设计。
比如,想让DDS输出的频率为1000Hz,那么在红外键盘上,分别按下1
、0
、0
、0
后,再按OK
键,即可得到相应的以1000Hz的频率输出的正弦波。
实际上我们用到的并不只是一个DDS,而是双通道的DDS,也就是说,一个DDS有两个输出通道,可以同时输出两个完全不相关的正弦波信号。可以想象成有两条路,一个车开得快,一个车开得慢这种。而每一个通道的输入都有两个控制字——频率控制字与相位控制字——故双通道就有四个控制字,分别为频率控制字fwd_a
、fwd_b
,相位控制字pwd_a
、pwd_b
。所以,除了输出相应的频率数据之外,还得对通道A和通道B进行选择。由于按下按键,FPGA开发板并不知道每个按键所对应的操作,这就需要对红外键盘每个按键的输出进行定义。
红外键盘外观如下:
根据《FPGA自学笔记——设计与验证》第5.4章节,编写出相应的RTL设计文件,上板调试并验证可得每个按键所对应的键值,如下表左边两侧所示。
进行状态划分之前,要先定义每个按键所对应的操作。设定如下:
键盘按键 | 按键操作 |
---|---|
EQ | OK |
CH | 设定相位控制字 |
CH- | 选定通道A 并设定频率控制字 |
CH+ | 选定通道B 并设定频率控制字 |
- | 回退 |
3 状态机的编写
3.1状态机的状态划分
考虑到按键使用的实际情况,将状态划分如下:
状态 | 符号 | 值 |
---|---|---|
空闲 | IDLE | 0 |
接收频率控制字 | RECEIVE_FWD | 1 |
过渡 | TRANSITION | 2 |
接收相位控制字 | RECEIVE_PWD | 3 |
读取数据 | READ | 4 |
产生标志信号 | FLAG | 5 |
每个状态的状态跳转条件如下:
在
IDLE
状态下,等待通道A或通道B设置按键,若有,则进入RECEIVE_FWD
状态;
若在此状态下按回退键,则依然保持IDLE
;
在
RECEIVE_FWD
状态下,
在按下数字按键之前,按下回退键,则说明按下通道A设置按键按错了,则回到IDLE
;
在按下数字按键之后,按下回退键,则说明想要清除上一步输入的数据;
等待各种数字按键,
若按下OK
键,说明不需要设置相位控制字,直接进入READ
状态;
若按下CH
键,则说明需要设置相位控制字,进入过渡状态TRANSITION
;
在
TARNSITION
状态下直接进入RECEIVE_PWD
状态;
在
RECEIVE_PWD
状态下,等待各种数字按键,当按下OK
后,说明频率控制字和相位控制字已设置完毕,进入READ
状态;
在
READ
状态下,读取数据,并进入FLAG
状态;
在
FLAG
状态,产生操作完成的out_flag
信号,并回到IDLE
状态。
其实产生标志信号的操作在READ
状态下就能够完成,此处为了理得更清楚,多加一个状态。
3.2 状态机所需的信号
状态机各种信号定义如下:
/*=============================================================================
# FileName: key_FSM.v
# Desc:
# Author: ohliver
# Email: ohliver@foxmail.com
# HomePage: https://blog.csdn.net/qq_15062763
# Version: 0.0.1
# LastChange: 2020-02-02 20:02:12
# History:
=============================================================================*/
module key_FSM(
//I
clk_50M ,
rstn ,
in_addr ,//这是红外键盘每个按键所对应的按键地址
in_data ,//这是红外键盘每个按键所对应的按键数据
in_flag ,//这是红外键盘解码完成后所发出的标志信号
//O
fwd_a ,
fwd_b ,
pwd_a ,
pwd_b ,
out_flag //这是状态机跳转完成发出的标志信号
);
input clk_50M ;
input rstn ;
input [15:0] in_addr ;
input [15:0] in_data ;
input in_flag ;
output reg [63:0] fwd_a ;
output reg [63:0] fwd_b ;
output reg [18:0] pwd_a ; //为什么是19位而不是32位
output reg [18:0] pwd_b ; //因为FPGA上没有那么多IO端口,所以必须少分配点。
output reg out_flag ;
parameter IDLE = 4'd0;
parameter RECEIVE_FWD = 4'd1;
parameter TRANSITION = 4'd2;
parameter RECEIVE_PWD = 4'd3;
parameter READ = 4'd4;
parameter FLAG = 4'd5;
reg [ 3:0] state ;
reg [ 3:0] n_state ;
reg channel_a ;
reg channel_b ;
reg in_flag_r ;
reg [ 3:0] key_value_tmp;
reg [31:0] key_value_fout;
reg [31:0] read_fout ;
reg [31:0] key_value_pout;
reg [31:0] read_pout ;
wire [31:0] key_data = {in_data, in_addr};
/*
说明:这里为什么需要将in_flag信号打一拍?
假设一个时钟周期为20ns(实际上也是20ns);
比如按下一次1键,则对应的key_data == 32'hF30CFF00; 信号key_one拉高
又假设我们按下一次按键需要10ms,而在这10ms内,key_data一直为F30CFF00,也就是说,信号key_one在10ms内一直拉高。
如果直接用key_one信号进行按键1的输入,则对于FPGA来说,它会认为我们按下了10ms/20ns = 500000次按键1,
而我们分明只是按了一次
所以,需要一个时钟周期的脉冲信号,对key_one的高电平进行限制,让我们按下一次按键1,FPGA也就只收到一次按键1.
很巧的是,按下按键1之后,红外键盘对应的解码模块在解码完成后,正好产生一个时钟周期的标志信号flag,在此文件中对应输入的信号 input in_flag;
同时,解码模块在产生标志信号flag的下一个时钟周期,才知道按键1被按下,也就是说,key_one是在flag(flag等同于in_flag)信号产生后的下一拍才会被拉高。
故需要将in_flag信号往后延迟一拍,变成in_flag_r信号,并和信号key_one进行逻辑与,产生出我们所需要的一个时钟周期的按键输入。
为了节省资源,将数字按键0~9,合起来一起与上in_flag_r,即为:
wire number = in_flag_r && (key_zero || key_one || key_two ||
key_three || key_four || key_five ||
key_six || key_seven|| key_eight||
key_nine);
*/
always @ (posedge clk_50M or negedge rstn) begin
if (!rstn)
in_flag_r <= 1'b0;
else
in_flag_r <= in_flag;
end
wire key_ch = in_flag_r && (key_data == 32'hB946FF00); //CH
wire key_ch_minus = in_flag_r && (key_data == 32'hBA45FF00); //CH-
wire key_ch_plus = in_flag_r && (key_data == 32'hB847FF00); //CH+
wire key_eq = in_flag_r && (key_data == 32'hF609FF00); //EQ
wire key_minus = in_flag_r && (key_data == 32'hF807FF00); //-
wire key_zero = (key_data == 32'hE916FF00);
wire key_one = (key_data == 32'hF30CFF00);
wire key_two = (key_data == 32'hE718FF00);
wire key_three = (key_data == 32'hA15EFF00);
wire key_four = (key_data == 32'hF708FF00);
wire key_five = (key_data == 32'hE31CFF00);
wire key_six = (key_data == 32'hA55AFF00);
wire key_seven = (key_data == 32'hBD42FF00);
wire key_eight = (key_data == 32'hAD52FF00);
wire key_nine = (key_data == 32'hB54AFF00);
wire number = in_flag_r && (key_zero || key_one || key_two ||
key_three || key_four || key_five ||
key_six || key_seven|| key_eight||
key_nine);
//对按键所代表的数值进行解码
always @ (*) begin
case (key_data)
32'hE916FF00: key_value_tmp = 4'd0;
32'hF30CFF00: key_value_tmp = 4'd1;
32'hE718FF00: key_value_tmp = 4'd2;
32'hA15EFF00: key_value_tmp = 4'd3;
32'hF708FF00: key_value_tmp = 4'd4;
32'hE31CFF00: key_value_tmp = 4'd5;
32'hA55AFF00: key_value_tmp = 4'd6;
32'hBD42FF00: key_value_tmp = 4'd7;
32'hAD52FF00: key_value_tmp = 4'd8;
32'hB54AFF00: key_value_tmp = 4'd9;
default : key_value_tmp = 4'd0;
endcase
end
wire fout_zero = (key_value_fout == 0); //定义key_value_fout=0
wire pout_zero = (key_value_pout == 0); //定义key_value_pout=0
/*
定义寄存器channel_a和channel_b的意义是
对通道A和通道B的fwd和pwd进行赋值
详见最后20行的代码
*/
always @ (posedge clk_50M or negedge rstn) begin
if (!rstn)
channel_a <= 1'b0;
else if (key_ch_minus)
channel_a <= 1'b1;
else if (state == IDLE)
channel_a <= 1'b0;
else
channel_a <= channel_a;
end
always @ (posedge clk_50M or negedge rstn) begin
if (!rstn)
channel_b <= 1'b0;
else if (key_ch_plus)
channel_b <= 1'b1;
else if (state == IDLE)
channel_b <= 1'b0;
else
channel_b <= channel_b;
end
3.3 状态转换
状态转换图如下:
三段式状态机编写如下:
//=================================================
//FSM part one
//=================================================
always @ (posedge clk_50M or negedge rstn) begin
if (!rstn)
state <= IDLE;
else
state <= n_state;
end
//==================================================
//FSM part two
//==================================================
always @ (*) begin
case (state)
IDLE:
if (key_ch_minus || key_ch_plus)
n_state = RECEIVE_FWD;
else if (key_minus)
n_state = IDLE;
else
n_state = IDLE;
RECEIVE_FWD:
if (key_minus && fout_zero)
n_state = IDLE;
else if (key_minus && (!fout_zero))
n_state = RECEIVE_FWD;
else if (key_eq)
n_state = READ;
else if (key_ch)
n_state = TRANSITION;
else
n_state = RECEIVE_FWD;
TRANSITION: n_state = RECEIVE_PWD;
RECEIVE_PWD:
if (key_minus && pout_zero)
n_state = TRANSITION;
else if (key_minus && (!pout_zero))
n_state = RECEIVE_PWD;
else if (key_eq)
n_state = READ;
else
n_state = RECEIVE_PWD;
READ: n_state = FLAG;
FLAG: n_state = IDLE;
default: n_state = IDLE;
endcase
end
//==================================================
//FSM part three
//==================================================
always @ (posedge clk_50M or negedge rstn) begin
if (!rstn) begin
key_value_fout <= 32'b0;
read_fout <= 32'b0;
key_value_pout <= 19'b0;
read_pout <= 19'b0;
out_flag <= 1'b0 ;
end
else begin
case (state)
IDLE:begin
key_value_fout <= 32'b0;
key_value_pout <= 19'b0;
out_flag <= 1'b0 ;
read_fout <= read_fout;
read_pout <= read_pout;
end
RECEIVE_FWD: begin
if (number)
key_value_fout <= (key_value_fout<<1) + (key_value_fout<<3) + key_value_tmp;
else if (key_minus && (!fout_zero))
key_value_fout <= (key_value_fout - key_value_tmp)/10;
else
key_value_fout <= key_value_fout;
end
TRANSITION: begin end
RECEIVE_PWD: begin
if (number)
key_value_pout <= (key_value_pout<<1) + (key_value_pout<<3) + key_value_tmp;
else if (key_minus && (!pout_zero))
key_value_pout <= (key_value_pout - key_value_tmp)/10;
else
key_value_pout <= key_value_pout;
end
READ: begin
read_fout <= key_value_fout;
read_pout <= key_value_pout;
end
FLAG: out_flag <= 1'b1;
default:begin
out_flag <= 1'b0;
key_value_fout <= 32'b0;
read_fout <= 32'b0;
key_value_pout <= 19'b0;
read_pout <= 19'b0;
end
endcase
end
end
说明:
- 对于
key_value_fout <= (key_value_fout<<1) + (key_value_fout<<3) + key_value_tmp;
语句,翻译成数学表达式即为:
key_value_fout = key_value_fout * 10 + key_value_tmp;
为了节省寄存器,将数字10
拆分成2+8
,而key_value_fout
乘2或乘8分别对应key_value_fout
左移一位和三位的操作,相比于使用乘法器,能够省下一些寄存器。 - 对于按下回退按键导致的除法操作:
key_value_fout <= (key_value_fout - key_value_tmp)/10;
本身在FPGA中很忌讳使用除法的,想到在硬件设计中,10 = 32/3
,故将上式转变为:
key_value_fout <= ((key_value_fout - key_value_tmp) * 3) >> 5;
但是在仿真的过程中发现,输入1000,回退一位,最终的结果是93,而非100,故勉为其难地用了除法。不过需要提醒的一点是,不使用除法,误差较大,优点是逻辑资源占用少,为641个;若使用除法,而且还是两处,虽然结果准确,但逻辑占用激增至1315个,两个除法所占用的逻辑数为674个,比我不使用除法写的代码所占用的逻辑资源还要多。
3.4 输出计算
//===========================================================
//caculate and output
//===========================================================
always @ (posedge clk_50M or negedge rstn) begin
if (!rstn) begin
fwd_a <= 64'b0 ;
fwd_b <= 64'b0 ;
pwd_a <= 19'b0 ;
pwd_b <= 19'b0 ;
end
else if (out_flag && channel_a) begin
fwd_a <= (1441151880*read_fout) >> 24 ; //(2882303761*read_fout)>>25
pwd_a <= read_pout ;
fwd_b <= fwd_b ;
pwd_b <= pwd_b ;
end
else if (out_flag && channel_b) begin
fwd_a <= fwd_a ;
pwd_a <= pwd_a ;
fwd_b <= (1441151880*read_fout) >> 24 ; //(2882303761*read_fout)>>25
pwd_b <= read_pout ;
end
else begin
fwd_a <= fwd_a ;
pwd_a <= pwd_a ;
fwd_b <= fwd_b ;
pwd_b <= pwd_b ;
end
end
endmodule
说明:
根据DDS中输出频率以及频率控制字的计算关系可得(详见《FPGA自学笔记——设计与验证》第294页):
F
o
u
t
=
B
∗
F
c
l
k
/
2
n
F_{out} = B*F_{clk}/2^n
Fout=B∗Fclk/2n
其中,n = 32,Fout是输出的频率值,B为频率控制字,在此verilog文件中,用fwd
表示。由于开发板使用的时钟频率为50MHz,故频率控制字B的计算方式为:
B
=
2
32
∗
F
o
u
t
/
F
c
l
k
=
2
32
/
50
,
000
,
000
∗
F
o
u
t
B = 2^{32} * F_{out} / F_{clk} = 2^{32}/50,000,000 * F_{out}
B=232∗Fout/Fclk=232/50,000,000∗Fout
即
B
=
85.89934592
∗
F
o
u
t
B= 85.89934592 * F_{out}
B=85.89934592∗Fout
而FPGA中是没有浮点小数运算的,也就是说,当输入85.89934592,FPGA会自动截断小数点,将其当做85进行计算,这样导致频率控制字的误差较大。采用如下方式能够避免精度不够的问题:
小数点之后的数89934592,通过计算可知,其二进制表示的数总共27位。
对89.89934592乘上227,既
89.89934592
∗
2
27
=
11529215046.06846976
89.89934592*2^{27} = 11529215046.06846976
89.89934592∗227=11529215046.06846976
保留整数11529215046,将频率控制字的计算方式变为
B
=
(
2
27
∗
85.89934592
∗
F
o
u
t
)
>
>
27
B= (2^{27}*85.89934592 * F_{out}) >> 27
B=(227∗85.89934592∗Fout)>>27
B
=
(
11529215046
∗
F
o
u
t
)
>
>
27
B= (11529215046* F_{out}) >> 27
B=(11529215046∗Fout)>>27
这样能很好地避免由于小数点的出现而造成的精度损失,不过缺点便是本来应该为32位的fwd
需要扩容成64位。另外,考虑到89.89934592 *227对应的整数,其二进制位数大于32位,故需要调整乘数因子,变为225;而89.89934592 * 227= 2882303761;同时
可见,该数对应的二进制正好是32位,且最高位为1,在Modelsim仿真中会报警告,即Modelsim会将这个二进制数当做有符号数处理,故再次改变乘数因子,调整为224。最终
B
=
(
2
24
∗
85.89934592
∗
F
o
u
t
)
>
>
24
B= (2^{24}*85.89934592 * F_{out}) >> 24
B=(224∗85.89934592∗Fout)>>24
即
B
=
(
1441151880
∗
F
o
u
t
)
>
>
24
B= (1441151880 * F_{out}) >> 24
B=(1441151880∗Fout)>>24
所以,对应的verilog代码为
fwd_a <= (1441151880*read_fout) >> 24 ;
fwd_b <= (1441151880*read_fout) >> 24 ;
4. testbench测试
/*=============================================================================
# FileName: key_FSM_tb.v
# Desc:
# Author: ohliver
# Email: ohliver@foxmail.com
# HomePage: https://blog.csdn.net/qq_15062763
# Version: 0.0.1
# LastChange: 2020-02-03 20:58:48
# History:
=============================================================================*/
`timescale 1ns/1ns
`define p 20
module key_FSM_tb;
reg clk_50M ;
reg rstn ;
reg [15:0] in_addr ;
reg [15:0] in_data ;
reg in_flag ;
wire [63:0] fwd_a ;
wire [63:0] fwd_b ;
wire [31:0] pwd_a ;
wire [31:0] pwd_b ;
wire out_flag;
initial clk_50M = 1'b1 ;
always #(`p/2) clk_50M = ~clk_50M ;
parameter CHANNEL_A = 5'd10; //button CH-
parameter CHANNEL_B = 5'd12; //button CH+
parameter PHASE = 5'd11; //button CH
parameter DELETE = 5'd16; //button -
parameter OK = 5'd18; //button EQ
initial begin
rstn <= 1'b0;
in_flag <= 1'b0;
in_addr <= 16'b0;
in_data <= 16'b0;
#(`p*10);
rstn <= 1'b1;
//=======================================
//通道A输出fwd=1000Hz,pwd=256
//channel_A
press_HT6221(CHANNEL_A);
//frequency = 1000Hz
press_HT6221(1 );
press_HT6221(0 );
press_HT6221(0 );
press_HT6221(0 );
//phase = (256/4096)*2*pi
press_HT6221(PHASE);
press_HT6221(2 );
press_HT6221(5 );
press_HT6221(6 );
//OK
press_HT6221(OK);
#(`p*3000);
//======================================
//通道B输出fwd=100Hz,pwd=25
press_HT6221(CHANNEL_B);
//frequency = 100Hz
press_HT6221(1 );
press_HT6221(0 );
press_HT6221(0 );
press_HT6221(0 );
press_HT6221(DELETE);
//phase = (25/4096)*2*pi
press_HT6221(PHASE);
press_HT6221(2 );
press_HT6221(5 );
press_HT6221(6 );
press_HT6221(DELETE);
//OK
press_HT6221(OK);
#(`p*3000);
//=====================================
//通道A输出fwd=10Hz,pwd=2
press_HT6221(CHANNEL_B);
//frequency = 10Hz
press_HT6221(1 );
press_HT6221(0 );
press_HT6221(0 );
press_HT6221(0 );
press_HT6221(DELETE);
press_HT6221(DELETE);
//phase = (2/4096)*2*pi
press_HT6221(PHASE);
press_HT6221(DELETE);
press_HT6221(2 );
press_HT6221(5 );
press_HT6221(6 );
press_HT6221(DELETE);
press_HT6221(DELETE);
//OK
press_HT6221(OK);
#(`p*3000);
//===============================
//什么都不输入,直接按OK键
press_HT6221(CHANNEL_B);
press_HT6221(OK);
#(`p*3000);
//===============================
//什么都不输入,直接按回退键
press_HT6221(CHANNEL_B);
press_HT6221(DELETE);
#(`p*3000);
$stop;
end
task press_HT6221(input [4:0] key_board);
begin
in_flag <= 1'b1;
#(`p);
in_flag <= 1'b0;
LUT(key_board);
#(`p*100);
//20ns *100 = 2us,
//waiting 2us and then
//push another key
end
endtask
task LUT (input [4:0] key_board_r);
begin
case(key_board_r)
5'd0 : {in_data, in_addr} <= 32'hE916FF00;
5'd1 : {in_data, in_addr} <= 32'hF30CFF00;
5'd2 : {in_data, in_addr} <= 32'hE718FF00;
5'd3 : {in_data, in_addr} <= 32'hA15EFF00;
5'd4 : {in_data, in_addr} <= 32'hF708FF00;
5'd5 : {in_data, in_addr} <= 32'hE31CFF00;
5'd6 : {in_data, in_addr} <= 32'hA55AFF00;
5'd7 : {in_data, in_addr} <= 32'hBD42FF00;
5'd8 : {in_data, in_addr} <= 32'hAD52FF00;
5'd9 : {in_data, in_addr} <= 32'hB54AFF00;
5'd10: {in_data, in_addr} <= 32'hBA45FF00;
5'd11: {in_data, in_addr} <= 32'hB946FF00;
5'd12: {in_data, in_addr} <= 32'hB847FF00;
5'd13: {in_data, in_addr} <= 32'hBB44FF00;
5'd14: {in_data, in_addr} <= 32'hBF40FF00;
5'd15: {in_data, in_addr} <= 32'hBC43FF00;
5'd16: {in_data, in_addr} <= 32'hF807FF00;
5'd17: {in_data, in_addr} <= 32'hEA15FF00;
5'd18: {in_data, in_addr} <= 32'hF609FF00;
5'd19: {in_data, in_addr} <= 32'hE619FF00;
5'd20: {in_data, in_addr} <= 32'hF20DFF00;
default: {in_data, in_addr} <= 32'h0;
endcase
end
endtask
key_FSM key_FSM(
.clk_50M (clk_50M),
.rstn (rstn ),
.in_addr (in_addr),
.in_data (in_data),
.in_flag (in_flag),
.fwd_a (fwd_a ),
.fwd_b (fwd_b ),
.pwd_a (pwd_a ),
.pwd_b (pwd_b ),
.out_flag (out_flag )
);
endmodule
经过调试,状态跳转以及数据输出都符合预期,是作此篇。