아두이노 기반 MPPT 태양광 충전기로 최적의 PV 전력 수확
부품 및 소모품
부품 비고 수량
아두이노 나노 R3 1
인덕터 100 uH 전류 정격은 필요 전력에 맞춰야 함 1
파워 MOSFET N-채널 1
Adafruit 표준 LCD - 16x2 화이트 온 블루 1
필요한 도구 및 기계
앱 및 온라인 서비스
프로젝트 소개
시중에는 많은 충전 컨트롤러가 있지만, 저렴한 일반 충전 컨트롤러는 태양광 패널로부터 최대 전력을 얻는 데 효율적이지 않습니다. 효율적인 제품들은 매우 비쌉니다.
그래서 배터리 요구 사항과 태양광 조건을 이해할 만큼 충분히 효율적이고 스마트한 나만의 충전 컨트롤러를 만들기로 했습니다. 태양광으로부터 최대 가용 전력을 끌어내어 배터리에 매우 효율적으로 저장하는 적절한 조치를 취합니다.
제 노력이 마음에 드신다면, 이 튜토리얼에 투표해 주세요.
1단계: MPPT란 무엇이며 왜 필요한가?
태양광 패널은 배터리 상태를 이해할 만큼 똑똑하지 않습니다. 12V/100와트 태양광 패널이 있다고 가정하면, 제조사에 따라 18V-21V 출력을 내지만, 배터리는 12V 공칭 전압으로 정격됩니다. 완충 시 13.6V, 완전 방전 시 11.0V입니다. 배터리가 13V 충전 중이고 패널이 18V, 5.5A를 100% 효율로 출력한다고 가정해 봅시다 (100%는 불가능하지만 가정). 일반 컨트롤러는 전류 이득 없이 전압을 13.6V로 낮추는 PWM 전압 레귤레이터 회로를 사용합니다. 과충전 보호와 야간 패널로의 누설 전류 방지만 제공합니다.
이 문제를 해결하기 위해 SMPS 벅 컨버터를 사용했습니다. 이 컨버터는 90% 이상의 효율을 가집니다. 90%조차도 낮은 편으로 간주됩니다.
두 번째 문제는 태양광 패널의 비선형 출력입니다. 최대 가용 전력을 수확하려면 특정 전압에서 작동해야 하며, 출력은 하루 종일 변합니다.
이를 해결하기 위해 MPPT 알고리즘을 사용합니다. MPPT(Maximum Power Point Tracking, 최대 전력점 추적)는 이름 그대로 패널로부터 최대 가용 전력을 추적하고 출력 파라미터를 변화시켜 조건을 유지합니다.
따라서 MPPT를 사용하면 패널이 최대 가용 전력을 생성하고, 벅 컨버터가 이 전하를 효율적으로 배터리에 저장합니다.
2단계: MPPT는 어떻게 작동하는가?
자세히 다루지는 않으니, 이해하고 싶다면 What is MPPT? 링크를 참조하세요.
이 프로젝트에서는 입력 V-I 특성과 출력 V-I를 모두 추적했습니다. 입력 V-I와 출력 V-I를 곱하면 와트 단위의 전력을 얻을 수 있습니다.
예: 17V 5A, 즉 17×5 = 85와트라고 가정합니다. 동시에 출력이 13V 6A, 즉 13×6 = 78와트입니다.
MPPT는 이전 입력/출력 전력과 비교하여 출력 전압을 증가시키거나 감소시킵니다.
이전 입력 전력이 높았고 출력 전압이 현재보다 낮았다면, 출력을 다시 낮춰 높은 전력 지점으로 돌아갑니다. 출력 전압이 높았다면 현재 전압을 이전 수준으로 높입니다. 이렇게 최대 전력점 주변에서 진동하며 작동합니다. 이 진동은 효율적인 MPPT 알고리즘으로 최소화됩니다.
3단계: 아두이노에 MPPT 구현하기
이것이 충전기의 두뇌입니다. 아래는 출력을 제어하고 MPPT를 단일 코드 블록으로 구현한 아두이노 코드입니다.
// Iout = 출력 전류 // Vout = 출력 전압 // Vin = 입력 전압
// Pin = 입력 전력, Pin_previous = 이전 입력 전력
// Vout_last = 이전 출력 전압, Vout_sense = 현재 출력 전압
void regulate(float Iout, float Vin, float Vout) {
if ((Vout > Vout_max) || (Iout > Iout_max) ||
((Pin > Pin_previous && Vout_sense < Vout_last) || (Pin < Pin_previous && Vout_sense > Vout_last))) {
if (duty_cycle > 0) { duty_cycle -= 1; }
analogWrite(buck_pin, duty_cycle);
} else if ((Vout < Vout_max) && (Iout < Iout_max) &&
((Pin > Pin_previous && Vout_sense > Vout_last) || (Pin < Pin_previous && Vout_sense < Vout_last))) {
if (duty_cycle < 240) { duty_cycle += 1; }
analogWrite(buck_pin, duty_cycle);
}
Pin_previous = Pin;
Vin_last = Vin;
Vout_last = Vout;
}
4단계: 벅 컨버터
N-채널 MOSFET을 사용하여 벅 컨버터를 만들었습니다. 보통 하이사이드 스위칭에는 P-채널 MOSFET을 선택하고, N-채널을 같은 목적으로 사용하려면 드라이버 IC나 부트스트랩 회로가 필요합니다.
하지만 벅 컨버터 회로를 수정하여 N-채널 MOSFET으로 로우사이드 스위칭을 구현했습니다. N-채널을 사용하는 이유는 저렴하고, 높은 전력 정격, 낮은 전력 소모 때문입니다. 이 프로젝트에서는 IRFZ44N 로직 레벨 MOSFET을 사용하여 아두이노 PWM 핀으로 직접 구동할 수 있습니다.
더 높은 부하 전류의 경우, 게이트에 10V를 인가하여 MOSFET을 완전 포화 상태로 만들고 전력 소모를 최소화하기 위해 트랜지스터를 사용해야 합니다. 저도 그렇게 했습니다.
회로도에서 보듯 MOSFET을 -V 측에 배치하여 패널의 +12V를 접지로 사용했습니다. 이 구성으로 최소한의 부품으로 N-채널 MOSFET 벅 컨버터를 사용할 수 있습니다.
하지만 단점도 있습니다. 양측 -V 전압이 분리되어 공통 기준 접지가 없습니다. 따라서 전압 측정이 매우 까다롭습니다.
아두이노를 태양광 입력 단자에 연결하고, 그 -V 라인을 아두이노 접지로 사용했습니다. 이때 전압 분배 회로를 사용하여 입력 전압을 쉽게 측정할 수 있습니다. 하지만 공통 접지가 없으므로 출력 전압을 쉽게 측정할 수 없습니다.
이를 위해 트릭을 사용했습니다. 출력 커패시터 양단의 전압을 측정하는 대신, 두 -V 라인 사이의 전압을 측정했습니다. 태양광 -V를 아두이노 접지로, 출력 -V를 측정할 신호/전압으로 사용합니다. 이 측정값을 측정된 입력 전압에서 빼면 출력 커패시터 양단의 실제 출력 전압을 얻을 수 있습니다.
Vout_sense_temp = Vout_sense_temp * 0.92 + float(raw_vout) * volt_factor * 0.08; // 입력 접지와 출력 접지 사이의 전압 측정
Vout_sense = Vin_sense - Vout_sense_temp - diode_volt; // 두 접지 간 전압 차이를 출력 전압으로 변환
전류 측정에는 ACS-712 전류 감지 모듈을 사용했습니다. 아두이노로 전원을 공급하고 입력 접지에 연결했습니다.
내부 타이머를 수정하여 핀 D6에서 62.5kHz PWM을 얻어 MOSFET을 구동합니다. 출력 차단 다이오드는 역누설 및 역극성 보호를 위해 필요하며, 원하는 전류 정격의 쇼트키 다이오드를 사용하세요. 인덕터 값은 주파수와 출력 전류 요구사항에 따라 달라집니다. 온라인 벅 컨버터 계산기를 사용하거나 100uH 5A-10A 부하용을 사용하세요. 인덕터 최대 출력 전류의 80-90%를 초과하지 마세요.
5단계: 마무리 작업
충전기에 추가 기능을 더할 수 있습니다. 제 것에는 매개변수를 표시하는 LCD와 사용자 입력을 받는 2개의 스위치가 있습니다.
최종 코드와 전체 회로도를 곧 업데이트하겠습니다.
6단계: 실제 회로도, BOM & 코드
업데이트: 코드, BOM, 회로도를 업로드했습니다. 제작이 더 쉬운 버전이라 제 것과 약간 다릅니다.
Solar_charger_tutorial_code.ino
코드
Solar_charger_tutorial_code.ino (아두이노)
///////////////////////////////////////////////////////////////////////////////////////
// 사용 약관
///////////////////////////////////////////////////////////////////////////////////////
#include <LiquidCrystal.h>
#include <avr/wdt.h>
#include <EEPROM.h>
#define vin_pin A1
#define vout_pin A0
#define iout_pin A2
#define iin_pin A3
#define lm35 A4
#define fan 5
#define buck_pin 6
#define menu 3
#define button 2
#define led 13
#define charge_led A5
#define light 4
uint8_t auto_mode = 1;
float Pin = 0, Pout = 0, Pin_previous = 0;
float efficiency = 0.0;
int raw_vin = 0, raw_vout = 0, raw_iout = 0, raw_iin = 0, raw_lm35 = 0;
float Vout_boost = 14.5, Vout_max = 15.0, Iout_max = 5.0, Vout_float = 13.5, Iout_min = 0.00, Vin_thresold = 10.0;
float Iout_sense, Iin_sense, Iin;
float Vout_sense, Vin_last;
float heat_sink_temp;
float Vin_sense;
uint8_t duty_cycle = 0;
float volt_factor = 0.05376; // 전압 읽기 보정용, 이 값 변경
String mode = "";
bool startup = true, lcd_stat = true, charge = true, mppt_init = true;
unsigned int count = 0;
LiquidCrystal lcd(7, 8, 9, 10, 11, 12);
void lcd_show(String data, int column, int row);
void UI();
void set_limits(int cmd, int temp);
void mem_read();
void mem_write();
void mppt();
void setup() {
wdt_disable();
watchdogSetup();
Serial.begin(115200);
mem_read();
pinMode(light, OUTPUT);
pinMode(charge_led, OUTPUT);
digitalWrite(charge_led, LOW);
digitalWrite(light, HIGH);
pinMode(led, OUTPUT);
pinMode(fan, OUTPUT);
pinMode(menu, INPUT);
pinMode(button, INPUT);
digitalWrite(menu, HIGH);
digitalWrite(button, HIGH);
TCCR0B = TCCR0B & 0b11111000 | 0x01; // PWM 최대 62.5kHz
analogWrite(buck_pin, 0);
lcd.begin(16, 2);
lcd_show("Solar Charger", 0, 0);
delay(64000); wdt_reset();
delay(64000); wdt_reset();
lcd_show("Vi Vb Ib ", 0, 0);
// 초기화용 평균값 계산
for (int i = 0; i < 10; i++) {
raw_iout += analogRead(iout_pin) - 513;
raw_iin += analogRead(iin_pin) - 513;
raw_vin += analogRead(vin_pin);
raw_vout += analogRead(vout_pin);
raw_lm35 += analogRead(lm35);
delay(2);
}
raw_iout /= 10; raw_iin /= 10; raw_vout /= 10; raw_vin /= 10;
Iout_sense = float(raw_iout) * 5 / 1023 / 0.066;
Iin_sense = float(raw_iin) * 5 / 1023 / 0.066;
Vout_sense_temp = float(raw_vout) * volt_factor;
Vin_sense = float(raw_vin) * volt_factor;
}
void watchdogSetup(void) {
cli();
wdt_reset();
WDTCSR |= (1 << WDCE) | (1 << WDE);
WDTCSR = (1 << WDIE) | (1 << WDE) | (1 << WDP3) | (0 << WDP2) | (0 << WDP1) | (1 << WDP0);
sei();
}
ISR(WDT_vect) {
Serial.println("WDT reset.");
asm volatile (" jmp 0");
}
void regulate(float Iout, float Vin, float Vout) {
mode = "Buck mode";
if ((Vout > Vout_max) || (Iout > Iout_max) ||
((Pin > Pin_previous && Vin_sense < Vin_last) || (Pin < Pin_previous && Vin_sense > Vin_last))) {
if (duty_cycle > 0) { duty_cycle -= 1; }
analogWrite(buck_pin, duty_cycle);
} else if ((Vout < Vout_max) && (Iout < Iout_max) &&
((Pin > Pin_previous && Vin_sense > Vin_last) || (Pin < Pin_previous && Vin_sense < Vin_last))) {
if (duty_cycle < 250) { duty_cycle += 1; }
analogWrite(buck_pin, duty_cycle);
}
Pin_previous = Pin;
Vin_last = Vin;
}
void auto_cutoff(float Iout, float Vin, float Vout) {
if (Vout <= Vout_float && Iout > Iout_min + 1) { charge = true; }
if ((Vout > Vout_max) && (Iout < Iout_min) && (charge == true)) {
charge = false;
Serial.println("Charging Completed.");
digitalWrite(led, HIGH);
digitalWrite(charge_led, LOW);
} else if (Vin < Vin_thresold) {
duty_cycle = 0; analogWrite(buck_pin, duty_cycle);
Serial.println("LOW Input Voltage.");
lcd_show("Input Volt. Low ", 0, 1);
wdt_reset();
for (int i = 0; i < 10; i++) {
digitalWrite(led, HIGH); digitalWrite(charge_led, LOW); delay(6000);
digitalWrite(charge_led, HIGH); digitalWrite(led, LOW); delay(6000);
}
wdt_reset();
} else if (heat_sink_temp > 80.0) {
duty_cycle = 0; analogWrite(buck_pin, duty_cycle);
Serial.println("Over Heat Shutdown");
lcd_show("Over Heat Fail ", 0, 1);
wdt_reset();
for (int i = 0; i < 10; i++) {
digitalWrite(led, HIGH); digitalWrite(charge_led, LOW); delay(4000);
digitalWrite(charge_led, HIGH); digitalWrite(led, LOW); delay(4000);
}
wdt_reset();
} else {
charge = true;
digitalWrite(charge_led, HIGH);
regulate(Iout_sense, Vin_sense, Vout_sense);
digitalWrite(led, LOW);
}
}
void soft_start() {
for (int i = 0; i < 20; i++) {
regulate(Iout_sense, Vin_sense, Vout_sense);
Serial.print("Vin= "); Serial.println(Vin_sense);
Serial.print("Vout= "); Serial.println(Vout_sense);
Serial.print("Iout= "); Serial.println(Iout_sense);
Serial.print("Duty cycle= "); Serial.println(duty_cycle);
Serial.print("Charger MODE: "); Serial.println(mode);
Serial.println("Soft Start Activated");
delay(32000);
}
startup = false; mppt_init = false;
}
void lcd_show(String data, int column, int row) {
lcd.setCursor(column, row);
if (data.length() > 0) {
for (int i = 0; i < data.length(); i++) lcd.print(" ");
lcd.setCursor(column, row);
lcd.print(data);
}
}
void lcd_num(float num, int column, int row) {
lcd.setCursor(column, row);
for (int i = 0; i < 6; i++) lcd.print(" ");
lcd.setCursor(column, row);
lcd.print(num);
}
void UI() { /* 메뉴 설정 UI 코드 */ }
void mem_save() { /* EEPROM 저장 코드 */ }
void mem_read() { /* EEPROM 읽기 코드 */ }
void set_limits(int cmd, int temp) { /* 시리얼 명령으로 설정값 변경 */ }
void loop() {
wdt_reset();
if (charge) { Vout_max = Vout_boost; } else { Vout_max = Vout_float; }
raw_vin = 0; raw_vout = 0; raw_iout = 0; raw_lm35 = 0;
if (digitalRead(menu) == LOW) { UI(); }
if (Serial.available() > 0) {
String data = Serial.readString();
int temp = data.toInt();
int func = temp % 10;
temp = temp / 10;
set_limits(func, temp);
}
for (int i = 0; i < 10; i++) {
raw_iout += analogRead(iout_pin) - 513;
raw_iin += analogRead(iin_pin) - 513;
raw_vin += analogRead(vin_pin);
raw_vout += analogRead(vout_pin);
raw_lm35 += analogRead(lm35);
delay(1);
}
raw_iout /= 10; raw_iin /= 10; raw_vout /= 10; raw_vin /= 10; raw_lm35 /= 10;
Iout_sense = float(raw_iout) * 5 / 1023 / 0.066;
Iin_sense = float(raw_iin) * 5 / 1023 / 0.066;
Vin_sense = Vin_sense * 0.92 + float(raw_vin) * volt_factor * 0.08;
Vout_sense = Vout_sense * 0.92 + float(raw_vout) * volt_factor * 0.08;
heat_sink_temp = heat_sink_temp * 0.92 + float(raw_lm35) * 0.48 * 0.08;
if (Iout_sense < 0.0) Iout_sense *= -1;
if (Iin_sense < 0.0) Iin_sense *= -1;
Pin = Vin_sense * Iin_sense;
Pout = Vout_sense * Iout_sense;
efficiency = Pout * 100 / Pin;
if (efficiency < 0.0) efficiency = 0.0;
if (count > 100) {
// 시리얼 출력 및 LCD 표시 코드
count = 0;
}
if (startup == false) {
if (auto_mode == 1) { auto_cutoff(Iout_sense, Vin_sense, Vout_sense); }
else { digitalWrite(charge_led, HIGH); regulate(Iout_sense, Vin_sense, Vout_sense); digitalWrite(led, ~digitalRead(led)); }
}
if (heat_sink_temp > 45.0) digitalWrite(fan, HIGH);
else if (heat_sink_temp < 37.0) digitalWrite(fan, LOW);
count++;
}
회로도