一、需求分析
设计一个简易的出租车计费系统,实现计价功能,计费标准为按里程收费,起步价为6.00元,当里程小于3公里时,按起步价收费,超过3公里后按1.2元/公里收费。
实现车辆行驶的模拟:能模拟汽车的启动,暂停,停止等状态。
计费显示部分设计:用LED数码管实时显示车费和汽车行驶里程,用一个按键切换车费和里程的显示,里程单位km,记程范围为0-99km。
二、输入输出端口
输入:系统时钟、系统复位、行驶模拟按键(启动、暂停、停止)、显示切换按键
输出:①数码管段选、位选(没有驱动芯片)
②ds数据、oe使能、shcp移位时钟、stcp寄存时钟(74hc595串并转换数码管驱动芯片)
三、模块介绍
1.计费逻辑状态机:
状态机的核心是一个case语块,是一种编程思想。case的条件是不同的状态(state),每个状态下分别进行各种操作,状态的切换由输入信号条件控制。在写法上可以只定义一个状态(state),或者定义两个状态(now_state、next_state)现态和次态。再写一个状态转移,让次态不断传递给现态,完成状态的切换。
在本项目中,使用状态机思想,描述汽车的启动、暂停、停止几种状态。
停止状态:此状态下对车费和里程清零,并判断启动键是否按下,进入启动状态
启动状态:让里程自增(模拟汽车行驶)让车费为6元,当里程大于等于3公里,车费开始每公里1.2元增加。
暂停状态:不进行操作,只判断启动和停止键是否按下进行跳转。
2.数码管动态显示:
数码管一般是由7个led灯组成,称为7段数码管,加上dp小数点称为8段。
如图,要显示数字“1”,bc亮,其他不亮,段码则为1001_1111,转为16进制从低位到高位数,为F9。开发板上有6位数码管,即6个这样的数码管连接在一起,通过位选来控制让哪一个数码管有效,再通过段选控制显示的内容。
所以,动态显示=位选信号从1-6扫描为周期,例如要显示数字“2023”,在选通1号数码管亮的时候,段选为“3”;接着选通2号数码管亮,段选输出“2”;接着让3号数码管亮,段选“0”;4号数码管亮,段选“2”,这样通过高速4次扫描,由于人眼暂留效应,看到的就是“2023”同时亮起的效果。
3.bcd_8421模块:
明白了数据的获取(计费逻辑)与显示(动态数码管),但实际上要想把一个数字的不同数位分别显示出来,还需要一个拆分功能,把一个数拆成小数位、个位、十位、百位等,然后把不同位分别送入不同数码管动态显示。
起初我是自己用除法写了拆分逻辑,进行电路综合后发现,在FPGA芯片中,除法非常占用资源,导致我的资源块超出(下图报错信息),无法进行布局布线设计。FPGA有硬件乘法器,但没有除法器。如果非要用除法,建议使用ip核除法软核,不过ip核也是使用“+,-,移位”实现的。所以不如自己写一个拆分模块。
assign fare_g[7:4] = (fare/10) % 10;
assign fare_g[11:8] = (fare/100) % 10;
assign fare_g[15:12] = (fare/1000) % 10;
拆分思想是,先补零,移位判断是否大于4,大于就加3再判断。对于十进制数的位数进行判断,例如“234”,3位,补3x4=12个0,进行如下操作后,“2”“3”“4”就被拆开分别放入3组4位二进制里了。(下图取自野火FPGA教程)
对费用数据、里程数据进行拆分后,分别放入段选寄存器中,按位把数据组一个一个送入595芯片,595芯片将串行传入的数据,进行移位存入寄存器,当一组8位数据存好,再复制到输出寄存器并行输出,编写控制移位时钟和寄存器时钟的时序,送入595芯片,即可实现串并转换。
如果没有595芯片,就需要编写动态扫描周期进程,按照位选段选时序要求,依次让数码管不同位显示不同数据。
四、代码
1.计费逻辑与显示数据生成模块
module data_gen //数据生成模块
#(
parameter CNT_MAX = 26'd49_999_999,
)
(
input wire sys_clk , // 系统时钟
input wire sys_rst_n , // 系统复位
input wire start , // 启动
input wire stop , // 停止
input wire pause , // 暂停
input wire key_switch , // 切换显示,
output reg [19:0] data, //数据输出
output reg [5:0] point, //小数点
output wire sign, //正负号
output reg seg_en //显示使能
);
parameter STOP = 4'b0000 , //停止
START = 4'b0001 , //启动
PAUSE = 4'b0010 ; //暂停,三个状态值
reg [25:0] cnt_100ms;
reg cnt_flag;
reg [10:0] fare; // 车费
reg [6:0] total_distance; // 总行驶里程
reg [3:0] state; // 车辆状态
//100ms的计费自增周期产生
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_100ms <= 26'd0;
else if(cnt_100ms == CNT_MAX)
cnt_100ms <= 26'd0;
else
cnt_100ms <= cnt_100ms + 1'b1;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_flag <= 1'b0;
else if(cnt_100ms == CNT_MAX - 1'b1)
cnt_flag <= 1'b1;
else
cnt_flag <= 1'b0;
//计费逻辑状态机进程
always@(posedge sys_clk or negedge sys_rst_n)
if(!sys_rst_n)
begin
fare <= 11'b0; //初始化车费、路程等清零
total_distance <= 7'b0;
state <= 4'b0;
end
else
begin //状态机各个状态转换
case (state)
STOP: // 停止状态
if(start == 1'b0)
begin
state <= START; // 切换到启动状态
end
else
begin
total_distance <= 7'b0;
fare <= 11'b0;
end
START: // 启动状态
if(pause == 1'b0)
begin
state <= PAUSE; // 切换到暂停状态
end
else if(cnt_flag == 1'b1)
begin
total_distance <= total_distance + 1'b1; // 模拟行驶
if(total_distance <= 7'd2)
begin
fare <= 11'd60; // 起步价6.00元
end // 这里用60代表6元,12代表1.2元
else
begin
fare <= fare + 11'd12; // 超过3公里,每公里1.2元
end
end
else if(stop == 1'b0) begin
state <= STOP; // 切换到停止状态
end
PAUSE: // 暂停状态
if(start == 1'b0)
begin
state <= START; // 切换到启动状态
end
else if(stop == 1'b0)
begin
state <= STOP; // 切换到停止状态
end
endcase
end
//显示切换进程
always@(posedge sys_clk or negedge sys_rst_n)
if(!sys_rst_n)
begin
data <= 20'd0;
point <= 6'b000_000;
end
else if(key_switch == 1'b0) //显示费用模式
begin
data <= fare;
point <= 6'b000_010;
end
else //显示里程模式
begin
data <= total_distance;
point <= 6'b000_000;
end
assign sign = 1'b0;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
seg_en <= 1'b0;
else
seg_en <= 1'b1;
endmodule
2.595芯片驱动控制模块
module hc595_ctrl
(
input wire sys_clk , //系统时钟
input wire sys_rst_n, //系统复位
input wire [7:0] seg , //数码管段选
input wire [5:0] sel , //数码管位选
output reg shcp , //移位时钟
output reg stcp , //寄存器时钟
output reg ds , //数据
output wire oe //使能
);
wire [13:0] data ;
reg [1:0] cnt ;
reg [3:0] cnt_bit ;
assign data = {seg[0],seg[1],seg[2],seg[3],
seg[4],seg[5],seg[6],seg[7],sel}; //把段位数据放入data
//产生周期扫描信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt <= 2'd0;
else if(cnt == 2'd3)
cnt <= 2'd0;
else
cnt <= cnt + 2'd1;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_bit <= 4'd0;
else if((cnt_bit == 4'd13) && (cnt == 2'd3))
cnt_bit <= 4'd0;
else if(cnt == 2'd3)
cnt_bit <= cnt_bit + 1'b1;
else
cnt_bit <= cnt_bit;
//按周期扫描信号,依次把数据传入595芯片ds
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
ds <= 1'b0;
else if(cnt == 2'd0)
ds <= data[cnt_bit];
else
ds <= ds;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
shcp <= 1'b0;
else if(cnt == 2'd2)
shcp <= 1'b1;
else if(cnt == 1'b0)
shcp <= 1'b0;
else
shcp <= shcp;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
stcp <= 1'b0;
else if((cnt == 1'b0) && (cnt_bit == 4'd0))
stcp <= 1'b1;
else if((cnt ==2'd2) && (cnt_bit ==4'd0))
stcp <= 1'b0;
else
stcp <= stcp;
assign oe = 1'b0;
endmodule
3.bcd_8421数据拆分模块
module bcd_8421
(
input wire sys_clk ,
input wire sys_rst_n ,
input wire [11:0] data , //要拆分的数据
output reg [3:0] unit , //拆分后的个位
output reg [3:0] ten , //十位
output reg [3:0] hun , //百位
output reg [3:0] tho //千位
);
reg [3:0] cnt_shift;
reg [27:0] data_shift;
reg shift_flag;
//循环移位次数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_shift <= 4'd0;
else if((cnt_shift == 4'd13)&& (shift_flag == 1'b1))
cnt_shift <= 4'd0;
else if(shift_flag == 1'b1)
cnt_shift <= cnt_shift + 1'b1;
else
cnt_shift <= cnt_shift;
//4位一组进行判断>4否? 大于就加3
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
data_shift <= 27'd0;
else if(cnt_shift == 4'd0)
data_shift <= {16'b0,data};
else if((cnt_shift <= 12) && (shift_flag == 1'b0))
begin
data_shift[15:12] <= (data_shift[15:12] > 4) ? (data_shift[15:12] +2'd3) : (data_shift[15:12]);
data_shift[19:16] <= (data_shift[19:16] > 4) ? (data_shift[19:16] +2'd3) : (data_shift[19:16]);
data_shift[23:20] <= (data_shift[23:20] > 4) ? (data_shift[23:20] +2'd3) : (data_shift[23:20]);
data_shift[27:24] <= (data_shift[27:24] > 4) ? (data_shift[27:24] +2'd3) : (data_shift[27:24]);
end
else if((cnt_shift <= 12) && (shift_flag == 1'b1))
data_shift <= data_shift << 1;
else
data_shift <= data_shift;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
shift_flag <= 1'b0;
else
shift_flag <= ~shift_flag;
//移位判断一个周期后,进行数据输出
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
begin
unit <= 4'd0;
ten <= 4'd0;
hun <= 4'd0;
tho <= 4'd0;
end
else if(cnt_shift == 5'd13)
begin
unit <= data_shift[15:12];
ten <= data_shift[19:16];
hun <= data_shift[23:20];
tho <= data_shift[27:24];
end
endmodule
4.数码管段选位选数据产生模块
`timescale 1ns/1ns
module seg_dynamic
(
input wire sys_clk ,
input wire sys_rst_n,
input wire [19:0] data ,
input wire [5:0] point ,
input wire sign ,
input wire seg_en ,
output reg [7:0] seg ,
output reg [5:0] sel
);
parameter CNT_MAX = 16'd49_999;
wire [3:0] unit ;
wire [3:0] ten ;
wire [3:0] hun ;
wire [3:0] tho ;
wire [3:0] t_tho ;
wire [3:0] h_hun ;
reg [23:0] data_reg ;
reg [15:0] cnt_1ms ;
reg flag_1ms ;
reg [2:0] cnt_sel ;
reg [5:0] sel_reg ;
reg [3:0] data_disp ;
reg dot_disp ;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
data_reg <= 24'd0;
else if((h_hun) || (point[5])) //最高位非零,则全显示
data_reg <= {h_hun,t_tho,tho,hun,ten,unit};
//次高位非零,少显示一位最高位
else if(((t_tho) || (point[4])) && (sign == 1'b1))
data_reg <= {4'd10,t_tho,tho,hun,ten,unit};
else if(((t_tho) || (point[4])) && (sign == 1'b0))
data_reg <= {4'd11,t_tho,tho,hun,ten,unit};
else if(((tho) || (point[3])) && (sign == 1'b1))
data_reg <= {4'd11,4'd10,tho,hun,ten,unit};
else if(((tho) || (point[3])) && (sign == 1'b0))
data_reg <= {4'd11,4'd11,tho,hun,ten,unit};
else if(((hun) || (point[2])) && (sign == 1'b1))
data_reg <= {4'd11,4'd11,4'd10,hun,ten,unit};
else if(((hun) || (point[2])) && (sign == 1'b0))
data_reg <= {4'd11,4'd11,4'd11,hun,ten,unit};
else if(((ten) || (point[1])) && (sign == 1'b1))
data_reg <= {4'd11,4'd11,4'd11,4'd10,ten,unit};
else if(((ten) || (point[1])) && (sign == 1'b0))
data_reg <= {4'd11,4'd11,4'd11,4'd11,ten,unit};
//只显示最低位一位
else if(((unit) || (point[0])) && (sign == 1'b1))
data_reg <= {4'd11,4'd11,4'd11,4'd11,4'd10,unit};
else
data_reg <= {4'd11,4'd11,4'd11,4'd11,4'd11,unit};
//1ms循环计数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_1ms <= 16'd0;
else if(cnt_1ms == CNT_MAX)
cnt_1ms <= 16'd0;
else
cnt_1ms <= cnt_1ms +1'b1;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
flag_1ms <= 1'b0;
else if(cnt_1ms == CNT_MAX -1'b1)
flag_1ms <= 1'b1;
else
flag_1ms <= 1'b0;
//从0到5循环数,选择当前显示的数码管
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_sel <= 3'd0;
else if((cnt_sel == 3'd5) && (flag_1ms == 1'b1))
cnt_sel <= 3'd0;
else if(flag_1ms == 1'b1)
cnt_sel <= cnt_sel + 3'd1;
else
cnt_sel <= cnt_sel;
//根据上面进程,让位选信号移位
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
sel_reg <= 6'b000_000;
else if((cnt_sel == 3'd0) && (flag_1ms == 1'b1))
sel_reg <= 6'b000_001;
else if(flag_1ms == 1'b1)
sel_reg <= sel_reg << 1;
else
sel_reg <= sel_reg;
//六个数码管轮流显示
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
data_disp <= 4'd0;
else if((seg_en == 1'b1) &&(flag_1ms == 1'b1))
case(cnt_sel)
3'd0: data_disp <= data_reg[3:0];
3'd1: data_disp <= data_reg[7:4];
3'd2: data_disp <= data_reg[11:8];
3'd3: data_disp <= data_reg[15:12];
3'd4: data_disp <= data_reg[19:16];
3'd5: data_disp <= data_reg[23:20];
default:data_disp <= 4'd0;
endcase
else
data_disp <= data_disp;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
dot_disp <= 1'b1;
else if(flag_1ms == 1'b1)
dot_disp <= ~point[cnt_sel];
else
dot_disp <= dot_disp;
//数码管显示数字与段码映射
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
seg <= 8'b1111_1111;
else
case(data_disp)
4'd0: seg <= {dot_disp,7'b100_0000};
4'd1: seg <= {dot_disp,7'b111_1001};
4'd2: seg <= {dot_disp,7'b010_0100};
4'd3: seg <= {dot_disp,7'b011_0000};
4'd4: seg <= {dot_disp,7'b001_1001};
4'd5: seg <= {dot_disp,7'b001_0010};
4'd6: seg <= {dot_disp,7'b000_0010};
4'd7: seg <= {dot_disp,7'b111_1000};
4'd8: seg <= {dot_disp,7'b000_0000};
4'd9: seg <= {dot_disp,7'b001_0000};
4'd10: seg <= 8'b1011_1111;
4'd11: seg <= 8'b1111_1111;
default: seg <= 8'b0000_0000;
endcase
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
sel <= 6'b000_000;
else
sel <= sel_reg;
bcd_8421 bcd_8421_inst
(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n),
.data (data ),
.unit (unit ),
.ten (ten ),
.hun (hun ),
.tho (tho ),
.t_tho (t_tho ),
.h_hun (h_hun )
);
endmodule
5.不使用595芯片版本,产生数码管扫描信号
//数据与数码管段码映射关系
always@(posedge sys_clk or negedge sys_rst_n)
if(!sys_rst_n)
begin
seg_data <= 8'd0;
end
else
begin
case (seg_cache)
4'd0: seg_data <= {point,SEG_0};
4'd1: seg_data <= {point,SEG_1};
4'd2: seg_data <= {point,SEG_2};
4'd3: seg_data <= {point,SEG_3};
4'd4: seg_data <= {point,SEG_4};
4'd5: seg_data <= {point,SEG_5};
4'd6: seg_data <= {point,SEG_6};
4'd7: seg_data <= {point,SEG_7};
4'd8: seg_data <= {point,SEG_8};
4'd9: seg_data <= {point,SEG_9};
default: seg_data <= 8'b1111_1111;
endcase
end
//产生数码管位选扫描更新1ms刷新
always@(posedge clk_1ms or negedge sys_rst_n)
if(!sys_rst_n)
cnt_dig <= 1'b0;
else if(cnt_dig == 2'd3)
cnt_dig <= 1'b0;
else
cnt_dig <= cnt_dig + 1'b1;
//数码管段选位选数据扫描1ms刷新
always@(posedge clk_1ms or negedge sys_rst_n)
if(!sys_rst_n)
begin
seg_cache <= 8'b1111_1111;
seg_dig <= 4'b0000;
end
else if(key_switch == 1'b0) //显示费用模式
begin
case (cnt_dig)
2'd0: begin
seg_dig <= 4'b1110; //点亮第1位数码管
seg_cache <= fare_g[3:0]; //费用的第1位
point <= 1'b0; //小数点不亮
end
2'd1: begin
seg_dig <= 4'b1101; //点亮第2位数码管
seg_cache <= fare_g[7:4]; //费用的第2位
point <= 1'b1; //小数点亮
end
2'd2: begin
seg_dig <= 4'b1011; //点亮第3位数码管
seg_cache <= fare_g[11:8]; //费用的第3位
point <= 1'b0; //小数点不亮
end
2'd3: begin
seg_dig <= 4'b0111; //点亮第4位数码管
seg_cache <= fare_g[15:12]; //费用的第4位
point <= 1'b0; //小数点不亮
end
default:;
endcase
end
else //显示里程模式
begin
case (cnt_dig)
2'd0: begin
seg_dig <= 4'b1110; //同上
seg_cache <= distance_g[3:0];
end
2'd1: begin
seg_dig <= 4'b1101;
seg_cache <= distance_g[7:4];
end
2'd2: begin
seg_dig <= 4'b1011;
seg_cache <= 4'd0;
end
2'd3: begin
seg_dig <= 4'b0111;
seg_cache <= 4'd0;
end
default:;
endcase
end
五、仿真与实物效果演示
1.费用与里程关系
60代表6元,12代表1.2元。由图可见:在里程从1km到3km递增时,费用固定60也即起步价6元;当里程4km,5km递增时,费用递增1.2元。符合设计要求。
2.数码管动态显示
Seg_data为数码管段选,seg_dig为数码管位选。由图可见:在费用fare为168,16.8元
位选1110,第1位数码管点亮,段码为00000000,显示数字8;
位选1101,第2位数码管点亮,段码为10000010,显示数字6;
位选1011,第3位数码管点亮,段码为01111001,显示数字1;
位选0111,第4位数码管点亮,段码为01000000,显示数字0。
其中第二位的小数点dp点亮,即代表第一位为小数,实际显示“016.8”,符合设计。
同上分析,在显示里程的时候,total_distance为35km,四位数码管依次显示“0035”。
3.上板演示
出租车计费系统演示
六、总结
通过本次项目实战,巩固了数码管动态显示知识,提高了代码编写思路,在编写逻辑算法时,没有年初刚开始学习FPGA时的那种无从下手。也遇到很多bug,仿真时没有关掉modelsim,仿真报错,原因要关掉才能重新仿真。数据更新与数码管显示的频率要有合理的区分度,不然数据已经变化了,数码管还没动态刷新一个周期。在例化模块的时候,该用wire线型还是reg寄存器型,需要考虑清楚对这个信号进行了时序逻辑操作还是组合逻辑。