资讯动态

AD9910 DDS例程解析:FTW/POW计算、IO_UPDATE同步与频相联动

发布时间:2026/10/14 19:02:58 来源:尧图企业网站定制
简介这套基于STM32的AD9910例程主要面向需要学习DDS直接数字合成技术的嵌入式开发者旨在演示如何通过寄存器配置实现输出信号频率与相位的灵活修改。压缩包共270个文件约12.57MB包含c与h源码文件、Keil工程文件uvprojx/uvoptx、编译生成的目标文件o/axf/hex/lst以及若干说明文本属于可从工程打开到编译烧录完整使用的项目资料。目前已有259人学习。这一例程重点覆盖系统时钟设定与倍频系数计算例如25MHz晶振配40倍频至1000MHz若换用40MHz晶振则需将倍频设为0x32同时结合STM32的SPI通信完成频率控制字FCW与相位偏移写入帮助理解相位累加器原理、频率分辨率与寄存器操作流程。整套代码和工程文件可直接参考移植适合正在调试AD9910信号发生器或需要快速上手DDS驱动开发的工程师与在校学生。1. 9910例程一颗DDS的频相可控到底能解决什么现场问题做信号源、激励源或者射频前端模拟的时候调频率往往容易调相位经常翻车改了频率相位跟着跳改了相位输出频率又抖一下。9910例程要解决的正是这个痛点——基于9910这颗DDS芯片把频率调谐字和相位偏移字的换算、写入、锁存全封装好你只需要告诉它“我要多少MHz、多少度”剩下的由驱动完成。它适合测试测量设备、超声/声呐激励、雷达回波模拟这些需要频率和相位独立可变的场景也适合刚接触DDS想少走弯路的新手。理解这个例程的关键是弄清FTW、POW和一次IO_UPDATE的关系。2. 频率和相位在9910里如何被“修改”FTW、POW与一次IO_UPDATE的完整链路直接改输出频率和相位是做不到的9910只认“字”。一个32位的频率调谐字决定输出波形的周期长度一个14位的相位偏移字决定每个周期的起始角度。把这两个字算对、写对、同时锁存生效就是把“修改频率和相位”这件事落地。下面先把链路拆开看。2.1 先把两个字看懂频率调谐字FTW与相位偏移字POW怎么算DDS内部是一个相位累加器每个系统时钟沿累加一次FTW累加器溢出即完成一个波形周期。既然累加器是32位那么输出频率就是f_sys乘上FTW与2^32的比值反过来就得到频率字公式FTW round(f_out × 2^32 / f_sys)相位偏移字则直接叠加在累加器输出上14位字对应360°所以POW round(phase_deg × 2^14 / 360)举一组实际例子。假设系统时钟f_sys 1 GHz要输出10 MHz、相位90°则FTW 10e6 × 4294967296 / 1e9 ≈ 42949673POW 90 × 16384 / 360 4096。这两个数最终以字节形式写入对应寄存器。目标输出f_sys1GHzFTWPOW10 MHz / 0°42949673010 MHz / 90°429496734096100 MHz / 180°4294967308192顺带算一下分辨率1 GHz系统时钟下频率分辨率约0.233 Hz相位分辨率约0.022°。如果你要做低到1 Hz级别的精确频点FTW算到整数位仍然有误差必须把误差折算成实际频率误差去评估能不能接受。换算时乘法要先用浮点或64位做避免32位乘法溢出我这里用llround做四舍五入而不是直接截断因为截断带来的频率误差比四舍五入大将近一倍在精密激励场景下会直接反映成输出频率偏差。// 频率字换算乘法让编译器用64位/浮点完成别用32位乘 uint32_t ftw_from_freq(double f_hz, double f_sys) { double ratio f_hz * 4294967296.0 / f_sys; return (uint32_t)llround(ratio); // 四舍五入误差最小 } // 相位字换算14位满量程对应360度 uint16_t pow_from_deg(double deg) { double ratio deg * 16384.0 / 360.0; return (uint16_t)llround(ratio); }逻辑上就是按比例映射到调谐字空间唯一需要留意的是f_sys必须与你实际配置的PLL倍频结果一致很多例程跑出来的频率差一半、差几倍问题都出在f_sys和实际不符。参数方面f_sys单位是Hzf_hz单位也是HzPOW函数接受的deg可以是0到360也可以接受负角度llround会帮你归整到最接近的14位值超过360度时按周期性自动折算回有效范围。2.2 例程里的写入路径缓冲寄存器、IO_UPDATE与多字同步9910内部寄存器分两层缓冲区和活动寄存器。SPI写入的是缓冲区只有IO_UPDATE引脚上来一个上升沿整个缓冲区的内容才会一次性锁存到活动寄存器。这就是为什么改频率和改相位可以并行不冲突——先分别把两个新字写进缓冲区再来一次IO_UPDATEDDS核同一时刻切换不会出现频率先变、相位后变的中间态。很多初学者在这里容易产生一个误解认为写一个寄存器马上就生效。实际不是。如果你在初始化时连续写CFR1、CFR2、CFR3然后写FTW、POW只要中间没有IO_UPDATE所有数据都还堆在缓冲区里最后一次性IO_UPDATE它们才统一生效。因此例程里通常会准备两个层次的接口一个负责“写缓冲区”一个负责“提交”。只写不提交频率相位纹丝不动写了就提交多个参数之间又会互相撕裂。正确的节奏是初始化阶段多次写、一次提交运行阶段改频改相时各写各的、再一次提交。还有一点容易被忽略9910的寄存器长度并不一致。CFR1到CFR4是32位控制状态寄存器是8位FTW是32位POW是16位。如果你不管什么寄存器都按4字节写后面的数据会把相邻寄存器吞掉常见现象就是“频率对了但相位乱”“改了相位输出直接没了”。所以例程里必须按寄存器实际长度组织字节而不是无脑发4字节。这个长度表通常就放在驱动头部写厂商驱动的人一般都会留一份换芯片批次时对照手册核对一次。2.3 单频还是Profile根据“改频改相”需求选对模式9910支持两种使用模式。单频模式最直接FTW和POW直接作用于DDS核心改一次立刻生效适合“用户随时给一个新频率、一个新相位”的交互式场景大多数“支持修改频率和相位”的例程选这个模式。Profile模式则把寄存器扩展成8组每组各存一套FTW、POW和幅度参数通过PS0到PS2三个引脚的电平组合来切换。它的优势是切换速度极快、不依赖SPI实时写入适合跳频通信、多频点预置这类场景但使用前需要先把各profile参数一次性灌好。如果你只是做一个可调信号源用单频模式开发量最小如果后续要做跳频先把数据写进profile寄存器、再用引脚切换比每次临时写FTW要稳定得多。9910例程名里只说“支持修改频率和相位”按惯例走单频模式同时兼容把数据写进profile0两种用法共用同一套FTW/POW换算函数。3. 把例程跑起来最小驱动代码与三处必调参数光有公式还不够得把它接进真实驱动里。下面这套是我常用的最小驱动结构一个SPI写寄存器函数两个上层设置函数再加三个初始化时必须核对参数。不依赖具体单片机型号拿到任何平台改一下SPI收发底层就能跑。3.1 串行封装写一个通用寄存器写入函数9910的SPI写入通常是先发地址字节再发数据字节数据长度按寄存器决定。片选CS在事务期间拉低结束拉高。下面这个函数就是最基础的寄存器写入封装注意它不负责触发IO_UPDATE因为提交动作需要统一控制。static void spi_write_raw(uint8_t addr, const uint8_t *data, uint8_t len) { cs_low(); // 片选拉低开始一次SPI事务 spi_exchange_byte(addr); // 发送寄存器地址 for (uint8_t i 0; i len; i) { spi_exchange_byte(data[i]); // 按寄存器长度发数据 } cs_high(); // 片选拉高结束事务 }逻辑上就是把一次寄存器访问拆成“地址数据”两个阶段数据长度由调用方给定。这里故意不写SPI底层收发细节是因为不同平台的SPI接口函数名和参数差异很大你只需要把spi_exchange_byte换成自己平台里“发送一个字节同时接收一个字节”的接口即可。参数方面的关键点有两个。一个是addr是寄存器地址具体地址值以芯片手册的寄存器映射表为准不同批次数据手册可能有差异不要照抄网络上的老例程地址不做核对。另一个是len必须与寄存器实际宽度一致8位寄存器传132位寄存器传4FTW传4POW传2。传错长度是后续排查时最隐蔽的问题之一。实践中我更推荐再加一层封装void ad9910_write_buffered(uint8_t addr, const uint8_t *data, uint8_t len) { spi_write_raw(addr, data, len); // 只写缓冲区不提交 } void ad9910_commit(void) { gpio_io_update_high(); delay_us(20); // 保证上升沿宽度满足手册要求 gpio_io_update_low(); }这样就把“写缓冲区”和“IO_UPDATE提交”分开了。初始化时连续多次调用ad9910_write_buffered最后调一次ad9910_commit运行过程中修改频率或相位也一样两个参数都写完再commit保证同步。滞后时间不用太长但IO_UPDATE高电平持续宽度要超过手册要求的最小时长我曾经因为用一个10微秒的窄脉冲导致偶发不锁存后来统一改成20微秒以上再没出过问题。3.2 set_frequency 与 set_phase从公式到两个可直接调用函数下面这个函数把前面的公式和写入封装串起来完成“目标频率→FTW→写寄存器→提交”的完整链路。void set_frequency(double freq_hz) { double f_sys get_sysclk_hz(); // 从初始化配置里取别写死 uint32_t ftw ftw_from_freq(freq_hz, f_sys); uint8_t b[4]; b[0] (uint8_t)(ftw 24); b[1] (uint8_t)(ftw 16); b[2] (uint8_t)(ftw 8); b[3] (uint8_t)(ftw); ad9910_write_buffered(REG_FTW, b, 4); // REG_FTW 按手册映射 ad9910_commit(); } void set_phase(double deg) { uint16_t pow pow_from_deg(deg); uint8_t b[2]; b[0] (uint8_t)(pow 8); b[1] (uint8_t)(pow 0xff); ad9910_write_buffered(REG_POW, b, 2); // REG_POW 按手册映射 ad9910_commit(); }这两个函数就是例程的使用入口。逻辑上set_frequency做的事是先拿到当前生效的系统时钟频率换算FTW按大端字节序拆成4个字节写入FTW寄存器最后IO_UPDATE提交。set_phase流程完全一样只是数据宽度变成16位14位有效字放在16位里。参数方面有几个容易踩的地方。get_sysclk_hz千万别在函数里写死成某个常量因为你改PLL倍频后这里会悄悄失效。POW的字节序在不同例程里有差异有的把14位放在16位的高位有的放在低位写完后如果发现相位总差一个固定倍数就调整一下这个字节序再试。最后要提醒set_frequency和set_phase各自单独commit也能工作但如果两个都要改最好先分别写缓冲区、最后统一commit一次否则在两次提交的间隙里DDS会短暂输出“新频率旧相位”或“旧频率新相位”的中间态这在相位敏感的系统里会造成误触发。3.3 三处必调参数系统时钟、PLL倍频与输出滤波器运行例程之前有三个参数必须确认它们不在代码里而在你的硬件配置里。第一是系统时钟f_sys。它通常来自外部参考时钟经过内部PLL倍频后的结果比如25 MHz参考时钟配40倍频得到1 GHz。这个值直接参与FTW换算错了所有频点全偏。第二是PLL配置。9910的倍频配置在CFR相关寄存器里必须确认PLL使能位被设置、倍频系数符合期望。很多人初始化时只写了默认寄存器PLL没使能系统时钟停留在参考时钟频率上输出频率低得离谱。第三是输出端滤波器。DDS输出天然带fs−f_out的高次镜像分量必须用低通滤波器压在预期频带内。滤波器截止频率要按最高输出频率来选不是按采样率。比如最高输出50 MHz就用截止60 MHz左右的低通别用100 MHz截止否则镜像频率会落在通带内。参数建议检查方式出错典型表现系统时钟f_sys对照参考时钟与PLL倍频相乘所有频点按比例偏移PLL使能与倍频读回CFR确认输出频率比设定值低很多输出滤波器看最高频点的镜像幅度高频段波形异常叠加这三个参数里前两个直接决定例程算出来的FTW对不对第三个决定你看到的输出波形干不干净。我调试时习惯先在频谱仪上看一眼最大输出频率附近有没有镜像峰如果有先解决滤波器再回头查FTW顺序不能反。4. 按这个顺序初始化9910例程的上电流程与生效验证例程能改频率和相位的前提是初始化配置正确。很多人拿到代码先改频率设置函数结果输出乱套问题往往出在初始化顺序上。这里把我常用的流程完整写出来并说明每一步为什么必须在那个位置。4.1 初始化顺序与完整C语言框架初始化顺序我固定为五步硬件复位、配置参考时钟与PLL、配置工作模式和DAC校准、统一IO_UPDATE提交、最后写默认频点并提交。void ad9910_init(void) { // 1. 硬件复位拉低复位引脚再释放确保寄存器回到默认态 gpio_reset_low(); delay_ms(10); gpio_reset_high(); delay_ms(10); // 2. 配置PLL与参考时钟倍频具体系数按系统时钟设计值换算 uint8_t pll_cfg[4] { ... }; // 来自手册的PLL倍频配置 ad9910_write_buffered(REG_CFR3, pll_cfg, 4); // 3. 配置工作模式单频模式、使能DAC校准CFR1/CFR2按需写入 uint8_t mode_cfg[4] { ... }; // CFR1/CFR2 数据按手册取 ad9910_write_buffered(REG_CFR1, mode_cfg, 4); // 4. 所有初始化配置统一提交避免中间态 ad9910_commit(); // 5. 设置一个默认输出频点验证链路是否通畅 set_frequency(10e6); set_phase(0.0); }顺序上要注意PLL必须先于工作模式配置因为系统时钟不稳定时写CFR1可能被错误锁存DAC校准使能位放在提交前最后写校准过程依赖时序过早使能可能在校准未完成时被后续写入打断。第5步的默认频点不要省它可以作为后面所有验证的基线。如果上电后直接进入用户交互流程一旦后续写入出错你连“初始化是否成功”都分不清。这套流程里有个细节值得注意第2步和第3步都用了ad9910_write_buffered而不是直接写寄存器就是为了保证所有初始化参数在同一时刻生效。如果你在PLL配置单独commit一次、工作模式又commit一次中间会有一段“PLL已生效但模式未正确配置”的不确定窗口虽然大多数时候不会炸但在要求严格的项目里我不建议赌这个小概率。4.2 验证“修改频率和相位”生效示波器与相位差测量初始化完成后建议按下面步骤验证而不是直接接负载。用示波器两个通道CH1接参考信号CH2接9910输出同步触发CH1。第一步验证频率把输出设为10 MHz用示波器频率测量或FFT看频谱确认主峰在10 MHz附近。注意直方图模式看频率比看周期更直观周期测量在低信噪比时容易判断错误。第二步验证相位保持频率不变把相位从0°依次改成90°、180°、270°观察CH2波形相对CH1的左右位移。90°对应1/4周期180°对应半周期如果位移方向相反说明POW字节序需要调整。第三步验证同步性连续快速调用set_frequency和set_phase观察波形是否有中间频率或中间相位闪现。正常的例程行为是波形平滑跳变到新状态中间态极其短暂肉眼不可见如果能看到明显的“先变频率再变相位”或者出现抖动说明两次commit之间出现了不希望的中间态需要改成统一提交。验证项操作通过标准频率示波器频率测量读数与设定值偏差在测量精度内相位0°/90°/180°/270°逐步改波形位移与理论值一致同步连续改频改相无可见中间态、无跳变毛刺4.3 改完不生效时先查这四处如果验证不通过优先按下面顺序排查。第一查IO_UPDATE引脚确认它确实接到了GPIO且能输出上升沿很多例程里这个引脚被误配置成开漏输出且没有上拉导致上升沿根本到不了芯片。第二查写入长度FTW和POW的字节数是否与寄存器宽度匹配多一个字节或少一个字节都会造成后续寄存器错位。第三查SPI时钟极性和相位9910对CPOL和CPHA有明确要求配反了会导致地址或数据移位表现出来就是写入后寄存器值完全不对。第四查片选时序CS必须在整个事务期间保持低电平不能在地址字节后提前释放否则后面的数据字节全部无效。我遇到过一个很隐蔽的情况改动频率后偶尔生效、偶尔不生效波形看着像随机跳。最后定位到是SPI时钟频率过高数据线上的建立时间不够把时钟从20 MHz降到10 MHz后彻底好了。如果你用的是高速单片机先降速试试往往比对着示波器抓波形更快。5. 9910例程常见问题排查5个让我翻过车的细节很多问题不是例程逻辑有问题而是细节配置和硬件接法上的坑。下面5种现象我都实际遇到过每条按“现象、原因、解决”梳理方便你对照排查。5.1 输出频率正好是设定值的一半PLL倍频没生效现象设定100 MHz实测50 MHz而且所有频点都是设定值的整数分之一频率越低越难发现。原因系统时钟实际只有你预期的一半。FTW公式里的f_sys用的还是1 GHz但硬件上PLL没有工作系统时钟停在500 MHz于是输出频率整体减半。解决先确认PLL配置是否写入成功、使能位是否置1再确认参考时钟幅度是否满足手册要求。有时参考时钟信号太弱内部振荡器起振但倍频器不工作也会出现这种减半现象。建议在初始化后读回CFR寄存器核对配置值再往下走。5.2 改频后输出毛刺乱跳IO_UPDATE与PS引脚的双重陷阱现象连续改频率时输出频率偶尔跳到完全无关的值过一会又自己回来像是芯片“发神经”。原因两个可能。一是IO_UPDATE引脚悬空或受到干扰产生了额外触发二是PS0~PS2引脚悬空噪声电平组合会随机选中一个profile寄存器而那个profile里存的旧数据被切换出来了。解决IO_UPDATE引脚外接100 kΩ下拉电阻确保安静电平为低PS0~PS2引脚在单频模式下全部固定接GND不要悬空。这个坑在开发板上特别常见因为板子默认把这三个脚做成了跳线帽没插跳线帽就等于悬空。5.3 相位改了等于没改POW写入时的位宽与字节序问题现象调用set_phase(90)后输出波形纹丝不动或者相位变化幅度不对比如设90°实际变化了约22.5°。原因POW是14位有效值但在16位寄存器里存在高字节和低字节的放置方式问题。如果你按16位大端写入但芯片实际期望14位放在高位那么实际生效的相位值只有你期望的四分之一左右。解决把POW的14位分别试试“放高14位”和“放低14位”两种对齐方式。判断方法很简单设180°如果波形反相了说明对齐正确如果完全没变或变化量不对就换另一种对齐方式重写。这一步确认后把所有相位相关的写入都统一用同一种对齐方式别再混用。5.4 写入全失败但SPI波形正常SDIO方向位没配现象逻辑分析仪上看SPI波形完全正常地址对、数据对、时序也对但读回寄存器全为默认值写入根本没生效。原因9910的SDIO引脚在配置成三线模式时是双向引脚如果初始化时没有把SDIO方向位配成“输入”也就是四线模式芯片会把SDIO当作输出数据字节被内部驱动拉走写入不到内部寄存器。解决在CFR配置里设置SDIO为四线模式或者在初始化第一步就写入这个方向位。部分例程还要求把SYNC_CLK引脚处理正确否则SPI时钟域不对也会产生类似问题。排查时用示波器测量SDIO线上的电平如果写数据期间SDIO上有明显的芯片驱动信号基本就是这个原因。5.5 频率漂移不锁定REFCLK幅值和PLL锁定检查现象输出频率存在缓慢漂移或者上电后一段时间频率才稳定稳定后又会随机偏移几十赫兹。原因参考时钟REFCLK幅值不足或者波形质量差导致内部PLL偶尔失锁也可能是PLL配置的倍频系数超出了手册规定的VCO范围环路无法稳定锁定。解决先检查REFCLK峰峰值是否满足手册要求必要时加一级缓冲器再送入再确认PLL倍频系数在允许范围内不要凑一个中间值。锁定状态可以从芯片状态位读出初始化后轮询等待锁定再继续别直接开始写FTW。这个坑在低功耗设计里特别容易出现因为为了省电把参考时钟幅度压得太低。6. 把例程升级成“频相联动”同步提交与RAM调制扫频基础例程能跑通后有两个升级方向特别实用。第一个是频相联动也就是一次提交同时切换频率和相位这在雷达模拟和相控阵激励里是刚需。做法已经在前面铺垫过了先连续调用set_frequency和set_phase的缓冲区版本把两个新字都写进缓冲区最后调用一次ad9910_commit。这样做的好处是频率和相位在同一时刻切换不会出现“频率已变但相位还没动”的中间状态。如果你直接用我前面写的set_frequency和set_phase由于它们各自内部都带commit就会产生两次提交这个细节一定要改掉。第二个升级方向是用RAM调制做扫频。9910内部有一段RAM可以预先写入一长串FTW数据然后使能RAM调制模式让芯片自动按地址顺序输出频率形成线性调频或三角扫频波形。这个方法的好处是扫频过程不占用MCU的SPI带宽芯片自己按内部时钟跑扫频速度可以很快。具体做法是先把扫频序列换算成FTW数组写入RAM区配置RAM调制模式和相关控制字最后IO_UPDATE提交。需要注意RAM区域大小有限扫频点数太多时需要分段或者改用主频控制配合profile切换实现粗粒度跳频。RAM调制模式下频率和相位不再是“改一个数”这么简单而是“改一串数”。它的调试思路也从“看单个频点对不对”变成“看整段扫频曲线是否平滑、有无跳变点”。如果扫频曲线在某个位置出现台阶多半是RAM数据边界处FTW衔接不对检查相邻两个FTW的差值是否连续即可。我自己在这个方向上有过一次教训把IO_UPDATE接到一个默认带上拉的GPIO上电时寄存器默认值还没配置好那个高电平就先触发了一次锁存导致初始化后的第一组配置被覆盖成随机值调试了整整三天最后把该引脚改成默认拉低并外部按下拉电阻才解决。从那以后我的原则是IO_UPDATE引脚默认必须是低电平所有触发都主动拉高再拉低绝不依赖引脚复位后的默认状态。这个习惯帮我避开了后面好几个项目的类似问题希望对你有帮助。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑