这是一个双通道数字示波器!
买一个不是更香?为啥要自己DIY?
事情是这样的。
我很想尝试自己DIY一个性能不错的示波器
顺便巩固学习一下——模拟电路设计、示波器知识、verilog程序、单片机程序、各种通信协议、屏幕与菜单等等……好提升自己的综合能力!
正好看到立创·逻辑派G1……那就开搞!

演示一下

下面,将分享项目的系统框图分析、软硬件设计说明、调试说明。同时,文末也会附上成本说明、开源网址。
AD模拟电路包括两个信号输入通道,由两路相同的信号调理电路进行处理,接入AD9288 ADC的双通道中,由FPGA进行读取。DA输出也由FPGA实现DDS产生可控波形输出,通过简单的调理电路输出。
项目采用了异构的主控,ADC、DAC与LCD显示由FPGA处理,充分发挥并行接口与逻辑电路的性能。对于电路的程控以及显示菜单、控件、事件的处理,为避免在FPGA布置大量复杂的处理逻辑,转由MCU进行处理,二者通过SPI协议进行通信。

底板_电源 原理图
底板_ADC模拟电路 原理图
底板_DAC模拟电路 原理图

底板_控制电路 原理图

底板_核心板 原理图

底板_PCB图

底板_PCB 3D图

顶板原理图

顶板PCB图
VDD_3.3V数字电源采用TPS82130 DCDC电路产生,满足较大的电流供给LCD、ADDA芯片等等;
VEE_4.5V为负压电源,用于各个运放的负电源轨供电,采用TPS5430 DCDC降压也是为了满足大电流的需求,若采用一般的电荷泵或LDO难以满足AD603等运放的供电;
VCC_4.5V与VCC_3.3V由低噪声LDO产生,减少电源纹波,用于模拟电源轨供电
模拟前端电路基本借鉴OSCFUN开源示波器的模拟电路,结合详细的设计教程,很适合新手学习示波器知识。
输入使用固态继电器控制交直流耦合,接着信号继电器选择x1 x10两路衰减,用于实现各个电压档位,衰减的同时满足1M高阻输入与阻抗匹配。后接一级跟随器,进入AD603实现的压控增益(VCA)电路,以实现精确的各档位增益控制,不采用模拟开关芯片避免阻抗的影响。放大输出后进入ADA4932差分放大器,作为AD9288的驱动。
3PD5651为电流输出型DAC,输出为差分信号,通过一级差分转单端的放大电路,后接跟随器,使用RC低通滤波滤除高频噪声,也可选择设计LC等更高阶的滤波器。
屏幕与8个按键放到了另一块PCB上,作为顶面面板,使用34pin同向fpc排线连接信号线,并用贴片螺母与螺丝螺柱固定上下PCB,屏幕为4.0寸无触摸LCD,tb链接:https://t.doruo.cn/1Nc3GiYN2 型号为ST7796-IPS显示屏(不带触摸)
底板上采用一片MCP4728产生四通道电压输出,并结合基准源与运放搬移电压,以此产生ADC前端双路的压控放大控制、电压偏置控制。控制电路的运放选择比较宽松,能有效处理直流信号不振荡即可。运放与基准源等的精度无需多虑,可以软件校准。
FPGA程序
AD9288输出的是补码,在每个时钟上升沿读取输出数据并转换为偏移码
wire [7:0] DIN_B = $unsigned(AD9288_DIN_B + 8'd127);always @(posedge clk) beginif (!rst_n) beginAD9288_DOUT_A <= 0;endelse beginAD9288_DOUT_A <= DIN_A;endend
由于实测发现读取的数据存在较大噪声与毛刺,后级接入简单的滑动均值滤波,可根据需要替换为更优的滤波方案
wire [11:0] sum = data_reg[0] + data_reg[1] + data_reg[2] + data_reg[3] + data_reg[4] + data_reg[5] + data_reg[6] + data_reg[7];assign dout = sum >> AVE_DATA_BIT; //右移3 等效为÷8
信号经过一个比较器后生成脉冲,捕获其上下边沿产生触发信号。
wire trig_pulse = trig_edge ? ((ad_data_r >= trig_level) && ((trig_ch ? ad_data_b : ad_data_a) < trig_level)) : ((ad_data_r <= trig_level) && ((trig_ch ? ad_data_b : ad_data_a) > trig_level)) ;
在数字波形存储的状态机中,先往环形ram预先存入一半深度的信号数据,等待触发,触发成功后继续存储直到达指定存储深度。
1:beginif(deci_valid) beginwrite_enable<=1;write_addr<=(write_addr+1'b1)&14'h3FFF;write_data_a<=ad_data_a; write_data_b<=ad_data_b;ad_cnt<=ad_cnt+1;state<=(ad_cnt>=8191)?(state+1'b1):state;endelse beginstate<=state;ad_cnt<=ad_cnt;write_addr<=write_addr;endend2:beginif(auto_trig||trig_flag)beginwrite_enable <= 1;write_addr <= (write_addr+1'b1)&14'h3FFF;trig_pos <= (write_addr+1'b1)&14'h3FFF;ad_cnt <= ad_cnt+1;write_data_a<=ad_data_a; write_data_b<=ad_data_b;state <= state+1;if(trig_mode==2) trig_ok<=1;else trig_ok<=0; endelse beginstate<=state;if(deci_valid) beginwrite_enable<=1;write_addr<=(write_addr+1'b1)&14'h3FFF;ad_cnt<=8191;write_data_a<=ad_data_a; write_data_b<=ad_data_b;end else beginstate<=state;ad_cnt<=ad_cnt;write_addr<=write_addr;endendend
对信号周期脉冲采用常见的等精度测量方案测量频率
在FPGA中布置了一组16位寄存器,长度可根据实际需要修改,编写一个SPI收发模块,用于接收MCU通过SPI总线发送的修改与访问寄存器指令与数据,根据寄存器地址将收到的数据写入寄存器中,单片机读取寄存器数据也同理
RECEIVE_ADDR: beginif (bit_count < 8) beginif(sck_pos)begin// 接收地址的每一位address[7-bit_count] <= MOSI;bit_count <= bit_count + 1;endendelse begin// 根据WR信号选择操作if (WR) begin // WR高电平为写操作current_state <= WRITE_DATA;bit_count <= 0;endelse begin // WR低电平为读操作current_state <= SEND_DATA;if(address < 8'd16) data_to_read <= regs[address[4:0]]; // 使用地址的低5位else if(address == 8'd32) data_to_read <= key_reg;bit_count <= 0;endendend
各个寄存器的值将会经过译码,生成各路控制线与数据线,控制全局模块的各个功能
/****耦合方式****/assign acdc = regs[0][0];assign dis_acdc_buff = acdc?"AC":"DC";/****触发边沿****/assign trig_edge = regs[1][2];assign dis_trig_edge_buff = trig_edge?"NEG":"POS";/****触发模式****/assign trig_mode = regs[1][1:0];assign dis_trig_mode_buff = (trig_mode==2'b0)?"AUTO ":((trig_mode==2'b01)?"NORMAL":"SINGLE");/****触发电平****/assign trig_level = regs[6][7:0];wire [3:0] trig_level_unit;wire [3:0] trig_level_ten;wire [3:0] trig_level_hun;wire [8:0] trig_level_1=trig_level>>2;bcd_8421 bcd_8421_1(.sys_clk(clk),.sys_rst_n(rst_n),.data(trig_level_1),.unit(trig_level_unit),.ten(trig_level_ten),.hun(trig_level_hun));assign dis_trig_level_buff = {4'd0,trig_level_hun,4'd0,trig_level_ten,4'd0,trig_level_unit};
寄存器表定义:
屏幕的显示逻辑较为复杂,使用了一个庞大的状态机,主状态机为整个刷新周期的循环。里面又分成一个个小状态机例如屏幕初始化、波形绘制、字符显示等等,他们又会调用更底层的画点、刷屏状态机,用高级编程语言的角度来看就是一个个函数,只不过放在verilog里面需要用状态跳转来实现复杂的逻辑。
SCAN:begincase(cnt_scan)5'd0: begin ram_addr<=trig_pos_minus_step;; cnt_scan <= cnt_scan + 1'b1; end // 使用预计算地址5'd1: begin ram_en <= HIGH; cnt_scan <= cnt_scan + 1'b1;end5'd2: begin ram_addr<=ram_addr_next; cnt_scan <= cnt_scan + 1'b1; end// RAM时钟使能5'd3: beginram_data_r1 <= (255-ram_data_a);ram_data_r2 <= (255-ram_data_b);cnt_scan <= cnt_scan + 1'b1;end5'd4: beginif(y_cnt == 256) beginy_cnt <= 0;if(x_cnt==400) begin x_cnt<=0; cnt_scan<=cnt_scan+1'b1; end// 如果是最后一行就跳出循环else beginx_cnt <= x_cnt + 1'b1;cnt_scan <= 5'd2; // 提早返回到地址更新,形成流水线endend else beginif(ram_data_r1==y_cnt)begincurrent_y1 <= ram_data_r1;prev_y1 <= current_y1;endif(ram_data_r2==y_cnt)begincurrent_y2 <= ram_data_r2;prev_y2 <= current_y2;endy_cnt <= y_cnt + 1'b1;y <= y_cnt;x <= ((ram_data_r1==y_cnt || ram_data_r2==y_cnt) && (x_cnt!=400))?x_cnt+1:x_cnt;if(trig_flag) color <= ORANGE;else if((ram_data_r1==y_cnt)||line_flag1) color <= YELLOW;else if((ram_data_r2==y_cnt)||line_flag2) color <= BLUE;else if(cor_flag) color <= WHITE;else if(cor_flag1) color <= GREY;else color <= color_b;state <= POINT; // 跳转至WRITE状态state_backback <= SCAN; // 执行完WRITE及DELAY操作后返回SCAN状态endend5'd5: begin cnt_scan <= 0; state <= MAIN; ram_en <= LOW; enddefault: state <= IDLE;endcaseend
UI界面设计:
DDS调用了高云的DDS_II IP核,其实这个IP核和XILINX的DDS IP核一模一样,熟悉XILINX的人马上就能直接使用起来。DDS根据32位频率控制字会产生指定频率的正弦波,将其通过一个比较器就得到了方波。同时dds还支持输出相位字,其波形是一个锯齿波,经过整流后可以生成三角波,这就形成了四个常见波形。
reg signed [9:0] square_wave; // 方波输出// 方波生成always @(posedge clk) beginif (dds_phase[31:16] < 16'h8000) // 比较相位累加器的高 16 位square_wave <= 10'd511; // 高电平(满量程)elsesquare_wave <= -10'd512; // 低电平(满量程)end/*****************************************************************************/wire signed [9:0] dds_phase_2 = dds_phase>>>22;reg signed [9:0] triangle_wave; // 10 位三角波输出// 三角波生成always @(posedge clk) beginif (dds_phase_2 >= 0) begintriangle_wave <= (dds_phase_2 - 12'd256)<<<1; // 映射到 -512 到 +511end else begintriangle_wave <= (12'd768 - dds_phase_2)<<<1; // 映射到 -512 到 +511end
使用移位加法可以控制输出的波形的幅度衰竭
reg signed [9:0] temp_add1, temp_add2;always @(posedge clk or negedge rst_n) beginif (!rst_n) begintemp_add1 <= 10'd0;temp_add2 <= 10'd0;end else begintemp_add1 <= (wave_reg >>> 1) + (wave_reg >>> 2);temp_add2 <= (wave_reg >>> 3);endendalways @(posedge clk or negedge rst_n) beginif (!rst_n)DAC_out_r <= 10'd0;elsecase (attenuation_sel)8'd0: DAC_out_r <= wave_reg;8'd1: DAC_out_r <= temp_add1 + temp_add2;8'd2: DAC_out_r <= temp_add1;8'd3: DAC_out_r <= (wave_reg >>> 1) + (wave_reg >>> 3);8'd4: DAC_out_r <= wave_reg >>> 1;8'd5: DAC_out_r <= (wave_reg >>> 2) + (wave_reg >>> 3);8'd6: DAC_out_r <= wave_reg >>> 2;8'd7: DAC_out_r <= wave_reg >>> 3;default: DAC_out_r <= wave_reg;endcaseend
MCU程序
使用GD32的硬件SPI读写FPGA的内部寄存器,可以尽可能减少响应时间。
// 写寄存器函数void SPI_WriteRegister(uint8_t address, uint16_t data) {// 组合发送数据:地址 + 高8位 + 低8位uint8_t tx_buf[3] = {address, (data >> 8) & 0xFF, data & 0xFF};gpio_bit_reset(SPI_GPIO_PORT, SPI_PIN_CS);gpio_bit_set(SPI_GPIO_PORT, SPI_PIN_WR);// 设置WR为写模式(高电平)for (int i = 0; i < 3; i++) {while (spi_i2s_flag_get(SPI_INSTANCE, SPI_FLAG_TBE) == RESET);spi_i2s_data_transmit(SPI_INSTANCE, tx_buf[i]);while (spi_i2s_flag_get(SPI_INSTANCE, SPI_STAT_RBNE) == RESET);spi_i2s_data_receive(SPI_INSTANCE);}gpio_bit_set(SPI_GPIO_PORT, SPI_PIN_CS);gpio_bit_reset(SPI_GPIO_PORT, SPI_PIN_WR);}// 读寄存器函数uint16_t SPI_ReadRegister(uint8_t address) {uint8_t tx_buf[3] = {address, 0xFF, 0xFF}; // 发送地址和填充字节uint8_t rx_buf[3];gpio_bit_reset(SPI_GPIO_PORT, SPI_PIN_CS);// 设置WR为读模式(低电平)gpio_bit_reset(SPI_GPIO_PORT, SPI_PIN_WR);for (int i = 0; i < 3; i++) {while (spi_i2s_flag_get(SPI_INSTANCE, SPI_FLAG_TBE) == RESET);spi_i2s_data_transmit(SPI_INSTANCE, tx_buf[i]);while (spi_i2s_flag_get(SPI_INSTANCE, SPI_STAT_RBNE) == RESET);rx_buf[i] = spi_i2s_data_receive(SPI_INSTANCE);}gpio_bit_set(SPI_GPIO_PORT, SPI_PIN_CS);// 组合返回的16位数据(高8位+低8位)return ((uint16_t)rx_buf[1] << 8) | rx_buf[2];}
由于逻辑派开发板设计,GD32引脚不足,所以编码器与各个按键连接到了fpga的引脚,然后在定时器中断处理中,通过上述的SPI接口读取FPGA存储按键电平的寄存器,并进行消抖、双击长按识别。
// 定时器中断服务程序void TIMER2_IRQHandler(void) {static bool A1=1,B1=1,A1_r=1,B1_r=1;static bool A2=1,B2=1,A2_r=1,B2_r=1;if (RESET != timer_interrupt_flag_get(TIMER2, TIMER_INT_FLAG_UP)) {timer_interrupt_flag_clear(TIMER2, TIMER_INT_FLAG_UP);uint16_t key_state = SPI_ReadRegister(32);A1 = (key_state&0x0400)==0x0400; B1 = (key_state&0x0800)==0x0800;A2 = (key_state&0x1000)==0x1000; B2 = (key_state&0x2000)==0x2000;if(!A1 && A1_r && B1) EC11_flag1 = 2;//反传A1_r = A1;if(!B1 && B1_r && A1) EC11_flag1 = 1;//正转B1_r = B1;if(!A2 && A2_r && B2) EC11_flag2 = 2;A2_r = A2;if(!B2 && B2_r && A2) EC11_flag2 = 1;B2_r = B2;for(uint8_t i = 0;i < 10;i++){key[i].key_status = (key_state >> i) & 0x1;//此处是判断按键是否稳定switch(key[i].click_status){case 0://状态0:第一次按下if(key[i].key_status == RESET){key[i].click_status = 1;//跳转状态1}break;case 1://状态1:电平已稳定if(key[i].key_status == RESET){key[i].click_status = 2;key[i].click_time = 0; //计时器清零,准备调用}else{key[i].click_status = 0;}break;.................................................
按键功能:
8个顶板按键:耦合方式、活跃通道切换、打开/关闭(通道)、光标模式、自动触发、正常触发、单次触发、暂停/启动
编码器1:切换控件、切换DAC选择位数
编码器2:选项切换、数值增减
编码器1按键:进入与退出
编码器2按键:切换粗调细调
将每个可操控的控件都定义一个任务函数,main主循环将依次访问各控件任务,执行事件响应
int main(void){systick_config();usart_init();printf("helloworld\n");bsp_spi_init();timer2_init();I2C_Init();ctrl_init();for(int i=0;i<17;i++){update_reg(i);}update_reg(20);update_ch_gear();update_reg(6);while(1){object_task();}}void TRIG_CH_task(){if(EC11_flag1 == 2){EC11_flag1=0;object++; update_reg(14);}else if(EC11_flag1 == 1){EC11_flag1=0;object=11; update_reg(14);}else if(EC11_flag2 == 1){EC11_flag2=0;trig_ch=!trig_ch;update_reg(1);}else if(EC11_flag2 == 2){EC11_flag2=0;trig_ch=!trig_ch;update_reg(1);}else if(key[8].signed_flag==1){key[8].signed_flag=0;}else if(key[9].signed_flag==1){key[9].signed_flag=0;}}
1.注意原理图标注的部分,有些地方是不用焊接的。
2.上下板直接要使用螺柱与贴片螺母连接,需要自备合适高度的螺柱、螺丝等,实测上下板需要有25mm的高度。
3.编码器在3D图中是直插的,实际上需要使用侧插的编码器。
4.原理图BOM不一定准确,建议根据器件型号和值来选。
5.上下板连接的排线选择34P-0.5mm-同向,如果买的是5cm或者更长,需要卷曲一下不然会翘出去。
6.各个电压量程的实现需要同时进行继电器控制衰竭档位切换与改变MCP4728输出AD603增益控制电压,校准信息在GD32的程序
typedef struct{uint16_t dac_poff;//通道增益控制字uint16_t vertical_offset;//通道垂直基线偏移bool gain_sel;//衰减选择}Calibration;//对应10个档位5 10 20 50 100 200 500 1000 2000 5000 mVCalibration ch1_cali[10]={{600,2330,1}, //5mV{900,2435,1}, //10mV{1300,2485,1},//20mV{1850,2520,1},//50mV{2270,2530,1},//100mV{2675,2530,1},//200mV{1425,2530,0},//500mV{1840,2535,0},//1V{2270,2540,0},//2V{2740,2535,0},//5V};
校准方法:
信号源输出直流0V,改变第二个参数即电压偏移,使0V的波形直线归零到正中央,偏移电压即可确定。
再让信号源输出一定的电压,例如1V档位时输入3V电压,改变第一个参数即放大增益,时波形直线落在3V的坐标线上,那么增益也确定下来。
MCP4728的控制字范围是0-4095。第三个参数是继电器控制x1 x10档位的,一般情况不用改。
7.板上有四个可调电容,用于调节示波器通道的阻抗匹配,当出现同一增益下直流量与交流量放大倍数不一致的时候,可以尝试调节电容,这将会改变交流信号的幅度与相位;或者当输入的方波失真,可进行调节电容校准。
8.由于设计思路改变以及代码的一些时序问题,部分预期的功能并没有实际实现,例如cursor测量功能、峰峰值测量、通道开关等等,但是也留了一些占位槽。
9.时序报告与资源用量:

逻辑派开发板138+AD9288芯片12+3PD5651芯片6+AD603芯片4.5x2+ADA4932芯片12x2+tps82130芯片6.5+AD8065芯片6x2+继电器共10+MCP4728芯片8.6+BNC接头3.5x3+4.0寸LCD屏幕37+剩余的各种阻容芯片和接线座50。
粗略计算的最终成本在300以上
可根据自己需求降本增效
本项目已开源!
——想复刻?想给作者点赞?可复制开源网址 前往原文。
https://oshwhub.com/greentor/logicpi-dual-channel-digital-osc