很多人第一次拿到AD9102的数据手册时,心里会松一口气:一颗集成了DDS和DAC的芯片,SPI配上就能出波形,看起来比搭分立DAC方案省心多了。等真正把STM32L552和它连在一起才发现,能出波形和出对波形完全是两码事。这篇博客把我用STM32L552驱动AD9102做任意波形发生器的完整过程、完整代码和避坑经历整理出来,希望能给打算在这套组合上做信号源、超声激励源、气体分析仪驱动的硬件工程师省下几个加班的夜晚。
1. 选型复盘:STM32L552配AD9102这套组合,到底适合干什么
1.1 为什么不用MCU查表刷DAC,要选集成DDS的AD9102
做信号发生器,很多人的第一反应是MCU内部查正弦表,然后通过SPI不断刷新外部DAC。这个思路在输出频率几十kHz到一两MHz的时候完全够用,但再往上走就吃力了:MCU的SPI时钟速度、中断响应时间、DAC建立时间、GPIO翻转速率,每一项都在拖后腿。
AD9102这颗芯片很不一样,它把DDS(直接数字频率合成器)和12位、180MSPS的DAC封装在一起,内部还有一个32k采样点的SRAM存储区。做规则波形时,MCU只需要通过SPI把频率调谐字、相位字、增益这些参数写进去,DDS就会持续输出对应频率的信号,波形数据根本不需要MCU实时干预。
这等于说,MCU从"搬运工"变成了"指挥官"。如果只是做一个1MHz正弦波,用普通MCU查表刷DAC也许还行;但要做几MHz、几十MHz的正弦波,或者要做带包络的超声激励脉冲,查表刷DAC的架构基本就废了。AD9102自带的DDS+SRAM混合模式,可以DDS做高频载波、SRAM里的自定义序列做包络,这种能力用纯MCU方案很难实现。
1.2 STM32L552在系统里承担什么角色
STM32L552不是那种一眼看上去很强壮的MCU,110MHz主频放在一众M7、M4里面不算亮眼,但它的定位从来不是拼算力,而是拼能效。Cortex-M33内核,超低功耗系列,待机电流能做到很低,这让它非常适合做便携式仪器、手持超声设备、现场信号源这类对供电敏感的产品。
放在这个项目里,STM32L552的SPI外设用来配置AD9102,GPIO用来控制复位和触发信号,串口接收上位机指令,再加上ADC采集一些反馈电压,一套系统主控的工作就全包了。它的GPIO翻转速度虽然比不上专用逻辑电路,但做控制绰绰有余,甚至后面我会说,连触发信号这种极其讲究时序的活,它用定时器输出也能干得很好。
还有一点,L5系列自带TrustZone安全功能,这个在普通仪器里用不太上,但如果你的产品需要做代码保护、防抄板,这颗芯片天然比普通M0/M3系列多一层保障。对我来讲,选择L552的核心理由是静态功耗足够低,而且一套代码里既要跑显示交互又要跑配置算法,Cortex-M33的处理能力刚好够用。
1.3 整体链路拆解
整个系统的信号链其实是这样的:外部参考晶振或者TCXO产生REFCLK时钟,进入AD9102的时钟引脚,经过内部PLL倍频后得到DAC采样时钟。MCU通过SPI配置AD9102的工作模式、DDS参数或者写入SRAM波形数据,配置完成后,通过触发信号让AD9102开始输出,IOUT+和IOUT-这对差分电流输出经过外部I/V转换和重建滤波器,最后变成示波器上看到的电压波形。
这里有个很多人容易忽略的点:AD9102不是上电就能用的,它的内部寄存器在复位后处于默认状态,PLL也没有锁定,必须由MCU完成一整套初始化流程。也就是说,AD9102功能再强,它也"不会自己跑程序",所有行为都依赖SPI指令,所以你会发现,折腾这颗芯片的时候,多半时间不是耗在电路上,而是耗在SPI时序和寄存器初始化顺序上。
2. AD9102的寄存器体系:波形生成核心逻辑逐一拆解
2.1 三种工作模式先分清
AD9102的工作模式大体可以分成三类:DDS模式、SRAM模式、DDS+SRAM混合模式。
DDS模式适合产生正弦波、三角波这类周期信号。内部相位累加器在每一个DAC采样时钟周期累加一次频率调谐字,查表输出对应幅度值,这个模式下波形连续、相位连续,频率分辨率可以达到很细,因为内部相位累加器是32位的。
SRAM模式则是把任意波形的采样点提前写入片内SRAM,然后从起始地址循环读到终止地址。这种模式没有DDS的频率连续性限制,你可以灌入自己算好的任意波形,比如一个猝发指数衰减脉冲、一个非对称的斜坡信号,都可以。
DDS+SRAM混合模式更有意思,典型应用就是超声成像里的激励脉冲:DDS产生一个高频正弦载波,SRAM里的包络数据作为幅度调制因子,两者相乘或叠加后输出。这个模式下单一芯片就能完成复杂的脉冲激励信号生成,省掉了外部模拟乘法器。选择哪种模式,取决于你的最终波形形态,不要在DDS模式下硬要输出任意波,也别在SRAM模式下追求连续相位变化。
2.2 DDS频率调谐字和相位字怎么配
DDS模式下的输出频率和频率调谐字(FTW)之间是严格的线性关系:
$$f_{out} = \frac{FTW \times f_{DAC}}{2^{32}}$$
反推一下就是:
$$FTW = \frac{f_{out} \times 2^{32}}{f_{DAC}}$$
举个例子,DAC采样时钟180MHz,想生成1MHz正弦波,FTW算出来约23888888,十六进制是0x016C94B8,需要拆成两个16位字写进寄存器。频率调谐字是32位无符号整数,相位控制字是16位的,可以直接设置初始相位动态范围。
这里要特别注意字节拆分顺序。AD9102的频率参数寄存器是分高字、低字分开写的,而且存在影子寄存器更新机制——低字节写入的瞬间才会把整个32位配置真正锁存。也就是说,必须先写高16位,再写低16位,顺序一旦反了,中间会出现一段频率不正确的过渡状态,而这段过渡状态可能正好被你触发输出,最后示波器上看到的波形频率就像抽风一样乱跳。
2.3 SRAM模式的加载流程
SRAM模式的核心流程是:设置SRAM访问模式,把内部地址计数器指到起始地址,然后连续写入16位采样点数据,写完再设置终止地址,最后触发输出。
写SRAM的时候有一个容易踩的bug:SPI连续写几十几百个数据,中间任何一次CS时序不干净,都可能导致一个采样点丢失或者错位。波形前面的点错了你肉眼根本发现不了,直到最后看时域波形突然冒出一个不该有的毛刺。所以我在驱动里加了一个校验策略:写完一批SRAM数据之后,把数据回读出来,和原始缓冲逐点比对。这个操作虽然多花几百微秒,但对批量交付的产品来说,能拦掉大量偶发性问题。
另外,SRAM模式的输出触发不止一种方式。可以软件触发,也可以用外部TRIG引脚硬触发。外部触发对相位的重复性有要求,这一点在后面踩坑部分详细讲。
2.4 DAC输出配置与上电时序
AD9102的输出是电流型差分输出,IOUT+和IOUT-满量程电流由外部Rset电阻和寄存器里的DAC增益位共同决定。Rset越小满量程越大,DAC增益位可以进一步微调。
输出级设计上需要注意,IOUT引脚不能直接拿示波器看波形,因为它输出的是电流,必须在外部接端接电阻或者运放I/V转换电路。如果直接把差分输出接到示波器高阻探头,你会觉得芯片"没输出",其实是负载压根没形成电流回路。
上电时序这块,数据手册明确要求VDDIO先上电、DAC模拟电源后上电,如果你的板子两路电源不是分轨控制,那至少要在软件复位流程里做足文章:上电后等电源稳定,再拉低复位引脚几百微秒,确保芯片在已知状态。有些设计图省事,RESET引脚直接悬空或者只接一个小电容,这样芯片很容易受上电浪涌干扰导致寄存器状态不确定,这点我到第4部分会用实例讲。
3. SPI底层时序与HAL驱动封装细节
3.1 CubeMX配置和物理连接
我用到的硬件连接大概是这几根线:
| 信号 | STM32L552引脚 | AD9102引脚 | 说明 |
|---|---|---|---|
| SCLK | PB13 | SCLK | SPI时钟 |
| SDIO | PB15 | SDIO | 主出从入 |
| SDO | PB14 | SDO | 主入从出,接上方便回读 |
| CS | PB12 | CS | 软件控制片选 |
| RESET | PB10 | RESET | 低电平复位 |
| TRIG | PA2 | TRIG | 硬件触发 |
CubeMX里把SPI2配置为全双工主机模式、8位数据长度、MSB先行。时钟极性和相位我最终用的是CPOL=0、CPHA=1,但你这块板子未必和我的完全一样,最好的办法是拿着数据手册时序图,对照SCLK的采样沿慢慢调,别照抄别人的配置。
CS不要用硬件NSS,用普通GPIO控制就够了。硬件NSS在多从机情况下牵涉到各种时序冲突,软件控制反而简单可靠。SPI时钟速度一开始也不要拉满,我调试阶段用的是4MHz左右,等全链路跑通了再考虑提速。杜邦线连接情况下,高速SPI很容易在信号沿上产生振铃,回读数据就可能错位。
3.2 写寄存器与读回校验
AD9102的SPI写操作是16位指令字加上16位数据。指令字bit15是读写标志,bit12到bit0是寄存器地址,数据字就是你要写入的值。
写寄存器函数的本质就是把这两段16位拼成4个字节,一次性通过HAL_SPI_Transmit发出去。这里有一个很实用的习惯:每次写完之后立即读回,哪怕只读一个字节对比。因为AD9102是SPI从机,你自己写的命令到底有没有被正确接收,没有任何中断引脚告诉你,回读是唯一可靠的确认手段。
我的驱动代码中会带一个自检函数,初始化完成后把所有关键寄存器的值读回来和期望值做比对,不匹配就返回错误码。这样在产线上烧录完程序,不需要额外仪器就能判断芯片和PCB有没有基本问题。
3.3 DDS正弦波配置与触发流程
配置DDS输出正弦波的完整流程是这样的:
- 拉低RESET引脚,保持至少100us,再释放,让芯片回到已知态。
- 配置PLL倍频系数,等待PLL锁定标志位置位。
- 按公式计算频率调谐字,先写高16位。
- 再写低16位,触发影子寄存器更新。
- 写相位字。
- 配置DAC增益和输出模式。
- 发出触发信号(软件或硬件),波形开始输出。
PLL锁定这一步特别重要。芯片刚上电时PLL没锁,内部DAC时钟并不是你预期的180MHz,如果不等锁定就配置DDS并触发,输出频率会和你算的完全对不上,而且带有很大抖动。在代码里加一个等待超时循环,等PLL锁定寄存器位被置为有效,再往下走。
3.4 低功耗唤醒后SPI外设必须重新初始化
如果你用的是STM32L552,看中的就是它的低功耗特性,那系统一定会进入STOP模式或者STANDBY模式。低功耗唤醒后,SPI外设的寄存器状态很可能已经丢失或者被复位,HAL库不会自动帮你恢复全部状态。
我踩过的情形是:系统从STOP2唤醒后,直接调用HAL_SPI_Transmit,返回值正常,但AD9102那边就是没反应,因为SCLK根本没有被正确输出。排查后发现问题出现在GPIO复用配置上没有恢复,SPI外设虽然被HAL库重新初始化了,但对应的PB13、PB15引脚还停留在模拟输入状态,时钟自然过不去。
解决方案很简单:唤醒后把SPI的DeInit和Init全部重新执行一遍,并且按顺序恢复GPIO复用配置,然后再跑AD9102的初始化流程。这件事最好封装成一个函数,操作系统唤醒回调里直接调用,别指望HAL库帮你兜底。
4. 实战踩坑记录:四条完整的排查链路
4.1 寄存器写不进去,最后发现是RESET引脚悬空
最开始调试时,板子焊好,程序烧进去,读回任何寄存器都是0,写进去也像石沉大海。SPI波形用示波器抓了,CS、SCLK、SDIO电平都对,电源电压也对,VDDIO和AVDD都正常,芯片表面温度也没有异常。
当时我排查了一整夜,最后无意中摸到RESET引脚,发现3.3V电平,看起来不像是低电平复位状态。但问题是从楼上接了一根飞线到地,碰一下RESET引脚,芯片就恢复了,说明它在正常工作,只是不知道什么时候被干扰拉低又自动释放。
再看原理图才发现RESET上面只画了一个0.1uF电容,没有任何电阻。低电平有效且悬空,这在强电磁环境或者上电瞬间是致命的,轻微噪声就可能让芯片不断复位,SPI当然写不进任何寄存器。后面我改为RESET引脚接10k上拉到VDDIO,并且在软件初始化时先拉低200us再释放,这个问题彻底消失。
这条排查链路给我最大的教训是:芯片"看起来没复位"不代表复位电路可靠,凡是低有效复位引脚必须要有明确的上拉或者驱动源,不能靠芯片内部上拉电。
4.2 读回一直是0xFF,SPI极性和相位差了一级
寄存器能写但读回全是0xFF,这个问题和上一个正好相反。初始化流程跑完,VDDIO正常,CS时序正常,写操作之后马上发读指令,结果SDO线回传的一直是0xFF。
一开始我怀疑SDO引脚没焊好,量了通路没问题,用示波器去抓SDO线上的数据,发现确实有翻转信号,但MCU采样到的全是1。这时候才意识到,是SPI的时钟极性和相位配置和AD9102输出数据的时序差了一拍。SCLK默认空闲电平不一样、采样沿位置不一样,都会导致发送端和接收端错开半个周期。
我把SCLK改为另外一种CPOL/CPHA组合后,回读值立即正常。这里不要试图硬背哪个模式是对的,不同批次、不同封装的器件手册描述可能有细微差异,正确做法是:先看手册时序图,确定数据是在SCLK上升沿还是下降沿更新、在哪个沿被主机采样,再去CubeMX里把对应的CPOL和CPHA设好。
4.3 正弦波频率偏了,败在影子寄存器的写入顺序
现象是DDS已经配置好,寄存器回读确认没有错误,但示波器上输出正弦波的频率和计算值差了接近10倍。一开始怀疑DAC采样时钟不对,回头检查PLL配置寄存器,发现PLL倍频系数确实被覆盖过。
继续深挖发现,真正的原因是初始化顺序里先写了低16位频率字、再写高16位频率字。AD9102某些寄存器的更新是在低字写入时触发的,低字先写就会导致一次不完整的32位配置被锁存,高字后面再写虽然能修正最终值,但中间状态已经影响了PLL或其他相关配置。
改正方法一句话就能讲明白:凡是分高字和低字的寄存器,永远先写高字,最后写低字。低字写入时一次性更新整个32位配置,保证中间不会出现半个字的错误值。类似这种影子寄存器机制,在ADI的各种DDS芯片里很常见,写代码前先看寄存器描述里有没有说明更新触发条件,比踩完坑再去猜高效得多。
4.4 波形起点抖动,别再用GPIO翻转做触发源
波形本身稳定,周期正确,幅度正确,但每次一按触发按钮,波形在示波器上的水平位置都会微微晃动,虽然只有几ns级别,但对于要做相位相关测量的场景来说,这个抖动完全不能忍。
排查的时候一度怀疑AD9102内部的触发边沿检测不稳定,后来用定时器直接输出PWM触发信号,抖动瞬间消失。真正的问题出在MCU的GPIO翻转上,我用HAL_GPIO_TogglePin翻转一个引脚作为触发脉冲,这个函数调用本身要经过函数跳转、寄存器读写,时间不确定,再加上中断、总线调度的干扰,触发沿相对AD9102的DAC时钟来说,相位是随机浮动的。
AD9102的触发边沿和单片机GPIO翻转的抖动叠加以后,脉冲出现的时刻不在固定的相位点上,波形出来就表现为水平抖动。正确做法是用定时器PWM产生干净的单脉冲,经过一个上升沿送给TRIG引脚,这样触发边沿相对DAC采样时钟有了确定性,相位稳定度立刻提升。
5. 完整驱动代码与PCB布局要点
5.1 驱动代码组织
我的驱动代码分成ad9102.h和ad9102.c两个文件,核心逻辑围绕寄存器读写、初始化、DDS配置、SRAM加载来组织。具体寄存器地址请以你手上的数据手册为准,下面代码里的地址是示例逻辑,不要把地址序号当成通用答案直接抄,一定要先对一遍手册。
ad9102.h主要定义引脚宏、寄存器地址和对外接口函数原型。ad9102.c里实现底层SPI通信、复位、初始化、DDS正弦波配置、SRAM写入、软件触发等函数。
5.2 关键源码
先看寄存器读写函数。写操作必须遵守"指令字+数据字拼成4字节连发"的规则,中间CS绝对不能抬起。
uint8_t AD9102_WriteReg(uint16_t reg, uint16_t val) { uint8_t buf[4]; /* 指令字:bit15 = 0 表示写,bit12~bit0 为寄存器地址 */ buf[0] = (uint8_t)((reg & 0x1FFF) >> 8); buf[1] = (uint8_t)(reg & 0xFF); buf[2] = (uint8_t)(val >> 8); buf[3] = (uint8_t)(val & 0xFF); AD9102_CS_LOW(); HAL_SPI_Transmit(&hspi2, buf, 4, 10); AD9102_CS_HIGH(); return 0; }读回函数逻辑类似,注意要先把SDIO切换到输入方向,或者像我这样接四线SDO,才能读到数据:
uint8_t AD9102_ReadReg(uint16_t reg, uint16_t *val) { uint8_t cmd[2], rx[2]; /* bit15 = 1 表示读 */ cmd[0] = (uint8_t)((0x8000 | (reg & 0x1FFF)) >> 8); cmd[1] = (uint8_t)(reg & 0xFF); AD9102_CS_LOW(); HAL_SPI_Transmit(&hspi2, cmd, 2, 10); HAL_SPI_Receive(&hspi2, rx, 2, 10); AD9102_CS_HIGH(); *val = ((uint16_t)rx[0] << 8) | rx[1]; return 0; }DDS正弦波配置函数,重点看FTW计算和先高后低的写入顺序:
uint8_t AD9102_ConfigDDS(uint32_t freq, uint32_t dac_clk, uint16_t phase) { uint32_t ftw; if (dac_clk == 0) { return 1; } ftw = (uint32_t)(((uint64_t)freq << 32) / dac_clk); /* 先写高16位 */ AD9102_WriteReg(AD9102_REG_DDS_TUNEWORD0, (uint16_t)(ftw >> 16)); /* 低16位最后写,触发更新 */ AD9102_WriteReg(AD9102_REG_DDS_TUNEWORD1, (uint16_t)(ftw & 0xFFFF)); AD9102_WriteReg(AD9102_REG_DDS_PHASE0, (uint16_t)(phase >> 16)); AD9102_WriteReg(AD9102_REG_DDS_PHASE1, (uint16_t)(phase & 0xFFFF)); /* 切换到DDS模式并启动 */ AD9102_WriteReg(AD9102_REG_MODE, 0x0001); return 0; }这段代码里有几处需要核对你手册的地方:TUNEWORD0、TUNEWORD1、PHASE0、PHASE1、MODE这些寄存器的实际地址编号,以及模式字里使能DDS的位定义。寄存器地址以手册为准,但写高在先、写低在后的原则是通用的。
5.3 PCB布局经验
硬件调试能顺不顺,往往在画板阶段就决定了。SPI的SCLK、SDIO、SDO三根线,在所有走线里要优先保证完整性和短距离,不要在中间穿过其他大电流回路。我在实际板卡上习惯在MCU端的SPI线上串联33R电阻,靠近MCU放置,这个电阻配合走线寄生电容能显著降低信号振铃。
模拟地和数字地不能直接混在一起。AD9102的AVDD、DVDD、VDDIO最好是分开走,再通过0欧电阻或者磁珠在电源入口处单点汇合。DAC输出IOUT+和IOUT-这两根线要尽量短,差分等长,底层参考地挖空干净,周围不要铺乱七八糟的其他信号线。
REFCLK参考时钟的走线更是重点,能用短走线就不要拉长,有条件的话在REFCLK附近加一个小电容到地做滤波。晶振负载电容取值可以让晶体厂商的规格书来定,别自己估算,误差太大会引起频率偏差。
去耦电容方面,每个电源引脚都放一个0.1uF陶瓷电容,位置紧贴芯片引脚,稍大容量的10uF或者22uF电容放在板子电源入口处。别把所有去耦电容堆成一排放在芯片同一侧,那样等于没放。
5.4 调试验证清单
文章最后,把我自己在项目里用的验证清单分享一下,每一条都是在实际调试中总结出来的步骤:
| 步骤 | 操作 | 通过标准 |
|---|---|---|
| 1 | 测量VDDIO、AVDD上电电压 | 电压稳定,无明显跌落 |
| 2 | 回读芯片ID寄存器 | 与手册预期值一致 |
| 3 | 回读全部关键寄存器 | 与写入值完全匹配 |
| 4 | DDS模式输出正弦波 | 频率误差在允许范围内 |
| 5 | SRAM写入后回读比对 | 数据逐点一致 |
| 6 | 硬件触发测试 | 多次触发波形相位无抖动 |
| 7 | 低功耗唤醒后重新初始化 | 再次触发输出正常 |
调试阶段其实最有帮助的还是读回校验这个习惯。AD9102不像串口设备有应答机制,你不主动读,就很难知道SPI命令到底执行没有。每次初始化之后把关键寄存器全部读回来打印出来,很多隐藏问题在波形出现之前就提前暴露了。
如果后面还有人要在这套组合上做多通道同步或者相位连续调频,可以继续在DDS动态重配上下文章。AD9102支持通过SPI在运行中更新频率字,配合定时器精确控制更新时间,就能实现跳频、线性调频这类高级功能。不过这些就需要更精细的时序测试了,等下次有空再单独写一篇展开聊。