资讯详情

资讯详情

STM32双路DAC+DMA同步正弦波生成实战

简介本资源是面向STM32嵌入式开发初学者与电子类竞赛如校级电子设计赛参赛者的双路可调波形发生器实战项目聚焦DAC波形生成与DMA高效驱动这一核心难点。项目基于STM32F10x系列完整实现正弦波、三角波、方波及锯齿波的双通道同步输出支持频率动态调节涵盖DAC配置、DMA循环传输、定时器触发、波形查表法及硬件滤波等关键实践环节。压缩包含204个文件以36个C源文件和39个H头文件构成主程序框架37个O目标文件与37个D依赖文件体现Keil工程编译结构另含AXF/HEX可执行镜像、UVPROJX工程文件及BAT一键清理脚本总大小5.29MB结构规范、模块清晰便于理解工程组织与调试流程。目前已有803人学习下载提供完整可运行代码、典型外设驱动范例及底层寄存器级配置逻辑是掌握STM32模拟信号生成与实时数据流控制的优质学习样本。1. 用 STM32 的两路 DAC DMA 实现高稳定度双通道正弦波信号发生绕过 CPU 干预实现微秒级同步输出你手头有一块 STM32F103 或 F407 开发板想输出两路相位可调、频率可变、幅度可控的正弦波——不是用 PWM 滤波那种粗糙方案也不是靠定时器中断反复写 DAC 寄存器CPU 占用率飙升、抖动大、通道间难对齐。真实工业场景里比如传感器激励源、音频测试基准、电机矢量控制中的参考信号生成要求两路 DAC 输出严格同步、无毛刺、连续无断点。这时DMA 驱动 DAC 成为唯一可靠路径CPU 只需初始化一次后续数据搬运由 DMA 控制器自主完成DAC 更新触发由定时器精确节拍控制。标题中 “19 校赛_信号端” 暗示这是高校电子设计竞赛常见题型“两路DAC DMA” 是硬性指标“copy7i2” 这类后缀常指向某次调试成功的工程副本——说明该方案已在实际硬件上验证可行。本文不讲抽象原理只聚焦如何在 STM32F103C8T6主流低成本型号或 STM32F407VGT6高性能型号上用标准 HAL 库复现这一能力覆盖 DAC 配置冲突、DMA 循环模式陷阱、定时器触发时序错位等真实踩坑点。2. 为什么必须用 DMA 驱动 DAC对比中断写寄存器与定时器触发的三重瓶颈2.1 中断方式写 DAC 寄存器CPU 成为最大噪声源当使用 TIM6/TRGO 触发 DAC 更新再在 DAC 更新中断中手动写入新值时问题立刻暴露执行延迟不可控中断响应上下文保存寄存器写入退出典型耗时 15–25 个系统时钟周期72MHz 下约 200–350ns且受其他中断抢占影响数据更新非原子操作若正弦表用 uint16_t 存储而 DAC 数据寄存器是 12 位DHR12R1/DHR12R2HAL_DAC_SetValue() 内部需掩码移位进一步增加不确定性双通道不同步若分别用两个中断服务函数更新 DAC1 和 DAC2即使代码完全相同编译器优化或指令流水线差异也会导致微秒级偏差。提示校赛评分细则中明确要求“两路输出相位差误差 ≤ 0.5°”对应 1kHz 正弦波即 ≤ 1.39μs 同步误差。中断方式根本无法满足。2.2 定时器 TRGO 直接触发 DAC硬件级同步的底层逻辑STM32 的 DAC 模块支持外部触发源EXTSEL[2:0]其中 TIM6_TRGO 是最常用选择。关键在于TIM6 计数器溢出或更新事件时自动产生 TRGO 信号DAC 收到 TRGO 后在下一个 APB1 时钟上升沿锁存 DHRx 寄存器值到 DAC 输出缓冲区此过程完全硬件化无软件介入延迟固定为 1–2 个 APB1 时钟周期通常 ≤ 14ns。但仅靠 TRGO 不够——它只解决“何时更新”不解决“更新什么”。此时 DMA 的价值凸显它能在 TRGO 触发瞬间自动将内存中下一个正弦样本值搬运至 DHRx 寄存器全程无需 CPU 干预。2.3 DMA 循环模式维持正弦波连续性的唯一解法正弦波是周期信号需无限循环查表。DMA 的DMA_NORMAL模式在传输完一次后即停止必须靠中断重载地址引入延迟而DMA_CIRCULAR模式让 DMA 自动将传输计数器归零并重启实现真正无缝循环。关键参数hdma_dac1-Init.MemInc DMA_MINC_ENABLE内存地址自增、hdma_dac1-Init.PeriphInc DMA_PINC_DISABLE外设地址固定为 DHR12R1、hdma_dac1-Init.Mode DMA_CIRCULAR致命陷阱若正弦表长度非 2 的整数次幂如 256 点DMA 传输数量hdma_dac1-Init.NbData必须严格等于表长否则循环边界错位导致波形跳变验证方法用示波器观察 DAC 输出关闭所有中断仅运行 DMATIM6若波形无任何台阶或突变即证明循环配置正确。3. 两路 DAC DMA 的最小可运行配置从 CubeMX 生成到关键代码补全3.1 CubeMX 图形化配置四步锁定硬件资源启用 DAC1 DAC2在 Analog DAC 页面勾选 “DAC1 Channel 1” 和 “DAC2 Channel 2”Mode 选 “Regular mode”Output Buffer 保持 Enable降低输出阻抗配置 TIM6 作为触发源在 Timers TIM6 页面Prescaler 设为 71使 CK_CNT 1MHzCounter Period 设为 99即 100 个计数周期TRGO 频率 1MHz / 100 10kHz配置 DMA1 Channel3DAC1与 Channel4DAC2在 DMA 页面为 DAC1 选择 DMA1_Channel3Direction 选 “Memory to Peripheral”Data Width 选 “Half Word”Burst 选 “Single”同理为 DAC2 分配 DMA1_Channel4生成代码前强制检查点击 “Project Manager” “Code Generator”勾选 “Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”避免 HAL 库版本兼容问题。3.2 手动补全正弦表与 DMA 初始化代码CubeMX 生成的MX_DAC_Init()仅初始化 DAC未配置 DMA 绑定。需在main.c的/* USER CODE BEGIN 4 */区域插入// 256 点正弦表12 位精度0~4095 const uint16_t sine_table[256] { 2048, 2148, 2247, 2345, 2442, 2538, 2632, 2725, 2815, 2903, 2989, 3072, 3152, 3229, 3303, 3374, // ...此处省略中间 224 个值实际需完整填充 2048, 1948, 1849, 1751, 1654, 1558, 1464, 1371, 1281, 1193, 1107, 1024, 944, 867, 793, 722 }; // 全局 DMA 句柄需在 main.c 顶部声明 DMA_HandleTypeDef hdma_dac1; DMA_HandleTypeDef hdma_dac2; void MX_DMA_Init(void) { /* DMA1_Channel3 init */ hdma_dac1.Instance DMA1_Channel3; hdma_dac1.Init.Direction DMA_MEMORY_TO_PERIPH; hdma_dac1.Init.PeriphInc DMA_PINC_DISABLE; // 外设地址固定DHR12R1 hdma_dac1.Init.MemInc DMA_MINC_ENABLE; // 内存地址自增 hdma_dac1.Init.PeriphDataAlignment DMA_PDATAALIGN_HALFWORD; hdma_dac1.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; hdma_dac1.Init.Mode DMA_CIRCULAR; // 关键循环模式 hdma_dac1.Init.Priority DMA_PRIORITY_HIGH; if (HAL_DMA_Init(hdma_dac1) ! HAL_OK) { Error_Handler(); } /* DMA1_Channel4 init */ hdma_dac2.Instance DMA1_Channel4; hdma_dac2.Init.Direction DMA_MEMORY_TO_PERIPH; hdma_dac2.Init.PeriphInc DMA_PINC_DISABLE; // 外设地址固定DHR12R2 hdma_dac2.Init.MemInc DMA_MINC_ENABLE; hdma_dac2.Init.PeriphDataAlignment DMA_PDATAALIGN_HALFWORD; hdma_dac2.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; hdma_dac2.Init.Mode DMA_CIRCULAR; hdma_dac2.Init.Priority DMA_PRIORITY_HIGH; if (HAL_DMA_Init(hdma_dac2) ! HAL_OK) { Error_Handler(); } /* 绑定 DMA 到 DAC */ __HAL_LINKDMA(hdac, DMA_Handle1, hdma_dac1); // DAC1 使用 Channel3 __HAL_LINKDMA(hdac, DMA_Handle2, hdma_dac2); // DAC2 使用 Channel4 }3.3 启动 DAC 与 DMA 的时序关键点在main()函数中MX_DAC_Init()后必须按严格顺序启动// 1. 启动 DAC1 DAC2使能输出 HAL_DAC_Start(hdac, DAC_CHANNEL_1); HAL_DAC_Start(hdac, DAC_CHANNEL_2); // 2. 启动 DMA 传输注意必须在 DAC 启动后 HAL_DMA_Start(hdma_dac1, (uint32_t)sine_table, (uint32_t)hdac.Instance-DHR12R1, 256); HAL_DMA_Start(hdma_dac2, (uint32_t)sine_table, (uint32_t)hdac.Instance-DHR12R2, 256); // 3. 启动 TIM6触发源最后启动 HAL_TIM_Base_Start(htim6);注意若先启动 TIM6它会立即发出 TRGO但此时 DMA 尚未就绪DAC 将输出默认值0x0000或上次残留值造成启动尖峰。此顺序错误是校赛现场最常见的波形异常原因。4. 调整正弦波频率与相位通过修改 TIM6 周期与正弦表偏移实现4.1 频率调节公式与实测验证DAC 输出频率f_out由下式决定f_out f_TIM6_TRGO / N其中f_TIM6_TRGO f_APB1 / ((PSC 1) * (ARR 1))N为正弦表点数。例如APB1 36MHzPSC35ARR99 →f_TIM6_TRGO 36MHz / (36 * 100) 10kHz256 点表 →f_out 10kHz / 256 ≈ 39.06Hz。要得到 1kHz 输出需f_TIM6_TRGO 1kHz * 256 256kHz则(PSC1)*(ARR1) 36MHz / 256kHz 140.625→ 取 PSC13, ARR914*10140实际f_out 36MHz/(14*10)/256 ≈ 1020.4Hz。4.2 双通道相位差控制内存地址偏移法正弦表本质是环形缓冲区。让 DAC2 比 DAC1 晚k个样本输出即可实现Δφ k * 360° / N相位差。修改 DMA 启动代码// DAC1 从表首开始 HAL_DMA_Start(hdma_dac1, (uint32_t)sine_table, (uint32_t)hdac.Instance-DHR12R1, 256); // DAC2 从第 64 个元素开始256 点表中 64/256 90° 相位超前 uint16_t *dac2_start_addr (uint16_t*)sine_table 64; HAL_DMA_Start(hdma_dac2, (uint32_t) dac2_start_addr, (uint32_t)hdac.Instance-DHR12R2, 256);提示dac2_start_addr必须确保地址对齐uint16_t*且256 - 64 192个后续元素足够 DMA 读取否则循环时地址越界。安全做法是让 DAC2 地址偏移量k满足0 ≤ k N。4.3 幅度缩放在正弦表生成阶段完成不要在运行时乘除耗时且引入浮点误差直接在 PC 端用 Python 生成带缩放的表import numpy as np # 生成 256 点、幅度 1000满幅 4095 的 24.4%、偏置 2048 的表 amp 1000 offset 2048 table [int(offset amp * np.sin(2*np.pi*i/256)) for i in range(256)] print(, .join(map(str, table)))将输出粘贴到sine_table[]中编译下载即可获得指定幅度正弦波。5. 排查波形失真与同步失效的五大硬核技巧5.1 示波器捕获 TRGO 与 DAC 输出边沿定位硬件时序连接示波器通道 1 接 TIM6 的 TRGO 引脚通常为 PA0需在 CubeMX 中启用 TIM6_ETR通道 2 接 DAC1 输出引脚PA4设置触发源为通道 1 上升沿时基调至 1μs/div。正常现象TRGO 上升沿后DAC1 输出在 1–2 个 APB1 时钟周期内跳变如 APB136MHz周期≈27.8ns应看到 ≤ 55ns 延迟。若延迟达数百 ns检查RCC-CFGR中 APB1 预分频是否被意外修改。5.2 DMA 传输完成中断误用禁用所有 DMA 中断校赛代码常错误启用HAL_DMA_Enable_IT()认为需要中断通知。但DMA_CIRCULAR模式下传输完成中断永不触发因无“完成”概念若误开中断每次 TRGO 触发都会进入空 ISRCPU 负载飙升且可能干扰其他外设。正确做法在MX_DMA_Init()中hdma_dac1.Init.ITMode DMA_IT_DISABLE;默认即禁用但显式写出更安全。5.3 电源噪声导致 DAC 输出台阶PCB 布局三原则即使代码完美劣质 PCB 仍致波形畸变问题现象PCB 改进措施输出有规律台阶DAC 供电VREF必须独立走线远离数字地用 10μF 钽电容 100nF 陶瓷电容滤波高频噪声叠加DAC 输出引脚紧邻模拟地铺铜避免跨越数字分割线正弦表存储于 SRAM勿放 Flash访问慢两路相互串扰DAC1PA4与 DAC2PA5走线等长、等距下方完整模拟地平面禁止与 USB/USART 走线平行走5.4 STM32F103 与 F407 的 DAC 差异避坑表特性STM32F103Cortex-M3STM32F407Cortex-M4应对策略DAC 分辨率12 位12 位表数据统一用uint16_t高位对齐DMA 请求映射DAC1→DMA1_Ch3, DAC2→DMA1_Ch4DAC1→DMA1_Ch5, DAC2→DMA1_Ch6F4 系列CubeMX 自动生成勿硬编码通道号定时器触发源TIM6_TRGO, TIM7_TRGOTIM6_TRGO, TIM7_TRGO, TIM2_TRGO更多选择优先用 TIM6兼容性最好输出缓冲器功耗低功耗模式下可能关闭缓冲器导致阻抗升高缓冲器更稳定支持更低输出阻抗F103 工程中务必确认DAC_CR的BOFF1/BOFF2位清零5.5 快速验证 DMA 是否真正接管用 GPIO 模拟波形轮廓在HAL_DAC_Start()后、HAL_TIM_Base_Start()前插入 GPIO 翻转代码// 在 TIM6 启动前用 GPIO 指示 DMA 状态 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6, GPIO_PIN_SET); // 启动前拉高 HAL_DMA_Start(hdma_dac1, (uint32_t)sine_table, (uint32_t)hdac.Instance-DHR12R1, 256); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6, GPIO_PIN_RESET); // DMA 启动后拉低用示波器看 PA6若为窄脉冲1μs证明 DMA 启动瞬时完成若持续高电平说明HAL_DMA_Start()卡死——大概率是 DMA 通道已被其他外设占用如 USART1_RX 使用 DMA1_Ch5需检查 CubeMX 中 DMA 资源分配冲突。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →