AD9833与STM32实战:DDS驱动、SPI时序与波形验证
简介面向STM32开发者的AD9833波形发生器驱动资料适合需要为嵌入式项目添加高精度可编程信号源的电子工程师与学习者。压缩包内含完整工程源码与说明文档以34个h头文件、33个c源文件为主辅以uvprojx工程配置、axf/hex固件、htm/doc说明及txt笔记等构成完整可编译的STM32标准外设库工程共222个文件整体约6.15MB。已有1332人学习下载。资料基于STM32F103平台使用标准外设库配置SPI通信驱动AD9833输出正弦波、方波、三角波内容覆盖GPIO控制、SPI通信时序、定时器、RCC、ADC、I2C等模块的初始化与调用并包含启动文件、链接脚本和编译中间文件便于直接打开工程对照学习。通过阅读可快速掌握AD9833寄存器配置、波形参数设置及STM32外设协同方法理解分频系数、相位累加等核心概念快速迁移到自定义频率与相位控制功能减少自行查阅手册和排查通信问题的时间。1. AD9833 与 STM3225MHz DDS 的使用手册里藏着哪些坑以「AD9833 STM32.zip」命名的资料包解压后内容基本就是一份 AD9833 使用手册 PDF、一个 STM32 驱动工程和一张模块原理图。这颗 25MHz DDS 在 STM32 项目里做可调信号源、扫频仪、超声激励都用得上3 线 SPI、28 位频率字、0.09Hz 分辨率调频调相都在寄存器层面完成。真正让工程师卡住的不是喂频率字而是控制字的保留位、FSYNC 与 SCLK 的相对时序、以及 VOUT 半幅输出这几个手册里写得很含蓄的点。下面按寄存器模型、SPI 最小驱动、切频与滤波、示波器验证四步把 AD9833 接 STM32 的完整路径走一遍。新手能照着点出正弦波熟手可以直接看第四章的 FSEL 乒乓切换和第五章的定标方法。2. AD9833 寄存器模型与 SPI 写入时序使用手册先看这三张表所有 AD9833 驱动最后都能归到「往 16 位寄存器写一帧」这一个动作上。读使用手册时盯住三处引脚定义与极限参数、寄存器寻址表、SPI 时序图。这三张表决定接线、写帧格式和 SPI 模式任何一个理解偏了后面全盘皆错而且芯片不给你任何读回机会。2.1 引脚与输出特性MCLK 上限、VOUT 的半幅输出模块上要接的引脚不多VCC、GND、MCLK、FSYNC、SCLK、SDATA、VOUT。MCLK 是 DDS 的采样时钟手册标称最高 25MHz输出频率理论上限是 MCLK/2实际超过 MCLK/4 后波形质量就要靠滤波器补救MCLK 由模块上焊好的晶振提供通常是 25.000MHz但也有 20.000MHz 的版本这直接影响频率字换算系数。FSYNC 是低有效的帧选通语义上像 SPI 的 CS但每 16 位要严格拉低一次不能挂在硬件 NSS 上。另一个容易被跳过的是输出特性VOUT 静态电平在 VDD/2 附近正弦幅度大约 0.6Vpp受 AVDD 影响3.3V 供电实测 0.55~0.65Vpp 都正常输出阻抗约 200Ω。拿这个数去对示波器就不会把零点几伏的摆幅误判成芯片坏了。2.2 寄存器寻址为什么 28 位频率字要拆成两个 14 位半字AD9833 是只写器件每个 16 位字的高 2 位决定去向00 是控制字01 进频率寄存器的低 14 位10 进高 14 位11 进相位寄存器。28 位频率字由此必须拆成两个 14 位半字写入两个半字都到位后输出才切到新的完整频率。剩余位里通常用 DB13 区分 FREQ0 和 FREQ1用 DB12 及保留位区分 PHASE0/PHASE1具体以你手上手册的寻址表为准。16 位字高 2 位 (DB15/DB14)功能常用帧头含寄存器选择位00写控制字0x0000 控制位定义01写频率字低 14 位FREQ0 用 0x4000FREQ1 用 0x500010写频率字高 14 位FREQ0 用 0x8000FREQ1 用 0x900011写 12 位相位字PHASE0 用 0xC000PHASE1 查手册确认这里容易踩的坑是字节序STM32 是小端直接HAL_SPI_Transmit(hspi1, (uint8_t *)word, 2, ...)会把低字节先发出去AD9833 要求 MSB first结果就是波形完全不亮或频率乱跳。正确做法是手工组包把高字节放前面。2.3 控制字位定义RESET、SLEEP、OPBITEN、MODE、FSEL控制字是被检查最多的 16 位。RESET 置 1 时相位累加器复位、VOUT 停在中间电平寄存器写入照常生效所以「先置位再清位」是最稳的初始化套路SLEEP1 关内部时钟路径SLEEP12 关 DAC省电场景两个都要照顾OPBITEN 把片内比较器接到 VOUT 输出方波配合 DB8 的 DIV2 决定方波是 f/2 还是 f/4MODE 把 DAC 波形切成三角波与 OPBITEN 互斥两者同时为 1 时方波优先。位掩码名称置 1 效果DB130x2000RESET复位相位累加器VOUT 回中间电平DB120x1000SLEEP1关闭内部 MCLK 路径DB110x0800SLEEP12DAC 掉电DB100x0400OPBITENVOUT 切到比较器输出方波DB80x0100DIV2方波频率 f/20 时为 f/4DB50x0020MODE1 三角波0 正弦DB30x0008FSEL1 输出选 FREQ1DB20x0004PSEL1 输出选 PHASE1提示AD9833 没有读回寄存器写没写对只能靠 VOUT 波形判断。这也是为什么后面要把「示波器验证」做成固定动作。2.4 FSYNC 与 SCLK 的时序关系SPI 为什么对齐到 Mode 2手册时序图里 SCLK 空闲为高数据在 SCLK 下降沿被采入FSYNC 拉低后要留出建立时间再送第一个下降沿。这个「空闲高 下降沿采样」对应标准 SPI Mode 2CPOL1, CPHA0HAL 里是SPI_POLARITY_HIGH SPI_PHASE_1EDGE。网上大量例程用 Mode 3 也能出波形是因为低速 SPI 下建立时间余量太大一旦把 SPI 提到 10MHz 以上Mode 3 写法会出现偶发丢字。手上没有现成库时模拟时序最直观先把下面这段跑通再换硬件 SPI 不迟。// 不依赖库的 AD9833 写帧先验证时序再换硬件 SPI void AD9833_Write(uint16_t word) { // FSYNC 是帧选通每 16 位拉低一次传输期间可保持低 HAL_GPIO_WritePin(FSYNC_PORT, FSYNC_PIN, GPIO_PIN_RESET); for (int i 15; i 0; i--) { HAL_GPIO_WritePin(SCLK_PORT, SCLK_PIN, GPIO_PIN_SET); // SCLK 空闲为高 HAL_GPIO_WritePin(SDATA_PORT, SDATA_PIN, // 先摆数据再给下降沿 (word (1u i)) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(SCLK_PORT, SCLK_PIN, GPIO_PIN_RESET); // 下降沿AD9833 此刻采样 } HAL_GPIO_WritePin(FSYNC_PORT, FSYNC_PIN, GPIO_PIN_SET); // 帧结束 }逻辑说明先写 SDATA 再拉低 SCLK保证数据在采样沿之前稳定FSYNC 每帧单独拉高拉低避免多帧连写时把帧边界弄混。低速调试时不需要加延时遇到偶发错字检查 SDATA 在 SCLK 下降沿前的建立时间是否够必要时在两个沿之间补几条 nop。3. STM32 上跑通 AD9833 最小驱动SPI 参数、频率字和波形切换实际工程里没人愿意一直用 GPIO 模拟 SPISTM32 硬件 SPI 接 AD9833 的关键就三个模式选对、FSYNC 用 GPIO、频率字计算别用浮点。这一章把 CubeMX 配置到 HAL 驱动一次讲完标准库工程的老代码也能照着改。3.1 CubeMX 里的 SPI 参数Mode 2、8 位位宽、FSYNC 挂 GPIOFSYNC 不要挂硬件 NSS。CubeMX 的 NSS 自动模式会在每字节之间翻转把 16 位帧拆成两截AD9833 直接当成两个孤立的半字输出完全错乱。正确做法是 NSS 设 SoftwareFSYNC 用任意普通 GPIO 手动控制。SPI 参数按前面时序结论设置Mode 2、8 位数据位宽、MSB first、软件 NSS分频在 72MHz APB 上取 /8 得 9MHz低于 AD9833 手册标称的 40MHz SCLK 上限余量充足。hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; // 只用 MOSI 时也可按需调整 hspi1.Init.DataSize SPI_DATASIZE_8BIT; // 8 位 手工组包端口最稳 hspi1.Init.CLKPolarity SPI_POLARITY_HIGH; // CPOL 1 hspi1.Init.CLKPhase SPI_PHASE_1EDGE; // CPHA 0即 Mode 2 hspi1.Init.NSS SPI_NSS_SOFT; // FSYNC 不用硬件 NSS hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_8; // 72MHz / 8 9MHz位宽选 8 位而不是 16 位是为了绕开 HAL 在 16 位传输时的字节序处理差异。老工程还在用标准库的对照SPI_CPOL_High / SPI_CPHA_1Edge两个宏改即可注意标准库宏命名和 HAL 相反别直接抄。3.2 频率字换算2^28 除以 MCLK移位代替浮点频率字公式是 FREQREG (fout × 2^28) / MCLK。25MHz 晶振下系数是 10.737418241kHz 对应频率字 10737量化误差在半 LSB 内输出分辨率 0.0931Hz。工程上不要写浮点乘法用 64 位左移再除既避免 double 在 ARM 上的软浮点开销也方便换晶振频率时只改一个宏。// ad9833_hal.cSPI1 PA5/PA6/PA7FSYNC PA4普通 GPIO #define AD9833_MCLK 25000000u // 模块上的晶振频率先看丝印再改 #define AD9833_FREQ0_LSB 0x4000u // 帧头FREQ0 低 14 位 #define AD9833_FREQ0_MSB 0x8000u // 帧头FREQ0 高 14 位 #define AD9833_PHASE0 0xC000u // 帧头PHASE0 #define CTRL_RESET 0x2000u // 控制字复位 #define CTRL_OPBITEN 0x0400u // 控制字方波输出 #define CTRL_MODE 0x0020u // 控制字三角波 static uint16_t ctrl 0x0000; // 控制字影子变量只改需要的位 void AD9833_Write(uint16_t word) { uint8_t buf[2] { (word 8) 0xFF, word 0xFF }; // 手工排成 MSB first HAL_GPIO_WritePin(FSYNC_PORT, FSYNC_PIN, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, buf, 2, 10); // 一个 16 位帧 HAL_GPIO_WritePin(FSYNC_PORT, FSYNC_PIN, GPIO_PIN_SET); } uint32_t AD9833_HzToWord(uint32_t hz) { // (hz * 2^28) / MCLK用移位展开成 64 位再除避免浮点 return (uint32_t)(((uint64_t)hz 28) / AD9833_MCLK); } void AD9833_SetFrequency(uint32_t hz) { uint32_t w AD9833_HzToWord(hz); AD9833_Write(AD9833_FREQ0_LSB | (w 0x3FFF)); // 先低 14 位 AD9833_Write(AD9833_FREQ0_MSB | ((w 14) 0x3FFF)); // 再高 14 位 } void AD9833_Init(void) { AD9833_Write(CTRL_RESET); // 复位VOUT 回中间电平 AD9833_SetFrequency(1000); // 默认 1kHz AD9833_Write(AD9833_PHASE0 | 0); // 相位 0 ctrl ~(CTRL_OPBITEN | CTRL_MODE); // 保持正弦 AD9833_Write(ctrl); // 清 RESET立即输出 }参数说明buf手工组包是为了绕开字节序和 16 位位宽的移植差异HzToWord左移 28 位再除F1 上编译成 ARM 除法指令比浮点快一个量级先写低半字再写高半字是惯例两个半字都写完输出寄存器才整体更新写一半时输出保持旧值不会出现中间频率。初始化里「先 RESET 再配寄存器最后清 RESET」的顺序不要颠倒RESET1 期间写寄存器是安全的清 RESET 那一帧才是输出生效点。3.3 波形切换的边界MODE 和 OPBITEN 不要同时置位目标频率频率字25MHz MCLK低 14 位帧高 14 位帧1kHz107370x29F10x69F10x8000100kHz1073740x1A36E0x636E0x80061MHz10737420x1061CE0x624E0x8041切方波时ctrl | CTRL_OPBITEN且保证 MODE 位是 0切三角波反过来。很多例程把 MODE 写死成固定值导致正弦、方波之间要写两次控制字才能复位状态。建议把ctrl留在内存里当影子寄存器每次只改需要的位再整字写回后面切频率、切波形、做扫频都省心。注意方波是相位累加器最高位经比较器输出的幅度接近满摆幅和正弦的半幅输出不是一回事后级增益要分开算。4. AD9833 扫频与切频实战FSEL 预热、RESET 对齐和输出滤波单频点输出只是第一步实际项目里更常做的是扫频、跳频和连续波切换。这一章讲怎么切频率不出现毛刺、怎么让扫频相位连续以及输出端为什么要加 RC 低通。4.1 无毛刺切频往「没在用的寄存器」里预写直接改写正在输出的 FREQ0 寄存器频率切换发生在某一帧写完之后对大多数场景够用但做扫频仪或连续跳频时这种「边输出边改写」会在切换瞬间同时改变频率和相位累加器的递进斜率输出出现毛刺。标准做法是把 FREQ0/FREQ1 当乒乓缓冲输出用 FREQ0 时把下一个频率写进 FREQ1再置 FSEL1 切过去下一次往 FREQ0 预写再切回。被预写的寄存器在改写期间不影响输出频率字和相位都是完整的。#define AD9833_FREQ1_LSB 0x5000u // FREQ1 低 14 位帧头 #define AD9833_FREQ1_MSB 0x9000u // FREQ1 高 14 位帧头 // use_freq1 1 时写 FREQ1 并切到 FREQ1 0 时写 FREQ0 并切回 FREQ0 void AD9833_SweepStep(uint32_t next_hz, uint32_t use_freq1) { uint32_t w AD9833_HzToWord(next_hz); if (use_freq1) { AD9833_Write(AD9833_FREQ1_LSB | (w 0x3FFF)); AD9833_Write(AD9833_FREQ1_MSB | ((w 14) 0x3FFF)); ctrl | CTRL_FSEL; // 切到 FREQ1FREQ0 留作下次预写 } else { AD9833_Write(AD9833_FREQ0_LSB | (w 0x3FFF)); AD9833_Write(AD9833_FREQ0_MSB | ((w 14) 0x3FFF)); ctrl ~CTRL_FSEL; } AD9833_Write(ctrl); // FSEL 与控制字同帧生效 }逻辑说明每次先写完两个半字再写带新 FSEL 的控制字顺序不能反。先写控制字再写频率字会让新的 FSEL 先切到一个旧频率上多输出一帧毛刺反而更明显。dwell 时间由主循环或定时器控制SPI 写一帧只要几十微秒扫频步进可以做到很密。4.2 RESET 的第二个用途扫频起点对齐扫频通常要求相位连续。只要不碰 RESET相位累加器一直在跑相邻两步频率不同但相位是连续延展的这是 DDS 扫频相对锁相环的天然优势。反过来做脉冲激励或想每次扫频从同一相位出发就在启动前写一次CTRL_RESET再清掉波形从 VOUT 中间电平重新拉起。RESET1 期间写频率寄存器是安全的4.1 的乒乓切频如果配合扫描循环可以在每次循环开头做一次复位对齐保证多次扫频的波形完全重叠。4.3 输出级DAC 镜像、直流偏置和 RC 低通VOUT 内部是 10 位 DAC 以 MCLK 为更新率输出频谱上除了目标频率还有 k×MCLK ± fout 的镜像分量。25MHz 更新率下第一镜像在 25MHz 附近直接进比较器或功放会有问题。常见做法是输出串 50~100Ω 再对地接 RC 低通截止频率取目标最高频率的 1.2~2 倍。VOUT 带约 VDD/2 的直流偏置接后级时先用隔直电容再配偏置电阻。很多模块上预留了 0805 焊盘按下面原则补件即可。最高输出频率RC 截止建议R100Ω参考电容值100kHz200kHz ~ 1MHz1.6nF ~ 8nF1MHz2MHz 左右约 800pF10MHz20MHz 左右约 80pF注意电容自谐振频率注意截止频率取得太低会把方波的边沿磨圆。方波走比较器路径如果需要陡峭边沿滤波应加在方波输出之后而不是之前。4.4 hd9833 这类变体型号的注意点资料包里如果出现 hd9833 这种写法通常是 AD9833 的兼容丝印或手册翻译时的笔误寄存器定义和引脚顺序一般直接复用。唯一要实测的是模块上的晶振25.000MHz 和 20.000MHz 丝印不同频率字系数差 25%上电先看晶振丝印再填AD9833_MCLK。个别兼容片对 SPI 建立时间要求更严把 SPI 分频降到 /16 先验证再逐步提速。5. AD9833 波形验证与排错串口频率字回显加示波器定标所有驱动跑完最后一步是验证而且 AD9833 没有回读路径只能靠外部测量。把下面三件事固定成流程十分钟内能定位绝大多数问题。5.1 双通道示波器定标的接法与判据CH1 夹 FSYNC 或 SCLKCH2 夹 VOUT。正常帧的特征是 FSYNC 每 16 个 SCLK 一个低脉冲VOUT 在清 RESET 那一帧之后才从中间电平开始出现波形。频率判据用示波器自带的频率测量和串口打印出来的频率字比对串口打印在 115200 波特率下足够。// 串口调试每次设频后回显频率字与示波器读数互相校验 uint32_t w AD9833_HzToWord(hz); printf(hz%lu FREQREG%lu\r\n, (unsigned long)hz, (unsigned long)w);判断方法示波器实测频率除以打印频率字再乘以AD9833_MCLK就能反推实际晶振。若 25MHz 铭牌的模块测出频率恒低 20%基本是晶振实际为 20MHz把宏改掉即可频率误差在 0.5% 以内说明频率字和 MCLK 假设都成立。5.2 波形异常速查表现象原因定位处理VOUT 恒为 VDD/2 直线RESET 未清或 SLEEP12 置位确认初始化末尾写了清 RESET 的控制字正弦有输出但幅度只有零点几伏正常VOUT 是半幅输出后级加放大不要改寄存器频率是设定值的一半或四分之一OPBITEN/DIV2 状态不对按手册核对 DB10、DB8切频瞬间出现毛刺直接改写了正在输出的 FREQ 寄存器改用 4.1 的乒乓预写偶发丢帧、频率随机跳SPI 时钟相位不对或 FSYNC 挂了硬件 NSS核对 Mode 2FSYNC 改普通 GPIO低速正常、提速后出错字节序或建立时间不足确认 buf 手工组包分频降到 /16 复测5.3 调试器连不上时的排查顺序Keil5 里报error: no stm32 target found时先别怀疑 SWD 配置量一下 AD9833 模块和主控是否共地共电源。模块上若带板载 LDO会把 5V 降到 3.3V和主控 3.3V 之间形成压差串一个 0Ω 电阻或者直接统一供电更省事。这个报错与 DDS 逻辑无关时用 ST-LINK Utility 的 connect 界面看 target voltage 也能快速定位是供电还是连接问题。本文还有配套的精品资源点击获取