hello, robot

4축 로봇암(우노에 서보 네 개)에 키보드 텔레옵을 붙이는 날이었어요. 배선은 멀쩡한데 실기에서 문제가 여덟 번 났고, 그중 다섯이 시리얼 명령과 액추에이터를 다루는 쪽이었어요.

고치고 보니 하나하나가 임베디드 시리얼 통신에서 반복되는 전형적인 함정이었어요.

기체4축 로봇암 · SG90 서보 ×4 보드아두이노 우노 연결USB 시리얼 · 키보드 텔레옵 상태 저장EEPROM 1 KB · 셀당 약 10만 회
parseInt 타임아웃
1 s
지나면 0을 반환
AVR int 폭
16비트
atoi("40000")이 음수
부팅 펄스
90°
attach 직후 급이동
보간 속도
1°/15 ms
비율로 나눠 동시 도착
종점 오차
0
정수 나눗셈으로도

다섯 함정을 한눈에

1

텔레옵이 아예 작동 안 함

실제포트를 여는 단계에서 PermissionError 13으로 죽어 있었어요
원인COM 포트는 한 번에 한 프로세스만 여는 배타 자원. IDE 시리얼 모니터가 잡고 있었어요
2

목표각이 0으로 찍히며 관절이 0도로 꺾임

원인Serial.parseInt()는 기본 타임아웃 1초 안에 숫자가 안 오면 0을 반환해요
조치줄 단위 버퍼링 — 개행이 와야 해석하고, 숫자 자리에 다른 문자가 섞이면 명령을 통째로 무시
3

큰 숫자를 넣으면 또 0도

원인AVR의 int는 16비트라 atoi("40000")이 음수로 감기고, constrain을 거쳐 0이 돼요
조치32비트를 반환하는 atol로 교체. 버퍼 길이상 최대 6자리라 이 부류가 통째로 사라져요
4

포트를 열 때마다 팔이 90도로 튐

원인포트를 열면 우노가 자동 리셋되고 펌웨어가 attach 직후 90도 펄스를 보내요. SG90은 자기 위치를 밖으로 알려주지 않는 개루프라 첫 펄스 위치로 최대 속도로 이동해요
조치도착할 때마다 네 관절 각도를 EEPROM에 저장 → 부팅 시 그 각도로 attach → 기준 자세까지 보간 복귀
5

연속 명령·시퀀스 재생에서 동작이 씹힘

원인통신 유실이 아니라 상태 어긋남. 호스트는 관절 각도를 로컬 변수로만 추적하는데 우노가 리셋되거나 명령이 거부되면 실제와 갈려요
조치응답 기반 동기화 — 펌웨어가 이동을 마칠 때마다 실제 자세를 한 줄로 보고하고 호스트가 덮어써요

숫자가 늦게 오면 0이 돼요

관절을 고르는 글자만 먼저 도착하고 숫자가 늦으면, 명령이 반쯤 도착한 상태가 “0도로 가라”로 해석돼요. 문자 단위로 스트림을 해석하는 한 이런 부분 입력 상태는 피할 수 없어요.

부분 수신이 0도 명령이 되는 과정
타임아웃을 늘리는 건 답이 아니에요. 경계가 어디로 가든 부분 입력이라는 상태 자체는 남아요.
00.250.50.7511.25관절 문자가 도착한 뒤 경과 시간 (초)스트림'B' 도착'90' 도착parseInt숫자를 기다림 (기본 타임아웃 1초)0 을 반환서보0도로 급이동숫자가 1초 안에 안 오면 명령이 반쯤 도착한 상태가 "0도로 가라"가 돼요

수정은 줄 단위 버퍼링이에요. 개행이 와서 한 줄이 완성됐을 때만 해석하고, 숫자 자리에 숫자가 아닌 문자가 섞이면 그 명령을 통째로 무시해요. 절반만 온 명령을 어떻게든 해석해 주는 게 아니라, 완성 안 된 입력은 없는 셈 치는 쪽이 견고해요.

같은 계열의 버그가 코드 리뷰에서 하나 더 나왔어요. AVR의 int는 16비트라 atoi("40000")이 음수로 감기고, 그 값이 constrain을 거치면 역시 0도가 돼요. 32비트 long을 반환하는 atol로 바꾸면 버퍼 길이상 들어올 수 있는 최대 6자리 숫자로는 절대 안 넘쳐요.

개루프 서보는 부팅 순간이 위험해요

감속 없는 급점프는 기어에도 프레임에도 부담이에요. 해결은 마지막 자세를 EEPROM에 기억시키는 거였어요. EEPROM은 전원이 꺼져도 내용이 남는 저장 공간인데, 우노에 1킬로바이트가 있고 셀당 약 10만 회의 쓰기 수명이 있어요.

쓰기 수명이 유한하니 아무 때나 쓰면 안 되는데, EEPROM.update()는 값이 바뀐 바이트만 실제로 써서 수명을 아껴줘요.

개루프 액추에이터의 “지금 어디인가”를 하드웨어가 못 주니, 소프트웨어가 마지막으로 보낸 값을 대신 기억하는 구조예요.

관절 넷이 같이 출발해서 같이 도착하게

관절마다 같은 속도로 움직이면 적게 도는 관절이 먼저 멈춰서 동작이 삐걱거려요.

관절 공간 보간
가장 많이 도는 관절의 이동각을 총 스텝 수로 잡고, 나머지는 자기 이동량을 그 스텝 수로 나눠요.
같은 속도1도/15 ms — 적게 도는 관절이 먼저 서요Base 60°Shoulder 30°먼저 멈춤Elbow 45°먼저 멈춤Wrist 12°먼저 멈춤비율 보간가장 많이 도는 관절의 스텝 수로 나눠요Base 60°Shoulder 30°Elbow 45°Wrist 12°동시 도착

전 관절이 동시에 출발해서 동시에 도착해요. 산업 로봇 컨트롤러가 하는 관절 공간 보간의 가장 단순한 형태이고, 정수 나눗셈으로 계산해도 종점 오차는 0이에요.

진실의 원본을 어디에 둘까

연속 명령이나 시퀀스 재생에서 동작이 씹히는 증상은 통신 유실이 아니라 상태 어긋남이 원인이었어요. 호스트가 믿는 각도와 실제 각도가 달라지면, 그 상태에서 보내는 절대각 명령은 엉뚱한 기준에서 출발한 점프가 돼요.

수정은 응답 기반 동기화예요. 펌웨어가 이동을 마칠 때마다 Base=90 Shoulder=45 같은 형식으로 실제 자세를 한 줄로 응답하고, 호스트는 그걸 파싱해 자기 상태를 덮어써요.

진실의 원본어긋나는 경우회복
호스트의 기억우노 리셋 · 명령 거부없음 — 계속 어긋난 채
실제로 움직인 쪽의 보고같은 상황다음 응답 한 줄로 복구

재생 중에 누른 키가 재생이 끝난 뒤 뒤늦게 실행되는 유령 입력은 재생 종료 시 키 버퍼를 비워서 막았고, 물리적으로 움직이는 물건에는 중단 수단이 기본이어야 해서 ESC 즉시 중단도 넣었어요.

남는 기준

다섯 문제의 공통 교훈이 하나예요. 임베디드 통신의 견고함은 정상 입력을 잘 처리하는 데서가 아니라, 비정상 입력을 통째로 무시하는 데서 나와요. 형식이 어긋난 명령을 버려도 다음 명령이 곧 다시 오지만, 어긋난 명령을 어떻게든 해석해 버리면 그게 물리적 움직임이 되니까요. 버리는 쪽이 언제나 싸요.

진단 습관도 하나 남았어요. “안 움직인다”는 증상에서 바로 배선과 하드웨어를 의심하기 쉬운데, 포트를 누가 잡고 있는지 프로세스부터 보는 쪽이 훨씬 빨랐어요.

지난주 좌우 바퀴 편차 보정 — 같은 지령으로 직진하지 않는 이유가 모터 쪽 이야기였다면, 이번엔 모터에 도착하기 전 단계인 통신과 상태 관리 이야기예요.