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

레시피는 S7F3부터 보내는 게 아니다 — S7F1이 먼저인 이유

PPBODY 300바이트가 와이어에서는 330바이트가 된다. S7F1/S7F2 승인 단계를 건너뛴 호스트가 무엇을 잃는지 실제 S7F1·S7F3 캡처로 본다.

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

레시피 업로드 코드를 처음 쓰면 대개 S7F3 한 방으로 끝낸다. PPID 넣고 PPBODY 넣고 보내면 되니까. 그런데 400KB짜리 바디를 전부 밀어 넣은 뒤에 S7F4가 거부로 돌아오면, 호스트는 그 400KB를 이미 다 쓴 상태다. 장비 저장소가 꽉 찼든, PPID가 이미 있든, 장비가 지금 그걸 받을 상태가 아니든 — 이유는 다 다르지만 비용은 같다. SEMI E5에 S7F1이 따로 있는 이유가 이거다. 바디를 보내기 전에 받아 줄지부터 묻는 단계다.

S7F3 한 건이 소켓에 나간 330바이트의 구성 — 길이 접두어 4, HSMS 헤더 10, L,2 2, PPID 항목 11, B 항목 헤더 3, 그리고 S7F1의 LENGTH가 센 PPBODY 300바이트 S7F3 — PPID ETCH_FAST, PPBODY 300 4 10 2 11 3 300 00 0001 46 00 0B 87 03 … 01 02 41 09 45 54 … 22 01 2C 3B 20 56 58 2D 39 30 30 30 … 길이접두어 HSMS 헤더 L,2 A[9] PPID B 항목 헤더 PPBODY S7F1의 LENGTH = 300 소켓에 나간 330 bytes

E5의 S7은 두 단계짜리 대화다

SEMI E5는 프로세스 프로그램 관리를 Stream 7에 모아 놨고, 업로드 경로는 primary 두 개로 나눠져 있다.

  • S7F1 Process Program Load Inquire — 바디는 L,2 { PPID, LENGTH }. "이 이름으로 이만큼 보내려는데 받겠나"를 묻는다.
  • S7F2 Process Program Grant — PPGNT 한 바이트로 답한다. 0이 승인이다. 0이 아닌 값들은 이미 갖고 있다, 공간이 없다, PPID가 유효하지 않다, 지금은 바쁘다 같은 사유를 구분하는데, 숫자와 사유의 대응은 E5의 PPGNT 표를 그대로 읽어야지 추측하면 안 된다.
  • S7F3 Process Program Send — L,2 { PPID, PPBODY }. 실제 바디를 싣는다.
  • S7F4 Process Program Acknowledge — ACKC7. 여기서도 0이 수락이다.

다운로드는 대칭이다. S7F5로 PPID를 요청하면 S7F6이 L,2 { PPID, PPBODY }를 돌려준다. 삭제는 S7F17/S7F18, 장비가 지금 갖고 있는 PPID 목록은 S7F19/S7F20이다.

호스트를 만드는 입장에서 중요한 건 S7F1이 선택이 아니라 협상이라는 점이다. LENGTH를 미리 알려 주면 장비는 버퍼를 잡거나, 공간이 모자라면 그 자리에서 거절할 수 있다. 이 단계를 건너뛰면 그 판단이 바디를 다 받은 뒤로 밀린다.

와이어에 올라간 S7F1

시뮬레이터의 passive 리스너(:5501)에 소켓을 직접 열고, Select를 끝낸 다음 S7F1을 보냈다. PPID는 ETCH_FAST, 보내려는 PPBODY는 300바이트다.

TX  Select.req    00 00 00 0A 00 0B 00 00 00 01 00 00 07 01
RX  Select.rsp    00 00 00 0A 00 0B 00 00 00 02 00 00 07 01

여기까지가 SELECTED. 이제 S7F1이다.

TX  S7F1  W=1     00 00 00 1D 00 0B 87 01 00 00 00 00 07 02
                  01 02
                  41 09 45 54 43 48 5F 46 41 53 54
                  B1 04 00 00 01 2C
    장비 디코드    stream 7, wbit true, function 1, ptype 0, stype 0, systemBytes 1794

헤더부터 읽으면 00 00 00 1D는 뒤따르는 29바이트, 즉 헤더 10 + 바디 19다. 00 0B가 SessionID 11. 87은 W-bit을 켠 Stream 7이고 — 최상위 비트가 W다(Header Byte 2) — 01이 Function 1이다.

바디 19바이트가 L,2 { PPID, LENGTH } 그대로다.

  • 01 02 — List, 원소 2개.
  • 41 09 + 45 54 43 48 5F 46 41 53 54 — A 항목, 길이 9, ETCH_FAST.
  • B1 04 + 00 00 01 2C — U4 항목, 길이 4, 값 300.

0x2C가 44, 0x01 2C가 300이다. 이 300이 바로 뒤에 보낼 PPBODY의 바이트 수다.

LENGTH가 세는 것과 소켓이 나르는 것은 다르다

같은 소켓으로 S7F3을 이어서 보냈다. 300바이트짜리 PPBODY를 실었다.

TX  S7F3  W=1     00 00 01 46 00 0B 87 03 00 00 00 00 07 03
                  01 02
                  41 09 45 54 43 48 5F 46 41 53 54
                  22 01 2C
                  3B 20 56 58 2D 39 30 30 30 20 72 65 63 69 70 65 …  (300 bytes)
    장비 디코드    stream 7, wbit true, function 3, ptype 0, stype 0, systemBytes 1795
    소켓 바이트    330

길이 접두어가 00 00 01 46 = 326이고, 여기에 접두어 자신의 4바이트를 더하면 소켓에 나간 330바이트가 된다. LENGTH로 알려 준 값은 300이었다. 차이 30바이트가 어디로 갔는지는 전부 셀 수 있다 — HSMS 헤더 10, L,2 2, PPID 항목 11(41 09 + 문자 9개), PPBODY의 항목 헤더 3.

그 항목 헤더 3바이트를 한 번 보고 갈 만하다. 22는 B(바이너리) 포맷에 길이 바이트 2개를 쓴다는 뜻이고, 이어지는 01 2C가 300이다. PPBODY가 255바이트를 넘는 순간 길이 바이트는 1개로 부족해진다(항목 헤더의 길이 바이트). 실무 레시피가 300바이트에서 끝나는 일은 없으니, 이 경로는 처음부터 2바이트나 3바이트 길이를 쓴다고 보면 된다.

그래서 S7F1의 LENGTH를 "이 트랜잭션이 네트워크에 만들 부하"로 읽으면 안 된다. 그건 PPBODY만 센 숫자다. 장비 쪽 버퍼 계산이 헤더까지 포함해야 하는 값이라면 그 차이만큼 어긋난다.

블록 분할을 걱정할 필요는 없다. SECS-I(E4)에서는 메시지가 블록으로 쪼개져 나가고 블록당 데이터가 244바이트로 제한되지만, HSMS(E37)에는 그런 분할이 없다. 4바이트 길이 접두어 하나가 메시지 전체를 감싼다. E4 시절 코드를 옮겨 오면서 244바이트 단위로 잘라 보내는 로직을 그대로 들고 오면, 장비는 그걸 잘린 메시지가 아니라 망가진 메시지로 본다.

PPID는 버전이 아니다

프로토콜이 주는 건 이름뿐이다. S7F20이 돌려주는 목록에도 PPID만 있다. 같은 ETCH_FAST가 지난주 것인지 오늘 것인지 구분할 필드가 E5에 없다.

현장에서 이게 어떻게 터지냐면, 호스트가 "이미 있으니 안 보낸다"고 판단하고 건너뛰는데 장비에 있는 건 두 번 전 버전인 경우다. 웨이퍼는 정상으로 처리되고 MES 기록도 깨끗하다. 그저 다른 레시피로 돌았을 뿐이다.

내가 쓰는 방법은 둘 중 하나다. PPID에 개정 번호를 넣어서 이름 자체를 유일하게 만들거나(ETCH_FAST_R7), 아니면 S7F5로 바디를 받아 와서 해시를 비교하는 것이다. 앞쪽이 싸고, 뒤쪽이 확실하다. 장비 저장소가 몇 개 안 들어가는 물건이면 앞쪽은 금방 한계에 부딪히니 그때는 뒤쪽으로 간다. 어느 쪽이든 "PPID가 같으면 같은 레시피"라는 가정만 코드에서 빼면 된다.

레시피 파라미터를 화면에서 직접 다루는 쪽 이야기는 레시피 파라미터 다운로드 검증에 따로 적어 뒀다.

이 캡처가 증명하지 않는 것

이 리스너는 데이터 메시지에 응답하지 않는다. SType 1(Select.req), 5(Linktest.req), 9(Separate.req)만 처리한다. 그래서 위 두 건에 S7F2도 S7F4도 오지 않았고, 그건 승인 거부가 아니라 미구현이다. S7F2의 PPGNT 값이나 S7F4의 ACKC7 값이 어떻게 나오는지는 이 캡처로 확인하지 못했다. 캡처가 증명하는 범위는 호스트가 내보낸 인코딩까지다 — 어느 바이트가 어디에 실리고, 수신 측 디코더가 그걸 stream 7 / function 1 / function 3으로 정확히 갈라 읽었다는 사실.

PPGNT와 ACKC7의 숫자별 의미는 E5 표를 보고 장비 업체 문서와 대조해야 한다. 두 문서가 어긋나는 경우가 실제로 있고, 그때 맞는 쪽은 장비다.

호스트 코드에서 볼 자리

  • S7F1을 먼저 보낸다. PPGNT가 0이 아니면 S7F3을 만들지 않는다. 이미 만든 바디를 들고 있더라도 소켓에는 올리지 않는다.
  • S7F1의 LENGTH는 PPBODY 바이트 수다. 항목 헤더나 HSMS 헤더를 더하지 않는다.
  • PPBODY 항목의 길이 바이트 개수를 크기에 따라 정한다. 255를 넘으면 1바이트로는 못 쓴다.
  • HSMS에서는 244바이트 블록 분할을 하지 않는다. E4 코드를 옮겨 왔다면 그 부분을 지운다.
  • PPGNT가 "지금 바쁘다"류일 때의 재시도 간격을 정해 둔다. 즉시 재시도는 같은 답을 다시 받는다.
  • PPID 동일성으로 업로드를 건너뛰지 않는다. 개정 번호를 이름에 넣거나 S7F5로 바디를 대조한다.
  • T3가 터진 S7F3을 그냥 재전송하지 않는다. 응답만 잃었을 수 있고, 그러면 장비는 같은 PPID를 두 번 받는다.

업로드 코드에서 제일 먼저 확인할 건 로그가 아니라 캡처다. 호스트 로그는 대개 S7F3 sent, 400KB라고만 쓰는데, S7F1을 보냈는지 안 보냈는지는 그 줄에 안 남는다.

위 캡처는 SECS/GEM 시뮬레이터의 passive 리스너를 상대로 만들었다. 소켓을 열어 Select를 끝내고 같은 바디를 보내면 같은 바이트가 나온다.