삼성전자 P4 현장에 무인지게차(AFL, Automated Fork Lift) 세 대를 넣고 2년을 붙들었다. 기록할 것이 많아 다섯 편으로 나눈다. 1편은 2024년 2월, 개발자 세 사람이 한꺼번에 사표를 낸 시점부터 그해 7월 내가 지게차와 함께 현장에 들어가기까지의 이야기다.
12024년 2월, 세 사람이 한꺼번에 나갔다
이 프로젝트를 만든 개발자 세 명이 같은 시기에 퇴사를 신청했다. 삼성전자 P4 현장에 지게차 세 대를 반입하기 여섯 달 전이었다.
일정은 이미 박혀 있었다. 72시간 연속 테스트를 통과한 뒤 7월에 현장 반입. 그 일정을 그대로 둔 채 만든 사람들이 빠졌다. 인수인계 기간으로 잡힌 것은 약 한 달, 후임은 두 명이었다. 나와, 원래 지게차팀에 있던 사원 한 명.
다른 사람이 개발한 코드를 분석하기란 결코 쉬운 일이 아니다. 그것을 한 달 안에 끝내라는 일정이었다. 설계서는 없었다. 받은 것은 IAR 워크스페이스 하나와, 현장에서 값을 만지며 남긴 메모장 파일 하나였다.
2두 달, 읽기만 했다
2월부터 3월까지 약 두 달을 인수인계와 공부에 썼다. 타깃은 STM32F407VET6, Cortex-M4 단일 칩이다. 컴파일러는 IAR EWARM, RTOS는 없다. 파일 헤더마다 2017년부터 2020년까지의 날짜가 박혀 있었다. UserDriver 폴더에만 모듈이 서른 개 남짓, 소스와 헤더를 합쳐 2만 5천 줄가량이었다.
덩치 큰 파일부터 열었다. 주행 제어를 담은 LGV_Control.c가 4,248줄, 상태 관리인 LGV_State.c가 2,276줄, 반사판 항법을 처리하는 NAV350.c가 1,130줄. 한 파일이 4천 줄이라는 것은 함수 하나가 수백 줄이라는 뜻이고, 그 안에 지난 몇 년의 현장 대응이 층층이 쌓여 있다는 뜻이다.
반대로 main.c는 290줄뿐이었다. 메인 루프가 하는 일은 워치독을 물고 현재 좌표를 백업 레지스터에 쓰는 것이 전부다.
낯선 구조는 아니었다. 로보스타에 있을 때 DSP의 RTOS 구조에서 메인 루프 안에 코드가 한 줄도 없는 프로그램을 다뤄본 적이 있다. 메인이 비어 있다는 것은 실제 일이 다른 문맥에서 돈다는 뜻이다. 그래서 바로 인터럽트 쪽을 찾아갔다.
while(1)
{
IWDG_ReloadCounter();
pos = POS_GetEstPosition();
RTC_WriteBackupRegister(RTC_BKP_DR2, (s32)pos.x);
RTC_WriteBackupRegister(RTC_BKP_DR3, (s32)pos.y);
RTC_WriteBackupRegister(RTC_BKP_DR4, (s32)(pos.phi*RAD_TO_DEG10));
}
main.c — 메인 루프는 위치를 백업할 뿐, 제어는 하지 않는다
이 백업 레지스터의 뜻은 나중에 알았다. 전원이 나가도 마지막 좌표와 레이어를 기억해 두었다가 재부팅하면 그 자리에서 다시 시작하기 위한 장치다. 현장에서 지게차 전원을 내렸다 올려도 위치를 잃지 않는 이유가 이 네 줄에 있었다.
3진짜 심장은 SysTick 안에 있었다
1ms 주기 SysTick_Handler 안에 시분할 스케줄러가 통째로 들어 있었다. RTOS 없이 나머지 연산 하나로 태스크를 배분하는 구조다.
if((SysTickCount%100)==1) EXTI_GenerateSWInterrupt(SW_IRQ_STATE_UPDATE); if((SysTickCount%25) ==2) NAV_TransCheck(); if((SysTickCount%50) ==4) Gyro_SendCmdCheck(); if((SysTickCount%50) ==6) EXTI_GenerateSWInterrupt(SW_IRQ_POSITION); if((SysTickCount%50) ==8) EXTI_GenerateSWInterrupt(SW_IRQ_AGV_CONTROL); if((SysTickCount%50)==10) State_CanSendNavi();
나머지 값 1, 2, 4, 6, 8, 10 — 같은 주기 안에서 시점을 어긋내 충돌을 피한다
주기가 같아도 나머지 값을 하나씩 어긋나게 두어 한 틱에 두 가지 일이 겹치지 않게 만들어 놓았다. 뒤에 기능을 추가할 때마다 나는 이 빈 나머지 값을 찾아 끼워 넣어야 했다.
더 흥미로운 것은 EXTI_GenerateSWInterrupt였다. 소프트웨어 인터럽트를 일부러 발생시켜 우선순위가 다른 컨텍스트에서 무거운 연산을 돌린다. EXTI 라인 5번부터 9번까지가 각각 상태 갱신, 항법 수신 처리, 위치 추정, 주행 제어, 명령 해석을 맡고 있었다. RTOS의 태스크를 인터럽트 우선순위로 흉내 낸 셈이다.
★ 이런 구조에서는 타이밍 설계가 코드가 아니라 나머지 값 안에 숨는다. 왜 25인지, 왜 나머지가 6인지는 소스 어디에도 적혀 있지 않다. 문서가 없으면 만든 사람의 머릿속을 역산하는 수밖에 없다.
4하드웨어는 CAN 버스에 매달려 있었다
소프트웨어 구조가 잡히자 배선을 따라갔다. 이 모션컨트롤러 보드는 혼자 일하지 않는다. CAN 버스에 여러 보드가 물려 있고 각자 고유한 ID로 대화한다.
차량 상태를 올려 보내는 VIU 보드가 0x220, 포크의 리프트·틸트 센서를 담당하는 FIU 보드가 0x241, 조향을 맡은 EPS 보드가 별도 ID를 쓴다. 안전 스캐너와 디지털 입출력은 CREVIS I/O 링크가 CANopen 방식으로 0x181, 0x281, 0x601, 0x701을 오간다. 컨트롤러가 상위로 올려 보내는 상태 패킷은 0x260부터 0x271까지 열두 개였다.
위치 인식은 SICK NAV350 레이저 스캐너가 기둥에 붙은 반사판을 읽어 삼각측량하는 방식이다. 여기에 자이로와 휠 엔코더를 섞어 좌표를 추정한다. 별도 NX 보드를 붙여 SLAM으로 대체하는 경로도 Position.c 안에 준비되어 있었지만, 현장에서 쓰는 것은 반사판 방식이었다.
5상태 기계와 알람 16종
지게차의 동작은 상태 기계로 표현된다. 수동 모드가 0번대, 자동 모드가 10번대다. 정지(10), 준비(11), 정상 주행(12), 감속 정지(13), 일시정지(14), 이동 완료(15), 알람 정지(16), 안전 감속(17), 포킹(18), 충전(19), 그리고 비상정지(20).
장애가 나면 알람 번호가 뜬다. 정의된 알람은 16가지. 비상정지, 범퍼 접촉, 모션 에러, 맵 에러, 경로 이탈, 헤딩 초과, 항법 에러, 적재 실패⋯. 뒤에 현장에서 내가 가장 많이 보게 될 것이 9번 ALARM_NAV_ERROR와 8번 ALARM_LOCAL_TARGET_OVER다. 이때는 그저 번호였다.
헤더를 읽다가 11번, 12번, 13번 옆에 “정의만 되어 있음”이라고 적힌 주석을 봤다. 선언만 하고 구현하지 않은 것들이다. 이런 흔적이 코드 곳곳에 흩어져 있었다.
6호기마다 다른 빌드, 그리고 남겨진 메모
가장 당황스러웠던 것은 상수 헤더였다. 차량 고유 값이 전부 #ifdef로 갈려 있었다.
//#define SOOSUNG_NO1 //#define SOOSUNG_NO2 #define DOOSAN_NO1 //#define DOOSAN_CB #ifdef DOOSAN_NO1 #define STEER_WHEEL_LENGTH (1525) #define WHEEL_SPEED_SCALE_FACTOR (1.52) #define NAV_CENTER_POS_OFFSET_X (1172) #define NAV_CENTER_POS_OFFSET_THETA (6) // 2024.08.21 = -6 #endif
LGV_Constant.h — 호기를 바꾸려면 주석을 옮기고 다시 빌드해야 한다
1호기용 펌웨어와 2호기용 펌웨어가 다른 바이너리라는 뜻이다. 현장에서 급하게 올리다가 호기를 착각하면 지게차가 엉뚱한 곳으로 간다. 이 구조를 파라미터 방식으로 바꾸는 일은 한참 뒤에야 손댈 수 있었다.
프로젝트 폴더에는 메모장 파일도 하나 있었다. 만든 사람이 현장에서 값을 만지며 남긴 기록이다.
*수정한 파라미터 1. gDiffAng : 40 → 45로 변경 2. AFL_GetEpsCurAng : 150 → 50 변경 3. NAV_CENTER_POS_OFFSET_THETA : -6 → +6으로 변경 4. steer = 850 // 변경 시도했으나 결과가 더 안 좋아 850 유지
인수인계로 받은 유일한 문서 — 설계서가 아니라 현장 기록이었다
“결과가 더 안 좋아 유지”라는 문장을 한참 들여다봤다. 이 값들이 이론이 아니라 수십 번의 시도에서 나왔다는 뜻이다. 근거는 없고 결과만 있는 숫자. 함부로 건드릴 수 없는 숫자다. 그리고 앞으로 내가 만들 숫자도 결국 같은 종류일 것이라는 예감이 들었다.
7그리고 나는 프로젝트에서 빠졌다
두 달을 읽고 나니 지도가 대강 그려졌다. 그 무렵 나는 이 프로젝트에서 제외되었다.
기술적인 이유는 아니었다. 조직 안의 문제였고, 여기에 길게 적을 일은 아니라고 본다. 다만 결과만 적자면, 두 달간 만든 지도를 덮고 다른 일로 갔다. 남은 후임은 한 명이 되었다.
87월, 대타로 다시 불려 갔다
2024년 7월, 반입을 코앞에 두고 남아 있던 그 사원마저 퇴사를 신청했다. 만든 사람도, 넘겨받은 사람도 모두 빠진 자리에 내가 대타로 들어갔다.
| 2024.02 | 개발자 3명 퇴사 신청 (반입 6개월 전). 인수인계 1개월, 후임 2명 |
|---|---|
| 2024.02~03 | 인수인계와 코드 분석 약 2개월 |
| 2024 봄 | 프로젝트에서 제외됨 |
| 2024.07 | 남은 후임 1명도 퇴사 신청 → 대타 투입, 지게차 3대와 함께 P4 반입 |
| 2024 연말 | 코드를 어느 정도 파악. 안정화 시작 |
1편이 다루는 구간 — 2024년 2월부터 7월까지
7월에 지게차 세 대와 내가 삼성전자 현장에 들어갔다. 문제는 엄청나게 많았다. 그리고 나는 소스코드를 100퍼센트 파악한 상태가 아니었다. 두 달 읽고 넉 달 손을 뗀 코드였다.
현장에서는 원인을 묻는다. 언제 고쳐지느냐고 묻는다. 나는 그 질문에 답할 수 있는 상태가 아니었다. 아는 것과 모르는 것의 경계가 어디인지조차 흐릿한 채로, 매일 알람 번호를 들여다봤다. 그때 받은 압박과 스트레스는 지금도 또렷하다.
돌아보면 안정화가 시작된 것은 2024년 연말부터다. 코드가 좋아져서가 아니라, 내가 코드를 어느 정도 알게 된 시점이 거기였기 때문이다. 결국 이 프로젝트에서 가장 오래 걸린 일은 고치는 일이 아니라 읽는 일이었다.
다음 편에서는 2024년 7월의 현장을 적는다. 반사판을 놓쳐 좌표를 잃는 문제, 랙 앞에서 포크가 빗나가는 문제, 그리고 로그 한 줄 없이 원인을 찾아야 했던 시간에 대해.

답글 남기기