정밀 서보 구동 태양 추적 플랫폼 - 최적 에너지 수확을 위한 이축 트래커
부품 및 소모품
| 부품 | 수량 | 비고 |
| 포토레지스터 (CdS 셀) | 4 | 태양광 강도 감지용 |
| Adafruit Flora RGB 네오픽셀 LED (4개 팩) | 1 | 또는 동등한 RGB 멀티컬러 LED |
| 스파크펀 12mm 푸시버튼 | 2 | 일반적인 상시개방 푸시버튼 |
| 온도 센서 | 1 | |
| 아두이노 우노 | 1 | |
| 180도 로봇 서보 (Futaba S3003 등) | 2 | 트위스트(방위각), 틸트(고도각)용 |
| 10kΩ 저항 | 6 | 포토레지스터 풀다운용 |
| 220Ω 저항 | 1 | LED 전류 제한용 |
| 100μF 커패시터 | 2 | 서보 전원 안정화용 |
| 태양광 패널 (80mm x 80mm) | 1 | 아마존 등에서 구매 가능 |
필요한 도구 및 재료
- 수공구, 소형 너트/볼트 (브라켓 제작용)
- 트위스트 서보 장착용 견고한 베이스 (나무 블록 등)
- 구부릴 수 있는 금속판 또는 알루미늄 시트 (L자 브라켓용, 약 3mm 두께, 2cm 미만 폭)
- 얇고 구부리기 쉬운 금속 (태양광 패널 플랫폼 홀더 및 트위스트 서보 마운트용, HVAC 덕트 고정용 철판 추천, 미리 구멍 뚫림)
앱 및 온라인 서비스
- 아두이노 IDE
- 마이크로소프트 엑셀 (문서화 및 노트용)
프로젝트 개요
두 개의 180도 서보와 네 개의 포토레지스터를 사용하여 소형 태양광 패널이 가장 강한 빛을 향하도록 추적하는 또 다른 구현입니다.
모터는 태양광 패널을 가장 밝은 빛 방향으로 향하게 하기 위해 작은 움직임을 반복합니다.
시스템 활성화 여부를 나타내는 멀티컬러 LED와, 모터를 활성화/비활성화하는 두 개의 푸시버튼이 있습니다.
시리얼 포트 인터페이스를 통해 숫자 코드/명령어를 보내 모터를 지정된 위치로 이동시키는 몇 가지 루틴이 프로그래밍되어 있습니다. 독립 이동 명령은 기계적 구조가 격렬하게 움직이지 않도록 즉각적인 위치 명령 대신 (어느 정도) 부드러운 움직임을 제공하도록 설계되었습니다.
코드 (Q2 버전 2.0)
// Scott Mangiacotti, Tucson, Arizona, May 2018
// Q2 Version 2.0
#include <Servo.h>
// 상수
const int GIVE_BACK_TIME = 125;
// 입력 핀
const int I_RED_BUTTON_PIN = 4;
const int I_GREEN_BUTTON_PIN = 2;
const int I_LDR1_PIN = A3; // 좌상
const int I_LDR2_PIN = A2; // 우상
const int I_LDR3_PIN = A1; // 좌하
const int I_LDR4_PIN = A0; // 우하
// 출력 핀
const int O_RED_LED_PIN = 9;
const int O_GREEN_LED_PIN = 11;
const int O_BLUE_LED_PIN = 10;
const int O_TWIST_SERVO_PIN = 5;
const int O_TILT_SERVO_PIN = 6;
// 전역 변수
bool gRunning = false;
bool gTrackToLightEnabled = false;
bool gVerboseDiagMode = false;
Servo gServoTwist;
int gServoTwistPositionCommand;
bool gServoTwistMoveIP;
Servo gServoTilt;
bool gServoTiltMoveIP;
int gServoTiltPositionCommand;
int gLDR1, gLDR2, gLDR3, gLDR4;
void setup() {
Serial.begin(9600);
pinMode(I_RED_BUTTON_PIN, INPUT);
pinMode(I_GREEN_BUTTON_PIN, INPUT);
pinMode(O_RED_LED_PIN, OUTPUT);
pinMode(O_GREEN_LED_PIN, OUTPUT);
pinMode(O_BLUE_LED_PIN, OUTPUT);
startup();
}
void loop() {
// 시리얼 명령 처리
if (Serial.available() > 0) {
int iControlCode = Serial.parseInt();
processSerialMessage(iControlCode);
}
// 그린 버튼: 추적 시작
int iGreenButton = digitalRead(I_GREEN_BUTTON_PIN);
if (iGreenButton == HIGH && gRunning == false) {
enableTracking();
gTrackToLightEnabled = true;
}
// 레드 버튼: 추적 정지
int iRedButton = digitalRead(I_RED_BUTTON_PIN);
if (iRedButton == HIGH && gRunning == true) {
disableTracking();
}
// 센서 읽기
readPhotoResistors();
averageTopTwoSensors();
averageBottomTwoSensors();
// 서보 위치 조정
if (gRunning == true) {
if (gTrackToLightEnabled == true) {
trackToLightSensors(); // 광센서 기반 자동 추적
} else {
smoothMoveTwist(); // 시리얼 명령 기반 부드러운 이동
smoothMoveTilt();
}
}
delay(GIVE_BACK_TIME);
}
// 추적 활성화
void enableTracking() {
gRunning = true;
gServoTwist.attach(O_TWIST_SERVO_PIN);
gServoTilt.attach(O_TILT_SERVO_PIN);
digitalWrite(O_GREEN_LED_PIN, HIGH);
digitalWrite(O_RED_LED_PIN, LOW);
Serial.println("servos enabled");
}
// 추적 비활성화
void disableTracking() {
gRunning = false;
gTrackToLightEnabled = false;
gServoTwist.detach();
gServoTilt.detach();
gServoTwistPositionCommand = gServoTwist.read();
gServoTwistMoveIP = false;
gServoTiltPositionCommand = gServoTilt.read();
gServoTiltMoveIP = false;
digitalWrite(O_RED_LED_PIN, HIGH);
digitalWrite(O_GREEN_LED_PIN, LOW);
Serial.println("servos disabled");
}
// 광센서 기반 태양 추적
void trackToLightSensors() {
float fTop, fBottom, fLeft, fRight;
int iTwistMoveCommand, iTiltMoveCommand;
int iMoveAmount = 5; // 스캔당 이동 각도 (GIVE_BACK_TIME과 조합으로 속도 결정)
iTwistMoveCommand = gServoTwist.read();
iTiltMoveCommand = gServoTilt.read();
fTop = averageTopTwoSensors();
fBottom = averageBottomTwoSensors();
fLeft = averageLeftTwoSensors();
fRight = averageRightTwoSensors();
// 트위스트(방위각) 이동 계산
if (fLeft > fRight) iTwistMoveCommand += iMoveAmount;
else if (fRight > fLeft) iTwistMoveCommand -= iMoveAmount;
// 틸트(고도각) 이동 계산
if (fTop > fBottom) iTiltMoveCommand += iMoveAmount;
else if (fBottom > fTop) iTiltMoveCommand -= iMoveAmount;
// 범위 제한
if (iTwistMoveCommand < 0) iTwistMoveCommand = 0;
if (iTwistMoveCommand > 179) iTwistMoveCommand = 179;
if (iTiltMoveCommand < 45) iTiltMoveCommand = 45;
if (iTiltMoveCommand > 135) iTiltMoveCommand = 135;
gServoTwist.write(iTwistMoveCommand);
gServoTilt.write(iTiltMoveCommand);
if (gVerboseDiagMode) {
Serial.print("tl,tr,bl,br,top,bottom,left,right,twist,tilt: ");
Serial.print(gLDR1); Serial.print(", "); Serial.print(gLDR2); Serial.print(", ");
Serial.print(gLDR3); Serial.print(", "); Serial.print(gLDR4); Serial.print(", ");
Serial.print(fTop); Serial.print(", "); Serial.print(fBottom); Serial.print(", ");
Serial.print(fLeft); Serial.print(", "); Serial.print(fRight); Serial.print(", ");
Serial.print(iTwistMoveCommand); Serial.print(", "); Serial.println(iTiltMoveCommand);
}
}
// 포토레지스터 값 읽기
void readPhotoResistors() {
gLDR1 = analogRead(I_LDR1_PIN);
gLDR2 = analogRead(I_LDR2_PIN);
gLDR3 = analogRead(I_LDR3_PIN);
gLDR4 = analogRead(I_LDR4_PIN);
}
// 부드러운 이동: 트위스트 서보
void smoothMoveTwist() {
int iCurrentPos = gServoTwist.read();
int iMoveAmountPerScan = 1; // 스캔당 1도씩 이동
if (iCurrentPos == gServoTwistPositionCommand) { gServoTwistMoveIP = false; return; }
gServoTwistMoveIP = true;
int iNewMoveCommand = iCurrentPos;
if (iCurrentPos < gServoTwistPositionCommand) iNewMoveCommand += iMoveAmountPerScan;
else iNewMoveCommand -= iMoveAmountPerScan;
if (iNewMoveCommand < 0) iNewMoveCommand = 0;
if (iNewMoveCommand > 179) iNewMoveCommand = 179;
gServoTwist.write(iNewMoveCommand);
if (gVerboseDiagMode) { Serial.print(iNewMoveCommand); Serial.print(", "); Serial.println(gServoTwistPositionCommand); }
}
// 부드러운 이동: 틸트 서보
void smoothMoveTilt() {
int iCurrentPos = gServoTilt.read();
int iMoveAmountPerScan = 1;
if (iCurrentPos == gServoTiltPositionCommand) { gServoTiltMoveIP = false; return; }
gServoTiltMoveIP = true;
int iNewMoveCommand = iCurrentPos;
if (iCurrentPos < gServoTiltPositionCommand) iNewMoveCommand += iMoveAmountPerScan;
else iNewMoveCommand -= iMoveAmountPerScan;
if (iNewMoveCommand < 0) iNewMoveCommand = 0;
if (iNewMoveCommand > 179) iNewMoveCommand = 179;
gServoTilt.write(iNewMoveCommand);
if (gVerboseDiagMode) { Serial.print(iNewMoveCommand); Serial.print(", "); Serial.println(gServoTiltPositionCommand); }
}
// 센서 평균값 계산 함수들
float averageTopTwoSensors() { return (gLDR1 + gLDR2) / 2.0; }
float averageBottomTwoSensors() { return (gLDR3 + gLDR4) / 2.0; }
float averageLeftTwoSensors() { return (gLDR1 + gLDR3) / 2.0; }
float averageRightTwoSensors() { return (gLDR2 + gLDR4) / 2.0; }
// 시리얼 명령 처리 (명령코드 2자리 + 값 3자리)
void processSerialMessage(int iControlCode) {
int iControlCommand = iControlCode / 1000;
int iControlValue = iControlCode % 1000;
Serial.print("control code: "); Serial.println(iControlCode);
Serial.print("command: "); Serial.println(iControlCommand);
Serial.print("value: "); Serial.println(iControlValue);
if (iControlCommand == 10) { // 기타 명령
if (iControlValue == 0) { gVerboseDiagMode = true; digitalWrite(O_BLUE_LED_PIN, HIGH); Serial.println("diag mode on"); }
else if (iControlValue == 1) { gVerboseDiagMode = false; digitalWrite(O_BLUE_LED_PIN, LOW); Serial.println("diag mode off"); }
else if (iControlValue == 2) { reportProductInfo(); }
else if (iControlValue == 3) { digitalWrite(O_RED_LED_PIN, HIGH); Serial.println("red led on"); }
else if (iControlValue == 4) { digitalWrite(O_RED_LED_PIN, LOW); Serial.println("red led off"); }
else if (iControlValue == 5) { digitalWrite(O_GREEN_LED_PIN, HIGH); Serial.println("green led on"); }
else if (iControlValue == 6) { digitalWrite(O_GREEN_LED_PIN, LOW); Serial.println("green led off"); }
else if (iControlValue == 7) { digitalWrite(O_BLUE_LED_PIN, HIGH); Serial.println("blue led on"); }
else if (iControlValue == 8) { digitalWrite(O_BLUE_LED_PIN, LOW); Serial.println("blue led off"); }
else if (iControlValue >= 9 && iControlValue <= 12) {
int ldrVals[4] = {gLDR1, gLDR2, gLDR3, gLDR4};
Serial.print("LDR"); Serial.print(iControlValue-8); Serial.print(" value: "); Serial.println(ldrVals[iControlValue-9]);
}
else if (iControlValue == 13) { enableTracking(); }
else if (iControlValue == 14) { disableTracking(); }
else if (iControlValue == 19) {
if (gRunning && !gServoTwistMoveIP && !gServoTiltMoveIP) {
gServoTwistPositionCommand = 90; gServoTiltPositionCommand = 90;
Serial.println("twist and tilt servos commanded to 90 degrees");
}
}
else if (iControlValue == 21) { if (gRunning) { gTrackToLightEnabled = true; Serial.println("Track to light enabled"); } }
else if (iControlValue == 22) { gTrackToLightEnabled = false; Serial.println("Track to light disabled"); }
else { Serial.print("invalid value: "); Serial.println(iControlValue); }
}
else if (iControlCommand == 11) { // 트위스트 서보 명령
if (iControlValue >= 0 && iControlValue <= 179) {
if (gRunning && !gServoTwistMoveIP) { gServoTwistPositionCommand = iControlValue; gServoTwistMoveIP = true;
Serial.print("Move twist servo: "); Serial.println(gServoTwistPositionCommand); }
} else { Serial.print("invalid value: "); Serial.println(iControlValue); }
}
else if (iControlCommand == 12) { // 틸트 서보 명령
if (iControlValue >= 0 && iControlValue <= 179) {
if (gRunning && !gServoTiltMoveIP) { gServoTiltPositionCommand = iControlValue; gServoTiltMoveIP = true;
Serial.print("Move tilt servo: "); Serial.println(gServoTiltPositionCommand); }
else { Serial.print("tilt not ready: "); Serial.print(gRunning); Serial.print(", "); Serial.println(gServoTiltMoveIP); }
} else { Serial.print("invalid value: "); Serial.println(iControlValue); }
}
Serial.println("-----");
}
// 시작 시퀀스: 자가 테스트, 서보 활성화, 광추적 모드 진입
void startup() {
int iDelay = 500;
reportProductInfo(); delay(iDelay);
processSerialMessage(10003); delay(iDelay); // 빨강 ON
processSerialMessage(10004); processSerialMessage(10005); delay(iDelay); // 빨강 OFF, 초록 ON
processSerialMessage(10006); processSerialMessage(10007); delay(iDelay); // 초록 OFF, 파랑 ON
processSerialMessage(10008); processSerialMessage(10009); processSerialMessage(10010); processSerialMessage(10011); processSerialMessage(10012); delay(iDelay); // LDR 값 출력
enableTracking(); delay(iDelay);
processSerialMessage(10019); delay(iDelay); // 홈 포지션(90,90)
disableTracking();
Serial.println("startup sequence completed");
}
void reportProductInfo() {
Serial.println("q version 2");
Serial.println("tucson, arizona usa");
Serial.println("may 2018");
Serial.print("checksum "); Serial.println("58BA-D969-2F82-08FD-2078-2777-396D-E1AA");
}
3D 모델 및 인클로저
조립 상세 도면 (ZIP)
회로도
프릿징 회로도 파일