一、项目需求:
该项目基于小脚丫FPGA的综合技能训练板,利用ADC制作一个数字电压表,具体功能如下:
1、旋转电位计可以产生0-3.3V的电压;
2、利用板上的串行ADC对电压进行转换;
3、将电压值在板上的OLED屏幕上显示出来;
4、通过按键可以锁定电压值,相当于万用表上面端的HOLD功能;
二、设计思路:
小脚丫FPGA的综合技能训练板上带有一颗SPI接口的串行ADC,可以采集电位计上的电压。板上的ADC是串行ADC,采样率最高为280ksps,可以对频率为20KHz以内的信号(音频信号的范围)进行采样;同时,板上采用了一块128*32分辨率的OLED作为信息显示终端,可以将ADC采集到的电压值显示在屏幕上。板上的OLED和ADC器件都使用SPI通信,因此该系统实现的核心在于解决SPI通信。
总体设计模块如下图1所示:
图1 总体设计模块图
三、模块实现:
本系统由ADC驱动模块、OLED驱动模块、ADC电压计算模块、功能按键实现模块和顶层总体逻辑控制模块,分别如下所述:
1、ADC驱动模块:
小脚丫FPGA的综合技能训练板上所带的ADC是一颗BB公司生产的8bit-串行ADC,该ADC最大可以达到280KSPS采样率,该ADC框图如图2所示:
图2 ADS7868框图
根据ADS7868手册提供的时序图可知该ADC的驱动SPI基本符合标准SPI通信,如图3所示:
图3 ADS7868时序图
根据该时序图,设计ADC的驱动状态机如图4所示:
图4 ADC驱动状态机
根据该状态机的状态转移设置,且由于输入时钟较快,因此采用分频代码对输入时钟进行8分频,使得ADS7868的驱动时钟符合datasheet中的时钟要求(<6.7us,即最大频率不超过149KHz)编写代码如下所示:
/*分频产生fclk*/
always @(posedge I_sys_clk or negedge I_rst_n) begin
if(~I_rst_n) begin
f_sck <= 0;
end
else begin
if(R_cnt == PERIOD-1) begin
f_sck <= ~f_sck;
R_cnt <= 0;
end
else begin
R_cnt <= R_cnt + 1;
end
end
end
/*采样状态机*/
always @(posedge I_sys_clk or negedge I_rst_n)
if(!I_rst_n) begin
O_adc_cs <= 1'b1; O_adc_clk <= 1'b1;
O_data <=0;
O_done <=1'b0;
bit_cnt <= 4'd8;
nop_cnt <= 4'd0;
R_state <= 4'd0;
end
else case(R_state)
8'd0 : begin
O_adc_cs <= 1'b1;
O_adc_clk <= 1'b1;
R_state <= 4'd1;
bit_cnt <= 4'd8;
nop_cnt <= 4'd0;
end
8'd1 : begin
O_done <= 1'b0;
if(nop_cnt == 4'd4) begin
R_state <= 4'd3;
end
else begin
O_adc_cs <= 1'b0;
O_adc_clk <= 1'b1;
R_state <= 4'd2;
end
end
8'd2 : begin
O_adc_cs <= 1'b0;
O_adc_clk <= 1'b0;
nop_cnt <= nop_cnt + 4'd1;
R_state <= 4'd1;
end
8'd3 : begin
if(bit_cnt == 0) begin
R_state <= 4'd5;
end
else begin
O_adc_cs <= 1'b0;
O_adc_clk <= 1'b1;
O_data[bit_cnt-1] <= I_adc_data;
R_state <= 4'd4;
end
end //4
8'd4 : begin
O_adc_cs <= 1'b0;
O_adc_clk <= 1'b0;
bit_cnt <= bit_cnt - 4'd1;
R_state <= 4'd3;
end
8'd5 : begin
O_adc_cs <= 1'b1;
O_adc_clk <= 1'b0;
O_done <= 1'b1;
R_state <= 4'd0;
end
default : begin
O_adc_cs <= 1'b1;
O_adc_clk <= 1'b1;
O_done <= 1'b0;
end
endcase
2、OLED驱动模块:
OLED使用FPGA驱动较为复杂,该128X32的点阵屏幕采用SSD1306,虽然SSD1306有多种驱动方式,但是在板子上该驱动方式仅为SPI驱动实现,4-wire SPI总线驱动时序图如图5所示,图6显示为SDD1306刷新RAM时地址与数据的分配关系:
图5 4-wire SPI驱动时序图
图6 地址与数据位分配关系
由于能力有限,且为了加快设计进度,在电子森林上找到了相关的驱动程序(项目地址见【https://www.eetree.cn/wiki/oled_spi_verilog】),但是该驱动程序是直接驱动方式,数据和位置都写死在程序中,可移植性和灵活性较差,因此,对该驱动程序做适当修改,将其改为RAM读写的驱动端口,这样,就可以在RAM的另一个写端口将要显示的内容按地址写入,随后,OLED驱动程序在RAM的读端口按地址读取要显示的数据。修改接口程序如下,提供ram的读地址和读信号:
case(cnt_main) //MAIN状态
5'd0: begin state <= INIT; end
5'd1:begin
ram_addr <= 4'd1;
end
5'd2: begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= ram_data[16*8-1:0];state <= SCAN; end
5'd3: begin
ram_addr <= 4'd2;
end
5'd4: begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= ram_data[16*8-1:0];state <= SCAN; end
5'd5:begin
ram_addr <= 4'd3;
end
5'd6: begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= ram_data[16*8-1:0];state <= SCAN; end
5'd7:begin
ram_addr <= 4'd0;
end
5'd8: begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= ram_data[16*8-1:0];state <= SCAN; end
// 5'd1: begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "OLED TEST ";state <= SCAN; end
// 5'd2: begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "OLED TEST ";state <= SCAN; end
// 5'd3: begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "OLED TEST ";state <= SCAN; end
// 5'd4: begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "OLED TEST ";state <= SCAN; end
// 5'd5: begin y_p <= 8'hb0; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd 1; char <= sw; state <= SCAN; end
// 5'd6: begin y_p <= 8'hb1; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd 1; char <= sw; state <= SCAN; end
// 5'd7: begin y_p <= 8'hb2; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd 1; char <= sw; state <= SCAN; end
// 5'd8: begin y_p <= 8'hb3; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd 1; char <= sw; state <= SCAN; end
default: state <= IDLE;
endcase
最后,修改后的的OLED程序部分如下所示,RAM的配置的log如图7所示:
GRAM_MY inst0_0 (
.WrAddress( WrAddress),
.RdAddress( RdAddress),
.Data(Data ),
.WE(1'b1 ),
.RdClock(I_sys_clk ),
.RdClockEn( 1'b1),
.Reset(!I_rst_n ),
.WrClock(I_sys_clk ),
.WrClockEn(1'b1 ),
.Q( ram_data)
);
图7 OLED的RAM配置log
3、ADC电压计算模块:
由于ADC采集后的结果为8bit的二进制数,如果要显示在OLED上,需要将该二进制数使用如下公式1计算最后得到的电压值,然后采用移位法将获得电压值转换为BCD数,并分为小数和整数显示:
ADC_Volt = 3.3X(CODE /255) (公式1)
设定ADC电压的显示为5个部分,第一部分为整数部分,第二部分为小数点,第三到第五部分为小数部分,代码如下所示:
ADS7868_DRIVER inst0(
.I_sys_clk(f_sck),
.I_rst_n(I_rst_n),
.I_adc_data(I_data),
.O_adc_clk(O_adc_clk),
.O_adc_cs(O_adc_cs),
.O_done(O_led),
.O_data(O_data)
);
wire [15:0] bin_code = O_data * 16'd129 + 16'd010;
reg [35:0] shift_reg;
//reg [19:0] bcd_code;
always@(bin_code or I_rst_n)begin
shift_reg = {20'h0,bin_code};
if(!I_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
/*BCD数拆分为一位整数,三位小数,用于OLED显示*/
assign new_bcd[39:32] = {4'b0,bcd_code[19:16]};
assign new_bcd[31:24] = 8'd46;
assign new_bcd[23:16] = {4'b0,bcd_code[15:12]};
assign new_bcd[15:8] = {4'b0,bcd_code[11:8]};
assign new_bcd[7:0] = {4'b0,bcd_code[7:4]};
4、功能按键实现模块:
为了便于后面实现电压测量过程中方便记录电压,因此增加了一个额外功能:电压显示锁,该功能就是在按一次按键后,OLED 会显示“LOCKED”字样,此时OLED 的电压会锁定在按键前的一刻的采样电压值。
为了保证按键都是每次按下有效,因此采用边沿检测的方法,每当检测到按键上升沿的同时,根据前一状态判断是锁定电压值还是解锁电压值,代码如下所示:
/*选择输出*/
assign new_bcd_t = (key_scan == 0) ? new_bcd_t : new_bcd;
/*按键上升沿检测*/
reg [1:0] key_buf;
wire W_key_pos;
always@(posedge I_sys_clk or negedge I_rst_n)
begin
if(~I_rst_n) begin
key_buf <= 0;
end
else begin
key_buf <= {key_buf[0],I_key_lock};
end
end
assign W_key_pos = (key_buf == 2'b01) ? 1'b1 : 1'b0;
always @(posedge I_sys_clk or negedge I_rst_n) begin
if(~I_rst_n) begin
key_scan <= 0;
end
else begin
if(W_key_pos) begin
key_scan <= ~key_scan;
end
else begin
key_scan <= key_scan;
end
end
end
5、顶层总体逻辑控制模块:
顶层控制逻辑负责控制整体顺序,并负责传递各个模块的数据,最主要的就是负责把ADC计算好的电压BCD码组装到显示RAM中,并且OLED从RAM中取出显示数据,并显示在OLED上面,代码如下所示:
reg [3:0] R_mainstate;
always @(posedge I_sys_clk or negedge I_rst_n) begin
if(~I_rst_n) begin
WrAddress <= 4'd0;
Data <= {" VOLTMETER "};
//WE <= 1'b0;
R_mainstate <= 4'd0;
end
else begin
case (R_mainstate)
4'd0 : begin
Data <= {" VOLTMETER "};
WrAddress <= 4'd0;
//WE <= 1'b1;
R_mainstate <= 4'd1;
end
4'd1 :begin
Data <= {"voltage : ",new_bcd_t,"V"};
WrAddress <= 4'd1;
R_mainstate <= 4'd2;
end
4'd2 : begin
Data <= {"liuyunfeng edit"};
WrAddress <= 4'd2;
//WE <= 1'b1;
R_mainstate <= 4'd3;
end
4'd3 :begin
if(~key_scan) begin
Data <= {" LOCKED "};
end
else begin
Data <= {" RUNNING "};
end
WrAddress <= 4'd3;
R_mainstate <= 4'd0;
end
default: begin
WrAddress <= 4'd0;
Data <= 128'd0;
//WE <= 1'b0;
R_mainstate <= 4'd0;
end
endcase
end
end
四、测试:
分为三组测试内容,分别为电压扫描测试、电压锁定测试和电压对比测试,具体测试详见b站视频。
五、遇到问题:
在开发中遇到电压校准存在问题,且误差较大,需要后续进行再次校正;同时,由于OLED 采用他人开源代码,导致并不是能非常方便的上手修改与使用,后续会重新修改方便扩展使用。
六、项目总结:
在本次项目开发中,首次接触到lattice的fpga和他的EDA工具,刚上手废了一段时间学习新的FPGA,并开发测试;同时在完成本项目中更加深刻的学习了SPI通信和OLED 的驱动方式。由于所用FPGA较少,本次项目也是系统的使用了一个全新的。轻量级FPGA平台完成了一次具备实用功能的项目,虽然本次项目完成还有很多不足,尤其是在电压测量精度上还有较大的改进。
附录:
完整的项目代码也已上传至我的github仓库:https://github.com/kevinliuyunfeng/STEP_MXO2_ADC.git