ARTICLE DETAIL

资讯详情

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

STM32 DAC+定时器+DMA实现三角波:频率与幅度精准控制

STM32 DAC+定时器+DMA实现三角波:频率与幅度精准控制 1. 为什么DAC加定时器是波形生成的最优解1.1 从一次踩坑经历说起刚接触STM32的DAC功能时我试过最朴素的做法在main函数的while循环里不断往DAC数据寄存器写值靠软件延时控制节奏。结果示波器上的波形抖得厉害频率稍微高一点就完全变形CPU占用率还飙到接近满载其他任务基本没法跑。后来改用定时器触发DAC的方式同样的芯片、同样的波形输出立刻变得干净利落CPU几乎不参与搬运数据频率稳定性也上了一个台阶。这个转变背后的逻辑其实不复杂。DAC要输出一个连续、平滑的模拟波形本质上要求数字量的更新必须严格等间隔。软件循环受中断、分支预测、其他任务抢占的影响时间间隔天然存在抖动抖动反映到模拟输出上就是波形失真。而硬件定时器的触发信号由时钟树直接驱动周期误差在纳秒级别这才是波形频率精准的根本保证。1.2 三角波为什么值得单独拿出来讲方波、正弦波、三角波是DAC输出里最常见的三种波形。方波最简单两个值来回切正弦波需要查表三角波介于两者之间它既有线性上升和下降的斜率要求又不像正弦波那样依赖大量采样点。三角波在电机驱动、音频合成、扫频测试、PWM载波模拟等场景里出现频率极高尤其是线性调频测距这类应用三角波的线性度直接决定了测距精度。标题里强调的“频率与幅度精准控制”恰恰是三角波生成的两个核心难点。频率由定时器触发周期和波形点数共同决定幅度由DAC参考电压和数字量范围决定。这两者如果只是“大概对”波形看着没问题一旦放到实际系统里做测量或驱动误差就会暴露出来。下面我把整套方案从原理到代码到调试完整拆一遍。1.3 适合哪些人参考这篇内容适合已经能点亮LED、跑通过串口、对STM32时钟树有基本概念的开发者。如果你正在做毕业设计、逆变器方案、数字电源、信号发生器或者任何需要模拟波形输出的项目这套方案可以直接抄作业。如果你连Keil5和芯片包都还没装好建议先把开发环境搭起来再回来看否则中间一些配置细节会卡住你。2. 整体方案设计与核心思路拆解2.1 硬件触发链路是怎么走通的整套方案的核心链路是定时器产生更新事件更新事件作为DAC的触发源DAC在每次触发时从数据寄存器取出新值转换输出。这里的关键在于STM32的DAC支持多种触发源包括定时器TRGO、外部中断线等。我们选择定时器TRGO是因为它和DAC之间的连接是芯片内部硬连的不需要CPU干预也不占用中断资源。具体到STM32F4系列DAC的触发源选择通过DAC_CR寄存器的TSEL位域配置。以DAC通道1为例TSEL设置为010对应TIM2的TRGO011对应TIM4的TRGO100对应TIM5的TRGO101对应TIM6的TRGO110对应TIM7的TRGO111对应TIM3的TRGO。不同型号的对应关系略有差异写代码前一定要翻对应型号的参考手册别凭记忆。2.2 为什么选定时器6或7来触发在F4系列里TIM6和TIM7是基本定时器功能非常纯粹只有一个向上计数的计数器和一个更新事件没有输入捕获、没有输出比较、没有PWM。正因为功能少它们的时钟路径最短触发抖动最小而且不占用通用定时器资源。如果你的项目里TIM2到TIM5已经被PWM、编码器、输入捕获占满了用TIM6或TIM7来触发DAC是最省心的选择。当然如果你用的是F1系列或者G0系列基本定时器的编号和触发映射会不一样。F1的DAC触发源里TIM6的TRGO是支持的但F1的基本定时器只有TIM6和TIM7配置方式也有区别。我下面以F4为例展开其他系列在关键配置处会标注差异。2.3 波形数据从哪来查表还是实时计算三角波的数字量序列有两种生成方式。第一种是预先算好一个周期的采样点存到数组里DAC触发时用DMA搬运或者中断里逐个写。第二种是在触发中断里实时计算当前应该输出的值。两种方式各有适用场景。查表法的优势是输出稳定、CPU开销可控适合固定频率、固定点数的场景。实时计算法灵活频率和幅度可以随时改但每次触发都要做运算点数多了CPU扛不住。我的做法是折中用查表法生成基础三角波表长取2的整数次幂方便做相位累加频率调整通过改定时器周期实现幅度调整通过改DAC参考电压或者对表值做缩放实现。2.4 频率和幅度的控制维度拆开看频率的公式是输出频率 定时器触发频率 / 每周期点数。定时器触发频率 定时器时钟 / (预分频1) / (自动重载值1)。所以调频率有两条路改定时器参数或者改每周期点数。改定时器参数更直接改点数会影响波形分辨率。幅度的控制更微妙。DAC的输出电压公式是Vout Vref * DOR / 409512位分辨率。要精准控制幅度要么调Vref要么调DOR的范围。调Vref需要外部电路支持调DOR范围纯软件就能做。比如你想要0到2V的输出Vref是3.3V那DOR的最大值应该是2/3.3*4095约等于2482。这个计算过程后面会详细展开。3. 核心细节解析与实操要点3.1 DAC数据寄存器的对齐方式别搞错STM32的DAC数据寄存器有12位有效数据但寄存器本身是32位的。对齐方式有左对齐和右对齐两种通过DAC_CR的DALIGN位配置。右对齐时数据放在bit11到bit0左对齐时放在bit15到bit4。我见过不少人波形输出不对查了半天发现是对齐方式设错了写进去的值被移位了4位输出幅度只有预期的十六分之一。注意如果你用DMA搬运数据到DACDMA的外设数据宽度要设成半字16位内存数据宽度也要匹配。用32位搬运会出错因为DAC期望的是16位数据。3.2 定时器TRGO的配置细节TRGO的触发事件源通过TIMx_CR2的MMS位域选择。可选的事件包括UG更新事件、CC1IF捕获比较1、CC2IF、CC3IF、CC4IF、OC1REF、OC2REF、OC3REF、OC4REF。我们要的是更新事件所以MMS设为010。这里有个容易忽略的点更新事件的产生时机。如果ARPE自动重载预装载使能了更新事件在计数器溢出并重载ARR时产生如果没使能ARR写入立即生效更新事件的节奏会受影响。对于DAC触发这种对周期稳定性要求高的场景建议使能ARPE让ARR的更新和计数器溢出同步。3.3 三角波表的生成与线性度保证三角波的理想数学表达是分段线性函数。生成表的时候上升段从0线性增到最大值下降段从最大值线性减到0。如果表长是N上升段占N/2个点下降段占N/2个点。注意下降段的起点不要重复最大值否则波形顶部会多一个平台。我一般用下面的方式生成表#define TRI_TABLE_SIZE 256 uint16_t tri_table[TRI_TABLE_SIZE]; void gen_tri_table(uint16_t max_val) { uint16_t half TRI_TABLE_SIZE / 2; for (uint16_t i 0; i half; i) { tri_table[i] (uint32_t)max_val * i / (half - 1); } for (uint16_t i 0; i half; i) { tri_table[half i] max_val - (uint32_t)max_val * i / (half - 1); } }这段代码里上升段最后一个点是max_val下降段第一个点是max_val - max_val/(half-1)避免了重复。线性度由整数除法保证误差不超过1个LSB。3.4 幅度精准控制的两种实现路径路径一固定Vref缩放表值。假设Vref3.3V想要输出幅度为A峰峰值那么表的最大值应该是A/2/3.34095如果波形以Vref/2为中心或者A/3.34095如果波形从0到A。这种方式纯软件改起来方便但分辨率会随幅度减小而降低。路径二调Vref。有些STM32型号的DAC有内部参考电压可选或者你可以外接一个可调基准源。这种方式分辨率不变但需要硬件支持。我一般用路径一因为大部分项目里Vref是固定的3.3V软件缩放足够用。如果对幅度精度要求极高比如要做精密测量那就得考虑路径二同时注意DAC的积分非线性INL和微分非线性DNL指标。3.5 DMA在其中的角色如果波形点数多、触发频率高靠中断逐个写DAC数据寄存器会占用大量CPU时间。这时候DMA就派上用场了。配置DMA从三角波表搬运数据到DAC数据寄存器循环模式每次定时器触发产生DMA请求DMA自动搬一个数据过去。CPU只需要在初始化时配置好之后完全不参与。DMA配置的关键点外设地址是DAC_DHR12R1右对齐12位内存地址是tri_table传输方向是内存到外设数据宽度都是半字模式是循环优先级根据系统情况定。触发源选择TIM6_TRGO或对应的定时器触发。4. 完整实操过程与关键代码实现4.1 开发环境与芯片选型确认我用的环境是Keil MDK5芯片是STM32F407VET6主频168MHz。如果你的芯片是F103系列主频和定时器时钟源不同下面的参数需要相应调整。F4的APB1定时器时钟是84MHzAPB2是168MHzTIM6挂在APB1上所以时钟是84MHz。F1的APB1定时器时钟是36MHz差别很大。提示确认定时器时钟频率的方法是在RCC配置完成后查看RCC_CFGR寄存器的PPRE1位域结合SystemCoreClock计算。别直接假设是主频APB预分频器会影响定时器时钟。4.2 时钟与GPIO初始化DAC通道1对应PA4引脚。初始化时要把PA4配置为模拟模式关闭上下拉速度设高。同时使能DAC时钟和TIM6时钟。RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE); GPIO_InitTypeDef gpio; gpio.GPIO_Pin GPIO_Pin_4; gpio.GPIO_Mode GPIO_Mode_AN; gpio.GPIO_PuPd GPIO_PuPd_NOPULL; GPIO_Init(GPIOA, gpio);这段代码里GPIO_Mode_AN是模拟模式这是DAC输出引脚必须的设置。如果设成复用推挽输出会不对。4.3 定时器参数计算与配置假设我要输出1kHz的三角波表长256点。那么定时器触发频率应该是1kHz * 256 256kHz。TIM6时钟84MHz预分频和自动重载值的计算84MHz / 256kHz 328.125。取预分频为0ARR为327实际触发频率 84MHz / 328 256.097kHz输出频率 256.097kHz / 256 1.00038kHz误差0.038%足够用。TIM_TimeBaseInitTypeDef tim; tim.TIM_Prescaler 0; tim.TIM_Period 327; tim.TIM_ClockDivision TIM_CKD_DIV1; tim.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM6, tim); TIM_SelectOutputTrigger(TIM6, TIM_TRGOSource_Update); TIM_ARRPreloadConfig(TIM6, ENABLE); TIM_Cmd(TIM6, ENABLE);TIM_SelectOutputTrigger的第二个参数选TIM_TRGOSource_Update对应MMS010。4.4 DAC初始化与触发源绑定DAC_InitTypeDef dac; dac.DAC_Trigger DAC_Trigger_T6_TRGO; dac.DAC_WaveGeneration DAC_WaveGeneration_None; dac.DAC_OutputBuffer DAC_OutputBuffer_Enable; dac.DAC_LFSRUnmask_TriangleAmplitude DAC_LFSRUnmask_Bit0; DAC_Init(DAC_Channel_1, dac); DAC_Cmd(DAC_Channel_1, ENABLE);DAC_Trigger_T6_TRGO就是TIM6的TRGO触发。DAC_WaveGeneration_None表示我们用自定义波形不用芯片内部的噪声或三角波发生器。输出缓冲器使能可以增强驱动能力但如果你的负载很轻、对精度要求极高可以关掉缓冲器减少失调。4.5 DMA配置与波形表搬运DMA_InitTypeDef dma; DMA_StructInit(dma); dma.DMA_Channel DMA_Channel_7; dma.DMA_PeripheralBaseAddr (uint32_t)DAC-DHR12R1; dma.DMA_Memory0BaseAddr (uint32_t)tri_table; dma.DMA_DIR DMA_DIR_MemoryToPeripheral; dma.DMA_BufferSize TRI_TABLE_SIZE; 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_Init(DMA1_Stream5, dma); DMA_Cmd(DMA1_Stream5, ENABLE); DAC_DMACmd(DAC_Channel_1, ENABLE);DMA1_Stream5的Channel7对应DAC1。这个映射关系在参考手册的DMA请求映射表里能查到不同型号可能不同务必确认。4.6 幅度调整的实操方法假设Vref3.3V我要输出峰峰值2V、以1.65V为中心的三角波。那么表的最大值应该是(1.651)/3.34095 3288最小值是(1.65-1)/3.34095 807。调用gen_tri_table时传入的参数不是4095而是3288然后整体加上807的偏移。void gen_tri_table_offset(uint16_t max_val, uint16_t offset) { uint16_t half TRI_TABLE_SIZE / 2; for (uint16_t i 0; i half; i) { tri_table[i] offset (uint32_t)max_val * i / (half - 1); } for (uint16_t i 0; i half; i) { tri_table[half i] offset max_val - (uint32_t)max_val * i / (half - 1); } }这样生成的表DAC输出就是带直流偏置的三角波。如果你不需要偏置offset传0即可。4.7 频率动态调整的实现如果项目需要在运行中改频率直接改TIM6的ARR值就行。但要注意改ARR时如果ARPE使能了新值会在下一个更新事件生效不会产生毛刺。改完之后输出频率立即变化波形相位连续。void set_tri_frequency(uint32_t freq_hz) { uint32_t trig_freq freq_hz * TRI_TABLE_SIZE; uint32_t arr 84000000 / trig_freq - 1; TIM6-ARR arr; }这个函数假设预分频为0。如果频率很低ARR会超过16位那就需要调预分频。比如输出10Hz触发频率2560HzARR32812超过65535了得把预分频设成1ARR变成16405。5. 常见问题与排查技巧实录5.1 波形频率不对差整数倍最常见的原因是表长和触发频率的对应关系搞错了。比如你表长256但DMA的BufferSize设成了128那DMA搬一半就循环了输出频率翻倍。或者你表长256但实际只填了128个有效数据后面128个是0波形就变成半周期三角波加半周期零。排查方法先用固定值填满表比如全部填2048看DAC输出是不是稳定的1.65V。如果是说明触发和DMA链路通了。然后再换成三角波表用示波器测周期和理论值对比。5.2 波形顶部或底部有平台这是三角波表生成时的经典问题。上升段和下降段的连接处如果处理不当会出现重复点或者跳变。重复点表现为平台跳变表现为毛刺。解决办法就是前面代码里写的上升段最后一个点是max_val下降段第一个点是max_val减去一个步进保证单调性。5.3 输出幅度比预期小很多先查DAC对齐方式。右对齐时数据写DHR12R1左对齐时写DHR12L1。如果你写的是DHR12L1但数据是按右对齐算的输出会小16倍。再查Vref实际值有些板子的3.3V实际是3.2V或者3.4V幅度会有几个百分点的偏差。最后查输出缓冲器的失调缓冲器使能时会有几十毫伏的失调关掉可以改善但驱动能力下降。5.4 DMA传输一半就停了检查DMA的循环模式有没有使能。如果设成Normal模式搬完BufferSize个数据就停了DAC输出保持最后一个值。另外检查DMA的传输完成中断有没有误触发导致DMA被关闭。还有一点DAC的DMA使能位DAC_DMACmd要在DMA配置完成之后再调用顺序反了可能不生效。5.5 高频时波形失真触发频率太高时DAC的建立时间可能不够。DAC从收到触发到输出稳定需要一定时间F4的DAC建立时间典型值是1微秒左右。如果你的触发周期接近或小于这个值输出幅度会下降、波形会变形。解决办法是降低触发频率或者换建立时间更短的DAC芯片。下面这张表整理了我遇到过的典型问题和对应排查方向现象可能原因排查方法频率差整数倍表长与BufferSize不匹配检查DMA BufferSize和表长顶部有平台表生成时重复点检查上升下降连接处幅度偏小对齐方式错误确认写的是DHR12R1还是DHR12L1DMA只搬一次循环模式未使能检查DMA_Mode_Circular高频失真DAC建立时间不足降低触发频率或换芯片波形有毛刺定时器ARR更新不同步使能ARPE5.6 一个容易被忽略的坑DAC输出缓冲器的使能条件DAC输出缓冲器在Vref低于2V时性能会下降如果你的Vref是1.8V或者更低建议关掉缓冲器但这样输出阻抗会变高后级电路要加高输入阻抗的缓冲。另外缓冲器使能时DAC的最低输出电压不是0V而是几十毫伏做精密应用时要把这个失调考虑进去。6. 进阶扩展与实战经验分享6.1 从三角波扩展到任意波形这套框架不只适用于三角波。把tri_table换成正弦表、锯齿表、任意自定义波形表其他配置完全不用改。正弦表用查表法生成注意归一化到DAC的满量程。锯齿表更简单从0线性增到最大值然后跳回0。任意波形表可以从MATLAB或者Python生成后导出成C数组。6.2 频率和幅度的联合动态调整有些应用需要频率和幅度同时变化比如扫频测试。这时候要注意改ARR和改表值不能在同一个触发周期内同时生效否则波形会有一个周期的异常。稳妥的做法是先改表值等一个完整周期后再改ARR或者用双缓冲机制在DMA传输完成中断里切换表。6.3 用定时器输出比较模式做更灵活的触发除了更新事件触发定时器的输出比较模式也可以触发DAC。这种方式的好处是可以精确控制触发点在周期内的位置适合做相位可调的波形合成。配置方法是把通道设为输出比较模式OCxREF作为TRGO源。但这种方式对定时器资源占用更多简单场景没必要用。6.4 实测数据与精度评估我用这套方案在F407上实测输出1kHz三角波峰峰值2V用六位半万用表测幅度误差在0.5%以内用示波器测频率误差在0.05%以内。线性度方面用示波器的差分功能测上升段偏差不超过2个LSB。这个精度对于大多数电机驱动、音频合成、测试信号源场景都够用了。如果要做更高精度的应用比如精密测量需要考虑DAC的INL和DNL、参考电压的温漂、输出缓冲器的失调和噪声。这些已经超出软件配置的范畴需要从硬件选型和电路设计上解决。6.5 资源占用与优化建议这套方案里CPU只在初始化时参与运行期间完全不占用。DMA占用一个流定时器占用一个基本定时器DAC占用一个通道。对于资源紧张的项目可以把表长减小到64或128节省RAM。但表长太小会影响波形分辨率三角波的线性度会下降。我的经验是128点是最低限度256点是甜点512点以上收益递减。提示如果RAM实在紧张可以用实时计算代替查表但触发频率不能太高否则CPU扛不住。计算一次三角波值只需要几次加法和比较在168MHz的F4上1MHz触发频率下CPU占用大约10%可以接受。6.6 一个实际项目中的教训之前做一个逆变器方案需要三角波作为PWM载波。一开始用软件循环生成频率稳定性差导致逆变输出谐波超标。后来改成定时器触发DAC加DMA的方案谐波立刻降下来了。但中间遇到一个问题DMA传输完成中断和定时器更新中断的优先级冲突导致偶尔丢一个点。解决办法是把DMA中断优先级设得比定时器中断高或者干脆不用DMA中断纯靠循环模式自动搬运。这个教训说明触发链路里的中断优先级配置不能马虎。6.7 调试工具与测量方法调试这套方案示波器是必须的。建议用带宽100MHz以上的示波器探头用10倍衰减避免探头电容影响DAC输出。测量频率时用示波器的频率计功能比看格子准。测量幅度时用AC耦合看峰峰值DC耦合看直流偏置。如果要测线性度用示波器的数学运算功能做差分或者用数据采集卡采一批点做最小二乘拟合。万用表也可以用来测DAC的直流输出验证表值对应的电压是否正确。比如表里填2048右对齐12位Vref3.3V理论输出应该是2048/4095*3.31.650V。实测如果偏差大先查Vref实际值再查缓冲器失调。这套方案我从F1用到F4从简单信号源用到逆变器载波稳定性一直很好。核心就是抓住触发链路的硬件同步这个关键点把频率和幅度的控制维度拆开分别用定时器和表值来管。剩下的就是细心配置、耐心调试。
返回列表