초음파 눈: 8x8 LED 매트릭스 거리 센서 인터랙티브 아트
부품 및 소모품
| 부품 |
수량 |
비고 |
| 초음파 센서 HC-SR04 |
2 |
좌/우 눈 거리 측정용 |
| 아두이노 나노 R3 |
1 |
소형, USB 전원/프로그래밍, 충분한 GPIO |
| 포토 레지스터 (LDR) |
1 |
주변 밝기 감지 → LED 밝기 자동 조절 |
| MAX7219 8x8 LED 매트릭스 모듈 |
2 |
데이지 체인 연결 가능 (SPI) |
| 하프 사이즈 브레드보드 |
1 |
납땜 없이 프로토타이핑 |
| 점퍼 와이어 |
다수 |
빨강/검정은 전원/그라운드용 권장 |
| 저항 330 옴 |
1 |
LDR 분압용 |
앱 및 온라인 서비스
프로젝트 소개
초음파 센서와 8x8 LED 매트릭스로 움직이는 "눈"을 만들었습니다. 거실이나 사무실 책상에 두면 지나가는 사람을 따라 눈을 움직이고, 깜빡이기도 하며, 주변 밝기에 따라 LED 밝기가 자동 조절됩니다. 재미있고 약간 으스스한 인터랙티브 아트 작품입니다.
필요한 것들
- HC-SR04 초음파 센서 2개: 3-4m 감지 거리, 3-4선으로 아두이노 연결 간편
- 8x8 LED 매트릭스 (MAX7219) 2개: 데이지 체인으로 한 모듈만 아두이노에 연결, SPI 통신 (5선)
- 아두이노 나노: 소형, USB 내장, GPIO 충분
- LDR + 330Ω 저항: 아날로그 핀으로 0-255 밝기 값 읽어 LED 강도 조절
중요: 제공 코드는 하드웨어 SPI 사용 (SCK=D13, MOSI=D11). 나노 외 보드 사용 시 해당 보드의 하드웨어 SPI 핀 확인 필수!
배선 가이드
전원 및 그라운드
- 나노 GND → 브레드보드 GND 레일 (검정)
- 나노 3.3V (또는 5V) → 브레드보드 VCC 레일 (빨강)
- 양쪽 레일 간 GND-GND, VCC-VCC 점퍼로 연결
- ※ 일부 매트릭스 모듈은 5V 필요할 수 있음. 불안정하면 5V 핀 사용
초음파 센서 2개
- 양쪽 VCC → VCC 레일, GND → GND 레일
- 센서 1 (왼쪽 눈): TRIG→D2, ECHO→D3
- 센서 2 (오른쪽 눈): TRIG→D4, ECHO→D5
8x8 LED 매트릭스 2개 (데이지 체인)
- 모듈 1 입력측: VCC→VCC, GND→GND, CS→D10, MOSI→D11, SCK→D13
- 모듈 1 출력측 → 모듈 2 입력측: GND-GND, VCC-VCC, CS-CS, MOSI-MOSI, SCK-SCK
LDR 밝기 센서
- 브레드보드 두 행에 LDR 다리 꽂기
- 한 쪽 다리 행 → 330Ω 저항 → GND 레일
- 다른 쪽 다리 행 → 빨강 와이어 → VCC 레일
- 저항 연결된 행 → 갈색 와이어 → 아날로그 핀 A5

코드 업로드 및 테스트
- USB 연결 후 아두이노 IDE에서
UltrasonicEyes 스케치 업로드
- 센서 앞을 지나가거나 손 흔들며 눈 움직임 확인
영구 제작 및 3D 케이스
- 브레드보드 버전 검증 후 퍼프보드에 납땜 (제작자 영상 참조)
- 3D 프린터로 케이스 2파트 출력 (설정 자유, PLA 권장)
- 조립 후 추가 확장: 정전식 터치 버튼으로 눈 모양 변경 모드 등 가능
전체 소스 코드
#include // LED 매트릭스 제어 라이브러리 (라이브러리 매니저에서 설치)
#include // 초음파 센서 라이브러리 (라이브러리 매니저에서 설치)
// LED 매트릭스 핀 (하드웨어 SPI 사용)
// #define CLK_PIN 13 // 하드웨어 SPI SCK - 나노에서 D13
// #define DATA_PIN 11 // 하드웨어 SPI MOSI - 나노에서 D11
#define CS_PIN 10 // Chip Select
#define MAX_DEVICES 2 // 2개 매트릭스 (양쪽 눈)
#define LIGHT A5 // LDR 아날로그 핀
MD_MAX72XX mx = MD_MAX72XX(CS_PIN, MAX_DEVICES);
// 초음파 센서 핀
#define t1 2 // 왼쪽 눈 TRIG
#define e1 3 // 왼쪽 눈 ECHO
#define t2 4 // 오른쪽 눈 TRIG
#define e2 5 // 오른쪽 눈 ECHO
#define maxDist 400 // 최대 감지 거리 (cm)
NewPing eyeR(t2, e2, maxDist); // 오른쪽 눈
NewPing eyeL(t1, e1, maxDist); // 왼쪽 눈
// 눈 상태: 0=정면, 1=오른쪽, 2=왼쪽
int currentState = -1;
long duration1, duration2;
int distance1, distance2;
float nextBlink = 0;
float lightAmount = 0;
// 눈 패턴 비트맵 (8x8, 열 단위)
uint8_t eye_forward[8] = {
0b00111100, 0b01000010, 0b01011010, 0b10101101,
0b10111101, 0b10011001, 0b01000010, 0b00111100
};
uint8_t eye_right[8] = {
0b00111100, 0b01000010, 0b01110010, 0b11011001,
0b11111001, 0b10110001, 0b01000010, 0b00111100
};
uint8_t eye_left[8] = {
0b00111100, 0b01000010, 0b01001110, 0b10010111,
0b10011111, 0b10001101, 0b01000010, 0b00111100
};
uint8_t eye_blink[8] = {
0b00000000, 0b00111100, 0b01111110, 0b11111111,
0b10111101, 0b11000011, 0b01111110, 0b00111100
};
void setup() {
mx.begin();
pinMode(t1, OUTPUT); pinMode(e1, INPUT);
pinMode(t2, OUTPUT); pinMode(e2, INPUT);
digitalWrite(t1, LOW); digitalWrite(t2, LOW);
pinMode(LIGHT, INPUT);
ShowEye_Forward();
currentState = 0;
nextBlink = millis() + 1000;
}
void loop() {
// 주변 밝기 읽기 (0-1023 → 0-MAX_INTENSITY 매핑)
lightAmount = analogRead(LIGHT);
lightAmount = (lightAmount / 255.0) * MAX_INTENSITY;
mx.control(MD_MAX72XX::INTENSITY, lightAmount);
// 왼쪽 눈 핑 (중앙값 필터 5회)
distance1 = eyeL.ping_median(5);
delay(500); // 간섭 방지 대기
// 오른쪽 눈 핑
distance2 = eyeR.ping_median(5);
// 랜덤 깜빡임 체크
if (nextBlink < millis()) {
nextBlink = millis() + random(2000, 10000); // 2-10초 후 다음 깜빡임
// 1-13 중 6 이하면 더블 깜빡임
if (random(1, 13) <= 6) ShowEye_Blink_Dbl();
else ShowEye_Blink();
delay(250);
return; // 이번 루프 종료
}
// 두 눈 거리 차이로 시선 방향 결정
float diff = distance2 - distance1;
if (abs(diff) < 250 || (distance1 == 0 && distance2 == 0)) {
// 차이가 250cm 미만이거나 둘 다 감지 안 됨 → 정면
ShowEye_Forward(); currentState = 0;
} else if (distance2 < distance1 && distance1 > 0) {
// 오른쪽 눈이 더 가까움 → 오른쪽 응시
ShowEye_Right(); currentState = 1;
} else if (distance1 < distance2 && distance2 > 0) {
// 왼쪽 눈이 더 가까움 → 왼쪽 응시
ShowEye_Left(); currentState = 2;
}
delay(250);
}
// 눈 패턴 표시 함수들
void ShowEye_Right() {
mx.clear();
mx.control(MD_MAX72XX::INTENSITY, lightAmount);
for (uint8_t row = 0; row < 8; row++) {
mx.setColumn(row, eye_right[row]); // 왼쪽 눈 (디바이스 0)
mx.setColumn(row + 8, eye_right[row]); // 오른쪽 눈 (디바이스 1)
}
}
void ShowEye_Left() {
mx.clear();
mx.control(MD_MAX72XX::INTENSITY, lightAmount);
for (uint8_t row = 0; row < 8; row++) {
mx.setColumn(row, eye_left[row]);
mx.setColumn(row + 8, eye_left[row]);
}
}
void ShowEye_Forward() {
mx.clear();
mx.control(MD_MAX72XX::INTENSITY, lightAmount);
for (uint8_t row = 0; row < 8; row++) {
mx.setColumn(row, eye_forward[row]);
mx.setColumn(row + 8, eye_forward[row]);
}
}
void ShowEye_Blink() {
mx.clear();
mx.control(MD_MAX72XX::INTENSITY, lightAmount);
for (uint8_t row = 0; row < 8; row++) {
mx.setColumn(row, eye_blink[row]);
mx.setColumn(row + 8, eye_blink[row]);
}
delay(150); // 깜빡임 유지
// 이전 상태로 복구
if (currentState == 0) ShowEye_Forward();
else if (currentState == 1) ShowEye_Right();
else if (currentState == 2) ShowEye_Left();
}
void ShowEye_Blink_Dbl() {
mx.clear();
mx.control(MD_MAX72XX::INTENSITY, lightAmount);
for (uint8_t row = 0; row < 8; row++) {
mx.setColumn(row, eye_blink[row]);
mx.setColumn(row + 8, eye_blink[row]);
}
delay(75);
// 잠깐 이전 상태 표시
if (currentState == 0) ShowEye_Forward();
else if (currentState == 1) ShowEye_Right();
else if (currentState == 2) ShowEye_Left();
delay(75);
// 다시 깜빡임
ShowEye_Blink();
}
3D 프린트 케이스
어떤 3D 프린터로도 출력 가능, 특별 설정 불필요. 프로젝트 페이지에서 STL 파일 다운로드.
회로도

Fritzing 파일: ultrasoniceyes_final_Th84xpUyB2.fzz