简介:这份文档面向通信原理、电子信息工程等专业的课程设计学习者,围绕A/D与D/A转换器的Matlab/Simulink仿真展开,可用于课程设计参考、实验报告撰写与仿真建模入门练习。内容先从原理入手,讲清模拟信号的采样、幅度离散化与编码,以及数字序列经低通滤波器还原模拟信号的正反过程;随后给出基于Simulink通信模块库的建模方案,逐条说明直流源、零阶保持器采样、量化器、整数转比特、缓冲串并转换等模块的作用与参数设置,并以输入常数17.5、量化后输出18为例分析波形与结果,其中对采样定理与量化误差的讨论便于读者对照理解。资源包共1个docx文档,约78KB,涵盖设计原理、仿真电路、模块说明、结果分析与总结,结构完整便于复现。已有338人学习下载,适合希望快速搭建A/D、D/A仿真链路并整理报告的中初级读者。
1. AD转换器和DA转换器仿真到底在仿什么
做一块 16 通道的数据采集板,采样率定 100 kSPS 还是 1 MSPS,ADC 选 12 位还是 16 位,前端抗混叠滤波器的截止频率压到多少,重建端又该用几阶滤波器——这几个数字一旦拍错,板子回来发现噪声底比预算高十几 dB,改版就是几周起步。AD转换器和DA转换器的仿真,就是把这一串判断从硬件阶段前移到模型阶段:用一组可执行的数学模型,把连续模拟量和离散码字之间的那座桥完整走一遍,看清量化、采样抖动、重建保持、单元失配各自往总误差里贡献了多少。
它解决的从来不是「画一条正弦」这么简单的事,而是在给定信号带宽、目标信噪比和器件约束之后,把链路预算算清楚。做音频接口、电机电流环、射频收发前端、传感器调理电路的工程师,都会在这件事上反复花时间。后面按 AD 采样链路、DA 重建链路、指标计算、闭环联调的顺序往下走,代码全部用 Python + Numpy 就能跑起来。
2. AD转换器仿真:从连续电压到量化码字
把 AD 转换过程拆开,无非是两个动作串起来:先在时间轴上采样,再在幅度轴上量化。这两个动作各自引入一类误差,而且它们的统计特性完全不同——混叠误差是确定性的,取决于滤波器抑制能力;量化误差在理想情况下是均匀分布的随机量,可以精确算出来。分清这两者,后面才知道该优化哪一头。
2.1 采样保持与量化器的数学描述
采样环节的输出可以写成x_s[n] = x(n·Ts + τ),其中Ts是采样周期,τ是实际孔径时刻相对理想时刻的偏移。当τ存在随机波动时,它就从单纯的相位偏移变成了误差源,这就是后面要讲的时钟抖动。
量化环节则是把连续幅度切成有限个台阶,每个台阶宽度为 1 个 LSB:
LSB = 2 · Vref / 2^N理想量化器的输出是输入信号减去量化误差,而量化误差在[-LSB/2, LSB/2]上近似均匀分布,其功率为LSB²/12。把这个结果代进满量程正弦波(幅度A = 2^(N-1)·LSB,信号功率A²/2)就能直接推出那条约等于6.02N + 1.76的经典公式。推导过程本身比结论有价值,因为它暴露了一个前提:输入必须是满量程正弦,且量化误差必须是白的。
| 位数 N | 理论 SNR (dB) | LSB/Vref 占比 |
|---|---|---|
| 8 | 49.9 | 0.781% |
| 10 | 61.96 | 0.195% |
| 12 | 74.0 | 0.0488% |
| 14 | 86.04 | 0.0122% |
| 16 | 98.08 | 0.00305% |
2.2 用 Numpy 搭一个 N 位理想 ADC 并复现 SNR 公式
先写一个参数化的理想 ADC,把它跑起来,再用仿真结果去对公式。
import numpy as np def adc_ideal(x, nbits, vref=1.0, vcm=0.0): """理想 N 位 ADC。x 为输入电压数组,范围 [-vref, vref]""" lsb = 2.0 * vref / (2 ** nbits) # 输入平移后按最近码字取整,得到整数码 code = np.round((x - vcm + vref) / lsb) code = np.clip(code, 0, 2 ** nbits - 1).astype(np.int64) # 反量化回电压,中码字对齐 0V xq = (code - 2 ** (nbits - 1)) * lsb - vcm return code, xq nbits, fs, N = 12, 100e6, 8192 M = 245 # 与 N 互质,满足相干采样 fin = M / N * fs # ≈ 2.99 MHz t = np.arange(N) / fs x = 0.999 * np.sin(2 * np.pi * fin * t) code, xq = adc_ideal(x, nbits) q = x - xq # 量化误差 snr_sim = 10 * np.log10(np.sum(x ** 2) / np.sum(q ** 2)) print(f"仿真 SNR = {snr_sim:.2f} dB, 理论 = {6.02 * nbits + 1.76:.2f} dB")这段代码里三个参数必须交代清楚。nbits决定 LSB 大小,改它就直接改 SNR;M取质数且保证M/N不可约,是为了让信号频率正好落在 FFT 的某一个 bin 上,避免频谱泄漏把噪声底抬高,这一步在行业里叫相干采样;输入幅度取0.999而不是 1.0,是为了避开削波同时只损失约 0.01 dB 的信噪比。跑出来的结果应该在 73.9 dB 附近,和理论值差不到 0.2 dB。如果差得远,先检查M是否和N互质。
2.3 时钟抖动与非线性的建模方法
理想模型对不上真实器件,主要差在两处:采样时刻不是均匀的,量化台阶也不是等宽的。
时钟抖动的处理是把采样时刻变成随机变量。抖动引入的误差近似等于信号导数乘以时间偏差,对正弦输入可以直接给出信噪比上限:
rng = np.random.default_rng(1) sigma_t = 1e-12 # 1 ps RMS 时钟抖动 x_j = 0.999 * np.sin(2 * np.pi * fin * (t + rng.normal(0, sigma_t, N))) _, xq_j = adc_ideal(x_j, nbits) snr_j = 10 * np.log10(np.sum(x_j ** 2) / np.sum((x_j - xq_j) ** 2)) # 解析上界:SNR = -20*log10(2*pi*fin*sigma_t) snr_limit = -20 * np.log10(2 * np.pi * fin * sigma_t) print(f"含抖动仿真 {snr_j:.2f} dB, 抖动上界 {snr_limit:.2f} dB")在 2.99 MHz 输入、1 ps 抖动的条件下,这个上界大约 94.5 dB,折算成位数约 15.4 位。结论很直接:12 位 ADC 配 1 ps 抖动完全够用,但如果你打算上 16 位,抖动就得压到 0.2 ps 以下,否则抖动会接管噪声预算。这个换算在选时钟源的时候比任何仿真都省事。
非线性则要改量化阈值,而不是改输出码字。给每个跳变电平加一个以 LSB 为单位的偏差,就得到带 DNL 的模型:
def adc_with_dnl(x, nbits, dnl, vref=1.0): """dnl 长度应为 2**nbits - 1,单位 LSB""" lsb = 2.0 * vref / 2 ** nbits k = np.arange(2 ** nbits - 1) th = (-2 ** (nbits - 1) + 0.5 + k) * lsb + dnl * lsb th = np.sort(th) # 防止非单调 code = np.searchsorted(th, x).astype(np.int64) xq = (code - 2 ** (nbits - 1)) * lsb return code, xq dnl = np.zeros(2 ** nbits - 1) dnl[[100, 101, 102]] = [0.4, -0.5, 0.6] # 局部台阶不齐 _, xq_dnl = adc_with_dnl(x, nbits, dnl)np.sort那一行别省。当 DNL 超过 ±1 LSB 时阈值会交叉,转换曲线出现非单调,searchsorted会给出错误的码字序列。真实器件说明书里的「无丢码」保证,本质上就是在约束这件事。
3. DA转换器仿真:从码字重建模拟波形
AD 是把模拟世界压成数字,DA 是反过来把数字重新铺回模拟世界。方向反了,误差的形态也换了:DA 的主要敌人不是量化噪声,而是零阶保持带来的频响凹陷、重建滤波器的相位失真,以及电流舵单元之间的失配。
3.1 零阶保持的频响与 sinc 衰减定量关系
DAC 每一拍输出一个码字对应的电平,并保持整整一个更新周期,这就是零阶保持。它在频域上的响应是一个 sinc 函数:
def zoh_gain_db(f, fs): """零阶保持的幅度响应,单位 dB""" return 20 * np.log10(np.abs(np.sinc(f / fs))) fs_dac = 100e6 for f in [1e6, 10e6, 25e6, 40e6, 50e6]: print(f"{f/1e6:>4.1f} MHz : {zoh_gain_db(f, fs_dac):.2f} dB")跑出来在 50 MHz 处大约是 -3.92 dB,在 25 MHz 处约 -0.9 dB。注意np.sinc的定义是sin(πx)/(πx),恰好就是零阶保持的归一化响应,不用自己写。这个衰减意味着:如果信号带宽一直用到奈奎斯特频率,高频部分的幅度会被削掉将近 4 dB,幅频平坦度直接崩掉。常见的应对是两种,要么提高 DAC 更新率做数字过采样,让信号落在远低于 fs/2 的位置;要么在数字端加一个反 sinc 补偿滤波器,把这段衰减提前抬回来。
3.2 重建滤波器的设计参数与仿真验证
在 Python 里验证重建滤波器,用scipy.signal出系数就够了:
from scipy import signal import numpy as np fs_dac, fc, osr = 100e6, 12e6, 8 n_taps = 129 # 在过采样率 osr 下设计,等效截止频率 fc b = signal.firwin(n_taps, fc / (fs_dac * osr / 2), window='kaiser', fs=fs_dac * osr) w, h = signal.freqz(b, worN=8192, fs=fs_dac * osr) mag_db = 20 * np.log10(np.abs(h) + 1e-12) # 通带波纹与阻带抑制 pass_idx = w <= fc stop_idx = w >= 2 * fc print(f"通带波动 {mag_db[pass_idx].max() - mag_db[pass_idx].min():.3f} dB") print(f"阻带抑制 {mag_db[stop_idx].max():.1f} dB")三个参数值得逐个盯。fc是通带边界,取信号最高频率再留 20% 余量;osr是过采样倍数,它把数字滤波器的过渡带拓宽了osr倍,所以同样的抽头数能换来更陡的滚降;n_taps直接决定阶数,阶数不够时阻带抑制先崩。判断滤波器是否合格,看输出波形的两个数:通带波纹控制在 0.1 dB 以内,阻带抑制在2·fc之后要压到 -70 dB 以下,否则 DAC 的采样镜像会漏进后级放大器。
另外把零阶保持跟重建滤波器串起来算总响应时,别忘了一个容易忽略的点:反 sinc 补偿最好放在数字滤波器内部一起设计,而不是在模拟端单独加一级,否则两级滤波器的相位叠加会让群时延变得很难看。
3.3 DAC 失配与毛刺的建模方法
真实 DAC 的每一位权重不会精确等于 2 的幂次方。给每个比特加一个相对偏差,就能模拟出这种失配:
def dac_binary(codes, nbits, sigma=0.0, seed=0): rng = np.random.default_rng(seed) w = 2.0 ** np.arange(nbits, dtype=float) w = w * (1 + rng.normal(0, sigma, nbits)) # 单元权重失配 bits = (codes[:, None] >> np.arange(nbits)) & 1 y = bits @ w return 2 * y / w.sum() - 1 # 归一化到 [-1, 1)sigma建议从 0.001 起,按 0.001 步进往上加,观察 SFDR 掉到多少。这里有个反直觉的现象:失配带来的不是噪声底抬升,而是固定的谐波杂散,所以蒙特卡洛跑出来的均值不会好看,得看谱线位置。这也是为什么高精度 DAC 普遍改用温度计码或分段结构——把大权重的跳变拆成多个小单元的轮转,失配被平均掉。
毛刺建模稍微麻烦一点,因为它是每个码字跳变处的瞬态:
def add_glitch(x, alpha=0.05, tau=0.3): y = x.copy() d = np.diff(x, prepend=x[0]) for i in np.where(np.abs(d) > 1e-9)[0]: n = np.arange(len(y) - i) y[i:] += alpha * d[i] * np.exp(-n / tau) return yalpha是毛刺相对跳变幅度的比例,tau控制衰减快慢,用采样点数计。这个模型是近似的,但它能告诉你一件事:当输入是高频大幅信号时,毛刺能量集中在跳变最密的时刻,用 FFT 分析会表现为宽带底噪抬升。用 RZ(归零)编码或增加输出端的去毛刺采样保持电路,一般都是针对这一点。
4. 用 FFT 频谱算 SNR、ENOB 和 SFDR
模型搭完了,接下来得有统一的尺子去量。AD/DA 仿真的通用做法是对输出序列做 FFT,把能量分类:信号 bin、谐波 bin、噪声底,然后分别算比值。这一步最容易出错,因为窗函数、bin 数和直流剔除方式稍有不同,同一组数据能算出差 5 dB 的结果。
4.1 相干采样条件与窗函数的选择
相干采样的条件很明确:fin/fs = M/N,M为质数,N为 2 的幂。满足这个条件时信号能量完全落在单个 bin 内,不需要加窗,频谱最干净,也最适合做精确的指标计算。代价是输入频率被约束死了,测真实器件的时候很难凑。
非相干采样就必须加窗。窗函数的选择看两个指标:
| 窗类型 | 主瓣宽度 (bin) | 旁瓣抑制 (dB) | 适用场景 |
|---|---|---|---|
| Rectangular | 2 | -13 | 相干采样 |
| Hann | 4 | -31 | 通用测量,泄漏适中 |
| Blackman-Harris | 8 | -92 | 小信号杂散测量,需高动态范围 |
| Flat-top | 10 | -93 | 幅度精度优先的幅频测试 |
主瓣越宽,靠近信号 bin 的杂散就越容易被「藏」进主瓣里算不到,所以测小杂散时必须用旁瓣抑制好的窗型,同时把信号附近的若干个 bin 从噪声统计里剔除。
4.2 从 FFT 频谱算 SNR、ENOB 和 SFDR 的完整脚本
把前面几节的结果接上,一个完整的指标计算函数如下:
def spec_metrics(x, fs, win='rect', sig_bins=5, dc_bins=4): """从时域序列计算 SNR / ENOB / SFDR""" N = len(x) if win == 'hann': w = np.hanning(N) elif win == 'bh': w = np.blackman(N) else: w = np.ones(N) # 相干采样时不加窗 X = np.abs(np.fft.rfft(x * w)) ** 2 freq = np.fft.rfftfreq(N, 1 / fs) k0 = np.argmax(X[1:]) + 1 # 跳过直流找信号 bin lo, hi = max(k0 - sig_bins, 1), min(k0 + sig_bins + 1, len(X)) Ps = X[lo:hi].sum() # 信号及邻近泄漏 Xn = X.copy() Xn[lo:hi] = 0 Xn[:dc_bins] = 0 # 剔除直流与近零频 Pn = Xn.sum() # 剩余全部算噪声+杂散 snr = 10 * np.log10(Ps / Pn) enob = (snr - 1.76) / 6.02 sfdr = 10 * np.log10(Ps / Xn.max()) # 最大杂散相对信号 spur_f = freq[np.argmax(Xn)] return dict(snr=snr, enob=enob, sfdr=sfdr, spur_f=spur_f) print(spec_metrics(xq, fs)) # 前面 12 位 ADC 的输出sig_bins取 5 是折中:太小会把信号自身的泄漏算进噪声,导致 SNR 偏低;太大又会把真实的近端杂散吃掉,导致结果虚高。用矩形窗相干采样时取 3 到 5 都合理,用 Blackman-Harris 这类窗时因为主瓣宽达 8 个 bin,至少要放到 10。dc_bins是剔除直流和超低频的分量,实际板上往往还有 50 Hz 工频和电源纹波,测 ADC 动态指标时必须排除,否则算出来的全是这些。
spur_f这个返回值容易被忽略,但它才是定位问题的关键。SFDR 差的时候,先看杂散落在哪个频率上:落在信号频率的整数倍上,是量化器非线性;落在M/N相关的固定位置,是采样时钟的杂散耦合;落在随机的低频率上,多半是电源或参考电压的问题。
4.3 指标对照表与三个常见误读
| 指标 | 定义 | 典型期望(12 位) | 主要来源 |
|---|---|---|---|
| SNR | 信号功率 / 积分噪声功率 | 70~74 dB | 量化噪声、热噪声 |
| SNDR | 信号功率 /(噪声 + 谐波) | 68~73 dB | 上述两者之和 |
| SFDR | 信号功率 / 最大杂散 | 80 dBc 以上 | DNL、失配、时钟 |
| ENOB | (SNDR − 1.76) / 6.02 | 11~11.8 位 | 综合 |
三个误读值得点出来。第一,把 SNR 等同于 ENOB,实际上一旦有谐波,SNDR 才是有效位数的来源,用 SNR 算出的 ENOB 会虚高一到两位。第二,忽略 FFT 长度的影响,N越大噪声底的统计越接近真实,用 1024 点算 16 位 ADC 的噪声底基本没意义,N至少取 8192,最好 65536。第三,混淆 dBFS 和 dBc,前者相对满量程,后者相对信号本身,做器件对比时必须统一,否则数据没法横向比。
5. 联合链路验证与仿真发散排查
单测完 AD 和 DA,最后一步是把两者串起来跑闭环。这一步的价值在于暴露那些单独测发现不了的耦合问题,比如 AD 的量化误差被 DA 重建后放大,或者 DA 的毛刺通过反馈回路重新耦合回采样端。做法很直接:把 AD 的输出码字直接喂给 DA 模型,中间不加任何补偿,看重建波形和原始模拟波形的差:
# 闭环:前文 12 位 ADC 的输出码直接送进 12 位 DAC xq_rec = dac_binary(code, nbits=12, sigma=0.002, seed=42) * 1.0 err_closed = x - xq_rec snr_closed = 10 * np.log10(np.sum(x ** 2) / np.sum(err_closed ** 2)) print(f"闭环 SNR = {snr_closed:.2f} dB")闭环跑完通常会看到比单测低 3 到 6 dB,这部分损失来自量化误差的二次叠加和失配。如果损失超过 8 dB,说明两条链路的噪声预算分配不平衡,得回去重新看是哪一级吃掉了余量。蒙特卡洛扫sigma从 0.0005 到 0.01,每次跑 100 组取均值和标准差,就能把失配预算反推出来,直接对应到器件选型上。
关于「仿真发散」,在 AD/DA 这类模型里通常不是数值积分炸掉,而是几个更隐蔽的表现形态。下面这张排查表是我一般会按顺序过一遍的:
| 现象 | 典型原因 | 处理办法 |
|---|---|---|
| FFT 本底随点数增加不断抬高 | 非相干采样且未加窗 | 改用互质 M/N,或换 Blackman-Harris 窗并加宽 sig_bins |
| 12 位 ADC 仿真只有 62 dB | 输入幅度不足或直流未剔除 | 幅度取 0.99 满量程,dc_bins至少设 4 |
| DAC 波形在 fs/2 附近凹陷 | 零阶保持 sinc 衰减未补偿 | 提高过采样比,或数字端加反 sinc 滤波器 |
| 瞬态仿真跑到毫秒级不收敛 | 码字跳变处步长被反复压缩 | 设最大步长上限,给 DAC 输出加最小转换时间 |
| 换种子结果差很多 | 失配/抖动样本数不够 | 蒙特卡洛跑 100 次以上,看均值与标准差 |
最后这一条提醒尤其值得留意:罚函数式的「换一组随机数结果就变脸」,往往说明样本量不足而不是模型本身有问题。把蒙特卡洛次数从 10 提到 200,如果标准差收敛到 0.3 dB 以内,模型就算是可信的了。相比之下,一味去调步长或者换求解器,反而是在给一个统计上还没站稳的结论做无用功。
本文还有配套的精品资源,点击获取