尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

基于QT与STM32的振动测量系统:ADC采样、串口通信与FFT频谱实现

发布时间:2026/9/15 15:41:07

资讯中心
01
ARTICLE

基于QT与STM32的振动测量系统:ADC采样、串口通信与FFT频谱实现

基于QT与STM32的振动测量系统:ADC采样、串口通信与FFT频谱实现
简介这是一份基于QT与STM32的振动测量系统完整工程资料定位于本科毕业设计、课程作业及嵌入式初/中级开发者覆盖从下位机数据采集到上位机界面展示的典型开发环节。压缩包共449个文件约21.16MB包含C/C源码、STM32驱动与启动文件、Keil工程文件、QT界面工程文件、PDF说明文档以及编译调试相关配置等源码与工程文档一体化目录结构完整便于直接打开工程对照学习和二次开发。目前已有113人学习下载虽然规模不大但功能链路完整。通过这套资料可以学习STM32定时器与ADC采样、串口通信、QtSerialPort应用、界面布局与数据实时显示也能了解振动信号处理、数据记录与GUI集成的方法硬件侧涉及单片机外设配置与启动流程软件侧涉及上位机串口收发与图表呈现从底层驱动到上层交互均有对应文件支撑适合系统性训练是一份高实践性的毕设/课设参考素材。1. 把 QT 上位机与 STM32 采样端拼起来难点不在单点而在数据链路拿到“基于QT以及STM32的振动测量系统”这套工程时第一反应往往是看 STM32 代码能不能烧录、QT 界面能不能起来。真正做完一遍会发现STM32 侧用标准外设库做定时器触发、ADC 采样、DMA 搬运QT 侧用 QSerialPort 收包画波形两边的单点都不难难的是把采样率、数据帧格式、GUI 刷新率和 FFT 窗口统一成一个链路。这套资源适合正在做课程设计或毕业设计的嵌入式方向学生也适合快速搭一个低成本振动监测原型机的工程师。下文以 STM32F103 QT 5 的典型组合为例从采集、通信、显示到联调按可复现的步骤拆开讲。2. 振动采集端用定时器触发 STM32 ADC让 DMA 把样本搬走2.1 为什么不能靠主循环延时采样很多初版代码习惯在主循环里软件启动 ADC 转换等待 EOC 标志置位然后调用 delay_us 固定延时。这种写法在直流采集场景没什么问题一旦放到 1kHz 以上的振动测量里缺陷立刻暴露delay_us 做不到严格等间隔中断、分支判断都会让下一次采样时刻漂移几十微秒。对 1kHz 信号来说50μs 抖动相当于相位的 18 度误差反映在频谱上就是噪声底抬高原本应该尖锐的振动谱线变宽测量系统自身的抖动和振动信号混在一起。正确的做法是让 TIM 定时器产生固定频率事件作为 ADC 外部触发源ADC 转换完毕由 DMA 自动搬运到内存CPU 只做打包和发送。2.2 采样率、ADC 采样时间与抗混叠匹配振动测量目标信号通常集中在几十 Hz 到几 kHzF103 的 12 位 ADC 完全够用。系统采样率建议取 2kHz 到 5kHz。采样率低于 2kHz 会漏掉高频特征高于 5kHz 则串口带宽和上位机绘制压力同时上涨。采样率 fS 由定时器频率除以 ARR1 决定ADC 时钟来自 APB2最高不能超过 14MHz。外部模拟加速度计的带宽通常只有几 kHz若传感器本身带宽超过 fS/2必须在 ADC 前加一级 RC 低通。以 10kΩ 电阻配 10nF 电容估算截止频率约 1.6kHz适合 2kHz 采样的系统。采样时间也要注意F103 支持从 1.5 到 239.5 周期的采样时间档档位越高输入等效阻抗要求越低噪声抑制越好振动测量推荐 55.5 周期或 239.5 周期不要为了追求速率选最低档。2.3 定时器触发 ADC 外部触发 DMA 循环的配置下面的代码基于 STM32 标准外设库压缩包里的 stm32f10x_tim.c 印证了这套工程是在 Keil MDK SPL 环境下维护的。TIM3 的更新事件作为 TRGO 输出直接连到 ADC1 的外部触发输入ADC1 转换结果由 DMA1 通道 1 循环写入内存缓冲区CPU 完全不参与采样循环。#define FS 2000 #define SAMPLES 256 static uint16_t adc_buf[SAMPLES]; void TIM3_Init(void) { TIM_TimeBaseInitTypeDef tim; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); tim.TIM_Prescaler 72 - 1; tim.TIM_Period 1000000 / FS - 1; tim.TIM_CounterMode TIM_CounterMode_Up; tim.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, tim); TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update); TIM_Cmd(TIM3, ENABLE); } void ADC1_DMA_Init(void) { GPIO_InitTypeDef gpio; ADC_InitTypeDef adc; DMA_InitTypeDef dma; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); gpio.GPIO_Pin GPIO_Pin_0; gpio.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, gpio); dma.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; dma.DMA_MemoryBaseAddr (uint32_t)adc_buf; dma.DMA_DIR DMA_DIR_PeripheralSRC; dma.DMA_BufferSize SAMPLES; dma.DMA_PeripheralInc DMA_PeripheralInc_Disable; dma.DMA_MemoryInc DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; dma.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; dma.DMA_Mode DMA_Mode_Circular; dma.DMA_Priority DMA_Priority_High; dma.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel1, dma); DMA_ITConfig(DMA1_IT_TC1 | DMA1_IT_HT1, ENABLE); adc.ADC_Mode ADC_Mode_Independent; adc.ADC_ScanConvMode DISABLE; adc.ADC_ContinuousConvMode DISABLE; adc.ADC_ExternalTrigConv ADC_ExternalTrigConv_T3_TRGO; adc.ADC_DataAlign ADC_DataAlign_Right; adc.ADC_NbrOfChannel 1; ADC_Init(ADC1, adc); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_ExternalTrigConvCmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); }关键在DMA_Mode_Circular缓冲区写满后自动回绕采样不中断DMA_ITConfig同时打开半传输和全传输中断正好把 256 个点分成两个 128 点的块。ADC_ExternalTrigConvCmd必须调用它使 ADC 真正进入外部触发等待状态漏掉这一行ADC 永远不启动。定时器预分频 72-1 把 72MHz 降到 1MHzPeriod 设成 1000000/FS-1所以修改 FS 宏就能调整采样率不需要动定时器结构体。ADC_SampleTime_239Cycles5是档位名代表 239.5 个 ADC 时钟周期配合 12MHz ADC 时钟约 21μs 完成一次转换2kHz 采样率留有余量。2.4 中断里只做搬运不做计算半传输和全传输中断是采样链路的节拍器。每次进入中断DMA 已经写完了对应半段缓冲区直接调用 processBlock 处理 128 个点处理完要立即清标志否则会重复进入同一事件。void DMA1_Channel1_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_HT1)) { DMA_ClearITPendingBit(DMA1_IT_HT1); processBlock(0, SAMPLES / 2 - 1); } if (DMA_GetITStatus(DMA1_IT_TC1)) { DMA_ClearITPendingBit(DMA1_IT_TC1); processBlock(SAMPLES / 2, SAMPLES - 1); } }processBlock 内部做原始值到电压的换算、按传感器灵敏度转成加速度再调用串口发送。不要在中断里做浮点 FFTF103 没有 FPU一次 256 点复数蝶形运算要消耗接近毫秒已经超过下一次 DMA 中断间隔会直接破坏采样连续性。频谱分析放到上位机这也是把原始 ADC 值原样送上去的原因。提示采样率、缓冲区大小和串口发送频率三个参数是绑定的。2000/256 约每秒 7.8 帧每帧 520 字节左右460800 波特率下完全不紧张如果把采样率调到 10kHz就得缩小 SAMPLES 或提高波特率否则串口会成为瓶颈。3. 串口数据链路设计一份容错的二进制帧协议3.1 为什么必须加帧头、长度和校验振动数据是连续流STM32 每次发一包QT 端用 QSerialPort readAll 读回来的数据不会自动按帧切分。一次 readAll 可能同时包含上一帧的尾、这一帧的全体和下一帧的头。如果直接按长度截断偶尔一字节丢失就会导致整条波形错位。给数据加帧头、长度和校验是防御这种错位的标准做法。帧头告诉解析器从哪里开始长度告诉解析器一帧占多少字节校验让损坏帧不会进入绘图缓冲区。这个思路不只在 QT 上位机里适用任何 MCU 与 PC 的串口通信都建议按这个结构设计。3.2 帧格式定义与波特率计算协议帧总共 4 len 1 字节len 是数据区字节数。偏移字段说明00xAA帧头 110x55帧头 22len数据区字节数3type0x01 时域数据0x02 数字传感器数据4..4len-1datauint16 little-endian原始 ADC 值4lensum第 0 到 4len-1 字节累加和的低 8 位波特率选择以整帧发送时间不超过采样周期为原则。每秒 7.8 帧每帧 520 字节一个字节 10 bit波特率 460800 时发送一帧约 11.3ms而一帧对应的采样时间为 128ms串口占用很低。实际工程里我一般把波特率固定在 460800 或 921600配合 2kHz 采样率都能满足。3.3 STM32 端的打包发送实现发送函数把 processBlock 计算出来的样本数组拼成一帧。示例代码是阻塞发送优点是帧不会被拆散缺点是发送期间如果发生 DMA 半满中断响应会稍微延后对 460800 波特率和 520 字节的帧来说阻塞时间约 11ms中断延迟在可接受范围课程设计阶段先跑通这个版本比引入发送中断更好理解。static void send_frame(uint8_t type, const uint16_t *data, int n) { uint8_t frame[520]; uint8_t sum 0; int len n * 2; int i; frame[0] 0xAA; frame[1] 0x55; frame[2] len; frame[3] type; for (i 0; i n; i) { frame[4 i * 2] data[i] 0xFF; frame[5 i * 2] data[i] 8; } for (i 0; i 4 len; i) sum frame[i]; frame[4 len] sum; for (i 0; i 4 len 1; i) { while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) RESET); USART_SendData(USART2, frame[i]); } }数据区低字节在前与 x86 小端一致QT 端直接按小端拼接即可。校验累加从帧头开始到数据区最后一个字节结果再对 256 取模8 位校验能拦截绝大多数随机误码如果现场电磁干扰很强可以把累加和升级成 CRC8。3.4 QT 端解析缓冲区剥帧而不是一次性截取上位机最常见的错误是在 onReadyRead 里按当前可读字节数直接解析第二次读取时帧就乱了。正确做法是维护一个成员缓冲区收到数据先 append再用循环把完整帧逐帧剥出来。下面代码放在 MainWindow 的串口槽函数里。void MainWindow::onReadyRead() { m_buffer.append(serial-readAll()); while (true) { int head m_buffer.indexOf(QByteArray::fromHex(AA55)); if (head 0) { m_buffer m_buffer.right(2); return; } if (head 0) m_buffer.remove(0, head); if (m_buffer.size() 4) return; quint8 len m_buffer[2]; if (len 512) { m_buffer.clear(); return; } int total 4 len 1; if (m_buffer.size() total) return; quint8 sum 0; for (int i 0; i 4 len; i) sum m_buffer[i]; if (sum m_buffer[4 len]) { parseData((const quint8 *)m_buffer.constData() 4, len / 2); } m_buffer.remove(0, total); } }indexOf 搜索帧头找不到时保留最后两个字节是为了处理帧头恰好被串口分两次读到的边界情况。校验通过后parseData 里把每两个字节拼成 uint16val buf[i*2] | buf[i*21] 8再代入传感器换算公式得到物理量。len 上限 512 字节对应 256 个样本超过 512 说明同步丢失直接清空缓冲区重新等帧头。4. 上位机可视化QCustomPlot 实时波形与自写 FFT 频谱4.1 实时绘图的刷新节奏QCustomPlot 是 QT 生态里常用的图线绘制库性能比 QWidget 手动重绘好。振动测量每秒产生几千个样本如果每收到一帧都调用 replot界面会卡到帧率只有个位数。更稳的节奏是用一个 30ms 定时器聚合刷新绘图数据先存到滚动数组定时器到点后一次性更新两个 Plot一个时域一个频域。void MainWindow::onPlotTimer() { ui-plotTime-graph(0)-setData(timeAxis, wave); ui-plotFreq-graph(0)-setData(freqAxis, mag); ui-plotTime-xAxis-setRange(timeAxis.first(), timeAxis.last()); ui-plotTime-yAxis-rescale(); ui-plotFreq-yAxis-rescale(); ui-plotTime-replot(); ui-plotFreq-replot(); }这段代码中 setData 直接整体替换曲线数据比逐点 addData 效率高几个量级。xAxis-setRange 固定到当前窗口实现滚动效果rescale 让 y 轴自动跟随幅值变化观察振动趋势非常方便。注意 replot 只能在主线程调用如果串口解析放在 QThread 里必须 emit 信号回到主线程槽函数。4.2 256 点滑动 FFT频谱泄漏处理振动信号的频率成分比时域波形更有诊断价值。取最近一帧 256 点做 FFT采样率 2kHz频率分辨率约为 7.8Hz。FFT 对整周期截断非常敏感直接对矩形窗数据做变换非整数倍频率会泄漏成一片背景噪声所以要先乘汉宁窗。自写基2蝶形 FFT 的幅值计算代码比较简洁static QVectordouble computeMagnitude(const QVectordouble samples) { const int n samples.size(); QVectordouble re(n), im(n, 0.0); for (int i 0; i n; i) re[i] samples[i] * 0.5 * (1.0 - qCos(2.0 * M_PI * i / (n - 1))); fft(re.data(), im.data(), n, false); QVectordouble mag(n / 2); for (int i 0; i n / 2; i) mag[i] 2.0 * qSqrt(re[i] * re[i] im[i] * im[i]) / n; return mag; } static void fft(qreal *re, qreal *im, int n, bool inverse) { for (int i 1, j 0; i n; i) { int bit n 1; for (; j bit; bit 1) j ^ bit; j ^ bit; if (i j) qSwap(re[i], re[j]), qSwap(im[i], im[j]); } for (int len 2; len n; len 1) { qreal ang (inverse ? 2.0 : -2.0) * M_PI / len; qreal wRe qCos(ang), wIm qSin(ang); for (int i 0; i n; i len) { qreal curRe 1.0, curIm 0.0; for (int j 0; j len / 2; j) { qreal uRe re[ij], uIm im[ij]; qreal vRe re[ijlen/2]*curRe - im[ijlen/2]*curIm; qreal vIm re[ijlen/2]*curIm im[ijlen/2]*curRe; re[ij] uRe vRe; im[ij] uIm vIm; re[ijlen/2] uRe - vRe; im[ijlen/2] uIm - vIm; qreal tmp curRe * wRe - curIm * wIm; curIm curRe * wIm curIm * wRe; curRe tmp; } } } if (inverse) for (int i 0; i n; i) { re[i] / n; im[i] / n; } }汉宁窗会压低幅值所以最后除 n 再乘 2 做幅度恢复。fft 函数是在位蝶形实现要求 n 为 2 的整数幂。samples 的长度必须与下位机 SAMPLES 一致否则频谱横轴对应的最高频率会偏移。如果发现 50Hz 工频附近出现明显泄漏带多半是窗口没加成功或采样率并非严格 2kHz。4.3 原始 ADC 值到加速度物理量的换算串口上传的是原始 ADC 值物理量换算放上位机这样更换传感器或灵敏度参数不用重新刷 STM32。常见的 F103 模拟量读取场景ADC 输入电压 raw * 3.3V / 4096。下表列出三类传感器的典型换算参数。传感器灵敏度零点加速度公式模拟加速度计 ±1.6g57mV/g1.5V(V - 1.5) / 0.057ADXL345 4g 模式15.6 LSB/g0raw / 15.6MPU6050 2g 模式16384 LSB/g0raw / 16384如果采用数字加速度计STM32 通过 I2C/SPI 读取寄存器串口帧 type 字段改成 0x02数据区直接放传感器原始寄存器值上位机按对应灵敏度换算即可。时域图 y 轴单位显示 g频谱图建议用线性幅值 g或 20log10 转 dB两种都可以但一个系统内要保持一致否则对比数据时容易误判振动强度。5. 联调排错与验证技巧从一串毛刺定位故障路段5.1 最常踩的几个联调点联调阶段的现象往往指向多个可能原因。串口完全没数据可能线接反也可能 DMA 中断没启动波形出现周期性毛刺可能是电源纹波也可能上位机解析帧错位。下面是按概率排序的检查顺序。现象检查顺序串口无数据USB 转串口 RXD 是否接 STM32 TX地线是否同地输出乱码波特率是否一致RCC 是否 72MHz波形毛刺多ADC 采样时间太低传感器电源是否干净RC 滤波是否漏焊GUI 卡死是否在串口槽函数里做了耗时解析是否每次数据都 replot压缩包里的 QSerialPort_github.pro.user 和 Project.axf 容易让人误以为工程已经完整实际跑起来发现串口没反应时先检查串口号和波特率是否和代码一致再用串口助手直接看 MCU 发出来的字节能少走一半弯路。5.2 用一节电池做完整的信号链验证没有信号发生器时可以用干电池加 10kΩ 电阻手动制造方波电阻一端接电池正极另一端接 ADC 输入杜邦线反复触碰电池正极或断开产生 1Hz 级随机方波。看到上位机时域图出现明显的两段电平说明采集、串口组帧、QT 解析三级链路全部打通。再打开频谱图如果主峰出现在频率接近触碰频率的低频段而其他频段噪声底低于主峰 20dB 以上链路就没有引入额外强干扰。这个测试的巧妙之处在于它一次性验证了时域响应、FFT 功能、校验丢弃机制和坐标轴单位。如果触碰时频谱 50Hz 处跟着亮起来说明工频已经通过参考地侵入 ADC需要在传感器供电和模拟地布局上找原因。更严谨的做法是调整采样率到 2048Hz、缓冲区到 1024 点得到 2Hz 的频率分辨率再进行整周期正弦信号的定量标定。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。