超级版主
- 积分
- 4671
- 金钱
- 4671
- 注册时间
- 2019-5-8
- 在线时间
- 1224 小时
|
1)实验平台:正点原子领航者V2FPGA开发板
2) 章节摘自【正点原子】《领航者ZYNQ之FPGA开发指南 V2.0》
3)购买链接:https://detail.tmall.com/item.htm?id=609032204975
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/fpga/zdyz_linhanz(V2).html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子FPGA技术交流QQ群:712557122
第三十章 IO扩展模块实验
随着时间的推进,正点原子的 FPGA 开发板款式越来越多,外设也越来越丰富,从简单的按键流水灯到复杂的光口网口,基本上可以说是满足了广大 FPGA 工程师的学习和项目开发。但是在实际项目开发的过程中往往会出现一些按键、LED 灯、数码管、拨码开关等基础器件不够用的情况,为此我们正点原子特地推出了 IO 扩展模块,这个模块旨在帮助大家丰富按键、LED 灯、数码管、拨码开关等基础器件,方便大家灵活开发实际项目。
本章包括以下几个部分:
30.1简介
30.2实验任务
30.3硬件设计
30.4软件设计
30.5下载验证
30.1简介
正点原子推出的 IO 扩展板包含了 8 个 LED、一个 x8 的拨码开关、四个八段数码管和一个 4x4 的矩阵 键盘。八颗LED可以使用FPGA单独控制也可以配合拨码开关去使用,当然拨码开关也可以用来控制其他外设。四个八段数码管同样可以使用 FPGA 单独控制,当开发板上的数码管不够用的时候就可以使用 IO 扩展板去代替。最后就是 4x4 的矩阵键盘了,共 16 个按键可以提供非常灵活的按键控制,当开发板上的按键不够用的时候就可以使用 IO 扩展板去提供一个额外的按键控制。
30.2实验任务
本节实验任务是通过拨码开关控制 IO 扩展板上的 LED 灯亮灭,然后将矩阵键盘的按键编号显示在数码管上。
30.3硬件设计
IO 扩展板模块的原理图如下所示:
图 7.5.13.1 IO扩展板硬件原理图
由上图可知,IO 扩展口模块的8颗LED灯是共阴极接法,其8个阳极全部引出,可以通过 FPGA(其 他单片机类芯片也可)对这八颗 LED 灯进行控制。拨码开关(Switch)也是共阴极接法,开关另一端全部上拉,当拨码开关断开时 SW0~SW7 全部是高电平,当拨码开关合上则 SW0~SW7 全部是低电平。我们可以使用拨码开关去控制一些其他外设,比如本节实验我们就可以使用拨码开关去控制颗 LED 灯。数码管大家就很熟悉了,IO 扩展模块的 4 个八段数码管的位选同样是共阴极接法,引出位选信号和段选信号可以使用 FPGA(其他单片机类芯片也可)对其进行控制。矩阵键盘是由 16 个按键组成的矩阵,分为四行四列, 其中四行全部是 3.3V 上拉,四列全部将端口引出,这 8 个端口(四行四列 8 个端口)全部可以连接到 FPGA上(其他单片机类芯片也可),可以通过 FPGA(其他单片机类芯片也可)对矩阵键盘进行行列扫描,判断出是哪一个按键被按下,扫描方法在下文会详细讲解。最后就是一排 20x2 的排针了,我们所有的端口都是连接到这个排针上的,大家在使用 IO 扩展板的时候可以将排针插到正点原子 FPGA 开发板的扩展口上去,这样每个端口就可以和 FPGA IO 引脚相连了。
IO 扩展板实物图如下图 所示。
图 7.5.13.2 实物图
本实验中,IO 扩展板管脚分配如下表所示。
表 30.3.1引脚分配表
对应的XDC约束语句如下所示:
- set_property -dict {PACKAGE_PIN U18 IOSTANDARD LVCMOS33} [get_ports sys_clk]
- set_property -dict {PACKAGE_PIN N16 IOSTANDARD LVCMOS33} [get_ports sys_rst_n]
- set_property -dict {PACKAGE_PIN J14 IOSTANDARD LVCMOS33} [get_ports seg_led_t[0]]
- set_property -dict {PACKAGE_PIN N18 IOSTANDARD LVCMOS33} [get_ports seg_led_t[1]]
- set_property -dict {PACKAGE_PIN M19 IOSTANDARD LVCMOS33} [get_ports seg_led_t[2]]
- set_property -dict {PACKAGE_PIN L16 IOSTANDARD LVCMOS33} [get_ports seg_led_t[3]]
- set_property -dict {PACKAGE_PIN M15 IOSTANDARD LVCMOS33} [get_ports seg_led_t[4]]
- set_property -dict {PACKAGE_PIN N20 IOSTANDARD LVCMOS33} [get_ports seg_led_t[5]]
- set_property -dict {PACKAGE_PIN U20 IOSTANDARD LVCMOS33} [get_ports seg_led_t[6]]
- set_property -dict {PACKAGE_PIN U19 IOSTANDARD LVCMOS33} [get_ports seg_led_t[7]]
- set_property -dict {PACKAGE_PIN P19 IOSTANDARD LVCMOS33} [get_ports sel_t[0]]
- set_property -dict {PACKAGE_PIN T20 IOSTANDARD LVCMOS33} [get_ports sel_t[1]]
- set_property -dict {PACKAGE_PIN M20 IOSTANDARD LVCMOS33} [get_ports sel_t[2]]
- set_property -dict {PACKAGE_PIN K14 IOSTANDARD LVCMOS33} [get_ports sel_t[3]]
- set_property -dict {PACKAGE_PIN T14 IOSTANDARD LVCMOS33} [get_ports key_row[0]]
- set_property -dict {PACKAGE_PIN U17 IOSTANDARD LVCMOS33} [get_ports key_row[1]]
- set_property -dict {PACKAGE_PIN Y16 IOSTANDARD LVCMOS33} [get_ports key_row[2]]
- set_property -dict {PACKAGE_PIN T15 IOSTANDARD LVCMOS33} [get_ports key_row[3]]
- set_property -dict {PACKAGE_PIN W16 IOSTANDARD LVCMOS33} [get_ports key_col[0]]
- set_property -dict {PACKAGE_PIN T16 IOSTANDARD LVCMOS33} [get_ports key_col[1]]
- set_property -dict {PACKAGE_PIN Y17 IOSTANDARD LVCMOS33} [get_ports key_col[2]]
- set_property -dict {PACKAGE_PIN V16 IOSTANDARD LVCMOS33} [get_ports key_col[3]]
- set_property -dict {PACKAGE_PIN N17 IOSTANDARD LVCMOS33} [get_ports led[0]]
- set_property -dict {PACKAGE_PIN P18 IOSTANDARD LVCMOS33} [get_ports led[1]]
- set_property -dict {PACKAGE_PIN V20 IOSTANDARD LVCMOS33} [get_ports led[2]]
- set_property -dict {PACKAGE_PIN W20 IOSTANDARD LVCMOS33} [get_ports led[3]]
- set_property -dict {PACKAGE_PIN R17 IOSTANDARD LVCMOS33} [get_ports led[4]]
- set_property -dict {PACKAGE_PIN R16 IOSTANDARD LVCMOS33} [get_ports led[5]]
- set_property -dict {PACKAGE_PIN W19 IOSTANDARD LVCMOS33} [get_ports led[6]]
- set_property -dict {PACKAGE_PIN W18 IOSTANDARD LVCMOS33} [get_ports led[7]]
- set_property -dict {PACKAGE_PIN V17 IOSTANDARD LVCMOS33} [get_ports swi[0]]
- set_property -dict {PACKAGE_PIN V18 IOSTANDARD LVCMOS33} [get_ports swi[1]]
- set_property -dict {PACKAGE_PIN T17 IOSTANDARD LVCMOS33} [get_ports swi[2]]
- set_property -dict {PACKAGE_PIN R18 IOSTANDARD LVCMOS33} [get_ports swi[3]]
- set_property -dict {PACKAGE_PIN Y18 IOSTANDARD LVCMOS33} [get_ports swi[4]]
- set_property -dict {PACKAGE_PIN Y19 IOSTANDARD LVCMOS33} [get_ports swi[5]]
- set_property -dict {PACKAGE_PIN P15 IOSTANDARD LVCMOS33} [get_ports swi[6]]
- set_property -dict {PACKAGE_PIN P16 IOSTANDARD LVCMOS33} [get_ports swi[7]]
复制代码
30.4程序设计
根据实验任务,我们画出程序框图,整个程序框架由三个子模块构成,第一个模块是矩阵键盘扫描模块,通过行列扫描检测按键的按下情况,并将按下的那个按键编号传递给数码管模块。数码管模块会把按键的编号显示在数码管上。拨码开关模块其实是和LED模块连在一起的,在代码中我们会直接把拨码开关的值赋给LED灯,因此当拨码开关断开swi端口被上拉,LED 灯点亮,反之LED灯熄灭。系统框图如下图所示:
图 7.5.13.1 程序框图
各模块端口及信号连接如下图所示:
图 7.5.13.2 RTL视图
由上图可知,FPGA 部分包括四个模块,顶层模块(top_matrix_keyboard)、矩阵键盘扫描模块(key_4x4)、数码管显示模块(seg_led)、拨码开关控制 led 灯模块(swi_led)。在顶层模块中完成对其它三个模块的例化,并实现各模块之间的信号传递。
1)顶层模块(top_matrix_keyboard):顶层模块主要是对其它三个子模块进行例化,实现子模块间的信号连接。
2)矩阵键盘扫描模块(key_4x4):矩阵键盘扫描模块主要是对IO扩展板上的矩阵键盘进行行列扫描, 定位出哪一个按键被按下并将其对应的编号传递给数码管显示模块。
3)数码管显示模块(seg_led):接收矩阵键盘扫描模块(key_4x4)传递出的按键编号值并将数据显示出来。
4)拨码开关模块(swi_led):主要检测拨码开关的开合状态,并将状态值赋给 led 灯控制 led 灯的亮灭。
顶层模块的代码如下:
- 1 module top_matrix_keyboard(
- 2 input sys_clk ,
- 3 input sys_rst_n ,
- 4 input [3:0] key_row ,
- 5 input [7:0] swi ,
- 6 output [3:0] key_col ,
- 7 output [3:0] sel_t ,
- 8 output [7:0] seg_led_t ,
- 9 output [7:0] led
- 10
- 11 );
- 12
- 13 //wire define
- 14 wire [3:0] key_value ;
- 15 wire key_flag ;
- 16
- 17 //*****************************************************
- 18 //** main code
- 19 //*****************************************************
- 20
- 21 //矩阵键盘扫描模块
- 22 key_4x4 u_key_4x4(
- 23 .sys_clk (sys_clk ),
- 24 .sys_rst_n (sys_rst_n),
- 25 .key_row (key_row ),
- 26 .key_col (key_col ),
- 27 .key_value (key_value),
- 28 .key_flag (key_flag )
- 29 );
- 30
- 31 //数码管显示模块
- 32 seg_led u_seg_led(
- 33 .clk (sys_clk ),
- 34 .rst_n (sys_rst_n),
- 35 .key_value (key_value),
- 36 .key_flag (key_flag ),
- 37 .sel_t (sel_t ),
- 38 .seg_led_t (seg_led_t)
- 39 );
- 40
- 41 //拨码开关模块
- 42 swi_led u_swi_led(
- 43 .clk (sys_clk ),
- 44 .rst_n (sys_rst_n),
- 45 .swi (swi ),
- 46 .led (led )
- 47 );
- 48 endmodule
复制代码
顶层模块主要就是例化三个子模块,在这里就不作过多介绍了,下面我们直接开始看矩阵键盘扫描模块,矩阵键盘扫描模块的代码如下:
其实要想看懂矩阵键盘的扫描代码就要先吃透矩阵键盘的硬件设计,在上文已经跟大家介绍了我们矩阵键盘的行扫描信号全部3.3V上拉,并且行列信号(四个行信号四个列信号)是全部接到 FPGA 引脚上的,我们要想检测具体哪个按键被按下只要扫描它的行列序号就可以。矩阵键盘的按键编号是按照从左往右的 顺序编码的,例如第0行第0列的按键编号就是“0”(编号从0开始),第 0 行第1列的按键编号就是“1”,依次类推矩阵16个按键编号就是0~15,每一个编号都有自已唯一对应的行列号。那怎么得到这个行列号呢?这里我们就以按键6(对应的行列号为第一行第二列,对应扩展板上的KEY7)按下为例给大家讲解,首先按键没被按下之前所有的行端口(key_row [3:0])因为上拉的关系全为高电平,此时我们让所有与列端口相连的FPGA IO输出低电平(也就是key_col [3:0]等于4'b0000),这样当按键6被按下时,行端口1(key_row [1])会因为与列端口2(key_col[2])导通(按键闭合)而由原本的上拉高电平变成低电平,这样行端口就被检测出来了,哪一行端口电平变成低电平就说明按键就在那一行。接下来我们再来扫描列端口,按键按下之前四个列端口全部输出为低电平,按键按下后先扫列端口0(第一列)。我们将列端口0保持低电平,列端口1~3全部拉高(也就是 key_col [3:0] 等于 4'b1110)看行端口(key_row [3:0])的值是否发生变化(行端口1是否由低电平恢复成高电平即key_row[3:0]是否等于 4'b1111),如果没有变化则说明按键的列序号就是0,如果发生变化了说明按键不在列端口0的位置(第一列),因为只有当列端口为低电平时才能拉低其对应行端口为低电平,否则行端口会恢复成高电平,那么我们就继续扫描下一列,将列端口1(第二列)置0,其他端口置1,看行端口电平是否变化(即判断key_row[3:0]是否恢复成4'b1111),如果不变化说明被按下的按键对应列序号就是1,反之则不是,我们继续扫描下一列,直到找到对应列为止。采用这种扫描的方法就可以找到被按下按键的具体行列位置了,就可以找到对应编号,我们把这个编号传递给数码管模块,让数码管把编号显示出来。了解了矩阵按键扫描原理后我们再来分析代码,代码第22~50行是按键消抖模块,它的工作原理很简单,就是将按键的值先打一拍(key_reg <= key_row),然后检测当前时钟下按键的状态和上一个时钟的状态是否一致,如果不一致则将计数器 delay_cnt 赋初值100万,如果一致则计数器从初值开始作减法计数,直到计数器计数到“1”,说明按键的状态一直稳定了100万个时钟周期,此时我们认为是一次有效的按键 触发,这时我们就可以拉高消抖完成标志key_flag_row(注意只拉高一个时钟)。代码第52~116行就是实现整个按键扫描的过程,它算一个简单的状态机,共有6个状态,其中状态0~4就是判断按键具体在哪一列,主要就是改变输出key_col[3:0]的值,看key_row[3:0]是否等于4'b1111,只要key_row[3:0]不等于4'b1111就说明一定有按键按下,然后再看列端口的值,四个列端口只保留一个端口为低电平,其余都为高电平,这样只有当被按下的按键刚好处于列端口为低电平的位置时 key_row[3:0]才能不等于4'b1111,因为按键闭合会使行端口上拉 3.3V 与列端口低电平导通,行端口的值被下拉成 0,否则行端口会一直处于上拉状态即key_row[3:0]等于 4'b1111。按照这个原理我们就把被按下的按键行列位置找到了,接下来就进入状态5将行列值寄存下来。最后代码118~144行会根据行列值把按键的编号翻译出来,然后传递给数码管模块去显示。
看完了矩阵按键扫描模块后我们再来看看数码管显示模块(seg_led)的代码,如下所示:
- 1 module seg_led(
- 2 input clk ,
- 3 input rst_n ,
- 4 input [3:0] key_value ,
- 5 input key_flag ,
- 6 output [3:0] sel_t ,
- 7 output [7:0] seg_led_t
- 8 );
- 9
- 10 //reg define
- 11 reg [3:0] sel ;
- 12 reg [7:0] seg_led;
- 13
- 14 //*****************************************************
- 15 //** main code
- 16 //*****************************************************
- 17
- 18 assign sel_t = ~sel ;//共阴极接法这里取反,如果共阳极这里就不取反
- 19 assign seg_led_t = ~seg_led;//共阴极接法这里取反,如果共阳极这里就不取反
- 20
- 21 always @(posedge clk or negedge rst_n)begin
- 22 if(!rst_n)
- 23 sel <= 4'b1111;
- 24 else if(key_flag)
- 25 sel <= 4'b0000;
- 26 else
- 27 sel <= 4'b1111;
- 28 end
- 29
- 30 always @(posedge clk or negedge rst_n)begin
- 31 if(rst_n==1'b0)
- 32 seg_led <= 8'b0;
- 33 else if (key_flag)begin
- 34 case (key_value)
- 35 4'd0 : seg_led <= 8'b01000000;
- 36 4'd1 : seg_led <= 8'b01111001;
- 37 4'd2 : seg_led <= 8'b00100100;
- 38 4'd3 : seg_led <= 8'b00110000;
- 39 4'd4 : seg_led <= 8'b00011001;
- 40 4'd5 : seg_led <= 8'b00010010;
- 41 4'd6 : seg_led <= 8'b00000010;
- 42 4'd7 : seg_led <= 8'b01111000;
- 43 4'd8 : seg_led <= 8'b00000000;
- 44 4'd9 : seg_led <= 8'b00010000;
- 45 4'd10: seg_led <= 8'b00011000;
- 46 4'd11: seg_led <= 8'b00000011;
- 47 4'd12: seg_led <= 8'b01000110;
- 48 4'd13: seg_led <= 8'b00100001;
- 49 4'd14: seg_led <= 8'b00000110;
- 50 4'd15: seg_led <= 8'b00001110;
- 51 default:
- 52 seg_led <= 8'b1111_1111;
- 53 endcase
- 54 end
- 55 else
- 56 seg_led <= 8'b1111_1111;
- 57 end
- 58
- 59 endmodule
复制代码
数码管模块的代码是非常简单的,本次实验是使用静态数码管来显示矩阵按键的编号的,因此四个数 码管显示的数字都一样。代码 21~28 行是控制数码管的位选信号,因为 IO 扩展模块的数码管是共阴极接法,所以是高电平点亮数码管,因此在代码的 18 和 19 行我们做了一个阴阳极转换,如果大家拿到的是共阴极接法的数码管就可以直接使用本节实验代码,如果是共阳极大家就可以将代码 18 和 19 行的取反运算去掉就行。从代码中可以看到当按键按下后标志位 key_flag 就会拉高,此时我们位选信号全部选中,如果按键没有按下即 key_flag 的值为低电平,则放开所有位选信号。接下来再看代码 30~57 行,这段代码是控制数 码管的段选信号,当按键按下即 key_flag 的值为高电平,此时开始检测按键编号 key_value 的值,通过一个 case 语句把按键值翻译成数码管的段选显示信号,这样就可以控制数码管显示数据了。
最后我们再来看看拨码开关模块(swi_led)的代码,如下所示:
- 1 module swi_led(
- 2 input clk ,
- 3 input rst_n ,
- 4 input [7:0] swi ,
- 5 output reg[7:0] led
- 6 );
- 7
- 8 //*****************************************************
- 9 //** main code
- 10 //*****************************************************
- 11
- 12 always @(posedge clk or negedge rst_n)begin
- 13 if(!rst_n)
- 14 led <= 8'b0000_0000;
- 15 else
- 16 led <= swi;
- 17 end
- 18
- 19 endmodule
复制代码
拨码开关模块(swi_led)的代码是非常简单的,它一共就一个 always 语句块,目的就是为了把拨码开 关的值赋给 led 灯的阳极端口用来控制 led 灯。
到此整个 IO 拓展口模块实验的代码就全部讲解完了。
30.5下载验证
首先我们将下载器与领航者开发板上的JTAG接口连接,下载器另外一端与电脑连接。然后将IO扩展板模块插到板子上的J3扩展口去,最后连接开发板的电源,并打开电源开关,如下图所示:
图 7.5.13.1硬件连接图
回到Vivado界面,我们将生成好的bit流文件下载到开发板中去,按下矩阵按键就可以看到数码管会显示矩阵按键的编号(注意是16进制显示),上下拨动拨码开关就可以控制led灯亮灭,效果如下图所示:
图 7.5.13.2 最终效果图 |
|