前阵子做一个可调模拟信号源的时候我一开始用的是定时器输出 PWM再用两级 RC 低通滤成直流。调到 1kHz 以下还算能看一旦想精确输出 1.234V 这种电平偏差和纹波就冒出来了。后来我把方案整个换成了独立的 DAC 数模转换控制器MCU 负责算数和下发DAC 芯片直接给出模拟电压。这个改动让信号链一下子干净了不少值得我专门写一篇把整个过程记录下来从选型、原理到校准和排障一起说清楚。这篇文章适合手里有单片机开发经验、但又不太熟悉模拟链路设计的人。如果你也遇到过 PWM 滤波方案精度不足、输出纹波偏大或者想做一个可以程控的模拟电压源、波形发生器、音频电平输出那么这篇内容应该能帮你直接落地。1. 从“PWM 凑合”到“独立 DAC”这个控制器解决了我的什么痛点1.1 我原来的 PWM 方案到底差在哪很多入门项目里大家习惯用 PWM 加低通滤波来“假装”一个模拟输出。原理不复杂MCU 输出高低电平占空比决定平均电压RC 滤波器把方波的高频分量滤掉剩下的直流分量就等于占空比乘以电源电压。听起来很顺但实际做精密输出时会遇到几个很现实的问题。第一是分辨率限制。普通定时器做 PWM 时计数器位数通常是 8 位到 16 位如果频率也受到限制实际可用的有效占空比档位可能只有几百到几千级。对一个 3.3V 供电的系统10 位分辨率对应的最小电压步进大约是 3.2mV想要输出 1.000V 和 1.003V 这样接近的电压往往已经到极限了。第二是纹波不好压。想要纹波小就得把 RC 时间常数拉大但时间常数一大输出电压的响应就变得很慢尤其在低频扫频信号里输出会明显滞后。第三是多路输出互相牵扯。如果你要同时输出两三个模拟量PWM 通道之间会占用同一个定时器资源改一路的频率或相位另一路也跟着受影响。这些坑单看不致命叠加起来就很麻烦。尤其我那个项目要输出精确到毫伏级别的直流电压PWM 加滤波的方案怎么调都差点意思。1.2 独立 DAC 能带来的实际变化换成 DAC 数模转换控制器以后MCU 不需要再花心思去维护 PWM 占空比和滤波参数只需要把数字码值通过 I2C 或 SPI 发给 DAC 芯片芯片内部就会把数字量转成一个真实的模拟电压。整个过程直接得多也稳定得多。举一个很常见的例子你要输出 2.500V12 位 DAC 在 5V 参考电压下每位对应的电压大约是 1.22mV计算下来正常写码值就能得到接近目标值的输出配合校准可以做到毫伏级误差。而同一需求用 PWM 方案你得一边算占空比一边调滤波参数还可能被温度漂移和电容老化折磨。这个控制器能用到的地方也很广泛。做信号发生器时需要产生正弦波、三角波、任意波形仪表类项目里需要可调的基准电压音频项目里要把数字音频流变成模拟声音电源方案里要给运放或者功率器件提供精确的偏置电压。可以说凡是需要“程序可控的模拟输出”都能用上这个方案。1.3 这篇博文覆盖的范围我不会去讲射频级别的超高速 DAC也不会深入 Δ-Σ DAC 的调制细节。这里的 DAC 数模转换控制器主要指单片机周边常见的中低速通用 DAC位数从 8 位到 16 位输出为电压型或者带内置缓冲的模块。整体重点放在“怎么选、怎么接、怎么调准、怎么排错”这四个问题上。2. DAC 内部到底在做什么从分辨率到建立时间2.1 “位数越多越准”这个说法有一半是错的DAC 的分辨率简单说就是数字量变化一位时模拟输出变化多少。计算方式不复杂用参考电压除以量化档位数。比如一个 12 位 DAC参考电压 5V量化档位数是 4096每一位对应的电压理论值是 1.22mV。公式大致是这样LSB VREF / 2^N其中 N 是位数VREF 是参考电压。16 位 DAC 不是简单地把 12 位的精度提升四倍而是把量化步进缩小到了 LSB 级别。但实际芯片还有几个误差指标这些指标往往比位数更能决定最终精度。INL 是积分非线性误差也就是实际输出曲线和理想直线之间的最大偏差。DNL 是微分非线性误差表示相邻两个数字码之间实际步进和理论步进之间的差。除此之外还有偏移误差和增益误差。偏移误差指数字码为 0 时输出电压不是 0增益误差指满量程输出和理论值整体偏离的比例。所以在看芯片时不能只看标称“16 位”还要看它在工作温度范围内的 INL 和 DNL 到底是多少。有的 16 位芯片在 25℃ 下表现很好但温度一上去误差就翻倍。反过来一些 12 位 DAC 配上精密外部基准和校准算法实测精度反而可能优于“标称 16 位但基准很烂”的方案。2.2 电压输出型、电流输出型和内置缓冲的区别DAC 按输出形式分有电压输出型和电流输出型两类。电压输出型最常见内部直接用 R-2R 梯形电阻网络或者电容阵列产生输出电压通常是单端输出。电流输出型更像是把数字量转换为对应的电流后面通常还要接一个外部运算放大器把电流变成电压这种方式常用于高速射频或通信系统里不太适合我们这种简单控制场景。输出电压还有个“带不带缓冲”的问题。带缓冲的 DAC输出可以直接带动一定的负载比如几 kΩ 到几十 kΩ 的输入阻抗使用上省事。不带缓冲的 DAC 输出阻抗较高后级必须要加运算放大器做阻抗变换否则一接负载电压就塌下去。我这次选型时优先选了带缓冲的模块省掉了一颗运放。但带缓冲也有一个副作用很多芯片的缓冲放大器输出范围不是真正的 0V 到 VREF最低可能到几十毫伏就到顶了。如果你需要真正的 0V 输出得仔细看数据手册里的输出范围说明必要时还是得加外部电路处理。2.3 建立时间和输出响应DAC 不是瞬间就稳定下来的。数字码写入后内部电容和电阻网络需要时间充电运放缓冲也需要爬坡这个时间叫建立时间。常见的中低速 DAC 建立时间从几微秒到几十微秒不等。对直流电压输出来说建立时间长短无所谓你怎么写码都来得及。但如果你要输出正弦波、方波这类动态信号建立时间就直接决定了最高更新速率。比如设定时间 10μs理论上每秒最多更新十万次但还要算上通信时间实际能有几万次就不错了。这一点在后面的实测排障里我会再展开。3. 怎么选“适合自己”的 DAC 芯片接口、通道、基准三维交叉3.1 通信接口决定了你能跑多快DAC 和 MCU 之间的通信方式最常见的是 I2C、SPI 和并行接口。I2C 的优势是引脚少只需要两根线可以挂在总线上并联多个设备。缺点是速度上限通常不高标准模式 100kHz快速模式 400kHz高速模式能到几 MHz 但大部分 MCU 支持不好。对直流输出或者几十 Hz 的波表输出I2C 完全够用想跑到几千 Hz 以上的波形就可能捉襟见肘。SPI 有四根线SCK、MOSI、MISO、CS。它速度更快常见能跑到几 MHz 到几十 MHz而且收发时序更灵活。缺点是每个设备要占用一个片选引脚设备多了引脚压力大。我实际调波形的时候最后就是从 I2C 换成了 SPI 才解决带宽问题。并行接口一般只在非常高速的 DAC 上使用一次传输 8 位或 16 位数据速度可以做到很高但会占用大量 MCU 引脚普通小项目基本不用考虑。下表是我自己选型时的判断维度对比维度I2C 接口SPI 接口并行接口引脚占用2 根可多设备4 根左右每设备占片选8 到 16 根典型更新速率几百 Hz 到几 kHz几十 kHz 到几 MHz几 MHz 以上适用场景直流输出、低速波表动态信号、多通道高速射频、视频开发难度低中中高3.2 通道数单通道、双通道、还是同步多通道通道数不是越多越好。对大部分项目单个确定性输出就够用。需要同时输出双路电压时优先选双通道 DAC省体积也省代码。但要注意“同步输出”和“顺序输出”的差异。很多 DAC 芯片有多个通道但只有一个内部数据寄存器。你往通道 A 写数据再往通道 B 写数据输出是分时更新的通道 A 已经在输出新值了通道 B 还是旧值。如果你的应用需要两个通道严格同时变化就要选带同步更新机制的芯片或者用额外的负载存器控制数据到什么时刻才统一锁存到输出。做双通道信号发生器时这一点非常重要否则两个正弦波之间的相位关系完全没法保证。3.3 基准电压最容易被人忽略的精度来源DAC 输出的满量程电压直接取决于参考电压 VREF。如果 VREF 用的是 MCU 的 3.3V 电源而 3.3V 本来就有几十毫伏的波动那 DAC 输出再准也白搭误差会原封不动地映射到输出端。所以比较讲究的做法是给 DAC 配一颗独立的外部基准电压源。基准芯片的温漂指标从几十 ppm/℃ 到几 ppm/℃ 都有温度变化 20℃误差可能从 0.1% 级别降到 0.01% 级别。如果只是简单实验用一颗精度 0.5% 的基准芯片就够如果要做仪表级输出就得挑低温漂、低噪声的基准芯片。我这次做的模块就单独留了 VREF 引脚没有和 MCU 电源直接短接。实测效果很明显换上独立基准后输出电压的稳定性肉眼可见地变好万用表读数尾数跳动也少了。3.4 我总结的选型顺序先确认你需要几位再看输出类型再决定接口最后盯着基准和温漂。多数 8 位 DAC 只适合做指示灯亮度控制或者粗调节12 位是性价比最高的档位做信号源和一般控制项目都合适14 位和 16 位适合仪表和精密音频但接好电路没那么容易噪声稍微处理不好后面几位就废了。建议新手先拿 12 位做通一个完整链路再考虑要不要往上升。4. 真做一次搭一个 05V 可调信号源与驱动缓冲4.1 项目思路我做的这个 DAC 数模转换控制器模块目标是接受 MCU 下发的数字码输出一个在 0 到 5V 之间可调的稳定电压。选用的核心是一颗 12 位、I2C 接口、带输出缓冲的 DAC 芯片再加上一个外部基准源和必要的滤波电容。控制端用常用的 MCU 开发板先把单电压输出调通再接一个低速正弦波测试动态性能。整体信号链是MCU - I2C 总线 - DAC 芯片 - 输出缓冲 - 负载。中间的每个环节都需要单独检查不能一上来就全接好。我习惯先把 DAC 输出端空载跑通再逐步加载这样排错范围小很多。4.2 接线细节接线看起来简单但有几个细节直接影响稳定性。SDA 和 SCL 必须接上拉电阻一般用 4.7kΩ 左右连接到 DAC 的供电电压。上拉电阻不接I2C 时序就会乱芯片偶尔能跑通偶尔卡死。MCU 和 DAC 模块尽量共地电源线也尽量短防止回路压降影响基准。DAC 的 VREF 引脚我用一颗独立基准芯片提供基准输出端对地并一个 10μF 胆电容和一个 0.1μF 陶瓷电容用来滤掉低频波动和高频噪声。输出端到后级负载之间如果负载阻抗低最好再接一级运算放大器缓冲。很多 DAC 内部缓冲只能驱动几毫安电流带个重负载就会往下塌。我最终的接法大概是这么几条VDD5V 供电对地并 10μF 和 0.1μF 电容VREF独立基准源输出滤波电容就近放置SDA / SCL接入 MCU标注接 4.7kΩ 上拉电阻上拉到 VDDVOUT接缓冲运放输入端运放输出再接负载AGND / DGND在模块底部单点相连不在两侧分别接地4.3 软件驱动要点DAC 的软件驱动核心就是把要输出的电压值换算成数字码再按照芯片要求的格式写进去。假设电压和码值是线性关系那么code (Vout / VREF) * 4095这个公式很基础实际写代码时还要处理浮点和整数转换以及在边界处做限幅。下面是一段简化驱动的示意代码实现把 12 位码值通过 I2C 写入 DAC。真实芯片可能有控制字节但思路一致。#include stdint.h #define DAC_I2C_ADDR 0x60 static uint16_t current_code 0; void dac_write_code(uint16_t code) { uint8_t frame[2]; // 12 位数据放在高 12 位之后根据具体芯片格式调整 code 0x0FFF; current_code code; frame[0] (code 8) 0x0F; frame[1] code 0xFF; i2c_write(DAC_I2C_ADDR, frame, 2); } void dac_set_voltage(float vout, float vref) { uint16_t code; if (vout 0) { code 0; } else if (vout vref) { code 4095; } else { code (uint16_t)((vout / vref) * 4095.0f 0.5f); } dac_write_code(code); }写码之后不要立刻测量先等一段时间让 DAC 完全稳定我一般在驱动里加一个延时或者直接在测试程序里等 10 到 20ms。刚上电时有些芯片内部还在自校准立马读输出容易读到中间态。4.4 加一级低通滤波和驱动缓冲虽然 DAC 输出本身就是模拟量但高频噪声和数字开关噪声还是可能存在。我在输出端加了一个简单的一阶 RC 低通截止频率设在 30kHz 左右对直流输出没有影响却能滤掉不少高频毛刺。后级运放我用的是一颗轨到轨输出型运放单电源供电输入接 DAC 输出输出直接连到测试设备。运放供电要加去耦电容并且输出端不要直接接大电容负载否则容易自激振荡。如果必须带容性负载可以在运放输出端串联一个小电阻隔离。5. 校准与标定不要迷信出厂误差自己先把万用表准备好5.1 为什么出厂精度不能全信DAC 芯片出厂前会标称精度但实际到板子上之后参考电压的误差、PCB 走线电阻、电源噪声、运放偏移都会叠加进来。尤其是很多模块级 DAC周边电路设计得比较随意实际输出和目标值之间很可能差出几十毫伏。做实验也许无所谓做仪表就绕不过去。所以我的习惯是每一块 DAC 模块焊接完成后都做一次现场标定把偏移误差和增益误差记下来写进校准参数里。5.2 两点校准法实操两点校准时选两个参考点一个靠近低端一个靠近高端。低端点选在满量程的 5% 左右高端点选在满量程的 95% 左右。这样能避免边界处非线性误差过大同时覆盖大部分实际使用的范围。假设低端写入码值 code_lo实测电压为 v_lo高端写入码值 code_hi实测电压为 v_hi。斜率就是slope (v_hi - v_lo) / (code_hi - code_lo)偏移量就是offset v_lo - slope * code_lo之后想输出目标电压 v_target校正后的码值为code_target (v_target - offset) / slope我实际测出来的一组数据是这样。VREF 设为 5V写入低端码值 200实测电压约 0.2448V写入高端码值 3900实测电压约 4.772V。算下来 slope 约 1.2215mV/LSBoffset 约为 0.2mV。如果不校准直接按理论公式算很多目标点会偏离 1 到 2 个 LSB校准之后误差普遍压到 1mV 以内。对应的代码可以这样写float cal_slope; float cal_offset; void cal_init(float v_lo, float v_hi, int code_lo, int code_hi) { cal_slope (v_hi - v_lo) / (code_hi - code_lo); cal_offset v_lo - cal_slope * code_lo; } uint16_t cal_code_for(float v_target) { float code_f (v_target - cal_offset) / cal_slope; if (code_f 0.0f) { code_f 0.0f; } else if (code_f 4095.0f) { code_f 4095.0f; } return (uint16_t)(code_f 0.5f); }校准参数一般放在 MCU 的 Flash 或 EEPROM 里每次开机时读取。如果板子要在不同温度下工作更讲究的做法是做多温度点校准但在我们这种普通项目里两三点就够了。5.3 校准后的实测对比为了直观说明校准效果我列一组不同目标电压的实测数据目标电压校准前实测校准后实测绝对误差0.500V0.5051V0.5004V0.4mV1.000V1.0060V0.9997V0.3mV2.500V2.5078V2.5002V0.2mV4.000V4.0089V3.9998V0.2mV校准不是万能的。如果芯片本身非线性很差两点校准中间段的误差可能仍然很大。那种情况就要考虑用多点分段校准或者直接换更高线性度的芯片。5.4 长期稳定性也要盯着校准完不代表一劳永逸。基准芯片会老化温度变化会引起漂移。同一块板子放几个月重新测输出电压可能已经偏移了几个毫伏。所以定期校正是必要的特别是用在关键设备里时最好每次开机都跑一次自检用内部 ADC 回读 DAC 输出和校准值比较误差超限就报警。6. 实测排障记录三个让我浪费了半天的问题6.1 问题一输出最低值不为零现象很典型写码值 0万用表测出来还是 15mV 左右。查了数据手册发现这颗 DAC 内置缓冲放大器本身就有偏移输出只能到几十毫伏。这个问题在带缓冲的 DAC 上很常见并不一定是芯片坏了。解决思路有两个。简单的方法是接受这个偏移在校准时把它归入 offset反正校准算法里已经包含偏移项。严格的方法是在 DAC 输出后面加一个反相求和电路用负电压把零点彻底拉到 0V但这个电路复杂度高普通项目没必要。我最后就用了校准方式因为这个偏移值非常稳定不会乱跳。6.2 问题二特定码值附近电压跳动写入某些码值时输出电压会在一两个毫伏范围内来回抖。最初我怀疑芯片噪声后来发现原因在地回路。我的测试线比较长且 DAC 模块的模拟地和数字地没有分开MCU 的 I2C 波形切换时把噪声耦合到了模拟输出上。处理办法是把模拟地和数字地单点连接DAC 芯片下方铺完整的地平面并缩短测试线。另外在 VOUT 端加一个 0.1μF 电容到地高频噪声立刻下来不少。这里有个度要注意电容加得太大会让输出响应变慢测量动态波形时容易滞后。6.3 问题三波形更新速率上不去我给信号源写了一个 64 点的正弦波波表希望输出 1kHz 正弦波。结果跑起来波形明显失真一看频率根本没上去。查代码发现每更新一个点I2C 都要写两字节数据加上地址、应答、起始停止位在 400kHz 速率下每次写入差不多要 70μs。64 个点更新一轮就要 4.5ms 左右换算下来正弦波最多到 200Hz 左右离 1kHz 差远了。这个问题的根子是通信接口带宽不是 DAC 本身速度不行。我把通信换成 SPI 后8MHz 时钟下写两字节只需要约 2μs加上片选操作也就 3μs 一次64 点一轮波形只要 0.2ms跑 5kHz 正弦绰绰有余。如果你的 MCU SPI 速率能达到 10MHz 以上动态输出能力会比 I2C 强得多。再往深处说波形更新的瓶颈不只通信还有软件循环里的浮点计算。正弦波波表生成时如果是动态计算每一点都要算正弦函数CPU 占用率也会很高。遇到这种情况建议预先把波表一次性算好放进内存运行时就只管发送波形数据不要每次现算。6.4 给未来的项目留几条经验这次做完后我最大的体会是把 DAC 当成一个“模拟链路起点”来看待。DAC 数模转换控制器从来不是孤立芯片而是 MCU 数字世界和真实模拟世界之间的枢纽。数字端写什么码值固然重要但参考电压干不干净、地平面完整不完整、输出缓冲能不能带动负载往往才是决定项目能走到多准的关键。如果你现在正准备开始一个类似的项目先别急着堆高位数芯片拿一块 12 位的普通 DAC 打通整个链路让它能稳定输出你需要的电压范围再做校准和动态波形。过程中把每一个环节的误差来源记录下来你会比直接上一颗高端芯片少走很多弯路。
阅读完成 · 觉得有帮助?