printf()가 일으킨 제어 오차 문제
임베디드 시스템 개발을 하다 보면, 예상치 못한곳에서 발목을 잡히는 경우가 종종 있다.
SSAFY 실습코치 시절 한 팀의 라즈베리파이와 아두이노를 활용한 프로젝트를 진행하면서 겪은
어이없는 문제를 공유하기 위해 이 글을 작성한다.
1. 프로젝트 개요
해당 프로젝트는 라즈베리파이와 아두이노를 연동해서 X-Y 플로터를 제어하는것이 목적이였다.
프로젝트의 큰 틀은 다음과 같다.
- 웹 기반 사용자 인터페이스: 스마트폰에서 라즈베리파이로 제어 명령을 전송
- 라즈베리파이: 디스플레이 출력, 무게 센서 데이터 처리, 아두이노 제어 신호 송신
- 아두이노: 제어신호를 받아 스테핑 모터를 조작하여 X-Y 좌표 이동
시스템 동작 순서는 다음과 같다.
1
2
3
4
1. 스마트폰에서 명령 전송
2. 라즈베리파이: 명령 수신 → 디스플레이 출력
3. 아두이노로 제어 신호 송신 → 스텝모터 구동
4. 라즈베리파이: 무게 실시간 디스플레이 출력
각 모듈을 독립적으로 테스트했을 때는 완벽하게 작동했고, 이를 통합하면 끝이겠다 싶었다.
2. 문제 발생
모든 기능을 통합한 후, 이상한 문제들이 엄청나게 발생했다.
그 중 가장 치명적인 것은, X-Y 위치 제어에서 예측할 수 없는 오차가 발생한 것이였다.
첫 번째 의심 : 하드웨어 문제
제일 먼저 의심한것은 하드웨어 문제였다.
특히 하드웨어에서 빵판에 선을 잘못 연결한다던가, 각 모듈에 접촉 불량은 흔한 일이였기 때문에
회로를 다시 점검하고, 모든 모듈의 동작을 다시 재확인했으나 이상이 없었다.
두 번째 의심 : 통신 오류
두번째로 의심한 것은 각 모듈의 통신 과정의 오류였다.
데이터가 제대로 전달되지 않거나, 형변환에서 오류가 발생한것은 아닐까 싶어서
#ifdef DEBUG
를 넣어 printf()
를 통해서 과정 하나하나를 모두 출력해보았다
그러나 모듈 테스트 자체에서는 이상없었다.
며칠간 이 팀의 프로젝트 하드웨어 전체를 뜯었다 붙였다, 코드를 완전히 갈아 엎었다 복구했다를 반복했다.
3. 더 많은 로그 = 더 큰 오류
처음에는 단순한 좌표 오차였다.
- 목표: (100, 50) → 실제: (102, 48) “캘리브레이션 문제인가?” 하고 넘어갔지만, 반복할수록 오차가 랜덤하게 발생하기 시작했다.
- 두 번째: (98, 52), 세 번째: (105, 46), 네 번째: (94, 54)
이상해서 로그를 찍기 시작했다.
1
2
3
printf("Moving to X: %d, Y: %d\n", target_x, target_y);
// X-Y 모터 구동
printf("Current position: X=%d, Y=%d\n", current_x, current_y);
그런데 로그를 추가하자 오차가 더 심해졌다. ±5mm에서 ±15mm 이상으로 벌어졌다.
의심스러워 로그를 더 상세하게 추가했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void move_xy(int x, int y) {
printf("[DEBUG] move_xy called: target=(%d,%d)\n", x, y);
for(int step = 0; step < total_steps; step++) {
printf("[DEBUG] Step %d/%d executing...\n", step, total_steps);
// 스텝 모터 신호 전송
send_step_signal();
printf("[DEBUG] Step %d completed\n", step);
usleep(STEP_DELAY); // 100μs 대기
}
printf("[DEBUG] move_xy finished\n");
}
로그를 더 상세하게 추가할 수록, 오차가 더 커지기 시작했다
뭔가 이상한걸 깨닫고 실행 시간을 측정하기 시작했다
디버깅용 문구가 없을 때는 약 50ms였지만,
있을 때는 최악의 경우 약 500~1000ms 로 엄청난 차이가 발생했다
범인은 printf()
였다!
5. 근본 원인 - 오픈 루프 제어의 한계
라즈베리파이는 다음과 같은 작업을 동시에 수행하고 있었다:
- 스마트폰 요청 처리용 웹 서버
- 실시간 LCD 업데이트
- 무게 센서 데이터 측정
- 아두이노와 지속적인 시리얼 통신
- 그리고
printf()
를 통한 stdout 버퍼 처리
하지만 진짜 문제는 스텝모터에 있었다.
진짜 문제 : 오픈 루프 제어의 한계
해당 프로젝트에서 사용한 NEMA17 스텝 모터는 피드백 루프(엔코더)가 없는 오픈 루프 방식으로 동작한다
오픈 루프(Open Loop)는 출력이 다시 입력으로 피드백되지 않는 제어 시스템
일반적으로 스텝모터는 엔코더 없이 동작하며,
정확히 몇 스텝을 주면 정확히 그만큼 움직인다고 가정한다.
그러나 현실에선 항상 그렇지 않다.
- 스텝 신호 간 타이밍이 일정하지 않으면 스텝 손실 발생
- 중간에 딜레이가 길어지면 타이밍 왜곡 발생
- 실제 위치는 명령한 것과 점점 어긋남 (누적 오차)
즉, 명령한 만큼 정확히 회전한다고 가정하고 신호를 보내며,
중간에 타이밍이 어긋나거나 스텝 손실이 발생해도 이를 감지하거나 보정하지 않는다
printf()가 미친 영향
이 때문에 printf()
처럼 시간을 소비하거나 루프를 지연시키는 작업이 치명적인 영향을 미친것이다
이미 약간의 스텝 손실이 일어나고 있는 상황에서
디버깅을 위해 printf()를 루프 내부에 추가했을 때,
스텝 사이의 딜레이(usleep)보다 printf()
실행 시간이 더 길어져서
스텝 타이밍이 불규칙하게 되었고, 결국 스텝 손실이나 오차 누적으로 이어진 것이였다.
개별 모듈에서는 작은 시스템에서만 동작했기 때문에 눈에 띄지 않았지만,
통합 시스템에서는 라즈베리파이가 수행하는 일이 많았기 때문에 오차가 발생했고,
디버깅용 로그를 추가하면 할수록 이 오차는 크게 벌어진 것이였다
6. 해결 방법
당장은 프로젝트 마감이 임박했기 때문에 모든 루프 내부의 printf()를 제거하고
CPU 부하를 줄이는 방식으로 문제를 해결했다.
그러나 장기적으로는 타이밍 문제를 방지하고 근본적인 해결을 위해서는
다음과 같은 방법을 고려할 수 있다.
소프트웨어적 방법
- 비동기 로깅 시스템 구현
printf()
대신 메시지를 로그 큐에 저장하고,- 별도 스레드 또는 타이머에서 로그를 출력한다
- 실시간 루프의 타이밍 안정성을 유지 가능
1
2
3
4
5
6
7
8
9
10
11
12
// 실시간 루프 내부
enqueue_log("[DEBUG] step %d\n", step); // 버퍼에만 저장
// 백그라운드 처리
void log_worker() {
while (1) {
if (!log_buffer_empty()) {
char* msg = dequeue_log();
printf("%s", msg);
}
}
}
- 간헐적 리셋 or 초기화 루틴 적용
- 주기적으로 원점 복귀 명령을 내려 오차 누적 방지 (홈 위치 복귀)
- 대부분의 3D 프린터나 CNC 장비에서 사용하는 방식이라고 함
- 오차가 누적되기 전에 기준점을 재설정하여 시스템의 정밀도와 신뢰성을 유지
하드웨어적 방법
- 리미트 스위치 설치해 홈 포지션 감지
- 일정 시간 간격으로 홈 스위치로 이동하여 절대 위치 재설정
- 전원 복구 후 초기 위치 보정에도 유용함
- 로터리 엔코더 부착하여 피드백 루프 구성
- 스텝모터 축에 엔코더를 연결하여 실시간 위치 측정
- 엔코더 데이터를 통해 스텝 손실 감지 및 보정 가능
- 그러나 가격, 회로 복잡성 증가하기 때문에 , 이런 경우엔 폐루프 스텝모터(Closed-loop stepper motor)나, 서보모터를 사용하는 것이 더 나을 수도 있을것 같다
마무리
임베디드 시스템 개발에서는 소프트웨어와 하드웨어가 서로 영향을 주고받기 때문에,
어느 한쪽만 보고 판단하면 문제를 놓치기 쉽다.
각각은 멀쩡하게 동작하더라도, 통합 시 예상치 못한 타이밍 이슈나
병목, 리소스 경쟁으로 인해 심각한 문제가 발생할 수 있다.
이번 경험은 단순한 printf()
한 줄이 시스템 전체의 동작에
얼마나 큰 영향을 줄 수 있는지를 잘 보여주었고,
그만큼 타이밍과 제어 흐름에 민감한 임베디드 시스템의 특성을 다시 한번 상기시켜줬다.