新闻详情

新闻详情

首页 / 资讯中心 / 详情

STM32F407定时器3触发ADC1+DMA正弦波采集与LCD显示实战

发布时间:2026/9/28 4:38:41来源:尧图网络
STM32F407定时器3触发ADC1+DMA正弦波采集与LCD显示实战
1. 项目缘起与整体设计思路1.1 为什么偏偏选定时器3来触发ADC1做嵌入式信号采集的朋友大概率都遇到过这个场景想用单片机采一个正弦波拿示波器一看波形乱七八糟频率稍微高一点就完全变形。很多人第一反应是ADC不够快其实十有八九是采样时机没控制好。用软件循环里手动启动ADC采样间隔完全取决于代码执行时间抖动大得离谱采出来的波形自然没法看。这个项目的核心思路很明确让定时器3TIM3的更新事件去硬件触发ADC1把采样节奏交给硬件CPU只负责搬运数据。这样做的好处是采样间隔由定时器周期严格决定抖动可以压到纳秒级采正弦波这种对等间隔要求高的信号效果立竿见影。选TIM3而不是TIM1或TIM2是有实际考量的。STM32F407的TIM1是高级定时器功能强但配置复杂还牵扯到刹车、死区这些电机控制相关的东西用来做单纯的触发源属于杀鸡用牛刀。TIM2和TIM5是32位定时器做触发源没问题但如果你后面还想用它们做别的计时任务资源就紧张了。TIM3是通用定时器16位挂在APB1总线上触发输出TRGO配置简单而且它和ADC1的配合在参考手册里有明确的映射关系踩坑概率最低。提示TIM3挂在APB1上APB1的时钟是84MHzF407主频168MHz时的标准配置但定时器时钟会倍频到168MHz这个细节后面算采样率的时候会用到别搞错了。1.2 整体数据流是怎么走的整个系统的数据流可以这样理解TIM3按设定频率产生更新事件通过TRGO输出触发信号ADC1收到触发后启动一次转换转换完成通过DMA把数据搬到内存缓冲区CPU在主循环里处理缓冲区数据并刷新LCD显示。这条链路里DMA是绝对不能省的一环。如果不用DMA每次ADC转换完成都要进中断采样率一高中断频率就爆炸CPU根本干不了别的活。用DMA之后ADC每转换一次DMA自动搬一个数据搬够设定数量才产生一次中断CPU负担瞬间降下来。LCD显示这块我选的是常见的SPI接口TFT屏分辨率128x160或者240x320都行。显示内容主要是实时波形和几个关键参数采样率、信号频率估算值。这里要注意LCD刷新不能放在ADC中断里做否则会严重拖慢采样节奏正确做法是在主循环里定时刷新。1.3 方案选型的几个关键取舍第一个取舍是采样率定多少。采正弦波按照奈奎斯特定理采样率至少是信号频率的2倍但实际工程里2倍根本不够看波形会严重失真。一般建议至少10倍以上想要波形好看20倍到50倍比较稳妥。假设你要采1kHz的正弦波采样率定在20kHz到50kHz之间比较合适。这个采样率决定了TIM3的触发频率也决定了DMA缓冲区的更新速度。第二个取舍是DMA用普通模式还是循环模式。普通模式下DMA搬完设定数量的数据就停了需要重新配置才能继续。循环模式下DMA搬完一圈自动回到起点继续搬适合连续采集。做实时波形显示循环模式更省心但要注意缓冲区大小和读取速度的匹配别出现读的时候数据被覆盖的情况。第三个取舍是ADC分辨率用12位还是降低到10位或8位。12位精度最高但转换时间也最长。F407的ADC在12位分辨率下最快转换时间是3个ADC时钟周期加12个采样周期左右。如果采样率要求很高可以适当降低分辨率换取速度。不过采正弦波一般对精度有一定要求12位是首选速度不够就调采样周期。2. 核心细节解析与实操要点2.1 时钟树配置一切计算的基础STM32F407的时钟配置是整个项目的地基采样率算得对不对全看时钟树配得准不准。标准配置是外部晶振8MHz经过PLL倍频到168MHz作为系统时钟。AHB不分频也是168MHz。APB1分频系数设为4所以APB1时钟是42MHz。但注意定时器挂在APB1上时如果APB1分频系数不为1定时器时钟会自动倍频所以TIM3的实际时钟是84MHz。等等这里有个容易搞混的地方。F407的APB1最大时钟是42MHzAPB2最大是84MHz。TIM3挂在APB1上APB1预分频器设为4时定时器时钟是APB1时钟的2倍也就是84MHz。而TIM1、TIM8这些高级定时器挂在APB2上APB2预分频器设为2时定时器时钟是168MHz。这个区别在算定时器周期时非常关键。假设我们要TIM3产生20kHz的触发频率定时器时钟84MHz那么定时器计数频率 84MHz / (预分频器1)触发频率 计数频率 / (自动重装载值1)取预分频器为0计数频率就是84MHz。自动重装载值 84MHz / 20kHz - 1 4200 - 1 4199。这样TIM3每50微秒产生一次更新事件触发ADC采样一次。注意自动重装载值不能设为0否则定时器行为异常。预分频器和自动重装载值的取值范围都是0到65535算出来超过这个范围就要调整预分频器。2.2 ADC1的触发源配置别选错了F407的ADC1有多个外部触发源包括TIM1的各个通道、TIM2的各个通道、TIM3的TRGO、TIM4的TRGO等等。在CubeMX里配置的时候要找到ADC1的External Trigger Conversion Source选项选择Timer 3 Trigger Out event。这里有个坑TIM3的TRGO可以来自多个事件包括更新事件、捕获比较事件、OC1REF、OC2REF等。我们要的是更新事件所以在TIM3的配置里要把Trigger Event Selection设为Update Event。如果选错了ADC触发频率会和你算的对不上。ADC1本身的配置也要注意几点分辨率12位扫描模式如果只采一个通道Disable就行如果要多通道轮流采要Enable连续转换模式Disable因为我们靠外部触发不是自由跑DMA连续请求Enable这样每次转换完都会发DMA请求采样时间根据信号源阻抗来定一般选84周期或144周期比较稳采样时间这个参数很多人忽略但它直接影响转换结果的准确性。信号源阻抗高的话采样时间太短采样电容充不满转换结果会偏小。F407的ADC采样时间可选3、15、28、56、84、112、144、480个ADC时钟周期。ADC时钟一般配置为21MHzAPB2的84MHz再分频84个周期大约是4微秒对于大多数信号源够用了。2.3 DMA配置循环模式下的缓冲区管理DMA的配置有几个关键点。首先是外设地址要指向ADC1的数据寄存器。内存地址指向我们定义的缓冲区数组。传输方向是外设到内存。数据宽度外设和内存都设为半字16位因为ADC结果是12位的用16位变量存刚好。模式选择这里要重点说。循环模式下DMA搬完指定数量的数据后自动回到起点适合连续采集。但缓冲区大小要合理设置太小了CPU来不及读就被覆盖太大了内存占用高且显示延迟大。一般设256或512个点比较合适。还有一个细节是DMA中断。循环模式下可以在传输完成一半和全部完成时各产生一次中断这样CPU可以在DMA搬前半部分时读后半部分实现乒乓缓冲进一步降低数据竞争的风险。不过对于LCD显示这种对实时性要求不极端的场景简单地在全部完成时中断一次也够用。// DMA配置示例基于标准外设库风格CubeMX生成的代码结构类似 #define ADC_BUF_SIZE 512 uint16_t adcBuffer[ADC_BUF_SIZE]; // DMA初始化关键参数 DMA_InitTypeDef DMA_InitStructure; DMA_InitStructure.DMA_Channel DMA_Channel_0; DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStructure.DMA_Memory0BaseAddr (uint32_t)adcBuffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralToMemory; DMA_InitStructure.DMA_BufferSize ADC_BUF_SIZE; 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_FIFOMode DMA_FIFOMode_Disable; DMA_Init(DMA2_Stream0, DMA_InitStructure);2.4 LCD显示部分别让刷新拖后腿LCD显示这块我用的是SPI接口的TFT屏驱动芯片常见的是ST7735或ILI9341。显示内容分两块一块是波形区域把ADC缓冲区里的数据映射成像素点画出来另一块是参数区域显示当前采样率、缓冲区大小、信号峰峰值等信息。波形绘制有个技巧不要每次刷新都清屏重画那样闪烁严重且速度慢。正确做法是用一个上一帧数据数组比较当前值和上一帧值只擦除变化的部分。或者更简单粗暴但有效的方法用双缓冲在内存里画好一整帧再一次性推送到LCD。SPI的时钟频率也要注意太快了LCD可能跟不上太慢了刷新率上不去。一般设到10MHz到20MHz之间比较稳妥。如果LCD支持DMA传输那就更好了SPI的DMA发送可以进一步解放CPU。实操心得LCD的背光控制最好单独用一个GPIO或者PWM控制调试的时候可以把背光关掉减少干扰也方便观察功耗变化。3. 实操过程与核心环节实现3.1 CubeMX配置全流程打开CubeMX选好STM32F407芯片型号第一步先把时钟树配好。外部晶振选HSEPLL Source选HSE然后按照168MHz的目标频率配置各个分频系数。具体参数PLLM8PLLN336PLLP2PLLQ7。这样系统时钟就是168MHzUSB时钟48MHz都符合规范。第二步配置TIM3。在Timers里找到TIM3Clock Source选Internal Clock。Prescaler设为0Counter Period设为4199对应20kHz触发频率。Trigger Event Selection选Update Event。这样TIM3的TRGO就会在每次更新时输出一个脉冲。第三步配置ADC1。在Analog里找到ADC1勾选你要用的通道比如IN0对应PA0引脚。Parameter Settings里Mode选IndependentResolution选12位Scan Conversion Mode根据通道数决定Continuous Conversion Mode选DisableDMA Continuous Requests选Enable。External Trigger Conversion Source选Timer 3 Trigger Out eventExternal Trigger Conversion Edge选Rising edge。第四步配置DMA。在ADC1的DMA Settings里点Add选DMA2 Stream0 Channel0这是ADC1的固定映射。Mode选CircularData Width外设和内存都选Half Word。Priority选High。第五步配置LCD的SPI接口。假设用SPI1配置为全双工主模式时钟分频到10MHz左右。再配几个GPIO作为LCD的DC、RST、CS控制引脚。第六步配置一个串口用于调试输出方便打印采样率和调试信息。生成代码的时候Toolchain选你用的IDE比如MDK-ARM或STM32CubeIDE。3.2 关键代码实现与参数计算生成代码后在main函数里启动各个外设。启动顺序很重要先启动DMA再启动ADC最后启动定时器。如果顺序反了可能出现定时器已经开始触发但ADC还没准备好的情况。// 启动顺序DMA - ADC - TIM HAL_DMA_Start(hdma_adc1, (uint32_t)ADC1-DR, (uint32_t)adcBuffer, ADC_BUF_SIZE); HAL_ADC_Start_DMA(hadc1, (uint32_t*)adcBuffer, ADC_BUF_SIZE); HAL_TIM_Base_Start(htim3);采样率的计算再强调一遍TIM3时钟84MHz预分频0自动重装载4199触发频率 84MHz / (01) / (41991) 20kHz。ADC每次触发转换一次所以采样率就是20kHz。如果信号是1kHz正弦波每个周期采20个点波形能看出大致形状但不够平滑。想要更平滑把自动重装载值改小比如2099触发频率就是40kHz每个周期40个点效果好很多。ADC的转换时间也要算一下确保小于触发间隔。ADC时钟配置为21MHz采样时间84周期转换时间12周期总共96周期约4.57微秒。触发间隔是50微秒20kHz远大于转换时间没问题。如果触发频率提高到100kHz间隔10微秒仍然大于4.57微秒也还能工作但余量就不大了。3.3 数据处理与波形显示ADC采到的原始数据是12位的范围0到4095。要显示成波形需要做几步处理。首先是去直流正弦波可能叠加了直流偏置算一下缓冲区的平均值每个点减去平均值。然后是归一化找到最大值和最小值把数据映射到LCD的Y轴范围内。// 简单的去直流和归一化处理 void processWaveform(uint16_t *buf, uint16_t len, uint8_t *yPoints, uint8_t yMax) { uint32_t sum 0; uint16_t min 4095, max 0; for (int i 0; i len; i) { sum buf[i]; if (buf[i] min) min buf[i]; if (buf[i] max) max buf[i]; } uint16_t avg sum / len; uint16_t range max - min; if (range 0) range 1; for (int i 0; i len; i) { int32_t centered (int32_t)buf[i] - avg; int32_t scaled (centered * yMax) / range yMax / 2; if (scaled 0) scaled 0; if (scaled yMax) scaled yMax; yPoints[i] (uint8_t)scaled; } }显示的时候把yPoints数组里的值作为Y坐标X坐标按顺序排列用LCD画点函数画出来。如果缓冲区有512个点而LCD宽度只有128像素需要做降采样每4个点取一个或者取每4个点的平均值。3.4 实测波形与参数调整记录我第一次测试的时候用信号发生器输入1kHz、1Vpp的正弦波采样率设的20kHz。LCD上显示的波形能看出正弦形状但毛刺比较多。分析原因一是采样点少二是信号发生器的输出阻抗和ADC采样时间不匹配。把采样率提高到50kHz自动重装载值改为1679波形明显平滑了。再把ADC采样时间从84周期增加到144周期毛刺进一步减少。这说明信号源阻抗偏高需要更长的采样时间让采样电容充分充电。后来我又试了不同频率的信号。100Hz的时候50kHz采样率每个周期500个点波形非常漂亮。10kHz的时候每个周期只有5个点波形就严重失真了这时候需要把采样率提高到200kHz以上但ADC转换时间就成了瓶颈需要降低分辨率或者减少采样周期。注意采样率不是越高越好要综合考虑ADC转换时间、DMA传输速度、CPU处理能力和LCD刷新率。盲目提高采样率可能导致数据丢失或显示卡顿。4. 常见问题与排查技巧实录4.1 波形显示异常的问题排查问题一波形完全是一条直线。这种情况先检查ADC有没有在转换。可以在DMA中断里翻转一个GPIO用示波器看有没有脉冲。如果没有说明ADC没启动或者触发源没配好。检查TIM3的TRGO有没有输出CubeMX里Trigger Event Selection是不是选的Update Event。还要确认ADC的External Trigger Conversion Source选对了没有。问题二波形频率对不上。比如输入1kHz显示出来是2kHz或者500Hz。这通常是采样率算错了。回头检查TIM3的时钟频率确认APB1的分频系数和定时器倍频关系。用示波器直接测TIM3的TRGO输出引脚如果有引出的话或者测DMA中断的频率反推实际采样率。问题三波形有规律性的跳变。这往往是DMA缓冲区被覆盖导致的。CPU读数据的速度跟不上DMA写入的速度读到的数据一半是新的一半是旧的。解决办法是加大缓冲区或者用双缓冲机制在DMA传输完成一半时就处理前半部分数据。问题四波形幅度不对。检查ADC的参考电压。F407的VREF默认接VDDA一般是3.3V。如果VDDA不稳或者有噪声转换结果会波动。可以在VREF引脚加一个0.1uF的陶瓷电容滤波。另外检查信号源的幅度有没有超出ADC输入范围超过了会削顶。4.2 常见问题速查表现象可能原因排查方法解决方案无波形显示ADC未启动测DMA中断频率检查启动顺序先DMA后ADC再TIM波形频率错误定时器时钟算错示波器测TRGO重新计算预分频和重装载值波形跳变DMA缓冲区覆盖观察跳变周期加大缓冲区或启用双缓冲幅度不准参考电压不稳万用表测VDDA加滤波电容检查信号源幅度波形毛刺多采样时间不足增大采样时间试试增加ADC采样周期数LCD刷新慢SPI时钟太低测SPI时钟频率提高SPI分频或改用DMA传输数据偶尔丢失中断优先级冲突检查NVIC配置调整DMA和ADC中断优先级4.3 几个容易踩的坑和独家技巧坑一忘了使能DMA的循环模式。默认是普通模式搬完一圈就停了波形只显示一瞬间就没了。在CubeMX里一定要把DMA Mode改成Circular。坑二ADC的DMA请求没使能。CubeMX里ADC配置有个DMA Continuous Requests不勾选的话ADC只会在第一次转换时发DMA请求后面就不发了。这个选项默认可能是不勾的要手动勾上。坑三中断优先级配错。DMA中断和ADC中断如果优先级冲突可能导致数据竞争。建议DMA中断优先级设得比ADC中断高或者干脆不用ADC中断只用DMA中断。技巧一用定时器的主从模式做更灵活的触发。如果以后需要多个ADC同步采样可以用一个定时器做主多个定时器做从实现更复杂的触发逻辑。技巧二在LCD上叠加显示触发信号。把TIM3的TRGO输出引到一个GPIO在LCD上画一条竖线表示触发时刻可以直观地看到采样点和信号相位的对应关系。技巧三用串口DMA发送波形数据到上位机。如果LCD显示不够直观可以把ADC数据通过串口DMA发到电脑上用Python或MATLAB画图分析。串口波特率设高一点比如921600传输512个16位数据大概11毫秒对实时性影响不大。技巧四ADC校准别忘了。F407的ADC在上电后建议执行一次校准可以显著提高精度。HAL库里有HAL_ADCEx_Calibration_Start函数在启动ADC之前调用一下。// ADC校准 HAL_ADCEx_Calibration_Start(hadc1); // 然后再启动DMA和ADC这个校准过程大概需要几毫秒只在上电初始化时做一次就行不用每次采样都做。4.4 性能优化与扩展思路如果发现CPU占用率还是偏高可以进一步优化。比如把LCD刷新频率降下来波形数据每采集10次刷新一次中间的数据用于计算峰峰值和频率。或者用硬件SPI的DMA传输把LCD数据搬运也交给DMACPU只负责准备数据。扩展方面这个框架可以很容易地改成多通道采集。把ADC的扫描模式打开配置多个通道DMA缓冲区里就会按顺序存放多个通道的数据。显示的时候分区域画不同通道的波形就行。还可以加一个简单的频率测量功能。对ADC数据做过零检测数一数单位时间内过零的次数就能估算出信号频率。这个功能在测量未知信号时很有用。最后再分享一个小技巧如果信号频率很低比如几十赫兹可以用定时器的输入捕获功能直接测频率比ADC采样再计算更准。ADC采样适合看波形形状测频率用输入捕获更专业。两者结合一个看长什么样一个看多快互补。我在实际调试中发现把ADC的采样时间从84周期增加到144周期后1kHz正弦波的波形毛刺明显减少但采样率上限也相应降低了。所以采样时间和采样率之间要权衡根据实际信号源阻抗来定。信号源阻抗低比如运放输出采样时间可以短一些阻抗高比如传感器直接输出采样时间就要长一些。这个没有固定公式实测调整最靠谱。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Flutter + OpenHarmony智慧养老护理App开发实践与踩坑记录 2026/9/28 5:45:14

Flutter + OpenHarmony智慧养老护理App开发实践与踩坑记录

做智慧养老这个方向,最快让我头皮发麻的不是业务复杂度,而是终端碎片化。项目上线那会儿,护理员手里的主力机还是三四年前的老安卓,楼层值班台配的是OpenHarmony的平板,管理端又有Windows的网页后台,老人房…

阅读更多 →
OpenClaw实战:从本地部署到Chrome扩展让AI自动检索新闻 2026/9/28 5:45:14

OpenClaw实战:从本地部署到Chrome扩展让AI自动检索新闻

1. 从"养虾"到给AI装上"手":这个项目到底在折腾什么先解释一下标题里的"养虾记录"。这两个字不是水产养殖,是我个人维护的一个记录频道名字,平时喜欢在里头随手记点技术折腾、实验过程和踩坑笔记,像…

阅读更多 →
提示工程数据治理新思路:用去中心化存储管好Prompt版本 2026/9/28 5:45:13

提示工程数据治理新思路:用去中心化存储管好Prompt版本

做提示工程最怕的一件事就是“谜一样的 Prompt”。你可能发现上个月还在正常输出的某条链路,这个月就莫名其妙“退化”了,怎么调试都找不到原因,最后定位到是有人悄悄改了 Prompt 里的两个参数。结果等于整个 AI 应用的行为被一个没有版本记录…

阅读更多 →
前后端分离应急物资管理系统实战:SpringBoot+Vue部署全攻略 2026/9/28 5:45:13

前后端分离应急物资管理系统实战:SpringBoot+Vue部署全攻略

2. 前后端分离的应急物资管理系统项目实战复盘做完整源码项目这么多年,前后端分离 应急物资管理系统这个组合我前前后后带过不少团队落地。选这个项目类型很讲究:核心业务清晰(物资入库、出库、库存盘点、预警统计),技…

阅读更多 →
基于Unet的眼底血管分割实战:数据集切片、训练与推理全流程 2026/9/28 5:45:06

基于Unet的眼底血管分割实战:数据集切片、训练与推理全流程

简介:本资源面向医学图像分割初学者与深度学习实践者,提供一套基于U-Net的眼底血管二分类分割完整方案,解决从数据准备到模型推理的全流程问题。压缩包共216个文件,以182张png切片图像、8个py脚本、1个pth权重文件及若干pyc、xml、…

阅读更多 →
Spring Boot SSE实时推送实战:原理、选型与避坑指南 2026/9/28 5:45:06

Spring Boot SSE实时推送实战:原理、选型与避坑指南

1. 实时数据推送的技术选型:为什么这次我押注SSE先说结论:在Spring Boot项目里做实时数据推送,很多人第一时间会想到WebSocket,但真正落到业务里,SSE(Server-Sent Events,服务端发送事件&#x…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉