资讯详情

STM32信号发生器设计:DDS与DAC波形生成原理及工程实现

📅 2026/10/14 18:15:32 | 华诺云谱 👁 阅读
STM32信号发生器设计:DDS与DAC波形生成原理及工程实现
简介这是一套基于STM32单片机的信号发生器完整工程包面向嵌入式开发者和电子设计爱好者重点解决波形生成算法、数模转换与硬件电路设计相结合的综合实践问题。压缩包共146个文件约1.46MB内容涵盖C语言源码33个.h、31个.c、Keil工程配置文件、Altium Designer设计的原理图与PCB文件、PDF说明文档以及hex固件等源码基于STM32标准外设库编写涉及定时器、ADC、DAC等模块的初始化与信号处理原理图和PCB则清晰呈现了主控、电源管理、信号调理及输出电路的原理与布局。目前已有2307人学习下载。项目通过内部DAC或PWM方式生成三角波、正弦波和方波并支持频率与幅度调节虽然缺少Proteus仿真图但用户可结合实物调试或自行搭建仿真环境验证。对希望从零掌握STM32信号发生器设计、或需要一套软硬件兼备的工程参考方案的开发者而言这套资源提供了清晰的工程范例和实用排错思路适合在实验室、课程设计或毕业设计等场景中参考。1. 「STM32信号发生器.rar」这个压缩包值不值得解开跑通「STM32信号发生器.rar」这个压缩包不管里面是完整工程还是一段裸代码你真正要解决的其实只有两件事让它输出指定波形还能方便地调频率。很多人拿到包的第一反应是解压、编译、下载结果板子上一点反应都没有然后开始怀疑工程坏了。信号发生器这类项目的调试顺序和普通LED工程相反——先确认硬件输出链路再确认波形数据链路最后才轮到算频率参数。它适合手里有带DAC的STM32板子、想自己做一台可调信号源的开发者也适合实验室临时要一个信号源又不打算买仪器的场景。下面按我实际跑通这类工程的顺序展开先定方案再对硬件然后拆核心代码最后把最容易踩坑的地方集中说一遍。2. 波形生成的三条路线从查表法到DDS再到DMA搬运2.1 波形表与相位累加器信号发生器的“数据链路”怎么搭一个周期正弦波按固定点数采样后存成数组DAC按顺序把数组里的值转成电压这是最直觉的方案也是查表法的基础。真正的工程差异在于“按什么节奏把数组喂给DAC”。如果靠主循环逐个写入每两个输出点之间会混入循环判断、函数调用的时间抖动频率高一点波形就在示波器上左右晃。于是工程里普遍用DDS直接数字频率合成的思路维护一个32位相位累加器phase_acc每次更新时给它加上一个固定步长phase_step然后取相位累加器的高位查表。相位累加器的值代表“这个周期走到哪了”输出频率由这个公式决定f_out f_update * phase_step / 2^32f_update是查表更新的频率在STM32上通常由定时器触发中断或者DAC的触发源决定。这种做法的好处一眼就能看出来要调频率只需要改一个phase_step变量波形相位始终是连续的频率分辨率取决于相位累加器位数32位时步长最小能到f_update除以2的32次方在500kHz更新频率下大约是0.00012Hz完全够用。相比之下直接改定时器重载值来调频的方案步进受限于计数器的位数低频段调不了那么细而且改ARR的瞬间很容易产生相位跳变。2.2 内置DAC、PWM滤波、外置DACSTM32资源下的方案取舍确定了“查表 相位累加器”的数据链路之后剩下的是模拟输出通道选哪条。我这里直接给一张对比表是我自己选型时反复用的方案输出质量典型上限频率CPU占用成本与复杂度内置DAC 查表 DMA12位波形平滑100kHz级别取决于更新率极低DMA自动搬运最低芯片自带PWM RC滤波等效位数低纹波明显几十kHz以内低最低但失真大外置SPI/I2C DAC14~16位可选动态范围大可达MHz级别低但需要额外芯片高适合高端需求内置DAC方案在STM32信号发生器里是绝对的主流。原因很直接F103这类芯片自带12位DAC两个通道配合DMA可以在不占用CPU的情况下持续输出波形。PWM滤波方案虽然省钱但PWM的位数和RC滤波的截止频率互相打架——想滤得干净就得压低截止频率频率上限跟着下来想提高频率纹波又压不住。外置DAC性能最好但一个几百点的.rar工程大概率不会去用硬件改动也大。我一般会先确认目标芯片带不带DAC。带就走内置DAC不带才考虑PWM方案。很多开发者在选型阶段翻车就是因为拿到工程后没看芯片型号默认“STM32都有DAC”结果板子焊完才发现PA4和PA5上什么都没有。2.3 波形表现在算还是离线生成一个影响日后维护的决定波形表可以在上电时用C语言的sin函数现场算也可以用上位机脚本离线算好然后以常量数组的形式编译进固件。我的选择一直是后者而且建议你也这么做。MCU现场调用sin会引入浮点库和计算耗时上电初始化慢堆栈小的工程还可能跑飞。更重要的是离线生成脚本可以随时改参数重新出表不用反复烧固件调试。这里给你一个能直接用的Python生成脚本输出256点、12位DAC右对齐的正弦表import math N 256 # 表长一个周期的采样点数 AMPLITUDE 2047 # 幅度12位DAC满量程4095留一点余量 OFFSET 2048 # 直流偏置让波形在0~3.3V之间摆动 def main(): vals [] for i in range(N): v OFFSET AMPLITUDE * math.sin(2 * math.pi * i / N) vals.append(int(round(v))) for i in range(0, N, 8): chunk vals[i:i8] print( , .join(f0x{v:04X} for v in chunk) ,) if __name__ __main__: main()这段脚本的输出可以直接粘进C源文件里的const数组。N取256而不是64是因为点数越多DAC输出波形越平滑尤其是高频段64点的正弦波在示波器上能明显看到台阶。幅度没有取满4095是给运放和后级电路留线性裕量避免削顶。OFFSET2048把波形中心放到中位单电源供电时输出范围才是0到VREF的完整区间。脚本本身不挑平台Windows、Linux、macOS都能跑只要装了Python就行。离线生成表还有一个额外好处你不需要在MCU里维护一堆数学公式。想改成三角波、锯齿波、自定义任意波形直接改脚本里的数学表达式重新生成数组固件侧一行代码都不用动。3. 硬件链路与工程骨架从芯片引脚到运放输出一次对上3.1 先确认你的芯片有没有DAC管脚、通道和参考电压拿到一个信号发生器工程第一步不是打开代码看逻辑而是打开数据手册看芯片型号。以最常见的STM32F103C8T6为例它带一个DAC外设两个输出通道PA4是DAC_OUT1PA5是DAC_OUT2。如果你手里的板子用的是不带DAC的型号比如某些低成本的G0系列或者纯F1的低配型号PA4和PA5只能当普通GPIO用那这个压缩包里的DAC方案就直接废了得换PWM方案。接线时还有一个容易漏的点DAC的参考电压。F103的DAC输出电压范围是0到VREFVREF通常和VDDA接在一起。如果板子上VREF引脚悬空DAC输出会异常。很多开发者把DAC输出端直接接示波器探头探头地线夹子接板子GND结果波形不对其实问题不在运放也不在代码而是VREF根本没接。DAC输出引脚还有一个特别的初始化要求PA4和PA5要配置为模拟输入模式不是复用推挽输出。这一点和UART、SPI的引脚配置完全不同。配置成复用推挽后DAC外设的输出不会正确送到引脚上表现就是输出电压一直为0。我后面避坑章节会再细说。3.2 DAC输出直连示波器的后果运放缓冲与RC滤波DAC的输出端不是理想电压源内部有输出阻抗带负载能力有限。直接把DAC输出接到后级电路或者长线缆上负载会把电压拉低波形顶部和底部会被压弯。正规做法是加一级单位增益跟随器用常见的轨到轨单电源运放电源和地之间加0.1uF去耦电容输出串一个几十欧的电阻再走线。这一级电路的作用是隔离让DAC看到的是高阻抗负载后级看到的是低阻抗驱动。如果波形还嫌毛糙可以在运放输出后再加一个RC低通截止频率按你最高输出频率的3到5倍来选。比如最高输出20kHzRC截止频率取80~100kHz既能压掉一部分高频噪声又不至于把有用信号幅度衰减太多。RC参数的经验公式是f 1 / (2 * pi * R * C)选R1kC1.8nF左右就是接近90kHz。下面这段是常见的DAC输出初始化代码以HAL库为例void dac_gpio_init(void) { __HAL_RCC_DAC_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef gpio {0}; gpio.Pin GPIO_PIN_4; /* DAC_OUT1注意同时初始化PA5就是通道2 */ gpio.Mode GPIO_MODE_ANALOG; /* DAC引脚必须是模拟输入模式 */ gpio.Pull GPIO_NOPULL; HAL_GPIO_Init(GPIOA, gpio); }代码里的关键是gpio.Mode GPIO_MODE_ANALOG。DAC输出引脚不是推挽也不是复用必须配成模拟模式。如果你用标准库对应的写法是GPIO_Mode_AIN效果一样。初始化顺序也值得注意先开外设时钟再初始化GPIO最后使能DAC通道顺序反了会出现偶发性的首次输出异常。3.3 按键、编码器和OLED人机交互模块怎么接才不干扰模拟输出信号发生器不能只输出一个固定频率总要有个方式调节频率、切换波形。最简单的方案是两个按键一个切换波形一个调频率。调节又分粗调和细调用一个按键循环加频率长按连续加短按加一档。这个方案够用但体验一般。工程一点的做法是上旋转编码器。编码器有A、B两相旋转时输出相位差90度的方波用定时器编码器接口模式直接解算方向和步数。编码器的两个引脚接在定时器的CH1和CH2上不用外部中断也不用软件消抖硬件帮我们解决了大部分问题。频率调节手感比按键好太多而且编码器本身带按键按下可以切换波形或菜单一个模块干两件事。交互模块的接线要注意和模拟输出区的隔离。编码器、按键、OLED的走线如果贴着DAC输出走数字信号跳变会耦合到模拟输出上波形上出现细小的毛刺。我的做法是PCB上把模拟输出区划成独立小块数字信号线和DAC输出线之间留出间隔必要时加一个磁珠或者0欧电阻把模拟地和数字地单点连接。手工搭面包板时则尽量让编码器和OLED的飞线远离PA4和运放输出那一侧。4. 核心代码拆解相位累加与DAC触发链路的落地写法4.1 把正弦表编译进Flash脚本生成的数组和C文件组织方式第2章的脚本生成了一组十六进制数组接下来把它组织成C代码。我习惯把波形表单独放一个文件比如wave_tables.c头文件里只暴露表名和表长这样以后加波形不用动主逻辑。/* wave_tables.c */ #include wave_tables.h const uint16_t sine_lut[LUT_SIZE] { 0x0800, 0x0819, 0x0833, 0x084C, 0x0866, 0x087F, 0x0899, 0x08B2, /* ... 由Python脚本生成的256个点 ... */ };表默认放在Flash的const段。查表时直接从Flash读DMA数据在某些型号上没问题但从Flash搬运到DAC的DMA访问在部分芯片上会插入等待周期影响最高更新率。更稳妥的做法是为DMA准备一块SRAM缓冲区上电时把波形表从Flash拷进SRAMDMA从SRAM读数据static uint16_t dma_buf[LUT_SIZE]; void wave_table_load(const uint16_t *src, uint16_t len) { memcpy(dma_buf, src, len * sizeof(uint16_t)); }这个拷贝只做一次成本可以忽略。换来的是DMA访问SRAM的时序更稳定频率上限更高调试时也更容易通过IDE的Live Watch查看缓冲区内容。缺点是多占256字节的SRAM对于信号发生器这种应用完全可以接受。4.2 相位累加与定时器触发核心逻辑只需要两个变量DDS的核心状态只有两个相位累加器phase_acc和相位步进phase_step。相位步进和目标频率之间的关系前面已经给过公式这里给出实际换算函数/* 给定目标频率和定时器更新频率计算相位步进 */ uint32_t freq_to_step(uint32_t freq_hz, uint32_t update_hz) { return (uint32_t)(((uint64_t)freq_hz 32) / update_hz); }用64位中间变量是为了防止32位乘法溢出。比如目标1kHz、更新率500kHz代入公式算出来的步进大约是0x00833333。频率越高步进越大查表索引走得更快。下面这段是定时器中断里的核心更新逻辑volatile uint32_t phase_acc 0; volatile uint32_t phase_step 0; const uint16_t *active_lut sine_lut; uint16_t dac_value; void TIM_UP_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); phase_acc phase_step; uint32_t index (phase_acc 24) 0xFF; /* 取高8位作为查表索引 */ dac_value active_lut[index]; DAC_SetChannel1Data(DAC_ALIGN_12B_R, dac_value); } }这里查表索引用的是相位累加器的高8位正好对应256点表。相位累加器是32位的高8位变化一圈代表一个完整周期低24位相当于相位细分。不需要对索引取模因为按位与0xFF本身就是模256运算比取模指令快得多。中断方式有一个明显瓶颈每输出一个点都要进一次中断更新率越高中断占CPU时间越多。更新率到500kHz时CPU几乎全耗在中断里。所以工程里更推荐用定时器触发DAC DMA搬运的方式DAC每次转换由定时器硬件触发DMA自动把波形表里的下一个点搬进DAC数据寄存器完全不进中断。这种模式下要换波形只需切换DMA的源地址要调频率只需改定时器的重载值。两种模式怎么选我的建议是低频、要精细调频的场景用DDS中断方式高频、要稳定输出的场景用定时器加DMA。压缩包里如果写的是中断查表你把它改成DMA方案也不难核心就是那几步配置。4.3 切换波形换查找表指针而不是改中断逻辑工程里如果要支持正弦波、方波、三角波、锯齿波四种波形不需要写四套输出逻辑。只需要生成四张表然后用一个指针指向当前生效的表typedef enum { WAVE_SINE 0, WAVE_SQUARE, WAVE_TRIANGLE, WAVE_SAWTOOTH, WAVE_MAX } wave_type_t; const uint16_t *wave_tables[WAVE_MAX] { sine_lut, square_lut, triangle_lut, sawtooth_lut }; void wave_select(wave_type_t type) { if (type WAVE_MAX) { active_lut wave_tables[type]; } }切换波形时要注意一点如果新表和旧表的表长不一致中断或DMA里的索引掩码也要跟着改。我见过的翻车案例多半是表长从256改成512后索引掩码还停留在0xFF结果波形变成了两段重复的错乱数据。避免方法是把掩码也放进一个结构体跟随波形表一起切换。方波其实不需要一张256点的表直接写成前半段是满幅、后半段是零幅的数组就行。用DAC输出方波的好处是幅度可以精确控制比GPIO翻转更灵活缺点是压摆率受DAC转换速度限制高频方波会变成梯形波。如果你要的是几十kHz以上的方波还是直接上GPIO翻转更实在。5. 信号发生器调试避坑五个高频翻车记录与排查路径5.1 现象DAC输出引脚电压始终为0很多人的第一反应是DAC坏了或者代码没下载进去。换一块板子还是一样才开始怀疑配置。原因多半是GPIO的Mode配置成了GPIO_MODE_AF_PP也就是复用推挽。DAC的引脚不是复用功能它需要配置成模拟输入模式。复用推挽模式下GPIO输出寄存器会接管引脚DAC信号根本到不了外部。解决方法是把GPIO初始化改成GPIO_MODE_ANALOG标准库对应GPIO_Mode_AIN重新初始化后输出就正常了。5.2 现象正弦波在示波器上明显有台阶波形表只有64个点或者定时器更新率太低。DAC输出本质是阶梯波点数越少台阶越明显。把波形表加到256点台阶在低频下基本看不见。如果点数已经加了还是有台阶检查一下DAC是不是工作在右对齐12位模式数据位没有对齐会让低几位丢失波形也会出现细微的量化噪声。5.3 现象波形整体抖动频率读数不稳定中断里执行了浮点运算或者查表逻辑里混入了条件分支。频率抖动在示波器上表现为波形左右晃动在频谱分析仪上表现为底噪抬高。解决方法是把频率换算放到主循环里做中断里只做相位累加和查表不要出现除法、浮点、函数调用这些耗时操作。另一个常见原因是DMA和CPU同时访问同一块SRAM缓冲区仲裁导致的延迟。把波形表放到独立的SRAM区或者改用DMA双缓冲抖动会明显改善。5.4 现象输出频率和设置值差了一倍定时器时钟树没看清楚。F103的APB1最高36MHz但定时器时钟在APB1预分频不为1时会翻倍到72MHz。如果你按36MHz计算预分频实际定时器跑在72MHz输出频率就差了一倍。排查方法是在线调试里读一下定时器时钟寄存器或者在初始化后把定时器的实际更新率回读出来打印。这类问题不是代码逻辑错是时钟树配置和芯片手册没对上。5.5 现象调频率的瞬间波形“咔哒”一声跳变调频率时phase_step直接赋值相位累加器还在原来的相位上突然改变步长造成相位不连续。对纯正弦波来说相位不连续意味着波形从一个点突然跳到另一个点扬声器里就是咔哒声示波器上能看到波形瞬间错位。解决方法是把频率更新放到按键抬起时执行或者在主循环里对相位步进做渐变让频率在几十毫秒内平滑过渡到新值。/* 相位步进渐变避免频率跳变 */ uint32_t target_step freq_to_step(target_freq, APP_UPDATE_HZ); while (phase_step ! target_step) { if (phase_step target_step) { phase_step (target_step - phase_step) / 16 1; } else { phase_step - (phase_step - target_step) / 16 1; } delay_ms(2); }这段代码让相位步进按指数趋近的方式逼近目标值。除以16再取整保证步长变化足够平滑同时不会永不到达。实际调音色时这个渐变慢一点没关系但如果是扫频测试场景渐变反而会拖慢响应那时就该关掉这个逻辑直接赋值。6. 进阶三个让信号源更接近“仪器”的习惯第一个习惯是给DAC做软件校准。DAC的线性误差和零位误差每一颗芯片都略有差异直接按理论值输出实测电压和设定电压可能有几十毫伏偏差。做法是先用万用表测两个校准点比如设0x0800测实际电压设0x0CC0测实际电压然后算出斜率和偏置把校准系数存到Flash末尾的独立扇区。以后每次上电读出来在设置DAC数据之前先做一次线性修正。这一步做完你的信号发生器就从“能出波形”变成了“幅度可信”。第二个习惯是直接用双通道输出正交波形。F103带两个DAC通道可以一个出正弦一个出余弦相位差固定在90度。把两个通道的LUT差四分之一周期用同一个定时器触发就能得到正交输出。这对测试音频电路、电机编码器模拟信号特别有用。比外接两个独立信号源对相位要准确得多因为用的是同一个时钟源。第三个习惯是留一个串口命令行接口。频率、波形、幅度、校准系数都通过串口ASCII命令读写。好处是调试自动化——你可以在PC端写脚本批量扫频测后级电路的频响曲线。我自己的习惯是每个信号发生器工程都放一个“cli”模块命令格式固定为“set freq 1000”“set wave sine”这种解析用strtok加sscanf不超过50行代码。后期不管是演示还是测试都不用去接按键狂按。这三个习惯里如果只记一条我会强烈建议你保留“波形表由上位机脚本生成”这个工作方式。它能把波形设计的灵活性和MCU侧的执行效率完全分开改波形不需要碰C代码改参数不需要重刷固件。很多项目工期拖久后代码都乱了但波形表生成脚本和数据文件始终是干净的那一层。希望这一套从方案到避坑的笔记能帮你把这个压缩包里的东西变成一台真正能用的信号源。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑