跳到主要内容

STEP-HR03-U81 快速入门

小脚丫 STEP FPGA
日期:2026/7/15

目录

小脚丫 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的亮和灭。

image1.png

图 1-1 STEP-HR03-U81板载LED硬件电路图

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

image2.png

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

image3.png

图 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]F3SW[0]D6
LED[1]E3SW[1]D7
LED[2]D3SW[2]D8
LED[3]C3SW[3]E8
LED[4]C4KEY[0]E2
LED[5]E4KEY[1]E1
LED[6]G4KEY[2]D1
LED[7]H4KEY[3]D2

4. 小结

下载完程序后就可以实现按键开关控制LED灯的亮灭。了解小脚丫STEP-HR03-U81上的外设LED、按键和开关的使用。

实验二 RGB LED与光源三基色的混合

在这个实验里我们将学习控制小脚丫STEP-HR03-U81上的RGB三色LED的显示。

1. 硬件说明

STEP-HR03-U81开发板上面有两个级联的WS2812 RGB LED灯,我们可以用按键或者开关控制两个WS2812的显示。

image4.png

图 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的独立控制。

b4b6ce79b4db77f70f7b5724b00115c9

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_inD5sw[0]D6
rst_n_inD2sw[1]D7
ws2812_doutC5sw[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译码器的真值表如下:

d5252fa5-4dfc-442a-8d07-3b0920de51a3

图 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]F3sw[0]D6
Led[ 1]E3sw[1]D7
led [2]D3sw[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的点亮或熄灭实现数字显示。通常数码管分为共阳极数码管和共阴极数码管,结构如下图所示:

d84ad1d4-6017-4917-aa28-f2b8f59e9068

图 4-1 数码管内部结构图

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

image8.png

图 4-2 小脚丫STEP-HR03-U81板载数码管电路图

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

db9bf49c-0699-4cb2-9c4f-a9fbeef08093

这其实是一个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程序后才能达到我们想要的效果。按照下面表格定义输入输出信号:

信号引脚信号引脚
clkD5dig[0]C7
rst_nD2dig[1]E7
sw[0]D6seg_sckB4
sw[1]D7seg_rckB5
sw[2]D8seg_dinB6
sw[3]E8

配置好以后编译下载程序。这样可以通过按键或者开关来控制相应的数码管显示数字。

4. 小结

了解了小脚丫数码管的工作原理,在下个实验我们将进行到有趣的时序逻辑。首先是如何控制时钟分频。

实验五 时钟分频

在之前的实验中我们已经熟悉了小脚丫的各种外设,掌握了verilog的组合逻辑设计,接下来我们将学习时序逻辑的设计。

1. 硬件说明

时钟信号的处理是FPGA的特色之一,因此分频器也是FPGA设计中使用频率非常高的基本设计之一。一般在FPGA中都有集成的锁相环可以实现各种时钟的分频和倍频设计,但是通过语言设计进行时钟分频是最基本的训练,在对时钟要求不高的设计时也能节省锁相环资源。在本实验中我们将实现任意整数的分频器,分频的时钟保持50%占空比。 

偶数分频:偶数倍分频相对简单,比较容易理解。通过计数器计数是完全可以实现的。如进行N倍偶数分频,那么通过时钟触发计数器计数,当计数器从0计数到N/2-1时,输出时钟进行翻转,以此循环下去。 

image10.png

图 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分频时钟。 

image11.png

图 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秒)。

信号引脚
clkD5
rst_nD2
clkoutF3

修改程序中的分频系数和计数器位数就能够调整LED闪烁速度(注意计数的最大值一定要保证超过分频系数N)。

4. 小结

在本实验学习了如何进行任意整数的分频设计,我们产生各种时钟,通过修改程序还能实验调整输出时钟的频率、相位以及占空比,非常灵活。同时学习了如何编写testbench文件,了解verilog中如何例化module,在后面的学习中将会经常用到。在下个实验我们将进一步了解时序逻辑,如何利用时钟来进一步设计,请看最常见的LED流水灯。

实验六 LED流水灯

在时钟分频实验中我们练习了如何处理时钟,接下来我们要学习如何利用时钟来完成时序逻辑。

1. 硬件说明

流水灯实现是很常见的一个实验,虽然逻辑比较简单,但是里面也包含了实现时序逻辑的基本思想。要用FPGA实现流水灯有很多种方法,在这里我们会用两种不同的方法实现。

(1)模块化设计

在之前的实验中我们做了3-8译码器和时钟分频,如果把这两个结合起来,我们就能搭建一个自动操作的流水LED显示。框图如下:

078e62a2-a162-4af1-a98f-d711bde8c44d

(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.引脚分配

按照下面表格定义输入输出信号 :

信号引脚信号引脚
clkD5led[3]C3
rstD2led[4]C4
led[0]F3led[5]E4
led[1]E3led[6]G4
led[2]D3led[7]H4

配置好以后编译下载程序。可以调整例化分频器时传递的参数来调整流水灯的速度。

实验七 按键消抖

在之前的实验中我们学习了如何用按键作为FPGA的输入控制,在本实验中将学习如何进行按键消抖,用按键完成更多的功能。

1. 硬件说明

按键是一种常用的电子开关,电子设计中不可缺少的输入设备。当按下时使开关导通,松开时则开关断开,内部结构是靠金属弹片来实现通断。

5a56cacf-82eb-44d8-9f39-e5c90ce7ca5b

2. 按键去抖原理:

抖动的产生:

通常的按键所用的开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。因而在闭合及断开的瞬间均伴随有一连串的抖动,为了不产生这种现象而作的措施就是按键消抖。

消除抖动的措施:

一般我们采用软件方法消抖。即检测到按键按下动作之后进行10ms~20ms左右的延时,当前沿的抖动消失之后再一次检测按键的状态。如果仍然是按下的电平状态,则认为这是一次真正的按键按下;同样检测到按键释放,也要做10ms~20ms延时,检测到后沿抖动消失后认为是一个完整的按键弹起过程。

消抖的好处:

执行按键消抖有两个好处:

消除误触发:我们想通过按键来翻转信号(例如按下一次led亮,在按一次led灭),如果没有进行消抖,则会产生很多误触发造成不必要的翻转。

记录按键次数:执行按键消抖可以让我们记录按键动作的次数,在很多应用里这非常有用。

在点亮LED实验中我们知道了小脚丫板子上按键的设计,当按键未被按下时,连接到FPGA管脚认为是高电平;当按键被按下时,连接到FPGA管脚认为是低电平。

要消除按键的抖动,我们需要去扫描按键,也就是不断的去采集按键的状态。软件消抖时我们一般只考虑按键按下时的抖动,而放弃对释放时抖动的消除。用系统时钟(频率较高)去采集按键状态,当检测到按下时用计数器延时20ms,再去检测按键状态,如果这时仍为按下状态,确认是一次按下动作,否侧的话认为无按键按下。如何检测按键状态变化就需要用到脉冲边沿检测的方法。

3. 脉冲边沿检测原理

检测按键按下时要用到脉冲边沿检测的方法,捕捉信号的突变、捕捉时钟的上升下降沿等经常会用到这种方法。简单地说就是用一个频率更高的时钟去触发要检测的信号,用两个寄存器去储存相邻两个时钟采集到的值,然后进行异或运算,如果不为零,代表发生了上升沿或者下降沿。

在按键消抖的过程中,同样运用了脉冲边沿检测。用两个寄存器储存相邻时钟采集的值(例如data_pre,data),然后将data取反与前一个值相与(state=data_pre&(~data)),如果为1,则判断有下降沿既按键按下由高到低;否则无变化。

将一个信号由连续时钟采集,相邻两个钟触发的值存入两个寄存器。理解verilog实现这个过程要充分了解其中的非阻塞赋值。

4. Verilog代码

本实验主要通过按键来控制led的翻转,当按下一次led变亮,再按下一次led变暗。首先我们做个试验,对按键不做处理通过按键来控制led翻转。

module top(
key, //按键输入
rst, //复位输入
led //led输出
);

input key,rst;
output reg led;

always @(key or rst)
if (!rst) //复位时led熄灭
led = 1;
else if(key == 0)
led = ~led; //按键按下时led翻转
else
led = led;
endmodule

未经过消抖的程序下载到小脚丫上会发现按键有时不能够控制led翻转,这是因为按键的抖动造成了led状态变化不可控,所以我们必须将抖动消除。下面是一种延时去抖的程序。

module debounce (clk,rst,key,key_pulse);

parameter N = 1; //要消除的按键的数量

input clk;
input rst;
input [N-1:0] key; //输入的按键
output [N-1:0] key_pulse; //按键动作产生的脉冲

reg [N-1:0] key_rst_pre; //定义一个寄存器型变量存储上一个触发时的按键值
reg [N-1:0] key_rst; //定义一个寄存器变量储存储当前时刻触发的按键值

wire [N-1:0] key_edge; //检测到按键由高到低变化是产生一个高脉冲

//利用非阻塞赋值特点,将两个时钟触发时按键状态存储在两个寄存器变量中
always @(posedge clk or negedge rst)
begin
if (!rst) begin
key_rst <= {N{1'b1}}; //初始化时给key_rst赋值全为1,{}中表示N个1
key_rst_pre <= {N{1'b1}};
end
else begin
key_rst <= key; //第一个时钟上升沿触发之后key的值赋给key_rst,同时key_rst的值赋给key_rst_pre
key_rst_pre <= key_rst; //非阻塞赋值。相当于经过两个时钟触发,key_rst存储的是当前时刻key的值,key_rst_pre存储的是前一个时钟的key的值
end
end

assign key_edge = key_rst_pre & (~key_rst);//脉冲边沿检测。当key检测到下降沿时,key_edge产生一个时钟周期的高电平

reg [17:0] cnt; //产生延时所用的计数器,系统时钟12MHz,要延时20ms左右时间,至少需要18位计数器

//产生20ms延时,当检测到key_edge有效是计数器清零开始计数
always @(posedge clk or negedge rst)
begin
if(!rst)
cnt <= 18'h0;
else if(key_edge)
cnt <= 18'h0;
else
cnt <= cnt + 1'h1;
end

reg [N-1:0] key_sec_pre; //延时后检测电平寄存器变量
reg [N-1:0] key_sec;


//延时后检测key,如果按键状态变低产生一个时钟的高脉冲。如果按键状态是高的话说明按键无效
always @(posedge clk or negedge rst)
begin
if (!rst)
key_sec <= {N{1'b1}};
else if (cnt==18'h3ffff)
key_sec <= key;
end
always @(posedge clk or negedge rst)
begin
if (!rst)
key_sec_pre <= {N{1'b1}};
else
key_sec_pre <= key_sec;
end
assign key_pulse = key_sec_pre & (~key_sec);

endmodule

以上就是一个N位按键的消抖程序,如果有按键按下会输出一个时钟周期的高脉冲。下面我们可以试试用这个按键消抖的输出来触发LED的显示,既按键一次LED翻转。你也可以不加按键消抖试试用按键来控制LED(按一次变亮,再按一次灭掉)。

下面的程序是例化调用debounce模块来控制LED:

module top (clk,rst,key,led);

input clk;
input rst;
input key;
output reg led;

wire key_pulse;

//当按键按下时产生一个高脉冲,翻转一次led
always @(posedge clk or negedge rst)
begin
if (!rst)
led <= 1'b1;
else if (key_pulse)
led <= ~led;
else
led <= led;
end
//例化消抖module,这里没有传递参数N,采用了默认的N=1
debounce u1 (
.clk (clk),
.rst (rst),
.key (key),
.key_pulse (key_pulse)
);
endmodule

引脚分配

设置好复位键可消抖的按键,编译完成后下载,通过按键就可以翻转LED。你也可以定义多个按键控制多个LED,还可以比较不加按键消抖情况下实际的效果对比如何。

信号引脚
clkD5
rstD2
keyE2
ledF3

6. 小结

在本实验学习了如何进行按键的消抖。在很多应用情况下我们必须采取消抖才能更好地控制逻辑。在下一个实验计时控制中我们将学习计时的显示和控制,在这里我们要用到按键的消抖以及数码管,我们甚至可以用小脚丫做一个计时器甚至电子表。

实验八 计时控制

在之前的实验中我们掌握了如何进行时钟分频、如何进行数码管显示与按键消抖的处理,那么在本节实验之中,我们将会实现一个篮球赛场上常见的24秒计时器。 

1. 硬件说明

在之前的实验中我们为读者详细介绍过小脚丫STEP-HR03-U81板卡上的按键、数码管、LED等硬件外设,在此不再赘述。本节将实现由数码管作为显示模块,按键作为控制信号的输入(包含复位信号和暂停信号),STEP-HR03-U81作为控制核心的篮球读秒系统,实现框图如下:

按键输入

分频时钟

数码管显示

STEP-HR03-U81

2. Verilog代码

 module HR3_counter
(
clk , //时钟
rst , //复位
hold , //启动暂停按键
dig , //数码管使能管脚
seg_rck , //74HC595的RCK管脚
seg_sck , //74HC595的SCK管脚
seg_din , //74HC595的SER管脚
led //led
);

input clk,rst;
input hold;
output [1:0] dig;
output seg_rck;
output seg_sck;
output seg_din;
output reg [7:0] led;

wire Sys_rst; //系统复位
wire por_reset;
wire clk1h; //1Hz时钟
wire hold_pulse; //按键消抖后信号
reg hold_flag; //按键标志位
reg back_to_zero_flag ; //计时完成信号
reg [6:0] seg [9:0];
reg [3:0] cnt_ge; //个位
reg [3:0] cnt_shi; //十位
reg [3:0] cnt_por;

//数码管显示
HR3_segment U3 (
.clk (clk ),
.rst_n (rst ),
.dat0 (cnt_ge ),
.dat1 (cnt_shi ),
.dig (dig ),
.seg_rck (seg_rck ), //74HC595的RCK管脚
.seg_sck (seg_sck ), //74HC595的SCK管脚
.seg_din (seg_din ) //74HC595的SER管脚
);

// 启动/暂停按键进行消抖
debounce U2 (
.clk(clk),
.rst(rst),
.key(hold),
.key_pulse(hold_pulse)
);
// 用于分出一个1Hz的频率
divide #(.WIDTH(32),.N(12000000)) U1 (
.clk(clk),
.rst_n(rst),
.clkout(clk1h)
);

//按键动作标志信号产生
always @ (posedge hold_pulse)
if(!rst==1)
hold_flag <= 0;
else
hold_flag <= ~hold_flag;
//计时完成标志信号产生
always @ (*)
if(!rst == 1)
back_to_zero_flag <= 0;
else if(cnt_shi==0 && cnt_ge==0)
back_to_zero_flag <= 1;
else
back_to_zero_flag <= 0;
//24秒倒计时控制
always @ (posedge clk1h or negedge rst) begin
if (!rst == 1) begin
cnt_ge <= 4'd4;
cnt_shi <= 4'd2;
end
else if(hold_flag == 1)begin
cnt_ge <= cnt_ge;
cnt_shi <= cnt_shi;
end
else if(cnt_shi==0 && cnt_ge==0) begin
cnt_shi <= cnt_shi;
cnt_ge <= cnt_ge;
end
else if(cnt_ge==0)begin
cnt_ge <= 4'd9;
cnt_shi <= cnt_shi-1;end
else
cnt_ge <= cnt_ge -1;
end
//计时完成点亮led
always @ ( back_to_zero_flag)begin
if (back_to_zero_flag==1)
led = 8'b0;
else
led = 8'b11111111;
end
endmodule

引脚分配

信号引脚信号引脚
clkD5led[0]F3
rstD2led[1]E3
holdE2led[2]D3
dig[0]C7led[3]C3
dig[1]E7led[4]C4
seg_sckB4led[5]E4
seg_rckB5led[6]G4
seg_dinB6led[7]H4

4. 小结

本实验主要介绍了计时器的实现方式,并且包含了复位与暂停功能,读者可自行修改程序内部的时钟参数来调节计时时间。下一节将介绍PWM调制技术的应用呼吸灯。

实验九 脉宽调制

本节将向您介绍数字电路设计中一种常用的控制技术——脉冲宽度调制(Pulse Width Modulation,PWM)。通过改变输出脉冲信号的占空比,可以实现对负载平均功率的调节。本实验将利用FPGA产生PWM信号,实现LED灯亮度的周期性变化,完成呼吸灯效果。

1. 硬件说明

呼吸灯的设计较为简单,我们使用12MHz的系统时钟作为高频信号做分频处理,调整占空比实现PWM,通过LED灯D1指示输出状态。

image14.png

image15.png

实现原理如上图所示,脉冲信号的周期为T,高电平脉冲宽度为t,占空比为t/T。为了实现PWM脉宽调制,我们需要保持周期T不变,调整高电平脉宽t的时间,从而改变占空比。

当t = 0时,占空比为0%,因为我们的LED硬件为低电平点亮,所以为最亮的状态。

当t = T时,占空比为100%,LED灯为最暗(熄灭)的状态。

结合呼吸灯的原理,整个呼吸的周期为最亮→最暗→最亮的时间,即t的值的变化:0→T→0逐渐变化,这个时间应该为2s

f5b5bb4c-36ae-4c88-b644-40e141117cc8

呼吸灯设计要求呼吸的周期为2s,也就是说LED灯从最亮的状态开始,第一秒时间内逐渐变暗,第二秒的时间内再逐渐变亮,依次进行。

本设计中需要两个计数器cnt1和cnt2,cnt1随系统时钟同步计数(系统时钟上升沿时cnt1自加1)范围为0~T,cnt2随cnt1的周期同步计数(cnt1等于T时,cnt2自加1)范围也是0~T,这样每次cnt1在0~T的计数时,cnt2为一个固定值,相邻cnt1计数周期对应的cnt2的值逐渐增大,我们将cnt1计数0~T的时间作为脉冲周期,cnt2的值作为脉冲宽度,则占空比 = cnt2/T,占空比从0%到100%的时间 = cnt2*cnt1 = T^2 = 1s = 12M个系统时钟,T = 2400,我们定义CNT_NUM = 2400作为两个计数器的计数最大值。

082d8ba0-0709-465b-8707-e831d27dd0fe

2. Verilog代码

module breath_led(clk,rst,led);

input clk; //系统时钟输入
input rst; //复位输出
output led; //led输出

reg [24:0] cnt1; //计数器1
reg [24:0] cnt2; //计数器2
reg flag; //呼吸灯变亮和变暗的标志位

parameter CNT_NUM = 3464; //计数器的最大值 period = (3464^2)*2 = 24000000 = 2s
//产生计数器cnt1
always@(posedge clk or negedge rst) begin
if(!rst) begin
cnt1<=13'd0;
end
else if(cnt1>=CNT_NUM-1)
cnt1<=1'b0;
else
cnt1<=cnt1+1'b1;
end

//产生计数器cnt2
always@(posedge clk or negedge rst) begin
if(!rst) begin
cnt2<=13'd0;
flag<=1'b0;
end
else if(cnt1==CNT_NUM-1) begin //当计数器1计满时计数器2开始计数加一或减一
if(!flag) begin //当标志位为0时计数器2递增计数,表示呼吸灯效果由暗变亮
if(cnt2>=CNT_NUM-1) //计数器2计满时,表示亮度已最大,标志位变高,之后计数器2开始递减
flag<=1'b1;
else
cnt2<=cnt2+1'b1;
end
else begin
if(cnt2<=0) //当标志位为高时计数器2递减计数
flag<=1'b0; //计数器2级到0,表示亮度已最小,标志位变低,之后计数器2开始递增
else
cnt2<=cnt2-1'b1;
end

end
else
cnt2<=cnt2; //计数器1在计数过程中计数器2保持不变
end

//比较计数器1和计数器2的值产生自动调整占空比输出的信号,输出到led产生呼吸灯效果
assign led = (cnt1<cnt2)?1'b0:1'b1;

endmodule

3. 引脚分配

引脚分配如下:

管脚名称clkrstled
FPGA管脚D5D2F3

4. 小结

脉宽调制是一种值得广大工程师在许多应用设计中使用的有效技术,你也可以根据本节介绍的流水灯程序,实现RGB三色灯的呼吸。在下一小节我们会学习状态机的使用方法:交通灯的设计。

实验十 简易交通灯

本节将向您介绍Verilog语法之中的精髓内容——状态机,并且将利用状态机实现十字路口的交通灯。

硬件说明与实现项目框图

11b0f13e-c1a7-4285-96e7-975cb5f0db0a

上图为十字路口交通示意图分之路与主路,要求如下:

·交通灯主路上绿灯持续15s的时间,黄灯3s的时间,红灯10s的时间;

·交通灯支路上绿灯持续7s的时间, 黄灯持续3秒的时间,红灯18秒的时间;

image19.png

根据上述要求,状态机设计框架分析如下:

·S1:主路绿灯点亮,支路红灯点亮,持续15s的时间;

·S2:主路黄灯点亮,支路红灯点亮,持续3s的时间;

·S3:主路红灯点亮,支路绿灯点亮,持续10s的时间;

·S4:主路红灯点亮,支路黄灯点亮,持续3s的时间;

image20.png

2. Verilog代码

本实验将结合前面章节中介绍的时钟分频模块,为后续控制逻辑提供所需的工作时钟。实验平台采用 STEP-HR03-U81 开发板上的 WS2812 RGB LED 作为交通灯显示器件,通过控制 RGB 三通道的亮度实现不同颜色显示,并模拟十字路口交通灯的工作状态。

WS2812 RGB LED 本身由红(R)、绿(G)、蓝(B)三种颜色通道组成,可以通过调节各通道的亮度组合出多种颜色。虽然器件没有独立的黄色通道,但根据 RGB 颜色混合原理,同时点亮红色和绿色通道即可产生黄色显示效果。因此,本实验利用红、黄、绿三种颜色分别模拟交通信号灯中的禁止通行、警示等待和允许通行状态。

接下来就是利用三段式状态机实现的交通灯部分:

module HR3_trafficled
(
clk , //时钟
rst_n , //复位
ws2812_dout

);

input clk,rst_n;
output ws2812_dout;

parameter S1 = 4'b00, //状态机状态编码
S2 = 4'b01,
S3 = 4'b10,
S4 = 4'b11;

parameter time_s1 = 4'd15, //计时参数
time_s2 = 4'd3,
time_s3 = 4'd7,
time_s4 = 4'd3;
//交通灯的控制
parameter led_s1 = 2'b00, // LED2 绿色 LED1 红色
led_s2 = 2'b01, // LED2 黄色 LED1 红色
led_s3 = 2'b10, // LED2 红色 LED1 绿色
led_s4 = 2'b11; // LED2 红色 LED1 黄色

reg [1:0] traffic_led;
reg [3:0] timecont;
reg [1:0] cur_state,next_state; //现态、次态

wire clk1h; //1Hz时钟

//产生1秒的时钟周期
divide #(.WIDTH(32),.N(12000000)) CLK1H (
.clk(clk),
.rst_n(rst_n),
.clkout(clk1h));
//第一段 同步逻辑 描述次态到现态的转移
always @ (posedge clk1h or negedge rst_n)
begin
if(!rst_n)
cur_state <= S1;
else
cur_state <= next_state;
end
//第二段 组合逻辑描述状态转移的判断
always @ (cur_state or rst_n or timecont)
begin
if(!rst_n) begin
next_state = S1;
end
else begin
case(cur_state)
S1:begin
if(timecont==1)
next_state = S2;
else
next_state = S1;
end

S2:begin
if(timecont==1)
next_state = S3;
else
next_state = S2;
end

S3:begin
if(timecont==1)
next_state = S4;
else
next_state = S3;
end

S4:begin
if(timecont==1)
next_state = S1;
else
next_state = S4;
end

default: next_state = S1;
endcase
end
end
//第三段 同步逻辑 描述次态的输出动作
always @ (posedge clk1h or negedge rst_n)
begin
if(!rst_n==1) begin
traffic_led <= led_s1;
timecont <= time_s1;
end
else begin
case(next_state)
S1:begin
traffic_led <= led_s1;
if(timecont == 1)
timecont <= time_s1;
else
timecont <= timecont - 1;
end

S2:begin
traffic_led <= led_s2;
if(timecont == 1)
timecont <= time_s2;
else
timecont <= timecont - 1;
end

S3:begin
traffic_led <= led_s3;
if(timecont == 1)
timecont <= time_s3;
else
timecont <= timecont - 1;
end

S4:begin
traffic_led <= led_s4;
if(timecont == 1)
timecont <= time_s4;
else
timecont <= timecont - 1;
end

default:begin
traffic_led <= led_s1;
end
endcase
end
end
HR3_RGB_LED u_RGB_ctrl(
.clk_in (clk ), // 12MHz
.rst_n_in (rst_n ), // low active reset
.dat (traffic_led),
.ws2812_dout (ws2812_dout)
);
endmodule

3.引脚分配

管脚名称clkrstWS2812_dout
FPGA管脚D5D2C5

配置好以后编译下载程序。您也可以试试修改程序,观察修改代码对于FPGA内部电路所造成的影响。

4. 小结

状态机是一类很重要的时序逻辑电路,是许多数字系统的核心部件,掌握状态机的使用是利用FPGA与CPLD进行开发的一项必会技能,本小节的交通灯程序即是利用三段式状态机描述方法实现的,希望读者能够快速掌握这项技能。