
做单片机项目时一旦需要“一个频率可调、波形干净、相位可控的正弦波信号”我第一个想到的往往就是AD9833。这颗ADI出品的低功耗DDS芯片在信号源、频率扫描、谐振测试、波形激励这些场景里出现频率非常高核心卖点就是一颗芯片直接输出正弦波、三角波和方波频率由寄存器数字控制频率分辨率能到0.1Hz级别非常适合做程控信号源。不过很多第一次接触这颗芯片的人拿到手里的波形往往不是想象中那么干净——示波器一看正弦波边缘带毛刺FFT一开基频旁边站着好几根谐波谱线。这到底算不算正常谐波从哪里来能不能压下去就成了从“能出波形”走向“波形能看、能测、能用”的关键一步。这篇文章我就从AD9833的寄存器配置、输出频谱特性、谐波来源分析到低通滤波实操把这颗芯片从“点亮”到“出干净波形”的全过程拆开讲清楚。1. 项目到底在解决什么问题为什么AD9833值得折腾1.1 AD9833是什么能做什么DDS芯片的正确打开方式AD9833是一颗内置DDS直接数字频率合成核心的可编程波形发生器芯片主时钟最高支持25MHz输出频率范围为0MHz到12.5MHz频率字长28位相位字长12位接口是标准SPI供电只需2.3V到5.5V。它比用单片机定时器翻转IO生成的方波高级在一点输出频率是由相位累加器连续累加得到的没有“中断抖动”这个概念理论上频率和相位都是连续可变的不存在用定时器做PWM信号时那种时钟周期量化误差。这颗芯片能生成三类基础波形正弦波、三角波、方波。正弦波和三角波由内部的10位DAC输出方波则由比较器输出——注意这里的“方波”不是从DAC出来的它是直接从MSB引脚输出的数字信号所以幅度不随频率变化也不能调幅值。对比下来绝大多数与谐波相关的项目用的都是它的正弦波输出因为正弦波的频谱纯净度直接决定了整个系统的信噪比和失真指标。我为什么强调“适合谁学、能解决什么问题”——如果你是做传感器激励、阻抗谱测量、锁相放大器的前置参考源、声学扫频、滤波器扫频测试甚至只是想给手头的DSP板卡提供一个频率精确的参考信号AD9833都是一颗便宜够用的器件。它解决的痛点很具体传统模拟文氏桥振荡器调频麻烦、温漂明显用MCU定时器DAC查表生成波形则需要吃满CPU时间频率做不高波形点数和谐波表现也受限。AD9833用硬件完成相位累加和波形查表CPU只需要在改频率时写一次SPI数据之后完全不用管它。1.2 谐波在这个项目里指什么从波形失真看问题本质谐波这个词在信号源项目里一般指两种东西。第一种是输出信号频谱中除了基波之外的那些整数倍频率分量比如1kHz正弦波旁边的2kHz、3kHz、4kHz分量这在有失真的系统中几乎必然存在。第二种是广义上的“不想要的频率成分”包括DDS的时钟泄漏、镜像频率、DAC量化噪声等它们不一定严格落在基波的整数倍上但都会被一并归入“波形不干净”的范畴。AD9833的正弦波输出谐波主要来源有几个方面。首先是10位DAC的量化噪声和微分非线性这在幅度较低时尤其明显会在频谱上形成一条不高的噪声底。其次是DDS的固有特性也就是相位截断和杂散问题。最后是输出端的时钟耦合。相位累加器虽然字长28位但查表地址线通常只取了高12位甚至更少相位截断会在频谱上带来与输出频率相关的杂散spur。而时钟耦合则表现为频谱上MCLK及其与输出频率的和差频分量。搞清楚这些谐波从哪里来直接决定了后续的应对手段。如果杂散来源于相位截断那是芯片DDS核心的固有行为放大器不会吸收外部低通也压不掉只能通过合理选择输出频段去规避如果谐波来源于DAC的非线性则可以用滤波器在频域上把它们挡住如果噪声来源于电源和地的耦合那这锅得由PCB布局来背。所以做波形谐波分析第一步不是上滤波器而是要先分辨你在频谱上看到的每一根谱线到底属于哪类来源。这也是我在这篇文章里坚持先讲原理再讲操作的原因。2. 硬件准备与基本原理先把底子打好2.1 核心器件与外围电路清单在正式开始配置芯片之前先过一遍硬件上的基本盘。AD9833本身对电路的要求不算苛刻但外围有几处细节如果按照数据手册的参考电路来搭能省掉后续很多麻烦。器件/参数建议值说明主控MCUSTM32、Arduino或其他带SPI的MCU用GPIO模拟SPI也可以速率控制在10MHz内即可AD9833供电3.3V或5V建议3.3V与MCU电平一致避免IO电平不匹配VCC去耦电容10uF100nF并联10uF电解/钽电容负责低频100nF贴片负责高频退耦参考时钟MCLK25MHz有源晶振或MCU的MCLK输出晶振质量直接影响输出频谱纯度优先用低抖动有源晶振输出负载建议接高阻或缓冲器输出端不要直接驱动50Ω重负载最好加一级运放缓冲CAP/2.5V引脚接100nF去耦电容到GND这是内部DAC的基准电压输出必须接电容否则波形会抖这里要特别提醒一句AD9833的数据手册里给了典型应用电路看起来很简单但如果直接把输出端接到示波器的50Ω输入上波形幅度会被拉低一大截而且谐波会明显变大。这是因为芯片内部DAC输出是电流型结构外部的负载阻抗会直接影响输出电压和失真。比较稳妥的做法是在输出端之后一级单位增益运放缓冲再进示波器或后端电路。如果实在不想加运放至少要让示波器用1MΩ输入档并在芯片输出脚到示波器探头之间串联一个几百欧到1kΩ的电阻减小电缆和探头电容带来的高频失配。电源方面还有一个小坑不要用Arduino板载的3.3V LDO直接给AD9833供电同时还在同一根线上带着舵机或者OLED这类负载。AD9833的DAC对电源纹波相当敏感电源上的高频噪声会在输出频谱上表现为MCLK附近的杂散和更多的谐波边带。实测下来用独立LDO或者至少加一个LC滤波器给VCC单独供电频谱干净度会有可感知的提升。测量谐波类指标时电源隔离比换更好的示波器探头还要见效快。2.2 为什么SPI配置方式决定了波形质量AD9833的SPI接口是16位一帧的写操作每次传输16bit数据。控制字、频率字、相位字都通过这16位写入芯片内部根据控制位的组合决定把数据放到哪个寄存器。SPI的时序算不上复杂但它有一个比较容易被忽略的特点芯片在SCLK上升沿采样数据MSB先行最高位DB15决定当前这帧是控制寄存器写入DB150还是数据寄存器写入DB151。也就是说AD9833并没有专门的寄存器地址引脚区分控制字和数据字完全靠每一帧的最高位。这个设计带来的结果是如果你在代码里拼接16位数据时搞错了位顺序芯片不会报错它只会“正常”地把错误数据写到错误位置表现就是明明写了频率寄存器输出频率却不变或者写了控制寄存器结果波形变成了三角波。这类问题在逻辑上很难查因为SPI通信本身看起来是通的读写函数也没报错。我见过好几个项目卡在这里最后是拿逻辑分析仪抓了SPI波形逐bit对照才查出来。所以用AD9833的代码建议第一件事就是把“拼接16位字”的函数单独封装做一个软件位定义表严格对照数据手册把每一位的作用注释清楚能省下大量调试时间。控制寄存器中与波形形状直接相关的几个位必须搞清楚MODE位决定输出正弦波还是三角波OPBITEN和DIV2位组合决定输出方波还是把MSB直接输出SLEEP1/SLEEP12控制DAC和内部时钟的关断。实际项目里如果要用方波很多人会忽略一个细节方波的输出引脚和正弦波不是同一个引脚它是从VOUT引脚旁边的MSB引脚独立输出的不存在“切换模式把VOUT变成方波”这种操作。这一点数据手册写得很清楚但标记不醒目容易看漏。3. 实操过程从寄存器配置到捕获波形的完整流程3.1 SPI初始化与寄存器写入门假设主控是STM32我们直接用硬件SPI速率设置为1MHz保证时序余量充足。初始化时序和普通SPI从机区别不大唯一需要注意的是AD9833对SCLK空闲电平有要求要配置为CPOL0、CPHA0也就是SCLK空闲为低、数据在上升沿采样。// SPI GPIO和硬件配置以STM32 HAL库为例 SPI_HandleTypeDef hspi2; void AD9833_SPI_Init(void) { hspi2.Instance SPI2; hspi2.Init.Mode SPI_MODE_MASTER; hspi2.Init.Direction SPI_DIRECTION_2LINES; hspi2.Init.DataSize SPI_DATASIZE_16BIT; // 16位一帧 hspi2.Init.CLKPolarity SPI_POLARITY_LOW; // CPOL0 hspi2.Init.CLKPhase SPI_PHASE_1EDGE; // CPHA0 hspi2.Init.NSS SPI_NSS_SOFT; hspi2.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_32; hspi2.Init.FirstBit SPI_FIRSTBIT_MSB; HAL_SPI_Init(hspi2); }这里把SPI数据宽度直接设置为16位是省事的关键。很多人会用8位模式然后拼两个字节发送那样也能工作但每次写寄存器都要调换高低字节顺序出错的概率高了不少。直接用16位模式软件代码就只需要关心一个uint16_t值逻辑清晰得多。接下来封装一个通用的写寄存器函数把所有“拉低CS、发送16位、拉高CS”的操作收拢到一起void AD9833_Write(uint16_t data) { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi2, (uint8_t *)data, 1, 10); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); }注意这里把uint16_t类型的指针直接传给HAL_SPI_Transmit在STM32小端模式下低字节在前而AD9833要求高字节在前也就是MSB first。所以如果直接用这个函数发送原始数值会得到高低字节颠倒的数据。解决办法有两个一是把数据做字节交换之后再发送二是在SPI初始化里就配置好发送16位时直接从高位开始传输。实际上HAL库在数据宽度大于8位时会按大端模式先发高字节所以只要确保data变量的值是芯片期望的整个16位数值即可不需要额外交换。不过为了代码可移植性比如换到Arduino或者ESP32上建议在写函数内部做一个明确的字节交换void AD9833_Write(uint16_t data) { uint8_t buf[2]; buf[0] (data 8) 0xFF; // 高字节先发 buf[1] data 0xFF; HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi2, buf, 2, 10); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); }这样写的好处是无论底层SPI是8位模式还是16位模式只要保证调用时先发高字节芯片就能正确解析。AD9833对CS低电平期间的SCLK边沿进行采样所以CS先拉低再传数据传完再拉高这个时序是硬性的不能省。3.2 正弦波输出的频率计算与相位处理示例AD9833的28位频率寄存器计算方式并不复杂核心公式就一个频率寄存器值 (目标频率 * 2^28) / MCLK频率以25MHz主时钟为例如果要输出1kHz正弦波FREQREG (1000 * 268435456) / 25000000 10737.41824取整数部分就是10737也就是0x29F1。这个0.4的尾数舍入会带来小于万分之一Hz的频率误差实际测量中完全感知不到。注意这里的取整方式有些代码直接用了浮点数运算再强转整型这在功能上没问题但要注意浮点精度对32位整数的影响。我之前在STM32上用double计算时发现由于目标频率和MCLK频率都是整数除法结果落在整数边界附近时浮点误差可能让结果差1导致频率偏小约0.09Hz。稳妥做法是使用整数运算或者用float计算后手动判断余数执行四舍五入而不是无脑floor。频率寄存器的写入还要注意一个B28位的问题。AD9833的频率寄存器是28位但SPI一次只能传16位因此写入频率值时需要拆成两次写操作分别写低14位和高14位。B28位就是控制这个拆分行为的开关B281时连续两次写频率寄存器第一次写低14位第二次写高14位芯片内部自动拼好B280时每次写操作都只更新由HLB位指定的一半。实际使用中强烈建议把B28置1这样代码写起来最直接。下面给出一段完整的初始化设置频率设置相位的示例代码#define AD9833_CTRL_RESET (1 8) #define AD9833_CTRL_B28 (1 13) #define AD9833_CTRL_SINE (0 1) // MODE0正弦波 void AD9833_Init(void) { AD9833_Write(AD9833_CTRL_RESET); // 复位把内部寄存器清零 AD9833_Write(AD9833_CTRL_B28 | AD9833_CTRL_SINE); // 开启B28、选择正弦波 } void AD9833_SetFrequency(uint32_t freq_reg) { uint16_t low freq_reg 0x3FFF; uint16_t high (freq_reg 14) 0x3FFF; AD9833_Write(0x4000 | low); // 写频率寄存器0低14位DB151、DB140表示频率寄存器0 AD9833_Write(0x4000 | high); // 写频率寄存器0高14位 } void AD9833_SetPhase(uint16_t phase_reg) { AD9833_Write(0x2000 | (phase_reg 0x0FFF)); // 写相位寄存器012位 } int main(void) { HAL_Init(); AD9833_SPI_Init(); AD9833_Init(); uint32_t freq (uint32_t)((1000.0f * 268435456.0f) / 25000000.0f); AD9833_SetFrequency(freq); AD9833_SetPhase(0); while (1) { // 主循环不需要做任何事AD9833会自动连续输出波形 } }大家留意一个细节前面提到初始化时先写了一个RESET控制字然后紧接着又写了控制寄存器B28正弦模式。这个RESET位在AD9833里作用是把DDS核心的状态复位到初始相位但注意复位之后要让RESET位归零否则DAC输出会被钳位到中电平看不到波形。我这里的写法是第二次写控制字时没有再置RESET位因此RESET被取消芯片正常工作。有些代码库习惯在初始化最后单独再写一次不带RESET的控制字同样可行效果一样。3.3 用示波器捕捉波形并做谐波分析芯片配置完成、输出端有波形之后真正的谐波分析才刚刚开始。把示波器探头接到VOUT引脚如果看到的是一个干净正弦波那只能说明配置成功要判断波形质量必须把显示模式切到FFT或者至少把时基拉开观察稳定后的频谱。示波器做谐波测量有几个关键设置第一采样率不能太低建议至少是输出频率的20倍以上否则会引入混叠把高频分量折叠到低频段让频谱分析失去意义第二打开平均模式一般来说平均16次到64次能把随机噪声压下去剩下的是确定性的谐波分量第三FFT窗口选择Hanning或者FlattopHanning适合看频谱分布Flattop适合读幅度精度具体看你是想“看有几根谱线”还是“读每一根的准确幅度”。一个常见的误区是用示波器自带的FFT功能直接读THD。示波器的FFT动态范围通常只有40到60dB而AD9833在理想配置下的谐波抑制往往只到40dB左右刚好卡在示波器分辨率的边缘。所以示波器FFT适合做快速定性检查真正要精确测量谐波抑制比应该用频谱分析仪或者至少用带更低位数的ADC采集后做FFT。如果项目处在方案验证阶段示波器FFT完全可以判断“这根谱线是电源噪声还是波形失真”——方法是拔掉信号源、只留探头接地看底噪如果频谱上的毛刺还在那问题就不在AD9833而在测量环境。实际测量时我还建议养成一个习惯固定输出频率不变分别记录不加滤波器和加滤波器后的频谱图。这样做有两个好处一是能直观看出每一级处理对谐波的实际抑制量二是可以确认滤波器有没有引入新的谐振峰。我见过有人加了LC低通之后高频谐波确实压下去了但基波附近出现了一个莫名其妙的凸起最后发现是LC滤波器的电感自谐振频率落在目标频段内换了一个高频特性更好的电感就解决了。这属于典型的“滤波反而引入问题”如果不做前后对比光看最终频谱很容易误判为芯片本身的问题。4. 谐波抑制的实战技巧与常见问题排查4.1 低通滤波器设计与阻抗匹配当你用FFT看到了谐波成分接下来就是决定哪些要压、哪些可以留。AD9833输出正弦波时主要的失真成分是二次、三次谐波以及MCLK及其与输出的差值分量。如果输出频率在几十kHz到几百kHz这个区间一个简单的二阶RC低通就能把二次谐波压掉20dB以上如果输出频率到了几MHzRC滤波器的寄生参数会明显影响效果这时应该考虑LC滤波器或者有源滤波器。低通滤波器的截止频率怎么选经验法则是放在目标最高输出频率的1.5到2倍。比如你需要的最高信号频率是100kHz那截止频率选150kHz到200kHz比较合适。选太低会把基波幅度也压掉造成输出频响不平坦选太高则谐波抑制效果不佳。具体到RC二阶低通截止频率计算公式是fc 1 / (2 * π * sqrt(R1 * R2 * C1 * C2))为了方便给出一个可直接使用的组合R1R21kΩC1C21nF时fc≈159kHz二阶滚降在二次谐波处的衰减约12dB。如果觉得不够可以换成三阶无源滤波器或者加入一级Sallen-Key有源低通后者还能顺带提供缓冲和增益把AD9833的高阻抗输出变成低阻抗输出后级负载对波形的影响会小很多。这里要强调一个很多人忽略的问题AD9833的DAC输出内阻并不是零数据手册给出的典型输出阻抗在几十欧到几百欧量级随频率和配置还会变化。这意味着你设计的RC滤波器截止频率是在“信号源内阻滤波器串联电阻”共同作用下实现的。如果你以为只要R11kΩ、C11nF而实际源内阻还有几百欧实测截止频率会比设计值低不少。解决的办法是先用示波器测一下芯片VOUT引脚在空载和带载时的幅度差估算出内阻再把这部分内阻一并计入滤波器设计。这也是为什么我建议在芯片输出端和滤波器之间加一级运放缓冲。有了缓冲器源内阻可以等效为接近零后面的RC滤波设计就能按照理想情况来算省心太多。4.2 接地、电源去耦与布局问题排查对照表谐波类问题排查到后期十有八九会落在“电源和地”这两个字上。AD9833的内部DAC对参考电压的稳定度很敏感而参考电压直接由VCC引脚经过内部基准电路生成所以VCC上的任何纹波都会以AM调制的方式叠加到输出信号上在频谱上表现为基波两侧的边带。故障现象可能原因处理方式输出正弦波顶部或底部削平DAC输出摆幅超过供电轨降低VCC纹波、减小输出幅度增益频谱上出现非整数倍频率的spur相位截断杂散或参考时钟串扰重新选择输出频率避开杂散频点或优化MCLK走线FFT底噪抬高、波形边缘毛刺电源高频噪声、地弹加强VCC去耦使用星形接地输出频率与设定值偏差较大MCLK实际频率与软件配置不一致用频率计校准MCLK换算寄存器值时应使用实际频率方波输出上升沿振铃探头接地线过长、输出走线阻抗不匹配使用接地弹簧缩短探头地线改变频率后输出短暂异常写频率寄存器时序被打断写频率寄存器时不响应中断或加软件互斥表格里第一条“正弦波削顶”经常被误判为芯片坏掉。其实AD9833的DAC输出是以VCC/2为中心、峰峰值接近VCC的如果供电是3.3V满幅输出大约能到3.3Vpp左右。但因为内部存在饱和区实际可用线性范围可能只有2.8Vpp如果后端加了放大电路很容易把波形顶到轨上削平。解决方案不是抱怨芯片而是把输出幅度设计为满幅的80%以内给DAC留出线性余量。布局上的经验我也分享一下MCLK的晶振尽量靠近AD9833的MCLK引脚走线短而直不要从DAC输出线底下穿越。SPI信号线的SCLK频率如果是1MHz其实还谈不上高速信号但SCLK走线与VOUT走线如果平行走了几厘米耦合到输出端的时钟串扰依然会在频谱上形成可观察的毛刺。我实际处理过一起类似问题现象是输出1kHz时频谱上在1.001MHz附近有一根谱线查了好久发现就是SCLK走线跟VOUT走线在PCB上并排了3厘米。把两根线分开、中间加地过孔屏蔽之后这根谱线直接消失了。4.3 从波形谐波延伸出去谐波思维在更多工程场景里的应用谐波这个词在电子工程里是频谱分析的基本语言但同样的思维也能迁移到其他看起来很不一样的方向。比如调试MIPI时钟信号、LVDS差分信号时示波器上看到的波形如果振铃过大本质上是高速方波的高次谐波分量被传输线阻抗不连续反射造成的又比如谐波减速器这种精密机械结构名字里带“谐波”指的是柔轮的周期性弹性变形但它也有类似“谐波误差”的概念体现在运动控制里就是末端定位的周期误差再比如交流电机里常说的谐波损耗本质上是逆变器输出的PWM方波含有大量高频谐波导致电机铁芯的涡流损耗增大。这些场景表面上跟AD9833毫无关联但分析问题的思路高度一致分清基波和多余频率成分找到多余频率成分的产生机制再针对机制去抑制。这也是为什么我建议每个做信号链的工程师都值得认真玩一次AD9833的谐波实验——它把“纯净信号”和“失真来源”之间的关系压缩到了一个非常直观的尺度上花一下午就能建立起属于你自己的频谱直觉。回到AD9833本身这颗芯片在谐波优化上还有不少可以继续深挖的方向比如用两级频率寄存器做FSK跳频、用相位寄存器实现PSK调制、降低MCLK频率以减小时钟串扰、输出加变压器做单端转差分等。如果你本身就有信号源方面的项目需求建议从这篇文章里提到的“先测再滤、先隔离再处理”这个思路开始先把你自己的AD9833输出频谱摸透再谈更复杂的优化手段。频谱分析这件事最怕的就是对着一个不知道来源的谐波瞎优化最后滤波器加了一堆真正的问题反而被掩盖了。