寒假在家一起练项目4——基于小脚丫FPGA的综合技能训练平台
基于小脚丫FPGA的综合技能训练平台项目演示,完成OLED显示时间、温度,定点报时等功能。
标签
FPGA
显示
红红火火恍恍惚惚
更新2021-03-01
1278

功能描述

  1. 实现一个可定时时钟的功能,用小脚丫FPGA核心模块的4个按键设置当前的时间,OLED显示数字钟的当前时间,精确到分钟即可,到整点的时候比如8:00,蜂鸣器报警,播放音频信号,最长可持续30秒;
  2. 实现温度计的功能,小脚丫通过板上的温度传感器实时测量环境温度,并同时间一起显示在OLED的屏幕上;
  3. 定时时钟整点报警的同时,将温度信息通过UART传递到电脑上,电脑上能够显示当前板子上的温度信息(任何显示形式都可以),要与OLED显示的温度值一致;
  4. PC收到报警的温度信号以后,将一段音频文件(自己制作,持续10秒钟左右)通过UART发送给小脚丫FPGA,蜂鸣器播放收到的这段音频文件,OLED屏幕上显示的时间信息和温度信息都停住不再更新;
  5. 音频文件播放完毕,OLED开始更新时间信息和当前的温度信息

模块划分

FieqRcXt1g8gBrrx_9c1-D7g1EA6

设计思路

温度信息获取模块

首先计数器分频产生1MHz的时钟信号,再使用1MHz时钟信号做触发完成状态机的功能。状态机分为IDLE状态、MAIN状态、INIT状态、WRITE状态、READ状态和DELAY状态。获取到16位的温度输出后,将其按照各位的含义,处理得到温度的整数部分和小数部分。最后,利用“加三移位法”将十六进制表示的数转为BCD码形式,以便在OLED上显示。
STEP FPGA驱动温度传感器DS18B20Z

module temperature_display
(
	input				clk_in,			//系统时钟
	input				rst_n_in,		//系统复位,低有效
	inout				one_wire,		//DS18B20Z传感器单总线,双向管脚
    output reg	[15:0]	data_out		//DS18B20Z有效温度数据输出
);
 
localparam	IDLE	=	3'd0;
localparam	MAIN	=	3'd1;
localparam	INIT	=	3'd2;
localparam	WRITE	=	3'd3;
localparam	READ	=	3'd4;
localparam	DELAY	=	3'd5;
 
//计数器分频产生1MHz的时钟信号
reg					clk_1mhz;
reg		[2:0]		cnt_1mhz;
always@(posedge clk_in or negedge rst_n_in) begin
	if(!rst_n_in) begin
		cnt_1mhz <= 3'd0;
		clk_1mhz <= 1'b0;
	end else if(cnt_1mhz >= 3'd5) begin
		cnt_1mhz <= 3'd0;
		clk_1mhz <= ~clk_1mhz;	//产生1MHz分频
	end else begin
		cnt_1mhz <= cnt_1mhz + 1'b1;
	end
end
 
reg		[2:0]		cnt;
reg					one_wire_buffer;
reg		[3:0]		cnt_main;
reg		[7:0]		data_wr;
reg		[7:0]		data_wr_buffer;
reg		[2:0]		cnt_init;
reg		[19:0]		cnt_delay;
reg		[19:0]		num_delay;
reg		[3:0]		cnt_write;
reg		[2:0]		cnt_read;
reg		[15:0]		temperature;
reg		[7:0]		temperature_buffer;
reg		[2:0] 		state = IDLE;
reg		[2:0] 		state_back = IDLE;
//使用1MHz时钟信号做触发完成下面状态机的功能
always@(posedge clk_1mhz or negedge rst_n_in) begin
	if(!rst_n_in) begin
		state <= IDLE;
		state_back <= IDLE;
		cnt <= 1'b0;
		cnt_main <= 1'b0;
		cnt_init <= 1'b0;
		cnt_write <= 1'b0;
		cnt_read <= 1'b0;
		cnt_delay <= 1'b0;
		one_wire_buffer <= 1'bz;
		temperature <= 16'h0;
	end else begin
		case(state)
			IDLE:begin		//IDLE状态,程序设计的软复位功能,各状态异常都会跳转到此状态
					state <= MAIN;	//软复位完成,跳转之MAIN状态重新工作
					state_back <= MAIN;
					cnt <= 1'b0;
					cnt_main <= 1'b0;
					cnt_init <= 1'b0;
					cnt_write <= 1'b0;
					cnt_read <= 1'b0;
					cnt_delay <= 1'b0;
					one_wire_buffer <= 1'bz;
				end
			MAIN:begin		//MAIN状态控制状态机在不同状态间跳转,实现完整的温度数据采集
					if(cnt_main >= 4'd11) cnt_main <= 1'b0;
					else cnt_main <= cnt_main + 1'b1;
					case(cnt_main)
						4'd0: begin state <= INIT; end	//跳转至INIT状态进行芯片的复位及验证
						4'd1: begin data_wr <= 8'hcc;state <= WRITE; end	//主设备发出跳转ROM指令
						4'd2: begin data_wr <= 8'h44;state <= WRITE; end	//主设备发出温度转换指令
						4'd3: begin num_delay <= 20'd750000;state <= DELAY;state_back <= MAIN; end	//延时750ms等待转换完成
 
						4'd4: begin state <= INIT; end	//跳转至INIT状态进行芯片的复位及验证
						4'd5: begin data_wr <= 8'hcc;state <= WRITE; end	//主设备发出跳转ROM指令
						4'd6: begin data_wr <= 8'hbe;state <= WRITE; end	//主设备发出读取温度指令
 
						4'd7: begin state <= READ; end	//跳转至READ状态进行单总线数据读取
						4'd8: begin temperature[7:0] <= temperature_buffer; end	//先读取的为低8位数据
 
						4'd9: begin state <= READ; end	//跳转至READ状态进行单总线数据读取
						4'd10: begin temperature[15:8] <= temperature_buffer; end	//后读取的为高8为数据
 
						4'd11: begin state <= IDLE;data_out <= temperature; end	//将完整的温度数据输出并重复以上所有操作
						default: state <= IDLE;
					endcase
				end
			INIT:begin		//INIT状态完成DS18B20Z芯片的复位及验证功能
					if(cnt_init >= 3'd6) cnt_init <= 1'b0;
					else cnt_init <= cnt_init + 1'b1;
					case(cnt_init)
						3'd0: begin one_wire_buffer <= 1'b0; end	//单总线复位脉冲拉低
						3'd1: begin num_delay <= 20'd500;state <= DELAY;state_back <= INIT; end	//复位脉冲保持拉低500us时间
						3'd2: begin one_wire_buffer <= 1'bz; end	//单总线复位脉冲释放,自动上拉
						3'd3: begin num_delay <= 20'd100;state <= DELAY;state_back <= INIT; end	//复位脉冲保持释放100us时间
						3'd4: begin if(one_wire) state <= IDLE; else state <= INIT; end	//根据单总线的存在检测结果判定是否继续
						3'd5: begin num_delay <= 20'd400;state <= DELAY;state_back <= INIT; end	//如果检测正常继续保持释放400us时间
						3'd6: begin state <= MAIN; end	//INIT状态操作完成,返回MAIN状态
						default: state <= IDLE;
					endcase
				end
			WRITE:begin		//按照DS18B20Z芯片单总线时序进行写操作
					if(cnt <= 3'd6) begin	//共需要发送8bit的数据,这里控制循环的次数
						if(cnt_write >= 4'd6) begin cnt_write <= 1'b1; cnt <= cnt + 1'b1; end
						else begin cnt_write <= cnt_write + 1'b1; cnt <= cnt; end
					end else begin
						if(cnt_write >= 4'd8) begin cnt_write <= 1'b0; cnt <= 1'b0; end	//两个变量都恢复初值
						else begin cnt_write <= cnt_write + 1'b1; cnt <= cnt; end
					end
					//对于WRITE状态中cnt_write来讲,执行过程为:0;[1~6]*8;7;8;
					case(cnt_write)
						//lock data_wr
						4'd0: begin data_wr_buffer <= data_wr; end	//将需要写出的数据缓存
						//发送 1bit 数据的用时在60~120us之间,参考数据手册
						4'd1: begin one_wire_buffer <= 1'b0; end	//总线拉低
						4'd2: begin num_delay <= 20'd2;state <= DELAY;state_back <= WRITE; end	//延时2us时间,保证15us以内
						4'd3: begin one_wire_buffer <= data_wr_buffer[cnt]; end	//先发送数据最低位
						4'd4: begin num_delay <= 20'd80;state <= DELAY;state_back <= WRITE; end	//延时80us时间
						4'd5: begin one_wire_buffer <= 1'bz; end	//总线释放
						4'd6: begin num_delay <= 20'd2;state <= DELAY;state_back <= WRITE; end	//延时2us时间
						//back to main
						4'd7: begin num_delay <= 20'd80;state <= DELAY;state_back <= WRITE; end	//延时80us时间
						4'd8: begin state <= MAIN; end	//返回MAIN状态
						default: state <= IDLE;
					endcase
				end
			READ:begin		//按照DS18B20Z芯片单总线时序进行读操作
					if(cnt <= 3'd6) begin	//共需要接收8bit的数据,这里控制循环的次数
						if(cnt_read >= 3'd5) begin cnt_read <= 1'b0; cnt <= cnt + 1'b1; end
						else begin cnt_read <= cnt_read + 1'b1; cnt <= cnt; end
					end else begin
						if(cnt_read >= 3'd6) begin cnt_read <= 1'b0; cnt <= 1'b0; end	//两个变量都恢复初值
						else begin cnt_read <= cnt_read + 1'b1; cnt <= cnt; end
					end
					case(cnt_read)
						//读取 1bit 数据的用时在60~120us之间,总线拉低后15us时间内读取数据,参考数据手册
						3'd0: begin one_wire_buffer <= 1'b0; end	//总线拉低
						3'd1: begin num_delay <= 20'd2;state <= DELAY;state_back <= READ; end	//延时2us时间
						3'd2: begin one_wire_buffer <= 1'bz; end	//总线释放
						3'd3: begin num_delay <= 20'd5;state <= DELAY;state_back <= READ; end	//延时5us时间
						3'd4: begin temperature_buffer[cnt] <= one_wire; end	//读取DS18B20Z返回的总线数据,先收最低位
						3'd5: begin num_delay <= 20'd60;state <= DELAY;state_back <= READ; end	//延时60us时间
						//back to main
						3'd6: begin state <= MAIN; end	//返回MAIN状态
						default: state <= IDLE;
					endcase
				end
			DELAY:begin		//延时控制
					if(cnt_delay >= num_delay) begin	//延时控制,延时时间由num_delay指定
						cnt_delay <= 1'b0;
						state <= state_back; 	//很多状态都需要延时,延时后返回哪个状态由state_back指定
					end else cnt_delay <= cnt_delay + 1'b1;
				end
		endcase
	end
end
 
assign	one_wire = one_wire_buffer;
 
endmodule

蜂鸣器模块

不同的震荡频率会产生不同的音节,通过赋给蜂鸣器不同的频率会奏出一段音乐。
具体乐谱在上位机中,使用python。
STEP FPGA驱动无源蜂鸣器模块

UART收发器

使用UART在板子和PC端进行数据传送。当整点时,向上位机发送温度信息,并接收上位机传输的乐谱信息,记录计时终点。
异步收发器UART

设置模式模块

在设置时间模块和计时模块之间切换,需确保时钟的相同。

//模式切换
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

OLED显示

一开始不明白OLED显示屏的工作原理和编程方式,后来经过查阅资料,并利用网上的点阵字库生成器生成16*16的点阵字库数据。
OLED显示屏有4页,这里参考了电子森林的开源代码和吴子贵同学的显示方式:1、2列显示时钟数据,3、4列显示温度数据,并且将5*8点阵编程为8*8。

资源占用

FgqUOUKkzavO-5fKHk8QlsoFRObV

上位机

借鉴吴子贵同学的乐谱及上位机代码,使用pyqt5制作简单的界面。

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'untitled.ui'
#
# Created by: PyQt5 UI code generator 5.15.2
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt5 import QtCore, QtGui, QtWidgets
import threading
import serial
import sys

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(600, 300)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.label = QtWidgets.QLabel(self.centralwidget)
        self.label.setGeometry(QtCore.QRect(40, 60, 81, 31))
        font = QtGui.QFont()
        font.setPointSize(18)
        self.label.setFont(font)
        self.label.setObjectName("label")
        self.textBrowser = QtWidgets.QTextBrowser(self.centralwidget)
        self.textBrowser.setGeometry(QtCore.QRect(140, 60, 241, 41))
        self.textBrowser.setObjectName("textBrowser")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 509, 22))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.label.setText(_translate("MainWindow", "当前温度:"))

    def refreshText(self):
        if (ser.isOpen()):
            line = ser.read(5)
            if (line):
                data = line.decode('gb18030') + "℃";  # 形成温度字符串
                self.textBrowser.setText(data)  # 更新标签温度
                ser.write(bytes.fromhex(wdata))  # 转为16进制数据发送
        else:
            print("open failed")
        timer = threading.Timer(0.001, self.refreshText)
        timer.start()

if __name__ == '__main__':
    port = "COM4"
    bps = 9600
    # 打开串口
    ser = serial.Serial(port, int(bps), timeout=0.01, bytesize=8, parity=serial.PARITY_NONE, stopbits=1)
    wdata = "B9 17 B9 6C B9 17 B9 6C B9 17 B9 6C A1 1A B9 6C B9 6C A1 1A B9 6C F1 F2 F3 F4 B9 17 6C 16 6C 16 A1 1A A1 1A A1 1A F3 13 F3 13 F3 13 C5 11 C5 11 C5 11 C5 11 F3 13 F3 13 F3 13 F3 13 F3 13 F3 13 F3 13 F3 13 F3 13 6C 16 B9 17 E4 1D 8C 23 E7 27 8C 23 8C 23 8C 23 8C 23 E4 1D E4 1D E4 1D A1 1A A1 1A A1 1A 6C 16 B9 17 6C 16 6C 16 DC 0B DC 0B"
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()

    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)

    MainWindow.show()
    sys.exit(app.exec_())

心得体会

  1. 对OLED显示屏和蜂鸣器有了新的了解。
  2. 与微型计算机课程相联系,对Uart收发、时钟分频模块都有了更深入的了解。
  3. 上位机编写较为简单,可以后期加入更多的功能,如扫描串口等。

 

 

附件下载
project.zip
烧录文件及上位机代码
团队介绍
北理电信
团队成员
杨夏巍
电信
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号