무엇을 만들고 있나What I Am Building

우측 편마비 환자용 다리 보조기구다. 한 줄로 요약하면 이렇다. 건강한 쪽 다리의 관절 각도를 읽어, 마비된 쪽 다리가 따라가야 할 목표 각도를 8비트 MCU 위의 신경망이 실시간으로 생성한다.

This is a lower-limb assistive orthosis project for right-side hemiplegia. In one sentence: a quantized neural network on an 8-bit MCU reads the joint angles of the unaffected leg and generates real-time target angles for the paretic leg to follow.

좌우로 밀어서 표 전체 내용을 확인하세요
항목내용
기간2025.07 ~ 진행 중
제어기Arduino Nano (ATmega328P)
구동L298N 모터 드라이버 2개 + JGB37-520 DC 기어드모터·엔코더 2개
기구카본튜브 6×4 링크, 3590S 다회전 포텐셔미터, 12V 파워서플라이, KiCad 회로 설계
소프트웨어Arduino C++ / Unity 6000.0.68f1 (ArticulationBody 시뮬 + 자동 검증) / Python 3 · PyTorch
역할1인 개발 — 기구·회로 설계, 펌웨어, 물리 시뮬레이션, 생체역학 해석, 학습 파이프라인
Swipe horizontally to view the full table
ItemDetails
Timeline2025.07 ~ In Progress
ControllerArduino Nano (ATmega328P)
Actuation2× L298N motor drivers + 2× JGB37-520 compact low-wattage geared motors with encoders
Mechanics & Hardware6×4 mm carbon tube links, 3590S multi-turn potentiometers, 12V bench power supply, KiCad PCB design
SoftwareArduino C++ / Unity 6000.0.68f1 (ArticulationBody simulation + automated checks) / Python 3 · PyTorch
RoleSolo development — mechanical & circuit design, firmware, physics simulation, biomechanical analysis, training pipeline

우측 편마비 사용자를 상정하고 설계했다. 지금은 구현 중이고, 이 글에 적은 검증은 전부 계산과 PC 대조 단계다. 사람이 착용한 상태로 시험한 단계가 아니다.

The system is designed around a right-side hemiplegic user profile. It is currently under development, and all verification reported in this post comes from analytical calculations and PC cross-checks—not human subject testing.

Unity 시뮬레이션 화면. 대퇴와 하퇴로 이루어진 다리 모델에 카본 링크와 커프, 관절 모듈이 붙어 있다
Unity에 실측 치수만으로 생성한 다리와 보조기 조립. 초록색이 커프, 빨간색이 관절 모듈이다.Leg and orthosis assembly generated in Unity from measured dimensions. Green parts are cuffs; red modules are joint actuators.
SynchroLeg 초기 2D 착용 개념도
초기 2D 착용 개념 설계도. 환측(우측) 보조기 프레임, 건측 추종 센서 벨트, 초기 골반 지지 밴드 배치 구상(프로토타입 초기 모델).Early 2D wearing concept diagram showing the affected-side (right) orthosis frame, unaffected-side sensor belt, and initial pelvic support band layout (early prototype concept).
초기 3D CAD 다리 장착 어셈블리 모델
초기 3D CAD 어셈블리 모델링. 인체 다리에 대퇴/하퇴 밴드와 관절 구동 모듈을 가배치해 본 프로토타입 초기 모델(완전 최종 결과물 아님).Early 3D CAD assembly mockup placing the thigh/shank bands and joint actuator modules on a human leg model (early prototype, not final geometry).
이 프로젝트에서 가장 신경 쓴 것"동작한다"가 아니라 "왜 이 수치인지 증명할 수 있다". 모든 설계 수치는 손계산 또는 문헌값과 교차 검증했고, 자동 검사 30항목으로 회귀를 막고 있다.
Core Engineering PrincipleNot just "it moves," but "I can prove why every number is what it is." Every design parameter is cross-checked against hand calculations or biomechanics literature, with a 30-item automated test suite guarding against regressions.

문제 — 이건 보조기구가 아니라 조종기였다The Problem — It Was a Manual Remote, Not an Assistive Device

초기 시스템은 전기적으로는 완성돼 있었다. 빵판 위에서 폐루프 위치 제어, PID, 스톨 감지, 소프트 리밋, EEPROM 캘리브레이션까지 전부 동작했다.

The initial prototype was electrically complete on the breadboard: closed-loop PID position control, stall detection, soft limits, and EEPROM calibration all worked.

그런데 제어 구조를 한 문장으로 요약해 보니 이랬다.

Then I summarized what the control loop actually did in one sentence:

당시 구조사용자가 손가락으로 가변저항을 돌린 만큼 모터가 그 각도로 간다. 즉 "전동 조종기" 이며, 시스템이 스스로 판단하는 부분이 0이다.
The v5 ArchitectureThe motor moves to whatever angle the user dials in with a finger potentiometer. In other words, it was a motorized remote control—the system made zero autonomous decisions.

잔존 근력이 없는 사용자에게 "두 관절을 손가락으로 매 순간 조종하세요"라고 요구하는 장치는 쓸모가 없다. 여기서 구조를 갈아엎었다.

Asking a user with limited motor control to manually dial two leg joints with their fingers on every step is impractical. That realization prompted a complete control redesign.

좌우로 밀어서 표 전체 내용을 확인하세요
구분기존 (v5)개선 (v6)
입력손가락 가변저항 1~2ch건측 고관절·무릎 각도 + 보행 위상
변환선형 map()학습된 목표각 생성기 (MLP)
사용자 역할매 순간 두 관절을 손으로 조종평소처럼 걷기 (의도 표현만)
개인화없음본인 데이터로 파인튜닝
Swipe horizontally to view the full table
AspectPrevious (v5)Redesigned (v6)
Input1–2ch finger potentiometersUnaffected hip & knee angles + gait phase
MappingLinear map()Learned target angle generator (MLP)
User's RoleManually steer two joints continuouslyWalk naturally (express gait intent via unaffected leg)
PersonalizationNoneFine-tuned on user-specific gait data

6개 계층으로 나누기Splitting Control into Six Layers

제어를 여섯 계층으로 분리했다. 원칙은 두 가지다. 각 계층은 위 계층이 실패해도 단독으로 동작한다, 그리고 상위 계층의 어떤 출력도 안전층을 우회할 수 없다.

I separated the control stack into six layers governed by two rules: each layer must function independently if a higher layer fails, and no output from any upper layer may bypass the L0 safety layer.

좌우로 밀어서 표 전체 내용을 확인하세요
계층역할상태
L0 안전층기계 스토퍼 · 비상정지 · 소프트 리밋 · 슬루레이트 · 스톨 감지. 모든 출력이 여기를 통과구현 완료
L1 대칭 미러링건측 각도를 보행 반주기만큼 지연. 학습 없는 규칙 기반 → 대조군구현 완료
L2 학습 목표각 생성기건측 각도·각속도·보행위상 → 환측 목표각. 실제 배포 모델파이프라인 완료
L3 중력·마찰 보상 FF관절각별 유지 PWM 테이블을 전향 보상으로 가산설계 완료
L4 온라인 적응 (ILC)걸음 단위 반복 오차를 EMA로 누적해 선보상계획
L5 RL 정책Unity ML-Agents 균형 정책. 실기 배포 안 함계획
Swipe horizontally to view the full table
LayerRoleStatus
L0 Safety LayerMechanical stops · E-stop · soft limits · slew-rate limiter · stall detection. Every command passes through hereImplemented
L1 Symmetric MirroringDelays unaffected leg angles by half a gait cycle. Rule-based baseline without MLImplemented
L2 Learned Target GeneratorMaps unaffected angles, angular velocities, and gait phase → affected target angles. Target deployment modelPipeline Complete
L3 Gravity & Friction FFAdds angle-dependent holding PWM lookup table as feedforward compensationDesigned
L4 Online Adaptation (ILC)Accumulates step-by-step repetitive tracking error via EMA for feedforward correctionPlanned
L5 RL PolicyUnity ML-Agents balance policy. Never deployed to physical hardwarePlanned
L5(강화학습)를 실기에 올리지 않는 이유sim-to-real 격차가 크고, 정책이 학습하지 않은 상태에 들어가면 예측 불가능한 토크를 낸다. 사람 다리에 붙는 장치에서 이건 허용할 수 없는 위험이다. RL은 시뮬레이션 검증과 시각적 시연 용도로 명확히 분리했다.
Why L5 (Reinforcement Learning) Is Excluded from HardwareBecause of the sim-to-real gap, an RL policy encountering an out-of-distribution state can output unpredictable torques. On a device attached to a human leg, that is an unacceptable hazard. RL is strictly confined to simulation benchmarking and visual demonstration.

우회 경로를 만들지 않는 것이 목표다Eliminating Bypass Paths in Code

안전층은 "막아 준다"고 말하는 것만으로는 부족하다. 코드 구조상 우회가 불가능해야 한다. 모드가 무엇이든 모터에 도달하기 전 같은 경로를 지나가는 것을 목표로 잡았다. 아직 거기까지는 못 갔다. 아래 코드에서 수동 모드는 공통 경로를 타지 않고 바로 빠져나간다 — 구동 펌웨어를 붙이기 전에 정리해야 할 부분이다.

Saying a safety layer exists is not enough—the code structure must make bypassing it impossible. The goal is for every control mode to flow through the exact same L0 clamping stage before touching motor PWM. There is still one gap left to close: in the snippet below, MODE_MANUAL returns early before the shared safety block, which must be unified before enabling live motor actuation.

// 모드가 무엇이든 최종 출력은 단 하나의 경로로만 나간다
switch (mode) {
  case MODE_MANUAL: targetFromPot(hip); targetFromPot(knee); return;
  case MODE_MIRROR: mirrorTargets(&tHip, &tKnee); break;         // L1
  case MODE_MODEL:  modelInfer(features, &tHip, &tKnee); break;  // L2
}
// ── 이 아래는 공통. 우회 경로 없음 ──
tHip  = clampSoftLimit(tHip,  HIP_MIN,  HIP_MAX);
tKnee = clampSoftLimit(tKnee, KNEE_MIN, KNEE_MAX);
hip.target  = slewLimit(hip.target,  tHip,  SLEW_DEG_PER_S * DT);
knee.target = slewLimit(knee.target, tKnee, SLEW_DEG_PER_S * DT);
if (!deadmanHeld() || eStopLatched()) { allOff(); return; }
// Regardless of mode, final output must pass through a single shared path
switch (mode) {
  case MODE_MANUAL: targetFromPot(hip); targetFromPot(knee); return;
  case MODE_MIRROR: mirrorTargets(&tHip, &tKnee); break;         // L1
  case MODE_MODEL:  modelInfer(features, &tHip, &tKnee); break;  // L2
}
// ── Shared L0 path below: no bypass allowed ──
tHip  = clampSoftLimit(tHip,  HIP_MIN,  HIP_MAX);
tKnee = clampSoftLimit(tKnee, KNEE_MIN, KNEE_MAX);
hip.target  = slewLimit(hip.target,  tHip,  SLEW_DEG_PER_S * DT);
knee.target = slewLimit(knee.target, tKnee, SLEW_DEG_PER_S * DT);
if (!deadmanHeld() || eStopLatched()) { allOff(); return; }

안전층 함수는 전부 순수 정적 함수로 만들었다. 덕분에 펌웨어(C)로 1:1 이식이 가능하고, Unity 플레이 모드 없이 단위 검사를 돌릴 수 있다.

All L0 safety functions are written as pure static functions, allowing 1:1 porting between C firmware and C# and enabling fast unit tests in Unity without entering Play Mode.

8비트 MCU에서 신경망 돌리기Running a Neural Network on an 8-Bit MCU

L2의 목표각 생성기는 7-16-16-2 MLP, 파라미터 434개다. 이걸 int16 고정소수점으로 양자화해 ATmega328P에서 실행한다. 활성값과 가중치는 Q12, 누산기는 int32, tanh 는 65점 룩업 테이블과 선형보간으로 대체했다.

The L2 target generator is a 7-16-16-2 MLP with 434 parameters. I quantized it to int16 fixed-point to run on the ATmega328P: activations and weights use Q12, accumulators use int32, and tanh is evaluated via a 65-point lookup table with linear interpolation.

/* 자동 생성된 헤더의 추론 함수 (일부) — AVR PROGMEM 대응 */
static void legassist_infer(const int16_t *raw_in, int16_t *out_d10)
{
  int16_t x[LEGASSIST_N_IN];
  int16_t h1[LEGASSIST_N_HIDDEN];

  /* 입력 정규화: Q16 역표준편차를 곱하고 Q12로 시프트 */
  for (i = 0; i < LEGASSIST_N_IN; i++) {
    int32_t diff = (int32_t)raw_in[i] - LA_RD16(la_x_mean_raw, i);
    int32_t scaled = (diff * LA_RD32(la_x_inv_std, i)) >> (LA_NORM_SHIFT - LA_ACT_SHIFT);
    x[i] = la_clamp16(scaled);
  }

  /* 1층: int32 누산기에 Q24로 쌓고 Q12로 내린 뒤 tanh LUT */
  for (i = 0; i < LEGASSIST_N_HIDDEN; i++) {
    int32_t acc = LA_RD32(la_b1, i);
    for (j = 0; j < LEGASSIST_N_IN; j++) {
      acc += (int32_t)LA_RD16(la_w1, i * LEGASSIST_N_IN + j) * (int32_t)x[j];
    }
    h1[i] = la_tanh(acc >> LA_ACT_SHIFT);
  }
  /* ... 2층, 출력층, 역정규화 ... */
}
/* Excerpt from auto-generated inference header — AVR PROGMEM compatible */
static void legassist_infer(const int16_t *raw_in, int16_t *out_d10)
{
  int16_t x[LEGASSIST_N_IN];
  int16_t h1[LEGASSIST_N_HIDDEN];

  /* Input normalization: multiply by Q16 inverse std and shift to Q12 */
  for (i = 0; i < LEGASSIST_N_IN; i++) {
    int32_t diff = (int32_t)raw_in[i] - LA_RD16(la_x_mean_raw, i);
    int32_t scaled = (diff * LA_RD32(la_x_inv_std, i)) >> (LA_NORM_SHIFT - LA_ACT_SHIFT);
    x[i] = la_clamp16(scaled);
  }

  /* Layer 1: accumulate in Q24 inside int32, shift to Q12, apply tanh LUT */
  for (i = 0; i < LEGASSIST_N_HIDDEN; i++) {
    int32_t acc = LA_RD32(la_b1, i);
    for (j = 0; j < LEGASSIST_N_IN; j++) {
      acc += (int32_t)LA_RD16(la_w1, i * LEGASSIST_N_IN + j) * (int32_t)x[j];
    }
    h1[i] = la_tanh(acc >> LA_ACT_SHIFT);
  }
  /* ... Layer 2, output layer, de-normalization ... */
}

설계 판단 1 — 모델은 각도를 출력한다. 토크나 PWM이 아니다.Design Decision 1 — Output Joint Angles, Not Torque or PWM

건측과 환측의 각도 관계는 운동학적 관계다. 누가 얼마나 힘을 냈는지와 무관하다는 뜻이다.

The coordination between the unaffected leg and the affected leg is a kinematic relationship—it does not depend on actuator force.

그래서 지금의 소형 저와트 모터로 모은 데이터로 학습해도, 나중에 구동계를 바꿨을 때 모델을 다시 학습할 필요가 없다. 바뀌는 것은 하위 추종 제어기(PID 게인, 중력보상)뿐이다. 토크를 출력하도록 설계했다면 구동계를 바꿀 때마다 전부 다시 해야 했다.

Because the model outputs target angles rather than motor torque, changing the motor or gear ratio later does not invalidate the trained model. Only the lower-level tracking controller (PID gains and gravity compensation) needs retuning.

설계 판단 2 — tanh 룩업 테이블을 헤더에 함께 내보낸다Design Decision 2 — Export the `tanh` LUT Directly in the Header

Python 참조 구현과 C 구현이 같은 표를 쓰면, 근사 오차가 서로 상쇄되는 게 아니라 아예 동일해진다. 그래서 3,000표본 검증에서 차이가 0으로 나온다. 이건 운이 아니라 설계다.

Because both the Python fixed-point reference and the C firmware read from the exact same LUT array, approximation differences do not merely cancel out—they are bit-for-bit identical. Across 3,000 test samples, the difference between Python and C is 0.

학습 모델이 새로 만드는 위험New Failure Modes Introduced by a Learned Model

신경망을 넣으면 기존에 없던 고장 방식이 생긴다. 그걸 따로 정의하고 방어책을 붙였다.

Adding a neural network introduces failure modes that do not exist in simple rule-based code. I cataloged each one and added explicit guards.

좌우로 밀어서 표 전체 내용을 확인하세요
위험발생 상황방어
분포 밖 입력 (OOD)학습하지 않은 자세에서 모델이 엉뚱한 각도 출력입력이 학습 분포 1~99 퍼센타일을 벗어나면 즉시 L1 미러링으로 강등
센서 단선·고착포텐셔미터 선이 빠져 ADC 값이 고정무변화 2초 지속 시 경고 및 폴트
위상 추정 실패발 스위치 오접촉으로 좌우 위상이 뒤집힘주기 연속성 검사, 비정상 주기는 채택하지 않고 직전 주기 유지
모델 갱신 사고잘못된 가중치가 올라감내보내기 단계에서 오차 0.5도 초과 시 헤더 생성 자체를 중단
Swipe horizontally to view the full table
RiskTrigger ScenarioGuard Mechanism
Out-of-Distribution Input (OOD)Unseen posture causes erratic angle predictionImmediate fallback to L1 mirroring if any input exceeds the 1st–99th training percentile
Sensor Disconnection / Stuck ValuePotentiometer wire detaches, freezing ADC readingWarning and fault latch if ADC reading remains unchanged for 2 seconds
Gait Phase Estimation FailureFoot switch bounce flips left/right phase timingStride period continuity check; reject abnormal stride durations and hold previous valid period
Bad Model ExportCorrupted or poorly quantized weights exportedExport script aborts header generation if quantization error exceeds 0.5°

실기에 올릴 수 있는 크기까지는 왔다Fitting Inside the 8-Bit MCU Budget

아래 표의 검증은 전부 PC 쪽에서 한 것이다. gcc 로 빌드해 파이썬 참조 구현과 출력을 맞춰 보고, avr-gcc 로 빌드해 플래시·RAM 이 들어가는지 확인했다. 보드에 올려 돌린 결과는 아직 없다. 그건 다음 단계다.

All checks in the table below were performed on the PC side: compiling with gcc to verify bit-exactness against Python, and compiling with avr-gcc to measure Flash and SRAM usage. On-board execution timing is the next step.

좌우로 밀어서 표 전체 내용을 확인하세요
검증 항목결과검증 방법
C 구현 vs Python 참조3,000표본 + 극단 입력 차이 0gcc 컴파일 후 실행 결과 직접 대조
PyTorch(float) vs 정수 구현최대 오차 0.27도 (기준 0.5도)동일 입력 15,769표본
ATmega328P 빌드플래시 2,412 B (7.4%), SRAM 18 B (0.9%)avr-gcc -Os 실제 빌드
자동 검사 스위트30항목 전부 통과안전층·지연선·위상추정·필터·역동역학
Swipe horizontally to view the full table
Verification ItemResultMethod
C Implementation vs Python Reference3,000 samples + edge cases: 0 differenceCompiled with gcc and compared outputs directly
PyTorch (float32) vs Fixed-Point (int16)Max error 0.27° (threshold: 0.5°)Evaluated across 15,769 identical input samples
ATmega328P Binary FootprintFlash 2,412 B (7.4%), SRAM 18 B (0.9%)Compiled with avr-gcc -Os
Automated Test SuiteAll 30 checks passedSafety layer, delay line, phase estimator, filters, inverse dynamics
원래는 ESP32로 갈 생각이었다신경망을 올린다니까 8비트로는 무리라고 보고 처음에는 ESP32로 넘어갈 계획이었다. 그런데 실제로 빌드해 보니 플래시 2,412 B, 전체의 7.4% 만 썼다. 92%가 남는다. 계산상의 주장이 아니라 실물 빌드로 확인된 숫자라 여기서 결정이 갈렸다 — Nano로 충분하다. 보드 구성을 복잡하게 늘리지 않고 경량 웨어러블이라는 전제를 그대로 유지할 수 있었다.
I Originally Planned to Switch to an ESP32Assuming a neural network would be too heavy for an 8-bit chip, I initially planned to migrate to an ESP32. But the actual avr-gcc build used only 2,412 bytes of Flash—7.4% of the ATmega328P, leaving 92% free. Because that was a measured compiler output rather than a rough estimate, I stayed with the Arduino Nano, keeping the hardware simple and preserving the lightweight wearable goal.

데이터 수집 펌웨어는 모터를 아예 안 돌린다The Data Collection Firmware Keeps Motors Physically Disabled

50Hz CSV 스트리밍, 활동 라벨 전환, 캘리브레이션 EEPROM 저장을 지원하는 별도 펌웨어를 만들었다. 여기서는 L298N 제어핀을 setup 에서 LOW로 고정하고 그 뒤로 건드리지 않는다.

I wrote a dedicated firmware build for 50Hz CSV streaming, activity label toggling, and EEPROM sensor calibration. In this build, all L298N control pins are pulled LOW in setup() and never touched again.

데이터를 모으는 단계에서 모터가 움직일 이유가 없고, 움직이면 위험만 늘어난다. 구동은 검증이 끝난 뒤 별도 펌웨어로 분리했다.

There is no reason for motors to spin while recording baseline human gait data, and enabling them during data collection only adds hazard. Motor actuation is isolated in a separate firmware target.

AVR에서 50Hz를 맞추는 법AVR의 float 출력은 한 값당 1ms에 가깝다. 값 몇 개만 보내도 50Hz(20ms)를 못 맞춘다. 그래서 각도를 0.1도 단위 정수로 보내고 PC에서 나눈다. 빠르고 오차도 없다.
Hitting 50Hz Serial Streaming on AVRFormatting floating-point numbers (Serial.print(float)) on an 8-bit AVR takes nearly 1 ms per value, which easily blows past a 20 ms (50Hz) frame budget. Instead, the firmware transmits angles as 0.1-degree integers and lets the PC divide by 10—fast and lossless.

1편 정리, 그리고 다음Part 1 Summary, and What Comes Next

여기까지가 "조종기"를 "스스로 목표를 만드는 장치"로 바꾸는 구조 설계와, 그 모델을 8비트 MCU에 실제로 올린 이야기다.

That covers redesigning the system from a manual potentiometer remote into an autonomous target-angle generator, and quantizing the MLP to fit comfortably inside an 8-bit ATmega328P.

다음 편에서는 가상 물리 시뮬레이션과 하드웨어 구성을 다룬다. Unity ArticulationBody 시뮬레이션이 3D 출력 전에 잡아낸 기구 간섭 오류, 그리고 브레드보드 배선을 정리하기 위해 설계한 듀얼 소켓 PCB와 센서 구성을 정리한다.

Part 2 covers the virtual physics simulation and hardware design: cross-checking joint torques with Unity ArticulationBody and Newton-Euler equations, catching a 23 mm thigh cuff interference before 3D printing, and designing a dual-socket Nano/ESP32 PCB with an AS5600 magnetic encoder.

그 과정에서 확인한 소형 저와트 모터의 역학적 한계와 준능동 구조로의 전환은 3편으로 이어진다.

The mechanical torque limits of the compact low-wattage geared motor discovered during that process—and the resulting pivot to a semi-active stance lock mechanism—follow in Part 3.