基于iCE40UP5K的FPGA学习平台实现数字电压表
使用iCE40UP5K的FPGA学习平台实现一个数字电压表的功能
标签
FPGA
显示
2022寒假在家练
电压测试
Andy
更新2022-03-03
西安电子科技大学
949
  • 项目需求:
  1. 旋转电位计可以产生0-3.3V的电压
  2. 利用板上的串行ADC对电压进行转换
  3. 将电压值在板上的OLED屏幕上显示出来
  • 整体功能框图:

     FkiWoeKL3Y5EzuTRhCs9b3g_esow

  • 操作方法:

滑动旋钮,在OLED上即可显示此时测得的电压值

  • 实现的思路:

首先逐条分析这些需求;

第一点:旋转电位计可以产生0-3.3V的电压

通过查看扩展板原理图可以看到,电位计上可以通过旋钮分得0-3.3V的电压,然后接入到了40Pin插座的34脚,然后再接入到高速比较器TP1961的正极输入端。

FuEa8fNe9dYupCXIABlVEoHa55L0

第二点:利用板上的串行ADC对电压进行转换

扩展板上并没有现成的ADC可以使用,实际上得用PWM波配合高速比较器实现ADC的功能。

首先PWM是由一串连续行走在某输出管脚上的0、1交替出现的信号组成,我们称高电平1为ON,低电平0为OFF,ON+OFF为一个周期T,ON的持续时间除以周期T就为占空比 - Duty Cycle,如下图。

FvVoWUFJjFd6EpMWwNrj_6HJhJzA

如果发送端用脉冲的占空比来传递“电压值”,也就是将某个数字的电压值对脉冲的占空比进行调制,就可以在接收端通过RC低通滤波器(也就是解调器)从调制脉宽的数据流中得到需要的模拟电压值,从而达到DAC的目的。看下图 - 假设脉冲的占空比为0的时候(整个周期全部为OFF - 低电平)代表电压值为0,占空比为100%的时候(整个周期全部为ON - 高电平)代表电压值为最高电压,比如3.3V,则40%的占空比就是40%*3.3V。占空比改变-每个周期的脉宽改变,也就意味着输出的电压值在改变,如下图:FhYUWFr4GhXV-cxAHKR2tgYodZGS

具体实现也很简单,首先我使用状态机将整个过程分成三个状态:IDLE、TEST、SHOW。这样做为了防止在OLED显示前一个测试结果的时候,新的测试结果也产生,可能会让OLED显示起来感觉在抖动,不稳定。IDLE是初始状态和一次完整测量之后的等待状态;TEST是进行电压测量的状态;SHOW是将数据显示到OLED上的状态。代码如下:

    wire IDLE_2_TEST;
    wire TEST_2_SHOW;
    wire SHOW_2_IDLE;

    assign IDLE_2_TEST = oled_res == 1'b1;
    assign TEST_2_SHOW = compare_i_negedge;
    assign SHOW_2_IDLE = oled_display_done;
    always @ (*)
    begin
        case (state)
            IDLE    : if (IDLE_2_TEST)  next = TEST;
            else                         next = IDLE ;
            TEST    : if (TEST_2_SHOW)  next = SHOW;
            else                         next = TEST;
            SHOW    : if (SHOW_2_IDLE)  next = IDLE;
            else                         next = SHOW;
                default : next = IDLE;
        endcase
    end

IDLE跳转到TEST的条件是OLED初始化完成;TEST跳转到SHOW的条件是检测到了比较器输出由高到低跳变;SHOW跳转到IDLE的条件是OLED显示完成,如下图所示:

FlTkgRhI3u5kPVu6bkxVpNa3IvvS

本次设计将0-3.3V分成34份,所以精度约为0.1V,先用一个计数器cnt_pulse来产生PWM波的周期,从0计数到33,再用一个计数器cnt_high来产生PWM波的高电平,每当cnt_pluse计数到等于33时,cnt_high累加1,代码如下:

    localparam pwm_divide    = 8'd34     ;

    localparam MAX_NUM_pulse = pwm_divide;

    reg  [7:0] cnt_pulse                 ;

    wire       add_cnt_pulse,end_cnt_pulse;

    assign add_cnt_pulse = (state == TEST);

    assign end_cnt_pulse = add_cnt_pulse && cnt_pulse==(MAX_NUM_pulse)-1;

    always @(posedge clk or negedge rst_n_i)

        begin

            if(!rst_n_i)                    cnt_pulse <= 1'b0;

            else if (state == SHOW)         cnt_pulse <= 0;

            else if (add_cnt_pulse)

                begin

                    if (end_cnt_pulse)      cnt_pulse <= 0;

                    else                    cnt_pulse <= cnt_pulse + 1'b1;

                end

        end

    localparam MAX_NUM_high = pwm_divide;

    reg  [7:0] cnt_high                 ;

    wire       add_cnt_high,end_cnt_high;

    assign add_cnt_high = end_cnt_pulse;

    assign end_cnt_high = add_cnt_high && cnt_high==(MAX_NUM_high)-1;

    always @(posedge clk or negedge rst_n_i)

        begin

            if(!rst_n_i)                    cnt_high <= 1'b0;

            else if (state == SHOW)         cnt_high <= 0;

            else if (add_cnt_high)

                begin

                    if (end_cnt_high)       cnt_high <= 0;

                    else                    cnt_high <= cnt_high + 1'b1;

                end

        end

最后将两个计数器进行比较,当cnt_high大于cnt_pulse的时候,便将pwm_o输出1,否则输出0;并且除了在TEST状态,其他状态的pwm_o都输出0,具体代码如下:

    always @ (posedge clk or negedge rst_n_i)
        if (~rst_n_i) pwm_o       <= 1'b0;
        else
            begin
                case (state)
                    IDLE    : pwm_o <= 1'b0;
                    TEST    : pwm_o <= (cnt_high > cnt_pulse);
                    SHOW    : pwm_o <= 1'b0;
                    default : pwm_o <= 1'b0;
                endcase
            end

最终,将得到一个占空比从0%开始递增,每次递增1/34,一直递增到33/34的PWM波,经过一个电阻和电容组成的低通滤波就可以将PWM中携带的电压信息“解调”成模拟的电压值。同时不断检测比较器的输出是否产生了一个由高变低的跳变,如果有,便是PWM波等效的直流电压大于电位计的电压,可以认为此时的cnt_high的值即为测得的电位计的值。

第三点:将电压值在板上的OLED屏幕上显示出来

此次用到的OLED规格是128*64的,和我去年寒假用的128*32大同小异,都是使用SSD1306进行驱动,有个128*64bit的RAM存储像素信息,可以理解为一个128列*64行的点阵,点阵存储的数据若为高位1,则OLED屏的对应像素点点亮(可以改设置低位点亮),因此只需要把“3.30V”等这些字符的字模数据发送到SSD1306就可以显示相应的字符了。此款芯片有多种通信方式,而安装在训练板上之后设置成了spi通信模式,所以需要先写一个spi发送数据的模块。

其次,每次发送给SSD1306的数据是8bit(1 word)的数据,学习了SSD1306芯片的数据手册和网上资料,需要先将芯片进行复位(复位拉低100ms左右)然后写入初始化信息(如显示模式,按行写或者按写等等),最后循环写入要显示的数据信息(循环是因为数据是会变的,要实时更新)。得到点阵信息之后初始化到例化的IP——ROM里面备用。例化现有的IP会比使用寄存器更节省资源。

然后,由于电压数据是用6位二进制数存储的,此时还应该先用“左移+3法“将其转换成BCD码存储,将整数位和十分位分别用8位二进制数表示,高4位表示整数位,低4位表示十分位.通过这个8位二进制数的高4位和低4位的值,得到所对应的阿拉伯数字的点阵数据对应的rom地址,读取其中的内容由spi发送给SSD1306即可。

需要注意的是,rom只有一个地址输入,需要根据此时所需要显示的数据,得到对应的地址。其中voltage_1、voltag_2等是使能信号,通过使能信号选择地址。

关键代码如下:

//write voltage_1
4'd2 :
    case(j)
        6'd0 :
            begin y <= y_voltage; voltage_1 <= 1'b1; j <= j + 1'b1;end
        //set the page address
        6'd1,6'd5://,6'd9,6'd13:
            if(spi_write_done)  begin start <= 1'b0; j <= j + 1'b1; end
            else                begin data <= {2'b00,4'hb,y}; start <= 1'b1; end
        //set the higher bit of colume address
        6'd2,6'd6://,6'd10,6'd14:
            if(spi_write_done)	begin start <= 1'b0; j <= j + 1'b1; end
            else    			begin data <= {2'b00,4'h1,seg_h_voltage_1}; 
            start <= 1'b1; end
        //set the lower bit of colume
        6'd3,6'd7://,6'd11,6'd15:
            if(spi_write_done)	begin start <= 1'b0; j <= j + 1'b1; end
            else    			begin data <= {2'b00,4'h0,seg_l_voltage_1}; 
            start <= 1'b1; end
        //write 8 colume
        6'd4 : //,6'd12:
            if(x==3'd7 & spi_write_done)
                begin y <= y + 1'b1; x <= 3'd0; j <= j + 1'b1; start <= 1'b0; end
                else
                    if(spi_write_done)  begin start <= 1'b0; x <= x + 1'b1; end
                    else                begin data <= {2'b01,writting_data}; start <= 
                    1'b1; end
        6'd8 : //,6'd16:
            if(x==3'd7 & spi_write_done)
                begin y <= y + 1'b1; x <= 3'd0; j <= j + 1'b1; start <= 1'b0; end
                else
                    if(spi_write_done)  begin start <= 1'b0; x <= x + 1'b1; end
                    else                begin data <= {2'b01,writting_data}; start <= 
                    1'b1; end
        6'd9 :
            begin voltage_1 <= 1'b0;j <= 6'd0;  i <= i + 1'b1; end
    endcase

这样为写一个字符需要用到的代码,先发送要显示的位置的页数(就是第几行)坐标,再发送要显示的列数高八位和低八位坐标,最后将从rom读出的字模数据写入到oled,即可显示出字符。

 

  • 完成的功能:

测量电位计上的电压

FqUB2Ds9Ion8T2KvOpsfIROQnfvh

FlxBL2CjxlwiZTH0fzlGpOWHBE7e

FlGw7N6lQNhkT6rLo-OlaBOziUd7

Fthz8awYk-1EJFyYnh_m3d_dih4S

  • 达到的性能:

       资源使用情况截图

FsPhv_dpr4k9ek9FAIZtfUn5tA2b

     

  • 遇到的主要难题和解决过程:
  1. 一开始我尝试想把电压分辨率做到0.01V,尝试将PWM波周期设为256个clk,相当于8位的DAC。做出来之后进行测试,发现一个奇怪的现象:当电位计从0V递增到1.60V(用电压表实测),数字量从0递增到200;当电位计从1.60V递增到3.30时,数字量从200递增到256。这相当不科学!于是接下来就是各种怀疑,猜测,找来找去都没发现有什么bug。后来我隐约感觉,PWM波经过低通滤波器输出到比较器上,到底是不是像原理上说的,等效为一个直流电平?为了验证想法,我用示波器测试比较器的负极输入端,结果让我瞬间清醒!测试结果如下图所示,原来比较器的负极输入端不是一个理想的直流电平,而是一些类似锯齿波的东西。分析了一下原理,觉得是当PWM输出高电平时,在比较器负极输入端前的电容上充电,而当PWM输出低电平时,电容由开始放电了,而PWM的占空比影响的,实际上是电容能充电到的电平高度!这些问题有点明朗了,在占空比比较高的时候,放电速度要快很多,所以实际上接入到比较器负极输入端的所谓“电平”,是相当不准确的,导致了测量结果也不准确!FrtRNi_GfBC1XHa8V9xJ4WbZ1xrD为了解决这一问题,我又重新去看了下PWM相关的原理,发现在苏老师写的系列文章其实有提到低通滤波器的选择。经过计算,板上的电容位1000pF,电阻位1kΩ,截止频率大约为159kHz,而我使用的时钟为12M晶振,PWM波的频率为352kHz,这个低通滤波器并不能把PWM的高次谐波滤干净!但是板子上的晶振和低通滤波器已经定好了不方便改,PWM的周期为34个clk也没法再低了,能做的只有提高运行的时钟了,于是想到了使用内部的PLL进行4倍频,经过测试,纹波改善了非常多,如下图所示:FqbmR6JyYW_RUTe5o-P-p8jHl8qJ
  2. 将运行时钟倍频到48M之后解决了PWM的问题,却引入了时序违例的问题,之前写的oled的驱动模块跑不了48M的频率,如下图所示:Frf0yDUyadzoUnwUJsbY2GPuv_SW

经过重新修改一些逻辑,插入寄存器,将时序违例减小了一部分,

FimhFsjFJwPQnZE2YxH5g1wMr30R

后面实在是不好再修改逻辑了,只能选择将oled驱动模块的时钟进行分频,最后才解决时序问题

FiLwfbEstWeT3_kaW6Y287qcsRkD

 

  • 未来的计划等:
  1. 有时间可以将OLED的逻辑重新写一下,争取可以跑更高的时序
  2. 测电压的算法可以尝试使用二分法,然后比较一下精度是否能达到要求,这样可以更快的测出结果
附件下载
src.rar
verilog代码
voltage_test_impl_1.rbt
rbt文件
voltage_test.rar
整个工程文件
团队介绍
西安电子科技大学-微电子学院
团队成员
Andy
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号