简介面向嵌入式开发者的AD9834驱动代码包专为需要快速生成高精度正弦波、方波与三角波的测试测量、通信系统和教育实验等场景设计目标芯片为ADI的DDS数字频率合成器。压缩包仅7KB共4个文件包含2个C源文件与2个头文件结构简洁。主驱动部分提供初始化、频率/幅度设置与波形切换函数通信层独立封装SPI/I2C寄存器读写操作便于更换底层总线头文件则集中了寄存器地址枚举、参数结构体与宏定义使得代码可移植性强能够适配嵌入式Linux、RTOS或裸机环境。通过研读源码可以掌握AD9834的频率合成原理、C语言结构体与枚举的实际用法、驱动分层设计思路以及MCU与模拟器件的接口实现方法对后续开发复杂电子系统大有裨益。该资源已有1046人学习浏览适合希望将信号发生器模块集成到项目中的嵌入式开发者参考借鉴。1. AD9834驱动代码怎么把一块DDS芯片用成可控信号源AD9834驱动代码这个标题看着直白真正调起来却有不少人卡在第一步。AD9834是一颗最高75MHz主时钟的DDS芯片能输出正弦、三角、方波频率和相位都通过寄存器配置还支持FSK和PSK调制。我第一次在某测试仪器原型上调它时被“频率值完全错乱”折腾了一个晚上芯片没坏原因是28位频率字要拆成两次14位写入写入顺序和复位位没处理好。这份驱动代码把寄存器操作、SPI时序和初始化流程整理成了可移植的C函数适合正在做信号发生器、频率合成器或扫频源又不想在底层寄存器上反复翻车的工程师。新手能顺着初始化流程调出第一个正弦波熟手可以直接改寄存器组合做调制输出。2. 先搞懂AD9834的寄存器与频率换算28位字、控制位和输出幅度算法2.1 三个寄存器组控制、频率和相位各管什么AD9834内部主要分为控制寄存器16位、频率寄存器0/1各28位和相位寄存器0/1各12位。控制寄存器承载了十几个功能位看起来像一张位表实际使用中真正要反复操作的只有B28、HLB、FSEL、PSEL、RESET这几个。频率寄存器存的是相位累加步长相位寄存器存的是输出波形的初始相位偏移。很多初次接触DDS的人容易把它当成普通SPI外设往总线里丢数据就完事。实际上每个16位字写进去之后由控制寄存器当前的位状态决定谁来接收。比如B28和HLB两位配合决定了28位频率字如何分两次写入。下面这张表列出常见控制位的位位置和核心作用写代码时对照着看不会乱。控制位位编号作用说明B28D13为1时允许28位频率字分两次写为0时按单次模式处理HLBD12写频率字时选择高14位或低14位FSELD11选择频率寄存器0或1PSELD10选择相位寄存器0或1RESETD9内部复位置1时DAC输出被强制清零SLEEP1D8休眠DAC降低功耗时用SLEEP12D7休眠内部时钟关闭MCLK输入OPBITEND6使能SIGNBIT引脚输出SIGNPIBD5选择SIGNBIT输出MCLK/2还是DAC的MSBDIV2D4配合SIGNBIT做二分频MODED1FSK/PSK调制模式选择2.2 频率字和相位字的换算两次14位写入的由来AD9834的频率分辨率是fMCLK除以2的28次方。75MHz主时钟下频率分辨率约0.28Hz对大多数信号发生器场景完全够用。设频率字为freqWord则换算公式为freqWord 目标频率 × 2^28 ÷ fMCLK重点在于AD9834的SPI帧是16位而频率字是28位所以必须拆成低14位和高14位分两次写。例如目标频率100kHz、主时钟75MHzfreqWord约等于357913十六进制是0x57559等下100000×2^28/75000000≈357913拆成低14位和高14位后分两次送进芯片。具体拆分逻辑在驱动代码里用位运算完成但脑子里要有这个模型第一次写低14位第二次写高14位顺序反了输出频率就是乱的。相位字换算更简单12位相位寄存器把360度分成4096份相位字 相位角度 ÷ 360 × 4096。90度对应1024180度对应2048。做PSK时两个相位寄存器各存一个相位字切换PSEL位即可实现相移。2.3 输出幅度取决于FS ADJUST电阻不取决于寄存器AD9834的IOUT引脚是电流输出不是电压输出。输出满量程电流由FS ADJUST引脚外接电阻决定常见做法是在FS ADJUST和AGND之间接一只6.8kΩ到20kΩ的电阻电阻越小满量程电流越大。我一般先用10kΩ电阻调试再根据实际幅度微调。这里有个容易忽略的点DAC输出的正弦波是单端电流直接接示波器看到的幅度很小且带直流分量。正确做法是让IOUT经过运放做电流转电压I-V转换再交流耦合输出。驱动代码里并不涉及这个电路但你在评估输出幅度时必须先知道满量程电流是多少否则会以为是驱动配置错了。3. 写一套可移植的AD9834驱动SPI时序、四个核心函数和代码注释3.1 引脚规划与时序约定SPI Mode 2和FSYNC的严格顺序AD9834的串行接口是标准三线SPISCLK、SDATA、FSYNC。其中FSYNC相当于片选但时序比普通SPI严格。FSYNC必须在SCLK为高电平时拉低然后开始传输16位数据数据传完后FSYNC保持高电平至少一个SCLK周期才能开始下一帧。AD9834在SCLK上升沿采样SDATA且空闲时SCLK为高对应SPI Mode 2CPOL1CPHA0。不同MCU的SPI外设对CPOL/CPHA的命名略有差异有的文库把CPHA翻译成“相位”含义容易搞反。我在项目里习惯用GPIO模拟SCLK而不是依赖硬件SPI外设原因后面第5章会详细说。下面是底层写16位字的代码。这段代码不依赖任何厂商库只要实现三个平台函数即可移植拉低FSYNC、拉高FSYNC、设置SDATA电平。// 平台相关函数由外部实现 static void ad9834_fsync_low(void); static void ad9834_fsync_high(void); static void ad9834_sclk_low(void); static void ad9834_sclk_high(void); static void ad9834_sdio_write(uint8_t bit); // 向AD9834写入一个16位字MSB在前 static void ad9834_write_word(uint16_t word) { ad9834_fsync_low(); // FSYNC必须在SCLK高时拉低 for (int i 15; i 0; i--) { ad9834_sclk_low(); // 下降沿准备切换数据 ad9834_sdio_write((word i) 1); ad9834_sclk_high(); // 上升沿AD9834采样数据 } ad9834_fsync_high(); // 帧结束FSYNC回到高电平 }逻辑说明每发一位数据前先把SCLK拉低保证SDATA在下降沿之后稳定然后在上升沿被AD9834采样。FSYNC拉低之前SCLK必须处于空闲高电平否则会产生一个多余的上升沿把帧边界搞错。参数说明word是16位数据调用时MSB先发送符合AD9834手册要求。如果你的MCU引脚翻转速度很慢可以在两个SCLK动作之间加几个空指令但不建议加太长延时因为AD9834支持最高40MHz的SCLK慢吞吞的时序会影响跳频速度。3.2 寄存器位定义与写控制寄存器函数先把控制位定义成宏代码可读性会好很多。下面这段直接按第2章的位表提取出来作为头文件内容。// AD9834控制寄存器位定义 #define AD9834_CTRL_B28 (1U 13) #define AD9834_CTRL_HLB (1U 12) #define AD9834_CTRL_FSEL (1U 11) #define AD9834_CTRL_PSEL (1U 10) #define AD9834_CTRL_RESET (1U 9) #define AD9834_CTRL_SLEEP1 (1U 8) #define AD9834_CTRL_SLEEP12 (1U 7) #define AD9834_CTRL_OPBITEN (1U 6) #define AD9834_CTRL_SIGNPIB (1U 5) #define AD9834_CTRL_DIV2 (1U 4) #define AD9834_CTRL_MODE (1U 1) // 写控制寄存器 static void ad9834_write_ctrl(uint16_t ctrl) { ad9834_write_word(ctrl); }逻辑说明控制寄存器是16位整体写入每位含义固定。写控制寄存器本身不区分地址而是靠控制位里的B28、HLB、FSEL、PSEL去影响后续写入的归属。参数说明ctrl这个值需要你自己组合宏。比如想设置B28并复位就传AD9834_CTRL_B28 | AD9834_CTRL_RESET。注意控制寄存器里未使用的位保持0不要随意置1。3.3 频率与相位设置函数28位字拆两次写入这是驱动里最核心的函数。设置频率的流程是先写控制寄存器把B28置1、HLB清0同时选好频率寄存器然后写低14位再写控制寄存器把HLB置1最后写高14位。// 设置频率reg0或1mclk_hz为主时钟频率 void ad9834_set_freq(uint8_t reg, uint32_t freq_hz, uint32_t mclk_hz) { // 用64位避免移位溢出 uint32_t word (uint32_t)(((uint64_t)freq_hz 28) / mclk_hz); uint16_t ctrl AD9834_CTRL_B28; if (reg 1) { ctrl | AD9834_CTRL_FSEL; // 选择频率寄存器1 } ad9834_write_ctrl(ctrl); // B281, HLB0, 指向低14位 ad9834_write_word(word 0x3FFF); // 写入低14位 ad9834_write_ctrl(ctrl | AD9834_CTRL_HLB); // HLB1, 指向高14位 ad9834_write_word((word 14) 0x3FFF); // 写入高14位 }逻辑说明((uint64_t)freq_hz 28) / mclk_hz就是把目标频率乘以2^28再除以主时钟换算成28位频率字。先用64位中间量再转回32位是为了防止大频率值左移之后溢出。低频时这个值很小但高频时接近2^28必须用64位保护。参数说明reg传0或1对应频率寄存器0和频率寄存器1。freq_hz不要超过mclk_hz的40%左右留出滤波裕量。mclk_hz是实际接入MCLK引脚的时钟频率不是芯片标称最大值很多人在这一步写错。设置相位函数更简单12位相位字左对齐放进16位帧的高12位即可。// 设置相位reg0或1phase_12bit范围为0~4095 void ad9834_set_phase(uint8_t reg, uint16_t phase_12bit) { uint16_t ctrl AD9834_CTRL_B28; if (reg 1) { ctrl | AD9834_CTRL_PSEL; // 选择相位寄存器1 } ad9834_write_ctrl(ctrl); ad9834_write_word((uint16_t)((phase_12bit 0x0FFF) 4)); }逻辑说明AD9834相位寄存器只有12位有效但SPI帧是16位所以数据放在D15到D4低四位补零。 4就是完成这个左对齐。写入相位寄存器不需要像频率寄存器那样分两次一次写完。参数说明phase_12bit的范围是0到4095对应0到360度。如果你想写90度传1024写180度传2048。注意别把角度值直接传进来要先自己做换算。3.4 初始化流程先复位再配置最后清复位上电后AD9834内部状态不确定正确流程是先写复位控制字再配置频率和相位最后清掉RESET位让DAC开始输出。下面是一个完整初始化函数。// 初始化复位、设置默认频率和相位、解除复位 void ad9834_init(uint32_t mclk_hz) { // 进入复位状态DAC输出保持为0 ad9834_write_ctrl(AD9834_CTRL_B28 | AD9834_CTRL_RESET); // 等待至少几个MCLK周期这里用延时代替 delay_us(10); // 默认输出1kHz正弦波相位0度 ad9834_set_freq(0, 1000, mclk_hz); ad9834_set_phase(0, 0); // 清除RESET位DAC开始正常输出 ad9834_write_ctrl(AD9834_CTRL_B28); }逻辑说明第一步写入RESET置1此时即使后面写了频率字DAC输出也是0这个状态方便做后续配置。配置完成后必须写一次不带RESET位的控制字芯片才会把频率字真正用起来。很多人忘了最后这一步结果IOUT测不出信号误以为是芯片坏了。参数说明delay_us(10)是经验值实际取决于MCLK。如果MCLK只有1MHz10微秒只相当于10个时钟周期建议至少延时20微秒主时钟高时可以缩短。4. 把驱动用起来单频输出、FSK跳频和PSK调相的典型场景4.1 单频正弦频率字计算和DAC电流转电压的连接单频输出是最基本的用法。初始化完成后直接调用ad9834_set_freq即可改变输出频率。下面代码演示从1kHz到10kHz步进扫频。#define MCLK_HZ 75000000UL // 线性扫频示例1kHz到10kHz步进100Hz for (uint32_t f 1000; f 10000; f 100) { ad9834_set_freq(0, f, MCLK_HZ); delay_ms(10); // 每个频点停留10ms }逻辑说明每次调用ad9834_set_freq都会把频率字重新计算并分两次写入。DDS的相位累加器是连续的改频率字不会导致波形相位跳变所以扫频过程很平滑不会出现类似PLL重新锁定时的毛刺。参数说明步进大小和延时时间要根据实际场景调整。做频响测试时步进要小、每个频点停留要长做产品演示时步进可以大一些。MCLK_HZ必须和板子上实际接入的时钟一致否则输出频率整体偏移。这里要补充IOUT的连接IOUT是电流输出不能直接驱动扬声器或ADC。我常用的电路是IOUT和IOUTB差分接入一个高速运放反馈电阻1kΩ到5kΩ运放输出再串一颗电容做交流耦合。如果只是验证驱动逻辑可以在IOUT对AGND之间跨一颗500Ω电阻用示波器DC耦合看波形但幅度和失真都不理想不适合作为最终方案。4.2 FSK跳频用FSELECT引脚做微秒级切换FSK的关键优势是切换速度快。AD9834支持两个频率寄存器通过FSELECT引脚或FSEL控制位实时选择。硬件切换不经过SPI所以延迟只有几个MCLK周期微秒级甚至更快。// FSK配置频率0为10kHz频率1为20kHz ad9834_set_freq(0, 10000, MCLK_HZ); ad9834_set_freq(1, 20000, MCLK_HZ); // 之后不需要再操作SPI直接切换FSELECT引脚电平 fs_pin_write(0); // 输出10kHz fs_pin_write(1); // 输出20kHz逻辑说明两个频率提前写进各自的频率寄存器切换只改FSELECT引脚电平。这个场景下控制寄存器的FSEL位可以不参与优先级由引脚决定。参数说明fs_pin_write是伪代码实际是你的GPIO写函数。需要注意FSELECT引脚不能悬空必须接确定的电平否则芯片会随机选频率。引脚切换时如果有抖动可以在软件里做一个简单的去抖或者用硬件RC滤波但不要加太长延时否则失去FSK意义。4.3 PSK调相相位寄存器0/1的切换PSK在数字通信里很常见AD9834实现PSK的思路和FSK对称两个相位寄存器分别存两个相位字通过PSEL位或PSELECT引脚切换。下面演示0度和180度两个状态。// PSK配置相位0为0度相位1为180度 ad9834_set_phase(0, 0); // 0度 ad9834_set_phase(1, 2048); // 180度4096的一半 // 切换到相位寄存器1180度输出 ad9834_write_ctrl(AD9834_CTRL_B28 | AD9834_CTRL_PSEL); // 切回相位寄存器00度输出 ad9834_write_ctrl(AD9834_CTRL_B28);逻辑说明ad9834_set_phase函数里已经根据reg参数写好了PSEL位这里再写控制寄存器是为了在不改变频率配置的情况下切换相位。注意控制字里的B28必须保持为1否则之后写频率字的方式会发生变化。参数说明如果PSK速率要求很高建议用PSELECT硬件引脚而不是软件写寄存器。引脚切换不需要等SPI时延更小。另外相位切换瞬间输出波形会有相位跳变这是PSK的正常特性不是毛刺。4.4 方波输出SIGNBIT与OPBITEN的组合AD9834除了DAC的正弦输出外还有一个SIGNBIT引脚可以输出方波。方波不是从IOUT得到的而是从内部比较器或MSB直接引出。配置方式如下。// 使能SIGNBIT输出选择DAC的MSB作为方波源 ad9834_write_ctrl(AD9834_CTRL_B28 | AD9834_CTRL_OPBITEN); // 如果想让SIGNBIT输出MCLK/2则加上SIGNPIB位 ad9834_write_ctrl(AD9834_CTRL_B28 | AD9834_CTRL_OPBITEN | AD9834_CTRL_SIGNPIB);逻辑说明OPBITEN1打开SIGNBIT引脚。SIGNPIB0时输出DAC的MSB也就是与正弦波同频率的方波SIGNPIB1时输出MCLK/2这个频率与设置的频率字无关。方波输出常用于给外部数字电路提供时钟或做频率计校验。参数说明第一段代码得到的方波频率等于fout占空比接近50%。第二段代码得到的方波频率是MCLK/2适合验证主时钟是否正常。两种模式不能同时输出两种方波SIGNBIT只有一个引脚。5. AD9834驱动常见问题与排查5个实际踩过的坑5.1 配置了频率但IOUT始终为零现象初始化函数完整跑了一遍频率和相位都写了但运放输出端测不到交流信号直流电平也接近0V。原因最后一步清RESET的控制字写错了。很多人清复位时习惯性带上了AD9834_CTRL_RESET或者直接复用前面配置频率时的控制字导致RESET位一直是1。RESET1时DAC输出被强制清零频率字写了也不生效。解决确认清复位时控制字不能带RESET位。我的习惯是单独写一行ad9834_write_ctrl(AD9834_CTRL_B28);保持B281同时RESET0。用逻辑分析仪抓一下最后一帧控制字能看到D9位是否为0。5.2 FSYNC时序不对频率值完全错乱现象设10kHz输出30kHz设1kHz输出3kHz频率字似乎被移位了。更奇怪的是每次上电后错乱程度还不一样。原因这是我第一次调AD9834翻车最多的地方。FSYNC在帧与帧之间没有恢复高电平或者是在SCLK低电平时拉低FSYNC导致芯片采样到多余边沿。用硬件SPI的NSS自动片选时NSS可能在每个字节之间自动翻转把16位帧拆成两次8位传输帧边界完全错位。解决用GPIO手动控制FSYNC严格遵守“SCLK高时拉低FSYNC传完16位后SCLK高时拉高FSYNC”的顺序。如果坚持用硬件SPI把NSS配置成软件控制不要用自动片选。另外两个16位帧之间插入至少一个SCLK周期的空闲时间。提示调时序问题不要靠猜用示波器同时看SCLK和FSYNC确认FSYNC拉低时SCLK是高电平帧结束时FSYNC也是在高电平状态下拉高的。5.3 SPI模式选错寄存器写入无反应现象代码逻辑看着没问题但输出频率不变化或者寄存器读回全0xFF虽然AD9834没有读接口但从行为看像数据没进去。原因SPI极性配置错误。AD9834上升沿采样SCLK空闲为高对应Mode 2。很多通用库默认是Mode 0CPOL0CPHA0数据在上升沿采样但空闲为低两者时序混在一起设备就罢工。解决使用软件模拟时序最稳妥SDATA在SCLK下降沿后改变上升沿前稳定。使用硬件SPI时把CPOL设为1、CPHA设为0。注意不同MCU的SPI外围库对CPHA的定义可能反过来务必看参考手册而不是想当然。5.4 把IOUT当电压输出万用表测不出波形现象用万用表直流档测IOUT引脚读数只有几十毫伏用示波器看也是一条平坦直线偶尔有尖峰噪声。原因AD9834的DAC输出是电流型IOUT引脚本身不输出电压。电流大小随正弦波变化但峰值也就几个毫安直接接高阻抗示波器探头几乎看不出波形。这让人误以为驱动代码没工作。解决IOUT对AGND跨接一只500Ω到1kΩ电阻做初测或者直接接运放I-V转换电路。示波器探头用1x档或者10x档校准后在运放输出端看波形。如果波形有直流偏置串联隔直电容再看交流分量。5.5 输出频率接近MCLK一半时出现镜像频率现象设主时钟75MHz想输出40MHz频谱仪上在35MHz附近出现一个很大的信号40MHz本身的幅度反而偏低。原因DDS的奈奎斯特限制fout超过MCLK/2即37.5MHz后镜像会折叠进目标频带。芯片本身不能滤除这个镜像需要外部低通滤波器。解决驱动层面把freqWord上限控制在2^27以内对应MCLK/2实际使用建议不超过MCLK的40%。电路层面在IOUT后加低通滤波器截止频率设在目标频率的1.5倍左右镜像自然被压掉。这个问题不是驱动代码能修的但驱动可以提前做频率越界保护比如在ad9834_set_freq里加一句freq_hz mclk_hz / 2的检查。6. 验证驱动的最后一公里示波器、重建滤波和满量程电流标定6.1 检查正弦波质量毛刺、杂散和带宽问题驱动代码调通只是第一步输出信号好不好还得靠仪器说话。我用示波器看波形时习惯用10x探头配短地线弹簧不要用长鳄鱼夹否则会引入额外电感看到的毛刺比实际大得多。先看波形顶部是否平滑如果顶部明显变圆或削平大概率不是DDS的问题而是后端运放摆率不够。运放的增益带宽积至少要选目标频率的5到10倍。接着看频谱。DDS输出天然带MCLK附近的镜像和杂散如果频谱上能看到fout两侧有对称的大信号说明重建滤波没做好。我一般用简单的一阶RC低通先验证确认镜像确实存在后再换椭圆滤波器。测试时设fout为1MHzMCLK为75MHz镜像会出现在74MHz附近示波器带宽不够可能看不到用频谱仪最直接。再给一个低成本验证技巧把SIGNBIT方波接到频率计读数应该和设置的fout一致。如果频率计读数准确说明频率字计算和SPI时序都没问题不确定因素就只剩下DAC模拟通路了。检查项期望结果判定方法FSYNC/SCLK时序FSYNC拉低时SCLK为高示波器双通道观察方波频率与fout一致SIGNBIT接频率计正弦波形无明显削顶、无抖动示波器观察运放输出杂散水平镜像低于主信号40dB以上频谱仪FFT6.2 满量程电流标定调FS ADJUST电阻FS ADJUST电阻决定输出幅度但它不是线性的直觉调节。我调试时会用一个10kΩ电位器串联5.6kΩ固定电阻临时替代FS ADJUST电阻先测运放输出幅度调到目标值后再用固定电阻替换电位器。注意测量时要在目标频率下测因为运放增益随频率变化低频测出来的幅度不能直接推到高频。有一点容易被忽略改变FS ADJUST电阻会影响满量程电流但不会改变频率精度。所以如果输出频率不对不要调这个电阻先回头检查频率字。从那以后我每次拿到新的DDS板子都强制走一遍这个流程先抓FSYNC和SCLK时序再测SIGNBIT方波最后看运放输出的频谱。不做这三步我不会去动寄存器参数。这套AD9834驱动代码包含寄存器位定义、可移植C驱动和初始化工程模板适合直接拉到项目里改改就能跑。希望帮到你。本文还有配套的精品资源点击获取