EIS电池阻抗测量芯片验证系统(五):CSV脚本解释器实现精髓

1. 为什么叫”脚本解释器”?

CSV 文件不是简单的”命令列表”,而是带有控制流脚本

能力 说明
顺序执行 按行顺序执行 I2C 命令
条件跳过 # 注释行不执行
延时 DELAY:N 暂停 N 秒
循环 LOOP BEGIN:N / LOOP END 重复执行 N 次
单命令重复 REPEAT:N 将当前行命令执行 N 次

这与解释型脚本语言(如 Shell、Python)的”读取→解析→执行”模式一致,因此称为类脚本解释器

2. 精髓一:CSV 即 DSL(领域特定语言)

2.1 CSV 作为脚本载体

选择 CSV 而非 JSON/YAML 的原因:

  • 表格化:每行一条指令,列对应参数
  • Excel 可编辑:测试人员可手动修改
  • 版本管理友好:diff 清晰
  • 注释简单# 开头即注释

2.2 第一列:控制语句

1
2
3
4
5
6
DELAY:5          →  延时 5 秒
LOOP BEGIN:2 → 循环开始,重复 2 次
LOOP END → 循环结束
REPEAT:3 → 当前命令重复 3 次
#注释 → 跳过
(空) → 普通 I2C 命令

解析逻辑i2c_command.py):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if row[0].startswith('#'):
ctrl_type = CTRL_COMMENT
elif row[0].startswith('LOOP BEGIN:'):
ctrl_type = CTRL_LOOP_BEGIN
repeat_count = int(row[0].split(':')[1])
elif row[0] == 'LOOP END':
ctrl_type = CTRL_LOOP_END
elif row[0].startswith('REPEAT:'):
ctrl_type = CTRL_REPEAT
repeat_count = int(row[0].split(':')[1])
elif row[0].startswith('DELAY:'):
ctrl_type = CTRL_DELAY
delay_time = int(row[0].split(':')[1])
else:
ctrl_type = CTRL_NONE # 普通命令

精髓:第一列既可以是”控制关键字”,也可以为空(表示普通命令),与后续列(cmd_type, slave_addr, reg_addr…)组合成一条完整指令。

3. 精髓二:执行引擎与状态机

3.1 执行上下文

1
2
3
4
5
self.execution_context = {
'current_index': 0, # 当前执行到第几条
'loop_stack': [], # 循环栈 [(start_idx, total, current), ...]
'running': True # 是否继续执行
}
  • current_index:指令指针,相当于 PC
  • loop_stack:支持嵌套循环
  • running:支持用户点击”停止”中断

3.2 事件驱动调度(避免栈溢出)

问题:若用递归实现 process_next_command() → execute → callback → process_next_command(),深层循环会导致栈溢出。

解决:用 QTimer.singleShot(0, self.process_next_command) 替代直接递归:

1
2
3
4
def process_next_command(self):
# ... 处理当前命令 ...
# 不直接递归,而是投递到事件循环
QTimer.singleShot(0, self.process_next_command)
  • singleShot(0, fn) 表示”下一帧”执行 fn
  • 每次调用 process_next_command 都在新的栈帧中,不会累积
  • 支持任意深度的 LOOP 嵌套和命令数量

3.3 循环实现

LOOP BEGIN

1
2
3
4
5
6
7
# 首次进入:入栈 (start_idx, total_count, 1)
self.execution_context['loop_stack'].append((
self.execution_context['current_index'],
command.repeat_count,
1
))
self.execution_context['current_index'] += 1

LOOP END

1
2
3
4
5
6
7
8
9
start_idx, total_count, current_count = self.execution_context['loop_stack'][-1]
if current_count < total_count:
# 更新计数,跳回 LOOP BEGIN
self.execution_context['loop_stack'][-1] = (start_idx, total_count, current_count + 1)
self.execution_context['current_index'] = start_idx
else:
# 循环结束,出栈
self.execution_context['loop_stack'].pop()
self.execution_context['current_index'] += 1

精髓:通过修改 current_index 实现”跳转”,用栈保存循环状态,实现结构化控制流。

4. 精髓三:异步回调与串行化

4.1 命令执行是异步的

I2C 命令通过串口发送,需等待 STM32 响应。若同步阻塞,会卡住 UI。

设计execute_command(command, callback) 接收回调:

1
2
3
4
5
6
7
8
9
10
def execute_command(self, command, callback=None):
self.serial_port.write(header_bytes)
# ...
response = self.serial_port.read(1)
if response and status == 0:
if callback:
callback(True)
else:
if callback:
callback(False)

4.2 回调中调度下一条

1
2
3
4
5
6
7
8
def on_command_complete(success):
if success:
self.execution_context['current_index'] += 1
QTimer.singleShot(0, self.process_next_command)
else:
# 失败则停止
self.start_button.setEnabled(True)
self.stop_button.setEnabled(False)

精髓:每条命令完成后,通过回调 + QTimer 驱动下一条,形成串行执行链,既保证顺序,又不阻塞 UI。

5. 精髓四:I2CCommand 的统一抽象

5.1 一行 CSV → 一个对象

1
2
3
4
5
@classmethod
def from_csv_row(cls, row):
# 解析控制语句、cmd_type、slave_addr、reg_addr、data_length、data
return cls(cmd_type, slave_addr, reg_addr, data_length, data,
ctrl_type, repeat_count, delay_time)
  • 一条 CSV 行可同时携带控制信息(ctrl_type, repeat_count)和I2C 参数
  • 例如 LOOP BEGIN:2, 2, 0x58, 0xF1, 0x3, , 表示:循环开始,且该行本身是一条读命令

5.2 命令编码

1
2
3
4
5
def get_command_header(self):
return [self.cmd_type, self.slave_addr, self.reg_addr, self.data_length]

def get_command_data(self):
return self.data # 写/比较命令的数据部分

上位机只需调用 get_command_header()get_command_data(),即可生成符合 STM32 协议的字节流。

6. 精髓五:EIS 扫频 = CSV 脚本驱动

6.1 多频率 = 多配置组

PyEisAdcCreator 根据 FREQ_INDEX: 0~0xF 生成 16 组配置,每组对应一个频率点。

6.2 CSV 中的”程序”

1
2
3
4
配置组1: 写寄存器 → DELAY:10 → 验证17个ADC点
配置组2: 写寄存器 → DELAY:10 → 验证17个ADC点
...
配置组16: ...

本质:CSV 描述了一个隐式循环——对每个频率点执行”配置→等待→验证”。
循环由行的顺序配置组分隔符体现,无需显式 LOOP,因为组数在生成时已固定。

6.3 可扩展性

若需”每个频率测 3 次取平均”,只需在每组外加:

1
2
3
LOOP BEGIN:3
... 配置组命令 ...
LOOP END

脚本解释器已支持,无需改代码。

7. 总结:脚本解释器的五层设计

内容
词法 CSV 行分割、列解析
语法 第一列控制关键字 + 后续列参数
语义 I2CCommand 对象(cmd_type, addr, data…)
执行 process_next_command + 回调 + QTimer
控制流 DELAY / LOOP / REPEAT / 顺序

精髓归纳

  1. CSV 即脚本:用表格形式表达”程序”
  2. 控制流入数据:第一列混入控制关键字
  3. 事件驱动执行:QTimer 替代递归,避免栈溢出
  4. 回调串行化:异步 I/O 通过回调驱动下一条
  5. 生成与解释分离:PyEisAdcCreator 生成,PyComSender 解释,各司其职

这种设计使 EIS 验证从”手工点寄存器”升级为”跑脚本”,实现流片前高质量自动化压测

6. 为什么测试的是”范围”而非精确值?

6.1 物理信号的本质:ADC 采样值

测试的核心不是寄存器,而是物理信号

1
DAC (已知数字正弦) → 运放 → 电池 → ADC 采样 V(t)
  • DAC 输出是确定的(已知 DAC 查表序列)
  • 电池是被测对象(阻抗 Z(ω) 未知,但可假设为标称值)
  • ADC 采样值 ≈ V(t) = Z(ω) × I(t)
  • 期望 ADC 值由 DAC 序列和标称阻抗计算得出

6.2 为什么需要范围比较?

  • 电芯一致性差异:不同电芯阻抗有 ±5% 偏差
  • 温度漂移:电芯阻抗随温度变化
  • ADC 量化误差:ADC 本身有 LSB 误差
  • 无法精确预测:电池是模拟器件,无法给出精确的期望值

因此:

  • 精确比较(类型3):要求 ADC 值与期望值完全一致,实际不存在
  • 范围比较(类型4):要求 ADC 值在 [低界, 高界] 范围内,允许测量误差

6.3 读命令用于什么场景?

当 ADC 值接近 0 时:

  • 范围比较无法判断正负(正 0.01 和负 0.01 都在 0 附近)
  • 此时用读命令(类型2)直接读回 ADC 值,由工程师判断是否合理

6.4 完整测试的物理闭环

1
2
3
4
5
6
7
8
9
10
11
12
13
PyEisAdcCreator (已知 DAC 序列 + 标称阻抗 → 计算期望范围)

生成 CSV (配置组 0~15,每个 = 写寄存器 + DELAY + 17个范围比较)

PyComSender (解释执行 → I2C 写/读/范围比较)

STM32 (接收命令 → I2C 操作 FPGA 寄存器)

FPGA (寄存器配置 → DAC 输出正弦 → ADC 采样 → 结果存入 EIS-Cell)

验证 (读回/范围比较 ADC 值 → Pass/Fail)

最终输出 (所有 16 频率 × 17 点 = 272 个验证结果)

一句话总结:测试验证的是 FPGA 芯片中 ADC 在电池两端实际采样到的电压信号 是否在预期范围内,从而判断整个 EIS 测量信号链路是否工作正常。


系列目录
01. EIS原理与电池阻抗谱
02. FPGA ADC激励与电池阻抗扫描原理
03. 上位机架构:PyQt与CSV流程
04. STM32命令转发与协议
05. CSV脚本解释器实现精髓(本文)