슬롯카(전동 미니카) 트랙에 레이저 게이트를 설치해 랩타임을 초/밀리초 단위로 측정하는 타이머입니다. 레이저 빔이 차단되면(차량 통과) 랩 완료로 인식해 현재 랩타임, 이전 랩타임, 베스트 랩타임, 총 랩 수를 GLCD에 실시간 표시합니다. 디바운스 로직으로 노이즈 방지, 전원 투자입 후 50ms 대기해 오작동 방지.
하드웨어 연결
KY-008 레이저 모듈 (3핀: -, +, S)
- → GND, + → 5V
S 핀을 5V에 직결하면 레이저 상시 켬. 원하면 아두이노 디지털 핀 연결해 제어 가능
레이저 수신 센서 (3핀: GND, VCC, SIGNAL)
GND → GND, VCC → 5V
SIGNAL → 아두이노 디지털 핀 2 (인터럽트 핀 권장, 폴링 방식 사용)
GLCD (KS0108, 20핀)
데이터 라인 8비트(D0~D7) + 제어선(RS, RW, EN, CS1, CS2, RST, VEE 대비조절, 백라이트) 필요. 아두이노 메가의 다수 GPIO 활용 권장. 커넥터 제작해 탈착식으로 만들면 편리.
가장 어려운 부분: 레이저와 센서를 트랙 양쪽에 정확히 정렬해 빔이 차체 하부를 통과하도록 맞추는 것. 높이가 안 맞으면 감지 실패.
전체 펌웨어 코드 (주석 보강)
/*
* 슬롯카 랩 타이머 - 레이저 게이트 방식
* 작성자: Tal Ofer
*/
#include <openGLCD.h>
// 랩 타임 변수들
unsigned long currentRunStartMillis = 0; // 현재 랩 시작 시각
unsigned long lastRunInMillis = 0; // 이전 랩 타임
unsigned long bestRunInMillis = 0; // 베스트 랩 타임
int currentLap = 0; // 현재 랩 번호
unsigned long savedMillis = 0; // 연산용 임시 저장
// GLCD 텍스트 영역 정의
gText t1; // 랩 수 + 현재 랩타임 (큰 폰트)
gText t2; // 이전 랩타임 (작은 폰트)
gText t3; // 베스트 랩타임 (작은 폰트, 흰색)
// 디스플레이용 분리 변수
int sec_val, milli_val;
// 레이저 게이트 센서
const int gateSensorPin = 2;
int gateSensorState = HIGH;
int lastGateSensorState = HIGH;
unsigned long lastDebounceTime = 0;
const int debounceDelay = 50; // ms
void setup() {
pinMode(gateSensorPin, INPUT);
delay(50); // 센서/레이저 안정화 대기
// GLCD 초기화
GLCD.Init(NON_INVERTED);
// 텍스트 영역 정의
t1.DefineArea(textAreaTOP, lcdnums14x24); // 상단 큰 숫자 폰트
t2.DefineArea(0, GLCD.CenterY, 8, 2, fixednums7x15); // 하단 좌
t3.DefineArea(GLCD.CenterX, GLCD.CenterY, 8, 2, fixednums7x15); // 하단 우
t3.SetFontColor(WHITE);
t3.ClearArea();
// 고정 라벨 출력
GLCD.SelectFont(System5x7);
GLCD.CursorTo(1, 4); GLCD.print("LAST");
GLCD.CursorTo(11, 4); GLCD.print("BEST");
// 변수 초기화
currentRunStartMillis = lastRunInMillis = bestRunInMillis = 0;
currentLap = 0;
}
void loop() {
int reading = digitalRead(gateSensorPin);
// 디바운스: 상태 변화 시 타이머 리셋
if (reading != lastGateSensorState) {
lastDebounceTime = millis();
}
// 디바운스 지연 후 상태 확정
if ((millis() - lastDebounceTime) > debounceDelay) {
if (reading != gateSensorState) {
gateSensorState = reading;
// 빔 차단됨 (LOW) = 차량 통과
if (gateSensorState == LOW) {
savedMillis = millis(); // 같은 시각 기준 연산
if (currentLap > 0) { // 첫 랩이 아니면
lastRunInMillis = savedMillis - currentRunStartMillis;
// 베스트 갱신
if (lastRunInMillis < bestRunInMillis || bestRunInMillis == 0) {
bestRunInMillis = lastRunInMillis;
}
}
// 새 랩 시작
currentRunStartMillis = savedMillis;
currentLap++;
}
}
}
lastGateSensorState = reading;
// --- 디스플레이 업데이트 ---
// 랩 카운트 (큰 폰트 영역)
t1.CursorTo(0, 0);
t1.Printf(F("%02d"), currentLap);
// 현재 랩 진행 시간
savedMillis = millis();
if (currentLap > 0) {
calcResultFromMillis(savedMillis - currentRunStartMillis, &sec_val, &milli_val);
} else {
calcResultFromMillis(0, &sec_val, &milli_val);
}
// 현재 랩타임 (큰 폰트, 초.밀리초)
t1.CursorTo(3, 0);
t1.Printf(F("%02d.%03d"), sec_val, milli_val);
// 이전 랩타임 (작은 폰트, 좌측)
calcResultFromMillis(lastRunInMillis, &sec_val, &milli_val);
t2.CursorTo(1, 1);
t2.Printf(F("%02d.%03d"), sec_val, milli_val);
// 베스트 랩타임 (작은 폰트, 우측, 흰색)
calcResultFromMillis(bestRunInMillis, &sec_val, &milli_val);
t3.CursorTo(1, 1);
t3.Printf(F("%02d.%03d"), sec_val, milli_val);
}
// 밀리초 → 초, 밀리초 분리
void calcResultFromMillis(unsigned long value, int *sec_val, int *milli_val) {
*sec_val = int(value / 1000);
*milli_val = value - *sec_val * 1000;
}
코드 핵심 포인트
openGLCD 라이브러리로 KS0108 GLCD 완전 제어. 폰트 크기별 영역 분할 표시
디바운스 로직: 기계적/광학적 노이즈로 인한 다중 카운트 방지 (50ms)
밀리초 정밀도: millis() 기반, calcResultFromMillis()로 초/밀리초 분리 표시