公司动态

从零搭建任意波形发生器:DAC选型、回放链路与固件实现

📅 2026/8/27 19:50:28
从零搭建任意波形发生器:DAC选型、回放链路与固件实现
我最早被“任意波形发生器”这个词拉进坑是因为一个很不起眼的实验要送给超声波换能器一组带包络的猝发脉冲频率、幅度、间隔全都得按实测数据来。普通信号发生器能出正弦、方波、三角波也能扫频但它没法把一个自定义的、点对点的波形序列直接变成模拟电压。Arbitrary Waveform GeneratorAWG解决的就是这个问题把用户想要的时间-电压曲线先离散成采样点存入波形存储器再用可控的采样时钟依次读出经过DAC转换成模拟信号最后滤波、放大输出。这个思路听起来不复杂真正做起来却牵扯到DAC选型、存储深度、时钟抖动、DMA搬运、上位机协议和输出级校准一堆事情。这篇文章就从我实际搭建的一台入门级AWG实验板讲起适合正在选型、画板子、写固件或者被波形失真困扰的朋友参考。1. 为什么固定波形信号源覆盖不了我的测试场景1.1 看似“任意”核心在“可自定义”信号发生器现在都有“任意波”功能但很多人的第一反应是任意波不就是把几个预设波形改一改嘛。其实不是。拿函数发生器来说它的内部通常基于DDS直接数字合成或者锁相环技术能够高效生成正弦、方波、锯齿波、脉冲这些“有解析表达式”的信号。可当你需要的波形根本写不成简单公式或者来自一组实际采集到的数据函数发生器就没办法了。AWG的核心是把波形看作一个“点序列”每个点对应某一个时刻的DAC输出值。只要采样率够、存储深度够理论上什么形状都能放。这个差别在实际项目中非常关键。比如我想模拟一个电机启动时的电流冲击波形里有缓慢上升的平台、快速下跌的尖峰、还有叠加的纹波。这种复杂形状如果强行用正弦或者脉冲拼接效率低不说细节完全对不上。AWG的做法简单粗暴先把波形在电脑上用Python或者Matlab算好再传给设备设备只负责按节奏回放。1.2 一个我实际遇到的传感器激励案例当时我需要给一个压电陶瓷片做扫频激励要求频率从30kHz线性扫到80kHz但幅度包络不能是平的而是要在60kHz附近压低模拟实际结构的谐振特性。用函数发生器的“扫频”功能只能做频率随时间线性变化幅度曲线是没法随意设定的。最后还是用AWG方案先在电脑上生成一段 0.1 秒的数据载波频率从30kHz到80kHz线性变化包络幅值通过一个数组指定60kHz处降低到原来的40%前后各加5ms的余弦爬升避免突然通断产生爆音。这段波形在电脑上是一组浮点数经过量化变成DAC代码后下载到设备里。回放时用示波器一看电流包络、扫频速率、边沿形状都符合预期。这个案例里没有任何一个标准波形能代替AWG的价值就在这里它不是让你去“选波形”而是让你“定义波形”。1.3 把需求翻译成AWG参数动手之前最好先把自己的需求量化不然很容易被厂商宣传的参数带偏。我一般按四个维度拆最高输出频率决定DAC更新率和重建滤波器不是只看DAC的“最大更新率”还要看你想保留多少阶谐波最低频率和最长单次波形时长决定存储深度低频长波形最吃内存幅度范围和输出阻抗决定后端运放和50Ω匹配方案触发同步关系决定要不要外部触发输入、触发到输出的延迟指标。把这几个问题写在纸上设计参数自然就出来了。我最初的需求其实只有两个采样率不低于1MSa/s单次波形长度不低于64K点触发延迟尽量小。后面所有选型都围绕这两条展开。2. 任意波形是怎么从数据流变成电压的回放链路拆解2.1 波形回放和DDS的分工与区别很多人把AWG和DDS混在一起。严格来说DDS是一种频率合成方法用相位累加器对频率字进行累加再用累加结果作为地址去读“标准波形表”最后经过DAC输出。DDS的好处是频率分辨率极高、切频方便但标准DDS的波形表通常是固定的正弦或者少量波形。AWG也可以借用DDS的架构比如用相位累加器产生读地址但波形表内容由用户直接写入因此可以输出任意波形。这个时候“采样率”不再是一个固定值而是通过时钟、分频器或者DDS频率字来调节。从信号链路看AWG回放过程可以拆成四段上位机或板级处理器把波形数据写入存储区回放控制逻辑按设定的采样时钟逐点读出数据数据送到DACDAC完成数字到模拟的转换DAC输出经过运算放大器、重建滤波器和输出阻抗匹配网络得到最终模拟信号。这个链路里最容易忽略的是第4段。DAC输出是一个“阶梯波”不是平滑曲线。阶梯的每个平台都有高频能量如果不加滤波输出频谱里会看到基于采样时钟的镜像频率叠加在目标波形上造成很奇怪的失真。2.2 采样率、存储深度和波形时长之间的关系这一段建议所有想做AWG的人都先算一遍。AWG能输出的单次波形时长由两个参数共同决定波形时长 存储深度点数 / 实际采样率举个例子假设波形存储是64K点采样率设成1MSa/s那么一次最多只能连续输出65536 / 1000000 65.536ms。想输出1秒波形要么把采样率降到65.536kSa/s要么把存储深度扩大到100万点。低频波形往往比高频波形更吃存储因为低频需要很长的时基而点数又得保持足够的采样密度来还原细节。还有一个容易算错的点点数并非越多越好还要看DAC位数和存储带宽。14位DAC每点占2字节100万点就是2MB。如果单片机没有外部存储器内部RAM不够大就只能在“波形长度”和“采样率”之间做取舍。做高速AWG时还要考虑存储带宽比如1GSa/s、2字节每点意味着存储系统要能提供2GB/s的读数速度这通常只有FPGA加DDR3/DDR4才能扛住。入门级项目用STM32这类MCU时别一上来就规划几十兆的波形先把低速链路跑通再说。2.3 从DAC到模拟输出重建滤波器并不是可选项DAC的本质是采样保持器它在每个采样周期内输出一个保持电平数学上相当于理想冲击再经过零阶保持。由此带来的频响是sin(x)/x形状高频幅度会下降H(f) sin(πf/fs) / (πf/fs)f接近采样率一半时幅度大约降到0.637也就是-3.92dB左右。也就是说如果你用1MSa/s的DAC输出一个接近500kHz的信号不修正的话幅度已经比低频时低了快4dB。如果是尖峰、脉冲这类丰富的波形高频分量被压得更厉害边缘会变钝。更麻烦的是镜像频率。采样保持输出的频谱里目标频率之外还会出现fs±f、2fs±f等镜像分量必须用低通滤波器压掉。滤波器阶数越高对镜像抑制越强但相位失真和过冲也会变明显。实际项目里常用“可切换滤波器组”或者“数字预补偿”在DAC之前把高频分量提前抬起来抵消sin(x)/x衰减。入门阶段至少要在模拟输出级加一个二阶到四阶的巴特沃斯低通截止频率按最高目标频率的2到3倍取。3. 关键器件选型DAC、参考电压、运放和时钟该怎么配3.1 DAC的四个参数比“位数”更重要选DAC时很多人只看“多少位”。位数的确决定理论最小步进但任意波形发生器对DAC的要求是综合性的至少要看四个参数。第一是更新率。DAC每秒能改多少次输出值直接决定采样率上限。低速用MCU内置DAC即可高速需要并行接口的DAC最好数据总线能直接接到FPGA的IO或者MCU的FMC接口。第二是有效位数ENOB。标称12位/14位只是理论分辨率实际在高速更新下受噪声、时钟抖动、电源纹波影响有效位数会下降。如果一款14位DAC在目标更新率下的ENOB只有10位那跟同价位12位高性能DAC相比可能没优势。第三是毛刺能量。DAC切换编码时内部模拟开关会产生短暂的瞬态电流毛刺能量大会在输出上表现为细小的尖峰尤其对任意波形影响明显因为任意波形几乎每个点都在切换毛刺会变成背景杂散。参数表里的glitch impulse越小越好。第四是输出类型。电流输出型DAC比如AD9744、DAC902更新率高但后面必须配跨阻放大器转电压电压输出型DAC很多低速型号使用方便但带宽和压摆率通常受限。低价项目常用的MCU内置DAC是电压型做500kSa/s以内的实验够用。3.2 参考电压与输出运放精密和带宽要分开满足DAC只是把“代码”变成“和参考电压成比例的东西”参考电压不稳输出波形幅度就会漂移。实际项目里建议用低噪声基准源比如ADR420、REF192之类不要直接从板上3.3V电源取除非你只需要非常低精度的演示。输出运放的选择要看两级第一级把DAC输出电流/电压缓冲成低阻抗信号第二级做幅度放大、偏置调整和输出阻抗匹配。低速高精度波形可以用精密运放高速任意波形必须用高带宽、高压摆率的运放。比如14位165MSPS的DAC后面至少要选增益带宽积100MHz以上的运放否则输出边缘会被运放“削圆”。再加上压摆率不够大信号方波会变成梯形波。3.3 时钟与触发波形质量的地基这个点容易被新手忽略。AWG的采样时钟如果是普通单片机内部RC抖动量会直接变成波形的相位噪声。尤其输出高频正弦时时钟抖动会让每个采样点在时间轴上轻微晃动频谱上就是边带变大、底噪抬高。做入门实验可以先忍受但想做能用的设备至少用外部晶振好一点用TCXO温度补偿晶振追求低相位噪声再考虑恒温晶振。触发同样重要。AWG常见的触发方式有软件触发、外部TTL触发、以及电平触发。软件触发简单但延迟不稳定因为代码执行路径不同外部触发建议走硬件引脚直接启动定时器或DMA避免中断调度抖动。3.4 不同预算下的参考配置我给一个保守但可复现的组合分两档入门低速方案STM32F303/F405内部DAC 定时器触发DMA 一级运放缓冲采样率做到500kSa/s到1MSa/s波形深度受内部RAM限制适合音频、生物电、控制信号模拟中速方案FPGA如ICE40、Cyclone IV或国产小规模FPGA 外部并行DAC如AD9744、DAC902 SRAM采样率可提升到几十MSa/s波形深度可以做到64K~1M点再往上就需要DDR、高速DAC和可切换多级滤波器那就是商业仪器的范畴了。选型时列一个小表会很直观型号位数更新率输出类型适合场景MCU内置DAC12位几百kSa/s~1MSa/s电压低速入门DAC90212位最高165MSPS电流中速项目成本低AD974414位最高165MSPS电流中速项目精度更好AD97088位最高125MSPS电流高速演示便宜具体型号选择要以手册为准我这里列出来的意义是帮你建立“先定采样率和输出带宽再反推DAC和存储方案”的思维。4. 样机搭建与固件实现从DMA回放到触发同步4.1 硬件框架我做的中速样机框架大致是STM32F405作为控制核心负责USB/串口通信、波形数据处理、配置参数DAC用AD974414位并行输入采样率由FPGA/复杂可编程逻辑器件控制波形数据放在外部SRAM控制器通过FMC总线写入回放由一片小规模FPGA完成负责从SRAM读数据、按采样时钟发送给DAC输出级用高速运放做电流转电压再经50Ω匹配电阻输出。如果你只想复现一个简单版本用STM32内置DAC最省事DMA把波形数组搬运到DAC的数据寄存器定时器触发转换固件代码量更少。下面我以这种方式讲固件逻辑因为它的结构和高速度方案完全一致只是DMA方向和数据宽度更简单。4.2 固件用定时器触发DMA回放AWG固件的核心思路是“不回填则不卡顿”。也就是波形要提前放好回放时CPU不参与逐点计算只负责配置DMA和定时器。第一步准备波形缓冲区。以12位MCU内置DAC为例DAC数据寄存器通常是16位右对齐代码里定义一个uint16_t数组数组长度就是波形点数。第二步配置DMA为循环模式从数组搬运到DAC的DHR寄存器数据宽度半字。第三步配置定时器的更新事件作为DMA请求源。这样每个定时器周期自动向DAC搬一个点采样率由定时器分频决定// 伪代码以STM32 HAL风格示意 TIM_HandleTypeDef htim2; htim2.Init.Prescaler 0; htim2.Init.Period (uint32_t)(TIM_CLOCK_HZ / SAMPLE_RATE) - 1; HAL_TIM_Base_Init(htim2); __HAL_TIM_ENABLE_DMA(htim2, TIM_DMA_CC1);如果使用DACDMA还要把DAC触发配置成定时器触发。此时整个回放过程CPU是空闲的可以继续处理下一段波形或通信。这个“硬件搬运而不是中断搬点”的设计是AWG固件最关键的一点一旦在定时器中断里逐点写DAC采样率很难做高而且抖动明显。4.3 触发与连续/单次模式AWG通常要有两种回放模式连续模式DMA循环搬运波形从头到尾不断重复适合看连续信号单次模式DMA正常模式搬完最后一个点就停等待下一次触发。单次模式在做瞬态激励时特别重要。触发来源可以是串口命令、外部引脚上升沿或定时器事件。低抖动设计应尽量让外部信号直接控制定时器/DMA而不是通过中断先翻转一下CPU。比如STM32的定时器可以配置外部触发输入作为从模式让外部脉冲直接启动主定时器触发延迟就固定在硬件层面不再是变量。还要注意“单次模式波形播放结束后下一次触发前要把DMA地址重置”。很多人第一次做触发第二次时波形变成乱的就是因为DMA搬运地址已经跑到缓冲区末尾没有重新指回开头。4.4 波形加载流程不管用串口、USB还是SD卡波形加载都建议走同一套流程先校验再入库最后再让回放逻辑取用。我以前犯过错波形数据还在接收过程中就启动回放结果输出到一半波形变了。正确顺序是上位机把波形数据和采样率配置打包发送MCU接收完整数据帧校验CRC校验通过后把数据拷贝到波形存储区用户触发“应用参数”命令更新定时器分频和DMA地址等待触发信号开始回放。这套流程虽然多了一步但能在问题出现时快速判断是通信问题还是固件问题。5. 波形编辑与上位机协议把“任意”落到点表5.1 从数学表达式到DAC点表AWG好玩的地方在于波形可以从任意地方来。可以来自Matlab脚本、传感器实际采集数据、电路仿真的瞬态结果也可以是手写的一串数组。但所有这些源最终都要变成DAC能“吃”的整数编码。这一步有三个关键点。第一必须考虑DAC的编码方式。很多DAC是二进制偏移码即0对应最低电压满量程对应最大电压也有些DAC是二进制补码0对应中间电压。搞错编码极性输出波形会变成“倒像”或者偏置错误。第二要保留浮点计算到整数的映射。设DAC位数为N参考电压范围0到VREF那么编码和电压关系是code (V / VREF) * (2^N - 1)如果输出会经过反向放大器或者加减偏置电路还要在软件里提前把目标电压转换成DAC引脚上的实际电压再进行量化。第三边界要削波。任意波形计算时偶尔会产生超出范围的负值或超过满量程的值不要硬写进DAC寄存器否则会回绕到另一端产生大幅跳变。应该在生成脚本里显式截断。5.2 一个生成突发脉冲串的Python示例我习惯用Python生成波形数据因为调试迭代快。下面这个例子生成一段带指数衰减包络的正弦猝发脉冲并导出成12位二进制文件import numpy as np SR 1_000_000 # 采样率1 MSa/s duration 20e-3 # 总时长20 ms t np.arange(0, duration, 1 / SR) f0 40_000 # 载波频率 40 kHz wave np.sin(2 * np.pi * f0 * t) # 在 2ms 到 18ms 之间施加指数衰减包络其他位置为0 burst np.zeros_like(wave) mask (t 2e-3) (t 18e-3) burst[mask] wave[mask] * np.exp(-(t[mask] - 2e-3) * 800) # 归一化到 12 位量程 0~4095 vref 3.3 code (burst / vref 0.5) * 4095 # 假设双极性输出被偏置到单极性 code np.clip(code, 0, 4095) code_u16 code.astype(np.uint16) # 小端序写文件 code_u16.tofile(burst.bin)这个脚本生成的文本就是DMA要搬的数据。你可以先把文件发到串口上位机再由上位机分包下发给设备也可以直接把同样的Python逻辑放在设备端MCU只接收数学参数在本地实时算波形点。低速场景下设备端计算可行但高速场景一定要预生成因为浮点正弦计算太慢不可能实时满足高采样率。5.3 传输帧格式与校验用串口或者USB传输波形数据时没有统一标准但一个保险的帧格式应该包含帧头、数据长度、采样率参数、波形模式、数据字段、CRC校验。我用的一个简化结构是这样的字段长度说明帧头2字节0xA5 0x5A数据长度2字节后面数据字段和参数部分的字节数采样率4字节32位无符号单位Sa/s模式1字节连续/单次/触发边沿数据N字节波形点表小端序CRC2字节对起始到数据末尾计算CRC16MCU端解析时先找帧头再读长度收满一帧后校验CRC。CRC不通过就丢弃不处理半包数据。如果是USB-HID设备一帧可能被拆成多个64字节包上位机需要做缓存和组包不要假设一包等于一帧。6. 实测校准平坦度、失真和幅值精度不能只靠标称值6.1 用正弦扫频看输出平坦度AWG做出来之后第一件事不是接上自己想要的复杂波形而是先输出标准正弦波做扫频。因为正弦波对系统带宽和失真的暴露最直接。我给自己的样机做过一组测试以1kHz正弦为参考先在1kHz处把幅度调到2Vpp并记下然后频率依次加到10kHz、50kHz、100kHz、300kHz、500kHz分别记录实际输出幅度。结果非常有意思在100kHz以内幅度基本平坦但到300kHz已经掉了约2dB到500kHz掉得更多偶尔还有轻微波动。这就是DAC的sin(x)/x衰减、模拟滤波器相位和运放增益带宽共同作用的结果。如果你希望高频段也保持恒定幅度需要在生成波形时做“逆sinc补偿”也就是在软件里把高频分量预先增益抬起来。频谱分析仪上看到镜像频率时就要检查重建滤波器是否接错了位置或者截止频率太高。6.2 失真和毛刺怎么测用示波器直接看波形只能发现明显失真真正要看频谱。可以用示波器FFT或者接一个简易频谱分析仪/声卡前端。重点看三样东西目标频率的谐波分量比如输出10kHz正弦时20kHz、30kHz处是否很大采样时钟镜像比如1MSa/s采样率下在990kHz附近有没有不该有的分量底噪和尖峰毛刺能量大的DAC会在整个频段上留下“抬高”的边带。如果发现毛刺能量明显优先检查DAC和运放的电源去耦。DAC模拟电源一定要单独滤波地线不要和数字信号线混在一起走回流。很多新手把DAC运放布线搞成“一根地线从单片机拉到输出端”结果数字噪声全串进模拟输出频谱底噪被抬得很高。6.3 幅值校准两点校正就够了输出级总是有增益误差和失调误差。校准方法很简单先让AWG输出一个和满量程一半对应的直流电平用万用表测实际电压再让AWG输出一个接近满量程的直流电平测第二个点。由这两个点可以算出一条直线V_actual k * V_target b然后把这个k和b存到设备里上位机生成波形前做逆变换V_to_output (V_target - b) / k。这样能补偿增益误差和偏置误差。但要注意两点校正不能消除DAC的非线性误差特别是INL积分非线性。如果应用对绝对精度要求高就要逐点生成校正表或者选择INL更低的DAC。输出阻抗也一定要在校准时考虑。很多信号源规定带50Ω负载输出如果你直接把示波器高阻输入接上去显示幅度会是标称的两倍。测量时要在输出端接50Ω负载或者用示波器的50Ω输入模式否则测出来的值没有参考意义。7. 最容易翻车的细节我把踩过的坑按优先级排了个序7.1 第一梯队波形的“硬件源”没打好如果把AWG比作一个播音员DAC就是嗓子参考电压和电源是气息。嗓子不行后面软件怎么修都有限。我的第一版样机在模拟电源上偷了懒直接用了单片机上的3.3V结果输出波形在每次数字总线刷新的瞬间都有毛刺。后来换成独立LDO供电、加磁珠隔离、地线做了单点连接毛刺立刻小了一个数量级。另一个常见问题是DAC数据线上下拉没处理好。并联DAC数据总线一旦时序不满足建立/保持时间有些点会读到错误编码波形上会出现“跳变到一个不该出现的电压”的瞬间单点毛刺。用示波器放大看是单点尖峰不是噪声而是数字时序问题。这时候不要急着换DAC先去查数据总线的setup/hold时序和FPGA/MCU的IO驱动强度。7.2 第二梯队固件时序和通信问题固件里最经典的问题是“DMA还没传完缓冲区就被覆盖了”。连续模式下输出一直在读波形数组如果上位机或者主循环同时往这个数组里写新数据波形就会在任意位置“断裂”。解决办法是使用双缓冲一个缓冲区给DMA回放另一个缓冲区接收新波形接收完整后再整体切换。MCU内部RAM不够时也可以用外部SRAM分两个区域切换指针而不是拷贝数据。触发问题也经常出现在“第二次触发无输出”。不少MCU的定时器从模式只支持一次触发触发之后需要重新配置从模式寄存器。排查这类问题时不要只盯着DMA状态还要看定时器是否还处于等待触发状态。建议在每次触发后打一个日志记录触发次数和DMA剩余计数能快速定位。通信层最大的坑是“上位机发送太快MCU串口接收缓冲区溢出”。解决思路很简单增加流控MCU每处理完一包就回复一个ACK上位机等收到ACK后再发下一包。没有流控批量下发看起来省事实际丢包重传更头疼。7.3 第三梯队你以为调好了实际没调好的细节最后一个容易翻车的地方是“输出线缆和示波器探头带来的负载效应”。AWG输出接长同轴线后如果末端是高阻探头传输线上的反射会叠加在波形上导致过冲或振铃。高频输出时要用50Ω同轴电缆并终端匹配而不是用一根杜邦线接到示波器高阻上。我自己的体会是做AWG最大的乐趣不是把系统调得“看起来能出波形”而是逐渐理解每一个模拟细节对最终结果的影响。下次你看到商业信号源的高价反复会想原来里面塞了那么多没人注意的功夫——从参考电压、DAC毛刺、滤波器曲线到输出级的50Ω匹配每一步都是直接决定波形能不能“任意”的关键。无论是自己做一个低成本的实验工具还是学习信号链路设计AWG都是一块特别好的磨刀石。