STM32驱动AD9833实现DDS波形发生器:原理、代码与调试 简介这是一份基于STM32控制AD9833输出正弦波、方波和三角波的C语言工程适合嵌入式初学者和需要快速实现信号发生功能的开发者。包内共有13个文件由7个.h头文件和6个.c源文件组成包含AD9833驱动、SPI通信、OLED显示、PWM控制与延时处理等模块代码结构清晰可直接阅读并移植到现有项目中。压缩包仅10KB体量轻巧便于下载后快速分析。已有3867人学习反映了其在嵌入式波形生成方向上的实用性。通过该工程可掌握AD9833频率与相位寄存器的配置思路理解STM32的GPIO复用、SPI时序以及驱动层的分层方法并借鉴其中OLED和PWM模块的写法为后续开发信号源、测试测量或物联网感知设备提供可复用的软件基础。 最近在折腾一个小型信号源手头正好有STM32F103和一颗ADI的AD9833模块就把“用STM32控制AD9833输出波形”这件事从硬件到代码完整走了一遍。如果你也想做函数信号发生器、扫频信号源或者给LCR测量、音频设备提供一个稳定可控的激励信号这个组合性价比很高STM32负责控制和交互AD9833负责把频率字直接合成正弦波、三角波和方波频率最高能到参考时钟的一半用25MHz晶振时输出12.5MHz以内的信号没什么压力。这篇文章我会把选型逻辑、寄存器计算、三线SPI时序和实际调试中的坑一次说完照着做基本能点亮波形。1. 备选方案那么多为什么是STM32加AD98331.1 DDS芯片和直接DAC方案的差别很多人看到“输出波形”第一反应是用STM32内部DAC加定时器DMA往DAC寄存器里灌正弦表数据。这个方案不是不能用但有两个绕不开的问题一是频率上限低DAC的更新速率受定时器触发频率和DMA带宽限制STM32F103跑到几百kHz正弦波就很吃力二是频率切换麻烦改频率等于改正弦表的步进或重算表代码开销大。AD9833这类DDS芯片把相位累加和查表逻辑做在硬件里你只需要通过SPI写一个28位的频率控制字输出频率就变了整个过程只涉及几次寄存器写入毫秒级响应而且频率分辨率可以做到0.1Hz级别这是普通DAC方案很难实现的。1.2 我这次的需求拆解和选型取舍做这个项目前我先列了一下需求可输出1Hz到1MHz正弦波频率步进尽量细波形能切换最好能预留扫频功能。基于这几点AD9833的28位频率寄存器、三路波形输出和独立的相位寄存器都能满足。STM32在这里的角色是“大脑”负责解析用户设定的频率值、换算成频率控制字、通过三线SPI喂给AD9833再用按键或串口做交互。至于为什么不用AD9834或AD9850主要考虑AD9833模块便宜、资料多、三线SPI省引脚而且3.3V供电下功耗低适合嵌入式场景。2. 硬件连接和电路细节2.1 STM32与AD9833三线SPI的接线AD9833的通信接口是典型的SPI从机但只有三根线SDATA数据、SCLK时钟、FSYNC帧同步低有效。控制寄存器、频率寄存器、相位寄存器都是通过16位一帧的数据写入。我的接线是这样AD9833引脚STM32引脚说明FSYNCPA4帧同步信号低电平有效SCLKPA5SPI时钟SDATAPA7SPI数据输入VDD3.3V模块供电注意稳压GNDGND共地这里我建议用GPIO模拟三线SPI而不是直接复用STM32的硬件SPI外设。原因后面代码环节详细说但先给结论AD9833只写不读时序又很简单GPIO模拟比硬件SPI更容易控制帧边界排错也直观。2.2 电源、参考时钟和输出端的处理AD9833的模拟和数字部分共用VDD供电范围2.3V到5.5V。我用的模块板载了25MHz有源晶振如果你手头是裸片或没带晶振的板子需要自己给MCLK引脚接25MHz有源晶振否则芯片不工作。电源去耦别省VDD引脚旁边放0.1uF陶瓷电容和10uF钽电容走线尽量短晶振附近不要走数字信号线。输出端是很多人忽略的重点。AD9833的IOUT引脚是DAC输出它不是推挽电压输出必须外接负载电阻到AGND才能形成电压信号。一般接200Ω电阻可以得到大约0.6Vpp的输出如果你希望幅度更大或带载能力更强后面要加一级运放做缓冲和放大。很多成品模块已经集成了运放电路但如果你自己画板这个负载电阻一定不能省否则示波器探头上看不到波形。3. AD9833寄存器体系和频率字计算3.1 三组寄存器的读写规则AD9833内部需要理解的是三组16位寄存器控制寄存器、频率寄存器、相位寄存器。每一帧16位数据的高两位决定这帧数据写给谁00是控制寄存器01是频率寄存器10和11是相位寄存器对应PHASE0和PHASE1。写入控制寄存器时高两位是00写入频率寄存器时基准是0x4000bit13选择FREQ0还是FREQ1写入相位寄存器时基准是0x8000bit12选择PHASE0还是PHASE1。控制寄存器里常用的几个位是B28bit15决定频率字是否需要分两次写入1表示分两次写完整的28位RESETbit11是软件复位置1后内部清零OPBITENbit8和MODEbit5配合决定输出正弦还是三角波。需要特别说明的是B28置1的情况下写频率字必须连续写两帧第一帧是低14位第二帧是高14位中间不可以在寄存器之间切换否则会出现频率字高低位错乱。3.2 频率控制字计算公式和实例AD9833的28位频率字和输出频率的关系是[ f_{out} \frac{FreqReg \times MCLK}{2^{28}} ]反过来根据目标频率计算频率字就是[ FreqReg \frac{f_{out} \times 2^{28}}{MCLK} ]我用25MHz参考时钟举个例子。如果目标输出1kHz正弦波那么FreqReg 1000 * 268435456 / 25000000结果是10737.418取整10737。实际输出频率为10737 * 25000000 / 268435456约999.8Hz误差大约0.02%对绝大多数应用都够了。如果目标输出1MHz算出来是10737418.24取整10737418实际频率基本是1.000000MHz。从这两个例子能看出来频率越低量化误差的相对影响越大但AD9833的频率分辨率25MHz/2^28约0.093Hz所以1Hz以上的信号精度还是可以接受。计算频率字时要注意C语言的整型溢出问题。如果直接写freq * 268435456UL当freq超过几十kHz时32位乘法的结果可能溢出。我在代码里用uint64_t做中间运算再截断安全很多。4. STM32代码实现从初始化到波形切换4.1 为什么我坚持用GPIO模拟三线SPI先说结论AD9833没有MISO回读功能只有写操作而且对帧边界要求严格。硬件SPI虽然也能驱动但要注意两点一是STM32的硬件SPI在发送16位数据时NSS引脚的拉低拉高时机不一定能精确控制在“正好16个时钟周期”的边界二是AD9833在SCLK下降沿采样数据而STM32硬件SPI默认时序往往是主机在时钟边沿更新数据上升沿/下降沿的采样关系容易搞混。实际测试中我见过很多人用硬件SPI模式0或模式3驱动AD9833出现频率偶发跳变、波形时有时无的问题本质就是时序沿不匹配。GPIO模拟就没有这些烦恼。AD9833要求SCLK空闲为高主机在FSYNC拉低后先把数据放到SDATA然后SCLK拉低产生下降沿AD9833在下降沿锁存数据再拉高SCLK准备下一位。整个过程完全由软件控制只要循环里顺序对帧边界一定干净。4.2 完整驱动代码和初始化序列我用的是标准库HAL库的逻辑完全一样只是GPIO读写函数换成HAL_GPIO_WritePin。先看模拟SPI写一个16位寄存器帧的函数#define AD9833_FSYNC_H() GPIO_SetBits(GPIOA, GPIO_Pin_4) #define AD9833_FSYNC_L() GPIO_ResetBits(GPIOA, GPIO_Pin_4) #define AD9833_SCLK_H() GPIO_SetBits(GPIOA, GPIO_Pin_5) #define AD9833_SCLK_L() GPIO_ResetBits(GPIOA, GPIO_Pin_5) #define AD9833_SDATA_H() GPIO_SetBits(GPIOA, GPIO_Pin_7) #define AD9833_SDATA_L() GPIO_ResetBits(GPIOA, GPIO_Pin_7) void AD9833_Write16(uint16_t data) { uint8_t i; AD9833_FSYNC_L(); for (i 0; i 16; i) { if (data 0x8000) AD9833_SDATA_H(); else AD9833_SDATA_L(); AD9833_SCLK_L(); AD9833_SCLK_H(); data 1; } AD9833_FSYNC_H(); }每个循环里先放数据、再拉低SCLK造下降沿正符合上述时序分析。注意FSYNC在16位传输过程中始终保持低必须等16个SCLK时钟全部跑完才拉高这一点特别重要很多人写成每发一位就拉一次FSYNC芯片会识别成多帧数据完全错乱。初始化序列按位字段写不搞网上流传的硬编码数字可读性和可移植性都好很多#define AD9833_B28 (1U 15) #define AD9833_RESET (1U 11) #define AD9833_MODE_TRI (1U 5) // 1为三角波0为正弦波 void AD9833_Init(void) { AD9833_Write16(AD9833_B28 | AD9833_RESET); // 进入复位 AD9833_Write16(AD9833_B28); // 退出复位 AD9833_Write16(AD9833_B28); // 确认正弦波模式 }这三帧的顺序不建议随意换。第一帧把复位位置1芯片内部清零第二帧把复位清掉第三帧保持配置。初始化完成后芯片默认输出FREQ0和PHASE0对应频率、相位的正弦波但此时频率寄存器是空的必须写频率字才有实际输出。4.3 频率字写入和波形切换的完整函数写频率字要按28位拆分两帧发送基准地址0x4000对应FREQ00x6000对应FREQ1void AD9833_SetFreq(uint8_t freq_sel, uint32_t freq_hz, uint32_t mclk_hz) { uint32_t freq_reg; uint16_t base; freq_reg (uint32_t)(((uint64_t)freq_hz * 268435456UL) / mclk_hz); freq_reg 0x0FFFFFFFUL; if (freq_sel 0) base 0x4000; // FREQ0 else base 0x6000; // FREQ1 AD9833_Write16(base | (freq_reg 0x3FFF)); // 低14位 AD9833_Write16(base | ((freq_reg 14) 0x3FFF)); // 高14位 } void AD9833_SetWave(uint8_t wave) { uint16_t ctrl AD9833_B28; if (wave 1) ctrl | AD9833_MODE_TRI; AD9833_Write16(ctrl); }AD9833_SetFreq里先计算频率字再拆成低14位和高14位两次写入。AD9833_SetWave切换正弦和三角波本质就是改MODE位。方波输出我这次没有用因为AD9833的方波其实是把内部比较器输出引出来需要额外设置OPBITEN、SIGN/PIB、DIV2等多个位不如直接用STM32的GPIO翻转或定时器输出PWM来得直观。5. 实测结果、常见问题和调试经验5.1 示波器实测波形观察写好代码后我用示波器分别抓了1kHz、100kHz和1MHz的正弦波输出。1kHz时波形很干净几乎没有可见台阶100kHz时能看到DAC固有的阶梯状毛刺但幅度稳定1MHz时波形开始有轻微失真这是DAC重建后的高频镜像和寄生电容共同作用的结果属正常现象。如果对波形纯净度有要求AD9833输出端接一个简单的RC低通滤波器截止频率取目标最高频率的3到5倍毛刺能压掉不少。用频率计实测1kHz设定值实际输出999.8Hz和前面算的误差吻合。如果你发现误差明显偏大优先检查MCLK实际频率是不是25.000MHz劣质模块上的晶振温漂或负载电容不对会导致频率偏差到几十上百ppm。5.2 常见问题速查表实际操作中遇到的问题我整理成了一张表现象可能原因解决办法完全没有输出复位位没释放检查初始化序列是否写入退出复位控制字完全没有输出IOUT端没接负载电阻在IOUT与AGND间接200Ω电阻完全没有输出MCLK没起振确认晶振供电和引脚连接输出频率是设定值的一半DIV2位被误置1检查控制字bit6是否为0波形是三角波而不是正弦MODE位错误确认控制字bit5为0频率偶尔跳变或不稳定FSYNC在16位中间被拉高保证一帧16个SCLK持续期间FSYNC保持低切换频率后输出不变FSELECT指向的频率寄存器没写入用示波器或逻辑分析仪确认写入基址是0x4000还是0x6000波形毛刺大电源去耦不足VDD旁加0.1uF和10uF电容输出幅度很小输出端负载电阻偏大换200Ω电阻或加运放缓冲值得单独提醒的是“切换频率后输出不变”这个坑。AD9833默认输出由控制寄存器的FSELECT位决定是FREQ0还是FREQ1如果你初始化时控制字不小心把FSELECT置成1但频率字写进的是FREQ0自然听不到变化。网上很多旧例程用0x2100和0x2000这套数字写控制寄存器如果照抄又没注意FSELECT位很容易踩这个雷。我在代码里直接用位字段宏就是为了避免这种不易察觉的配置错误。5.3 几个实际项目中的调试心得最后补充几条常规文档里不会写的经验。第一调通之后如果你要做扫频或者快速跳频不要每次都重写28位频率字。AD9833硬件上给你准备了FREQ0和FREQ1两个频率寄存器你可以预先把两个目标频率写进去之后只需要写一帧控制字切换FSELECT位就能在两个频率之间跳转时间开销从两帧缩短到一帧而且没有频率字拆装过程的中间态。这个技巧在做扫频信号源时特别实用频率切换速度可以做到微秒级。第二逻辑分析仪比示波器更适合排查SPI时序问题。示波器只能看到SCLK和SDATA的宏观电平逻辑分析仪可以一帧一帧解码出实际的16位数据。我当时定位“频率跳变”问题就是靠逻辑分析仪发现FSYNC在每传8位后意外拉高了一次修正后才彻底解决。第三IOUT的输出幅度和参考电压有关而AD9833内部参考电压是固定1.2V左右所以输出幅度天然受限。如果你需要更大摆幅或可调幅度后级运放几乎是必须的设计时给运放留一个电位器调增益工程上会灵活很多。这个项目做完之后我又顺手把扫频逻辑和串口交互加上了现在可以按键设置起始频率、截止频率和扫频步进配合ADC采集就能搭一个简易的频率响应测试工具。如果你也在做类似的东西建议先照着上面的流程把固定频率输出点亮再去折腾扫频和交互一步步来出问题也好定位。本文还有配套的精品资源点击获取