ARTICLE DETAIL

资讯详情

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

STM32F103 DMA+DAC 输出正弦波:原理、配置与代码实现

STM32F103 DMA+DAC 输出正弦波:原理、配置与代码实现 简介采用DMA与DAC协同工作的STM32F103正弦波输出工程例程适合嵌入式开发者、单片机初学者及需要模拟波形输出的项目参考。工程围绕50Hz基准正弦波生成展开完整演示了预计算数字样本表、配置DMA循环搬运、DAC输出刷新以及通过调整发送速率改变频率的关键环节。实现中涉及的DMA传输完成中断、内存地址递增、DAC对齐方式等细节也可对照代码深入理解。资源包总计156个文件大小为7.26MB其中包含34个C源文件和33个头文件覆盖系统初始化、定时器、RCC、Flash等标准外设库模块同时带有uvprojx、uvoptx工程配置文件、axf/hex/map等编译与调试产物以及供清理中间文件的bat脚本便于直接打开工程查看或按需移植。已有4954人学习下载适合希望快速理解DMA触发DAC工作机制、并在实际项目中实现可调频率波形输出的开发者参考。1. 方案整体设计思路1.1 为什么选 DMADAC 而不是直接操作寄存器STM32F103 输出正弦波这件事我最早是直接用 DAC 加定时器中断然后在中断里改输出值做的。当时感觉挺简单结果一上示波器就发现问题波形频率稍微调高一点CPU 就被中断占满主循环里其他任务全部卡死而且波形的点数稍微多一点中断里计算不过来波形就开始出现台阶和毛刺。后来换了 DMADAC 方案一下子清爽了。核心思路是用 DMA 把内存里的正弦波采样点数据自动搬运到 DAC 的数据寄存器里搬运过程完全不需要 CPU 参与。CPU 只要负责两件事初始化的时候把正弦波查找表准备好运行的时候根据需求调整输出频率。也就是说正弦波输出这件事本身是“硬件自动运行”的CPU 几乎零负担随便加按键、OLED 显示、串口通信都没问题。这个方案的本质是“用硬件替代中断”的思路。DMA 控制器和 DAC 外设都在芯片内部互相之间可以直接通信不需要 CPU 中转。这个组合在波形发生器、逆变器控制、电机驱动、音频播放这些场景里都很常用理解了 F103 的 DMADAC换到 STM32F4、F1 的 HAL 库工程也基本是同一套逻辑。1.2 F103 的 DAC 和 DMA 资源分析STM32F103 内置的 DAC 是两个 12 位电压输出型转换器分别挂在 PA4 和 PA5 引脚上。12 位意味着可以把参考电压一般是 3.3V分成 4096 个等级所以输出电压的表达式是Vout DAC_数据寄存器值 / 4096 × Vref这里有个重要细节DAC 不是“输出一定电压”的器件而是“输出一定比例电压”的器件。当 DAC 寄存器写入 2048 时输出大约是 1.65VVref 为 3.3V 时。F103 的 DMA1 有 7 个通道DAC 通道对应的 DMA 请求是 DMA1 通道 3。DMA 在这里起的作用就是把 SRAM 里存储的正弦波采样数据一个接一个地搬到 DAC_DHR12R1数据保持寄存器里。注意是“循环搬运”这样波形才能连续不断地输出不需要反复重启 DMA。还要特别注意区分 DHR 和 DOR。DHR 是软件或者 DMA 写入的寄存器DOR 是真正生效的输出寄存器。DMA 搬运数据是写到 DHRDAC 外设会在下一个 APB1 时钟周期把 DHR 的数据锁存到 DOR这个过程是自动的。这也意味着如果 DMA 搬运速度太快数据可能会在 DHR 里堆积导致输出波形滞后——这个后面讲参数配置的时候会细说。1.3 这个方案解决的核心问题用 DMADAC 输出正弦波解决的问题主要三个波形连续性DMA 循环搬运模式下数据输出是均匀、连续、不需要 CPU 干预的不会出现中断响应不及时导致的波形抖动。频率可调灵活性可以通过调整定时器触发频率来调节输出波形的周期用户可以自由设定目标频率范围可以从几十赫兹到几十千赫兹。CPU 解放CPU 资源被释放出来可以在主循环里处理按键扫描、串口指令、液晶显示甚至同时跑多个任务。这个方案的适用场景也很明确需要产生稳定的周期性模拟信号同时对 CPU 占用率有要求的项目。比如说实验室信号发生器、电机驱动中的正弦波 PWM 参考信号、逆变器中的 SPWM 载波基准、音频信号模拟等。如果只是点个灯、跑个呼吸灯效果完全不需要这么复杂用定时器 PWM 就够了。2. 正弦波生成原理与参数计算2.1 采样点数据是怎么来的写代码之前得先把数据准备好。正弦波是一个连续信号但在数字系统里只能输出离散的点然后把离散点转换成连续信号。就拿一个周期 360° 的正弦波来说可以把它切成 N 个等分每个等分点记录一个电压值。把这些电压值按顺序存到数组里DMA 就按顺序一个点一个点地输出DAC 就把这些数字点转成模拟电压值。所以第一步是算采样点。比如取 256 个点一个周期每个点的计算公式是采样值[i] (sin(2π × i / 256) 1) × 2048这里有两个关键操作。第一个是sin(2π × i / 256)计算结果范围在 -1 到 1 之间。第二个是1再乘以 2048把结果映射到 0 到 4095 之间因为 DAC 是 12 位的寄存器值只能写 0 到 4095。如果不做偏移处理输出波形就会在 0 附近上下摆动没法输出完整的正弦波。实际写代码时可以用 C 语言在初始化时算好这个数组也就是提前生成查找表。这样可以避免每次输出都实时计算正弦函数节省 CPU 时间。代码比较简单uint16_t sine_table[256]; void sine_table_init(uint16_t points) { for (uint16_t i 0; i points; i) { sine_table[i] (uint16_t)((sin(2 * 3.1415926 * i / points) 1) * 2048); } }这里的 3.1415926 是手动写的宏为了精度可以替换成 math.h 里的 M_PI。2.2 频率控制的数学关系输出频率怎么算这里的关键是理解“DAC 每隔多久更新一次输出电压”。这个更新频率由定时器触发 DMA 的频率决定。假设 DAC 的更新频率也叫采样率是 Fs一个正弦波周期里有 N 个采样点那么输出正弦波的频率 f 就是f Fs / N举个例子如果定时器触发频率是 128kHz查找表有 256 个点那么输出正弦波频率就是 128000 / 256 500Hz。如果想要 1kHz 正弦波在采样点固定的情况下就得把采样率提升到 256kHz。在 F103 上定时器的触发频率又由定时器时钟、预分频器PSC和自动重装载值ARR共同决定。公式是Fs TIM_CLK / (PSC 1) / (ARR 1)所以调频就有一个选择问题是改 PSC 和 ARR还是改采样点数 N。实际工程里两种方式都有各有优劣。我后面调频率用的是改 ARR 的方式因为这样查找表可以固定下来DMA 传输长度不用动代码逻辑最简单。2.3 采样点数怎么选采样点数 N 直接关系到波形质量。点数太少波形会呈明显的阶梯状点数太多相同频率下需要更高的采样率对定时器频率要求也更高。我的经验是 128 到 512 之间比较合适。128 点波形阶梯感比较明显但不加滤波也能看出大致形状适合简单演示。256 点常用档位阶梯感不明显配合一阶 RC 滤波后能得到比较平滑的正弦波。512 点波形非常漂亮但需要更高的定时器频率支持F103 最高 72MHz 主频的 APB1 定时器时钟下可以轻松做到但一般没必要。实际项目里我会根据目标频率范围来反推采样点数。比如想输出 1kHz 到 10kHz又希望采样点数不低于 128那么采样率至少需要 128 × 10000 1.28MHz。这也意味着定时器触发频率要达到 1.28MHz。F103 的定时器最高时钟是 72MHzAPB1 倍频后完全做得到。注意DAC 输出的是离散电压值即使采样点数再多输出终归是“台阶状”的电压只是台阶间距很小。要获得真正平滑的正弦波必须在 DAC 输出引脚后面加一级低通滤波器把高频台阶滤掉。这个后面单独讲。3. 核心代码实现与配置3.1 CubeMX 配置要点我用的是 STM32CubeMX 生成工程然后手动补代码效率最高。配置步骤如下第一步打开 DAC 外设。选择 DAC 的 Output Buffer 为 Enable这样 DAC 输出驱动能力更强可以带一定负载如果输出阻抗很高可以关掉 Buffer 省一点功耗。然后使能 DAC 通道的 DMA 请求勾选 DMA Continuous Requests这一步很关键不勾选的话DMA 搬运完一轮数据后就会停止波形只输出一个周期就没了。第二步配置 DMA。DMA 选择 DMA1 通道 3这是 DAC 通道 1 对应的 DMA 请求方向是 Memory-to-Peripheral从内存到外设优先级可以选 High。模式一定要选 Circular循环模式也就是说 DMA 自动循环搬运查找表数组从头到尾再从头开始这样波形才能连续输出。第三步配置定时器。我用 TIM2 作为触发源因为 TIM2 是 32 位定时器ARR 的调节范围更大频率调节的细腻度更好。配置 TIM2 为内部时钟触发向上计数模式。预分频器 PSC 和自动重装载值 ARR 的搭配决定了触发频率。第四步DAC 触发方式选择 Timer 2 Trigger Out event意思是 DAC 不再由软件写寄存器触发更新而是由 TIM2 的更新事件来触发 DMA 搬运下一个数据。这一步很多人容易漏不配置的话 DAC 的输出不会自动更新。3.2 主代码框架CubeMX 生成的工程基础上核心代码其实不多。第一步是初始化正弦查找表uint16_t sine_table[256]; void sine_table_init(void) { for (uint16_t i 0; i 256; i) { sine_table[i] (uint16_t)((sin(2 * 3.1415926 * i / 256) 1) * 2048); } }注意DMA 搬运的内存地址必须是数据类型的地址sine_table 是 uint16_t 数组一个元素占 2 字节对应 DAC 的 12 位数据寄存器。DMA 传输数据宽度要设置为 Half Word16 位这一点不能错。如果设成 Byte8 位DMA 只会搬运低位字节波形会乱掉。启动 DMA 搬运并开启 DAC 输出的代码如下HAL_DAC_Start_DMA(hdac, DAC_CHANNEL_1, (uint32_t *)sine_table, 256, DAC_ALIGN_12B_R); HAL_TIM_Base_Start(htim2);这里 HAL_DAC_Start_DMA 做了两件事启动 DMA 搬运同时启动 DAC 输出。参数里的 DAC_ALIGN_12B_R 表示 12 位右对齐意思是我们把 16 位数据的高 12 位对应到 DAC 上低 4 位忽略。因为 sine_table 的元素已经是 0~4095 的范围正好匹配。3.3 频率调节的具体实现频率调节的核心就是改 ARR 值。为什么改 ARR 而不是改 PSC因为 PSC 做的是预分频改动后需要重新计算计数器时序而且 PSC 是 16 位寄存器范围有限ARR 在 TIM2 上也是 32 位直接重载更精准。假设目标输出频率为 f_target采样点数 N256定时器时钟为 72MHzF103 的 APB1 定时器时钟经过倍频后为 72MHz那么Fs f_target × N ARR 1 72MHz / Fs ARR 72MHz / (f_target × N) - 1代入数值如果要输出 1kHz 正弦波N256ARR 72000000 / (1000 × 256) - 1 281.25 - 1取整为 280。这里取整会带来一点频率误差但实际上可以接受后面可以通过调整 PSC 做一个粗调。实际项目中我更推荐用一个函数来设置频率void set_sine_freq(uint32_t freq_hz) { // 256 个采样点TIM2 时钟 72MHz // 先计算 ARR保证 ARR 在合理范围内 uint32_t arr 72000000 / (freq_hz * 256) - 1; __HAL_TIM_SET_AUTORELOAD(htim2, arr); // 产生一次更新事件让新 ARR 立即生效 __HAL_TIM_SET_COUNTER(htim2, 0); __HAL_TIM_ENABLE(htim2); }调用这个函数时只需要set_sine_freq(2000)就能输出 2kHz 正弦波。实测下来在 300Hz 到 20kHz 这个范围内频率误差控制在 1% 以内完全满足一般信号发生器需求。3.4 按键调频与串口调频的扩展如果希望做成真正的“频率可调”设备还得加上交互界面。最简单的就是按键调频每次按键频率增加或减少一个步进。比如每次按一下加 100Hz长按可以快速推进。按键处理要注意消抖。最简单的消抖方式是延时 20ms 后读引脚但这样会占 CPU 时间。更好的方式是使用定时器轮询按键每 5ms 读一次引脚连续两次读到相同状态才认为按键有效。另一种更灵活的方案是串口调频。通过串口接收频率指令比如发送F5000\r\n就设置输出频率为 5kHz。在中断里接收一帧数据然后在主循环里解析调用 set_sine_freq。这个方案非常适合和上位机配合做自动化测试。串口调频需要处理的细节是频率参数是 ASCII 字符串要转成数字。用sscanf或者自己写解析都可以。我的习惯是定义一个结构体存储接收缓冲区当收到\r\n时置标志位主循环中处理if (usart_rx_flag 1) { uint32_t f; sscanf(usart_buf, F%lu, f); set_sine_freq(f); memset(usart_buf, 0, sizeof(usart_buf)); usart_rx_flag 0; }4. 常见问题与排查技巧4.1 波形输出一个周期就停止这是最典型的问题之一。现象是正弦波输出一次然后输出变成了固定电平不再变化。原因是 DMA 没有工作在循环模式。CubeMX 配置时DMA 的 Mode 必须设置为 Circular同时 DAC 的 DMA Continuous Requests 必须启用。还有一个容易忽略的地方在 CubeMX 里配置 DMA 后默认生成的代码可能在MX_DAC_Init()里配置了请求但是如果没有调用HAL_DAC_Start_DMA而是调用了HAL_DAC_Start那么 DMA 根本不会启动。DMA 搬运启动必须用HAL_DAC_Start_DMA。4.2 波形有严重台阶或毛刺输出波形呈现明显阶梯状这是因为采样点数少或者低通滤波没做好。可以分两步排查先增加采样点数到 512看看台阶是否变小如果台阶变小了说明问题在点数上可以保持 256 点然后加滤波器如果台阶没有变化说明采样率设置有问题DMA 搬运的数据可能没有覆盖整个正弦周期而是重复搬运了其中一部分。毛刺问题通常来自 DAC 输出引脚上的噪声或者电源不稳。在 DAC 输出引脚加一个 10nF 电容到地能把高频毛刺压下去。要注意的是这个电容会和 DAC 输出阻抗构成一个低通滤波器电容太大比如 1uF会导致波形响应变慢高频信号幅度衰减明显。4.3 频率调节范围受限如果发现频率上限上不去先算一下当前配置的极限。F103 的 TIM2 时钟是 72MHz最少 256 点一个周期理论上最高频率是 72MHz / 256 281kHz。但实际上 DAC 的输出建立时间约 1us也就是说 DAC 最快 1us 更新一次电压值采样率最高 1MHz。再考虑到 256 点波形最高频率确实在 281kHz 左右。如果实际只能到 100kHz 就上不去了那就要检查定时器配置看是不是 APB1 预分频没有配置好导致 TIM2 时钟只有 36MHz。4.4 DMA 和 CPU 访问同一块内存的冲突当 DMA 在搬运查找表数据时如果 CPU 同时读取同一个数组可能出现总线冲突DMA 传输变慢波形异常。解决办法是把查找表定义到 SRAM 的不同区域或者将数组定义为const放入 Flash。F103 的 DMA 可以直接访问 Flash但实际使用中我会把正弦查找表放在 SRAM 里并且用__attribute__((aligned(4)))对齐确保 DMA 可以高效访问。4.5 启动时的毛刺信号DAC 上电瞬间输出引脚会有一个短暂的电压跳变这是 DAC 输出缓冲器在启动时的固有行为。要消除这个毛刺可以在代码里先让 DAC 输出引脚输出低电平写入 0延时 50ms 后再启动 DMA 搬运。4.6 一个重要排查工具示波器调这种东西示波器是必须的。如果暂时没示波器也可以用一个音频输入加 Audacity 录音软件把 DAC 输出接到声卡线路输入观察波形形状和频率。这种方式精度当然不如示波器但初调足够。5. 低通滤波器选型与波形质量DAC 直接输出的信号经过示波器观察能看到明显的“阶梯状”电压。这是因为 DAC 输出电压是离散的每两个采样点之间的跳变是瞬时的。要获得平滑的正弦波必须后面接滤波器。最常用的就是一阶 RC 低通滤波器。RC 低通的截止频率公式是fc 1 / (2π × R × C)需要注意的是截止频率必须远高于目标正弦波频率同时远低于采样频率。比如采样率是 256kHz正弦波频率是 1kHz那么 RC 滤波器的截止频率可以取 10kHz 到 30kHz 之间。取高了滤波效果不明显波形还有台阶取低了1kHz 信号幅度会被衰减。我实测的配置是R 1kΩC 10nF截止频率约为 15.9kHz。用这个参数输出 1kHz 正弦波时波形已经非常平滑而输出 10kHz 时幅度衰减大约 0.5dB基本可以接受。如果要更高质量的波形可以考虑压控电压源二阶低通滤波器用运放加 RC 搭建但一般做信号发生器一阶 RC 其实是性价比最高的选择。6. 实测效果与个人心得整块板子调试下来最让我惊喜的是 DMA 模式下 CPU 占用率几乎为零。我同时在主循环里跑了一个 128×64 OLED 显示频率还开了串口中断收发波形输出完全不受影响。这种感觉和之前用中断烧 CPU 的方案对比太明显了。实测的几个频率点1kHz、5kHz、10kHz、20kHz示波器测到的频率和程序设定值误差都在 0.5% 以内。误差来源于取整计算和 DHR 锁存的一两个时钟周期的延迟对绝大多数应用场景来说已经足够了。如果后续想把频率范围扩展到更低比如 1Hz 到 100Hz直接把查找表采样点数增加到 4096配合慢速定时器输出波形质量会更好。要往高频走比如超过 100kHz就得考虑压缩采样点数或者换用更高主频的芯片了。在做这个项目的过程中我踩过最深的坑是 CubeMX 里 DAC 的 DMA Continuous Requests 没有勾选以及 DMA 模式没有设置为 Circular两个问题独立出现都导致波形只输出一周期就卡住。所以一开始调不出波形时优先检查这两项再看代码逻辑。还有一点经验之谈建议在工程里加一个计数变量在 DMA 中断里翻转一个 GPIO 电平用示波器看这个电平的周期就可以直接验证 DMA 搬运的频率是否和目标一致。这种“旁路验证法”在排查问题时效率特别高。搞清楚了 F103 这套 DMADAC 组合你会发现它在其他场景比如 ADC 的 DMA 采样、串口 DMA 收发里也有非常相似的工作模式可以把这套思维方式平移到整个 F103 系列甚至其他 MCU 上。本文还有配套的精品资源点击获取
返回列表