
简介面向FPGA数字信号处理进阶学习者的DDS与AD/DA闭环测试工程覆盖DDS波形生成、DAC输出、ADC自环回采与Matlab频谱校验的完整链路适合搭建信号采集验证平台或学习Xilinx DDS IP应用的开发者参考。RAR压缩包46.11MB共613个文件包含75个v格式RTL源码、Vivado约束与比特流、Tcl/Bat执行脚本、do仿真脚本及Matlab分析脚本覆盖工程构建、硬件调试与频谱验证等环节目录清晰便于按模块取用。已有320人学习浏览。工程适配黑金AN108模块提示34针插头与底板对齐的安装细节内含1MHz与3MHz正弦信号生成配置、ILA捕获的ADC采样数据及频谱分析脚本配合VIO修改频率字即可复现从DAC波形合成到ADC采样验证的完整过程对排查数据采集链路与校核信号频率有直接参考价值。1. 项目定位一条 DDSADDA 自测链路的固件样板如果你手头有一块同时焊了 DDS 信号源、高速 ADC、DAC 的板卡最头疼的事不是画板而是上电之后不知道链路哪一段是通的波形到底有没有产生、采样回来的数据对不对、DAC 还原出来跟原始信号差多少。这个“DDS ADDA 测试工程文件”干的事就是把这条信号链变成一个可自检、可回环、可观测的固件系统——用 DDS 产生正弦波接入 ADC 采集再经 DAC 回放同时通过串口上位机查看数据。它本质上是一个给模数混合板卡做“体检”的测试固件适合做硬件验证的工程师、做嵌入式开发的熟手也适合刚接触 ADDA 和 DDS 的初学者当作一份能跑通的参考工程。2. 硬件链路与工程结构信号从 DDS 到 ADC 再回到 DAC 的完整走向2.1 信号链路走向板子上的“体检回路”这个测试工程最核心的思路是构造一条闭环信号链先让 DDS 芯片生成一个频率可设的正弦波经过前端模拟调理放大或衰减后送入 ADC 的采样引脚固件把 ADC 的结果读回来一方面通过串口把采样值实时上报另一方面把数据重新映射到 DAC 输出这样在示波器上可以直接看到“原始正弦波”和“ADC 采样后经 DAC 还原的正弦波”两条曲线。有两条曲线放在一起对比整条链路哪里有问题就非常直观DDS 输出正常、DAC 还原波形不对问题大概率出在 ADC 采样参数或数据映射上DDS 输出正常、ADC 数据上传不正常问题大概率出在采样触发或 DMA 搬运上连 DDS 输出都没有优先查 SPI 时序和芯片配置寄存器。这样的回环设计比单纯测一块 ADC 或 DAC 要实用的多。它不仅能验证单点器件好坏还能验证整个信号调理链路的增益、偏置、带宽是否合理正好是这个测试工程文件中“测试”两个字的分量所在。2.2 芯片分工与选型思路这套测试工程围绕三种器件展开。DDS 选择的是经典的可编程波形发生器通过 SPI 接口配置频率和相位寄存器输出正弦波作为信号源ADC 选择的是并行或 SPI 接口的逐次逼近型SARADC工作在单次或连续转换模式由定时器触发采样DAC 则使用片内集成的数模转换器负责把数字采样值还原成模拟信号。之所以把 DDS 和 ADDA 放在同一块板子上是因为工程验证阶段最怕“信号源不可控”。用外部函数发生器做信号源你永远要怀疑连接线和仪器设置是否正确而让板子自己生成信号、自己采集、自己还原所有环节都掌握在固件手里出问题时只需要看寄存器配置即可。在固件层面主控使用了常见的 ARM Cortex-M3 内核微控制器内置了足够用的 ADC 和 DAC 外设。DDS 芯片与主控之间通过 SPI 通信ADC 通过片选信号和并行数据总线读回采样值DAC 则直接挂在主控内部总线上软件写数据即可更新输出。整条链路没有额外的操作系统全部以裸机状态机方式运行中断服务函数里只做数据搬运和标志置位具体波形处理和串口上报放在主循环里完成。2.3 工程目录文件和它们的作用拿到这套工程文件后目录结构长这样dds_adda_test/ ├── App/ │ ├── main.c // 主循环命令处理 回环控制 │ ├── dds_driver.c // DDS 芯片驱动SPI 写寄存器 频率配置 │ ├── adc_driver.c // ADC 采集驱动DMA 定时器触发 │ ├── dac_driver.c // DAC 输出驱动波形回放映射 │ └── uart_cmd.c // 串口命令解析设置频率、增益、模式 ├── Bsp/ │ ├── spi.c // SPI 底层初始化与字节收发 │ ├── timer.c // 采样触发定时器配置 │ └── gpio.c // 片选、复位、控制引脚初始化 ├── Core/ │ ├── startup.s // 启动文件 │ └── system.c // 时钟初始化 └── Doc/ └── test_guide.md // 接线说明和测试步骤// main.c 中回环模式的简化伪代码 int main(void) { uint16_t sample[ADC_BUF_LEN]; // 采样缓冲区由 DMA 在后台填充 uint8_t uart_cmd_buf[64]; System_Init(); // 时钟、GPIO、外设初始化 DDS_Init(); // 复位 DDS 芯片并写入默认频率 ADC_StartDMA(sample, ADC_BUF_LEN); // 启动 DMA 采集 while(1) { if (UART_GetCommand(uart_cmd_buf)) // 收到串口命令 { CMD_ParseAndExecute(uart_cmd_buf); // 解析并执行例如改频率 } if (g_sample_flag 1) // DMA 半满/满中断置位 { DAC_OutputWaveform(sample); // 把采样值映射到 DAC 输出 UART_SendData(sample, ADC_BUF_LEN); // 同时上报给 PC g_sample_flag 0; } } }工程分层很清晰App 层放驱动和业务逻辑Bsp 层放底层外设初始化Core 层放启动和时钟。如果你拿到的是其他项目可能看不到这么整洁的目录但这套结构适合用来快速定位某个功能在哪个文件里。3. 核心代码拆解频率字、SPI 时序与采集回环3.1 频率控制字的换算一次除法决定输出频率精度DDS 输出频率的公式是固定的频率控制字 期望频率 × 相位累加器位宽系数 ÷ 系统时钟。常见的相位累加器位宽是 28 位所以系数为 2 的 28 次方。这套工程里 DDS 系统时钟使用 16 MHz 有源晶振那么要输出 10 kHz 正弦波计算如下#define DDS_FCLK 16000000UL // DDS 芯片主频 16 MHz #define DDS_ACCUM_BITS 28 // 相位累加器位宽 uint32_t DDS_CalcFreqWord(uint32_t freq_hz) { // 频率字 (期望频率 * 2^28) / 系统时钟 // freq_hz 的单位是 Hz结果向上取整避免长期累计误差 uint64_t tmp (uint64_t)freq_hz * (1UL DDS_ACCUM_BITS); uint32_t fw (uint32_t)((tmp DDS_FCLK / 2) / DDS_FCLK); return fw; }这段代码有两个关键点一是用了 64 位中间变量做乘法避免 32 位溢出——因为 10 kHz × 2 的 28 次方约等于 2.68×10 的 12 次方早就超过 uint32_t 的表示范围了二是做了四舍五入让频率误差在长时间累计时均匀分布。实际算出来 10 kHz 对应的频率字大约是 167772把这个值写进 DDS 芯片的频率寄存器后输出频率与目标频率的偏差只在万分之几的量级普通测试完全够用。3.2 AD9834 的 SPI 写寄存器16 位指令的时序细节DDS 芯片的寄存器操作是通过 SPI 写入 16 位控制字来完成的。写入过程中片选信号拉低后芯片在时钟上升沿锁存数据写完 16 位后片选拉高芯片内部把数据存入目标寄存器。为了防止两个 16 位指令之间产生误写每次写入前要保证片选信号至少低电平持续几个微秒void DDS_WriteRegister(uint16_t reg_value) { // 拉低片选通知 DDS 芯片开始接收指令 GPIO_ResetBits(DDS_CS_PORT, DDS_CS_PIN); // 等待片选低电平稳定避免毛刺导致误判 DelayUs(2); // 以 16 位帧格式发送寄存器值 SPI_SendData16(DDS_SPI, reg_value); while (SPI_GetFlagStatus(DDS_SPI, SPI_FLAG_TXE) RESET); // 拉高片选数据在此时被芯片锁存生效 GPIO_SetBits(DDS_CS_PORT, DDS_CS_PIN); }注意这里 SPI 必须配置成 16 位帧格式否则芯片会把两次 8 位写当成两条指令来解释。另外一个容易踩的坑是 SPI 时钟极性和相位这套工程里 DDS 芯片要求在时钟空闲时为高电平、在第二个边沿采样数据也就是 SPI 模式 2。如果初始化时配成了模式 0 或模式 3写进去的频率字有时生效、有时不生效表现非常像接触不良。写频率寄存器时需要先用控制寄存器打开 28 位长度模式B281然后把频率字拆成两个 14 位段、分两次写入低 14 位和高 14 位寄存器。频率字位宽是 28 位而一次 SPI 帧只有 18 位有效数据所以必须这样分两次uint32_t fw DDS_CalcFreqWord(10000); // 计算 10 kHz 频率字 // 第一次写入频率字低 14 位寄存器地址为 FREQ0_LO DDS_WriteRegister(0x4000 | (fw 0x3FFF)); // 第二次写入频率字高 14 位寄存器地址为 FREQ0_HI DDS_WriteRegister(0x4000 | (fw 14) 0x3FFF);uint16_t ctrl 0x2000; // 控制寄存器选择 FREQ0 寄存器、开启输出 DDS_WriteRegister(ctrl); // 让频率字生效3.3 ADC 采集与 DMA 搬数据不占 CPU 的采样回路ADC 采样如果每个点都靠 CPU 去读高频下 CPU 会被完全占死导致串口命令无法响应。这套工程的做法是用定时器产生固定频率的触发脉冲去启动 ADC 转换转换完成后数据由 DMA 自动搬运到内存缓冲区等缓冲区半满或全满时触发中断主循环再取数据做处理。// 定时器触发 ADC 采样DMA 自动把结果写入 sample[] // 每触发一次ADC 转换一个点转换结果直接搬到内存 void ADC_StartDMA(uint16_t *buf, uint16_t len) { // 配置 DMA 通道数据从 ADC 数据寄存器搬到 buf DMA_Config(DMA_CHANNEL_ADC, (uint32_t)ADC_DR, (uint32_t)buf, len); // 开启 DMA 半满和全满中断 DMA_ITConfig(DMA_CHANNEL_ADC, DMA_IT_HT | DMA_IT_TC, ENABLE); // 使能定时器触发每 1 us 触发一次 ADC 转换 TIMER_Start(TIM_SAMPLE, 72); // 72 MHz 主频分频后 1 MHz 触发 ADC_Cmd(ENABLE); // 开启 ADC }这里的分频数是 72 意味着采样率约为 1 MHz也就是每秒采一百万个点。如果信号是 10 kHz 的正弦波每个周期能采 100 个点在示波器上看到的是一个光滑的波形足够评估采样的连续性。如果想要更高的采样率可以适当提高定时器分频但要注意 ADC 时钟不能超过数据手册上限否则转换结果会出现跳变。3.4 DAC 回放与回环模式采集到数据之后测试工程做了两件事通过串口把原始采样值发送到 PC 端画波形同时把采样值缩放后写到 DAC输出还原波形。这里的映射有个细节——ADC 是 12 位分辨率DAC 也是 12 位分辨率但两者量程可能不完全一致需要做偏移修正。默认情况下直接把 ADC 的高位截断到 DAC 位数即可// ADC 采样值 12 位DAC 输出也是 12 位 void DAC_OutputWaveform(uint16_t *adc_buf, uint16_t len) { uint16_t vref_mid 2048; // DAC 输出中点对应 1.65V即 VREF/2 for (uint16_t i 0; i len; i) { // 把 ADC 采样值转换为 DAC 编码值 // 这里假设 ADC 与 DAC 参考电压一致可以直接映射 uint16_t dac_code adc_buf[i]; DAC_WriteChannel(DAC_CHANNEL_1, dac_code); DelayUs(1); // 给 DAC 输出缓冲器留出建立时间 } }逻辑本身不复杂但“回环”的意义在于如果 DDS、ADC、DAC 三个环节都正常你用示波器同时看 DDS 输出引脚和 DAC 输出引脚应该能观察到两路频率相同、相位接近的正弦波如果 DAC 端的波形出现明显台阶或失真说明采样率不够或者 DAC 建立时间没满足。这套工程把原本需要三台仪器才能完成的验证压缩到一块板子和一台示波器就能完成。4. 编译烧录与联调跑通整个回环的实操步骤4.1 编译环境与工程导入这套工程基于常见的 MDK-ARM 或者 GCC 工具链工程文件后缀是 .uvprojx 或 CMakeLists.txt。MDK 方式最简单直接用 Keil 打开工程目录下的 .uvprojx在 Options for Target 里确认芯片型号和调试器配置然后点 Load 烧录。GCC 方式需要在命令行里执行cmake make生成固件再用调试器烧录。如果你拿到的是源代码而非完整工程文件建议先用 STM32CubeMX 重新生成一个包含 SPI、定时器、ADC、DAC 外设的初始化代码然后把这些驱动文件的函数声明挂进自己的工程框架。这一步的核心是保证外设时钟开启顺序正确——DDS 的 SPI 外设和 ADC 的 DMA 外设挂在不同的时钟总线上初始化时应该先打开 GPIO 和 SPI 时钟再配置 DMA最后配置定时器触发源。4.2 接线与信号连接板上连接顺序是这样的DDS 芯片输出引脚经过一个射频阻抗匹配网络接到 ADC 的模拟输入通道DDS 的 SCLK、SDATA、FSYNC 三条信号线接到主控的 SPI 引脚主控与上位机之间通过 USB 转串口连接串口参数为 115200-8-N-1。如果板子上有 SMA 接口建议先用一根短的同轴线把 DDS 输出直接接入一个外部示波器通道确认信号源本身是否工作正常。接线时有几个容易忽略的地方DDS 芯片的供电引脚需要就近放置 0.1uF 和 10uF 去耦电容否则高频下电源噪声会直接体现在输出波形上ADC 的模拟地应该与数字地在芯片下方单点连接避免数字开关噪声耦入模拟链路。实际测试工程里如果发现 ADC 采样值低两位总在跳动先检查这两个问题通常比换芯片管用。4.3 串口联调命令怎么用、波形长什么样板子烧录完成后打开串口终端按下回车会看到命令提示符。这套工程支持的命令不多但覆盖了测试核心场景命令作用示例freq Hz设置 DDS 输出频率freq 25000amp 0~255设置 DDS 输出幅度控制字amp 128dacmode 0/10直通原始采样值1回环映射dacmode 1sample Hz设置 ADC 采样率sample 500000start启动回环测试start比如输入freq 10000然后输入start示波器 DDS 输出通道上应该看到 10 kHz 正弦波片上 ADC 采样点通过串口打印出来之后用任意串口示波器工具打开能看到正弦曲线的离散点排列。此时再把dacmode切到 1DAC 输出引脚同样能看到 10 kHz 波形。如果三个地方都能看到同频波形说明整条链路完全打通可以进入下一步改造。5. 测试工程避坑指南容易翻车的五个位置5.1 DDS 输出全 0 或无波形现象是板子烧录完成后串口命令正常回显但示波器上 DDS 输出什么信号都没有。原因通常是 SPI 初始化模式下DDS 芯片没有正确接收到控制字。先检查片选引脚是否被复用成了其他外设再检查 SPI 模式是模式 2 还是模式 0。解决方法是先发一次控制寄存器复位命令等输出稳定后再用逻辑分析仪抓取 SCLK、SDATA、FSYNC 时序确认配置频率字的指令确实被芯片解析到。5.2 正弦波变成三角波或出现削顶如果 DDS 输出波形在接近峰值处被削平这属于模拟链路问题不是数字部分的问题。原因是 DDS 输出端的放大器增益过高信号幅度超过了运放的输出电压摆幅。解决方法是把放大器的反馈电阻调小或者在信号进入 ADC 之前加一个衰减电阻网络。这种问题在纯固件测试时看不出来只有用示波器观察时才会暴露所以不要第一反应去改代码。5.3 ADC 采集值全 0 或全满量程现象是串口打印出来的数据要么全部是 0要么全部是 4095。原因是采样通道选错了或者 ADC 的参考电压没有接对。比如 ADC 的采样通道配成了 PA0 但信号实际接在 PA1 上又或者参考电压引脚悬空ADC 内部比较器没有有效基准。解决方法是先用万用表量一下采样引脚上的直流电平确认信号范围在 ADC 输入范围中间再检查 ADC 通道配置代码里的通道编号是否与原理图一致。5.4 DMA 搬完数据后中断不触发现象是串口命令响应正常程序也能进入主循环但 g_sample_flag 一直为 0回环不工作。原因是 DMA 的中断标志没被清除或者 DMA 配置的缓冲区长度是奇数导致半满和全满中断交替产生始终到不了全满。解决方法是先屏蔽半满中断只保留全满中断再把缓冲区长度改为偶数并初始化为 0然后单步调试确认进入 DMA_IRQHandler 后标志位确实被读取。5.5 SPI 写寄存器偶尔生效、偶尔不生效现象是频率字有时写成功了有时输出还是上一次的频率重新上电后表现不一致。原因是 SPI 发送完成后没有等待发送结束标志就直接拉高了片选信号导致最后一个时钟边沿数据不稳。解决方法是发送完 16 位指令后要等待SPI_FLAG_TXE以及SPI_FLAG_BSY全部置位再拉高片选。这个 bug 在实际工程里非常隐蔽因为 8 位模式的时序恰好能容忍这个误差16 位模式下就暴露了。具体修改示例void DDS_WriteRegister(uint16_t reg_value) { GPIO_ResetBits(DDS_CS_PORT, DDS_CS_PIN); DelayUs(2); SPI_SendData16(DDS_SPI, reg_value); // 等待发送缓冲为空且 SPI 总线不忙 while (SPI_GetFlagStatus(DDS_SPI, SPI_FLAG_TXE) RESET); while (SPI_GetFlagStatus(DDS_SPI, SPI_FLAG_BSY) SET); GPIO_SetBits(DDS_CS_PORT, DDS_CS_PIN); }这个“发送完立即拉高片选”的坑我栽过一次那次排查花了一下午最后用逻辑分析仪才发现最后一位数据根本没锁存进芯片。6. 二次开发思路把“测一下”升级成“自动校准”这套工程文件交付的不只是一个跑通的回环更是一个可以扩展的测试平台。最常见的改造方向是加一个“扫频测试”子命令——让固件自动从 100 Hz 扫到 100 kHz在每个频点上采集一段数据计算幅度并记录最后把频响曲线打印到串口。实现思路是在命令解析里增加一个sweep start分支主循环内部循环调用DDS_SetFreq()和ADC_GetAmplitude()把傅里叶变换简化为在全周期内寻找最大值和最小值即可// 扫频测试中的核心计算一个完整周期内的信号幅度 uint32_t ADC_GetAmplitude(void) { uint16_t min_val 4095, max_val 0; for (uint16_t i 0; i ADC_BUF_LEN; i) { if (sample_buf[i] max_val) max_val sample_buf[i]; if (sample_buf[i] min_val) min_val sample_buf[i]; } return (uint32_t)(max_val - min_val); }更进一步可以在这个基础上做自动增益校准预设一组目标幅度实测幅度与目标之间做一次比例修正把修正系数写进 DDS 的幅度寄存器。这样当信号链路增益漂移时板子能自己把波形调到标准幅度产线上不用逐台手动微调。改造这个测试工程时我建议你保留它的“分层结构”习惯DDS 驱动、ADC 驱动、命令解析三部分互不依赖新增功能时不要在 main 函数里堆逻辑而是照着 App 层新建一个calib.c只暴露一个入口函数给命令解析器调用。我在后续的多个板卡验证里都把这份工程的扫频结果作为链路验收依据。从那以后每次拿到新板卡我都强制自己先跑一遍“DDS 信号源 ADC 回采 DAC 回放”的闭环测试再谈功能和性能优化——这条流程省下的调试时间远比我当初写这个测试工程的时间多。希望这些拆解对你跑通和改造这套工程有帮助。本文还有配套的精品资源点击获取