一. 实验目的
掌握有限状态机的设计方法。;
能够使用 SystemVerilog 进行三段式状态机的建模。
二. 实验环境
操作系统:Windows 10 或 Ubuntu 16.04
开发环境:Xilinx Vivado 2018.2
硬件平台:远程 FPGA 云平台
三. 实验原理
有限状态机分为 Moore 型状态机和 Mealy 型状态机。前者,状态机的输出仅由当前状态决定,如图 4-1 所示,在状态转换图的绘制中,输出信息标在状态(圆圈)中。

采用硬件描述语言进行状态机建模时,建议使用 3 段式。第一段描述状态的转换(即对状态机中的寄存器进行建模),采用时序逻辑实现;第二段描述状态转换条件和规律(即对状态机中的次态逻辑进行建模),采用组合逻辑实现;第三段描述输出逻辑,根据实际设计需要可采用组合逻辑或时序逻辑实现。三段式状态机建模的模板如下所示。
//第一段,同步时序 always 模块,描述状态的转换
always_ff (posedge clk) begin //同步复位
if(!rst_n)
current_state <= S0;
else
current_state <= next_state; //注意,使用的是非阻塞赋值
end
//第二段,组合逻辑 always 模块,描述状态转移条件判断
always_comb begin
case(current_state)
S1: if(...)
next_state = S2; //阻塞赋值
...
endcase
end
//第三段,同步时序 always 模块(组合逻辑也可以),描述状态机的输出
always @ (posedge clk) begin
...//初始化
case(next_state)
S1: out1 <= 1'b1; //注意是非阻塞逻辑
S2: out2 <= 1'b1;
default:... //default 的作用是免除综合工具综合出锁存器
endcase
end四. 实验内容
采用有限状态机,基于 SystemVerilog HDL 设计并实现一个报纸自动贩售机。整个工程的顶层模块如图 4-3 所示,输入/输出端口如表 4-1 所示。使用 4 个七段数码管实时显示已付款和找零情况。其中,两个数码管对应“已付款”,另两个数码管对应“找零”,单位为分。通过 1 个拨动开关对数字钟进行复位控制。使用两个按键模拟投币,其中一个按键对应 5 分,另一个按键对应 1 角。使用 1 个LED 灯标识出售是否成功,灯亮表示出售成功,否则表示已付款不够,出售失败。
假设报纸价格为 15 分,合法的投币组合包括:
⚫ 1 个 5 分的硬币和一个 1 角的硬币,不找零
⚫ 3 个五分的硬币,不找零
⚫ 1 个 1 角的硬币和一个 5 分的硬币,不找零
⚫ 两个 1 角的硬币是合法的,找零 5 分。
当投入硬币的组合为上面 4 种之一时,则购买成功,LED 灯亮。购买成功后,LED灯持续亮 10 秒,然后自动熄灭,同时 4 个数码管也恢复为 0。

报纸自动贩售机由 4 部分构成。
⚫ 第一部分是计时器模块,该模块又由 3 个子模块构成,分别是计数器电路、使能时钟生成电路和边沿检测电路。
⚫ 第二部分是整个自动贩售机电路的核心——贩售机状态机。状态机根据投币情况产生“已付款”和“找零”输出。此外,如果已付款超过 15 分,则将 LED 灯点亮,表示出售成功。
⚫ 第三部分是两个 8 位二进制转 BCD 模块,分别将二进制的“已付款”和“找零”值转化为 BCD 编码,即 10 进制数。本实验中,该模块不需要实现,由教师直接提供 IP 使用。
⚫ 第四部分是 7 段数码管动态扫描显示模块,它实现“已付款”和“找零”值的最终显示。

完成上述分秒数字钟的设计,需要有以下几点需要注意:
7 段数码管动态扫描必须采用使能时钟实现,扫描频率为 1KHz(1ms)。
必须通过边沿检测电路识别“5 分”和“1 角”按键按下产生的上升沿,以用于后续处理。
用于计时的时钟频率为 25MHz(40ns)。
由于 7 段数码管扫描周期是 1ms,购买成功后需要等待 10s,从而造成仿真时间过长。为了加快仿真速度,可以在仿真的时候使用较大的计时单位和扫描速度。
五. 实验步骤
第一部分、计时器模块
1.使能时钟生成器
首先系统的时钟主频为25MHz,所以时钟周期为1/25MHz = 40ns
我们要产生1ms的使能信号,只需经过1ms/40ns = 25000个时钟周期
因此可以定义一个计时器,每当counter == 24999时,就产生一个使能信号,并把counter重置为0
module enable_clock(
input logic sys_clk,
input logic sys_rst_n,
output logic enable_1ms
);
logic [14:0] counter;
//为了加快仿真速度,从25000调整到24
parameter integer ONE_MS = 25000; //输入时钟频率为25MHz
always_ff @(posedge sys_clk)
if(~sys_rst_n)
counter <= 15'd0;
else if(counter == ONE_MS -1)
counter <= 15'd0;
else
counter <= counter + 1;
assign enable_1ms = (counter == ONE_MS -1);
endmodule2.边沿检测电路
在头歌中已经联系过了边沿检测的书写,主要的原理就是保存上一时刻的信号值,如果上一时刻的信号为0,下一时刻的信号为1,那么就可以检测到一个上升沿。
module edge_detection(
input logic sys_clk,
input logic sys_rst_n,
input logic signal_in,
output logic edge_detection
);
logic signal_in_d; //上一时刻的signal_in
always_ff @(posedge sys_clk)
if(~sys_rst_n)
signal_in_d <= 0;
else
signal_in_d <= signal_in;
assign edge_detection = ~signal_in_d & signal_in; //上升沿
endmodule通过边沿检测电路,我们可以检测到硬币被投入的时机,并且防止投币被多次计算
3.计数器
这一部分主要是为了在售出成功后保持10s的LED灯亮起,所以要设计一个10s的计数器。我们可以借鉴上面的1ms使能信号的产生,这里无非是250000000个时钟周期。
module timer_10s(
input logic sys_clk,
input logic sys_rst_n,
input logic start,
output logic done
);
logic[31:0] count;
parameter integer TEN_SECONDS = 250000000;
always_ff @(posedge sys_clk)
if(~sys_rst_n)
count <= 0;
else if(start)
count <= (count == TEN_SECONDS-1) ? 0 : count + 1;
else
count <= 0;
assign done = (count == TEN_SECONDS-1);
endmodule第二部分、贩售状态机
我们首先定义如下几个状态
typedef enum logic [2:0] {
IDLE = 3'b000, //空闲
PAID5 = 3'b001,
PAID10 = 3'b010,
PAID15 = 3'b011,
PAID20 = 3'b100,
WAIT_RESET = 3'b101
} state_t;首先我们根据题目的描述,给出贩售状态机的状态转换图

状态转移比较简单,每一次都是从次态变为现态,代码如下
// State transition
always_ff @(posedge sys_clk) begin
if (~sys_rst_n)
state <= IDLE;
else
state <= next_state; //次态变为现态
end次态逻辑里面定义了每种状态下的下一时刻的状态,代码如下
其中timer_done表示10s已经到了,那么就把PAID15和PAID20下一时刻的状态设置为WAIT_RESET
// Next state logic
always_comb begin
case (state)
IDLE: begin
if (edge_detected_coin5)
next_state = PAID5;
else if (edge_detected_coin10)
next_state = PAID10;
else
next_state = state;
end
PAID5: begin
if (edge_detected_coin5)
next_state = PAID10;
else if (edge_detected_coin10)
next_state = PAID15;
else
next_state = state;
end
PAID10: begin
if (edge_detected_coin5)
next_state = PAID15;
else if (edge_detected_coin10)
next_state = PAID20;
else
next_state = state;
end
PAID15: begin
// 保持住现在的状态
if(timer_done)
next_state = WAIT_RESET;
else
next_state = state;
end
PAID20: begin
// 保持住现在的状态
if(timer_done)
next_state = WAIT_RESET;
else
next_state = state;
end
WAIT_RESET: next_state = IDLE;
default: next_state = IDLE;
endcase
end输出逻辑分别定义了price,change,open(表示LED灯是否亮起),这里我多定义了一个LED状态,用led表示
// Output logic
assign price = (state == WAIT_RESET) ? 8'd0 : total_paid; // Price represents the total paid amount
assign change = (state == WAIT_RESET) ? 8'd0 : change_amount;
assign open = (state == PAID15 || state == PAID20);
assign led = (state == PAID15 || state == PAID20); // LED on for 10 seconds after successful purchase然后再分别定义total_paid和change_amount
// Total paid logic
always_ff @(posedge sys_clk) begin
if (~sys_rst_n)
total_paid <= 8'd0;
else if(state == WAIT_RESET) //这句话保证了在出售成功后10s能够自动清零
total_paid <= 8'd0;
else if (edge_detected_coin5)
total_paid <= total_paid + 8'd5;
else if (edge_detected_coin10)
total_paid <= total_paid + 8'd10;
end
// Change calculation logic
always_ff @(posedge sys_clk) begin
if (~sys_rst_n)
change_amount <= 8'd0;
else if (state == PAID20) //因为只有PAID20状态为合法状态,所以只需要定义固定的找零值为5
change_amount <= 8'd5;
else
change_amount <= 8'd0;
endvending_machine_fsm模块代码如下
module vending_machine_fsm(
input logic sys_clk,
input logic sys_rst_n,
input logic coin5,
input logic coin10,
output logic [7:0] change,
output logic [7:0] price,
output logic open,
output logic led
);
// 定义枚举类——状态
typedef enum logic [2:0] {
IDLE = 3'b000, //空闲
PAID5 = 3'b001,
PAID10 = 3'b010,
PAID15 = 3'b011,
PAID20 = 3'b100,
WAIT_RESET = 3'b101
} state_t;
state_t state, next_state;
logic [7:0] total_paid;
logic [7:0] change_amount;
logic timer_done;
logic enable_1ms;
logic edge_detected_coin5, edge_detected_coin10;
timer_10s timer_10s_inst(
.sys_clk(sys_clk),
.sys_rst_n(sys_rst_n),
.start(state == PAID15 || state == PAID20),
.done(timer_done)
);
// Instantiate timer module for edge detection and 1ms enable signal
timer_module timer_module_inst(
.sys_clk(sys_clk),
.sys_rst_n(sys_rst_n),
.coin5(coin5),
.coin10(coin10),
.enable_1ms(enable_1ms),
.edge_detected_coin5(edge_detected_coin5),
.edge_detected_coin10(edge_detected_coin10)
);
// Output logic
assign price = (state == WAIT_RESET) ? 8'd0 : total_paid; // Price represents the total paid amount
assign change = (state == WAIT_RESET) ? 8'd0 : change_amount;
assign open = (state == PAID15 || state == PAID20);
assign led = (state == PAID15 || state == PAID20); // LED on for 10 seconds after successful purchase
// State transition
always_ff @(posedge sys_clk) begin
if (~sys_rst_n)
state <= IDLE;
else
state <= next_state; //次态变为现态
end
// Next state logic
always_comb begin
case (state)
IDLE: begin
if (edge_detected_coin5)
next_state = PAID5;
else if (edge_detected_coin10)
next_state = PAID10;
else
next_state = state;
end
PAID5: begin
if (edge_detected_coin5)
next_state = PAID10;
else if (edge_detected_coin10)
next_state = PAID15;
else
next_state = state;
end
PAID10: begin
if (edge_detected_coin5)
next_state = PAID15;
else if (edge_detected_coin10)
next_state = PAID20;
else
next_state = state;
end
PAID15: begin
// 保持住现在的状态
if(timer_done)
next_state = WAIT_RESET;
else
next_state = state;
end
PAID20: begin
// 保持住现在的状态
if(timer_done)
next_state = WAIT_RESET;
else
next_state = state;
end
WAIT_RESET: next_state = IDLE;
default: next_state = IDLE;
endcase
end
// Total paid logic
always_ff @(posedge sys_clk) begin
if (~sys_rst_n)
total_paid <= 8'd0;
else if(state == WAIT_RESET)
total_paid <= 8'd0;
else if (edge_detected_coin5)
total_paid <= total_paid + 8'd5;
else if (edge_detected_coin10)
total_paid <= total_paid + 8'd10;
end
// Change calculation logic
always_ff @(posedge sys_clk) begin
if (~sys_rst_n)
change_amount <= 8'd0;
else if (state == PAID20)
change_amount <= 8'd5;
else
change_amount <= 8'd0;
end
endmodule第三部分、8为二进制转BCD模块
本实验中,该模块不需要实现,由教师直接提供 IP 使用。
IP核实例化后结构如下

第四部分、7段数码管动态扫描显示模块
为了实现动态扫描,还是要利用到之前定义的使能时钟生成器。
此外,还需要实现七段数码管的译码器,即把十进制0~9翻译成对应的8位数码管电路(本实验是8位,最高位始终无效),注意是共阳极低电平有效,代码如下
module seven_segment_decoder(
input logic [3:0] digit,
output logic [7:0] seg
);
always_comb begin
case (digit)
4'd0: seg = 8'b11000000; // 0
4'd1: seg = 8'b11111001; // 1
4'd2: seg = 8'b10100100; // 2
4'd3: seg = 8'b10110000; // 3
4'd4: seg = 8'b10011001; // 4
4'd5: seg = 8'b10010010; // 5
4'd6: seg = 8'b10000010; // 6
4'd7: seg = 8'b11111000; // 7
4'd8: seg = 8'b10000000; // 8
4'd9: seg = 8'b10010000; // 9
default: seg = 8'b11111111; // Blank
endcase
end
endmodule在实现4位七段数码管显示模块时,应当实例化上面的两个模块。由于是4位需要来回扫描,因此可以定义一个scan_state来表示扫描到的位置,每次扫描到对应的位置,都使能相应的数码管,从而显示不同位置上的数字。这些数字从左到右分别是change_bcd[7:4], change_bcd[3:0], paid_bcd[7:4], paid_bcd[3:0]。
模块代码如下:
module seven_segment_display(
input logic sys_clk,
input logic sys_rst_n,
input logic [7:0] paid_bcd,
input logic [7:0] change_bcd,
output logic [7:0] a_to_g,
output logic [3:0] an
);
// Internal signals
logic [1:0] scan_state;
logic [3:0] digit;
logic [7:0] seg;
logic enable_1ms;
// Instantiate the enable clock module to generate 1ms enable signal
enable_clock enable_clock_inst (
.sys_clk(sys_clk),
.sys_rst_n(sys_rst_n),
.enable_1ms(enable_1ms)
);
// Instantiate the segment decoder
seven_segment_decoder decoder (
.digit(digit),
.seg(seg)
);
// Scan state machine
always_ff @(posedge sys_clk) begin
if (~sys_rst_n)
scan_state <= 2'd0;
//为了加快仿真速度,这里调整为50个时钟周期(1/10 时间)
else if (enable_1ms) // 25MHz clock / 1KHz = 25000 如果计数器 counter 达到24999(即1ms),则将 scan_state 加1。
scan_state <= (scan_state == 2'd3) ? 2'd0 : scan_state + 2'd1;//该变量用于跟踪当前正在扫描的数码管,2'd0到2'd3分别表示4个数码管。
end
// Digit and anode control
always_comb begin
case (scan_state)
2'd0: begin
digit = paid_bcd[3:0];
an = 4'b0001; // Enable AN0
end
2'd1: begin
digit = paid_bcd[7:4];
an = 4'b0010; // Enable AN1
end
2'd2: begin
digit = change_bcd[3:0];
an = 4'b0100; // Enable AN2
end
2'd3: begin
digit = change_bcd[7:4];
an = 4'b1000; // Enable AN3
end
default: begin
digit = 4'd0;
an = 4'b0000;
end
endcase
end
// Assign segment output
assign a_to_g = seg;
endmodule以上,便完成了所有子模块的实现,现在可以构造顶层模块vend
第五部分、顶层模块
module vend(
input sys_clk, sys_rst_n,
input coin5, coin10,
output [3 : 0] an,
output [7 : 0] a_to_g,
output open
);
logic led;
logic [7:0] paid_bin, change_bin;
logic [7:0] paid_bcd, change_bcd;
// Instantiate FSM
vending_machine_fsm fsm (
.sys_clk(sys_clk),
.sys_rst_n(sys_rst_n),
.coin5(coin5),
.coin10(coin10),
.change(change_bin),
.price(paid_bin),
.open(open),
.led(led)
);
// Instantiate bin2bcd for paid and change
bin2bcd bin2bcd_paid (
.bin(paid_bin),
.bcd(paid_bcd)
);
bin2bcd bin2bcd_change (
.bin(change_bin),
.bcd(change_bcd)
);
// Instantiate 7-segment display
seven_segment_display display (
.sys_clk(sys_clk),
.sys_rst_n(sys_rst_n),
.paid_bcd(paid_bcd),
.change_bcd(change_bcd),
.a_to_g(a_to_g),
.an(an)
);
endmodule第六部分、仿真测试
由于本实验的模块过多,所以我采用了单元测试的方法,分别测试了timer_module, vending_machine_fsm, seven_segment_display, vend模块,以下便写出测试的代码和仿真结果
1.timer_module
为了加快仿真速度,我们把SIMULATION_CLK_PERIOD设置为4,仿真等待时间设置为20(总仿真时间仅有1000ns),把使能时钟中的25000改为24
`timescale 1ns / 1ps
module timer_module_tb;
// Parameters for simulation
localparam integer SIMULATION_CLK_PERIOD = 4; // 4ns for 250MHz clock to speed up simulation
localparam integer SIMULATION_WAIT_TIME = 20; // 20ns for each test case observation
// Inputs
logic sys_clk;
logic sys_rst_n;
logic coin5;
logic coin10;
// Outputs
logic enable_1ms;
logic edge_detected_coin5;
logic edge_detected_coin10;
// Instantiate the timer module
timer_module uut (
.sys_clk(sys_clk),
.sys_rst_n(sys_rst_n),
.coin5(coin5),
.coin10(coin10),
.enable_1ms(enable_1ms),
.edge_detected_coin5(edge_detected_coin5),
.edge_detected_coin10(edge_detected_coin10)
);
// Clock generation
initial begin
sys_clk = 0;
forever #(SIMULATION_CLK_PERIOD / 2) sys_clk = ~sys_clk;
end
// Stimulus process
initial begin
// Initialize Inputs
sys_rst_n = 0;
coin5 = 0;
coin10 = 0;
// Apply reset
#(SIMULATION_WAIT_TIME);
sys_rst_n = 1;
// Test edge detection for coin5
#(SIMULATION_WAIT_TIME);
coin5 = 1;
#(SIMULATION_WAIT_TIME);
coin5 = 0;
#(SIMULATION_WAIT_TIME * 5); // Wait to observe edge detection
// Test edge detection for coin10
#(SIMULATION_WAIT_TIME);
coin10 = 1;
#(SIMULATION_WAIT_TIME);
coin10 = 0;
#(SIMULATION_WAIT_TIME * 5); // Wait to observe edge detection
// Observe enable_1ms signal (accelerated to observe in short time)
#(SIMULATION_WAIT_TIME * 20); // Short wait to observe 1ms signal
// End of simulation
$stop;
end
endmodule仿真结果

可以看到,在检测到coin5和coin10上升沿的时候,边沿检测的值为1
2.vending_machine_fsm
这里可以分四种情况来测,我这里只列举出一种情况(5+5+10)
`timescale 1ns / 1ps
module vending_machine_fsm_tb;
// Parameters for simulation
localparam integer SIMULATION_CLK_PERIOD = 4; // 4ns for 250MHz clock to speed up simulation
localparam integer SIMULATION_WAIT_TIME = 20; // 20ns for each test case observation
// Inputs
logic sys_clk;
logic sys_rst_n;
logic coin5;
logic coin10;
// Outputs
logic [7:0] change;
logic [7:0] price;
logic open;
logic led;
// Instantiate the vending machine FSM module
vending_machine_fsm uut (
.sys_clk(sys_clk),
.sys_rst_n(sys_rst_n),
.coin5(coin5),
.coin10(coin10),
.change(change),
.price(price),
.open(open),
.led(led)
);
// Clock generation
initial begin
sys_clk = 0;
forever #(SIMULATION_CLK_PERIOD / 2) sys_clk = ~sys_clk;
end
// Stimulus process
initial begin
// Initialize Inputs
sys_rst_n = 0;
coin5 = 0;
coin10 = 0;
// Apply reset
#(SIMULATION_WAIT_TIME);
sys_rst_n = 1;
// Test case 4: Insert 5 cents, then another 5 cents, and then 10 cents to reach 20 cents
#(SIMULATION_WAIT_TIME);
coin5 = 1;
#(SIMULATION_WAIT_TIME);
coin5 = 0;
#(SIMULATION_WAIT_TIME * 2); // Short wait to observe state change
// Insert second 5 cents
#(SIMULATION_WAIT_TIME);
coin5 = 1;
#(SIMULATION_WAIT_TIME);
coin5 = 0;
#(SIMULATION_WAIT_TIME * 2); // Short wait to observe state change
// Insert 10 cents
#(SIMULATION_WAIT_TIME);
coin10 = 1;
#(SIMULATION_WAIT_TIME);
coin10 = 0;
#(SIMULATION_WAIT_TIME * 2); // Short wait to observe state change
// End of simulation
$stop;
end
endmodule仿真结果

3.seven_segment_display
我分别测试了paid_bcd = 15, change_bcd = 5 和 paid_bcd = 20, change_bcd = 10,两种情况,这两种情况真实中并不能出现,该测试仅仅是为了观察动态扫描的每一位是否正常显示
`timescale 1ns / 1ps
module seven_segment_display_tb;
// Parameters for simulation
localparam integer SIMULATION_CLK_PERIOD = 4; // 1ns for 1GHz clock to speed up simulation
localparam integer SIMULATION_WAIT_TIME = 20; // 10ns for each test case observation
// Inputs
logic sys_clk;
logic sys_rst_n;
logic [7:0] paid_bcd;
logic [7:0] change_bcd;
// Outputs
logic [7:0] a_to_g;
logic [3:0] an;
// Instantiate the seven segment display module
seven_segment_display uut (
.sys_clk(sys_clk),
.sys_rst_n(sys_rst_n),
.paid_bcd(paid_bcd),
.change_bcd(change_bcd),
.a_to_g(a_to_g),
.an(an)
);
// Clock generation
initial begin
sys_clk = 0;
forever #(SIMULATION_CLK_PERIOD / 2) sys_clk = ~sys_clk;
end
// Stimulus process
initial begin
// Initialize Inputs
sys_rst_n = 0;
paid_bcd = 8'h00;
change_bcd = 8'h00;
// Apply reset
#(SIMULATION_WAIT_TIME);
sys_rst_n = 1;
// Test case: Display paid amount = 15, change amount = 5
#(SIMULATION_WAIT_TIME);
paid_bcd = 8'h15; // 15 in BCD
change_bcd = 8'h05; // 5 in BCD
#(SIMULATION_WAIT_TIME * 10); // Observe for a while
// Test case: Display paid amount = 20, change amount = 10
#(SIMULATION_WAIT_TIME);
paid_bcd = 8'h20; // 20 in BCD
change_bcd = 8'h10; // 10 in BCD
#(SIMULATION_WAIT_TIME * 10); // Observe for a while
// End of simulation
$stop;
end
endmodule仿真结果

将对应的a_to_g还原,可以发现这里的实现逻辑是正确的
4.vend
这里我测试了两种情况,投入15分和投入20分,两种情况都能实现动态扫描(an[3:0]),且每位动态扫描的过程中都能够正确的显示,并且实现了投入15分后等待10s后(仿真时将时间加快了)会自动清零重新开始计数。
仿真结果

至此,所有模块的仿真测试均通过。
六. 实验结果




评论