4. 실기기 연동
4.1하드웨어 구성과 안전수칙
4부는 2부에서 시뮬레이션으로 다룬 TIKI-MINI 패키지를 진짜 로봇에서 돌립니다. 이 장은 전원을 켜기 전에 로봇이 어떤 하드웨어이고 어떻게 다뤄야 안전한지 정리합니다. 근거는 tiki_* 패키지의 소스, 제품 사양, 모터 컨트롤러 펌웨어 소스입니다.
제품 사양
| 항목 | 사양 |
|---|---|
| 차체 크기 | 200 x 60 x 210 mm (W x H x D, 카메라 미장착 기준), 무게 2 kg, 알루미늄과 스틸 |
| 로봇 컴퓨터 | Raspberry Pi 4 Compute Module (쿼드코어 Cortex-A72 ARMv8 64-bit, RAM 4 GB) |
| 로봇 제어기 | Espressif ESP32 |
| 모터 | DC 기어드 모터, 감속비 1:90, 엔코더 13 ppr(홀센서), 최대 175 rpm |
| 주변장치 | GPIO, I2C, SPI, RMT 등 |
| 배터리 | 18650 리튬이온 3S2P, 10.8 V 6700 mAh, 보호회로(PCM) 내장, KC 인증. 최대 방전 전류 5 A, 구동 시간 최대 8시간(모터를 돌리지 않으면 최대 17시간) |
| 센서 | 배터리 전압/전류 모니터링(내장), 9축 IMU, Sony 8M 픽셀 CSI 카메라 |
| 통신 | USB 시리얼(펌웨어 업그레이드, 리눅스 셸 접속), Wi-Fi 2.4 GHz/5 GHz IEEE 802.11 b/g/n/ac, Bluetooth 5.0, BLE |
| 소프트웨어 | Ubuntu Mate 22.04.4, Python 3.10, JupyterLab, ROS2 Humble. 언어는 Python과 C++ |
사양과 패키지를 맞춰 보면 이렇습니다.
- 로봇 컴퓨터가 Raspberry Pi 4 Compute Module(64-bit ARM)이고 ROS2 Humble이 들어 있어, 4.3장에서 확인할 로봇의 CPU 아키텍처와 ROS 배포판의 답이 됩니다. 사양에는 SDK가 Ubuntu Mate 22.04.4, Python 3.10, JupyterLab, ROS2 Humble로 적혀 있습니다. 실제로 로봇에 시리얼로 접속하면
Ubuntu 22.04.5 LTS(커널5.15.0-1070-raspi,aarch64)로 나옵니다(4.2장). - 모터 컨트롤러는 사양의 로봇 제어기인 ESP32이고, 로봇 컴퓨터와는
/dev/ttyAMA1로 AT 명령을 주고받습니다. 모터 드라이버는 H-브리지 방식입니다. - 엔코더는 13 ppr이고 코드는 한 바퀴를 1170틱으로 셉니다. 1170 = 13 x 90이고 제품 사양에 감속비 1:90, 엔코더 13 PPR이 적혀 있어 정확히 맞아떨어집니다.
- 모터 최대 속도 175 rpm은 감속 후 바퀴 축 기준입니다. 바퀴 반지름 0.0325 m로 환산하면 약 0.6 m/s(175/60 x 2π x 0.0325)이고, 엔코더로는 초당 약 3400틱입니다. 컨트롤러가 직진을 0.2 m/s로 제한하므로 하드웨어가 낼 수 있는 속도의 약 3분의 1만 씁니다.
- LiDAR는 Slamtec RPLIDAR C1입니다. 스캔 주파수는 보통 10 Hz이고 흰색 물체 기준 최대 12 m까지 잽니다. 패키지는
rplidar_ros로 이 LiDAR를 구동합니다. - 지지 바퀴(캐스터)는 차체 뒤쪽에 2개 있습니다. 구성품은 충전기와 USB-C 케이블입니다.
겉모습과 전원
- ① 전원 버튼: 누르면 빨간 불이 켜지고 부팅 부저음이 납니다.
- ② OLED 화면: 전원 버튼 오른쪽에 있고 부팅 중과 부팅 뒤의 상태를 보여 줍니다.
전원을 켜면 제어기 보드와 로봇 컴퓨터(SBC)가 동시에 부팅합니다. 제어기 부팅 화면에는 펌웨어 버전이 나옵니다(예: Control Firmware v 1.2.0). ROS2는 펌웨어 v1.2.0 이상부터 지원합니다. 버전이 낮으면 펌웨어 업로드 가이드로 먼저 업데이트해야 합니다. 부팅이 끝나면 OLED에 배터리 전압(예: 12.3 V), 배터리 전류(예: 300 mA), 모터 모드(IDLE 등)와 로그 영역이 표시됩니다. 로그가 없으면 EMPTY LOG로 나옵니다. 로봇 컴퓨터는 부팅에 평균 20초쯤 걸립니다.
패키지가 알려 주는 하드웨어
| 항목 | 값 | 근거 |
|---|---|---|
| 구동 바퀴 | 좌우 2개 | URDF의 left_wheel_joint, right_wheel_joint |
| 바퀴 반지름 | 0.0325 m | URDF, tiki_controllers.yaml |
| 바퀴 간격(중심 사이) | 0.1924 m | 조인트 y좌표 ±0.0962, 컨트롤러 wheel_separation |
| 엔코더 | 바퀴 한 바퀴에 1170틱 | diffdrive_tiki.cpp의 2π/1170 |
| 모터 컨트롤러 연결 | 시리얼 /dev/ttyAMA1, 921600 baud, 8N1, 흐름 제어 없음 | diffdrive_tiki.cpp의 on_init |
| 모터 컨트롤러 프로토콜 | 줄 단위 텍스트 AT 명령 | 같은 파일 |
| 2D LiDAR | USB 시리얼 /dev/ttyUSB0, 460800 baud, rplidar_ros 드라이버 | rplidar.launch.py, tiki.launch.py |
| 속도 제한 | 직진 ±0.2 m/s, 회전 ±0.85 rad/s | tiki_controllers.yaml |
바퀴 둘레는 2π x 0.0325 = 0.2042 m이고 한 바퀴가 1170틱이므로 틱 하나는 약 0.17 mm만큼 굴러간 거리입니다. 직진 최고 속도 0.2 m/s에서는 바퀴가 초당 약 0.98바퀴(6.15 rad/s) 돌고 엔코더는 초당 약 1146틱을 셉니다.
몸체 치수
URDF의 시각(visual) 도형에서 계산한 값입니다. 충돌 모델이 없는 시각용 모델이므로 실제 제품 치수와 몇 mm 다를 수 있습니다.
| 부분 | 크기 또는 위치(m) |
|---|---|
base_link 상자 | 0.1 x 0.16 x 0.048 |
back_side 상자 | 0.121 x 0.214 x 0.048, base_link에서 x축 방향으로 -0.1105 |
| 바퀴 | 반지름 0.0325, 폭 0.0265, 중심은 base_link에서 x -0.0085, y ±0.0962 |
| 전체 길이 | 약 0.221 (x가 -0.171에서 0.05까지) |
| 전체 폭 | 약 0.219 (바깥 바퀴 면 사이) |
| 전체 높이 | 약 0.143 (LiDAR 윗면까지, base_footprint 기준) |
| LiDAR | lidar_mount가 base_link 위 z 0.05에 있고(x, y는 base_footprint와 같음), 윗부분 laser는 yaw π로 장착 |
가로세로가 22 cm 안팎이고 높이가 14 cm 정도인 작은 로봇입니다. 제품 사양은 200 x 60 x 210 mm(W x H x D, 카메라 제외)입니다. 폭과 길이는 URDF 값과 1~2 cm 안쪽으로 비슷하고, 높이 60 mm는 LiDAR를 뺀 차체로 보입니다(URDF의 143 mm는 LiDAR 윗면까지). URDF에는 바퀴 둘 외에 지지점이 없지만 실제 제품은 차체 뒤쪽에 캐스터 2개가 받칩니다.
모터 컨트롤러와 AT 명령
tiki_base의 플러그인 DiffDriveTiki는 모터 컨트롤러(ESP32)와 시리얼 포트로 텍스트 명령을 주고받습니다. 펌웨어 쪽 시리얼은 UART2, 921600 bps, 8N1이고 모든 명령은 `
`으로 끝납니다. 플러그인이 보내는 명령은 다섯 가지입니다.
| 명령 | 언제 | 뜻 |
|---|---|---|
AT+MODE=2 | 시작할 때 한 번 | 모터 모드를 PID(2)로 설정 |
AT+PID=1.4,7.4,0,1.4,7.4,0 | 시작할 때 한 번 | 왼쪽 P, I, D 다음에 오른쪽 P, I, D 이득 |
AT+EL? | 매 제어 주기(읽기) | 왼쪽 엔코더 누적 카운트를 묻는다 |
AT+ER? | 매 제어 주기(읽기) | 오른쪽 엔코더 누적 카운트를 묻는다 |
AT+M=왼쪽,오른쪽 | 매 제어 주기(쓰기) | 두 바퀴의 목표 속도 정수 |
응답 형식은 두 가지입니다. 쓰기 명령은 OK 한 줄만 돌려줍니다. 읽기 명령은 OK 한 줄을 먼저 돌려주고 이어서 값 한 줄을 돌려줍니다. 값이 여럿이면 쉼표로 구분합니다. 그래서 읽기는 두 줄입니다.
모터 모드는 0 NONE(정지), 1 PWM, 2 PID, 3 ROS입니다. 범위를 벗어난 값은 0으로 처리하고, 모드를 바꾸면 엔코더 카운트와 PID, 목표값이 모두 0으로 초기화되며 모터가 멈춥니다. AT+PID의 이득은 쓰면 플래시에 저장되어 재부팅 뒤에도 유지됩니다.
AT 명령 전체 표 (펌웨어 소스 기준)
| 명령 | 종류 | 인자와 응답 |
|---|---|---|
AT+MODE=n / AT+MODE? | 쓰기, 읽기 | 모터 모드 0~3. 위에서 설명한 모드 번호 |
AT+PID=lKp,lKi,lKd,rKp,rKi,rKd / AT+PID? | 쓰기, 읽기 | 왼쪽 P, I, D 다음에 오른쪽 P, I, D 순서의 실수 여섯 개. 기본값은 좌우 모두 0.7, 3.7, 0 |
AT+TPID=... / AT+TPID? | 쓰기, 읽기 | ROS 모드(3)에서 쓰는 PID. 형식은 AT+PID와 같고 기본값 0.7, 3.7, 0 |
AT+M=왼쪽,오른쪽 | 쓰기 | 두 바퀴의 목표 속도 정수. 오른쪽 값은 부호를 바꿔 적용(두 모터가 서로 마주 보게 장착) |
AT+ML=값, AT+MR=값 | 쓰기 | 왼쪽, 오른쪽 바퀴만 |
AT+SPD? | 읽기 | 왼쪽RPM,오른쪽RPM,왼쪽틱,오른쪽틱. 틱은 최근 샘플 구간(33.3 ms)의 증가분 |
AT+EL?, AT+ER? | 읽기 | 왼쪽, 오른쪽 엔코더 누적 카운트 |
AT+IMU? | 읽기 | 오일러각 x,y,z(IMU가 꺼져 있으면 0,0,0) |
AT+IR? | 읽기 | 적외선 센서 세 개의 디지털 값 왼쪽,가운데,오른쪽 |
AT+TOF? | 읽기 | 거리 mm(측정 실패나 비활성이면 -1) |
AT+CUR? | 읽기 | 배터리 전류 mA |
AT+BAT? | 읽기 | 배터리 전압 V |
AT+LED=번호,R,G,B | 쓰기 | RGB LED 색 |
AT+OLED=문자열 / AT+OLEDCLEAR | 쓰기, 실행 | OLED 로그 줄 추가 / 지우기 |
AT+BUZON=주파수 / AT+BUZOFF | 쓰기, 실행 | 부저 켜기(0 이하이면 끔) / 끄기 |
AT+META? | 읽기 | 펌웨어 버전 문자열 |
엔코더는 33.3 ms(30 Hz)마다 샘플링하고 ENC_CPR은 1170(13 PPR x 감속비 90)입니다.
제어 주기는 controller_manager의 update_rate 50 Hz, 곧 20 ms입니다. 컨트롤러 매니저는 한 주기에 read(), 컨트롤러 갱신, write()를 차례로 실행합니다. 이 로봇에서는 read()가 AT+EL?과 AT+ER?를 보내고, write()가 AT+M=...을 보냅니다.
코드는 상수 하나로 변환합니다.
double rad_per_tick = (2 * M_PI) / 1170;
// read(): 위치[rad] = 엔코더 값 * rad_per_tick
// write(): 목표 틱 = static_cast<int>(바퀴 명령[rad/s] / rad_per_tick)
static_cast<int>는 소수점 아래를 0 쪽으로 버립니다. 그래서 0.2 m/s 직진은 6.1538 rad/s이고 1145.9를 버린 1145가 AT+M=1145,1145로 나갑니다. 제자리 회전 0.85 rad/s는 바퀴가 각각 반대로 2.516 rad/s이므로 AT+M=-468,468처럼 나갑니다.
아래 컴포넌트에서 속도와 틱을 바꿔 가며 한 주기를 단계별로 재생해 보세요. 엔코더 값은 예시입니다.
숫자를 바꾸면 계산이 다시 됩니다. 엔코더 값은 예시입니다.
직전 주기의 틱 (왼쪽, 오른쪽)
이번 주기에 늘어난 틱 (왼쪽, 오른쪽)
TX AT+MODE=2\r\n // 모터 모드를 PID(2)로 설정
RX OK // 쓰기 명령은 OK 한 줄
시작 1: 모드 설정 (on_init, 1회)
포트를 연 직후 한 번만 보냅니다. 응답 한 줄(OK)을 읽어 로그로 찍습니다.
한 주기에 보내는 명령은 30바이트이고, 921600 baud 8N1(바이트당 10비트)에서 송신만 약 0.33 ms입니다. 제어 주기 20 ms(50 Hz)와 비교해 보세요. 응답을 받는 시간은 계산에 넣지 않았습니다.
코드를 읽으며 눈여겨볼 점은 응답 줄 수입니다.
응답 줄 수가 명령마다 다릅니다. AT+EL?과 AT+ER?는 두 줄을 읽고 첫 줄(OK)을 버린 뒤 둘째 줄의 값을 atoi로 정수 변환합니다. AT+M과 시작 명령은 OK 한 줄만 읽습니다.
LiDAR
tiki_bringup의 rplidar.launch.py는 rplidar_ros의 rplidar_node를 실행합니다. 기본값은 channel_type serial, serial_port /dev/ttyUSB0, serial_baudrate 460800, frame_id laser, angle_compensate true, scan_mode Standard입니다. 측정 결과는 /scan 토픽(sensor_msgs/msg/LaserScan)으로 나옵니다. URDF에서 laser 링크는 yaw π(약 180도)로 놓여 있어 laser 프레임의 x축이 로봇의 뒤쪽을 향합니다. 스캔이 로봇 모델과 어긋나 보이면 4.4장에서 RViz로 확인하세요.
속도 제한과 숫자 읽기
컨트롤러 설정 tiki_controllers.yaml은 linear.x를 ±0.2 m/s, angular.z를 ±0.85 rad/s로 제한하고 가속도와 저크 제한은 두지 않습니다. 명령이 0에서 0.2 m/s로 바로 바뀌어도 부드럽게 올려 주는 단계가 없다는 뜻입니다.
안전수칙
작은 로봇이라도 모터는 케이블과 손가락을 문 채로 돌 수 있습니다. 안전을 위해 다음 순서를 습관으로 만드세요.
- 첫 실행은 가능하면 바퀴를 바닥에서 띄운 채로 합니다. 방향과 반응을 확인한 뒤 바닥에 내립니다.
- 속도 명령은 0.05 m/s 이상으로 시작합니다. 컨트롤러가 0.2 m/s로 제한하지만 처음에는 그 절반 이하로 확인합니다.
- 정지 명령을 입력해 둔 터미널을 항상 하나 열어 둡니다.
- 이상이 보이면 원인을 찾기 전에 먼저 멈춥니다. 소프트웨어 정지가 듣지 않으면 로봇 뒤쪽의 모터 전원 버튼으로 모터 전원을 끕니다.
전원과 배터리
- 전원을 끌 때는 전원 버튼으로 shutdown합니다.
- 모터 전원 버튼은 로봇 뒤쪽에 있고 모터 전원만 켜고 끕니다. 로봇 컴퓨터는 켜진 채로 모터만 멈출 때 씁니다.
- 배터리는 3S2P(10.8 V, 6700 mAh) 리튬이온팩이고 최대 방전 전류는 5 A입니다. 잔량은 OLED의 전압 표시로 보고, 저전압이 되면 충전 알림이 뜹니다. 펌웨어는 저전압 경고 상태에서 모터 모드를 NONE(0)으로 바꿔 모터를 멈춥니다.
- 충전 중에는 주행이 되지 않게 되어 있습니다. 충전기를 분리한 뒤 주행합니다.
- 배터리 전압과 전류는 ROS2 토픽으로 나오지 않습니다. 제어기의 AT 명령(
AT+BAT?전압 V,AT+CUR?전류 mA)으로만 읽습니다.
주행 전 점검표
실습할 때마다 처음부터 확인합니다. 체크 상태는 이 브라우저에만 저장됩니다.
확인한 항목: 0 / 17
체크 상태는 이 브라우저에만 저장됩니다. 실습할 때마다 모두 해제하고 처음부터 확인하세요.
1) 전원을 켜기 전
사람 발, 가방, 전선, 문턱이 없는지 본다. 로봇 몸체는 URDF 기준 가로세로 약 22 cm다.
낙하하면 로봇과 바닥 모두 다친다. 첫 실습은 바닥에서 하는 편이 안전하다.
받침대가 로봇을 안정적으로 받치는지 확인한다.
케이블이 바퀴에 감기면 모터에 부하가 걸리고 LiDAR 포트가 빠진다.
OLED의 전압 표시를 본다. 충전 중에는 주행이 되지 않는다.
로봇 뒤쪽에 있고 모터 전원만 끈다. 손이 닿는 위치인지 확인한다.
2) launch 하기 전
두 컴퓨터에서 echo $ROS_DOMAIN_ID를 실행해 값을 비교한다.
ls -l /dev/ttyAMA1 /dev/ttyUSB0, 그리고 groups 출력에 dialout이 있는지 본다.
ros2 node list가 비어 있어야 한다. 같은 포트를 두 프로세스가 열면 응답이 뒤섞인다.
영(0) Twist를 발행하는 명령을 입력해 두고 Enter만 누르면 되게 한다.
3) 주행 중
컨트롤러가 0.2 m/s, 0.85 rad/s로 제한하지만 낮은 값에서 방향과 반응부터 확인한다.
ros2 topic info /cmd_vel의 Publisher count로 확인한다. 같은 도메인의 다른 사람 프로그램도 포함된다.
정지 명령 입력, 안 되면 로봇 뒤쪽의 모터 전원 버튼으로 모터 전원을 끈다.
4) 끝난 뒤
하드웨어 플러그인은 종료할 때 정지 명령을 보내지 않고 펌웨어에는 워치독이 없다. 움직이는 중에 끄지 않는다.
소프트웨어로 멈추는 방법
영(0) Twist 발행
가장 먼저 쓰는 방법은 속도 0을 보내는 것입니다. 정지 터미널에 미리 입력해 두세요.
ros2 topic pub --once -w 1 /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}"
--once는 한 번만 보내고 끝내라는 옵션이고 -w 1은 구독자가 한 개 이상 연결된 뒤에 보내라는 옵션입니다. 둘 다 Humble의 ros2 topic pub 소스에서 확인했습니다. 연결이 불안한 네트워크에서는 한 번 보낸 명령이 유실되면 로봇이 멈추지 않으므로 같은 명령을 반복해서 보내세요. 모터 컨트롤러 펌웨어는 속도 0 명령을 받아야 멈춥니다. 지속해서 보내려면 --once -w 1 대신 -r 10을 쓰고 로봇이 멈춘 것을 본 뒤 Ctrl+C로 끕니다.
명령이 끊기면 멈춥니다
diff_drive_controller에는 cmd_vel_timeout이라는 파라미터가 있고, Humble 소스의 기본값은 0.5초입니다. 이 프로젝트의 tiki_controllers.yaml은 이 값을 바꾸지 않으므로 마지막 명령이 0.5초보다 오래되면 컨트롤러가 속도를 0으로 바꿉니다. 소스의 update()에서 확인했고, 값 0.0은 타임아웃을 끕니다.
정지는 이 타임아웃에만 맡기지 말고 영 Twist를 직접 보낸 뒤 바퀴가 멈춘 것을 눈으로 확인하세요.
launch를 끄는 방법과 주의
launch 터미널에서 Ctrl+C를 누르면 모든 프로세스가 종료됩니다. 종료하기 전에 아래 순서를 지킵니다.
- 영 Twist를 보내거나 조종 프로그램의 정지 키를 누릅니다.
- 바퀴가 멈춘 것을 눈으로 확인합니다.
- 그다음에 launch를 Ctrl+C로 끕니다.
컨트롤러만 멈추고 싶다면 ros2 control set_controller_state diff_controller inactive가 있습니다. 컨트롤러가 비활성화되면 소스상 halt()가 바퀴 속도 명령을 0으로 만듭니다. 이 명령의 인자는 ros2controlcli Humble 소스에서 확인했지만 실제 로봇에서 시험하지는 못했습니다. 로봇이 서 있을 때 먼저 연습해 보세요.
한눈에 정리
- 구동부는 반지름 0.0325 m 바퀴 둘, 바퀴 간격 0.1924 m, 엔코더 1170틱/회전이다.
- 모터 컨트롤러(ESP32)는
/dev/ttyAMA1(921600 baud, 8N1)에서 AT 명령으로 제어한다:AT+MODE=2,AT+PID=...,AT+EL?,AT+ER?,AT+M=l,r. 쓰기는OK한 줄, 읽기는OK와 값 두 줄로 응답한다. - 한 제어 주기(20 ms)마다 두 번 읽고 한 번 쓴다. 속도는
static_cast<int>로 버려 틱 정수가 된다. - LiDAR는 RPLIDAR C1이고
/dev/ttyUSB0에서rplidar_ros로 동작해/scan을 낸다. - 컨트롤러 제한은 0.2 m/s와 0.85 rad/s이다. 명령이 끊기면
cmd_vel_timeout(기본값 0.5초)에 따라 컨트롤러가 속도를 0으로 바꾼다. - 정지는 영 Twist 발행으로 하고, 멈춘 것을 확인한 뒤 launch를 끈다. 모터 전원 버튼은 로봇 뒤쪽에 있다.
확인 문제
Q1. 컨트롤러가 왼쪽 바퀴에 3.0 rad/s를 지시했습니다. 플러그인이 보내는 AT 명령의 왼쪽 값은 얼마입니까?
3.0 / (2π/1170) = 3.0 / 0.0053704 = 558.6이고 소수점 아래를 버리므로 558입니다. 오른쪽 값과 합쳐 AT+M=558,오른쪽값으로 나갑니다.
Q2. 키보드 조종 프로그램을 끄고 0.5초쯤 뒤에 로봇이 멈췄습니다. 이 동작은 어디서 오고, 정지할 때는 무엇을 합니까?
diff_drive_controller의 cmd_vel_timeout(기본 0.5초)이 오래된 명령을 0으로 바꾸기 때문입니다. 정지할 때는 이 타임아웃에만 맡기지 말고 영 Twist를 직접 보내 멈춘 것을 눈으로 확인합니다.
Q3. 주행 중인 로봇의 launch를 끄기 전에 어떤 순서를 지킵니까?
먼저 영 Twist를 보내거나 조종 프로그램의 정지 키를 누르고, 바퀴가 멈춘 것을 눈으로 확인한 뒤 launch를 끕니다.
다음 장
4.2장에서 내 VM과 로봇을 같은 네트워크에 연결하고 ssh와 ROS_DOMAIN_ID, 시간 동기화를 맞춥니다. 그 전에 점검표를 인쇄하거나 메모해 두고 모터 전원 버튼의 위치를 손으로 확인해 두세요.