
简介本资源是一套基于STM32单片机实现的高实用性嵌入式信号采集与分析系统面向电子类专业本科生、嵌入式初学者及课程设计/毕设实践者解决模拟信号实时采集、频域分析与音频异常检测等典型工程问题。项目完整复现2014年TI杯电子设计竞赛D题——带啸叫检测与抑制的音频功率放大器涵盖ADC电压电流同步采样、LCD实时示波显示、FFT频谱生成、啸叫识别及数字陷波器抑制等核心功能模块。压缩包含256个文件以78个C源码和90个头文件.c/.h构成主程序逻辑辅以40个汇编启动文件.s、27个Flash配置文件.fcf及Keil工程文件.uvprojx/.uvoptx整体仅1.15MB轻量易部署。已有420人学习下载提供可直接编译运行的完整工程、清晰的模块化代码结构、配套hex固件及Python数据处理脚本助力读者深入理解嵌入式信号处理全流程实现。1. 为什么用 STM32 做电压电流采集LCD 示波比“只看数字”强一个数量级你手头有一块 STM32F103C8T6 最小系统板接了两个分流电阻和运放调理电路能测 0–30V 电压和 0–5A 电流——但串口打印出来的只是V: 12.47V, I: 1.83A这样的静态数值。问题来了当电机启停、LED 灯带突变、电源纹波干扰出现时这些平均值完全掩盖了毫秒级的尖峰、跌落与振荡。而真正需要诊断的恰恰是这些瞬态行为。本项目不是简单读 ADC 再写 LCD而是把 STM32 变成一台嵌入式示波器用片内 ADC 高速采样≥100kS/sDMA 自动搬运数据FSMC 或 SPI 驱动 40pin LCD 屏如 ILI9341在 320×240 像素上实时绘制双通道波形电压电流支持触发、缩放、冻结、坐标网格——所有逻辑运行在裸机或 FreeRTOS 下不依赖 PC。适合电源调试、电机驱动验证、电池充放电监测等需现场波形观察的工业与教学场景。新手可从单通道电压波形起步熟手可扩展至四通道同步采样FFT 频谱分析。2. ADC 采样链路设计从信号调理到 DMA 搬运每一步都决定波形保真度2.1 为什么必须做信号调理直接接 ADC 会丢掉关键细节STM32F103 的 ADC 输入范围是 0–3.3V但实际电压电流信号常超出此限如 24V 电源、5A 电流经 0.01Ω 分流电阻产生 50mV。若直接接入轻则饱和失真重则损坏 IO。常见做法是采用两级调理电压通道分压电阻网络如 100kΩ 15kΩ将 0–30V 缩至 0–3.91V再经 LM358 跟随器隔离并用 TVS 二极管如 P6KE3.3A钳位至 3.3V电流通道0.01Ω 分流电阻 INA199 电流检测放大器增益 50V/V输出 0–2.5V 对应 0–5A同样加 RC 低通滤波R1kΩ, C10nF → fc≈15.9kHz抑制开关噪声。提示分压电阻需选用 0.1% 精度、低温漂50ppm/℃型号否则 30V 量程下 0.5% 误差即达 ±150mV远超示波需求。2.2 ADC 配置核心参数采样周期、分辨率与触发方式以 STM32F103 为例ADC1 用于电压ADC2 用于电流双 ADC 模式可同步采样。关键配置如下表参数推荐值说明分辨率12-bitF103 最高支持8-bit 波形太粗糙256 级12-bit 提供 4096 级量化满足 30V 量程下 ≈7.3mV 分辨率采样周期1.5 cycles最小对应 14MHz ADCCLK 下单次采样时间 ≈107ns实际总采样率受转换时间限制12-bit 典型为 1μs/次 → 理论最大 1MS/s但受 DMA 和 LCD 刷新制约实测稳定 100kS/s触发源定时器 TRGOTIM2避免软件触发抖动TIM2 设置为 100kHz 更新事件ARR71, PSC0 72MHz精准控制采样间隔对齐方式右对齐数据直接存入 uint16_t 数组无需位移操作// 初始化 ADC1电压通道 PA0与 ADC2电流通道 PA1 void ADC_Init(void) { ADC_InitTypeDef ADC_InitStructure; ADC_CommonInitTypeDef ADC_CommonInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_ADC1 | RCC_APB2PERIPH_ADC2, ENABLE); // 双 ADC 模式ADC1 主ADC2 从同步规则转换 ADC_CommonInitStructure.ADC_Mode ADC_Mode_RegSimult; // 同步规则采样 ADC_CommonInitStructure.ADC_Prescaler ADC_Prescaler_Div2; // 72MHz/236MHz ADCCLK ADC_CommonInitStructure.ADC_DMAAccessMode ADC_DMAAccessMode_1; // 每次转换后 DMA 请求 ADC_CommonInitStructure.ADC_TwoSamplingDelay ADC_TwoSamplingDelay_5Cycles; ADC_CommonInit(ADC_CommonInitStructure); ADC_InitStructure.ADC_Resolution ADC_Resolution_12b; ADC_InitStructure.ADC_ScanConvMode DISABLE; // 单通道非扫描 ADC_InitStructure.ADC_ContinuousConvMode DISABLE; // 单次转换由 TIM2 触发 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_T2_TRGO; // TIM2 更新事件 ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfConversion 1; ADC_Init(ADC1, ADC_InitStructure); ADC_Init(ADC2, ADC_InitStructure); ADC_Cmd(ADC1, ENABLE); ADC_Cmd(ADC2, ENABLE); }2.2.1 为什么禁用连续转换模式连续模式下 ADC 自动重复采样但无法精确控制采样点间隔受内部状态机影响导致波形横轴时间刻度失真。使用定时器触发每个采样点严格对应 TIM2 的更新事件保证时间轴线性——这是示波功能成立的前提。2.3 DMA 配置零 CPU 干预搬运双通道数据ADC1 和 ADC2 共享一个 DMA 通道DMA1 Channel1需配置为双缓冲模式交替存储电压与电流数据// DMA 初始化传输 2048 个 uint16_t1024 点 × 2 通道 void DMA_Init(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPERIPH_DMA1, ENABLE); DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; // ADC1 数据寄存器地址 DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)adc_buffer; // 内存起始地址 DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize 2048; // 总字数 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 传输完成中断用于波形刷新 DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE); NVIC_EnableIRQ(DMA1_Channel1_IRQn); DMA_Cmd(DMA1_Channel1, ENABLE); }注意ADC1-DR是 ADC1 的数据寄存器地址但双 ADC 模式下DMA 实际从 ADC1-DR 读取的是 ADC1 值从 ADC2-DR 读取的是 ADC2 值硬件自动切换。因此adc_buffer中数据按[V0, I0, V1, I1, ..., V1023, I1023]顺序排列无需额外索引计算。3. LCD 驱动与波形绘制从 40pin 接口初始化到实时刷新优化3.1 40pin LCD 接口选型与硬件连接要点本项目采用 ILI9341 驱动的 3.2 TFT 屏典型 40pin 接口其并行接口8/16bit速度远高于 SPI适合波形高频刷新。关键引脚连接如下以 STM32F103C8T6 为例LCD 引脚STM32 引脚功能说明D0–D7PB0–PB78-bit 数据总线复用 GPIOBRS (DC)PB8寄存器/数据选择WR (RD)PB9写使能低电平有效CSPB10片选低电平有效RSTPB11复位高电平有效BL (背光)PA8 (PWM)通过 TIM1 CH1 控制亮度提示40pin 接口通常含 VCC、GND、LED、LED- 等电源引脚务必确认背光电压多数为 3.3V 或 5V避免烧毁 LED。若使用 5V 背光需外接 LDO 或 MOSFET 开关控制。3.2 FSMC 配置让 LCD 像内存一样被快速访问ILI9341 在 FSMC 模式下映射为 NOR Flash地址空间为0x60000000。配置 FSMC_Bank1_NORSRAMx 为 8-bit 数据总线、异步模式void FSMC_LCD_Init(void) { FSMC_NORSRAMInitTypeDef FSMC_NORSRAMInitStructure; FSMC_NORSRAMTimingInitTypeDef pTiming; RCC_AHBPeriphClockCmd(RCC_AHBPERIPH_FSMC, ENABLE); // 地址建立时间1 个 HCLK 周期13.89ns 72MHz pTiming.FSMC_AddressSetupTime 1; pTiming.FSMC_AddressHoldTime 0; pTiming.FSMC_DataSetupTime 3; // 数据保持时间 ≥25ns设为 3→41.67ns pTiming.FSMC_BusTurnAroundDuration 0; pTiming.FSMC_CLKDivision 0; pTiming.FSMC_DataLatency 0; FSMC_NORSRAMInitStructure.FSMC_Bank FSMC_Bank1_NORSRAM1; FSMC_NORSRAMInitStructure.FSMC_DataAddressMux FSMC_DataAddressMux_Disable; FSMC_NORSRAMInitStructure.FSMC_MemoryType FSMC_MemoryType_SRAM; FSMC_NORSRAMInitStructure.FSMC_MemoryDataWidth FSMC_MemoryDataWidth_8b; FSMC_NORSRAMInitStructure.FSMC_BurstAccessMode FSMC_BurstAccessMode_Disable; FSMC_NORSRAMInitStructure.FSMC_WaitSignalPolarity FSMC_WaitSignalPolarity_Low; FSMC_NORSRAMInitStructure.FSMC_AsynchronousWait FSMC_AsynchronousWait_Disable; FSMC_NORSRAMInitStructure.FSMC_WrapMode FSMC_WrapMode_Disable; FSMC_NORSRAMInitStructure.FSMC_WaitSignalActive FSMC_WaitSignalActive_BeforeWaitState; FSMC_NORSRAMInitStructure.FSMC_WriteOperation FSMC_WriteOperation_Enable; FSMC_NORSRAMInitStructure.FSMC_WaitSignal FSMC_WaitSignal_Disable; FSMC_NORSRAMInitStructure.FSMC_ExtendedMode FSMC_ExtendedMode_Disable; FSMC_NORSRAMInitStructure.FSMC_WriteBurst FSMC_WriteBurst_Disable; FSMC_NORSRAMInitStructure.FSMC_ReadWriteTimingStruct pTiming; FSMC_NORSRAMInitStructure.FSMC_WriteTimingStruct pTiming; FSMC_NORSRAMInit(FSMC_NORSRAMInitStructure); FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM1, ENABLE); }3.2.1 为什么不用 SPISPI 最高理论速率约 18MHzF103 SPI1 APB2但实际写像素需发送命令参数数据单像素耗时 1μs而 FSMC 并行写入单像素仅需 2 个总线周期≈27.7ns快 30 倍以上。1024 点波形刷新FSMC 可在 3ms 内完成SPI 则需 100ms无法满足实时性。3.3 波形绘制算法双缓冲增量更新CPU 占用率 15%全屏重绘320×24076800 像素会阻塞 ADC 采样。采用双缓冲策略frame_buffer[2][320]两个 320 像素宽的行缓冲区存储当前波形 Y 坐标0–239lcd_buffer[2][320*240]两帧完整屏幕缓冲后台 DMA 刷新每次 DMA 传输完成中断中仅计算新采样点对应的 1 像素列X 固定Y 根据 ADC 值映射并更新frame_buffer主循环中当frame_buffer满 320 点调用LCD_DrawWave()将整行波形写入lcd_buffer再触发 FSMC 刷新。// 将 ADC 值映射为屏幕 Y 坐标0顶部239底部 #define VOLTAGE_SCALE (239.0f / 4095.0f * 30.0f) // 30V 量程 → 0–239 #define CURRENT_SCALE (239.0f / 4095.0f * 5.0f) // 5A 量程 → 0–239 void UpdateWavePoint(uint16_t v_adc, uint16_t i_adc, uint16_t x_pos) { static uint16_t frame_idx 0; int16_t v_y 239 - (int16_t)(v_adc * VOLTAGE_SCALE / 30.0f); // 电压顶部为 0V int16_t i_y 239 - (int16_t)(i_adc * CURRENT_SCALE / 5.0f); // 电流顶部为 0A v_y CLAMP(v_y, 0, 239); i_y CLAMP(i_y, 0, 239); // 绘制电压线红色与电流线蓝色在同一 X 列 lcd_buffer[active_buffer][x_pos v_y * 320] RED; lcd_buffer[active_buffer][x_pos i_y * 320] BLUE; if (frame_idx 320) { frame_idx 0; // 切换缓冲区并刷新 active_buffer ^ 1; LCD_FillScreen(lcd_buffer[active_buffer ^ 1]); // FSMC DMA 刷新 } }注意CLAMP()宏防止 ADC 异常值如短路导致 Y 坐标越界。实际部署中建议在 ADC 初始化后添加校准步骤短接输入端读取零点偏移再测已知电压修正增益。4. 触发与交互逻辑实现类似台式示波器的核心功能4.1 边沿触发机制捕获瞬态事件的关键无触发的滚动波形无法稳定观察脉冲。本项目实现软件边沿触发基于最近 N 个采样点判断#define TRIGGER_DEPTH 64 // 触发前保留 64 点 typedef enum { TRIG_OFF, TRIG_RISING, TRIG_FALLING } trigger_mode_t; trigger_mode_t trigger_mode TRIG_RISING; uint16_t trigger_level 2048; // ADC 中值1.65V uint8_t trigger_state 0; // 0等待1已触发 void CheckTrigger(uint16_t v_adc) { static uint16_t history[TRIGGER_DEPTH]; static uint8_t hist_idx 0; // 移入新采样点 history[hist_idx] v_adc; hist_idx (hist_idx 1) % TRIGGER_DEPTH; if (trigger_state 0) { // 检查上升沿前一点 level当前点 ≥ level uint16_t prev history[(hist_idx - 2 TRIGGER_DEPTH) % TRIGGER_DEPTH]; if (trigger_mode TRIG_RISING prev trigger_level v_adc trigger_level) { trigger_state 1; // 从 history[(hist_idx - TRIGGER_DEPTH) % TRIGGER_DEPTH] 开始显示 } // 下降沿类似... } }4.1.1 触发位置如何影响波形显示触发后frame_buffer不再按自然顺序填充而是从触发点开始先填TRIGGER_DEPTH个历史点从history[]回溯再填后续实时点。这样屏幕上显示的是“触发前 64 点 触发后 256 点”符合示波器习惯。4.2 LCD 屏显示中文用字模提取工具生成 GB2312 点阵用户需在 LCD 上显示“电压”、“电流”、“触发ON”等中文标签。常见做法是使用 PCtoLCD2002 工具将 GB2312 字库提取为 16×16 点阵数组存入 Flash// 示例电 字的 16×16 点阵每行 2 字节共 16 行 const uint8_t font16x16_zh[] { 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // ... 256 字节省略 }; void LCD_DisplayChinese(uint16_t x, uint16_t y, const uint8_t *font_data, uint16_t color) { for (uint8_t row 0; row 16; row) { for (uint8_t col 0; col 16; col) { uint8_t bit (font_data[row * 2 (col / 8)] (0x80 (col % 8))) ? 1 : 0; if (bit) LCD_SetPoint(x col, y row, color); } } }提示40pin LCD 接口带宽充足可支持 16×16 中文但需预留 Flash 空间GB2312 常用字约 3000 个每个 32 字节 → 96KB。若空间紧张可只提取项目所需汉字如“电、压、流、触、发、ON、OFF”等 20 字。4.3 旋钮与按键交互调节触发阈值与时间基准硬件上接入一个 10kΩ 电位器接 ADC3_IN0和两个轻触按键PA12、PA13电位器映射为触发阈值0–30V → ADC 值 0–4095实时更新trigger_levelKEY1PA12短按切换触发模式OFF → RISING → FALLING长按1s进入校准模式KEY2PA13短按切换时基1ms/div → 5ms/div → 10ms/div改变 TIM2 的 ARR 值。// 按键扫描主循环中调用 void KEY_Scan(void) { static uint32_t key1_down_time 0; static uint8_t key1_pressed 0; if (!GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_12)) { if (!key1_pressed) { key1_pressed 1; key1_down_time HAL_GetTick(); } if (HAL_GetTick() - key1_down_time 1000) { EnterCalibration(); // 长按进入校准 } } else { if (key1_pressed) { if (HAL_GetTick() - key1_down_time 500) { CycleTriggerMode(); // 短按切换模式 } key1_pressed 0; } } }5. 抗干扰与精度优化解决 ADC 数据漂移与 LCD 亮度波动的实际技巧5.1 ADC 数据漂移的三大根源与对应措施实测中常出现“静止信号下 ADC 值跳动 ±5 LSB”这会导致波形基线抖动。根本原因及对策如下漂移来源现象解决方案电源噪声所有通道同步跳动在 VREF 引脚就近加 100nF 陶瓷电容 10μF 钽电容ADC 供电用独立 LDO如 AMS1117-3.3而非 MCU 的 3.3V参考电压波动VREF 未接稳压源随负载变化必须使用内部 VREFINT 校准或外部精密基准如 REF3033禁用 VDDA 作为参考PCB 布局干扰高频信号线靠近 ADC 走线ADC 输入走线远离晶振、SWD、电机驱动线铺地平面隔离输入端加 RC 滤波R100Ω, C100nF → fc15.9kHz注意ADC_GetCalibrationValue()在初始化后必须调用一次否则内部校准未生效。F103 的校准值存储在 Flash 中每次上电需重新加载。5.2 LCD 亮度动态调节用 PWM 避免闪烁与功耗浪费背光亮度直接影响波形可视性。直接用 GPIO 控制 LED 会因占空比低而闪烁必须用 PWM// TIM1 CH1 输出 PWM 控制背光PA8 void PWM_Backlight_Init(uint16_t duty_cycle) { // duty_cycle: 0–10000.1% 精度 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_TIM1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 999; // 1kHz PWM72MHz / (9991) 72kHz → /72 1kHz TIM_TimeBaseStructure.TIM_Prescaler 71; // PSC71 → 72MHz/(711)1MHz 计数频率 TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse duty_cycle; // 初始占空比 TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM1, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE); }5.2.1 为什么用 TIM1 而非通用定时器TIM1 是高级定时器支持互补输出与死区插入虽本项目未用但其 PWM 频率稳定性优于 TIM2/TIM3后者为通用定时器时钟源易受 APB1 分频影响。1kHz PWM 频率远高于人眼临界融合频率60Hz彻底消除可见闪烁。5.3 一个关键技巧用 ADC 的注入通道做零点校准每次开机时自动短接电压/电流输入端通过继电器或模拟开关读取 64 次 ADC 值取平均作为零点偏移v_offset和i_offset。后续所有采样值减去该偏移uint16_t v_offset 0, i_offset 0; void CalibrateZero(void) { uint32_t sum_v 0, sum_i 0; // 短接输入端硬件动作 HAL_GPIO_WritePin(GPIOB, GPIO_Pin_12, GPIO_PIN_SET); // 控制继电器闭合 for (int i 0; i 64; i) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); sum_v ADC_GetConversionValue(ADC1); ADC_SoftwareStartConvCmd(ADC2, ENABLE); while (!ADC_GetFlagStatus(ADC2, ADC_FLAG_EOC)); sum_i ADC_GetConversionValue(ADC2); Delay_us(100); // 避免采样过快 } v_offset sum_v / 64; i_offset sum_i / 64; // 断开短接 HAL_GPIO_WritePin(GPIOB, GPIO_Pin_12, GPIO_PIN_RESET); }此技巧可将零点漂移从 ±10 LSB 降至 ±1 LSB使 30V 量程下的绝对误差从 ±300mV 改善至 ±30mV满足大多数工业监测需求。本文还有配套的精品资源点击获取