This is an old revision of the document!
Nume: Toma Bogdan-Gabriel
Grupa: 333AC
Scopul acestui proiect este de a crea un ciclocomputer. Acesta este un dispozitiv ce se monteaza, de obicei, pe ghidonul bicicletei si furnizeaza date despre traseul curent (kilometri parcursi, timpul care a trecut de la inceperea traseului precum si caloriile arse).
Cine poate beneficia, cel mai mult, de pe urma utilizarii unui astfel de dispozitiv:
}
“Inima” acestui proiect este o placuta Arduino Nano, ce utilizeaza microcontroller-ul ATmega328.
Principalele modulele folosite sunt:
- Magnetometrul are sarcina de a contoriza turele rotii. Cunoscand acest numar si diametrul rotii putem calcula distanta parcursa pentru fiecare tura / pentru intregul traseu.
- Senzorul Giroscop are sarcina de a detecta panta / rampa pentru a putea calcula corect caloriile arse
- Ecranul LCD are rolul de a afisa datele pe care le manipulam utilizand butoanele ce vor fi incluse in proiect
Biblioteci folosite:
Mediu de dezvoltare:
Pentru inceput, functiile clasice arduino (si anume setup() si loop()) arata in felul urmator:
void setup() { Serial.begin(9600); setupButtons(); setupI2C(); initGyro(); setupLCD(); cli(); setupInterrupt(); sei(); }
void loop() { interrupt = true; gyro(); listenForCycleModesOrRec(); resetSpeedIfIdleLong(); }
Acum, sa le luam pe fiecare in parte si sa le discutam putin.
void setupButtons() { PORTD |= ((1 << PD3) | (1 << PD4) | (1 << PD5)); DDRB |= (1 << PB1); }
Aceasta functie configureaza pinii D3, D4, D5 ca fiind input (nealterand registrul DDRD) si activeaza rezistentele de pull-up (registrul PORTD).
Pinul D9 este configurat ca output (deoarece este folosit pentru PWM la led).
void setupI2C() { Wire.begin(); }
Aceasta functie porneste comunicatia I2C prin initializarea Wire.
void initGyro() { Wire.beginTransmission(MPU); Wire.write(0x3B); Wire.endTransmission(false); Wire.requestFrom(MPU, 6, true); AccX = (Wire.read() << 8 | Wire.read()) / 16384.0; AccY = (Wire.read() << 8 | Wire.read()) / 16384.0; AccZ = (Wire.read() << 8 | Wire.read()) / 16384.0; AngleY = (atan(-1 * AccX / sqrt(pow(AccY, 2) + pow(AccZ, 2))) * 180 / PI) + 1.58; }
Aceasta functie initializeaza unghiul Y, calculandu-l folosind informatiile din accelerometru, nu giroscop. (abia dupa initializare intra in joc giroscopul)
void setupLCD() { lcd.begin(16, 2); }
Aceasta functie porneste ecranul LCD prin initializarea lcd.
void setupInterrupt() { EICRA = 0; EICRA |= (1 << ISC01); EIMSK = 0; EIMSK |= (1 << INT0); }
Aceasta functie configureaza intreruperea (de fiecare data cand magnetometrul detecteaza un camp magnetic, cu alte cuvinte roata a facut o rotatie, este declansata o intretupere). Registrul EICRA este modificat astfel incat bitul ISC01 sa fie activ. Acest lucru cauzeaza ca intreruperea sa aiba loc pe frontul descrescator al pinului INT0 (D2) (Am ales frontul descrescator pentru a fi siguri ca roata face o rotatie completa). Registrul EIMSK activeaza intreruperea pentru pinul INT0.
Acestea au fost functiile ce se gasesc in setup(). Acum ne indreptam atentia catre functiile din loop(), care sunt:
void gyro() {
Wire.beginTransmission(MPU); Wire.write(0x45); Wire.endTransmission(false); Wire.requestFrom(MPU, 2, true); GyroY = (Wire.read() << 8 | Wire.read()) / 131.0 * (-1) + 2.2; if(abs(GyroY) >= 0.5) GyroAngleY += GyroY * (millis() - prevTimeGyro) / 1000; prevTimeGyro = millis(); }
Aceasta functie citeste, continuu, valoarea unghiului Y.
Mai intai este initializata transmisia catre giroscop (MPU este adresa giroscopului). Apoi accesam registrul 0x45, unde se gasesc valorile de interes, si anume GYRO_YOUT, valoare care se intinde pe 2 registrii a cate 8 biti, asadar cerem a fi cititi 2 registrii din giroscop. Aplicam niste corectii, si anume, impartim la 131, inmultim cu -1 si adunam 2.2, iar apoi daca valoarea citita este mai mare decat 0.5 o adaugam in variabila GyroAngleY tinand cont de durata acesteia. La final ar trebui sa ramanem cu unghiul ciclocomputer-ului.
void listenForCycleModesOrRec() { if(!(PIND & (1 << PD5))) { if(pressedPD[5] == false) { prevTimeButton = millis(); pressedPD[5] = true; } else if(millis() - prevTimeButton >= 1000 && PWMOn == false && freeze == false && rec == false) { PWMOn = true; initPWM(); } } else if(PIND & (1 << PD5) && pressedPD[5] == true) { pressedPD[5] = false; PWMOn = false; resetPWM(); if(freeze) { kilometers = 0; calories = 0; seconds = 0; minutes = 0; hours = 0; freeze = false; } else if(millis() - prevTimeButton < 1000 || rec == true) { if(rec) { freeze = true; rec = false; } else if(mode < 1) mode++; else mode = 0; } else if(mode == 0) { rec = true; baseTime = millis(); } } if(mode == 0){ rideMode(); } else if(mode == 1) { setWheelMode(); } if(rec) { seconds = ((millis() - baseTime) / 1000) % 60; minutes = (seconds / 60) % 60; hours = (minutes / 60) % 24; } }
Aceasta functie are rolul de a asculta pentru apasarile butoanelor si de a actiona corespunzator. Este alcatuita din elemente simple, dar intr-adevar voluminoase, pentru a indeplini urmatoarele functionalitati:
(notam: D5 - Butonul 1, D4 - Butonul 2, D3 - Butonul 3)
Acum este momentul potrivit pentur a prezenta functiile folosite in functia anterioara.
void rideMode() { if(millis() - prevTimeLCD >= 500){ lcd.clear(); lcd.setCursor(0, 0); if(kilometers < 10) lcd.print(String(kilometers, 2) + " km"); else if(kilometers >= 10 && kilometers < 100) lcd.print(String(kilometers, 1) + " km"); else if(kilometers >= 100 && kilometers < 1000) lcd.print(String(kilometers, 0) + " km"); else lcd.print("999 km"); lcd.setCursor(0, 1); (hours >= 10) ? lcd.print(String(hours)) : lcd.print("0" + String(hours)); lcd.setCursor(2, 1); lcd.print(":"); lcd.setCursor(3, 1); (minutes >= 10) ? lcd.print(String(minutes)) : lcd.print("0" + String(minutes)); lcd.setCursor(5, 1); lcd.print(":"); lcd.setCursor(6, 1); (seconds >= 10) ? lcd.print(String(seconds)) : lcd.print("0" + String(seconds)); lcd.setCursor(8, 0); lcd.print("Cal"); lcd.setCursor(12, 0); lcd.print(String((int)calories)); lcd.setCursor(9, 1); lcd.print("Kmh"); lcd.setCursor(13, 1); lcd.print(String((int)speedKmh)); prevTimeLCD = millis(); } }
Aceasta functie are rolul de a afisa pe LCD informatiile de interes modului default, si anume, Ride Mode.
void setWheelMode() { if(millis() - prevTimeLCD >= 500) { lcd.clear(); lcd.setCursor(0, 0); lcd.print("Wheel diameter:"); lcd.setCursor(0, 1); lcd.print(String(wheelDiameterCm) + " cm"); prevTimeLCD = millis(); } if(!(PIND & (1 << PB3)) && pressedPD[3] == false) { pressedPD[3] = true; wheelDiameterCm += 10; } else if(PIND & (1 << PB3)) pressedPD[3] = false; if(!(PIND & (1 << PB4)) && pressedPD[4] == false) { pressedPD[4] = true; if(wheelDiameterCm >= 10){ wheelDiameterCm -= 10; } } else if(PIND & (1 << PB4)) pressedPD[4] = false; }
Aceasta functie a rolul de a afisa pe LCD informatiile de interes modului 2, si anume, Set Wheel Mode. Tot aici apasarile butoanelor 2 si 3 conduc la modificarea diametrului rotii.
void initPWM() { TCCR1A = 0; TCCR1A |= ((1 << COM1A0) | (1 << WGM12)); OCR1A = 31270; TCNT1 = 0; }
Aceasta functie configureaza PWM pentru led. Setam in registrul TCCR1A bitii COM1A0, responsabil pentru trecerea pinului OC1A (D9) in starea low atunci cand valoarea din timer ajunge la valoarea prestabilita, si anume, 31270, si bitul WGM12, responsabil pentru setarea modului CTC.
void resetPWM() { TCCR1A = 0; }
Opusul functiei prezentate anterior.
Acum, “piesa de rezistenta”, functia ce se executa in intrerupere:
ISR(INT0_vect) { if(interrupt) { distance = (2 * 3.14 * (wheelDiameterCm / 2)) / 100000; if(rec){ kilometers += distance; if(GyroAngleY >= 0.6) calories += distance * 32 * 3 * GyroAngleY / 1.72; else if (GyroAngleY >= -0.6 && GyroAngleY < 0.6) calories += distance * 32; else calories += distance * 32 * -1 / 3 * GyroAngleY / 1.72; } speedKmh = distance * 3600000/(millis() - prevTimeKmh); prevTimeKmh = millis(); interrupt = false; } }
Aceasta functie calculeaza distanta, in km, bazat pe diametrul rotii (variabile booleana interrupt este folosita pentru a nu avea intretuperi multiple (un fel de debouncing pentru intreruperi). Tot aici, in cazul in care inregistram (variabila rec este true) contorizam km, calculam caloriile si viteza. Calculul caloriilor se bazeaza pe date colectate din sursele ce urmeaza a fi citate mai jos, si pe regula de 3 simpla (pe scurt un om arde, in medie, de 3 ori mai multe calorii cand pedaleaza pe o rampa avand panta 3% (1.72 grade). De aici ne putem da seama de caloriile arse in functie de panta). Pentru calculul vitezei se tine cont de diametrul rotii si de timpul dintre 2 intreruperi. in cazul in care acesta este mai mare de 1 secunda, viteza se reseteaza folosind functia urmatoare.
void resetSpeedIfIdleLong() { if(millis() - prevTimeKmh >= 1000) speedKmh = 0; }
Aceasta functie reprezinta sfarsitul lungului sir de functii folosite (ura!). Variabilele folosite de alungul programului sunt urmatoarele:
LiquidCrystal lcd(12, 11, 6, 7, 8, 10); const int MPU = 0x68; float GyroY = 0, GyroAngleY; float prevTimeGyro = 0; float prevTimeKmh = 0; float prevTimeButton = 0; float prevTimeLCD = 0; int baseTime = 0; float kilometers = 0, distance = 0; int hours = 0, minutes = 0, seconds = 0; float wheelDiameterCm = 0; float calories = 0; int mode = 0; float speedKmh = 0; bool rec = false; bool interrupt = true; bool pressedPD[7] = { false }; bool PWMOn = false; bool freeze = false;
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.