基于小脚丫FPGA的数字电压表
一、硬件连接示意图
其中,电位计采集到的电压信号由ADS7868进行AD转换到FPGA里面,经过内部数字逻辑转换,将电压值在oled屏幕上显示,同时经串口发送给电脑上位机显示。
二、顶层模块
其中,顶层三个例化模块负责与硬件中的三个主要模块负责对接。代码如下:
module top(
input clk,
input rst,
input adc_sda, //adc7868 SPI总线SDA
output adc_cs, //adc7868 SPI总线CS
output adc_clk, //adc7868 SPI总线SCK
input uart_rxd_pin, //串口接收引脚
output uart_txd_pin, //串口发送引脚
output reg [13:0]led, //led灯全部熄灭
output oled_csn, //OLED使能
output oled_rst, //OLED复位
output oled_dcn, //OLED数据指令控制
output oled_clk, //olEd时钟
output oled_dat //OLED数据
);
//用于 电压采集 例化模块的连接
wire adc_cs0,adc_clk0;
wire [19:0]bcd_code;
assign adc_cs = adc_cs0 ;
assign adc_clk = adc_clk0;
//例化 电压采集模块
voltage_acquisition u1(
.clk(clk),
.rst(rst),
.adc_dat(adc_sda),
.adc_clk(adc_clk0),
.adc_cs(adc_cs0),
.bcd_code(bcd_code)
);
wire oled_csn0; //OLED使能
wire oled_rst0; //OLED复位
wire oled_dcn0; //OLED数据指令控制
wire oled_clk0; //olEd时钟
wire oled_dat0; //OLED数据
assign oled_csn = oled_csn0;
assign oled_rst = oled_rst0;
assign oled_dcn = oled_dcn0;
assign oled_clk = oled_clk0;
assign oled_dat = oled_dat0;
//检测电压值是否发生变化
reg [7:0]voltage,voltage_pre; //当前状态/前一个状态
always @(posedge clk or negedge rst)
if(!rst) begin
led <= 14'b111_111_1111_1111; //led灯全部熄灭
voltage <= 8'd0;
voltage_pre <= 8'd0;
end
else begin
voltage <= {bcd_code[19:12]};
voltage_pre <= voltage;
end
reg v_change_flag; //电压值变化标志位
always @(posedge clk or negedge rst) begin
if(!rst)
v_change_flag <= 1'b0;
else if(voltage == voltage_pre)
v_change_flag <= 1'b0;
else
v_change_flag <= 1'b1;
end
//电压值改变 触发 串口发送标志位
reg [23:0]time_cnt;
always @(posedge clk or negedge rst) begin
if(!rst) begin
txd_ready_flag <= 1'b0;
time_cnt <= 24'd0;
end
else if(v_change_flag == 1'b1) begin
txd_ready_flag <= 1'b1;
time_cnt <= 24'd0;
end
else if(time_cnt == 24'd6_000_000) begin
time_cnt <= 24'd0;
txd_ready_flag <= 1'b1;
end
else begin
time_cnt <= time_cnt + 1'b1;
txd_ready_flag <= 1'b0;
end
end
wire [7:0]v_data;
assign v_data = voltage;
//例化 oled显示模块
oled12832 u2(
.clk(clk), //12MHz系统时钟
.rst_n(rst), //系统复位,低有效
.voltage(v_data),
.oled_csn(oled_csn0), //OLED液晶屏使能
.oled_rst(oled_rst0), //OLED液晶屏复位
.oled_dcn(oled_dcn0), //OLED数据指令控制
.oled_clk(oled_clk0), //OLED时钟信号
.oled_dat(oled_dat0) //OLED数据信号
);
//例化 电压采集模块
reg txd_ready_flag;
wire [7:0]txd_data;
assign txd_data = voltage;
wire uart_txd_pin0;
assign uart_txd_pin = uart_txd_pin0;
wire rxd_finish_flag;
wire [7:0]rxd_data;
uart_validdata u4(
.clk(clk),
.rst(rst),
.uart_rxd_pin(uart_rxd_pin),
.txd_ready_flag(txd_ready_flag),
.txd_data(txd_data), //高四位是整数的bcd码值,低四位是小数的bcd码值
.uart_txd_pin(uart_txd_pin0),
.rxd_finish_flag(rxd_finish_flag),
.rxd_data(rxd_data)
);
endmodule
三、电压采集模块
电压采集模块主要由 ADC7868_driver 和 bin_to_bcd 两个部分组成,功能分别是:
ADC7868_driver:通过spi协议读取当前的数字信号(8'b0000_0000 ~ 8'b1111_1111)。
bin_to_bcd:将当前计算出来的二进制存储的电压信号转化为bcd码存储的电压信号。
代码如下(主要参考简易电压表设计,其中ADC081S101与ADC7868都是3线的spi通信接口,驱动逻辑大致都相同,可以简单修改后使用):
module voltage_acquisition(
input clk,
input rst,
input adc_dat, //adc7868 SPI总线SDA
output adc_clk, //adc7868 SPI总线SCK
output adc_cs, //adc7868 SPI总线CS
output [19:0]bcd_code
);
wire adc_done;
wire [7:0]adc_data;
wire cs,sclk;
assign adc_cs = cs;
assign adc_clk = sclk;
ADC7868_driver u1(
.clk(clk),
.rst_n(rst),
.adc_cs(cs),
.adc_clk(sclk),
.adc_dat(adc_dat),
.adc_done(adc_done),
.adc_data(adc_data)
);
wire [15:0]bin_code;
assign bin_code = adc_data * 16'd130;
bin_to_bcd u2(
.rst_n(rst),
.bin_code(bin_code),
.bcd_code(bcd_code)
);
endmodule
(一)ADC7868_driver模块
功能:将电位计的电压信号(模拟量)转为数字信号。
代码如下(参考简易电压表设计其中ADC081S101与ADC7868都是3线的spi通信接口,驱动逻辑大致都相同,可以简单修改后使用):
module ADC7868_driver(clk,rst_n,adc_cs,adc_clk,adc_dat,adc_done,adc_data);
input clk; //系统时钟
input rst_n; //系统复位,低有效
output reg adc_cs; //SPI总线CS
output reg adc_clk; //SPI总线SCK
input adc_dat; //SPI总线SDA
output reg adc_done; //ADC采样完成标志
output reg [7:0] adc_data; //ADC采样数据
parameter
HIGH = 1'b1,
LOW = 1'b0;
reg [7:0] cnt; //计数器
always @(posedge clk or negedge rst_n) begin
if(!rst_n) cnt <= 1'b0;
else if(cnt >= 8'd34) cnt <= 1'b0;
else cnt <= cnt + 1'b1;
end
reg [7:0] data;
always @(posedge clk or negedge rst_n)
if(!rst_n) begin
adc_cs <= HIGH; adc_clk <= HIGH;
end else case(cnt)
8'd0 : begin adc_cs <= HIGH; adc_clk <= HIGH; end
8'd1 : begin adc_cs <= LOW; adc_clk <= HIGH; end
8'd2,8'd4,8'd6,8'd8,8'd10,8'd12,8'd14,8'd16,
8'd18,8'd20,8'd22,8'd24,8'd26,8'd28,8'd30,8'd32:
begin adc_cs <= LOW; adc_clk <= LOW; end
8'd3 : begin adc_cs <= LOW; adc_clk <= HIGH; end //0
8'd5 : begin adc_cs <= LOW; adc_clk <= HIGH; end //1
8'd7 : begin adc_cs <= LOW; adc_clk <= HIGH; end //2
8'd9 : begin adc_cs <= LOW; adc_clk <= HIGH; data[7] <= adc_dat; end //3
8'd11 : begin adc_cs <= LOW; adc_clk <= HIGH; data[6] <= adc_dat; end //4
8'd13 : begin adc_cs <= LOW; adc_clk <= HIGH; data[5] <= adc_dat; end //5
8'd15 : begin adc_cs <= LOW; adc_clk <= HIGH; data[4] <= adc_dat; end //6
8'd17 : begin adc_cs <= LOW; adc_clk <= HIGH; data[3] <= adc_dat; end //7
8'd19 : begin adc_cs <= LOW; adc_clk <= HIGH; data[2] <= adc_dat; end //8
8'd21 : begin adc_cs <= LOW; adc_clk <= HIGH; data[1] <= adc_dat; end //9
8'd23 : begin adc_cs <= LOW; adc_clk <= HIGH; data[0] <= adc_dat; end //10
8'd25 : begin adc_cs <= LOW; adc_clk <= HIGH; adc_data <= data; end //11
8'd27 : begin adc_cs <= LOW; adc_clk <= HIGH; adc_done <= HIGH; end //12
8'd29 : begin adc_cs <= LOW; adc_clk <= HIGH; adc_done <= LOW; end //13
8'd31 : begin adc_cs <= LOW; adc_clk <= HIGH; end //14
8'd33 : begin adc_cs <= LOW; adc_clk <= HIGH; end //15
8'd34 : begin adc_cs <= HIGH; adc_clk <= HIGH; end
default : begin adc_cs <= HIGH; adc_clk <= HIGH; end
endcase
endmodule
(二)bin_to_bcd模块
功能:将电压值(二进制)转换为bcd码形式(可以理解为C编程中的除法和取余),便于对单个数字做处理和驱动显示等。
代码如下:
module bin_to_bcd(rst_n,bin_code,bcd_code);
input rst_n;
input [15:0]bin_code;
output reg [19:0]bcd_code;
reg [35:0] shift_reg; //
always @(bin_code or rst_n)begin
shift_reg = {20'h0,bin_code}; //20 + 16 = 36 位
if(!rst_n)
bcd_code = 0;
else begin
repeat(16) begin //循环16次
//BCD码各位数据作满5加3操作,
if (shift_reg[19:16] >= 5) shift_reg[19:16] = shift_reg[19:16] + 2'b11;
if (shift_reg[23:20] >= 5) shift_reg[23:20] = shift_reg[23:20] + 2'b11;
if (shift_reg[27:24] >= 5) shift_reg[27:24] = shift_reg[27:24] + 2'b11;
if (shift_reg[31:28] >= 5) shift_reg[31:28] = shift_reg[31:28] + 2'b11;
if (shift_reg[35:32] >= 5) shift_reg[35:32] = shift_reg[35:32] + 2'b11; //最高位
shift_reg = shift_reg << 1;
end
bcd_code = shift_reg[35:16];
end
end
endmodule
四、oled显示模块
这里直接使用的是开源出来的代码,就不展示了。参考oled显示。
五、串口收发协议模块
(一)串口收发基本模块
主要参考串口监视系统设计
下图为串口收发1byte数据底层RTL视图
串口收发的基本模块如上图,主要由四个部分组成
u0:baud-串口发送的节拍模块(匹配波特率),串口发送每一位的数据都由此模块产生的脉冲信号 来决定。
u1:baud-串口接收的节拍模块(匹配波特率),串口接收每一位的数据都由此模块产生的脉冲信号 来采样。
u2:uart_txd-串口发送模块,将需要发送的tx_data_in[7:0](加起始位和停止位)按照band的节拍频率 来依次循环地从uart_txd_pin引脚上发送数据。
u3:uart_txd-串口接收模块,将按照baud的节拍频率采集到的数据(去除起始位和停止位)依次地存储,最后接收时存储到到rx_data_out[7:0]。
具体源代码可以参考串口监视系统设计
(二)串口收发协议(自定义)
1.自定义协议介绍
(1)串口收发的数据分析
<1>本项目串口收发只有两个终端设备,不需要分配和识别地址。
<2>收发的数据为电压值,整数位和小数位,分别组成一个byte发送。
<3>发送的byte遵循ASCII码值(8位有效数据)。
(2)自定义串口上层协议
<1>第一个byte:字符'z' (对应8位ASCII码值为 8’d122 )
<2>第二个byte:电压值整数位的ASCII码值 (8‘d48≤ <8’d58)
<3>第三个byte:字符'x' (对应8位ASCII码值为 8’d120 )
<4>第四个byte:电压值小数位的ASCII码值 (8‘d48≤ <8’d58)
<5>没有结束符
2.自定义协议模块
本人觉得我写的这个代码可移植性不强(但是代码可用且稳定),后续会逐渐补充完善。代码展示就不贴了,有需要的可以去附件区自行下载。
(三)labview编写的上位机
我曾经在其他项目中接触并且实际编写了用485(基于串口)传输的测试控制平台,所以对于Labview这个软件做上位机使用还比较熟悉。
以下是我编写的上位机的前面板和后面板截图:
六、实物展示
七、遇到的问题
1.oled的显示问题,我用了一种很笨的方式来显示16X16字符的汉字,听说可以采用ROM存储的方式显示,但没有什么参考代码,只能下回在试试了。
2.ADC7868的驱动问题,我试了一下用例程ADC081S101的代码直接套用,可以直接套用(但是有人说这样有问题,刷新时候有问题),后面我按照数据手册写了一下,感觉没有什么变化。
3.我自己加了一个上位机显示的功能,同步上存在问题,比如说较快的转动电位计,在0ms的时刻检测到电位计转动到2.5V,串口发送2.5V这个数据,在1ms的时候才能发送结束,但是在0.5ms的时候电位计又发生了改变为2.6V,所以就会把2.6V这个数据丢失,造成不同步的问题。问题正在解决。