基于ICE40UP5K FPGA实现的数字电压表
本次实验基于ICE40UP5K FPGA学习平台,结合板卡上的电压比较器编写的数字电压表,使用了Lattice官方提供的SSD ADC代码和硬禾学堂的OLED驱动程序,来完成本次实验
标签
嵌入式系统
FPGA
ICE40UP5K
显示
Lattice
ICE40
2022寒假在家练
amirror
更新2022-03-04
南京农业大学
822

一、项目需求

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

二、实现功能

   通过旋转电位计改变电阻两端分压,进而进行测量显示

1.刚上电之后这里数据没有办法直接刷到显示屏上面,会产生如图所示的图象,只需要按一下reset键(即run按键),就可以正常使用

FqybZqVdIbjPtlHjazFVsdX9YMTCFgsZvNDVfEO0y4xfAxsHxLgdtq9p

2.随着调节滑动变阻器,显示屏上的电压值产生变化

FqHklbYN3zANn68-Yvuzvd-T-UKIFpX7Ue6_rvVRpgeEjp3VSzK8Kzdk

三、实现思路

1.利用板卡上自带的电压比较器模块,结合FPGA内部资源编写一个Delta-sigma ADC

2.电压比较器一段连接输入电压,一端连接FPGA产生的PWM信号,当PWM信号十分接近待测电压时,此时的PWM电压值即为我们的测量值

3.将测量值写入一个存储器中,让OLED不断读取寄存器中的值来显示电压值

Fu1E-lOjb08gcAD8HMq-VYm8LXCv

四、实现过程

4.1 Delta-sigma ADC

这里ADC的核心代码虽然不是我自己编写的,我还是事先大致了解了一下Delta-sigma ADC的工作原理,通过不断地作差求和来输出比较结果,再通过低通滤波器筛选出所需信号,但是这里附加滤波器会产生时序延迟,还需要再进行额外的工作来消除延迟。

FmBA-c0c3tqZydq8xzzFz-a9mQF9

通过查看官方给出的使用手册,我们知道analog_cmp是待测电压输入端口,analog_out是PWM信号的输出端口,digital_out即为我们测量电压的二进制值,sample_rdy是一个使能信号,这里没有用到暂时不管。

FjRz4ooubhk08w3rpXXWnN0PTu5o

例化ADC模块代码如下

ADC_top ADC_inst(    
		    .i_clk_in(I_sys_clk), //comment out to use internal clock 62.5MHz
		    .i_rst_in(i_rst_n),
		    .i_analog_cmp(C_OUT2),
		    .o_digital_out(digital_out),
		    .o_analog_out(PWM_2),
		    .o_sample_rdy()
		    
		    );

注:这里由于ADC对时钟频率要求较高,故采用FPGA系统内部时钟

例化方法如下(在官方给出的ADC顶层文件中有例化代码)

wire I_sys_clk;
HSOSC 
#( 
  .CLKHF_DIV ("0b10") 
) u_HSOSC ( 
  .CLKHFEN (1'b1), 
  .CLKHFPU (1'b1), 
  .CLKHF   (I_sys_clk) 
);

4.2 OLED驱动程序

这里直接使用硬禾学堂提供的OLED驱动代码,将其中写死在oled上的内容改为从存储器中读取。

oled代码中将状态机,SCAN状态代码改写

SCAN:begin	//刷屏状态,从RAM中读取数据
						if(cnt_scan == 5'd11) begin
							if(num) cnt_scan <= 5'd3;
							else cnt_scan <= cnt_scan + 1'b1;
						end else if(cnt_scan == 5'd12) cnt_scan <= 1'b0;
						else cnt_scan <= cnt_scan + 1'b1;
						case(cnt_scan)
							5'd 0:	begin oled_dcn <= CMD; char_reg <= y_p; state <= WRITE; state_back <= SCAN; end		//定位列页地址
							5'd 1:	begin oled_dcn <= CMD; char_reg <= x_pl; state <= WRITE; state_back <= SCAN; end	//定位行地址低位
							5'd 2:	begin oled_dcn <= CMD; char_reg <= x_ph; state <= WRITE; state_back <= SCAN; end	//定位行地址高位
 
							5'd 3:	begin num <= num - 1'b1;end
							5'd 4:	begin oled_dcn <= DATA; char_reg <= 8'h00; state <= WRITE; state_back <= SCAN; end	
							5'd 5:	begin oled_dcn <= DATA; char_reg <= 8'h00; state <= WRITE; state_back <= SCAN; end	
							5'd 6:	begin oled_dcn <= DATA; char_reg <= 8'h00; state <= WRITE; state_back <= SCAN; end	
							5'd 7:	begin oled_dcn <= DATA; char_reg <= mem[char[(num*8)+:8]][39:32]; state <= WRITE; state_back <= SCAN; end
							5'd 8:	begin oled_dcn <= DATA; char_reg <= mem[char[(num*8)+:8]][31:24]; state <= WRITE; state_back <= SCAN; end
							5'd 9:	begin oled_dcn <= DATA; char_reg <= mem[char[(num*8)+:8]][23:16]; state <= WRITE; state_back <= SCAN; end
							5'd10:	begin oled_dcn <= DATA; char_reg <= mem[char[(num*8)+:8]][15: 8]; state <= WRITE; state_back <= SCAN; end
							5'd11:	begin oled_dcn <= DATA; char_reg <= mem[char[(num*8)+:8]][ 7: 0]; state <= WRITE; state_back <= SCAN; end
							5'd12:	begin state <= MAIN; end
							default: state <= IDLE;
						endcase
					end

在顶层文件中例化OLED模块

OLED12832 inst0_1(
	.clk(I_sys_clk),		
	.rst_n(i_rst_n),		

	.ram_clk_en(RdClockEn),
	.ram_addr(RdAddress),
	.ram_data(ram_data),


	.oled_rst(OLED_RSTn),	
	.oled_dcn(OLED_DorC),	
	.oled_clk(OLED_SCK),	
	.oled_dat(OLED_MOSI)	
);

4.3 RAM IP核的调用

在IP核中找到Memory相关的IP,使用向导对其进行调用

Fmd9T4mIPDx-yDaw9A0pnYZoOKSN

在生成的IP核的文件夹中有例化的模板文件,将其中的代码复制到顶层文件中,完成例化

GRAM_MY inst0_0(.wr_clk_i(I_sys_clk ),
        .rd_clk_i(I_sys_clk ),
        .rst_i(!i_rst_n),
        .wr_clk_en_i(1'b1 ),
        .rd_en_i(1'b1 ),
        .rd_clk_en_i(1'b1 ),
        .wr_en_i(1'b1 ),
        .wr_data_i(Data ),
        .wr_addr_i(WrAddress),
        .rd_addr_i(RdAddress),
        .rd_data_o(ram_data ));

并且将相应的端口连接到oled驱动程序上

4.4 BCD码的转换

由于ADC输出的数据为对应二进制编码,所以需要将其转化成对应十进制数,这里直接采用野火教程中的BCD码转换代码。并且先将原二进制数用移位操作扩大27倍(原因是Verilog程序在计算的过程中会抹去小数位,扩大以提高精度),后转化成十进制数的万位千位百位分别作为测量值的个位和小数的前两位。

注:这里只保留了两位小数是由于采样位宽为8位(官方代码就为8位宽),最小精确位为小数点后两位,对应分辨率3.3/256 = 0.0129

// 将adc转出的数据转化为bcd码
wire [15:0]				vol_bin = digital_out << 7; 
wire [(4*8-1):0]		vol_dec; 
//wire [3:0]				dis_ten;
wire [3:0]				dis_hun;
wire [3:0]				dis_tho;
wire [3:0]				dis_t_tho;

//assign vol_dec = {4'd0,dis_t_tho,".",4'd0,dis_tho,4'd0,dis_hun,4'd0,dis_ten};
assign vol_dec = {4'd0,dis_t_tho,".",4'd0,dis_tho,4'd0,dis_hun};

bcd_8421 u_bcd_8421(
	.sys_clk(I_sys_clk), 
	.sys_rst_n(i_rst_n), 
    .data(vol_bin), 

    .unit(),            //个位
    .ten(),      //十位
    .hun(dis_hun),      //百位
    .tho(dis_tho),      //千位
    .t_tho(dis_t_tho),  //万位
    .h_hun() 			//十万位
	);

五、资源使用情况

FpsFoFSXSffXcyEXBkxv6IgKSauG

六、总结及未来计划与建议

6.1 一些废话

这个项目的实现过程可谓一波三折,最开始入手这个板卡是因为当初想学习risc-v,然后对于一个完全没有接触过FPGA的小白来说,似乎确实有点不太现实,于是就从零开始一步一步跟着野火的教程学习FPGA开发,中间接触到了许多东西,状态机,IP核的使用,动态显示模块的编写。收获良多。当我的具备一定水平的时候我决定从最简单的项目——电压表入手,开始制作自己的小项目。

在电压表项目中,最开始我想到的方法是使用SAR ADC来实现电压的测量,因为上个学期刚刚学过的《电子测量》中,有相关的介绍。

Fq-uOtl2AZQeVtIX7WGeJ7tQrtmj

后来在查阅资料的时候,发现一种新型的ADC—— Δ-Σ ADC具有更好的性能,更高的分辨率,而被广泛使用,也找到了Lattice官方所提供的相关代码,并且由于水平受限,也没自己从零编写,而是直接调用了。

当然项目中间也遇到许许多多的小问题,小bug,在这里就不一一细说了。

6.2 未来计划

1、继续深入学习FPGA开发的相关知识,具备更高的编程水平

2、努力学好相关的专业知识,例如数字信号处理,数字图像处理等,FPGA是一个很好的工具,但学会怎么实现也很重要。

3、研究risc-v的开发与移植,我发现RISC v是一个十分有意思的架构,并由于其开源的特性,具有良好的学习环境,希望今后能从CPU架构,基于RISC v的操作系统入手进行更深入的学习。

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