|
|
민선생의 전자공방 ・ 2026. 8. 21. 14:19
도립진자, 그 균형의 기록
Inverted Pendulum: Swing-up부터 PID 밸런싱까지
민선생의 전자공방
2026
980ms 직립 순간
스윙업 성공영상은 블로그에서 확인부탁드립니다.
https://blog.naver.com/hansikmin/224385609541
스윙업은 부드럽게 성공하나, 유지가 어렵네요^^; 끝까지 봐주세요^^
시작하며
도립진자는 제어공학에서 가장 유명한 "악당"입니다. 카트 위에 젓가락처럼 세운 막대가 넘어지지 않도록, 그 카트를 좌우로 움직여 균형을 잡는 문제입니다. 말은 간단한데, 이 안에 스텝모터 제어, 관성센서(IMU), 필터링, 그리고 PID 제어의 거의 모든 함정이 압축되어 들어 있습니다.
이 프로젝트는 두 개의 막으로 이루어집니다. 첫 막은 "스윙업(Swing-up)" — 아래로 축 늘어진 진자를 에너지를 실어 수직 근처까지 흔들어 올리는 과정입니다. 둘째 막은 "밸런싱(Balancing)" — 그렇게 세워 올린 진자를, 이번엔 PID 제어로 넘어지지 않게 붙잡는 과정입니다.
PID 알고리즘은 980ms라는 순간을 만들어냈지만, 이게 매번 재현되는 안정적 결과는 아니었습니다. 카트가 필요한 가속도를 구조적으로 못 내주니까요.
이번 콘텐츠는 그 980ms에 이르기까지, 무엇을 몰랐고, 무엇을 잘못 짚었고, 결국 무엇으로 풀었는지에 대한 내용입니다.
내가 만든 인버티드펜듈럼은 가지고 있던 부품과 가성비 높은 부품으로 만들어 결론적으로 원하는 가속력을 만들어 내지 못하였고, 펜듈럼이 스윙업은 되었으나 카트의 움직을 만들어줄 만한 가속도를 구조적으로 만들어 내지 못해 유지는 솔직히 성공하지 못했습니다.
내가 정말 결과를 봐야한다면 스텝모터를 서보모터로 바꿨을것 같습니다. ^^
그래도 어찌저찌 브레드 보드에 회로를 구성해 만든 여러 실습을 종합해 뭔가 의미있는 프로젝트의 결과를 같이 얻고자 했습니다.
구성한 하드웨어에서는 제가 시도 해볼수 있는 것든 다 해본것 같습니다만 180도를 넘어가는 펜듈럼 막대를 반대 방향으로 향하게 해주기에는 제가 갖고있던 스텝모터의 가속도로는 부족했습니다. 아쉽지만 나만의 방학 프로젝트는 여기서 마무리합니다. ^^
시작은 했으니 반은 간거고 스윙업을 했으니 7부 능선을 넘었는데.....
ㅎㅎ
고지에 완벽하게 도달하지는 못했습니다.
핑게지만 방학이 끝나 이제 책상을 정리할 때도 되었고... ^^;
제가 해볼만한것은 다 해봤고, 가성비를 최우선으로 만든 인버티드펜듈럼 하드웨어로는 이게 한계인것 같습니다.
이제 아쉽지만 프로젝트를 마무리 하며 소회를 정리합니다.
1장 — 하드웨어: 무대를 세우다
도립진자가 서 있을 무대부터 소개합니다.
● 제어보드: Arduino UNO R4 WiFi
● 스텝 드라이버: DRV8825, 1/8 마이크로스테핑(0.025mm/step, 40steps/mm)
● 관성센서(IMU): MPU-6050 (I2C 주소 0x68)
● 동력 전달: GT2 벨트 + LM가이드 레일(실측 유효이동 약 334~338mm, 세션마다 미세하게 다름)
● 모터: NEMA17 스텝모터
● 진자: 카본봉 750mm (유효 진자 길이 L_eff = 718mm, 자유진동 주기 실측으로 역산)
● 전원: 20V
이 중 가장 눈여겨볼 부분은 "엔코더가 없다"는 점입니다. 스텝모터는 원래 "내가 몇 스텝 움직였다"를 스스로 세는 방식이라, 별도의 위치 센서 없이도 소프트웨어가 카트 위치를 계속 계산해서 알고 있을 수 있습니다. 이게 이 프로젝트 전체의 가장 큰 장점이자, 뒤에서 보시게 될 몇몇 미스터리의 씨앗이기도 했습니다.
1-1. 전체 구성 사진
인버티드 펜듈럼 — 전체 구성 한눈에 보기
정면 모습 (카트가 중앙, 카본봉 펜듈럼이 하사점으로 늘어진 상태), 이 상태에서 좌우로 스윙(좌우로 운동하며 운동에너지로 직립까지 하도록)하며 직립하게 하는것이 목표입니다.
인버티드펜듈럼 하드웨어 구성
1-2. 구성 부품 목록
| 구분 | 부품 | 규격·설정 | 역할 |
| 제어기 | Arduino UNO R4 WiFi | ARM Cortex-M4 48MHz | 메인 제어 · Non-blocking 스케줄러 |
| 스텝모터 | NEMA 17 | 1.8°/step · 200 step/rev | 카트 구동 |
| 모터드라이버 | DRV8825 | 1/8 마이크로스텝 M0=H · M1=H · M2=L | 스텝 펄스 증폭 및 전류 제어 |
| 각도센서 | MPU-6050 | I2C 0x68 · Roll 값 사용 | 펜듈럼 각도·각속도 측정 |
| 디스플레이 | I2C LCD 20×4 | I2C 0x27 | 초기화·호밍 상태 표시 |
| 안전장치 | 리밋스위치 ×2 | 병렬 연결 → D3 평시 LOW · 터치 HIGH | 호밍 끝점 감지 + 비상정지 |
| 기구부 | LM 가이드 | 전체 420mm | 카트 직선 이동 레일 |
| 벨트 | GT2 타이밍 벨트 | 20T × 2mm 풀리 200 step = 40mm | 모터 → 카트 동력 전달 |
| 펜듈럼 막대 | 카본 낚싯대 | 820mm · 42g | 가볍고 긴 막대 → 제어 여유 확보 |
| 전원 | 20V DC | 아두이노 + 드라이버 공유, 노트북 파워서플라이 사용 | 모터 토크 충분히 확보 |
✅ 부품 선정 핵심 이유 (★아두이노 R4 선택의 이유)
• Arduino UNO R4 WiFi 선택의 결정적 이유 (핵심 두뇌): 기존 아두이노 R3(ATmega328, 8비트, 16MHz)는 복잡한 소수점 연산을 소프트웨어로 흉내 내기 때문에 연산 속도가 매우 느립니다. 반면 R4는 32비트 ARM Cortex-M4(48MHz) 코어를 탑재하고 있으며, 특히 하드웨어적으로 소수점 연산을 가속하는 **FPU(부동소수점 연산 장치)**를 내장하고 있습니다. 덕분에 atan2 각도 계산, 칼만 필터, PID 제어의 복잡한 실수 연산을 수 마이크로초(μs) 이내에 처리하여 스텝모터의 초고속 제어(2,000Hz 이상)를 가능하게 해줍니다.
• 카본봉 (820mm / 42g): 길고 가벼울수록 쓰러지는 주파수가 낮아져 제어 반응 시간에 여유가 생깁니다. (무거운 쇠봉을 썼던 30년 전의 실패를 반면교사 삼아 선택!)
• DRV8825 1/8 마이크로스텝: 1 step = 0.025mm의 고분해능으로 덜컹거림 없는 미세 위치 제어가 가능합니다.
• LM 가이드: 마찰력이 낮고 직선 정밀도가 뛰어나 모터의 부하를 최소화하고 제어 안정성을 극대화합니다.
1-3. 제어부 배선
Inverted Pendulum(역진자) 브레드보드 회로 배선도(배선도 툴 Wokwi)
실제 제어부 — Arduino R4 · DRV8825 드라이버 · 브레드보드 배선
핀 배선 정리
| 아두이노 핀 | 연결 부품 | 신호·기능 | 비고 |
| D3 | 리밋스위치 ×2 | Homing 끝점 + 비상정지 | INPUT_PULLUP · 디바운싱 20ms |
| D4 | DRV8825 DIR | 모터 방향 제어 | CW=HIGH / CCW=LOW |
| D5 | DRV8825 STEP | 스텝 펄스 출력 | micros() Non-blocking |
| D6 | DRV8825 EN | 드라이버 Enable | Active LOW · 정지 시 HIGH |
| A4 (SDA) | MPU-6050 / LCD | I2C 데이터 | 스윙업 후 LCD 갱신 차단 |
| A5 (SCL) | MPU-6050 / LCD | I2C 클럭 | MPU-6050 전용화 |
| DRV8825 마이크로스텝 설정 |
| ⚠️ DRV8825 마이크로스텝 핀 설정 (반드시 확인!) |
| |
| 1/8 마이크로스텝 : M0=HIGH, M1=HIGH, M2=LOW ← 모터의 부드러운 동작을 위해 펜듈럼 제어에 1/8을 사용 |
| |
| 분해능 : 200 step/rev × 8 = 1,600 step/rev → 1 step = 0.025mm |
| |
| STEP 최소 펄스폭 : HIGH ≥ 1.9μs, LOW ≥ 1.9μs → 총 ≥ 4μs 보장 필수 |
| → 9편 DRV8825 탈조 방지 편 참조 |
1-4. 카트 & MPU-6050 장착
▲ 카트 상세 — MPU-6050이 펜듈럼 회전축에 직접 장착된 모습
2장 — 1막: 스윙업, 에너지가 만드는 리듬
스윙업 성공
문제: 손이 아니라 에너지로
가장 먼저 부딪힌 벽은 이겁니다 — 아래로 축 늘어진 진자를, 사람 손처럼 섬세하게 밀어서는 절대 위로 못 올립니다. 한 번 밀어서는 어림도 없고, 그렇다고 무작정 세게 밀면 카트가 레일 끝에 부딪힙니다.
답은 "에너지"였습니다. 진자가 지금 얼마만큼의 역학적 에너지를 갖고 있는지를 실시간으로 계산하고, 그 값이 진자를 수직으로 세우는 데 필요한 에너지(위치에너지 최댓값)에 도달할 때까지, 카트를 진자의 흔들림과 박자를 맞춰 밀어주는 방식입니다.
| E = L_eff · ω² / (2g) + (1 − cosθ) 목표: E = 2.0 |
이 식 하나를 제대로 세우기까지도 여러 차례의 실측과 수정이 필요했습니다 — 진자 길이(L_eff)를 자유진동 주기로 역산해서 바로잡고, 자이로 센서의 측정 범위를 각속도가 빠른 순간까지 놓치지 않도록 넓히고 나서야, 에너지 계산이 실제 움직임과 맞아떨어지기 시작했습니다.
완성: "Catch & Throw"
최종적으로 자리 잡은 방식에는 이런 이름을 붙였습니다 — Catch & Throw. 진자가 아래에서 위로 올라가는 첫 타격을 주고, 진자가 95도를 넘어서면 반대 방향으로 2차 타격을 추가하고, 카트가 달릴 활주로를 세 구간(도움닫기 · 2차타격 · 비상정지)으로 나누어 배분하는 방식입니다. 여러 번의 "악순환"(진자가 힘을 못 받고 제자리에서 진동만 반복하는 상태)을 벗어나는 로직까지 더해서야 비로소 안정적으로 작동했습니다.
완성된 스윙업의 성공률은 약 40% 수준이었습니다. 100%가 아니라는 게 아쉬울 수 있지만, 이 정도로도 "충분히 만족스럽다"고 판단하고 다음 막으로 넘어갔습니다 — 모든 것을 완벽하게 만들기보다, 다음 문제로 넘어갈 타이밍을 아는 것도 엔지니어링의 일부라고 생각합니다.
3장 — 2막: 센서의 배신, 180도의 함정
미스터리: "눈으로는 넘어갔는데, 로그엔 없다"
스윙업이 안정되고 나서, 진자가 수직(180도) 근처에서 반대편으로 완전히 한 바퀴 돌아버리는 현상을 여러 차례 눈으로 목격했습니다. 그런데 이상하게도, 그 순간이 로그에는 나타나지 않았습니다. 처음엔 로그를 어디까지 받았는지(복사가 잘렸는지)를 의심했고, 실제로 아두이노 IDE 시리얼 모니터의 복사 기능 자체에 알려진 한계가 있다는 것도 확인했습니다.
하지만 로그를 온전히 받아내는 방법(PuTTY 세션 로깅)까지 갖추고 나서도 의문은 남았습니다. 완전한 로그 안에서도, 180도 근처의 센서 값이 이상하게 요동치는 순간들이 분명히 있었습니다.
진짜 원인: 계산식이 찢어지는 자리
범인은 가속도계 각도를 구하는 함수, atan2였습니다. 진자가 정확히 거꾸로 선 상태에서는 이 함수의 결과가 +180도와 -180도 사이를 수학적으로 오갈 수 있는 경계 위에 놓입니다. 아주 미세한 흔들림만으로도 그 값이 크게 튀어버립니다.
그런데 여기서 한 걸음 더 들어가야 진짜 원인이 보였습니다. 문제는 "가속도계가 튄다"는 사실 자체가 아니라, 자이로(각속도)와 가속도계 값을 섞는 상보필터(Complementary Filter)의 산수 방식이었습니다.
| (수정 전) cfAngle = α·(cfAngle + gyro·dt) + (1−α)·accelAngle |
이 식은 두 각도를 "직선 위의 숫자"처럼 단순히 빼서 섞습니다. 하지만 각도는 원 위의 값입니다. 예를 들어 178도와 -179도는 원 위에서 겨우 3도 떨어져 있을 뿐인데, 이 식은 그 차이를 357도로 착각합니다. 딱 이 순간에 아주 작은 자이로 신뢰도(가속도계를 조금이라도 반영하는 순간)만 있어도, 필터가 엉뚱한 방향으로 크게 끌려갔습니다.
해법은 "최단거리 각도차(angDiff)" 함수 하나였습니다. 두 각도의 차이를 원 위에서의 최단 거리로 다시 계산해서 섞는 방식입니다.
| circularDiff = angDiff(accelAngle, gyroPredicted) cfAngle = gyroPredicted + (1 − α) · circularDiff |
평소(180도 근처가 아닌 구간)에는 기존 식과 수학적으로 완전히 동일한 결과를 내면서, 오직 경계를 넘나드는 순간에만 다르게(그리고 올바르게) 작동하는 수정이었습니다. 이 한 줄의 수정 이후, 완전회전이 실제로 일어나는 순간에도 필터 값은 매끄럽게, 정직하게 그 사건을 따라갔습니다.
4장 — 3막: 포화와의 사투
캐스케이드: 두 겹의 눈
밸런싱은 두 겹의 제어 루프로 짰습니다. 안쪽 루프(내부루프)는 "진자가 얼마나 기울었나"만 보고 빠르게(5ms마다) 반응합니다. 바깥쪽 루프(외부루프)는 "카트가 중앙에서 얼마나 벗어났나"를 천천히(20ms마다) 보고, 내부루프의 목표 각도를 아주 살짝 흔들어 카트를 중앙으로 유도합니다.
이 구조 자체는 금방 자리 잡았습니다. 문제는 다른 데 있었습니다 — 캐치(핸드오버) 이후 초반 200~300ms는 거의 매번 훌륭하게 버티다가, 그 이후엔 마치 약속이나 한 듯 무너졌습니다.
발견: 포화라는 물리적 벽
로그를 하나하나 대조한 끝에 원인을 찾았습니다. 카트의 속도가 최고 속도(처음엔 260mm/s)에 도달해서 "일정한 속도로만" 달리는 구간에서는, 물리적으로 가속도가 0에 가깝습니다. 그런데 진자를 실제로 바로세우는 힘은 속도가 아니라 가속도에서 나옵니다. 즉 최고 속도에 붙박이는 순간부터는, P·D 게인이 아무리 큰 출력을 요구해도 카트가 더는 응답할 수 없었던 겁니다.
이후의 모든 시도는 사실상 이 하나의 물리적 벽을 다루는 여러 각도의 시도였습니다.
다양한 시도
● 외부루프 포화 억제 — 내부루프가 이미 한계 속도로 사투 중일 때는, 외부루프의 "중앙으로 더 기울여라"라는 지시를 포화율에 비례해 부드럽게 줄였습니다. "생존이 급한데 목적지 좌표를 들이미는" 상황을 막기 위해서였습니다.
● 밸런싱 전용 속도 상한 확장 — 스윙업의 최고 속도(260)와 밸런싱의 최고 속도를 분리하고, 밸런싱 쪽만 300으로 올렸습니다. "포화가 되더라도 조금 더 여유 있게" 되도록 하기 위해서였습니다.
● 게인 다이어트 — P 게인을 낮출수록 포화에 도달하는 시점 자체가 늦춰진다는 걸 실측으로 확인했습니다(-25 → -12 → -8). 다만 게인을 낮출수록 일단 포화된 뒤의 발산 속도는 오히려 살짝 빨라지는 트레이드오프도 함께 발견했습니다.
● 핸드오버 위치 게이트 재검토 — 캐치가 성공하는지 실패하는지를 가르는 결정적 변수가 "캐치되는 순간 카트가 어디 있었는가"라는 걸 실측 데이터로 확인했습니다. 성공한 캐치는 모두 여유 있는 위치였고, 실패(완전회전)한 캐치는 모두 정확히 레일 끝(150mm)에 카트가 고정돼 있었습니다. 핸드오버를 허용하는 위치 범위를 과감히 레일 끝까지 넓히고, 대신 위치가 가장자리에 가까울수록 외부루프가 더 세게 반응하도록(최대 4배) 보강했습니다. 그리고 일단 넘겨받은 뒤의 안전 판정에는 10mm의 여유를 더 얹어, 각도 제어가 거의 완벽한데도 여유가 없어서 조기 종료되는 경우를 줄였습니다.
현장에서의 우선순위 전환
마지막 단계에서는 "가장 오래 버티는 것"보다 "영상에 담을 만한 순간을 놓치지 않고 자주 만드는 것"으로 목표를 바꿨습니다. 핸드오버가 걸리는 조건(각도·자이로 게이트)을 조금 더 느슨하게 열고, 그 대신 게인을 다시 끌어올려(-8에서 -14로) 다소 나쁜 조건에서 시작해도 빠르게 잡아채는 전략으로 바꾸었습니다. 이 조합에서 마침내 980ms를 기록했습니다.
5장 — 대단원: 980ms
아쉬운 980mS의 직립
최종적으로 도립진자는 핸드오버 이후 980ms 동안 곧게 선 채로 버텼습니다. 처음 스윙업을 시작할 때만 해도, 진자가 카트 위에서 단 1초 가까이 스스로 서 있는 모습은 상상 속에서만 존재했습니다.
이 980ms라는 숫자가 세계 기록이거나 이론적 최댓값은 아닙니다. 하지만 이 숫자 뒤에는, 최소한 열 번이 넘는 서로 다른 버전의 코드와, 그보다 훨씬 많은 실측 로그, 그리고 매번 "왜?"라고 되물었던 시간이 쌓여 있습니다.
최종버전 아두이노 코드
첨부파일
swingup_PID_v4_14__final_20260821.ino
/*
* ================================================================
* Inverted Pendulum — 스윙업 v29.1 + 이중루프(캐스케이드) PID 밸런싱 (v4.14)
* ※ 버전 표기 규칙: 이 헤더·LCD 문구는 파일을 수정할 때마다 반드시 같이 갱신한다.
* (v3.6 표기가 v4.4까지 안 바뀐 채 방치됐던 사고가 있었음 — 재발 방지용 메모)
*
* [핸드오버 — 스윙업 → 밸런싱]
* - 4중 게이트: 각도>175 AND |gyro|<30 AND |pos|<135mm AND !isrRun(카트 자연정지)
* - lastDir=0 / balTiltRef=0 / balGyroInit=false 등, 이전 세션 잔여 상태 진입시 리셋
*
* [내부(각도) 루프] BAL_KP=-25(확정) · BAL_KD=-4.5(실측 3배 증강)
* - D항은 글리치필터(200°/s) + 저역통과필터(0.88) 이중 보호, 램핑 BAL_ACCEL_STEP=90
*
* [외부(위치) 루프 — 캐스케이드] BAL_KP_POS/KD_POS 둘 다 음수, 아직 실기 미검증
*
* [센싱 검증 — 여러 실측 로그로 확인된 결론]
* - 카트 급가속/급반전 순간 가속도계가 겉보기 기울기로 20~65° 튐(ACC-GAP 플래그로 확인)
* - 그런데도 필터링된 각도(Ang)는 매번 매끄럽게 유지됨 → 필터 자체는 신뢰할 만함
* - 단, 실제 물리각도와 일치하는지(계통오차)는 READY상태 VERIFY 출력으로 별도 확인 필요
* - 배선 꼬임 확인용 누적회전(totalRotationDeg)도 추가됨
*
* [모니터링] err/P/D/out 분리, IMPROVE/WORSEN/STEADY 추세, 진동주기(per)·zc 자동추정,
* 시도종료 SUMMARY, BAL_FAIL_HOLD_FOR_LOG·SWING_WRAP_HOLD_FOR_LOG(둘 다 실패/완전회전
* 시 그 자리에 정지 — 60초 세션 스크롤에 묻혀 사건을 놓치는 문제 대응)
*
* [v4.5] 로그 줄 길이·주기 재검토 — 115200bps에서 긴 줄(~140자) 전송에 ~12ms 걸리는데
* 10ms 간격으로 찍고 있어 시리얼버퍼가 밀려 제어루프를 붙잡았을 가능성 발견.
* 줄 단축 + 간격 25ms로 완화(스윙업/밸런싱 로그 둘 다). "1순위: 스윙업 100% 유지"에
* 로그 오버헤드도 포함된다는 걸 뒤늦게 반영.
* [v4.6] 177~180도 근접 시 "*** NEAR-180 ... ***" 별도 마커 줄 추가(100ms 제한) —
* 시리얼 모니터에서 그 구간을 눈으로 쉽게 찾기 위함. 100ms당 최대 1줄이라 오버헤드 없음.
* [v4.7] 근본 원인 수정 — 상보필터가 가속도계값을 "원형 각도"가 아니라 단순 뺄셈으로
* 섞고 있었음(atan2가 ±180에서 불연속인 게 문제가 아니라, 그 불연속값을 직선 숫자처럼
* 섞은 산수가 문제). angDiff(최단거리)로 섞도록 수정 — cfAngle 규약(0=바닥,180=꼭대기)은
* 그대로라 스윙업 코드 무영향. NEAR-180/GAP 마커에 applied(실제 적용된 보정량) 추가해서
* 수정이 먹히는지 바로 검증 가능(GAP은 커도 applied는 작아야 정상).
* → 실기로 확인됨: 캐치오차 1.4°, 649ms 홀드까지 도달(역대 최고).
* [v4.8] [방향B] 내부루프 포화(vSet=260 도달) 시 외부루프(Tilt) 영향을 포화율에 비례해
* 부드럽게 억제. 649ms 홀드 로그에서 포화 시점부터 발산 시작 + Tilt 부호반전이 오차를
* 키운 순간이 실측 확인되어 반영. 순환참조 방지 위해 "직전 틱"의 포화율 사용(5ms 지연).
* 로그에 sat: 필드 추가(1=억제없음, 0=완전억제) — 새 출력경로 없이 기존 줄에만 추가.
* → 실기 결과: 억제는 의도대로 작동(sat:0.00 확인)했으나 발산은 v4.7보다 오히려 빨랐음.
* 즉 원인이 아니었다는 게 실측으로 반증됨 — B 로직 자체는 유지(원 목적엔 유효).
* [v4.9] [방향C] 밸런싱 전용 최고속도 분리 — BAL_LIMIT_MMS(300), setSpeedMMS_Bal().
* 실측: vSet=260(구 상한) 도달 후 300ms+ sat:0.00로 붙박인 채 오차 3.4°→37.1° 지속 악화.
* 일정속도 구간=가속도≈0=추가 교정력 없음이라는 가설 검증용. setSpeedMMS()는 스윙업
* 여러 곳이 공유하므로 절대 안 건드리고 별도 함수로 분리(스윙업 완전 무관).
* ★ 300도 미검증 — 탈조음 들리면 즉시 낮출 것.
* [v4.10] 게인 동기화 + 위치게이트 재검토.
* - BAL_KP/KD를 그간 실기로 검증된 값(-25→-12→-8, -4.5→-2.0→-1.5)으로 동기화.
* 낮출수록 포화시작 지연(729→846→881ms)되나 포화후 발산은 오히려 빨라짐(0.088→0.125°/ms).
* - BAL_CATCH_POS_MM 135→145mm. v4.8·v4.9 안전장치 이전 기준이던 135mm 때문에
* 각도·속도·정지 조건 다 맞는데 위치(141.6/142.8mm)만 넘어 완전회전까지 가버린
* 사례가 반복 확인되어(3회) 여유를 5mm로 줄임.
* [v4.11 — 되돌림] 진단 3종(호밍범위·리밋터치·타격클램프) 추가했으나 PuTTY 5회 전수
* 탈조+리밋터치. 원인: firePush/fireSecondPush가 startMoveISR() 직전에 곧바로
* Serial.print()해서 시간이 제일 중요한 순간을 블로킹 — 리밋터치가 실측 레일범위
* 169mm인데 소프트웨어상 41.5mm에서 발생(탈조로 위치추적이 크게 어긋난 정황).
* v4.10으로 전량 되돌리고 v4.12에서 다시 시도.
* [v4.12] v4.10 기준으로 진단 3종 재적용, 단 타격클램프는 "지연 보고"로 재설계.
* firePush/fireSecondPush는 값만 저장(대입 몇 개, 초고속)하고, 출력은 이미 안전이
* 검증된 swingControl() 정기 로그 시점으로 미룸 — 타격 시작 그 순간엔 출력 전혀 없음.
* 호밍범위·리밋터치 출력은 원래도 타이밍과 무관해서(부팅 1회 / 모터정지 후) 그대로 유지.
* → 실기 7회 전수조사: 성공 3회(pos 120~135mm) vs 완전회전 4회(전부 정확히 pos:150.0,
* 2차타격이 소프트리밋에 클램프된 게 원인) — 위치 하나가 성공/실패를 완벽히 가름.
* [v4.13] 게이트 145→150mm(SOFT_LIMIT_MM 자체, 마진 0)로 확장 + 가장자리 대응 강화.
* - 위치가 가장자리에 가까울수록(제곱비례) 외부루프 게인을 최대 4배까지 부스트
* (BAL_POS_BOOST_MAX). 중앙 근처 캐치는 거의 무영향, 가장자리 캐치만 처음부터
* 훨씬 강하게 중앙으로 당김. BAL_TILT_LIMIT(상한)은 그대로 — 더 빨리 도달할 뿐.
* - ★ 게이트 비교연산자 <를 <=로 수정. 150.0 자체를 포함해야 하는데 <는 150.0을
* 걸러내는 실수를 검토 중 발견하여 즉시 수정(안 고쳤으면 이번 변경의 목적 자체가
* 무효화될 뻔함).
* → 실기 8회: 처음으로 pos:150.0/-150.0 캐치 2회 성공. 5차(150.0)는 813ms로 역대
* 최고 기록(부스트가 150→55mm까지 실제로 끌고 옴). 6차(-150.0)는 각도오차 최대
* 2.6도로 거의 완벽했으나 여유 0이라 134ms만에 SOFT LIMIT HIT — 게이트 확장의
* 예상된 트레이드오프가 실측으로 확인됨.
* [v4.14] 밸런싱 전용 안전마진 분리 — BAL_SOFT_LIMIT_MM(160), SOFT_LIMIT_MM(150)과 별개.
* v4.13 6차 시도(각도 거의 완벽, 여유 0으로 조기종료)에 대한 대응. 핸드오버 게이트는
* 그대로 150까지 허용(가장자리 캐치 자체는 유지), 넘겨받은 뒤의 안전판정만 10mm 더
* 여유를 줌. 실측 레일범위(매번 167~169mm)에 여전히 7mm+ 남아 안전. 스윙업의
* SOFT_LIMIT_MM(150)과 그걸 쓰는 firePush/fireSecondPush/swingControl은 전혀 무관.
* ================================================================
*/
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <FspTimer.h>
// ── 핀 정의 ─────────────────────────────────────
#define MPU_ADDR 0x68
#define PIN_LIMIT 3
#define PIN_DIR 4
#define PIN_STEP 5
#define PIN_EN 6
// ── MPU6050 레지스터 ────────────────────────────
#define REG_PWR_MGT 0x6B
#define REG_SMPLRT 0x19
#define REG_CONFIG 0x1A
#define REG_GYRO_CFG 0x1B
#define REG_ACCEL_CFG 0x1C
#define REG_ACCEL_OUT 0x3B
#define REG_GYRO_OUT 0x43
...........
마지막 실행 로그
..........
t:19226, ang:172.3, accRaw:174.0, gyro:38, E:2.007/2.0, pos:-100.2, vSet:255, vAct:-258, cart:-1
t:19255, ang:174.2, accRaw:177.9, gyro:71, E:2.051/2.0, pos:-107.5, vSet:255, vAct:-252, cart:-1
t:19283, ang:175.6, accRaw:178.5, gyro:33, E:2.009/2.0, pos:-114.7, vSet:255, vAct:-259, cart:-1
>>> TARGET REACHED! HAND-OVER TO PID BALANCING <<<
gate values -> ang:176.17 gyro:38.48 pos:-117.9
BAL t:122 err:-3.0 P:42 D:-94 out:-52 [IMPROVE] per:- zc:0 sat:1.00 | Ang:-0.0 Tilt:3.0 boost:2.85 Pos:-118 vSet:52 Gyro:32 Filt:38
BAL t:149 err:-2.9 P:40 D:-71 out:-31 [IMPROVE] per:- zc:0 sat:1.00 | Ang:0.1 Tilt:3.0 boost:2.82 Pos:-117 vSet:31 Gyro:3 Filt:29
BAL t:175 err:-1.6 P:23 D:-69 out:-46 [IMPROVE] per:- zc:0 sat:1.00 | Ang:1.4 Tilt:3.0 boost:2.80 Pos:-116 vSet:46 Gyro:44 Filt:28
.........
첨부파일
putty_loge.log
6장 — 사색: 만약 서보모터였다면
이건 검증된 사실이 아니라, 이번 여정을 마치고 돌아보는 사색에 가깝습니다. 이번 프로젝트의 가장 근본적인 물리적 벽은 "스텝모터가 최고 속도에서 가속도를 잃는다"는 것이었습니다. 만약 엔코더 피드백을 갖춘 서보모터를 썼다면, 이 벽 자체가 훨씬 높은 곳에 있었을 가능성이 큽니다 — 고속에서도 가속력을 상당 부분 유지할 수 있고, 다소 거친 명령에도 위치를 계속 스스로 교정하기 때문에, 지금 저희가 마주쳤던 "포화"라는 벽의 성격 자체가 달라졌을 것입니다.
다만 이건 어디까지나 물리적 가능성에 대한 사색이지, 저희가 실제로 검증한 내용은 아닙니다. 스텝모터와 엔코더 없는 구성이었기에 오히려 위치 제어의 원리를 아주 직관적으로 다룰 수 있었던 것도 사실이고, 그 나름의 배움이 있었습니다.
7장 — 미래의 도전자들에게
● 하드웨어의 물리적 한계를 먼저 인정할 것 — 최고 속도와 가속도가 어디까지인지 실측으로 먼저 확인하고, 그 안에서 소프트웨어의 타협점을 찾는 것이 순서입니다.
● 모든 정답은 로그에 있습니다 — 0.1초도 안 되는 순간을 수십 줄의 데이터로 쪼개어 대조하는 집요함이, 결국 눈으로는 절대 보이지 않는 원인(예: 센서 계산식의 수학적 결함)을 찾아냈습니다.
● 변수는 한 번에 하나씩 — 스윙업, 필터, 제어 게인을 동시에 건드리면, 결과가 좋아져도 나빠져도 무엇 때문인지 알 수 없습니다. 통제된 환경에서 단일 변수만 바꾸는 것이 결국 가장 빠른 지름길이었습니다.
● 가설은 로그로 검증하되, 반증도 흔쾌히 받아들일 것 — 이번 여정에서 몇몇 그럴듯한 가설(예: 특정 상수를 좁히면 좋아질 것이다)이 실측 데이터로 정확히 반증되는 경험을 여러 차례 했습니다. 그때마다 가설을 고집하지 않고 데이터가 가리키는 방향으로 다시 돌아간 것이, 결국 완주할 수 있었던 이유라고 생각합니다.
맺음말
도립진자는 끝나지 않는 문제로 유명합니다. 완벽한 균형이란 애초에 없고, 그저 "얼마나 오래 버티는가"라는 질문만 남습니다. 이번 여정에서 저희는 980ms라는 답을 얻었습니다. 완벽하진 않지만, 그 숫자에 이르기까지의 모든 실패와 발견이, 그 자체로 이미 충분히 완결된 이야기라고 생각합니다. 많이 아쉽지만 하드웨어적인 제약이 있어 더 이상의 시도는 의미가 없다 판단하였습니다. 많이 아쉽지만 이번 프로젝트는 여기서 마무리 합니다.
나만의 방학 프로젝트는 이제 마무리 합니다. ^^
금세 방학이 끝나 2학기가 시작되었습니다. 이제 책상위의 펜듈럼은 정리하고 업무 모드로 전환합니다. ^^
이 글을 읽고 계신 미래의 어느 도전자께 —
용기 있게, 자신있게,,, 미래의 시대에 지식은 AI가 채워줄수 있습니다. 그렇지만 도립진자를 세우기 위해 하드웨어를 만들고, 시도하고, 포기하지 않고 튜닝하는 모든 경험은 AI가 줄 수 없습니다.
앞으로의 시대에는 지식보다 경험이 나의 경쟁력이자 역량이 되는 시대가 되지 않을까요?
실패를 두려워 하지 말고 끝까지 끈기있게 도전해 보십시요. 경험이란것이 성공에서도 배우고, 실패에서도 배우고 얻는 것입니다.
여러분에게 실력과 함께 축복과 행운히 늘 함께하시어 멋진 엔지니어로 성장하시길 진심으로 바랍니다. ^^
아자아자!!! 화이팅~~~~ !!!! ^^
태그#도립진자#인버티드펜듈럼#아두이노R4#스윙업#PID제어#캐스케이드제어#스텝모터#DRV8825#MPU6050#브레드보드IO#include#define
