# ASCOM Dome 驱动设计（PLC + 编码器 + 霍尔零位）

## 1. 系统组成

- 圆顶控制器：PLC，负责电机启停、方向、按时间运行、编码器计数
- 编码器：计数圆顶转动步数
- 霍尔传感器：0° 零位校准
- PC 端：ASCOM Dome 驱动，负责角度换算、定位决策、修正

## 2. PC 端保存的参数

| 参数 | 说明 |
|---|---|
| Rev_Time | 圆顶转一圈的时间 (s) |
| Total | 圆顶转一圈的编码器步数 |
| Speed | 圆顶速度 = 360 / Rev_Time (°/s) |
| Steps_Per_Deg | Total / 360 |
| C0 | 启停补偿角度 (°)，实测拟合得到 |
| DeadBand | 死区，建议 1° |
| MinRunTime | 电机最短可响应运行时间 (s) |
| Home_Offset | 霍尔零位对应的步数（SyncToAzimuth 修改） |

角度换算（在 PC 端完成，PLC 不需要知道 Total）：

```
Az = (Cur_Step MOD Total) * 360 / Total
```

## 3. 方案 1：按时间转动 + 一次编码器修正

PLC 只按时间转电机，所有判断放在 PC 端。PLC 不比较步数，因此不存在 360° 附近"找不到目标步数"的问题。

> 两种方案共用同一套硬件（PLC + 编码器 + 霍尔硬件清零）：方案 1 见第 3~7 节；方案 2（双 HSC 增量步数停机，精度更高）见第 8 节。

### 流程

1. PC 读取当前角度 Cur，计算 offset = 目标 − 当前，并选方向（offset > 180° 时换向，offset = 360 − offset）
2. 发送命令：方向 + 运行时间 `t = (offset − C0) / Speed`
3. PLC 按时间转动后停机，PC 等待 Slewing = False（超时 `t × 1.2 + 10` s 则 Abort 并报警）
4. PC 再读一次角度，计算残差 err；若 |err| > DeadBand，按同样方法再转一次
5. 修正后仍超出死区，则报警，不再继续转

### 注意事项

- **计时放在 PLC**：PC 只发"方向 + 毫秒数"，由 PLC 定时器停机。Windows + 串口/网络抖动可达几十~上百 ms。
- **启停补偿**：实测两组（如转 2 s 和 10 s）读出实际转角，线性拟合 `角度 = Speed × t + C0`，反算 `t = (offset − C0) / Speed`，对小角度修正尤其重要。
- **最小转动**：残差 < 死区，或算出时间 < MinRunTime，不动作，避免频繁点动。
- **霍尔清零保留**：霍尔上升沿时 `Cur_Step := Home_Offset`，避免编码器漂移逐圈累积。建议固定使用同一边沿。

## 4. PLC 端逻辑（ST）

```pascal
// 霍尔上升沿清零
IF R_TRIG_Home.Q THEN Cur_Step := Home_Offset; END_IF;

// 收到命令：Dir, Run_ms
IF Cmd_Start THEN
    Motor_Dir := Dir;  Motor := TRUE;  T_Run(IN := TRUE, PT := Run_ms);
END_IF;

IF T_Run.Q OR Cmd_Abort THEN
    Motor := FALSE;  T_Run(IN := FALSE);
END_IF;

Slewing := Motor;
```

## 5. PC 端 SlewToAzimuth 核心逻辑（C#）

```csharp
void SlewToAzimuth(double target)
{
    for (int i = 0; i < 2; i++)                // 粗定位 + 一次修正（最多两次）
    {
        double err = target - ReadAzimuth();   // 读 PLC 步数换算成角度
        err = ((err % 360) + 540) % 360 - 180; // 归一化到 [-180, 180)，自动选最短方向
        if (Math.Abs(err) < DeadBand) return;

        int dir = err > 0 ? 1 : -1;
        double t = (Math.Abs(err) - C0) / Speed;   // 扣除启停补偿
        if (t < MinRunTime) return;

        plc.Move(dir, (int)(t * 1000));
        WaitUntilStopped(timeout: t * 1.2 + 10);   // 超时则 AbortSlew 并报警
    }
}
```

ASCOM 要求 `SlewToAzimuth` 异步返回，上述循环需放在后台线程执行，`Slewing` 属性在整个过程（含修正）中保持 True。

## 6. 精度估算

以圆顶转速 3°/s 为例：

- 第一次转 90°，速度波动 ±5% → 误差约 ±4.5°
- 修正转 4.5°，±5% 波动 → 约 0.2°，加计时抖动约 0.15°
- 最终误差在 ±0.5° 以内，天窗宽度足够覆盖

## 7. ASCOM 接口实现要点

| 接口 | 实现 |
|---|---|
| Azimuth | 读 PLC 步数，按第 2 节公式换算 |
| SlewToAzimuth | 第 5 节逻辑，后台线程 |
| Slewing | PLC Slewing 位 或 修正流程未结束 |
| AbortSlew | 发送 Cmd_Abort，终止后台线程 |
| FindHome | 单向转动直到霍尔触发 |
| AtHome | 霍尔信号状态 |
| SyncToAzimuth | 重新计算 Home_Offset |
| Park | SlewToAzimuth(Park 角度) |
| SetupDialog | Rev_Time、Total、C0、DeadBand、MinRunTime |

## 8. 方案 2：双 HSC 增量步数停机

### 8.1 原理

- **HSC0**：A/B 正交 4 倍频，模式 10，霍尔接硬件复位，只用于读取方位角（同方案 1）。
- **HSC1**：A/B 正交 4 倍频，模式 9，不接复位，专做"剩余步数"计数。启动时 CV 清零、PV 设为 PC 下发的带符号步数，到达 CV=PV 时硬件中断停机。

HSC1 不受霍尔清零影响，PC 无需判断是否过零点；PLC 不做任何计算。整个转动只进一次中断，与脉冲频率无关。正交计数下，停机时的来回抖动正反抵消。

### 8.2 接线（S7-200 CPU 224/226）

| 信号 | HSC0（模式 10，读角度） | HSC1（模式 9，剩余步数） |
|---|---|---|
| 编码器 A | I0.0 | I0.6（并联） |
| 编码器 B | I0.1 | I0.7（并联） |
| 霍尔复位 | I0.2 | 不接 |

| 其他信号 | 地址 |
|---|---|
| 电机运行 / 方向 | Q0.0 / Q0.1 |
| 带符号步数 PV = dir × (N − Lead)（PC 写） | VD100 |
| 方向（PC 写，1=正转） | V104.0 |
| 冗余时间（PC 写，单位 0.1 s） | VW110 |
| 启动 / 中止（PC 写） | M0.0 / M0.2 |
| 到位 / 超时标志（PC 读） | M1.0 / M1.1 |
| 当前角度计数 / 剩余步数进度（PC 读） | HC0 / HC1 |

每个输入点只能分配给一个 HSC，"共用编码器"指 A、B 线在端子上并联接两组输入。并联注意：

- 推挽 / HTL 24V 输出驱动两路 PLC 输入一般没问题（每路约 4 mA）
- 集电极开路输出需确认上拉及源型/漏型公共端匹配
- 5V 差分（RS422）输出不能直接接 24V 输入，需加转换模块
- S7-200 正交模式最高约 20 kHz，圆顶转速通常远低于此

### 8.3 PLC 程序（STL）

```
Network 1   // 首次扫描：HSC0 读角度（模式10，霍尔复位），HSC1 剩余步数（模式9）
LD     SM0.1
MOVB   16#F8, SMB37     // HSC0: 允许, 更新CV/PV/方向, 4倍频
MOVD   0, SMD38
MOVD   16#7FFFFFFF, SMD42
HDEF   0, 10
HSC    0
HDEF   1, 9             // HSC1 模式9：A/B 正交，无复位
ENI

Network 2   // 启动沿
LD     M0.0
EU
=      M0.1

Network 3   // CV 清零，PV = PC 下发的带符号步数，启动
LD     M0.1
MOVB   16#E0, SMB47     // HSC1: 允许, 更新CV/PV, 4倍频
MOVD   0, SMD48         // CV = 0
MOVD   VD100, SMD52     // PV = dir × (N − Lead)
HSC    1
ATCH   INT_0, 13        // 事件13: HSC1 CV=PV
R      M1.0, 2          // 清到位 / 超时标志
S      Q0.0, 1
R      M0.0, 1

Network 4   // 方向
LD     V104.0
=      Q0.1

Network 5   // 冗余时间保护
LD     Q0.0
TON    T37, VW110

Network 6   // 超时或 PC 中止：停机
LD     T37
O      M0.2
R      Q0.0, 1
DTCH   13
R      M0.2, 1

Network 7   // 超时标志
LD     T37
S      M1.1, 1

INT_0   // 到位
LD     SM0.0
R      Q0.0, 1
DTCH   13
S      M1.0, 1
```

### 8.4 PC 端 SlewToAzimuth 核心逻辑（C#）

```csharp
void SlewToAzimuth(double target)
{
    for (int i = 0; i < 2; i++)                    // 增量定位 + 可选一次修正
    {
        double err = ((target - ReadAzimuth()) % 360 + 540) % 360 - 180;  // 最短路径
        if (Math.Abs(err) < DeadBand) return;

        int dir = err > 0 ? 1 : -1;
        long n = (long)Math.Round(Math.Abs(err) * Total / 360.0);       // 4 倍频单位，与 HSC0 一致
        if (n <= Lead) return;                     // 小于滑行量，不动作

        double time = Math.Abs(err) / Speed * 1.2 + 10;                  // 冗余时间
        plc.Move(dir, dir * (n - Lead), time);     // 写 V104.0 / VD100 / VW110(×10)，置 M0.0
        WaitUntilStopped(time + 5);                // M1.0 到位；M1.1 超时则报警
    }
}
```

`ReadAzimuth()` 与方案 1 相同：`Az = (HC0 MOD Total) × 360 / Total + Home_Az`（HC0 可能为负，取模用 `((a % m) + m) % m`）。

### 8.5 精度与注意事项

- **精度**：停机由编码器硬件比较触发，误差只剩 Total 换算误差（不累积）和 Lead 离散性，一般 0.1°~0.3°，通常无需修正。
- **方向符号**：PV 的正负须与实际计数方向一致，实测一次正转时 HC1 是增大还是减小，必要时 `dir` 取反。
- **Lead 标定**：多次实测停机后的滑行步数取平均。
- **单位统一**：Total、N、Lead 均为 4 倍频计数。
- **超时保护保留**：编码器打滑或断线时 HC1 不变化，靠冗余时间停机。
- **开机先 FindHome**：霍尔清零前 HC0 与实际方位无对应关系。
- **ASCOM 接口**：同第 7 节；`AbortSlew` 置 M0.2；SetupDialog 增加 Lead。

### 8.6 型号差异

- **S7-200 CPU 221/222**：无 HSC1，改用 HSC4 正交（I0.3/I0.4），中断事件改为 HSC4 的 CV=PV。
- **S7-200 SMART**：HSC1 仅单相，正交改用 HSC2（I0.2/I0.3）；HSC0 模式 10 复位输入为 I0.4，霍尔接线相应调整。

> 以上 STL 按 S7-200 系统手册 HSC 用法编写，未经实机验证。输入点分配、SMB37/SMB47 各位含义、中断事件号请对照所用 CPU 手册确认。

## 9. ASCOM 软件架构（采用方案 1）

结论：通讯采用**单独线程**，且只有这一个线程接触串口/TCP；多程序共享采用 **ASCOM LocalServer（exe）**，所有客户端连到同一进程，由它独占 PLC 连接。

### 9.1 Modbus 通讯层

**为什么要独立线程**

- 串口只能被一个对象打开，Modbus RTU 严格一问一答，并发请求会导致帧错乱。
- NINA、SGP 等客户端会高频轮询 `Azimuth`、`Slewing`，每次现场读 PLC 响应慢（每个往返几十 ms），多客户端还会并发冲突。

**结构：轮询线程 + 状态缓存 + 命令队列**

- **通讯线程**：唯一持有 Modbus 连接，每 200–500 ms 读一次状态寄存器（HC0、电机运行位、霍尔状态、到位/超时标志），写入线程安全的状态缓存并记录时间戳。
- **属性读取**：`Azimuth`、`Slewing`、`AtHome` 等 getter 直接返回缓存值，立即返回。
- **写命令**：方向 + 时间 + 启动、中止等进入命令队列，由通讯线程在两次轮询之间插队执行；中止命令优先级最高。
- **定位流程**：放在另一个后台任务中（读角度 → 下发转动 → 等待停止 → 读角度 → 修正一次），读靠缓存、写靠队列，通讯线程不会被长时间等待阻塞。

**异常处理**

- 超时重试 2–3 次；连续失败标记"通讯中断"，ASCOM 端抛 `DriverException`，后台自动重连。
- **PLC 侧自保护**：转动时间由 PLC 定时器控制，PC 掉线电机也会停。建议增加心跳：PC 定时写计数寄存器，PLC 发现数秒未变化则强制停机。

**S7-200 选型**

- S7-200：Micro/WIN Modbus Slave 库，Port0，RTU（RS485）。V 区映射到 40001 起的保持寄存器；32 位 HC0 拆成两个 16 位寄存器，字序需实测确认。
- 防止高低字撕裂：PLC 每个扫描周期把 HC0 复制到 VD 区，PC 一次读出连续两个寄存器。
- S7-200 SMART 原生支持 Modbus TCP，网口更稳定、便于远程。

### 9.2 多程序共享

InProc DLL 会被每个客户端加载进各自进程，各自打开同一串口，第二个程序必然失败。

| 方式 | 说明 | 评价 |
|---|---|---|
| InProc DLL | 每个客户端各自加载 | 不能共享，排除 |
| LocalServer（exe） | ASCOM 官方模板，COM 进程外服务，所有客户端共用一个 exe，exe 独占串口 | 推荐，兼容所有 Windows ASCOM 客户端 |
| Alpaca（HTTP REST） | 驱动内置 HTTP 服务，客户端走网络访问 | 适合跨机器/跨平台，可作为第二阶段 |

**LocalServer 设计要点**

- **连接引用计数**：A 程序 `Connected = false` 不能关闭 B 程序仍在用的串口；计数归零才真正断开 Modbus。
- **单一状态源**：所有客户端读取同一缓存，数据一致。
- **命令冲突策略**：后到命令生效。新的 `SlewToAzimuth` 先取消正在进行的定位任务（含修正），再执行新目标；`AbortSlew` 来自任何客户端都立即生效。
- **进程生命周期**：最后一个客户端释放后 exe 自动退出（模板自带）；可提供状态窗口显示角度、通讯状态、日志。

**Slaving（圆顶跟随望远镜）**：不在驱动内实现，交给 NINA / SGP / ACP 或 ASCOM Dome Control 周期性调用 `SlewToAzimuth`。驱动只需保证新命令可打断旧命令，并设死区。

### 9.3 ASCOM 接口相关

- **接口版本**：IDomeV2，或 Platform 7 的 IDomeV3（`Connect()` / `Connecting` 异步，适配后台通讯线程）。
- **异步语义**：`SlewToAzimuth`、`FindHome` 立即返回，`Slewing` 在整个流程（含修正）中保持 True。
- **配置保存**：Total、Rev_Time、C0、DeadBand、串口参数存 ASCOM Profile，在 SetupDialog 中修改。
- **日志**：ASCOM TraceLogger 记录 Modbus 收发、定位残差、修正次数，便于标定 C0。

### 9.4 分层

1. **Modbus 通讯层**：独立线程，连接、轮询、命令队列、重连
2. **设备模型层**：状态缓存、角度换算、定位流程（后台任务）
3. **ASCOM 接口层**：参数校验和转发，不含通讯逻辑
4. **LocalServer 外壳**：COM 注册、多客户端共享、连接引用计数
5. （可选）**Alpaca 层**：复用第 2 层，对外提供 HTTP 接口

第 1、2 层可先做成独立测试程序，对着 PLC 调通并标定 C0，再套 ASCOM 外壳。
