
简介面向嵌入式开发者这份驱动代码以STM32F103RCT6的SPI2接口控制AD9833数字波形发生器支持正弦波、三角波、方波三类输出频率可通过修改频率寄存器实现连续可调代码结构清晰接口简单不用额外硬件电路可直接加入基于STM32的项目中适合信号模拟、波形发生、教学实验以及快速原型验证等使用场景。资源包共4个文件包括2个头文件与2个C源文件分别完成SPI外设初始化和AD9833控制逻辑的封装控制部分包含频率设定、波形切换等基础函数压缩包仅6KB资源非常轻量便于阅读理解与二次改造。目前已有4132人学习下载口碑良好。通过这份资源读者可以了解STM32硬件SPI的寄存器配置流程、AD9833的28位频率字计算与写入时序以及软件模块的划分思路代码还预留了波形和频率修改接口方便快速移植到其他STM32型号或改用GPIO模拟SPI能有效缩短嵌入式开发周期。 最近在做一台带扫频功能的小信号发生器主控选的是STM32F103RCT6DDS芯片用AD9833通信走的就是标题里那个SPI2。一开始我也想过用IO口模拟时序去驱动AD9833后来仔细算了算定时器翻转IO的方式在高频输出时CPU占用太难看干脆把SPI2外设用起来。这篇文章就把整个驱动过程完整拆开讲一遍包括为什么选SPI2、AD9833的SPI时序到底有什么猫腻、28位频率字怎么拆成两个16位命令字以及实测过程中我踩过的几个坑。如果你正准备用STM32F103RCT6的SPI2去驱动AD9833或者已经接好了硬件但输出没有波形这篇文章应该能帮你省下不少折腾时间。1. SPI2不是随便选的从外设时钟到引脚分布先说清楚1.1 AD9833需要什么、STM32F103RCT6能提供什么AD9833是ADI出的一款低功耗DDS芯片通过SPI接口接收频率字和相位字可以直接输出正弦波、三角波和方波。它的核心优势在于频率分辨率做到了28位用25MHz的MCLK时频率步进可以做到约0.1Hz调整频率不需要切换电阻电容写几个寄存器就行特别适合做信号发生器、扫频源、PLL参考源之类的应用。STM32F103RCT6这颗芯片自带3个SPI外设标题里提到的SPI2就是其中一个。SPI2挂在APB1总线上APB1最高36MHz对外设来说完全够用。AD9833需要的信号线其实很少SCLK、SDATA、FSYNC再加上一个RESET引脚总共4根控制线。SDATA是数据输入从STM32的MOSI接过去就行MISO根本用不上所以整个接线非常简洁。1.2 为什么不用SPI1以及SPI2的引脚与时钟细节很多人上来就习惯用SPI1但SPI1的默认引脚PA5、PA6、PA7在不少项目里早就被占了。我手头这块板子的SPI1接了LCD和外部Flash再挂AD9833就得做引脚重映射或者飞线不划算。SPI2的默认引脚在PB13、PB14、PB15独立一组拿来专门伺候AD9833再合适不过。信号STM32F103RCT6引脚连接到AD9833说明SPI2_SCKPB13SCLK复用推挽输出SPI2_MOSIPB15SDATA复用推挽输出FSYNCPB12FSYNC普通GPIO输出不用硬件NSSRESETPB8RESET普通GPIO输出MISOPB14不接配置为输入浮空即可这里有个容易被忽略的点PB12本来就是SPI2的NSS引脚但我不建议直接让STM32的硬件NSS去控制FSYNC。原因后面讲时序的时候会说这是AD9833驱动里最大的坑之一。时钟方面SPI1挂APB2最高72MHzSPI2挂APB1最高36MHz。AD9833的串行时钟上限在40MHz左右VCC3.3V时也有余量所以SPI2的36MHz上限不会成为瓶颈。实际使用中我还会把SPI2的波特率预分频配到16分频SCK大概在2MHz级别远低于芯片上限抗干扰和信号完整性都更有保障。另外如果你偶尔想用串口ISP下载程序BOOT0引脚记得按常规画法接一个10k下拉电阻到地这样默认从Flash启动不会莫名其妙进入ISP模式。2. AD9833的SPI时序为什么一堆人在这里翻车2.1 FSYNC不是普通片选一次写入必须跨完整16位边界AD9833的FSYNC低电平有效但它和普通SPI从设备的CS片选不太一样。普通SPI从设备的CS可以在每个字节或每个16位帧之间随意拉高再拉低AD9833也允许FSYNC在16位边界上拉高重新开始但问题在于很多人直接用硬件NSSNSS会在每个16位帧传输结束后自动翻转偏偏AD9833在B28模式下需要连续写入两个16位字才能完成一次28位频率更新中间FSYNC一旦按普通片选的节奏弹起来芯片就会认为是一条新的传输命令频率字就拼不完整。FSYNC真正的规则是拉低之后芯片就知道要开始接收命令字了整个命令字必须完整走满16位如果要连续写两个16位字FSYNC可以保持低电平不跳动这样更接近数据手册里推荐的时序也更好排查问题。所以我干脆把PB12配成普通推挽输出软件控制FSYNC的拉低和拉高彻底绕开硬件NSS的自动翻转行为。2.2 SCK空闲电平、采样边沿与STM32 SPI配置的对应关系AD9833要求SCLK空闲时为高电平并且在SCLK的下降沿锁存SDATA上的数据。对应到STM32的SPI配置就是CPOLHigh、CPHA1Edge。CPOL决定空闲电平CPHA决定数据在第一个边沿还是第二个边沿被采样。这里有读者可能会混淆说CPHA1Edge到底是SPI Mode几。CPOLHigh、CPHA1Edge对应的是SPI Mode 2。如果你把CPOL改成Low、CPHA配成0有时候也能工作那是因为AD9833对采样窗口的要求有一定冗余但这不是可靠的做法尤其在SCK频率拉高、连线变长之后边沿稍有偏差就会偶发出错。我踩过一次比较典型的坑用标准外设库初始化SPI2时SPI_CPOL写成了SPI_CPOL_LowSPI_CPHA保持1Edge结果AD9833偶尔能输出波形偶尔输出一串乱频。逻辑分析仪一看就明白了SCLK下降沿到来时SDATA上的数据还没稳定芯片采到的是毛刺。把CPOL改成High之后问题立刻消失。3. 28位频率字拆成16位命令字寄存器地图与拼接逻辑3.1 控制寄存器各位的含义和初始化顺序AD9833的每个16位命令字最高两位DB15:DB14决定了这条命令写给谁。这是拆包的基础一定要记牢。DB15:DB14作用00写控制寄存器01写频率寄存器0FREQ010写频率寄存器1FREQ111写相位寄存器通常DB13再区分PHASE0或PHASE1控制寄存器里有几个位对初始化至关重要。B28位在DB13把它置1表示28位频率字按两次16位写入这是最省事的模式。RESET位在DB9置1会让芯片进入复位状态清零后开始正常工作。MODE位在DB4:DB3控制输出波形00是正弦波01是三角波10和11是两种方波模式。OPBITEN位在DB6它控制方波输出使能如果你只想输出正弦波这个位必须保持0。我推荐的初始化顺序是先写0x2100到控制寄存器把B28置1、RESET置1让芯片先复位。接着写频率字最后写0x2000把RESET位清零芯片就开始输出了。0x2100和0x2000的差别就在DB9这一位前者是复位状态后者是运行状态。3.2 频率字拆包、相位字写入与切换寄存器频率字计算很简单核心公式是freq_word f_out * 2^28 / MCLK2^28等于268435456。拿25MHz的MCLK举例想输出1kHz正弦波freq_word 1000 * 268435456 / 25000000 ≈ 10737.42取整10737。这个freq_word是一个28位无符号数最高两位不用实际有效位是低28位。拆分时低16位命令字是0x4000加上freq_word的低14位高16位命令字是0x4000加上freq_word的高14位。注意是低14位不是低16位因为AD9833的16位命令字里最高两位被地址占了剩余14位才是数据。那为什么用0x4000作为基地址而不是0x0000因为DB15:DB14必须是010x4000二进制就是0100 0000 0000 0000正好把最高两位设成01FREQ0的低14位数据空间就落在bit13到bit0上。写FREQ1时基地址改成0x8000。相位寄存器是12位写PHASE0用0xC000加上12位相位值写PHASE1通常用0xE000加相位值。如果你需要FSK或PSK调制可以把两个频率或两个相位都预写好之后只改控制寄存器里的FSELECT位和PSELECT位来快速切换不需要反复重写频率字。4. 驱动代码落地SPI2初始化与写寄存器实现4.1 GPIO、SPI2初始化标准外设库版本工程环境基于STM32标准外设库用HAL库的读者对照着改即可原理完全一样。先看GPIO和SPI2的初始化#define AD9833_FSYNC_L() GPIO_ResetBits(GPIOB, GPIO_Pin_12) #define AD9833_FSYNC_H() GPIO_SetBits(GPIOB, GPIO_Pin_12) #define AD9833_RESET_L() GPIO_ResetBits(GPIOB, GPIO_Pin_8) #define AD9833_RESET_H() GPIO_SetBits(GPIOB, GPIO_Pin_8) void AD9833_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE); // SCK PB13、MOSI PB15复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_13 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // MISO PB14浮空输入AD9833不返回数据但要避免引脚悬空 GPIO_InitStructure.GPIO_Pin GPIO_Pin_14; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, GPIO_InitStructure); // FSYNC PB12、RESET PB8普通推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_12 | GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); AD9833_FSYNC_H(); AD9833_RESET_H(); SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_16b; SPI_InitStructure.SPI_CPOL SPI_CPOL_High; SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_16; SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial 7; SPI_Init(SPI2, SPI_InitStructure); SPI_NSSInternalSoftwareConfig(SPI2, SPI_NSSInternalSoft_Set); SPI_Cmd(SPI2, ENABLE); }这段代码里有几个细节值得单独说明。第一SPI_Direction我用的是双线全双工虽然MISO完全没用到但这样配置最简单不容易出问题。第二SPI_NSS设置为Software之后还调用了SPI_NSSInternalSoftwareConfig把SSI位置1这一步非常关键。STM32主模式下如果NSS配置为软件模式但SSI位保持0SPI外设可能无法正常产生时钟输出很多人SPI没有波形就是漏了这一步。第三GPIO速度我配到50MHz对2MHz级别的SCK来说富余量充足不至于因为IO翻转速率不够导致边沿变缓。4.2 写寄存器函数与连续32位写入的细节写完初始化接下来是核心的写寄存器函数。AD9833一次完整的事务最少是16位B28模式下更新一个完整频率寄存器需要32位连续传输。void AD9833_Write16(uint16_t data) { while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI2, data); while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_BSY) SET); } void AD9833_WriteCmd(uint16_t data) { AD9833_FSYNC_L(); AD9833_Write16(data); AD9833_FSYNC_H(); } void AD9833_WriteFreq(uint8_t reg, uint32_t freq_word) { uint16_t base (reg 0) ? 0x4000 : 0x8000; AD9833_FSYNC_L(); AD9833_Write16(base | (uint16_t)(freq_word 0x3FFF)); AD9833_Write16(base | (uint16_t)((freq_word 14) 0x3FFF)); AD9833_FSYNC_H(); }这里尤其注意AD9833_WriteFreq函数FSYNC在整个32位传输过程中始终保持低电平直到两个16位命令字都通过BSY标志位验证发送完毕才把FSYNC拉高。这样既符合AD9833数据手册里B28模式的推荐时序也避免了两条16位命令中间FSYNC抖动带来的错位风险。BSY标志位的等待不能省。如果发完第一个16位就立刻写第二个SPI外设忙不过来数据就会丢。标准库的BSY标志位会在移位寄存器完成最后一个位传输后自动清零所以等它是最稳妥的。驱动的初始化调用顺序如下AD9833_Config(); AD9833_WriteCmd(0x2100); // B281, RESET1 AD9833_WriteFreq(0, 10737); // 写入1kHz正弦波频率字 AD9833_WriteCmd(0x2000); // B281, RESET0开始输出写完之后在AD9833的VOUT引脚上应该能看到1kHz正弦波信号。如果用的是HAL库只需把SPI_InitStructure换成HAL_SPI_Init把等待标志位的操作换成HAL_SPI_Transmit核心的FSYNC控制逻辑和频率字拼接不变。5. 实测排错从逻辑分析仪看波形逐步定位的完整链路5.1 现象一完全没有信号输出这时候千万先别怀疑代码把硬件和协议一层层剥开看。第一步量AD9833的供电。VCC必须在2.3V到5.5V之间推荐3.3V。注意AGND和DGND要共地CAP/2.5V引脚必须对地接一个0.1uF退耦电容很多DDS芯片无输出就是这个小电容没焊或者焊错位置。第二步查MCLK。AD9833需要外部时钟源我用的25MHz有源晶振接MCLK引脚。用示波器探头检查晶振输出引脚如果看不到25MHz振荡信号芯片自然什么都干不了。第三步用逻辑分析仪同时抓SCK、SDATA和FSYNC三条线。先把SPI初始化代码跑起来观察SCK是否有时钟输出。如果SCK完全平直问题大概率出在SPI外设本身检查RCC_APB1Periph_SPI2有没有开检查SSI位有没有置1检查PB13和PB15的复用模式配对了没有。如果SCK有波形但AD9833没输出再看SDATA上的数据是不是完整的16位帧。逻辑分析仪解码SPI时注意把CPOL设成High否则解出来的数据全是反的。重点确认FSYNC低电平期间SCK正好数了16个完整边沿多一个少一个都会出问题。最后一步确认控制寄存器的RESET位真正被清零了。AD9833上电后内部处于复位状态如果初始化时只写了0x2100忘了写0x2000芯片就会一直待在复位状态什么波形都没有。5.2 现象二输出频率不对或者波形完全不对有输出但频率不对这类问题多数出在软件计算和写入顺序上。频率字计算必须以实际MCLK频率为准。我调试的时候犯过一个低级错误手里的有源晶振标签写的是25MHz实测其实是24MHz结果输出频率一直偏差4%后来才想起来用频率计实测MCLK再算频率字。写入顺序也很重要。AD9833在B28模式下必须先写低16位再写高16位。如果先写高后写低频率值会变得非常离谱。检查AD9833_WriteFreq函数里的两句调用顺序就知道了。如果输出了三角波或者方波而不是正弦波重点检查控制寄存器里的MODE位和OPBITEN位。MODE必须为00OPBITEN必须为0。有些人为了方便直接把0x2000和0x2020混写0x2020的DB5被置1等于把输出切到了方波分频模式波形自然就不对了。5.3 现象三SPI2配置正确但SCK依然没有输出这个坑我在前面提过但值得单独拿出来说。SPI_NSS配成Soft之后必须调用SPI_NSSInternalSoftwareConfig把内部NSS信号拉高。STM32主模式下如果内部NSS信号不是高电平SPI外设在发送数据时根本不会进入主模式状态SCK就一直保持空闲电平。另外如果你图省事把SPI_Direction配成SPI_Direction_1Line_Tx必须同步设置SPI_BiDirectionalMode的BIDIOE位否则数据方向没有指向输出MOSI始终是输入状态。与其折腾单线模式不如老老实实用2Lines_FullDuplexMISO悬空也不会影响工作。6. 进阶玩法扫频输出、双频切换和信号链滤波6.1 用FREQ0/FREQ1实现快速双频切换AD9833有两个独立的28位频率寄存器分别在地址01和10。这两个寄存器可以在初始化时全部预写好之后运行时只需要改控制寄存器里的FSELECT位就能在两种频率之间瞬间切换不需要重新写频率字。这个特性非常适合做FSK调制或者双音多频信号源。举例来说把FREQ0配成1kHzFREQ1配成10kHz然后用一个定时器中断每100ms翻转一次FSELECT位输出端就能得到交替变化的双频信号。切换速度只取决于写一次控制寄存器的时间比逐位重算频率字快得多。同样的道理也适用于PHASE0和PHASE1PSELEC位可以快速切换相位做PSK实验很方便。6.2 输出幅度、偏置和后级滤波AD9833的VOUT引脚输出的是带有直流偏置的信号偏置大约在VCC/2附近而且芯片的输出负载能力很弱。直接把VOUT接到示波器能看到波形但一旦后级接了低阻抗负载幅度立刻被拉垮波形还会变形。实际项目里我的习惯是在VOUT后面加一级高输入阻抗的运放缓冲比如AD8606或者OPA2350运放输出端串一个隔直电容再去驱动下一级。如果做扫频源或者信号发生器隔直电容后面最好再接一个低通滤波器滤掉DDS输出端的采样时钟毛刺和谐波分量。还有一点要注意AD9833的理论输出上限是MCLK的一半也就是25MHz晶振下最高可以输出约12.5MHz但实际输出幅度会随着频率升高明显下滑。在MCLK的四分之一以内使用波形幅度和失真度才比较理想。所以我一般把这种方案定位在几kHz到几MHz的中低频信号源超过这个范围就该考虑换更高性能的DDS或者加入自动增益控制电路了。这套驱动从初始化到频率字拆分再到实测排错整体链路并不复杂但每个环节都有值得留神的细节。尤其是FSYNC的时序处理和SPI2内部NSS的软件配置这两个地方绕过去了后面的调试会顺畅很多。本文还有配套的精品资源点击获取