ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于STM32F103的AD9106四通道DDS驱动设计与调试

基于STM32F103的AD9106四通道DDS驱动设计与调试 简介面向嵌入式开发者和电子工程技术人员这套基于STM32F103VCT6微控制器与AD9106直接数字合成芯片的完整驱动工程提供了从底层初始化到上层波形输出的全套实现方案。通过编程可灵活产生上锯齿波、下锯齿波、三角波、正弦波、直流输出、噪声以及自定义任意波形适合用于信号发生器产品的快速原型开发也适合DDS技术与STM32应用的学习研究。工程文件总计75个其中以33个C源文件与35个头文件为核心覆盖主程序、外设驱动、AD9106寄存器配置及波形生成等模块同时包含Keil工程文件、启动文件、编译生成的HEX固件和调试配置压缩包整体约292KB目录按照CORE、SYSTEM、HARDWARE、STM32F10x_FWLib等模块清晰组织便于定位与复用。资源内部对初始化流程、AD9106参数配置、波形数据生成、串口及按键交互、异常处理均有相应代码实现可直接编译烧录并根据实际需求二次修改。目前已有170人学习下载尤其适合需要快速搭建DDS信号源或深入理解嵌入式驱动开发的工程师与爱好者。 搞信号发生类项目的人很多都卡在“单板四路可调波形”这个需求上。若只堆 AD9834 这类单通道 DDS要么拼板要么拼同步调试时间翻倍用 FPGA 又太重。我最后选了 AD9106这颗四通道 12 位 DDS 芯片内部自带 DDS 内核和波形 RAM配一颗 STM32F103VCT6 就能完成整机控制。下面把整个驱动工程的实现思路、寄存器访问顺序和排错过程完整写出来给正在做同类信号源模块的朋友当参考。1. 为什么选 AD9106四通道 DDS 加波形 RAM 的组合很能打先说到底解决什么问题。常见单通道 DDS 做多路输出时一般采取“多片并联”方案但这会带来两个麻烦一是每片需要单独配置SPI 时序一旦交错就会出现相位错位二是多片之间的基准时钟如果不完全同源输出频率会存在固定偏差不要说做相控阵激励就连做两路立体声调频都能听出拍频。AD9106 把四路 DAC 放在同一颗芯片里共享同一个 DDS 内核和同一份参考时钟输出之间的相位关系天然是确定的这是一个非常大的优势。AD9106 内部结构可以简单分成三块DDS 正弦/三角波发生器、1024×16bit 波形 RAM、四路 12 位电流型 DAC。DDS 模式适合连续正弦波、三角波、方波这类规则波形频率由 24 位调谐字控制波形 RAM 模式适合任意波形你可以把任意一段不超过 1024 点的序列灌进去芯片自己循环播放。这两种模式还能混合使用比如用 DDS 产生载波、再用波形 RAM 数据做幅度调制所以它不只是一颗“高级正弦波发生器”更像一个微型任意波形合成器。选 STM32F103VCT6 的理由很直接100 脚封装、256KB FlashSPI1 正好可以把 PA5/PA6/PA7 用起来资源足够又不会过剩。AD9106 的 SPI 支持到 40MHzF1 的 SPI1 最高 18MHz写寄存器完全不是瓶颈。整个项目最难的部分不在 F1 侧而在 AD9106 的初始化顺序和寄存器位域理解上。把寄存器访问顺序理清楚代码写起来其实非常机械。2. 硬件连接与上电时序VCT6 引脚规划比想象中更需要谨慎2.1 引脚分配和最小系统接线我用的接法如下如果你自己画板可以照抄SPI1_SCKPA5接 AD9106 SCLKSPI1_MOSIPA7接 AD9106 SDINSPI1_MISOPA6接 AD9106 SDOUT读回调试用SPI1_CSPA4接 AD9106 CSB复位控制PB0接 AD9106 RESET触发控制PB1接 AD9106 TRIGGER多片同步或按需输出时用AD9106 的 SDOUT 其实可以省掉如果不做寄存器读回校验数据手册允许把 SDOUT 悬空SPI_CONFIG 里相应的 SDO 位也可以不使能。但我的建议是务必接上实际调试时你总会有“寄存器到底写进去没有”的疑问读回校验是最快的排查手段。AD9106 输出是差分电流型IOUTP 和 IOUTN 需要跨接负载电阻或者经过 I/V 变换电路。最简单的调试做法是在两个输出引脚之间跨一个 100Ω 电阻用示波器直接测两端差分信号注意这样带宽和幅度都会打折只能用来验证功能。正式做产品还是建议接运放或巴伦做单端转换并且走线尽量短避免寄生电容影响高频输出。2.2 参考时钟与复位时序AD9106 的 DAC 时钟决定输出采样率直接关系到 DDS 最高输出频率和波形 RAM 播放速度。参考时钟可以从 STM32 的 MCO 引脚输出我不太建议这么做因为 MCO 输出的时钟抖动比较大会影响输出信号的相噪如果只是测功能倒是无所谓。稳妥的做法是板上一颗独立的有源晶振或 TCXO25MHz 到 125MHz 都能用视你想要的最高输出频率而定。DDS 模式下输出频率不能超过 DAC 采样率的一半实际使用建议控制在四分之一以下保证重建波形带外杂散可控。上电时序是新手最容易忽略的地方。AD9106 需要一个电源稳定后的硬件复位推荐流程是先给芯片供电等待电源稳定后把 RESET 引脚拉高再拉低或者拉低再拉高具体极性看你的板子有没有反相处理总之要让 RESET 引脚产生一个完整的复位脉冲。复位完成后再等至少 100μs让内部状态机稳定然后才开始写 SPI 配置寄存器。如果上电后直接写寄存器大概率第一次写入会丢后续配置全乱。3. AD9106 寄存器访问机制SPI 帧格式和初始化顺序3.1 16 位指令帧与数据帧AD9106 的 SPI 写操作分成两个 16 位段第一段是指令字最高位 bit15 为 0 表示写操作低 14 位是寄存器地址第二段是该寄存器要写入的数据。读操作同理指令字最高位为 1然后芯片会在后续 SCK 周期里把数据从 SDOUT 移出。也就是说对于任意一次写寄存器你至少要发送两个 16 位。因为 STM32F103 的硬件 SPI 可以配置成 16 位数据帧模式所以我直接让 SPI 按 16 位模式工作。指令字构造如下#define AD9106_READ_CMD_MASK 0x8000 static uint16_t make_cmd(uint16_t reg_addr, uint16_t rw_flag) { return (uint16_t)(rw_flag | ((reg_addr 0x7FFF) 1)); }这里把寄存器地址左移一位是因为 AD9106 指令字里 bit0 是保留位或者 NOP 位实际地址从 bit1 开始对齐。如果你用的芯片批次或者内核版本不同最好是先读一次官方数据手册里的 SPI 时序图确认否则写寄存器会全部错位。我最初没注意这个细节导致读回寄存器一直是 0xFFFF折腾了几个小时才发现是地址移位算错了。3.2 DDS 调谐字与输出频率计算DDS 模式下输出频率由 24 位调谐字 FTW 决定计算公式是f_out (FTW × f_CLK) / 2^24举个例子参考时钟 25MHz想输出 1MHzFTW (1000000 × 16777216) / 25000000 671088对应十六进制是 0x0A3D70高 8 位写入 0x04 寄存器低 16 位写入 0x05 寄存器最后写 0x06 寄存器触发更新。这里有一个常见误区FTW 是 24 位不是 16 位。我第一次按 16 位来算输出频率和期望值差了四倍看示波器上的频率怎么都对不上。3.3 初始化顺序的状态机不同项目里 AD9106 的初始化顺序可以略有差异但我验证下来最稳的流程是硬件复位等待稳定写 SPI_CONFIG0x00关闭不需要的特殊功能进入标准 4 线 SPI 模式写 DAC_IOUT0x03配置输出电流范围写 DAC_CONFIG0x02配置 DAC 通道使能和输出极性写 DDS_CONFIG0x01选择 DDS 正弦、三角波或者波形 RAM 模式写 DDS 调谐字、相位寄存器写 DDS_UPDATE0x06让 DDS 内核加载新配置如果是波形 RAM 模式继续加载波形数据并触发输出这个顺序的核心逻辑是先保证数字接口可靠再保证 DAC 模拟输出有正确的工作点最后才启动 DDS/RAM 播放。如果把第 5 步提前DDS 内核可能在一个未定义的状态下跑起来后面再改模式还会引入额外的毛刺。4. 工程代码拆解从 SPI 底层读写到波形生成4.1 底层 SPI 初始化和寄存器读写工程使用 STM32F10x 标准外设库驱动文件分成 ad9106.h 和 ad9106.c 两个文件。SPI1 初始化这里不展开全部代码关键配置如下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_Low; SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_4; SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE);这里 CPOL/CPHA 我用了模式 0也就是 CPOLLow、CPHA1Edge。因为我手里的 AD9106 时序图对 DIN 采样沿是这么定义的。如果你发现读回全是错的可以尝试把 CPHA 换成 2Edge也就是模式 1这是排查 SPI 极性问题最有效的两个切换点。寄存器读写函数的核心逻辑如下void AD9106_WriteReg(uint16_t addr, uint16_t data) { GPIO_ResetBits(AD9106_CS_PORT, AD9106_CS_PIN); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_SendData16(SPI1, make_cmd(addr, 0x0000)); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); SPI_I2S_ReceiveData(SPI1); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_SendData16(SPI1, data); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); SPI_I2S_ReceiveData(SPI1); GPIO_SetBits(AD9106_CS_PORT, AD9106_CS_PIN); } uint16_t AD9106_ReadReg(uint16_t addr) { uint16_t val 0; GPIO_ResetBits(AD9106_CS_PORT, AD9106_CS_PIN); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_SendData16(SPI1, make_cmd(addr, AD9106_READ_CMD_MASK)); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); (void)SPI_I2S_ReceiveData(SPI1); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_SendData16(SPI1, 0x0000); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); val SPI_I2S_ReceiveData(SPI1); GPIO_SetBits(AD9106_CS_PORT, AD9106_CS_PIN); return val; }这段代码有一个细节值得注意每次发送完数据后都要读一次 RXNE 来清掉 SPI 接收缓冲否则第二次发送前 RXNE 一直有数据会导致发送卡死在 TXE 等待里。这是我早期用库函数写驱动时经常踩的坑。4.2 DDS 模式下的正弦波输出配置DDS 模式初始化函数我封装成了下面这样输入目标频率和系统时钟频率函数内部把 FTW 算好并写入寄存器void AD9106_Init_DDS(uint32_t f_out, uint32_t f_clk) { uint32_t ftw (uint64_t)f_out * (1UL 24) / f_clk; AD9106_WriteReg(0x00, 0x0000); // SPI_CONFIG AD9106_WriteReg(0x03, 0x0000); // DAC_IOUT默认电流范围 AD9106_WriteReg(0x02, 0x0000); // DAC_CONFIG使能四路输出 AD9106_WriteReg(0x01, 0x0001); // DDS_CONFIG选择正弦输出模式 AD9106_WriteReg(0x04, (ftw 16) 0xFF); // DDS_TUNE_WORD_MSB AD9106_WriteReg(0x05, ftw 0xFFFF); // DDS_TUNE_WORD_LSB AD9106_WriteReg(0x0B, 0x0000); // DDS_PHASE初相为 0 AD9106_WriteReg(0x06, 0x0001); // DDS_UPDATE加载配置 }注释里的寄存器地址对应关系是基于我的参考手册不同子版本可能有细微差异实际移植时请对照你的数据手册逐项核对。这里尤其要注意 0x01 寄存器的 bit 定义它既管 DDS 模式还是波形 RAM 模式也管波形类型是正弦还是三角波。如果初始化后输出的是三角波而不是正弦波多半是这一位的定义没核对好。4.3 波形 RAM 上传与循环输出任意波形模式需要对 AD9106 内部波形 RAM 做批量写入。普通寄存器写一次只能写一个 16 位数据但波形 RAM 支持连续写入模式把要写的数据依次写入 DLOAD 寄存器每写一次内部地址自动加一。我封装了下面的函数void AD9106_LoadWaveform(uint16_t *sample_buf, uint16_t sample_len) { AD9106_WriteReg(0x0D, sample_len - 1); // PATRAM_BURST突发长度 AD9106_WriteReg(0x0C, 0x0001); // PATRAM_TIMES循环次数 // 连续写入波形数据 for (uint16_t i 0; i sample_len; i) { AD9106_WriteReg(0x0E, sample_buf[i]); // PATRAM_DLOAD } // 切到波形 RAM 模式并触发输出 AD9106_WriteReg(0x01, 0x0003); AD9106_WriteReg(0x06, 0x0001); }注意 sample_len 不能超过 1024AD9106 波形 RAM 深度就是 1024 点。如果超过后面写入的数据会把前面的覆盖掉。sample_buf 里的 16 位数据直接对应 DAC 输出码值AD9106 是 12 位 DAC所以有效数据只占低 12 位高 4 位要填 0。生成正弦波样本时用标准的查表法即可for (uint16_t i 0; i 1024; i) { uint32_t s 2047 2047 * sin(2 * PI * i / 1024); sample_buf[i] (uint16_t)(s 0x0FFF); }这段代码把正弦波输出偏置在满量程中点幅度为正负满量程的 1/2适合直接用示波器观察。放在实际项目里如果不想带直流偏置可以改成偶数个样本并对成分布从 IOUTN 端取差分信号把直流分量抵消掉。5. 实测信号与故障排查从波形对不上到完全静默5.1 寄存器读回校验是第一步驱动写完不要急着看输出波形先做寄存器读回。用一个已知的寄存器地址写入已知值再读回来比对能快速确认 SPI 底层是否通。我常用 0x0B 这个相位寄存器做测试AD9106_WriteReg(0x0B, 0x5A5A); uint16_t check AD9106_ReadReg(0x0B);如果 check 读出来不是 0x5A5A问题大概率在 SPI 模式或指令字移位上。这时候先用示波器抓 CS、SCK、MOSI 三个引脚的时序确认 CS 低电平时间是否覆盖了完整的指令字加数据字。我遇到过一次 CS 由另一个定时器控制导致数据发完后 CS 提前拉高AD9106 把这次传输判定为无效读回自然不对。5.2 输出频率不对先查调谐字宽度和时钟频率输出频率和期望值差好几倍时我一般按下面顺序排查确认 FTW 是按 24 位算的很多参考代码里写的是 16 位这一错就是 256 倍。确认参考时钟 f_CLK 和实际板子上的晶振频率一致比如你预期 25MHz 实际贴了 24MHz差别虽然不大但仪器精度够高时能看出来。检查 0x04 和 0x05 两个寄存器写入顺序必须先写高 8 位再写低 16 位反过来运行时会出现瞬间错误的频率毛刺。5.3 输出静默检查 DAC 电流负载和使能位AD9106 是电流输出如果 IOUTP/IOUTN 悬空示波器什么都看不到。先把负载电阻焊上再用万用表测一下 IOUTP 对 GND 的电压正常应该有几十毫伏到几百毫伏的直流偏置如果没有说明 DAC 输出级可能没上电或者 DAC_CONFIG 里的通道使能位没打开。DAC_CONFIG 的通道使能位是易错点。有些参考代码默认四个通道全开但芯片复位后通道是关的必须显式写使能。我调试时遇到过一次只有第一通道有输出、其余三路全静默的情况后来看了寄存器定义才发现第二到第四通道的使能位不在同一个字节里需要单独置位。5.4 波形 RAM 波形被截断或畸变波形 RAM 模式下如果输出的波形严重畸变一般是两个原因一是样本数据高 4 位没有清零12 位 DAC 只认低 12 位高 4 位带了垃圾数据二是突发长度配置不对导致芯片在波形中间回绕看起来像被截断。把 sample_len-1 写入 0x0D 寄存器这个细节容易被写成 sample_len芯片就会多播一个点虽然单次看不出问题高频播放时会出现周期性的毛刺。6. 工程层面的扩展建议和调试心得这个工程做完之后我又做了两个小扩展都很实用。第一个是多片 AD9106 同步共享同一根 SPI 总线和同一个触发信号每片单独一条 CS 线初始化时先把四片全部配置好最后统一发触发这样多片之间的输出相位差能做到非常小。第二个是利用 F103 的定时器 PWM 输出接到 AD9106 的 TRIGGER 引脚实现脉冲式激励比如每来一个触发信号就播放一段指定长度的波形这种模式在超声检测和雷达模拟里很好用。PCB 布局上要给 AD9106 的电源引脚加足够多的去耦电容模拟电源和数字电源尽量单点相连。我最早一版板子把去耦电容放得太远结果 10MHz 以上输出波形噪声明显偏大后来把 100nF 电容贴到芯片电源引脚旁边波形干净了很多。软件开发上有两点体会。一是寄存器地址和位域定义建议通过宏统一管理不要直接散落在函数里写魔法数AD9106 寄存器多项目一复杂就会改漏。二是把 SPI 底层读写和上层波形控制拆开比如 ad9106.h 里只暴露 Init、SetFrequency、LoadWaveform、Trigger 这几个接口遇到不同项目替换底层 SPI 到其他 MCU 时上层逻辑基本不用动。最后说一个可能很多人会忽略的小技巧在波形 RAM 模式里混合 DDS 输出时不同模式切换后最好重新触发一次 DDS_UPDATE否则芯片可能保留上一次模式的输出状态导致切模式后几微秒内出现一段错误的模拟电平。这个现象在示波器上很难抓到但如果你后面接了功率放大级这几微秒的毛刺足够让后端保护电路误动作。加上一次空更新寄存器操作成本极低换来的是输出切换过程干净很多。本文还有配套的精品资源点击获取
返回列表