← 전체 글
SECS/GEM/약 16분 읽기/ 조회

S6F11 timestamp는 host 시계가 아니다 — S2F17로 읽고 S2F31로 맞추기

설비 시계가 밀리면 link는 멀쩡한데 MES의 run 재구성이 어긋난다. S2F17로 읽고 S2F31로 맞추는 절차와 TIACK 0의 함정을 실제 capture로 본다.

SECS/GEMMESSCADA문제 해결체크리스트

Lot 하나를 재구성하는데 Process Start가 Process Complete보다 40초 늦게 찍혀 있다. Host log를 열어 보면 socket은 끊긴 적이 없고, Linktest도 전부 통과했다. HSMS 화면은 SELECTED, GEM은 COMMUNICATING. 어디에도 빨간 줄이 없다.

원인은 대개 시계다. S6F11 event report에 붙는 시각은 host가 수신한 시각이 아니라 설비가 찍은 시각인 경우가 많고, 설비 시계는 NTP를 안 받는 채로 몇 달씩 돈다. 하루 2초씩 밀리는 industrial PC를 반년 굴리면 6분이다. 그 6분이 report 순서를 뒤집는다.

Host 쪽에서 이걸 확인하는 통로는 두 개다. 읽는 쪽이 S2F17/S2F18, 맞추는 쪽이 S2F31/S2F32.

Host가 S2F17로 설비 시계를 읽고, S2F31로 시각을 보내고, S2F32에서 TIACK 0을 받은 뒤 다시 S2F17로 읽어도 시계는 그대로인 교환 Host설비 S2F17 S2F18 — 2026-09-07T00:05:42.176Z S2F31 — 2026090710150000 S2F32 — TIACK 0 S2F17 S2F18 — 2026-09-07T00:05:42.678Z 시계는그대로

시계가 밀려도 link는 초록색이다

이게 이 문제의 성질이다. HSMS는 시각을 검증하지 않는다. T3, T5, T6, T7, T8 중 어느 timer도 설비 시계를 보지 않는다 — timer는 전부 경과 시간으로 돌아가지, 절대 시각으로 돌지 않는다. 그래서 시계가 6분 밀린 설비도 SELECTED고 COMMUNICATING이다.

깨지는 건 저 아래, MES가 event를 시간 축에 늘어놓을 때다. 설비 A는 정확하고 설비 B는 6분 빠르면, 두 설비를 거친 wafer 하나의 이력이 순서가 뒤집힌 채로 저장된다. Report 설정 자체를 의심하기 시작하면 event report link 구조를 다 뒤지고도 아무것도 못 찾는다. 거기는 멀쩡하기 때문이다.

그래서 host 쪽 규칙은 단순하다. S1F13 직후에 S2F17을 한 번, 그리고 주기적으로 한 번씩. 나는 8시간 간격을 쓴다. 돌아온 값과 host 시계의 차이를 그냥 기록만 해도, 나중에 "이 tool은 그때 3분 빨랐다"는 한 줄이 생긴다.

실제로 오간 바이트

SECS/GEM 시뮬레이터의 HSMS listener를 상대로 socket 하나 열고 그대로 돌린 결과다. 합성이 아니라 socket에 실제로 오간 frame이다. 시뮬레이터는 우리가 만들어 공개해 둔 도구이고, 여기서는 설비 역할을 맡는다. 아래에서 걸리는 두 가지 — S2F18을 ISO 8601로 돌려주는 것, S2F32에 TIACK 0을 주면서 RTC는 안 건드리는 것 — 은 실제 설비에서도 그대로 나오는 동작이다. 여기서는 그게 wire 위에 찍혀 있을 뿐이다.

Select.req와 S1F13은 자리를 잡는 단계다. S1F13에 W-bit이 있어야 COMMUNICATING으로 넘어간다 — SELECTED와 COMMUNICATING은 다른 상태다.

TX Select.req      00 00 00 0A 00 0B 00 00 00 01 00 00 04 01
RX Select.rsp      00 00 00 0A 00 0B 00 00 00 02 00 00 04 01
TX S1F13 W         00 00 00 0C 00 0B 81 0D 00 00 00 00 04 02  01 00
RX S1F14           00 00 00 37 00 0B 01 0E 00 00 00 00 04 02  01 02 21 01 00 …

Header는 SessionID 00 0B(11), byte 2가 stream에 W-bit(81 = S1, W-bit 켜짐), byte 3이 function(0D = 13), 마지막 4바이트가 SystemBytes다. SEMI E37 header 배치 그대로다.

이제 시계를 읽는다.

TX S2F17 W         00 00 00 0A 00 0B 82 11 00 00 00 00 04 03
RX S2F18           00 00 00 24 00 0B 02 12 00 00 00 00 04 03
                   41 18 32 30 32 36 2D 30 39 2D 30 37 54 30 30 3A 30 35 3A 34 32 2E 31 37 36 5A

S2F17은 body가 없다. 길이가 00 00 00 0A, 즉 header 10바이트뿐이다. 82는 stream 2에 W-bit, 11은 function 17.

돌아온 body가 이 글의 핵심이다. 41은 SECS-II item format byte로 ASCII 형식에 길이 바이트 1개, 18은 길이 24다. 그 뒤 24바이트를 latin1로 읽으면 2026-09-07T00:05:42.176Z다.

24자다. 이 값은 잘 만들어진 ISO 8601 문자열이지만, SECS-II TIME item으로 host parser가 기대하는 길이가 아니다. 구현들이 실제로 받아 주는 건 YYMMDDhhmmss 12자와 YYYYMMDDhhmmsscc 16자 두 가지다 — 이 두 길이가 SEMI E5 표준 본문에 어떤 절로 적혀 있는지는 내가 직접 확인하지 않았다. unverified로 둔다. 확실한 건 이 capture 안의 24바이트고, 12나 16을 고정 폭으로 자르는 parser가 이걸 받으면 2026-09-07T0을 시각이라고 우긴다는 사실이다.

여기서 host 쪽이 해야 할 일: 길이를 먼저 보고 분기하라. 12도 16도 아니면 시각으로 해석하지 말고 그대로 log에 남긴 뒤 alarm을 올려라. 조용히 파싱해서 이상한 값을 MES에 넣는 것보다 낫다.

S2F32의 TIACK 0은 시계를 맞췄다는 뜻이 아니다

읽었으니 맞춰 본다. 16자 TIME을 실어 S2F31을 보냈다.

TX S2F31 W         00 00 00 1C 00 0B 82 1F 00 00 00 00 04 04
                   41 10 32 30 32 36 30 39 30 37 31 30 31 35 30 30 30 30
RX S2F32           00 00 00 0D 00 0B 02 20 00 00 00 00 04 04  21 01 00

41 10은 ASCII 16바이트, 내용은 2026090710150000. 설비가 방금 보고한 00:05보다 열 시간 넘게 앞선 10:15다. 답은 21 01 00 — binary item 1바이트, 값 0. TIACK 0, 받아들였다는 뜻이다.

그리고 바로 다시 읽었다.

TX S2F17 W         00 00 00 0A 00 0B 82 11 00 00 00 00 04 05
RX S2F18           00 00 00 24 00 0B 02 12 00 00 00 00 04 05
                   41 18 32 30 32 36 2D 30 39 2D 30 37 54 30 30 3A 30 35 3A 34 32 2E 36 37 38 5A

00:05:42.678Z. 첫 번째 읽기가 00:05:42.176Z였으니 502 ms 흘렀을 뿐이다. 10:15로 점프하지 않았다. TIACK 0을 받고도 시계는 그대로다.

이게 현장에서 제일 자주 밟는 지뢰다. 설비가 S2F31을 "문법적으로 접수했다"는 의미로 TIACK 0을 주고, 실제 RTC는 안 건드린다. Host log에는 "clock set OK"가 찍히고, 다음 주에 event 순서가 또 뒤집힌다.

그러니 S2F31 뒤에는 반드시 S2F17을 한 번 더 보내고 값을 비교하라. 이건 방어적 코딩이 아니라 유일한 검증 수단이다. TIACK은 설비의 주장이고, 두 번째 S2F18은 증거다.

한 가지 더 확인했다. 14자짜리 — 12도 16도 아닌 길이 — 를 실어 S2F31을 보내도 같은 답이 왔다.

TX S2F31 W         00 00 00 1A 00 0B 82 1F 00 00 00 00 04 06
                   41 0E 32 30 32 36 30 39 30 37 31 30 31 35 30 30
RX S2F32           00 00 00 0D 00 0B 02 20 00 00 00 00 04 06  21 01 00

41 0E = ASCII 14바이트. TIACK 0. 길이 검증을 아예 안 한다는 뜻이다. TIACK 0이 나왔다고 형식이 맞았다고 결론짓지 말 것.

형식이 깨지면 S2F32가 아예 안 온다

Item 자체를 망가뜨리면 얘기가 달라진다. 길이 바이트는 12를 선언하고 실제로는 2바이트만 실어 보냈다.

TX S2F31 W         00 00 00 0E 00 0B 82 1F 00 00 00 00 04 07  41 0C 32 30
RX S9F7            00 00 00 16 00 0B 09 07 00 00 00 00 03 EA
                   21 0A 00 0B 82 1F 00 00 00 00 04 07

S2F32가 아니라 S9F7 Illegal Data다. Body는 binary 10바이트(21 0A)이고, 그 10바이트는 문제가 된 message의 header 그대로 — 00 0B 82 1F 00 00 00 00 04 07. MHEAD다.

SystemBytes를 보면 요청은 00 00 04 07인데 S9F7은 00 00 03 EA를 달고 왔다. 이건 설비 결함이 아니라 정상 동작이다. S9Fx는 응답이 아니라 설비가 새로 여는 primary message라서 자기 SystemBytes를 쓴다. 원래 message를 가리키는 정보는 header가 아니라 body의 MHEAD에 들어간다 — stream 9 error message가 원래 그런 구조다.

그래서 host 입장에서는 S2F31 transaction이 응답을 못 받은 채로 남는다. T3는 그대로 다 돈다. S9F7을 따로 처리하지 않는 host의 log에는 "무응답" 한 줄만 남는다. 원인을 알려 주는 message는 이미 도착해 있었는데도.

받는 방법은 정해져 있다. stream 9는 SystemBytes 매칭과 별개의 경로로 받고, MHEAD 10바이트에서 stream/function/SystemBytes를 뽑아 대기 중인 transaction을 그 자리에서 실패시킨다. 그러면 T3가 만료될 때까지 기다릴 이유가 없다.

Timezone은 wire에 없다

S2F18에도 S2F31에도 timezone field가 없다. 12자든 16자든 zone 정보를 담을 자리 자체가 없다. 설비는 자기 local time을 보내고, host는 그게 무슨 zone인지 모른다.

이 capture의 값에는 Z가 붙어 있어 UTC임을 알 수 있지만, 그건 이 구현이 ISO 8601을 쓰기 때문이지 SECS-II가 zone을 실어 주기 때문이 아니다. 표준 형식으로 오는 순간 zone은 사라진다.

실무에서 나오는 결과:

  • Host는 UTC, 설비는 KST. 차이 9시간. 너무 커서 금방 잡힌다.
  • 둘 다 local이지만 DST를 쓰는 fab. 1년에 두 번, 한 시간짜리 구멍과 한 시간짜리 중복이 생긴다. 3월의 그 한 시간에 끝난 lot은 시작 시각이 종료 시각보다 뒤다.
  • 설비가 UTC로 보고하는데 host가 local로 해석. 매일 같은 offset이라 drift로 안 보이고, 그냥 "이 tool은 원래 그렇다"로 넘어간다.

Host 쪽 대응은 하나다. 설비별로 zone을 설정값으로 들고 있어라. Wire에 없는 정보를 추론하려 하지 말고 equipment table에 timezone: Asia/Seoul 한 줄을 두고 S2F18 값에 그걸 적용해라. DST가 있는 zone이면 IANA 이름을 쓰고 고정 offset을 쓰지 마라. +09:00은 한국에서는 맞지만 이 코드가 다른 fab으로 복사되는 순간 틀린다.

Host 쪽 체크리스트

  1. S1F13이 COMMACK 0으로 돌아온 직후 S2F17을 한 번 보낸다. 응답이 없거나 SxF0이면 그 설비는 clock sync 대상에서 제외하고 그 사실을 기록한다.
  2. S2F18 body의 길이를 먼저 본다. 12, 16, 그 외로 분기. 그 외는 파싱하지 말고 원문 그대로 남긴다.
  3. Host 시계와의 차이를 계산해서 설비별로 저장한다. 임계값은 상황에 따라 다르지만, wafer 단위 event를 쓰는 라인이면 2초를 넘어가면 볼 만하다.
  4. S2F31을 보낼 권한이 있는 설비만 보낸다. Recipe가 도는 중에 시계를 점프시키면 진행 중인 timer가 어떻게 되는지는 설비 벤더만 안다.
  5. S2F31 뒤에는 항상 S2F17을 다시 보내 실제로 바뀌었는지 확인한다. TIACK 0만 믿지 않는다.
  6. S9F7을 stream 9 handler에서 받아 MHEAD로 원 transaction을 실패시킨다. 안 그러면 형식 오류가 T3 timeout으로 위장한다.
  7. 설비가 S2F31을 무시하는 게 확인되면 host 쪽에서 offset을 들고 보정한다. 벤더 대응을 기다리는 동안 MES는 계속 돌아야 한다.

7번을 오래 끌지는 마라. Offset 보정은 drift가 선형일 때만 맞고, RTC가 밀리는 속도는 온도에 따라 변한다. 계절이 바뀌면 보정값도 틀린다.

직접 재현해 보려면 SECS/GEM 시뮬레이터의 HSMS listener에 socket을 붙이고 위 frame을 그대로 써 보면 된다. 위 hex는 전부 거기서 나온 것이다.