키티봇(KITtyBot): 아두이노 프로 미니·서보·IR 리모컨·전원 내장 DIY 스마트 로봇 키트
부품 및 소모품
부품
수량
비고
스파크펀 아두이노 프로 미니 328 - 5V/16MHz
1
서보 (타워프로 MG996R)
12
SG-90 또는 동일 폼팩터 9g 서보
아다프루트 UBEC DC/DC 스텝다운(벅) 컨버터 - 5V @ 3A 출력
1
아다프루트 미니 리모컨
1
다른 리모컨도 가능. IRLib 라이브러리와 잘 작동함
아다프루트 IR 수신 센서 - TSOP38238
1
1kΩ 저항
1
2.2kΩ 저항
1
7.4V / 500mAh 리포(LiPo) 배터리
1
R/C 용도 이 정도 용량이면 적당
필요 도구 및 장비
3D 프린터 (일반)
납땜 인두 (일반) — 브레드보드 버전은 납땜 불필요할 수 있음
앱 및 온라인 서비스
아두이노 IDE
프로젝트 소개
소개
일반적인 '거미'나 '곤충' 스타일이 아닌 '포유류' 스타일의 4족 보행 로봇을 만들고 싶었습니다. 보스턴 다이내믹스(Boston Dynamics) 로봇 등 유명 4족 연구 로봇에서 영감을 받았습니다. 이런 로봇은 무게중심이 높고 발이 몸체 아래에 있어 쉽게 넘어지기 때문에 제작이 꽤 까다롭습니다.
목표는 아두이노와 저가 마이크로 서보로 저렴한 로봇을 만드는 것입니다. 당연히 한계는 있습니다. 완벽하진 않지만, 몇 대를 만들어 보니 이 영상처럼 걷는 동작은 구현할 수 있었습니다. 빵빤한 자금 지원을 받는 연구 프로젝트에서는 겪지 않을 '최소 예산으로 최선 다하기' 자체가 하나의 도전이자 재미입니다. :)
초기에 역기구학(Inverse Kinematics, IK) 연구가 필수적임을 알았습니다. 코드에 원하는 발 위치로부터 관절 각도를 계산하는 방정식 세트가 들어 있습니다. 이를 이용해 몸체 이동(네 발을 반대 방향으로 동시 이동), 완전한 발 동작(들어 올리기 → 지정된 방향으로 이동 → 내리기) 같은 반복 작업용 함수로 확장했습니다.
다음 과제는 보행 패턴(걸음걸이, Gait) 연구입니다. 로봇이 어떻게 걷고 돌지를 몸체·발 움직임으로 정의하는 것입니다. 제 로봇은 항상 정적 안정 보행(Statically stable gait)을 사용합니다. 한 번에 한 발만 들어 새 위치에 놓고, 나머지 세 발로 몸체를 지탱합니다. 무게중심이 이 세 발이 이루는 삼각형 안에 있어야 넘어지지 않습니다. 전진, 후진, 좌회전, 우회전 네 가지 표준 보행 패턴을 개발했습니다. 이는 발·몸체 이동 함수를 조합해 전체 시퀀스로 만듭니다.
여러 서보가 정해진 시간 동안 서로 다른 스트로크를 할 때 동기화해 부드러운 움직임을 만들기 위한 동기식 서보 이동 함수도 만들었습니다.
완전 무방비 리포 배터리를 사용합니다. 위험할 수 있으니 주의하세요. 주요 위험: 과방전(너무 깊게/빨리 방전). 첫 번째는 실수로 단락하지 않는 한 괜찮습니다. 일반 R/C 배터리는 25C 방전율이라 이 경우 12A까지 허용되지만 UBEC가 어떤 상황에서도 2A 이상 안 나가게 제한합니다. 두 번째 위험은 소프트웨어 감시 기능으로 방지합니다. 아날로그 핀으로 전압 측정해 7.0V 미만이면 로봇을 정지시킵니다.
배터리는 반드시 전용 충전기로 충전하고, 충전 중 자리를 비우지 마세요. 배터리는 로봇에서 분리(벨크로로 장착)해 방화 백이나 인화물질에서 안전한 거리에서 충전하세요. 보관도 안전하게. 리포 배터리 잘 모르시면 지역 R/C 매장에서 배터리·충전기·방화 백 세트로 사시고 경고 문구 꼭 읽고 판단하세요. :)
로봇 조립
제공된 파일로 부품을 출력하세요. 사진들을 보며 조립 순서를 익힌 뒤 시작하세요. 저는 스칸디나비아 출신이지만 이 설명은 이케아(IKEA)나 레고(LEGO) 매뉴얼 수준은 아닙니다. :)
고관절(힙 조인트)을 먼저 조립합니다. 양면 테이프(좋은 것)로 부품을 붙였습니다. 접착제도 가능하지만 부러지면 수리 불가능해 서보 하나 망가져도 관절 통째로 교체해야 합니다.
서보 서포트를 한 서보 하단에 작동축과 일직선으로 붙입니다. 다른 서보를 축이 수직이 되게 붙입니다. 아래 사진은 앞쪽 오른쪽, 뒤쪽 왼쪽 고관절입니다. 나머지 두 모서리는 미러링해 만드세요.
진행 전 12개 서보 모두 센터링(중립 위치)되었는지 확인하세요. 가장 좋은 방법은 PCB(또는 브레드보드) 조립해 모든 서보 연결하고 코드 업로드. 아두이노 시작 시 모든 서보가 센터(90도 명령)로 갑니다. 로봇 조립 후 나중에 미세 조정 필요합니다.
다음은 '다리(Thigh)'라고 부르는 '상지' 부품 장착. 이 부품에는 서보 혼(Servo horn, 서보에 딸려오는 것)과 맞는 홈 있습니다. 혼을 홈에 접착하세요. 3D 프린트 재질과 혼의 나일론 플라스틱 모두에 붙는 접착제 써야 합니다. 제가 쓴 글루건은 괜찮았고, 순간접착제(CA)는 브랜드마다 달라 복불복이었습니다.
다리는 고관절에 60도 각도로 결합. 서보가 센터링된 상태에서 이 각도에 최대한 가깝게 위치 잡으세요. 서보에 딸린 나사(보통 세 개 중 짧은 것)로 혼을 스플라인에 고정합니다. 아래는 조립된 다리와 고관절 사진 (혼은 생략/모델링 안 함).
하지(Lower leg)도 조립. 서보를 나사로 다리 부품에 고정합니다. 서보에 딸린 나사(보통 긴 '목재용' 나사 두 개) 사용.
이제 다리를 몸체에 장착합니다. 로봇 앞뒤에 '범퍼(Bumper)'라 부르는 부품이 있습니다 (자동차 범퍼처럼). 여기에도 서보 혼 홈 있습니다. 혼을 접착해 넣으세요. 상지(다리)의 서보 서포트를 몸체 해당 구멍에 밀어 넣으세요. 양쪽 다 하면 범퍼로 고정합니다. 다리가 약 12도 바깥쪽으로 향하게 (발끝이 20mm 바깥으로) 배치. 범퍼는 남은 긴 서보 나사로 몸체에 고정.
마지막으로 하지(아래 다리)를 장착. 상지와 반대 방향으로 꺾여 발끝이 각 다리 조립체의 고관절 바로 아래 오게 합니다.
이제 로봇 조립 완료. 아래 사진처럼 보여야 합니다. 상단 이미지/영상과 디자인이 약간 다릅니다. 몸체 설계를 단순화하고 더 견고하게 바꿨습니다. 서보 서포트와 고관절 혼 위치가 서로 바뀌었습니다. 3D 이미지대로 조립하고 사진/영상에 헷갈리지 마세요.
각 관절 각도를 정확히 이론값에 맞출 순 없습니다. SG-90 서보 스플라인 개수가 21개라 한 칸당 17도 간격입니다. 조립 시 10~20도 이내로 맞추는 게 최선이고, 나머지 오차는 코드에서 중립 위치를 조정해 보정합니다. 코드 내 중립 위치 조정법은 아래에 있습니다. 다시 모든 서보 연결해 아두이노 켜고 중립 위치 확인하며 기계적 조정(스플라인 한두 칸 이동)도 병행하세요. 작업 중 서보가 실수로 돌아갈 수 있습니다.
전자회로 연결
두 가지 옵션: 브레드보드에 모두 올리기, 또는 제공된 프리칭 파일로 PCB 제작.
브레드보드에 전원/그라운드 라인을 서보들에 연결할 때 조심하지 않으면 전압 강하 문제가 생길 수 있습니다. 서보 하나가 최대 600mA까지 먹는데 접촉 불량이면 동작이 불안정해집니다. PCB는 전원 라인 구리 패턴이 아주 넓어 납땜만 잘하면 문제없습니다.
제 설계엔 전원 스위치 없습니다. 배터리 연결/해제로 온/오프 합니다. 스위치 달고 싶으면 배터리 커넥터 뒤, 7.4V가 아두이노와 UBEC로 가기 전에 다는 게 좋습니다.
브레드보드 버전
프로 미니, 서보 커넥터, 기타 전자부품을 하프 사이즈 브레드보드 한 장에 올릴 수 있습니다. 아래 그림처럼 회로 구성하세요. 특히 서보용 5V 전원/그라운드 연결엔 짧은 점퍼 와이어 쓰세요. 서보 커넥터는 긴 수핀 헤더를 3핀씩 잘라 브레드보드에 꽂아 만듭니다.
그림에 없는 것: 배터리와 UBEC. 배터리 커넥터에 맞게 납땜이 필요할 수 있습니다. 커넥터에서 점퍼 와이어 두 가닥을 브레드보드 하단 '파워 레일'에 연결해 프로 미니에 공급 (RAW, GND 핀). 또한 7.4V 공급선에서 A0 핀으로 전압 분배용 저항 두 개 연결: 2.2kΩ은 양극 쪽, 1kΩ은 그라운드 쪽. 완충 시 8V 이상인 배터리 전압을 5V 이하로 나눠 아날로그 핀에서 읽을 수 있게 합니다.
UBEC 출력 측엔 서보 커넥터가 있습니다. 브레드보드 상단 '파워 레일' 중간에 수핀 헤더 두 개 꽂아 두면 서보 전원 분배가 균일해집니다.
IR 수신기는 A1 핀에 연결, 5V 공급. 수신기 핀이 길어 브레드보드 구멍에 바로 꽂힙니다.
아래 회로도와 완성된 브레드보드 사진 참조. 사진은 핀아웃/연결이 다른 구버전 로봇이지만 점퍼 와이어/서보 커넥터 연결 아이디어 참고용으로 보세요.
PCB 버전
아래 프리칭 파일로 PCB 제작 가능. 프리칭 서비스에서 주문하거나 제조용 파일로 내보내 외부 제작 의뢰 가능. 이 로봇 전용으로 모든 서보, IR, 전압 측정 커넥터 있고, 남은 핀들도 커넥터로 빼놔 향후 확장 용이.
몸체에 PCB 모서리에 맞는 작은 '패드' 있습니다. 여기서는 D3~D5 커넥터 있는 모서리가 앞/왼쪽 오게 배치. PCB에 마운트 구멍 있지만 양면 테이프로 몸체에 붙여도 잘 고정됩니다.
배터리
배터리는 벨크로로 몸체 아랫면에 고정. 전용 평평한 면 있습니다. 7.4V/500mAh 리포 배터리는 보통 55×30×10mm 정도 폼팩터라 이 공간에 잘 들어갑니다.
마지막으로 서보 선들을 깔끔하게 묶어 걸리지 않게 정리. 걷는 4족 생물체 같아 보여 훨씬 멋집니다. :)
모든 걸 맞췄는데도 넘어지면 무게중심 확인. 배터리 위치를 벨크로로 옮겨 밸런스 잡으세요.
다시 강조: 리포 배터리 조심히 다루세요.
향후 개선 아이디어
이 로봇을 공개하며 디자인 개선(기능 추가, 다른 레이아웃, 더 멋진 외관)을 환영합니다. 코드는 약간 다른 레이아웃/크기 로봇에도 재사용 가능합니다. 아래 스케치는 코드 내 상수들 의미입니다. 모든 IK와 이동 함수는 다른 치수 로봇에서도 작동해야 합니다. 좌표계: x가 전방 방향.
리모컨 버튼 몇 개가 남았는데 여기에 기능 부여하면 재밌을 겁니다 (춤추기, 다른 동작 시퀀스 등).
저는 아날로그 입력 실험 중입니다. '걷면서 회전' 보행 패턴으로 로버를 어느 정도 조향하거나 자이로/나침반으로 경로 보정하는 것도 연구했습니다. 초음파 센서 달고 자율 회피 행동도 추가했습니다. 현재 프로젝트는 아날로그 조종+자율 행동을 스마트폰으로 통합 제어하는 것. 블링크(Blynk), ESP8266, 장치 간 시리얼 통신 등 새로 배울 게 많네요. 조만간 개선판 로봇을 선보이거나(아니면 나보다 실력 좋은 누군가가 먼저 하겠죠) :)
코드
KITtyBot2 코드 (Arduino)
아두이노 미니에 업로드
/* IR 제어 버전 KITtyBot 2. 아두이노 프로 미니와 내가 설계한 PCB 보드 사용 (프리칭 스케치 Kittybotmini.fzz)
이전 로봇 KITtyBot, KITtyBot mini 기반, IR 리모컨으로 제어
NEC(아다프루트) 리모컨과 IRLib2 라이브러리 사용, https://github.com/cyborg5/IRLib2
IRLib2 라이브러리 저장소에서 다운로드해 지침대로 설치
일반 치수는 원본 KITtyBot과 유사하나 감마-알파 축 사이 12mm 변위 있음 (서보가 위아래로 장착)
보행/회전 걸음걸이 계속 조정 중이지만 현재 이게 가장 안정적
제작: Staffan Ek 2017 */
#include
#include
#include // 디코드 베이스 먼저 포함
#include // 사용하는 프로토콜만 포함
#define MY_PROTOCOL NEC // IR 제어 프로토콜 (NEC)
long Previous;
IRrecv My_Receiver(A1); // A0 핀에서 수신
IRdecodeNEC My_Decoder;
const int servonum = 12; // 서보 개수
Servo servo[servonum]; // 서보 객체 생성
const float servodeg0[12] = {90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90}; // 서보 중립 위치 (공칭 90도에서 보정 필요)
float servodegnew[servonum]; // 목표 서보 위치 (도)
float servodegold[servonum]; // 현재 서보 위치
// KITtyBot mini용 아래 값들 업데이트
const int servodir[12] = { +1, +1, -1, -1, -1, +1, -1, -1, -1, +1, +1, +1 }; // 회전 방향 (양수 = 서보 반시계 방향)
const float pi = 3.1416;
const float alfa0 = pi / 6; // 알파 중립 위치 (30도)
const float beta0 = pi / 3; // 베타 중립 위치 (60도)
const float jointlength = 50; // 다리 부품 길이 (상/하 동일)
const float width = 120; // 폭 (발 사이 y거리, 토아웃 포함)
const float leng = 120; // 길이 (발 사이 x거리)
const float distag = 12; // 알파-감마 축 간 거리
const float toeout0 = 20; // 감마 서보 중심에서 발 바깥쪽 거리 (발이 바깥으로 향한 거리)
const float leglength0 = 2 * jointlength * cos(alfa0);
const float gamma0 = asin(toeout0 / (leglength0 + distag)); // 감마 중립 위치 (토아웃 20mm, distag 12mm 고려)
const float bodyradius = sqrt(pow((width / 2), 2) + pow((leng / 2), 2)); // 대각선 길이 (중심에서 발 모서리까지)
const float phi0 = atan(width / leng); // bodyradius와 x축(전방) 사이 각도
const float height0 = sqrt(pow(leglength0 + distag, 2) - pow(toeout0, 2)); // 로봇 정상 높이 (각도/거리 변경 시 업데이트 필요)
float leglength[4] = {sqrt(pow(height0, 2) + pow(toeout0, 2)), sqrt(pow(height0, 2) + pow(toeout0, 2)), sqrt(pow(height0, 2) + pow(toeout0, 2)), sqrt(pow(height0, 2) + pow(toeout0, 2)) }; // 레그 길이 초기값
unsigned long timestep = 500; // 각 시퀀스 소요 시간 (servomove() 사용 시)
int steplength = 40; // 보행 시 한 걸음 x방향 길이 (전진/후진 크립)
float phi = 20; // 회전 시 회전각 (도 단위, 라디안 아님!)
// 이동 변수
float footpos[12]; // 발 위치, 순서: LeftFront_xyz, LeftRear_xyz, RightFront_xyz, RightRear_xyz
float stepturn[12] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // 회전 시 발 이동량
// 발 위치는 각자 시작 위치 기준 보정
const float jointangle0[12] = {alfa0, beta0, 0, alfa0, beta0, 0, alfa0, beta0, 0, alfa0, beta0, 0};
float jointangle[12]; // 관절 각도 벡터, 순서: LeftFront_AlfaBetaGamma 등
const int voltagepin = A0; // 전압 측정 핀
int lowvolt = 0; // 전압 7.0V 미만이면 로봇 정지 플래그
int mode = 0; // 현재 명령된 보행 모드: 전진, 후진, 좌회전, 우회전
void setup() {
Serial.begin(9600);
Serial.println("KITtyBot mini");
// 설정 확인용 출력. 지워도 됨.
Serial.print("Gamma0: "); Serial.println(gamma0);
Serial.print("Leglength0: "); Serial.println(leglength0);
Serial.print("Bodyradius: "); Serial.println(bodyradius);
Serial.print("Phi0: "); Serial.println(phi0);
Serial.print("Height0: "); Serial.println(height0);
servo[0].attach(3); servo[1].attach(4); servo[2].attach(5);
servo[3].attach(6); servo[4].attach(7); servo[5].attach(8);
servo[6].attach(2); servo[7].attach(A3); servo[8].attach(12);
servo[9].attach(11); servo[10].attach(10); servo[11].attach(9);
for (int i = 0; i < servonum; i++) {
servodegnew[i] = servodeg0[i];
servodegold[i] = servodegnew[i];
servo[i].write(servodegnew[i]);
}
delay(5000);
My_Receiver.enableIRIn(); // 수신기 시작
}
void loop() {
voltmeasure(); // 최소 여기서는 전압 체크
while (lowvolt == 0) {
bodyxyz(0, 0, 0); // 일단 모두 센터로
servomove();
mode = 0;
IRread();
Serial.print("Mode: "); // 시리얼 모니터링용, 지워도 됨
Serial.println(mode);
switch (mode) {
case 1: forwardcreep(); break;
case 2: reversecreep(); break;
case 3: leftturn(); break;
case 4: rightturn(); break;
}
voltmeasure();
delay(500);
}
if (lowvolt == 1) {
bodyxyz(0, 0, -30); // 몸체 낮춤, 배터리 충전 필요 신호
servomove();
}
}
// 아래는 올바른 순서로 호출되어 새 각도 계산하는 함수들
void lengthangles() {
// 앞왼쪽 발
jointangle[2] = gammaleft(footpos[1], footpos[2]);
leglength[0] = legleft(footpos[0], footpos[2], jointangle[2]);
jointangle[1] = beta(leglength[0]);
jointangle[0] = alfafront(footpos[0], jointangle[1], leglength[0]);
// 뒤왼쪽 발
jointangle[5] = gammaleft(footpos[4], footpos[5]);
leglength[1] = legleft(footpos[3], footpos[5], jointangle[5]);
jointangle[4] = beta(leglength[1]);
jointangle[3] = alfarear(footpos[3], jointangle[4], leglength[1]);
// 앞오른쪽 발
jointangle[8] = gammaright(footpos[7], footpos[8]);
leglength[2] = legright(footpos[6], footpos[8], jointangle[8]);
jointangle[7] = beta(leglength[2]);
jointangle[6] = alfafront(footpos[6], jointangle[7], leglength[2]);
// 뒤오른쪽 발
jointangle[11] = gammaright(footpos[10], footpos[11]);
leglength[3] = legright(footpos[9], footpos[11], jointangle[11]);
jointangle[10] = beta(leglength[3]);
jointangle[9] = alfarear(footpos[9], jointangle[10], leglength[3]);
}
// IK 계산용 함수들
// 감마, '위쪽' 고관절 서보
float gammaleft(float dy, float dz) {
float gresult = atan((toeout0 + dy) / (height0 - dz)) - gamma0;
return gresult;
}
float gammaright(float dy, float dz) {
float gresult = gamma0 - atan((toeout0 - dy) / (height0 - dz));
return gresult;
}
// 다리 길이 계산 (알파 축에서 발끝까지)
float legleft(float dx, float dz, float gamma) {
float lresult = sqrt(pow(leglength0 - (dz / cos(gamma0 + gamma)), 2) + pow(dx, 2));
if (lresult > 2 * jointlength) lresult = 2 * jointlength; // 너무 길면 이후 함수 불안정
return lresult;
}
float legright(float dx, float dz, float gamma) {
float lresult = sqrt(pow(leglength0 - (dz / cos(gamma0 - gamma)), 2) + pow(dx, 2));
if (lresult > 2 * jointlength) lresult = 2 * jointlength;
return lresult;
}
// 베타, '무릎 관절'
float beta(float leg) {
float bresult = 2 * acos(leg / (2 * jointlength));
return bresult;
}
// 알파, 다른 고관절 서보
float alfafront(float dx, float beta, float leg) {
float aresult = (beta / 2) - asin(dx / leg);
return aresult;
}
float alfarear(float dx, float beta, float leg) {
float aresult = (beta / 2) + asin(dx / leg);
return aresult;
}
// 회전 각도 f(도) 기반 발 위치 계산. stepturn에 저장해 footpos에 사용
void turnpos(float f) {
stepturn[0] = bodyradius * cos(phi0 + (f * pi / 180)) - leng / 2;
stepturn[1] = bodyradius * sin(phi0 + (f * pi / 180)) - width / 2;
stepturn[3] = bodyradius * cos(pi - phi0 + (f * pi / 180)) + leng / 2;
stepturn[4] = bodyradius * sin(pi - phi0 + (f * pi / 180)) - width / 2;
stepturn[6] = bodyradius * cos(2 * pi - phi0 + (f * pi / 180)) - leng / 2;
stepturn[7] = bodyradius * sin(2 * pi - phi0 + (f * pi / 180)) + width / 2;
stepturn[9] = bodyradius * cos(pi + phi0 + (f * pi / 180)) + leng / 2;
stepturn[10] = bodyradius * sin(pi + phi0 + (f * pi / 180)) + width / 2;
}
// 위 함수들로 구한 관절 각도(라디안)를 서보 위치(도)로 변환
void servopos() {
for (int i = 0; i < 12; i++) {
servodegnew[i] = servodeg0[i] + servodir[i] * (jointangle[i] - jointangle0[i]) * 180 / pi;
}
}
// 동기화된 서보 이동 알고리즘. 모든 서보가 timestep 끝에 목표 위치 도달
void servomove() {
int servotimeold[servonum];
int servotimenew[servonum];
int SERVOPULSE[servonum];
float servodeg[servonum];
float servodegspeed[servonum];
unsigned long starttime = millis();
unsigned long timenow = starttime;
for (int i = 0; i < servonum; i++) {
servodegspeed[i] = (servodegnew[i] - servodegold[i]) / timestep;
servodeg[i] = servodegold[i];
servotimeold[i] = starttime;
}
while ((timenow - starttime) < timestep) {
for (int i = 0; i < servonum; i++) {
servotimenew[i] = millis();
servodeg[i] += servodegspeed[i] * (servotimenew[i] - servotimeold[i]);
servo[i].write(servodeg[i]);
servotimeold[i] = servotimenew[i];
}
timenow = millis();
}
for (int i = 0; i < servonum; i++) {
servo[i].write(servodegnew[i]);
servodegold[i] = servodegnew[i];
}
}
// 동기화 불필요 시 빠른 이동 (한 다리 들기/옮기기 등)
void servomovefast() {
for (int i = 0; i < servonum; i++) {
servo[i].write(servodegnew[i]);
servodegold[i] = servodegnew[i];
}
delay(100);
}
// 발 위치(xyz 좌표) 계산
void footxyz(int i, float x, float y, float z) {
footpos[3 * i] = x;
footpos[3 * i + 1] = y;
footpos[3 * i + 2] = z;
lengthangles();
servopos();
}
// 발 이동, 현재 위치에 원하는 값 더하기
void footmovexyz(int i, float x, float y, float z) {
footpos[3 * i] += x;
footpos[3 * i + 1] += y;
footpos[3 * i + 2] += z;
lengthangles();
servopos();
}
// 몸체 위치 xyz 좌표로 계산
void bodyxyz(float x, float y, float z) {
// 몸체 이동 = 발을 반대 방향으로 이동, 따라서 마이너스 부호
for (int i = 0; i < 4; i++) {
footpos[3 * i] = -x;
footpos[3 * i + 1] = -y;
footpos[3 * i + 2] = -z;
}
lengthangles();
servopos();
}
// 몸체 이동, 기존 위치에 좌표 더하기
void bodymovexyz(float x, float y, float z) {
for (int i = 0; i < 4; i++) {
footpos[3 * i] -= x;
footpos[3 * i + 1] -= y;
footpos[3 * i + 2] -= z;
}
lengthangles();
servopos();
}
// 몸체 비틀기 (desired angle phi)
void bodytwist(float f) {
turnpos(-f);
for (int i = 0; i < 12; i++) {
footpos[i] += stepturn[i];
}
lengthangles();
servopos();
}
// 발 동작: 들어올리기 → xy 이동 → 내리기
void footstep(int i, float x, float y) {
footmovexyz(i, 0, 0, 30);
servomovefast();
footmovexyz(i, x, y, 0);
servomovefast();
footmovexyz(i, 0, 0, -30);
servomovefast();
}
// 회전 각도 기반 발 동작, 발이 회전 궤적을 따라 이동
void footstepangle(int i, float f) {
turnpos(f);
footmovexyz(i, 0, 0, 30);
servomovefast();
footmovexyz(i, stepturn[3 * i], stepturn[3 * i + 1], 0);
servomovefast();
footmovexyz(i, 0, 0, -30);
servomovefast();
}
// 전압 체크, 저배터리 시 lowvolt 플래그 변경
void voltmeasure() {
/* 7.6V 배터리를 2.2k 저항(BAT→voltagepin), 1.0k 저항(voltagepin→GND)으로 분압
5V 아날로그 입력으로 16V 측정 범위 확보 */
float voltsig = analogRead(voltagepin);
float voltage = voltsig * 16 / 1023.0;
Serial.print("Battery: ");
Serial.println(voltage);
if (voltage < 7.0) {
lowvolt = 1;
} else {
lowvolt = 0;
}
}
// IR 수신 함수, 아다프루트 예제 기반
void IRread() {
if (My_Receiver.getResults()) {
Serial.print("Recieving ");
Serial.println(My_Decoder.value);
if (My_Decoder.decode()) {
if (My_Decoder.value == 0xFFFFFFFF) {
// 버튼 홀드 감지, 이전 값 유지
My_Decoder.value = Previous;
}
switch (My_Decoder.value) {
// 방향키 감지 시 mode 파라미터 설정
case 0xfda05f: mode = 1; break; // 전진
case 0xfdb04f: mode = 2; break; // 후진
case 0xfd10ef: mode = 3; break; // 좌회전
case 0xfd50af: mode = 4; break; // 우회전
}
Previous = My_Decoder.value;
}
My_Receiver.enableIRIn();
} else {
mode = 0;
}
}
// 전진 크립(Crawl) 걸음걸이
void forwardcreep() {
bodymovexyz(steplength / 4, -toeout0, 0); // 전진 자세 잡기, 오른쪽으로 기울임
servomove();
footstep(1, steplength / 2, 0); // 뒤왼쪽 다리 반걸음
footstep(0, steplength / 2, 0); // 앞왼쪽 다리 반걸음
bodymovexyz(steplength / 4, 2 * toeout0, 0); // 몸체 전진 및 왼쪽으로 이동 (오른쪽 다리 움직일 준비)
servomove();
while (mode == 1) { // 전진 명령(mode 1) 유지되는 동안 반복
footstep(3, steplength, 0); // 뒤오른쪽 다리 한걸음
footstep(2, steplength, 0); // 앞오른쪽 다리 한걸음
bodymovexyz(steplength / 2, -2 * toeout0, 0); // 몸체 전진 및 오른쪽으로 이동
servomove();
footstep(1, steplength, 0); // 뒤왼쪽 다리 한걸음
footstep(0, steplength, 0); // 앞왼쪽 다리 한걸음
bodymovexyz(steplength / 2, 2 * toeout0, 0); // 몸체 전진 및 왼쪽으로 이동
servomove();
// while 루프 한 바퀴 돌면 제자리지만 steplength만큼 전진함
IRread(); // 여전히 전진 명령이면(mode==1) 반복
}
// while 루프 종료, 정상 자세로 복구
footstep(3, steplength / 2, 0); // 반걸음으로 모든 다리 중립화
footstep(2, steplength / 2, 0);
bodyxyz(0, 0, 0); // 몸체 센터
servomove();
}
// 후진 걸음걸이 (주석 적음, forwardcreep 참조)
void reversecreep() {
bodymovexyz(-steplength / 4, -toeout0, 0);
servomove();
footstep(0, -steplength / 2, 0);
footstep(1, -steplength / 2, 0);
bodymovexyz(-steplength / 4, 2 * toeout0, 0);
servomove();
while (mode == 2) {
footstep(2, -steplength, 0);
footstep(3, -steplength, 0);
bodymovexyz(-steplength / 2, -2 * toeout0, 0);
servomove();
footstep(0, -steplength, 0);
footstep(1, -steplength, 0);
bodymovexyz(-steplength / 2, 2 * toeout0, 0);
servomove();
IRread();
}
footstep(2, -steplength / 2, 0);
footstep(3, -steplength / 2, 0);
bodyxyz(0, 0, 0);
servomove();
}
// 원하는 phi 각도로 좌회전
void leftturn() {
while (mode == 3) {
bodyxyz(toeout0 / 2, toeout0, 0); // 행동 전 왼쪽으로 기울임
servomove();
footstepangle(3, phi); // 뒤오른쪽 발 새 위치로
footstepangle(2, phi); // 앞오른쪽 발 새 위치로
footxyz(0, -toeout0 / 2 - stepturn[0], toeout0 - stepturn[1], 0);
footxyz(1, -toeout0 / 2 - stepturn[3], toeout0 - stepturn[4], 0);
footxyz(2, -toeout0 / 2, toeout0, 0);
footxyz(3, -toeout0 / 2, toeout0, 0);
// 몸체 비틀기 + 왼쪽으로 기울임. 절대 좌표로 써서 오차 최소화
servomove();
footstepangle(0, phi); // 앞왼쪽 발
footstepangle(1, phi); // 뒤왼쪽 발
IRread(); // 왼쪽 버튼 여전히 눌림(mode==3)이면 반복
}
bodyxyz(0, 0, 0); // 회전 끝나면 몸체 센터
servomove();
}
// 우회전. 좌회전과 대칭되게 구현. 둘 다 테스트 중.
void rightturn() {
while (mode == 4) {
bodyxyz(-toeout0 / 2, toeout0, 0); // 왼쪽으로 기울임
servomove();
footstepangle(2, -phi); // 앞오른쪽 발
footstepangle(3, -phi); // 뒤오른쪽 발
footxyz(0, toeout0 / 2 - stepturn[0], toeout0 - stepturn[1], 0);
footxyz(1, toeout0 / 2 - stepturn[3], toeout0 - stepturn[4], 0);
footxyz(2, toeout0 / 2, toeout0, 0);
footxyz(3, toeout0 / 2, toeout0, 0);
servomove();
footstepangle(1, -phi); // 뒤왼쪽 발
footstepangle(0, -phi); // 앞왼쪽 발
IRread();
}
bodyxyz(0, 0, 0);
servomove();
}
커스텀 파트 및 인클로저
로봇 몸체. PCB와 배터리 장착.
범퍼. 몸체 양끝에 장착 (자동차 범퍼처럼). 두 개 필요.
고관절 서포트. 네 개 출력.
상지(Thigh, 다리 윗부분). 네 개 출력.
하지(Lower leg, 발). 네 개 출력.
모든 부품 조립도. 출력용 아님.
회로도
PCB 제작용 프리칭 파일. 프리칭에서 주문하거나 적합한 포맷으로 내보내 외부 제작 의뢰.