用STM32F103ZET6+TFTLCD做个桌面小示波器:ADC+TIM+DMA采样与按键调频实战
用STM32F103ZET6打造桌面级示波器从硬件搭建到波形优化全指南在电子工程领域示波器就像工程师的眼睛能够直观展现电信号的动态变化。但对于大多数爱好者和初学者来说专业示波器的价格门槛往往令人望而却步。本文将带你用STM32F103ZET6这款经典MCU配合常见的TFTLCD屏幕和基础外设打造一台功能完备的桌面级示波器。不同于简单的代码堆砌我们将重点探讨硬件架构设计、采样策略优化以及显示效果提升等实战技巧。1. 硬件架构设计与核心元件选型1.1 主控芯片与显示模块的黄金组合STM32F103ZET6作为Cortex-M3内核的经典代表其内置的12位ADC和丰富定时器资源使其成为DIY示波器的理想选择。在实际项目中我们搭配了正点原子精英板配套的320×240分辨率TFTLCD这种组合既保证了性能又控制了成本主频优势72MHz工作频率确保实时处理能力ADC特性1μs转换时间支持最高1MHz采样率内存容量64KB SRAM为数据缓冲提供充足空间显示接口FSMC并行接口实现高速刷屏提示选择LCD时优先考虑带控制器如ILI9341的模块可大幅减轻MCU的图形处理负担。1.2 信号调理电路的关键设计专业示波器前端的模拟电路设计极为复杂但在我们的DIY版本中可以通过简化设计实现基本功能// 简易分压电路参数计算示例 #define R1 10000 // 输入电阻10kΩ #define R2 1000 // 分压电阻1kΩ float voltage_divider(float Vin) { return Vin * (R2/(R1R2)); // 分压比1:11 }实际硬件连接时建议在ADC输入端加入保护电路元件作用推荐参数稳压二极管输入过压保护3.3V钳位电压电阻分压网络信号幅度适配10:1分压比低通滤波器抗混叠滤波截止频率500kHz2. 采样系统核心ADCTIMDMA协同工作2.1 三重奏的精密配合传统轮询采样方式会占用大量CPU资源而我们的方案采用硬件自动化的采样架构TIM定时器产生精确的采样时钟ADC模块按触发信号执行转换DMA控制器自动搬运采样数据到内存void ADC_DMA_Config(void) { DMA_InitTypeDef DMA_InitStructure; ADC_InitTypeDef ADC_InitStructure; // DMA1通道1配置 DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)ADC_ConvertedValue; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize SAMPLING_POINTS; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel1, DMA_InitStructure); DMA_Cmd(DMA1_Channel1, ENABLE); // ADC1配置 ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode ENABLE; ADC_InitStructure.ADC_ContinuousConvMode DISABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_T2_TRGO; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_1Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); ADC_ExternalTrigConvCmd(ADC1, ENABLE); }2.2 采样率动态调整策略通过74HC165扩展IO读取按键状态实时改变TIM的ARR和PSC值来调整采样率void TIM2_Adjust_SamplingRate(uint8_t key_val) { typedef struct { uint16_t arr; uint16_t psc; } TimerConfig; const TimerConfig presets[] { {9,17}, // 400kHz {9,22}, // 313kHz {9,34}, // 206kHz {9,69}, // 103kHz {140,9}, // 51.4kHz {702,9} // 10.2kHz }; if(key_val 6 key_val 11) { TIM_TimeBaseInitTypeDef TIM_BaseStruct; TIM_BaseStruct.TIM_Period presets[key_val-6].arr; TIM_BaseStruct.TIM_Prescaler presets[key_val-6].psc; TIM_BaseStruct.TIM_ClockDivision 0; TIM_BaseStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_BaseStruct); } }3. 波形处理与频率分析技术3.1 时域与频域双重视角对于周期性信号特别是方波输入捕获是最直接准确的测量方式void IC_Measure_PWM(void) { TIM_ICInitTypeDef TIM_ICInitStructure; // 上升沿捕获配置 TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0F; TIM_ICInit(TIM3, TIM_ICInitStructure); // 下降沿捕获配置同一通道 TIM_ICInitStructure.TIM_Channel TIM_Channel_2; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Falling; TIM_PWMIConfig(TIM3, TIM_ICInitStructure); TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1); TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset); TIM_Cmd(TIM3, ENABLE); }对于复杂信号FFT算法能有效提取频域特征# FFT结果后处理伪代码实际工程用STM32 DSP库 def process_fft(fft_output, sampling_rate): n len(fft_output) magnitude np.abs(fft_output[:n//2]) * 2 / n freq_bins np.fft.fftfreq(n, 1/sampling_rate)[:n//2] dominant_freq freq_bins[np.argmax(magnitude)] return dominant_freq, magnitude3.2 测量精度提升技巧采样窗口优化确保采集整数个信号周期均值滤波对多次测量结果取平均插值算法提高频率分辨率触发同步使用硬件触发稳定波形显示4. 显示优化与人机交互设计4.1 流畅波形显示的核心技巧直接清屏会导致明显的闪烁现象我们采用背景板局部刷新策略// 波形背景模板生成 void Generate_Background(void) { uint16_t bg_color RGB(240,240,240); // 浅灰色背景 uint16_t grid_color RGB(200,200,200); // 网格线颜色 // 填充背景色 LCD_Fill(0, 0, LCD_WIDTH-1, LCD_HEIGHT-1, bg_color); // 绘制网格 for(int x0; xLCD_WIDTH; x20) { LCD_DrawLine(x, 0, x, LCD_HEIGHT-1, grid_color); } for(int y0; yLCD_HEIGHT; y20) { LCD_DrawLine(0, y, LCD_WIDTH-1, y, grid_color); } // 保存背景到缓冲区 LCD_ReadBuffer(background_buffer, 0, 0, LCD_WIDTH, LCD_HEIGHT); } // 波形刷新函数 void Refresh_Waveform(uint16_t *wave_data) { // 恢复背景 LCD_WriteBuffer(background_buffer, 0, 0, LCD_WIDTH, LCD_HEIGHT); // 绘制新波形 for(int i1; iSAMPLING_POINTS; i) { LCD_DrawLine(i-1, wave_data[i-1], i, wave_data[i], RGB(0,120,255)); } }4.2 人机交互界面设计利用74HC165扩展的按键实现多功能控制按键编号功能长按功能K1暂停/继续保存当前波形K2时基放大时基最大K3时基缩小时基最小K4触发电平增加自动触发K5触发电平减少单次触发K6切换显示模式系统设置在实现按键扫描时采用状态机模型处理短按和长按typedef enum { KEY_IDLE, KEY_DEBOUNCE, KEY_PRESSED, KEY_LONG_PRESS } KeyState; void Key_Scan(void) { static KeyState state[KEY_NUM] {KEY_IDLE}; static uint32_t press_time[KEY_NUM]; for(int i0; iKEY_NUM; i) { switch(state[i]) { case KEY_IDLE: if(Key_Read(i)) { state[i] KEY_DEBOUNCE; press_time[i] HAL_GetTick(); } break; case KEY_DEBOUNCE: if(HAL_GetTick() - press_time[i] 20) { // 消抖 state[i] Key_Read(i) ? KEY_PRESSED : KEY_IDLE; } break; case KEY_PRESSED: if(!Key_Read(i)) { Key_Handler(i, SHORT_PRESS); state[i] KEY_IDLE; } else if(HAL_GetTick() - press_time[i] 1000) { Key_Handler(i, LONG_PRESS); state[i] KEY_LONG_PRESS; } break; case KEY_LONG_PRESS: if(!Key_Read(i)) { state[i] KEY_IDLE; } break; } } }5. 系统优化与性能提升5.1 内存管理策略由于STM32F103ZET6的RAM有限需要精心设计缓冲区双缓冲机制一个缓冲区用于采集另一个用于显示处理采样点数优化根据显示分辨率选择合适点数通常320点足够数据压缩存储对历史波形采用差分编码压缩#pragma pack(push, 1) typedef struct { uint16_t *raw_buffer; // 原始采样数据 uint16_t *disp_buffer; // 显示处理后的数据 uint32_t write_idx; // 当前写入位置 uint8_t buffer_lock; // 缓冲区锁标志 } WaveBuffer; #pragma pack(pop) void Buffer_Init(WaveBuffer *buf, uint16_t size) { buf-raw_buffer malloc(size * sizeof(uint16_t)); buf-disp_buffer malloc(size * sizeof(uint16_t)); buf-write_idx 0; buf-buffer_lock 0; }5.2 实时性保障措施中断优先级配置TIM触发中断最高优先级DMA传输中断次高优先级按键中断普通优先级关键代码优化使用__asm volatile(nop)精确延时关键函数添加__attribute__((section(.fastcode)))启用FPU加速浮点运算// 在Keil MDK中的分散加载文件示例 LR_IROM1 0x08000000 0x00080000 { // 512KB Flash ER_IROM1 0x08000000 0x00080000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_IRAM1 0x20000000 0x00010000 { // 64KB SRAM .ANY (RW ZI) *.o (FASTCODE, First) } }6. 进阶功能扩展思路6.1 波形存储与回放添加SPI Flash或SD卡存储支持实现波形记录功能void Save_Waveform(uint16_t *data, uint32_t size) { W25QXX_Init(); // 初始化SPI Flash W25QXX_Write((uint8_t*)size, 0, 4); // 先写入数据长度 W25QXX_Write((uint8_t*)data, 4, size*2); // 写入波形数据 } void Load_Waveform(uint16_t *buffer) { uint32_t size; W25QXX_Read((uint8_t*)size, 0, 4); W25QXX_Read((uint8_t*)buffer, 4, size*2); }6.2 网络通信接口通过ESP8266模块增加WiFi功能实现远程监控数据传输协议设计使用自定义二进制协议提高传输效率添加CRC校验确保数据完整性上位机软件对接开发Python客户端接收并分析数据实现波形实时显示和参数测量# Python简易接收端示例 import socket import matplotlib.pyplot as plt def waveform_receiver(): sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((0.0.0.0, 8888)) plt.ion() fig, ax plt.subplots() line, ax.plot([], []) while True: data, _ sock.recvfrom(2048) samples np.frombuffer(data, dtypenp.uint16) line.set_ydata(samples) line.set_xdata(range(len(samples))) ax.relim() ax.autoscale_view() fig.canvas.flush_events()在完成基础版本后尝试将系统时钟超频至128MHzADC采样率提升至2MSPS。这个过程中发现虽然采样率提高了但信号完整性明显下降。通过优化PCB布局缩短ADC输入走线长度并在电源引脚添加0.1μF去耦电容后系统在1.5MSPS下能够稳定工作。这种性能边界的探索过程往往比最终结果更有实践价值。