# DDSU666 Modbus 协议（从官方 PDF 提取）

> 来源：`docs/references/DDSU666 Single phase Smart Meter.pdf`
> （CHINT / 正泰仪表 **DDSU666 Single phase Smart Meter — Operation Manual**，文档号 `ZTY0.464.1224`，版本 **V2**，2020 年 8 月；厂商 Zhejiang Chint Instrument & Meter Co., Ltd.）
> 本文件是 PDF 的可读化提取，供本项目的 Modbus 采集驱动设计参考。**以官方 PDF 为准**，本文件如有出入以 PDF 为准。
> 同类文档见 SDM120 的 `SDM120-Modbus-Protocol.md`；两表差异较大，见下方 §6「与 SDM120 的关键差异」。

## 设备速览（来自手册 Table 1 / Table 5）

| 项 | DDSU666（直接接入） | DDSU666-CT（经互感器） |
| --- | --- | --- |
| 精度等级 | Active Class B | Active Class C |
| 参考电压 | 230 V | 230 V |
| 电流规格 | 0.25–5(80) A | 0.015–1.5(6) A |
| 表常数 | 800 imp/kWh | 6400 imp/kWh |
| 接入方式 | 直接接入 | 经电流互感器 |

- 单相电子式电能表，DIN35mm 导轨安装；测量电压、电流、有功/无功功率、频率、功率因数、正/反向有功电能。
- 电能测量范围 `0～999999.99 kWh`（LCD 只显示 6 位，自动移动小数点）。
- 通信：RS485，**Modbus-RTU**（也支持 DL/T 645-2007，可切换，见 §5 `0005H ChangeProtocol`）。
- 手册 Table 1 标注 Frequency Reference = 60Hz，但 LCD 示例又写 `F=50.00Hz`（手册自身不一致）；**实际频率以寄存器 `200EH` 读数为准**，不要把 50/60 写死。

## 0. 本项目的接入方式（重要）

DDSU666 物理层是 **Modbus RTU（RS-485 串口）**，半双工。和 SDM120 一样，本项目通过一个 **Modbus-TCP 网关**接入：

- 后端用 **Modbus TCP**（`IP:port`）连到网关，网关在串口侧转成 RTU 与电表通信。
- TCP 帧用 MBAP header、**无 CRC**（CRC 由网关在 RTU 侧处理）。本文档里 RTU 帧的 `CRC (Lo/Hi)` 字段在 TCP 模式下不需要我们关心。
- **Slave Address / Unit ID = 电表的通信地址 Addr**（范围 1–247；面板按键只能设 1–99；见 §5 `0006H`），在 TCP 请求里作为 unit id 传入。
- 若以后直连串口（RTU），才需要管波特率 / 数据格式 / CRC：**默认串口格式是 8 数据位、无校验、2 停止位（8N2）**，与 SDM120 的 8N1 不同——直连时务必对齐。
- 电表必须处于 **Modbus 协议模式**（而非 DL/T 645）才能用本协议；可经面板长按切换，或写 `0005H = 2`（见 §5）。

## 1. 协议帧格式（Appendix A）

异步传输，按字节为单位。一帧 10 位字符 = **1 起始位(0) + 8 数据位(无校验) + 2 停止位(1)**（其它格式可定制）。

### 信息帧结构（Table A.1）

| 字段 | 长度 | 说明 |
| --- | --- | --- |
| Start（起始） | >3.5 字符静默 | 帧间至少 3.5 字符空闲时间作为分隔 |
| Address code（地址码） | 1 字节 | 目标从机地址 1–247；每个从机在总线上地址唯一 |
| Function code（功能码） | 1 字节 | 仅支持 **03H / 10H**（见 §2） |
| Data（数据域） | n 字节 | 随功能码不同而不同（起始地址、寄存器数、寄存器数据等） |
| CRC check code（CRC 校验） | 2 字节 | 16-bit CRC（**低字节在前、高字节在后**；多项式 `A001`） |
| End（结束） | >3.5 字符静默 | 帧间静默 |

> TCP 网关模式下 Start/End 静默与 CRC 由网关处理，本项目不关心。

### 功能码 03H 示例（读寄存器，Table A.3/A.4）

读从机 `01H`、起始地址 `0CH`、2 个寄存器：

- 主机发送：`01 03 00 0C 00 02 04 08`（最后 `04 08` 是 CRC，低字节 `04` 在前）。
- 从机返回（设 `0CH/0DH` 内容为 `0000H`、`1388H`）：`01 03 04 00 00 13 88 F7 65`
  - `04` = 字节数；`00 00` = `0CH` 数据；`13 88` = `0DH` 数据；`F7 65` = CRC（低字节 `F7` 在前）。

> **注意**：单个 16-bit 寄存器内是「高字节在前、低字节在后」（Table A.4 里 `0DH` 数据返回 `13 88` = `0x1388`）。这一点对解码浮点的字节序很关键，见 §3。

### 功能码 10H 示例（写多个寄存器，Table A.5/A.6）

向从机 `01H`、起始地址 `00H` 连续写 3 个寄存器 `0002H,1388H,000AH`：

- 主机发送：`01 10 00 00 00 03 06 00 02 13 88 00 0A 9B E9`
  - `06` = 写入字节数（3 寄存器 × 2 字节）；随后是 3 个寄存器的数据；末尾 `9B E9` CRC。
- 从机返回：`01 10 00 00 00 03 80 08`（回显起始地址 + 寄存器数 + CRC `80 08`）。

### 异常响应（Table A.7/A.8）

- 异常时返回的 Function Code = **原功能码 + 128**（即最高位置 1，`03H→83H`、`10H→90H`）。
- 数据为单字节 Error Code：

| Error Code | 含义 | 说明 |
| --- | --- | --- |
| `01H` | Illegal function code | 收到的功能码本表不支持 |
| `02H` | Illegal register address | 寄存器地址超出有效范围 |
| `03H` | Illegal data value | 数据值超出对应地址的取值范围 |

## 2. 功能码

DDSU666 **只支持两个功能码**（Table A.2）：

| 功能码 | 作用 | 说明 |
| --- | --- | --- |
| **03H** | Read register（读寄存器） | 读一个或多个寄存器——**测量值、电量、配置全部走它** |
| **10H** | Write multiple registers（写多个寄存器） | 向 n 个连续寄存器写 n 个 16-bit 数据（改配置 / 清电量） |

> ⚠️ **DDSU666 没有「输入寄存器 / FC04」概念**——所有数据（包括电压电流功率）都用 **FC 03H** 读保持寄存器。这是它和 SDM120（测量值走 FC04）最大的踩坑差异，见 §6。

## 3. 数据编码

DDSU666 有两类数据：

1. **配置 / 参数寄存器（§5，`0000H`–`0010H`）**：每个 1 个寄存器、`16-bit with symbols`（**16 位有符号整数**）。
2. **测量 / 电量寄存器（§4，`2000H`+ / `4000H`+）**：每个参数 = **32-bit IEEE-754 单精度浮点**（手册写 “single precision floating decimal”），占 **2 个相邻寄存器**（Length = 2 Word）。

**浮点字节序 / 字序**

- **字节序（byte order）= 大端：寄存器内高字节在前** —— 由 Table A.4 的 `0x1388` 返回为 `13 88` 确认。
- **字序（word order，两个寄存器谁是高 16 位）**：手册**没有给出浮点解码的实例**，未明确标注。按标准 Modbus 浮点惯例应为**大端字序（高寄存器在前，`ABCD`）**，与本项目 SDM120 驱动一致（`registers_to_float` 用 `>f`，高寄存器在前）。
- ⚠️ **需上机实测确认**：读 `2000H`（电压）应解出 ~230V 这样的合理值；若解出乱数，多半是字序相反，改成「低寄存器在前」再试。CHINT 同系列（DTSU/DDSU666）现场固件偶有字序差异，**首次接入务必用一个已知量（电压）校验**，不要凭手册想当然。

> Python 解码（大端、高寄存器在前）：`struct.unpack('>f', struct.pack('>HH', hi_reg, lo_reg))[0]`。
> pymodbus：`BinaryPayloadDecoder.fromRegisters(regs, byteorder=Endian.BIG, wordorder=Endian.BIG)`。

## 4. 测量 / 电量寄存器表（FC 03H 读）

全部为只读、`Float`（32-bit），每项占 **2 个寄存器**。地址为 Modbus 协议原始地址（即帧里的 Start Register Address Hi/Lo），手册用十六进制。

### 4.1 瞬时量（“Electric quantity of the secondary side”，`2000H` 段）

| 地址(hex) | 参数 | 代号 | 单位 | 备注 |
| --- | --- | --- | --- | --- |
| `2000H` | 电压 Voltage | U | V | |
| `2002H` | 电流 Current | I | A | |
| `2004H` | 有功功率 Active power | P | **kW** | 手册标注 “the unit is KW”——**不是 W** |
| `2006H` | 无功功率 Reactive power | Q | **kvar** | |
| `2008H` | （保留 RESERVED） | — | — | 占 2 寄存器，跳过 |
| `200AH` | 功率因数 Power factor | PF | — | 无量纲 |
| `200CH` | （保留 RESERVED） | — | — | 占 2 寄存器，跳过 |
| `200EH` | 频率 Frequency | Freq | Hz | |

### 4.2 电量（“Electrical data of the secondary side”，`4000H` 段）

| 地址(hex) | 参数 | 代号 | 单位 | 备注 |
| --- | --- | --- | --- | --- |
| `4000H` | 正向（导入）有功电能 Active in electricity | Ep | kWh | 正向 / forward active energy |
| `400AH` | 反向（导出）有功电能 Reverse in electricity | -Ep | kWh | 反向 / reverse active energy |

> 手册里 `4000H` 与 `400AH` 之间（`4002H`–`4009H`）未列出，视为保留/未文档化。

**读取分块建议**

- 瞬时量：`2000H`–`200FH` 共 **16 个寄存器连续**，一次块读即可覆盖 U…Freq（含两段 RESERVED，解码时跳过）。
- 电量：`4000H`（2 寄存器）与 `400AH`（2 寄存器）相距较远，分两小块读，或读 `4000H`–`400BH`（12 寄存器）一次取出后挑用——以网关 / 电表是否允许跨保留地址块读为准，谨慎起见分开读更稳。

### 常用核心子集（日常监控够用）

电压 `2000H`、电流 `2002H`、有功功率 `2004H`（kW）、功率因数 `200AH`、频率 `200EH`、正向有功电能 `4000H`、反向有功电能 `400AH`。

## 5. 配置 / 参数寄存器表（FC 03H 读 / FC 10H 写）（Table 9）

每个 1 个寄存器、**16-bit 有符号整数**。`R/W` 列来自手册。

| 地址(hex) | 代号 | 含义 | R/W | 取值 / 说明 |
| --- | --- | --- | --- | --- |
| `0000H` | UCode | 编程密码 Programming password code | R/W | 写配置前的密码字 |
| `0001H` | REV. | 保留；**实际读出的是版本号** | R | |
| `0002H` | ClrE | 电能清零 CLr.E | R/W | **写 `1` 清除总电量**（不可逆，慎用） |
| `0003H`–`0004H` | RESERVED | 保留 | — | |
| `0005H` | ChangeProtocol | 协议切换 | R/W | **`2` = Modbus-RTU**，`1` = DL/T 645-2007 |
| `0006H` | Addr | 通信地址 | R/W | 1–247（面板按键仅 1–99） |
| `0007H`–`000AH` | RESERVED | 保留 | — | |
| `000BH` | Meter type | 表型 Meter type | R | 只读设备类型标识 |
| `000CH` | BAud | 通信波特率 | R/W | **`1`=2400bps，`2`=4800bps，`3`=9600bps**（手册寄存器仅列这三档；通信章另提到也支持 1200bps） |
| `000DH`–`0010H` | RESERVED | 保留 | — | |

> ⚠️ 写 `0002H`（清电量）、`0006H`（改地址）、`000CH`（改波特率）、`0005H`（切协议）都会改变电表状态或通信参数，配错可能**清空累计电量**或**导致通信中断**。本项目默认**只读采集**，不在自动化链路里写电表配置寄存器。
> 写配置通常需先经 `0000H UCode` 密码字校验；具体密码值手册正文未给出，需向厂商确认或经面板操作。

## 6. 与 SDM120 的关键差异（迁移 / 复用驱动时必看）

本项目已有 SDM120 profile（`app/integrations/modbus/profiles/sdm120.yaml`）。DDSU666 **不能照搬**，主要差异：

| 维度 | SDM120 (Eastron) | DDSU666 (CHINT) |
| --- | --- | --- |
| 读测量值功能码 | **FC 04**（输入寄存器 3X） | **FC 03**（保持寄存器，无 FC04） |
| 测量值起始地址 | `0x0000` 起（30001） | 瞬时量 `0x2000` 起；电量 `0x4000`/`0x400A` |
| 有功功率单位 | **W**（瓦） | **kW（千瓦）** —— 入库前注意换算 / 单位标注 |
| 无功功率单位 | VAr | kvar |
| 配置寄存器格式 | Float（FC03/16） | **16-bit 有符号整数**（FC03/10） |
| 写功能码 | 16 / 0x10 | 10H（同 0x10） |
| 串口默认格式 | 8N1（1 停止位） | **8N2（2 停止位）** |
| 多协议 | 仅 Modbus | Modbus **与 DL/T 645-2007 可切换**（需确保在 Modbus 模式） |
| 浮点字序 | 大端、高寄存器在前（手册有实例佐证） | 字节序大端已确认；**字序手册无实例，需上机实测** |

## 7. 给本项目采集驱动的要点小结

1. 走 **Modbus TCP 网关**：`ModbusTcpClient(host, port)`，`slave=<Addr>`；电表须在 **Modbus 协议模式**。
2. 所有读取（测量 + 电量 + 配置）都用 **FC 03H**——**没有 FC04**。
3. 测量值在 `0x2000` 段、电量在 `0x4000`/`0x400A`，均为 **float32**；解码大端字节序，**字序默认高寄存器在前但务必用电压实测校验**。
4. **有功功率单位是 kW、无功是 kvar**——与 SDM120 的 W/VAr 不同，新建 profile / 入库映射时单位别抄错。
5. 配置寄存器（`0x0000`–`0x0010`）是 **16-bit 有符号整数**，不是 float。
6. 默认**只读**；`0002H` 写 1 会**清空累计电量**、`0006H/000CH/0005H` 会改通信参数，自动化链路里一律不写。
7. 新建 profile 时这是 `ddsu666` 这一个 register profile 的定义；建议 `function_code: 3`、`word_order: big`（先按大端字序，接入后用电压读数验证）、瞬时量与电量分块读。

## 8. 实测记录（真机验证，2026-06-30）

首次接入一台 **DDSU666 直接接入版（5(80)A）** 实测，确认以下几点：

- **字序大端，确认无误**：电压 / 电流 / 频率 / 电能用「大端、高寄存器在前」解码全部得到合理值（如 233.9 V / 0.055 A / 49.99 Hz），与 §3 的假设一致。`ddsu666.yaml` 的 `word_order: big` / `byte_order: big` **无需修改**，§3 里「字序需上机实测」一项可视为已关闭。
- **FC03 读通**：所有量走 FC03，profile `ddsu666` 在采集链路（CLI `read` / 设备 `/test` / 后台轮询）中工作正常。
- **低电流下「瞬时功率读 0、但电能照常累加」**：测试负载仅为一台 PoE 交换机（≈230 V / 0.05 A，真实有功仅几瓦），**远低于本表测量量程下限 Imin≈0.25 A**。此工况下：
  - 瞬时 `active_power`（`0x2004`）与 `power_factor`（`0x200A`）寄存器返回**全零**（原始 hex `0x0000 0x0000`）；电表 LCD 上功率在 0~3.3 W、PF 在 0~0.6 之间抖动。
  - 抖动成因 = **低电流测量噪声 + 开关电源（SMPS，无 PFC）畸变电流**（电流为电压峰值附近的窄脉冲、谐波重 → 畸变功率因数天然偏低）。**不是**「采样率与开关频率拍频」：计量芯片 SH79F7019 采样在 kHz 量级，远低于开关电源 50–200 kHz 的开关频率，两者不在一个频段。
  - 但累计电能 `import_energy`（`0x4000`）**正常累加**（实测 0 → 0.01 kWh）——电表内部积分器在防潜动起始电流（`0.004·Ib`≈0.02 A）之上照常计量。
  - **结论**：低于量程下限时**瞬时功率 / PF 不可信，但电能计量不丢**；电流进入量程（正常负载）后瞬时量即稳定可信。这是 5(80)A 大量程表对极小负载的固有特性，**非缺陷、非解码问题**。
- **排错提示**：若日后看到 DDSU666「功率一直 0」，先确认负载电流是否在 Imin 以上——多半是负载太轻而非链路故障；可用 `scripts/modbus_cli probe --fc 3 --address 0x2000 --count 16` 看 `0x2004/0x2005` 原始寄存器是否真为全零佐证。
