基于ICE40UP5K的数字电压表设计
基于2022冬寒假在家练项目,利用所提供的ICE40UP5K FPGA 学习平台,实现了测量旋转电位计电压并显示到OLED屏幕上的功能。
标签
FPGA
数字电压表
2022寒假在家练
smallcracker
更新2022-03-02
哈尔滨工业大学
613

项目描述

本项目实现了基于ICE40UP5K平台的数字电压表设计,实现了测量旋转电位计产生的电压并将测量结果显示在OLED屏幕上的功能。

设计思路

硬件部分,旋转电位计的输出已经连接到比较器的同相输入端。我们所要做的,只有软件部分即(图中黄色部分)

  1. 驱动这个ADC电路,测量电位计产生的点位大小。
  2. 将测量结果映射到0-3.3V。
  3. 将测量结果显示到OLED屏幕

FhxNYLNUKzL_sw3RH4bt-m9WcHvk

硬件介绍

ADC部分主要电路如图所示,其中Ain2端口已经默认连接至滑动变阻器,如果在比较器的另一端口输入一PWM波,经滤波器滤波后同Ain2比较,结果可以读取C_OUT2获得。通过不断增大PWM占空比,最终可以逼近 Ain2的真实值。

FpR0WF61AaghU3NHpakyuCcHdsLC

实现的功能

项目实现的功能为测量旋转电位计产生的电压并将测量结果显示在OLED屏幕上。FuDsGoWurn_tq8RN_I5HBoxFC_j0

主要代码片段

1.ADC测量模块

ADC部分程序由Lattice官网提供的Simple Sigma-Delta ADC例程修改而来,ADC_top模块为ADC部分的顶层调用模块,sigmadelta_adc模块负责获取采样值,这里展示部分核心代码,完整代码可以查看文末工程文件。

module sigmadelta_adc (
	clk,                    
	rstn,                   
	digital_out,            
	analog_cmp,	            
	analog_out,             
	sample_rdy);            

parameter 
ADC_WIDTH = 8,              // ADC Convertor Bit Precision
ACCUM_BITS = 10,            // 2^ACCUM_BITS is decimation rate of accumulator
LPF_DEPTH_BITS = 3;         // 2^LPF_DEPTH_BITS is decimation rate of averager

//input ports
input	clk;                            // sample rate clock
input	rstn;                           // async reset, asserted low
input	analog_cmp ;                    // input from LVDS buffer (comparitor)

//output ports
output	analog_out;                     // feedback to comparitor input RC circuit
output  sample_rdy;                     // digital_out is ready
output [ADC_WIDTH-1:0]	digital_out;    // digital output word of ADC

reg                         delta;          // captured comparitor output
reg [ACCUM_BITS-1:0]	    sigma;          // running accumulator value
reg [ADC_WIDTH-1:0]	        accum;          // latched accumulator value
reg [ACCUM_BITS-1:0]	    counter;        // decimation counter for accumulator
reg							rollover;       // decimation counter terminal count
reg							accum_rdy;      // latched accumulator value 'ready' 


always @ (posedge clk)
begin
    delta <= analog_cmp;        // capture comparitor output
end

assign analog_out = delta;      // feedback to comparitor LPF


always @ (posedge clk or negedge rstn)
begin
	if( ~rstn ) 
    begin
		sigma       <= 0;
		accum       <= 0;
		accum_rdy   <= 0;
    end else begin
        if (rollover) begin
            // latch top ADC_WIDTH bits of sigma accumulator (drop LSBs)
            accum <= sigma[ACCUM_BITS-1:ACCUM_BITS-ADC_WIDTH];
            sigma <= delta;         // reset accumulator, prime with current delta value
        end else begin
            if (&sigma != 1'b1)         // if not saturated
                sigma <= sigma + delta; // accumulate 
        end
        accum_rdy <= rollover;     // latch 'rdy' (to align with accum)
    end
end
box_ave #(
    .ADC_WIDTH(ADC_WIDTH),
    .LPF_DEPTH_BITS(LPF_DEPTH_BITS))
box_ave (
    .clk(clk),
    .rstn(rstn),
    .sample(accum_rdy),
    .raw_data_in(accum),
    .ave_data_out(digital_out),
    .data_out_valid(sample_rdy)
);

always @(posedge clk or negedge rstn)
begin
	if( ~rstn ) begin
		counter <= 0;
		rollover <= 0;
		end
	else begin
		counter <= counter + 1;       // running count
		rollover <= &counter;         // assert 'rollover' when counter is all 1's
		end
end

endmodule

box_ave 模块则实现了对采样结果的滤波,方法是对连续采集到的N个采样值取平均值。实现了低通滤波效果。

module box_ave (
	clk,
	rstn,
	sample,
	raw_data_in,
	ave_data_out,
    data_out_valid);

parameter 
ADC_WIDTH = 8,				// ADC Convertor Bit Precision
LPF_DEPTH_BITS = 4;         // 2^LPF_DEPTH_BITS is decimation rate of averager

//input ports
input	clk;                                // sample rate clock
input	rstn;	                            // async reset, asserted low
input	sample;				                // raw_data_in is good on rising edge, 
input	[ADC_WIDTH-1:0]	raw_data_in;		// raw_data input

//output ports
output [ADC_WIDTH-1:0]	ave_data_out;		// ave data output
output data_out_valid;                      // ave_data_out is valid, single pulse

reg [ADC_WIDTH-1:0]	ave_data_out;		

reg [ADC_WIDTH+LPF_DEPTH_BITS-1:0]      accum;          // accumulator
reg [LPF_DEPTH_BITS-1:0]                count;          // decimation count
reg [ADC_WIDTH-1:0]  					raw_data_d1;    // pipeline register

reg sample_d1, sample_d2;                               // pipeline registers
reg result_valid;                                       // accumulator result 'valid'
wire accumulate;                                        // sample rising edge detected
wire latch_result;                                      // latch accumulator result

always @(posedge clk or negedge rstn)
begin
	if( ~rstn ) begin
		sample_d1 <= 0;	
		sample_d2 <= 0;
        raw_data_d1 <= 0;
		result_valid <= 0;
	end else begin
		sample_d1 <= sample;                // capture 'sample' input
		sample_d2 <= sample_d1;             // delay for edge detection
		raw_data_d1 <= raw_data_in; 	    // pipeline 
		result_valid <= latch_result;		// pipeline for alignment with result
	end
end

assign		accumulate = sample_d1 && !sample_d2;	    // 'sample' rising_edge detect
assign		latch_result = accumulate && (count == 0);	// latch accum. per decimation count

always @(posedge clk or negedge rstn)
begin
	if( ~rstn ) begin
		count <= 0;	  
	end else begin
	    if (accumulate)	count <= count + 1;         // incr. count per each sample
	end
end

always @(posedge clk or negedge rstn)
begin
	if( ~rstn ) begin
		accum <= 0;	
	end else begin
        if (accumulate)
            if(count == 0)                      // reset accumulator
    		    accum <= raw_data_d1;           // prime with first value
            else
                accum <= accum + raw_data_d1;   // accumulate
	end	
end

always @(posedge clk or negedge rstn)
begin
	if( ~rstn ) begin
        ave_data_out <= 0;
    end else if (latch_result) begin            // at end of decimation period...
        ave_data_out <= accum >> LPF_DEPTH_BITS;	  // ... save accumulator/n result
    end
end

assign data_out_valid = result_valid;       // output assignment

endmodule

2. ADC采样得到的采样值为8位二进制数,为了阅读的方便,我将采样值映射到0~3.30。映射采用的方法是依次获取个位,十分位和百分位的数值,个位和百分位的获取是通过直接比较的方法,而十分位的获取是通过累加的方法。

always @ (posedge clk or negedge rst_n) begin
		if(!rst_n) begin
			cnt_main <= 3'd1;
			cnt_tens <= 3'd0;
			cnt_ones <= 3'd0;
			temp_A <= A;
			temp_ones <= 4'd0;
			final_hundreds <= 2'd0;
			temp_tens <= 4'd0; 
			final_ones <= 4'd0;
			final_tens <= 4'd0;
			rdy <= 0;
		end else begin
			case (cnt_main)
				INIT1:begin
					cnt_main <= 3'd1;
					cnt_tens <= 3'd0;
					cnt_ones <= 3'd0;
					temp_A <= A;
					temp_ones <= 4'd0;
					final_hundreds <= 2'd0;
					temp_tens <= 4'd0; 
					final_ones <= 4'd0;
					final_tens <= 4'd0;
					rdy <= 0;
				end 
				HUNDREDS1:begin
					cnt_main <= 3'd02;
					if(temp_A > 8'd231) begin final_hundreds <= 2'd03; temp_A <= temp_A - 8'd231;end
					else if(temp_A > 8'd154) begin final_hundreds <= 2'd02; temp_A <= temp_A - 8'd154;end
					else if(temp_A > 8'd77) begin final_hundreds <= 2'd01; temp_A <= temp_A - 8'd77;end
				end
				TENS1:begin
					case(cnt_tens)
						3'd00: begin if(temp_A > 8'd62) begin temp_A <= temp_A - 8'd62; temp_tens <= temp_tens + 8'd08; end cnt_tens <= cnt_tens + 3'd01; end
						3'd01: begin if(temp_A > 8'd31) begin temp_A <= temp_A - 8'd31; temp_tens <= temp_tens + 8'd04; end cnt_tens <= cnt_tens + 3'd01; end
						3'd02: begin if(temp_A > 8'd15) begin temp_A <= temp_A - 8'd15; temp_tens <= temp_tens + 8'd02; end cnt_tens <= cnt_tens + 3'd01; end
						3'd03: begin if(temp_A > 8'd07) begin temp_A <= temp_A - 8'd07; temp_tens <= temp_tens + 8'd01; end cnt_tens <= cnt_tens + 3'd01; end
						3'd04: begin cnt_main <= 3'd03; final_tens <= temp_tens; cnt_tens <= 3'd0; end 
						default: ;
					endcase
				end
				ONES1:begin
					case(temp_A)
						8'd01: final_ones <= 8'd01;
						8'd02: final_ones <= 8'd03;
						8'd03: final_ones <= 8'd04;
						8'd04: final_ones <= 8'd05;
						8'd05: final_ones <= 8'd06;
						8'd06: final_ones <= 8'd08;
						8'd07: final_ones <= 8'd09;
						default: final_ones <= 8'd00;
					endcase
					rdy <= 1;	
					cnt_main <= 3'd04;
				end
				default: cnt_main <= 3'd00;
			endcase
		end
	end

3.最终,将获得的电压数值显示到OLED屏幕上,为了便于观看,我修改了电子森林所提供的例程,对字体进行了放大,此外修改了部分代码,实现了电压显示部分的不断刷新。这里展示程序的主要循环流程。

		if(!rst_n) begin
			cnt_main <= 1'b0; cnt_init <= 1'b0; cnt_scan <= 1'b0; cnt_write <= 1'b0;cnt_chinese <= 1'b0;
			y_p <= 1'b0; x_ph <= 1'b0; x_pl <= 1'b0;
			num <= 1'b0; char <= "                "; char_reg <= 1'b0;
			num_delay <= 16'd5; cnt_delay <= 1'b0; cnt <= 1'b0;
			oled_csn <= HIGH; oled_rst <= HIGH; oled_dcn <= CMD; oled_clk <= HIGH; oled_dat <= LOW;
			state <= IDLE; state_back <= IDLE;
		end else begin
			case(state)
				IDLE:begin
						cnt_main <= 1'b0; cnt_init <= 1'b0; cnt_scan <= 1'b0; cnt_write <= 1'b0;
						y_p <= 1'b0; x_ph <= 1'b0; x_pl <= 1'b0;
						num <= 1'b0; char <= "                "; char_reg <= 1'b0;
						num_delay <= 16'd5; cnt_delay <= 1'b0; cnt <= 1'b0;mem_hanzi_num<=8'd0;
						oled_csn <= HIGH; oled_rst <= HIGH; oled_dcn <= CMD; oled_clk <= HIGH; oled_dat <= LOW;
						state <= MAIN; state_back <= MAIN;
					end
				MAIN:begin
						if(cnt_main >= 5'd18) cnt_main <= 5'd17;//接下来执行空操作,实现数据只刷新一次
						else cnt_main <= cnt_main + 1'b1;
						case(cnt_main)	//MAIN状态							
							5'd0 :	begin state <= INIT; end
																														
							5'd1 :	begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "                ";state <= SCAN; end
							5'd2 :	begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "                ";state <= SCAN; end												
							5'd3 :	begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "                ";state <= SCAN; end
							5'd4 :	begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "                ";state <= SCAN; end
							5'd5 :	begin y_p <= 8'hb4; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "                ";state <= SCAN; end
							5'd6 :	begin y_p <= 8'hb5; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "                ";state <= SCAN; end
							5'd7 :	begin y_p <= 8'hb6; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "                ";state <= SCAN; end
							5'd8 :	begin y_p <= 8'hb7; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "                ";state <= SCAN; end						
							5'd9 :	begin y_p <= 8'hb0; x_ph <= 8'h12; x_pl <= 8'h00; mem_hanzi_num <= 8'd0; state <= CHINESE; end
							5'd10:	begin y_p <= 8'hb2; x_ph <= 8'h12; x_pl <= 8'h00; mem_hanzi_num <= 8'd2; state <= CHINESE; end
							5'd11:	begin y_p <= 8'hb0; x_ph <= 8'h13; x_pl <= 8'h00; mem_hanzi_num <= 8'd4; state <= CHINESE; end
							5'd12:	begin y_p <= 8'hb2; x_ph <= 8'h13; x_pl <= 8'h00; mem_hanzi_num <= 8'd6; state <= CHINESE; end
								
							5'd13:	begin y_p <= 8'hb0; x_ph <= 8'h14; x_pl <= 8'h00; mem_hanzi_num <= 8'd8; state <= CHINESE; end
							5'd14:	begin y_p <= 8'hb2; x_ph <= 8'h14; x_pl <= 8'h00; mem_hanzi_num <= 8'd10; state <= CHINESE; end
							5'd15:	begin y_p <= 8'hb0; x_ph <= 8'h15; x_pl <= 8'h00; mem_hanzi_num <= 8'd12; state <= CHINESE; end
							5'd16:	begin y_p <= 8'hb2; x_ph <= 8'h15; x_pl <= 8'h00; mem_hanzi_num <= 8'd14; state <= CHINESE; end
							
							5'd17:	begin y_p <= 8'hb6; x_ph <= 8'h12; x_pl <= 8'h08; num <= 5'd6;  char <= str[48:0] ;state <= SCAN; end
							5'd18:	;//begin y_p <= 8'hb4; x_ph <= 8'h12; x_pl <= 8'h08; num <= 5'd6;  char <= "World!";state <= SCAN; end		
							
							default: state <= IDLE;   //如果你需要动态刷新一些信息,此行应该取消注释
						endcase
					end
				INIT:begin	//初始化状态
						case(cnt_init)
							5'd0:	begin oled_rst <= LOW; cnt_init <= cnt_init + 1'b1; end	//复位有效
							5'd1:	begin num_delay <= 16'd25000; state <= DELAY; state_back <= INIT; cnt_init <= cnt_init + 1'b1; end	//延时大于3us
							5'd2:	begin oled_rst <= HIGH; cnt_init <= cnt_init + 1'b1; end	//复位恢复
							5'd3:	begin num_delay <= 16'd25000; state <= DELAY; state_back <= INIT; cnt_init <= cnt_init + 1'b1; end	//延时大于220us
							5'd4:	begin 
										if(cnt>=INIT_DEPTH) begin	//当25条指令及数据发出后,配置完成
											cnt <= 1'b0;
											cnt_init <= cnt_init + 1'b1;
										end else begin	
											cnt <= cnt + 1'b1; num_delay <= 16'd5;
											oled_dcn <= CMD; char_reg <= cmd[cnt]; state <= WRITE; state_back <= INIT;
										end
									end
							5'd5:	begin cnt_init <= 1'b0; state <= MAIN; end	//初始化完成,返回MAIN状态
							default: state <= IDLE;
						endcase
					end
					
					

遇到的主要难题

在实现的过程中,我在放大汉字的过程中遇到了一些困难。因为起初不是很熟悉屏幕地址写入的规则 ,所以显示的汉字总是出现一些形变和杂点。

FqUO3FPJUlYYoWlJFltdKItxcV5_

最终我重新设置汉字的拆分规则,将汉字拆分为左上,右上,左下,右下4个部分,解决了汉字的正常显示问题。

同时,如何将采样得到的八位二进制数,转化为0-3.3V电压值也曾给我困惑。我学习了如何将二进制码转化为十进制码,实现了在屏幕上显示0-255十进制数。但是进一步的转化反而更加困难。因此我采用了一种比较原始但十分有效的方法,即通过直接比较的方式,分别获取电压值的个位,十分位和百分位(具体过程可以浏览上面展示的代码)。

此外,我也遇到了一个至今也不能解释的问题:即其中用到的一个计数器尽管已经有了响应的初始化代码,系统依然不能正常初始化,表现为计数器计数频率明显偏高。只有在手动按下rst键进行初始化之后才能正常运行。对此,我参考了如下内容,设计了一个开机自动触发的信号对程序进行初始化操作,最终解决了这个问题。

未来可以改进的地方

通过波形图展示旋转电位计产生电压的变化

项目资源使用报告

Number of slice registers: 386 out of 5280 (7%)
Number of I/O registers: 5 out of 117 (4%)
Number of LUT4s: 1340 out of 5280 (25%)
Number of logic LUT4s: 1032
Number of inserted feedthru LUT4s: 68
Number of replicated LUT4s: 32
Number of ripple logic: 104 (208 LUT4s)
Number of IO sites used: 12 out of 39 (31%)
Number of IO sites used for general PIO: 12
Number of IO sites used for I3C: 0 out of 2 (0%)
(note: If I3C is not used, its site can be used as general PIO)
Number of IO sites used for PIO+I3C: 12 out of 36 (33%)
Number of IO sites used for OD+RGB IO buffers: 0 out of 3 (0%)
(note: If RGB LED drivers are not used, sites can be used as OD outputs,
see TN1288 iCE40 LED Driver Usage Guide)
Number of IO sites used for PIO+I3C+OD+RGB: 12 out of 39 (31%)
Number of DSPs: 0 out of 8 (0%)
Number of I2Cs: 0 out of 2 (0%)
Number of High Speed OSCs: 0 out of 1 (0%)
Number of Low Speed OSCs: 0 out of 1 (0%)
Number of RGB PWM: 0 out of 1 (0%)
Number of RGB Drivers: 0 out of 1 (0%)
Number of SCL FILTERs: 0 out of 2 (0%)
Number of SRAMs: 0 out of 4 (0%)
Number of WARMBOOTs: 0 out of 1 (0%)
Number of SPIs: 0 out of 2 (0%)
Number of EBRs: 6 out of 30 (20%)
Number of PLLs: 0 out of 1 (0%)
Number of Clocks: 2

引脚分配

(yrgb引脚系学习过程中使用,可以不分配引脚)

Fh0Sy5P8kvQxqIOV4ikkw_SZYNa2

附件下载
完整项目项目.7z
完整radiant项目
源代码.7z
里面只有源代码文件,顶层模块为top
团队介绍
大二学生 许明辉
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号