STEP-HR03-U81 快速入门
小脚丫 STEP FPGA
日期:2026/7/15
目录
- 小脚丫 STEP-HR03-U81
- 实验一 点亮 LED
- 实验二 RGB LED 与光源三基色的混合
- 实验三 3-8 译码器
- 实验四 数码管显示
- 实验五 时钟分频
- 实验六 LED 流水灯
- 实验七 按键消抖
- 实验八 计时控制
- 实验九 脉宽调制
- 实验十 简易交通灯
小脚丫 STEP-HR03-U81
本文档为FPGA初学者入门项目,以STEP-HR03-U81(核心芯片HR03PN0U81C7)版本核心板硬件为例,使用Verilog作为编程语言,目的是让初学者快速了解基本的逻辑实现和FPGA的编程过程,我们由浅入深,从如何点亮LED、数码管到利用状态机去完成交通灯的设计。开发板具体的硬件信息请查看文档STEP-HR03-U81硬件手册。
实验一 点亮LED
在这个系列教程里你将更深入学习FPGA的设计同时更深入了解我们的小脚丫。如果你还没有开始使用小脚丫,也可以从这里一步一步开始你的可编程逻辑学习。请先准备好软硬件文档,因为FPGA的设计是和硬件息息相关,会经常用到这些文档。你可以先安装好Fuxi设计工具,这是京微齐力推出的设计工具,或者使用 STEP FPGA 在线编程平台 设计。对于很多刚刚接触电子硬件的人来说,第一件事就是点亮LED,在这个 例程里,将带您实现这一功能。
1. 硬件说明
STEP-HR03-U81开发板虽然很小巧,上面也集成了不少外设,在本实验里我们就看看如何用FPGA控制简单外设,如何用按键或者开关控制LED的亮和灭。

图 1-1 STEP-HR03-U81板载LED硬件电路图
这是开发板上的8个红色LED,LED1~8信号连接到FPGA的引脚,作为FPGA输出信号控制。当FPGA输出低电平时LED变亮,当FPGA输出高电平时LED熄灭。

图 1-2 STEP-HR03-U81板载轻触按键硬件电路图

图 1-3 STEP-HR03-U81板载拨码开关硬件电路图
这是开发板上4个按键和4个开关,Key1~4是按键控制信号,SW1~4是开关控制信号,都连接到FPGA的引脚,作为FPGA的输入信号。当按键断开时,FPGA输入为高电平,当按键按下时,FPGA输入为低电平;当开关断开(OFF)时,FPGA输入为低电平,当开关合上(ON)时,FPGA输入为高电平。
所以我们可以用开关或者按键来控制LED的亮灭。
2. Verilog代码
module HR3_LED (key,sw,led);
input [3:0] key; //按键输入信号
input [3:0] sw; //开关输入信号
output [7:0] led; //输出信号到LED
assign led = {key,sw};//assign连续赋值。大括 号是拼接符,表示把key和sw拼接组成一个新的8位数赋值给led
endmodule
3. 引脚分配
综合(synthesize)完成之后一定要配置FPGA的引脚到相应的外设,这样下载FPGA程序后才能达到我们想要的效果。
| 信号名称 | 分配管脚 | 信号名称 | 分配管脚 |
|---|---|---|---|
| LED[0] | F3 | SW[0] | D6 |
| LED[1] | E3 | SW[1] | D7 |
| LED[2] | D3 | SW[2] | D8 |
| LED[3] | C3 | SW[3] | E8 |
| LED[4] | C4 | KEY[0] | E2 |
| LED[5] | E4 | KEY[1] | E1 |
| LED[6] | G4 | KEY[2] | D1 |
| LED[7] | H4 | KEY[3] | D2 |
4. 小结
下载完程序后就可以实现按键开关控制LED灯的亮灭。了解小脚丫STEP-HR03-U81上的外设LED、按键和开关的使用。
实验二 RGB LED与光源三基色的混合
在这个实验里我们将学习控制小脚丫STEP-HR03-U81上的RGB三色LED的显示。
1. 硬件说明
STEP-HR03-U81开发板上面有两个级联的WS2812 RGB LED灯,我们可以用按键或者开关控制两个WS2812的显示。

图 2-1 STEP-HR03-U81板载LED硬件电路图
这是开发板上集成了2个WS2812 RGB LED,采用的是级联方式连接,前级WS2812的DOUT引脚连接到后级WS2812的DIN引脚上,形成串行数据链路。第一级WS2812的DIN引脚连到FPGA上的COLOR引脚上,由FPGA输出控制信号,实现对两颗WS2812的颜色控制。
WS2812接收到的数据按照固定格式传输,每颗LED包含24位颜色数据,其中高8位为绿色(G)数据,中间8位为红色(R)数据,低8位为蓝色(B)数据,即GRB格式。数据传输时,FPGA按照WS2812规定的时序发送0和1对应的高低电平脉冲,WS2812内部电路根据接收到的数据更新对应LED的亮度。
在级联模式下,FPGA发送的一组数据会依次经过各级WS2812。第一个WS2812接收到前24位数据后,将其锁存为自身的颜色控制数据,并将后续数据通过DOUT继续发送给下一级WS2812。因此,FPGA只需要连接第一级WS2812的DIN,就可以实现对多个WS2812 RGB LED的独立控制。

2. Verilog代码
在每帧数据发送开始时,将输入数据寄存到 sw_r 中,并在整个发送周期保持不变,避免输入数据在发送过程中发生变化而导致显示异常。
always@(posedge clk_in)begin
sw_r <= (bit_cnt=='d0)?sw:sw_r;
duty_r <= 4'd7; // less than 8
end
亮度调整并完成48bit数据的拼接。
assign Light_led_1_g = Color_led_1_g << duty_r;
assign Light_led_1_r = Color_led_1_r << duty_r;
assign Light_led_1_b = Color_led_1_b << duty_r;
assign Light_led_0_g = Color_led_0_g << duty_r;
assign Light_led_0_r = Color_led_0_r << duty_r;
assign Light_led_0_b = Color_led_0_b << duty_r;
assign led0_data = {Light_led_1_g,Light_led_1_r,Light_led_1_b,
Light_led_0_g,Light_led_0_r,Light_led_0_b};
根据拨码开关确定WS2812要显示的颜色。
always@(posedge clk_in or negedge rst_n_in)
begin
if(!rst_n_in) begin
Color_led_1_g <= 'b0;
Color_led_1_r <= 'b0;
Color_led_1_b <= 'b0;
Color_led_0_g <= 'b0;
Color_led_0_r <= 'b0;
Color_led_0_b <= 'b0;
end else case (sw_r)
4'd0: begin
Color_led_1_g <= 'b0;
Color_led_1_r <= 'b0;
Color_led_1_b <= 'h03;
Color_led_0_g <= 'b0;
Color_led_0_r <= 'b0;
Color_led_0_b <= 'b0;
end
……
4'd7: begin
Color_led_1_g <= 'b0;
Color_led_1_r <= 'b0;
Color_led_1_b <= 'b0;
Color_led_0_g <= 'h03;
Color_led_0_r <= 'h03;
Color_led_0_b <= 'h03;
end
default: begin
Color_led_1_g <= 'b0;
Color_led_1_r <= 'b0;
Color_led_1_b <= 'b0;
Color_led_0_g <= 'b0;
Color_led_0_r <= 'b0;
Color_led_0_b <= 'b0;
end
endcase
end
数据发送过程中,每一位数据均按照WS2812通信时序进行输出。程序首先根据当前发送的数据位 current_bit 判断需要发送逻辑"0"还是逻辑"1",然后利用计数器 clk_cnt 精确控制输出信号高、低电平持续时间。当发送逻辑"1"时,高电平持续时间为 T1H;当发送逻辑"0"时,高电平持续时间为 T0H。由于两种数据位的总周期均为 BIT_TOTAL,因此只需控制高电平持续时间即可生成符合WS2812协议要求的波形。
wire current_bit;
assign current_bit = led0_data[47 - bit_cnt];
always @(posedge clk_in or negedge rst_n_in) begin
if (!rst_n_in) begin
ws2812_dout <= 1'b0;
reset_cnt <= 0;
bit_cnt <= 0;
clk_cnt <= 0;
sending <= 1'b0;
end
else begin
// ---------------------------
// reset period
// ---------------------------
if (!sending) begin
ws2812_dout <= 1'b0;
if (reset_cnt < RESET_TIME - 1) begin
reset_cnt <= reset_cnt + 1'b1;
end
else begin
reset_cnt <= 0;
sending <= 1'b1;
bit_cnt <= 0;
clk_cnt <= 0;
end
end
// ---------------------------
// sending data
// ---------------------------
else begin
clk_cnt <= clk_cnt + 1'b1;
// output waveform
if (current_bit) begin
// send logic 1
if (clk_cnt < T1H)
ws2812_dout <= 1'b1;
else
ws2812_dout <= 1'b0;
end
else begin
// send logic 0
if (clk_cnt < T0H)
ws2812_dout <= 1'b1;
else
ws2812_dout <= 1'b0;
end
// one bit finished
if (clk_cnt == BIT_TOTAL - 1) begin
clk_cnt <= 0;
if (bit_cnt == 47) begin
sending <= 1'b0;
bit_cnt <= 0;
ws2812_dout <= 1'b0;
end
else begin
bit_cnt <= bit_cnt + 1'b1;
end
end
end
end
end
3. 引脚分配
综合(synthesize)完成之后一定要配置FPGA的引脚到相应的外设,这样下载FPGA程序后才能达到我们想要的效果。
| 信号名称 | 分配管脚 | 信号名称 | 分配管脚 |
|---|---|---|---|
| clk_in | D5 | sw[0] | D6 |
| rst_n_in | D2 | sw[1] | D7 |
| ws2812_dout | C5 | sw[2] | D8 |
下载完程序后就可以实现3个开关控制WS2812 RGB LED灯的不同颜色显示,PS:小心比较刺眼。
4. 小结
了解小脚丫STEP-HR03-U81的外设WS2812 RGB LED灯。之前实验都是开关和按键直接控制LED,在下一个实验3-8译码器将学习如何用组合逻辑实现控制LED显示。
实验三 3-8译码器
在这个实验里我们将学习如何用Verilog来实现组合逻辑。
1. 硬件说明
组合逻辑电路是数字电路的重要部分,电路的输出只与输入的当前状态相关的逻辑电路,常见的有选择器、比较器、译码器、编码器、编码转换等等。在本实验里以最常见的3-8译码器为例说明如何用Verilog实现。3-8译码器的真值表如下:

图 3-1 3-8译码器真值表
从前面的实验可以知道,当FPGA输出信号到LED为高电平时LED熄灭,反之LED变亮。同时我们可以以开关的信号模拟3-8译码器的输入,这样控制开关我们就能控制特定的LED变亮。
2. Verilog代码
module HR3_decode38 (sw,led);
input [2:0] sw;//开关输入信号,利用了其中3个开关作为3-8译码器的输入
output [7:0] led;//输出信号控制特定LED
reg [7:0] led; //定义led为reg型变量,在always过程块中只能对reg型变量赋值
//always过程块,括号中sw为敏感变量,当sw变化一次执行一次always中所有语句,否则保持不变
always @ (sw)
begin
case(sw) //case语句,一定要跟default语句
3'b000: led=8'b0111_1111; //条件跳转,其中“_”下划线只是为了阅读方便,无实际意义
3'b001: led=8'b1011_1111; //位宽'进制+数值是Verilog里常数的表达方法,进制可以是b、o、d、h(二、八、十、十六进制)
3'b010: led=8'b1101_1111;
3'b011: led=8'b1110_1111;
3'b100: led=8'b1111_0111;
3'b101: led=8'b1111_1011;
3'b110: led=8'b1111_1101;
3'b111: led=8'b1111_1110;
default: ;
endcase
end
endmodule
3. 引脚分配
综合(synthesize)完成之后一定要配置FPGA的引脚到相应的外设,这样下载FPGA程序后才能达到我们想要的效果。
| 信号名称 | 分配管脚 | 信号名称 | 分配管脚 |
|---|---|---|---|
| led[0] | F3 | sw[0] | D6 |
| Led[ 1] | E3 | sw[1] | D7 |
| led [2] | D3 | sw[2] | D8 |
| led [3] | C3 | ||
| led [4] | C4 | ||
| led [5] | E4 | ||
| led [6] | G4 | ||
| led [7] | H4 |
下载完程序后就可以实现3个开关控制不同LED灯的显示,3-8译码器完成。
4. 小结
实现了一个简单的组合逻辑3-8译码器,在下一个数码管显示实验我们将学习如何通过译码实现控制数码管的显示。
实验四 数码管显示
本实验将会让你熟悉小脚丫上最后一种有意思的外设七段数码管。
1. 硬件说明
数码管是工程设计中使用很广的一种显示输出器件。一个7段数码管(如果包括右下的小点可以认为是8段)分别由a、b、c、d、e、f、g位段和表示小数点的dp位段组成。实际是由8个LED灯组成的,控制每个LED的点亮或熄灭实现数字显示。通常数码管分为共阳极数码管和共阴极数码管,结构如下图所示:

图 4-1 数码管内部结构图
共阴8段数码管的信号端低电平有效,而共阳端接高电平有效。当共阳端接高电平时只要在各个位段上加上相应的低电平信号就可以使相应的位段发光。比如:要使a段发光,则在a段信号端加上低电平即可。共阴极的数码管则相反。 可以看到数码管的控制和LED的控制有相似之处,在小脚丫STEP-HR03-U81开发板上有两位共阴极数码管,这里使用了74HC595芯片,使用串行传输节省FPGA引脚 。

图 4-2 小脚丫STEP-HR03-U81板载数码管电路图
数码管所有的信号都连接到FPGA的管脚,作为输出信号控制。核心板上的数码管是共阴极,FPGA只要将对应的DIG引脚拉低,就可以输出信号控制数码管的某一段LED亮或者灭。这样我们可以通过开关来控制FPGA的输出,和3-8译码器实验一样,通过组合逻辑的输出来控制数码管显示数字,下面是数码管显示的表格:

这其实是一个4-16译码器,如果我们想数码管能显示16进制可以全译码,如果只想显示数字,可以只利用其中10个译码,下面看看如果用Verilog来实现。
2. Verilog代码
首先创建一个数码管的字库,字库数据与段码有关,这里字库数据 [MSB~LSB]={G,F,E,D,C,B,A}。
reg[6:0] seg [15:0];
always @(*) begin
seg[0] = 7'h3f; // 0
seg[1] = 7'h06; // 1
seg[2] = 7'h5b; // 2
seg[3] = 7'h4f; // 3
seg[4] = 7'h66; // 4
seg[5] = 7'h6d; // 5
seg[6] = 7'h7d; // 6
seg[7] = 7'h07; // 7
seg[8] = 7'h7f; // 8
seg[9] = 7'h6f; // 9
seg[10] = 7'h77; // A
seg[11] = 7'h7c; // b
seg[12] = 7'h39; // C
seg[13] = 7'h5e; // d
seg[14] = 7'h79; // E
seg[15] = 7'h71; // F
end
计数器分频并产生分频脉冲信号。
//计数器对系统时钟信号进行计数
reg [9:0] cnt = 1'b0;
always@(posedge clk or negedge rst_n) begin
if(!rst_n) cnt <= 1'b0;
else if(cnt>=(CNT_40KHz-1)) cnt <= 1'b0;
else cnt <= cnt + 1'b1;
end
//根据计数器计数的周期 产生分频的脉冲信号
reg clk_40khz = 1'b0;
always@(posedge clk or negedge rst_n) begin
if(!rst_n) clk_40khz <= 1'b0;
else if(cnt<(CNT_40KHz>>1)) clk_40khz <= 1'b0;
else clk_40khz <= 1'b1;
end
状态机完成74HC595时序。
//使用状态机完成数码管的扫描和74HC595时序的实现
reg [15:0] data;
reg cnt_main;
reg [1:0] dig_r;
reg [5:0] cnt_write;
reg [2:0] state = IDLE;
reg [3:0] pwm_cnt;
always@(posedge clk_40khz or negedge rst_n) begin
if(!rst_n) begin //复位状态下,各寄存器置初值
state <= IDLE;
cnt_main <= 3'd0; cnt_write <= 6'd0;
seg_din <= 1'b0; seg_sck <= LOW; seg_rck <= LOW;
pwm_cnt <= 'b0;
end else begin
case(state)
IDLE:begin //IDLE作为第一个状态,相当于软复位
state <= MAIN;
cnt_main <= 3'd0; cnt_write <= 6'd0;
seg_din <= 1'b0; seg_sck <= LOW; seg_rck <= LOW;
pwm_cnt <= 'b0;
end
MAIN:begin
cnt_main <= (cnt_main == 'd1)?'d0:cnt_main + 1'b1;
state <= WRITE; //在配置完发给74HC595的数据同时跳转至WRITE状态,完成串行时序
case(cnt_main)
//对8位数码管逐位扫描
//data [15:8]为段选, [7:0]为位选
3'd0: begin
data <= {dot_en[0],seg[sw_r]};
dig_r <= (dat_en[0] && (pwm_cnt <= duty))?2'b10:2'b11; end
3'd1: begin
data <= {dot_en[1],seg[sw_r]};
dig_r <=(dat_en[1] && (pwm_cnt <= duty))?2'b01:2'b11; end
default: data <= {8'h00,8'hff};
endcase
end
WRITE:begin
if(cnt_write >= 6'd17) cnt_write <= 1'b0;
else cnt_write <= cnt_write + 1'b1;
case(cnt_write)
//74HC595是串行转并行的芯片,3路输入可产生8路输出,而且可以级联使用
//74HC595的时序实现,参考74HC595的芯片手册
6'd0: begin seg_sck <= LOW; seg_din <= data[7]; end
6'd1: begin seg_sck <= HIGH; end
6'd2: begin seg_sck <= LOW; seg_din <= data[6]; end
6'd3: begin seg_sck <= HIGH; end
6'd4: begin seg_sck <= LOW; seg_din <= data[5]; end
6'd5: begin seg_sck <= HIGH; end
6'd6: begin seg_sck <= LOW; seg_din <= data[4]; end
6'd7: begin seg_sck <= HIGH; end
6'd8: begin seg_sck <= LOW; seg_din <= data[3]; end
6'd9: begin seg_sck <= HIGH; end
6'd10: begin seg_sck <= LOW; seg_din <= data[2]; end
6'd11: begin seg_sck <= HIGH; end
6'd12: begin seg_sck <= LOW; seg_din <= data[1]; end
6'd13: begin seg_sck <= HIGH; end
6'd14: begin seg_sck <= LOW; seg_din <= data[0]; end
6'd15: begin seg_sck <= HIGH; end
6'd16: begin
seg_rck <= HIGH;
dig <= dig_r;
if(pwm_cnt == 'd6)
pwm_cnt <= 'd0;
else
pwm_cnt <= pwm_cnt + 1'b1;
end //当16位数据传送完成后RCK拉高,输出生效
6'd17: begin seg_rck <= LOW; state <= MAIN; end
default: ;
endcase
end
default: state <= IDLE;
endcase
end
end
3. 引脚分配
综合(synthesize)完成之后一定要配置FPGA的引脚到相应的外设,这样下载FPGA程序后才能达到我们想要的效果。按照下面表格定义输入输出信号:
| 信号 | 引脚 | 信号 | 引脚 |
|---|---|---|---|
| clk | D5 | dig[0] | C7 |
| rst_n | D2 | dig[1] | E7 |
| sw[0] | D6 | seg_sck | B4 |
| sw[1] | D7 | seg_rck | B5 |
| sw[2] | D8 | seg_din | B6 |
| sw[3] | E8 |
配置好以后编译下载程序。这样可以通过按键或者开关来控制相应的数码管显示数字。
4. 小结
了解了小脚丫数码管的工作原理,在下个实验我们将进行到有趣的时序逻辑。首先是如何控制时钟分频。
实验五 时钟分频
在之前的实验中我 们已经熟悉了小脚丫的各种外设,掌握了verilog的组合逻辑设计,接下来我们将学习时序逻辑的设计。
1. 硬件说明
时钟信号的处理是FPGA的特色之一,因此分频器也是FPGA设计中使用频率非常高的基本设计之一。一般在FPGA中都有集成的锁相环可以实现各种时钟的分频和倍频设计,但是通过语言设计进行时钟分频是最基本的训练,在对时钟要求不高的设计时也能节省锁相环资源。在本实验中我们将实现任意整数的分频器,分频的时钟保持50%占空比。
偶数分频:偶数倍分频相对简单,比较容易理解。通过计数器计数是完全可以实现的。如进行N倍偶数分频,那么通过时钟触发计数器计数,当计数器从0计数到N/2-1时,输出时钟进行翻转,以此循环下去。
图 4-1 偶数分频时序图
奇数分频: 如果要实现占空比为50%的奇数倍分频,不能同偶数分频一样计数器记到一半的时候输出时钟翻转,那样得不到占空比50%的时钟。以待分频时钟CLK为例,如果以偶数分频的方法来做奇数分频,在CLK上升沿触发,将得到不是50%占空比的一个时钟信号(正周期比负周期多一个时钟或者少一个时钟);但是如果在CLK下降沿也触发,又得到另外一个不是50%占空比的时钟信号,这两个时钟相位正好相差半个CLK时钟周期。通过这两个时钟信号进行逻辑运算我们可以巧妙的得到50%占空比的时钟。
总结如下:对于实现占空比为50%的N倍奇数分频,首先进行上升沿触发进行模N计数,计数选定到某一个值进行输出时钟翻转,然后经过(N-1)/2再次进行翻转得到一个占空比非50%奇数n分频时钟。再者同时进行下降沿触发的模N计数,到和上升沿触发输出时钟翻转选定值相同值时,进行输出时钟时钟翻转,同样经过(N-1)/2时,输出时钟再次翻转生成占空比非50%的奇数n分频时钟。两个占空比非50%的n分频时钟进行逻辑运算(正周期多的相与,负周期多的相或),得到占空比为50%的奇数n分频时钟。

图 4-1 奇数分频时序图
2. Verilog代码
module HR3_divde ( clk,rst_n,clkout);
input clk,rst_n; //输入信号,其中clk连接到FPGA的C1脚,频率为12MHz
output clkout; //输出信号,可以连接到LED观察分频的时钟
//parameter是verilog里常数语句
parameter WIDTH = 24; //计数器的位数,计数的最大值为 2**WIDTH-1
parameter N = 12_000_000; //分频系数,请确保 N < 2**WIDTH-1,否则计数会溢出
reg [WIDTH-1:0] cnt_p,cnt_n; //cnt_p为上升沿触发时的计数器,cnt_n为下降沿触发时的计数器
reg clk_p,clk_n; //clk_p为上升沿触发时分频时钟,clk_n为下降沿触发时分频时钟
//上升沿触发时计数器的控制
always @ (posedge clk or negedge rst_n ) //posedge和negedge是verilog表示信号上升沿和下降沿
//当clk上升沿来临或者rst_n变低的时候执行一次always里的语句
begin
if(!rst_n)
cnt_p<=0;
else if (cnt_p==(N-1))
cnt_p<=0;
else cnt_p<=cnt_p+1; //计数器一直计数,当计数到N-1的时候清零,这是一个模N的计数器
end
//上升沿触发的分频时钟输出,如果N为奇数得到的时钟占空比不是50%;如果N为偶数得到的时钟占空比为50%
always @ (posedge clk or negedge rst_n)
begin
if(!rst_n)
clk_p<=0;
else if (cnt_p<(N>>1)) //N>>1表示右移一位,相当于除以2去掉余数
clk_p<=0;
else
clk_p<=1; //得到的分频时钟正周期比负周期多一个clk时钟
end
//下降沿触发时计数器的控制
always @ (negedge clk or negedge rst_n)
begin
if(!rst_n)
cnt_n<=0;
else if (cnt_n==(N-1))
cnt_n<=0;
else cnt_n<=cnt_n+1;
end
//下降沿触发的分频时钟输出,和clk_p相差半个时钟
always @ (negedge clk)
begin
if(!rst_n)
clk_n<=0;
else if (cnt_n<(N>>1))
clk_n<=0;
else
clk_n<=1; //得到的分频时钟正周期比负周期多一个clk时钟
end
assign clkout = (N==1)?clk:(N[0])?(clk_p&clk_n):clk_p; //条件判断表达式
//当N=1时,直接输出clk
//当N为偶数也就是N的最低位为0,N(0)=0,输出clk_p
//当N为奇数也就是N最低位为1,N(0)=1,输出clk_p&clk_n。正周期多所以是相与
endmodule
测试文件,进行功能仿真时需要编写testbench测试文件。verilog里的testbench文件和源文件一样也是.v文件,仿真能让我们更直观的观察信号波形。这里我们可以使用Modelsim等仿真工具进行仿真。
`timescale 1ns/100ps //仿真时间单位/时间精度,时间单位要大于或者等于时间精度
module divide_tb(); //测试文件也是一个module,因为用于仿真所以无需输入输出信号
reg clk,rst_n; //需要产生的激励信号定义,激励信号需要过程块产生所以定义为reg型变量
wire clkout; //需要观察的输出信号定义,定义为wire型变量
//初始化过程块
initial
begin
clk = 0;
rst_n = 0;
#25 //#表示延时25个时间单位
rst_n = 1; //产生了一个初始25ns低电平,然后变高电平的复位信号
end
always #10 clk = ~clk; //每隔10ns翻转一次clk信号,也就是产生一个时钟周期20ns的clk,频率为50MHz
//module调用例化格式
divide #(.WIDTH(4),.N(11)) u1 ( //#后面的()中为参数传递,如果不传递参数就是所调用模块中的参数默认值
//divide表示所要例化的module名称,u1是我们定义的例化名称,必须以字母开头
.clk (clk), //输入输出信号连接。 .clk表示module本身定义的信号名称;(clk)表示我们在这里定义的激励信号
.rst_n (rst_n), //在testbench里定义的信号名称可以与所要调用module的端口信号名称不同
.clkout (clkout)
);
endmodule
3. 引脚分配
小脚丫上的系统时钟连接到FPGA的D5脚,时钟为12MHz。你可以通过仿真波形观察分频时钟(注意仿真的时间是有限的,所以分频时钟频率需要较高)。如果我们想通过眼睛观察LED的闪烁,那么需要设置参数N和WIDTH得到一个频率较低的时钟(例如N=12000000,WIDTH=24,分频时钟周期为1秒)。
| 信号 | 引脚 |
|---|---|
| clk | D5 |
| rst_n | D2 |
| clkout | F3 |
修改程序中的分频系数和计数器位数就能够调整LED闪烁速度(注意计数的最大值一定要保证超过分频系数N)。
4. 小结
在本实验学习了如何进行任意整数的分频设计,我们产生各种时钟,通过修改程序还能实验调整输出时钟的频率、相位以及占空比,非常灵活。同时学习了如何编写testbench文件,了解verilog中如何例化module,在后面的学习中将会经常用到。在下个实验我们将进一步了解时序逻辑,如何利用时钟来进一步设计,请看最常见的LED流水灯。
实验六 LED流水灯
在时钟分频实验中我们练习了如何处理时钟,接下来我们要学习如何利用时钟来完成时序逻辑。
1. 硬件说明
流水灯实现是很常见的一个实验,虽然逻辑比较简单,但是里面也包含了实现时序逻辑的基本思想。要用FPGA实现流水灯有 很多种方法,在这里我们会用两种不同的方法实现。
(1)模块化设计
在之前的实验中我们做了3-8译码器和时钟分频,如果把这两个结合起来,我们就能搭建一个自动操作的流水LED显示。框图如下:

(2)循环赋值
这是一种很简洁的实现流水灯效果逻辑,就是定义一个8位的变量,在每个时钟上升沿将最低位赋值给最高位,其他位右移一位,这就实现了循环赋值。这8位输出到LED就能实现流水灯。
2. Verilog代码
模块化设计是用硬件描述语言进行数字电路设计的精髓,代码可重复利用。而且模块化的设计使得程序的结构也很清晰。这里我们首先看看流水灯的模块化设计。利用了之前的3-8译码器和分频器,你需要把这两个程序也拷贝到一个工程。
module HR3_flashled (clk,rst,led);
input clk,rst;
output [7:0] led;
reg [2:0] cnt ;//定义了一个3位的计数器,输出可以作为3-8译码器的输入
wire clk1h;//定义一个中间变量,表示分频得到的时钟,用作计数器的触发
//例化module decode38,相当于调用
decode38 u1 (
.sw(cnt),//例化的输入端口连接到cnt,输出端口连接到led
.led(led)
);
//例化分频器模块,产生一个1Hz时钟信号
divide #(.WIDTH(32),.N(12000000)) u2 (//传递参数
.clk(clk),
.rst_n(rst),//例化的端口信号都连接到定义好的信号
.clkout(clk1h)
);
//1Hz时钟上升沿触发计数器,循环计数
always @(posedge clk1h or negedge rst)
if (!rst)
cnt <= 0;
else
cnt <= cnt +1;
endmodule
模块化设计结构清晰,verilog语言是很灵活的。对于流水灯还有一种很简洁的实现方法:
3.引脚分配
按照下面表格定义输入输出信号 :
| 信号 | 引脚 | 信号 | 引脚 |
|---|---|---|---|
| clk | D5 | led[3] | C3 |
| rst | D2 | led[4] | C4 |
| led[0] | F3 | led[5] | E4 |
| led[1] | E3 | led[6] | G4 |
| led[2] | D3 | led[7] | H4 |
配置好以后编译下载程序。可以调整例化分频器时传递的参数来调整流水灯的速度。
实验七 按键消抖
在之前的实验中我们学习了如何用按键作为FPGA的输入控制,在本实验中将学习如何进行按键消抖,用按键完成更多的功能。
1. 硬件说明
按键是一种常用的电子开关,电子设计中不可缺少的输入设备。当按下时使开关导通,松开时则开关断开,内部结构是靠金属弹片来实现通断。

2. 按键去抖原理:
抖动的产生:
通常的按键所用的开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马 上稳定地接通,在断开时也不会一下子断开。因而在闭合及断开的瞬间均伴随有一连串的抖动,为了不产生这种现象而作的措施就是按键消抖。
消除抖动的措施:
一般我们采用软件方法消抖。即检测到按键按下动作之后进行10ms~20ms左右的延时,当前沿的抖动消失之后再一次检测按键的状态。如果仍然是按下的电平状态,则认为这是一次真正的按键按下;同样检测到按键释放,也要做10ms~20ms延时,检测到后沿抖动消失后认为是一个完整的按键弹起过程。
消抖的好处:
执行按键消抖有两个好处:
消除误触发:我们想通过按键来翻转信号(例如按下一次led亮,在按一次led灭),如果没有进行消抖,则会产生很多误触发造成不必要的翻转。
记录按键次数:执行按键消抖可以让我们记录按键动作的次数,在很多应用里这非常有用。
在点亮LED实验中我们知道了小脚丫板子上按键的设计,当按键未被按下时,连接到FPGA管脚认为是高电平;当按键被按下时,连接到FPGA管脚认为是低电平。
要消除按键的抖动,我们需要去扫描按键,也就是不断的去采集按键的状态。软件消抖时我们一般只考虑按键按下时的抖动,而放弃对释放时抖动的消除。用系统时钟(频率较高)去采集按键状态,当检测到按下时用计数器延时20ms,再去检测按键状态,如果这时仍为按下状态,确认是一次按下动作,否侧的话认为无按键按下。如何检测按键状态变化就需要用到脉冲边沿检测的方法。