一、需求分析

        设计一个简易的出租车计费系统,实现计价功能,计费标准为按里程收费,起步价为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寄存器型,需要考虑清楚对这个信号进行了时序逻辑操作还是组合逻辑。

推荐阅读

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。