双通道示波器项目
一、项目简介
1、项目参与人员:
- 杜晓雷
- 李雨轩
- 高浩卓
- 邬超
2、项目需求
- 双通道数据采样
- 模拟信号0-10MHz
- 最高100Msps采样率
- 输入信号幅度:10mVpp ~ 20Vpp
- 通过SPI送到树莓派进行数据处理
- 留出CSI MIPI接口
二、硬件设计
PCB项目主要为硬件,需要收取信号,将其进行衰减,处理,放大后经由模数转换器输出到树莓派进行显示,完成示波器的作用
1、项目设计流程
(1) 项目整体方案
(2) 关键器件
- ADM8660 adm660_8660.pdf
- LT3032 lt3032.pdf
- ADA4940 ada4940-1_4940-2.pdf
- MAX19516max19516-1078790.pdf
- Lattice 1200HC FPGA machxo2familydatasheet-948089.pdf
- ADA4817 ada4817-1_4817-2.pdf
- ADG658 adg658_659.pdf
(3) 电路设计和PCB
- 原理图: raspberry_os终极版.pdf
(4) 心得
在器件选择方面,需要参考电路各部分以及器件的特性(如额定电流,封装等),才能找到最适合的元件。参阅数据手册在了解器件特性这一方面是最为有效的方法。因此在选定元件的时候,应当大量参阅不同元件的数据手册,从而从中择取适合的元件。同时设计时一定要预留测试点,方便日后焊接电路时进行测量调试
2、项目调试
(1) 功能调试
首先构筑由AD8660和LT3032组成的电源模块,通过测试点验证5v输入接入后经由AD8660可以反向出-5v电压,经由LT3032后可输出正负3.3v电压。
其次焊接FPGA,JTAG以及相关元件,通过JTAG烧入程序来验证FPGA是否工作正常
之后焊接一路输入上的元件,确保一路输入元件没有缺少之后,接入电源以及函数发生器。通过函数发生器产生波形输入。使用示波器进行测试,确保一路输入的功能没有异常,验证电路合理性
最后焊接剩下的一路输入,以同上方法进行测试确保功能正常。至此PCB电路组装完毕
(2) 问题和解决方法
问题1: 由于没有仔细核对数据手册与数据库的封装,Maxim 19516 模数转换器封装尺寸发生错误。 解决方法:将PCB封装图重新进行了设计,重新打板
问题2:焊接完毕电源电路(主要由AD8660与LT3032组成)后,经过测试+3.3v输出产生问题,输出电压大约为+2.4v
解决方法:首先重新焊接电路,未成功。增加了负载之后正电压输出恢复正常值
(3) 遗留问题
(4) 调试总结
调试过程中遭遇了一些问题。其中最为严重的是导致了需要重新打板的芯片封装问题。由此可见数据手册的重要性相当高,需要给予相当的重视。由电源问题也可知有时需要多思考才能发现问题的根源,也可以避免调试过程中绕弯路。
3、PCB项目总结
本次在PCB制作过程中,组员齐心协力,共同积极参与了从PCB的规划,设计,到最终成果的所有过程。这大幅提高了设计的效率,并降低了错误率。即使如此,在设计中仍然出现了相当大的挑战以及几处错误,其中尤为显著的问题是一处芯片封装发生错误。错误来源于没有将数据库中的封装具体尺寸与数据手册进行认真对比,由此产生了这项问题。从中可以得出必须认真检查从规划,设计到最终成果的每一个步骤,才能有效避免错误的发生。
三、FPGA逻辑
FPGA 主要的目的在于从软件层面上抓取由硬件的模数转换器送来的数据,进行处理,从而送到树莓派进行显示
1、硬件连接
具体连接可参照上文原理图
交直流耦合(CH1对应PT12C,CH2对应PR9D接口,负责选择输入信号为交流或直流)
晶体振荡器 (PL3A接口,负责提供时钟)
选择器(CH1对应PT12A,PT12B, CH2对应PT10C,PT10D接口,FPGA 向选择器发送信号选择衰减倍数)
模数转换器 (接受数字信号进行处理)
树莓派(SPI接口,发送数据至此进行显示)
2、工作原理
数据传输到FPGA处理后再送入树莓派显示
选择器控制衰减倍数
不同幅值的信号需要不同的衰减倍数确保能够送入后续电路进行处理。衰减倍数的控制是通过选择器进行的。FPGA会输出相应的数值进行选择。
- 小于1Vpp(±0.5V)输出'00',衰减1倍(即不衰减);
- 在1Vpp(±0.5V)和10Vpp(±5V)之间的输出'01',衰减10倍;
- 超过10Vpp(±5V)输出'10',衰减100倍
FPGA控制交直流耦合
由于示波器需要测量交流与直流两种信号,需要在输入处进行信号类型的选择。通过树莓派由SPI传输至FPGA (例:用if语句进行判断,输入1切换为交流,输入2切换为直流)
3、代码设计
(1) 模块划分
(2) 关键模块实现
顶层模块的代码:
module osc_top( input ss,sck,sdin,clkin, //4线SPI、FPGA输入时钟、以及复位键 input DCLKA, DCLKB, //ADC采样时钟输入 //input DORA,DORB, //溢出标志 output clk, //输出的时钟 input [7:0] CH1, //CH1和CH2数据输入 input [7:0] CH2, output A0_1, //衰减倍数控制位 output A1_1, output A0_2, output A1_2, output reg DC_CH1, //AC/DC控制 output reg DC_CH2, output sdout, //SPI数据输出 output Full_CH1, //fifo1写满标志,传给树莓派 output Full_CH2 //fifo2写满标志,传给树莓派 ); wire clk_100MHz; wire rstb = 1'b1; //系统复位 wire [7:0] rdata; /********************衰减倍数选择***********************/ /*****************注意:shuaijian_level=01不衰减;00衰减十倍;11衰减100倍*******************/ reg [1:0] shuaijian_level; always @ (posedge clk_100MHz or negedge rstb) begin if (!rstb) shuaijian_level <= 2'b00; else if (rdata >= 8'd61 && rdata <= 8'd65) shuaijian_level <= 2'b01; else if (rdata >= 8'd66 && rdata <= 8'd68) shuaijian_level <= 2'b00; else if (rdata >= 8'd69 && rdata <= 8'd72) shuaijian_level <= 2'b11; else shuaijian_level <= shuaijian_level; end assign A0_1 = ~shuaijian_level[0]; assign A1_1 = shuaijian_level[1]; assign A0_2 = ~shuaijian_level[0]; assign A1_2 = shuaijian_level[1]; /********************耦合方式控制***********************/ always@(posedge done or negedge rstb) begin if(!rstb) begin DC_CH1 <= 1'b0; DC_CH2 <= 1'b0; end else if(rdata == 8'd24) begin DC_CH1 <= ~DC_CH1; end else if(rdata == 8'd25) begin DC_CH2 <= ~DC_CH2; end else begin DC_CH1 <= DC_CH1; DC_CH2 <= DC_CH2; end end /********************SPI通信,读数据和写数据***********************/ wire done; wire [7:0] tdata; wire [7:0] tdata_ch1; wire [7:0] tdata_ch2; reg channel_select; always @ (posedge clk_100MHz) begin if (rdata == 8'd31) channel_select = 1'b0; else if (rdata == 8'd32) channel_select = 1'b1; else channel_select = channel_select; end assign tdata = (channel_select)?tdata_ch2:tdata_ch1; spi_slave U_spi_slave ( .rstb(rstb), .ss(ss), .sck(sck), .sdin(sdin), .sdout(sdout), .done(done), .rdata(rdata), .tdata(tdata) ); /********************波形数据写入FIFO***********************/ wire Empty_CH1; reg WrEn_CH1; reg RdEn_CH1; wire Empty_CH2; reg WrEn_CH2; reg RdEn_CH2; reg [7:0] display_a; always@ (posedge DCLKA) //将过来的数丢进寄存器中 begin display_a <= CH1; end reg [7:0] data_in_last_ch1; always @ (posedge DCLKA) begin data_in_last_ch1 <= display_a; end //连续多次上升 reg [4:0] up_or_down_flag_ch1; always @ (posedge DCLKA) begin if (display_a < data_in_last_ch1) up_or_down_flag_ch1 <= {up_or_down_flag_ch1[3:0],1'b0}; else up_or_down_flag_ch1 <= {up_or_down_flag_ch1[3:0],1'b1}; end reg [7:0] trigger_REF = 8'h00; always @ (posedge done or negedge rstb) begin if (!rstb) trigger_REF <= 8'h00; else if (rdata == 8'd81) trigger_REF <= trigger_REF + 3'd5; else if (rdata == 8'd82) trigger_REF <= trigger_REF - 3'd5; else trigger_REF <= trigger_REF; end /* reg [7:0] trigger_REF; always @ (*) begin if (!rstb) trigger_REF = 8'h00; else if (rdata == 8'd81) trigger_REF = trigger_REF + 3'd5; else if (rdata == 8'd82) trigger_REF = trigger_REF - 3'd5; else trigger_REF = trigger_REF; end */ always @ (*) begin //fifo1写使能 if(!rstb) WrEn_CH1 = 1'b0; else if (Full_CH1) //如果满了,就不再写 WrEn_CH1 = 1'b0; else if (Empty_CH1 && (up_or_down_flag_ch1==5'b11111) && (data_in_last_ch1==trigger_REF)) WrEn_CH1 = 1'b1; else WrEn_CH1 = WrEn_CH1; end always @ (*) begin //fifo1读使能 if(!rstb) RdEn_CH1 = 1'b0; else if (Full_CH1 && (rdata==8'd31)) //数据满了,并且收到spi发来信号,就开始读 RdEn_CH1 = 1'b1; else if (Empty_CH1) //开启后一直读知道读空 RdEn_CH1 = 1'b0; else RdEn_CH1 = RdEn_CH1; end reg [7:0] display_b; always@ (posedge DCLKB) begin display_b <= CH2; end reg [7:0] data_in_last_ch2; always @ (posedge DCLKB) begin data_in_last_ch2 <= display_b; end reg [2:0] up_or_down_flag_ch2; always @ (posedge DCLKB) begin if (display_b < data_in_last_ch2) up_or_down_flag_ch2 <= {up_or_down_flag_ch2[1:0],1'b0}; else up_or_down_flag_ch2 <= {up_or_down_flag_ch2[1:0],1'b1}; end always @ (*) begin if(!rstb) WrEn_CH2 = 1'b0; else if (Full_CH2) WrEn_CH2 = 1'b0; else if (Empty_CH2 && (up_or_down_flag_ch2==3'b111) && (data_in_last_ch2 == trigger_REF)) WrEn_CH2 = 1'b1; else WrEn_CH2 = WrEn_CH2; end always @ (*) begin if(!rstb) RdEn_CH2 = 1'b0; else if (Full_CH2 && (rdata == 8'd32)) RdEn_CH2 = 1'b1; else if (Empty_CH2) RdEn_CH2 = 1'b0; else RdEn_CH2 = RdEn_CH2; end myFIFO U_fifo_ch1 ( .Data(display_a), .WrClock(DCLKA), .RdClock(done), .WrEn(WrEn_CH1), .RdEn(RdEn_CH1), .Reset(~rstb), .RPReset(~rstb), .Q(tdata_ch1), .Empty(Empty_CH1), .Full(Full_CH1) ); myFIFO U_fifo_ch2 ( .Data(display_b), .WrClock(DCLKB), .RdClock(done), .WrEn(WrEn_CH2), .RdEn(RdEn_CH2), .Reset(~rstb), .RPReset(~rstb), .Q(tdata_ch2), .Empty(Empty_CH2), .Full(Full_CH2) ); /********************100MHz,由PLL倍频产生***********************/ PLL U_PLL ( .CLKI(clkin), .CLKOP(clk_100MHz) ); /**************ADC采样时钟,cnt_set是指每隔多少个点采一次*************/ reg [17:0] cnt_set = 18'b1; divide U_divide (.rst_n(rstb), .clk(clk_100MHz), .clkout(clk), .N(cnt_set) ); always@(*) //通过spi发来的命令控制cnt begin if(!rstb) begin cnt_set = 18'b1; end else begin case(rdata) 8'd1: begin cnt_set = 18'd1; end 8'd2: begin cnt_set = 18'd1; end 8'd3: begin cnt_set = 18'd1; end 8'd4: begin cnt_set = 18'd1; end 8'd5: begin cnt_set = 18'd1; end 8'd6: begin cnt_set = 18'd1; end 8'd7: begin cnt_set = 18'd3; end 8'd8: begin cnt_set = 18'd5; end 8'd9: begin cnt_set = 18'd10; end 8'd10:begin cnt_set = 18'd24; end 8'd11:begin cnt_set = 18'd47; end 8'd12:begin cnt_set = 18'd94; end 8'd13:begin cnt_set = 18'd235; end 8'd14:begin cnt_set = 18'd469; end 8'd15:begin cnt_set = 18'd938; end 8'd16:begin cnt_set = 18'd2344; end 8'd17:begin cnt_set = 18'd4688; end 8'd18:begin cnt_set = 18'd9375; end 8'd19:begin cnt_set = 18'd23438; end 8'd20:begin cnt_set = 18'd46875; end 8'd21:begin cnt_set = 18'd93750; end 8'd22:begin cnt_set = 18'd234375; end default:begin cnt_set = cnt_set; end endcase end end endmodule
分频器的代码
module divide(rst_n,clk,clkout,N); input rst_n; input clk; input [17:0] N; output clkout; //定义输入输出 parameter width = 18; reg [width-1:0] cnt_p,cnt_n; reg clk_p,clk_n; always @(posedge clk or negedge rst_n ) begin if(!rst_n) cnt_p<=0; else if(cnt_p==(N-1)) cnt_p<=0; else cnt_p<=cnt_p+1'b1; end always @(posedge clk or negedge rst_n ) begin if(!rst_n) clk_p<=0; else if(cnt_p<(N>>1)) clk_p<=0; else clk_p<=1; end always @(negedge clk or negedge rst_n ) begin if(!rst_n) cnt_n<=0; else if(cnt_n==(N-1)) cnt_n<=0; else cnt_n<=cnt_n+1'b1; end always @(negedge clk ) begin if(!rst_n) clk_n<=0; else if(cnt_n<(N>>1)) clk_n<=0; else clk_n<=1; end assign clkout = (N==1)?clk:(N[0])?(clk_n&clk_p):clk_p; endmodule
SPI模块
此款PFGA里的EFB中有SPI的IP核,但用了WISHBONE,接口较繁琐,故舍弃这种方案。
FIFO模块
module spi_slave (rstb,ss,sck,sdin, sdout,done,rdata,tdata); input rstb,ss,sck,sdin; input [7:0] tdata; output sdout; //slave out master in output reg done; output reg [7:0] rdata; reg [7:0] treg,rreg; reg [3:0] nb; wire sout; assign sout=treg[7]; assign sdout=(!ss)?sout:1'bz; //if 1=> send data else TRI-STATE sdout //read from sdin always @(posedge sck or negedge rstb) begin if (rstb==0) begin rreg = 8'h00; rdata = 8'h00; done = 1'b0; nb = 4'd0; end else if (!ss) begin //MSB first, in@lsb -> left shift rreg ={rreg[6:0],sdin}; //increment bit count nb=nb+1'b1; if(nb!=4'd8) done=1'b0; else begin rdata=rreg; done=1'b1; nb=4'd0; end end //if(!ss)_END if(nb==8) end //send to sdout always @(negedge sck or negedge rstb) begin if (rstb==1'b0) begin treg = 8'hFF; end else begin if(!ss) begin if(nb==4'd0) treg=tdata; else begin //MSB first, out=msb -> left shift treg = {treg[6:0],1'b1}; end end //!ss end //rstb end //always
(3) 关键知识点/难点
此次示波器项目,需要对FPGA进行大量编程,FPGA本身涉及例如FIFO存储等未有接触过的内容,同时程序逻辑亦十分严密,需要厘清思路方可写出正确的逻辑。下图为其中一项内容FIFO之逻辑
同时,为了能够将最终波形显示于树莓派,需编写图形界面,此亦为以往未有之挑战。图形界面编写上,用到了QT软件(C++语言),组内成员均无使用经验,因此图形界面的编写也为一大挑战
4、资源报告
Design Summary
Number of registers: 134 out of 1520 (9%) PFU registers: 134 out of 1280 (10%) PIO registers: 0 out of 240 (0%) Number of SLICEs: 149 out of 640 (23%) SLICEs as Logic/ROM: 149 out of 640 (23%) SLICEs as RAM: 0 out of 480 (0%) SLICEs as Carry: 62 out of 640 (10%) Number of LUT4s: 291 out of 1280 (23%) Number used as logic LUTs: 167 Number used as distributed RAM: 0 Number used as ripple logic: 124 Number used as shift registers: 0 Number of PIO sites used: 32 + 4(JTAG) out of 80 (45%) Number of block RAMs: 2 out of 7 (29%) Number of GSRs: 1 out of 1 (100%) EFB used : No JTAG used : No Readback used : No Oscillator used : No Startup used : No POR : On Bandgap : On Number of Power Controller: 0 out of 1 (0%) Number of Dynamic Bank Controller (BCINRD): 0 out of 4 (0%) Number of Dynamic Bank Controller (BCLVDSO): 0 out of 1 (0%) Number of DCCA: 0 out of 8 (0%) Number of DCMA: 0 out of 2 (0%) Number of PLLs: 1 out of 1 (100%) Number of DQSDLLs: 0 out of 2 (0%) Number of CLKDIVC: 0 out of 4 (0%) Number of ECLKSYNCA: 0 out of 4 (0%) Number of ECLKBRIDGECS: 0 out of 2 (0%)
四、树莓派端程序
1、Qt图形界面
2、Qt程序模块(用C++编写)
五、成果演示
六、心得体会
- 本次项目是相当大的挑战,不论是硬件还是程序方面都有相当大的困难。
- 首先是硬件上的选定,我们参考了大量的资料与数据才得以选择出合适的元件。例如选择器由于参数原因前后更换了数个
- 选定元件之后绘制电路图,电路图较为复杂,且由于电路图庞大的体量中途亦有不少小错与不规范之处花费了一些时间修正。
- 电路焊接中出现了数个小问题以及一个大问题(封装),导致重新打板,更使人认识到了仔细参阅数据手册的重要性
- 软件部分为这次项目最大的挑战。由于显示的原因不仅需要撰写FPGA程序,亦需要在树莓派创建图形界面,这一点上毫无经验的原因也进行了大量的调研和学习才得以实现。FPGA方面逻辑错综复杂,需要考虑的因素很多,也是一大难处。累积工程量巨大,但最终通过分工也基本完成
- 最终大部分原本预定的功能均已实现,示波器可正常运转,实现双通道测量,改变时间轴/伏值轴量程,调整交直流耦合。
- 本次项目中是我们大量学习到了编程逻辑,电路规划,器件选择等相当多的重要的知识,相信这些知识应当让人受益匪浅,将在未来学习工作中起到重大的帮助作用
七、可改进的不足之处
- 板子上可以加一个RESET按键。
- 选择耦合方式的光耦继电器AQY282S的控制位处应该串一个电阻,而不是直接接到FPGA引脚上。
- 模拟开关ADG658的模拟输入不能超过±3.6V(详见数据手册),导致我们的方案只能测量7.2Vpp以内的波形,未达到20Vpp的要求。改进思路:(1)1倍衰减通道上加一个光耦继电器,控制这一路的通断;(2)进入模拟开关之前先把信号衰减三倍(这样就在7V以内了)。