Celem zaprezentowanego projektu było skonstruowanie regulowanego obciążenia prądu stałego, które będzie kontrolowane przez moduł Arduino Nano z podłączonym wyświetlaczem LCD i enkoderem obrotowym. Finalne rozwiązanie oferuje tryby pracy przy stałym prądzie i stałej mocy, może współpracować ze źródłami o napięciu do 30 V i z prądem do 20 A, jeśli zastosowany radiator będzie w stanie rozproszyć taką ilość mocy.
W artykule zaprezentowano, krok po kroku, jak zbudować urządzenie tego rodzaju. Na stronie źródłowej jest dostępny film obrazujący działanie i budowę układu, który warto obejrzeć, jeśli opis i zamieszczone tu schematy nie będą wystarczająco jasno opisywały wszystkich etapów konstrukcji urządzenia.
Potrzebne elementy
Do budowy układu potrzebne będą następujące elementy:
- moduł Arduino Nano,
- wyświetlacz LCD 16×2 z interfejsem I²C,
- enkoder obrotowy,
- sterownik MOSFET TC4420,
- tranzystor MOSFET IRFZ44N,
- sensor prądu ACS712,
- uchwyt bezpiecznika,
- bezpiecznik 20 A,
- złącza do podłączania obciążenia, np. gniazda bananowe.
Schemat układu
Konstrukcja elektroniczna układu jest bardzo prosta. Schemat ideowy urządzenia pokazano na rysunku 1. Centralnym elementem układu jest, oczywiście, moduł Arduino, który steruje wszystkimi podzespołami. Do Arduino podłączone są elementy interfejsu użytkownika - wyświetlacz LCD (U4) oraz enkoder obrotowy (U5).
Wyświetlacz podłączony jest za pomocą interfejsu I²C, co pozwala zaoszczędzić liczbę potrzebnych pinów mikrokontrolera. Wymagane są tylko dwie linie sygnałowe - SDA (danych) oraz SCL (zegara). Jeśli nie będziemy w stanie pozyskać takiego wyświetlacza, to moduł Arduino, zastosowany w tym projekcie ma dostateczną liczbę linii GPIO, aby wysterować interfejs LCD z klasycznym interfejsem równoległym i sterownikiem HD44780 z interfejsem 4- lub 8-bitowym. Do kontrolowania urządzenia służy pojedynczy enkoder obrotowy, który jest podłączony do mikrokontrolera. W enkoderze znajduje się również przycisk, który został podłączony osobną linią do Arduino.
W torze sygnałowym obciążenia znajdują się trzy elementy, umieszczone pomiędzy linią VCC, a masą układu. Rozpoczynając od dodatniego bieguna zasilania, prąd przechodzi przez układ ACS712 (U3 na schemacie na rysunku 1), a następnie przez tranzystor Q1 i bezpiecznik F1. Sensor ACS712 firmy Allegro to w pełni zintegrowany sensor do pomiaru prądu, który bazuje na czujniku Halla. Układ ten mierzy płynący prąd poprzez pomiar pola magnetycznego wokół przewodnika (znajdującego się we wnętrzu obudowy scalonego sensora - rysunek 2), co gwarantuje niską rezystancję elementu pomiarowego (niski spadek napięcia, a co za tym idzie niewielkie straty mocy) oraz izolację galwaniczną pomiędzy układem pomiarowym, a torem prądu. Układ ten ma wyjście analogowe, które podłączone jest do wejścia przetwornika analogowo-cyfrowego modułu Arduino.
Znajdujący się dalej tranzystor Q3 pełni kluczową rolę w pracy urządzenia. Jest on sterowany przez driver bramki TC4420 (U2), który jest kontrolowany przez mikrokontroler. Arduino steruje układem za pomocą przebiegu PWM tak, aby prąd (mierzony za pomocą układu ACS712) lub moc (obliczana z płynącego prądu i napięcia VCC mierzonego za pomocą dzielnika R2/R3 i wejścia analogowego A1) była zgodna z zadaną.
W torze obciążenia ostatnim elementem jest bezpiecznik F1 (na schemacie na rysunku 1 pokazano bezpiecznik 10 A), który pełni rolę zabezpieczenia układu. Wartość tego bezpiecznika należy dobrać w zależności od innych elementów układu i wydajności chłodzenia tranzystora Q1.
Autor zmontował układ na płytce uniwersalnej, korzystając z gotowych modułów, które wystarczyło tylko połączyć. Gotową, polutowaną płytkę z modułami pokazano na fotografii 1.
Oprogramowanie
Wybrane, najistotniejsze fragmenty oprogramowania modułu pokazano na listingu 1 (pominięto funkcje screen0...screen6, odpowiedzialne za wyświetlanie poszczególnych ekranów menu, inicjalizację zmiennych oraz blok #define dla poszczególnych wyprowadzeń itp).
#include <Wire.h>
#include <LiquidCrystal_I²C.h>
ISR(PCINT2_vect) {
if (digitalRead(sw) == LOW) {
button = true;
}
}
void isr0 () {
TurnDetected = true;
up = (digitalRead(clk) == digitalRead(dt));
}
void setup() {
// Inicjalizacja portu szeregowego
Serial.begin(115200);
// Inicjalizacja LCD
lcd.begin();
// Konfiguracja pinów
pinMode(sw, INPUT_PULLUP);
pinMode(clk, INPUT);
pinMode(dt, INPUT);
pinMode(pwm, OUTPUT);
pinMode(currentsense, INPUT);
digitalWrite(pwm, LOW);
// Konfiguracja ADC
ADCSRA &= ~PS_128;
ADCSRA |= (1 << ADPS1) | (1 << ADPS0);
PCICR |= 0b00000100;
PCMSK2 |= 0b00010000;
// Konfiguracja przerwania
attachInterrupt(0, isr0, RISING);
// Konfiguracja timerów i PWM
TCCR1A = 0;
TCCR1A = (1 << COM1A1) | (1 << WGM11);
TCCR1B = 0;
TCCR1B = (1 << WGM13) | (1 << WGM12) | (1 << CS10);
ICR1 = 2047;
OCR1A = 0;
// Inicjalizacja znaków użytkownika
lcd.createChar(0, customChar1);
lcd.createChar(1, customChar2);
lcd.clear();
lcd.print(" ADJ CONST LOAD");
delay(2000);
screen0();
lcd.setCursor(0, 0);
lcd.write((uint8_t)0);
}
void loop() {
if (currentmode) {
curcurrentraw = analogRead(currentsense);
// Obliczenie aktualnego prądu
curcurrent = ((curcurrentraw - zerocurrent)
* (5000.00 / 1023.00) / 100.00);
// Aktualizacja wyświetlacza
if (counter == 5000) {
lcd.setCursor(4, 1);
lcd.print(curcurrent);
lcd.print("A ");
counter = 0;
}
if (curcurrent < current) {
OCR1A++;
} else {
OCR1A = OCR1A - 1;
}
counter++;
delayMicroseconds(100);
}
if (powermode) {
curcurrentraw = analogRead(currentsense);
curcurrent = ((curcurrentraw - zerocurrent)
* (5000.00 / 1023.00) / 100.00);
curvoltraw = analogRead(voltagesense);
curvoltage = curvoltraw
* (5000.00 / 1023.00)
* 7.20 /1000.00;
curpower = curvoltage * curcurrent;
if (counter == 5000) {
lcd.setCursor(4, 1);
lcd.print(curpower);
lcd.print("W ");
counter = 0;
}
if (curpower < power) {
OCR1A++;
} else {
OCR1A = OCR1A - 1;
}
counter++;
delayMicroseconds(100);
}
if (TurnDetected) {
delay(200);
switch (screen) {
case 0:
switch (arrowpos) {
case 0:
if (!up) {
screen0();
lcd.setCursor(0, 1);
lcd.write((uint8_t)0);
arrowpos = 1;
} break;
case 1:
if (up) {
screen0();
lcd.setCursor(0, 0);
lcd.write((uint8_t)0);
arrowpos = 0;
} break;
}
break;
case 1:
switch (arrowpos) {
case 0:
if (!up) {
screen1();
lcd.setCursor(0, 1);
lcd.write((uint8_t)0);
arrowpos = 1;
} break;
case 1:
if (up) {
screen1();
lcd.setCursor(0, 0);
lcd.write((uint8_t)0);
arrowpos = 0;
} else {
screen1();
lcd.setCursor(7, 1);
lcd.write((uint8_t)0);
arrowpos = 2;
} break;
case 2:
if (up) {
screen1();
lcd.setCursor(0, 1);
lcd.write((uint8_t)0);
arrowpos = 1;
} break;
} break;
case 2:
if (up) {
power = power + 0.1;
lcd.setCursor(7, 0);
lcd.print(power);
lcd.print("W");
lcd.write((uint8_t)1);
lcd.print(" ");
} else {
power = power - 0.1;
if (power < 0) {
power = 0;
}
lcd.setCursor(7, 0);
lcd.print(power);
lcd.print("W");
lcd.write((uint8_t)1);
lcd.print(" ");
} break;
case 4:
switch (arrowpos) {
case 0:
if (!up) {
screen4();
lcd.setCursor(0, 1);
lcd.write((uint8_t)0);
arrowpos = 1;
} break;
case 1:
if (up) {
screen4();
lcd.setCursor(0, 0);
lcd.write((uint8_t)0);
arrowpos = 0;
} else {
screen4();
lcd.setCursor(7, 1);
lcd.write((uint8_t)0);
arrowpos = 2;
} break;
case 2:
if (up) {
screen4();
lcd.setCursor(0, 1);
lcd.write((uint8_t)0);
arrowpos = 1;
} break;
} break;
case 5:
if (up) {
current = current + 0.1;
lcd.setCursor(9, 0);
lcd.print(current);
lcd.print("A");
lcd.write((uint8_t)1);
lcd.print(" ");
} else {
current = current - 0.1;
if (current < 0) {
current = 0;
}
lcd.setCursor(9, 0);
lcd.print(current);
lcd.print("A");
lcd.write((uint8_t)1);
lcd.print(" ");
} break;
}
TurnDetected = false;
}
if (button) {
delay(200);
switch (screen) {
case 0:
if (arrowpos == 0) {
screen = 1;
screen1();
lcd.setCursor(0, 0);
lcd.write((uint8_t)0);
} else {
screen = 4;
screen4();
lcd.setCursor(0, 0);
lcd.write((uint8_t)0);
} break;
case 1:
switch (arrowpos) {
case 0:
screen = 2;
screen2();
break;
case 1:
powermode = true;
screen = 3;
screen3();
break;
case 2:
screen = 0;
screen0();
lcd.setCursor(0, 0);
lcd.write((uint8_t)0);
break;
} break;
case 2:
screen = 1;
screen1();
lcd.setCursor(0, 0);
lcd.write((uint8_t)0);
break;
case 3:
powermode = false;
OCR1A = 0;
counter = 0;
screen = 1;
screen1();
lcd.setCursor(0, 0);
lcd.write((uint8_t)0);
break;
case 4:
switch (arrowpos) {
case 0:
screen = 5;
screen5();
break;
case 1:
screen = 6;
screen6();
currentmode = true;
counter = 0;
break;
case 2:
screen = 0;
screen0();
lcd.setCursor(0, 0);
lcd.write((uint8_t)0);
break;
} break;
case 5:
screen = 4;
screen4();
lcd.setCursor(0, 0);
lcd.write((uint8_t)0);
break;
case 6:
screen = 4;
screen4();
lcd.setCursor(0, 0);
lcd.write((uint8_t)0);
currentmode = false;
OCR1A = 0;
break;
}
arrowpos = 0;
button = false;
}
}
Oprogramowanie podzielone jest na dwie główne funkcje - setup() oraz loop(). Pierwsza z nich konfiguruje i inicjalizuje wszystkie potrzebne peryferia mikrokontrolera. Druga to nieskończona pętla, w której realizowana jest zasadnicza funkcjonalność programu. Układ może znajdować się w jednym z dwóch trybów pracy - w trybie stabilizacji prądu (currentmode) lub stabilizacji mocy (powermode). W zależności od tego, jaki tryb jest aktywny, w pętli loop() aktywowana jest odpowiednia sekcja. Ich struktura jest podobna - poleceniem analogRead(currentsense) dokonywany jest pomiar prądu, poprzez pomiar napięcia z sensora prądu, a następnie obliczana jest poprawka dla sygnału PWM sterującego tranzystorem obciążenia. W przypadku stabilizacji mocy, poleceniem analogRead(voltagesense) mierzone jest dodatkowo napięcie VCC, które jest potrzebne do obliczenia mocy.
Enkoder obsługiwany jest za pomocą systemu przerwań mikrokontrolera, jednak żeby obsługa przerwania nie trwała zbyt długo, w samym callbacku ISR ustawiana jest tylko odpowiednia flaga, a obsługa menu realizowana jest w pętli głównej loop() programu. W zależności od tego, czy enkoder obrócił się, czy naciśnięty został przycisk, aktywowana jest odpowiednia sekcja firmware - odpowiednio TurnDetected lub button. W ramach tych sekcji wyświetlane są odpowiednie dane na ekranie i konfigurowane poszczególne ustawienia. Odpowiadają za to funkcje screen_(), które zostały pominięte na listingu 1.
Podsumowanie
Po zaprogramowaniu modułu Arduino za pomocą opisanego firmware pozostaje tylko umieścić wszystkie pokazane na fotografii 2 elementy wewnątrz obudowy. Po podłączeniu portów wyjściowych obciążenia układ jest gotowy do działania. Podczas finalnego montażu należy zadbać o odpowiednie chłodzenie tranzystora obciążającego - od niego zależą maksymalne parametry układu.
Oczywiście układ można rozbudować i udoskonalać. Menu, o ile w pełni funkcjonalne, jest dosyć skomplikowane w korzystaniu i oprogramowaniu. Można zmienić je na klasyczną klawiaturę bądź w inny sposób sterować mikrokontrolerem. Z uwagi na dużą liczbę pozostałych wyprowadzeń Arduino, system można rozbudować o np. termistor lub inny sensor temperatury do monitorowana temperatury tranzystora MOSFET, co pozwoli chronić go przed przegrzaniem podczas pracy. Można też dodać sterowanie wentylatorem chłodzącym, jeśli taki zastosowano, aby dobierać jego obroty do obciążenia układu lub temperatury tranzystora.
Nikodem Czechowski, EP
Bibliografia: