项目

一般

简介

行为

任务支持 #2544

已关闭

调研并实验大号无刷电机的FOC控制

由 曹晏彬 在 大约 2 个月 之前添加. 更新于 大约一个月 之前.

状态:
已完成
优先级:
C级
指派给:
开始日期:
2026-09-14
计划完成日期:
% 完成:

100%

预期时间:


文件

AS5048A坏帧治理_v20_整合文档.md (21.5 KB) AS5048A坏帧治理_v20_整合文档.md 孙乃宇, 2026-09-23 09:46
main.cpp (26.4 KB) main.cpp 孙乃宇, 2026-09-23 09:47

由 曹晏彬 更新于 大约 2 个月 之前

  • 项目 从 29 变更为 软件开发组

由 孙乃宇 更新于 大约一个月 之前

  • 状态 从 新建 变更为 进行中

由 孙乃宇 更新于 大约一个月 之前

AS5048A 编码器坏帧治理 —— 位置保持抖动修复整合文档

版本:v20 | 日期:2026-09-22(次日整理) | 硬件:MKS ESP32 FOC V2.0 + GM5208-12(12N/14P)+ AS5048A(SPI 14bit)
固件:桌面\work\配置文件\9-22\SimpleFOC_ESP32FOC\src\main.cpp(525 行 / 27,015 B,单 UTF-8 BOM,LF)
验证报告(图形版):D:\workspace\Project1\位置保持抖动_修复验证报告.html


0. TL;DR

项 结论
用户现象 「给的位置是 200.123,运动之后还是会有细微的抖动,频率不高」
根因 不是控制环问题。AS5048A 的坏帧能通过偶校验被当成真实位置;再叠加传感器层的「伪展开」,会把连续域的圈数整圈拽走
反例教训 传感器层做「冻结式守卫」(超阈值沿用上一输出)是严重回归:位置反馈停住而环继续加力 ⇒ 正反馈暴走(100 s 内 7 次 ±36~54°、Vq 打到 ±2V 限幅)
修法 v20 四道防线:真·模 2π 归约 → 帧级步长合理性(越界重读,不冻结)→ 死总线帧点名剔除 → 5 点中值
结果 σ_rob 0.0255°(1.16 LSB = 编码器分辨极限);原始峰峰 2.18° → 0.20°(11×)、|vel|max 93 → 1.0 °/s(93×)、Vqpp 0.773 → 0.042 V(18×)、暴走 0 次
瞬态 100° 阶跃超调 +3.93° / +4.59°、残差 ~0.00°、翻转 1(不降反升,v16 为 +4.52°/2 翻转)
改动范围 只动固件。配置文件与 DLL 未改;位置环参数(P_angle=20/20、限速 1.5、PID_vel=0.5/1.5、LPF_velocity.Tf=0.10)一个都没动

1. 问题与背景

调试目标是一个云台电机(GM5208-12)的单轴位置保持。移动到大角度目标(200.123 rad ≈ 31.9 圈)并停下后,
轴在视觉上静止,但编码器读数上仍有小幅、低频的扰动,且偶尔「抽一下」。

这不是普通的噪声,而是坏读数进入位置环:位置环把假位置当成真误差 ⇒ 立刻输出一记力矩脉冲 ⇒ 轴被踢一小步。
静止时看到的就是「细微抖动、频率不高」。

修复过程中还暴露出两个更隐蔽的问题(都在传感器层),见 §4。


2. 硬件与软件基线

项 值
控制板 MKS ESP32 FOC V2.0(开发板,非成品驱动器)
电机 GM5208-12,12N/14P ⇒ 极对数 7,相阻 ≈15.2 Ω
限压策略 限「电压」,工作 2.0 V,硬件上限 4.0 V
编码器 AS5048A(SPI,14 bit,无 I²C),引脚固定 CLK18 / MISO19 / MOSI23 / CS5
帧结构 bit15 = 偶校验、bit14 = 错误标志 EF、bit13..0 = 14bit 角度
库 arduino-cli + esp32 core 2.0.17 + SimpleFOC 2.2.1
主循环 实测 ~6.4~6.7 kHz(随固件版本略有差异)
控制结构 级联:target → P_angle → 速度指令 → PID_velocity → Vq(MotionControlType::angle)
单位 位置目标 弧度(100° = 1.745)
串口 COM7 @ 115200

2.1 关键背景:SimpleFOC 丢掉了两个有效性位

// SimpleFOC MagneticSensorSPI::read() 的最后两句
register_value = register_value >> (1 + data_start_bit - bit_resolution);
return register_value & 0x3FFF;     // ★ 校验位和 EF 全被丢掉

⇒ 每一帧坏数据都被当成「真实位置」喂进位置环。这是整个问题的入口,
所以必须在传感器层自己读帧、自己校验、自己滤。


3. 排查路径(版本演进)

版本 传感器侧做法 定位
v13 无任何滤波(基线) 复现抖动:σ_rob 4.271°
v14 / v15 中间探索版 —
v16 自写 AS5048AGuarded:偶校验重读 + 3 点中值(连续域) + X 自检命令 σ 掉到 1.16 LSB;但仍有单点毛刺
v17 5 点中值 + LPF_velocity.Tf 0.03 → 0.10 吃掉 2 连坏帧;压掉速度估计的量化尖峰
v18 中值后加 「物理步长守卫」(超 0.5° 就沿用上一输出) ❌ 严重回归,见 §4.3
v19 守卫改为「只记录不拦截」+ 剔除死总线帧 0x0000/0xFFFF 暴走 7 → 1 次;并靠现场记录器抓到真正根因
v20 真·模 2π 归约 + 帧级步长合理性(越界重读) ✅ 暴走 0 次,毛刺进入噪声底

4. 根因分析

4.1 坑 A:坏帧能通过偶校验

坏帧形态 为什么拦不住 解码结果
0x0000(MISO 恒低 / 器件无应答) 1 的个数 = 0(偶数) ⇒ 偶校验通过 0°
0xFFFF(MISO 恒高) 1 的个数 = 16(偶数) ⇒ 偶校验通过 359.98°
0x4001 / 0x4004(EF=1 + 角度≈0) 与好帧结构完全一样、偶校验也成立 0.02° / 0.09°

后一类最恶劣:任何位级校验与模式匹配都抓不到它,只能靠「这一帧在物理上可能出现吗」来判。

实测(本机 100% 采样)

  • bit14 (EF) 恒为 1(100% 的帧)⇒ EF 不能当判据;
  • bit15 确是偶校验位,但几乎从不失败(1.29 M 帧只命中 2 次);
  • 健全帧的低 14 位角度完全正确(实测 2963 ⇒ 65.10°,真值 65.11°);
  • 坏帧率 ≈ 0.25%(≈16 帧/秒 @6.7 kHz),且与 PWM 开关耦合相关(使能 2.0 次/秒 vs 失能 0.12 次/秒)。

4.2 坑 B(更隐蔽、更致命):「一次 ±2π 修正」不是真展开

// ✗ 老写法:只在 |a - last| < 3π 时才等价于"取最近等价角"
float d = a - last;
if (d >  _PI) d -= _2PI;
if (d < -_PI) d += _2PI;
float u = last + d;

关键矛盾:

  • last 是连续域值,大角度目标下可达 ±200 rad;
  • 坏帧给出的 a 恒在 [0, 2π);
  • ⇒ 差值能到 200 rad,一次修正远远不够,u 落到错误的圈数上。

后果:把 3 个这样的值塞进中值窗口,中值(以及 last)就被整圈拽走 ⇒
motor.shaft_angle 直接跳 360° 的整数倍。

现场证据(v19 固件的 8 条现场记录器吐出来的,真值 5.3452 rad):

EV rx=4004 good=1 | a=0.0015 u=6.2847 m=6.2847 last=12.5652 (rad)
EV rx=4001 good=1 | a=0.0004 u=6.2836 m=6.2836 last=12.5641 (rad)

读法:rx = SPI 原始帧;a = 解码角度;u = 展开后连续值;m = 中值;last = 上一输出。
0x4004 被当成合法帧(good=1,偶校验成立),而 last = 12.5652 ≈ 2π×2 —— 圈数已经错了。

4.3 反例:传感器层「冻结式守卫」是严重回归

思路很自然:中值输出与上一输出相差超过物理可能的一步,就判为假读数、沿用上一个输出。
实测是灾难(同目标 200.123 rad / 100 s / 120 Hz):

固件 基准 超调/毛刺 |vel|max Vqpp Vq 打到 ±1.9V 的点 暴走段(>3°)
v16 — 2.18° 93.2 °/s 0.773 V 0 0
v17 — 2.44° 23.5 °/s 0.157 V 0 0
v18 冻结式守卫(0.5°) 65.66° 515.8 °/s 3.980 V 17 7
v19 冻结 → 只记录 37.24° 342.2 °/s 0.522 V 0 1
v20 真归约 + 重读式检查 0.20° 1.0 °/s 0.042 V 0 0

机理:冻结输出 = 让位置反馈停在旧值,而位置环仍在按那个旧值加力
⇒ 环把轴真推走 ⇒ 中值追着真位置跑 ⇒ 与冻结值越差越远 ⇒ 继续冻结 —— 正反馈自激。
同时被污染的展开锚点会让 Sensor::update() 的整圈计数一起错掉。

★★ 结论:传感器层永远不要「冻结输出」。要丢弃就重读。

另注:Sensor::update() 里有 if (|d_angle| > 0.8·2π) full_rotations += ...,
所以一旦展开值被拽走,圈数错误还会被放大成整圈级的 shaft_angle 跳变 —— 这也解释了
为什么 v18 的毛刺量级是 36°/54° 而不是几度。


5. v20 方案

5.1 四道防线(顺序即优先级,越靠前越治本)

# 防线 作用 阈值
① 真·模 2π 归约 坏帧误差被限制在 ≤π,不可能改变圈数(治本) —
② 帧级步长合理性(越界即重读,最多 3 次) 3 连以上坏帧也进不了中值窗口 AS5048_STEP_EPS = 0.035 rad ≈ 2.0°
③ 死总线帧点名剔除 0x0000 / 0xFFFF 能骗过偶校验 精确匹配
④ 5 点中值(连续域) 兜底吃掉 ≤2 连坏帧 窗口 5 点
+ 现场记录器(只计数不干预) 抓坏帧现场,供串口打印 触发阈 2.9°

5.2 核心代码

// ① 真·模 2π 归约(v20 核心修复)
float d = a - last;
d -= _2PI * roundf(d / _2PI);      // 归约到 (-π, π],与 |d| 多大无关
float u = last + d;

h[hi % AS5048_MED_N] = u;
hi++;
if (hi < AS5048_MED_N) { last = u; return u; }         // 先把窗口填满

// ④ 5 点中值(连续域)
float m = median5(h[0], h[1], h[2], h[3], h[4]);
if (fabsf(m - u) > AS5048_MED_EPS) n_jump++;           // 被挤掉的帧(计数)
if (fabsf(m - last) > AS5048_BURST_EPS) {              // 连串穿透:只记录,不拦截
  n_burst++;
  Ev& e = ev[ev_i & 7]; ev_i++;                        // 记现场,供 X 打印
  e.rx = rx; e.a = a; e.u = u; e.m = m; e.last = last; e.good = good ? 1u : 0u;
}
last = m;
n_ok++;
return m;
// ③ 死总线帧点名剔除
//    它们的偶校验恰好都能通过(1 的个数 0 / 16 都是偶数),只查校验根本拦不住。
static bool deadBus(uint16_t rx) { return rx == 0x0000 || rx == 0xFFFF; }

// ② 帧的"步长合理性":把原始角度按 2π 归约到 (-π, π] 与上一连续值比较
bool stepPlausible(uint16_t rx) {
  if (!primed || !guard) return true;                  // 对齐期轴会大范围扫动
  float a = (float)(rx & 0x3FFF) * (_2PI / (float)AS5048_CPR);
  float d = a - last;
  d -= _2PI * roundf(d / _2PI);
  return fabsf(d) <= AS5048_STEP_EPS;
}

bool readValid(uint16_t& rx) {
  for (int k = 0; k < 3; k++) {                        // 最多读 3 次,挑一帧"干净"的
    uint16_t r = frame();
    if (deadBus(r)) { n_dead++; dbg_par = r; continue; }   // 死总线:丢弃重读
    if (!parityOk(r)) {                                    // 校验不过:丢弃重读
      n_par++; dbg_par = r;
      if ((r >> 14) & 0x1) n_ef++;
      continue;
    }
    rx = r;
    if (stepPlausible(r)) return true;
    n_glitch++;                                            // 越界:重读
  }
  return false;   // 三次都没拿到"干净"帧 ⇒ 调用方沿用 last,只冻结 1 帧
}

5.3 阈值怎么定(AS5048_STEP_EPS = 2°)

轴在一个控制周期内物理上能走多远:

  • 位置模式限速 1.5 rad/s、主循环 6.6 kHz ⇒ 0.013°/周期;
  • 退到速度模式上限 8 rad/s ⇒ 0.070°/周期;
  • 主循环即便卡 20 ms 也才 1.7°。

⇒ 取 2°:对 6.6 kHz 有 150 倍余量,能容忍 20 ms 的抖动;
而坏帧(角度≈0 而真值 306°)差 ~5.3 rad ⇒ 轻松命中。


6. 实测数据

6.1 版本横向对比(同目标 200.123 rad / 保持 100 s / 120 Hz)

固件 σ_rob = LSB 稳健峰峰 原始峰峰 |vel|max Vqpp Vq饱和点 暴走段(>3°) JU
v16 0.0255° 1.16 0.774° 2.177° 93.2 °/s 0.773 V 0 0 4258
v17 0.0255° 1.16 0.155° 2.441° 23.5 °/s 0.157 V 0 0 4556
v18 0.0255° 1.16 2.808° 65.655° 515.8 °/s 3.980 V 17 7 5386
v19 0.0255° 1.16 0.550° 37.242° 342.2 °/s 0.522 V 0 1 4503
v20 0.0255° 1.16 0.086° 0.200° 1.0 °/s 0.042 V 0 0 536

σ_rob 从 v16 到 v20 一直是 0.0255°(1.16 LSB) —— 那是 14 bit 编码器的分辨极限
(LSB = 360/16384 = 0.0220°),σ 已经没有优化空间。
真正的差别全在毛刺上:原始峰峰 11×、|vel|max 93×、Vqpp 18×。
★ 验收必须把 σ、原始峰峰、|vel|max、Vqpp、暴走段数一起报,只报 σ 会得出「没改善」的错误结论。

6.2 v20 保持抖动(200.123 rad,100 s,119 Hz,11,853 点)

指标 值
σ_rob 0.0255°(1.16 LSB)
稳健峰峰(0.5~99.5%) 0.086°
原始峰峰 0.200°
1 秒窗口最大峰峰 0.132°
|vel| 峰值 1.0 °/s
Vq 峰峰 0.042 V(离 ±2.0 V 限幅很远)
暴走事件(>3°) 0 次

6.3 瞬态回归(_step100.py)

项目 结果
100° 阶跃 #1 超调 +3.93°、到位 2.9 s、末态残差 +0.003°、翻转 1
100° 阶跃 #2 超调 +4.59°、到位 2.7 s、末态残差 −0.002°、翻转 1
5° 小阶跃 #1 峰值 +0.191°、末态 −0.031°、|vel|max 3.56 °/s
5° 小阶跃 #2 峰值 +0.317°、末态 −0.003°、|vel|max 4.90 °/s
参考(v16 同口径) 超调 +4.52°、翻转 2

⇒ 瞬态不降反升,证明所有改动都只作用在「传感器读数」这一层,没有碰控制环。

6.4 计数(v20 一次阶跃会话)

SA=160617  JU=77  BS=0  PA=516  EF=463  DB=0  GL=421  loop=5942Hz
  • BS = 0 ⇒ 连串穿透已被前面三道防线全部拦下;
  • GL = 421 ⇒ 步长检查确实在干活,且每次重读都拿到了干净帧 ⇒ 输出零毛刺;
  • 对比 v18 的 BS=7112:那是「检测到但冻结输出」的次数,正是暴走的来源。

7. 可复用的诊断方法论

  1. 先做判别器:把电机失能再采一次。
    失能后角度仍自己漂/跳 ⇒ 传感器问题;纹丝不动(σ≈1 LSB)⇒ 环的问题。
    这是「到底是传感器还是控制环」最省事的硬判据。
  2. 度量必须 ≥100 Hz 采样,且不能只给 σ。
    单点坏帧会把 σ、峰峰、到位时间全毁掉。四条一起给:
    稳健 σ(MAD×1.4826)/ 稳健峰峰 / 原始峰峰 / 单点尖刺数。
  3. 给固件装一个「现场记录器」。
    当出现可疑事件时,把原始帧 / 解码值 / 展开值 / 中值 / 上一输出存进 8 条环形缓冲,
    用同一条 debug 命令打印。这比「猜坏帧长什么样」快一个数量级 ——
    本次就是靠它一次定案的(rx=0x4004 + last=12.5652)。
  4. 别在传感器层做「冻结/沿用上一输出」的守卫,要丢弃就重读。
  5. 串口探针的两个坑(会伪造出完全错误的结论):
    • 命令回显竞争:SimpleFOC 的 B<目标> 会回显目标值。发完立刻读角度可能读到回显本身
      ⇒ 误判「瞬间到位」,而电机其实还在以 ~85°/s 横穿 199 rad。已因此得到过
      完全错误的「保持抖动 680° 峰峰 / 975 尖刺」。
      ⇒ 正确做法:开测前 drain() 一次,之后只做「写一行/读一行」靠应答前缀自同步;
      到位判定要「连续 N 次在带内 + 至少 1 s」。
    • 每条命令前等 150 ms 静默会把采样率降到 6 Hz,3 Hz 以上什么都看不到。

8. 文件清单 / 工具 / 回退

8.1 固件与备份

文件 说明
...\9-22\SimpleFOC_ESP32FOC\src\main.cpp 当前 v20(525 行 / 27,015 B)
main.cpp.bak_v19 … .bak_v13 逐版备份,逐个可回退
main.cpp.bak_before_as5048a 未接管传感器层之前的原始版
_build_single\out\SimpleFOC_Single.ino.bin 已烧录的 v20 二进制(307,856 B,md5 eec40a7095138eb13023784f86cf022b)
_build_single\out_prev_v16\、out_prev_v13\ 二进制级备份
_build_single\compile_v19.log / compile_v20.log 编译日志(0 错误)
_build_single\upload_v19.log / upload_v20.log 烧录日志(各 4 段 Hash of data verified)

8.2 脚本(均在 D:\workspace\Project1\,用 Python 3.10 跑,含 pyserial)

脚本 用途
_hold100.py <ver> 聚焦:移动 200.123 + 保持 100 s,存 _hold100_<ver>.json
_step100.py 聚焦:100° / 5° 阶跃回归 + 计数
_cmp_versions.py v16→v20 横向对比表
_verify_v18.py / _verify_v18b.py 全量验证(100° 阶跃 + 小目标 + 100 s 保持 + 计数)
_patch_v19.py / _patch_v20.py 固件补丁(字节安全:锚点 count==1 断言、单 BOM 校验、LF 校验)
_gen_report5.py 生成图形版 HTML 报告
_save_trace.py / _save_trace2.py 保存原始轨迹
_sweep_bvf.py LPF_velocity.Tf 在线扫参(免烧录)
_dis_vs_en.py 失能/使能判别器

8.3 数据文件

文件 内容
_hold_trace2.json v16 的 200.123 / 100 s 轨迹
_hold_trace3.json v17 同口径轨迹
_hold_trace4.json v18 同口径轨迹(暴走对照)
_hold100_v19.json / _hold100_v20.json v19 / v20 同口径轨迹
_hold100_v19.out v19 的 X 输出(含 EV 现场证据)
_step100_v20.out v20 的阶跃回归 + 计数
_cmp_versions.out 版本对比表
位置保持抖动_修复验证报告.html 图形版报告(轨迹图、柱状对比、现场证据)

8.4 回退方法

# 源码回退(任选一版)
copy  ...\9-22\SimpleFOC_ESP32FOC\src\main.cpp.bak_v19  main.cpp      # 回到 v19
copy  ...\9-22\SimpleFOC_ESP32FOC\src\main.cpp.bak_v17  main.cpp      # 回到 v17(无守卫版)

# 重新编译 + 烧录
cd D:\workspace\Project1\_build_single
copy <main.cpp> SimpleFOC_Single\SimpleFOC_Single.ino
arduino-cli.exe --config-file D:\workspace\tools\arduino-cli\arduino-cli.yaml ^
    compile --fqbn esp32:esp32:esp32 --output-dir out SimpleFOC_Single
arduino-cli.exe --config-file D:\workspace\tools\arduino-cli\arduino-cli.yaml ^
    upload -p COM7 --fqbn esp32:esp32:esp32 --input-dir out

★ Git Bash 里调 arduino-cli 必须 MSYS_NO_PATHCONV=1 + D:/... 正斜杠,
否则路径被转成 d:\d\workspace\... 报 Can't open sketch。


9. 后续建议

按收益排序:

  1. 从源头减少坏帧(电气层)。实测坏帧率与 PWM 开关耦合强相关(使能 2.0 次/秒 vs 失能 0.12 次/秒)。
    可尝试:

    • 缩短 SPI 走线、远离功率回路;
    • MISO 串 100 Ω、对地 10~47 pF;
    • SPI 时钟 1 MHz → 400 kHz(AMS 上限 10 MHz,但降速对时序余量有好处);
    • 编码器供电加去耦(100 nF + 10 µF)。
      软件层已经兜住,但源头干净能让余量更大。
  2. 分辨率硬上限:14 bit ⇒ 0.0220°/LSB。当前 σ 已压到 1.16 LSB,
    想再提升只能换更高位编码器(如 AS5047P 14bit 同档、或 17~21 bit 磁编/光编)。

  3. 临时调试手段(免烧录):BVF0.03 可关掉速度滤波看原始速度噪声;
    BLV/BAI/BAP/BVP/BVI/BLU 在线调位置环与限幅;
    BMD1 开 monitor、BMG7 取全状态。


附录 A · 串口命令与计数含义

A.1 控制命令

命令 作用
A|B<rad> 设位置目标(单位弧度,100° = 1.745)。A 是 B 的别名
BE1 / BE0 使能 / 失能。★ 上电目标 = 当前位置,BE1 不会跳回零点
BMD1 / BMD0 开 / 关监视输出
BMG7 一次性打印全状态:target;Vq;Vd;Cq;Cd;vel;angle
BMG6 一次性打印角度
BLV/BAI/BAP/BVP/BVI/BLU/BVF 在线调 限速/位置环I/位置环P/速度环P/速度环I/限压/速度滤波 Tf
#4 清速度环积分(换向/停车后不"憋劲")
X 传感器自检:计数 + 原始帧环形缓冲 + 连串穿透现场

A.2 X 计数含义

缩写 含义 v20 期望值
SA 有效帧数(accepted) —
JU 被中值挤掉的帧 小
BS 连串穿透(中值输出与上一输出差 >2.9°) 恒 0
PA 偶校验失败的读次数 小(每帧最多计 3 次)
EF EF 位置位的次数 仅统计
DB 死总线帧(0x0000/0xFFFF) 少量
GL 步长越界重读次数 数百/会话,正常
loop 主循环频率 ~6.5 kHz

A.3 EV 现场行格式

EV rx=<原始帧HEX> good=<0/1> | a=<解码值> u=<展开值> m=<中值> last=<上一输出> (rad)

附录 B · 复现命令

cd D:\workspace\Project1

# 1) 聚焦:100 s 保持(约 4.5 分钟,含 143 s 行程)
python _hold100.py v20

# 2) 聚焦:阶跃回归 + 计数(约 1 分钟)
python _step100.py

# 3) 版本横向对比
python _cmp_versions.py

# 4) 重新生成图形版报告
python _gen_report5.py
# 5) 手工看传感器现场(串口 115200)
#    开串口会触发 ESP32 自动复位,setup()(含 initFOC)约 8.7 s 才进 loop()
#    发 X 后读一行 SA... + 4 行 RG... + EV 行

附录 C · 结论速查卡

现象:停在目标后"细微抖动、频率不高"
  ↓
第一步:电机失能再采一次
  ├─ 失能后纹丝不动  ⇒ 控制环问题(查齿槽井 / P_angle.I / 限幅)
  └─ 失能后仍跳/漂   ⇒ 传感器坏帧(本文档的情形)
        ↓
     不要调 P/I(只会掩盖症状)
        ↓
     传感器层做三件事:
       ① 真·模 2π 归约   d -= 2π*roundf(d/2π)
       ② 帧级步长合理性   越界即重读(不冻结!)
       ③ 死总线帧剔除     0x0000 / 0xFFFF(它们能骗过偶校验)
       + 5 点中值兜底 + 现场记录器
        ↓
     验收:σ(1.16 LSB) + 原始峰峰 + |vel|max + Vqpp + 暴走段数,一起报

由 孙乃宇 更新于 大约一个月 之前

  • 状态 从 进行中 变更为 已完成
  • % 完成 从 0 变更为 100
行为

导出 Atom PDF