多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

STM32与AD9833 DDS信号发生器:低成本高精度波形生成方案

STM32与AD9833 DDS信号发生器:低成本高精度波形生成方案 1. 为什么我要用STM32加DDS方案做信号发生器信号发生器这个东西玩电子的朋友应该都不陌生。调试电路的时候需要正弦波测试滤波器的时候需要扫频信号给学生上课演示的时候需要方波和三角波手头没有一台像样的信号源很多事情就是卡在那里推不动。市面上的成品信号发生器能输出到几兆赫兹、带调制功能的价格基本都在大几百到几千块对于个人爱好者和小型实验室来说这个投入不算小。而低端的函数发生器芯片方案比如经典的XR2206或者ICL8038虽然便宜但频率稳定度差、波形失真大、调节靠电位器用起来精度完全没法看。我这次做的这个项目核心思路就是用STM32做控制大脑搭配一颗DDS专用芯片来产生波形。DDS是Direct Digital Synthesis的缩写中文叫直接数字频率合成。它的原理说起来也不复杂把一个完整周期的正弦波采样成很多个点存到芯片内部的查找表里然后按照你设定的频率以固定的时钟节拍去“读”这些点读得快输出频率就高读得慢输出频率就低。因为整个过程是数字控制的所以频率精度可以做到非常高而且切换频率的时候相位是连续的不会像模拟方案那样出现跳变和毛刺。STM32在这套方案里承担的角色是接收用户输入、计算频率控制字、通过SPI或者并口把控制字写给DDS芯片、驱动显示屏显示当前参数、处理按键或者旋转编码器的输入。如果你用的是STM32F103这种经典型号主频72MHz跑这些任务绰绰有余。整个系统的成本可以控制在几十块钱以内性能却能达到专业入门级信号发生器的水平。这个项目适合谁呢我觉得有三类人可以重点关注。第一类是电子相关专业的学生尤其是正在做毕业设计或者课程设计的信号发生器是一个非常好的综合练习项目涉及单片机、SPI通信、人机交互、模拟电路等多个知识点。第二类是嵌入式开发的初学者想找一个有一定复杂度但又不至于太难的实战项目来练手。第三类是实验室或者创客空间的技术人员需要一台低成本、可定制、方便二次开发的信号源设备。源码我全部开源你可以直接拿来用也可以根据自己的需求修改波形表、增加调制功能、换用不同的显示屏。2. 整体方案设计与核心器件选型2.1 DDS芯片的选型对比与决策逻辑做DDS信号发生器绕不开的就是选哪颗DDS芯片。市面上常见的方案主要有ADI公司的AD9833、AD9834、AD9850、AD9851这几款还有国产的一些替代型号。我当初选型的时候把这几款都仔细对比了一遍最后选了AD9833原因后面会详细说。先看AD9833。这颗芯片是10引脚MSOP封装非常小供电范围2.3V到5.5V最高时钟频率25MHz输出频率最高可以到12.5MHz。它内部有一个28位的频率寄存器什么意思呢就是说频率控制字是28位的配合25MHz的参考时钟频率分辨率可以达到25MHz除以2的28次方算下来大约是0.093Hz。这个分辨率对于绝大多数应用来说已经非常精细了。AD9833输出的是正弦波、方波和三角波三种波形通过SPI接口控制接线简单编程也不复杂。再看AD9850。这颗芯片性能更强最高时钟频率125MHz输出频率可以到40MHz以上但它是28引脚SSOP封装体积大不少功耗也高而且价格比AD9833贵了好几倍。AD9851跟AD9850类似内置了6倍频器可以用30MHz晶振产生180MHz的内部时钟但价格更贵。我最终选AD9833的理由很明确第一我的应用场景主要是音频到几百千赫兹的范围偶尔需要到一两兆赫兹AD9833完全覆盖第二SPI接口只需要三根线跟STM32连接非常方便不像AD9850的并口需要一堆IO第三价格便宜淘宝上模块也就十几块钱芯片本身几块钱第四封装小做出来的板子可以很紧凑。如果你需要输出几十兆赫兹的高频信号那AD9833确实不够用得考虑AD9850或者AD9851甚至上FPGA方案。但对于大部分调试和教学场景AD9833的性能够了。2.2 STM32主控的选型与资源分配主控这边我用的是STM32F103C8T6也就是大家常说的“蓝板”或者“最小系统板”上那颗芯片。72MHz的Cortex-M3内核64KB Flash20KB RAM对于这个项目来说资源是够用的。选它的原因也很实际资料多、价格便宜、开发工具链成熟、社区支持好。你要是用STM32F4系列当然性能更好但价格也上去了而且对于DDS控制这个任务来说F1的性能已经过剩了。资源分配上我大致是这样规划的。SPI1用来跟AD9833通信配置成主机模式时钟极性跟AD9833的要求匹配。一个定时器用来做按键扫描和旋转编码器的解码我用的TIM2配置成1ms中断一次。另一个定时器TIM3用来做频率扫描时的步进定时。USART1留给串口调试和上位机通信方便在电脑上直接设置频率。剩下的GPIO分配给OLED显示屏和几个功能按键。这里有个细节值得说一下。STM32F103的SPI时钟最高可以到18MHz但AD9833的SPI接口最高只支持40MHz所以时钟分频设置要注意别超了。我一般用9MHz左右稳定可靠。另外AD9833的SPI模式是CPOL0CPHA0也就是空闲时时钟低电平第一个边沿采样这个在初始化的时候一定要配对否则写进去的数据全是乱的。2.3 模拟输出与滤波电路的设计考量AD9833输出的信号是电流型的需要外接一个运放做I/V转换把电流信号变成电压信号。我用的是一颗低功耗的轨到轨运放比如OPA2340或者TLV2372单电源供电电路简单。反馈电阻选10kΩ左右输出幅度大概在0.6Vpp的样子如果需要更大的幅度后面再加一级同相放大器增益设个2到3倍就能到1Vpp以上。输出端一定要加低通滤波器。为什么因为DDS输出的是阶梯波虽然经过内部DAC之后已经比较平滑了但还是会有高频杂散。截止频率根据你需要的最高输出频率来定一般取最高输出频率的1.5到2倍。比如你最高输出1MHz那滤波器截止频率设在1.5MHz到2MHz之间。我用的是二阶巴特沃斯低通用运放搭的元件少效果也不错。还有一点AD9833的参考时钟质量直接影响输出信号的相位噪声和杂散性能。如果你对信号纯度要求高可以用一颗温补晶振或者恒温晶振给AD9833提供时钟而不是用STM32的MCO输出。我一开始图省事用STM32的MCO输出25MHz给AD9833实测下来杂散比较大后来换了一颗独立的25MHz有源晶振频谱干净了很多。这个经验后面还会细说。3. 核心细节解析与实操要点3.1 AD9833寄存器配置与频率控制字计算AD9833内部有多个寄存器控制起来其实不复杂但有几个关键点必须搞清楚。它有两个频率寄存器FREQ0和FREQ1两个相位寄存器PHASE0和PHASE1还有一个控制寄存器。你可以选择用哪个频率寄存器也可以让两个频率寄存器交替使用来实现FSK调制。频率控制字的计算公式是这样的FREQREG (输出频率 × 2^28) / 参考时钟频率。举个例子假设参考时钟是25MHz你想输出1kHz的正弦波那FREQREG (1000 × 268435456) / 25000000 10737.41824取整就是10737。实际输出频率就是10737 × 25MHz / 2^28 999.9997Hz误差小到可以忽略。写寄存器的时候要注意28位的数据要分两次写先写低14位再写高14位。控制寄存器的配置也要小心B28位要置1表示用28位频率寄存器否则它只认14位。还有RESET位上电后要先置1复位配置完再清0。这些细节如果搞错了芯片要么不输出要么输出频率完全不对。我踩过的一个坑是SPI的片选信号。AD9833的FSYNC引脚就是片选低电平有效。每次写数据之前拉低写完16位数据之后拉高。如果你用STM32的硬件SPI片选可以用软件控制GPIO也可以用硬件NSS。我建议用软件控制因为AD9833的时序要求片选在最后一个时钟下降沿之后才能拉高硬件NSS有时候时序对不上。这个坑我调了大半天才找到原因。3.2 STM32的SPI通信配置与常见问题STM32的SPI配置看起来简单但实际用起来坑不少。我以STM32F103的标准库为例说一下关键配置。SPI1挂在APB2总线上时钟72MHz预分频系数设8得到9MHz的SPI时钟。数据宽度设16位因为AD9833一次接收16位数据。CPOL和CPHA都设0跟AD9833的要求匹配。NSS设软件管理也就是SPI_NSS_Soft。波特率预分频器设SPI_BaudRatePrescaler_8。传输方向设全双工虽然我们只写不读但全双工模式也没问题。发送数据的时候先拉低片选然后调用SPI_I2S_SendData函数把16位数据写进去接着等SPI_I2S_GetFlagStatus检查TXE标志确认数据发送完成最后拉高片选。这里有个细节发送完成之后最好再等一小段时间确保最后一个时钟沿已经结束再拉高片选。我一般加一个几微秒的延时用DWT或者简单的循环延时都行。还有一个常见问题是SPI时钟太快导致数据出错。AD9833的数据手册上写最高支持40MHz的SCLK但实际用的时候如果你走线比较长或者没有做好阻抗匹配9MHz以上就可能出现误码。我建议先用低速调试确认功能正常之后再逐步提高时钟频率。另外SPI的MOSI和SCLK走线尽量短远离模拟输出部分减少数字噪声耦合到模拟信号上。3.3 人机交互旋转编码器与OLED显示人机交互这块我用了旋转编码器加OLED的方案。旋转编码器用来调节频率顺时针增加逆时针减少按下可以切换步进值比如1Hz、10Hz、100Hz、1kHz循环切换。OLED用的是0.96寸的SSD1306I2C接口显示当前频率、波形类型、幅度等信息。旋转编码器的解码我用的是定时器中断加状态机的方式。TIM2配置成1ms中断在中断里读取编码器的A相和B相电平跟上一次的状态做比较判断旋转方向和步数。这种方法比外部中断更稳定不会因为抖动产生误判。状态机的状态转换表我放在代码里了四个状态对应A、B两相的四种组合每次变化根据转换方向判断是正转还是反转。OLED显示这块我用的是现成的SSD1306驱动库移植过来改一下I2C的底层读写函数就能用。显示刷新率不用太高10Hz就够了太频繁反而影响主循环的响应速度。显示内容我做了个简单的界面布局上面一行显示频率值中间显示波形类型下面显示幅度和步进值。字体用的是6x8和8x16两种频率值用大字体其他信息用小字体。这里有个经验旋转编码器的机械抖动比想象中严重尤其是便宜的那种。我在中断里加了软件消抖连续两次读到相同的状态才认为是有效变化。另外编码器的步进值切换逻辑要设计好不然用户转半天频率没变化体验很差。我的做法是按下编码器切换步进值同时在OLED上显示当前步进值让用户心里有数。4. 完整实操流程与核心环节实现4.1 硬件连接与电路搭建先把硬件连接理清楚。AD9833模块一般有五个引脚VCC、GND、SCLK、SDATA、FSYNC。VCC接3.3V或者5V都可以AD9833支持宽电压。SCLK接STM32的PA5SDATA接PA7FSYNC接PA4。这三个是SPI1的引脚对应关系是SCK、MOSI和自定义的片选。AD9833的输出引脚OUT接运放的输入端经过I/V转换和滤波之后输出到BNC或者排针。OLED的I2C接口接STM32的PB6和PB7分别是SCL和SDA。旋转编码器有三个引脚A相接PB0B相接PB1公共端接GND按下开关接PB10。按键这边我另外加了三个独立按键分别接PB11、PB12、PB13用来切换波形、调节幅度、进入菜单。电源部分整个系统用USB 5V供电STM32板子上有3.3V稳压器AD9833和OLED都从3.3V取电。如果你要自己做PCB注意模拟部分和数字部分要分开布局地平面要完整。AD9833的模拟输出走线尽量短远离SPI时钟线。滤波器的元件精度用1%的电阻和5%的电容就够了不用追求太高。晶振尽量靠近AD9833的时钟输入引脚走线包地处理。4.2 软件框架与主循环设计软件这边我用的是前后台架构没有上RTOS。主循环里轮询处理按键事件、更新显示、检查串口命令。定时器中断负责编码器解码和频率扫描的步进定时。SPI通信是阻塞式的因为写AD9833的寄存器很快几十个微秒就完成了不会影响系统响应。主循环的流程是这样的先初始化系统时钟、GPIO、SPI、I2C、定时器、串口。然后初始化AD9833写入默认频率和波形。接着进入while(1)循环在循环里调用按键处理函数、编码器处理函数、显示刷新函数、串口命令解析函数。每个函数都设计成非阻塞的执行时间很短保证循环周期在10ms以内。频率更新的逻辑是这样的当编码器检测到旋转事件时根据当前步进值调整目标频率然后计算新的频率控制字通过SPI写给AD9833。同时更新OLED显示。整个过程在几毫秒内完成用户感觉是实时的。波形切换和幅度调节也是类似的流程只是写的寄存器不同。串口命令这块我定义了一套简单的协议比如“FREQ 1000”设置频率为1000Hz“WAVE SIN”设置正弦波“WAVE SQU”设置方波。这样你可以用电脑的串口助手直接控制信号发生器也方便后续做上位机软件。串口接收我用的是中断加环形缓冲区的方式不会丢数据。4.3 频率扫描与调制功能的实现频率扫描是我后来加的一个功能实用性很强。比如你要测试一个滤波器的幅频特性就需要从低频到高频扫一遍记录每个频点的输出幅度。手动一个一个频率去设置太麻烦了用扫描功能就方便很多。实现思路是这样的设定起始频率、终止频率、扫描步进和每步的停留时间。用一个定时器中断来触发频率更新每次中断把当前频率加上步进值写到AD9833直到达到终止频率。停留时间决定了扫描速度一般设10ms到100ms。扫描过程中OLED实时显示当前频率方便观察。调制功能我实现了简单的FSK和ASK。FSK就是频移键控用两个频率寄存器交替切换切换速率由定时器控制。ASK是幅移键控通过控制输出运放的增益或者用数字电位器调节幅度。这两个功能在通信实验里很有用比如模拟串口通信或者红外遥控的信号。代码里我把这些功能都封装成了独立的函数通过菜单或者串口命令来调用。这样主循环不会因为某个功能阻塞系统始终保持响应。如果你要加更多的调制方式比如PSK或者QAM可以在现有框架上扩展原理都是类似的。5. 常见问题与排查技巧实录5.1 输出波形异常问题排查调试过程中最常见的问题就是输出波形不对。我把遇到过的几种情况和排查方法整理了一下。第一种情况完全没有输出。先检查AD9833的供电是否正常用万用表量VCC引脚应该是3.3V或者5V。然后检查SPI通信是否正常用示波器看SCLK、SDATA、FSYNC三个引脚写数据的时候应该有波形。如果SPI没波形检查STM32的SPI初始化代码确认引脚配置和时钟使能都对了。如果SPI有波形但AD9833没输出检查控制寄存器的RESET位是否清0了B28位是否置1了。第二种情况输出频率不对。先确认参考时钟频率是不是你以为的那个值。如果你用STM32的MCO输出实际频率可能跟设定值有偏差。用频率计或者示波器量一下AD9833的时钟输入引脚。然后检查频率控制字的计算是否正确特别是2的28次方这个系数别搞错了。还有写频率寄存器的时候高低14位的顺序别弄反了。第三种情况波形失真或者杂散大。先检查输出滤波器的截止频率是否合适太低会把有用信号也滤掉太高则滤不干净高频杂散。然后检查参考时钟的质量用STM32的MCO输出通常杂散比较大换独立晶振会好很多。还有AD9833的电源引脚要加去耦电容100nF和10uF并联尽量靠近芯片放置。第四种情况方波输出不正常。AD9833的方波是从DAC的MSB直接输出的幅度是固定的不受频率控制字影响。如果你发现方波频率不对检查是不是误用了正弦波的频率寄存器。方波的频率跟正弦波是同一个频率寄存器控制的切换波形的时候不需要重新写频率。5.2 SPI通信失败与数据错乱SPI通信失败是另一个高频问题。我遇到过几次每次原因都不太一样。有一次是片选信号的问题。我用的是软件控制GPIO做片选但初始化的时候忘了把GPIO配置成推挽输出默认是浮空输入导致片选信号一直是高阻态AD9833根本收不到数据。后来把GPIO模式改成GPIO_Mode_Out_PP就好了。还有一次是SPI时钟极性设错了。AD9833要求CPOL0CPHA0我一开始设成了CPOL1结果数据完全错乱。用逻辑分析仪抓了一下波形发现时钟空闲时是高电平跟AD9833的要求反了。改过来之后立马正常。另外一次是SPI速度太快。我设了18MHz结果数据偶尔出错输出频率跳来跳去。降到9MHz之后就稳定了。所以如果你遇到偶发性的数据错误先降速试试。还有一个隐蔽的问题STM32的SPI在发送16位数据的时候如果用了DMA或者中断要注意数据对齐。我一开始用8位数据模式发两次结果高低字节顺序搞反了AD9833收到的数据完全不对。后来改成16位数据模式一次发完问题解决。5.3 常见问题速查表问题现象可能原因排查方法解决方案无输出供电异常万用表量VCC检查电源连接无输出SPI无波形示波器看SCLK检查SPI初始化无输出RESET位未清读控制寄存器清RESET位频率不对参考时钟偏差频率计量时钟脚换晶振或校准频率不对控制字算错重新计算核对公式波形失真滤波器截止频率不当扫频观察调整滤波器杂散大参考时钟质量差频谱分析换独立晶振方波异常频率寄存器用错检查代码统一频率寄存器SPI数据错乱时钟极性错误逻辑分析仪改CPOL/CPHASPI数据错乱速度太快降速测试降低预分频编码器不灵抖动未消示波器看波形加软件消抖OLED不亮I2C地址错扫描I2C地址改地址配置5.4 实操心得与避坑建议第一个心得AD9833的参考时钟非常关键。我一开始用STM32的MCO输出25MHz给AD9833想着省一颗晶振。实测下来输出信号的相位噪声明显偏大频谱上能看到很多杂散。后来换了一颗独立的25MHz有源晶振杂散改善了大概10dB以上。如果你对信号纯度有要求这一步千万别省。第二个心得输出滤波器的设计要留余量。我第一版滤波器截止频率设在了最高输出频率的1.2倍结果在最高频率处幅度衰减了将近3dB。后来改成2倍幅度平坦度好多了。滤波器的阶数也不用太高二阶巴特沃斯就够了阶数太高会引入额外的相移和群延迟。第三个心得编码器的步进值切换逻辑要符合直觉。我一开始设计的步进值是1、10、100、1000循环但用户从1000切回1的时候要按三次体验不好。后来改成按下编码器弹出菜单用旋转选择步进值再按下确认操作更直观。虽然代码复杂了一点但用户体验提升明显。第四个心得串口命令的解析要健壮。我一开始用简单的字符串比较结果用户输入“freq 1000”和“FREQ 1000”结果不一样。后来改成不区分大小写并且支持前后空格兼容性好多了。另外命令的回复要清晰比如设置成功返回“OK”参数错误返回“ERROR”方便上位机判断。第五个心得代码开源的时候要把注释写清楚。我见过很多开源项目代码能跑但注释几乎没有别人想改都不知道从哪下手。我在关键函数和寄存器配置的地方都加了详细注释特别是频率控制字的计算、SPI时序的注意事项、滤波器的参数选择依据。这样别人拿到代码能快速理解也方便二次开发。6. 源码结构与二次开发建议6.1 工程目录与关键文件说明源码我按功能模块分了几个文件夹。Core文件夹里是STM32的主函数和中断处理Drivers文件夹里是标准库和硬件驱动Hardware文件夹里是AD9833、OLED、编码器的驱动App文件夹里是应用层的逻辑包括频率计算、菜单管理、串口协议。每个文件夹里都有对应的头文件和源文件结构清晰方便查找。关键文件有几个。ad9833.c和ad9833.h是AD9833的驱动里面封装了初始化、写频率、写相位、切换波形等函数。oled.c和oled.h是SSD1306的驱动包含了I2C底层读写和显示刷新函数。encoder.c和encoder.h是旋转编码器的解码逻辑。main.c里是主循环和初始化代码。app_freq.c里是频率控制字的计算和更新逻辑。编译环境我用的是Keil MDK 5也提供了Makefile和PlatformIO的配置文件方便用VSCode或者CLion开发。如果你用STM32CubeMX生成工程把Hardware和App文件夹里的文件添加进去就行注意SPI和I2C的初始化配置要跟我的代码匹配。6.2 二次开发方向与扩展思路这个项目的扩展空间很大我列几个方向供参考。第一个方向是增加调制功能。现在只有基础的FSK和ASK你可以加PSK、QAM、扫频、猝发等模式。AD9833支持相位寄存器做PSK很方便。扫频功能可以用定时器触发频率更新来实现。猝发模式需要控制输出的使能可以用一个GPIO控制输出运放的使能引脚。第二个方向是提高输出频率范围。AD9833最高到12.5MHz如果你需要更高频率可以换AD9850或者AD9851软件框架基本不用大改主要是SPI或者并口的驱动要重写。再往上就得考虑FPGA加高速DAC的方案了那是另一个量级的项目。第三个方向是增加幅度控制。现在幅度是固定的你可以加一个数字电位器或者可编程增益放大器用STM32的DAC或者PWM控制增益实现幅度的数字调节。这样就能做ASK调制和幅度扫描了。第四个方向是加上位机软件。串口协议已经定义好了你可以用Python或者C#写一个上位机通过串口设置频率、波形、扫描参数还能画频谱图。Python的话用PyQt或者Tkinter做界面pyserial做串口通信matplotlib做绘图很快就能搭起来。第五个方向是换用FreeRTOS。现在的代码是前后台架构功能多了之后主循环可能会有点吃力。你可以把按键处理、显示刷新、串口通信、频率更新分别放到不同的任务里用信号量和队列做同步。STM32F103的RAM跑FreeRTOS是够的注意任务栈大小别设太小。6.3 开源协议与使用说明源码我采用的是MIT协议你可以自由使用、修改、分发包括商业用途只需要保留版权声明就行。我在代码头部加了版权信息和协议说明你二次开发的时候可以改成自己的名字但建议保留原作者的贡献说明这是开源社区的基本礼仪。使用的时候有几个注意事项。第一AD9833的参考时钟频率在代码里是一个宏定义你如果换了晶振记得改这个值否则频率全错。第二SPI的引脚配置在spi.c里如果你换了STM32型号或者引脚要对应修改。第三OLED的I2C地址默认是0x78如果你的模块地址不一样在oled.h里改一下。第四串口的波特率默认是115200你可以在usart.c里修改。如果你在使用的过程中发现了bug或者有改进建议欢迎提issue或者pull request。我平时也会关注这个项目看到有价值的问题会及时回复。开源项目的生命力在于社区的参与大家一起用、一起改项目才能越来越好。最后再分享一个小技巧调试DDS输出的时候如果没有频谱仪可以用一个简单的LC谐振电路加示波器来粗略判断输出质量。把输出信号通过一个已知频率的LC谐振回路观察幅度变化就能大致判断信号的频率和纯度。这个方法虽然不精确但在没有专业仪器的时候很实用。我自己在没有频谱仪的那段时间就是靠这个方法调滤波器的。
返回列表