尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
STM32F407软件DDS函数发生器:定时器触发DAC与DMA实现波形生成
简介这份资源面向学习STM32嵌入式开发与信号发生技术的初学者及工程师提供一套基于STM32F407的DDS函数发生器完整工程。方案采用软件配合DAC的方式可生成任意频率、任意占空比、0至3.3V幅值的正弦波、方波与三角波直接适配正点原子探索者STM32F407ZGT6开发板无需改动代码即可运行适合用于通信测试、信号分析与课程设计等场景。压缩包共307个文件约10.96MB以C源码、头文件、编译中间文件与工程配置为主涵盖核心驱动、固件库、硬件抽象层及用户代码等模块并附有说明文档与Keil工程文件便于直接编译下载。目前已有376人学习下载。通过该工程可掌握DDS查表与定时器、DAC协同输出的实现思路理解波形频率、占空比与幅值的软件调节方法并借助现成目录结构快速上手调试与二次开发。1. 从一块 F407 开发板说起为什么我要用软件 DDS 替代专用芯片去年帮一个做音频测试的朋友搭信号源他原本的方案是买现成的 DDS 专用芯片比如 AD9833 那一类焊上去、写几个寄存器就能出正弦波。问题是芯片输出幅度固定、频率分辨率受限于参考时钟想扫个频还得外挂 MCU 反复算控制字。后来我翻出吃灰的 STM32F407 探索板琢磨着能不能用片内 DAC 加定时器纯软件把 DDS 跑起来。结论是能跑而且频率分辨率、波形种类、扫频逻辑全在自己手里改一行参数就能换一种输出。这套基于 STM32F407 的 DDS 函数发生器核心就是定时器触发 DAC、DMA 搬运波形表、按键或串口调参适合想学 DDS 原理又不想被专用芯片绑死的嵌入式开发者也适合拿它当课程设计或信号源练手项目的人。2. 系统架构与硬件链路定时器、DMA、DAC 怎么串起来2.1 为什么选 F407 而不是 F103F103 也能做 DDS但 F407 有几个硬指标更舒服。主频 168MHz定时器触发频率可以拉得更高输出正弦波的上限频率更宽DAC 是 12 位双通道配合 DMA 双缓冲能减少波形切换时的毛刺再就是 FPU 和更多 RAM波形表可以放得更大相位累加器用浮点算也不心疼。常见做法是把一个完整周期的正弦波采样 256 或 512 个点存成 uint16_t 数组放 Flash 或 RAMDMA 循环搬运到 DAC 数据寄存器定时器每个触发周期搬一个点。输出频率公式是 f_out f_timer / N其中 N 是波形表点数f_timer 是定时器触发频率。比如表长 256想要 1kHz 正弦波定时器就得跑 256kHz。2.2 硬件连接与引脚分配DAC 通道 1 对应 PA4通道 2 对应 PA5。如果只做单通道输出PA4 接示波器或后级运放即可。按键用 PE2、PE3、PE4 这类普通 GPIO串口用 USART1 的 PA9/PA10 接 USB 转串口模块。注意 PA4 默认是模拟输入配置成 DAC 输出后不要再复用成其他功能。供电方面VDDA 和 VREF 要接干净的 3.3VDAC 输出满量程就是 VREF想输出双极性波形得外接运放做偏置和放大。2.3 数据流从相位累加器到 DAC 引脚整个链路是这样的定时器更新事件触发 DMA 请求DMA 从波形表里取一个点写入 DAC_DHR12R1DAC 立刻转换输出。相位累加器不在中断里跑而是提前把频率控制字算好直接改定时器的 ARR 值来调输出频率。这样做的好处是 CPU 几乎不参与波形生成主循环只负责按键扫描和串口解析。如果要做频率微调或扫频改 ARR 就行响应很快。// 波形表256 点正弦12 位 DAC 范围 0~4095 const uint16_t sine_table[256] { 2048, 2145, 2242, /* ... 中间省略 ... */ 1951 }; // DAC 初始化开启 DMA 循环模式 void DAC_DMA_Init(void) { DAC_InitTypeDef DAC_InitStruct; DMA_InitTypeDef DMA_InitStruct; // 使能时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); // DMA1 Stream5 通道7 对应 DAC 通道1 DMA_InitStruct.DMA_Channel DMA_Channel_7; DMA_InitStruct.DMA_PeripheralBaseAddr (uint32_t)DAC-DHR12R1; DMA_InitStruct.DMA_Memory0BaseAddr (uint32_t)sine_table; DMA_InitStruct.DMA_DIR DMA_DIR_MemoryToPeripheral; DMA_InitStruct.DMA_BufferSize 256; DMA_InitStruct.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStruct.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStruct.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStruct.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStruct.DMA_Mode DMA_Mode_Circular; DMA_InitStruct.DMA_Priority DMA_Priority_High; DMA_Init(DMA1_Stream5, DMA_InitStruct); // DAC 配置定时器触发DMA 使能 DAC_InitStruct.DAC_Trigger DAC_Trigger_T2_TRGO; DAC_InitStruct.DAC_WaveGeneration DAC_WaveGeneration_None; DAC_InitStruct.DAC_OutputBuffer DAC_OutputBuffer_Enable; DAC_Init(DAC_Channel_1, DAC_InitStruct); DAC_Cmd(DAC_Channel_1, ENABLE); DAC_DMACmd(DAC_Channel_1, ENABLE); DMA_Cmd(DMA1_Stream5, ENABLE); }这段代码里几个关键点DMA 模式必须是 Circular否则搬完 256 个点就停了外设地址不递增因为始终写同一个 DAC 寄存器内存地址递增才能依次取表里的点。DAC_Trigger 选 T2_TRGO意思是定时器 2 的更新事件作为触发源后面配置 TIM2 时要把 TRGO 设成 Update。2.4 定时器配置与频率计算TIM2 挂在 APB1 上时钟 84MHz。要得到 f_timer预分频和自动重装载值这样算f_timer 84MHz / ((PSC1)*(ARR1))。假设 PSC 固定为 0ARR 84M / f_timer - 1。比如要输出 1kHz 正弦波表长 256f_timer 256kHzARR 84M/256k - 1 327。实际写代码时把 ARR 做成变量按键调频就是改这个值。void TIM2_Init(uint32_t arr) { TIM_TimeBaseInitTypeDef TIM_InitStruct; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_InitStruct.TIM_Prescaler 0; TIM_InitStruct.TIM_Period arr; TIM_InitStruct.TIM_ClockDivision TIM_CKD_DIV1; TIM_InitStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_InitStruct); // 更新事件作为 TRGO 输出 TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); TIM_Cmd(TIM2, ENABLE); }改频率时先 TIM_Cmd(DISABLE)改 TIM_SetAutoreload(TIM2, new_arr)再 TIM_Cmd(ENABLE)避免运行中改 ARR 导致第一个周期异常。3. 核心模块拆解波形生成、调参与输出切换3.1 波形表生成正弦、方波、三角波、锯齿波正弦表可以用 Python 或 MATLAB 提前算好也可以运行时用 arm_sin_f32 生成。方波最简单前 128 个点填 4095后 128 个点填 0。三角波线性递增再递减。锯齿波从 0 到 4095 循环。所有表统一 256 点长度切换波形就是换 DMA 的 Memory0BaseAddr然后重新使能 DMA 流。// 方波表 const uint16_t square_table[256] { // 前 128 个 4095后 128 个 0 }; // 切换波形改 DMA 源地址 void Waveform_Switch(const uint16_t *new_table) { DMA_Cmd(DMA1_Stream5, DISABLE); while (DMA_GetCmdStatus(DMA1_Stream5) ! DISABLE); DMA_SetCurrDataCounter(DMA1_Stream5, 256); DMA1_Stream5-M0AR (uint32_t)new_table; DMA_Cmd(DMA1_Stream5, ENABLE); }注意改 M0AR 之前必须关 DMA 流否则可能搬一半旧表一半新表输出波形会闪一下。DMA_SetCurrDataCounter 也要重置不然计数器停在中间值。3.2 频率与幅度调节按键和串口两种方式按键调频用两个键一个加一个减每次改 ARR 的步进值。步进可以分档比如 1Hz、10Hz、100Hz、1kHz短按切换档位长按连续调。串口调参更灵活发 “FREQ 1000” 设频率“WAVE SINE” 切波形“AMP 50” 调幅度。幅度调节不能直接改 DAC 输出得在波形表上做乘法或者外接数字电位器。软件做法是维护一个幅度系数DMA 搬运前对表值缩放但这样 CPU 要参与。更常见的做法是提前生成几组不同幅度的表切换时换表。// 串口命令解析片段 void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { char ch USART_ReceiveData(USART1); // 拼命令遇到换行符解析 if (ch \n) { cmd_buffer[cmd_index] \0; Parse_Command(cmd_buffer); cmd_index 0; } else { cmd_buffer[cmd_index] ch; } } }Parse_Command 里用 sscanf 或字符串比较都行关键是命令要短、解析要快别在中断里做浮点运算。3.3 扫频与突发模式定时器中断里改 ARR扫频就是每隔固定时间改一次 ARR比如 10ms 改一次从 100Hz 线性扫到 10kHz。用 TIM3 做扫频定时器中断里算下一个频率点然后更新 TIM2 的 ARR。突发模式是输出一段波形后停一段时间靠控制 DAC 的使能位或 DMA 的使能位实现。注意改 ARR 时如果 TIM2 还在跑最好先关再改再开否则可能出现一个周期特别长或特别短。// TIM3 中断每 10ms 更新一次输出频率 void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); current_freq sweep_step; if (current_freq sweep_end) current_freq sweep_start; uint32_t arr 84000000 / (current_freq * 256) - 1; TIM_Cmd(TIM2, DISABLE); TIM_SetAutoreload(TIM2, arr); TIM_Cmd(TIM2, ENABLE); } }这里 84000000 是 TIM2 时钟256 是表长。实际用的时候把这两个数定义成宏换板子或改表长时只改宏定义。4. 避坑与常见问题调不通时先查这几处4.1 输出一直是直流或没波形现象示波器接 PA4只有一条直线不随频率变化。原因通常是 DMA 没启动或 DAC 没使能。解决先查 DMA_Cmd 和 DAC_Cmd 是否都调用了再看 DMA 的 Circular 模式有没有配错。还有一个隐蔽情况是 DAC 输出缓冲没开输出阻抗太高示波器探头一接就拉低。把 DAC_OutputBuffer_Enable 加上。4.2 波形频率对不上差整数倍现象设 1kHz 出来 2kHz 或 500Hz。原因多半是表长和定时器频率算错了。比如表长实际是 128 但代码里按 256 算频率就翻倍。解决确认波形表数组长度和 DMA_BufferSize 一致再核对 f_timer f_out * N 这个公式。另外 TIM2 时钟不是 168MHzAPB1 定时器时钟是 84MHz别拿 168M 去算。4.3 切换波形时输出闪断或毛刺现象换波形瞬间输出跳变。原因DMA 没关就改源地址或者新旧表长度不一致。解决切换前先关 DMA 流等 DMA_GetCmdStatus 返回 DISABLE再改 M0AR 和计数器最后重新使能。所有波形表统一长度别一个 256 一个 512。4.4 高频段波形失真严重现象输出频率超过 20kHz 后正弦波变成三角波甚至锯齿。原因每个周期采样点太少比如 256 点表在 100kHz 输出时定时器触发频率 25.6MHz已经超过 DAC 建立时间和 DMA 响应速度。解决高频段换短表比如 64 点或 32 点或者降低输出频率上限。F407 的 DAC 在 12 位下建立时间约 1us理论极限也就 1MHz 左右实际能到 200kHz 就不错了。4.5 串口命令没反应现象发命令后频率不变。原因中断优先级冲突或者命令解析里用了阻塞式发送导致接收丢字节。解决检查 USART1 和 TIM2 的中断优先级别让定时器中断打断串口接收。命令解析里别用 printf 重定向用非阻塞发送或直接回显简单字符。5. 二次开发与改造从固定波形到任意波形发生器这套代码最值钱的地方不是输出正弦波而是它把 DDS 的骨架搭好了。想改成任意波形发生器只需要把波形表换成用户上传的数据。串口加一条 “LOAD” 命令接收 256 个 12 位数据存到 RAM 数组然后把 DMA 源地址指过去。注意 RAM 数组要用attribute((aligned(4))) 对齐DMA 访问非对齐地址会出错。// 任意波形缓冲区 __attribute__((aligned(4))) uint16_t arb_buffer[256]; // 串口接收完 256 个点后调用 void Load_Arb_Waveform(void) { Waveform_Switch(arb_buffer); }如果想做双通道输出DAC 通道 2 对应 PA5需要再开一个 DMA 流。两个通道可以独立调频也可以共用同一个定时器触发但用不同表。双通道时注意 DMA1 的流不能冲突通道 1 用 Stream5通道 2 用 Stream6。另一个改造方向是加屏幕。F407 探索板自带 LCD 接口把频率、波形类型、幅度显示上去按键交互更直观。屏幕刷新别放在主循环里死等用定时器触发局部刷新。我一般会把 UI 和波形生成彻底分开UI 只改变量波形生成只读变量避免互相阻塞。验证改造是否成功最直接的办法是接示波器看波形同时用串口打印当前 ARR 和表地址。如果波形不对先确认表数据对不对再确认 DMA 搬的是不是这张表。我习惯在切换波形后加一句串口输出把表名和首地址打出来比盲猜快得多。从那以后我每次改 DMA 源地址之前都强制走一遍“关流、等状态、改地址、重置计数器、开流”这五步少一步都可能翻车。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

数字电路实验复习全攻略:从组合逻辑、时序逻辑到Verilog代码与实验报告闭环

数字电路实验复习全攻略:从组合逻辑、时序逻辑到Verilog代码与实验报告闭环

简介:这份资料是西北工业大学数字电子技术实验的完整学习包,面向电子工程、计算机科学等专业正在修读数电课程或准备实验考核的本科生。内容覆盖逻辑门电路、组合与时序逻辑、存储器及FPGA等主题,配合布尔代数、卡诺图、状态机设计与Verilog …

📅 2026/10/19 8:11:15
C/C++程序注释删除实战:手写状态机实现词法分析

C/C++程序注释删除实战:手写状态机实现词法分析

1. 项目概述:这道题到底在考什么1.1 题面理解:不只是“把注释删掉”这么简单PTA平台上的“删除C/C程序中的注释”是一道非常经典的词法分析题。我把它“存档”,是因为这道题看起来像是字符串替换的入门操作,实际上却暗藏不少坑&am…

📅 2026/10/19 8:11:15
污水处理站用施耐德TM218 PLC开发程序:流程、逻辑与避坑指南

污水处理站用施耐德TM218 PLC开发程序:流程、逻辑与避坑指南

污水处理站用施耐德TM218 PLC做程序开发,我来聊聊完整流程和踩过的坑我自己做过几个中小型污水处理站的电控项目,从早期的继电器控制一路做到PLC集中控制,接触施耐德TM218那批PLC也有五六年了。很多刚接触这个型号的朋友,一上来就…

📅 2026/10/19 8:11:15
MORE NEWS

更多资讯

📰

数据结构期末考什么?从一道综合题拆解复习重点与考场策略

1. 从一道期末题看数据结构到底在考什么每年一到期末季,数据结构这门课的讨论热度就会直线上升。我带的几届学生里,几乎所有人都会在复习阶段问同一个问题:这门课到底在考什么?是背概念,还是写代码?是记结论…

📰

GCC编译选项实战指南:从-O0到-O3的优化与调试

1. 为什么你需要重新认识GCC编译选项很多人第一次接触GCC,都是从一句gcc hello.c -o hello开始的。这条命令能跑通,于是很多人就默认自己“会用GCC”了。但真正到了要排查一个段错误、要定位一个只在特定优化级别下才出现的诡异Bug、要把一个几十万行的C…

📰

GCC编译选项实战指南:从调试到优化的高频选项详解

1. 为什么你需要吃透gcc编译选项很多人写C/C代码,编译命令永远是那一句gcc test.c -o test,能跑就行。直到某天遇到一个段错误,排查了整整两天,最后发现是编译时没开调试信息,core dump拿到手里连行号都看不到&#xf…

📰

内核启动早期打印:从原理到实战的完整实现指南

1. 从一行乱码说起:内核启动早期打印到底在解决什么问题如果你曾经在调试一块新板子,或者给一个嵌入式系统做底层适配,大概率遇到过这样的场景:串口工具打开,上电,屏幕上刷出来几行字,然后卡死。…

📰

Java开发者大模型应用转型指南:AutoGen与CrewAI多Agent实战

1. 为什么是这个时候转?Java开发者跨入大模型应用的真实动机与筛选逻辑先讲一个我最近遇到的事。一个写了五年Java后端的朋友来找我,手里已经用Spring Boot撸过一个客服工单系统,问我大模型应用到底值不值得转。我没急着回答,先问…

📰

MFC集成ChartCtrl实现实时波形显示与性能优化

简介:这份源码资源面向具备一定C基础的Windows开发者,尤其是需要在MFC工程中快速实现数据可视化的初学者与工程人员。它通过一个可运行的对话框程序,演示如何借助ChartCtrl控件完成波形数据的采集、绘制与实时刷新,解决传统绘图方…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬