差别

这里会显示出您选择的修订版和当前版本之间的差别。

到此差别页面的链接

两侧同时换到之前的修订记录 前一修订版
后一修订版
前一修订版
book_excise_keyscan_7seg [2021/08/18 16:31]
zili
book_excise_keyscan_7seg [2021/08/18 17:22] (当前版本)
zili
行 1: 行 1:
 ## 键盘扫描及数码管显示实验 ## 键盘扫描及数码管显示实验
  
-### 实验内容 +### 1. 实验内容 
 +通过本实验熟悉矩阵键盘的动态扫描工作原理,以及七段数码管的扫描显示原理,提高复杂FPGA数字逻辑设计及输入输出接口设计技能。本实验要求实现对4*4扫描键盘上输入键的进行识别,并把输入按键相应的行值和列值在七段数码管L7和L8上显示出来。
  
 \\ \\
 \\ \\
-### 实验原理+### 2. 实验原理 
 +#### 2.1 扫描键盘原理  
 +{{ :​图10-1.png |图10-1 4×4扫描键盘连接示意图}} 
 +<WRAP centeralign>​ 
 +**图10-1 4×4扫描键盘连接示意图** 
 +</​WRAP>​
  
 +图10-1为4×4键盘的连接示意图,S3-S18为16个按键,KR0-KR3 为行线输入端,KC0-KC3 为列线输出端。\\
 +扫描键盘的工作原理如下:\\
 +(1)首先由输出端KC0-KC3 向所有的列线输出高电平,读取各行线KR0-KR3的状态。若所有的行线输出全为低电平,则表明无键按下,反之,若有某行行线的输出为高电平,则表明有键按下,如图10-2(a)所示。\\
 +(2)当检测到行线输出为电平后,需要先进行消抖,这是因为按键在闭合和打开的瞬间会产生许多尖脉冲,持续时间约几毫秒到几十毫秒,需要等电平稳定后再进行扫描。具体方法是每隔4ms读一次行线输出状态,直到连续8次读取的输出完全相同,则认为抖动已经消除。消抖时间为 4ms×8=32ms。\\
 +(3) 在消抖结束后进行键盘扫描,即在KC0-KC3四条列扫描线上依次输出高电平,在每次输出高电平期间,读取各行线KR0-KR3的值。如果在该列有按键被按下,则KR0-KR3必然有一个为高。例如,KC0-KC3输出“0100”,此时若读取 KR0-KR3的状态为“1000”,则表明按键 S5 被按下,如图10-2(b)所示。
 +{{ :​图10-2.png |图10-2 键盘扫描过程示意图}}
 +<WRAP centeralign>​
 +**图10-2 键盘扫描过程示意图**
 +</​WRAP>​
  
 +#### 2.2 八段数码管显示原理
 +{{ :​图10-3.jpg |图10-3 数码管连接}}
 +<WRAP centeralign>​
 +**图10-3 数码管连接**
 +</​WRAP>​
  
 +八段数码管具有两种显示方式:一种是独立显示方式,即每个数码管具有单独的8根数据线和 1 根选通信号线,可以同时控制各个显示器的显示结果。另一种是扫描显示模式,即所有显示器共用八根数据线,各自再有1根选通信号线,采用时分的方式循环选通各个数码管进行显示。 ​
  
 +独立显示模式实现简单,但是需要占用大量的信号线,例如8个数码管一共需要9×8=72根信号线,因此实际中一般采用扫描显示模式。
 +
 +扫描显示模式利用了人眼的视觉暂留的特性,即只要扫描的频率足够高,数码管显示循环周期足够短,则在人眼看来数码管的影像就是连续存在的。对于每个数码管来说,如果让它每隔时间T闪现一次,而每次闪现时间为t(满足t<​T),则当 T足够小(例如 T=10ms 时),我们看这个数码管就像是一直显示的。如果要同时显示 8个数码管,则我们可以把周期 T 平均分成 8 段,每个数码管分别利用其中的一段时间进行显示,即 t=T/8。
  
 \\ \\
 \\ \\
  
-### 程序设计+### 3. 程序设计 
 +#### 3.1 总体架构 
 +整个程序由5个模块组成(图10-4):\\ 
 +1. 顶层模块Key_scan将各子模块实例化并将各接口连接起来;\\ 
 +2. scan_clk模块产生扫描键盘所要求的周期为4ms的脉冲信号;\\ 
 +3. scan模块负责键盘扫描,识别按键;\\ 
 +4. data_2_disp模块负责输入键值的译码,因为要分别对行值和列值进行译码,因此用两个data_2_disp模块,​一个是行地址显示的译码disp1,一个是列地址显示的译码disp2;\\ 
 +5. disp_scan实现两个数码管的扫描显示。\\ 
 +{{ :​图10-4.png |图10-4 程序总体架构}} 
 +<WRAP centeralign>​ 
 +**图10-4 程序总体架构** 
 +</​WRAP>​
  
 +#### 3.2 key_scan模块(key_scan.v)
 +Key_san是顶层模块,实例化各模块。scan_clk输出的时钟scan_clk送给scan模块,作为控制键盘扫描周期的时钟;scan模块输出的行地址r_reg和列地址c_reg作为data_2_disp模块的输入,经译码输出Hex[1:​0]。 disp_scan扫描显示HEX[0:​1]的值,分别表示行值和列值。
  
 +Key_scan输入输出接口如下:
  
-\\ +<code verilog> ​ 
-\\ +input clk,​reset;​  ​  //​50MHz系统时钟。 
-### 仿真结果+input [3:​0]kr;​  ​ //​行线输出接口。 
 +output [3:​0]kc;​  ​ //​列线输入接口。 
 +output [7:​0]HEX_sel;​  ​ //​数码管选择输出,为高表示相应数码管被选中。 
 +output [7:​0]HEX_seg;​ //​数码管的显示值,8段数码管由8为控制,为高表示显示
  
 +</​code>​
  
 +#### 3.3 scan_clk模块(scan_clk.v)
 +scan_clk模板生成周期为4ms的脉冲信号作为键盘扫描时钟,周期为1us的脉冲信号作为Signaltap逻辑分析仪参考时钟。
  
 +<code verilog> ​
 +scan_clk输入输出接口如下:
 +<code verilog> ​
 +input clk,​reset;​ //​50MHz时钟和复位。
 +output scan_clk;​ //​周期是4ms的键盘扫描时钟
 +output sig_clk;​ //​为signaltap逻辑分析仪时钟
  
 +</​code>​
 +
 +键盘扫锚时钟scan_clk周期为4ms,脉宽为一个时钟周期(图10-5)。
 +{{ :​图10-5.png |图10-5 扫描时钟波形}}
 +<WRAP centeralign>​
 +**图10-5 扫描时钟波形**
 +</​WRAP>​
 +
 +#### 3.4 scan模块(scan.v)
 +scan模块扫描键盘,获取按下的键的行值和列值。
 +
 +Scan模块输入输出接口如下:
 +<code verilog> ​
 +input clk,​reset;​ //​50MHz时钟和复位。
 +input [3:​0]kr;​ //​行线输出接口
 +input scan_clk;​ //​周期是4ms的键盘扫描时钟
 +output [3:​0]kc;​ //​列线输出接口
 +output [3:​0]add_r;​ //​有键按下时行线的地址
 +output [3:​0]add_c;​ //​有键按下时列线的地址
 +output ​ keyout_en ​      //​按键值输出使能
 +
 +</​code>​
 +
 +kr、kc和scan_clk用于实现对键盘按键的扫描;add_r和add_c输出按键的行值和列值;keyout_en是按键输出使能,当keyout_en为高时,表示add_r和add_c有效,keyout_en宽度为一个时钟周期,如图10-6所示。
 +{{ :​图10-6.png |图10-6 ​ Scan模块行列值输出接口时序}}
 +<WRAP centeralign>​
 +**图10-6 ​ Scan模块行列值输出接口时序**
 +</​WRAP>​
 +Scan的主状态机如图10 7所示。
 +{{ :​图10-7.png |图10-7 ​ Scan模块的状态机设计}}
 +<WRAP centeralign>​
 +**图10-7 ​ Scan模块的状态机设计**
 +</​WRAP>​
 +主状态机State有四个状态:Waiting、Anti-jitter、scanning和Ending状态。waiting状态表示无按键被按下,等待按键;anti-jitter状态表示已经检测到按键被按下,但是处于消抖状态;scanning状态指在消抖工作完成后,扫描键盘列线,获取按键值;Ending 状态指在扫描结束后,延时200ms后再进入waiting状态,这样保证每隔200ms扫描一次键盘,防止两次扫描时间过短。
 +<code verilog> ​
 +
 +Waiting: ​   //​ Waiting状态,等待按键按下
 +begin
 + add_r <= 4'​b1111
 + add_c <= 4'​b1111;​
 + kc <= 4'​b1111; ​       //​每行列线上都输出高电平
 + delay <= 1'b0;
 +keyout_en <= 1'b0;
 +
 + if(kr > 4'​b0000) ​ begin  ​   //​如果行线中有键按下
 + state <= anti_jitter;​ //​下个周期进入消抖状态
 + kr_last <= kr;     ​
 +  keyout_en <= 1'b0;
 + end else
 +state <= waiting;
 + end
 +
 + anti_jitter:​ //​anti_jitter状态:进入消抖状态。
 +begin
 + add_r <= 4'​b1111;​
 + add_c <= 4'​b1111;​
 + kr_last <= kr;
 + kc <= 4'​b1111;​
 + if(delay == 3'​b111) begin
 +state <= scaning;​ //​如果同一行线的为高电平时间超过八个周期则进入scanning状态
 + delay <= 1'b0;
 + kc <= 4'​b1000;//​从最左边的列线开始扫描
 + end
 +else begin
 + if(kr_last == kr)
 +delay <= delay + 1'b1; //​本次行线电平和上次一样则delay+1
 + else if(kr == 4'​b0000)
 +state <= waiting;//​如果是0,则进入waiting状态
 + else
 + delay <= 1'​b0; ​ //​如果Kr输入不为0,则重新进行消抖过程。
 + end
 + end
 +
 +scaning: //​scaning状态,对列线进行扫描
 +begin
 +kc <= {kc[0],​kc[3:​1]}; ​   //​从最左边开始扫描列线
 + hold <= 6'b0;
 + if(kc == 4'​b0001 )      //​扫描到最右边的列线
 + state <= ending;
 +
 + if((temp_r != 4'​b0000)&&​(temp_c != 4'​b0000)) begin //​扫描到键盘
 + add_r <= temp_r; ​          //​行地址赋值
 + add_c <= temp_c;​  ​        //​列地址赋值
 + keyout_en <= 1'​b1; ​        //​键盘扫描输出使能信号
 + end
 + else
 + keyout_en <= 1'​b0;​
 + end
 +
 + ending: ending状态,键盘扫描完成后延时200ms
 + begin
 + keyout_en <= 1'b0;
 + if(hold == 6'​b11_0001) //​时间间隔为 200ms
 + begin
 + state <= waiting;
 + hold <= 6'b0;
 + end
 + else
 + hold <= hold + 1'b1;
 + end   
 +
 +</​code>​
 +
 +当扫描到键盘后需要对键盘行地址和列地址进行译码。以下是行地址译码的程序,列地址译码与此相同。
 +<code verilog> ​
 +always @(kr)
 +begin
 + case(kr)
 + 4'​b1000:​temp_r = 4'​b0100; ​ //​第四条行线地址
 + 4'​b0100:​temp_r = 4'​b0011;​ //​第三条行线地址
 + 4'​b0010:​temp_r = 4'​b0010;​ //​第二条行线 地址
 + 4'​b0001:​temp_r = 4'​b0001; ​ //​第一条行线地址
 + default: temp_r = 4'​b0000;​
 + endcase
 +end
 +
 +</​code>​
 +
 +#### 3.5 data_2_disp模块(data_2_disp.v)
 +由于scan模块输出的按键行值和列值都是4比特二进制数。但八段数码管的输入为八位,其中七位输入用于显示一位阿拉伯数字,每一位控制数码管的一段,另一位用于显示小数点。因此需要将输入的4bits数据译码为8bits二进制数据,控制八段数码管各段的显示。图10-8为八段数码管显示示意图。
 +{{ :​图10-8.png |图10-8 八段数码管示意图}}
 +<WRAP centeralign>​
 +**图10-8 八段数码管示意图**
 +</​WRAP>​
 +在data_2_disp定义了一个8位寄存器segs[7:​0]。寄存器每一位分别对应着数码管的{H,​G,​F,​E,​D,​C,​B,​A},如果某一位寄存器输出为低电平时,对应的那一段数码管亮起。\\
 +data_2_disp的输入输出接口为:
 +<code verilog> ​
 +input [3:​0] data;​ //​输入位宽为4的二进制数
 +output [7:​0] segs;​ //​译码输出,变为8bit二进制数
 +
 +</​code>​
 +
 +程序说明:
 +<code verilog> ​
 +always @(data)
 + begin
 + case(data)
 + 4'​h0:​ segs = 8'​b11000000; ​ //​输入为0时,ABCDEF段亮起
 + 4'​h1:​ segs = 8'​b11111001; ​ //​输入为1时,BC段亮起
 + 4'​h2:​ segs = 8'​b10100100; ​ //​输入为2时,ABEFG段亮起
 + 4'​h3:​ segs = 8'​b10110000;​ //​输入为3时,ABCDG段亮起
 + 4'​h4:​ segs = 8'​b10011001;​ //​输入为4时,BCFG段亮起
 + 4'​h5:​ segs = 8'​b10010010;​ //​输入为5时,ACDFG段亮起
 + 4'​h6:​ segs = 8'​b10000010;​ //​输入为6时,ACDEFG段亮起
 + 4'​h7:​ segs = 8'​b11111000;​ //​输入为7时,ABC段亮起
 + 4'​h8:​ segs = 8'​b10000000;​ //​输入为8时,ABCDEFG段亮起
 + 4'​h9:​ segs = 8'​b10010000;​ //​输入为9时,ABCDFG段亮起
 + default: segs = 8'​b11111111;​
 + endcase
 + end
 +
 +</​code>​
 +
 +#### 3.6 disp_scan模块(disp_scan.v)
 +disp_scan对两个数码管进行扫描显示。用八位HEX_sel寄存器对两个数码管选通。这里只需要显示两位数码管,所以只用HEX_sel低两位进行选通即可。\\
 +disp_scan端口说明:
 +<code verilog> ​
 +input clk,​reset;​ //​时钟和复位信号
 +input scan_clk;​ //​周期是4ms的键盘扫描时钟
 +input  [7:0] Hex1;     //​行值
 +input  [7:0] Hex0;     //​列值
 +output ​ [7:0] HEX_sel; ​ //​数码管选通信号
 +output ​ [7:0] HEX_seg;​ //​数码管显示数字
 +always @(posedge clk or negedge reset)
 +begin
 + if( !reset )
 + HEX_sel <= 8'​b1111_1101;​
 + else
 + begin
 + count <= count+1'​b1;
 + if(count == 18'​b1) //​每五毫秒循环一次
 + HEX_sel <= {HEX_sel[7:​2],​HEX_sel[0],​HEX_sel[1]};​
 + end
 +end
 +
 +always @(HEX_sel)
 + begin
 + case(HEX_sel)
 + 8'​b1111_1101:​ HEX_seg = Hex0;​ //​当HEX_sel[1]为低时,显示行地址;​
 + 8'​b1111_1110:​ HEX_seg = Hex1;​ //​当HEX_sel[0]为低时,显示列地址;​
 + default:​ HEX_seg = 8'​b1111_1111;​
 + endcase
 + end
 +
 +</​code>​
 \\ \\
 \\ \\
 +### 4. 仿真结果
 +{{ :​图10-9.png |图10-9 仿真结果}}
 +<WRAP centeralign>​
 +**图10-9 仿真结果**
 +</​WRAP>​
 +本实验的 Testbench为test_key.v,主要用于对scan模块进行仿真。test_key.v的时钟与系统时钟一样为50MHz,由延时语句输入一系列kr的值,调用scan.v扫描程序观察相应的kc输出时各变量的状态。
  
-### 演示序文件说明+图10-9是键盘扫描的仿真波形,整个扫描过由状态机来控制,系统复位的状态是wating=4’h1,在waiting状态下输出为4’b1111。如果检测到输入不全为低电平则转到去抖动状态anti_jitter=4’h2。此时继续检测输入端口,如果连续8次(delay的值)检测输入值都相同,则说明有键按下,进入下一个状态scaning=4’h4,​ 否则认为是毛刺。在scaning状态下kc依次输出4’b1000、4’b0100、4’b0010、4’b0001,然后根据输入kr的值来确定具体是哪一个键按下。在图中可以看到当add_r=4’h4 ,​add_c=4’h2时,此时kr=4’h8,​kc=4’h1确定第4行第1列的键——S18按下。扫描结束后进入ending=4’h8状态,经过4ms*8=32ms之后,又进入下一轮扫描过程。
  
 +在anti_jitter状态下修改参数delay的变化情况可以控制去抖动的时间;在ending状态下修改delay的变化范围可以改变连续两次扫描之间的时间间隔,这些值可以根据实际情况进行调整。
  
 +\\
 +\\
  
 +### 5. 运行结果
 +在signaltap里选择要观察的信号,如图10-10。其中将Keyout_en设置为上边沿触发,这样当键盘扫描减速,keyout_en拉高后,signaltap会暂停。在设定好观察的信号后,对程序进行编译。将程序下载到开发系统上,选择signaltap的Autorun Analysis。然后按下任意一个按键,如按下行地址为2,列地址为4的键。则会看到如下结果,add_c和add_r分别键盘显示列地址和行地址,在数码管上可以看到行值和列值的显示。
 +{{ :​图10-10.png |图10-10 程序运行结果}}
 +<WRAP centeralign>​
 +**图10-10 程序运行结果**
 +</​WRAP>​
  
 \\ \\
 \\ \\
 +### 6. 演示程序文件说明
 +|文件名|功能|
 +|Keyscan.v|顶层模块。|
 +|scan_clk.v|产生扫描和逻辑分析仪所要求的时钟。|
 +|data_2_display.v|按键值译码。|
 +|scan.v|扫描键盘。|
 +|disp_scan.v|显示按键值。|
 +|Test_key.v|用于Modelsim仿真的测试程序,对scan.v进行仿真。|
 +
 +### 7. 演示程序使用
 +  演示设备:核心板、扩展板。
 +  演示方法:把程序下载到开发板上之后,按下键盘区任意一个键,可以观察到数码管L7和L8分别显示当前所按键的行值和列值。
 +
  
-### 演示程序使用