AEGLOSLAB ROS 2 Jazzy 대전

제6부 하드웨어 인터페이스

6.7 드라이버 작성 원칙 — 시리얼·CAN·EtherCAT

6.7.1 이 장에서 배우는 것 / 선수 지식

6.6에서 제어 주기를 지배하는 것이 대개 하드웨어 통신이라고 했다. 이 장은 그 통신을 다룬다.

로봇에서 쓰이는 버스는 몇 가지로 수렴한다 — 시리얼(UART·RS-485), CAN, EtherCAT, 그리고 이더넷 기반 벤더 프로토콜. 각각 특성이 다르고, 그 특성이 달성 가능한 제어 주기와 시스템 구조를 결정한다.

이 장은 버스별 세부 명세가 아니라 드라이버를 견고하게 만드는 원칙과 버스 선택이 시스템에 미치는 영향에 집중한다. 프로토콜 상세는 각 표준·벤더 문서를 가리킨다(BK-04).

배우는 것:

  • 공통 원칙 — 상태 기계·타임아웃·재연결·진단.
  • 시리얼 — 프레이밍·체크섬, 그리고 지연 타이머 함정.
  • CAN — SocketCAN·프레임 설계·버스 부하.
  • EtherCAT — 왜 실시간에 적합한가, 그 대가는.
  • 버스 선택 — 요구에서 버스로.
  • 장치 식별 — udev 규칙.
  • 하드웨어 없이 개발·테스트 — 모의와 재생.

선수 지식: 6.2(하드웨어 컴포넌트), 6.6(실시간 제약·주기), 6.9(진단 — 함께 읽으면 좋다). 리눅스 장치·터미널 기본 지식을 가정한다.

정직: 버스별 라이브러리·API는 선택지가 많고 프로젝트마다 다르다. 아래 코드는 원칙을 보이기 위한 개념 구현이며 특정 라이브러리를 권하지 않는다. EtherCAT은 특히 구현·라이선스·마스터 스택 선택이 복잡하며, 여기서는 개념과 판단 기준만 다룬다(확인 필요). 이 환경에서 검증하지 않았다(목차 3).(→ 171차 확인: vcan(가상 CAN)·socat(가상 시리얼)로 실제 하드웨어 없이 CAN·시리얼 관련 코드는 전부 컴파일·실행해 확인했다 — 6.7.2·6.7.3·6.7.4 실행 확인(171차) 참고. EtherCAT은 실제 슬레이브·마스터 스택이 필요해 이번에도 검증하지 못했고, ethercat_driver_ros2는 표준 apt에 없음을 확인했다 — 6.7.5 참고.)

6.7.2 공통 원칙

버스가 무엇이든 견고한 드라이버는 같은 구조를 갖는다.

연결 시도 성공 신원 확인 연속 실패 실패 복구 불일치 임계 초과 드라이버 상태 기계 DISCONNECTED CONNECTING IDENTIFYING 장치 확인 OPERATIONAL DEGRADED
원칙설명관련 장
명시적 상태 기계연결·식별·동작·열화·재연결6.2.6
모든 I/O에 타임아웃무한 대기 금지6.6.3
장치 신원 확인잘못된 장치에 명령 금지6.2.6
연속 실패 임계일시적 노이즈와 실제 장애 구분6.2.7
진단 노출오류율·상태를 밖으로6.9
사전 할당 버퍼실시간 경로 무할당2.6
체크섬·프레이밍손상된 데이터를 쓰지 않음아래

타임아웃 설계가 특히 중요하다:

$$t_{\text{timeout}} < T_{\text{주기}} - t_{\text{나머지}}$$

타임아웃이 주기보다 길면 한 번의 실패가 주기 전체를 밀어버린다. 통신 왕복 시간의 2~3배로 설정하되, 주기 예산 안에 들어와야 한다.

재연결 전략:

// 지수 백오프 — 무한 재시도로 CPU·버스를 태우지 않게
void attempt_reconnect() {
  const auto now = std::chrono::steady_clock::now();
  if (now < next_retry_time_) return;                  // 아직 대기

  if (try_connect()) {
    retry_delay_ = kMinRetryDelay;                     // 성공 시 리셋
    state_ = State::IDENTIFYING;
  } else {
    retry_delay_ = std::min(retry_delay_ * 2, kMaxRetryDelay);
    next_retry_time_ = now + retry_delay_;
    ++reconnect_attempts_;                             // 진단용
  }
}

함정: 재연결을 매 주기 시도하면 장치가 없을 때 CPU와 버스를 태운다. USB 장치가 빠진 상태에서 1 kHz로 open()을 시도하는 것은 시스템에 부담이며, 로그도 폭주한다(1.11.5). 백오프는 선택이 아니라 필수다.

견해: 재연결이 성공했을 때 무엇을 할지 미리 정하라. 모터가 재연결되면 이전 명령을 이어갈 것인가, 정지 상태로 시작할 것인가. 정지가 안전한 기본값이며, 이어가려면 상위에서 명시적으로 재활성화하게 하는 편이 좋다(6.2.6의 활성화 초기화와 같은 논리).

실행 확인(171차) — 위 attempt_reconnect()를 책의 코드 그대로 컴파일·실행해 배가 수열을 확인했다. 컴파일 시 #include <algorithm>(std::min)이 누락돼 있어(책 코드 스니펫엔 없음) 추가해야 했다. 5회 연속 실패하도록 제어하니 재시도 지연이 100→200→400→800→1600→3200ms로 정확히 2배씩 늘었고, 5번째 실패에서 kMaxRetryDelay=3200ms와 정확히 일치해 상한 캡핑도 확인됐다. 원본: tools/gap_harness/gap171/backoff_test/(backoff_test.cpp·backoff_result.log).

6.7.3 시리얼 — UART·RS-485

가장 흔하고 가장 단순하며, 함정도 명확하다.

#include <termios.h>
#include <fcntl.h>
#include <unistd.h>

bool open_serial(const std::string & device, int baud) {
  fd_ = ::open(device.c_str(), O_RDWR | O_NOCTTY | O_NONBLOCK);
  if (fd_ < 0) return false;

  struct termios tty{};
  if (tcgetattr(fd_, &tty) != 0) return false;

  cfmakeraw(&tty);                       // 8N1, 가공 없음
  cfsetispeed(&tty, B1000000);           // 보율
  cfsetospeed(&tty, B1000000);

  tty.c_cflag |= (CLOCAL | CREAD);
  tty.c_cflag &= ~CRTSCTS;               // 흐름 제어 없음
  tty.c_cc[VMIN] = 0;                    // 논블로킹 읽기
  tty.c_cc[VTIME] = 0;

  if (tcsetattr(fd_, TCSANOW, &tty) != 0) return false;
  tcflush(fd_, TCIOFLUSH);               // 잔여 데이터 제거
  return true;
}

실행 확인(171차) — 위 open_serial()을 실제 하드웨어 없이 socat의 가상 시리얼 포트 쌍으로 컴파일·실행해 확인했다. socat -d -d pty,raw,echo=0,link=... pty,raw,echo=0,link=...로 서로 연결된 두 가상 포트(ptyA↔ptyB)를 만들고, 책의 코드(cfmakeraw+1Mbps+CLOCAL|CREAD+비블로킹 VMIN=0/VTIME=0)를 그대로 컴파일해 한쪽에 쓰고 다른 쪽에서 읽었다 — "HELLO_SERIAL_TEST_42"(20바이트)를 정확히 20바이트 그대로 수신했다. 이 코드는 별도 include 없이 그대로 컴파일됐다. 원본: tools/gap_harness/gap171/serial_test/(serial_test.cpp·writer_result.log·reader_result.log).

6.7.3.1 지연 타이머 함정

이것이 시리얼에서 가장 비용이 큰 함정이다.

USB-시리얼 변환 칩(FTDI 등)은 작은 데이터를 모아서 보내는 버퍼링을 한다. 기본 지연 타이머가 16 ms인 경우가 흔하며, 이는 제어 주기를 60 Hz 이하로 제한한다.

# 현재 지연 타이머 확인
cat /sys/bus/usb-serial/devices/ttyUSB0/latency_timer

# 1 ms로 변경 (임시)
echo 1 | sudo tee /sys/bus/usb-serial/devices/ttyUSB0/latency_timer

영구 적용은 udev 규칙으로:

# /etc/udev/rules.d/99-robot-serial.rules
SUBSYSTEM=="usb-serial", DRIVER=="ftdi_sio", \
  ATTR{latency_timer}="1"

함정: "1 Mbps인데 왜 100 Hz밖에 안 나오지"의 답이 대개 이것이다. 보율은 충분한데 칩이 데이터를 보유하고 있다. USB-시리얼을 쓰는 로봇이라면 반드시 확인하라 — 설정 한 줄로 제어 주기가 10배 달라질 수 있다.

정직: 지연 타이머의 존재·경로·기본값은 칩과 드라이버에 따라 다르다(확인 필요). FTDI 계열에서 잘 알려져 있으며, 다른 칩은 다른 메커니즘을 쓸 수 있다. 내장 UART(USB 아님)에는 해당하지 않는다.

6.7.3.2 프레이밍과 체크섬

시리얼은 바이트 스트림이므로 메시지 경계를 직접 만들어야 한다.

요소목적
시작 바이트(예: 0xFF 0xFE)프레임 시작 식별
길이 필드얼마나 읽을지
명령·ID무엇에 대한 것인지
페이로드데이터
체크섬·CRC손상 검출
// 개념: 프레임 파싱 — 사전 할당 링버퍼 사용 (2.6)
bool parse_frame(const uint8_t * data, size_t len, Frame & out) {
  if (len < kMinFrameLen) return false;
  if (data[0] != 0xFF || data[1] != 0xFE) return false;

  const uint8_t payload_len = data[2];
  if (len < static_cast<size_t>(payload_len) + kHeaderLen + kCrcLen) return false;

  const uint16_t crc_calc = crc16(data + kHeaderLen, payload_len);
  const uint16_t crc_recv = (data[kHeaderLen + payload_len] << 8) |
                             data[kHeaderLen + payload_len + 1];
  if (crc_calc != crc_recv) {
    ++crc_errors_;                       // 진단(6.9)
    return false;
  }
  // ... 파싱 ...
  return true;
}

함정: 체크섬 없이 시리얼을 쓰지 말라. 전기적 노이즈·타이밍 문제로 바이트가 손상되며, 그것을 그대로 모터 명령으로 쓰면 로봇이 예상치 못한 동작을 한다. 로봇에서 이것은 안전 문제다.

함정: 동기화 상실(desync)을 처리하라. 한 바이트를 놓치면 이후 모든 프레임이 어긋난다. 시작 바이트를 찾아 재동기화하는 로직이 필요하며, 없으면 한 번의 노이즈로 통신이 영구히 깨진다.

실행 확인(171차) — 위 parse_frame()을 책 코드 그대로 옮기고 표준 CRC-16/CCITT-FALSE로 crc16()을 채워(책은 호출만 보이고 구현은 생략) 5개 테스트 벡터로 확인했다. ① 정상 프레임 — 파싱 성공, payload 정확히 추출. ② CRC 1비트 손상 — 거부, crc_errors_ 카운터 정확히 1 증가. ③ 시작 바이트 어긋남(동기화 상실 시뮬레이션) — 거부. ④ 길이 필드는 있으나 CRC 바이트가 잘림(트렁케이션) — 거부. ⑤ 최소 길이 미달 — 거부. 5개 전부 책의 설계와 정확히 일치했다. 원본: tools/gap_harness/gap171/frame_test/(frame_parse_test.cpp·frame_parse_result.log).

6.7.3.3 RS-485 다중 드롭

여러 장치가 한 버스를 공유한다.

고려설명
주소 지정각 장치에 ID
반이중송신·수신 전환 시간 필요
폴링순차 질의 → 장치 수에 비례해 주기 증가
종단 저항전기적 요구

$$T_{\text{주기}} \geq N_{\text{장치}} \times (t_{\text{요청}} + t_{\text{응답}} + t_{\text{전환}})$$

장치가 많으면 주기가 급격히 악화된다. 관절 6개를 순차 폴링하면 하나당 1 ms여도 6 ms가 필요하다.

견해: 동기 읽기(sync read) 기능이 있는 프로토콜을 선호하라. 여러 장치의 상태를 한 번의 요청으로 받는 기능이며, 다이나믹셀 등 여러 서보 프로토콜이 제공한다. 이것 하나로 주기가 수 배 좋아진다.

6.7.4 CAN

차량·산업에서 검증된 버스이며, 로봇에서도 널리 쓰인다.

# SocketCAN 설정
sudo ip link set can0 type can bitrate 1000000
sudo ip link set up can0

# CAN-FD (더 큰 페이로드·높은 속도)
sudo ip link set can0 type can bitrate 500000 dbitrate 2000000 fd on
sudo ip link set up can0

# 확인·모니터링
ip -details -statistics link show can0
candump can0
cansend can0 123#DEADBEEF
#include <linux/can.h>
#include <linux/can/raw.h>
#include <sys/socket.h>

bool open_can(const std::string & iface) {
  sock_ = socket(PF_CAN, SOCK_RAW | SOCK_NONBLOCK, CAN_RAW);
  if (sock_ < 0) return false;

  struct ifreq ifr{};
  std::strncpy(ifr.ifr_name, iface.c_str(), IFNAMSIZ - 1);
  ioctl(sock_, SIOCGIFINDEX, &ifr);

  struct sockaddr_can addr{};
  addr.can_family = AF_CAN;
  addr.can_ifindex = ifr.ifr_ifindex;

  // 관심 있는 ID만 받도록 필터 — 커널이 걸러 준다
  struct can_filter filters[2] = {
    {0x100, CAN_SFF_MASK},
    {0x200, CAN_SFF_MASK},
  };
  setsockopt(sock_, SOL_CAN_RAW, CAN_RAW_FILTER, filters, sizeof(filters));

  return bind(sock_, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr)) == 0;
}

CAN의 성질:

성질설명
프레임 크기CAN: 8바이트, CAN-FD: 최대 64바이트
우선순위ID가 낮을수록 우선(중재)
멀티마스터모든 노드가 송신 가능
에러 처리 내장CRC·ACK·에러 프레임·버스 오프
전기적 견고성차동 신호, 노이즈에 강함

프레임 설계가 중요하다:

원칙이유
중요한 메시지에 낮은 ID중재에서 우선권을 얻는다
8바이트(또는 64) 안에 맞추기분할하면 복잡·지연
주기적 상태 + 이벤트 명령버스 부하 예측 가능
ID 대역 분리명령·상태·진단 구역

버스 부하 계산:

$$U = \sum_i \frac{n_{\text{bits},i} \cdot f_i}{R_{\text{bitrate}}}$$

부하율이 높으면 지연이 급격히 악화된다. 낮은 우선순위 메시지가 계속 지연된다. 일반적으로 50~70% 이하로 유지하는 것이 권장된다(확인 필요: 시스템 요구에 따라 다름).

함정: 버스 오프(bus-off) 상태를 처리하라. 에러가 누적되면 CAN 컨트롤러가 스스로 버스에서 빠진다. 자동 복구를 켜거나 드라이버가 감지해 재초기화해야 하며, 그렇지 않으면 통신이 영구히 멈춘다.

# 자동 복구 설정
sudo ip link set can0 type can bitrate 1000000 restart-ms 100
# 상태 확인
ip -details link show can0 | grep -i state

함정: CAN 소켓 버퍼가 넘치면 프레임이 유실된다. 읽기가 느리면 커널 큐가 차고, candump로는 보이는데 애플리케이션에서 놓치는 상황이 생긴다. 논블로킹으로 매 주기 큐를 비우라.

CANopen: ROS 2에는 CANopen 통합 패키지가 있다(확인 필요: ros2_canopen의 Jazzy 지원 상태)(→ 171차 확인: ros-jazzy-canopen 메타패키지와 하위 23개 이상 패키지가 전부 실재 — 아래 실행 확인(171차) 참고). 표준 CANopen 장치(모터 드라이버 다수)를 쓴다면 직접 구현보다 이쪽을 검토할 가치가 있다.

실행 확인(171차, Jazzy) — open_can()의 CAN_RAW 필터가 실제로 커널 레벨에서 걸러지는지, ros2_canopen의 Jazzy 지원 상태를 확인했다.

  • CAN_RAW 필터 실측. ip link add dev vcan0 type vcan로 가상 CAN 버스를 만들고(modprobe vcan은 컨테이너 자체 /lib/modules에 vcan.ko가 없어 실패했지만, ip link add ... type vcan은 호스트 커널이 이미 갖고 있어 그대로 성공 — 컨테이너의 /lib/modules 부재가 실제 모듈 가용성과 다를 수 있다는 참고 사항), 책의 open_can()을 그대로 컴파일해 실행했다(컴파일 시 #include <string> 누락 발견, 책 코드 스니펫엔 없음 — 추가해 해결). ID 0x100·0x200·0x300·0x123 네 프레임을 전부 버스에 보낸 결과, 필터({0x100, CAN_SFF_MASK}·{0x200, CAN_SFF_MASK})가 걸린 수신자는 정확히 0x100·0x200 둘만 수신했다(total received=2) — "커널이 걸러 준다"는 주석을 실측으로 확인했다.
  • ros2_canopen — Jazzy 지원 포괄적으로 확인. apt-cache search canopen으로 ros-jazzy-canopen(메타패키지)과 canopen-core·canopen-402-driver(CIA402 모터 프로파일)·canopen-base-driver·canopen-master-driver·canopen-proxy-driver·canopen-inventus-driver(BMS)·canopen-ros2-control·canopen-fake-slaves(테스트용 모의 슬레이브)·canopen-interfaces·canopen-utils 등 23개 이상의 하위 패키지가 전부 실재함을 확인했다.

컨테이너 gap171j(--privileged). 원본: tools/gap_harness/gap171/can_test/(can_filter_test.cpp·receiver_result.log), tools/gap_harness/gap171/pkg_availability_check.log.

6.7.5 EtherCAT

실시간 제어에서 사실상의 산업 표준이다.

왜 실시간에 적합한가:

마스터가 프레임 하나 전송 슬레이브 1 통과하며 읽고 쓰기 슬레이브 2 통과하며 읽고 쓰기 슬레이브 N 프레임이 마스터로 돌아옴
특성설명
온더플라이 처리프레임이 지나가며 각 슬레이브가 자기 부분을 읽고 씀
한 프레임에 전체 I/O슬레이브 수와 무관하게 왕복 한 번
분산 클럭(DC)슬레이브 간 나노초 수준 동기
높은 주기kHz 단위 제어 가능
표준 이더넷 물리층일반 케이블·NIC

시리얼·CAN과의 결정적 차이: 장치 수가 주기에 거의 영향을 주지 않는다(6.7.3.3의 부등식과 대조).

대가:

대가설명
마스터 스택 필요SOEM(오픈소스)·상용 스택
실시간 커널 필수주기를 지키지 못하면 슬레이브가 오류
전용 NIC 권장다른 트래픽과 공유하면 지터
설정 복잡도ESI 파일·PDO 매핑·상태 기계
하드웨어 비용EtherCAT 지원 장치
학습 곡선가파르다

슬레이브 상태 기계:

상태의미
INIT초기
PRE-OP메일박스 통신만
SAFE-OP입력 유효, 출력 안전값
OPERATIONAL정상 동작

함정: 주기를 놓치면 슬레이브가 SAFE-OP로 떨어진다. 워치독이 동작해 안전 상태로 가는 것이며, 이는 기능이지 버그가 아니다. 그래서 EtherCAT은 실시간 커널이 선택이 아니라 요구사항이다(6.6.7).

정직: ROS 2의 EtherCAT 통합은 선택지가 여럿이고 성숙도가 다르다(확인 필요)(→ 171차: apt-cache search soem·apt-cache search ethercat·ethercat_driver_ros2로 직접 검색했으나 표준 ROS apt 저장소에서 관련 패키지를 전혀 찾지 못했다(0건) — 소스 빌드 경로는 시도하지 않아 "존재하지 않는다"고 단정하지 않는다. ros2_canopen(CAN 기반)과 대조적으로, apt만으로는 EtherCAT 통합에 접근할 수 없다는 정도만 확인됐다. 성숙도·라이선스 검토는 여전히 미확인). ethercat_driver_ros2 등 커뮤니티 패키지와 벤더 제공 스택이 있으며, 라이선스(SOEM은 GPL 계열 조건 확인 필요)와 지원 범위를 검토해야 한다. 이 영역은 도입 전 조사가 필수인 부분이다.

견해: EtherCAT은 요구가 명확할 때 도입하라 — kHz 제어, 다축 동기, 산업용 드라이브. 그렇지 않다면 복잡도와 비용이 이득을 넘는다. 그리고 도입한다면 실시간 시스템 구축(6.6.8)이 함께 와야 한다 — 둘은 분리할 수 없다.

6.7.6 버스 선택

~100 Hz 100~500 Hz ~4 많음 500 Hz 이상 예 버스 선택 제어 주기 요구? 시리얼 · CAN 장치 수? 시리얼(sync read) · CAN CAN · EtherCAT EtherCAT 또는 벤더 실시간 이더넷 장치가 이미 정해져 있나? 장치가 지원하는 버스
축시리얼CANEtherCAT
주기 상한낮음~중간중간높음
장치 수 영향크다(폴링)중간(버스 부하)작다
구현 난이도낮음중간높음
실시간 요구낮음중간필수
노이즈 내성낮음(RS-485는 좋음)높음높음
비용낮음낮음높음
진단 기능직접 구현내장풍부

견해: 버스는 대개 하드웨어 선택에 따라오지 결정하는 것이 아니다. 모터 드라이버가 CAN만 지원하면 CAN을 쓴다. 그러나 하드웨어를 고르는 단계에 참여할 수 있다면, 목표 제어 주기에서 역산해 버스를 요구사항에 넣으라 — 나중에 "주기가 안 나온다"를 발견하면 하드웨어를 바꿔야 한다.

6.7.7 장치 식별과 udev

/dev/ttyUSB0은 안정적이지 않다. USB 장치 순서·부팅 타이밍에 따라 바뀐다.

# 장치 속성 확인
udevadm info -a -n /dev/ttyUSB0 | head -40
# /etc/udev/rules.d/99-robot-devices.rules
# 시리얼 번호로 고정 심볼릭 링크
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", \
  ATTRS{serial}=="A50285BI", SYMLINK+="robot_arm", MODE="0666", \
  ATTR{device/latency_timer}="1"

SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", \
  ATTRS{serial}=="A9UX2W3D", SYMLINK+="robot_base", MODE="0666"
sudo udevadm control --reload-rules && sudo udevadm trigger
ls -l /dev/robot_arm

URDF에서 안정적인 이름을 쓴다(6.1.7):

<param name="device">/dev/robot_arm</param>
이점설명
안정성순서와 무관
가독성/dev/robot_arm이 /dev/ttyUSB0보다 명확
권한MODE로 그룹 문제 해결(6.3.8)
설정 통합지연 타이머도 함께(6.7.3.1)

견해: udev 규칙은 로봇 배포의 필수 구성 요소로 취급하라. 프로비저닝 스크립트·Docker 이미지·설치 문서에 포함시킨다(14.8). 개발자마다 수동으로 설정하면 "내 머신에서는 되는데"가 생긴다.

실행 확인(172차) — 위 udev 규칙을 udevadm으로 부분 확인했다(문법·로딩은 확인, 완전한 매칭은 실제 하드웨어가 없어 확인하지 못함). 책의 규칙을 그대로 /etc/udev/rules.d/99-robot-devices.rules에 저장하고 udevadm verify로 문법을 검사하니 "1 udev rules files have been checked. Success: 1 Fail: 0"로 정확했다. udevadm test가 이 파일을 실제로 읽는지도 확인했는데, 처음엔 임의 경로(/root/udev_test/rules.d/)에 둬서 무시됐다 — udevadm test는 표준 udev 규칙 디렉터리(/etc/udev/rules.d/ 등)만 읽는다는 것을 실측으로 확인했다. 표준 경로로 옮기니 "Reading rules file: /etc/udev/rules.d/99-robot-devices.rules"가 시스템 규칙들과 함께 정확한 순서로 나타났다. 다만 완전한 ATTRS 매칭(심볼릭 링크 생성)은 확인하지 못했다 — 컨테이너에 실제 USB-시리얼 장치나 그 sysfs 트리가 없어, 테스트 가능한 실제 tty 장치 노드가 존재하지 않았다(/dev/ttyS4·/dev/console 모두 노드 자체가 없었음). 정직하게 부분 확인으로 남긴다. 원본: tools/gap_harness/gap172/udev_test/(99-robot-devices.rules·udevadm_verify_result.log·udevadm_test_console.log·udevadm_test_ttyS4.log).

함정: 컨테이너에서 udev 심볼릭 링크를 쓰려면 장치를 매핑해야 한다.

docker run --device=/dev/robot_arm:/dev/robot_arm ...
# 또는 (덜 안전)
docker run --privileged -v /dev:/dev ...

6.7.8 하드웨어 없이 개발·테스트

6.2.8의 mock을 버스 수준으로 확장한다.

① 가상 시리얼 포트:

# 두 개의 연결된 가상 포트 생성
socat -d -d pty,raw,echo=0 pty,raw,echo=0
# → /dev/pts/3 과 /dev/pts/4 가 서로 연결됨

# 한쪽에 모의 장치 스크립트, 다른 쪽을 드라이버가 사용

② 가상 CAN:

sudo modprobe vcan
sudo ip link add dev vcan0 type vcan
sudo ip link set up vcan0

# 테스트
candump vcan0 &
cansend vcan0 123#DEADBEEF

vcan이 특히 유용하다 — 실제 CAN 하드웨어 없이 드라이버·프로토콜을 완전히 시험할 수 있고, CI에서도 동작한다(4.10).

③ 프로토콜 모의 장치:

# 개념: 모의 모터 드라이버 — 실제 하드웨어처럼 응답
class MockMotorDevice:
    def __init__(self, port):
        self.positions = [0.0] * 6
        self.port = port

    def run(self):
        while True:
            frame = self.read_frame()
            if frame.cmd == CMD_READ_STATE:
                self.send_state()
            elif frame.cmd == CMD_WRITE_POS:
                self.positions = frame.positions   # 즉시 반영(이상적 서보)
            # 고장 주입: 확률적으로 응답 생략·체크섬 오류

견해: 모의 장치에 고장 주입을 넣으라. 응답 누락·체크섬 오류·지연 증가·연결 끊김을 흉내 내면 오류 처리 경로를 실제로 시험할 수 있다. 이것이 없으면 재연결·백오프·진단 코드는 현장에서 처음 실행된다 — 그리고 대개 거기서 버그가 드러난다.

④ 기록·재생:

# CAN 트래픽 기록
candump -l can0            # → candump-*.log
# 재생
canplayer -I candump-*.log

실제 로봇의 통신을 기록해 두면 회귀 테스트에 쓸 수 있다. 특정 상황(고장·경계 조건)의 트래픽을 보관하면 그것을 재현할 수 있다.

6.7.9 진단 항목

드라이버가 반드시 내보내야 할 것(6.2.10·6.9):

항목왜
연결 상태현재 상태 기계 상태
통신 오류율케이블·노이즈 열화 조기 탐지
체크섬·CRC 오류 수전기적 문제
재연결 횟수불안정성 지표
왕복 시간(평균·최대)주기 여유(6.6.6)
타임아웃 발생 수장치 응답성
마지막 성공 통신 시각두절 감지
장치 보고 오류 코드과전류·과열·엔코더 오류
버스 상태(CAN)에러 카운터·버스 오프
슬레이브 상태(EtherCAT)OP·SAFE-OP 등

견해: 오류율 추이가 가장 유용한 예방 지표다. 케이블이 마모되거나 커넥터가 헐거워지면 오류율이 서서히 오르고, 완전히 끊기기 전에 알 수 있다. 절대 오류 수보다 최근 구간의 비율을 보는 것이 좋다.

6.7.10 함정 카탈로그

#함정증상대처
1USB-시리얼 지연 타이머 기본값주기가 60 Hz에 갇힘latency_timer=1·udev
2체크섬 없음손상 데이터를 명령으로CRC 필수
3동기화 상실 미처리한 번 노이즈에 영구 고장재동기화 로직
4무한 대기 I/O주기 이탈·정지타임아웃 필수
5타임아웃이 주기보다 김한 번 실패에 주기 붕괴예산 안에
6재연결 백오프 없음CPU·버스·로그 폭주지수 백오프
7재연결 후 이전 명령 재개예상치 못한 동작정지로 시작
8RS-485 순차 폴링만장치 수만큼 주기 증가sync read
9CAN 버스 오프 미처리통신 영구 정지restart-ms·감지
10CAN 소켓 큐 미배출프레임 유실매 주기 비우기
11CAN 버스 부하 과다저우선순위 지연부하율 계산
12EtherCAT을 일반 커널에서SAFE-OP 낙하RT 필수(6.6)
13/dev/ttyUSB0 직접 사용장치 순서 바뀌면 오동작udev 심볼릭 링크
14장치 신원 미확인잘못된 장치에 명령ID 확인
15고장 주입 테스트 없음오류 처리가 현장에서 첫 실행모의 장치
16오류율 미노출열화를 모름진단(6.9)

1번을 강조한다. USB-시리얼 지연 타이머는 이 장에서 비용 대비 효과가 가장 큰 항목이다. 설정 한 줄로 제어 주기가 자릿수로 달라질 수 있고, 모르면 "하드웨어 한계"로 오해한다.

6.7.11 체크포인트

  • 견고한 드라이버의 공통 원칙 일곱 가지를 말할 수 있는가. 타임아웃이 주기 예산 안에 있어야 하는 이유는?
  • 재연결에 백오프가 필수인 이유와, 재연결 성공 후의 안전한 기본 동작은?
  • USB-시리얼 지연 타이머가 무엇이며, 확인·변경 방법과 영구 적용 방법을 말할 수 있는가.
  • 시리얼 프레이밍의 다섯 요소와, 체크섬이 안전 문제인 이유를 설명할 수 있는가.
  • 동기화 상실을 처리하지 않으면 무슨 일이 생기는가.
  • RS-485 다중 드롭에서 주기가 장치 수에 비례하는 이유와 완화 방법은?
  • CAN의 성질 다섯 가지와, 프레임 설계 원칙 네 가지를 말할 수 있는가.
  • CAN 버스 부하율을 식으로 쓸 수 있는가. 버스 오프를 처리하지 않으면?
  • EtherCAT이 실시간에 적합한 구조적 이유와, 장치 수가 주기에 영향을 덜 주는 이유를 설명할 수 있는가.
  • EtherCAT의 대가 여섯 가지는? 왜 실시간 커널이 선택이 아닌가.
  • 버스 선택 축 일곱 가지를 비교할 수 있는가. 하드웨어 선택 단계에서 무엇을 요구사항에 넣어야 하는가.
  • udev 규칙이 해결하는 문제 네 가지와, 컨테이너에서의 주의점은?
  • 하드웨어 없이 개발하는 네 가지 방법과, 고장 주입이 왜 중요한가.
  • 드라이버가 진단으로 내보내야 할 항목 여섯 가지 이상과, 가장 유용한 예방 지표는?

다음 장 6.8는 이 통신의 반대편 — MCU 위에서 ROS 2를 실행하는 micro-ROS를 다룬다. 왜 필요하고, 어떤 제약이 있으며, 언제 커스텀 프로토콜 + 브리지가 더 나은가.