---
title: "하드웨어 초보가 2.5주 만에 자작 옥토콥터를 띄우고, 모터 고장까지 버티는 강화학습 컨트롤러를 만든 이야기"
published: 2026-06-28T04:17:44.000Z
canonical: https://jeff.news/article/4450
---
# 하드웨어 초보가 2.5주 만에 자작 옥토콥터를 띄우고, 모터 고장까지 버티는 강화학습 컨트롤러를 만든 이야기

하드웨어 경험이 거의 없던 개발자가 Fusion 360, CNC 가공, Betaflight, MuJoCo, PPO를 써서 커스텀 옥토콥터를 만들고 실제 비행까지 성공한 기록이다. 목표는 단순 드론 제작이 아니라, 모터 1개나 2개가 죽어도 강화학습 정책이 추력을 재분배해서 버티는 컨트롤러를 실제 하드웨어에 올리는 것. 시뮬레이션에서는 단일·이중 모터 고장뿐 아니라 일부 3개 모터 고장까지 버텼고, 이제 관건은 시뮬레이션에서 배운 정책이 현실에서도 통하느냐다.

- 하드웨어 경험 거의 없는 개발자가 2.5주 만에 커스텀 옥토콥터를 직접 설계하고 실제로 띄움
  - Fusion 360으로 CAD를 만들고, G10 유리섬유와 탄소섬유를 CNC로 가공한 뒤 손으로 조립함
  - 시작 전 상태가 꽤 빡셈. 드론을 날려본 적도 없고, CAD도 처음이고, 납땜도 거의 안 해봤고, 복잡한 강화학습 정책도 처음이었다고 함

- 목표는 그냥 멋진 드론이 아니라, 모터가 죽어도 버티는 강화학습 기반 비행 컨트롤러임
  - 최종 목표는 단일, 이중, 심지어 일부 4개 모터 고장까지 시뮬레이션에서 버티는 컨트롤러를 만들고, 그걸 실제 하드웨어에 제로샷으로 올리는 것
  - 현재 단계는 프레임 제작과 일반 비행은 성공했고, 강화학습 정책은 시뮬레이션에서 성과가 나온 상태

- 프로젝트 진행 단계도 꽤 명확함
  - 1단계는 CAD 설계, CNC 절단, 프레임·모터·프로펠러 조립
  - 2단계는 전자부품 배선 후 일반 비행 컨트롤러 기반 옥토콥터로 띄우기
  - 3단계는 일반 비행과 2개 모터 고장을 버티는 강화학습 정책 훈련
  - 4단계는 시뮬레이션-현실 전이(sim-to-real)를 끝내고 실제 필드 테스트에서 임의의 2개 모터를 꺼도 살아남게 만들기

## 시뮬레이션에서 먼저 터진 문제들

- 처음 학습이 안 된 이유는 하이퍼파라미터 문제가 아니라 액션 출력 설계 버그였음
  - Gaussian policy가 무한 범위 평균값을 내는데, 환경은 모터 명령을 0~1로 하드 클리핑하고 있었음
  - PPO는 클리핑 전 값으로 그래디언트를 계산하니, 모터 명령이 클립 경계 밖으로 밀려나면 다시 안쪽으로 끌어오는 신호가 사라짐
  - 8개 모터 중 일부가 이런 식으로 포화되면 드론이 한쪽으로 기울며 넘어지고, 하이퍼파라미터를 아무리 만져도 안 고쳐지는 종류의 문제였음
  - 해결책은 tanh로 출력을 눌러서 hover throttle 주변의 residual 형태로 명령을 내게 만드는 것. 이 한 방으로 학습 전 생존 시간이 7스텝에서 205스텝으로 뛰었다고 함

- 보상 설계도 함정이 있었음. 살아있는 것 자체가 보상이 아니었던 셈
  - 오픈루프 hover 테스트를 찍어보니 매 스텝 보상이 0.00으로 나옴
  - 실제로 드론이 안정되는 높이 약 1.9m에서 +0.1 생존 보너스가 -0.1 고도 패널티와 정확히 상쇄되고 있었음
  - 결국 200스텝 버티다 추락하는 정책과 바로 추락하는 정책이 같은 리턴을 받는 구조였고, PPO 입장에서는 버틸 이유가 없었음
  - 생존 보상을 0.1에서 1.0으로 올리자 hover가 매 스텝 +0.9를 벌게 됐고, 그제야 정책이 살아남는 행동을 배우기 시작함

> [!IMPORTANT]
> 이 글의 제일 좋은 디테일은 강화학습이 마법처럼 해결했다는 얘기가 아니라, 액션 포화와 보상 상쇄라는 아주 현실적인 버그 2개가 학습 전체를 망치고 있었다는 점임.

- 최종 시뮬레이션 정책은 4만3400개 파라미터짜리 MLP임
  - 2000만 스텝 학습 뒤, 모터 고장 종류별 생존 시간과 보상이 크게 개선됨
  - 학습하지 않은 3개 모터 고장 상황에도 일부 일반화됐고, 물리적으로 회복 가능한 케이스에서는 버텼다고 함
  - 3개 인접 모터를 꺼서 물리적으로 회복 불가능한 케이스에서도 바로 뒤집히지 않고 7.2초 동안 버티며 가라앉았다고 함

- 예상과 달리 ‘보상 불가능한 yaw’라고 생각했던 케이스도 완전히 망하진 않았음
  - 같은 회전 방향 모터 2개가 90도 간격으로 죽으면 이론상 yaw 토크가 안 맞아 자유롭게 빙글빙글 돌아야 한다고 봤음
  - 그런데 정책은 모든 해당 케이스에서 heading drift를 약 초당 13도 안쪽으로 붙잡음
  - 작성자가 만든 ‘이 케이스는 답 없음’ 휴리스틱이 너무 비관적이었다는 결론

## 왜 하필 강화학습인가

- 작성자는 애초에 이 프로젝트를 강화학습을 현실 하드웨어에서 배우기 위한 장난감으로 설계했다고 밝힘
  - 즉 ‘드론 문제를 풀려다 보니 강화학습을 썼다’기보다는, ‘강화학습 프로젝트를 하고 싶어서 고장 내성 옥토콥터를 골랐다’에 가까움
  - 그래도 기술적으로 강화학습을 고른 이유는 있음

- 기존 모델 예측 제어(MPC)와 비교했을 때, 강화학습은 추론 비용이 작다는 장점이 있음
  - MPC는 매 타임스텝마다 최적화 문제를 풀어야 함
  - 반면 강화학습 정책은 약 5만 파라미터짜리 네트워크를 한 번 forward pass 하는 구조라, 8개 모터 명령에도 1ms 안팎으로 가능할 거라고 봄

- 고장 상태를 따로 감지하지 않아도 된다는 점도 큼
  - MPC는 보통 시스템 상태를 꽤 잘 알아야 하고, 모터가 죽었는지 알려주는 fault detector가 별도로 필요할 수 있음
  - 강화학습 정책은 명령한 동작과 실제 관측된 움직임의 차이를 보고 고장 상태를 암묵적으로 추론하도록 학습시킬 수 있음

- 모델 오차에도 강화학습 쪽이 더 편할 수 있음
  - 싼 모터 8개가 완전히 동일할 리 없고, 관성 텐서 측정도 대충의 근사치일 수밖에 없음
  - 도메인 랜덤화(domain randomization)를 강하게 걸면, 정책이 이런 모델 오차를 학습 중에 미리 맞고 자라게 됨
  - 작성자도 MPC가 더 믿을 만한 선택일 수는 있지만, 이 프로젝트에서는 재미와 학습 목적 때문에 강화학습을 1순위로 둔다고 함

## 하드웨어 쪽 숫자가 은근히 미쳤음

- 드론 무게는 배터리 장착 기준 약 1kg임
  - 비행 컨트롤러를 제외한 거의 완성 상태에서 측정한 값
  - 설계 초기에는 무게 최적화를 크게 신경 쓰지 않았고, 이후 부품 추가부터는 무게를 더 의식하기 시작함

- 추력 여유가 엄청 큼
  - 각 모터는 완충된 6S 배터리에서 70% throttle 기준 약 950gf, 최대 1393gf 추력을 냄
  - 8개 모터 전체로는 70% throttle만 써도 7600gf, 즉 7.6kg 추력
  - 1kg짜리 드론 기준 추력대중량비가 7.6:1이고, hover에는 모터당 125gf 정도만 필요함
  - hover throttle이 15~20% 수준이라 모터 손실을 버틸 여유가 꽤 넓음

- 옥토콥터는 모터 1개 고장에는 원래 꽤 강함
  - 모터 하나가 죽어도 전체 추력 용량은 약 11000gf에서 9750gf로 줄어드는 정도라, 5kg 가까운 드론까지도 2:1 추력대중량비를 유지할 수 있음
  - Betaflight의 PID 루프는 수 kHz로 돌며, 자이로가 기울어졌다고 보고하면 살아있는 반대편 모터를 더 밀어줌
  - yaw 불균형도 믹서가 어느 정도 보정해서, 느린 yaw drift나 반응성 저하는 있어도 보통 바로 떨어지진 않음

- 진짜 문제는 2개 모터가 죽는 순간부터임
  - 특히 같은 회전 방향 모터 2개가 90도 간격으로 죽으면 yaw 토크 불균형과 공간적 추력 비대칭이 동시에 터짐
  - 기존 static mixer는 이미 죽은 모터에도 계속 추력을 요구하기 때문에 제어가 무너질 수 있음
  - 작성자가 강화학습으로 풀고 싶은 실패 모드가 바로 이 지점임

## 시뮬레이션-현실 전이를 어떻게 준비하나

- 시뮬레이터는 MuJoCo를 선택함
  - Isaac Lab 같은 NVIDIA 중심 도구는 맥 환경에서 부담이 컸고, 이 문제는 8개 추력점이 붙은 단일 강체라 MuJoCo로 충분하다고 판단함
  - 노트북 CPU에서 약 128개 환경을 병렬로 돌릴 수 있다고 함

- 모델은 CAD가 아니라 실제 측정값을 기반으로 만들고 있음
  - 전체 질량, 관성 텐서, 모터 추력 곡선, 모터 시간 상수, hover throttle point를 측정 대상으로 잡음
  - 관성 텐서는 bifilar pendulum 테스트로 측정했고, 20회 진동 시간을 기준으로 계산함
  - 최종 시스템 질량은 1.177kg으로 측정한 계산도 등장함

- 시뮬레이션-현실 전이에서 특히 무서운 건 모터 지연과 루프 지연임
  - 실제 모터는 명령한 속도에 도달하는 데 20~50ms가 걸림
  - 실제 드론에서는 IMU 읽기, 추론, 시리얼 쓰기, ESC 반응까지 합쳐 약 15~30ms 지연이 생김
  - 시뮬레이션에서 즉시 반응하는 모터로만 학습하면, 실제 하드웨어에 올렸을 때 정책이 과하게 흔들리거나 진동할 수 있음

> [!WARNING]
> 모터 레벨 제어에서 지연 시간을 빼먹으면 시뮬레이션에서는 천재처럼 보이던 정책이 실제 드론에서는 바로 불안정해질 수 있음. 작성자가 루프 지연을 가장 무서운 요소로 본 이유가 이거임.

- 도메인 랜덤화도 꽤 세게 걸 예정임
  - 질량은 ±10%, 모터별 추력 상수는 ±15% 범위에서 흔듦
  - 중심 질량, 배터리 전압 저하, 센서 노이즈도 랜덤화 대상
  - cheap motor들이 서로 완전히 같지 않다는 걸 직접 측정한 8개 데이터 포인트가 있다고 함. 이런 디테일이 진짜 현실적임

## 학습 구조와 배포 아키텍처

- 학습 알고리즘은 PPO와 PufferLib 조합을 고름
  - SAC는 샘플 효율이 좋지만, 작성자는 시뮬레이션 스텝이 거의 공짜라 샘플 효율이 가장 큰 문제가 아니라고 봄
  - PPO는 병렬 환경을 많이 돌리기 쉽고, 강한 랜덤화가 들어간 환경에서도 비교적 안정적이라고 판단함

- 비대칭 액터-크리틱(asymmetric actor-critic)도 사용함
  - 학습 중 critic은 실제 드론에서는 알 수 없는 정보를 볼 수 있음. 예를 들면 어떤 모터가 죽었는지, 정확한 추력 상수, 실제 속도 같은 값
  - actor는 실제 센서에서 얻을 수 있는 관측값만 봄
  - 배포할 때 critic은 버리므로, 훈련 안정성을 얻으면서 실제 배포 입력 제약은 유지하는 구조임

- 별도 fault detector는 일단 두지 않음
  - 정책은 최근 5개 관측과 액션 프레임을 보고, 명령과 실제 움직임의 차이에서 고장을 추론해야 함
  - 이게 잘 되면 ‘어떤 모터가 죽었다’는 명시적 진단 없이도 추력 재분배가 가능해짐

- 배포 계획은 중간에 크게 바뀜
  - 초기에는 Raspberry Pi 4나 Teensy 같은 companion computer를 붙이고, MSP를 통해 Betaflight에 8개 모터 명령을 보내려 했음
  - 그런데 Betaflight의 안전 모델과 충돌함. disarm이나 링크 손실 상황에서 모터가 안정적으로 멈추지 않을 수 있다는 게 문제
  - 그래서 별도 마이크로컨트롤러를 빼고, 이미 탑재된 STM32H743 480MHz M7 비행 컨트롤러의 Betaflight 펌웨어 안에 정책을 직접 컴파일하는 방향으로 바꿈
  - 이러면 루프 지연도 크게 줄어듦

```mermaid
sequenceDiagram
    participant 센서 as IMU와 센서
    participant 펌웨어 as Betaflight 펌웨어
    participant 정책 as 강화학습 정책
    participant 모터 as 8개 모터
    센서->>펌웨어: 자세와 자이로 값 전달
    펌웨어->>정책: 최근 관측과 액션 히스토리 입력
    정책->>펌웨어: 모터별 추력 명령 8개 출력
    펌웨어->>모터: DSHOT 기반 모터 명령 적용
    모터->>센서: 실제 움직임 변화 발생
    센서->>펌웨어: 명령과 관측의 차이가 다음 입력에 반영
```

- 실제 실험은 꽤 단순하고 무서움
  - 시뮬레이션에서 생존율이 괜찮으면 정책을 올림
  - 실제로 날린 뒤 송신기에서 모터를 꺼봄
  - 수백만 번의 시뮬레이션 추락이 현실에서 의미 있는 제어 정책이 됐는지 확인하는 단계만 남음

---

## 기술 맥락

- 이 프로젝트의 핵심 선택은 기존 PID 루프 위에 보조 AI를 얹는 게 아니라, 강화학습 정책이 8개 모터 명령을 직접 내리게 하려는 거예요. 그래야 모터가 죽었을 때 고정된 믹서가 계속 죽은 모터에 추력을 배분하는 문제를 피할 수 있거든요.

- MuJoCo와 PPO를 고른 이유도 현실적이에요. 작성자의 문제는 복잡한 3D 월드가 아니라 8개 추력점이 달린 강체 제어라서 MuJoCo로 충분하고, CPU에서 병렬 환경을 많이 돌릴 수 있으니 PPO의 낮은 샘플 효율이 큰 약점이 아니에요.

- 도메인 랜덤화가 중요한 이유는 실제 드론의 물리 파라미터가 깔끔하지 않기 때문이에요. 모터별 추력 차이, 관성 측정 오차, 배터리 전압 저하, 센서 노이즈를 학습 때부터 흔들어야 현실에 올렸을 때 정책이 작은 오차에 바로 무너지지 않아요.

- 배포 아키텍처를 STM32H743 펌웨어 내부 실행으로 바꾼 것도 꽤 큰 결정이에요. 외부 보드에서 시리얼로 8개 모터 명령을 보내면 안전 모델과 지연 시간이 문제 되는데, 비행 컨트롤러 안에서 직접 돌리면 루프가 짧아지고 disarm 같은 기본 안전 동작과도 더 잘 맞출 수 있어요.

- 보상 설계 버그 사례가 특히 실무적이에요. 생존 보너스와 고도 패널티가 상쇄돼서 ‘오래 버티기’가 아무 가치 없는 상태가 됐고, 이 상태에서는 모델 크기나 알고리즘을 바꿔도 답이 안 나와요. 강화학습 프로젝트에서 보상 로그를 직접 까봐야 하는 이유가 딱 이런 데 있어요.

## 핵심 포인트

- 0년 하드웨어 경험에서 출발해 2.5주 만에 직접 설계·가공·조립한 옥토콥터를 띄움
- 최종 목표는 모터 고장 상황에서도 살아남는 강화학습 기반 비행 제어 정책을 실제 드론에 올리는 것
- 시뮬레이션 정책은 4만3400개 파라미터짜리 MLP이며 2000만 스텝 학습 뒤 일부 3모터 고장까지 일반화함
- PPO, MuJoCo, 도메인 랜덤화, 비대칭 액터-크리틱 같은 시뮬레이션-현실 전이 기법이 핵심으로 쓰임
- 별도 라즈베리파이나 마이크로컨트롤러 대신 STM32H743 비행 컨트롤러 펌웨어에 정책을 직접 넣는 방향으로 아키텍처를 바꿈

## 인사이트

재밌는 포인트는 ‘드론을 만들었다’가 아니라 제어 문제를 일부러 어렵게 만든 다음, 강화학습이 기존 PID·믹서가 깨지는 구간을 넘을 수 있는지 실험한다는 점임. 실무 개발자 입장에서도 시뮬레이션 버그, 보상 설계, 배포 아키텍처, 지연 시간 같은 ML 시스템의 지저분한 현실이 한 글에 꽤 잘 드러난다.
