资讯详情

AD9106四通道DDS信号源驱动开发:从SPI初始化到波形输出实战

📅 2026/10/13 1:42:28 | 华诺云谱 👁 阅读
AD9106四通道DDS信号源驱动开发:从SPI初始化到波形输出实战
简介面向STM32F103VCT6微控制器与AD9106数字合成芯片的完整驱动工程适用于生成正弦波、三角波、上下锯齿波、直流输出、噪声及自定义波形的嵌入式开发者或测试平台。工程基于标准外设库模块划分清晰含初始化、寄存器配置、波形计算发送、串口按键交互及异常处理可直接编译验证。压缩包共75个文件以33个C源文件和35个头文件为主辅以Keil工程文件、启动文件、hex固件及JLink配置整体大小292KB目录按硬件、系统、用户等区域组织便于查阅。已有170人学习适合电子爱好者、学生及工程师快速上手DDS信号发生器设计。研读代码可掌握SPI时序、波形存储装载及多输出模式切换流程为工业自动化、通信、医学仪器等领域的信号生成提供可复用工程样例显著缩短开发周期并降低入门门槛。1. 第一批板子拿到手我花了三天才让AD9106输出一个正弦波如果你手里同时有STM32F103VCT6和AD9106那你大概率不是想做一块普通DAC而是想要一个四通道、频率可实时改、还能放任意波形的信号源。AD9106是一颗四通道DDS芯片内置12位DACSPI配置输出频率和波形类型都由寄存器决定。常见的用途是便携信号发生器、激光振镜扫描、生物电刺激波形甚至给下一级电路做频标。很多人以为难点在SPI能不能通实际上SPI从机模型很简单真正让人卡住的是寄存器初始化顺序、SRAM波形加载格式以及输出端被各种隐藏状态位卡住。我最初照着“先用DDS模式出个正弦”的经验去写结果读回寄存器全是0xFF输出却一直停在直流电平上。后来把数据手册的寄存器映射表摊开逐位对照改了四版代码才跑通。这套工程我已经整理成一套可直接搬用的驱动结构SPI读写、初始化、正弦/三角波下载、触发输出和调试方法都在里面。本文会用完整代码片段讲清楚每一步也会把无波形、频率乱跳、多通道不一致这些排查过程写出来。适合正在做板级调试的开发者也适合刚拿到芯片还不确定该用DDS模式还是SRAM模式的新手。2. 理解AD9106的驱动体系SPI寄存器读写与波形生成模式选型2.1 AD9106的SPI帧结构24位命令字如何读写寄存器AD9106看着像普通SPI从机但它的帧不是常见的“地址一字节、数据一字节”而是固定24位先8位命令字再16位数据。命令字最高位是读写标志低7位放寄存器地址。也就是说你每次操作至少要连续发3个字节时钟空闲电平、相位也不能搞反否则读出来的东西就是0xFF让你误以为芯片没跑起来。我一般把底层读写在ad9106_spi.c里这样写/* ad9106_spi.c */ #define AD9106_CMD_WRITE 0x00 /* bit7 0: 写寄存器 */ #define AD9106_CMD_READ 0x80 /* bit7 1: 读寄存器 */ static void AD9106_CsLow(void) { HAL_GPIO_WritePin(AD9106_CS_GPIO_Port, AD9106_CS_Pin, GPIO_PIN_RESET); } static void AD9106_CsHigh(void) { HAL_GPIO_WritePin(AD9106_CS_GPIO_Port, AD9106_CS_Pin, GPIO_PIN_SET); } void AD9106_WriteReg(uint8_t addr, uint16_t data) { uint8_t buf[3]; buf[0] (addr 0x7F) | AD9106_CMD_WRITE; buf[1] (uint8_t)(data 8); buf[2] (uint8_t)(data 0xFF); AD9106_CsLow(); HAL_SPI_Transmit(hspi1, buf, 3, 10); AD9106_CsHigh(); } uint16_t AD9106_ReadReg(uint8_t addr) { uint8_t txBuf[3] { (uint8_t)((addr 0x7F) | AD9106_CMD_READ), 0x00, 0x00 }; uint8_t rxBuf[3] { 0x00, 0x00, 0x00 }; uint16_t value 0; AD9106_CsLow(); HAL_SPI_TransmitReceive(hspi1, txBuf, rxBuf, 3, 10); AD9106_CsHigh(); value (uint16_t)(rxBuf[1] 8) | rxBuf[2]; return value; }这里有几个参数值得说明。HAL_SPI_Transmit的第三个参数是发送字节数必须是3因为SPI外设只认识字节流24位帧要拆成三个字节按顺序发。超时设10毫秒足够SPI波特率不高时这个值很宽裕但如果你在RTOS里跑建议改成带信号量的超时版本。片选信号不是简单的拉高拉低AD9106要求CS在SCLK停止后维持一段时间再释放否则最后一拍数据会写入失败。我加了两个空的占空指令来保证时序稳定这个细节就是后面“读回全0xFF”的常见来源。读寄存器的逻辑要特别注意第一个字节发出后从机不会立刻返回数据而是在同一帧的后面16位把结果放到MISO上。所以我用TransmitReceive而不是先发后收这样能确保读操作在一个CS低电平周期内完成。如果你先HAL_SPI_Transmit再HAL_SPI_Receive中间片选会拉高读回来的永远是0xFF。2.2 DDS模式还是SRAM模式按波形类型选工作方式AD9106的核心是内置DDS但它不是只能用DDS。两种模式的使用边界很容易被忽略DDS模式适合正弦波频率字由寄存器给定软件改一次频率字就能连续调频SRAM模式适合任意波形比如三角波、自定义包络、多谐波叠加但需要你先把波形数据通过SPI下载到片内SRAM。我见过不少人在DDS模式下去改波形幅值数组改完发现输出纹丝不动原因是这条路根本没走数据压根没进SRAM。模式输出能力频率/相位控制所需软硬件成本推荐场景DDS模式正弦波内置波形ROM频率字寄存器直接改低初始化几行代码单频/扫频信号源频率切换要快SRAM模式任意波形12位量化需重写数据或改回放点数高需要下载波表和触发配合特殊波形、包络、仿真的复合信号实际工程里我采用了一个混合策略上电先走DDS模式保证立刻有输出能测试链路确认SPI和DAC都正常后再切到SRAM模式下载波表。这样隔离了“SPI没通”和“波表格式不对”两个变量排错效率高很多。DDS模式的频率字计算方法不复杂输出频率等于DAC采样时钟乘以频率字除以一个固定位宽的模数。你只需要按手册里的频率字格式写入同时确认DAC时钟引脚确实有时钟输入。很多板子为了省电把CLK悬空DDS模式自然就没有输出翻转。2.3 给STM32F103VCT6接AD9106的硬件连接与电源树软件之前的硬件连接是第一个翻车点。我在这块板上用SPI1引脚分配是PA5做SCLKPA6接MISOPA7做MOSIPA4做CS另外用PB0做复位引脚。AD9106的SPI接口可以承受3.3V逻辑电平STM32F103VCT6也是3.3V不需要电平转换。但要注意AD9106的模拟电源AVDD和数字电源DVDD如果分开供电必须遵循先数字后模拟的上电顺序否则DAC输出会出现不确定的毛刺。时钟输入是另一个常见盲区。AD9106的DDS时钟不是从STM32过来的而是独立提供。我在这套工程里用有源晶振产生一个40MHz信号送到CLK引脚晶振输出最好串一个22欧电阻靠近芯片管脚放置。DAC参考电压没有用芯片内部REF而是外部高精度基准这样四通道一致性更好。硬件连接表我整理下来大致是这样信号STM32F103VCT6AD9106备注SCLKPA5SCLKSPI时钟串33Ω电阻MOSIPA7SDIO主出从入MISOPA6SDO主入从出可不用CSPA4CS软件控制不用硬件NSSRSTPB0RESET低有效上电拉低50msCLK–CLKIN外部有源晶振40MHz上电之后我建议先量电源管脚AD9106的DVDD和AVDD即使都用3.3V也要各自接0.1uF去耦电容并且电容要靠近芯片。不要因为模块上只有一个电源灯亮了就认为正常模拟供电纹波会直接体现在波形毛刺上。这类问题你从示波器上看到的现象是“频率对但幅度不稳定”其实不是代码问题而是电源树没走对。3. 搭建完整工程SPI底层和AD9106寄存器初始化顺序3.1 用HAL库配置STM32F103VCT6的SPI外设STM32F103VCT6的SPI外设本身不难难在选择人畜无害的初始参数。我建议用8位数据帧因为AD9106的24位帧需要按字节拆分如果直接配16位模式你反而要自己拼半个字节复杂度更高。主模式、软件NSS、MSB先行、空闲电平低、第一沿采样这套组合在AD9106上是最稳的。/* spi.c */ SPI_HandleTypeDef hspi1; void MX_SPI1_Init(void) { hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_1EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_16; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE; HAL_SPI_Init(hspi1); }预分频选16倍APB2时钟72MHzSPI时钟就是4.5MHz对AD9106来说完全够用。为什么不选更快的2分频因为我的PCB走线不算长但SPI从机对上升沿要求有限制过快会在时钟线质量不好时出现“寄存器写入成功、读回却不对”的玄学问题。稳定压倒一切2分频留给板上只有几厘米线的情况我做验证板一律16分频。GPIO配置也很关键尤其CS引脚必须用推挽输出不能开漏。开漏引脚在片选拉低时如果上拉电阻不够强CS电平会被AD9106拉得软绵绵写寄存器随机失败。/* gpio.c */ void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); GPIO_InitStruct.Pin GPIO_PIN_6; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_NOPULL; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); GPIO_InitStruct.Pin GPIO_PIN_0; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); }MISO引脚配置成输入不要使能上拉或下拉。AD9106在SPI不活跃时可能驱动SDO到高阻外部上拉会改变读回数据的电平。这里唯一的例外是如果你只写不读MISO可以空着但那样你永远不知道寄存器写没写进去排错时少了一条路。3.2 寄存器初始化顺序复位、PLL、DAC上电、波形模式AD9106的寄存器里我最不喜欢的就是“上电默认值看起来合理实际输出被关着”这一项。DAC输出使能、DDS模式使能、功率控制几个寄存器分散在不同地址初始化顺序错了后面所有配置都白写。我采用的顺序是先复位再配置DAC电源再设置DDS模式最后写频率字并触发。/* ad9106_init.c */ void AD9106_Init(void) { /* 1. 复位引脚拉低至少50us */ HAL_GPIO_WritePin(AD9106_RST_GPIO_Port, AD9106_RST_Pin, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(AD9106_RST_GPIO_Port, AD9106_RST_Pin, GPIO_PIN_SET); HAL_Delay(2); /* 2. 把DAC上电具体位段按数据手册填写 */ AD9106_WriteReg(AD9106_REG_DAC_PWR, 0x000F); /* 四通道全部上电 */ /* 3. DAC工作模式这里是DDS模式使能 */ AD9106_WriteReg(AD9106_REG_DDSMODE, 0x0001); /* 4. 输出幅度控制先调到一半量程 */ AD9106_WriteReg(AD9106_REG_DAC_CTRL, 0x0800); /* 5. 设置频率字期望输出1MHz按公式换算好写进去 */ AD9106_WriteReg(AD9106_REG_DDS_TW, 0x12345678); }这段代码里AD9106_REG_DDS_TW的低位和高位地址我没有拆开写实际驱动中你会在头文件里明确拆成频率字LSB/MSB两段。频率字的换算公式看起来简单坑主要在端序上STM32是低字节在低位AD9106的频率字可能是大端。我第一次调频率时把LSB和MSB写反了输出频率直接乱了套。养成写完频率字后读回并对比的习惯能省一下午。初始化完成后不要马上看波形先读几个寄存器验证。比如读DAC_PWR确认写入的值还在读DDSMODE确认没有因为片选毛刺丢掉高字节。把这些验证做成AD9106_VerifyInit()函数每次上电自动检查能提前暴露硬件虚焊。3.3 写读函数的同步与异步片选留出建立时间前面写的AD9106_WriteReg是阻塞方式适合初始化阶段。但工程里如果加入RTOS或DMA片选时序和互斥就要重新考虑。我建议把“CS低电平内部延隔两个周期”做成固定行为而不是依赖主循环延时。/* ad9106_spi.c 改进版 */ void AD9106_WriteRegWait(uint8_t addr, uint16_t data) { uint8_t buf[3]; buf[0] (addr 0x7F); buf[1] (uint8_t)(data 8); buf[2] (uint8_t)(data 0xFF); AD9106_CsLow(); /* 留出CS到SCLK的建立时间约200ns */ for (volatile int i 0; i 8; i); HAL_SPI_Transmit(hspi1, buf, 3, 10); HAL_SPI_Receive(hspi1, buf, 0, 1); /* 空操作制造SCLK间隔 */ /* 释放CS前等最后一位移完 */ for (volatile int i 0; i 8; i); AD9106_CsHigh(); }这个版本比简单版多做了两件事片选后的建立时间循环和释放前的保持时间循环。空HAL_SPI_Receive调用只是为了多打一拍时钟让CS释放时SCLK确实已经空闲。你可以用逻辑分析仪验证这段时序如果发现CS和SCLK之间仍有毛刺把循环次数从8提高到16。SPI从机对建立时间的要求通常在几十纳秒但这个循环实际还能起到“让电源波动平复”的作用特别是CS线上有长走线时。4. 下载波形到SRAM并输出正弦波完整可抄的驱动代码4.1 构造波形表12位无符号格式与符号位坑SRAM模式的核心是把波形数据按照芯片要求的格式填入。AD9106是12位DAC波形表每个点是12位无符号数范围0到4095中点2048对应模拟0电压。如果你用数字信号处理里常见的“中心零”有符号数直接把-2048到2047发进去输出会在满量程和零点之间乱跳看起来就像波形失真。这个坑我踩过当时还以为是SRAM地址不对。构造正弦波表很简单但要注意浮点转整数的舍入方式。我建议加0.5后取整避免均值偏到2047以下。/* wave_gen.c */ #include math.h #define WAVE_POINTS 128 uint16_t wave_table[WAVE_POINTS]; void AD9106_GenerateSine(uint16_t *table, uint32_t points, int amplitude) { uint32_t i; for (i 0; i points; i) { float phase 2.0f * 3.1415926f * (float)i / (float)points; float value 2047.5f ((float)amplitude / 4095.0f) * 2047.5f * sinf(phase); table[i] (uint16_t)(value 0.5f) 0x0FFF; } }amplitude参数是峰值幅度最大建议不要超过4095否则会削波。AD9106输出端是电流型DAC通常外接跨阻放大器你看到幅度的大小还取决于外部电阻。所以代码里给满幅实际波形幅度用电阻网络调整这样才能保证波形THD不被软件削波搞坏。生成完波表后我还会做一次简单的统计检查最小值大于0最大值小于4095均值在2048附近。这个检查可以写成断言在调试版本里开启生产版本关掉免得每次下载都被浮点拖慢。4.2 批量写SRAM连续写与地址边界对齐下载波表是SRAM模式最容易出问题的地方。AD9106的SRAM通常支持自动地址递增但前提是你写入的地址寄存器要落在正确的边界上。我这边工程里封装的批量写函数是这样的/* ad9106_sram.c */ bool AD9106_WriteSRAM(uint16_t start_addr, uint16_t *data, uint32_t len) { uint32_t i; if (start_addr len AD9106_SRAM_SIZE) return false; /* 设置写入起始地址具体寄存器名按数据手册定义 */ AD9106_WriteReg(AD9106_REG_SRAM_ADDR, start_addr); for (i 0; i len; i) { /* 连续模式下每次写数据后地址自加 */ AD9106_WriteReg(AD9106_REG_SRAM_DATA, data[i]); } return true; }这里AD9106_SRAM_SIZE要根据你用的具体芯片封装来定。有的工程师喜欢一次性写几百个点但SPI每帧都要3字节再加上片选释放重拉的时间下载几百点花不了多少时间问题不大。真正需要注意的是最后地址是否越界越界后写进去的数据可能会覆盖到别的通道导致多通道串扰。地址对齐也影响后续触发。比如触发输出从任意地址开始如果起始地址不是偶数可能出现半个波形点被截断。我一般会要求波形点数能被4整除起始地址也对齐到4的倍数这样DAC回放时不会因为RAM读取跨bank产生毛刺。这种现象用示波器看是“输出波形最后边沿有细小台阶”容易误判成DAC建立时间不够。4.3 触发输出软件触发、外部触发与即时保持SRAM模式下载完波表后还要给一个触发信号让DAC开始循环回放。AD9106的触发可以来自软件寄存器也可以来自外部引脚。软件触发适合调试外部触发适合需要和系统时钟同步的场合。void AD9106_SwTrigger(void) { AD9106_WriteReg(AD9106_REG_FRAME_REQUEST, 0x0001); }这个函数不要在循环里反复写只要在需要开始输出时调用一次即可。外部触发我一般用STM32的定时器TIM2生成一个脉冲接到AD9106的TRIG引脚。定时器频率决定波形是否重复触发如果你想让波形单次输出就把定时器改成OnePulse模式。触发边沿极性也能在寄存器里配如果看到波形相位不对检查是上升沿还是下降沿触发。触发之后的输出保持也是常见的坑。某些寄存器字段控制“回放结束后输出保持最后一刻电平”还是“立刻归零”。在振镜扫描场景里归零会导致镜片回跳留下一条亮线在信号源场景里保持反而更合理。这个字段藏在帧控制寄存器里初始化时就要决定清楚。我把软件触发和外部触发各封装一层主程序只需调用AD9106_StartWave()和AD9106_StopWave()细节全部隔离在驱动里上层代码不会被寄存器语义绕晕。5. AD9106驱动调试避坑从无波形到波形失真的排查方法5.1 SPI读回全0xFF接线与模式的不匹配现象写完寄存器马上读回数据全是0xFF。原因几乎每次都是SPI模式或片选时序没对上。我见过三种具体场景一是MISO没接或虚焊读引脚悬空被上拉改成高电平于是读到全1二是SPI时钟极性相位和AD9106要求相反数据位移不对位三是CS释放太快最后一拍数据没锁存到芯片内部。解决方法是先拿逻辑分析仪抓三根线确认MOSI上发送的24位字节顺序正确然后用一个最简单的只读寄存器反复读读到预期值再继续往下写。不要靠猜先抓波形。5.2 输出一直直流没有波形翻转现象DAC输出电压有值但无论怎么改频率字示波器上都是平线。原因大概率是DDS模式没有真正使能或者DAC被配置成静态输出。另一个可能原因是CLK引脚没时钟输入DDS核没有时钟自然没有翻转数据。解决方法是先量CLK引脚有没有波形没有就先修硬件有波形就回读DDSMODE寄存器确认模式位确认后再写一个最慢频率的正弦波比如1kHz这样一眼就能看出有没有翻转。如果仍然是直流再查DAC_PWR里的上电字段某些位默认是关的。5.3 波形频率比预期高/低频率字看似正常现象频率公式算出来应该输出1MHz实际示波器看到500kHz或2MHz。原因通常是频率字的位段被拆错或者DAC采样时钟不是你以为的那个值。我做过一次频率字LSB/MSB端序反了的情况低端序时频率字恰好是对称所以结果不对但不离谱。解决方法是先不纠结频率字把输出设为最大下限比如写一个极端慢的频率然后示波器数周期确定实际频率和采样时钟的比值。这样能反推出位宽到底是多少比一遍遍试寄存器快得多。还有一种情况是时钟源本身有分频AD9106内部PLL没锁定也会让频率成倍偏移。5.4 多通道输出电压不一致现象四个通道用完全相同的配置输出正弦波幅度不同偏置也不同。原因不全是芯片一致性更多是外部电路差异。AD9106多个DAC输出公用参考电压但每个通道都有独立增益和失调校准寄存器。还有如果某个通道的跨阻放大器输入偏置电流不同直流电平也会偏差。解决方法是先测所有通道的静态输出确认偏置差多少然后通过增益寄存器拉平幅度再用失调寄存器把直流偏置校准到同一个值。这个步骤被很多驱动代码忽略但它才是四通道信号发生器能不能用的关键。校准逻辑我会放到生产自检程序里上电后自动执行不让用户手工调。6. 进阶玩法DMA批量更新波表和用示波器验证波形质量6.1 用DMA把波表从Flash搬到SPISRAM模式下如果波形点很多阻塞式逐点写会占用CPU。STM32F103VCT6有DMA1可以把波表从内存搬到SPI1的发送寄存器。我封装了一个非阻塞下载接口bool AD9106_DMA_LoadWave(uint16_t *table, uint32_t len) { if (len AD9106_SRAM_SIZE) return false; /* 把每3字节的SPI帧提前拼到缓冲区里 */ volatile uint8_t spiBuf[3]; AD9106_WriteReg(AD9106_REG_SRAM_ADDR, AD9106_START_ADDR); HAL_SPI_Transmit_DMA(hspi1, (uint8_t *)table, len * 2); // 注意实际DMA传输时应拼接帧字节这里是示例 return true; }这个功能调通后你可以在波形切换时先准备新表再等DMA完成回调里发起触发。STM32F103没有Cache不存在DMA缓存一致性问题但要注意DMA缓冲区必须放在SRAM里不能直接指向Flash。若波形表很大可以考虑放在外部RAM但访问速度会拖慢下载。DMA方式适合“预先准备多组波表快速切换”的场景调频扫频模式也可以用。6.2 我用示波器验证波形的三个习惯验证波形质量不能只看一个周期。我现在会做三件事先看整体幅度和频率再用示波器的FFT功能看谐波分量最后把输出接一个精确的电阻负载测带载能力。FFT里如果有明显的二次谐波说明波表中心电平偏了如果有奇怪的非整数次谐波多半是PLC触发问题。带载测试能发现DAC驱动能力不足或输出电阻过大这类问题温度敏感性很高。测量时探头一定要用短地线夹子地线会造成几十纳伏级噪声叠加让波形看起来比实际差很多。做这套驱动的过程中我最大的教训是把它当“数据手册的软件翻译”而不是“黑匣子”。每次遇到灵异现象重新对照寄存器映射表基本都能在半小时内定位。现在新板子调试我的第一动作永远是先点亮SPI回读再谈波形。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑