제8부 실시간 시스템 응용
8.3 MPU-MCU 통신 지연 분석 — Bridge의 실측
8.1~8.2에서 MCU 자체는 결정론적으로 설계할 수 있음을 확인했다. 하지만 실전 애플리케이션은 MCU 혼자 완결되지 않는다 — 설정값은 MPU(App Lab, Python)에서 MCU로 전달되고, 상태는 MCU에서 MPU로 전달된다. 이 경로가 바로 Bridge(App Lab의 시각적 설정 레이어는 "Visual Bridge"라 부른다, 3.4)다. 이 장은 이 통신 경로에 얼마나 지연·지터가 있는지, 왜 이 책이 그 수치를 단정하지 않는지, 그리고 직접 측정하는 방법을 다룬다.
8.3.1 Bridge/Visual Bridge 구조 되짚기
3.4에서 다루는 내부 구조를 실시간성 관점에서 요약하면 다음과 같다.
- MPU와 MCU는 내부 UART(및 SPI) 버스로 물리적으로 연결되어 있다. Zephyr 보드 문서에 "Internal UART and SPI buses connected to the QRB2210"이 명시되어 있다 — 사용자가 이 링크를 직접 열 필요는 없고, 라이브러리가 대신 관리한다.
- MPU 쪽에는
arduino-router라는 라우터 서비스가 실행된다. RPC(Remote Procedure Call)를 MessagePack으로 직렬화해 주고받는 스타(star) 토폴로지 라우터 — 여러 클라이언트(스케치, Python 앱, 웹 UI 등)가 라우터에 등록하고, 라우터가 요청을 해당 서비스로 중계한다. - MCU 쪽에서는
Arduino_RouterBridge라이브러리가 이 RPC 클라이언트 역할을 한다. - Visual Bridge는 App Lab에서 이 RPC 연결을 코드 없이(또는 최소한의 코드로) 시각적으로 배선하는 상위 레이어다. 즉 "Visual Bridge의 지연"을 물을 때 실제로 재는 대상은 그 아래 있는 RPC(MessagePack) over 내부 UART 경로다.
8.3.2 지연이 쌓이는 지점 — 계층별 분해
한 번의 "MCU → MPU" 왕복(round trip)에는 최소한 다음 단계가 순서대로 포함된다.
각 단계가 지연에 기여하는 방식이 다르다.
- 1·5(직렬화·역직렬화): MessagePack은 경량 이진 포맷이라 이 자체의 CPU 비용은 작지만 0은 아니다. MCU 쪽은 결정론적이므로 이 비용은 비교적 일정하다.
- 2(UART 전송): 물리 계층 전송 시간은 보율과 페이로드 크기로 계산 가능하다(아래 8.3.4 참고) — 이 부분은 원리상 예측 가능하다.
- 3·4(MPU 쪽 처리): 여기가 지터의 핵심 후보다.
arduino-router가 Linux 프로세스로서 범용 스케줄러 위에서 실행된다(8.1.3) — 다른 프로세스가 CPU를 쓰고 있으면 라우터가 즉시 깨어나 처리한다는 보장이 없다. 대상 클라이언트(Python 앱 등)도 마찬가지로 스케줄링 대기를 거친다.
즉 MCU→MPU 방향의 물리적 전송(2번)은 결정론적으로 계산할 수 있지만, MPU 쪽 처리(3·4번)는 Linux 스케줄링 지터의 영향을 그대로 받는다. 이것이 이 장의 핵심 결론이자, 다음 절에서 "공식 수치를 단정하지 않는" 이유다.
8.3.3 왜 이 책은 구체적 지연 수치를 단정하지 않는가
정직하게 밝혀야 할 것: 이 책을 쓰는 시점 기준으로,
- Arduino 공식 문서·데이터시트에 Bridge/Visual Bridge의 왕복 지연·지터 공식 수치는 확인되지 않는다.
- 내부 UART의 보율(baud rate) 자체는
arduino-router의 systemd 서비스 설정(--serial-baudrate 115200)에서 확인된다(3.4.3) — 다만 이는 배포된 소프트웨어 설정값을 읽은 것이지 공식 스펙 문서에 못박힌 수치는 아니다. 프레이밍 오버헤드, 라우터의 큐잉 정책에 대한 공식 스펙은 이 책 집필 시점 검색으로는 명확히 확인되지 않는다. - 커뮤니티(포럼·블로그)에 개별 사용자의 사용 후기는 있을 수 있지만, 재현 가능한 측정 방법론과 함께 제시된 신뢰할 수 있는 벤치마크는 확인되지 않는다.
따라서 이 책에서 "Bridge 왕복 지연은 몇 ms다"라고 단정하는 것은 추측을 사실처럼 제시하는 것이며, BOOK.md(BK-04)의 정직성 원칙에 어긋난다. 이 절은 명시적으로 "검증 필요"로 남긴다. 대신 아래 8.3.4에서 독자가 자신의 보드로 직접 재현 가능한 측정 방법을 제시한다 — 이 책의 다음 개정에서 실측치로 채울 수 있는 자리다.
한 가지는 원리로부터 확신을 갖고 말할 수 있다: Bridge를 경유하는 왕복은 MCU 로컬 인터럽트 처리(8.2)보다 최소 한두 자릿수 이상 느리고, MPU 스케줄링 지터의 영향을 받는 만큼 지터도 크다. 그래서 hard real-time 루프(8.4의 PID, 8.5의 setpoint 스트리밍)는 Bridge 응답을 기다리지 않는 방식으로 설계해야 한다는 결론은 수치 없이도 성립한다.
8.3.4 측정 방법론 — 직접 측정하기
공식 수치가 없다면 독자가 직접 측정할 수 있어야 한다. 다음은 MCU에서 타임스탬프를 기록하고 MPU가 그대로 되돌려 보내게 해서 왕복 시간(round-trip time, RTT)을 재는 절차다.
8.3.4.1 설계
- MCU가
micros()로 타임스탬프 $t_0$를 기록하고, Bridge RPC로 MPU 쪽 함수를 호출하며 이 값을 인자로 넘긴다. - MPU(Python) 쪽 RPC 핸들러는 받은 값을 그대로(에코) 즉시 MCU로 되돌려준다 — 자체 처리 로직 없이 최소한의 왕복만 만든다.
- MCU가 응답을 받은 시각 $t_1$을 다시
micros()로 기록한다. - $\Delta t = t_1 - t_0$를 왕복 지연으로 기록한다. 편도 지연이 아니라 왕복 지연이라는 점에 주의 — MPU와 MCU 클럭이 동기화되어 있지 않으므로 편도만 따로 재려면 별도의 시각 동기화가 필요하다(일반적으로 왕복의 절반으로 근사하되, 왕복 경로가 대칭이라는 가정이 전제된다는 것도 함께 적어둔다).
// MCU(스케치) 쪽 — 개념 예시. Bridge RPC 호출 함수명·시그니처는
// Arduino_RouterBridge 라이브러리 버전에 따라 다를 수 있어
// 공식 레퍼런스로 재확인 필요(확인 필요).
unsigned long t0, rttSum = 0;
const int SAMPLES = 1000;
unsigned long rttMax = 0;
int sampleCount = 0;
void onEchoReply(unsigned long echoedT0) {
unsigned long t1 = micros();
unsigned long rtt = t1 - echoedT0;
rttSum += rtt;
if (rtt > rttMax) rttMax = rtt;
sampleCount++;
}
void loop() {
if (sampleCount < SAMPLES) {
t0 = micros();
// bridge.call("echo", t0); // 실제 RPC 호출 API는 확인 필요
delay(10); // 측정 자체가 Bridge를 과도하게 두드리지 않도록 간격을 둔다
} else {
Serial.print("avg RTT(us): ");
Serial.println(rttSum / SAMPLES);
Serial.print("max RTT(us): ");
Serial.println(rttMax);
}
}
8.3.4.2 통계 처리에서 놓치지 말아야 할 것
- 평균만 보지 않는다. 제어 루프 설계에 의미 있는 것은 평균이 아니라 최댓값(worst-case)과 p99(상위 1% 지점)다 — 지터가 큰 시스템은 평균은 낮아도 꼬리가 길다.
- 부하 조건을 바꿔가며 측정한다. MPU가 유휴 상태일 때와, 비전 추론(제9부)·네트워크 업로드가 동시에 실행될 때의 지연·지터를 각각 측정해 비교한다 — 실제 애플리케이션은 유휴 상태로 동작하지 않는다.
- 샘플 수를 충분히 확보한다. 수십 개로는 꼬리 분포를 파악하지 못한다. 수천~수만 회 반복해 히스토그램을 작성한다.
- 페이로드 크기별로도 측정한다. 스칼라 값 하나(정수)와, 배열·구조체처럼 큰 페이로드를 보낼 때 직렬화·전송 시간이 어떻게 늘어나는지 비교하면 설계에 실질적인 도움이 된다.
8.3.5 설계에 주는 함의
측정 결과가 무엇이든(정확한 수치는 검증 필요), 원리에서 나오는 설계 원칙은 명확하다.
- Bridge를 hard real-time 루프의 "안"에 넣지 않는다. PID 제어 스텝(8.4)이나 setpoint 인코딩(8.5) 같은 루프는 Bridge 응답을 기다리지 않고 MCU 로컬 상태만으로 매 스텝 완결되게 만든다.
- Bridge는 "설정값 갱신"과 "상태 보고"의 비동기 채널로 취급한다. 예: MPU가 목표 속도·목표 위치를 계산해 Bridge로 MCU에 흘려주면, MCU는 그 값을 로컬 변수에만 반영하고 실제 제어 스텝은 자기 타이머 주기로 독립 실행한다. 이는 8.5에서 "MPU가 미션을 계획하고 MCU가 그 결과를 결정론적으로 실행"하는 구성으로 그대로 이어진다.
- Bridge 값이 갱신되지 않는 상황(연결 끊김·지연 폭증)에 대비한다. 마지막으로 받은 값을 유지하는 것이 안전한지, 타임아웃 후 안전 상태로 전환해야 하는지는 애플리케이션에 달렸다 — 이 판단 패턴은 8.5.7(워치독·페일세이프)에서 더 구체화한다.
8.3.6 다음 장으로
이제 MCU 루프의 원리(8.1~8.2)와 MCU-MPU 통신의 한계(이 장)를 모두 갖췄다. 8.4에서는 이를 종합해 실제로 동작하는 PID 모터 제어 루프를 스케치로 구현한다.