# FRA 测试详解 —— 新人入门文档

> 本文档完全基于本仓库代码整理(`vcm_drv_h563`,STM32H563 平台),面向刚入职的应届生。
> 建议配合同目录下的交互动画一起学习:打开 `index.html`。

---

## 目录

1. [背景:我们在做什么产品,为什么需要 FRA](#1-背景)
2. [FRA 是什么:目的与基本原理](#2-fra-是什么)
3. [系统硬件架构:FPGA 在哪里,为什么用 FPGA](#3-系统硬件架构)
4. [三种测试模式:PLANT / OPEN / CLOSE](#4-三种测试模式)
5. [代码架构:分层与文件地图](#5-代码架构)
6. [完整执行流程(跟着代码走一遍)](#6-完整执行流程)
7. [FPGA 具体做了哪些事](#7-fpga-具体做了哪些事)
8. [测试结果与数据格式换算](#8-测试结果与数据格式换算)
9. [评定标准:PM、GM、-3dB 带宽](#9-评定标准)
10. [FRA 结果与 PID 的关系(重点)](#10-fra-结果与-pid-的关系)
11. [工程应用实例:环路增益自动校准](#11-工程应用实例)
12. [调试手段与常用命令](#12-调试手段与常用命令)
13. [附录:FPGA 寄存器表 / 采样点表 / 术语表](#13-附录)

---

## 1. 背景

本项目是 **VCM(Voice Coil Motor,音圈马达)驱动芯片** 的 MCU 测试/调试固件。VCM 用在手机摄像头模组里,分两类:

- **AF(Auto Focus,自动对焦)**:推动镜头沿光轴(Z 轴)移动;
- **OIS(Optical Image Stabilization,光学防抖)**:推动镜头在 X/Y 平面平移,抵消手抖。

驱动芯片(本仓库支持 PD9701A / PD9702A / PD9711A 等)内部是一个 **数字闭环伺服系统**:

```
目标位置(target code) → [误差] → PID 控制器 → 驱动电流 → 线圈+磁路(马达) → 镜头位置
        ↑                                                          |
        └────────────── Hall 传感器位置反馈(code_fb) ←──────────────┘
```

闭环系统有一个根本问题:**PID 参数配得好不好、系统稳不稳定、响应快不快,肉眼看不出来**。马达本身是一个"弹簧-质量-阻尼"二阶谐振系统(谐振频率通常几十~一百多 Hz,阻尼很小),PID 稍有不当就会振荡、啸叫、过冲。所以每一颗马达 + 驱动芯片组合,都需要一种客观的测量手段来回答:

- 这个马达本体(被控对象)的谐振频率、增益是多少?
- 加上 PID 后,整个环路稳定吗?稳定裕度够不够?
- 闭环带宽多少?对焦/防抖响应够不够快?

这个测量手段就是 **FRA**。

## 2. FRA 是什么

FRA = **Frequency Response Analysis,频率响应分析**。也就是自动化控制里教科书上的"扫频测 Bode 图"。

### 2.1 基本原理

对一个(近似)线性系统,输入一个正弦波,稳态输出仍是 **同频率** 的正弦波,只是:

- **幅值变了** → 幅值比就是该频率下的 **增益(Gain)**,工程上用 dB 表示:`Gain(dB) = 20·log10(A_out / A_in)`;
- **相位移了** → 相位差就是该频率下的 **相位(Phase)**,单位度。

把频率从低到高扫一遍(本项目默认 10 Hz ~ 10 kHz,对数分布 150 个点),每个频率都测一组 (Gain, Phase),画出来就是 **Bode 图**。Bode 图完整刻画了系统的动态特性。

### 2.2 单频点如何算出 Gain 和 Phase(相关法 / 单点 DFT)

本仓库 MCU 本地版算法 [fra_calc.c](../Core/Application/fra/mcu/libfra/fra_calc.c) `fra_get_sng_freq_result()` 展示了标准做法,FPGA 内部做的是同样的数学:

对输入序列 `x[i]` 和输出序列 `y[i]`,用同频的 sin/cos 做相关积分(即该频率上的 DFT):

```c
i_sin_sum += sin(2π·f·i/fs) * fra_in[i];   // 输入的 I 分量
i_cos_sum += cos(2π·f·i/fs) * fra_in[i];   // 输入的 Q 分量
o_sin_sum += sin(2π·f·i/fs) * fra_out[i];  // 输出的 I 分量
o_cos_sum += cos(2π·f·i/fs) * fra_out[i];  // 输出的 Q 分量

phase_in  = atan2(i_cos_sum, i_sin_sum);
phase_out = atan2(o_cos_sum, o_sin_sum);
phase     = phase_out - phase_in;                    // 相位差
gain      = sqrt(o_sin²+o_cos²) / sqrt(i_sin²+i_cos²); // 幅值比
gain_db   = 20 * log10(gain);
```

这种"相关法"的好处:**只取激励频率那一根谱线**,噪声和其它频率成分被积分平均掉,抗干扰能力远强于直接量峰峰值。

> `fra_calc.c` 里还有 `phase_compen_deg(fs, comp_us) = fs·comp_us·1e-6·360`:采样/通信链路有固定时间延迟时,相位要按 `延迟×频率×360°` 补偿——频率越高,同样的时延造成的相位滞后越大。这个概念在 FPGA 方案里对应相位补偿寄存器(0x70)。

## 3. 系统硬件架构

### 3.1 为什么要 FPGA

FRA 要求"**写激励、读响应**"严格同步且节拍稳定。激励是通过 I2C 把正弦采样点逐个写进驱动 IC 寄存器、再逐点读回反馈寄存器实现的。10 kHz 的正弦,每周期还要采几十个点,I2C 得跑到 2~3.3 MHz 并且时序不能抖动——MCU 软件做这件事既慢又抖(中断、调度都会引入相位噪声),所以把"正弦生成 + 高速 I2C 收发 + 单频 DFT 计算"整体下放到 FPGA 硬件流水线,MCU 只负责配置与取结果。

### 3.2 连接拓扑(从代码还原)

```
┌─────────┐  UART/PB协议   ┌──────────────┐   I2C(MCU为主机)    ┌─────────────┐
│ 上位机   │ ⇄ ──────────  │ MCU STM32H563 │ ⇄ ───────────────  │    FPGA      │
│ (WPF GUI)│               │  本仓库固件    │   FPGA从地址 0x50   │  (信号发生+  │
└─────────┘               └──────────────┘                     │   DFT测量)   │
                                  ↑  UART: FPGA主动上报结果/波形  └──────┬──────┘
                                  └──────────────────────────────────┘ │ I2C(FPGA为主机, 2MHz)
                                                                        ▼
                                                              ┌─────────────────┐
                                                              │ VCM驱动IC        │
                                                              │ (PD9701A/9702A/  │
                                                              │  9711A, 如0x1C)  │
                                                              └────────┬────────┘
                                                                       │ 驱动电流/Hall反馈
                                                                       ▼
                                                                  VCM 马达+镜头
```

关键点:**FPGA 同时是 MCU 的 I2C 从机(0x50),又是驱动 IC 的 I2C 主机**。测试期间 MCU 把驱动 IC 的总线控制权"让"给 FPGA。

代码证据:
- `fra_fpga_driver.h:18` `#define FPGA_SLV_ADDR 0x50`;
- `fpga_i2c_dev_slv_addr_cfg(slv_addr)`(0x01 寄存器)告诉 FPGA 该去操作哪个驱动 IC 地址;
- `fpga_i2c_freq_set(FPGA_I2C_FREQ_2M)`(0x11 寄存器)设置 FPGA→驱动 IC 的 I2C 速率(2 MHz);
- 结果回传有两条路:UART(默认,`fra_fpga_recv_fra_data_cplt()` 回调)或 I2C 轮询(编译宏 `GET_FRA_DONE_STATE_BY_I2C` / `GET_FRA_DATA_BY_I2C`)。

### 3.3 对照:MCU 本地方案

`Core/Application/fra/mcu/` 下还有一套不依赖 FPGA 的本地 FRA(定时器节拍 + I2C 收发 + `fra_calc.c` 软件 DFT),以及 `fra5022`/`fra_dac`/`ad7606b` 等模拟链路方案。本文聚焦 FPGA 方案,但 `fra_calc.c` 是理解 FPGA 内部算法的最好教材。

## 4. 三种测试模式

`fra_fpga_vcm.h` 定义:

```c
typedef enum { PLANT_MODE = 0, OPEN_MODE, CLOSE_MODE, ERR_MODE } E_IC_TYPE;
```

三种模式的区别是 **正弦激励注入环路的位置** 和 **测量的传递函数** 不同(`vcm_dev_setup()` 按模式预置驱动 IC):

| 模式 | 注入点 | 测什么 | 驱动 IC 预置(代码) | 用途 |
|---|---|---|---|---|
| **PLANT**(被控对象) | DAC code(直接给功率级,绕过 PID) | 马达+驱动级本体 P(s) | `dac_mode_enable` + `plant_fra_set_code(0)` | 看谐振频率 f0、阻尼、DC 增益,建模 |
| **OPEN**(开环) | 环路输入点(loop-in,PID 之前) | 开环传函 L(s)=C(s)·P(s) | `dac_mode_disable` + `set_target(0)` + active | **PM/GM、环路增益校准**(最重要) |
| **CLOSE**(闭环) | 目标位置 target code | 闭环传函 T(s)=L/(1+L) | 同 OPEN | **-3dB 带宽**、闭环谐振峰 |

理解三者关系:PLANT 告诉你"马达长什么样";OPEN 告诉你"PID×马达合起来稳不稳";CLOSE 告诉你"用户视角的响应性能"。三者数学上互相可推:`L = C·P`,`T = L/(1+L)`。

另外每种模式下 FPGA 用的驱动 IC 寄存器不同(`fra_fpga_driver.h`):写入 `FPGA_PLANT_WR_ADDR 0x16`(DAC code)或 `FPGA_CLOSE_WR_ADDR 0x12`(target);读回默认 `0x4C`(code_fb),fast 模式改读 `0x3B`(LPF 后 code_fb,见 `fra_test.c` 的 `OIS_CODE_FB_LPF`),闭环还有 `FPGA_CLOSE_RD_ADDR 0x74`。

> 细节:测试前 `vcm_disable_lpf()` 会把 PD9711A 的 codeFB/Hall 两级 LPF 关掉(测完恢复)——因为 FRA 要测系统"本征"频响,反馈路径上的滤波器会额外引入幅值衰减和相位滞后,污染测量结果。

## 5. 代码架构

自顶向下五层(都在 `Core/Application/` 下):

```
① 入口层(两个入口)
   ├─ nanopb/vcm_drv_pb_codec_fra_report.c   ← 上位机 GUI 走 PB 协议
   └─ usmart/usmart_config.c                 ← 串口命令行(fra_scan_test 等)
② 测试编排层
   └─ user/vcm_test/fra_test/fra_test.c      ← fra_test():参数校验、5种扫描模式、
                                                回调分发、PM/GM/-3dB 计算 ★
   └─ user/fra/fra_app.c                     ← 简化命令封装(run_fra_scan_exe 等)
③ FPGA 方案 API 层
   └─ fra/fpga/fra_fpga_main.c               ← g_fra_fpga_drv_api,fra_func() 取接口
       (fra_fpga_api.h 定义 fra_test_prms_t / fra_test_api_t)
④ 扫频控制层
   └─ fra/fpga/fra_fpga_ctrl.c               ← 频点表 logspace、fpga_setup、
                                                扫频主循环(重试/回中)★
   ├─ fra/fpga/driver/fra_fpga_vcm.c         ← 驱动 IC 预置/恢复(关LPF、DAC/target模式)
   └─ fra/fpga/driver/fra_fpga_driver.c      ← FPGA 寄存器级 I2C 读写 ★
⑤ 数据层
   ├─ fra/fpga/data/fra_fpga_data_handle.c   ← 等完成标志、定点→dB/deg 换算、
                                                采样点表、结果缓存 ★
   └─ fra/fpga/alg/fra_fpga_alg.c            ← u14.2 / Q28 定点格式转换
```

接口用函数指针表解耦(嵌入式常见手法):`fra_func()` 返回 `fra_test_api_t*`,上层只调 `fra_func()->fra_scan(...)`,不关心底下是 FPGA 还是别的方案。

## 6. 完整执行流程

以最常用的 **扫频测试** 为例(usmart 命令 `fra_scan_test(axis, mode, type, pid, amp, offset, start_freq, end_freq, cnts, dummy_cycles)`):

### 第 1 步:参数校验与时间预估 — `fra_test()` → `fra_check()`

- 校验轴号、扫描点数(≤ `FRA_MAX_DATA` 500)、模式、类型;
- `estimate_scan_time()`:按每频点"周期数/频率 + 回中等待 10ms"累加,预估总时长回调给 GUI 进度条。

### 第 2 步:模式配置 — `fra_exe()`

- 按扫描模式调 `fpga_fra_read_config()`:告诉 FPGA 从哪个从机/寄存器读反馈(默认 code_fb LPF 0x3B)、FFT 模式(normal/fast/opt)、稳态等待周期 `dummy_cycles`;
- normal 模式还开 `fpga_fra_fft_range_adjust(1, 30, 600, 2)`:**30~600 Hz(谐振区)额外做 2 次平均**,提高谐振峰附近的信噪比。

### 第 3 步:准备 — `fra_fpga_scan_test()`(fra_fpga_main.c)

- 注册逐点回调 `fra_data_callbackfunc`(每测完一个频点实时推给 GUI);
- `fra_fpga_set_sine_state(os, amp, phase)` 存正弦参数;
- `fra_fpga_init()` → `puya_logspace(min, max, n, 10, freq_list)` 生成**对数均分频点表**(Bode 图横轴是对数的,对数分布让低频高频信息量均衡)。

### 第 4 步:一次性硬件设置 — `fra_fpga_setup()`(fra_fpga_ctrl.c)

```
fra_data_init()          清结果缓存、相位解缠状态
vcm_dev_setup()          驱动IC:关LPF → 按模式进 DAC/target 模式并回中
fpga_setup()             FPGA:软复位 0x00 → 配目标IC地址 0x01 → 反馈读地址 0x0B/0x0C
                         → FFT模式 0x21/0x22 → 正弦 os/amp/phase 0x04~0x09(按IC除2适配位宽)
                         → I2C速率2M 0x11 → FFT等待 0x17/18 → IC类型 0x03
                         → pid_sel 掩码 0x0A → DAC取反 0x20(PD9701A) → FRA模式 0x02[6:5]
sys_delay_ms(10)
```

> `pid_sel`(0=home PID,1=centering PID)通过 0x0A 写给 FPGA:同一颗 IC 内有多组 PID 参数(对应不同工作场景),FRA 可以分别测哪组。

### 第 5 步:扫频主循环 — `fra_fpga_scan()`

对频点表每个频率:

1. `adjust_frequency()`:把频率对齐到 **0.25 Hz 栅格**(FPGA 频率寄存器分辨率 = 1/4 Hz,`freq_reg = freq × 4`,u14.2 定点);
2. 范围检查(超出 `FRA_FPGA_FREQ_MIN/MAX` 直接报错退出);
3. 最多重试 `FPGA_FRA_RETRY_CNT = 3` 次:
   - `vcm_dev_back_to_center()` 马达回中 + 等 10 ms(每个频点从同一静态工作点开始,避免上个频点残余运动污染本次测量);
   - `fra_fpga_start()`:写频率寄存器 0x0E/0x0F,写 START 位 0x02[0](debug 模式加 bit7);**此后测量完全由 FPGA 自主完成**;
   - `fra_result_data_handle()`:MCU 以 1 ms 节拍轮询"完成"信号(UART 回调标志,或 I2C 读 0x45 == 0x03),超时 `FRA_FPGA_TIMEOUT_LIMIT` 返回超时错误;
   - 收到后解析数据 → 换算(见第 8 节)→ 存入 `fpga_fra_result[]` 并回调上层。
4. 全部频点完成后 `vcm_dev_recover_init_state()`:退 DAC 模式、回 standby、恢复 LPF。

### 第 6 步:结果计算与上报 — `update_fra_result()` → `cal_fra_result()`

扫完整条曲线后在 MCU 上计算 GM、PM、0dB 穿越频率、-180° 翻转频率、-3dB 频率(算法见第 9 节),连同曲线一起经 PB 协议回传上位机(`fra_res_callbackfunc_pb()`,注意做了大小端转换 `htonf_custom`)。

### 5 种扫描模式(fra_test.h `fra_mode`)

| 模式 | 说明 |
|---|---|
| `FRA_SCAN_FREQ_MODE` | 标准对数扫频(默认) |
| `FRA_SCAN_STEP_MODE` | 线性步进扫频(start→end 固定步长,逐点调 `fra_single`;GM 计算用不同的相位解缠逻辑) |
| `FRA_FAST_MODE` | 快速模式:实际只测一半频点(FPGA 走 fast 路径、读 0x3B),MCU 端相邻两点**插值**补出中间点(`fra_data_callbackfunc` 的 fast 分支) |
| `FRA_OPT_MODE` | 优化模式:FPGA 每点平均 4 次(0x22=4),更准但更慢(时间×2.8 估算) |
| `FRA_SINGLE_FREQ_MODE` | 单频点测试(环路增益校准、-3dB 校准都用它) |

## 7. FPGA 具体做了哪些事

FPGA 对 MCU 而言是一个"寄存器盒子"(完整寄存器表见附录)。从代码(寄存器语义 + 采样点表 + debug 数据格式)可以完整还原 FPGA 收到 START 后在硬件里做的事:

1. **DDS 正弦生成**:按 `f = freq_reg/4` Hz 生成数字正弦 `u[n] = os + amp·sin(2πf·t[n] + φ)`。os/amp/phase 来自 0x04~0x09(MCU 写入时已 ×16,FPGA 内部为 12bit 有符号量程 ±2048);
2. **I2C 主机写激励**:以配置速率(2 MHz)把每个采样点写入驱动 IC 的注入寄存器——PLANT 模式写 DAC code(0x16),OPEN 模式写 loop-in,CLOSE 模式写 target(0x12)。**采样节拍就是 I2C 写-读事务的节拍**,所以 I2C 速率决定了每周期能采多少点(见附录采样点表);
3. **I2C 主机读响应**:同一节拍从 0x0B/0x0C 指定的从机/寄存器(默认 code_fb)读回系统响应 `y[n]`;
4. **稳态等待**:先跑 `dummy_cycles`(0x17/0x18,FFT wait)个周期不计入统计——系统从静止到进入正弦稳态需要时间,暂态数据会带来测量误差;
5. **采集与单频 DFT**:按当前频段采 `sample_cycle` 个整周期 × `2^sample_points_log` 个点(整周期采样避免频谱泄漏),对 `u[n]`、`y[n]` 做 I/Q 相关累加(与第 2.2 节公式相同),得幅值比与相位差;若 0x22 平均次数 >0,重复测量取平均;0x41 可读出实际 FFT 周期计数;
6. **相位补偿**:0x70 提供相位补偿值,修正 I2C 写-读之间固有时延造成的相位偏差;
7. **输出结果**:
   - Gain → 0x48(4 字节,**s16.16 定点**,线性幅值比);
   - Phase → 0x4C(4 字节,**Q28 定点,弧度**);
   - 完成标志 0x45 = 0x03;
   - 同时(默认路径)把 `freq(2B)+gain(4B)+phase(4B)` 经 **UART 主动推给 MCU**;
   - **debug 模式**(START bit7):把原始时域序列 `sin_in/sin_out`(各 int16,×16 缩放)整段上传,MCU 端按 `get_fra_sample_point_delay()` 算出的采样间隔打印 `ts, sin_in, sin_out` 三列(`fra_app.c` `run_fra_single_point_exe`),可直接画时域波形验证。

一句话总结:**FPGA = DDS 信号源 + 高速 I2C 收发机 + 单频锁相测量仪(lock-in amplifier)三合一**,MCU 只做"配置-启动-等待-换算"。

## 8. 测试结果与数据格式换算

每个频点产出三元组 `(freq, gain_db, phase_deg)`,存 `fpga_fra_result[]`(double,3 个一组)。换算在 `fra_result_data_show()`:

| 量 | 原始格式 | 换算 | 说明 |
|---|---|---|---|
| freq | u14.2(Hz×4) | `unsigned_16_to_double_14_2()` | 分辨率 0.25 Hz |
| gain | s16.16 定点(线性比值) | 高16位整数+低16位小数 → `MAG2DB()` 即 20·log10 | NaN→-96 dB,下限截到 **-66 dB**(噪底) |
| phase | Q28 定点(弧度) | `signed_32_to_double(phase, 28)` → `radian_to_degree(rad, fra_last_deg)` | 带 **相位解缠**:参考上一频点相位展开,避免 ±180° 跳变(所以扫频曲线的相位可以连续到 -300° 以下) |

## 9. 评定标准

FRA 曲线本身只是数据,工程上用四个派生指标评定(`cal_fra_result()` + `cal_neg3db_freq_phase_margin()`,结构体 `fra_res_t`):

### 9.1 相位裕度 PM(Phase Margin)—— 开环曲线

找 **增益 = 0 dB 穿越点**(增益符号翻转的相邻两点,线性插值),该点相位 + 180° 就是 PM:

```c
res->phase_margin = p1 + (-g1)/(g2-g1)*(p2-p1) + 180.0f;   // 归一化到 ±180°
res->gain_zero_freq = f1 * powf(f2/f1, k);                  // 0dB 穿越频率(对数插值)
```

物理含义:在增益穿越频率上,相位离 -180°(正反馈临界点)还差多少度。**PM 越小系统越接近振荡**;PM 代表了对"额外相位滞后"(温度、装配、老化引起)的容忍量。代码里若有多个 0dB 穿越,取频率最高的那个(`fra_data[i].freq > pm_freq` 判断)。

### 9.2 增益裕度 GM(Gain Margin)—— 开环曲线

找 **相位 = -180° 穿越点**(限 1 kHz 以内,step 模式有专门的跨 ±360° 解缠处理),该点增益取负:

```c
res->gain_margin = -(gain@phase=-180°);      // 正值,单位 dB
res->phase_flip_freq = f1 * powf(f2/f1, k);  // -180° 翻转频率
```

物理含义:相位已经滞后到 -180° 的频率上,增益离 0 dB 还有多少 dB 余量。**如果环路增益整体涨了 GM 这么多 dB(马达灵敏度漂移、增益校准误差),系统就会振荡。**

### 9.3 -3dB 带宽 —— 闭环曲线

`cal_neg3db_freq_phase_margin()`:在闭环曲线上找增益穿越 **-3 dB** 的点(线性插值),得:

- `freq_neg3db`:闭环带宽,直接决定对焦/防抖的响应速度;
- `phase_margin_neg3db = 180 - |phase@-3dB|`:-3dB 点处的相位裕度参考值。

(近期提交 `ee198467 "sync -3db cal from keystone"` 正是围绕这个指标的校准功能。)

### 9.4 判定门限在哪里?

**代码里不硬编码 PASS/FAIL 门限**——固件把 `gain_margin / phase_margin / gain_zero_freq / phase_flip_freq / freq_neg3db / phase_margin_neg3db / scan_time` 全量上报(PB `report_fra`),由上位机/产线按产品规格判定。行业常用经验值(供理解量级,非本仓库规定):

- PM ≥ 35°~45°(伺服系统教科书推荐 45°~60°;太大则响应偏慢);
- GM ≥ 6~10 dB;
- 带宽按产品需求(AF 通常几十~上百 Hz 量级)。

另外隐含的"合格线":扫频过程中单点失败重试 3 次仍失败、频率越界、超时,整个测试返回错误;环路增益校准有明确容差(见第 11 节)。

## 10. FRA 结果与 PID 的关系

这是理解 FRA 价值的核心。记开环传函 `L(s) = C(s)·P(s)`,其中 `P(s)` 是马达被控对象(PLANT 模式测的),`C(s)` 是驱动 IC 里的数字 PID:

```
C(s) = Kp + Ki/s + Kd·s      (概念形式;IC 内为离散实现,另有 loop gain 整体系数)
```

### 10.1 P(比例)——上下平移增益曲线

P 增大 → `|L|` 整条曲线**等量上移**(相位曲线不变):

- 0 dB 穿越频率(`gain_zero_freq`)右移 → 闭环带宽↑、响应更快;
- 但穿越点落到相位更滞后的区域 → **PM↓**;
- 相位 -180° 点的增益也抬高 → **GM↓**。

**P 是"快"与"稳"的直接交换比。** 驱动 IC 里的 **loop gain 寄存器**(如 PD9711A AF 的 0x698/0x699)在数学上等效于对整个 C(s) 乘系数,效果与 P 相同——这就是为什么环路增益校准(第 11 节)只需在 10 Hz 测一个点:整条曲线是刚性平移的,一个点就能标定整体偏差。

### 10.2 I(积分)——抬低频,吃相位

- 低频增益按 -20 dB/dec 抬升 → 稳态误差↓、抗低频扰动(重力、姿态)↑;
- 代价是积分固有 **-90° 相位滞后**,若积分转折频率(Ki/Kp)离穿越频率太近,穿越点相位被拖低 → **PM 被显著侵蚀**;
- FRA 开环曲线上的表现:低频段斜率变陡、低频相位掉向 -90°。

### 10.3 D(微分)——相位超前,救 PM

- 在穿越频率附近提供最高接近 +90° 的**相位超前** → **PM↑**,允许更高带宽;
- 高频增益按 +20 dB/dec 抬升 → 放大高频噪声、并把谐振峰抬高可能损害 **GM**;
- 所以实际 IC 中 D 项总是配低通滤波器;而 FRA 测试时把反馈 LPF 关掉,就是为了不让这些滤波器的附加相位混进被测特性。

### 10.4 汇总表

| 增大 | 低频增益 | 穿越频率/带宽 | PM | GM | 典型副作用 |
|------|---------|--------------|----|----|-----------|
| Kp / loop gain | ↑ | ↑ | ↓ | ↓ | 过冲、振铃 |
| Ki | ↑↑ | ≈ | ↓ | ≈ | 低频振荡、积分饱和 |
| Kd | ≈ | 略↑ | **↑** | 视谐振峰而定 | 高频噪声放大 |

### 10.5 闭环视角

- 闭环 `T = L/(1+L)`:PM 不足 ⇔ 闭环在穿越频率附近出现**谐振峰**(时域=过冲/振铃);
- -3dB 带宽近似等于开环 0 dB 穿越频率;
- 所以"开环 PM/GM"与"闭环谐振峰/带宽"是同一件事的两个投影,动画 4 里可以拖滑块直观体会。

## 11. 工程应用实例:环路增益自动校准

[user_af_pid.c](../Core/Application/user/af_app/tuning/user_af_pid.c) `user_af_loop_gain_calibration()` 是 FRA 在产线的典型落地——**用 OPEN 模式单频点 FRA 把每颗马达的环路增益标到一致**:

```
1. loop_gain_home_full_run_op():预运动(ball 型马达先满行程往复 5 次消除静摩擦滞回)后回中
2. 关 loop gain 开关,OPEN 模式在指定频率(典型 10 Hz)测单点增益
      get_loop_gain_err() → fra_func()->fra_single(...) → gain_err = target - measured
3. 检查 gain_err ∈ [LOOP_GAIN_MIN_DB, 12dB],超限直接 FAIL(马达/装配异常)
4. 写补偿:af_set_loop_gain(raw_gain_err + 0.15) → 复测
5. 不达标再迭代,最多 3 次;每次微调补偿偏置(+0.25 / +0.05)
6. 合格判据:复测误差落在 (−0.3, 0] dB 区间(GAIN_ADJUST_PRECISION = −0.3)
```

为什么选 10 Hz:远低于谐振频率,增益平坦、相位接近 0,测量最稳,且如 10.1 所说增益偏差是全频段平移的,低频一点即可代表整体。

同类应用还有:coil flux 校准的 `*_coil_flux_fc_scan`(扫 fc 找最优)、`pd9711a_home_full_calib`、相位延迟测试等,都复用同一套 FRA 基础设施。

## 12. 调试手段与常用命令

串口 usmart 命令(`usmart_config.c`):

```text
fra_scan_test(run_axis, mode, type, pid, amp, offset, start_freq, end_freq, cnts_or_step, dummy_cycles)
    run_axis: 0=X 1=Y 2=AX3 3=AF
    mode:     0=对数扫频 1=步进 2=fast 3=优化 (4=单频不支持此命令)
    type:     0=plant 1=open 2=close
    pid:      0=home pid 1=centering pid
    例: fra_scan_test(3,0,1,0,200,0,10,10000,150,2)   → AF 开环 10Hz~10kHz 150点

fra_single_freq_test(run_axis, type, pid, amp, offset, single_freq, dummy_cycles)
fpga_select_exe()                          → 探测 FPGA(读 0x3A==0xFA)并打印固件版本
fpga_fra_config_exe(amp, os, min_freq, max_freq, scan_freq_num)
run_fra_scan_exe(slv_addr, fra_type, pid_sel)
run_fra_single_point_exe(slv_addr, fra_type, pid_sel, debug)   → debug=1 输出时域原始波形
```

排查思路:

- **测试超时**(`fra_result_data_handle timeout`):查 FPGA 供电/连接(先 `fpga_select_exe()`)、UART 回传线、驱动 IC 地址是否正确;
- **某频点反复失败重试**:马达是否装夹松动、amp 是否过大导致撞行程末端;
- **曲线噪声大**:换 `FRA_OPT_MODE`(4 次平均)或调 `fpga_fra_fft_range_adjust` 区间平均;增大 amp(信噪比↑)但注意线性范围;
- **低频相位怪异**:检查 dummy_cycles 是否太小(稳态未建立);
- **debug 单点模式**:直接看 sin_in/sin_out 时域波形,是最底层的真相。

## 13. 附录

### 13.1 FPGA 寄存器表(fra_fpga_driver.h,从机地址 0x50)

| 寄存器 | 名称 | 说明 |
|---|---|---|
| 0x00 | RST | 写 0x01 软复位 |
| 0x01 | I2C_ADDR | 被测驱动 IC 的 I2C 地址 |
| 0x02 | START | bit0=启动;bit[6:5]=模式(PLANT/OPEN/CLOSE<<5);bit7=debug(上传时域原始数据) |
| 0x03 | IC_TYPE | 被测 IC 型号(PD9701A=0/9702A=1/9711A=2) |
| 0x04/05 | OS_H/L | 正弦直流偏置(写入值=物理值×16,12bit 有符号) |
| 0x06/07 | AMP_H/L | 正弦幅值(×16;PD9702A/9711A 因 12bit 位宽 MCU 侧先除 2) |
| 0x08/09 | PHASE_H/L | 初始相位(度×16,±180) |
| 0x0A | PD9702A_TARGET_MASK | pid_sel(home/centering PID 选择掩码) |
| 0x0B/0x0C | READ_SLV/REG_ADDR | 反馈读取的从机地址/寄存器地址(默认 0x1A/0x4C;fast 模式用 0x3B) |
| 0x0E/0F | FREQ_PH/PL | 激励频率,u14.2(Hz×4,分辨率 0.25Hz);测完也可读出 |
| 0x11 | I2C_FREQ | FPGA→驱动 IC 的 SCL 速率(0x0C=2M,0x07=3.3M…) |
| 0x17/18 | WAIT_H/L | FFT 前稳态等待(dummy cycles) |
| 0x20 | DAC_INV | DAC 输出取反(PD9701A PLANT 模式按 IC 内 0x8C 配置联动) |
| 0x21 | FAST_MODE | 快速 FFT 模式开关 |
| 0x22 | AVE_MODE | 每频点平均次数(OPT 模式=4) |
| 0x30 | FIRMWARE_VERSION | FPGA 固件版本 3 字节 |
| 0x3A | VERSION | 器件识别码,读到 0xFA 即认定 FPGA 在位 |
| 0x41 | FRA_PERIOD | 实际 FFT 周期计数(4 字节) |
| 0x45 | FRA_COMP | 完成标志,==0x03 表示本频点测量完成 |
| 0x48 | FRA_GAIN | 增益结果,4 字节 s16.16 定点(线性比值) |
| 0x4C | FRA_PHASE | 相位结果,4 字节 Q28 定点(弧度) |
| 0x70 | PHASE_COMP | 相位补偿值(2 字节) |

### 13.2 采样点表(fra_fpga_data_handle.c,I2C=2M 档)

| 频段(Hz) | 采样周期数 | 每周期点数(2^n) |
|---|---|---|
| 0~9 | 4 | 1024 |
| 9~30 | 4 | 512 |
| 30~300 | 4 | 256 |
| 300~600 | 8 | 128 |
| 600~1200 | 8 | 64 |
| 1200~2400 | 8 | 64 |
| 2400~5000 | 16 | 64 |
| 5000~10000 | 32 | 64 |

规律:频率越高,单周期时间越短,I2C 事务塞不下太多点 → 点数减少;为保总样本量与精度 → 周期数增加。低频点多但周期数少,兼顾总时长。MCU 端 `get_fra_sample_point_delay()` 用同一张表反推采样间隔,用于 debug 时域数据的时间轴。

### 13.3 术语表

| 术语 | 含义 |
|---|---|
| VCM | 音圈马达,摄像头对焦/防抖执行器 |
| Plant / 被控对象 | 控制回路中"被控制的物理系统"(马达+驱动级) |
| Bode 图 | 增益(dB)与相位(°)对频率(对数轴)的曲线 |
| DFT / 相关法 | 用同频 sin/cos 相关积分提取单一频率分量的幅相 |
| DDS | 直接数字频率合成,FPGA 生成正弦的方式 |
| PM / GM | 相位裕度 / 增益裕度,稳定性余量 |
| 0dB 穿越频率 | 开环增益=1 的频率,≈闭环带宽 |
| -3dB 带宽 | 闭环增益跌到 DC 值 -3dB 的频率 |
| 相位解缠(unwrap) | 消除 ±180° 相位跳变使曲线连续 |
| loop gain | 环路整体增益系数,IC 寄存器,等效缩放 PID 输出 |
| code / code_fb | 位置目标码 / Hall 反馈位置码 |
| dummy cycles | 计算前丢弃的稳态建立周期 |

---

*文档基于代码版本:分支 `pr_request_563_jingyu33_br`(2026-07)。如代码更新,请以源码为准。*
