우리 팀은 로봇 손에 대한 오랜 역사를 가지고 있습니다. 한동안 신뢰할 수 있는 의수 제작을 시도했지만, 이 프로젝트에서는 기존 오픈소스 손인 InMoov의 좋은 예제를 사용합니다.
손 조립 세부 사항은 다루지 않겠습니다. 프로젝트 사이트에 잘 설명되어 있고 꽤 복잡합니다. 저는 완전히 새로운 제어 부분에 집중하겠습니다. :) 이 기술이 시간이 지남에 따라 어떻게 발전했는지 다음 프로젝트에서 확인해 보세요: 8채널 EMG with LEDs
1. 신호 처리
제어는 EMG(근전도) - 근육의 전기적 활동을 기반으로 합니다. EMG 신호는 3개의 uECG 장치로 얻습니다(원래는 ECG 모니터용이지만, 범용 ADC 기반이라 EMG를 포함한 모든 생체 신호 측정 가능). EMG 처리를 위해 uECG에는 32빈 스펙트럼 데이터와 "근육 윈도우" 평균(75~440Hz 사이의 평균 스펙트럼 강도)을 전송하는 특수 모드가 있습니다. 스펙트럼 이미지는 다음과 같습니다:
수직 축은 주파수(각 플롯에서 하단이 저주파, 상단이 고주파 - 0~488Hz, 약 15Hz 단계), 수평 축은 시간(왼쪽이 과거 데이터, 전체 약 10초)입니다. 강도는 색상으로 인코딩: 파랑-낮음, 초록-중간, 노랑-높음, 빨강-더 높음.
신뢰할 수 있는 제스처 인식을 위해서는 이러한 이미지의 적절한 PC 처리가 필요합니다. 하지만 로봇 손 손가락의 단순 활성화를 위해서는 3개 채널의 평균값만 사용해도 충분합니다. uECG는 특정 패킷 바이트에서 이 값을 편리하게 제공하므로 아두이노 스케치로 파싱할 수 있습니다. 이 값들은 훨씬 단순해 보입니다:
빨강, 초록, 파랑 차트는 엄지, 약지, 중지를 각각 쥐었을 때 다른 근육 그룹에서 측정된 3개 uECG 장치의 원시 값입니다. 육안으로는 이 케이스들이 명확히 구분되지만, 프로그램이 서보 출력을 위해 이 값들을 "손가락 점수"로 변환해야 합니다. 문제는 근육 그룹 신호들이 "혼합"되어 있다는 것입니다. 1번째와 3번째 케이스에서 파란 신호 강도는 비슷하지만 빨강과 초록이 다릅니다. 2번째와 3번째에서는 초록 신호가 같지만 파랑과 빨강이 다릅니다. 이를 "분리"하기 위해 비교적 간단한 공식을 사용했습니다:
S0 = V0² / ((V1 × a0 + b0) × (V2 × c0 + d0))
여기서 S0은 채널 0의 점수, V0, V1, V2는 채널 0, 1, 2의 원시 값, a, b, c, d는 수동으로 조정한 계수입니다(a와 c는 0.3~2.0, b와 d는 15와 20, 센서 배치에 따라 조정 필요). 채널 1과 2에도 동일한 점수 계산 적용 후 차트는 거의 완벽하게 분리되었습니다:
동일한 제스처(이번엔 약지, 중지, 엄지 순)에 대해 신호가 명확해져 임계값과 비교만으로 서보 움직임으로 쉽게 변환 가능합니다.
2. 회로도
회로도는 상당히 단순합니다. nRF24 모듈, PCA9685 또는 유사 I2C PWM 컨트롤러, 모든 서보를 동시에 구동할 수 있는 고용량 5V 전원 공급 장치(안정적인 작동을 위해 최소 5A 정격 필요)만 있으면 됩니다.
연결 목록:
nRF24 핀 1 (GND) - 아두이노 GND
nRF24 핀 2 (Vcc) - 아두이노 3.3V
nRF24 핀 3 (Chip Enable) - 아두이노 D9
nRF24 핀 4 (SPI:CS) - 아두이노 D8
nRF24 핀 5 (SPI:SCK) - 아두이노 D13
nRF24 핀 6 (SPI:MOSI) - 아두이노 D11
nRF24 핀 7 (SPI:MISO) - 아두이노 D12
PCA9685 SDA - 아두이노 A4
PCA9685 SCL - 아두이노 A5
PCA9685 Vcc - 아두이노 5V
PCA9685 GND - 아두이노 GND
PCA9685 V+ - 고용량 5V
PCA9685 GND - 고용량 GND
손가락 서보: PCA 채널 0-4 (엄지-채널 0, 검지-채널 1 등)
3. EMG 센서 배치
근육 활동을 기록하는 uECG 장치를 올바른 위치에 배치하는 것이 중요합니다. 다양한 옵션이 가능하지만 각각 다른 신호 처리 접근법이 필요하므로 제가 사용한 배치를 공유합니다:
직관적이지 않을 수 있지만, 엄지 근육 신호는 팔의 반대쪽에서 더 잘 보입니다. 따라서 센서 중 하나는 반대쪽에 배치하고, 모두 팔꿈치 근처에 배치합니다(근육의 대부분이 그곳에 있지만, 개인의 차이가 크므로 본인의 근육 위치를 확인하세요).
4. 코드
메인 프로그램을 실행하기 전에 각 uECG 장치의 단위 ID를 찾아야 합니다(라인 101 주석 해제 후 장치를 하나씩 켜서 확인). 이를 unit_ids 배열(라인 37)에 입력하세요.
#include <SPI.h>
#include <RF24.h>
#include <RF24_config.h>
#include <nRF24L01.h>
#include <Adafruit_PWMServoDriver.h>
#include <Wire.h>
#define SERVOMIN 150 // 최소 펄스 길이 카운트 (4096 중)
#define SERVOMAX 600 // 최대 펄스 길이 카운트 (4096 중)
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();
int rf_cen = 9; // nRF24 chip enable pin
int rf_cs = 8; // nRF24 CS pin
RF24 rf(rf_cen, rf_cs);
// 파이프 주소 - uECG 측에서 하드코딩
uint8_t pipe_rx[8] = {0x0E, 0xE6, 0x0D, 0xA7, 0, 0, 0, 0};
uint8_t swapbits(uint8_t a) { // uECG 파이프 주소는 비트 순서가 바뀜
uint8_t v = 0;
if (a & 0x80) v |= 0x01;
if (a & 0x40) v |= 0x02;
if (a & 0x20) v |= 0x04;
if (a & 0x10) v |= 0x08;
if (a & 0x08) v |= 0x10;
if (a & 0x04) v |= 0x20;
if (a & 0x02) v |= 0x40;
if (a & 0x01) v |= 0x80;
return v;
}
long last_servo_upd = 0; // 마지막으로 서보 값을 업데이트한 시간
byte in_pack[32]; // 수신 RF 패킷용 배열
unsigned long unit_ids[3] = {4294963881, 4294943100, 28358}; // 알려진 uECG ID - 본인의 ID로 변경 필요
int unit_vals[3] = {0, 0, 0}; // 이 ID들의 uECG 값
float tgt_angles[5]; // 5개 손가락 목표 각도
float cur_angles[5]; // 5개 손가락 현재 각도
float angle_open = 30; // 손가락 펴진 각도
float angle_closed = 150; // 손가락 접힌 각도
void setup() {
SPI.begin();
SPI.setBitOrder(MSBFIRST);
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
for (int x = 0; x < 8; x++)
pipe_rx[x] = swapbits(pipe_rx[x]);
rf.begin();
rf.setDataRate(RF24_1MBPS);
rf.setAddressWidth(4);
rf.setChannel(22);
rf.setRetries(0, 0);
rf.setAutoAck(0);
rf.disableDynamicPayloads();
rf.setPayloadSize(32);
rf.openReadingPipe(0, pipe_rx);
rf.setCRCLength(RF24_CRC_DISABLED);
rf.disableCRC();
rf.startListening(); // uECG 데이터 수신 대기
// uECG는 롱 프레스로 raw 데이터 모드로 전환해야 함
// 기본적으로 BLE 모드로 전송되어 nRF24로 수신 불가
Serial.begin(115200); // 디버깅용 시리얼 출력
pwm.begin(); // PWM 드라이버 시작
pwm.setPWMFreq(60); // 아날로그 서보는 ~60Hz 업데이트
for (int i = 0; i < 5; i++) {
tgt_angles[i] = angle_open;
cur_angles[i] = angle_open;
}
}
void setAngle(int n, float angle) {
pwm.setPWM(n, 0, SERVOMIN + angle * 0.005556 * (SERVOMAX - SERVOMIN));
}
float angle_speed = 15; // 손가락 이동 속도
float v0 = 0, v1 = 0, v2 = 0; // 3채널 필터링된 근육 활동 값
void loop() {
if (rf.available()) {
rf.read(in_pack, 32); // 패킷 처리
byte u1 = in_pack[3]; // 32비트 단위 ID, 각 uECG마다 고유
byte u2 = in_pack[4];
byte u3 = in_pack[5];
byte u4 = in_pack[6];
unsigned long id = (u1 << 24) | (u2 << 16) | (u3 << 8) | u4;
// Serial.println(id); // 이 줄 주석 해제하여 uECG ID 목록 확인
if (in_pack[7] != 32) id = 0; // 잘못된 패킷 타입: EMG 모드에서 이 바이트는 32여야 함
int val = in_pack[10]; // 근육 활동 값
if (val != in_pack[11]) id = 0; // RF 노이즈로 패킷 손상 가능하므로 2바이트에 값 중복 저장, CRC 없음
// 현재 ID에 해당하는 ID 찾고 값 채우기
for (int n = 0; n < 3; n++)
if (id == unit_ids[n])
unit_vals[n] = val;
}
long ms = millis();
if (ms - last_servo_upd > 20) { // 서보 너무 자주 업데이트 안 함
last_servo_upd = ms;
for (int n = 0; n < 5; n++) { // 손가락별로 목표/현재 각도 불일치 시 조정
if (cur_angles[n] < tgt_angles[n] - angle_speed / 2) cur_angles[n] += angle_speed;
if (cur_angles[n] > tgt_angles[n] + angle_speed / 2) cur_angles[n] -= angle_speed;
}
for (int n = 0; n < 5; n++) // 손가락에 각도 적용
setAngle(n, cur_angles[n]);
// 지수 평균화: 단일 피크가 손가락 상태에 영향 주는 것 방지
v0 = v0 * 0.7 + 0.3 * (float)unit_vals[0];
v1 = v1 * 0.7 + 0.3 * (float)unit_vals[1];
v2 = v2 * 0.7 + 0.3 * (float)unit_vals[2];
// 원시 값으로부터 점수 계산
float scor0 = 4.0 * v0 * v0 / ((v1 * 0.3 + 20) * (v2 * 1.3 + 15));
float scor1 = 4.0 * v1 * v1 / ((v0 * 2.0 + 20) * (v2 * 2.0 + 20));
float scor2 = 4.0 * v2 * v2 / ((v0 * 1.2 + 20) * (v1 * 0.5 + 15));
// 디버깅용 점수 출력
Serial.print(scor0);
Serial.print(' ');
Serial.print(scor1);
Serial.print(' ');
Serial.println(scor2);
// 각 점수를 임계값과 비교하여 손가락 상태 변경
if (scor2 < 0.5) // 약한 신호 - 손가락 펴기
tgt_angles[0] = angle_open;
if (scor2 > 1.0) // 강한 신호 - 손가락 접기
tgt_angles[0] = angle_closed;
if (scor1 < 0.5) {
tgt_angles[1] = angle_open;
tgt_angles[2] = angle_open;
}
if (scor1 > 1.0) {
tgt_angles[1] = angle_closed;
tgt_angles[2] = angle_closed;
}
if (scor0 < 0.5) {
tgt_angles[3] = angle_open;
tgt_angles[4] = angle_open;
}
if (scor0 > 1.0) {
tgt_angles[3] = angle_closed;
tgt_angles[4] = angle_closed;
}
}
}
5. 결과
약 2시간의 실험으로 꽤 신뢰할 수 있는 작동을 달성했습니다(영상은 일반적인 경우):
완벽하지는 않고, 이 처리로는 손가락 펴기/접기만 인식 가능하며 5개 손가락 각각이 아닌 3개 근육 그룹만 감지합니다(엄지, 검지+중지 함께, 약지+새끼손가락 함께). 하지만 신호를 분석하는 "AI"가 여기서는 단 3줄 코드에 각 채널의 단일 값만 사용합니다. PC나 스마트폰에서 32빈 스펙트럼 이미지를 분석하면 훨씬 더 많은 것을 할 수 있을 것입니다. 또한 이 버전은 3개 uECG 장치(EMG 채널)만 사용합니다. 더 많은 채널이면 정말 복잡한 패턴 인식도 가능할 것입니다. 그게 이 프로젝트의 포인트입니다 - 관심 있는 분들을 위한 출발점 제공 :) 손 제어가 이런 시스템의 유일한 응용은 아닙니다.