
简介面向嵌入式开发者的完整DDS信号生成驱动工程基于意法半导体STM32F103微控制器与亚德诺AD9106芯片适合需要实现高精度波形输出的工程师和电子爱好者参考使用。工程覆盖从系统初始化到波形生成的全流程包括时钟系统配置、通用输入输出引脚设置、外设接口适配以及AD9106寄存器参数写入波形方面支持上锯齿波、下锯齿波、三角波、直流输出、噪声、正弦波及自定义波形等常见模式并附带用户交互与异常处理模块便于实际调试和二次开发。压缩包内含七十五个文件以三十五个头文件与三十三个源文件为主体另有KEIL工程配置、烧录固件及链接脚本等整体仅二百九十二KB目录按CORE、SYSTEM、HARDWARE等模块划分层次清晰方便定位与移植。目前已有约一百七十人学习下载适合希望通过实际例程快速掌握DDS技术、搭建信号发生器原型或进一步研究寄存器级控制与嵌入式驱动设计的中高级开发者。 做了很长时间的嵌入式信号源方案我最后选定的组合是 STM32F103VCT6 加 ADI 的 AD9106。这套驱动代码从底层 SPI 寄存器读写到上层频率/幅度控制全部打通输出四路同步正弦波、方波、三角波和任意波形都没问题。当时项目要求四通道独立可调、相位严格同步市面上的信号发生器模块要么通道少、要么同步精度差最后直接用 DDS 芯片解决反而最省事。这篇内容就把整个驱动的设计思路、寄存器配置、代码实现、还有调试踩过的坑完整写出来给正在做类似信号源、波形发生器、或者 AD9106 相关项目的朋友一个可以直接参考的工程样板。1. 项目整体设计与思路拆解1.1 为什么选 STM32F103VCT6 AD9106 这套组合先聊芯片选型。AD9106 是 ADI 推出的四通道、12 位、180 MSPS 的 DDS/DAC 芯片内部集成了 DDS 核、波形 RAM、DAC 输出级和 SPI 配置接口。单芯片就能输出四路同步波形还支持任意波形回放这在多通道信号激励、MEMS 传感器驱动、医疗超声激励等场景非常合适。当时对比过 AD9833、AD9850 这些经典 DDS 芯片它们大多只有单通道或双通道要凑四路就得用四颗芯片同步性根本没法保证所以 AD9106 这种多通道合封方案是唯一解。控制端选用 STM32F103VCT6主要看中它的资源匹配度72 MHz 主频跑 SPI 通信绰绰有余512 KB Flash 可以存任意波形表48 KB SRAM 做 DMA 缓冲也够用。更重要的是这个芯片太成熟了资料多、封装通用、做小批量打样成本也低。整套系统里 STM32 只是扮演“配置员”的角色不用参与实时波形运算实时性完全靠 AD9106 的硬件 DDS 核保证这样 MCU 资源还能留给 UI 交互、通信协议等其他功能。1.2 系统组成与硬件连接规划整个系统硬件分成三个部分电源与参考时钟、STM32 主控、AD9106 射频输出。电源部分给 STM32 和 AD9106 供电AD9106 的 AVDD/DVDD 用 3.3 V 即可但需要注意模拟电源和数字电源分开走线尽量用磁珠或电感隔离避免数字噪声串到 DAC 输出。参考时钟用独立有源晶振给 AD9106 的 MCLKIN 引脚提供时钟这是整套系统最关键的信号之一时钟质量直接决定输出波形的相位噪声和杂散。STM32 和 AD9106 之间通过 SPI 连接只需要四根线SCLK、SDIOMOSI、CS、以及读回用的 SDO也可以配置成三线模式。AD9106 的 SPI 从机模式支持最高 40 MHz 时钟STM32F103 的 SPI1 挂载在 APB2 总线上最高 18 MHz完全够用。VCT6 封装引脚丰富我用的是 SPI1 的 PA5/PA6/PA7 引脚CS 随便配一个 GPIO 控制触发同步信号用定时器 PWM 输出接到 AD9106 的 TRIGGER 引脚后面讲 CUC 多通道同步时会再细说。注意AD9106 的 DAC 输出是电流型输出不要直接接运放输入端要先通过变压器或电阻网络转成电压信号再做幅度调理和阻抗匹配。2. AD9106 核心机制与驱动设计要点2.1 DDS 频率字计算原理DDS 的核心思想就是查表加累加器。AD9106 内置一个 32 位相位累加器每个 MCLK 时钟周期累加一次频率调谐字 FTWFrequency Tuning Word累加器的高位作为地址去查找正弦波 ROM 表再经过 DAC 输出模拟波形。输出频率和累加字的关系很简单f_out FTW × f_MCLK / 2^32反过来想要输出指定频率频率字就是FTW f_out × 2^32 / f_MCLK举个例子MCLK 用 180 MHz想要输出 2 MHz 正弦波代入公式FTW 2e6 × 4294967296 / 180e6 ≈ 47721.857取整后是 47721此时实际输出频率约 1.999993 MHz误差只有 7 Hz 左右。这个误差对于多数应用完全可接受。如果要求更高可以在代码里用浮点运算计算 FTW或者用双模小数分频的思路在用户界面显示实际输出频率而不是理想频率。相位控制字是 16 位的范围 0 到 65535 对应 0° 到 360°分辨率大约 0.0055°。多通道同步输出时只要给每个通道设置不同的相位字就能得到精确的相位差关系。2.2 SPI 接口时序与指令格式AD9106 的 SPI 接口指令格式分两部分前 16 位是控制字后 16 位是数据字。控制字的 bit15 表示读写方向1 为读、0 为写低 14 位是寄存器地址。写操作就是 CS 拉低后连续发送 32 位数据先控制字后数据字读完拉高 CS。时序模式必须配置为 SPI Mode 1即 CPOL0、CPHA1时钟空闲低电平、第二个边沿采样。这一点非常关键我曾经用默认的 Mode 0 去通信读回来的寄存器全是 0xFF排查了半天才发现是相位没对上。STM32F103 配置成 SPI 主模式时有个特点接收和发送是同时进行的读 16 位数据时要同时发送 16 位 dummy 数据否则读不到正确内容。写寄存器的时候就简单了直接开 16 位数据帧连续发送两次即可。2.3 寄存器配置顺序与关键寄存器AD9106 寄存器数量不少但驱动核心只需要关注几组。配置顺序建议是复位、SPI 控制、DDS 频率相位、波形模式、DAC 输出控制、最终触发更新。复位后第一件事是写 SPI_CONTROL0x0000寄存器主要配置 LFSR 是否使能、数据帧格式、时钟分频等。很多例程直接用默认值也能通信但保险起见我会在这个寄存器里把 LFSR 关闭避免协议解析异常。然后配置 DDS 频率寄存器 DDS_TUNE0_FREQ0x0003和 DDS_TUNE0_FREQMSB0x0004前者是频率字低 16 位后者是高 16 位。相位字在 DDS_TUNE0_PHASE0x0005。波形模式由 WAVE0~WAVE30x0014~0x0017寄存器控制每个通道独立配置可以选 DDS 输出、RAM 输出、或两者混合。DAC 输出有 DAC_CTRL0x0012和 DAC_CTRL20x0013两个寄存器控制满量程电流、输出极性、单端还是差分模式。幅度和直流偏置分别由 AMP00x0009和 OFFSET00x000A控制其中幅度寄存器是带符号的注意正负范围。最后一个关键点是触发更新。AD9106 内部很多寄存器并不是写完立即生效需要在 TRIGGER0x0007寄存器写入触发值后内部锁存的数据才统一更新到工作寄存器。这个机制保证了多通道之间不会因为写入时间差产生相位偏移是同步输出的核心。寄存器地址作用配置要点SPI_CONTROL0x0000SPI 模式控制关闭 LFSR确认数据帧格式DDS_TUNE0_FREQ0x0003频率字低 16 位按 FTW 公式计算DDS_TUNE0_FREQMSB0x0004频率字高 16 位按 FTW 公式计算DDS_TUNE0_PHASE0x0005相位字0~65535 对应 0~360°TRIGGER0x0007触发更新配置完成后写触发值AMP00x0009通道 0 幅度带符号注意范围WAVE00x0014通道 0 波形模式选 DDS/RAM/混合DAC_CTRL0x0012DAC 输出控制满量程、极性等3. 完整工程代码实现3.1 工程结构与初始化流程工程基于 STM32 标准外设库组织核心文件包括 main.c、ad9106.c、ad9106.h、spi 初始化相关代码。如果习惯用 CubeMX也可以用 HAL 库重新封装底层 SPI 读写逻辑完全一样。关键是把 AD9106 寄存器定义、SPI 读写接口、频率计算、通道配置这几层剥离开方便复用。初始化流程按照上一节的顺序来先初始化系统时钟、SPI GPIO、CS/触发 GPIO然后调用 AD9106_Init()在 Init 里面依次执行软件复位、写 SPI_CONTROL、配置四个通道的频率和相位、设置波形模式、配置 DAC 输出、最后触发更新使能。整个 Init 做好之后应用层只需要调 AD9106_SetFrequency(channel, freq)、AD9106_SetAmplitude(channel, amp) 这些接口就能动态修改输出。3.2 寄存器读写底层实现底层读写函数是整个驱动的地基必须保证时序准确。写寄存器函数的逻辑就是前面说的 16 位控制字加 16 位数据字注意 16 位数据要按高字节在前的方式顺序发出STM32 配置成大端模式发送即可。CS 控制用普通 GPIO拉低前确保 SPI 已经空闲拉高后留一点时间间隔让 AD9106 内部逻辑稳定。读寄存器稍微麻烦一点因为 STM32 的全双工 SPI 在接收数据时必须在 MOSI 上发时钟。标准的做法是发完控制字后再发一个 0x0000 dummy 数据同时把 SDO 返回的 16 位数据接收下来。有些朋友读不到数据就是漏了第二步的发送操作。void AD9106_WriteReg(uint16_t regAddr, uint16_t regData) { uint16_t cmd regAddr 0x3FFF; AD9106_CS_LOW(); SPI1_WriteRead16(cmd); SPI1_WriteRead16(regData); AD9106_CS_HIGH(); } uint16_t AD9106_ReadReg(uint16_t regAddr) { uint16_t cmd (1 15) | (regAddr 0x3FFF); uint16_t dummy 0x0000; AD9106_CS_LOW(); SPI1_WriteRead16(cmd); uint16_t val SPI1_WriteRead16(dummy); AD9106_CS_HIGH(); return val; }3.3 SPI 底层初始化配置SPI1 初始化时要注意数据帧格式必须配置为 16 位CPOL 和 CPHA 对应 Mode 1时钟分频根据实际时钟频率调整。STM32F103 的 SPI1 最高支持 18 MHzAD9106 最高 40 MHz所以直接用 72 MHz 的主频做二分频得到 36 MHz 其实超了 AD9106 规格稳妥起见我用的是四分频即 18 MHz测试下来完全稳定。GPIO 配置方面SCK、MOSI 配置为复用推挽输出MISO 配置为浮空输入或上拉输入CS 配置为通用推挽输出。注意 AD9106 的 SDIO 引脚是双向的如果配置成三线模式读写方向切换需要在软件里控制引脚方向比较麻烦。我直接用了四线模式SDIO 只做 MOSISDO 单独接 MISO逻辑清晰底层驱动也简单。3.4 初始化与频率设置示例整个初始化函数按顺序执行就好保持代码可读性。复位操作通过 SPI_CONTROL 寄存器的复位位触发写完复位位后建议延时 10 ms 等内部模拟电路稳定再继续后续配置。void AD9106_Init(void) { // 软件复位 AD9106_WriteReg(0x0000, 0x1000); // 置位 RESET 位 Delay_ms(10); AD9106_WriteReg(0x0000, 0x0000); // 清除复位位 // 配置 SPI 控制关闭 LFSR标准 SPI 模式 AD9106_WriteReg(0x0000, 0x0002); // 默认输出 1MHz 正弦波 AD9106_SetFrequency(0, 1000000U); AD9106_SetAmplitude(0, 18000); // 约满量程的 55% AD9106_SetWavePattern(0, AD9106_WAVE_DDS_SIN); // 触发更新让配置生效 AD9106_WriteReg(0x0007, 0x0001); AD9106_WriteReg(0x0007, 0x0000); }设置频率的函数内部就是做 32 位乘法除法然后拆成两个 16 位寄存器写入。为了速度可以全部用整数运算频率单位 Hz、MCLK 单位 Hz计算结果直接截断。void AD9106_SetFrequency(uint8_t channel, uint32_t freqHz) { uint64_t ftw (uint64_t)freqHz * 4294967296ULL / MCLK_HZ; uint16_t low (uint16_t)(ftw 0xFFFF); uint16_t high (uint16_t)((ftw 16) 0xFFFF); uint16_t regLow AD9106_REG_DDS_TUNE0_FREQ channel * 2; AD9106_WriteReg(regLow, low); AD9106_WriteReg(regLow 1, high); AD9106_WriteReg(0x0007, 0x0001); // 触发更新 AD9106_WriteReg(0x0007, 0x0000); }这里有四个通道所以 DDS 频率寄存器地址是连续排列的通道 0 对应 0x0003/0x0004通道 1 对应 0x0005/0x0006 吗实际上不是通道 1 的频率寄存器在 0x00034相位寄存器、幅度寄存器等也是按通道偏移排列。写代码的时候一定要对照手册的寄存器地址表不能想当然连续递增。3.5 多通道同步与 CUC 实现细节多通道同步是 AD9106 的核心卖点也是实际项目中最容易出问题的地方。芯片提供了相干更新模式CUC保证所有通道在同一时钟沿更新频率、相位、幅度等参数。如果不用 CUC即使寄存器配置内容相同通道间也可能因为写入顺序和时间差出现不可预知的相位关系。CUC 模式实现步骤先把 SPI_CONTROL 或 TRIGGER 相关寄存器中的 CUC_EN 位使能然后正常写各通道的频率、相位、幅度寄存器所有寄存器写完后再写一次触发寄存器硬件会在下一个触发沿把所有配置一次性锁存生效。实际调测时我用示波器同时观察两个通道的输出开启 CUC 后波形起点完全对齐相位差和配置值一致效果立竿见影。void AD9106_EnableCUC(uint8_t enable) { uint16_t val AD9106_ReadReg(0x0007); if (enable) { val | 0x0002; // 假设 bit1 为 CUC 使能位实际以手册为准 } else { val ~0x0002; } AD9106_WriteReg(0x0007, val); }提示CUC 的使能位具体在哪个寄存器、哪个 bit不同批次手册可能标注有差异移植时一定以你手头 datasheet 为准不要直接照抄网上的代码我就在这里栽过跟头。4. 实测中的常见问题与排查技巧实录4.1 SPI 读回寄存器全 FF这个问题在调试前期出现频率最高。先确认硬件接线SCLK、MOSI、MISO、CS 有没有接反GND 是否共地然后确认 SPI 模式是不是 Mode 1CPOL/CPHA 任何一个错了数据就完全错乱。软件层面STM32 的 SPI 在主模式收数据时必须同时发送 dummy 字节如果漏了这一步读回的全是 0x00 而不是 0xFF。最后还要检查 AD9106 的供电是否正常有些芯片在 DVDD 没上电时 SDO 引脚会输出高阻或者异常电平表现也是读回全 FF。顺带说一句刚焊完板子第一次上电先用万用表量一遍电源对地阻抗排除短路再插调试器。4.2 有配置但输出端没有任何波形寄存器读写都正常但示波器探头上什么都看不到这种问题一般出在输出链路或触发更新环节。先看 DAC 输出是不是电流型输出没有加负载电阻示波器探头如果直接接在 DAC 输出引脚上由于没有电流回路读到的就是一条直线。接一个 50 欧姆电阻到地再测试或者用差分探头跨接在输出和地之间。然后检查触发更新是否执行了AD9106 很多寄存器要触发后才生效如果只写了配置没触发DAC 可能一直输出默认值或者零电平。还有一个容易忽略的细节WAVE 寄存器没有选择 DDS_SIN 模式而是默认的 RAM 模式RAM 又没有写入有效波形输出自然是空。4.3 输出频率和设定值偏差大频率偏差一般分两种情况系统误差和配置误差。系统误差来自频率字取整FTW 计算时小数部分被截断输出频率比设定值低一点点频率越高偏差越明显这是正常的。配置误差则是 MCLK 实际频率和你程序里定义的常量不一致导致的如果板上晶振是 25 MHz 但代码里写的是 180 MHz输出频率会差好几倍。排查方法是先量一下 MCLK 引脚的实际频率用示波器或频率计都可以再用实际频率回代计算 FTW。我曾经调试一块板子有源晶振标称 100 MHz 实测只有 99.98 MHz虽然偏差不大但做高精度输出时这个误差就必须修正。4.4 多通道相位不同步的处理心得通道间相位不一致最大的坑就是没开启 CUC 模式。不开启 CUC 时每个通道的寄存器写入后延迟不同触发更新的时机也有先后最终输出相位是随机的。开了 CUC 模式后所有通道的更新被对齐到同一个时钟沿相位关系就明确了。第二个坑是触发信号的时序。TRIGGER 引脚上如果有毛刺或者干扰可能导致误触发。建议触发引脚走线远离大电流开关节点必要时并联一个小电容滤波。软件触发时也要注意写触发寄存器后要清零复位下一次更新才能再次触发。这个方法分享给正在用 AD9106 做多通道同步信号源的朋友调试时先把四个通道设定成不同频率和相位比如通道 0 输出 1 MHz 0°通道 1 输出 1 MHz 90°用四通道示波器同时观察能快速确认 CUC 是否真正生效。我实际测试时遇到过通道 0 和通道 1 相位差对但通道 2 和通道 3 相对漂移的情况后来发现是那一路 SPI 数据线过长导致误码缩短线长并降低 SPI 时钟到 9 MHz 后彻底解决。最后再分享一个实用技巧在 ad9106.h 里把寄存器地址全部定义成宏用通道号做偏移统一管理维护起来会轻松很多。调试时加一个读回寄存器校验的函数每次写完配置后读回来比对一下能第一时间发现 SPI 通信错误别等到输出波形不对了再回头查寄存器。这套代码目前已经稳定跑了大半年四通道连续输出 24 小时无异常希望这篇内容能帮你少走弯路。本文还有配套的精品资源点击获取