正点原子 发表于 2023-1-28 09:26:42

《ATK-DFPGL22G之FPGA开发指南_V1.0》第十五章IO扩展模块实验

1)实验平台:正点原子紫光PGL22G开发板
2)购买链接:https://item.taobao.com/item.htm?&id=692368045899
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-340253-1-1.html
4)正点原子官方B站:https://space.bilibili.com/394620890
5)正点原子FPGA交流群:994244016




第十五章IO扩展模块实验
随着时间的推进,正点原子的FPGA开发板款式越来越多,外设也越来越丰富,从简单的按键流水灯到复杂的光口网口,基本上可以说是满足了广大FPGA工程师的学习和项目开发。但是在实际项目开发的过程中往往会出现一些按键、LED灯、数码管、拨码开关等基础器件不够用的情况,为此我们正点原子特地推出了IO扩展模块,这个模块旨在帮助大家丰富按键、LED灯、数码管、拨码开关等基础器件,方便大家灵活开发实际项目。
本章包括以下几个部分:
15.1简介
15.2实验任务
15.3硬件设计
15.4软件设计
15.5下载验证

15.1简介
正点原子推出的IO扩展板包含了8个LED、一个x8的拨码开关、四个八段数码管和一个4x4的矩阵键盘。八颗LED可以使用FPGA单独控制也可以配合拨码开关去使用,当然拨码开关也可以用来控制其他外设。四个八段数码管同样可以使用FPGA单独控制,当开发板上的数码管不够用的时候就可以使用IO扩展板去代替。最后就是4x4的矩阵键盘了,共16个按键可以提供非常灵活的按键控制,当开发板上的按键不够用的时候就可以使用IO扩展板去提供一个额外的按键控制。
15.2实验任务
本节实验任务是通过拨码开关控制IO扩展板上的LED灯亮灭,然后将矩阵键盘的按键编号显示在数码管上。
15.3硬件设计
IO 扩展板模块的原理图如下所示:
图 15.3.1 IO扩展板硬件原理图
由上图可知,IO扩展口模块的8颗LED灯是共阴极接法,其8个阳极全部引出,可以通过FPGA(其 他单片机类芯片也可)对这八颗LED灯进行控制。拨码开关(Switch)也是共阴极接法,开关另一端全部上拉,当拨码开关断开时SW0~SW7全部是高电平,当拨码开关合上则SW0~SW7全部是低电平。我们可以使用拨码开关去控制一些其他外设,比如本节实验我们就可以使用拨码开关去控制8颗LED灯。数码管大家就很熟悉了,IO扩展模块的4个八段数码管的位选同样是共阴极接法,引出位选信号和段选信号可以使用FPGA对其进行控制。
矩阵键盘是由16个按键组成的矩阵,分为四行四列,其中四行全部是3.3V上拉,四列全部将端口引出,这8个端口(四行四列8个端口)全部可以连接到FPGA上,可以通过FPGA对矩阵键盘进行行列扫描,判断出是哪一个按键被按下。如果想要看懂矩阵键盘的扫描代码就要先吃透矩阵键盘的硬件设计,矩阵键盘的行扫描信号全部3.3V上拉,并且行列信号(四个行信号四个列信号)是全部接到FPGA引脚上的,我们要想检测具体哪个按键被按下只要扫描它的行列序号就可以。矩阵键盘的按键编号是按照从左往右的顺序编码的,例如第0行第0列的按键编号就是“0”(编号从0开始),第0行第1列的按键编号就是“1”,依次类推矩阵16个按键编号就是0~15,每一个编号都有自已唯一对应的行列号。那怎么得到这个行列号呢?这里我们就以按键6(对应的行列号为第一行第二列,对应扩展板上的KEY7)按下为例给大家讲解,首先按键没被按下之前所有的行端口(key_row )因为上拉的关系全为高电平,此时我们让所有与列端口相连的FPGA IO输出低电平(也就是key_col 等于4'b0000),这样当按键6被按下时,行端口1(key_row )会因为与列端口2(key_col)导通(按键闭合)而由原本的上拉高电平变成低电平,这样行端口就被检测出来了,哪一行端口电平变成低电平就说明按键就在哪一行。
接下来我们再来扫描列端口,按键按下之前四个列端口全部输出为低电平,按键按下后先扫列端口0(第一列)。我们将列端口0保持低电平,列端口1~3全部拉高(也就是key_col 等于4'b1110)看行端口(key_row )的值是否发生变化(行端口1是否由低电平恢复成高电平即key_row是否等于 4'b1111),如果没有变化则说明按键的列序号就是0,如果发生变化了说明按键不在列端口0的位置(第一列),因为只有当列端口为低电平时才能拉低其对应行端口为低电平,否则行端口会恢复成高电平,那么我们就继续扫描下一列,将列端口1(第二列)置0,其他端口置1,看行端口电平是否变化(即判断key_row是否恢复成4'b1111),如果不变化说明被按下的按键对应列序号就是1,反之则不是,我们继续扫描下一列,直到找到对应列为止。采用这种扫描的方法就可以找到被按下按键的具体行列位置了,就可以找到对应编号,我们把这个编号传递给数码管模块,让数码管把编号显示出来。
最后就是一排20x2的排母了,我们所有的端口都是连接到这个排母上的,大家在使用IO扩展板的时候可以将排母插到正点原子FPGA开发板的扩展口上去,这样每个端口就可以和FPGA IO引脚相连了。
IO扩展板实物图如下图所示:
图 15.3.2 实物图
本实验中,IO扩展板管脚分配如下表所示。
表 15.3.1引脚分配表
由于对应的FDC约束语句太长,所以这边省略了对应的约束语句,FDC文件存放在该例程文件下的prj文件夹中,如有需求可以从该路径获取。
15.4程序设计
根据实验任务,我们画出程序框图,整个程序框架由三个子模块构成,第一个模块是矩阵键盘扫描模块,通过行列扫描检测按键的按下情况,并将按下的那个按键编号传递给数码管模块。数码管模块会把按键的编号显示在数码管上。拨码开关模块其实是和LED模块连在一起的,在代码中我们会直接把拨码开关的值赋给LED灯,因此当拨码开关断开swi端口被上拉,LED灯点亮,反之LED灯熄灭。系统框图如下图所示:
图 15.4.1 程序框图
各模块端口及信号连接如下图所示:
图 15.4.2 IO扩展模块实验原理图
由上图可知,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    key_row    ,
5       input    swi      ,
6       output key_col    ,
7       output sel_t      ,
8       output seg_led_t,
9       output led
10
11      );
12
13//wire define
14wire     key_value    ;
15wire          key_flag   ;
16
17//*****************************************************
18//**                  main code                     
19//*****************************************************
20
21//矩阵键盘扫描模块
22key_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//数码管显示模块
32seg_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//拨码开关模块
42swi_led   u_swi_led(
43      .clk   (sys_clk),
44      .rst_n   (sys_rst_n),
45      .swi   (swi      ),
46      .led   (led      )
47);
48endmodule
顶层模块主要就是例化三个子模块,在这里就不作过多介绍了,下面我们直接开始看矩阵键盘扫描模块,矩阵键盘扫描模块的代码如下:
1   module key_4x4(
2       input                sys_clk   ,   //50MHZ
3       input                sys_rst_n ,
4       input           key_row   ,   //行
5   
6       output reg    key_col   ,   //列
7       output reg    key_value ,   //键值
8       output reg         key_flag
9   );
10
11//reg define
12reg state       ;//状态标志
13reg key_col_reg ;//寄存扫描列值
14reg key_row_reg ;//寄存扫描行值
15reg delay_cnt   ;
16reg key_reg_row ;
17reg del_cnt = 0 ;//延迟计数器
18reg       del_en=0    ;//延迟完成标志
19reg       key_flag_row;//消抖完成标志
20//*****************************************************
21//**                  main code                     
22//*****************************************************
23//按键消抖模块
24always @(posedge sys_clk or negedge sys_rst_n) begin
25      if (!sys_rst_n) begin
26          key_reg_row   <= 4'b1;
27          delay_cnt <= 32'd0;
28      end
29      else begin
30          key_reg_row <= key_row;                                                               
31            if(key_reg_row != key_row)               //如果按键被按下
32                  delay_cnt <= 32'd1000000;                      //消抖计数器为20ms
33            else if(key_reg_row == key_row) begin   
34                   if(delay_cnt > 32'd0)                                       
35                     delay_cnt <= delay_cnt - 1'b1;
36                   else
37                     delay_cnt <= delay_cnt;
38            end         
39      end   
40end
41
42         //按键消抖完成标志模块
43always @(posedge sys_clk or negedge sys_rst_n) begin
44      if (!sys_rst_n)
45          key_flag_row<= 1'b0;            
46      else begin
47            if(delay_cnt == 32'd1)            //消抖计数器等于1
48                  key_flag_row<= 1'b1;      //说明消抖标志完成
49            else if(del_en)
50                  key_flag_row<= 1'b0;
51      end   
52end
53
54         //按键扫描延迟打拍模块
55always @(posedge sys_clk or negedge sys_rst_n) begin
56      if (!sys_rst_n) begin
57          del_en <= 1'b0;
58          del_cnt <= 1'b0;
59      end   
60      else begin
61          if(del_cnt == 4'd3) begin               
62            del_cnt <= 1'd0;
63            del_en <= 1'b1;
64          end
65          else begin
66            del_cnt <= del_cnt + 1'b1;
67            del_en <= 1'b0;
68          end
69      end      
70end
71
72//按键扫描模块
73always @(posedge sys_clk or negedge sys_rst_n) begin
74      if(!sys_rst_n) begin
75          key_col<=4'b0000;
76          state<=0;
77      end
78      else begin
79          if(del_en) begin
80             case (state)                                                      
81                0:                                                               
82                   begin
83                      key_col<=4'b0000;
84                      key_flag<=1'b0;
85                         if((key_row!=4'b1111)&&(key_flag_row)) begin   
86                            state<=1;                                        //如果行扫描不都是高电平说明有按键按下
87                            key_col<=4'b1110;      //跳转到状态1并且先判断第一列
88                         end
89                         else
90                            state<=0;
91                   end
92                1:
93                   begin                                                                            //进入状态1
94                        if(key_row!=4'b1111)//如果行扫描仍没有全部拉高
95                           state<=5;                                    //说明就是第一列,跳转到状态5
96                        elsebegin
97                           state<=2;                                       //如果不是第一列则跳转状态二
98                           key_col<=4'b1101;//并且判断第二列
99                        end
100                   end
101                2:
102                  begin                                                                      //进入状态2
103                         if(key_row!=4'b1111)   //如果行扫描仍没有全部拉高
104                           state<=5;                                          //说明就是第二列,跳转到状态5
105                         elsebegin               
106                           state<=3;                                          //如果不是第二列则跳转状态三
107                           key_col<=4'b1011;//并且判断第三列
108                         end
109                  end
110               3:
111                  begin                                                                      //进入状态3
112                         if(key_row!=4'b1111)   //如果行扫描仍没有全部拉高
113                           state<=5;                                     //说明就是第三列,跳转到状态5
114                         else begin
115                           state<=4;                                          //如果不是第三列则跳转状态四
116                           key_col<=4'b0111;//并且判断第四列
117                         end
118                  end
119               4:
120                  begin                                                                     //进入状态4
121                         if (key_row!=4'b1111) //如果行扫描仍没有全部拉高
122                           state<=5;                                       //说明就是第四列,跳转到状态5
123                         else
124                           state<=0;
125                  end
126               5:
127                  begin                                                                           //进入状态4
128                         if(key_row!=4'b1111)begin
129                           key_col_reg<=key_col;//将列扫描的值赋值给列扫描寄存器
130                           key_row_reg<=key_row;//将行扫描的值赋值给行扫描寄存器
131                           state<=5;
132                           key_flag<=1'b1;
133                         end            
134                         else
135                           state<=0;
136                  end            
137         endcase
138         end   
139   end            
140 end
141
142 //按键赋值模块
143 always @ ( posedge sys_clk ) begin
144   if(key_flag==1'b1)
145       begin
146          case ({key_col_reg,key_row_reg})   //将列扫描寄存器与行扫描寄存器进行位拼接
147                                  //第一列按键的赋值      
148             8'b1110_1110:key_value<=4'd0;
149             8'b1110_1101:key_value<=4'd4;
150             8'b1110_1011:key_value<=4'd8;
151             8'b1110_0111:key_value<=4'd12;
152             //第二列按键的赋值
153             8'b1101_1110:key_value<=4'd1;
154             8'b1101_1101:key_value<=4'd5;
155             8'b1101_1011:key_value<=4'd9;
156             8'b1101_0111:key_value<=4'd13;
157             //第三列按键的赋值
158             8'b1011_1110:key_value<=4'd2;
159             8'b1011_1101:key_value<=4'd6;
160             8'b1011_1011:key_value<=4'd10;
161             8'b1011_0111:key_value<=4'd14;
162             //第四列按键的赋值
163             8'b0111_1110:key_value<=4'd3;
164             8'b0111_1101:key_value<=4'd7;
165             8'b0111_1011:key_value<=4'd11;
166             8'b0111_0111:key_value<=4'd15;
167         endcase
168       end   
169 end
170
171 endmodule
通过之前硬件设计我们已经了解了矩阵按键扫描原理,然后结合我们上面所写的代码可以进行分析,代码第24~52行是按键消抖模块,就是将按键的值先打一拍(key_reg <= key_row),然后检测当前时钟下按键的状态和上一个时钟的状态是否一致,如果不一致则将计数器delay_cnt赋初值20ms,如果一致则计数器从初值开始作减法计数,直到计数器计数到“1”,说明按键的状态一直稳定了20ms,此时我们认为是一次有效的按键触发,这时我们就可以拉高消抖完成标志key_flag_row(注意只拉高一个时钟)。
代码第53~68行是进行一个延迟打拍的操作,主要是为了让行扫描和列扫描在状态机中能够抓取到一个更加稳定状态。
代码第70~137行就是实现整个按键扫描的过程,它算一个简单的状态机,共有6个状态,其中状态0~4就是判断按键具体在哪一列,主要就是改变输出key_col的值,看key_row是否等于4'b1111,只要key_row不等于4'b1111就说明一定有按键按下,然后再看列端口的值,四个列端口只保留一个端口为低电平,其余都为高电平,这样只有当被按下的按键刚好处于列端口为低电平的位置时key_row才能不等于4'b1111,因为按键闭合会使行端口上拉3.3V与列端口低电平导通,行端口的值被下拉成0,否则行端口会一直处于上拉状态即key_row等于4'b1111。按照这个原理我们就把被按下的按键行列位置找到了,接下来就进入状态5将行列值寄存下来。
最后代码139~165行会根据行列值把按键的编号翻译出来,然后传递给数码管模块去显示。
数码管显示模块(seg_led)的代码,如下所示:
1   module seg_led(
2       input      clk          ,
3       input      rst_n      ,
4       input key_value    ,
5       input      key_flag   ,
6       output sel_t      ,
7       output seg_led_t   
8   );
9   
10//reg define
11reg sel    ;
12reg seg_led;
13
14//*****************************************************
15//**                  main code                     
16//*****************************************************
17
18assign sel_t   = ~sel    ;//共阴极接法这里取反,如果共阳极这里就不取反
19assign seg_led_t = ~seg_led;//共阴极接法这里取反,如果共阳极这里就不取反
20
21always @(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;
28end
29
30always@(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;
57end   
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       swi    ,
5       output reg    led   
6   );
7   
8   //*****************************************************
9   //**                  main code                     
10//*****************************************************
11
12always @(posedge clk or negedge rst_n)begin
13      if(!rst_n)
14               led <= 8'b0000_0000;
15      else
16          led <= swi;
17end
18
19endmodule
拨码开关模块(swi_led)的代码比较简洁,它使用了一个always语句块,把拨码开关的值赋给led灯的阳极端口用来控制led灯。
15.5下载验证
首先我们将下载器与开发板上的JTAG接口连接,下载器另外一端与电脑连接。然后将IO扩展板模块插到板子上的P3扩展口去,最后连接开发板的电源,并打开电源开关,如下图所示:
图 15.5.1硬件连接图
回到Pango界面,我们将生成好的bit流文件下载到开发板中去,按下矩阵按键就可以看到数码管会显示矩阵按键的编号(注意是16进制显示),上下拨动拨码开关就可以控制led灯亮灭,效果如下图所示:
图 15.5.2 最终效果图
页: [1]
查看完整版本: 《ATK-DFPGL22G之FPGA开发指南_V1.0》第十五章IO扩展模块实验