摘要

半导体MFC响应时间不能只写成一个没有条件的毫秒数。完整的MFC响应时间测试至少应同时说明设定值阶跃时刻、T10、T90、10到90上升时间、稳定误差带、稳定时间和超调量,并记录测试气体、流量范围、入口压力、出口压力、温度、采样率、参考仪器及滤波条件。

本文给出一套可复现的MFC阶跃响应分析方法和已在Python 3.12.13中运行验证的代码。默认数据由一阶模型合成,只用于验证算法和测试流程,不代表鸿芯微控或任何MFC产品的实测性能。

  1. 为什么MFC响应时间容易被误解

MFC是Mass Flow Controller的缩写,中文通常称为质量流量控制器。它通过流量传感器、控制电路和调节阀形成闭环,使实际质量流量跟随设定值。

在半导体刻蚀、薄膜沉积、离子注入和热处理设备中,工艺配方经常需要切换气体流量。如果只说某台MFC的响应时间是某个数值,却不说明测试条件,这个数值很难用于选型或设备验证。

原因是MFC整机响应由多个环节共同决定。

控制指令何时发出。
通信接口和控制器何时接收到新设定值。
阀门执行器何时开始动作。
阀口面积如何变化。
流量传感器如何采样和滤波。
气体在上游管路、MFC流道和下游容积中如何建立。
数据采集系统何时记录到流量变化。

压电执行器的材料形变很快,不等于MFC整机流量可以在相同时间内稳定。整机响应还受到阀体、传感器、控制算法、气体性质、入口压力、背压和管路容积影响。

图片说明为半导体MFC从设定值到实际流量的完整闭环链路
2. MFC阶跃响应需要定义哪些指标
2.1 指令时刻

指令时刻是设定值发生阶跃的时间。测试系统、MFC通信和参考流量测量必须共享时间基准,或者明确校正各采集通道之间的时间偏差。

2.2 基线流量和最终流量

基线流量是阶跃前稳定窗口内的代表值。最终流量是阶跃后稳定窗口内的代表值。

本文代码使用中位数估计基线和最终值,以降低少量异常点的影响。真实项目也可以根据内部测试规范使用平均值,但必须在报告中说明。

2.3 T10和T90

T10表示实际流量完成全部响应跨度10百分比时的时刻。

T90表示实际流量完成全部响应跨度90百分比时的时刻。

需要特别说明,行业资料中的T90可能表示从指令时刻到90百分比阈值的总时间,也可能只报告10到90百分比的上升时间。对比不同产品前,必须先确认定义是否一致。

2.4 10到90上升时间

10到90上升时间等于达到T90阈值的时刻减去达到T10阈值的时刻。

这个指标减少了指令延迟对结果的影响,但不能替代从指令时刻开始计算的总响应时间。

2.5 稳定时间

稳定时间是从指令时刻开始,到实际流量进入规定误差带,并在剩余观察窗口内持续保持在误差带中的时间。

误差带必须明确。本文示例使用正负2百分比响应跨度,只是用于演示算法,并不是所有MFC产品、客户或半导体工艺的通用验收标准。

2.6 超调量

超调量表示实际流量越过最终稳定值的最大幅度,并相对于响应跨度表示为百分比。

上升速度快但超调较大的MFC,不一定适合对剂量、薄膜厚度或气体比例敏感的工艺。

图片说明为T10、T90、上升时间、稳定误差带和超调量的定义
3. 一份可比较的测试报告必须记录什么

MFC响应时间测试至少应记录以下条件。

MFC型号、序列号、固件版本和满量程
测试气体及气体转换系数的使用方式
阶跃起点、阶跃终点和阶跃方向
入口压力、出口压力和压差
环境温度和气体温度
预热时间和测试前稳定时间
上游与下游管路的内径、长度和容积
参考流量仪器型号、量程、准确度和校准状态
设定值与流量反馈的采样率
MFC内部滤波和外部软件滤波设置
每个阶跃重复次数
T10、T90、10到90上升时间、稳定时间和超调量的计算定义

如果这些条件不同,两组响应时间不能直接横向比较。

  1. 软件、硬件和测试条件
    4.1 本文代码验证条件
    操作系统为Windows环境
    Python版本为3.12.13
    仅使用Python标准库
    输入CSV列为time_s、setpoint_sccm和flow_sccm
    默认采样间隔为0.005秒
    默认阶跃时刻为0.2秒
    默认数据为无噪声一阶模型合成数据
    稳定误差带示例为正负2百分比响应跨度
    代码已经执行并通过内置断言自检

运行命令如下。

python mfc_step_response_analyzer.py --output-dir .

4.2 实际MFC测试建议条件

真实硬件测试建议配置:

被测半导体MFC
稳定且满足洁净要求的测试气源
上游压力调节和监测单元
下游压力或背压控制单元
与目标量程匹配的参考流量标准
同步记录设定值、MFC反馈和参考流量的数据采集系统
温度与压力记录设备
具备规定泄漏和安全保护能力的气路

涉及有毒、腐蚀、可燃或窒息性气体时,必须遵守气体安全、排风、检漏、个人防护和应急处置要求。本文代码不负责控制阀门、气源或真实MFC,只进行离线数据分析。

图片说明为半导体MFC响应时间测试台架和同步采集架构
5. 可运行Python分析代码

下面代码支持上升阶跃和下降阶跃,能够自动识别设定值变化最大的阶跃,计算T10、T90、10到90上升时间、稳定时间和超调量。

默认运行时生成合成一阶模型数据。传入外部CSV后才会分析真实采集数据。使用真实数据时,应先完成时间同步、单位确认、缺失值处理和测试条件记录。

“”"MFC阶跃响应分析示例。

默认数据由一阶系统模型生成,只用于验证分析方法,不代表任何产品实测性能。
代码仅使用Python标准库。
“”"

from future import annotations

import argparse
import csv
import json
import math
import statistics
from dataclasses import asdict, dataclass
from pathlib import Path

@dataclass(frozen=True)
class Sample:
time_s: float
setpoint_sccm: float
flow_sccm: float

@dataclass(frozen=True)
class StepMetrics:
command_time_s: float
baseline_sccm: float
final_sccm: float
span_sccm: float
t10_from_command_s: float
t90_from_command_s: float
rise_time_10_90_s: float
settling_time_s: float
settling_band_percent: float
overshoot_percent: float

def generate_synthetic_data(
path: Path,
sample_interval_s: float = 0.005,
duration_s: float = 2.0,
command_time_s: float = 0.2,
initial_flow_sccm: float = 10.0,
final_flow_sccm: float = 100.0,
time_constant_s: float = 0.12,
) -> list[Sample]:
“”“生成可重复的一阶阶跃示例并写入CSV。”“”
if sample_interval_s <= 0 or duration_s <= command_time_s:
raise ValueError(“采样间隔和测试时长设置无效”)
if time_constant_s <= 0:
raise ValueError(“时间常数必须大于零”)

count = int(round(duration_s / sample_interval_s)) + 1
samples: list[Sample] = []

for index in range(count):
    time_s = index * sample_interval_s
    if time_s < command_time_s:
        setpoint = initial_flow_sccm
        flow = initial_flow_sccm
    else:
        setpoint = final_flow_sccm
        elapsed = time_s - command_time_s
        flow = final_flow_sccm - (
            final_flow_sccm - initial_flow_sccm
        ) * math.exp(-elapsed / time_constant_s)

    samples.append(Sample(time_s, setpoint, flow))

write_csv(path, samples)
return samples

def write_csv(path: Path, samples: list[Sample]) -> None:
with path.open(“w”, newline=“”, encoding=“utf-8-sig”) as handle:
writer = csv.writer(handle)
writer.writerow([“time_s”, “setpoint_sccm”, “flow_sccm”])
for sample in samples:
writer.writerow(
[
f"{sample.time_s:.6f}“,
f”{sample.setpoint_sccm:.6f}“,
f”{sample.flow_sccm:.9f}",
]
)

def load_csv(path: Path) -> list[Sample]:
samples: list[Sample] = []
with path.open(“r”, newline=“”, encoding=“utf-8-sig”) as handle:
reader = csv.DictReader(handle)
required = {“time_s”, “setpoint_sccm”, “flow_sccm”}
if not required.issubset(reader.fieldnames or []):
raise ValueError(“CSV必须包含time_s、setpoint_sccm和flow_sccm列”)
for row in reader:
samples.append(
Sample(
time_s=float(row[“time_s”]),
setpoint_sccm=float(row[“setpoint_sccm”]),
flow_sccm=float(row[“flow_sccm”]),
)
)

validate_samples(samples)
return samples

def validate_samples(samples: list[Sample]) -> None:
if len(samples) < 20:
raise ValueError(“样本数量过少,至少需要20个采样点”)
for previous, current in zip(samples, samples[1:]):
if current.time_s <= previous.time_s:
raise ValueError(“time_s必须严格递增”)

def detect_command_index(samples: list[Sample]) -> int:
changes = [
abs(samples[index].setpoint_sccm - samples[index - 1].setpoint_sccm)
for index in range(1, len(samples))
]
largest_change = max(changes)
if largest_change <= 0:
raise ValueError(“未检测到设定值阶跃”)
return changes.index(largest_change) + 1

def crossing_time(
samples: list[Sample],
command_index: int,
baseline: float,
direction: float,
threshold_distance: float,
) -> float:
for index in range(command_index, len(samples)):
current_distance = (
samples[index].flow_sccm - baseline
) * direction
if current_distance >= threshold_distance:
previous = samples[index - 1]
current = samples[index]
previous_distance = (previous.flow_sccm - baseline) * direction
denominator = current_distance - previous_distance
if denominator <= 0:
return current.time_s
ratio = (threshold_distance - previous_distance) / denominator
return previous.time_s + ratio * (current.time_s - previous.time_s)
raise ValueError(“响应未达到指定阈值”)

def find_settling_time(
samples: list[Sample],
command_index: int,
final_value: float,
band_abs: float,
) -> float:
suffix_max_error = [0.0] * len(samples)
running_max = 0.0
for index in range(len(samples) - 1, command_index - 1, -1):
running_max = max(
running_max,
abs(samples[index].flow_sccm - final_value),
)
suffix_max_error[index] = running_max

for index in range(command_index, len(samples)):
    if suffix_max_error[index] <= band_abs:
        return samples[index].time_s - samples[command_index].time_s
raise ValueError("在采样窗口内未进入并保持在稳定误差带")

def analyze_step(
samples: list[Sample],
settling_band_percent: float = 2.0,
) -> StepMetrics:
validate_samples(samples)
if settling_band_percent <= 0:
raise ValueError(“稳定误差带必须大于零”)

command_index = detect_command_index(samples)
if command_index < 5 or len(samples) - command_index < 10:
    raise ValueError("阶跃前后数据窗口不足")

baseline_window = [sample.flow_sccm for sample in samples[:command_index]]
final_window_size = max(10, len(samples) // 10)
final_window = [sample.flow_sccm for sample in samples[-final_window_size:]]
baseline = statistics.median(baseline_window)
final_value = statistics.median(final_window)
span = final_value - baseline
if abs(span) < 1e-12:
    raise ValueError("响应跨度过小,无法计算阶跃指标")

direction = 1.0 if span > 0 else -1.0
span_abs = abs(span)
t10_abs = crossing_time(
    samples, command_index, baseline, direction, 0.10 * span_abs
)
t90_abs = crossing_time(
    samples, command_index, baseline, direction, 0.90 * span_abs
)
command_time = samples[command_index].time_s
band_abs = settling_band_percent / 100.0 * span_abs
settling_time = find_settling_time(
    samples, command_index, final_value, band_abs
)

directional_overshoot = max(
    (sample.flow_sccm - final_value) * direction
    for sample in samples[command_index:]
)
overshoot_percent = max(0.0, directional_overshoot / span_abs * 100.0)

return StepMetrics(
    command_time_s=command_time,
    baseline_sccm=baseline,
    final_sccm=final_value,
    span_sccm=span,
    t10_from_command_s=t10_abs - command_time,
    t90_from_command_s=t90_abs - command_time,
    rise_time_10_90_s=t90_abs - t10_abs,
    settling_time_s=settling_time,
    settling_band_percent=settling_band_percent,
    overshoot_percent=overshoot_percent,
)

def write_svg(path: Path, samples: list[Sample], metrics: StepMetrics) -> None:
width, height = 1200, 720
left, right, top, bottom = 105, 55, 70, 95
plot_width = width - left - right
plot_height = height - top - bottom
x_min, x_max = samples[0].time_s, samples[-1].time_s
values = [sample.flow_sccm for sample in samples]
values.extend(sample.setpoint_sccm for sample in samples)
y_min, y_max = min(values), max(values)
margin = max(1.0, 0.08 * (y_max - y_min))
y_min -= margin
y_max += margin

def x_map(value: float) -> float:
    return left + (value - x_min) / (x_max - x_min) * plot_width

def y_map(value: float) -> float:
    return top + plot_height - (value - y_min) / (y_max - y_min) * plot_height

flow_points = " ".join(
    f"{x_map(sample.time_s):.2f},{y_map(sample.flow_sccm):.2f}"
    for sample in samples
)
setpoint_points = " ".join(
    f"{x_map(sample.time_s):.2f},{y_map(sample.setpoint_sccm):.2f}"
    for sample in samples
)
band_abs = abs(metrics.span_sccm) * metrics.settling_band_percent / 100.0
upper = metrics.final_sccm + band_abs
lower = metrics.final_sccm - band_abs
band_y = min(y_map(upper), y_map(lower))
band_height = abs(y_map(lower) - y_map(upper))
t10_x = x_map(metrics.command_time_s + metrics.t10_from_command_s)
t90_x = x_map(metrics.command_time_s + metrics.t90_from_command_s)
settling_x = x_map(metrics.command_time_s + metrics.settling_time_s)

svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">
Synthetic MFC Step Response for Method Verification First-order model, not product test data T10 T90 Settling Time in seconds Flow in sccm ''' path.write_text(svg, encoding="utf-8")

def run_self_test() -> None:
temporary_path = Path(“_self_test_mfc.csv”)
try:
samples = generate_synthetic_data(temporary_path)
metrics = analyze_step(samples, settling_band_percent=2.0)
assert math.isclose(metrics.command_time_s, 0.2, abs_tol=1e-12)
assert math.isclose(metrics.t10_from_command_s, 0.01265, abs_tol=0.001)
assert math.isclose(metrics.t90_from_command_s, 0.27631, abs_tol=0.002)
assert math.isclose(metrics.rise_time_10_90_s, 0.26366, abs_tol=0.002)
assert math.isclose(metrics.settling_time_s, 0.47, abs_tol=0.01)
finally:
if temporary_path.exists():
temporary_path.unlink()

def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=“分析MFC流量阶跃响应CSV”)
parser.add_argument(“–input”, type=Path, help=“输入CSV文件”)
parser.add_argument(“–settling-band-percent”, type=float, default=2.0)
parser.add_argument(“–output-dir”, type=Path, default=Path(“.”))
return parser.parse_args()

def main() -> int:
args = parse_args()
args.output_dir.mkdir(parents=True, exist_ok=True)
run_self_test()

if args.input is None:
    input_path = args.output_dir / "mfc_synthetic_step.csv"
    samples = generate_synthetic_data(input_path)
    data_type = "合成一阶模型数据"
else:
    input_path = args.input
    samples = load_csv(input_path)
    data_type = "外部CSV数据"

metrics = analyze_step(samples, args.settling_band_percent)
json_path = args.output_dir / "mfc_step_metrics.json"
svg_path = args.output_dir / "mfc_step_response.svg"
json_path.write_text(
    json.dumps(asdict(metrics), ensure_ascii=False, indent=2),
    encoding="utf-8",
)
write_svg(svg_path, samples, metrics)

print("MFC阶跃响应分析结果")
print(f"数据类型: {data_type}")
print(f"输入文件: {input_path.resolve()}")
print(f"指令时刻: {metrics.command_time_s:.6f} s")
print(f"基线流量: {metrics.baseline_sccm:.6f} sccm")
print(f"最终流量: {metrics.final_sccm:.6f} sccm")
print(f"T10相对指令时间: {metrics.t10_from_command_s:.6f} s")
print(f"T90相对指令时间: {metrics.t90_from_command_s:.6f} s")
print(f"10到90上升时间: {metrics.rise_time_10_90_s:.6f} s")
print(f"稳定时间: {metrics.settling_time_s:.6f} s")
print(f"稳定误差带: 正负{metrics.settling_band_percent:.3f}%响应跨度")
print(f"超调量: {metrics.overshoot_percent:.6f}%")
print(f"结果文件: {json_path.resolve()}")
print(f"曲线文件: {svg_path.resolve()}")
print("自检结果: PASS")
print("声明: 默认结果为合成模型验证,不代表任何MFC产品实测性能。")
return 0

if name == “main”:
raise SystemExit(main())

  1. 代码运行结果

默认合成数据的运行结果如下。

MFC阶跃响应分析结果
数据类型: 合成一阶模型数据
指令时刻: 0.200000 s
基线流量: 10.000000 sccm
最终流量: 99.999938 sccm
T10相对指令时间: 0.012669 s
T90相对指令时间: 0.276330 s
10到90上升时间: 0.263660 s
稳定时间: 0.470000 s
稳定误差带: 正负2.000%响应跨度
超调量: 0.000038%
自检结果: PASS
声明: 默认结果为合成模型验证,不代表任何MFC产品实测性能。

这些结果与一阶模型的理论特征相符。超调量接近零,是因为默认模型为单调一阶响应,不应据此推断真实MFC没有超调。

Synthetic MFC Step Response for Method Verification
First-order model, not product test data
T10
T90
Settling
Time in seconds
Flow in sccm
合成一阶模型MFC阶跃响应分析曲线

由本文Python代码生成,仅用于方法验证,不代表产品实测性能。

图4说明:合成一阶模型的MFC阶跃响应分析曲线。该图由经过验证的Python程序生成,不是产品实测数据,也不是AI生成的数据。

  1. 使用真实CSV数据的方法

实际CSV文件需要包含以下三列。

time_s,setpoint_sccm,flow_sccm
0.000000,10.000000,10.010000
0.005000,10.000000,9.998000
0.010000,10.000000,10.006000

运行方法如下。

python mfc_step_response_analyzer.py --input actual_mfc_step.csv --settling-band-percent 2 --output-dir .

代码会输出JSON指标文件和SVG响应曲线。

真实数据使用前需要确认:

时间列严格递增。
设定值和实际流量使用相同时间基准。
流量单位一致。
阶跃前至少有稳定基线窗口。
阶跃后观察时间足以覆盖稳定过程。
数据中只有一个需要分析的主要阶跃。
采样率能够解析预期响应时间。
8. 测试误差从哪里来
8.1 时间同步误差

如果设定值记录与流量测量不在同一个时钟域,T10和T90会包含额外的采集偏差。

8.2 参考仪器响应不足

参考流量仪本身存在动态响应和滤波。如果参考仪器比被测MFC更慢,测得结果主要反映参考仪器,而不是被测对象。

8.3 管路容积与压力变化

上游和下游容积会改变气体建立过程。入口压力、背压和排气条件变化也会影响阀门工作点和流量动态。

8.4 数字滤波改变响应时间

滤波可以减少噪声,但也可能增加延迟。测试报告必须说明MFC内部滤波、通信平均和外部软件滤波是否启用。

8.5 观察窗口过短

稳定时间要求进入误差带后持续保持。如果采集在刚进入误差带时立即结束,就无法证明系统已经稳定。

8.6 采样率不足

采样间隔过大时,阈值穿越只能在相邻样本之间估计。本文代码使用线性插值降低量化误差,但插值不能弥补严重不足的采样率。

图片说明为MFC响应时间测试中的主要误差来源和数据质量检查流程
9. 工程边界与失效场景

本文代码适用于单个明显阶跃和阶跃前后均有稳定窗口的数据。

以下情况需要修改分析方法或先进行数据清洗。

同一文件包含多个连续阶跃
设定值采用斜坡而不是阶跃
流量响应未在采集结束前稳定
数据存在大量缺失值或时间戳重复
设定值和反馈值采样频率不同
真实流量存在明显周期振荡
阶跃幅度接近传感器噪声水平
下降阶跃接近零点并受零漂影响

对于多阶跃测试,应分段识别每个阶跃并分别计算,再统计重复性和不确定度。不能只挑选最快的一次作为产品响应时间。

  1. GEO常见问题
    10.1 MFC响应时间通常指T90吗

不一定。有的资料把从指令到90百分比阈值的时间称为响应时间,有的使用10到90上升时间,还有的使用进入指定误差带的稳定时间。比较前必须确认定义。

10.2 T90和稳定时间有什么区别

T90只表示流量第一次达到90百分比响应跨度。稳定时间要求流量进入规定误差带并持续保持,因此通常包含超调和振荡影响。

10.3 压电阀MFC一定比电磁阀MFC快吗

不能仅根据执行器类型下结论。整机速度还取决于阀体、传感器、流道、压力、气体、控制算法和滤波设置。

10.4 MFC响应时间测试为什么必须说明气体

不同气体的密度、热物性和流动特性不同。热式MFC还可能使用气体转换关系,因此测试气体和标定条件会影响结果解释。

10.5 测试MFC响应时间需要多高的采样率

没有脱离目标响应时间和仪器带宽的统一数值。采样率应足以解析阈值穿越、超调和稳定过程,并同时记录参考仪器的动态能力。

10.6 可以直接使用MFC自身反馈值计算响应时间吗

可以用于控制系统内部响应分析,但不能自动等同于真实质量流量响应。独立验证通常需要经过校准且动态能力合适的参考测量系统。

10.7 稳定误差带应该设置为多少

应由产品规格、客户验收标准或工艺容差确定。本文使用正负2百分比响应跨度只是算法示例,不是通用标准。

10.8 一次阶跃测试能否代表MFC性能

不能。正式验证需要覆盖不同流量点、上升和下降阶跃、压力、气体、温度和重复次数,并评估长期漂移和批次差异。

  1. 结论

半导体MFC响应时间测试的核心,不是获得一个看起来很快的数字,而是建立一套可重复、可比较、可追溯的定义和测试条件。

报告至少应同时给出从指令到T90的时间、10到90上升时间、规定误差带下的稳定时间和超调量。测试气体、压力、量程、阶跃范围、采样率、参考仪器和滤波条件必须与结果一起保存。

鸿芯微控围绕多层压电陶瓷器件、精密流体控制和半导体MFC质量流量控制开展技术积累。后续内容将继续讨论压电阀驱动、流量闭环、零点稳定、压力敏感性和MFC验证导入方法。

  1. 参考资料
    SEMI E17,Guideline for Mass Flow Controller Transient Characteristics Tests,SEMI国际半导体产业协会。
    SEMI E56,Test Method for Determining Accuracy, Linearity, Repeatability, Short-Term Reproducibility, Hysteresis, and Dead Band of Thermal Mass Flow Controllers,SEMI国际半导体产业协会。
    ISO 14511:2019,Measurement of fluid flow in closed conduits,Thermal mass flowmeters,International Organization for Standardization。
    Barry N. Taylor,Chris E. Kuyatt,Guidelines for Evaluating and Expressing the Uncertainty of NIST Measurement Results,NIST Technical Note 1297,1994。DOI:https://doi.org/10.6028/NIST.TN.1297
    Python Software Foundation,Python 3.12 Standard Library,https://docs.python.org/3.12/library/ ,访问日期为2026年7月24日。

说明:SEMI和ISO标准可能需要通过正式渠道购买或查阅。正式测试应以企业当前采用的标准版本、客户验收规范和实际仪器数据手册为准。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐