前段时间做一台小批量产线校准工装,被一个老问题折腾得够呛:输出电压/激励电流需要微调,过去全靠面板上的机械电位器,操作员一台一台拧,拧完热机半小时又飘,重复性完全看手感。后来我把“可调”这件事彻底换成数字方案,核心用了一颗 AD5293BRUZ-20-RL7 数字电位计,控制端接 STM32F091RC,通过 SPI 远程写入电阻值,把校准流程做成了全自动闭环。
这篇内容不是抄数据手册,而是这次项目从选型、电路、固件到现场调试的完整记录。如果你也在做校准设备、增益微调、传感器激励这类需要“可控电阻”的场景,并且考虑用数字电位器代替机械电位器,那这篇应该能帮你把坑提前踩平。
1. 为什么把机械电位器换成可编程电阻
1.1 校准和工业场景对“可调”的三个硬指标
工业校准场景里,“可调电阻”这四个字背后其实是三个硬指标。第一个是寿命。机械电位器靠碳膜和触点接触,产线上一天校准几十台,几个月后触点磨损,阻值就开始飘,甚至出现旋转时跳变。第二个是可重复性。同一台设备,上午校准和下午校准,如果靠人手拧电位器,结果很难完全一致;操作员心情好和心情不好,手感都不一样,这对批量生产来说是灾难。第三个是远程可控。产线设备一旦装进机柜,再去拧面板电位器就很麻烦;而自动校准、自动补偿、温度漂移修正,都要求电阻值能被程序改写。
数字电位计正好把这三个问题同时解决。它没有物理触点,阻值靠内部开关网络切换,寿命长;阻值由数字码决定,同一台设备写同一个码,阻值高度一致;接口通常就是 SPI/I2C,单片机一写就完事,自动化和闭环都变得非常自然。
1.2 数字电位器选型时真正要看的参数
数字电位器型号很多,但挑选时重点其实就几个:分辨率、端到端阻值、接口、供电、是否需要非易失。分辨率决定了最小可调步进,端到端阻值决定了调节范围,接口影响 MCU 的资源占用,供电决定模拟通路的信号摆幅,非易失则决定掉电后要不要重新写。
这次项目里,我的需求是 20kΩ 量级的调节范围,分辨率最好能到 10bit 以上,毕竟校准精度越高越好;接口必须带回读,否则调试的时候心里没底;供电要能覆盖常见的工业模拟信号电平,不能只支持 3.3V/5V 这种小信号。挑了一圈下来,AD5293BRUZ-20-RL7 比较符合:10bit 分辨率,1024 个抽头,20kΩ 端到端阻值,SPI 接口带 SDO 回读,模拟端供电范围宽,还有 20 次可编程的非易失烧写功能。
1.3 为什么控制端选 STM32F091RC
控制端我用的 STM32F091RC,这不是随便抓一颗就上的。F091 是 Cortex-M0 内核,最高 48MHz,主频不高但在这种低频控制场景完全够用。它带有多路 SPI、I2C、UART,还有 12bit ADC,刚好可以在做闭环校准时把 ADC 当反馈通道,一颗芯片同时完成“写电阻”和“测结果”两件事。
更重要的是,F091RC 的封装和 Flash 容量适合做工业小设备。LQFP64 封装,焊接调试都方便;256KB Flash、32KB RAM,跑一个 RTOS、存几百组校准参数毫无压力。而且这颗芯片的 SPI 外设在 3.3V 供电下工作稳定,只要解决好和 AD5293 的逻辑电平匹配,整个系统非常简单,不需要外挂复杂逻辑芯片。
2. 硬件设计:3.3V 单片机如何驱动一颗高压数字电位器
2.1 先把 AD5293BRUZ-20-RL7 这个型号看明白
新手拿到型号经常懵,其实 ADI 的型号后缀是有规律的。AD5293 是基础型号,单通道数字电位器,10bit 分辨率。BRUZ 中的 RU 代表 TSSOP 封装,Z 代表无铅,B 通常表示等级或温度范围。后面的 -20 表示端到端电阻是 20kΩ,也就是 A 端到 B 端的总电阻。最后的 RL7 是包装形式,表示 7 英寸卷带包装,适合 SMT 贴片机。
这个后缀一定要认真看,因为同系列还有 -50、-100 等不同阻值版本,选错了整个模拟通路的量程就变了。我这次选 -20 是因为目标电路需要的可调范围在 0 到 20kΩ 左右,如果选 100kΩ 版本,同样的 10bit 分辨率,每一步对应的电阻变化量会从约 19.5Ω 变成约 97.6Ω,精度直接下降一个数量级。
2.2 电源方案与模拟端的供电考虑
AD5293 这颗芯片的设计定位是“模拟信号通路”,供电范围比普通 5V 数字电位器宽得多。具体 VDD/VSS 极限值以手册为准,我在这块板子上是给模拟部分单独供了一路 12V,让 A、W、B 三个端子的信号摆幅能覆盖工业常见的 0~10V 区间。
这里有一个容易踩的坑:如果模拟信号是 0~5V,而数字电位器供电只给 3.3V,那么 A/B/W 端的电压摆幅会被供电限制,信号削顶。所以先确认你电路里的被调信号范围,再决定电源方案。如果被调信号是单极性的,用单电源就行;如果是双极性交流信号,通常需要双电源。我这次是单极性 0~10V,就用单电源。
数字部分和模拟部分最好分开供电,F091 的 3.3V 通过 LDO 单独给,AD5293 的模拟电源从 12V 过来,两者之间用 0Ω 电阻做单点连接,避免数字开关噪声窜进模拟通路。
2.3 逻辑电平匹配:3.3V 控制 12V 芯片,直接连可能翻车
这个环节是最多人在原理图阶段翻车的地方。STM32F091RC 的 IO 是 3.3V 电平,而 AD5293 的数字接口如果参考的是 12V 或 5V 电源,那么它的 VIH 门槛可能高于 3.3V 输出。换句话说,单片机输出高电平 3.3V,在 AD5293 眼里可能不算高电平,SPI 命令就会随机失败。
我这次没有直接短接,而是看了手册里的 VIH/VIL 参数之后,在 SPI 信号线上加了一级电平转换。最简单的做法是使用支持 3.3V 到 5V/12V 电平转换的芯片,或者用开漏输出加上拉电阻的方式,具体取决于 AD5293 的数字电源引脚是怎么供电的。
注意:不同 AD5293 型号/版本对逻辑电平的要求可能不一样,原理图定稿前务必把手册里的 VIH、VIL、VOH、VOL 参数和单片机实际输出的逻辑电平对照一遍。实在没把握,就用慢速光耦或者电平转换芯片,稳最重要。
2.4 引脚连接表与实际接线记录
下面是我这块板子上的实际连接方式,SPI1 用的是 STM32F091RC 的 PA5/PA6/PA7,片选单独用 PA4 控制。AD5293 的片选引脚在数据手册里叫 SYNC,低电平有效,作用和普通 SPI 的 CS 一样。
| STM32F091RC 引脚 | 功能 | AD5293 引脚 | 说明 |
|---|---|---|---|
| PA5 | SPI1_SCK | SCLK | SPI 时钟 |
| PA7 | SPI1_MOSI | SDI | 命令/数据输入 |
| PA6 | SPI1_MISO | SDO | 回读数据输出 |
| PA4 | GPIO 推挽输出 | SYNC | 片选,低有效 |
| 3.3V 电源域 | - | 逻辑参考 | 需要确认是否与芯片匹配 |
| 12V | - | VDD | 模拟通路供电 |
A、W、B 三个模拟端子,一般 A 和 B 接被调电路的两端,W 是滑臂输出。具体哪一端接哪边,要根据你希望的默认调节方向决定,因为 AD5293 的 A、W、B 不是完全对称使用的。
2.5 量程与步进计算:20kΩ / 1024 意味着什么
AD5293BRUZ-20-RL7 的端到端电阻是 20kΩ,分辨率 10bit,一共 1024 个位置。按理想线性模型,每写一个码,A-W 之间的电阻变化量大约是 20kΩ ÷ 1024 ≈ 19.5Ω。看着很小,但在工业校准场景里,这个步进配上分压电路之后,输出变化往往在 mV 甚至 μV 级别,完全够用。
有一点必须提醒:数字电位器的 W 端并不是理想 0Ω,内部有一定阻值的 wiper 电阻,通常在几十欧姆量级。也就是说,写入 code=0 时,A-W 之间不是真正的 0Ω,而是 wiper 电阻。做校准表的时候,不要从 “code=0 => 0Ω” 这个假设出发,要以上电实测为准。
另外,如果负载直接接在 W 端,且负载阻抗不够高,会和内部梯形电阻网络分压,导致阻值关系非线性,甚至误差很大。所以 W 端输出一定要接高输入阻抗的运放缓冲,这一点很重要。
3. 固件实现:STM32F091RC 通过 SPI 驱动 AD5293
3.1 SPI 模式选择和 20bit 帧结构,这里最容易出错
AD5293 的串行接口是 20bit 帧,MSB first,最高 2bit 是命令,中间 8bit 保留为 0,低 10bit 是数据。这就有个很尴尬的问题:STM32 的硬件 SPI 帧长度可以配 8bit 或 16bit,但 20bit 刚好两边都不挨着。我最终用 8bit 模式,把 20bit 命令左移 4 位补成 24bit,连续发 3 个字节,这样最高 20bit 就是有效命令,后面多出的 4bit 芯片会忽略。
SPI 的时钟极性和相位也要注意。我第一版用的是常见的 Mode0(CPOL=0,CPHA=0),结果写入后电阻值纹丝不动,用逻辑分析仪抓了 SCLK 和 SDI 才发现数据采样沿对不上。后来按 Mode3(CPOL=1,CPHA=1)配置,写入和回读一次通过。
提示:不同批次、不同手册版本的 AD5293,SPI 模式推荐可能有差异。不要在原理图上赌“肯定就是 Mode0”,编程之前先看手册串行接口章节的时序图,再用逻辑分析仪实测确认。
3.2 AD5293 的三条基本命令:写、读、复位
AD5293 的 20bit 命令字,最关键的是高 2bit 命令位:
- 01:写 RDAC 寄存器,把低 10bit 的数据写入,电阻值立刻变化
- 10:读 RDAC 寄存器,把当前码值通过 SDO 读回来
- 11:复位,把寄存器恢复到默认值
写操作要特别注意边界处理。低 10bit 数据的有效范围是 0~1023,对应十六进制 0x000~0x3FF。如果传入 1024 或 2048 这种超过 10bit 的数,高 bit 会被截断,可能写成一个完全错误的值。所以驱动函数里一定要做钳位。
3.3 驱动代码:初始化、写入、回读
下面是我在 STM32F091RC 上实际跑通的驱动代码,用的是 STM32CubeMX 生成的 HAL 库外设句柄。代码不复杂,重点在帧拼接和片选时序。
// ad5293.h #ifndef AD5293_H #define AD5293_H #include "main.h" #define AD5293_MAX_CODE 1023U #define AD5293_MID_CODE 512U #define AD5293_CMD_WRITE 0x01U #define AD5293_CMD_READ 0x02U #define AD5293_CMD_RESET 0x03U void AD5293_Init(void); void AD5293_WriteCode(uint16_t code); uint16_t AD5293_ReadCode(void); void AD5293_Reset(void); #endif// ad5293.c #include "ad5293.h" #include "spi.h" #define AD5293_CS_LOW() HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET) #define AD5293_CS_HIGH() HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET) static void AD5293_SendFrame20(uint32_t frame20) { uint8_t buf[3]; // 20bit 帧补 4 个 0,变成 24bit,SPI 8bit 模式发 3 字节 uint32_t frame24 = frame20 << 4; buf[0] = (frame24 >> 16) & 0xFF; buf[1] = (frame24 >> 8) & 0xFF; buf[2] = frame24 & 0xFF; AD5293_CS_LOW(); HAL_SPI_Transmit(&hspi1, buf, 3, 10); AD5293_CS_HIGH(); } void AD5293_WriteCode(uint16_t code) { uint32_t frame; if (code > AD5293_MAX_CODE) { code = AD5293_MAX_CODE; } // 命令位 01 放在 bit19/bit18,数据放在低 10bit frame = ((uint32_t)AD5293_CMD_WRITE << 18) | (code & 0x3FF); AD5293_SendFrame20(frame); } void AD5293_Reset(void) { AD5293_SendFrame20((uint32_t)AD5293_CMD_RESET << 18); } uint16_t AD5293_ReadCode(void) { uint8_t cmd[3] = {0x00, 0x00, 0x00}; uint8_t rsp[3] = {0}; uint32_t word24 = 0; uint16_t code = 0; // 第一帧:发读命令 cmd[0] = ((uint32_t)AD5293_CMD_READ << 6) & 0xFF; // 读命令 10 移到 24bit 帧顶部 AD5293_CS_LOW(); HAL_SPI_TransmitReceive(&hspi1, cmd, rsp, 3, 10); AD5293_CS_HIGH(); // 第二帧:发 0,同时从 SDO 读回数据 cmd[0] = 0x00; cmd[1] = 0x00; cmd[2] = 0x00; AD5293_CS_LOW(); HAL_SPI_TransmitReceive(&hspi1, cmd, rsp, 3, 10); AD5293_CS_HIGH(); // 回读数据是 10bit,位于 24bit 帧的中间 word24 = ((uint32_t)rsp[0] << 16) | ((uint32_t)rsp[1] << 8) | rsp[2]; code = (word24 >> 4) & 0x3FF; return code; }这个回读实现是我在项目板上验证过的,但不同版本的 AD5293 在回读时序上可能有细节差异,如果你发现回读数据不对,第一时间拿逻辑分析仪看 SDO 的数据落在哪一拍,而不是怀疑代码逻辑。读操作的 CS 处理方式也要以手册时序图为准。
3.4 上电默认值、不掉电保存和 20 次熔断
AD5293 的 RDAC 寄存器默认是易失的,上电后寄存器会恢复到一个默认值,通常在中间码附近(大约 code 512,对应约 10kΩ),而不是保存你上次写的值。很多项目在这一点上踩坑:以为上电自动恢复上次校准值,结果每次上电都回到初始状态,后级电路的工作点完全不对。
解决方式有两种。第一种是每次上电后由 MCU 主动写一次目标值,这种方式灵活,也方便做不同校准场景切换,工业上大多数情况都用这种方式。第二种是使用 AD5293 的 20 次可编程熔断功能,把当前码值永久烧进芯片,之后上电自动加载,相当于把校准值固化在芯片内部。
我强烈建议样机阶段不要熔断,因为熔断不可逆,20 次用完了芯片就再也改不了默认值。先用 RAM 方式写,等所有场景都验证完毕、校准值基本稳定之后,再考虑是否熔断。而且熔断操作对时序和电压有要求,必须在手册规定的条件下进行,量产品有批量烧写需求的话,建议用专门的烧录工装,不要靠产线人员手工操作。
4. 校准应用落地:从手动电位器到自动闭环
4.1 场景一:电压基准微调 / DAC 满量程校准
第一个实战场景是电压基准微调。产线上有一批信号源模块,输出满量程 10.000V,但是因为基准源和分压电阻的误差,实际输出可能是 10.052V。过去靠人工拧电位器,把输出调到万用表显示 10.000V。现在把分压电阻的下臂换成 AD5293BRUZ-20-RL7,单片机写一个初始码值,然后用精密万用表测出实际输出,算出偏差,再微调码值,反复几次就能收敛。
这里有个关键点:AD5293 的端到端电阻有一定的初始容差,同一颗型号在不同板子上,同一个 code 对应的绝对电阻值可能不一样。所以每台设备都必须单独校准,不能拿一台设备的 code-电压表直接套到另一台。校准好的数据可以存到 STM32F091RC 的 Flash 里,每台设备保存自己的一份表,这样批量生产时每台都是独立参数,不会互相干扰。
4.2 场景二:传感器激励电流调节
第二个场景是传感器激励电流。比如给压力传感器供电的恒流源,理想输出是 1.000mA,但受运放失调和电阻误差影响,实际可能是 0.985mA。用数字电位器去调整恒流源电路里的设定电阻,MCU 根据 ADC 采样到的采样电阻电压,自动调整码值,直到电流精确落在目标窗口。
这个场景的好处是,老化之后的参数漂移也可以通过程序定时修正。以前机械电位器老化漂了,只能派人去现场打开机箱重新拧;现在只要把校准程序跑一遍,自动把电阻值修回来,甚至可以在设备自检流程里加上这一项,大大降低运维成本。
4.3 基于 ADC 反馈的自动二分搜索校准流程
校准的核心是让系统自己找到“最优码值”。我采用的是很朴素的二分搜索:假设被测信号随 code 单调递增/递减,那么每次把搜索区间对半砍,最多 10 次就能定位到 1024 个码中的最优位置,因为 2 的 10 次方正好是 1024。
下面是伪代码思路,实际项目中代码差不多就是这样:
uint16_t lo = 0; uint16_t hi = AD5293_MAX_CODE; uint16_t best = AD5293_MID_CODE; uint32_t bestErr = 0xFFFFFFFF; uint32_t target = ReadTargetFromFlash(); for (uint8_t i = 0; i < 12; i++) { uint16_t mid = (lo + hi) / 2; AD5293_WriteCode(mid); HAL_Delay(2); // 等待模拟通路稳定 uint32_t adc = ReadAdcAverage(8); int32_t err = abs((int32_t)adc - (int32_t)target); if (err < bestErr) { bestErr = err; best = mid; } // 根据单调方向决定下次搜索区间 if (adc < target) { lo = mid; // 输出偏小,往大调 } else { hi = mid; // 输出偏大,往小调 } } AD5293_WriteCode(best); SaveCalibration(best);这里要注意方向问题。你的电路里 code 增加可能是输出增加,也可能是输出减少,取决于 AD5293 的 A/W/B 接法。实际代码里把方向做成可配置的标志位,不要写死在二分逻辑里。
4.4 实测效果与误差控制
实测下来,这套系统的重复性比机械电位器好很多。同一台设备,关机重启之后重新写同一个 code,测出来的输出基本稳定;而机械电位器受温度、振动和触点氧化影响,很难做到这种一致性。
但精度不是白来的。AD5293 本身是 10bit,理论上足够细,但整个链路上还有分压电阻的温漂、运放的失调、参考源的漂移,以及 STM32 内置 ADC 的量化误差。F091 内置 12bit ADC,参考电压 3.3V 时,理论分辨率约 0.8mV,做粗校准可以,想做到更高精度,反馈端还是要外接精密 ADC 或者用 6.5 位万用表做离线标定。
另外,被调信号如果是要供给负载的,一定在 W 端后面加运放缓冲,不要让负载直接吃数字电位器的输出阻抗,否则负载一变,分压关系就变,校准值全部失效。
5. 常见问题排查与工业应用避坑记录
5.1 SPI 写进去没反应,八成是极性和帧结构问题
这是出现频率最高的问题。现象是代码跑了一遍,万用表量 A-W 电阻纹丝不动。排查步骤就三步。
第一步,确认 SPI 模式。用逻辑分析仪抓 SCLK 和 SDI,看看 SCLK 空闲电平是高还是低,数据在上升沿还是下降沿采样,和芯片手册时序图对比。我这次就是 Mode0 不行,换成 Mode3 立刻正常。第二步,确认 20bit 帧拼接对不对。把发送的第一个字节打印出来看,和高两位命令码比对,很多时候是补位补错了。第三步,确认 SYNC 片选时序。片选拉低后要有足够时间让 SCK 跑完 24 个时钟,片选拉高后也要留一点时间让芯片完成内部锁存。如果 CS 释放太快,命令可能没被正确识别。
5.2 回读数据全是 0 或者乱码
回读数据不对,首先要确认 SDO 在硬件上有没有真正接对。PA6 是 STM32F091 的 SPI1_MISO,但如果你 CubeMX 配置时把引脚复用配错了,回读就会失败。其次,回读通常需要两帧:第一帧发读命令,第二帧芯片才把数据从 SDO 送出来。如果只发一帧就去读数据,大概率读到的是上一次的残留值或者全 0。
还有一个容易被忽略的点:回读数据在 24bit 帧里的位置,不一定和写入数据完全对齐。不同芯片手册的定义不同,需要用逻辑分析仪把 SDO 波形抓出来,数一下数据位落在哪一段,再调整代码里的移位位数。
5.3 上电瞬间状态不确定,怎么处理
数字电位器上电默认值一般是中间码,但在电源爬升过程中,内部逻辑不一定完全确定,W 端电压可能出现短暂的不确定跳变。如果后级电路对电压冲击敏感,比如接了精密运放或者直接进 ADC,最好在 AD5293 的 SYNC 引脚上加一个上拉电阻,让芯片在上电期间保持禁用状态,等 STM32 初始化完成、电源稳定之后,再通过软件拉低并写入目标值。
另外,尽量让 MCU 在启动后第一时间就把 AD5293 初始化成预设值,不要等到业务主循环再处理。比如做校准工装时,开机先做“写初始码值 -> 回读验证 -> 点亮准备就绪指示灯”,再进入自动校验流程,这样操作员一眼就能看出上位机有没有连上、初始化有没有成功。
5.4 工业现场的干扰和布线问题
工业环境不像实验室那么友好,电机启停、继电器动作都会产生干扰。AD5293 的模拟端走线要尽量短粗,远离继电器和变压器。SYNC、SCLK、SDI 这三根数字线最好包地处理,不要和模拟信号线平行走长距离。
如果 SPI 线超过 10cm,建议降低 SPI 时钟,我从 6MHz 降到 1MHz,实测稳定很多。再不行就加 RC 滤波,在靠近 AD5293 的一端串联 33Ω 电阻,对抑制振铃有明显帮助。
另外,数字电位器是易失性配置,如果工业现场出现死机或复位,需要有一个机制重新初始化。我在应用层加了看门狗,配合“每次启动重新写入”的逻辑,避免出现设备复位后 AD5293 停在默认码值导致输出异常的状态。
5.5 快速排查表
下面这个表是我这次项目整理出来的,适合现场快速定位问题。
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| 写入后电阻不变 | SPI 极性/相位错误,片选时序不对 | 逻辑分析仪抓 SCLK/SDI/SYNC 时序 |
| 回读全 0 或乱码 | SDO 接线错误,回读帧数不够,移位不对 | 检查 MISO 配置,拆成两帧回读 |
| 上电默认值不理想 | RDAC 易失,上电回到中间码 | 软件初始化时立即写入目标值,或熔断 |
| 输出带载后漂移 | W 端负载阻抗过低 | 加运放缓冲 |
| 现场偶尔跳变 | 干扰耦合进 SPI 线 | 降低时钟、包地、加 RC 滤波 |
| 某一台设备校准值不对 | 端到端电阻存在容差 | 逐台校准,不要复制参数 |
5.6 关于 20-TP 熔断的最后提醒
再强调一次这个功能。AD5293 的 20-TP,也就是 20 次可编程非易失功能,确实很诱人,因为可以把校准值直接存在芯片里,上电即用。但它有次数上限,一旦用完,芯片的默认值就永远固定了。
如果你要给量产产品用这个功能,我建议流程是:研发阶段全部用 RAM 方式写,固化代码里写目标值;等到批次稳定、校准算法验证完毕,再决定要不要熔断。熔断操作要严格按照手册里的电压和时序要求来,最好通过工装夹具批量执行,不要在产线终端上让操作员一条条手动操作。
最后再分享一点个人心得
如果让我重新做这个项目,我唯一会改变的地方,是在画原理图之前就把回读时序验证完。第一次做数字电位器控制,千万别急着上闭环自动校准,先写一个最简单的“写入 code -> 万用表量电阻 -> 回读打印”小工具,把基本读写链路跑通,再往上叠应用逻辑。这个习惯帮我挡掉了至少三次接线错误和两次 SPI 模式设错的问题。
AD5293BRUZ-20-RL7 和 STM32F091RC 这个组合,在中小批量校准设备、工业仪表、传感器激励源这些场景里非常实用,成本不高,效果却明显比机械电位器稳定。希望这篇记录能帮你少走一段弯路。