基于小脚丫FPGA的综合技能训练平台完成的数字电压表,能够在数码管上显示温度,OLED显示屏上显示电压,伴随流水灯的闪烁和RGBLED灯呼吸,数据通过串口发送到上位机,在GUI中显示。
作品完成了3个要求:
-
旋转电位计可以产生0-3.3V的电压
-
利用板上的串行ADC对电压进行转换
-
将电压值在板上的OLED屏幕上显示出来
作品框图
- 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
}
]
}
]
}
效果如下所示
总结
作品顺利完成,实现了要求,代码风格简洁,不同的模块都进行了单独的封装。
但是,这中间的很多代码,如串口的发送与接收,ADC的采集,OLED显示屏的驱动,温度传感器的驱动都是参考小脚丫的例程,如果完全由个人编写的话,可能无法顺利按时完成作品。FPGA的学习还有很长的路要走。
在这中间我也遇到过一个很奇怪的BUG,温度传感器的数值一旦通过OLED显示,则数值会直接变成0,数码管的数值也会变成0。由于时间原因,这个问题没有彻底解决,在后面的时间内,会着手通过调试来查明原因。
致谢
首先感谢硬禾课堂提供的学习机会,能够在完成一个小作品的过程中学习成长。与自学相比,这样的学习方式更加有趣,更有驱动力,提供的例程也可以避免遇到挫折而无法继续。
其次感谢小脚丫FPGA的开发者平台,通过开发者平台的抽象,我可以忽略底层开发软件的细节,避免冗杂的操作步骤。尽管开发平台还有一定的BUG,但我相信这的确是一个正确的方向,也给我的开发带来了很大的助力。
我已将我的项目公开,欢迎克隆。https://www.stepfpga.com/project/397