2021暑期一起练项目1——基于小脚丫FPGA的数字电压表
今年暑假我很高兴有机会参加硬禾学堂和电子森林举办的暑假一起练活动,制作了我的第一台”数字电压表“。通过这个项目,我学会了很多硬件开发思路,熟悉了硬件开发流程,也对verilog这种硬件描述语言有了更好的认识。
标签
FPGA
数字逻辑
显示
SPI
SSD1306
数字电压表
会伏丘
更新2022-02-28
北京工业大学
1413

一、项目介绍

   此项目是基于Lattice XO2-4000HC FPGA完成的一个数字电压表,具体功能如下:

   1、旋转电位计可以产生0-3.3V的电压

   2、利用板上的串行ADC对电压进行转换

   3、将电压值在板上的OLED屏幕上显示出来

   4、将电压值在板上的八段数码管上显示出来

   5、通过LED灯点亮数量来反映电压大小

   前三项为项目要求内容,后两项为自主发挥部分。

 

二、元件分析与系统级设计

      此次采用了STEP-MXO2小脚丫的FPGA模块与综合技能训练板,其中板载了许多的资源。其中OLED与ADC就是本项目所需要的功能模块。

FlHr5m8nSYxv5I0Pf8EoIhlOAIXp                                             

                                             图1:小脚丫FPGA功能图

      此外,核心板自身带有两个八段数码管和8个LED灯,符合拓展部分的需要。      

      按功能划分模块有顶层模块,ADC采集模块,OLED显示模块,数码管显示模块,LED显示模块。

Fle_8fTFaUGJBhUehQL_3x5OhnlP

                                                   图2:系统级设计

三、ADC模块&数据处理

      ADC采用的ADS7868元件,8位 280KSPS 串行 ADC,采用SPI通讯协议。

      对于SPI采用mode3 , CPHA=1 , CPOL=1  

FsvB4HD_qS06mzMfbXVd1zsj_q1S                                                   

                                                图3:SPI模式3时序

Fiflcg8O0xU_7BMTwDVmAGr5gLRD

                                                  图4:ADS7868工作时序

      由SPI时序和ADC工作时序可知,SCLK空闲时刻为高,先使能ADC,后SCLK开始变化,前三个SCLK上升沿时信息为无用信息,从第四个上升沿开始(包含)连续八个时钟接收八位数据,高位在前低位在后。从第十二个上升沿开始为无用信息,直到第十六个SCLK时钟,完成一个工作周期,SCLK置高后CS置高。

      按设计,需要手动通过状态机切换来配置ADC时钟,会造成实际ADC时钟频率只有模块输入时钟1/2。本身的12M系统时钟得到的6M ADC时钟无法满足ADC工作要求。所以需要通过PLL进行倍频得到24M的模块输入时钟。

      采集完成后得到0x00-0xFF的数字量结果,对应输入模拟电压值为0-3.3。要使用介于0-3.3V的数据前需要先对0-255的结果进行放缩处理。因为FPGA与verilog本身对小数的计算能力较弱且需要占用大量资源,所以选择将0-255放大至0-33000(乘129)。且因为分辨率有限,使用时仅取00000-33000的前三位且手动标注小数点即可。

      此外,0-33000是以四位十六进制存储的,无法按十进制取前几位,且数码管和OLED需要按单个字符进行显示。所以需要将四位16进制数转化为五位BCD码。这里通过移位和进位进行完成,避免了使用除法。

 

/**********************************************************
模块名:ADC_spi_DRIVER
更新日期:2021.8.30
功能:
	驱动基于SPI的8位串行模数转换器 ADS7868
	将捕获数字值量转换回电压值(HEX)  0~255 -> 0-33000
	将十六进制电压转换结果转换为BCD形式并输出

*部分借鉴于电子森林中的开源教程*
**********************************************************/
module ADC_spi_DRIVER
(
input				    clk_24M,		//需要一个更高频时钟  12M->24M
input				    sys_rst_n,  		//系统复位      低有效

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

output	reg [7:0]		adc_data,		//ADC采样数据 0~255
output  reg	[15:0]     	vol_result,		//四位十六进制温度结果
output 	reg	[19:0]		vol_bcd			//五位BCD码 温度结果
);


parameter HIGH = 1'b1;				
parameter LOW = 1'b0;

reg adc_done;

reg [7:0] cnt; //计数器
//状态机计数
always @(posedge clk_24M or negedge sys_rst_n) begin
	if(!sys_rst_n) 
		cnt <= 1'b0;
	else if(cnt >= 8'd34) 
		cnt <= 1'b0;
	else 
		cnt <= cnt + 1'b1;
end


reg [7:0] data;							//临时存放捕获的数据
//根据spi协议传输数据 采用模式三 
//即空闲高电平 第二边沿开始采样(上升沿采样)
always @(posedge clk_24M or negedge sys_rst_n) begin
	if(!sys_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						//先使能ADC 后开始时钟变化
		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						//采样等工作在上升沿 所以下降沿只需给时钟即可

		//进入16个时钟的ADC周期
		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位结果均获取完毕
		8'd25 : begin adc_cs <= LOW;  adc_clk <= HIGH; adc_data <= data; end //11		统一传输给adc_data变量 可以保证adc_data是一组完整的数据
		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
end


//转换为电压结果		0~255 -> 0~33000
always @(negedge adc_done) begin
    vol_result <= adc_data * 16'd129; 
end 


reg		[35:0]		shift_reg;					//计算用的临时变量
//将四位十六进制电压结果转换为五位BCD码
always@(vol_result or sys_rst_n)begin				//当vol_result更新时
	shift_reg = {20'h0,vol_result};
	if(!sys_rst_n) 
		vol_bcd = 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
		vol_bcd = shift_reg[35:16];				//取出最终有用的部分 赋值给输出的reg
	end  
end

endmodule

 

四、OLED显示模块

      OLED采用SSD1306协议,采用显存显示的方式。简单的理解方式就是屏幕上128x32个像素点对应显存中128x32个单元位。按一定顺序将点阵信息更新到显存的对应单元中,oled便会按显存的信息来更新屏幕。而复杂的地方在于对于页、段、列,和换行方式等,需要将显示数据和设置命令“配套“使用,且这个状态机编写起来逻辑极为复杂,所以这部分我也借鉴了较多范例上的代码。

Fro4_DqZ-EBhHf4x8w3-7W5kgizO                                               

                                             图5:SSD1306显存方式

 

/**********************************************************
模块名:OLED_spi_DRIVER
更新日期:2021.9.3
功能:
	驱动基于SSD1306&SPI的128x32 OLED
	使用8*8点阵字库,每行显示128/8=16个字符
	将电压转换结果通过OLED显示出来

*部分借鉴于电子森林中的开源教程*
**********************************************************/
module OLED_spi_DRIVER
(
	input				sys_clk,		//系统时钟
	input				sys_rst_n,		//系统复位 低有效
 
	input 	[11:0]		oled_vol_result,//要显示的电压值 仅保留前三位 

	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传输信号
);
 

localparam INIT_DEPTH = 16'd25; //LCD初始化的命令的数量
localparam IDLE = 6'h1, MAIN = 6'h2, INIT = 6'h4, SCAN = 6'h8, WRITE = 6'h10, DELAY = 6'h20;
localparam HIGH	= 1'b1, LOW = 1'b0;                     //cs线的使能
localparam DATA	= 1'b1, CMD = 1'b0;                     //ds线使能
 

reg [7:0] cmd [24:0];									//命令集
reg [39:0] mem [122:0];									//字库


reg	[7:0]	y_p, x_ph, x_pl;							//OLED RAM 页 行高 行低
reg	[(8*21-1):0] char;									//
reg	[7:0]	num, char_reg;				
reg	[4:0]	cnt_main, cnt_init, cnt_scan, cnt_write;
reg	[15:0]	num_delay, cnt_delay, cnt;					
reg	[5:0] 	state, state_back;							//用于操作间跳转和回跳


//组成一个用于显示的变量  8位二进制代表一个字符
//效果为 X.XXV  (X代表任意数字)
wire [39:0] showhex;										

assign showhex[39:36]   = 1'b0;								//电压个位
assign showhex[35:32]   = oled_vol_result[11:8];			//除了对应的ASCII码 前十六位是对应显示的 即0x00-0x0F也能对应显示出0-F

assign showhex[31:24]   = 8'd46;                            //小数点 .  46

assign showhex[23:20]   = 1'b0;								//电压小数点后一位
assign showhex[19:16] 	= oled_vol_result[7:4];        		//

assign showhex[15:12]   = 1'b0;								//电压小数点后两位
assign showhex[11:8] 	= oled_vol_result[3:0];				

assign showhex[7:0]     = 8'd86;                            //字母 V  86


//OLED显示
always@(posedge sys_clk or negedge sys_rst_n) begin
	if(!sys_rst_n) 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 <= 1'b0; 
		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 <= 1'b0; 
				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 <= MAIN; 
				state_back <= MAIN;
			end

			MAIN:begin
				if(cnt_main >= 5'd5) 
					cnt_main <= 5'd5;
				else 
					cnt_main <= cnt_main + 1'b1;
					
				case(cnt_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 <= "voltage:        ";	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 <= " Designed by WYD";	state <= SCAN; end
					//保留状态机23 因为之前可能会有别的程序的残留显示 需要刷屏清空一下
					5'd5:	begin y_p <= 8'hb0; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd5; char <= showhex ; state <= SCAN; end	//D

					default: state <= IDLE;
				endcase
			end
			
			INIT:begin	//初始化状态									6
				//会先rst_oled置低 然后依次传送25条配置指令 返回MAIN
				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

			SCAN:begin	//刷屏状态,从RAM中读取数据刷屏	显示字符
				if(cnt_scan == 5'd11) begin				//num为这句话一共多少个字符
					if(num) 							
						cnt_scan <= 5'd3;				//而对一句话而言只需要定位一遍
					else 								//所以显示完一个字符后跳过定位接下一个字符
						cnt_scan <= cnt_scan + 1'b1;	//当所有字符显示完了(num=0) 会跳到cnt12 
				end 			
														
				else if(cnt_scan == 5'd12) 				//cnt清零且回到MAIN
					cnt_scan <= 1'b0;

				else 
					cnt_scan <= cnt_scan + 1'b1;		
					
				case(cnt_scan)
					5'd0:	begin oled_dcn <= CMD; char_reg <= y_p; state <= WRITE; state_back <= SCAN; end		//定位列页地址
					5'd1:	begin oled_dcn <= CMD; char_reg <= x_pl; state <= WRITE; state_back <= SCAN; end	//定位行地址低位
					5'd2:	begin oled_dcn <= CMD; char_reg <= x_ph; state <= WRITE; state_back <= SCAN; end	//定位行地址高位

					5'd3:	begin num <= num - 1'b1;end	
					5'd4:	begin oled_dcn <= DATA; char_reg <= 8'h00; state <= WRITE; state_back <= SCAN; end	//将5*8点阵编程8*8
					5'd5:	begin oled_dcn <= DATA; char_reg <= 8'h00; state <= WRITE; state_back <= SCAN; end	//将5*8点阵编程8*8
					5'd6:	begin oled_dcn <= DATA; char_reg <= 8'h00; state <= WRITE; state_back <= SCAN; end	//将5*8点阵编程8*8
					5'd7:	begin oled_dcn <= DATA; char_reg <= mem[char[(num*8)+:8]][39:32]; state <= WRITE; state_back <= SCAN; end
					5'd8:	begin oled_dcn <= DATA; char_reg <= mem[char[(num*8)+:8]][31:24]; state <= WRITE; state_back <= SCAN; end
					5'd9:	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
					//变量[起始地址+:数据位宽] 等价于 变量[(起始地址+数据位宽-1):起始地址]
					5'd12:	begin state <= MAIN; end
					default: state <= IDLE;
				endcase
			end

			WRITE:begin	//WRITE状态,将数据按照SPI时序发送给屏幕
				if(cnt_write >= 5'd17) 
					cnt_write <= 1'b0;
				else 
					cnt_write <= cnt_write + 1'b1;
					
				case(cnt_write)
					5'd0:	begin oled_csn <= LOW; end							//使能OLED
					5'd1:	begin oled_clk <= LOW; oled_dat <= char_reg[7]; end	//先发高位数据
					5'd2:	begin oled_clk <= HIGH; end
					5'd3:	begin oled_clk <= LOW; oled_dat <= char_reg[6]; end
					5'd4:	begin oled_clk <= HIGH; end
					5'd5:	begin oled_clk <= LOW; oled_dat <= char_reg[5]; end
					5'd6:	begin oled_clk <= HIGH; end
					5'd7:	begin oled_clk <= LOW; oled_dat <= char_reg[4]; end
					5'd8:	begin oled_clk <= HIGH; end
					5'd9:	begin oled_clk <= LOW; oled_dat <= char_reg[3]; end
					5'd10:	begin oled_clk <= HIGH; end
					5'd11:	begin oled_clk <= LOW; oled_dat <= char_reg[2]; end
					5'd12:	begin oled_clk <= HIGH; end
					5'd13:	begin oled_clk <= LOW; oled_dat <= char_reg[1]; end
					5'd14:	begin oled_clk <= HIGH; end
					5'd15:	begin oled_clk <= LOW; oled_dat <= char_reg[0]; end	//后发低位数据
					5'd16:	begin oled_clk <= HIGH; end
					5'd17:	begin oled_csn <= HIGH; state <= DELAY; end	//
					default: state <= IDLE;
				endcase
			end

			DELAY:begin	//延时状态
				if(cnt_delay >= num_delay) begin
					cnt_delay <= 16'd0; state <= state_back; 
				end
				else 
					cnt_delay <= cnt_delay + 1'b1;
			end
			
			default: state <= IDLE;
		endcase
	end
end
 

//OLED配置指令数据
always@(posedge sys_rst_n)	begin
		cmd[ 0] = {8'hae};      //关闭显示

		cmd[ 1] = {8'h00};      //设置行低四位
		cmd[ 2] = {8'h10};      //设置行高四位
		cmd[ 3] = {8'h00}; 		//???这四行不太懂了
		cmd[ 4] = {8'hb0};      //设置页0

		cmd[ 5] = {8'h81};      //设置对比度
		cmd[ 6] = {8'hff};      //最大对比度

		cmd[ 7] = {8'ha1};      //段重定义设置,bit0:0,0->0 ;1,0->127;
		cmd[ 8] = {8'ha6}; 

		cmd[ 9] = {8'ha8};      //设置驱动路数
		cmd[10] = {8'h1f}; 

		cmd[11] = {8'hc8};      //设置COM扫描方向;bit3:0,普通模式;1,重定义模式 COM[N-1]->COM0; N:驱动路数

		cmd[12] = {8'hd3};      //设置显示偏移
		cmd[13] = {8'h00};      //偏移默认为0
            
		cmd[14] = {8'hd5};      //设置时钟分频因子
		cmd[15] = {8'h80};

		cmd[16] = {8'hd9};      //设置预充电周期
		cmd[17] = {8'h1f};      //[3:0],PHASE 1;[7:4],PHASE 2;

		cmd[18] = {8'hda};      //设置COM硬件引脚配置
		cmd[19] = {8'h00};      //[5:4]配置

		cmd[20] = {8'hdb};      //设置VCOMH 电压倍率
		cmd[21] = {8'h40};      //[6:4] 000,0.65*vcc;001,0.77*vcc;011,0.83*vcc

		cmd[22] = {8'h8d};      //电荷泵设置
		cmd[23] = {8'h14};      //开启电荷泵

		cmd[24] = {8'haf};      //开启显示
end 


//5*8点阵字库数据	对应其十进制ASCII码
always@(posedge sys_rst_n)begin
	mem[  0] = {8'h3E, 8'h51, 8'h49, 8'h45, 8'h3E};   // 48  0
	mem[  1] = {8'h00, 8'h42, 8'h7F, 8'h40, 8'h00};   // 49  1
	mem[  2] = {8'h42, 8'h61, 8'h51, 8'h49, 8'h46};   // 50  2
	mem[  3] = {8'h21, 8'h41, 8'h45, 8'h4B, 8'h31};   // 51  3
	mem[  4] = {8'h18, 8'h14, 8'h12, 8'h7F, 8'h10};   // 52  4
	mem[  5] = {8'h27, 8'h45, 8'h45, 8'h45, 8'h39};   // 53  5
	mem[  6] = {8'h3C, 8'h4A, 8'h49, 8'h49, 8'h30};   // 54  6
	mem[  7] = {8'h01, 8'h71, 8'h09, 8'h05, 8'h03};   // 55  7
	mem[  8] = {8'h36, 8'h49, 8'h49, 8'h49, 8'h36};   // 56  8
	mem[  9] = {8'h06, 8'h49, 8'h49, 8'h29, 8'h1E};   // 57  9
	mem[ 10] = {8'h7C, 8'h12, 8'h11, 8'h12, 8'h7C};   // 65  A
	mem[ 11] = {8'h7F, 8'h49, 8'h49, 8'h49, 8'h36};   // 66  B
	mem[ 12] = {8'h3E, 8'h41, 8'h41, 8'h41, 8'h22};   // 67  C
	mem[ 13] = {8'h7F, 8'h41, 8'h41, 8'h22, 8'h1C};   // 68  D
	mem[ 14] = {8'h7F, 8'h49, 8'h49, 8'h49, 8'h41};   // 69  E
	mem[ 15] = {8'h7F, 8'h09, 8'h09, 8'h09, 8'h01};   // 70  F

	mem[ 32] = {8'h00, 8'h00, 8'h00, 8'h00, 8'h00};   // 32  sp 
	mem[ 33] = {8'h00, 8'h00, 8'h2f, 8'h00, 8'h00};   // 33  !  
	mem[ 34] = {8'h00, 8'h07, 8'h00, 8'h07, 8'h00};   // 34  
	mem[ 35] = {8'h14, 8'h7f, 8'h14, 8'h7f, 8'h14};   // 35  #
	mem[ 36] = {8'h24, 8'h2a, 8'h7f, 8'h2a, 8'h12};   // 36  $
	mem[ 37] = {8'h62, 8'h64, 8'h08, 8'h13, 8'h23};   // 37  %
	mem[ 38] = {8'h36, 8'h49, 8'h55, 8'h22, 8'h50};   // 38  &
	mem[ 39] = {8'h00, 8'h05, 8'h03, 8'h00, 8'h00};   // 39  '
	mem[ 40] = {8'h00, 8'h1c, 8'h22, 8'h41, 8'h00};   // 40  (
	mem[ 41] = {8'h00, 8'h41, 8'h22, 8'h1c, 8'h00};   // 41  )
	mem[ 42] = {8'h14, 8'h08, 8'h3E, 8'h08, 8'h14};   // 42  *
	mem[ 43] = {8'h08, 8'h08, 8'h3E, 8'h08, 8'h08};   // 43  +
	mem[ 44] = {8'h00, 8'h00, 8'hA0, 8'h60, 8'h00};   // 44  ,
	mem[ 45] = {8'h08, 8'h08, 8'h08, 8'h08, 8'h08};   // 45  -
	mem[ 46] = {8'h00, 8'h60, 8'h60, 8'h00, 8'h00};   // 46  .
	mem[ 47] = {8'h20, 8'h10, 8'h08, 8'h04, 8'h02};   // 47  /
	mem[ 48] = {8'h3E, 8'h51, 8'h49, 8'h45, 8'h3E};   // 48  0
	mem[ 49] = {8'h00, 8'h42, 8'h7F, 8'h40, 8'h00};   // 49  1
	mem[ 50] = {8'h42, 8'h61, 8'h51, 8'h49, 8'h46};   // 50  2
	mem[ 51] = {8'h21, 8'h41, 8'h45, 8'h4B, 8'h31};   // 51  3
	mem[ 52] = {8'h18, 8'h14, 8'h12, 8'h7F, 8'h10};   // 52  4
	mem[ 53] = {8'h27, 8'h45, 8'h45, 8'h45, 8'h39};   // 53  5
	mem[ 54] = {8'h3C, 8'h4A, 8'h49, 8'h49, 8'h30};   // 54  6
	mem[ 55] = {8'h01, 8'h71, 8'h09, 8'h05, 8'h03};   // 55  7
	mem[ 56] = {8'h36, 8'h49, 8'h49, 8'h49, 8'h36};   // 56  8
	mem[ 57] = {8'h06, 8'h49, 8'h49, 8'h29, 8'h1E};   // 57  9
	mem[ 58] = {8'h00, 8'h36, 8'h36, 8'h00, 8'h00};   // 58  :
	mem[ 59] = {8'h00, 8'h56, 8'h36, 8'h00, 8'h00};   // 59  ;
	mem[ 60] = {8'h08, 8'h14, 8'h22, 8'h41, 8'h00};   // 60  <
	mem[ 61] = {8'h14, 8'h14, 8'h14, 8'h14, 8'h14};   // 61  =
	mem[ 62] = {8'h00, 8'h41, 8'h22, 8'h14, 8'h08};   // 62  >
	mem[ 63] = {8'h02, 8'h01, 8'h51, 8'h09, 8'h06};   // 63  ?
	mem[ 64] = {8'h32, 8'h49, 8'h59, 8'h51, 8'h3E};   // 64  @
	mem[ 65] = {8'h7C, 8'h12, 8'h11, 8'h12, 8'h7C};   // 65  A
	mem[ 66] = {8'h7F, 8'h49, 8'h49, 8'h49, 8'h36};   // 66  B
	mem[ 67] = {8'h3E, 8'h41, 8'h41, 8'h41, 8'h22};   // 67  C
	mem[ 68] = {8'h7F, 8'h41, 8'h41, 8'h22, 8'h1C};   // 68  D
	mem[ 69] = {8'h7F, 8'h49, 8'h49, 8'h49, 8'h41};   // 69  E
	mem[ 70] = {8'h7F, 8'h09, 8'h09, 8'h09, 8'h01};   // 70  F
	mem[ 71] = {8'h3E, 8'h41, 8'h49, 8'h49, 8'h7A};   // 71  G
	mem[ 72] = {8'h7F, 8'h08, 8'h08, 8'h08, 8'h7F};   // 72  H
	mem[ 73] = {8'h00, 8'h41, 8'h7F, 8'h41, 8'h00};   // 73  I
	mem[ 74] = {8'h20, 8'h40, 8'h41, 8'h3F, 8'h01};   // 74  J
	mem[ 75] = {8'h7F, 8'h08, 8'h14, 8'h22, 8'h41};   // 75  K
	mem[ 76] = {8'h7F, 8'h40, 8'h40, 8'h40, 8'h40};   // 76  L
	mem[ 77] = {8'h7F, 8'h02, 8'h0C, 8'h02, 8'h7F};   // 77  M
	mem[ 78] = {8'h7F, 8'h04, 8'h08, 8'h10, 8'h7F};   // 78  N
	mem[ 79] = {8'h3E, 8'h41, 8'h41, 8'h41, 8'h3E};   // 79  O
	mem[ 80] = {8'h7F, 8'h09, 8'h09, 8'h09, 8'h06};   // 80  P
	mem[ 81] = {8'h3E, 8'h41, 8'h51, 8'h21, 8'h5E};   // 81  Q
	mem[ 82] = {8'h7F, 8'h09, 8'h19, 8'h29, 8'h46};   // 82  R
	mem[ 83] = {8'h46, 8'h49, 8'h49, 8'h49, 8'h31};   // 83  S
	mem[ 84] = {8'h01, 8'h01, 8'h7F, 8'h01, 8'h01};   // 84  T
	mem[ 85] = {8'h3F, 8'h40, 8'h40, 8'h40, 8'h3F};   // 85  U
	mem[ 86] = {8'h1F, 8'h20, 8'h40, 8'h20, 8'h1F};   // 86  V
	mem[ 87] = {8'h3F, 8'h40, 8'h38, 8'h40, 8'h3F};   // 87  W
	mem[ 88] = {8'h63, 8'h14, 8'h08, 8'h14, 8'h63};   // 88  X
	mem[ 89] = {8'h07, 8'h08, 8'h70, 8'h08, 8'h07};   // 89  Y
	mem[ 90] = {8'h61, 8'h51, 8'h49, 8'h45, 8'h43};   // 90  Z
	mem[ 91] = {8'h00, 8'h7F, 8'h41, 8'h41, 8'h00};   // 91  [
	mem[ 92] = {8'h55, 8'h2A, 8'h55, 8'h2A, 8'h55};   // 92  .
	mem[ 93] = {8'h00, 8'h41, 8'h41, 8'h7F, 8'h00};   // 93  ]
	mem[ 94] = {8'h04, 8'h02, 8'h01, 8'h02, 8'h04};   // 94  ^
	mem[ 95] = {8'h40, 8'h40, 8'h40, 8'h40, 8'h40};   // 95  _
	mem[ 96] = {8'h00, 8'h01, 8'h02, 8'h04, 8'h00};   // 96  '
	mem[ 97] = {8'h20, 8'h54, 8'h54, 8'h54, 8'h78};   // 97  a
	mem[ 98] = {8'h7F, 8'h48, 8'h44, 8'h44, 8'h38};   // 98  b
	mem[ 99] = {8'h38, 8'h44, 8'h44, 8'h44, 8'h20};   // 99  c
	mem[100] = {8'h38, 8'h44, 8'h44, 8'h48, 8'h7F};   // 100 d
	mem[101] = {8'h38, 8'h54, 8'h54, 8'h54, 8'h18};   // 101 e
	mem[102] = {8'h08, 8'h7E, 8'h09, 8'h01, 8'h02};   // 102 f
	mem[103] = {8'h18, 8'hA4, 8'hA4, 8'hA4, 8'h7C};   // 103 g
	mem[104] = {8'h7F, 8'h08, 8'h04, 8'h04, 8'h78};   // 104 h
	mem[105] = {8'h00, 8'h44, 8'h7D, 8'h40, 8'h00};   // 105 i
	mem[106] = {8'h40, 8'h80, 8'h84, 8'h7D, 8'h00};   // 106 j
	mem[107] = {8'h7F, 8'h10, 8'h28, 8'h44, 8'h00};   // 107 k
	mem[108] = {8'h00, 8'h41, 8'h7F, 8'h40, 8'h00};   // 108 l
	mem[109] = {8'h7C, 8'h04, 8'h18, 8'h04, 8'h78};   // 109 m
	mem[110] = {8'h7C, 8'h08, 8'h04, 8'h04, 8'h78};   // 110 n
	mem[111] = {8'h38, 8'h44, 8'h44, 8'h44, 8'h38};   // 111 o
	mem[112] = {8'hFC, 8'h24, 8'h24, 8'h24, 8'h18};   // 112 p
	mem[113] = {8'h18, 8'h24, 8'h24, 8'h18, 8'hFC};   // 113 q
	mem[114] = {8'h7C, 8'h08, 8'h04, 8'h04, 8'h08};   // 114 r
	mem[115] = {8'h48, 8'h54, 8'h54, 8'h54, 8'h20};   // 115 s
	mem[116] = {8'h04, 8'h3F, 8'h44, 8'h40, 8'h20};   // 116 t
	mem[117] = {8'h3C, 8'h40, 8'h40, 8'h20, 8'h7C};   // 117 u
	mem[118] = {8'h1C, 8'h20, 8'h40, 8'h20, 8'h1C};   // 118 v
	mem[119] = {8'h3C, 8'h40, 8'h30, 8'h40, 8'h3C};   // 119 w
	mem[120] = {8'h44, 8'h28, 8'h10, 8'h28, 8'h44};   // 120 x
	mem[121] = {8'h1C, 8'hA0, 8'hA0, 8'hA0, 8'h7C};   // 121 y
	mem[122] = {8'h44, 8'h64, 8'h54, 8'h4C, 8'h44};   // 122 z
end
 
endmodule

 

五、数码管显示模块

      数码管模块在initial部分中先预存了10个数字对应的七段数码管值,使用时可根据BCD值直接输出。根据八段数码管的特性,小数点dot独占一位,与数字部分没有冲突。总输出时将数字部分与小数点部分按位取或即可得到最终的八段数码管值输出。

由于预先已将电压结果转换为BCD码形式,所以直接使用BCD前八个位进行显示即可。手动将第一个数码管打开小数点。

 

/**********************************************************
模块名:Segment_LED_DRIVER
更新日期:2021.9.2
功能:
	驱动一位共阴极8段数码管

*部分借鉴于电子森林中的开源教程*
**********************************************************/
module Segment_LED_DRIVER 
(   
	input seg_dot,					//是否显示小数点
	input [3:0] seg_data,			//要显示的数字(BCD)

	output [8:0] seg_led          	//选位使能 dot g f e d c b a
);


reg [8:0] seg [9:0];               	//定义了一个reg型的数组变量,相当于一个10*9的存储器,存储器一共有10个数,每个数有9位宽
reg [8:0] dot [1:0];        	   	//用来存放小数点开启和不开启的状态


//initial模块中先预存10个数字对应的7段数码管值
initial begin                          
    seg[0] = 9'h3f;                                         //对存储器中第一个数赋值9'b0_0011_1111,相当于共阴极接地,DP点变低不亮  显示数字  0
	seg[1] = 9'h06;                                         //显示数字  1
	seg[2] = 9'h5b;                                         //显示数字  2
	seg[3] = 9'h4f;                                         //显示数字  3
	seg[4] = 9'h66;                                         //显示数字  4
	seg[5] = 9'h6d;                                         //显示数字  5
	seg[6] = 9'h7d;                                         //显示数字  6
	seg[7] = 9'h07;                                         //显示数字  7
	seg[8] = 9'h7f;                                         //显示数字  8
	seg[9] = 9'h6f;                                      	//显示数字  9

	dot[0] = 9'h00;											//不显示小数点
	dot[1] = 9'h80;  										//显示小数点
end


//仅一位数码管 组合逻辑即可完成
assign seg_led = seg[seg_data]|dot[seg_dot];             	//数字结果和小数点结果相与就能得到最终输出的

endmodule

 

六、LED显示模块

      设计通过LED的点亮数量来反映电压大小,板载8个数码管,可对应0x00-0xFF二进制的前三位变化。通过一组case语句将二进制转化为独热码即可。

 

/**********************************************************
模块名:LED_DRIVER
更新日期:2021.8.31
功能:
	驱动核心板上的8个共阳极LED灯
    通过灯的亮灭数量来反映电压值大小

*部分借鉴于电子森林中的开源教程*
**********************************************************/
module LED_DRIVER
(
    input   sys_clk,
    input   sys_rst_n,

    input   [2:0]       vol_data,           

    output  reg [7:0]   LED_out     
);


always @(posedge sys_clk or negedge sys_rst_n) begin

    if(!sys_rst_n)
        LED_out <= 8'b11111111;             //复位全灭

    else
        case(vol_data)                      //LED8在下 LED1在上
            3'd0 :  LED_out <= 8'b01111111; 
            3'd1 :  LED_out <= 8'b00111111;
            3'd2 :  LED_out <= 8'b00011111;
            3'd3 :  LED_out <= 8'b00001111;
            3'd4 :  LED_out <= 8'b00000111;
            3'd5 :  LED_out <= 8'b00000011;
            3'd6 :  LED_out <= 8'b00000001;
            3'd7 :  LED_out <= 8'b00000000;
            
            default:LED_out <= 8'b11111111;
        endcase
end

endmodule

 

七、顶层模块

      例化每个模块,因为使用两个数码管显示,所以单数码管显示模块要例化两遍。

 

/**********************************************************
!!!!!!顶层模块!!!!!!
模块名:ADC_Voltmeter
更新日期:2021.9.5
功能:
	利用ADC制作一个数字电压表
    旋转电位计可以产生0-3.3V的电压
    利用板上的串行ADC对电压进行转换
    将电压值在板上的OLED屏幕上显示出来
    将电压值在板上的八段数码管上显示出来
    通过LED灯点亮数量来反映电压大小

*部分借鉴于电子森林中的开源教程*
**********************************************************/
module ADC_Voltmeter
(
    input sys_clk,                                      //系统时钟  12M

    input sys_rst_n,                                    //系统复位 低有效

    input adc_input,                                    //ADC数据信号

    output adc_cs,                                      //ADC使能信号
    output adc_clk,                                     //ADC时钟

    output  [8:0] seg_led_1,                            //八段数码管1 控制输出
    output  [8:0] seg_led_2,                            //八段数码管2 控制输出

    output  [7:0] LED_out,                              //对应8个LED输出

	output oled_cs,	                                    //OLCD液晶屏使能
	output oled_res,	                                //OLCD液晶屏复位
	output oled_dc,	                                    //OLCD数据指令控制
	output oled_clk,	                                //OLCD时钟信号
	output oled_mosi	                                //OLCD传输信号
);

wire clk_24M;                                           //12M主频在此不能满足ADC模块的需要 需倍频得到更高时钟

wire [7:0]  adc_data;                                   //ADC采样数据 0~255
wire [15:0] vol_result;                                 //四位十六进制温度结果
wire [19:0] vol_bcd;                                    //五位BCD码 温度结果


PLL u_PLL
(
    .CLKI               (sys_clk),
    .CLKOP              (clk_24M)
);


ADC_spi_DRIVER u_ADC_spi_DRIVER   
(
    .clk_24M            (clk_24M),		                //系统时钟      倍频得24M
    .sys_rst_n          (sys_rst_n),  	                //系统复位,低有效
    
    .adc_cs             (adc_cs),		                //SPI总线CS
    .adc_clk	        (adc_clk),                      //SPI总线SCK
    .adc_dat	        (adc_input),                    //SPI总线SDA

    .adc_data	        (adc_data),                     //ADC采样数据
    .vol_result         (vol_result),
    .vol_bcd            (vol_bcd)
);


Segment_LED_DRIVER u_Segment_LED_DRIVER_1 
(
    //左数码管
    .seg_dot            (1'b1),                         //左管显示小数点
	.seg_data           (vol_bcd[19:16]),               //温度值第一位

	.seg_led            (seg_led_1)                     // 使能  dot g f e d c b a 
);


Segment_LED_DRIVER u_Segment_LED_DRIVER_2               
(
    //右数码管
    .seg_dot            (1'b0),
	.seg_data           (vol_bcd[15:12]),               //温度值第二位

	.seg_led            (seg_led_2)                     // 使能  dot g f e d c b a
);


OLED_spi_DRIVER u_OLED_spi_DRIVER
(
	.sys_clk            (sys_clk),		                //12MHz系统时钟
	.sys_rst_n		    (sys_rst_n),                    //系统复位,低有效

    .oled_vol_result    (vol_bcd[19:8]),                //要显示的温度结果(BCD只保留前三位)       
 
	.oled_csn           (oled_cs),	                    //OLCD液晶屏使能
	.oled_rst           (oled_res),	                    //OLCD液晶屏复位
	.oled_dcn           (oled_dc),	                    //OLCD数据指令控制
	.oled_clk           (oled_clk),	                    //OLCD时钟信号
	.oled_dat           (oled_mosi)	                    //OLCD数据信号
);


LED_DRIVER u_LED_DRIVER
(
    .sys_clk        (sys_clk),
    .sys_rst_n      (sys_rst_n),

    .vol_data       (adc_data[7:5]),                    //八个LED 所以直接用ADC结果的高三位做判断即可

    .LED_out        (LED_out) 
);

endmodule

 

八、问题

1、除法报错

      在ADC模块中,有一个需求是需要将转换得到的四位16进制0~33000转化为五位BCD码。最初设想的方式是通过做除法取商和余的方式得到。可在综合时却会报错,不是那种写在ERROR和WARNING里面的错,而是在output界面中出现error code 999,并提示软件错误,几经修改都未能正确,尝试使用Synplify来综合也没有很好的改观。

      几经尝试后,发现这个问题就出现在除法上,一旦去掉了这部分十六进制转BCD码,就可以正常进行综合。但网上未能找到相关案例和错因分析。

Fu4GxpJ6kRQJiT1l_mqp7ZuwfIohFoTuYnGTaeKWbv49-xf-bVgDZ7Dy                     

               图6 疑似出错代码                                        图7:错误提示

个人推测是由于FPGA与MCU不同,它对除法操作需要使用大量的资源及特别复杂的运算导致的。最终采用移位+手动进位的方式完成了转换BCD码。

       *能用移位和进位代替的除法运算一定不要直接用“/”号!*

 

2、OLED显示方式

      SSD1306采用的显存显示模式,数据要按页、列存入指定的位置才能在屏幕中显示出来。虽然我理解了工作原理,但对SSD1306工作方式配置命令依然存在不少疑惑。这也导致了我在尝试在屏幕上绘制图案时的失败,好在这个项目不需要额外绘制图案,所以并未造成过多影响。

九、小结

      本人是初学FPGA,这也是第一次编写较多模块项目级的Verilog代码。和之前学习过的STM32不同,像这种硬件语言由于是并行执行的,就需要一种截然不同的编程逻辑和思考方式。如果说之前的单片机是需要顾前后,那HDL就需要在顾前后的同时,兼顾同时发生的左右,在一个模块中的几个Always块间是如此,不同的模块间亦是如此。

      除此之外,这个项目让我对ADC、OLED、七段数码管等器件有了更深的认识。特别是SPI通信协议,通过这次项目实战的训练,我已经对它掌握的炉火纯青了。

      最后,感谢电子森林和硬禾学堂提供的开发板和项目实战计划,同时给出了大量的开源学习资料和教学视频,让我能在学习FPGA的道路上乘风破浪。

 

附件下载
电子森林2021暑假一起练项目一.pdf
项目报告
源代码与二进制代码文件.zip
.jed也在里面
Complete_Project.rar
整个工程目录
团队介绍
北京工业大学 信通学院 通信工程专业
团队成员
会伏丘
aka会十万伏特de皮卡丘 BJUT通信工程专业在读本科生 是小白 渣渣 划水怪 梦想有一天也能成为野生钢铁侠那样的 orz
王奕达
会伏丘本体 北京工业大学 大三在读
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号