做一個小項目 - 用小腳丫FPGA來測光,體會如何用全數字器件測量模擬信號。

測量環境光的變化最簡單、便宜的器件就是LDR,也稱之為光敏電阻,適用于檢測環境光的強度和環境的亮度。由于它體積小巧、便宜,易于使用,方便安裝在面包板或者PCB上,被廣泛用于各種創客產品中用作模擬傳感器或環境光控制的開關來使用。可以通過MCU的ADC進行環境光亮度的測量,也可以通過電位計+比較器的方式通過MCU的數字IO進行感知。
它的樣子如下:

工作原理,就是不同的光強度下,其輸出阻抗不同,典型曲線見下圖:

如果按照下面的電路圖連接,LDR兩端的電壓隨著光線的強度增強而降低。

合理設定串接的電阻值,和LDR兩端的電壓的閾值,就可以通過LDR做光控開關,電路超級簡單,但應用卻非常廣泛,比如生活中常用的:
我今天要介紹的是用小腳丫FPGA上的8個LED來指示環境光的強度,那就需要將LDR兩端的電壓信號進行量化。
問題來了,小腳丫FPGA是全數字的IO,板上沒有模擬信號的入口,如果要采集外部的模擬量,那就需要用到模數轉換器ADC,即便使用串行的ADC也會增加不少成本(比LDR要貴不少),還需要編寫I2C或SPI的邏輯來將ADC的數據讀出來。
有沒有更偏移、更簡單的方式呢?如果能夠用價廉物美的方式讀取外部的模擬信號,那很多沒有數字接口的模擬傳感器、電位計等都可以進行量化了。
答案是肯定的,只需要一個比較器和一個電阻/一個電容構成的低通濾波器就可以了,需要用到2個FPGA的IO,一個輸出PWM信號,一個輸入比較器的結果信號,用KiCad繪制一個簡單的原理圖:

說明一下 - 原理圖中的比較器用的是高速的,RC常數也選得截止頻率比較高,是為了可以適配音頻等頻率有可能變化較快的信號(把圖中的R3換成Micphone),在測量光強的場景下,光強的變化很緩慢,可以使用非常低速的比較器以及較低截止頻率的RC參數。
繪圖的時候不需要自己建庫,KiCad系統自帶的庫已經非常豐富,可以直接拿來用:

KiCad工具中有LDR的原理圖符號、封裝以及3D模型
最后做了一個小模塊:

可以插在面包板上的模塊的3D效果圖
同小腳丫FPGA一起放在插到面包板上,并通過杜邦線將3.3V和GND連上,PWM和Do信號在面包板上自動連接。

上圖中的LED顯示是已經下載了LDR檢測代碼的,隨便用一個紙片或電路板對LDR進行遮擋,可以看到8個LED的變化。

由于阻抗和光強的曲線不是線性的(但是單調的)如果要做更精準的測量,需要針對實際的曲線進行定標,比較簡單的方式就是做一個查找表LUT,在FPGA里實現起來也非常簡單。
有同學會問,用比較器和RC濾波器怎么就能測出來了?
其實這是利用了串行ADC的基本原理 -?1位的比較器,通過多次比較最終判斷出準確的數字,在我們這個示例中用到的就是sigma delta方式的ADC。

詳細的工作原理和實現代碼在電子森林網站上都有(https://www.eetree.cn/project/detail/255),在這里不再贅述。
下面講述一下編程:

編程使用我們的Web IDE也非常簡單,根本不需要再安裝任何軟件,只需要打開網頁www.stepfpga.com,登陸你自己已經創建好的賬號,創建一個工程:

在網頁里編寫代碼

圖形化管腳分配
Sigma Delta的頂層模塊
module?ADC_top?(clk_in,rstn,digital_out,analog_cmp,analog_out,sample_rdy);parameterADC_WIDTH = 8, // ADC Convertor Bit PrecisionACCUM_BITS = 10, // 2^ACCUM_BITS is decimation rate of accumulatorLPF_DEPTH_BITS = 3, // 2^LPF_DEPTH_BITS is decimation rate of averagerINPUT_TOPOLOGY = 1; // 0: DIRECT: Analog input directly connected to + input of comparitor// 1: NETWORK:Analog input connected through R divider to - input of comp.//input portsinput clk_in; // 62.5Mhz on Control Demo boardinput rstn;input analog_cmp; // from LVDS buffer or external comparitor//output portsoutput analog_out; // feedback to RC networkoutput sample_rdy;output [7:0] digital_out; // connected to LED field on control demo bd.//**********************************************************************//// Internal Wire & Reg Signals////**********************************************************************wire clk;wire analog_out_i;wire sample_rdy_i;wire [ADC_WIDTH-1:0] digital_out_i;wire [ADC_WIDTH-1:0] digital_out_abs;assign clk = clk_in;//***********************************************************************//// SSD ADC using onboard LVDS buffer or external comparitor////***********************************************************************sigmadelta_adc #(.ADC_WIDTH(ADC_WIDTH),.ACCUM_BITS(ACCUM_BITS),.LPF_DEPTH_BITS(LPF_DEPTH_BITS))SSD_ADC(.clk(clk),.rstn(rstn),.analog_cmp(analog_cmp),.digital_out(digital_out_i),.analog_out(analog_out_i),.sample_rdy(sample_rdy_i));assign digital_out_abs = INPUT_TOPOLOGY ? ~digital_out_i : digital_out_i;//***********************************************************************//// output assignments////***********************************************************************assign digital_out = ~digital_out_abs; // invert bits for LED displayassign analog_out = analog_out_i;assign sample_rdy = sample_rdy_i;endmodule
Sigma?Delta模塊:
module sigmadelta_adc (clk,rstn,digital_out,analog_cmp,analog_out,sample_rdy);parameterADC_WIDTH = 8, // ADC Convertor Bit PrecisionACCUM_BITS = 10, // 2^ACCUM_BITS is decimation rate of accumulatorLPF_DEPTH_BITS = 3; // 2^LPF_DEPTH_BITS is decimation rate of averager//input portsinput clk; // sample rate clockinput rstn; // async reset, asserted lowinput analog_cmp ; // input from LVDS buffer (comparitor)//output portsoutput analog_out; // feedback to comparitor input RC circuitoutput sample_rdy; // digital_out is readyoutput [ADC_WIDTH-1:0] digital_out; // digital output word of ADC//**********************************************************************//// Internal Wire & Reg Signals////**********************************************************************reg delta; // captured comparitor outputreg [ACCUM_BITS-1:0] sigma; // running accumulator valuereg [ADC_WIDTH-1:0] accum; // latched accumulator valuereg [ACCUM_BITS-1:0] counter; // decimation counter for accumulatorreg rollover; // decimation counter terminal countreg accum_rdy; // latched accumulator value 'ready'//***********************************************************************//// SSD 'Analog' Input - PWM//// External Comparator Generates High/Low Value////***********************************************************************always @ (posedge clk)begindelta <= analog_cmp; // capture comparitor outputendassign analog_out = delta; // feedback to comparitor LPF//***********************************************************************//// Accumulator Stage//// Adds PWM positive pulses over accumulator period////***********************************************************************always @ (posedge clk or negedge rstn)beginif( ~rstn )beginsigma <= 0;accum <= 0;accum_rdy <= 0;end else beginif (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 valueend else beginif (&sigma != 1'b1) // if not saturatedsigma <= sigma + delta; // accumulateendaccum_rdy <= rollover; // latch 'rdy' (to align with accum)endend//***********************************************************************//// Box filter Average//// Acts as simple decimating Low-Pass Filter////***********************************************************************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));//************************************************************************//// Sample Control - Accumulator Timing////************************************************************************always @(posedge clk or negedge rstn)beginif( ~rstn ) begincounter <= 0;rollover <= 0;endelse begincounter <= counter + 1; // running countrollover <= &counter; // assert 'rollover' when counter is all 1'sendend
數字低通濾波器模塊
module box_ave (clk,rstn,sample,raw_data_in,ave_data_out,data_out_valid);parameterADC_WIDTH = 8, // ADC Convertor Bit PrecisionLPF_DEPTH_BITS = 4; // 2^LPF_DEPTH_BITS is decimation rate of averager//input portsinput clk; // sample rate clockinput rstn; // async reset, asserted lowinput sample; // raw_data_in is good on rising edge,input [ADC_WIDTH-1:0] raw_data_in; // raw_data input//output portsoutput [ADC_WIDTH-1:0] ave_data_out; // ave data outputoutput data_out_valid; // ave_data_out is valid, single pulsereg [ADC_WIDTH-1:0] ave_data_out;//**********************************************************************//// Internal Wire & Reg Signals////**********************************************************************reg [ADC_WIDTH+LPF_DEPTH_BITS-1:0] accum; // accumulatorreg [LPF_DEPTH_BITS-1:0] count; // decimation countreg [ADC_WIDTH-1:0] raw_data_d1; // pipeline registerreg sample_d1, sample_d2; // pipeline registersreg result_valid; // accumulator result 'valid'wire accumulate; // sample rising edge detectedwire latch_result; // latch accumulator result//***********************************************************************//// Rising Edge Detection and data alignment pipelines////***********************************************************************always @(posedge clk or negedge rstn)beginif( ~rstn ) beginsample_d1 <= 0;sample_d2 <= 0;raw_data_d1 <= 0;result_valid <= 0;end else beginsample_d1 <= sample; // capture 'sample' inputsample_d2 <= sample_d1; // delay for edge detectionraw_data_d1 <= raw_data_in; // pipelineresult_valid <= latch_result; // pipeline for alignment with resultendendassign accumulate = sample_d1 && !sample_d2; // 'sample' rising_edge detectassign latch_result = accumulate && (count == 0); // latch accum. per decimation count//***********************************************************************//// Accumulator Depth counter////***********************************************************************always @(posedge clk or negedge rstn)beginif( ~rstn ) begincount <= 0;end else beginif (accumulate) count <= count + 1; // incr. count per each sampleendend//***********************************************************************//// Accumulator////***********************************************************************always @(posedge clk or negedge rstn)beginif( ~rstn ) beginaccum <= 0;end else beginif (accumulate)if(count == 0) // reset accumulatoraccum <= raw_data_d1; // prime with first valueelseaccum <= accum + raw_data_d1; // accumulateendend//***********************************************************************//// Latch Result//// ave = (summation of 'n' samples)/'n' is right shift when 'n' is power of two////***********************************************************************always @(posedge clk or negedge rstn)beginif( ~rstn ) beginave_data_out <= 0;end else if (latch_result) begin // at end of decimation period...ave_data_out <= accum >> LPF_DEPTH_BITS; // ... save accumulator/n resultendendassign data_out_valid = result_valid; // output assignmentendmodule
上述的3個文件雖然來自Lattice官網提供,但適用于所有的FPGA,我已經在多個不同的型號上嘗試過,都能工作。
這個功能非常有用,在Lattice的FPGA上只占用了大約50個LUTS,可以以比較低的代價獲得一個ADC的功能。
更多與數字電路相關的FPGA代碼案例可以點擊下面的小程序查看,頁面中的案例需要跳轉到www.stepfpga.com網站,可以通過H5直接訪問: