AEGLOSLAB Arduino UNO Q 완전 가이드

제3부 소프트웨어 스택

3.4 Bridge — MPU-MCU 통신의 내부

제3부 소프트웨어 스택의 마지막 장. 3.3에서 App Lab이 "Visual Bridge"라는 이름으로 내세운 MPU-MCU 통신 기능을, 여기서는 실제 구현까지 분석한다. 통신 지연·타이밍 특성을 실측 데이터로 다루는 것은 제8부 8.3(MPU-MCU 통신 지연 분석)의 몫이다 — 이 장은 "무엇이 어떻게 연결돼 있는가"라는 구조 이해에 집중한다.

3.4.1 이름 정리 — "Visual Bridge"와 "Bridge"

3.3.2에서 언급했듯, "Visual Bridge"는 store.arduino.cc 제품 페이지가 쓰는 마케팅 명칭이다. Arduino의 기술 문서(UNO Q User Manual, App Lab 문서)는 이 기능을 단순히 "Bridge"라고 부른다. 다행히 이번에는 "개념만 설명하고 세부는 확인 필요"로 끝낼 필요가 없다 — 공식 User Manual이 라이브러리 이름, 배후 서비스, 트랜스포트, API까지 상당히 구체적으로 문서화해 두었다. 이 장은 그 내용을 바탕으로 한다.

"The Arduino UNO Q uses RPC (Remote Procedure Call) to exchange data between the Linux (Qualcomm MPU) side and the real-time STM32 MCU. This mechanism allows functions running on one processor to be invoked transparently from the other, as if they were local calls." — UNO Q User Manual, "Bridge - Remote Procedure Call (RPC) Library"

3.4.2 3단 구조 — 라이브러리, 서비스, 소켓

Bridge는 세 층으로 나뉜다.

  1. Bridge 라이브러리(사용자 API): Arduino_RPClite 프레임워크 위에 만들어진 계층으로, MCU 스케치가 쓰는 BridgeClass(begin()·call()·notify()·provide()·provide_safe())를 제공한다. MPU 쪽은 Python의 arduino.app_utils 모듈이 같은 역할을 한다.
  2. arduino-router(배후 리눅스 서비스): 실제 트래픽을 처리하는 백그라운드 데몬. User Manual은 이를 "traffic controller"라 부른다. Star 토폴로지로 MessagePack RPC를 구현한다.
  3. Unix 도메인 소켓(/var/run/arduino-router.sock): 리눅스 프로세스들이 라우터와 통신하는 실제 경로. Bridge/arduino.app_utils는 이 소켓 접근을 자동으로 감춰 주지만, 필요하면 어떤 언어에서든(msgpack만 구현하면) 직접 접속할 수 있다.
Unix 소켓 LPUART1 /dev/ttyHS1 ↔ Serial1 115200 bps Python 스크립트 (main.py) arduino-router (Star 토폴로지) 다른 C++/Rust Linux 앱 MCU 스케치 (Bridge 라이브러리)

Star(별) 토폴로지라는 표현이 중요하다. 단순 시리얼 통신은 보통 1:1(포인트투포인트)이지만, 라우터는 여러 리눅스 프로세스가 동시에 MCU와(그리고 서로) 통신하도록 중계한다. User Manual이 드는 예:

  • Linux ↔ MCU: Python 스크립트가 센서를 읽는 동안, 별도 C++ 애플리케이션이 모터를 명령하는 식의 다중 접속.
  • Linux ↔ Linux: 라우터를 거쳐 리눅스 프로세스끼리도 RPC를 주고받을 수 있다 — 한 Python 스크립트가 함수를 노출하면 다른 Python·C++ 프로세스가 MCU를 거치지 않고 바로 호출.

라우터는 서비스 디스커버리도 겸한다 — 클라이언트(Python 스크립트든 MCU 스케치든)가 노출하려는 함수를 "등록"하면, 라우터가 이름과 목적지의 디렉터리를 유지하며 호출을 올바른 곳으로 라우팅한다.

소스는 공개돼 있다: arduino/arduino-router(라우터 서비스), arduino-libraries/Arduino_RouterBridge(MCU 측 라이브러리).

3.4.3 물리 트랜스포트 — 확인된 것과 미확인 부분

라우터가 두 프로세서 사이에서 실제로 쓰는 하드웨어 자원은 문서에 명시돼 있다.

  • 리눅스(MPU) 쪽: 시리얼 디바이스 /dev/ttyHS1
  • MCU 쪽: 하드웨어 시리얼 포트 Serial1
  • 보드레이트: 115200 bps (systemd 서비스 설정에서 확인)

⚠️ 예약 자원 경고(공식 문서): /dev/ttyHS1(Linux)이나 Serial1(Arduino/Zephyr)을 사용자 코드에서 직접 열면 안 된다. 이 인터페이스는 arduino-router 서비스가 배타적으로 점유하며, 직접 접근을 시도하면 Bridge 전체가 오동작한다.

미확인 부분 — 정직하게 남겨 둔다: ArduinoCore-zephyr 저장소의 커뮤니티 이슈(#252)는 보드의 하드웨어 블록 다이어그램에 STM32U585-QRB2210 사이의 프로세서 간 연결로 LPUART1과 SPI3 두 가지가 표시돼 있음을 지적하며, "현재 공개 펌웨어가 실제로 쓰는 물리 트랜스포트가 무엇인지"와 "SPI3는 향후 고속 데이터 교환용으로 예약된 것인지"를 공식적으로 질문했다. 이 책 조사 시점 기준으로 이 이슈에는 공식 답변이 달리지 않은 상태였다. arduino-router의 systemd 서비스 설정(--serial-port /dev/ttyHS1 --serial-baudrate 115200)을 보면 현재 배포본은 UART(LPUART1) 경로를 쓰는 것으로 보이지만, SPI3 회선이 실제로 예비용인지, 향후 펌웨어에서 활성화될 예정인지는 확인하지 못했다. 카메라 스트림 같은 대역폭이 큰 데이터를 MCU 경유로 옮기는 설계를 고려 중이라면, 이 지점은 커뮤니티 포럼·GitHub 이슈로 최신 상태를 재확인할 가치가 있다.

디버깅용으로 라우터 서비스 자체를 분석할 수도 있다.

systemctl status arduino-router                 # 라우터 동작 상태
sudo systemctl restart arduino-router            # 통신이 막혔을 때 재시작(재부팅 불필요)
journalctl -u arduino-router -f                  # 실시간 로그

로그를 더 자세히 보고 싶으면 서비스 파일에 --verbose를 추가한다.

sudo nano /etc/systemd/system/arduino-router.service
# ExecStart= 줄 끝에 --verbose 추가
sudo systemctl daemon-reload
sudo systemctl restart arduino-router

3.4.4 MCU 쪽 API — BridgeClass

MCU 스케치에서 Bridge를 쓰려면 Arduino_RouterBridge.h를 인클루드한다.

메서드역할
Bridge.begin()Bridge와 내부 시리얼 트랜스포트 초기화
Bridge.call(method, args...)리눅스 쪽 함수를 호출하고 결과를 기다림(동기)
Bridge.notify(method, args...)응답을 기다리지 않는 호출(fire-and-forget)
Bridge.provide(name, function)로컬 MCU 함수를 리눅스에 RPC로 노출
Bridge.provide_safe(name, function)위와 같지만, 실행이 메인 loop() 컨텍스트에서 이뤄지도록 보장

provide()와 provide_safe()의 차이가 실전에서 가장 흔한 함정이다. provide()로 등록한 함수는 RPC 처리 전용 백그라운드 스레드(높은 우선순위)에서 곧바로 실행된다 — 짧고 스레드 안전한 코드에만 써야 한다. digitalWrite()나 Serial 같은 표준 Arduino API처럼 메인 루프 컨텍스트를 가정하는 호출과 상호작용해야 한다면 provide_safe()를 써야 한다. 이것은 __loopHook을 통해 메인 loop()가 유휴 상태일 때 콜백을 실행하도록 지연시킨다.

⚠️ 데드락 함정: provide()(또는 provide_safe()) 콜백 함수 안에서 Bridge.call(), Monitor.print(), Serial.print()를 호출하면 안 된다. 하나의 통신에 응답하는 도중 새 통신을 시작하는 셈이 되어 시스템 교착(deadlock)을 일으킨다 — User Manual이 굵게 강조하는 경고다.

동시성 안전은 Zephyr 뮤텍스(k_mutex)로 보장된다(3.2에서 다룬 Zephyr 커널 프리미티브가 여기서 실제로 쓰이는 지점이다). 들어오는 요청은 전용 백그라운드 스레드(updateEntryPoint)가 계속 폴링해서 처리한다.

RpcCall은 비동기 RPC를 나타내는 헬퍼 클래스이고, Monitor는 리눅스 쪽에서 MCU로 텍스트 스트림을 보내는 사전 정의 객체다(가상 Serial Monitor 역할, 내부적으로 mon/write라는 RPC 메서드를 쓴다). 다만 App Lab 플랫폼 0.55.0부터는 레거시 Monitor 없이 표준 Serial.print()/Serial.println()을 그대로 App Lab 콘솔에 출력할 수 있게 됐다 — 옛 프로젝트 호환을 위해 Monitor도 계속 지원된다.

3.4.5 실전 예제 — Python이 MCU 함수를 원격 호출

가장 단순한 형태: MPU의 Python 스크립트가 1초마다 MCU에 등록된 함수를 호출해 LED를 토글하는 예제다(User Manual과 공식 예제 프로젝트 arduino_uno_q_blink_cli가 공유하는 패턴). MCU 쪽은 Bridge.provide("set_led_state", ...)로 함수를 등록만 해 두고, MPU의 Python이 Bridge.call("set_led_state", ...)로 그 이름을 부른다 — 라우터가 이름으로 상대를 찾아 중계하므로 어느 쪽이 먼저 등록하든 상관없다(3.4.2).

이 장은 구조(무엇이 어떻게 연결돼 있는가)에 집중하므로, sketch.ino·main.py 전체 코드와 App.run()·Bridge.call()의 실제 문법은 4.2.3에서 그대로 확인한다. 여기서는 그 호출 한 번이 실제로 거치는 경로만 시퀀스로 짚는다.

Python 스크립트 (MPU, main.py) arduino-router (Unix 소켓) 스케치 (MCU, Bridge 라이브러리) Bridge.call("set_led_ state", True) MessagePack 인코딩 LPUART1 프레임 전송 (/dev/ttyHS1 → Serial1, 115200bps) provide()로 등록된 set_led_state() 실행 응답 프레임 결과 반환(Bridge.call이 반환)

3.4.6 저수준 접근 — Unix 소켓에 직접 접속하기

Bridge/arduino.app_utils가 소켓 통신을 감춰 주지만, 라우터의 프로토콜이 표준 MessagePack RPC이기 때문에 이를 지원하는 어떤 언어(Python, C++, Rust, Go 등)에서도 App Lab 헬퍼 없이 직접 소켓에 접속할 수 있다. User Manual이 제공하는 예제를 요약하면:

sudo apt install python3-msgpack   # msgpack 라이브러리 설치
import socket
import msgpack

SOCKET_PATH = "/var/run/arduino-router.sock"

# MessagePack-RPC 요청 형식: [type=0(Request), msgid, method명, 인자 배열]
request = [0, 1, "set_led_state", [True]]
packed_req = msgpack.packb(request)

with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
    client.connect(SOCKET_PATH)
    client.sendall(packed_req)
    response = msgpack.unpackb(client.recv(1024))
    print(f"Router Response: {response}")

이 경로는 App Lab 생태계 바깥의 기존 리눅스 애플리케이션에 Arduino 함수를 통합하고 싶을 때(예: 이미 실행 중인 C++/Rust 서비스에서 MCU를 제어), 또는 Bridge 프로토콜 자체를 디버깅할 때 유용하다.

3.4.7 엣지케이스 정리

  • /dev/ttyHS1·Serial1 직접 열기 금지(3.4.3) — 이미 라우터가 점유. 사용자 코드가 이를 무시하고 열면 Bridge가 경고 없이 오동작한다.
  • provide() 콜백 안에서 Bridge.call()/Monitor.print()/Serial.print() 금지(3.4.4) — 데드락.
  • provide()(백그라운드 스레드)와 provide_safe()(메인 loop 컨텍스트)를 혼동하면 하드웨어 API 호출이 스레드 안전성 문제로 크래시할 수 있다 — 표준 Arduino API를 호출하는 콜백은 반드시 provide_safe().
  • SPI3의 역할은 미확인(3.4.3) — 향후 고대역폭 트랜스포트로 쓰일 가능성이 있지만 공식 답변 없음.
  • 레거시 Monitor vs 신형 Serial: 0.55.0 이전 프로젝트는 Monitor, 이후로는 Serial 직접 사용이 권장된다 — 오래된 예제 코드를 참고할 때 버전 차이를 염두에 둘 것.

3.4.8 다음으로

이 장은 Bridge의 구조(라우터·소켓·트랜스포트·API)를 다뤘다. 이 구조가 실제로 어느 정도의 지연·지터를 갖는지 — 예를 들어 Bridge.call() 한 번의 왕복 시간이 밀리초 단위로 얼마나 되는지, MPU 쪽 프로세스 스케줄링이 그 지연에 얼마나 영향을 주는지 — 는 이 책이 지어내지 않고 제8부 8.3(MPU-MCU 통신 지연 분석 — Bridge의 실측)에서 직접 측정해 다룬다. 이 구조 위에서 실제로 PID 모터 제어 루프를 어떻게 구현하는지는 8.4로 이어진다.