This shows you the differences between two versions of the page.
pm:prj2024:amocanu:stefan.maruntis [2024/05/26 16:53] stefan.maruntis [Descriere generală] |
pm:prj2024:amocanu:stefan.maruntis [2024/05/27 10:22] (current) stefan.maruntis [Rezultate Obţinute] |
||
---|---|---|---|
Line 1: | Line 1: | ||
====== Articulație panou solar "Sunflower" ====== | ====== Articulație panou solar "Sunflower" ====== | ||
+ | |||
+ | <note> | ||
+ | * Nume: Mărunțiș Andrei | ||
+ | * Grupă: 333CA | ||
+ | </note> | ||
===== Introducere ===== | ===== Introducere ===== | ||
Line 37: | Line 42: | ||
* 1x Display LCD | * 1x Display LCD | ||
* 1x Motor pas cu pas 28BYJ-48 | * 1x Motor pas cu pas 28BYJ-48 | ||
- | |||
==== Schema electrică ==== | ==== Schema electrică ==== | ||
Line 64: | Line 68: | ||
* [[https://www.arduino.cc/reference/en/libraries/liquidcrystal-i2c/|Liquid Crystal I2C]] - pentru intefațarea cu display-ul LCD | * [[https://www.arduino.cc/reference/en/libraries/liquidcrystal-i2c/|Liquid Crystal I2C]] - pentru intefațarea cu display-ul LCD | ||
* [[https://www.arduino.cc/reference/en/libraries/altsoftserial/|AltSoftSerial]] - pentru comunicația cu modulul GPS | * [[https://www.arduino.cc/reference/en/libraries/altsoftserial/|AltSoftSerial]] - pentru comunicația cu modulul GPS | ||
- | * [[https://www.arduino.cc/reference/en/libraries/tinygpsplus/|TinyGpsPlus]] - pentru parsarea datelor de la modulul GPS, in format NMEA | + | * [[https://www.arduino.cc/reference/en/libraries/tinygpsplus/|TinyGpsPlus]] - pentru parsarea datelor de la modulul GPS, în format NMEA |
* [[https://www.arduino.cc/reference/en/libraries/unistep2/|Unistep2]] - pentru controlul **asincron** al motorului pas cu pas | * [[https://www.arduino.cc/reference/en/libraries/unistep2/|Unistep2]] - pentru controlul **asincron** al motorului pas cu pas | ||
* [[https://www.arduino.cc/reference/en/libraries/time/|Time]] - pentru un ceas facil pe Arduino | * [[https://www.arduino.cc/reference/en/libraries/time/|Time]] - pentru un ceas facil pe Arduino | ||
+ | * [[https://www.arduino.cc/reference/en/libraries/timerinterrupt/|TimerInterrupt]] - pentru execuția periodică a anumitor instrucțiuni | ||
==== Modul de funcționare ==== | ==== Modul de funcționare ==== | ||
Line 79: | Line 84: | ||
În timp ce microcontroller-ul efectuează aceste operații, pe LCD vor fi afișate date despre tensiunea generată de panoul solar și ora locală. | În timp ce microcontroller-ul efectuează aceste operații, pe LCD vor fi afișate date despre tensiunea generată de panoul solar și ora locală. | ||
+ | ==== Module de cod ==== | ||
- | <note> | + | La începutul fișierului am directivele de preprocesor pentru a include librăriile externe folosite în cadrul proiectului și declararea structurilor de date corespunzătoare diverselor periferice ca variabile globale. Spre exemplu, următoarea bucată de cod îmi inițializează o structura de comunicație cu display-ul având în vedere adresa slave-ului (0x27) și dimensiunea ecranului (2 linii a câte 16 coloane): |
- | Descrierea codului aplicaţiei (firmware): | + | |
- | * mediu de dezvoltare (if any) (e.g. AVR Studio, CodeVisionAVR) | + | |
- | * librării şi surse 3rd-party (e.g. Procyon AVRlib) | + | |
- | * algoritmi şi structuri pe care plănuiţi să le implementaţi | + | |
- | * (etapa 3) surse şi funcţii implementate | + | |
- | </note> | + | |
+ | <code c++> | ||
+ | #include <LiquidCrystal_I2C.h> | ||
+ | LiquidCrystal_I2C lcd(0x27, 16, 2); | ||
+ | </code> | ||
+ | Orice proiect Arduino are la bază 2 funcții: ''setup'', funcție care se rulează o singură dată la pornirea în execuție a codului, și ''loop'', funcție care se execută încontinuu cât timp microcontroller-ul este pornit și funcționează. | ||
+ | |||
+ | În cadrul funcției ''setup'' încep prin a inițializa conexiunile cu perifericele. Spre exemplu, pentru LCD pornesc comunicația prin I2C cu următoarele funcții implementate în biblioteca LiquidCrystal I2C: | ||
+ | |||
+ | <code c++> | ||
+ | // Init LCD | ||
+ | lcd.init(); | ||
+ | lcd.backlight(); | ||
+ | lcd.setCursor(0, 0); | ||
+ | </code> | ||
+ | |||
+ | În mod asemănător inițializez pe rând fiecare periferic, unele dintre ele afișând eventuale erori pe LCD, cum ar fi accelerometrul: | ||
+ | |||
+ | <code c++> | ||
+ | // Start the I2C bus for accelerometer, 0x68 address | ||
+ | Wire.begin(); | ||
+ | Wire.setClock(400000); | ||
+ | imu.Config(&Wire, bfs::Mpu6500::I2C_ADDR_PRIM); | ||
+ | /* Initialize and configure IMU */ | ||
+ | if (!imu.Begin()) | ||
+ | { | ||
+ | lcd.print("Error initializing communication with IMU"); | ||
+ | while (1) | ||
+ | { | ||
+ | } | ||
+ | } | ||
+ | /* Set the sample rate divider */ | ||
+ | if (!imu.ConfigSrd(19)) | ||
+ | { | ||
+ | lcd.print("Error configured SRD"); | ||
+ | while (1) | ||
+ | { | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | În mod special merită menționată comunicația cu Serial Monitorul din VS Code. La începutul fișierului este definită o constantă care spune compilatorului dacă să includă zonele de cod legate de Serial Monitor în sursa finală, iar orice bucată de cod referitoare la Serial Monitor este înconjurată de directive de preprocesor, astfel: | ||
+ | |||
+ | <code c++> | ||
+ | #define DEBUG_SERIAL false | ||
+ | |||
+ | #if DEBUG_SERIAL | ||
+ | Serial.begin(9600); | ||
+ | #endif | ||
+ | </code> | ||
+ | |||
+ | După ce se termină inițializarea tuturor perifericelor, se trece în etapa în care se caută semnal GPS. Aceasta este ceva mai complexă și are, de altfel, dedicată propria funcție. | ||
+ | |||
+ | Înainte de toate, se inițializează comunicația prin UART cu modulul GPS: | ||
+ | |||
+ | <code c++> | ||
+ | #include <SoftwareSerial.h> | ||
+ | |||
+ | // The serial connection to the GPS device | ||
+ | SoftwareSerial ss(RXPin, TXPin); | ||
+ | |||
+ | void initGPS() | ||
+ | { | ||
+ | ss.begin(GPSBaud); | ||
+ | | ||
+ | // ... | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | Urmează o parte de cod care afișează pe LCD un mesaj cât timp se caută semnal GPS: | ||
+ | |||
+ | <code c++> | ||
+ | volatile int count = 0; | ||
+ | |||
+ | void initGPS() | ||
+ | { | ||
+ | // ... | ||
+ | |||
+ | lcd.setCursor(0, 0); | ||
+ | lcd.print("GPS signal"); | ||
+ | | ||
+ | while (1) | ||
+ | { | ||
+ | lcd.setCursor(10, 0); | ||
+ | for (int i = 0; i <= count % 3; i++) | ||
+ | { | ||
+ | lcd.print("."); | ||
+ | } | ||
+ | for (int i = count % 3; i < 3; i++) | ||
+ | { | ||
+ | lcd.print(" "); | ||
+ | } | ||
+ | // ... | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | Astfel, se afișează mesajul "Searching GPS...", iar numărul de puncte se schimbă la fiecare secundă. Pentru a contoriza această secundă, inițializez un Timer care generează o întrerupere la fiecare 1s în care incrementez variabila ''count'' de mai sus: | ||
+ | |||
+ | <code c++> | ||
+ | #define TIMER_INTERRUPT_DEBUG 2 | ||
+ | #define _TIMERINTERRUPT_LOGLEVEL_ 0 | ||
+ | #define USE_TIMER_1 true | ||
+ | #include <TimerInterrupt.h> | ||
+ | |||
+ | void GPSTimerHandler() | ||
+ | { | ||
+ | count++; | ||
+ | } | ||
+ | |||
+ | void initGPS() | ||
+ | { | ||
+ | ITimer1.init(); | ||
+ | count = 0; | ||
+ | | ||
+ | if (ITimer1.attachInterruptInterval(TIMER_INTERVAL_MS, GPSTimerHandler, TIMER_DURATION_MS)) | ||
+ | { | ||
+ | #if DEBUG_SERIAL | ||
+ | Serial.print(F("Starting ITimer1 OK, millis() = ")); | ||
+ | Serial.println(millis()); | ||
+ | #endif | ||
+ | } | ||
+ | else | ||
+ | { | ||
+ | #if DEBUG_SERIAL | ||
+ | Serial.println(F("Can't set ITimer1. Select another freq. or timer")); | ||
+ | #endif | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | În interiorul buclei se citesc efectiv datele primite de la GPS, caracter cu caracter, în format NMEA care este parsat cu ajutorul librăriei TinyGps++: | ||
+ | |||
+ | <code c++> | ||
+ | void initGPS() | ||
+ | { | ||
+ | while (1) | ||
+ | { | ||
+ | // ... | ||
+ | | ||
+ | if (ss.available() > 0) | ||
+ | { | ||
+ | char c = ss.read(); | ||
+ | #if DEBUG_SERIAL | ||
+ | Serial.print(c); | ||
+ | #endif | ||
+ | gps.encode(c); | ||
+ | if (gps.location.isUpdated()) | ||
+ | { | ||
+ | lcd.clear(); | ||
+ | lcd.setCursor(0, 0); | ||
+ | lcd.print("Lat="); | ||
+ | lcd.print(gps.location.lat(), 6); | ||
+ | lcd.setCursor(0, 1); | ||
+ | lcd.print("Long="); | ||
+ | lcd.print(gps.location.lng(), 6); | ||
+ | foundLoc = true; | ||
+ | break; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | În cazul în care se găsește semnal, se afișează pe display locația primită de la GPS, în caz contrat un mesaj de eroare și locația default anume hol EC. | ||
+ | |||
+ | După ce se termină procesul de căutare a locației se inițializează ceasul intern al microcontrollerului cu valoarea primită de la GPS, astfel: | ||
+ | |||
+ | <code c++> | ||
+ | void calibrateTime() | ||
+ | { | ||
+ | if (gps.time.isValid() && gps.date.isValid()) | ||
+ | { | ||
+ | int year = gps.date.year(); | ||
+ | int month = gps.date.month(); | ||
+ | int day = gps.date.day(); | ||
+ | int hour = gps.time.hour(); | ||
+ | int minute = gps.time.minute(); | ||
+ | int second = gps.time.second(); | ||
+ | // Set the Time to the time retrieved from the GPS | ||
+ | // Assume current location is UTC+3 | ||
+ | setTime(hour + 3, minute, second, day, month, year); | ||
+ | } | ||
+ | else | ||
+ | { | ||
+ | setTime(19, 0, 0, 27, 5, 2024); | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | Pentru a avea ora locală se presupune că timezone-ul în care se află locația găsită este UTC+3 (ora de vară a României). Folosind datele de locație se poate determina timezone-ul, însă asta presupune o catalogare a acestora, întrucât timezone-urile sunt separate doar aproximativ după longitudine. | ||
+ | |||
+ | După terminarea acestui proces se trimite semnalul de standby către GPS, pentru a reduce consumul de energie: | ||
+ | |||
+ | <code c++> | ||
+ | #define PMTK_STANDBY "$PMTK161,0*28" | ||
+ | |||
+ | ss.println(PMTK_STANDBY); | ||
+ | </code> | ||
+ | |||
+ | În următoarea etapă de funcționare a proiectului, microcontroller-ul citește periodic date de la accelerometru pentru a ajusta poziția panoului solar. Inițializez un nou timer pentru asta asemănător cu ce am prezentat mai sus, iar întreruperea va seta o variabilă numită ''flag'' pe ''true'' pentru a semnaliza funcției ''loop'' că trebuie să citească date noi de la accelerometru. | ||
+ | |||
+ | Codul care se ocupă de citirea acestor date de la accelerometru și calculul direcției după care se va orienta panoul solar este următorul: | ||
+ | |||
+ | <code c++> | ||
+ | // Defines the number of steps per rotation for stepper motor | ||
+ | const int stepsPerRevolution = 4096; | ||
+ | // Define some steppers and the pins they will use | ||
+ | // pins for IN1, IN2, IN3, IN4, steps per rev, step delay(in micros) | ||
+ | Unistep2 stepper(8, 9, 10, 11, stepsPerRevolution, 1000); | ||
+ | |||
+ | double ang_speed = 0; | ||
+ | double target_angle = 0; | ||
+ | int target_step = 0; | ||
+ | int current_step = 0; | ||
+ | |||
+ | void loop() | ||
+ | { | ||
+ | // We need to call run() frequently, so we place it in the loop() | ||
+ | stepper.run(); | ||
+ | |||
+ | // Read accelerometer data | ||
+ | if (flag) | ||
+ | { | ||
+ | if (imu.Read()) | ||
+ | { | ||
+ | ang_speed = imu.gyro_z_radps(); | ||
+ | ang_speed -= ACCEL_NOISE; | ||
+ | if (ang_speed > ACCEL_TOL || ang_speed < -ACCEL_TOL) | ||
+ | { | ||
+ | Serial.println("Ajung aici"); | ||
+ | target_angle = target_angle - (ang_speed * ACCEL_READ_DELAY_MS / 1000) * STEPPER_MULT; | ||
+ | while (target_angle > 2 * PI) | ||
+ | { | ||
+ | target_angle -= 2 * PI; | ||
+ | } | ||
+ | while (target_angle < 0) | ||
+ | { | ||
+ | target_angle += 2 * PI; | ||
+ | } | ||
+ | } | ||
+ | target_step = (int)(stepsPerRevolution * (target_angle + (hour() > 12 ? PI : 0)) / (2 * PI)); | ||
+ | if (abs(target_step - current_step) > STEPPER_TOL) | ||
+ | { | ||
+ | stepper.moveTo(target_step); | ||
+ | } | ||
+ | | ||
+ | // .... | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | Înainte de toate, motorul pas cu pas este pseudo-asincron: microprocesorul din Arduino Uno (ATMega328P) nu este capabil de multithreading, astfel că simulez acest efect apelând la fiecare execuție a loop-ului metoda ''stepper.run()'' care va roti panoul solar către o poziție setată în prealabil cu un apel al metodei ''stepper.moveTo''. | ||
+ | |||
+ | Formulele folosite sunt foarte simple: | ||
+ | |||
+ | * Având în vedere că eșantionez valoarea vitezei unghiulare cu perioadă fixă (0.1s), pot să înmulțesc această valoare cu timpul pentru a obține "aria de sub grafic" anume diferența de unghi față de ultima măsurătoare. Valorile efective le calculez cu o anumită toleranță | ||
+ | * Mă asigur că unghiul obținut (care va fi exprimat în radiani) este în intervalul [0, 2π) | ||
+ | * Calculez poziția la care trebuie să deplasez panoul solar folosind regula de 3 simplă (un cerc este 2π și corespunde 4096 de pași pentru motorul pas cu pas, deci pot să calculez câți pași corespund unghiului calculat) | ||
+ | * Dacă diferența dintre poziția corectă și poziția actuală a motorului este peste un anumit prag, trimit comanda **asincronă** către stepper să se rotească. | ||
+ | |||
+ | În ceea ce privește orientarea automată a panoului către soare, am implementat un model extrem de simplu în care soarele răsare la Est și apune la Vest (emisfera nordică). Se poate implementa un model mai complex cu formulele găsite [[https://www.pveducation.org/pvcdrom/properties-of-sunlight/the-suns-position|aici]]. | ||
+ | |||
+ | Cât timp microcontroller-ul se ocupă de ajustarea unghiului panoului solar, pe LCD se afisează date despre tensiunea generată de panou, respectiv ora, folosind următoarele funcții: | ||
+ | |||
+ | <code c++> | ||
+ | void showVoltage(int row) | ||
+ | { | ||
+ | // Read battery voltage from ADC | ||
+ | int voltage = analogRead(A0); | ||
+ | float voltage_f = voltage * (5.0 / 1023.0); | ||
+ | // multiply by 10 due to divider | ||
+ | voltage_f *= 10; | ||
+ | lcd.setCursor(0, row); | ||
+ | lcd.print("Voltage: "); | ||
+ | lcd.print(voltage_f); | ||
+ | lcd.print("V"); | ||
+ | lcd.print(" "); | ||
+ | } | ||
+ | |||
+ | void showTime(int row) | ||
+ | { | ||
+ | lcd.setCursor(0, row); | ||
+ | lcd.print("Time: "); | ||
+ | if (hour() < 10) | ||
+ | { | ||
+ | lcd.print("0"); | ||
+ | } | ||
+ | lcd.print(hour()); | ||
+ | lcd.print(":"); | ||
+ | if (minute() < 10) | ||
+ | { | ||
+ | lcd.print("0"); | ||
+ | } | ||
+ | lcd.print(minute()); | ||
+ | lcd.print(":"); | ||
+ | if (second() < 10) | ||
+ | { | ||
+ | lcd.print("0"); | ||
+ | } | ||
+ | lcd.print(second()); | ||
+ | lcd.print(" "); | ||
+ | } | ||
+ | |||
+ | void loop() | ||
+ | { | ||
+ | // Show battery voltage | ||
+ | showVoltage(0); | ||
+ | // Show current time | ||
+ | showTime(1); | ||
+ | } | ||
+ | </code> | ||
===== Rezultate Obţinute ===== | ===== Rezultate Obţinute ===== | ||
- | <note tip> | + | În practică, modulul GPS are nevoie de destul de mult timp pentru a se conecta la sateliți. Prima dată când se conectează poate să dureze foarte mult (chiar și 15 minute), mai ales pentru copiile chinezești. După aceea, modulul GPS se va conecta în câteva minute la satelit (1-5 minute), având în vedere blocurile înalte din orașul București. Într-o zonă mai liberă este de așteptat să se conecteze chiar mai repede de atât. |
- | Care au fost rezultatele obţinute în urma realizării proiectului vostru. | + | |
- | </note> | + | |
- | ===== Concluzii ===== | + | Cu toate acestea, merită menționat faptul că GPS-ul are nevoie de minim 3 (ideal peste 4) sateliți pentru a stabili locația curentă, însă el are și funcționalitatea de a procura ora universală dacă se poate conecta la minim 1 satelit. Acest lucru se întâmplă adesea chiar și înăuntrul casei, oferind modului GPS și funcționalitate de RTC fiabil. |
- | ===== Download ===== | + | În ceea ce privește rotația panoului solar după "soare", acesta își păstrează direcția corectă cu o eroare mică, în general mai puțin de 30 grade. |
- | <note warning> | + | Pentru un videoclip de prezentare a proiectului, click mai jos: |
- | O arhivă (sau mai multe dacă este cazul) cu fişierele obţinute în urma realizării proiectului: surse, scheme, etc. Un fişier README, un ChangeLog, un script de compilare şi copiere automată pe uC crează întotdeauna o impresie bună ;-). | + | |
- | Fişierele se încarcă pe wiki folosind facilitatea **Add Images or other files**. Namespace-ul în care se încarcă fişierele este de tipul **:pm:prj20??:c?** sau **:pm:prj20??:c?:nume_student** (dacă este cazul). **Exemplu:** Dumitru Alin, 331CC -> **:pm:prj2009:cc:dumitru_alin**. | + | {{url>https://youtube.com/embed/tyS7yVPt54g|Videoclip prezentare proiect}} |
- | </note> | + | ===== Concluzii ===== |
+ | |||
+ | Articulația obținută pentru panou este una foarte potrivită pentru dispozitive alimentate solar și care se deplasează, spre exemplu vapoare, automobile. Având în vedere funcționalitatea de stabilizator față de rotații, este un dispozitiv unic pe piață. | ||
+ | ===== Download ===== | ||
+ | {{:pm:prj2024:amocanu:proiect_amaruntis.zip|Arhiva de cod}} | ||
===== Jurnal ===== | ===== Jurnal ===== | ||
* 4 mai 2024: Definitivarea temei proiectului și inițializarea paginii de wiki | * 4 mai 2024: Definitivarea temei proiectului și inițializarea paginii de wiki | ||
* 20 mai 2024: Realizat schema electrică | * 20 mai 2024: Realizat schema electrică | ||
+ | * 26 mai 2024: Completat pagina de wiki | ||
===== Bibliografie/Resurse ===== | ===== Bibliografie/Resurse ===== | ||
Line 116: | Line 430: | ||
- [[https://docs.arduino.cc/learn/electronics/stepper-motors/|Motor pas cu pas pe Arduino]] | - [[https://docs.arduino.cc/learn/electronics/stepper-motors/|Motor pas cu pas pe Arduino]] | ||
- [[https://www.instructables.com/How-to-Connect-I2C-Lcd-Display-to-Arduino-Uno/|Tutorial LCD I2C]] | - [[https://www.instructables.com/How-to-Connect-I2C-Lcd-Display-to-Arduino-Uno/|Tutorial LCD I2C]] | ||
+ | - [[https://randomnerdtutorials.com/guide-to-neo-6m-gps-module-with-arduino/|Ghid modul GPS NEO-6MV2]] | ||
+ | - [[https://www.pveducation.org/pvcdrom/properties-of-sunlight/the-suns-position|Poziția soarelui]] | ||
+ | - [[https://lastminuteengineers.com/28byj48-stepper-motor-arduino-tutorial/|Ghid control motor pas cu pas folosing Arduino]] | ||
<html><a class="media mediafile mf_pdf" href="?do=export_pdf">Export to PDF</a></html> | <html><a class="media mediafile mf_pdf" href="?do=export_pdf">Export to PDF</a></html> | ||