摘要
压电执行器推力不能只用一只力传感器顶住执行器后读取峰值。可复现的方法是在规定驱动电压、预紧力、温度、安装和准静态加载条件下,同时测量增量反力与增量位移,得到固定电压下的力位移曲线。
自由位移是规定预紧基线下、增量外载接近零时的驱动位移。阻塞力是相同条件下增量位移为零时的理论或实测反力。在线性近似中,可先拟合位移随增量反力的直线,再由斜率估计系统等效刚度,并用零位移截距估计阻塞力。
阻塞力不等于最大允许压缩载荷,预紧力也不等于输出推力。直接硬阻塞、偏心加载、拉伸、剪切和冲击都可能损伤压电陶瓷。本文Python代码只对已审核的准静态数据做线性拟合,自检数据由公式构造,不代表任何压电执行器的真实推力或产品规格。
1. 压电执行器推力测试需要区分哪些量
| 术语 | 定义 | 不能混淆的概念 |
|---|---|---|
| 自由位移 | 规定预紧基线下,增量反向载荷接近零时的驱动增量位移 | 不是完全无机械约束,也不是任意电压下的最大行程 |
| 工作推力 | 在指定工作位移、驱动电压和机械边界下产生的增量反力 | 不是一个脱离位移条件的固定数字 |
| 阻塞力 | 指定驱动条件下,增量位移为零时对应的增量反力 | 不等于最大允许压缩载荷或破坏载荷 |
| 预紧力 | 为保持压缩接触和满足结构要求而施加的基线压缩力 | 不属于驱动电压产生的增量推力 |
| 等效刚度 | 选定力位移区间内增量力与增量位移变化的比值 | 测试结果可能同时包含执行器、夹具和框架刚度 |
本文以增量量进行分析。测试开始前先在规定预紧力和零驱动基线下归零,后续force_n与displacement_um分别表示相对该基线的增量反力和增量位移。
2. 自由位移、刚度与阻塞力的线性近似
在选定的准静态工作区间内,可用下列ASCII公式做一阶近似。
x = x_free + slope * F
slope must be negative
k_equivalent = -1 / slope
F_block = -x_free / slope
F_work = k_equivalent * (x_free - x_work)
其中x是增量位移,F是与驱动伸长方向相反的增量载荷,x_free是拟合直线在F等于零处的截距。若位移单位为um、力单位为N,则等效刚度单位为N每um,阻塞力单位为N。
2.1 为什么优先做安全区间外推
对许多堆叠压电执行器,直接把位移机械锁死后再快速升压会产生很高应力和瞬态风险。更稳妥的工程方法是在制造商允许的压缩载荷区间内测量多个力位移点,再将拟合线外推到增量位移为零的位置。外推结果必须标注为估计值。
2.2 为什么系统刚度不等于执行器本体刚度
如果位移传感器跨越了夹具、力传感器、柔性铰链或框架,它测到的是整个载荷链的变形。应尽量在执行器两端直接测位移,或者通过独立标定得到夹具柔度并进行不确定度评估。
3. 压电执行器推力测试架构
4. 压电执行器推力测试流程
- 读取具体执行器和驱动器资料,确认电压极性、允许电压、预紧范围、压缩载荷和安装要求。
- 校准力传感器、位移传感器和实际电压测量链,并记录采样率与时间同步方式。
- 检查端面平行度和加载同轴度,必要时使用适合的球面座或柔性导向结构。
- 在断电状态施加规定预紧力,等待机械与温度稳定后建立力和位移基线。
- 施加目标电压并等待规定稳定窗口,记录增量自由位移。
- 在安全压缩区间内分级增加反向载荷,每个点同时记录增量力、增量位移、电压和温度。
- 按加载与卸载方向分别重复,检查滑移、迟滞、蠕变和传感器回零。
- 只在残差可接受的区间拟合直线,并把阻塞力写成外推值或直接实测值。
5. 软件、硬件与测试条件
| 类别 | 必须记录的内容 |
|---|---|
| 被测执行器 | 型号、样品编号、结构状态、极化方向、安装与公开权限 |
| 驱动条件 | 驱动器型号、实际电压、波形、上升时间、保持时间、限流和放电方式 |
| 预紧与加载 | 预紧力、加载方向、加载速率、每级保持时间和允许载荷边界 |
| 力测量 | 力传感器型号、量程、校准状态、采样率和安装位置 |
| 位移测量 | 传感器原理、量程、校准状态、安装位置、分辨率和带宽 |
| 机械链 | 框架刚度、夹具柔度、端面平行度、同轴度和导向方式 |
| 环境与统计 | 温度、稳定时间、样品数、重复次数、拟合区间和异常值规则 |
| 本文软件 | Windows 11 64位,Python 3.12,仅使用csv、json、math和statistics标准库 |
动态冲击力、谐振状态和快速脉冲推力属于另一类测试,需要更高带宽的力与位移传感器、动态校准和结构模态分析。本文只处理准静态力位移数据。
6. Python力位移拟合代码
下面代码按series_id分别拟合位移与增量反力。一个序列只能包含同一电压、预紧力、温度条件和加载方向。代码不会判断载荷是否安全,也不会补偿夹具柔度或压电迟滞。
from __future__ import annotations
import argparse
import csv
import json
import math
import statistics
from collections import defaultdict
from pathlib import Path
REQUIRED_COLUMNS = {
"series_id",
"direction",
"voltage_v",
"preload_n",
"force_n",
"displacement_um",
"temperature_c",
}
VALID_DIRECTIONS = {"loading", "unloading"}
def finite_float(value: str, field: str, line_number: int) -> float:
try:
number = float(value)
except ValueError as exc:
raise ValueError(f"invalid {field} at line {line_number}") from exc
if not math.isfinite(number):
raise ValueError(f"non-finite {field} at line {line_number}")
return number
def nonnegative_float(value: str, field: str, line_number: int) -> float:
number = finite_float(value, field, line_number)
if number < 0:
raise ValueError(f"{field} must not be negative at line {line_number}")
return number
def parse_row(raw: dict[str, str], line_number: int) -> dict[str, object]:
series_id = raw["series_id"].strip()
direction = raw["direction"].strip()
if not series_id:
raise ValueError(f"empty series_id at line {line_number}")
if direction not in VALID_DIRECTIONS:
raise ValueError(f"invalid direction at line {line_number}")
return {
"series_id": series_id,
"direction": direction,
"voltage_v": nonnegative_float(raw["voltage_v"], "voltage_v", line_number),
"preload_n": nonnegative_float(raw["preload_n"], "preload_n", line_number),
"force_n": nonnegative_float(raw["force_n"], "force_n", line_number),
"displacement_um": finite_float(
raw["displacement_um"], "displacement_um", line_number
),
"temperature_c": finite_float(
raw["temperature_c"], "temperature_c", line_number
),
}
def read_rows(path: Path) -> list[dict[str, object]]:
rows: list[dict[str, object]] = []
with path.open("r", encoding="utf-8-sig", newline="") as handle:
reader = csv.DictReader(handle)
if reader.fieldnames is None:
raise ValueError("CSV has no header")
missing = REQUIRED_COLUMNS.difference(reader.fieldnames)
if missing:
raise ValueError(f"missing columns: {sorted(missing)}")
for line_number, raw in enumerate(reader, start=2):
rows.append(parse_row(raw, line_number))
if not rows:
raise ValueError("CSV contains no data rows")
return rows
def linear_fit(x_values: list[float], y_values: list[float]) -> tuple[float, float, float, float]:
if len(x_values) < 3 or len(set(x_values)) < 3:
raise ValueError("each series needs at least three distinct force points")
x_mean = statistics.fmean(x_values)
y_mean = statistics.fmean(y_values)
sxx = sum((x - x_mean) ** 2 for x in x_values)
sxy = sum((x - x_mean) * (y - y_mean) for x, y in zip(x_values, y_values))
slope = sxy / sxx
intercept = y_mean - slope * x_mean
residuals = [
y - (intercept + slope * x)
for x, y in zip(x_values, y_values)
]
residual_sum = sum(value * value for value in residuals)
total_sum = sum((y - y_mean) ** 2 for y in y_values)
r_squared = 1.0 if total_sum == 0 else 1.0 - residual_sum / total_sum
max_abs_residual = max(abs(value) for value in residuals)
return slope, intercept, r_squared, max_abs_residual
def summarize(rows: list[dict[str, object]]) -> list[dict[str, object]]:
grouped: dict[str, list[dict[str, object]]] = defaultdict(list)
for row in rows:
grouped[str(row["series_id"])].append(row)
summaries: list[dict[str, object]] = []
for series_id, selected in sorted(grouped.items()):
voltages = {float(row["voltage_v"]) for row in selected}
preloads = {float(row["preload_n"]) for row in selected}
directions = {str(row["direction"]) for row in selected}
if len(voltages) != 1 or len(preloads) != 1 or len(directions) != 1:
raise ValueError(f"series {series_id} mixes test conditions")
forces = [float(row["force_n"]) for row in selected]
displacements = [float(row["displacement_um"]) for row in selected]
slope, intercept, r_squared, max_residual = linear_fit(forces, displacements)
if slope >= 0:
raise ValueError(f"series {series_id} does not have a negative force slope")
equivalent_stiffness = -1.0 / slope
blocking_force = -intercept / slope
summaries.append({
"series_id": series_id,
"direction": next(iter(directions)),
"voltage_v": next(iter(voltages)),
"preload_n": next(iter(preloads)),
"point_count": len(selected),
"force_n_min": min(forces),
"force_n_max": max(forces),
"free_displacement_um_fit": intercept,
"slope_um_per_n": slope,
"equivalent_stiffness_n_per_um": equivalent_stiffness,
"blocking_force_n_extrapolated": blocking_force,
"fit_r_squared": r_squared,
"max_abs_residual_um": max_residual,
"temperature_c_min": min(float(row["temperature_c"]) for row in selected),
"temperature_c_max": max(float(row["temperature_c"]) for row in selected),
})
return summaries
def create_self_test(path: Path) -> None:
# Formula-generated teaching values, not product measurements.
free_displacement_um = 10.0
stiffness_n_per_um = 5.0
forces_n = [0.0, 10.0, 20.0, 30.0, 40.0]
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8", newline="") as handle:
writer = csv.writer(handle)
writer.writerow([
"series_id", "direction", "voltage_v", "preload_n",
"force_n", "displacement_um", "temperature_c",
])
for force_n in forces_n:
displacement_um = free_displacement_um - force_n / stiffness_n_per_um
writer.writerow([
"TEACHING_LOAD", "loading", 100.0, 20.0,
force_n, displacement_um, 25.0,
])
def main() -> None:
parser = argparse.ArgumentParser(description="Fit piezo force-displacement data.")
source = parser.add_mutually_exclusive_group(required=True)
source.add_argument("--input", type=Path)
source.add_argument("--self-test", action="store_true")
parser.add_argument("--output-dir", type=Path, default=Path("."))
args = parser.parse_args()
args.output_dir.mkdir(parents=True, exist_ok=True)
if args.self_test:
input_path = args.output_dir / "piezo_force_self_test.csv"
create_self_test(input_path)
else:
input_path = args.input
rows = read_rows(input_path)
summaries = summarize(rows)
if args.self_test:
first = summaries[0]
checks = [
math.isclose(float(first["free_displacement_um_fit"]), 10.0, rel_tol=1e-12),
math.isclose(float(first["equivalent_stiffness_n_per_um"]), 5.0, rel_tol=1e-12),
math.isclose(float(first["blocking_force_n_extrapolated"]), 50.0, rel_tol=1e-12),
]
if not all(checks):
raise AssertionError(f"self-test failed: {first}")
report = {
"data_status": "formula-generated self-test" if args.self_test else "user input",
"row_count": len(rows),
"series_count": len(summaries),
"self_test_status": "passed" if args.self_test else "not_applicable",
"series": summaries,
"boundary": (
"The code does not validate safe load, compensate fixture compliance, "
"control high voltage, or turn extrapolated force into a product rating."
),
}
with (args.output_dir / "piezo_force_summary.json").open(
"w", encoding="utf-8"
) as handle:
json.dump(report, handle, ensure_ascii=False, indent=2)
print(json.dumps(report, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
6.1 运行自检
python piezo_force_fit.py --self-test --output-dir self_test_output
6.2 分析真实测试数据
python piezo_force_fit.py --input measured_force_displacement.csv --output-dir result
真实CSV中的force_n和displacement_um必须是相对预紧基线的增量量。加载与卸载应使用不同series_id,不能混合拟合。
series_id,direction,voltage_v,preload_n,force_n,displacement_um,temperature_c
LOAD_100V,loading,100,20,10,8.1,25
7. 可验证数据与测试方法
本文没有连接真实压电执行器、驱动器、力传感器或位移传感器,因此不发布产品自由位移、刚度和阻塞力实测值。下表只描述代码自检的公式构造数据。
| 项目 | 自检设置 | 数据状态 | 程序应恢复 |
|---|---|---|---|
| 自由位移 | 10 um | 公式构造值 | 拟合截距10 um |
| 等效刚度 | 5 N每um | 公式构造值 | 斜率换算5 N每um |
| 阻塞力 | 50 N | 公式构造值 | 零位移外推50 N |
| 增量反力点 | 0、10、20、30、40 N | 教学点 | 五点线性拟合 |
| 真实产品性能 | 无样品和测量链数据 | 未测试 | 不得从自检推断 |
真实测试至少应给出原始力位移数据、拟合区间、加载方向、残差、框架柔度、力与位移不确定度。若阻塞力位置超出真实测量区间,应明确写为外推值,并说明外推距离。

8. 常见错误与失效风险
8.1 把预紧力当成执行器推力
预紧力是测试或安装的基线压缩力。驱动推力应使用相对于基线的增量反力表示,两者不能直接相加后作为输出性能。
8.2 把最大压缩载荷当成阻塞力
最大允许压缩载荷是结构安全边界,阻塞力是指定电压下零增量位移对应的驱动力定义。两者来源和用途不同。
8.3 直接硬阻塞并快速升压
刚性锁死、驱动过快和力传感器过载会带来机械与电气风险。只有器件和夹具文件明确允许时,才能进行直接阻塞测试。
8.4 忽略偏心、端面和平行度
偏心会引入弯矩和侧向力,使力读数和位移读数失真,并可能损伤多层压电陶瓷堆叠。
8.5 用准静态结果代表动态冲击力
动态条件还受惯性、结构模态、传感器带宽和驱动电流限制。准静态阻塞力不能直接替代高频或冲击工况测试。
9. 常见问题

9.1 压电执行器推力怎么计算
在线性近似区间,可用等效刚度乘以自由位移与工作位移之差。但刚度和自由位移必须来自同一电压、预紧、温度和安装条件。
9.2 阻塞力是否等于刚度乘以自由位移
在理想线性模型中可以这样估算。真实执行器存在非线性和迟滞,应通过力位移曲线验证,并注明是实测还是外推。
9.3 预紧力越大,输出推力越大吗
不能这样推断。预紧用于满足压缩接触和结构要求,过大预紧会占用载荷余量并增加应力,必须服从具体器件手册。
9.4 可以把压电执行器直接顶在力传感器上测吗
只有在同轴、端面适配、预紧受控、量程足够和高压安全措施完整时才可进行。裸陶瓷直接硬接触通常风险较高。
9.5 为什么不同夹具测出的阻塞力不同
框架刚度、力传感器柔度、位移测点、端面接触、同轴度和预紧都会改变系统力位移关系。报告必须描述完整载荷链。
9.6 阻塞力可以长期连续输出吗
阻塞力是特定静态定义,不等于长期允许负载或连续动态能力。长期工况还要考虑温升、应力、驱动占空比和寿命边界。
9.7 推力测试需要测实际驱动电压吗
需要。驱动器在容性负载下可能限流或出现电压动态偏差,只记录设定电压不能证明执行器端实际电压。
9.8 本文代码能判断预紧和载荷是否安全吗
不能。代码只拟合离线数据,不读取器件额定值、不检测偏心或裂纹,也不控制高压、加载机构和紧急停止。

10. 结论
压电执行器推力测试应把自由位移、工作推力、阻塞力、预紧力和最大允许载荷分开。先建立预紧基线,再在同一电压和温度下测量多个增量力位移点,才能形成可复算的推力结论。
刚度乘以自由位移只能作为线性近似。真实报告还必须说明夹具柔度、加载方向、拟合区间、残差和不确定度。没有制造商允许的机械与电气边界时,不应直接进行硬阻塞测试。
参考资料
- IEEE,ANSI/IEEE Std 176-1987,IEEE Standard on Piezoelectricity,1988。DOI:https://doi.org/10.1109/IEEESTD.1988.79638
- Kenji Uchino,Piezoelectric Actuators and Ultrasonic Motors,Kluwer Academic Publishers,1997。DOI:https://doi.org/10.1007/978-1-4615-6401-5
- Bernard Jaffe、William R. Cook Jr.、Hans Jaffe,Piezoelectric Ceramics,Academic Press,1971,ISBN 978-0-12-379550-2。
- JCGM 100:2008,Evaluation of measurement data - Guide to the expression of uncertainty in measurement,Joint Committee for Guides in Metrology。https://www.bipm.org/en/committees/jc/jcgm/publications
- Python Software Foundation,statistics - Mathematical statistics functions,Python 3.12文档。https://docs.python.org/3.12/library/statistics.html
说明:正式测试还必须引用具体执行器、驱动器、力传感器、位移传感器和夹具的当前版本规格书与校准文件。



277

被折叠的 条评论
为什么被折叠?



