2021暑假一起练-基于小脚丫FPGA综合技能训练板的数字电压表
基于小脚丫FPGA的综合技能训练平台完成的数字电压表,能够在数码管上显示温度,OLED显示屏上显示电压,伴随流水灯的闪烁和RGBLED灯呼吸,数据通过串口发送到上位机,在GUI中显示。
标签
嵌入式系统
FPGA
小脚丫
电压表
student
更新2021-09-10
873

基于小脚丫FPGA的综合技能训练平台完成的数字电压表,能够在数码管上显示温度,OLED显示屏上显示电压,伴随流水灯的闪烁和RGBLED灯呼吸,数据通过串口发送到上位机,在GUI中显示。

作品完成了3个要求:

  1. 旋转电位计可以产生0-3.3V的电压
  2. 利用板上的串行ADC对电压进行转换
  3. 将电压值在板上的OLED屏幕上显示出来

作品框图

FmKImHw3HeGhIGZS21Llx0RMPCAa

  • 8个LED灯以流水灯的方式闪烁
  • RGBLED以呼吸灯的方式呼吸
  • 通过单线协议读取DS18B20的温度
  • 通过数码管显示温度
  • 通过ADC测量电位器的电压
  • 测量的电压在OLED上显示
  • 温度与电压通过串口发送到上位机
  • 上位机收到数据后,通过GUI显示数据

内容分析 1.流水灯LED

板子上的LED灯循环闪烁,每个LED点亮时间为1秒钟,之后熄灭,点亮下一盏LED灯。利用分频器对板载时钟进行分频,产生1Hz的时钟信号,用来驱动LED灯。使用3-8译码器,以节约存储空间。

/* flashled.v */
module flashled (clk,rst,led);
 
	input clk,rst;						
	output [7:0] led;				
 
 
        reg   [2:0] cnt ;                               //定义了一个3位的计数器,输出可以作为3-8译码器的输入
 
        wire clk1h;                                     //定义一个中间变量,表示分频得到的时钟,用作计数器的触发        
 
        //例化module decode38,相当于调用
        decode38 u1 (                                   
			.sw(cnt),                       //例化的输入端口连接到cnt,输出端口连接到led  
			.led(led)
			);
 
        //例化分频器模块,产生一个1Hz时钟信号		
        divide #(.WIDTH(32),.N(12000000)) u2 (         //传递参数
			.clk(clk),
			.rst_n(rst),                   //例化的端口信号都连接到定义好的信号
			.clkout(clk1h)
			);                             
 
        //1Hz时钟上升沿触发计数器,循环计数		
        always @(posedge clk1h or negedge rst)
	     if (!rst)
		cnt <= 0;
	     else
		cnt <= cnt +1;
 
endmodule

参考资料:https://www.stepfpga.com/doc/%E6%B5%81%E6%B0%B4%E7%81%AF

2.RGBLED

板载RGBLED共有两组,作品中使用的为第一组。每组RGBLED由3个小灯珠组成,分别为红色、绿色和蓝色,每个小灯珠均可通过IO口进行单独控制,也可以多个一起控制,3个一起点亮时,发光为白色,稍微偏青色。

呼吸灯每8秒钟完成一次呼吸,使用PWM,给LED灯上施加数字信号,通过调整数字信号的占空比(调整占空比 = 调整有效值)来控制LED灯的亮度。根据原理图可知,呼吸灯低电平有效,也就是说,占空比为0的时候最亮,占空比为1的时候最暗。

/* breath_led.v */
module breath_led(
    input clk,
    input rst,
    output  led
);
    parameter CNT_NUM = 2000;
    reg [10:0]cnt1, cnt2;
    reg cnt1_clk;
    reg flag;//1+0-
    wire clk_1M;
    
    always@(posedge clk_1M, negedge rst)
    begin
        if(!rst)
        begin
            cnt1 <= 9'b0;
            cnt1_clk <= 1'b0;
        end
        else if(cnt1 == CNT_NUM-1)
        begin
            cnt1 <= 9'b0;
            cnt1_clk <= 1'b1;
        end
        else
        begin
            cnt1 <= cnt1 + 1'b1;
            cnt1_clk <= 1'b0;
        end
    end
    
    always@(posedge cnt1_clk, negedge rst)
    begin
        if(!rst)
        begin
            cnt2 <= 9'b0;
            flag <= 1'b1;
        end
        else 
        begin
            if(flag)//+
            begin
                if(cnt2 == CNT_NUM-1)//+满了
                    flag <= 1'b0;
                else
                    cnt2 <= cnt2 + 1'b1;
            end
            else//if(flag)//+
            begin
                if(cnt2 == 0)//+满了
                    flag <= 1'b1;
                else
                    cnt2 <= cnt2 - 1'b1;
            end
        end
    end
    
    
    divide #(.WIDTH(4), .N(12))u1
    (.clk(clk), .rst_n(rst), .clkout(clk_1M));
    
    
    
    assign led = (cnt1>cnt2)?1'b1:1'b0;
    
    
    
    
    
endmodule

参考资料https://www.stepfpga.com/doc/%E5%91%BC%E5%90%B8%E7%81%AF

3.单线协议读取DS18B20的温度

DS18B20是常用的一款温度传感器芯片,只需要一根总线就可以实现通信,非常的方便。

DS18B20测量温度范围宽,测量范围为 -55 ℃ ~+ 125 ℃ ; 在 -10~+ 85°C范围内,精度为 ± 0.5°C 。

按照一定的时序进行读写,就可以获得温度数值。

驱动代码内容过长,不在此赘述,可以下载附件中的源码做进一步的研究。

参考链接https://www.stepfpga.com/doc/%E6%B8%A9%E5%BA%A6%E4%BC%A0%E6%84%9F%E5%99%A8%E6%A8%A1%E5%9D%97

 

4.通过数码管显示温度

数码管是工程设计中使用很广的一种显示输出器件。一个7段数码管(如果包括右下的小点可以认为是8段)分别由a、b、c、d、e、f、g位段和表示小数点的dp位段组成。实际是由8个LED灯组成的,控制每个LED的点亮或熄灭实现数字显示。通常数码管分为共阳极数码管和共阴极数码管。

根据原理图可知,板载数码管为共阴数码管,且阴极没有直接接地,而是作为一个可配置的IO,用于数码管的显示控制。

数码管所有的信号都连接到FPGA的管脚,作为输出信号控制。FPGA只要输出这些信号就能够控制数码管的那一段LED亮或者灭。这样我们可以通过开关来控制FPGA的输出。

数码管同8路LED一样,通过3-8译码器进行显示控制。译码需要遵循一定的规则。具体如下。

module Seg_led
(
input 		[3:0]	seg_data,	//seg_data input
input				seg_dot,	//segment dot control
output				seg_sel,	//segment com port
output reg	[7:0]	seg_led		//MSB~LSB = DP,G,F,E,D,C,B,A
);

always@(seg_data)
	case(seg_data)
		4'h0: seg_led = {seg_dot,7'h3f};  //  0
		4'h1: seg_led = {seg_dot,7'h06};  //  1
		4'h2: seg_led = {seg_dot,7'h5b};  //  2
		4'h3: seg_led = {seg_dot,7'h4f};  //  3
		4'h4: seg_led = {seg_dot,7'h66};  //  4
		4'h5: seg_led = {seg_dot,7'h6d};  //  5
		4'h6: seg_led = {seg_dot,7'h7d};  //  6
		4'h7: seg_led = {seg_dot,7'h07};  //  7
		4'h8: seg_led = {seg_dot,7'h7f};  //  8
		4'h9: seg_led = {seg_dot,7'h6f};  //  9
		4'ha: seg_led = {seg_dot,7'h77};  //  A
		4'hb: seg_led = {seg_dot,7'h7C};  //  b
		4'hc: seg_led = {seg_dot,7'h39};  //  C
		4'hd: seg_led = {seg_dot,7'h5e};  //  d
		4'he: seg_led = {seg_dot,7'h79};  //  E
		4'hf: seg_led = {seg_dot,7'h71};  //  F
		default: seg_led = {seg_dot,7'h00};
	endcase

assign seg_sel = 1'b0;	//共阴极,使能

endmodule 

参考链接https://www.stepfpga.com/doc/4._%E6%95%B0%E7%A0%81%E7%AE%A1%E6%98%BE%E7%A4%BA

5.通过ADC测量电位器的电压

这一部分的代码主要参考了Training_Demo for step-mxo2。将ADC采样数据按规则转换为电压数据,将处理后的ADC数据进行BCD转码,最终送到后续的处理步骤。原文的显示通过数码管进行显示,而我最终送到OLED显示屏上进行显示。

module Volt_meter
(
input				clk,		//系统时钟
input				rst_n,		//系统复位,低有效

output				adc_cs,		//SPI总线CS
output				adc_clk,	//SPI总线SCK
input				adc_dat,	//SPI总线SDA

// output  			seg1_sel,	//数码管位选
// output  	[7:0]	seg1_led,	//数码管段选
// output  			seg2_sel,	//数码管位选
// output  	[7:0]	seg2_led,	//数码管段选
output      [3:0]   data1,
output      [3:0]   data2
);

wire adc_done;
wire [7:0] adc_data;
//ADC功能,例化
ADS7868 u2
(
.clk				(clk			),	//系统时钟
.rst_n				(rst_n			),	//系统复位,低有效
.adc_cs				(adc_cs			),	//SPI总线CS
.adc_clk			(adc_clk		),	//SPI总线SCK
.adc_dat			(adc_dat		),	//SPI总线SDA
.adc_done			(adc_done		),	//ADC采样完成标志
.adc_data			(adc_data		)	//ADC采样数据
);

//将ADC采样数据按规则转换为电压数据(乘以0.0129),这里我们直接乘以129,得到的数据经过BCD转码后小数点左移4位即可
wire [15:0]	bin_code = adc_data * 16'd129;
wire [19:0]	bcd_code;

//将处理后的ADC数据进行BCD转码,例化
bin_to_bcd19 u3
(
.rst_n				(rst_n			),	//系统复位,低有效
.bin_code			(bin_code		),	//需要进行BCD转码的二进制数据
.bcd_code			(bcd_code		)	//转码后的BCD码型数据输出
);

//Segment led display module
// Seg_led seg[1:0] 
// (
// .seg_data			(bcd_code[19:12]	),	//seg_data input
// .seg_dot			({1'b1,1'b0}		),	//segment dot control
// .seg_sel			({seg1_sel,seg2_sel}),	//segment com port
// .seg_led			({seg1_led,seg2_led})	//MSB~LSB = DP,G,F,E,D,C,B,A
// ); 

assign {data1, data2} = bcd_code[19:12];

endmodule

 

无网页参考链接,参考资源见附件

6.测量的电压在OLED上显示

OLED的驱动同样参考了Training_Demo for step-mxo2,原代码通过拨动核心板拨码开关控制OLED显示的数据在0~F之间变化。作品代码修改为根据输入的电压值显示对应的电压。

由于代码过长,此处仅展示模块头部。

module OLED12832
(
	input				clk,		//12MHz系统时钟
	input				rst_n,		//系统复位,低有效
 
	input		[3:0]	sw,		//
	input       [3:0]   data1,
	input       [3:0]   data2,
// 	input       [11:0]  thermo, 
	output	reg			oled_csn,	//OLCD液晶屏使能
	output	reg			oled_rst,	//OLCD液晶屏复位
	output	reg			oled_dcn,	//OLCD数据指令控制
	output	reg			oled_clk,	//OLCD时钟信号
	output	reg			oled_dat	//OLCD数据信号
);

参考资料见附件

7.温度与电压通过串口发送到上位机

同样,参考Training_Demo for step-mxo2完成。原代码以16进制发送0-255,作品代码修改为根据输入的电压值和温度传感器的数值,按照一定的通讯协议发送到串口。

/* uart_seg.v */
module uart_seg 
(
input					clk,		//系统时钟 12MHz
input					rst_n,		//系统复位,低有效

input					fpga_rx,	//UART接收输入
output					fpga_tx,	//UART发送输出

	input       [3:0]   data1,
	input       [3:0]   data2,
	input       [7:0]  thermo

// output  				seg1_sel,	//数码管位选
// output  		[7:0]	seg1_led,	//数码管段选
// output  				seg2_sel,	//数码管位选
// output  		[7:0]	seg2_led	//数码管段选
);
	
reg [23:0] cnt;
reg [7:0]punctuation[2:0];

initial
begin
    punctuation[0] = 8'h2F; // /
    punctuation[1] = 8'h2A; // *
    punctuation[2] = 8'h2C; // ,
end

always @(posedge clk or negedge rst_n)
	if(!rst_n) cnt <= 1'b0;
	else if(cnt >= 24'd11_999_999) cnt <= 1'b0;
	else cnt <= cnt + 1'b1;

reg tx_data_valid;
reg [7:0] tx_data_in;
reg [7:0] counter;
always @(posedge clk or negedge rst_n) //每秒发一个数据,串口助手用16进制显示
	if(!rst_n) 
	begin tx_data_valid <= 1'b0; tx_data_in <= 1'b0; counter <= 1'b0; end
	else if(cnt == 24'd0_999_999) 
	begin tx_data_valid <= 1'b1; tx_data_in <= punctuation[0]; end 
	else if(cnt == 24'd1_999_999) 
	begin tx_data_valid <= 1'b1; tx_data_in <= punctuation[1]; end 
	else if(cnt == 24'd2_999_999) 
	begin tx_data_valid <= 1'b1; tx_data_in <= thermo[7:4]+8'd48; end 
	else if(cnt == 24'd3_999_999) 
	begin tx_data_valid <= 1'b1; tx_data_in <= thermo[3:0]+8'd48; end 
	else if(cnt == 24'd4_999_999) 
	begin tx_data_valid <= 1'b1; tx_data_in <= punctuation[2]; end 
	else if(cnt == 24'd5_999_999) 
	begin tx_data_valid <= 1'b1; tx_data_in <= data1+8'd48; end 
	else if(cnt == 24'd6_999_999) 
	begin tx_data_valid <= 1'b1; tx_data_in <= 8'd46; end // .
	else if(cnt == 24'd7_999_999) 
	begin tx_data_valid <= 1'b1; tx_data_in <= data2+8'd48; end 
// 	else if(cnt == 24'd8_999_999) 
// 	begin tx_data_valid <= 1'b1; tx_data_in <= punctuation[2]; end 
// 	else if(cnt == 24'd9_999_999) 
// 	begin tx_data_valid <= 1'b1; tx_data_in <= counter; counter <= counter+1'b1; end 
	else if(cnt == 24'd10_999_999) 
	begin tx_data_valid <= 1'b1; tx_data_in <= punctuation[1]; end 
	else if(cnt == 24'd11_999_999) 
	begin tx_data_valid <= 1'b1; tx_data_in <= punctuation[0]; end 
// 	else if(cnt >= 24'd11_999_999) 
// 	begin tx_data_valid <= 1'b1; tx_data_in <= counter; counter <= counter+1'b1; end 
	else 
	begin tx_data_valid <= 1'b0; tx_data_in <= tx_data_in; end

wire rx_data_valid;
wire [7:0]	rx_data_out;
//Uart_Bus module
Uart_Bus u1
(	
.clk					(clk			),	//系统时钟 12MHz
.rst_n					(rst_n			),	//系统复位,低有效
//负责FPGA接收UART芯片的数据
.uart_rx				(fpga_rx		),	//UART接收输入
.rx_data_valid			(rx_data_valid	),	//接收数据有效脉冲
.rx_data_out			(rx_data_out	),	//接收到的数据
//负责FPGA发送数据给UART芯片
.tx_data_valid			(tx_data_valid	),
.tx_data_in				(tx_data_in		),
.uart_tx				(fpga_tx		)
);

// Seg_led seg[1:0] 
// (
// .seg_data	(rx_data_out),	//seg_data input
// .seg_dot	({1'b0,1'b0}),	//segment dot control
// .seg_sel	({seg1_sel,seg2_sel}),	//segment com port
// .seg_led	({seg1_led,seg2_led})	//MSB~LSB = DP,G,F,E,D,C,B,A
// );
endmodule

参考资料见附件

8.上位机收到数据后,通过GUI显示数据

这也是我觉得比较好玩的一个东西。在搜集串口的相关资料时,无意中找到了苏老师发布的串口数据可视化、处理程序,里面精美的界面深深吸引了我。在经过多方面的比较以后,我选择了跨平台、多功能串行数据可视化程序 - Serial Studio,它简洁的界面,完善的功能,无不散发着开发者智慧的光芒,也为本次作品增光添彩。

json文件如下所示。

{
   "t":"Digital Voltmeter",
   "g":[
      {
         "t":"Mission Status",
         "d":[
            {
               "t":"Temperature",
               "v":"%1",
               "u":"°C",
               "g":true,
               "w":"bar",
               "min":20,
               "max":37
            },
            {
               "t":"Voltage",
               "v":"%2",
               "g":true,
               "u":"V",
               "w":"bar",
               "min":0,
               "max":3.3
            }
         ]
      }
   ]
}

效果如下所示

Fs2R-qD4iiumJalO0G_gUZYHBOUlFlsrV3dvXocZD4RuFc5uJ7KR_I0k

总结

作品顺利完成,实现了要求,代码风格简洁,不同的模块都进行了单独的封装。

但是,这中间的很多代码,如串口的发送与接收,ADC的采集,OLED显示屏的驱动,温度传感器的驱动都是参考小脚丫的例程,如果完全由个人编写的话,可能无法顺利按时完成作品。FPGA的学习还有很长的路要走。

在这中间我也遇到过一个很奇怪的BUG,温度传感器的数值一旦通过OLED显示,则数值会直接变成0,数码管的数值也会变成0。由于时间原因,这个问题没有彻底解决,在后面的时间内,会着手通过调试来查明原因。

致谢

首先感谢硬禾课堂提供的学习机会,能够在完成一个小作品的过程中学习成长。与自学相比,这样的学习方式更加有趣,更有驱动力,提供的例程也可以避免遇到挫折而无法继续。

其次感谢小脚丫FPGA的开发者平台,通过开发者平台的抽象,我可以忽略底层开发软件的细节,避免冗杂的操作步骤。尽管开发平台还有一定的BUG,但我相信这的确是一个正确的方向,也给我的开发带来了很大的助力。

我已将我的项目公开,欢迎克隆。https://www.stepfpga.com/project/397

附件下载
程序.zip
包含自己的单个模块测试程序,最终程序为pro,顶层文件为top文件
参考资料.zip
团队介绍
南京邮电大学是国家工业和信息化部与江苏省人民政府共建的高校。在长期的办学过程中,人才培养质量高,毕业生社会声誉好。
团队成员
大名
电子与光学工程学院微电子学院
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号