手把手教你用STM32F103ZET6的ADC+TIM+DMA三件套,做个能测频率的简易示波器
手把手教你用STM32F103ZET6的ADC+TIM+DMA三件套实现高精度频率测量
在嵌入式开发中,信号采集与分析是调试和验证的重要环节。本文将带你深入探索如何利用STM32F103ZET6的三大核心外设——ADC、TIM和DMA,构建一个既能采集波形又能精确测量频率的实用工具。不同于市面上昂贵的专业设备,这个方案成本低廉但功能强大,特别适合项目调试和学习研究。
1. 硬件架构设计思路
1.1 系统组成框图
整个系统由三个关键模块构成:
- 信号输入调理电路:负责将外部信号调整到MCU可处理的电压范围(0-3.3V)
- STM32F103ZET6核心板:搭载ARM Cortex-M3内核,主频72MHz
- TFTLCD显示模块:320×240分辨率,用于实时波形展示
关键参数对比表:
| 模块 | 主要功能 | 性能指标 |
|---|---|---|
| ADC | 模拟信号数字化 | 12位精度,1μs转换时间 |
| TIM | 精确采样控制 | 16位计数器,最高72MHz |
| DMA | 数据高效传输 | 7通道,支持循环模式 |
1.2 时钟树配置要点
正确的时钟配置是系统稳定运行的基础:
// 系统时钟初始化示例 void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; // 配置HSE振荡器 RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; HAL_RCC_OscConfig(&RCC_OscInitStruct); // 配置时钟总线 RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2); }注意:APB1总线最大频率为36MHz,配置时需确保不超过此限制
2. ADC采样与DMA传输实现
2.1 多通道ADC配置
采用规则组连续采样模式,配合DMA实现无CPU干预的数据搬运:
// ADC初始化代码片段 void ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig = {0}; hadc1.Instance = ADC1; hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE; hadc1.Init.ContinuousConvMode = ENABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 1; HAL_ADC_Init(&hadc1); // 配置采样通道 sConfig.Channel = ADC_CHANNEL_0; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_71CYCLES_5; HAL_ADC_ConfigChannel(&hadc1, &sConfig); // 启动DMA传输 HAL_ADC_Start_DMA(&hadc1, (uint32_t*)&adc_buffer, BUFFER_SIZE); }2.2 双缓冲技术应用
为避免数据竞争,采用乒乓缓冲策略:
- 设置两个等大小的内存区域BufferA和BufferB
- DMA完成BufferA传输后触发中断,自动切换到BufferB
- 主程序处理BufferA数据时,DMA继续填充BufferB
内存管理关键点:
- 缓冲区大小应为2的整数次幂(如1024点)
- 对齐到4字节边界提升DMA效率
- 使用
__attribute__((aligned(4)))确保内存对齐
3. 定时器精确触发机制
3.1 TIM主从模式配置
利用TIM2作为主定时器触发ADC采样:
void TIM2_Config(uint32_t freq) { TIM_HandleTypeDef htim2; uint32_t prescaler = (SystemCoreClock / 1000000) - 1; uint32_t period = (1000000 / freq) - 1; htim2.Instance = TIM2; htim2.Init.Prescaler = prescaler; htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = period; htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_Base_Init(&htim2); // 配置触发输出 TIM_MasterConfigTypeDef sMasterConfig = {0}; sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_ENABLE; HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig); HAL_TIM_Base_Start(&htim2); }3.2 动态频率调整算法
通过按键实时改变采样率:
void Adjust_Sample_Rate(uint8_t key) { static const uint32_t freq_table[] = { 100000, // 100kHz 50000, // 50kHz 20000, // 20kHz 10000, // 10kHz 5000 // 5kHz }; if(key < sizeof(freq_table)/sizeof(freq_table[0])) { uint32_t new_freq = freq_table[key]; TIM2->ARR = (SystemCoreClock / (TIM2->PSC + 1)) / new_freq - 1; } }提示:采样率选择应遵循奈奎斯特准则,至少为信号最高频率的2倍
4. 频率测量核心算法
4.1 过零检测法实现
适用于正弦波等周期性信号:
# 伪代码描述算法流程 def zero_cross_detect(samples): zero_crossings = [] for i in range(1, len(samples)): if (samples[i-1] < mid_value and samples[i] >= mid_value): zero_crossings.append(i) if len(zero_crossings) < 2: return 0 avg_period = (zero_crossings[-1] - zero_crossings[0]) / (len(zero_crossings)-1) return sample_rate / avg_period4.2 FFT频谱分析优化
针对STM32优化的实数FFT实现:
void FFT_Analysis(float* input, float* output, uint16_t size) { arm_rfft_fast_instance_f32 fft_inst; arm_rfft_fast_init_f32(&fft_inst, size); // 执行FFT变换 arm_rfft_fast_f32(&fft_inst, input, output, 0); // 计算幅值 for(uint16_t i=0; i<size/2; i++) { float real = output[2*i]; float imag = output[2*i+1]; output[i] = sqrtf(real*real + imag*imag); } // 寻找峰值频率 uint16_t max_idx = 0; float max_val = 0; arm_max_f32(output, size/2, &max_val, &max_idx); float freq = (float)max_idx * sample_rate / size; }窗函数选择建议:
| 窗类型 | 主瓣宽度 | 旁瓣衰减 | 适用场景 |
|---|---|---|---|
| 矩形窗 | 窄 | 13dB | 瞬态信号 |
| 汉宁窗 | 中等 | 31dB | 一般频谱分析 |
| 平顶窗 | 宽 | 44dB | 幅值精度要求高 |
5. 显示优化与用户体验
5.1 动态波形绘制技巧
采用差异刷新策略提升显示流畅度:
- 记录前一帧波形所有点坐标
- 新帧绘制前,先用背景色擦除旧轨迹
- 仅更新变化超过阈值的像素区域
void Draw_Waveform(uint16_t* samples, uint16_t count) { static uint16_t prev_samples[MAX_SAMPLES]; uint16_t x, y_prev, y_now; for(uint16_t i=0; i<count; i++) { x = i * SCREEN_WIDTH / count; y_prev = SCREEN_HEIGHT - (prev_samples[i] * SCREEN_HEIGHT >> 12); y_now = SCREEN_HEIGHT - (samples[i] * SCREEN_HEIGHT >> 12); // 只重绘变化明显的点 if(abs(y_now - y_prev) > REDRAW_THRESHOLD) { LCD_DrawPixel(x, y_prev, BACKGROUND_COLOR); // 擦除旧点 LCD_DrawPixel(x, y_now, WAVEFORM_COLOR); // 绘制新点 } } memcpy(prev_samples, samples, count*sizeof(uint16_t)); }5.2 界面元素布局方案
推荐界面分区:
- 顶部20%:显示测量参数(频率、Vpp等)
- 中间60%:主波形显示区
- 底部20%:控制按钮和状态指示
在STM32F103上实现时,发现直接使用LTDC控制器驱动LCD比FSMC接口快3倍,但需要额外的外部RAM作为帧缓冲区。经过实测,在320x240 16位色模式下,全屏刷新率可达45fps,完全满足波形显示需求。
