寒假在家一起练项目4(小脚丫)演示
利用小脚丫FPGA的综合技能训练平台,实现了定时、测温、报警、控制等功能。
标签
FPGA
guier~
更新2021-02-22
1268

一、功能描述

(1)、实现了一个可定时时钟的功能。用小脚丫FPGA核心模块的4个按键设置当前的时间,OLED显示数字钟的当前时间,精确到分钟。

(2)、实现了温度计的功能。通过功能底板上的温度传感器实时测量环境温度,并显示在OLED的屏幕上;

(3)、实现了与PC通信的功能。定时时钟到达整点时,将温度信息通过UART传递到电脑上,电脑上通过串口助手显示与OLED一致的温度信息;同时,PC端在接收到温度信息后,将一段音频信息通过UART发送给小脚丫,驱动小脚丫底板上的蜂鸣器播放这段音频。注:在播放音频文件时,OLED屏幕上的信息停止刷新,音频文件播放完毕后,OLED继续更新相关信息。

二、模块划分

Fkz8eafN1Z9l0srV-bWJjNUJGxBU

三、设计思路简述

总体思路是根据功能先编写、测试好各个子模块,之后编写顶层模块,串联起各模块接口信号。在实现项目时,借鉴(白嫖)了不少“小脚丫开源社区”(https://www.stepfpga.com/doc/stepfpgaboard)项目中的代码,同时也从网上大佬那里取了不少经,在此向各位大佬表示感谢。

(1)、无源蜂鸣器模块

想让蜂鸣器奏乐,控制好音调(频率)和节拍(时长)即可。具体实现时,可将来自上位机的字节数据作为计数的终点,翻转蜂鸣器输出信号,再控制好同一音符的重复频次。

module beeper
(
input					clk_in,		
input					rst_n_in,	
input					tone_en, //蜂鸣器使能信号	
input   [15:0]            tone,    //接收来自上位机的乐谱
output	reg				piano_out	
);

reg [6:0] music;
		
reg [15:0] time_end;

always@(*) begin
	time_end = tone;//根据接收的乐谱,确定计时终点
end
 
reg [17:0] time_cnt;
always@(posedge clk_in or negedge rst_n_in) begin
	if(!rst_n_in) begin
		time_cnt <= 1'b0;
	end else if(!tone_en) begin
		time_cnt <= 1'b0;
	end else if(time_cnt>=time_end) begin
		time_cnt <= 1'b0;
	end else begin
		time_cnt <= time_cnt + 1'b1;
	end
end
 
always@(posedge clk_in or negedge rst_n_in) begin
	if(!rst_n_in) begin
		piano_out <= 1'b0;
	end else if(time_cnt==time_end) begin
		piano_out <= ~piano_out;	
	end else begin
		piano_out <= piano_out;
	end
end
 
endmodule

(2)、RAM读写控制模块

为了能让蜂鸣器奏乐,实现时将上位机发送的乐谱数据暂存在了RAM中,然后从RAM中读出乐谱信息,并驱动蜂鸣器奏乐。项目中调用了diamond提供的伪双端口RAM IP,只需按照手册上的时序信息编写读写控制模块即可:

module ram_ctrl#
(								//parameter是verilog里参数定义
parameter	MC	=	80		//表示乐谱包含的字节个数
)
( 
    input clk           ,
    input rst_n              ,
    input bps_en_rx            ,//表示一个字节数据接收完毕:0表示完成接收 1表示接收中
					
    output reg [8:0]wraddress,
    output reg [8:0]rdaddress,
    output reg we,
	output reg rdclocken,
	output reg wrclocken
        );

    reg [6:0] count    ;

            
        
    //向ram写入数据
    /* assign we = !bps_en_rx ;//每次接收完成都把ram写使能打开 */
    always@(posedge clk or negedge rst_n)begin
         if(!rst_n)begin
			wraddress <= 9'b0;
			count <= 7'd0;
		 end
         else if (wrclocken)begin//写时钟使能有效,开始写入
			wraddress <= wraddress + 1 ; //每一次写完数据,地址加一
			count <= count + 7'd1;       //每次接收完数据后,将计数器加一
		 end
         else begin
			wraddress <= wraddress ;
			count <= count;
		 end
    end
	//捕捉we的上升沿,产生wrclocken信号
	reg temp_we;
	always@(posedge clk or negedge rst_n)begin
		if(!rst_n)begin
			temp_we <= 1'b0;
			wrclocken <= 1'b0;
		end
		else begin
			we <= !bps_en_rx;
			temp_we <= we;
			wrclocken <= we & (!temp_we);
		end
	end
       

	//产生读时钟有效信号
    always@(posedge clk or negedge rst_n)begin
        if(!rst_n)
			rdclocken <= 1'd0;
        else if(count == MC)//当乐谱的所有字节都写入到ram时,可以启动rdclocken来进行读操作
			rdclocken <= 1'b1;
        else
			rdclocken <= rdclocken;
    end 	
    //从到dpram读出数据:地址自加一
    always@(posedge clk or negedge rst_n)begin
        if(!rst_n)
			rdaddress <= 9'd0;
        else if(rdclocken)//读时钟使能有效时,进行读操作
			rdaddress <= rdaddress + 1;
        else
			rdaddress <= rdaddress;
    end    
       
		
endmodule

(3)、温度信息获取模块

首先根据单总线获取到16位的温度输出,之后将其按照各位的含义,处理得到温度的整数部分和小数部分。最后,利用“加三移位法”将十六进制表示的数转为BCD(8421)形式,以方便后面在OLED上显示:

//将采集的温度信息转为十进制,分为整数部分和小数部分
module ds18b20z(
input [15:0] in,

output [7:0] out_int,//温度整数部分
output [7:0] out_f//温度小数部分
);

wire [7:0] high,low;
wire [7:0] temp1,temp2;

assign high = in[15:8];
assign low = in[7:0];
assign temp1 = (high << 4) + (low >> 4);
assign temp2 = low & 8'h0F;
assign out_int = temp1;
assign out_f = (temp2 * 5)>>3;//相当于乘以0.625,获得小数部分

endmodule

//二进制转BCD(8421)码模块,采用加三移位法
 module bin_to_bcd(
	 input [7:0] binary,
	 output reg [3:0] Hundreds,//百位表示
	 output reg [3:0] Tens,//十位表示
	 output reg [3:0] Ones//个位表示
 );


integer i;
always @(binary)begin
	Hundreds = 4'd0;
	Tens = 4'd0;
	Ones = 4'd0;
	
	for(i = 7; i >= 0; i = i - 1)begin
		if(Hundreds >= 5)
			Hundreds = Hundreds + 3;
		if(Tens >= 5)
			Tens = Tens + 3;
		if(Ones >= 5)
			Ones = Ones + 3;
			
		Hundreds = Hundreds << 1;
		Hundreds[0] = Tens[3];
		Tens = Tens << 1;
		Tens[0] = Ones[3];
		Ones = Ones << 1;
		Ones[0] = binary[i];
	end
end
endmodule

(4)、时间设置

由于要进行时间的设置,故引入了一个模式的切换,当键按下时,系统跳到另一个状态。同时,在进行时间设置前,不要忘记按键消抖:

//模式切换
always @(posedge clk,negedge rst_n)begin
	if(!rst_n)begin
		mode <= 1'b1;
	end
	else if(key_pulse[0])begin//表示模式键已经按下
		mode <= ~mode;
	end
	else begin
		mode <= mode;
	end			
end

//设置时间
always @(posedge clk or negedge rst_n)begin
	if(!rst_n)begin
		hh1 <= h1;
		hh2 <= h2;
		mm1 <= m1;
		mm2 <= m2;
	end
	else if(!mode)begin
		if(key_pulse[1])begin//时钟位增加一
			hh2 <= hh2 + 4'd1;
			if(hh2 == 4'd9)begin                                                     
				hh2 <= 4'd0;
				hh1 <= hh1 + 4'd1;
			end
			else if(hh2 == 4'd3 && hh1 == 4'd2)begin
				hh1 <= 4'd0;
				hh2 <= 4'd0;
			end
		end
		else if(key_pulse[2])begin//分钟位增加一
			mm2 <= mm2 + 4'd1;
			if(mm2 == 4'd9)begin
				mm2 <= 4'd0;
				mm1 <= mm1 + 4'd1;
			
				if(mm1 == 4'd5)begin
				mm1 <= 4'd0;
				mm2 <= 4'd0;
				end
			end
		end
	end
	else begin
		hh1 <= h1;
		hh2 <= h2;
		mm1 <= m1;
		mm2 <= m2;			
	end
end

(5)、OLED显示

当整点播放音乐时,OLED停止刷新。实现时,将字符处理的MAIN状态加上蜂鸣器使能判断:

MAIN:begin
					//蜂鸣器响则屏幕停止刷新
					if(cnt_main >= 5'd30 && !t_piano_en2) cnt_main <= 5'd1;
					else if(cnt_main >= 5'd27 && t_piano_en2) cnt_main <= 5'd25;
					else 
						cnt_main <= cnt_main + 1'b1;
					case(cnt_main)	//MAIN状态

中文字符的显示:

利用点阵字库生成器生成16*16的点阵字库数据,并用G和H的ascii码作为其存储器的下标:

mem[ 71] = {8'h10,8'h60,8'h02,8'h8C,8'h00,8'h00,8'hFE,8'h92,
                    8'h92,8'h92,8'h92,8'h92,8'hFE,8'h00,8'h00,8'h00,
                    8'h04,8'h04,8'h7E,8'h01,8'h40,8'h7E,8'h42,8'h42,
                    8'h7E,8'h42,8'h7E,8'h42,8'h42,8'h7E,8'h40,8'h00};   // 65  G 温
		mem[ 72] = {8'h00,8'h00,8'hFC,8'h24,8'h24,8'h24,8'hFC,8'h25,
                    8'h26,8'h24,8'hFC,8'h24,8'h24,8'h24,8'h04,8'h00,
                    8'h40,8'h30,8'h8F,8'h80,8'h84,8'h4C,8'h55,8'h25,
                    8'h25,8'h25,8'h55,8'h4C,8'h80,8'h80,8'h80,8'h00};   // 66  H 度

由于利用了16*16的表示形式,故在OLED显示时,通过改变列页地址,在两页上显示一个字符,按序写页1页2:

case(cnt_main)	//MAIN状态
						5'd0:	begin state <= INIT; end
						5'd1:	begin num_delay <= 24'd10; y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd2; char <= "  ";state <= SCAN; end
						5'd2:	begin num_delay <= 24'd10; y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd2; char <= "  ";state <= SCAN; end
						//h1	
						5'd3:	begin num_delay <= 24'd10; y_p <= 8'hb0; x_ph <= 8'h12; x_pl <= 8'h00; num <= 5'd1; char <= t_h12;state <= SCAN; end
						5'd4:	begin num_delay <= 24'd10; y_p <= 8'hb1; x_ph <= 8'h12; x_pl <= 8'h00; num <= 5'd1; char <= t_h12;state <= SCAN; end
						//h2	
						5'd5:	begin num_delay <= 24'd10; y_p <= 8'hb0; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd1; char <= t_h22;state <= SCAN; end
						5'd6:	begin num_delay <= 24'd10; y_p <= 8'hb1; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd1; char <= t_h22;state <= SCAN; end
							
						//m1
						5'd7:	begin num_delay <= 24'd10; y_p <= 8'hb0; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd1; char <= t_m12;state <= SCAN; end
						5'd8:	begin num_delay <= 24'd10; y_p <= 8'hb1; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd1; char <= t_m12;state <= SCAN; end
						//m2
						5'd9:	begin num_delay <= 24'd10; y_p <= 8'hb0; x_ph <= 8'h16; x_pl <= 8'h00; num <= 5'd1; char <= t_m22;state <= SCAN; end
						5'd10:	begin num_delay <= 24'd10; y_p <= 8'hb1; x_ph <= 8'h16; x_pl <= 8'h00; num <= 5'd1; char <= t_m22;state <= SCAN; end
						//空格
						5'd11:	begin num_delay <= 24'd10; y_p <= 8'hb0; x_ph <= 8'h17; x_pl <= 8'h00; num <= 5'd1; char <= " ";state <= SCAN; end
						5'd12:	begin num_delay <= 24'd10; y_p <= 8'hb1; x_ph <= 8'h17; x_pl <= 8'h00; num <= 5'd1; char <= " ";state <= SCAN; end
						//“温度:”	
						5'd13:	begin num_delay <= 24'd10; y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd3; char <= "GH:";state <= SCAN; end
						5'd14:	begin num_delay <= 24'd10; y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd3; char <= "GH:";state <= SCAN; end
						//十位
						5'd15:	begin num_delay <= 24'd10; y_p <= 8'hb2; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd1; char <= data_decade;state <= SCAN; end
						5'd16:	begin num_delay <= 24'd10; y_p <= 8'hb3; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd1; char <= data_decade;state <= SCAN; end
						//个位	
						5'd17:	begin num_delay <= 24'd10; y_p <= 8'hb2; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd1; char <= data_units;state <= SCAN; end
						5'd18:	begin num_delay <= 24'd10; y_p <= 8'hb3; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd1; char <= data_units;state <= SCAN; end
						//小数点
						5'd19:	begin num_delay <= 24'd10; y_p <= 8'hb2; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd1; char <= ".";state <= SCAN; end
						5'd20:	begin num_delay <= 24'd10; y_p <= 8'hb3; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd1; char <= ".";state <= SCAN; end
						//小数部分
						5'd21:	begin num_delay <= 24'd10; y_p <= 8'hb2; x_ph <= 8'h16; x_pl <= 8'h00; num <= 5'd1; char <= out_f[3:0];state <= SCAN; end
						5'd22:	begin num_delay <= 24'd10; y_p <= 8'hb3; x_ph <= 8'h16; x_pl <= 8'h00; num <= 5'd1; char <= out_f[3:0];state <= SCAN; end

为了产生冒号闪动的效果,在冒号显示后增加了空格的显示,并各自增加了1s的显示延时,注意将冒号显示放在最后,避免造成数字等显示延时:

//冒号闪动	
						5'd25:	begin num_delay <= 24'd10; y_p <= 8'hb0; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd1; char <= ":";state <= SCAN; end
						5'd26:	begin num_delay <= 24'd10; y_p <= 8'hb1; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd1; char <= ":";state <= SCAN; end									
						5'd27:  begin num_delay <= 24'd12000000;  state <= DELAY; state_back <= MAIN; 	                   end
						5'd28:	begin num_delay <= 24'd10; y_p <= 8'hb0; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd1; char <= " "; state <= SCAN; end
						5'd29:	begin num_delay <= 24'd10; y_p <= 8'hb1; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd1; char <= " "; state <= SCAN; end
						5'd30:  begin num_delay <= 24'd12000000;  state <= DELAY; state_back <= MAIN; 	                   end	

四、资源占用情况

FrQakDOHLZBvtm5Nxdchq20uOyYe

五、总结感悟

1、当代码通过了功能仿真,上板测试却总是出错时,或许可以考虑考虑,是不是硬件出了问题,比如这次因为自己懒得焊接排针,OLED总是无法显示,最后平生第一次拿起了焊接枪,OLED终于乖乖显示了(在此感谢硬禾学堂的客服老师);

2、不要忘了异步时钟的同步;

3、一定要认真阅读文档,细节决定成败。

六、未来的计划

好好学习,天天向上,多找类似的项目练练手,多多锻炼自己的文档阅读及coding的能力。

附件下载
源码、 jed文件、 上位机代码、 文档.zip
源码、 jed文件、 上位机代码、 文档等
点阵字模生成工具.zip
点阵字模生成工具
团队介绍
北航 集成电路
团队成员
吴子贵
想入FPGA门的白菜
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号