news 2026/10/14 1:21:00

STM32F407简易示波器制作教程:ADC采样与DMA波形显示实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F407简易示波器制作教程:ADC采样与DMA波形显示实践

简介:基于STM32F407的简易示波器完整源码,适合嵌入式初学者或关注ADC采集、LCD显示的开发者,可帮助理解从信号调理到波形绘制的完整链路。项目用C语言编写,Visual Studio Code中编辑、Keil5.3编译,架构为主循环加采集中断加定时界面更新。实现一路ADC定时采集,通过继电器控制放大电路增益,独立按键调整采样周期与电压范围,在3.2寸TFTLCD上绘制采集曲线,并计算周期、频率、峰峰值等参数。资源共171个文件,以73个C源码、65个头文件为主体,辅以少量PNG图片、工程配置与Hex文件,压缩包仅1.18MB,可直接导入Keil工程研读。已有3346人学习下载,覆盖ADC采集、LCD波形显示、继电器增益控制与按键交互逻辑,适合作STM32F407入门与嵌入式综合练习参考。

1. 简易示波器怎么从零搭:为什么选STM32F407而不是普通单片机

调试一块电机驱动板时,万用表只能告诉你“有没有电压”,却看不出PWM边沿的振铃和通信线上的毛刺。手头如果有一台基于STM32F407的简易示波器——哪怕分辨率只有12位、带宽只有几百kHz——也能把这些波形看清楚。F407有独立的ADC、DMA通道和168MHz主频,配合源代码里那套“定时器触发、DMA搬运、波形绘制”的框架,就是一台能连续采样的示波器,和用声卡看音频的玩具玩法不是一个量级。这篇笔记写给想做低频模拟信号调试工具的嵌入式开发者,以及想把F4外设串起来练手的人:你会知道硬件怎么搭、采样参数怎么设、跑起来会遇到哪些坑。


2. 模拟前端与硬件链路:探针到ADC之间的电路设计

2.1 阻抗匹配与输入保护:探头不是一根导线

很多第一次做简易示波器的人,直接把被测信号用一根杜邦线连到STM32F407的ADC引脚上,测出来的波形要么幅度不对,要么在某个频率上突然塌下去。这不是ADC的锅,而是信号源阻抗和ADC输入结构不匹配。

F407的ADC是开关电容结构,采样瞬间会有电荷注入,等效输入阻抗并不高,尤其在采样周期很短的时候。如果信号源阻抗达到几十kΩ,采样保持电容还没来得及充满,转换结果就会偏小,高阻抗信号源配短采样周期时偏差更明显。常见做法是在ADC前面加一级运放跟随器,用轨到轨运放把信号源的阻抗隔离掉,让ADC看到一个低阻抗源。

输入保护也不能省。被测板子上的毛刺、反电动势或者误接高压,都可能直接打坏ADC引脚。最简保护电路是两个二极管钳位加一个串联电阻:电阻串在输入端限流,二极管把电压钳在GND和VDD之间。我一般用双二极管把信号钳位到3.3V供电轨,再串一个1kΩ电阻进ADC,牺牲一点带宽换安全。这个成本只有几毛钱,却能避免“采集卡突然烧了”这种血泪事故。

2.2 信号调理:分压、偏置与增益切换

F407的ADC只能测0到VREF+(通常等于3.3V)之间的电压,所以被测信号范围必须映射到这个区间。测0~3.3V的数字信号最简单,想测正负电压或者超过3.3V的模拟信号,就要做分压和偏置。

常见做法是电阻分压加隔直电容。比如要测-5V~+5V的信号,先用两只等值电阻分压,把信号缩到一半,再叠加一个1.65V直流偏置,把-5V映射到约-0.85V?这里要算清楚。更稳妥的是用运放搭加法器:Vout = Vin * Rf/Rin + Vref,通过选电阻同时完成缩放和偏置。

增益切换是另一个容易被忽略的点。直接测3.3V信号和测10mV的小信号,ADC满量程都是4096,但10mV信号在12位下只占大约12个LSB,波形几乎是一条噪声带。简易方案用固定增益不够灵活,我一般会在模拟开关加两挡运放增益之间切换,比如1倍和10倍,用两个GPIO控制。不用高档仪表放大器的原因是,F407自带12位ADC,前端做太高的增益只会把噪声一起放大,够用就行。

2.3 主控外设规划:ADC、DMA、TIM、FSMC各负责什么

STM32F407做示波器的硬件基础在于外设齐全:3个12位ADC、2个DMA控制器、十几个定时器、FSMC并行总线接口,甚至还有一个DAC和一个DMA2D图形加速器。单片机上常见的8位MCU要么ADC采样率不够,要么没有DMA导致CPU被采样中断占满,要么LCD接口慢得波形刷新像幻灯片。F407把这些短板都补上了。

整个系统里每个外设的职责可以这样分:

外设职责关键点
ADC1波形采样12位,外部触发模式,采样周期按需配置
DMA2 Stream0搬运采样数据到内存循环模式,半传输/全传输中断配合双缓冲
TIM3产生精确的采样触发脉冲通过TRGO事件触发ADC,采样率由预分频和自动重装值决定
FSMC驱动并行接口LCD像操作SRAM一样写LCD,刷新率远高于GPIO模拟
DAC1自检信号源输出方波或正弦波,接回ADC校验整条链路
USART1与上位机通信把原始采样点传回电脑,便于分析和存储

引脚规划时要注意冲突。ADC1的通道1在PA1,这是比较干净的一个引脚;DAC1在PA4;FSMC的数据总线占用PD14~PD15、PE7~PE15等;USART1是PA9/PA10。先画一张引脚分配表再画PCB,能少走很多弯路。这块我走过一次“回头路”:先选了LCD的FSMC引脚,结果把ADC引脚挤到和DAC复用,最后只能改板。


3. 定时器触发ADC采样与DMA搬运:核心代码一步步写

3.1 采样不依赖CPU:触发源为什么选定时器

两种常见采样方案摆在面前:一是ADC连续转换加DMA,让ADC自己不停地采;二是定时器周期触发ADC,每个上升沿采一次。

连续转换模式看似简单,但采样间隔完全由ADC转换时间决定,改分辨率或采样周期就会改变等效采样率,而且ADC转换结束事件触发DMA搬运的节奏并不均匀。示波器最忌讳的就是采样间隔不均匀——波形会变形,FFT频谱会出现杂散。定时器触发则完全不同:TIM3的计数时钟是固定的,预分频和自动重装值一设,触发脉冲间隔就精确稳定,所以我把TIM3作为采样时钟源。

F407的定时器触发是通过TRGO(主模式输出)实现的,把TIM3的更新事件映射到TRGO,再把ADC1的外部触发源选成TIM3_TRGO。ADC每次收到上升沿就启动一次转换,转换完由DMA把结果搬走,CPU全程不参与采样过程。

3.2 ADC初始化:外部触发与DMA请求配置

先初始化ADC1的引脚和基本参数。以PA1作为采样输入为例:

GPIO_InitTypeDef gpio = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_ADC1_CLK_ENABLE(); gpio.Pin = GPIO_PIN_1; gpio.Mode = GPIO_MODE_ANALOG; HAL_GPIO_Init(GPIOA, &gpio); ADC_HandleTypeDef hadc1 = {0}; hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; // ADCCLK = 84MHz / 4 = 21MHz hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode = DISABLE; // 单通道,不扫描 hadc1.Init.ContinuousConvMode = DISABLE; // 不用连续转换,等定时器触发 hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING; hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T3_TRGO; HAL_ADC_Init(&hadc1);

这段配置的核心是最后两行:ExternalTrigConvEdge指明上升沿触发,ExternalTrigConv选定触发源为TIM3的TRGO。ClockPrescaler选4分频,得到21MHz的ADC时钟,这是F407比较稳妥的配置。

ADC时钟频率直接决定转换速度。对于12位分辨率,一次转换最少需要“采样周期+12”个ADCCLK周期,采样周期最小为3。在21MHz下,最大单通道采样率是21MHz/(3+12)=1.4Msps。如果你看到网上有人开到2Msps以上还没问题,多半用的是双ADC交叉采样或者降低了分辨率。

3.3 DMA循环模式与半缓冲切换:等效双缓冲

ADC转换结果需要DMA搬进内存,否则每采一个点都要进一次中断,168MHz的CPU也会被频繁打断。DMA配置的关键是循环模式加半传输中断:

DMA_HandleTypeDef hdma_adc = {0}; __HAL_RCC_DMA2_CLK_ENABLE(); hdma_adc.Instance = DMA2_Stream0; // ADC1的DMA请求映射到DMA2 Stream0 hdma_adc.Init.Channel = DMA_CHANNEL_0; hdma_adc.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_adc.Init.PeriphInc = DMA_PINC_DISABLE; hdma_adc.Init.MemInc = DMA_MINC_ENABLE; hdma_adc.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; // ADC12位,用16位搬运 hdma_adc.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_adc.Init.Mode = DMA_CIRCULAR; // 循环模式,DMA永远不停 hdma_adc.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hdma_adc); __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc);

循环模式意味着DMA把一块内存填满后自动回到起始地址继续写。传统做法是在DMA传输完成中断里把整块数据复制出去处理,但这样有半个缓冲周期的时间在等数据,浪费了采集时间。

更高效的做法是直接利用“半传输完成中断”和“传输完成中断”:当DMA写满前半块时,后半块还在继续采;主逻辑处理前半块。当写满整块时,前半块又开始被覆盖,此时后半块的数据是完整的。这种交替使用方式,我一般叫它软件双缓冲,不需要开启F407的硬件双缓冲模式也能做到不丢点。注意两个回调函数里的处理时间必须小于半个缓冲区的采集时间,否则缓冲区会被覆盖出问题。

3.4 定时器参数:采样率怎么算才准

TIM3的时钟在F407上来自APB1定时器时钟,系统主频168MHz时是84MHz。采样率就是TIM3的更新频率:

TIM_HandleTypeDef htim3 = {0}; __HAL_RCC_TIM3_CLK_ENABLE(); htim3.Instance = TIM3; htim3.Init.Prescaler = 83; // 84MHz / 84 = 1MHz计数频率 htim3.Init.Period = 999; // 计1000个数,1kHz触发一次 htim3.Init.CounterMode = TIM_COUNTERMODE_UP; HAL_TIM_Base_Init(&htim3); TIM_MasterConfigTypeDef masterCfg = {0}; masterCfg.MasterOutputTrigger = TIM_TRGO_UPDATE; // 更新事件作为TRGO输出 masterCfg.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(&htim3, &masterCfg);

采样率公式是:采样率 = TIM3时钟 / (PSC+1) / (ARR+1)。所以上面这个配置就是 84MHz / 84 / 1000 = 1kHz,适合观察低频信号。

常用挡位可以直接套这组参数:

目标采样率PrescalerPeriod说明
1kHz83999适合看传感器慢信号
100kHz839适合看PWM和音频信号
1MHz830接近单ADC上限,注意ADC转换时间

把Period设0不代表禁止更新,ARR=0时计数器每计一个数就溢出,TIM3计数频率是多少,触发频率就是多少。最终采样率受到ADC转换时间的硬限制,1MHz以上建议把分辨率降到8位,或者安排ADC1和ADC2交叉采样。

3.5 启动采样与源码结构

启动顺序有讲究。先启动ADC的DMA传输,再启动TIM3,否则定时器先转起来时DMA还没准备好,第一批触发事件会被丢掉。正确的顺序:

uint16_t adcBuf[4096]; HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adcBuf, 4096); HAL_TIM_Base_Start(&htim3);

然后DMA的循环搬运就自己跑了。在中断回调里,只需要做缓冲区的“交接”动作:

void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { uint32_t *p = (uint32_t *)hadc->DMA_Handle->Instance->M0AR; HAL_GPIO_WritePin(DRAM_CS_GPIO_Port, DRAM_CS_Pin, GPIO_PIN_SET); (void)p; } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { uint32_t *p = (uint32_t *)hadc->DMA_Handle->Instance->M0AR; HAL_GPIO_WritePin(DRAM_CS_GPIO_Port, DRAM_CS_Pin, GPIO_PIN_RESET); (void)p; }

实际工程里我不会在中断里做处理,而是置一个标志位,主循环看到标志后在前后半缓冲区之间切换显示指针。源码组织上我习惯分三层:Driver存放ADC、TIM、DMA、LCD、串口的驱动,App存放触发检测、波形绘制和帧协议,Core放主循环和中断入口。分清楚之后,想换LCD或者改采样率,都只动一个文件,不用翻整个工程。


4. 波形显示与触发同步:把数字点变成屏幕上稳定的波形

4.1 电平触发软件算法:为什么波形会横向乱跑

没有触发机制的示波器,屏幕上看到的波形是不断左右滚动的,因为每次缓冲区起始点不固定,波形画出来的位置自然就在漂。要稳住波形,必须让每次显示的起点对准信号上同一类位置。

最常见的做法是上升沿触发:在缓冲区内找到第一个从低于触发电平跳变到高于触发电平的点,然后从这个点开始向后取固定长度的数据画波形。这样无论数据从哪儿开始,波形在屏幕上的位置是确定的。

uint32_t Scope_FindTriggerIndex(const uint16_t *buf, uint32_t len, uint16_t level, uint32_t startIdx) { for (uint32_t i = startIdx + 1; i < len; i++) { if (buf[i - 1] < level && buf[i] >= level) { return i; } } return 0; // 没找到,返回0表示保持上一帧 }

这个算法看起来简单,但要注意触发电平必须落在信号幅度范围之内。电平设成0或者4096都永远找不到上升沿,表现为波形偶尔冻结不刷新。另一个问题是噪声:信号上叠加噪声时,会在触发电平附近反复穿越,触发点抖动,波形看起来有毛边。噪声大时先做一点软件滤波,或者把触发电平抬高一些,就不会那么敏感。

4.2 LCD绘制波形:坐标映射和刷新效率

找到触发点后,就可以把采样数据映射到LCD坐标。假设屏幕宽240、高320,ADC是12位:

#define LCD_WIDTH 240 #define LCD_HEIGHT 320 #define ADC_FULL 4096 void Scope_DrawWaveform(const uint16_t *data, uint32_t count, uint32_t trigIdx) { uint32_t xPrev = 0, yPrev = 0; LCD_ClearScreen(LCD_COLOR_BLACK); for (uint32_t i = 0; i < count; i++) { uint32_t x = (LCD_WIDTH - 1) * i / (count - 1); uint32_t y = (LCD_HEIGHT - 1) * (data[(trigIdx + i) % count]) / ADC_FULL; y = (LCD_HEIGHT - 1) - y; // LCD的Y轴向下,波形要反一下 if (i == 0) { LCD_DrawPixel(x, y, LCD_COLOR_GREEN); } else { LCD_DrawLine(xPrev, yPrev, x, y, LCD_COLOR_GREEN); } xPrev = x; yPrev = y; } }

代码里data[(trigIdx + i) % count]是环形取数的写法,从触发点开始环绕整个缓冲区,避免拷贝数据。这个细节能省掉每次触发都要memcpy的开销。

绘制效率是简易示波器最容易卡住的地方。逐点写屏幕会被LCD总线速度限制,如果LCD画线函数是逐像素设置坐标再写数据,240个点也要几毫秒。我一般会做两件事:第一,先用LCD的区域填充命令清背景,而不是整屏整屏擦;第二,把画线函数改成尽量按行扫描,减少坐标跳变,很多LCD控制器连续写同一行比跳变写快很多。F407有FSMC并行总线,配合DMA2D还能进一步提速,但DMA2D主要适合整块区域填充,画波形这种稀疏图形还是靠优化画线逻辑最实际。

4.3 串口上位机方案:把采样数据搬到电脑

屏幕小的LCD看波形细节有限,存储深度也就几千点。很多时候我们需要把ADC原始数据传回电脑,用软件画波形、算频率、存文件。这时要设计一个简单的串口帧协议。

我一般用这样的帧格式:帧头固定为0xAA 0x55,接着是2字节数据长度,然后是原始采样数据,最后加1字节累加和校验。单片机端发送逻辑不复杂:

uint8_t uartBuf[2 + 2 + 4096 * 2 + 1]; void Scope_SendFrame(const uint16_t *data, uint32_t len) { uint32_t idx = 0; uartBuf[idx++] = 0xAA; uartBuf[idx++] = 0x55; uartBuf[idx++] = (len >> 8) & 0xFF; uartBuf[idx++] = len & 0xFF; uint8_t sum = 0; for (uint32_t i = 0; i < len; i++) { uartBuf[idx++] = data[i] & 0xFF; uartBuf[idx++] = (data[i] >> 8) & 0xFF; sum += data[i] & 0xFF; sum += (data[i] >> 8) & 0xFF; } uartBuf[idx++] = sum; HAL_UART_Transmit(&huart1, uartBuf, idx, 100); }

上位机端我习惯用Python快速验证,几行代码就能画出来:

import serial, struct, matplotlib.pyplot as plt ser = serial.Serial('COM10', 921600, timeout=1) data = ser.read(4) assert data[:2] == b'\xaa\x55' length = struct.unpack('>H', data[2:4])[0] raw = ser.read(length * 2 + 1) values = struct.unpack('<' + 'H' * length, raw[:length * 2]) plt.plot(values) plt.show()

串口波特率决定了数据吞吐上限。921600bps每秒约100KB,4096点一帧数据约8KB,刷新率撑死十几帧,基本够看波形形态。要更高刷新率就降低传输长度或者提高波特率,F407的USART通常能稳定跑1.5Mbps,前提是上位机和线材都靠谱。


5. 简易示波器最常见的5个坑:从采样撕裂到电压偏差的排查手册

5.1 DMA半缓冲里做数据处理,波形偶尔撕裂

现象:采集到的正弦波在屏幕中间某处突然水平平移了一段,左右两半波形接不上。最初我以为是LCD绘制问题,后来发现数据本身就不连续。

原因:虽然用了半传输中断,但在回调里做的是数据拷贝,拷贝期间DMA仍在往后半段写。如果拷贝时间超过了半段缓冲的采集时间,DMA完成一轮循环回到前半段时,内存里已经是新旧数据混在一起了。半缓冲时间在高采样率下会非常短,1MHz采样、缓冲4096点,半段只有2ms,如果回调里还做了滤波、画图,很容易超时。

解决:把“拷贝”改成“指针切换”。维护两个显示缓冲区A和B,DMA写A时显示B,DMA写B时显示A。DMA缓冲区的地址随半传输和全传输中断切换,主循环只拿当前可用的缓冲区指针。这样中断回调里只置一个标志,处理数据的大头全在主循环做,没有拷贝也就没有竞争条件。

5.2 采样率设到2Msps以上就丢点

现象:把TIM3触发频率调到2MHz,屏幕上波形周期完全不对,频率读数也乱跳。用串口把原始数据发到电脑看,发现数据点之间的时间间隔不均匀,有的地方点特别稀。

原因:F407单ADC在12位模式下,ADCCLK为21MHz时最大转换率只有1.4Msps。触发频率超过转换能力后,ADC还没来得及完成上一次转换,下一次触发就到了,规则通道一次只能处理一个转换请求,后面的触发就被丢弃。这是硬件上限,不是代码能调好的。

解决:先算上限,再选方案。如果2Msps只是理想值,实际可以把采样周期调成3个周期,ADCCLK保持21MHz,得到1.4Msps;或者牺牲分辨率,8位模式转换周期变成“3+8”个周期,上限提高到约1.9Msps。真正的2Msps以上要用双ADC交叉采样:ADC1和ADC2各采各的,DMA把两路结果合并,等效采样率翻倍。交叉采样的代码复杂度上升不少,我一般只在确实需要时才上。

5.3 测电压偏大,而且数值随USB供电变化

现象:测量一颗标称2.7V的锂电池,示波器显示2.88V;换一个USB口供电,显示又变成2.81V。用万用表量F407的3.3V供电,发现是3.38V而不是3.30V。

原因:F407的ADC参考电压VREF+在绝大多数开发板上直接连着VDD,而VDD来自板载LDO或DC-DC,精度和负载调整率都有限。ADC的满量程4096对应的是实际VREF+电压,不是理论3.3V。供电电压偏高,ADC读数自然偏大。

解决:用芯片内部的基准电压VREFINT做校准。F407内部有一个约1.21V的参考电压,它会随供电一起飘,但飘的规律和ADC通道相同。先测量VREFINT通道的ADC值,反算出实际参考电压:

float vrefint_value; // 在初始化时读ADC16通道获得 float Scope_GetActualVref(void) { // VREFINT典型值1.21V,实际值因批次而异 return 1.21f * 4096.0f / vrefint_value; }

以后每次测电压,就用这个实际参考电压替代3.3V做换算。这样的校准不能替代精密基准源,但能把误差从百分之几压到百分之一以内。要更准就外接一个精密基准芯片,把VREF+接到独立基准上,整个ADC的地也单独处理。

5.4 按一次按键后波形冻结几十毫秒

现象:菜单按键每按一下,LCD上的波形就停一下,像卡顿一样。在波形刷新过程中按按键,卡顿更明显。

原因:按键处理写在主循环里,还用了while(!HAL_GPIO_ReadPin(...))这种等待释放的死循环。按下到释放的几百毫秒里,主循环始终卡在按键扫描上,LCD刷新和缓冲区切换全停了。虽然DMA还在采样,但显示逻辑没机会运行,波形看起来就是冻结的。

解决:按键扫描改成状态机,检测到按下立即返回,不等待释放。或者利用定时器中断每5ms扫一次按键,主循环只读取按键事件。不要把任何阻塞操作放进采集刷新路径上。我自己的习惯是:波形刷新永远放在主循环最靠前的位置,按键、串口、菜单全部放到主循环后面,这样即便某个任务出问题,波形显示优先级最高。

5.5 显示的频率比真实值低几十Hz

现象:信号发生器输出1.000kHz正弦波,示波器显示980Hz。低频时差得不多,频率越高误差越大。

原因:频率测量用的是计数法:统计一屏波形里有几个完整周期,再用采样率除以周期点数。如果缓冲区只有4096点,采样率1kHz,能捕获的周期数很少,计数误差一个点就会让频率显示差了很多。触发点的离散化也是一个采样周期,正负误差叠加后读数就偏了。

解决:改用更长的测量窗口。在DMA缓冲区中找相邻两个上升触发点,数出两者之间的采样点数N,频率就是采样率除以N。采样率越高、N越大,分辨率越高。测量时建议让一屏至少包含10个周期,这样误差能摊薄到可接受范围。还有一种误差来自晶振本身,开发板上的8MHz或者25MHz晶振精度一般在几十ppm,对1kHz信号影响很小,但如果用内部RC振荡器跑系统时钟,误差可能达到1%,那就得考虑外接晶振了。


6. 让示波器更好用:校准、自检与FFT扩展的落地经验

硬件和驱动跑稳之后,可以给这个简易示波器补三个实用的能力。第一个是自检信号源。F407自带DAC,用定时器中断按查表方式更新DAC输出,就能生成已知幅度和频率的方波或正弦波。把DAC输出用一根跳线接回ADC输入,整个采样链路的正确性一目了然:幅度对不对、触发稳不稳、采样率准不准,全都能在几千点数据里验证出来。这个习惯帮我绕开了不少“玄学”问题。

第二个是校准。测量误差来源不只是VREFINT校准能解决的,分压电阻的精度、运放的失调电压、LCD上显示的坐标舍入误差都会叠加。我会记录一组已知电压和ADC实测值,做两点线性校正,把offset和gain两个系数存进Flash。这样每次开机自动校准,省得每次换板子都要拿万用表对一遍。

第三个是FFT频谱显示。F407带FPU和DSP指令,算4096点复数FFT只需要几毫秒,完全能在实时性要求不高的场景里边采边算。用CMSIS-DSP库的arm_cfft_f32先对采样数据做FFT,再用arm_cmplx_mag_f32取模,就能画出幅度谱。配合前面提到的触发功能,可以在固定信号下观察频谱泄漏和噪声底,对小信号调试很有帮助。

我自己的习惯是每次改完采样参数,先让DAC输出一个1kHz正弦波做自检,确认幅度、触发和频率都正常后,再去测目标板。这个小动作看起来多花两分钟,实际上把采样时钟配置错误、DMA缓冲越界这类问题挡在了测量现场之外。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 1:20:49

微信小程序购物车源码解析:数据结构、状态同步与本地持久化实践

简介&#xff1a;这份源码面向具备微信小程序开发基础的开发者&#xff0c;提供一套可直接运行的购物车功能实现&#xff0c;适用于电商平台或任何需要在线购物模块的小程序项目。包内共24个文件&#xff0c;以JavaScript逻辑脚本、WXSS样式表、WXML结构模板和JSON配置文件为主…

作者头像 李华
网站建设 2026/10/14 1:20:36

STM32寄存器白话指南:从GPIO到串口,手把手入门操作

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:20:08

Scratch画圆进阶:用坐标与弧度实现圆心半径可控的圆

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:19:46

Python入门练手:逢七拍腿游戏实现与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:19:33

ICME23 知识蒸馏复现 · SLDNet阶段性沉淀

ICME23 知识蒸馏复现 阶段性沉淀项目&#xff1a;ICME 2023《Collaborative Spatial-Temporal Distillation for Efficient Video Deraining》 数据&#xff1a;NTURain&#xff08;合成测试集 8 序列 真实雨 7 序列&#xff09; 阶段&#xff1a;链路打通并验证&#xff0c;…

作者头像 李华
网站建设 2026/10/14 1:19:28

Python爬虫实战:requests+BeautifulSoup采集豆瓣电影TOP250

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华