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

> 来源：`docs/references/SDM120-MODBUS_Protocol.pdf`
> （Eastron SDM120 Modbus Smart Meter Modbus Protocol Implementation **V2.4**）
> 本文件是 PDF 的可读化提取，供本项目的 Modbus 采集驱动设计参考。**以官方 PDF 为准**，本文件如有出入以 PDF 为准。

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

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

- 后端用 **Modbus TCP**（`IP:port`）连到网关，网关在串口侧转成 RTU 与电表通信。
- TCP 帧用 MBAP header、**无 CRC**（CRC/Error Check 由网关在 RTU 侧处理）。本文档里 RTU 帧的 `Error Check (Lo/Hi)` 字段在 TCP 模式下不需要我们关心。
- **Slave Address / Unit ID = 电表的 Meter ID**（默认 `1`，范围 1–247），在 TCP 请求里作为 unit id 传入。
- 若以后直连串口（RTU），才需要管波特率 / 校验位 / CRC（见 §5 holding 寄存器）。

## 1. 协议帧格式

MODBUS 定义 master 查询 / slave 响应的格式。Eastron 电表用 16-bit 寄存器在主从间传值，**但实际数据是 32-bit IEEE-754 浮点**，因此每个测量参数占**两个相邻的 16-bit 寄存器**。

### Query（master → slave，RTU 帧）

| 字段 | 说明 |
| --- | --- |
| Slave Address | 8-bit，目标从机地址 1–247（0=广播，Eastron 不支持广播） |
| Function Code | 8-bit，功能码（Eastron 支持 **03 / 04 / 08 / 16(=0x10)**） |
| Start Address (Hi/Lo) | 16-bit 起始寄存器地址；**寄存器成对使用、从 0 开始，所以起始地址必须是偶数** |
| Number of Points (Hi/Lo) | 16-bit 请求的寄存器数量；**也必须是偶数**（成对读浮点） |
| Error Check (Lo/Hi) | 16-bit CRC（RTU 模式；TCP 网关模式无此字段） |

### Response（slave → master）

| 字段 | 说明 |
| --- | --- |
| Slave Address | 响应从机地址 |
| Function Code | 与查询相同的功能码（表示识别并已响应） |
| Byte Count | 8-bit，本次返回的数据字节数 |
| Data (寄存器对) | 每个寄存器 Hi byte / Lo byte，按"高寄存器在前"排列 |
| Error Check (Lo/Hi) | 16-bit CRC（RTU 模式） |

### Exception Response（异常响应）

- 异常响应的 Function Code = 查询功能码 **OR 0x80**（最高位置 1）。
- 数据是单字节 Error Code（异常码）。
- PDF 正文写了"见后文 Table Of Exception Codes"，但提取的 8 页里**没有附上该表**。标准 Modbus 异常码（供参考，非本 PDF 内容）：`01` 非法功能、`02` 非法数据地址、`03` 非法数据值、`04` 从机设备故障。

## 2. 功能码

| 功能码 | 作用 | 寄存器区 |
| --- | --- | --- |
| **04** | Read Input Registers（读输入寄存器，3X）—— **所有测量值都在这里** | 30001+ |
| **03** | Read Holding Registers（读保持寄存器，4X）—— 配置项 | 40001+ |
| **16 / 0x10** | Write Holding Registers（写保持寄存器，4X）—— 改配置 | 40001+ |
| 08 | Diagnostics（诊断） | — |

> ⚠️ **测量值用 FC 04（输入寄存器），不是 FC 03。** 这是最常见的踩坑点。

## 3. 浮点数据编码

- 每个参数 = **32-bit IEEE-754 float**，占两个相邻 16-bit 寄存器。
- **字序（word order）= 大端：高寄存器在前。**
- **字节序（byte order）= 大端：寄存器内高字节在前。**
- 即整体就是标准大端 float（`>f`），4 字节顺序 = `[Reg1 Hi][Reg1 Lo][Reg2 Hi][Reg2 Lo]`。

**实例（来自 PDF）**

| 含义 | 原始 4 字节 (hex) | 解码值 |
| --- | --- | --- |
| Volts 1 | `43 66 33 34` | `230.2` V |
| Demand Time | `3F 80 00 00` | `1.0` |
| Network Node | `42 70 00 00` | `60.0` |

> Python 解码：`struct.unpack('>f', bytes([0x43,0x66,0x33,0x34]))[0]` → `230.2`。
> pymodbus 用 `BinaryPayloadDecoder.fromRegisters(regs, byteorder=Endian.BIG, wordorder=Endian.BIG)`。

## 4. 输入寄存器表（FC 04 读测量值）

全部为 `Float`，长度 4 字节，每项占 2 个寄存器。"Hex 起始"是 Modbus 协议起始地址（即 Start Address Hi/Lo）。

| 寄存器 | 参数 | 单位 | Hex 起始 |
| --- | --- | --- | --- |
| 30001 | Voltage（电压） | Volts | `0000` |
| 30007 | Current（电流） | Amps | `0006` |
| 30013 | Active power（有功功率） | Watts | `000C` |
| 30019 | Apparent power（视在功率） | VA | `0012` |
| 30025 | Reactive power（无功功率） | VAr | `0018` |
| 30031 | Power factor（功率因数） | — | `001E` |
| 30071 | Frequency（频率） | Hz | `0046` |
| 30073 | Import active energy（导入有功电能） | kWh | `0048` |
| 30075 | Export active energy（导出有功电能） | kWh | `004A` |
| 30077 | Import reactive energy（导入无功电能） | kvarh | `004C` |
| 30079 | Export reactive energy（导出无功电能） | kvarh | `004E` |
| 30085 | Total system power demand | W | `0054` |
| 30087 | Maximum total system power demand | W | `0056` |
| 30089 | Import system power demand | W | `0058` |
| 30091 | Maximum import system power demand | W | `005A` |
| 30093 | Export system power demand | W | `005C` |
| 30095 | Maximum export system power demand | W | `005E` |
| 30259 | Current demand | Amps | `0102` |
| 30265 | Maximum current demand | Amps | `0108` |
| 30343 | Total active energy（总有功电能） | kWh | `0156` |
| 30345 | Total reactive energy（总无功电能） | kvarh | `0158` |

**读取分块建议**：`0x0000–0x005E`（30001–30095）地址连续，可一次块读；`0x0102/0x0108`、`0x0156/0x0158` 各为独立小块。整表用 2–3 次块读即可覆盖，减少 Modbus 事务数。

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

电压 `0000`、电流 `0006`、有功功率 `000C`、功率因数 `001E`、频率 `0046`、导入有功电能 `0048`、导出有功电能 `004A`、总有功电能 `0156`。

## 5. 保持寄存器表（FC 03 读 / FC 16 写配置）

| 寄存器 | 参数 | Hex 起始 | 格式 | 说明 |
| --- | --- | --- | --- | --- |
| 40013 | Relay Pulse Width | `000C` | Float | 继电器脉宽 60/100/200 ms，默认 100ms |
| 40019 | Network Parity Stop | `0012` | Float | 0=1停止位无校验(默认)，1=1停止位偶校验，2=1停止位奇校验，3=2停止位无校验；**改后需重启生效** |
| 40021 | Meter ID | `0014` | Float | 从机地址 1–247，默认 1 |
| 40029 | Baud rate | `001C` | Float | 0=2400(默认)，1=4800，2=9600，5=1200 |
| 40087 | Pulse 1 output mode | `0056` | Float | 0001 导入有功，0002 导入+导出有功，0004 导出有功(默认)，0005 导入无功，0006 导入+导出无功，0008 导出无功 |
| 463745 | Time of scroll display | `F900` | HEX 2字节 | 滚动显示时间 0–30s，默认 0（不滚动） |
| 463761 | Pulse 1 output | `F910` | HEX 2字节 | 0000:0.001kWh/imp(默认)，0001:0.01，0002:0.1，0003:1 kWh/imp |
| 463777 | Measurement mode | `F920` | HEX 2字节 | 1:total=import，2:total=import+export(默认)，3:total=import-export |
| 464513 | Serial number | `FC00` | uint32 4字节 | 序列号，**只读** |
| 464515 | Meter code | `FC02` | Hex 2字节 | 设备码=`0020`，**只读** |
| 464516 | Software version | `FC03` | Hex 2字节 | 软件版本，**只读** |

> ⚠️ 写保持寄存器（改 Meter ID / 波特率 / 校验位等）会改变电表通信参数，配错可能导致**通信中断**。本项目默认**只读采集**，不建议在自动化链路里写电表配置。

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

1. 走 **Modbus TCP 网关**：`ModbusTcpClient(host, port)`，`slave=<Meter ID>`。
2. 测量值用 **FC 04 / 输入寄存器**，按 §4 地址读。
3. 解码 **大端 float32**（word & byte 都大端，高寄存器在前）。
4. 起始地址与数量**都用偶数**（成对读）。
5. 默认**只读**，不写电表配置寄存器。
6. 不同型号电表 → 不同"寄存器 profile"。本表是 `sdm120` 这一个 profile 的定义。
