
简介STM32H750是高性能Cortex-M7 MCU该工程借助定时器触发DMA搬运正弦波样本至双路DAC输出实现相位差可调的双路正弦波发生器。适用于嵌入式信号生成、音频处理、电机控制等场景适合正在学习STM32高级定时器、DMA传输以及CubeMX代码生成的开发者参考。压缩包共680个文件、约96.69MB以C源文件383个和H头文件127个为核心同时包含大量CMSIS DSP数学库.a/.lib、MDK5编译生成的中间文件.o/.crf、链接脚本.icf/.sct以及最终固件.axf/.hex还保留了.ioc与.uvprojx工程配置便于直接打开、重编译与移植。已有2784人学习下载。工程演示了从CubeMX图形化配置TIM、DMA、DAC到MDK5编译下载的完整链路代码注释清晰、目录结构完整读者可据此理解波形样本表生成、DMA循环传输和两通道相位差调节的具体实现也可在此框架上扩展频率、幅值或波形类型是学习高级外设协同应用的典型案例。 经常有人问我怎么用STM32输出一路干净的模拟波形尤其是正弦波。如果用单片机IO口翻转模拟波形难看不说频率还上不去用外置DDS芯片成本又上来了。其实只要手头的MCU带DAC外设配合定时器和DMA就能绕开CPU输出频率稳定、相位可控的正弦波。这个项目就是把STM32H750的TIM、DMA、DAC三个外设串起来实现双路可调相位差的正弦波发生器全程用STM32CUBEMX配置工程代码在MDK5里编译调试。整个过程不复杂但里面有几个坑值得记下来。先说结论这个方案的核心思路就是用一个定时器触发DMA搬运数据DMA把存放在内存里的正弦波采样点表自动搬到DAC的数据寄存器里DAC再把这些数字量转成模拟电压输出。整个过程CPU不参与所以波形频率稳定还能双路独立控制相位差。适合的读者有两类一类是想搞清楚TIM触发DMADAC这套经典组合怎么玩的嵌入式开发者另一类是需要低成本产生可调相位差双路正弦波但又不想上DDS的电子工程师。1. 项目整体设计与方案选型思路1.1 这套方案的原理骨架要理解这个项目先得把TIM、DMA、DAC三者之间的协作关系理清楚。STM32H750内部每个DAC通道都可以配置成由定时器触发转换也就是说不需要CPU去写DAC的数据寄存器而是由定时器的更新事件或触发输出去启动一次DAC转换。而DMA的作用是在每次DAC需要新数据时自动从内存的波形表里取出下一个采样点送到DAC的数据寄存器里。这三者串起来之后数据流向是这样的内存中的正弦波采样点表存放在一个数组里DMA的源地址指向这个数组目标地址指向DAC的数据寄存器。定时器以固定的频率产生触发事件DMA收到触发后搬运一次数据DAC完成一次转换并输出。当DMA把一个周期的采样点全部搬完之后触发循环传输又重新从数组开头开始搬这样DAC就能连续输出周期性的正弦波。1.2 为什么不用定时器中断或CPU直写有朋友可能会问我直接在定时器中断里给DAC寄存器赋值不行吗行但代价很大。输出一个完整的正弦波周期假设一个周期64个采样点频率1kHz那么定时器中断频率就是64kHz即每15.6微秒就要进一次中断。如果这个期间CPU还要处理其他任务比如跑个界面、控制电机频繁的中断会严重影响实时性也会带来波形抖动的风险。而DMA方案的核心优势在于数据搬运完全不占CPU。DMA控制器自己完成从内存到DAC寄存器的传输CPU可以在搬运期间处理其他任务或者干脆进入低功耗模式。更重要的是DMA的传输是硬件级别的精确不会像中断那样存在响应延迟的抖动波形频率的精确度和稳定性都高一个量级。我实测下来同样是1kHz的正弦波中断方式的频率误差受主频波动影响比较大而DMA方式几乎没有抖动。1.3 双路相位差如何实现双路相位差是这个项目的核心需求。DAC1和DAC2分别独立输出一路正弦波两路之间有可调的相位差。实现思路其实很朴素两路DAC各自分配一条DMA通道各自指向一张波形表。如果两路指向完全相同的波形表且DMA同时启动那两路输出就是同相位的。要让一路落后或超前只需要让其中一路的DMA从波形表的另一个偏移位置开始读数据。什么意思呢假设一张波形表有64个采样点代表一个完整的360度周期。每个采样点之间相差360/645.625度。如果第二路DMA从下标8的位置开始搬数据那么第二路输出就比第一路落后45度。如果从下标16开始就是落后90度。通过调整DMA初始的偏移地址就能实现相位差控制而且是硬件级精确不会漂移。2. Cubemx配置细节与关键参数计算2.1 首先把时钟树配好使用STM32CUBEMX时先选定芯片型号STM32H750VBT6然后进入Clock Configuration页面配置时钟树。我习惯把主频配置为400MHz这是H750的标称主频。注意H750需要外部高速晶振HSE或内部高速RCHSI作为PLL的参考时钟我用了8MHz的外部晶振通过PLL倍频到400MHz。H750的DAC外设挂载在APB1总线上APB1频率默认是100MHz。DAC的时钟源其实不关键关键在于定时器的时钟。如果定时器选择TIM6它也挂在APB1上那么TIM6的输入时钟就是100MHz。不过要注意如果APB1预分频器不等于1定时器时钟会自动被加倍。CUBEMX里会自动计算我们只需要在配置时确认定时器的实际时钟频率是多少。2.2 定时器配置决定了正弦波频率这个项目里定时器触发频率直接决定了DMA的搬运频率而搬运频率除以表长就是正弦波的输出频率。公式是这样的正弦波频率 定时器触发频率 / 波形表采样点数。如果定时器触发频率是64kHz波形表64个点正弦波频率就是1kHz。如果想把频率调高到10kHz一是可以加大定时器触发频率二是可以减少采样点数。比如一个周期32个点定时器触发频率320kHz输出就是10kHz。CUBEMX里配置TIM6时主要设置两个参数Prescaler预分频和Counter Period自动重装载值。定时器触发频率 定时器时钟 /Prescaler1×Counter Period1。假设定时器时钟100MHz要让触发频率为64kHz那么Prescaler1×Counter Period1 1562.5取整后Prescaler设为1Counter Period设为780实际频率约64.1kHz误差很小对模拟输出来说可以接受。在CUBEMX的TIM6配置界面里把触发输出TRGO设置为Update Event这样每次定时器计数器溢出就会产生触发信号这个触发信号就是DMA的请求源。2.3 DAC和DMA参数的坑DAC部分在CUBEMX中使能DAC1的两个通道Output Buffer建议开启。这个缓冲器能提高输出驱动能力缺点是会增加一点功耗。数据格式选12位右对齐这样DAC寄存器值范围是0到4095对应0到参考电压。STM32H750的DAC参考电压通常接到VDDA如果VDDA是3.3V那么输出范围就是0到3.3V。DMA配置是重头戏。DAC1的每个通道对应一个DMA请求分别是DMA1的Channel1和Channel2。DMA方向选择Memory-to-PeripheralDMA模式选择Circular循环模式。这里有个关键设置在CUBEMX的DMA设置界面里有一个Momory到Peripheral增量地址的选项Peripheral的地址增量必须设为Fixed因为DAC数据寄存器地址不变Memory地址增量要设为Increment因为要从数组的各个单元依次读数据。还有个容易忽略的参数叫Data Width数据类型要设置成Half Word也就是16位。因为DAC的数据寄存器是16位的虽然我们实际只用低12位但DMA是按16位为单位搬运的如果按Byte搬运会出错。2.4 MDK5工程生成前的最后确认全部配置完成后在Project Manager里选择工具链为MDK-ARM V5然后生成代码。这里有个提醒H750的默认启动文件访问的Flash起始地址是0x08000000但H750内部Flash实际只有128KB调试时我把代码放在内部Flash里没问题。如果代码超过128KB那就要配置外部QSPI Flash启动这是另一个话题了这里不展开。生成的工程用MDK5打开后首先要确认Target选项卡里的芯片型号正确其次要把优化等级视情况调整。调试阶段建议选-O0方便看变量。发布时可以改成-O2但要确认DMA搬运逻辑不受编译器优化的影响。3. 代码实现与双路相位差控制3.1 用代码生成正弦波采样点表正弦波采样点的生成其实可以在C代码里用数学函数在初始化时计算也可以用表格直接预先算好填进去。我在项目里用了运行时计算的方案好处是调整采样点数和幅值不用改表格逻辑上更灵活。关键代码是这样的#define SAMPLE_NUM 64 // 每周期采样点数 #define DAC_FULL_SCALE 4095 // 12位DAC满量程 #define AMPLITUDE 2048 // 正弦波幅值可调 uint16_t sineTable[SAMPLE_NUM]; void GenerateSineWaveTable(void) { for (int i 0; i SAMPLE_NUM; i) { double angle (double)i * 2.0 * 3.1415926 / SAMPLE_NUM; double value (sin(angle) 1.0) / 2.0; // 把-1到1映射到0到1 sineTable[i] (uint16_t)(value * AMPLITUDE); } }这里有个细节上电时DAC会输出一个当前电压值对应的模拟量如果波形表里第一个值是0那一上电瞬间会有一个从0开始的爬升过程。为了避免这个现象我会把波形表的第一个点初始化成中值2048让上电时DAC输出在中间电压附近。这个表中每一个点对应一个角度。存储时用的是12位右对齐格式也就是数值范围0到4095。幅值变量AMPLITUDE控制正弦波的峰峰值2048意味着输出在0到2048之间变化对应电压0到约1.65V。如果希望满幅输出就把AMPLITUDE设为4095这时输出范围就是0到3.3V。3.2 双路DAC的DMA通道配置与启动两路DAC的DMA初始化代码CUBEMX生成的函数分别叫HAL_DAC_Start_DMA。在代码中这样调用HAL_DAC_Start_DMA(hdac1, DAC_CHANNEL_1, (uint32_t *)sineTable, SAMPLE_NUM, DAC_ALIGN_12B_R); HAL_DAC_Start_DMA(hdac1, DAC_CHANNEL_2, (uint32_t *)sineTable, SAMPLE_NUM, DAC_ALIGN_12B_R);从字面意思看两路DMA都指向同一个sineTable头地址这会导致两路输出完全同相。要实现相位差第二路的DMA要指向表中间的一个偏移位置。这里就有一个C语言指针的细节uint32_t类型的指针加1实际地址会加4个字节而表里的元素是uint16_t占2个字节。所以不能直接用sineTable offset这种写法要转成uint16_t指针再偏移。正确的写法是uint16_t *ptr_ch2 sineTable PHASE_OFFSET; HAL_DAC_Start_DMA(hdac1, DAC_CHANNEL_2, (uint32_t *)ptr_ch2, SAMPLE_NUM, DAC_ALIGN_12B_R);PHASE_OFFSET是一个全局变量用来动态调整相位差。比如PHASE_OFFSET SAMPLE_NUM / 8那么相位差就是360/845度。如果想实时调相位只需要改变PHASE_OFFSET的值然后重新调用HAL_DAC_Start_DMA即可。但要注意在运行中重新调用Start_DMA函数DMA会重新从新的偏移地址开始搬运这个过程中会有短暂的波形中断。如果应用场景不允许波形中断就需要用双缓冲DMA Double Buffer配合乒乓缓存来实现无缝切换这属于进阶玩法后面有时间再单独写。3.3 相位差的频率无关特性相位差本质上是以角度表示的同一个偏移点在不同频率下对应的绝对时间延迟不同但相位差保持不变。也就是说无论正弦波频率是多少PHASE_OFFSET SAMPLE_NUM / 8都对应45度相位差。这一点在测试中非常方便因为调频不影响相位差调相位差不影响频率两个参数可以独立调试。我在实际测试中用示波器同时观察两路输出把PHASE_OFFSET从0逐渐调到31可以看到两路波形的相对位置连续变化没有跳变和毛刺说明DMA搬运很稳定。3.4 仔细确认DMA中断与回调函数项目里涉及DMA传输完成中断。CUBEMX生成代码时如果勾选了DMA的Interrupt那么在main.c里会出现对应的中断回调处理入口。我们的场景是循环模式DMA会一直搬运传输完成中断表示搬完了一个周期的数据此时回调函数HAL_DAC_ConvCpltCallbackCh1会被调用。在这个回调里可以更新一些状态标志比如实时更新波形输出计数。但我并不建议在回调里做复杂操作因为回调是在中断上下文里执行的处理不当会影响DMA的下一轮搬运导致波形异常。实测下来回调函数里如果执行超过几微秒的操作波形边缘就有可能产生毛刺。4. MDK5编译与H750环境避坑4.1 从CUBEMX到MDK5的工程生成CUBEMX生成MDK5工程时会自动生成启动文件startup_stm32h750xx.s、系统初始化文件system_stm32h7xx.c和一系列外设驱动文件。用MDK5打开项目后第一件事是配置下载器和Flash算法。H750内部Flash是128KB所以在Debug设置里Flash Download区域要选STM32H7x_128KB的Flash算法。另一个常见坑是C/C的Include路径。CUBEMX生成的工程一般会自动带上HAL库的头文件路径但如果你手动加了中间层文件很容易漏掉路径导致编译时提示找不到stm32h7xx_hal.h。在Options for Target的C/C标签页里把所有包含头文件的目录都加进去并把优化等级先调到-O0。4.2 编译告警与HAL库版本差异H750的HAL库版本迭代比较快不同版本的API细节稍有不同。比如有些版本的DAC_Start_DMA函数定义里第二个参数需要传入DAC_HandleTypeDef指针有些版本又不同。编译时如果报参数类型不匹配别急着改代码先确认自己用的HAL库版本再查一下对应版本的API参考。还有一个在MDK5里经常遇到的告警是warning: #111-D: statement is unreachable这通常是宏定义开关把某段代码屏蔽了编译器检测不到可达路径。不影响功能但建议把这类告警清理掉保持代码干净因为以后排查问题时满屏告警会掩盖真正的错误。4.3 实际编译时的优化设置编译阶段我建议一开始用-O0优化等级方便用J-Link在线调试时观察变量。当功能稳定后再切换到-O2并重新测试波形。因为编译器在高优化等级下可能改变循环中的局部变量存储方式如果正弦波表生成函数里用了double类型计算高优化下sin()的调用有可能被优化成查表近似这一点对输出精度影响微乎其微但确实存在。另外H750主频跑400MHz时如果代码里使用了浮点运算要确保启用了FPU。在MDK5的Options中Target页签里把Floating Point Hardware设置为Single Precision这样sin()计算会快很多。如果不设置编译器会用软件浮点生成波形表时耗时明显。5. 波形调试与常见问题排查实录5.1 波形失真或带毛刺怎么办用示波器观察DAC输出最常见的现象是正弦波上有毛刺或台阶感。毛刺多半来自电源噪声如果DAC参考电压和MCU数字电源共用数字部分的开关噪声会耦合到模拟输出上。我试过在DAC输出端加一个100nF的电容到地作为简易低通滤波器毛刺明显改善。或者用RC滤滤根据实际频率调整截止频率。台阶感则是采样点数不足导致的。一个周期只有16个采样点时每个输出点之间的电压跳变较大波形看起来像阶梯波。我的建议频率在1kHz左右时采样点数至少取64个波形就很平滑了。若还想更平滑可以在DAC输出端加二阶低通滤波器截止频率设置为1.5倍的正弦波频率。5.2 相位差不对或两路不同步两路DAC共用一个定时器触发按理说应该严格同步但实际测试中有时会出现相位差不等于设定值的现象。排查思路是这样的先确认两个DMA通道的优先级设置。如果DMA1的Channel1优先级高于Channel2并且两路DMA请求同时发生那么Channel2的数据搬运会延迟一个时钟周期导致相位偏差。把两个DMA通道的优先级设为相同或者在CUBEMX里把DMA的优先级都设为High能有效减少这个问题。更隐蔽的一个原因是DMA内存地址的增量步长设置错误。如果把Memory Increment误设为Byte那么每搬一次数据地址只加1读取的将是同一个数组的高字节和低字节交错的数据波形完全不是正弦波。检查CUBEMX里DMA的Memory地址增量是否为Half Word。5.3 DMA连续请求与循环模式的配合在DMA配置界面里有个参数叫Request Mode可选Single Mode或Burst Mode。本项目的场景里定时器每次触发只产生一次DMA请求搬运一个数据所以选择Single Mode就够了。如果误设成了Burst模式每次触发会搬运多个数据导致输出频率翻倍甚至错乱。另外H750的DMA还有FIFO配置。FIFO开启时DMA会先把数据攒到FIFO里再写入外设寄存器这会引入几个周期的延迟而且可能造成数据错位。我把FIFO模式配置为DisabledDMA直接传输波形更干净。CUBEMX默认情况FIFO也是关闭的但如果你手动改过配置记得确认回来。5.4 后台任务影响了波形稳定即使DMA不占CPU如果CPU上下文中执行了长时间关中断的代码也会影响DAC输出波形。因为关中断会延迟DMA中断回调从而延迟下一个DMA周期开始的准备。实测中如果主循环里执行了很长的延时函数比如HAL_Delay(1000)示波器上能看到输出波形在延时前后出现一个短暂的毛刺。要解决这个问题需要确保主循环中的临界区代码尽量短。必要时可以用DMA的Half Transfer中断来提前准备下一轮数据而不是等全部传输完成再准备这样留给任务的时间窗口更大。这也是我喜欢在最终方案中启用Half Transfer中断回调的原因。5.5 必查清单与速查表根据我实际调试过程中踩过的坑整理了一个检查清单方便大家排错。当波形不对时从第一步开始逐项核对检查项正确状态错误后果DMA数据传输宽度Half Word(16bit)波形乱码、阶梯错位DMA循环模式Circular只输一个周期就停了DMA内存地址增量Increment搬运数据重复或错乱DAC输出缓冲Enabled输出阻抗高波形畸变定时器触发源Update Event不触发DMA无波形DMA优先级两路一致相位偏差、抖动FIFO状态Disabled数据错位、波形毛刺优化等级调试用-O0调试信息不全6. 扩展玩法与后续升级方向6.1 从正弦波到任意波形这套TIMDMADAC的组合不只是能输出正弦波。波形的形状完全由波形表的内容决定。如果你把波形表换成三角波、锯齿波、方波甚至任意自定义波形数据DAC就能输出对应的任意周期信号。用MATLAB或Python把需求波形采样生成一个数组下载到MCU里稍作修改就能变成任意波形发生器。我经常用一个Python脚本在电脑上生成波形表数组然后粘贴到源码里调试效率比手撸数组高很多。比如生成一个带谐波的正弦波加点噪声的测试信号加入表中用来测试模拟电路的特性。6.2 用LUT实现相位连续可调的高级方案本项目里的相位差调整是重新调用DMA启动函数来实现的切换过程存在短暂的波形中断。如果应用背景是连续调相比如电机控制里的坐标变换那就需要相位连续可调的方向。一个思路是把波形表扩大为两倍前半张表存0到360度的波形后半张表存同样的波形DMA启动时指向中间位置通过偏移地址连续滑动实现相位的连续调整。另一个思路是使用DAC的双缓冲模式。DMA和DAC各自有数据寄存器先缓存下一轮的数据等同步信号到来时再切换从而避免中断。这个方案实现复杂一些但性能上限高。6.3 频率实时调整方法如果只是改变定时器的重装载值就能改变输出频率。HAL库提供了__HAL_TIM_SET_AUTORELOAD宏在主循环里调用即可。但要注意修改重装载值时定时器当前计数器的值可能已经大于新的重装载值导致需要等待一个完整的计数周期才生效所以频率切换会有几十微秒的采样延迟。对于大多数应用这个延迟完全可以接受。我在实际项目中用按键中断来调整频率每按一次键改变重装载值同时把当前频率通过串口打印出来。这样调音、调信号源都很方便示波器上看到的频率变化是平滑连续的。做个总结吧这套方案的本质在于把硬件外设按照DMA传输链的方式串联数据从内存到DAC几乎零CPU开销从而精确稳定地输出模拟波形。开发过程中我最大的体会是外设配置里的每一处细节都会反映在最终的波形上宁可先花时间把CUBEMX里每个参数搞清楚也不要急着写业务代码。等到波形真正出来并稳定了那种成就感是很难替代的。如果你在调试中碰到奇怪的问题欢迎对照前面的检查清单逐项排查大多数坑都能找到方向。本文还有配套的精品资源点击获取