内阻仪怎样从ADC读数里提取1kHz微弱信号
一串 ADC 读数看起来起伏很大,真正想测的信号却可能藏在里面。
以交流内阻测量为例,前端把电池对激励电流的响应放大,送到 ADC。读数里除了目标频率,还可能混着直流偏置、谐波和噪声。直接拿最大值减最小值,得到的往往是“所有成分一起摇晃”的幅度。
如果激励是 1 kHz,就有一个更明确的办法:让每个采样值分别与同频的余弦、负正弦相乘,再累加。与参考频率相干的成分留下来,其他成分则按采样窗口和频率关系被抑制。这就是本文要实现的单频 I/Q 检波。
图 1 离线合成示例。1 kHz 分量的峰值只有 80 个 ADC 码,2 kHz 分量达到 600 个码;图中的提取曲线由参考模块计算,不是示波器实测。
这套方法也适合交流传感器和相敏测量。真正容易出错的地方,不只是公式,而是公式前面的数据到底按什么顺序、什么速率进入内存,以及公式后面怎样把复数响应变成电阻。
先沿着电路找到真正的ADC输入
用于分析的电路以 RA2E1 为核心。模拟信号经过 U12.2 的偏置和放大网络,输出到 P001_ADC1。
图 2 高清原理图局部。R39、R41 和 C33 参与反馈,R40、C32 构成到地支路,增益随频率变化;不能只看到一个电阻比就把整段当成固定倍数放大器。
再顺着网络名查 MCU,会发现一件很关键的事:P000/AN000 和 P001/AN001 接在同一条模拟线上,分别是 48、47 脚。
图 3 两个引脚接同一网络,不代表它们在同一时刻转换,也不代表它们已经组成均匀的两倍速采样。
PCB 也要一起看。下图保留原版铜皮,用来定位 MCU、U12 及其周围走线。顶底层采用同一观察方向,因此底面丝印呈镜像。
图 4 顶层与底层高清导出。本次没有修改铺铜或完成 DRC 修复;关键引脚和反馈网络以图 2、图 3 的放大局部为准。
本专题只处理已经取得的 ADC 快照。原板还有电源焊盘与底层地铜待核查的问题,不能据此直接下单复刻,更不能把算法实验当成保护耐压或整机精度验证。
一个32位word里装了两次转换
结合实际初始化代码与 RA2E1 手册,可以把数据格式理清楚。
ADANSA0=0x03 选择 AN000、AN001。ADCSR=0x8200 使用单次扫描并使能触发;ADC 基址加 0x10 的寄存器是 ADSTRGR,这里的 0x0900 用于选择触发源,不是设置累加次数。
在所核对的正常 12 位、右对齐配置下,ADDR0 位于 0x4005C020,ADDR1 位于紧接着的 0x4005C022。按小端格式读成一个 32 位 word,低 16 位对应通道 0,高 16 位对应通道 1。
陷阱就在这里:内存相邻,不等于时间等间隔。
手册规定单次扫描按通道顺序转换。若触发率为 25 kHz,两路都完成且没有漏触发,那么每条通道各有 25 kS/s。通道间隔由转换时序决定,不能把两个半字直接串起来,就宣布得到均匀的 50 kS/s。
图 5 时序关系示意。δ 仅表示两次顺序转换存在间隔,图中位置不是测得的真实延迟。每次扫描只选固定的一条 lane,时间轴才容易定义清楚。
参考模块因此采用一个简单约定:每个 word 只取同一条 lane。以 25 kS/s 取 250 点,窗口长度为 10 ms,恰好包含 10 个 1 kHz 周期。250 点 DFT 的第 10 个频点,就是要提取的 1 kHz。
把直流留在检波结果外面
令 θn = 2π×10×n/250,将一帧的均值记为 μ,峰值形式的输出为:
I = (2/250) × Σ[(xn−μ)cos(θn)]
Q = (2/250) × Σ[(xn−μ)(−sin(θn))]
这里采用负正弦的 DFT 约定。如果输入为 Acos(θn+φ),输出约为 A(cosφ+j sinφ)。I、Q 都以 ADC 峰值码数为单位。
每周期有 25 个采样点,因此余弦、负正弦各存 25 个系数即可。系数按 32768 缩放,使用 int32_t 存放,余弦峰值 +32768 可以正常表示。
直流处理也不能偷懒。查表量化后,系数之和不一定严格为零;先把整数均值截断再相减,也会引入额外偏差。实现中保留采样和、系数和,用 64 位整数完成去均值的分子,最后归一化。
下面是附件 iq250.c 的处理函数。配套的头文件、25 相位查表及生成脚本均在代码包中。
#include "iq250.h"
#include "iq250_table.h"
enum iq_status iq250_analyze(
const uint32_t *words, size_t word_count,
size_t offset_words, unsigned lane,
struct iq_result *out)
{
if (!words || !out || lane > 1)
return IQ_BAD_ARGUMENT;
if (offset_words > word_count ||
word_count - offset_words < 250)
return IQ_SHORT_FRAME;
int64_t xc = 0, xs = 0, dc = 0;
int64_t cs = 0, ss = 0;
for (size_t n = 0; n < 250; ++n) {
uint32_t x = (words[offset_words + n]
>> (16u * lane)) & 0xffffu;
if (x > 4095) return IQ_BAD_CODE;
if (x == 0 || x == 4095) return IQ_AT_RAIL;
int32_t c = iq_cos_q15[n % 25];
int32_t s = iq_neg_sin_q15[n % 25];
xc += (int64_t)x * c;
xs += (int64_t)x * s;
dc += x;
cs += c;
ss += s;
}
const double den = 250.0 * 250.0 * 32768.0;
struct iq_result next = {
2.0 * (double)(250 * xc - dc * cs) / den,
2.0 * (double)(250 * xs - dc * ss) / den
};
*out = next;
return IQ_OK;
}
这段代码先检查长度和 lane,再检查格式与 ADC 电源轨码,发生错误时不改写输出。0、4095 被保守拒绝,但这并不能检出所有模拟削顶:如果运放先饱和、输出仍落在 ADC 范围内,软件只看码值就未必知道。
64 位也不是为了显得稳妥。按 250 点、12 位码和上述系数范围估算,去均值分子的中间项达到约 8.4×10¹² 的量级,已经超出 32 位有符号整数。最后使用 double 归一化;在 Cortex-M23 上的运行库和耗时仍需实机评估。
回放一帧公开数据能说明什么
处理模块还回放了项目页面公开的 512 个 ADC 数字。按相邻两个半字组成 word,并遵循所核对计算入口的 +0x18 偏移,跳过前 6 个 word,分别处理后面的两条 250 点序列。
图 6 上图展示前 75 次扫描,下图是通道 0 相对于 1 kHz 重建波形的残差。数据来自公开记录,未经插补;这不是本次新采集的板上波形。
离线结果中,通道 0 得到 I≈407.406、Q≈118.647,通道 1 得到 I≈408.274、Q≈114.999。两路响应接近,但不完全相同,不能因此假定通道间不存在时延或模拟误差。
更不能直接把这些数写成毫欧。它们目前只是经过检波的 ADC 码,需要参考标准和有效的测量模型,才能进一步讨论阻抗。
还有一个边界要留住:跳过 6 个 word 只是这份回放和对应入口的明确约定,不是 DTC 的通用规律。所核对 DTC 初始化使用 CRA=0xFA00,而手册要求 repeat 模式的 CRAH、CRAL 初值相同。本专题没有验证原程序的启动搬运行为。
集成时应向函数交付稳定、按时间排序的完整快照。不能一边让 DTC 改写环形区,一边把它当成静止数组计算;也不能仅凭 10 ms 的 SysTick 就认定一帧已经完整。停止采集后复制、或经验证的双缓冲交接,都需要在目标板上另行完成。
电阻值不能用I和Q的模长冒充
内阻测量关心的是阻抗实部。若结果是 Z=R+jX,|Z| 同时包含电阻与电抗;线缆和器件的相位影响不会因为求了平方根就消失。
一种可检查的简化标定模型是:同档位、同激励、同接线极性和相位原点下,分别取得短路响应 S、已知纯阻标准的响应 C、被测对象的响应 D。
Z = Rstd × (D−S)/(C−S)
先减去短路偏置,再用标准响应消除共同的复数增益,计算后分别保留 R、X。标准件在 1 kHz 下必须足够接近纯阻,前端也须保持稳定、线性;换量程、表笔或激励发生漂移,都可能使这套标定失效。
附件的 iq250_calibrate() 已实现复数除法、有限值检查和参考响应阈值检查。阈值 min_delta 要依据实际噪声设置;不能拿测试样例里的数值直接当成量产参数。
合成验证中,把目标设为 0.2+j0.1 Ω,通过带偏置与相位的模型生成整数 ADC 帧,实际 C 程序恢复出约 0.199390+j0.100113 Ω。这是量化输入下的算法验证,不能解释成仪器达到某个毫欧精度。
让验证结果比曲线更有说服力
验证不只喂一个漂亮正弦波。参考程序在 Windows 主机实际编译运行,并用独立的 numpy 浮点 FFT 作对照:
| 检查内容 | 本次结果 |
|---|---|
| 长度、lane、格式、轨码、标定异常等接口检查 | 22 项通过 |
| 240 帧随机双通道信号 | 480 次复数结果对照通过 |
| C 定点查表与浮点 FFT 的最大复数差 | 约 0.005643 个 ADC 峰值码 |
| 整数直流平移后重新计算 | I、Q 完全相同 |
| 700~1300 Hz、每 5 Hz 一点的扫频 | 121 帧完成 |
| 主机 C 编译与 Cortex-M23 目标对象编译 | 均通过严格警告检查 |
图 7 左图为 480 次对照误差,右图为参考 C 程序计算的扫频响应。输入均为离线生成,曲线不代表实物仪器性能。
一个值得保留的小结果是:只有 2 kHz、峰值 600 码的相干整数采样,在 1 kHz 输出中仍留下约 0.126 个码。理想连续数值上的正交关系,不会替整数 ADC 消除量化误差。
10 ms 矩形窗口也不是万能窄带滤波器。目标频率偏离 1 kHz 时,响应会下降并产生泄漏;激励与采样不相干、窗口起点漂移,都会影响幅值和相位。加长窗口可以收窄频率响应,同时会增加等待时间,这个取舍要由测量场景决定。
把代码跑起来,再接近硬件
代码包提供最小 C 库、主机 CLI、查表生成器、测试脚本和验证记录。固定验证环境为 Python 3.12.14、numpy 2.3.5、Zig 0.13.0 内含的 Clang。
python -m pip install numpy==2.3.5
python build.py --zig C:/tools/zig/zig.exe
python verify.py
./build/iq250_cli.exe analyze ./build/source_snapshot.bin
build.py 同时生成主机程序和 thumb-freestanding-eabi / cortex_m23 对象。目标对象已编译,但没有链接启动代码、接入外设驱动或烧录。想落到硬件,下一步应先验证触发周期、顺序扫描完成条件、快照交接和相位原点,再做标准件与温漂验证。
把这些入口条件讲清楚,I/Q 才是一段能迁移的测量方法。否则再漂亮的小数点,也可能只是在精确计算一帧来历不明的数据。
资料、许可与源码
取材项目《#第八届立创电赛#基于瑞萨单片机的电池内阻测试仪》,原作者 eda_kxijzpvac,源码标识 yjmwxwx。完整入口 https://oshwhub.com/eda_kxijzpvac/battery 。所核对项目、EDA 与 ProProject 为 2023-10-09 版本,项目声明 GPL 3.0。
图 2~4 由对应在线嘉立创 EDA 工程重新高清导出、裁剪或组合,保留原工程内容与必要标识,按原 GPL 3.0 许可标识;没有修复原板铺铜。图 6 的 512 个数字逐值转录自项目公开记录,附件附来源说明及 GPL 3.0 全文。图 1、5、7 为本次独立绘制的合成示意、时序示意和程序验证图。
技术依据为瑞萨《RA2E1 Group User's Manual: Hardware》R01UH0852EJ0170 Rev.1.70,重点核对 15.2.5、29.2.1、29.2.16、29.2.17、29.3.2.1 节。本文独立完成单 lane 快照解析、去直流 I/Q 参考实现、复数标定及主机验证;未移植原汇编、未验证原板保护耐压、整机精度或实时采集。正文独立文字采用 CC BY 署名许可,新增参考代码按 MIT 发布;第三方材料的 GPL 许可另行保留。
完整代码与验证记录见本文附件 ra2e1-iq-reference.zip,下载入口为 https://img.dfrobot.com.cn/mc-front/5caab1791804207b131ae7df/842cb6c42d833ea3a48ee30c22ca1744.zip 。包内 README 说明构建方法、芯片/引脚、输入契约和集成边界,NOTICE 说明第三方材料来源。
技术交流,欢迎关注微信公众号:美男子玩编程。



