基于GD32的简易示波器DIY:从硬件设计到软件实现的完整指南

发布时间:2026/8/20 17:37:53
基于GD32的简易示波器DIY:从硬件设计到软件实现的完整指南 大家好我是专注于嵌入式开发的技术博主。最近在做一个GD32简易示波器的项目从原理图设计、PCB绘制、外壳面板制作到程序编写完整地走了一遍。这个过程踩了不少坑也积累了很多实战经验。如果你对嵌入式系统设计、信号采集或DIY硬件感兴趣想亲手打造一个属于自己的示波器那么这篇文章就是为你准备的。本文将详细拆解整个项目的实现过程提供完整的代码、配置文件和设计思路确保你能从零开始复现这个项目。1. 项目背景与核心概念1.1 什么是简易示波器示波器是电子工程师的“眼睛”用于观察电信号的波形、测量电压、频率等参数。商业示波器功能强大但价格昂贵。我们这里要做的“简易示波器”是基于GD32微控制器MCU为核心通过ADC模数转换器采集外部模拟信号再将采集到的数据通过LCD屏幕显示出来的一套嵌入式系统。它虽然无法与专业设备在带宽和精度上媲美但足以用于观察单片机PWM、传感器输出、音频信号等低频应用是学习嵌入式开发和信号处理的绝佳实践项目。1.2 为什么选择GD32GD32是国内兆易创新GigaDevice推出的基于ARM Cortex-M内核的32位通用MCU。它与STM32系列在软件和硬件上高度兼容具有丰富的资源、较高的性价比和稳定的供货。对于这个项目我们需要MCU具备足够快的ADC采样率、足够的RAM来存储波形数据、以及驱动LCD屏幕的能力GD32F103系列或更高性能的型号完全能够胜任。1.3 项目核心组成一个完整的示波器项目不仅仅是写程序它涵盖了硬件和软件的全栈设计硬件电路原理图 PCB负责信号输入调理、MCU最小系统、电源管理、显示接口等。结构设计外壳 面板为电路板提供保护和固定设计用户交互界面如旋钮、按键孔位。嵌入式软件程序核心是ADC采样、数据处理、波形显示、用户界面控制逻辑。接下来我们将从环境准备开始一步步完成这个“麻雀虽小五脏俱全”的项目。2. 环境准备与版本说明在动手之前我们需要搭建好开发环境。请注意以下版本为本文撰写时的常用版本实际操作时请以官网最新版本或项目稳定版本为准。2.1 硬件设计与EDA工具EDA工具立创EDA专业版。它是一款优秀的国产在线电路设计软件集成元件库、原理图绘制、PCB布局、3D预览和下单功能对个人开发者非常友好。我们使用它来完成原理图和PCB设计。MCU型号本文以GD32F103C8T6为例。这是一款LQFP48封装拥有64KB Flash20KB RAM2个12位ADC的芯片性价比极高。显示模块2.4寸或2.8寸TFT LCD屏驱动芯片为ILI9341或ST7789使用SPI或FSMC接口。信号输入设计BNC接口或简易探针接口配合前端调理电路衰减、保护。外壳设计使用Fusion 360或SolidWorks等3D建模软件也可以使用立创EDA的“面板PCB”功能制作亚克力外壳。2.2 软件开发环境IDE/编译器Keil MDK-ARM (uVision)或ARM GCC VSCode。本文示例代码基于Keil MDK因为它与STM32的生态兼容性好资料丰富。开发包需要安装GD32 Device Family Pack (DFP)。在Keil的Pack Installer中搜索“GigaDevice”并安装对应系列的DFP这样才能在新建工程时选择GD32的芯片型号。固件库使用GD32标准外设库或HAL库。标准库更接近寄存器操作易于理解底层HAL库封装程度高移植方便。本文为清晰起见使用标准库进行讲解。程序下载/调试J-Link或GD-Link仿真器。也可以使用串口ISP方式下载。2.3 示例项目结构预览一个典型的工程目录结构如下你可以先建立起来GD32_Scope_Project/ ├── Hardware/ # 硬件设计文件 │ ├── Schematic/ # 立创EDA原理图文件 │ ├── PCB/ # 立创EDA PCB文件 │ └── Panel/ # 外壳面板设计文件如DXF ├── Software/ # 软件源码 │ ├── MDK_Project/ # Keil工程目录 │ │ ├── User/ # 用户应用代码 │ │ ├── GD32F10x_Firmware_Library/ # GD32固件库 │ │ ├── CMSIS/ # Cortex微控制器软件接口标准 │ │ └── GD32_Scope.uvprojx # Keil工程文件 │ └── Docs/ # 软件设计文档 └── README.md # 项目总说明3. 硬件设计原理图与PCB硬件是项目的基石一个稳定可靠的硬件设计能避免很多软件调试阶段的灵异问题。3.1 核心电路GD32最小系统最小系统是MCU能工作的最基本电路包括电源电路使用AMS1117-3.3V LDO将5V输入转换为3.3V为MCU和部分外设供电。注意电源输入输出端要加滤波电容如10uF和0.1uF并联。复位电路简单的RC复位电路10k电阻上拉0.1uF电容到地或使用专用复位芯片。时钟电路外部高速时钟HSE使用8MHz无源晶振配两个20pF负载电容。内部时钟也可用但外部时钟精度更高。启动模式配置通过BOOT0和BOOT1引脚设置启动方式通常BOOT0接地从主Flash启动。调试接口预留标准的SWD接口SWDIO, SWCLK, GND, 3.3V用于连接J-Link/GD-Link下载调试。在立创EDA中绘制这部分原理图时可以从元件库中搜索“GD32F103C8T6”并放置然后根据数据手册Datasheet和参考设计连接上述外围电路。3.2 关键外设电路信号输入与显示信号输入调理电路目的保护MCU的ADC输入引脚防止过高电压损坏提供一定的输入阻抗可能需要进行衰减或放大。简易设计一个电压跟随器运放构成提高输入阻抗后接两个钳位二极管如1N4148到3.3V和GND进行过压保护。再经过一个RC低通滤波器如1k电阻和100pF电容滤除部分高频噪声。接口预留一个BNC接口或两个排针信号、地用于连接探头。[信号输入] - [BNC接口] - [保护二极管] - [电压跟随器] - [RC滤波] - [GD32 ADC引脚]TFT LCD显示接口SPI模式节省IO口但刷新速度较慢。需要连接SCK, MOSI, CS, DC, RESET, BL背光。FSMC模式仅限大容量型号并行接口刷新速度快。需要连接16位数据线D0-D15以及控制线CS, RS, WR, RD等。本文为简化采用SPI模式。背光控制使用一个N-MOS管如AO3400由MCU的PWM引脚控制实现屏幕亮度调节。3.3 PCB布局布线要点完成原理图后进入PCB设计阶段。这是将逻辑连接转化为实体铜箔走线的过程。板框与定位首先根据外壳尺寸或自己设定的尺寸绘制板框。放置好固定孔。模块化布局将电路按功能模块摆放电源部分、MCU及最小系统、信号输入部分、显示接口、调试接口。各模块相对集中。布线优先级电源线最宽主电源路径如5V到LDO3.3V到各芯片走线要宽通常20-30mil以上。信号线分类模拟信号ADC输入线要远离数字信号时钟、SPI线必要时用地线隔离。ADC输入线尽量短。时钟线处理外部晶振尽量靠近MCU的OSC_IN和OSC_OUT引脚走线短而粗包地处理。过孔与铺铜合理使用过孔进行层间切换。最后对顶层和底层进行铺铜覆铜并连接到地网络GND这能提供良好的屏蔽和回流路径。设计规则检查DRC布线完成后一定要运行DRC检查是否有未连接的网络、间距错误、短路等问题。在立创EDA中完成PCB后可以使用其3D预览功能查看效果并生成Gerber文件用于打样。4. 软件设计嵌入式程序编写硬件准备就绪后我们开始编写“灵魂”——嵌入式程序。程序的核心任务是定时采集ADC数据处理并显示在LCD上。4.1 工程搭建与基础驱动首先在Keil中新建一个工程选择设备为“GD32F103C8T6”。将GD32标准外设库的文件添加到工程中。通常需要以下分组CMSIS核心系统文件。Firmware/GPIO, RCC, ADC, TIM, SPI, ...外设驱动库。User用户应用代码。基础驱动包括系统时钟初始化、延时函数、LED/按键调试等。这里先实现一个简单的系统时钟配置将系统时钟设置为72MHz外部8MHz晶振通过PLL倍频。// File: system_gd32f10x.c (部分关键配置) void system_clock_config(void) { // 使能外部高速时钟 rcu_osci_on(RCU_HXTAL); while(SUCCESS ! rcu_osci_stab_wait(RCU_HXTAL)); // 配置AHB、APB2、APB1预分频器 rcu_ahb_clock_config(RCU_AHB_CKSYS_DIV1); rcu_apb2_clock_config(RCU_APB2_CKAHB_DIV1); rcu_apb1_clock_config(RCU_APB1_CKAHB_DIV2); // 配置PLLHXTAL作为源9倍频 - 8MHz * 9 72MHz rcu_pll_config(RCU_PLLSRC_HXTAL, RCU_PLL_MUL9); rcu_osci_on(RCU_PLL); while(SUCCESS ! rcu_osci_stab_wait(RCU_PLL)); // 选择PLL作为系统时钟源 rcu_system_clock_source_config(RCU_CKSYSSRC_PLL); while(0 rcu_system_clock_source_get()); }4.2 ADC采样与DMA传输示波器需要高速、连续地采样。如果使用查询或中断方式读取ADC会占用大量CPU时间且可能丢失数据。最佳方案是使用ADC规则通道 DMA模式。思路配置一个定时器TIM产生固定频率的触发信号TRGO去触发ADC开始转换。ADC转换完成后自动通过DMA将数据搬运到指定的内存数组缓冲区中。这样CPU几乎不干预采样过程。// File: adc_dma.c #define ADC_BUFFER_SIZE 1024 // 采样缓冲区大小 uint16_t adc_buffer[ADC_BUFFER_SIZE]; // DMA目标缓冲区 void adc_dma_config(void) { // 1. 使能时钟 rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_ADC0); rcu_periph_clock_enable(RCU_DMA0); // 2. 配置ADC引脚 (PA0 作为模拟输入) gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_0); // 3. 配置DMA dma_parameter_struct dma_init_struct; dma_deinit(DMA0, DMA_CH0); dma_struct_para_init(dma_init_struct); dma_init_struct.periph_addr (uint32_t)(ADC_RDATA(ADC0)); // ADC数据寄存器地址 dma_init_struct.memory_addr (uint32_t)adc_buffer; // 内存地址 dma_init_struct.direction DMA_PERIPHERAL_TO_MEMORY; // 外设到内存 dma_init_struct.memory_width DMA_MEMORY_WIDTH_16BIT; dma_init_struct.periph_width DMA_PERIPHERAL_WIDTH_16BIT; dma_init_struct.priority DMA_PRIORITY_HIGH; dma_init_struct.number ADC_BUFFER_SIZE; // 传输数据量 dma_init_struct.periph_inc DMA_PERIPH_INCREASE_DISABLE; // 外设地址不递增 dma_init_struct.memory_inc DMA_MEMORY_INCREASE_ENABLE; // 内存地址递增 dma_init_struct.circular_mode DMA_CIRCULAR_MODE_ENABLE; // 循环模式关键 dma_init(DMA0, DMA_CH0, dma_init_struct); dma_channel_enable(DMA0, DMA_CH0); // 4. 配置ADC adc_deinit(ADC0); // ADC时钟分频ADC时钟不能超过14MHz。系统时钟72MHz6分频得12MHz。 rcu_adc_clock_config(RCU_ADCCK_APB2_DIV6); adc_mode_config(ADC_MODE_FREE); // 独立模式 adc_special_function_config(ADC0, ADC_CONTINUOUS_MODE, ENABLE); // 连续转换模式 adc_special_function_config(ADC0, ADC_SCAN_MODE, DISABLE); // 单通道非扫描 adc_resolution_config(ADC0, ADC_RESOLUTION_12B); // 12位分辨率 adc_data_alignment_config(ADC0, ADC_DATAALIGN_RIGHT); // 数据右对齐 adc_channel_length_config(ADC0, ADC_REGULAR_CHANNEL, 1); // 规则通道序列长度为1 // 配置规则通道通道0PA0采样时间55.5个周期 adc_regular_channel_config(ADC0, 0, ADC_CHANNEL_0, ADC_SAMPLETIME_55POINT5); adc_external_trigger_config(ADC0, ADC_REGULAR_CHANNEL, ENABLE); // 使能外部触发 // 选择定时器1的TRGO事件作为触发源 adc_external_trigger_source_config(ADC0, ADC_REGULAR_CHANNEL, ADC_EXTTRIG_REGULAR_T1_TRGO); // 5. 使能DMA请求 adc_dma_mode_enable(ADC0); adc_dma_request_after_last_enable(ADC0); // 可选在最后一次转换后产生DMA请求 // 6. 使能ADC adc_enable(ADC0); delay_ms(1); // 等待ADC稳定 adc_calibration_enable(ADC0); // ADC校准 // 7. 配置定时器1产生触发信号 (例如 10kHz 采样率) // 假设TIM1时钟为72MHz预分频(PSC)设为71自动重载值(ARR)设为99 // 则更新频率 72MHz / ((711)*(991)) 10kHz timer_auto_reload_value_config(TIMER1, 99); timer_prescaler_config(TIMER1, 71, TIMER_PSC_RELOAD_NOW); timer_master_output_trigger_source_select(TIMER1, TIMER_TRI_OUT_SRC_UPDATE); // 更新事件作为TRGO timer_enable(TIMER1); // 8. 开始ADC转换 adc_software_trigger_enable(ADC0, ADC_REGULAR_CHANNEL); }这段代码配置了ADC在定时器触发下以10kHz的速率连续采样PA0引脚电压并通过DMA循环存储到adc_buffer数组中。4.3 TFT LCD驱动与图形显示接下来需要驱动LCD屏幕。首先实现底层的SPI发送和GPIO控制函数然后编写ILI9341的初始化序列和基本画点函数。// File: ili9341.c (部分关键函数) // 假设引脚定义 #define LCD_SPI SPI1 #define LCD_CS_PIN GPIO_PIN_2 #define LCD_CS_PORT GPIOA #define LCD_DC_PIN GPIO_PIN_3 #define LCD_DC_PORT GPIOA #define LCD_RST_PIN GPIO_PIN_4 #define LCD_RST_PORT GPIOA // 写命令 void LCD_Write_Cmd(uint8_t cmd) { LCD_DC_PORT-BCR LCD_DC_PIN; // DC0命令 spi_i2s_data_transmit(SPI1, cmd); while(RESET spi_i2s_flag_get(SPI1, SPI_FLAG_TBE)); } // 写数据 void LCD_Write_Data(uint8_t dat) { LCD_DC_PORT-BOP LCD_DC_PIN; // DC1数据 spi_i2s_data_transmit(SPI1, dat); while(RESET spi_i2s_flag_get(SPI1, SPI_FLAG_TBE)); } // 设置显示窗口 void LCD_SetWindow(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { LCD_Write_Cmd(0x2A); // 列地址设置 LCD_Write_Data(x1 8); LCD_Write_Data(x1 0xFF); LCD_Write_Data(x2 8); LCD_Write_Data(x2 0xFF); LCD_Write_Cmd(0x2B); // 行地址设置 LCD_Write_Data(y1 8); LCD_Write_Data(y1 0xFF); LCD_Write_Data(y2 8); LCD_Write_Data(y2 0xFF); LCD_Write_Cmd(0x2C); // 开始写入GRAM } // 在指定位置画一个点 (颜色为RGB565格式) void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color) { if(x LCD_WIDTH || y LCD_HEIGHT) return; LCD_SetWindow(x, y, x, y); LCD_Write_Data(color 8); LCD_Write_Data(color 0xFF); }有了画点函数就可以实现画线、画矩形、显示字符和波形了。我们需要一个函数将ADC缓冲区中的数据绘制到屏幕上。4.4 波形显示逻辑这是示波器程序的核心显示逻辑。思路是将adc_buffer中的一组数据比如256个点映射到LCD屏幕的Y坐标上然后用线连接起来形成波形。// File: waveform.c #define GRID_WIDTH 10 // 网格每格像素 #define GRID_OFFSET_X 30 // 波形区左侧偏移 #define GRID_OFFSET_Y 20 // 波形区顶部偏移 #define WAVEFORM_HEIGHT 200 // 波形显示区域高度 #define WAVEFORM_WIDTH 240 // 波形显示区域宽度 uint16_t prev_x, prev_y; // 记录上一个点的位置 void Draw_Grid(void) { // 绘制背景网格此处省略具体代码... // 使用浅灰色画横线和竖线 } void Draw_Waveform(uint16_t *buffer, uint16_t size, uint16_t color) { uint16_t i, x, y; uint16_t adc_value; float voltage; prev_x GRID_OFFSET_X; // 假设ADC参考电压为3.3V12位ADC满量程值为4095 prev_y GRID_OFFSET_Y WAVEFORM_HEIGHT - (buffer[0] * WAVEFORM_HEIGHT / 4095); for(i 1; i size; i) { x GRID_OFFSET_X (i * WAVEFORM_WIDTH / size); adc_value buffer[i]; // 将ADC值转换为屏幕Y坐标 (注意屏幕坐标系原点在左上角) y GRID_OFFSET_Y WAVEFORM_HEIGHT - (adc_value * WAVEFORM_HEIGHT / 4095); // 使用 Bresenham 算法画线连接上一个点和当前点 LCD_DrawLine(prev_x, prev_y, x, y, color); prev_x x; prev_y y; } } // 主循环中的调用示例 while(1) { // 1. 清空波形区域 (用黑色矩形覆盖) LCD_Fill(GRID_OFFSET_X, GRID_OFFSET_Y, GRID_OFFSET_XWAVEFORM_WIDTH, GRID_OFFSET_YWAVEFORM_HEIGHT, BLACK); // 2. 重画网格 Draw_Grid(); // 3. 绘制最新的波形数据 (例如取缓冲区前256个点) Draw_Waveform(adc_buffer, 256, GREEN); // 4. 可以添加其他UI如电压档位、时基信息等 // 5. 延时或等待垂直同步控制刷新率 delay_ms(50); // 约20Hz刷新率 }4.5 用户界面与控件一个实用的示波器需要有交互界面来调整参数例如电压档位V/div通过软件缩放ADC值的映射比例来模拟。时基Time/div通过改变ADC的采样频率调整定时器的ARR值来实现。触发Trigger实现边沿触发使波形稳定。可以在adc_buffer中搜索特定的上升沿或下降沿然后从那个点开始显示。这些功能可以通过外接的旋转编码器或按键来调整。程序需要扫描这些输入设备更新相应的参数变量并重绘屏幕上的参数显示区域。5. 结构设计外壳与面板硬件软件联调成功后一个裸露的电路板既不安全也不美观。我们可以为其设计一个外壳。5.1 设计思路测量精确测量PCB板、屏幕、按键/编码器、BNC接口的尺寸和位置。建模使用3D建模软件如Fusion 360创建外壳模型。通常分为上盖和下盖。开孔在模型上为屏幕、接口、按键、散热孔等开出精确的孔位。固定设计支柱和螺丝孔位用于固定PCB板和上下盖。面板可以在上盖内侧贴上打印了刻度线和标识的面板贴纸或者直接在外壳表面进行丝印。5.2 简易方案亚克力外壳对于快速原型使用立创EDA的面板PCB功能或激光切割亚克力板是很好的选择。在立创EDA中可以将外壳设计成多层亚克力板堆叠的结构每层板就是一块“PCB”用镂空和打孔来构成外壳。设计好后可以直接在嘉立创下单选择“面板PCB”材质得到切割好的亚克力板。最后用螺丝和铜柱组装起来。这种方法成本低加工快非常适合DIY项目。6. 系统联调与常见问题将烧录好程序的PCB板、屏幕、外壳组装起来接通电源你就能看到自己制作的示波器了但第一次成功总是伴随着调试。6.1 常见问题与排查思路问题现象可能原因排查思路屏幕白屏或花屏1. 电源电压不足或不稳。2. 复位引脚未正确拉高。3. SPI时序或初始化序列错误。4. 屏幕背光未开启。1. 测量屏幕VCC电压是否为3.3V。2. 检查LCD_RST引脚是否已置高。3. 用逻辑分析仪抓取SPI时序或逐行检查初始化代码是否与屏幕型号匹配。4. 检查背光控制引脚电平。ADC采样值不准或跳动大1. 参考电压不稳。2. 模拟输入引脚受到数字信号干扰。3. 采样时间设置太短。4. 前端调理电路有问题。1. 测量VDDA模拟电源和VSSA模拟地的电压质量最好用LC滤波。2. 检查PCB布局模拟走线是否远离数字走线。3. 适当增加ADC_SAMPLETIME如239.5周期。4. 输入一个稳定的电压如分压得到的1.65V看ADC值是否稳定。波形刷新闪烁严重1. 屏幕刷新率太低。2. 清屏和画图之间没有同步产生撕裂。1. 优化Draw_Waveform函数减少不必要的计算使用DMA传输刷屏。2. 可以采用双缓冲区机制或确保在屏幕消隐期间更新显存。定时器触发ADC不工作1. 定时器时钟未使能。2. 定时器未产生TRGO信号。3. ADC未正确配置为外部触发模式。1. 检查RCU_TIMER1时钟使能。2. 检查timer_master_output_trigger_source_select配置。3. 检查adc_external_trigger_config和adc_external_trigger_source_config。程序下载失败1. 仿真器连接不稳定。2. BOOT模式设置错误。3. 芯片Flash被写保护。1. 检查SWD连线SWDIO, SWCLK, GND, 3.3V。2. 确保BOOT0引脚接地。3. 使用GD32 MCU ISP工具解除保护。6.2 调试技巧善用LED在程序关键节点如进入主循环、ADC完成一次DMA传输控制LED闪烁可以快速判断程序是否跑飞。串口打印将ADC采样值、内部变量通过串口发送到电脑使用串口助手查看是分析数据最直接的方法。逻辑分析仪对于调试SPI、I2C通信时序逻辑分析仪是神器可以直观看到波形和数据。分段测试不要一次性写完所有代码。先测试LCD显示字符再测试ADC读取固定电压最后将两者结合。7. 项目优化与扩展方向基础功能实现后可以考虑以下优化和扩展让你的示波器更强大7.1 性能优化使用FSMC驱动LCD如果MCU支持将SPI驱动改为FSMC并行驱动可以极大提高屏幕刷新率使波形更流畅。优化波形绘制算法目前的Draw_Waveform函数画了无数条短线。可以优化为只绘制变化的部分或使用硬件加速如果MCU有LCD-TFT控制器。增加波形缓冲区使用双缓冲区或多缓冲区一个用于DMA接收一个用于波形显示避免读写冲突。7.2 功能扩展多通道支持利用GD32的多个ADC实现双通道示波器。自动测量在软件中实现频率、周期、峰峰值、均方根值等参数的自动计算与显示。触发模式完善边沿触发、脉宽触发等使波形稳定显示。波形存储与回放将ADC数据存入外部Flash如W25Q128之后可以回放分析。USB虚拟串口将采集到的数据实时上传到电脑利用上位机软件如Python PyQt5进行更复杂的分析和显示。7.3 工程实践建议代码模块化将ADC驱动、LCD驱动、波形处理、用户界面等写成独立的.c/.h文件提高可读性和可移植性。使用RTOS随着功能复杂可以考虑移植FreeRTOS或RT-Thread将采样、显示、按键扫描等任务分离使程序结构更清晰。电源管理考虑加入锂电池充电管理电路和低功耗模式制作成便携设备。文档与注释为你关键的硬件设计选择如为什么用这个阻值和软件算法写好注释方便日后回顾和他人理解。从一颗GD32芯片开始到最终捧在手里的一个能显示波形的小设备这个完整的过程涵盖了嵌入式开发的大部分核心技能电路设计、PCB layout、C语言编程、外设驱动、算法实现和系统调试。它不仅仅是一个示波器更是一个绝佳的嵌入式系统学习平台。希望这篇长文能为你提供清晰的路径和实用的代码助你成功复现并改进这个项目。如果在实践中遇到问题欢迎在社区交流讨论很多时候解决问题的过程比结果更有价值。