拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32定时器触发ADC实现精准采样:CubeMX配置与实战

STM32定时器触发ADC实现精准采样:CubeMX配置与实战 做嵌入式开发有一段时间的朋友大概率会遇到这样一个场景系统需要按一个非常稳定的频率去采集模拟信号比如100kHz的电流采样或者电网同步采样里的工频倍数采样。这个“稳定”是重点可别小看它实测下来很多看似能跑的方案频率其实都是飘的。我最早做这类采样时习惯在定时器中断里置一个标志位然后主循环里看到标志位就启动一次ADC转换。原理上没错但实际用示波器一测触发间隔能差出好几个微秒中断响应延迟、ADC配置寄存器写入时间、主循环忙闲程度都会引入抖动。后来换了STM32CubeMX里“定时器硬件直接触发ADC”的方案CPU完全不用参与触发频率精准到定时器时钟周期的整数倍这才算把采样频率真正“钉”住了。这篇文章就是要把这套实战配置从头到尾讲清楚采样频率的计算方法、CubeMX图形化配置步骤、生成代码里哪些细节必须亲手处理以及实测中经常踩的坑。内容主要基于STM32F103系列展开但思路和公式对F4、G4、H7同样适用适合正在做数据采集、电机控制、电源转换、信号测量的朋友参考。1. 为什么非要“定时器触发ADC”而不是在定时器中断里启动转换先别急着打开CubeMX动手之前得把方案选对。不少人第一次接触“定时器触发ADC”这个概念时会下意识觉得多此一举我明明可以在定时器更新中断里调用HAL_ADC_Start()效果不是一样吗表面上都是“每隔固定时间去转换一次”但实际上差别非常大。1.1 定时器中断里手动启动ADC问题出在哪定时器中断里启动ADC核心问题有两个第一个是抖动。HAL_ADC_Start()这个函数内部要做的事情远比想象中多要检查状态、要配置DMA、要点外设代码执行时间不稳定。而定时器中断本身也有出入栈开销如果主循环里正好在处理其他中断或长时间临界区中断响应就会被延后。这些因素叠加起来触发间隔就不再是严格的“定时器周期”而是“定时器周期随机延迟”这就是抖动。第二个问题是CPU占用。如果采样频率是100kHz那CPU每10us就要进一次中断每次进去都要执行几十上百条指令。CPU大部分时间被采样操作占用算法任务只能见缝插针。更麻烦的是如果ADC转换期间主程序还想操作同一个ADC外设稍不留神就会产生访问冲突排查这种问题比解决采样频率本身还费时间。1.2 硬件触发链路的好处0抖动、0占用定时器触发ADC的硬件链路是定时器计数到某个条件后产生一个TRGO信号这个信号直接送到ADC的触发输入端ADC硬件检测到该信号后自动启动转换。整个过程中CPU不参与没有中断延迟没有代码执行时间差触发时刻由硬件电路直接决定精准度只取决于定时器本身的时钟精度。这个方案一上来就是“硬件级精准”。定时器的计数时钟来自晶振/锁相环误差在ppm量级触发间隔就是时钟周期的整数倍。做电机控制里的相电流采样时我实测过同一套配置连续几千个周期的触发间隔波动小于一个定时器计数周期。CPU占用方面也舒服得多。定时器只管发触发信号ADC转换完成后通过DMA把结果搬走全程不需要打断CPU。只有在DMA搬完一批数据后才会进一次中断通知CPU来取。CPU大部分时间都在做真正的数据处理而不是疲于应付采样。1.3 TRGO和ADC触发信号是如何映射的硬件触发不是随便找一个定时器就能直接接到ADC上的中间需要配置“触发源映射”。STM32的ADC通常有若干个外部触发源可选比如F103系列ADC1支持TIM1的TRGO、TIM2的TRGO、TIM3的TRGO等具体通道在CubeMX下拉框里都能看到。这里的核心对象是定时器的TRGOTrigger Output信号。定时器内部有好几个事件可以输出到TRGO比较匹配事件OCxREF、更新事件Update Event、使能信号Enable等。CubeMX里定时器配置页面有一个“Trigger Output (TRGO)”下拉框选“Update Event”就是计数器溢出时产生触发选“OC1REF”就是比较通道1电平变化时产生触发。这两种选择在采样场景里有明显区别。Update Event用起来最简单每次计数周期结束产生一次触发适合等间隔采样。OC1REF则可以将触发点精确放在PWM周期的某个相位上例如开关电源里希望采样点落在高边开关管导通的中间时刻这时用比较通道触发比用更新事件灵活得多。提示不管用哪种触发事件最终的核心配置规则是一致的——定时器产生触发信号的间隔决定了ADC的采样频率。2. 先算清楚这笔账采样频率到底由哪几个数决定配置CubeMX之前我强烈建议先把频率计算这一步做扎实。很多人在图形界面里随手填几个数最后发现实际采样频率和预期差了一倍回头排查半天其实是计算口径没搞对。2.1 定时器一端的公式PSC、ARR与计数时钟定时器触发频率的计算公式核心就一个触发频率 定时器计数时钟 / ((PSC 1) * (ARR 1))其中PSC是预分频系数定时器时钟先除以(PSC1)得到计数时钟ARR是自动重装载值计数器从0数到ARR产生一次更新事件。举个例子STM32F103的TIM2挂在APB1总线上当APB1分频系数为2时TIM2的时钟是72MHz这个倍频机制后面还会细说。如果设置PSC71计数时钟就是1MHz再设置ARR99那么溢出频率就是1MHz/10010kHz触发周期100us。如果目标频率是10kHz这样一组参数就是精准答案。这里有个地方容易误解CubeMX里PSC和ARR填的是“要加1之后的数还是原始值”答案是填入原始值库内部会帮你加1。所以PSC71表示72倍分频ARR99表示计数从0到99共100次。还要注意时钟树给定时器提供的时钟源。F103的APB1总线时钟如果设置为36MHz即AHB的72MHz二分频定时器时钟会自动乘以2变成72MHz。这也是很多人算出的频率和实测不一致的原因——没有把定时器时钟的×2机制考虑进去。2.2 ADC一端的底线转换时间必须小于触发周期定时器能发出多快的触发信号是一回事ADC能不能在这个时间内完成转换是另一回事。如果触发信号来了上一次转换还没结束ADC就会忽略这次触发或产生总线冲突实际采样频率根本达不到定时器标称的数值。ADC转换时间的计算公式是Tconv (采样时间 12.5) / ADC时钟频率这12.5个时钟周期是逐次逼近型ADC完成电荷重分配和比较所需的固定周期数12位分辨率下就是这个值。采样时间则可以在CubeMX里选择常用值有1.5、7.5、13.5、28.5、41.5、55.5、71.5、239.5个ADC时钟周期。举个实际例子STM32F103的ADC时钟最高为14MHz我在CubeMX里配置ADC预分频为6ADC时钟就是72/612MHz。如果选择采样时间为13.5个周期总的转换时间就是(13.512.5)/12MHz≈2.17us。那么理论上定时器触发频率最高可以到约460kHz实际工程中留50%以上余量比较稳妥。反过来在需要高精度采样的场景里如果你为了提高信号源阻抗适应性把采样时间拉长到239.5周期转换时间变成(239.512.5)/12MHz≈21us对应的最高采样率就掉到了约47kHz再往上调定时器频率数据就开始异常了。2.3 常用采样频率的PSC/ARR组合速查基于STM32F103的72MHz定时器时钟我整理了几个常用的组合直接照着配置就能用目标采样率PSCARR实际采样率触发周期1kHz719991kHz1ms10kHz719910kHz100us50kHz711950kHz20us100kHz719100kHz10us200kHz359200kHz5us最后一个组合验证一下定时器时钟72MHzPSC35分频后计数时钟为2MHzARR9表示计数10次触发频率2MHz/10200kHz。如果ADC时钟是12MHz、采样时间为7.5周期转换时间为(7.512.5)/12MHz≈1.67us小于5us的触发周期可以跑通。注意实际ADC时钟频率受APB2总线和ADC预分频影响不同型号略有差异组合前一定要先确认自己的时钟树配置。3. 手把手配置STM32CubeMX里定时器ADCDMA的完整设置这节我以STM32F103C8T6最小系统板为例目标是实现ADC1的通道1以10kHz频率采样用DMA把数据搬运到内存全程不占用CPU。打开STM32CubeMX按下面的步骤操作。3.1 新建工程与时钟树设置把系统时钟摆正新建一个MCU工程选择STM32F103C8Tx。首先进入“System Core - RCC”把HSE高速外部时钟设为“Crystal/Ceramic Resonator”。然后在“Clock Configuration”页面里把输入时钟设为8MHz具体看板载晶振频率有些板子是25MHz的要和原理图对齐将HCLK设为72MHz。设置HCLK为72MHz后CubeMX会自动调整AHB分频、APB1分频、APB2分频。这里要特别确认APB1分频是否为2此时APB1总线时钟36MHz但定时器时钟自动×2为72MHzAPB2分频是否为1APB2总线时钟72MHz。这个状态是正确的后面很多计算都基于这个时钟树。技巧晶振频率填错是最隐蔽的错误之一。芯片默认按外部晶振8MHz来锁相如果板子实际是25MHz晶振那系统时钟根本不是72MHz所有定时器频率也跟着全错。拿到新开发板第一件事就是查原理图的晶振参数。3.2 TIM2的触发输出配置左侧列表找到“Timers - TIM2”勾选“Activated”。页面里主要设置这几项Prescaler填入71对应72MHz/721MHz计数时钟Counter Mode选“Up”向上计数即可Counter Period填入99对应100次计数溢出Auto-reload preload建议Enable防止运行中更新ARR时产生毛刺Trigger Output (TRGO)选“Update Event”。TRGO选Update Event后TIM2每次计数溢出都会产生一个触发脉冲。如果你需要用比较通道把采样点放在特定相位则要额外配置某个OC通道的CCR值同时把TRGO选为对应的OC1REF。CubeMX的“NVIC Settings”里TIM2中断可以不勾选。用硬件触发ADC时定时器完全不需要进中断让它安静地跑计数就比较合适。当然如果你需要统计采样次数或定时校准可以勾选更新中断这个按需来。3.3 ADC1的触发源、对齐方式与采样时间左侧找到“Analog - ADC1”勾选“Activated”。然后进行以下配置IN1对应PA1引脚勾选作为采样通道。如果板上接的是电位器或传感器信号接到PA1即可Scan Conversion ModeDisable因为只采集一个通道Continuous Conversion ModeDisable。这个很关键使用外部触发场景下连续转换模式要关掉否则触发逻辑会乱Discontinuous Conversion ModeDisableExternal Trigger Source选“Timer 2 Trigger Out event”对应的就是TIM2的TRGOExternal Trigger Conversion Edge选“Rising Edge”让计数溢出时产生的上升沿信号触发转换Data Alignment选“Right alignment”12位数据右对齐CPU处理起来最直观Sampling Time选“13.5 Cycles”兼顾精度和速度。这里有个细节External Trigger Source和Conversion Edge的选项在不同型号的CubeMX配置界面里略有差异但意思是一样的从触发源中选择TIM2 TRGO边沿选上升沿。3.4 DMA搬运数据的配置思路ADC转换结果保存在只有一位的数据寄存器里如果不及时取走下一次转换会直接覆盖掉。最省心的做法就是让DMA自动搬运。进入“ADC1 - DMA Settings”点击“Add”添加DMA请求。DMA模式选“Circular”数据宽度Data Width建议设成Half Word因为ADC结果用16位就装得下。Direction保持“Peripheral To Memory”。为什么要选Circular模式因为定时器会持续不断地触发ADC数据源源不断产生。循环模式让DMA搬完一整轮后自动回到起点继续搬省去反复启停的麻烦。如果选Normal模式搬完一轮DMA就停了ADC数据开始覆盖后续全是废数据这一点务必注意。DMA设置完成后还需要定义一个足够大的数组来接收数据。CubeMX的DMA配置里不直接指定数组但后续在代码里你完全可以手动定义一个如uint16_t adc_data[1024]的数组在调用HAL_ADC_Start_DMA时传进去。4. 代码生成之后还要亲手处理的关键细节CubeMX生成代码后工程可以直接编译下载但离“精准采样”还差几处关键的代码补充。这些细节如果不处理硬件基本不动功能无法真正跑起来。4.1 CubeMX生成代码中不能乱动的部分自动生成的MX_ADC1_Init()、MX_TIM2_Init()是核心初始化结构基本固定不需要大改。但有两个细节要留意。第一是ADC的ExternalTrigConv赋值。生成代码里类似这样hadc1.Init.ExternalTrigConv ADC_EXTERNALTRIGCONV_T2_TRGO;如果在CubeMX里选好了TIM2触发这行代码就是定时器触发ADC的“开关”。如果你发现这个值变成ADC_SOFTWARE_START说明CubeMX界面里触发源没选对或者没有选中“Timer 2 Trigger Out event”。第二是TIM2的MasterConfig。对应代码是sMasterConfig.MasterOutputTrigger TIM_TRGO_UPDATE;这行决定了TRGO输出哪个事件。如果你希望在特定相位触发ADC这里会变成TIM_TRGO_OC1REF。这两个参数任何一个偏了触发链路都接不通。4.2 别忘了ADC校准与启动顺序STM32F1系列的ADC有一个校准功能可以消除内部电容失配带来的偏移误差。校准不能随便做必须在ADC没有开始转换时进行并且最好在每次上电初始化后执行一次。在main()中初始化外设后先启动定时器、再启动DMA模式ADC这个顺序不能乱HAL_TIM_Base_Start(htim2); HAL_ADCEx_Calibration_Start(hadc1); HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_data, ADC_BUFFER_SIZE);先把定时器跑起来这样校准和DMA启动时定时器的触发信号就已经在了链路是通的。校准函数必须要在HAL_ADC_Start_DMA之前调用否则校准时的启动条件不满足结果可能异常。启动DMA后ADC就开始等定时器触发信号了。定时器每产生一次更新事件ADC转换一次DMA自动把结果搬进数组。整个过程不需要CPU介入。4.3 回调函数到底在哪一层处理比较好数据采集进去了总得让CPU知道。最常用的回调是HAL_ADC_ConvCpltCallback。这个回调在DMA把指定长度的数据全部搬运完成后触发。如果你的缓冲区设置成长度1024那每采集满1024个点就会进入一次回调。void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { adc_data_ready 1; } }在回调里只是置一个标志位会建议性更强一些数据处理放到主循环里完成避免长时间占用中断上下文。如果一定要在回调里做运算请确保运算时间远小于DMA填满整个缓冲区的时间否则数据刷新速度会追不上处理速度。如果你的应用需要更细粒度的采样结果例如每一个采样点都要立刻用到可以把DMA缓冲区长度设成1这样每次转换完成都会触发一次回调。代价是CPU会被频繁打扰失去了DMA批量搬运的意义。一般更推荐把缓冲区设大配合DMA半满中断实现“乒乓缓冲”既保证数据连续性又留足处理时间。4.4 如何实测验证采样频率是否真的精准配置完之后不要直接相信“看起来频率对了”拿仪器验证一下更靠谱。最简单的方法是在定时器更新中断里反转一个GPIO用示波器或频率计测量这个GPIO的翻转频率。由于定时器触发频率和更新事件频率相同翻转频率的一半就是实际采样频率。更好的验证方法是给ADC引脚输入一个已知频率的正弦波用信号发生器生成比如1kHz然后以宣称的10kHz采样率采集一段时间对数据作FFT观察频谱峰值是否精确落在1kHz位置。实测下来如果定时器时钟稳定FFT峰值和输入频率的偏差通常小于0.1%这个结果比任何口头保证都有说服力。如果没有信号发生器也可以用ADC采样一个5V串联电阻分压出的直流电压前后改变电阻值观察采集数值更新速度是否符合设定的采样周期。这个方法粗糙一点但对验证链路是否通很快、很直接。5. 实测翻车记录常见问题与排查速查配置思路上讲完了但实际调试中总会出现几个高发问题。我把自己的“翻车记录”整理出来包括现象、原因和解决办法照着排查能省不少时间。5.1 采样频率翻倍、减半或者完全不触发现象实测触发频率是预期值的2倍、0.5倍或者ADC根本不转换。先看定时器配置。如果频率翻倍常见原因是分频数设得不对。比如你填了PSC71但库里实际使用的是72倍分频这时候若把PSC误填成72实际分频就是73倍频率会略低而不是翻倍。真正导致翻倍的情况通常是把ARR填成了计数次数而不是重载值比如想ARR99却填成100计数多了1次频率约降1%。频率减半最常见的原因是APB1预分频理解错了。CubeMX的时钟树里如果把APB1分频从2改成4定时器时钟从72MHz变成36MHzPSC71时计数时钟500kHz触发频率自然就减半了。这一点在F1系列上尤其容易忽略。完全不触发的话优先查三个方面TIM2的TRGO事件选的是不是Update EventADC触发源选的到底是不是TIM2以及定时器是否真的启动了。代码里忘了HAL_TIM_Base_Start(htim2)是新手最常踩的坑——ADC没有触发信号自然一动不动。5.2 同一数值反复刷新看起来像没有在采样现象DMA缓冲区里的数据始终是同一个值或者变化极其缓慢完全不像在实时采集。先排除信号源的问题用万用表量一下ADC引脚确认电压确实在变化。如果电压变了但数据不变问题大概率出在DMA配置或内存访问上。检查DMA数据宽度是否是Half Word方向是否为“外设到内存”。如果配置成Byte宽度高位字节会丢失数据看起来会整体偏移如果方向设反数据完全不动。另外确认DMA的缓冲区地址是否有效如果传入的是局部变量数组地址DMA写入的内容会在函数返回后失效看起来就像没采样到。还遇到过一种情况DMA配置成了Normal模式搬完一轮后停摆ADC继续转换但没人取数后面缓冲区里全是最早的值自然“看起来没变化”。改成Circular模式即可。5.3 多通道数据错位现象采集2个或更多通道时通道A的数据出现在通道B的位置或者通道数据顺序不稳定和预期对应不上。先确认CubeMX里ADC规则组的“Number Of Conversion”与通道排列。比如有两个通道要先把IN1设为转换序列的第1个IN2设为第2个顺序设置错会直接导致数据错位。再看DMA缓冲区长度。多通道场景下每触发一次会依次转换序列中的所有通道DMA缓冲区长度应该等于“通道数×单通道的搬运次数”。如果缓冲区长度只设为1数据就会互相覆盖取回来的数据自然无法对应通道。最后是扫描模式的配置。多通道采集时要把Scan Conversion Mode设为Enable让每帧触发信号到来时按序列顺序扫过所有通道。如果忘记开启扫描模式每次触发只会转换序列里的第一个通道其他通道的数据永远出不来。5.4 数据毛刺大、跳变明显现象采集到的正弦波或直流电压上出现明显的毛刺数据跳变几甚至几十个LSB数据不光滑。毛刺来源通常是两层。第一层是信号源阻抗过高。ADC内部有一个采样电容采样开关闭合的瞬间电容要对信号源充电如果信号源阻抗太大电容充电时间不够采样结果就会偏小或抖动。解决办法是增大ADC采样时间把采样时间从13.5周期改成71.5或239.5周期或者在信号源和ADC之间加一级电压跟随器运放接成单位增益缓冲。第二层是硬件布局和参考电压问题。ADC引脚走线如果和开关节点、PWM线路靠得太近高频噪声会耦合进来。实测中把ADC引脚和PWM引脚拉开距离、靠近模拟地放置毛刺明显改善。另一个容易忽略的点是VREF如果开发板VREF没有用独立基准源而是直接在电源上取了3.3V电源纹波会直接反映在采样结果里这时即使软件怎么滤波都压不住低频分量。5.5 快速排查速查表把上面这些经验汇总成一张表建议收藏备用现象可能原因排查/解决方向采样频率与预期不符APB1分频、定时器时钟倍频理解错误确认时钟树里定时器时钟数值完全不触发转换TRGO事件选错、触发源没选对、定时器未启动检查MasterOutputTrigger、ExternalTrigConv、HAL_TIM_Base_Start数据一直不变DMA配置错误、缓冲区地址失效检查DMA宽度、方向、缓冲区生命周期多通道数据错位通道顺序、扫描模式、DMA长度配置错误确认规则组顺序、Scan Enable、缓冲区长度毛刺多、跳变大信号源阻抗过高、VREF不干净、布线耦合增大采样时间、加跟随器、检查布局6. 把触发时刻“准”到相位级别扩展玩法和进阶思路基础版定时器触发ADC已经能解决绝大多数“固定频率采样”需求但实际工程中还有一个更高级的需求不仅采样频率要准采样时刻在PWM周期内的位置也要准。比如电机控制里最常用的“同步采样”就是典型场景。开关电源或逆变器控制中电流波形通常是三角波或锯齿波采样点落在开关周期的不同相位读到的电流值差异很大。如果你只在固定时间间隔采样不关心和PWM载波的相对关系采回来的就是“随机相位”的电流做闭环控制时会发现电流环的响应忽好忽坏。这时候把定时器配置为PWM输出模式比如中心对齐模式ARR控制PWM周期CCR控制占空比然后将TRGO事件从Update Event改成OC1REF。这样每隔一个PWM周期在比较值到达的瞬间触发一次ADC采样采样点就稳定地落在指定的相位上。我在做三相电机FOC控制时相电流采样就是这个方案每载波周期触发一次采样点放在PWM开启后的中点位置电流纹波最小、信号最干净。DMA的半满中断配合乒乓缓冲也是工程化采样的常见做法。缓冲区长度设成2的整数次幂比如256DMA搬完前半段触发半满中断CPU处理前128个点同时DMA继续往后搬再搬完128个点触发满中断CPU处理剩余数据。ADC不断采样CPU交错处理数据流无缝衔接。这个模式适合需要高吞吐加上实时处理的场景比如音频采集、振动信号分析。最后再分享一个经验如果你要采集的通道数不多但频率要求高可以考虑用双ADC交替采样比如F103的ADC1和ADC2可以配置成同步规则组配合定时器触发等效采样率直接翻倍。这种玩法在更复杂的信号采集项目里非常有用我准备后面单独写一篇实操记录。做嵌入式就是这样从“能跑”到“跑得准”之间往往就差这几步。定时器触发ADC这套方案一旦亲手配一遍、实测验证过频率以后做任何需要精准采样的项目都会很自然地想到用它。
返回列表