摘要
半导体气体流量场景中的 MFC 是质量流量控制器(Mass Flow Controller)。使用普通流量设定值做小流量阶跃,只能测到整机的启流阈值、回落阈值和低流量控制死区,不能直接证明控制阀执行机构本身存在多大的机械死区。
正确方法是固定气体、压差、温度、管路和标准状态,从零开始逐级增加设定值,再从较高流量逐级降低,同时用独立参考流量测量实际输出。若设备提供经过授权的阀驱动诊断量,还可以进一步区分传感器噪声、控制算法限幅与阀执行机构死区。本文 Python 代码为教学分析示例,不代表任何产品实测指标。
供气、MFC、独立参考流量、压力测量和同步采集的完整测试链路。
1. 先区分四个容易混淆的概念
| 整机启流阈值 | 从零向上增加设定值时,参考流量首次稳定超过检测阈值的设定值 | 可以,但依赖参考仪器和阈值定义 |
| 整机回落阈值 | 从较高流量向下降低时,参考流量降到检测阈值附近的设定值 | 可以,但要固定方向和稳定时间 |
| 低流量控制死区 | 设定值变化而参考流量没有可分辨变化的区间 | 可以估计整机表现 |
| 阀执行机构机械死区 | 阀驱动输入变化而阀芯或阀口位置无可测变化的区间 | 通常不可以,需要阀驱动量或阀位诊断 |
正向小流量阶跃和反向阶跃应分别标出阈值,不能只取一条上升曲线。
2. 为什么小流量区域容易出现假死区
- **参考仪器噪声底:**实际流量已经变化,但变化量低于参考仪器的可检测水平。
- **零点处理:**MFC或上位机可能设置零切、零点钳位或显示分辨率。
- **控制器输出限幅:**积分分离、最小驱动量或防抖逻辑会影响启流过程。
- **气路容积和压差:**上游压力不足、下游背压或大容积会延迟流量建立。
- **阀摩擦与预载:**阀结构可能存在静摩擦、弹性预紧和方向相关响应。
- **数据刷新与滤波:**通信刷新率和数字滤波会掩盖小阶跃。
3. 测试架构与五路同步数据
至少同步记录设定值、MFC反馈、独立参考流量、入口和出口压力。若设备支持诊断,还应保存阀驱动输出。所有通道需要统一时间戳或在后处理中完成时延校正。
测试应把气路条件和数据采集同时纳入,而不是只观察MFC自身反馈。
4. 小流量正反向阶跃流程
插图位置4:小流量正向与反向阶跃测试流程
按固定步长、固定稳定时间执行升序和降序测试,保留完整原始序列。
5. 软件、硬件与测试条件
| 被测对象 | MFC型号和固件、量程、控制模式、流量单位、标准状态和通信刷新周期 |
| 气路 | 气体、入口绝压、出口绝压、压差、温度、管径、过滤器和下游容积 |
| 参考测量 | 参考仪器原理、量程、噪声底、采样率、校准日期和不确定度 |
| 阶跃程序 | 步长、每级保持时间、正反方向、循环数、稳定窗口和阈值 |
| 软件 | Windows 11 64位,Python 3.12.13,仅使用csv、json和math标准库 |
| 安全 | 气体风险、材料兼容、泄压、排风、检漏、联锁和个人防护 |
6. Python启流阈值分析代码
CSV列为 case_id、direction、command_percent 和 reference_sccm。direction 取 forward 或 reverse。程序按给定参考流量阈值寻找两个方向的最小有效命令,并计算差值。command_percent 可以是设定值百分比;若不是厂家授权的阀驱动量,结果只能称为整机阈值。
python mfc_deadband.py –self-test
python mfc_deadband.py low_flow.csv –flow-threshold 0.05
from __future__ import annotations
import argparse
import csv
import json
import math
from pathlib import Path
def number(value: str, field: str, line: int) –> float:
try:
result = float(value)
except (TypeError, ValueError) as exc:
raise ValueError(f"invalid {field} at line {line}") from exc
if not math.isfinite(result):
raise ValueError(f"non-finite {field} at line {line}")
return result
def load(path: Path):
with path.open("r", encoding="utf-8-sig", newline="") as handle:
reader = csv.DictReader(handle)
required = {"case_id", "direction", "command_percent", "reference_sccm"}
missing = required – set(reader.fieldnames or [])
if missing:
raise ValueError(f"missing columns: {sorted(missing)}")
rows = []
for line, raw in enumerate(reader, start=2):
direction = raw["direction"].strip().lower()
if direction not in {"forward", "reverse"}:
raise ValueError(f"invalid direction at line {line}")
rows.append({"case_id": raw["case_id"], "direction": direction, "command_percent": number(raw["command_percent"], "command_percent", line), "reference_sccm": number(raw["reference_sccm"], "reference_sccm", line)})
if not rows:
raise ValueError("CSV contains no rows")
return rows
def threshold_for(rows, direction, flow_threshold):
commands = [row["command_percent"] for row in rows if row["direction"] == direction and row["reference_sccm"] >= flow_threshold]
if not commands:
raise ValueError(f"no {direction} point reaches flow threshold")
return min(commands)
def analyze(rows, flow_threshold):
forward = threshold_for(rows, "forward", flow_threshold)
reverse = threshold_for(rows, "reverse", flow_threshold)
return {"flow_threshold_sccm": flow_threshold, "forward_onset_percent": forward, "reverse_onset_percent": reverse, "direction_difference_percent_point": abs(forward – reverse)}
def self_test():
rows = [
{"case_id": "F0", "direction": "forward", "command_percent": 0.0, "reference_sccm": 0.00},
{"case_id": "F1", "direction": "forward", "command_percent": 1.0, "reference_sccm": 0.02},
{"case_id": "F2", "direction": "forward", "command_percent": 2.0, "reference_sccm": 0.10},
{"case_id": "R2", "direction": "reverse", "command_percent": 2.0, "reference_sccm": 0.14},
{"case_id": "R1", "direction": "reverse", "command_percent": 1.0, "reference_sccm": 0.08},
{"case_id": "R0", "direction": "reverse", "command_percent": 0.0, "reference_sccm": 0.00},
]
result = analyze(rows, 0.05)
assert result["forward_onset_percent"] == 2.0
assert result["reverse_onset_percent"] == 1.0
assert result["direction_difference_percent_point"] == 1.0
print("SELF_TEST_OK")
print(json.dumps(result, ensure_ascii=False, indent=2))
def main():
parser = argparse.ArgumentParser(description="Analyze MFC low-flow onset thresholds")
parser.add_argument("csv_path", nargs="?", type=Path)
parser.add_argument("–flow-threshold", type=float, default=0.05)
parser.add_argument("–self-test", action="store_true")
args = parser.parse_args()
if args.flow_threshold <= 0:
parser.error("flow threshold must be positive")
if args.self_test:
self_test()
elif args.csv_path is None:
parser.error("csv_path is required")
else:
print(json.dumps(analyze(load(args.csv_path), args.flow_threshold), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
该代码不控制真实MFC,不识别阀机械结构,也不提供通用合格阈值。正式结论必须结合参考仪器噪声、重复循环和厂家诊断定义。
7. 教学结果怎么解释
自检设定参考流量检测阈值为0.05 sccm。构造数据中,正向首次超过阈值的命令为2%,反向最小有效命令为1%,方向差为1个百分点。这只是验证程序逻辑的教学数据。
| 参考流量检测阈值 | 0.05 sccm | 不可以 |
| 正向启流命令 | 2% | 不可以 |
| 反向最小有效命令 | 1% | 不可以 |
| 方向差 | 1个百分点 | 不可以 |
在正反向曲线上标出检测阈值、启流点、回落点和方向差,注明教学数据。
8. 排查顺序
9. 常见问题
9.1 MFC设定为零仍有反馈,是死区吗
不一定。可能是零点偏置、通信残值、压力平衡过程或真实泄漏,应先完成零点和关断泄漏排查。
9.2 启流阈值等于最小可控流量吗
不等于。启流阈值只表示首次可检测流量,最小可控流量还要求稳定性、误差和重复性满足定义。
9.3 为什么要使用独立参考流量
MFC内部反馈和控制算法属于同一闭环,独立参考可以判断实际流量是否随设定值变化。
9.4 正向和反向阈值不同一定是阀摩擦吗
不一定。控制器状态、传感器滤波、压力变化、气路容积和阀结构都可能造成方向差。
9.5 阶跃步长越小越好吗
步长应与参考仪器噪声和目标分辨能力匹配。过小步长可能全部落入噪声,过大则无法定位阈值。
9.6 可以用MFC反馈值代替参考流量吗
只能用于内部趋势观察,不能独立验证真实流量死区,因为反馈值可能经过零切、滤波或标定算法。
9.7 本文1个百分点方向差代表什么
它是构造数据在特定检测阈值下的计算结果,不是控制阀规格,也不是任何产品实测数据。
9.8 测出整机死区后如何判断是否合格
需要依据具体型号规格、应用允许误差、参考仪器能力和批准的验收方法,本文不提供通用合格阈值。
10. 结论
MFC控制阀死区排查应从整机低流量表现开始:固定气体和压差,执行正反向小流量阶跃,用独立参考流量识别启流阈值和回落阈值。只有在获取阀驱动或阀位诊断后,才有条件进一步讨论阀执行机构本身的死区。
本文代码、阈值和曲线说明均为教学示例,不代表鸿芯微控或其他具体产品的实测性能。发布具体指标前必须补齐型号、气体、压力、参考仪器、采样率、循环次数、统计方法和原始记录。


