제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 공통 원칙
버스가 무엇이든 견고한 드라이버는 같은 구조를 갖는다.
| 원칙 | 설명 | 관련 장 |
|---|---|---|
| 명시적 상태 기계 | 연결·식별·동작·열화·재연결 | 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>누락 발견, 책 코드 스니펫엔 없음 — 추가해 해결). ID0x100·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
실시간 제어에서 사실상의 산업 표준이다.
왜 실시간에 적합한가:
| 특성 | 설명 |
|---|---|
| 온더플라이 처리 | 프레임이 지나가며 각 슬레이브가 자기 부분을 읽고 씀 |
| 한 프레임에 전체 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 버스 선택
| 축 | 시리얼 | CAN | EtherCAT |
|---|---|---|---|
| 주기 상한 | 낮음~중간 | 중간 | 높음 |
| 장치 수 영향 | 크다(폴링) | 중간(버스 부하) | 작다 |
| 구현 난이도 | 낮음 | 중간 | 높음 |
| 실시간 요구 | 낮음 | 중간 | 필수 |
| 노이즈 내성 | 낮음(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 함정 카탈로그
| # | 함정 | 증상 | 대처 |
|---|---|---|---|
| 1 | USB-시리얼 지연 타이머 기본값 | 주기가 60 Hz에 갇힘 | latency_timer=1·udev |
| 2 | 체크섬 없음 | 손상 데이터를 명령으로 | CRC 필수 |
| 3 | 동기화 상실 미처리 | 한 번 노이즈에 영구 고장 | 재동기화 로직 |
| 4 | 무한 대기 I/O | 주기 이탈·정지 | 타임아웃 필수 |
| 5 | 타임아웃이 주기보다 김 | 한 번 실패에 주기 붕괴 | 예산 안에 |
| 6 | 재연결 백오프 없음 | CPU·버스·로그 폭주 | 지수 백오프 |
| 7 | 재연결 후 이전 명령 재개 | 예상치 못한 동작 | 정지로 시작 |
| 8 | RS-485 순차 폴링만 | 장치 수만큼 주기 증가 | sync read |
| 9 | CAN 버스 오프 미처리 | 통신 영구 정지 | restart-ms·감지 |
| 10 | CAN 소켓 큐 미배출 | 프레임 유실 | 매 주기 비우기 |
| 11 | CAN 버스 부하 과다 | 저우선순위 지연 | 부하율 계산 |
| 12 | EtherCAT을 일반 커널에서 | 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를 다룬다. 왜 필요하고, 어떤 제약이 있으며, 언제 커스텀 프로토콜 + 브리지가 더 나은가.