9910 DDS例程:频率相位修改原理、代码与避坑指南

发布时间:2026/10/13 21:07:41
9910 DDS例程:频率相位修改原理、代码与避坑指南 简介这是一份面向STM32开发者的AD9910 DDS例程包专注于解决高频信号发生器与通信设备中频率、相位精确控制的问题。例程围绕系统时钟配置展开以25MHz晶振配合40倍频实现1000MHz系统时钟为基础同时给出40MHz晶振下倍频系数调整为0x3250倍的对应方案便于开发者根据硬件实际灵活适配。通过频率控制字FCW计算、相位累加器溢出处理与SPI/I2C寄存器配置可直观理解AD9910从时钟链路到信号输出的完整工作链路。压缩包共270个文件约12.57MB其中c/h源码与Keil工程文件uvprojx/uvoptx构成主项目框架o/axf/hex为编译中间与固件产物txt文档辅助说明配置要点同时包含bat辅助脚本结构与用途一目了然。已有259人学习使用适合正在调试AD9910底层驱动或需要掌握DDS频率字计算、相位偏移设置及系统时钟管理的中级嵌入式开发者既能直接移植例程也可参照理解数据手册中关键寄存器的实际含义。1. 9910 例程核心价值从「只能出波」到「频率相位随手改」手里同时压着两台 9910 的板卡A同学调了一周频率能出、相位死活改不动最后发现不是芯片不行是例程里压根没把相位字写到正确的寄存器这种「改频率如喝水、改相位如爬山」的体验几乎所有做过 DDS 控制的人都有。标题里「9920例程 支持修改频率和相位」其实就点了两个痛点一是这套例程能直接搬二是它把频率和相位两个维度都从寄存器层打通了。对刚入门的新手来说拿到的是「照着烧就能出波」的最小工程对已经写过不少控制代码的熟手来说值得关注的则是它处理相位字换算、IO_Update 时序和 Profile 切换的方式。这篇文章就从如何把这套例程落在自己板子上说起把原理、代码路径、寄存器参数和常见的坑一次讲透。2. 频率相位修改的底层逻辑先从 DDS 的寄存器格局看起2.1 为什么很多例程只能改频率FTW 与 POW 的职责划分任何 DDS 芯片的输出参数最终都落在三个量化对象上频率调谐字FTW、相位偏移字POW和幅度控制字。频率修改之所以在例程里好做是因为 FTW 的映射关系最直白输出频率与系统时钟成正比、与 2 的 N 次方成反比写一个无符号整数就能覆盖频点而相位修改要绕一道弯——它不是把「度数」直接塞进寄存器而是需要把角度按 360 度比例映射成相位字这一步换算没做对相位写进去就是乱的。多数「只能改频率」的例程本质不是硬件不支持相位而是软件层只封装了 set_frequency()把 POW 相关的位段完全晾在一边。9910 这类芯片在默认单频模式下输出信号的相位由两个字段共同决定Profile 寄存器里自带的相位字以及实时相位累加器的基础值。如果你只改 FTW 不改 POW波形只会变快变慢初相始终停在同一个点上。所以拿到 9910 例程第一件事不是上电测波形而是翻它的寄存器映射表确认代码里有没有出现 POW 相关的写操作以及这些写操作最终落在哪个 Profile 上。这个动作能在十分钟内分辨「支持修改频率和相位」是真功能还是拿改频率的代码套了个标题。2.2 最小控制流程SPI 串行初始化、IO_Update 与软复位用 SPI 控制 9910 的标准路径一般分四步先配串口模式把芯片切进串行编程状态然后按偏移地址写入 CFR1/CFR2/CFR3 这几张主配置寄存器把内部时钟倍频器、SYNC_CLK 使能、Profile 模式这些位段定下来接着写 Profile 寄存器组把默认的 FTW 和 POW 灌进去最后拉一次 IO_Update 引脚或写 IO_Update 寄存器让片上状态机把影子寄存器真正锁存到工作寄存器。这里有一个新手经常忽略的细节9910 的寄存器多数是缓冲型buffered设计你通过 SPI 写入的数据先进影子寄存器不会立刻生效。真正让参数「落地」的动作是 IO_Update 的上升沿。很多例程里 set_frequency() 写完寄存器就返回了上位机看到返回成功就以为输出变了其实波形纹丝未动。后面第 4 章会详细说这个坑这里先记住结论每次修改频率或相位至少给一次 IO_Update且高电平保持时间要满足手册最小脉宽。软复位则要谨慎用。CFR1 里的软复位位一旦置位所有寄存器恢复出厂值相当于芯片重新上电。如果在跑扫频或者相位连续切换的场合误触发软复位会出现一段时间的输出毛刺。我一般在初始化流程的最后统一软复位一次运行阶段就不再碰这个位。2.3 频率字与相位字的量化公式改动频率和相位前先把量化公式立起来。设系统时钟为 f_sys9910 是 1 GSPS 量级但实际取决于参考时钟和内部倍频器的配置DDS 累加器位宽为 N通常 32 位目标频率为 f_out那么频率调谐字 FTW 的整数取值为FTW round(f_out / f_sys × 2^N)注意两个边界当 f_out 超过奈奎斯特带宽f_sys / 2时输出会混叠出镜像频率例程里最好加上限幅判断当 f_out 是小数且要求毫米级精度时建议用 64 位整数做中间运算再截断避免浮点误差导致频偏。相位字的公式类似。目标相位偏移为 φ单位度相位累加器同样按 2^N 份来映射整周所以POW round(φ / 360 × 2^N)这里 N 一般取 16相位字寄存器位宽所以相位分辨率只有 360 / 65536 度约 0.0055 度。如果你想做高精度移相光靠 POW 不够还要靠 Profile 切换时刻与 SYNC_CLK 的配合后面第 5 章会讲。2.4 例程里最该复用的三处封装拿到一份可用的 9910 例程我一般只关心三个函数的实现SPI 收发底层、写寄存器封装、参数到字值的换算。SPI 底层看的是时序是字节还是位传输以及片选拉低后地址位和数据位是否严格按手册帧格式走写寄存器封装看的是否有 IO_Update 锁存逻辑换算函数则直接决定了你上层给一个频率值能不能稳定变成 FTW。这三处如果能从例程里原样搬剩下的就是查表调参数。如果例程的换算函数用的是 float 直接除建议自己改成「分子分母都放大再截断」的整数算法避免在低频段出现可见的频点抖动。血泪经验板子在不同温度下跑浮点算出来的寄存器字在临界频点偶尔会差 1 个 LSB表现为输出频率偶发跳变改成整数运算后从此一劳永逸。3. 改频率和相位的关键代码路径从寄存器到输出3.1 平台无关的 SPI 写寄存器函数所有 9910 例程都会有一个最底层的写寄存器函数它的职责就是「通过 SPI 总线把一段数据送进指定寄存器」。通常帧结构是 8 bit 寄存器地址加 32 bit 数据高位在前。下面是很常见的一套写法void spi_write_reg(uint8_t reg_addr, uint32_t reg_data) { uint8_t buf[5]; buf[0] reg_addr 0x7F; /* 地址位: 写操作时 bit7 为 0 */ buf[1] (reg_data 24) 0xFF; /* 数据从高位字节开始 */ buf[2] (reg_data 16) 0xFF; buf[3] (reg_data 8) 0xFF; buf[4] reg_data 0xFF; cs_low(); /* 拉低片选, 启动一次事务 */ for (int i 0; i 5; i) { spi_write_byte(buf[i]); /* 每个字节高位先出 */ } cs_high(); /* 片选拉高, 结束事务 */ }这段代码做了三件事把 32 位数据拆成 4 个字节、按地址加数据的顺序发出、用片选包住整个事务。注意 reg_addr 做了 0x7F这是为了防止误把读/写标志位带进地址避免对寄存器造成意外写保护。如果例程里用 16 位地址比如带多寄存器突发模式的芯片则这段循环要改成先发地址再连续发多组数据结构类似但长度不同。这里有个更隐蔽的要点cs_high() 之后不要立刻发下一条指令。部分 9910 板卡的总线在片选释放后需要几个系统时钟的静默期否则下一次事务的第一个字节会被吞掉表现为「寄存器看着写成功了实际数值没变」。保守做法是在 cs_high() 之后加一个 1us 级延时如果板卡跑在高速 SPI10 MHz 以上这个延时尤其重要。3.2 set_frequency 的实现与整数运算频率修改的核心函数很多例程里长这样int set_frequency(uint32_t freq_hz, uint32_t sys_clk_hz) { uint64_t temp; uint32_t ftw; if (freq_hz sys_clk_hz / 2) /* 奈奎斯特限幅 */ return -1; temp (uint64_t)freq_hz 32; /* 等价于乘 2^32 */ temp temp / sys_clk_hz; /* 得到频率字 */ ftw (uint32_t)(temp 0xFFFFFFFF); spi_write_reg(REG_FTW0, ftw); /* 写入频率调谐字 */ pulse_io_update(); /* 触发锁存生效 */ return 0; }我这里故意用移位而不是浮点数除法原因是 32 位乘法在多数单片机上比浮点运算快而且没有任何舍入歧义。(uint64_t)freq_hz 32这一步是安全的因为 freq_hz 是 32 位左移 32 后仍落在 64 位范围内如果你写成freq_hz * 4294967296ULL效果一样但可读性稍差。代码里的 REG_FTW0 是寄存器偏移量的宏定义不同板卡上的映射关系可能不同需要按自己板子的硬件手册核对。这个函数还有一层隐含逻辑例程支持修改频率不代表必须暴露「系统时钟」给调用者。很多工程会把 sys_clk_hz 编译成常量让 set_frequency 只收一个目标频率。那样做对调用方最友好但遇到不同参考时钟的板子就要改宏重新编译。我一般保留这个参数上位机下发校频命令时可以直接调整不用动固件。3.3 set_phase 的实现与相位字换算相位修改函数与频率函数有九成相似唯一的变化是换算公式int set_phase(int16_t phase_deg) { uint32_t pow; /* 归一化到 0 ~ 359.999 度 */ if (phase_deg 0) phase_deg 360; if (phase_deg 360) phase_deg - 360; pow ((uint32_t)phase_deg 16) / 360; /* 相位字位宽 16 位 */ if ((((uint32_t)phase_deg 16) % 360) 180) pow 1; /* 四舍五入 */ spi_write_reg(REG_POW0, pow); pulse_io_update(); return 0; }这里的相位字位宽是 16所以映射公式是phase_deg * 65536 / 360。我用了移位加取模再判断的方式做四舍五入避免浮点误差。最后对余数 180的判断等价于「余数过半就进一位」这能让相位精度平均误差落在 0.0027 度以内。要注意uint32_t)phase_deg 16这个表达式phase_deg 是 int16_t先转成 uint32_t 再移位符号位不会带入计算。如果在 dspic 这类 16 位单片机上直接phase_deg 16编译器可能把结果截成 16 位得到 0——这是很多例程移植后相位改不动的最常见原因。改成先提升位宽再移位几乎能杜绝这个坑。3.4 一个综合示例同时改频率和相位并做 Profile 切换真实工程里频率和相位往往不是单独改的而是在一个特定时刻同时跳变。比如做相控阵波束扫描要同时更新多路 DDS 的频率和相位并保证这些更新在同一时刻生效。9910 的 Profile 机制就是为此设计的。下面示例展示了如何把频率和相位预先写到 Profile 0再触发切换void update_profile_0(uint32_t freq_hz, int16_t phase_deg, uint32_t sys_clk_hz) { uint32_t ftw calc_ftw(freq_hz, sys_clk_hz); uint32_t pow calc_pow(phase_deg); spi_write_reg(REG_FTW0, ftw); /* Profile0 频率字 */ spi_write_reg(REG_POW0, pow); /* Profile0 相位字 */ spi_write_reg(REG_PROFILE_SEL, 0x00); /* 选中 Profile0 */ pulse_io_update(); }这里没有先用 IO_Update 让 FTW 先生效、再让相位生效而是一次性写完两个字后统一触发锁存。9910 内部对同一 Profile 的频率和相位字是同拍采样的所以不会出现「频率变了相位没变」或反过来。这也是例程支持修改频率和相位时最该保留的时序逻辑——分开写、分开发 IO_Update在示波器上能看到频率和相位先后跳变这在很多同步场景里是致命的。函数里的 calc_ftw / calc_pow 是上一节两个公式的函数化封装。Profile_SEL 寄存器是 3 bit 宽度可以选 0~7 共 8 个 Profile对应硬件上的 PS0~PS2 引脚。如果你用引脚切 Profile软件写这个寄存器的动作可以省掉但寄存器值与引脚电平同时生效时以最终被选中的 Profile 为准。3.5 正确验证用示波器看初相而不是只看频率改完频率和相位验证方法也有讲究。初级验证是看频谱仪或示波器频率确认频率对吗、相位有没有变化。更可靠的验证是用示波器的两个通道一路接 DDS 输出一路接一个固定参考信号比如另一个通道输出同一个频点 0 相位的信号把触发设为参考通道上升沿再对比 DDS 通道的过零点位置。相位从 0 度改到 90 度过零点会明显左移或右移四分之一周期。另一个更简单的办法是看 DDS 自身输出的 I/O 更新信号和输出波形的关系每次 IO_Update 之后输出的第一个过零点相对脉冲沿的延迟应该随着 POW 值单调变化。如果这个延迟是乱的不用怀疑硬件先回头查 POW 换算和寄存器地址映射。还有如果你的板卡有多个 9910验证相位时一定要让它们共享同一个参考时钟否则看到的相位差根本不可重复——这不是例程的问题是硬件架构的问题。4. 9910 例程移植避坑频率能改相位不能改的五个典型原因4.1 只改 FTW 没改 POW相位字被默认值覆盖现象set_phase() 调用成功寄存器读回 POW 也变了但输出波形的初相纹丝不动。原因例程初始化流程里Profile 寄存器组可能被整体写入一次默认值而 set_phase() 改的是另一个地址段或者板子上有多片 9910 共用 SPI 总线set_phase 写到的芯片和实际输出信号的芯片不是同一颗。解决先用示波器或逻辑分析仪确认 SPI 片选拉的是哪一颗再读回目标寄存器的当前值如果例程里有 memcpy 式批量初始化把 set_phase 的写操作放在批量初始化之后。4.2 相位字换算位宽不对16 位芯片上的截断现象在 32 位平台上开发好的代码移植到 16 位 MCU 后相位只在 0 和 180 两个值跳。原因phase_deg 16在 16 位 int 上发生整型提升时编译器按 16 位运算处理高位被丢弃。解决圆弧公式全改成 uint32_t 显式类型再不行就写一个单独的乘除函数不要依赖编译器隐式提升。类似的问题也出现在 FTW 换算上只是低位丢失不易察觉频率高时能看到频点每隔一段时间跳一下。4.3 IO_Update 时序不够数据只进了影子寄存器现象写寄存器函数全部执行完返回值正常输出参数隔了很久才变偶尔还要重新上电才生效。原因IO_Update 脉冲宽度不够或者脉冲后沿恰好落在 SPI 时钟附近锁存行为不稳定。解决用逻辑分析仪抓 IO_Update 相对 SPI 片选释放的时序把脉冲宽度放宽到手册最低值的 2 倍并确保 IO_Update 与 SPI 时钟之间至少留出几个时钟周期的裕量。最稳妥的写法是在 pulse_io_update() 里先拉高、延时 100ns、拉低、再延时 100ns而不是一个纯软件翻转。4.4 相位累加器复位被忽略连续相位模式下初相不可控现象每次上电后同一个频率和相位值示波器上看到的初相都不一样。原因DDS 的相位累加器在持续运行上电时刻不同累加器当前值就不同而 POW 只是叠加在这个累加值上的偏移所以你看到的绝对相位会漂。解决要在改相位的同时把相位累加器清零或加载预设值。9910 的 CFR1 里有相关控制位初始化流程里应当把该位先置位再清除确保相位修改从确定的状态开始。很多例程不处理这个位导致「相位修改」在单次测量里看着有效丢电重来就乱。4.5 多片同步时只改了数据没改时钟相位漂移随时间累积现象两块 9910 板卡设置完全相同的频率和相位示波器上初相对齐跑几分钟后开始缓慢错开。原因两片芯片的参考时钟源不同步哪怕是同一个晶振的两个输出引脚走线长度不同也会引入相位噪声相位累加器各跑各的相位差必然随时间累积。解决硬件上让两片芯片共用同一个 SYNC_CLK 和 REF_CLK并在初始化里按照手册顺序做多片同步软件上定期重新下发一次相位字并触发 IO_Update 来「校相」。这种漂移不是你例程能根治的但例程保留周期校相接口能给上层软件一个兜底手段。5. 把例程改成扫频模式数字斜坡发生器的活用与边界5.1 用 DRG 代替逐点改频扫频曲线更平滑如果你需要的是从 10 MHz 到 100 MHz 的连续扫频手动逐个写 FTW 会带来两个问题一是每次写寄存器都要发 5 个字节再加 IO_Update扫频速度被 SPI 总线拖慢二是相邻频点之间的跳变是阶跃式的在频域会看到离散谱线不是连续扫频效果。9910 内部带了数字斜坡发生器DRG能自动从起始频率字递增或递减到终止频率字步长和斜率可配扫频路径由硬件状态机完成不受 SPI 速度限制。DRG 模式的修改路径和单频模式有本质区别单频模式直接写 FTW0 就完了DRG 模式要写斜坡速率寄存器、斜坡下限寄存器、斜坡上限寄存器还要把 CFR1 里的 DRG 使能位打开。很多例程标题写着「支持修改频率和相位」但初始化代码里把 CFR1 配成了固定单频模式——你在这个基础上写频率字实际效果和直接改 FTW 没区别扫频功能形同虚设。用之前先读回 CFR1 确认模式位。5.2 DRG 与相位修改的冲突斜坡期间 POW 仍然有效在 DRG 扫频过程中POW 依然参与输出相位合成所以频率在自动变化的同时相位偏移仍然可以用来做调制。但注意一个副作用DRG 每次步进会重新触发一次内部相位累加器的加载如果你的 POW 值在扫频过程中被更新新相位会在下一个斜坡步进时生效而不是立即生效。实践中表现为相位修改有 1~2 个步进的延迟对相位切换时机敏感的场景要提前在时间上补偿这个延迟。具体补偿方式是在上层把「修改相位」的指令提前到预期生效时刻之前发出提前量等于斜坡一个步进时间。这个时间可以从斜坡速率寄存器的值和系统时钟算出来每个步进经过的系统周期数 速率寄存器值 ×系统时钟 / 100 MHz。如果你的例程没有现成接口可以先写一个斜坡查询函数轮询 DRG 的当前值等它越过预设阈值后再写 POW也能做到毫秒级同步。5.3 斜坡步长与频率分辨率的取舍DRG 的步长是一个 32 位字每个步进会加到当前 FTW 上所以扫频的频率阶跃是「步长 × 频率分辨率」。举个例子系统时钟 1 GHzFTW 分辨率约 0.233 Hz设置步长为 2^20约 1048576则每个步进的频率增量约 244 kHz。如果你做雷达线性调频这个步长带来的离散谱会让旁瓣升高这时要把步长调小同时把斜坡速率调慢保证扫频总时间不变。这个取舍在例程里通常写成两个参数FSTEP 和 RATE。FSTEP 决定扫频间隔RATE 决定每步的时间。两者相乘再乘以总步数就是整个扫频周期。改动时优先动 RATE不要动 FSTEP——频率分辨率是物理特性压缩步长会让扫频曲线变毛糙而 RATE 只是时间维度的问题。5.4 建立一个简单的扫频验证表格参数调整是否合理我习惯用一张表来对照验证而不是只看最终波形。参数项设置值预期结果失败时的现象FSTEP2^20频率阶跃约 244 kHz频谱出现可分辨离散线RATE1000每步约 10 us扫频总时间与设定不符FIT[25:24]01上升沿扫频输出频率单调上升输出频率反向或周期性回扫POW90 度扫频全程相位偏移恒定相位随频率变化而非固定这张表的最后一行是很多人忽略的相位偏移在扫频过程中应该保持不变除非你刻意做相位调制。如果你看到相位跟着频率走说明 POW 在扫频过程中被 DRG 的某些位段间接覆盖了去查 CFR1 里 DRG 相关的控制位多半是把「斜坡加载到相位累加器」和「斜坡加载到 FTW」同时打开了。5.5 回到例程本身什么时候该用 Profile什么时候该用 DRG一句话结论需要做点频跳变和相位跳变时用 Profile需要做连续频率扫描时用 DRG。Profile 切换是离散的、由外部引脚或寄存器触发适合雷达频点切换、跳频通信这类应用DRG 是连续的、由硬件自动推进适合线性调频、扫频测试这类应用。两者可以同时启用但要把触发条件理顺不要让 Profile 选择信号在 DRG 斜坡中途翻转。工程里最容易翻车的就是这种组合场景DRG 扫到一半外部 PS 引脚切了 Profile扫频曲线直接跳回起点整段数据作废。要是例程里没有对这种冲突做互锁建议自己加一段状态机保护。6. 最后的习惯每次改动前先做一组「频率-相位」回归验证浮点改整数、IO_Update 加宽、DRG 参数调整——这些改动单独拿出来都非常合理但合在一起就可能破坏原有行为。我现在的习惯是每次改完例程固定跑一组回归序列频率点取 10 MHz、50 MHz、121.3 MHz 三个值相位取 0°、45°、90°、180°、270° 五个值两两组合后共 15 组数据全部用示波器自动测量初相和频率与理论值比对。整个流程跑完不到十分钟但已经帮我抓出过好几次「频率对着、相位差 180°」的隐蔽问题。这个回归序列的选取是有讲究的121.3 MHz 这个频率点在 1 GHz 系统时钟下对应的 FTW 是 520093696一个非整数倍频点能验证整数换算的精度45° 和 270° 分别对应相位字 8192 和 49152覆盖了相位字的四分之一和四分之三边界最容易触发截断错误。如果你的例程是用浮点写的这个序列还会暴露低频段和小角度的舍入抖动。还有一个更实用的技巧把频率和相位修改合并成一个原子操作。很多例程提供了两个独立函数但上层调用时常常分开调用中间隔了一条 I/O 操作指令于是两个参数在示波器上明显有先后顺序。合并成一个函数后内部连续写 FTW 和 POW再统一拉一次 IO_Update两个参数的生效时刻就只差一个 SPI 事务的时间对大多数应用都足够用了。希望这些从寄存器到验证路径上的方法和教训能帮你少走几段弯路。9910 的频率相位修改并不神秘把字换算、寄存器映射和 IO_Update 时序这三件事抓牢剩下的事情就只是调参数了。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询