Honeywell HC900을 Modbus TCP로 직접 폴링 → gRPC → C# 크롤러 → PostgreSQL. 기존 Experion OPC UA 데이터 경로를 HC900 직접 통신으로 대체. - industrial-comm/cpp: C++ Modbus 게이트웨이 (gRPC 서버) - src: C# .NET 8 ASP.NET Core 크롤러 + 웹 UI (3-Layer) - mcp-server: Python FastMCP (RAG/NL2SQL/P&ID) - 다중 컨트롤러(N-Controller) 지원 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
12 KiB
Experion Indexed Address → HC900 Fixed Modbus Memory Map 변환 공식
대상: HC900-C70 (C4 컨트롤러), Experion HS R530 OPC 서버
출처: C4-All-Modbus-Map.csv, Sinam_Tag_all.xlsx (SourceAddressPV/OP/MD 컬럼), HC900 Communications Manual Rev.13
1. 배경
Experion OPC 서버는 HC900 Modbus 레지스터를 직접 주소 대신 Indexed Address 형식으로 참조한다.
Sinam_Tag_all.xlsx의 SourceAddressPV / SourceAddressOP / SourceAddressMD 컬럼에 이 형식이 기록된다.
예시:
C4 LOOP 1 LOOPSTAT → C4 컨트롤러, PID Loop #1, Loop Status Register
C4 TAG 32 VALUE → C4 컨트롤러, Signal Tag #32, 값
C4 MATH_VAR 5 VALUE → C4 컨트롤러, Variable #5, 값
C4 LOOPX 27 LOOPSTAT → C4 컨트롤러, 확장 Loop #27 (25-32), Loop Status Register
2. Indexed Address 형식
{CTRL} {PARTITION} {N} {PARAM}
CTRL : 컨트롤러 식별자 (C1, C2, C3, C4)
PARTITION : LOOP | LOOPX | TAG | MATH_VAR
N : 1-based 인덱스 번호
PARAM : 파라미터명 (하단 §5 참조)
3. HC900 Fixed Map 파티션별 Modbus 주소 공식
3-1. PID Loop (1-24번) — LOOP
base_addr(N) = 0x0040 + (N - 1) × 0x0100 (N = 1 ~ 24)
Modbus_addr = base_addr(N) + param_offset
| Loop# | Base Addr | HC900 태그명 (C4) |
|---|---|---|
| 1 | 0x0040 | FIC-9101 |
| 2 | 0x0140 | FICQ-9114 |
| 3 | 0x0240 | FIC-9113 |
| 4 | 0x0340 | FIC-9118 |
| 5 | 0x0440 | FIC-9116 |
| 6 | 0x0540 | TIC-9111A |
| 7 | 0x0640 | PIC-9111A |
| 8 | 0x0740 | FIC-9201 |
| 9 | 0x0840 | FIC-9213 |
| 10 | 0x0940 | FIC-9214 |
| 11 | 0x0A40 | FIC-9218 |
| 12 | 0x0B40 | FIC-9216 |
| 13 | 0x0C40 | TIC-9211 |
| 14 | 0x0D40 | PIC-9211B |
| 15 | 0x0E40 | LIC-9113 |
| 16 | 0x0F40 | LIC-9213 |
| 17 | 0x1040 | FICQ-10101 |
| 18 | 0x1140 | FICQ-10113 |
| 19 | 0x1240 | FICQ-10114A |
| 20 | 0x1340 | FIC-10118 |
| 21 | 0x1440 | LICA-10113 |
| 22 | 0x1540 | FICQ-10116 |
| 23 | 0x1640 | TIC-10111A |
| 24 | 0x1740 | PIC-10111A |
3-2. 확장 PID Loop (25-32번) — LOOPX
base_addr(N) = 0x7840 + (N - 25) × 0x0100 (N = 25 ~ 32)
Modbus_addr = base_addr(N) + param_offset
| Loop# | Base Addr | HC900 태그명 (C4) |
|---|---|---|
| 25 | 0x7840 | FICQ-10201 |
| 26 | 0x7940 | FICQ-10213 |
| 27 | 0x7A40 | FICQ-10214 |
| 28 | 0x7B40 | FICQ-10218 |
| 29 | 0x7C40 | LIC-10213 |
| 30 | 0x7D40 | FIC-10216 |
| 31 | 0x7E40 | TIC-10211 |
| 32 | 0x7F40 | PIC-10211A |
3-3. Signal Tag (1-1000번) — TAG
Modbus_addr(N) = 0x2000 + (N - 1) × 2 (N = 1 ~ 1000)
데이터 타입: float 32 (2 레지스터), Read-only
Signal Tag 번호와 HC900 태그명의 매핑은 HC Designer "Tag Information" 리포트 또는
C4-All-Modbus-Map.csv>Signal Tags 1-1000파티션 참조.
3-4. Variable / Math Variable (1-600번) — MATH_VAR
Modbus_addr(N) = 0x18C0 + (N - 1) × 2 (N = 1 ~ 600)
데이터 타입: float 32 (2 레지스터), Read/Write
4. 주소 영역 요약
| Partition | Experion 키워드 | 시작 주소 | 끝 주소 | 공식 | 개수 |
|---|---|---|---|---|---|
| System Parameters | — | 0x0000 | 0x003F | — | — |
| PID Loop 1-24 | LOOP |
0x0040 | 0x17FF | 0x0040+(N-1)×0x100 | 24 loops |
| PID Loop 25-32 | LOOPX |
0x7840 | 0x7FFF | 0x7840+(N-25)×0x100 | 8 loops |
| Variable 1-600 | MATH_VAR |
0x18C0 | 0x1D6F | 0x18C0+(N-1)×2 | 600 tags |
| Signal Tag 1-1000 | TAG |
0x2000 | 0x27CF | 0x2000+(N-1)×2 | 1000 tags |
| Signal Tag 1-4000 | — | 0x3B60 | 0x5A9F | 0x3B60+(N-1)×2 | 4000 tags |
5. Loop 파라미터 오프셋 테이블
C4-All-Modbus-Map.csv Loop 1 (FIC-9101, base=0x0040) 기준 실측값.
모든 루프에 동일하게 적용된다.
5-1. Float 파라미터 (float 32 = 2 레지스터)
| Experion 파라미터명 | HC900 태그 접미사 | Offset (Hex) | Offset (Dec) | Access | 설명 |
|---|---|---|---|---|---|
PV |
.PV |
+0x0000 | +0 | R | 프로세스 변수 |
RSP |
.RSP_SP2 |
+0x0002 | +2 | R/W | Remote SP (SP2) |
WSP |
.WSP |
+0x0004 | +4 | R/W | Working Set Point |
OP |
.Output |
+0x0006 | +6 | R/W | Output |
| — | .PV__B_ |
+0x0008 | +8 | R | PV (B bank) |
| — | .Gain_1_Prop_Band_1 |
+0x000C | +12 | R/W | Gain 1 / Prop Band 1 |
| — | .Direction |
+0x000E | +14 | R | Direction (0=Direct, 1=Reverse) |
RESET1 |
.Reset_1 |
+0x0010 | +16 | R/W | Reset 1 (Integral) |
RATE1 |
.Rate_1 |
+0x0012 | +18 | R/W | Rate 1 (Derivative) |
| — | .Scan_Cycle_Time |
+0x0014 | +20 | R | Scan Cycle Time |
| — | .PV_low_range |
+0x0016 | +22 | R | PV Low Range |
| — | .PV_high_range |
+0x0018 | +24 | R | PV High Range |
| — | .Alarm_1_SP1 |
+0x001A | +26 | R/W | Alarm 1 SP1 |
| — | .Alarm_1_SP2 |
+0x001C | +28 | R/W | Alarm 1 SP2 |
| — | .Gain2_Prop_Band_2 |
+0x0020 | +32 | R/W | Gain 2 / Prop Band 2 |
| — | .Reset_2 |
+0x0024 | +36 | R/W | Reset 2 |
| — | .Rate_2 |
+0x0026 | +38 | R/W | Rate 2 |
LSP1 |
.LSP1 |
+0x002A | +42 | R/W | Local SP 1 |
LSP2 |
.LSP2 |
+0x002C | +44 | R/W | Local SP 2 |
| — | .Alarm_2_SP1 |
+0x002E | +46 | R/W | Alarm 2 SP1 |
| — | .Alarm_2_SP2 |
+0x0030 | +48 | R/W | Alarm 2 SP2 |
| — | .SP_low_limit |
+0x0034 | +52 | R/W | SP Low Limit |
| — | .SP_high_limit |
+0x0036 | +54 | R/W | SP High Limit |
| — | .Output_Low_Limit |
+0x003A | +58 | R/W | Output Low Limit |
| — | .Output_High_Limit |
+0x003C | +60 | R/W | Output High Limit |
| — | .Ratio |
+0x0046 | +70 | R/W | Ratio |
| — | .Bias |
+0x0048 | +72 | R/W | Bias |
| — | .Deviation |
+0x004A | +74 | R | Deviation (SP-PV) |
| — | .Manual_Reset |
+0x004E | +78 | R/W | Manual Reset |
| — | .Feed_forward_Gain |
+0x0050 | +80 | R/W | Feedforward Gain |
5-2. Integer 파라미터 (unsigned 16 = 1 레지스터)
| Experion 파라미터명 | HC900 태그 접미사 | Offset (Hex) | Offset (Dec) | Access | 설명 |
|---|---|---|---|---|---|
| — | .Enable_Disable_Fuzzy |
+0x00B7 | +183 | R/W | Fuzzy Enable (0/1) |
| — | .Demand_Tune_Req |
+0x00B8 | +184 | R/W | Autotune Request |
AMSTAT |
.Auto_Man_State |
+0x00BA | +186 | R/W | 0=Manual, 1=Auto |
| — | .LSP_Select_State |
+0x00BB | +187 | R/W | SP 선택 (0=SP1, 1=SP2) |
| — | .Rem_Loc_SP_State |
+0x00BC | +188 | R/W | Remote/Local SP |
| — | .Tune_Set_State |
+0x00BD | +189 | R/W | Tune Set |
LOOPSTAT |
.Loop_Status_Register |
+0x00BE | +190 | R | Loop 상태 (bit-packed) |
Loop 블록 크기: 루프당 0x00C0 (192) 레지스터 할당 (0x0040~0x00FF)
float 32 = 2 레지스터 사용 (even address), unsigned 16 = 1 레지스터
6. 주소 계산 예시
예시 1: FICQ-9101 (AnalogPoint) — C4 LOOP 1 LOOPSTAT
LOOP 번호: 1
base_addr = 0x0040 + (1-1) × 0x0100 = 0x0040
LOOPSTAT 오프셋 = +0x00BE
Modbus_addr = 0x0040 + 0x00BE = 0x00FE (= 255 dec)
HC900 태그: FIC-9101.Loop_Status_Register @ 0x00FE
예시 2: FICQ-9101 — C4 LOOP 1 AMSTAT (Auto/Manual State)
base_addr = 0x0040
AMSTAT 오프셋 = +0x00BA
Modbus_addr = 0x0040 + 0x00BA = 0x00FA (= 250 dec)
HC900 태그: FIC-9101.Auto_Man_State @ 0x00FA
예시 3: FICQ-10214 (AnalogPoint) — C4 LOOPX 27 LOOPSTAT
LOOPX 번호: 27 (25번부터 시작)
base_addr = 0x7840 + (27-25) × 0x0100 = 0x7840 + 0x0200 = 0x7A40
LOOPSTAT 오프셋 = +0x00BE
Modbus_addr = 0x7A40 + 0x00BE = 0x7AFE
HC900 태그: FICQ-10214.Loop_Status_Register @ 0x7AFE
예시 4: P-9114 (StatusPoint) — C4 TAG 32 VALUE
TAG 번호: 32
Modbus_addr = 0x2000 + (32-1) × 2 = 0x2000 + 62 = 0x203E (= 8254 dec)
HC900 태그: P_9114 @ 0x203E (C4-All-Modbus-Map.csv Signal Tag #32)
예시 5: XV-9101 (StatusPoint) — C4 MATH_VAR 1 VALUE (OP source)
MATH_VAR 번호: 1
Modbus_addr = 0x18C0 + (1-1) × 2 = 0x18C0
HC900 태그: XV9101AUTO @ 0x18C0 (C4-All-Modbus-Map.csv Variable #1)
7. 검증 결과 요약 (C4 컨트롤러)
Sinam_Tag_all.xlsx × C4-All-Modbus-Map.csv 교차 검증 결과:
| 패턴 | 매핑 건수 | 검증 상태 |
|---|---|---|
C4 LOOP N * (1-24) |
24 | ✓ 전수 일치 |
C4 LOOPX N * (25-32) |
8 | ✓ 전수 일치 |
C4 TAG N VALUE |
~120 | ✓ 전수 일치 |
C4 MATH_VAR N VALUE |
~55 | ✓ 전수 일치 |
| 합계 | 207 | ✓ |
8. Python 공식 구현
def experion_indexed_to_modbus(indexed_addr: str, loop_tag_map: dict) -> dict | None:
"""
Experion Indexed Address → HC900 Modbus 주소 변환.
loop_tag_map: {loop_num: hc900_base_tag}
예: {1: 'FIC-9101', 2: 'FICQ-9114', ...}
반환: {'hc900_tag': str, 'modbus_addr': int, 'dtype': str}
"""
import re
# 파라미터 → (offset, dtype)
PARAM_OFFSET = {
'PV': (0x0000, 'float32'),
'RSP': (0x0002, 'float32'),
'WSP': (0x0004, 'float32'),
'OP': (0x0006, 'float32'),
'RESET1': (0x0010, 'float32'),
'RATE1': (0x0012, 'float32'),
'LSP1': (0x002A, 'float32'),
'LSP2': (0x002C, 'float32'),
'AMSTAT': (0x00BA, 'uint16'),
'LOOPSTAT': (0x00BE, 'uint16'),
'VALUE': (0x0000, 'float32'),
}
# LOOP / LOOPX
m = re.match(r'\w+ (LOOP|LOOPX) (\d+) (\w+)', indexed_addr)
if m:
loop_type, n, param = m.group(1), int(m.group(2)), m.group(3)
base = (0x0040 + (n-1)*0x0100) if loop_type == 'LOOP' \
else (0x7840 + (n-25)*0x0100)
offset, dtype = PARAM_OFFSET.get(param, (None, None))
if offset is None:
return None
hc900_base = loop_tag_map.get(n, f'LOOP{n}')
suffix = {
'PV':'PV','RSP':'RSP_SP2','WSP':'WSP','OP':'Output',
'RESET1':'Reset_1','RATE1':'Rate_1','LSP1':'LSP1','LSP2':'LSP2',
'AMSTAT':'Auto_Man_State','LOOPSTAT':'Loop_Status_Register',
}.get(param, param)
return {'hc900_tag': f'{hc900_base}.{suffix}',
'modbus_addr': base + offset, 'dtype': dtype}
# TAG (Signal Tag)
m = re.match(r'\w+ TAG (\d+) VALUE', indexed_addr)
if m:
n = int(m.group(1))
return {'hc900_tag': f'SIG_TAG_{n}', # 실제 태그명은 CSV 조회 필요
'modbus_addr': 0x2000 + (n-1)*2, 'dtype': 'float32'}
# MATH_VAR (Variable)
m = re.match(r'\w+ MATH_VAR (\d+) VALUE', indexed_addr)
if m:
n = int(m.group(1))
return {'hc900_tag': f'VAR_{n}', # 실제 태그명은 CSV 조회 필요
'modbus_addr': 0x18C0 + (n-1)*2, 'dtype': 'float32'}
return None
# 사용 예
loop_tag_map = {
1: 'FIC-9101', 2: 'FICQ-9114', 3: 'FIC-9113',
# ... (C4-All-Modbus-Map.csv에서 생성)
}
result = experion_indexed_to_modbus('C4 LOOP 1 LOOPSTAT', loop_tag_map)
# → {'hc900_tag': 'FIC-9101.Loop_Status_Register', 'modbus_addr': 0x00FE, 'dtype': 'uint16'}
result = experion_indexed_to_modbus('C4 TAG 32 VALUE', loop_tag_map)
# → {'hc900_tag': 'SIG_TAG_32', 'modbus_addr': 0x203E, 'dtype': 'float32'}
9. 주의사항
-
이 공식은 Fixed Map 기준: Custom Map을 사용하는 컨트롤러에서는 주소가 다를 수 있다.
C4-All-Modbus-Map.csv("Modbus All Partitions Report")가 실제 매핑의 최종 정보원. -
컨트롤러별 루프 배치가 다름: C1~C4 각 컨트롤러마다 Loop #1에 배치된 PID 블록이 다르다.
이 문서의 "Loop 번호 → HC900 태그" 테이블은 C4 컨트롤러 전용. -
Signal Tag 실제 태그명:
TAG N VALUE→ Modbus 주소는 공식으로 계산하지만,
HC900 태그명 (예:P_9114)은 반드시C4-All-Modbus-Map.csv의 Signal Tags 파티션을 조회해야 한다. -
Float 바이트 오더: HC900 기본값은 FP B (Big Endian), 4바이트 순서 = High word first.
Modbus 레지스터 2개 읽은 뒤struct.pack('>HH', r[0], r[1])로 디코딩. -
MATH_VAR = Variables: Experion 용어 "MATH_VAR"은 HC900 "Variable" (0x18C0~)을 가리킨다.
R/W 가능 (Write: FC06 단일 또는 FC16 다중 레지스터).