Bike monitoring device

Introducere

Tudor Ioana Octavia 331CA

Proiectul vizează crearea unui dispozitiv de monitorizare pentru bicicletă. Acest device va furniza informații despre viteza mea de deplasare, viteza medie, distanța parcursă, temperatura si presiunea curenta alaturi de punctul cardinal spre care ma indrept, dar si datele giroscopice si accelerometrice. În plus, la alegerea utilizatorului, se va seta un target de distanță pentru a motiva performanța și a îmbunătăți rezultatele fizice.

Am ales acest proiect deoarece mersul pe bicicleta este una dintre pasiunile mele preferate si am vrut sa construiesc ceva ce imi poate fi util atat mie cat si altor oameni cu acelasi hobby, dar si sa lucrez la ceva la care chiar sa imi faca placere.

Din experienta mea, consider ca acest dispozitiv poate face experiența ciclismului mai plăcută, oferind o modalitate simplă și eficientă de a monitoriza datele relevante. Cu ajutorul său, utilizatorii pot seta obiective precise de distanță, se pot adapta la condițiile meteorologice și pot analiza tehnica de pedalare pentru a maximiza eficiența și siguranța. Totodata, acest device ofera o vedere clara asupra efortului parcurs dupa un antrenament pe bicicleta, mentinandu-te motivat.

Descriere generală

Dispozitivul meu imi va afisa initial pe un display LCD 1602 IIC/I2Cviteza curenta, viteza medie si distanta parcursa. Viteza este calculata folosind un senzor de camp magnetic (Hall). Voi avea in plus 4 butoane cu urmatoarele functionalitati:

  • Posibilitatea de alegere a dimensiunii rotii de bicicleta in vederea calcularii corecte a datelor despre viteza si distanta
  • Setarea unei distante target de atins prin apasari repetate ale butonului corespunzator pentru a creste distanta dorita. La atingerea distantei alese un se va aprinde un LED albastru timp de 2 secunde.
  • Afisarea temperaturii ambientale si a presiunii alaturi de punctul cardinal spre care ma indrept folosind Modulul 10DOF MPU9250 și BMP280
  • Afisarea coordonatelor giroscopice si accelerometrice folosind acelasi modul.

In acest fel utilizatorul va putea avea o evidenta clara a efortului depus.

Hardware Design

Lista Piese:
  • Arduino Nano
  • Ecran LCD 1602 IIC/I2C
  • 4 Butoane push fara retinere, 7mm, 2 pini
  • Rezistor 1k
  • PCB
  • KY-024 Linear magnetic Hall Sensor
  • LED 5mm albastru transparent
  • Fire
  • Modul 10DOF MPU9250 și BMP280
  • Baterie externa 5V
Schema circuitului:

Descrierea legaturilor:

Ecran LCD 1602 IIC/I2C → afisare 2 linii 16 coloane

  • GND - GND
  • VCC - 5V
  • SDA - A4
  • SCL - A5

Senzor magnetic Hall (KY-024)

Detecteaza prezenta unui camp magnetic. Daca se pune un magnet pe roata si acest senozr pe furca bicicletei, se poate determina viteza curenta cu care merge biciclistul. Senzorul are trei componente principale pe placa sa de circuit. Are unitatea de senzor din partea frontală a modulului care măsoară fizic zona și trimite un semnal analog către a doua unitate, amplificatorul. Amplificatorul amplifică semnalul, în funcție de valoarea de rezistență a potențiometrului, și trimite semnalul la ieșirea analogică a modulului. A treia componentă este un comparator care comută ieșirea digitală și LED-ul dacă semnalul scade sub o anumită valoare. Se poate controla sensibilitatea ajustând potențiometrul.

  • GND - GND
  • +V - 5V
  • DO (digital signal) - D6

Modul 10DOF MPU9250 și BMP280 → giroscop, accelerometru, presiune, temperatura, magnetometru

  • Vin - 5V
  • GND - GND
  • SCL - A5
  • SDA - A4

Buton 1 → seteaza dimensiunea rotii

  • GND - GND
  • OUT - D12

Button 2 → incrementeaza distanta target (are si contor pentru debouncing implementat in cod)

  • GND - GND
  • OUT D3

Button 3 → trigger sa afisez presiunea, temperatura si punctul cardinal

  • GND - GND
  • OUT - D8

Button 4 → trigger sa afisez datele giroscopice si accelerometrice

  • GND - GND
  • OUT - D9

LED → se aprinde la atingerea distantei target timp de 2s

  • - - GND
  • + - D7

Rezistenta are rol de a limita curentul ce trece prin LED. Fără o rezistență care să limiteze acest curent, LED-ul poate primi mai mult curent decât capacitatea sa nominală, ceea ce poate duce la arderea și distrugerea acestuia rapid.

Notiuni utilizate:

In realizarea proiectului am folosit urmatoarele laboratoare studiate:

  • Lab0: GPIO - Se folosește pentru a gestiona butoanele și LED-ul. Pinii sunt configurați ca intrări (pentru butoane) și ieșiri (pentru LED), utilizând funcțiile de citire și scriere digitală.
  • Lab2: Întreruperi - Functia mea de intrerupere ISR(TIMER1_COMPA_vect) semnalizează apariția unui eveniment care trebuie tratat de către procesor fără a aștepta finalizarea întregului ciclu de procesare în loop(), si anume detectarea rotatiilor bicicletei, debouncing pentru butoane si verificarea atingerii distantei target.
  • Lab3: Timere - Am configurat timerul 1 să genereze o întrerupere la fiecare 1 ms pentru masurarea exacta a timpului intre 2 rotatii, pentru debouncing-ul butoanelor si pentru controlul duratei de aprindere a LED-ului
  • Lab6: I2C - Folosit in comunicarea cu IMU (pentru a citi datele de la accelerometru si giroscop masurate de MPU9250). Se mai foloseste si pentru a citi ajustările necesare de calibrare din ROM-ul de fuziune al MPU9250 și pentru a citi valori brute ale câmpului magnetic, care sunt esențiale pentru determinarea direcției de deplasare. De asemenea si senzorul BMP280 comunica tot prin I2C.
Poze de pe parcurs

Software Design

Mediu de dezvoltare: ArduinoIDE

Descrierea Codului

Biblioteci utilizate:
#include <LiquidCrystal_I2C.h> // pt interactiunea cu ecrane LCD prin protocol I2C; controleaza afisajul fara a necesita multe conexiuni cabluri
 
#include <Wire.h>  // faciliteaza schimbul de date dintre microcontroller  si dispozitive I2C, adica senzorii mei
 
#include <Adafruit_Sensor.h> // interfata pentru senzorii Adafruit, unifică codul de accesare a 
//senzorilor și reduce complexitatea necesară pentru a citi date
 
#include "Adafruit_BMP280.h" // specifica senzor BMP 280, ofera functii directe de interactionare cu senzorul
 
#include "I2C.h" // functionalitati suplimentare pentru comunicarea I2C fata de Wire.h
Define-uri si constante
#define LED 7
#define B1 12
#define B2 3
#define B3 8
#define B4 9
#define reed 6
#define BMP280_I2C_ADDRESS 0x76 // adresa I2C pentru senzorul BMP280

#define MPU9250_IMU_ADDRESS 0x68 // adresa IMU
#define MPU9250_MAG_ADDRESS 0x0C // adresa magnetometru

/*
Setările de scalare completă pentru giroscop, exprimate în grade pe secundă (DPS).
(0x00, 0x08, 0x10, 0x18) reprezintă setările de configurare pentru diferitele sensibilități maxime ale giroscopului.
*/
#define GYRO_FULL_SCALE_250_DPS  0x00
#define GYRO_FULL_SCALE_500_DPS  0x08
#define GYRO_FULL_SCALE_1000_DPS 0x10
#define GYRO_FULL_SCALE_2000_DPS 0x18

/*
Setările de scalare completă pentru accelerometru, exprimate în multiple ale accelerației gravitaționale G
*/
#define ACC_FULL_SCALE_2G  0x00
#define ACC_FULL_SCALE_4G  0x08
#define ACC_FULL_SCALE_8G  0x10
#define ACC_FULL_SCALE_16G 0x18

#define TEMPERATURE_OFFSET 21 // din documentatie

#define G 9.80665 // constanta gravitațională
#define NUM_READINGS 72  // Numărul de citiri pentru medierea (unghiului dat de magnetometru)
Initializari si declari de variabile

Explicatiile fiecarei variabile sunt date in comentarii deoarece am vrut sa fie usor de vizualizat si explicatia si codul descris.

float headingReadings[NUM_READINGS];  // Array pentru stocarea citirilor unghiurilor pt determinarea punctului cardinal
int headingIndex = 0;  // Indexul curent pentru înregistrarea noilor citiri
float totalHeading = 0;  // Suma totală a citirilor din array
float averageHeading = 0;  // Media citirilor
 
 
Adafruit_BMP280 bmp; // creeare instanta a senzorului BMP280 pentru masurarea presiunii si temperaturii
LiquidCrystal_I2C lcd(0x27, 16, 2); // initializare display LCD cu 16 col si 2 linii
 
float radius = 13; // raza roata de bicicleta
int reedVal; // valoare senzor citita
long timer_one_rot = 0; // cronometru care masoara timpul intre 2 rotatii complete detectate de senzorul Reed 
float speed = 0.0;  // viteza curenta
float circumference;
int tire_index = 0; // index pentru elementele din vectorul tire_dimensions
float lastSize = radius; // ultima val a razei rotii utilizata 
 
int max_reed_nr = 212; // timp min intre 2 rotatii pt debouncing pt a preveni citiri false
int reed_nr;  // este contorul actual care se decrementeaza la fiecare apel al întreruperii până când poate fi considerată o nouă rotație validă
 
// pentru detectarea apasarii butoanelor retin starile lor anterioare
int old_button1_state = HIGH;
int old_button2_state = HIGH;
int old_button3_state = HIGH;
int old_button4_state = HIGH;
 
int readings = 0; // numara citirile de viteza pentru calculul vitezei medii
int max_readings = 100; // nr max de masuratori ale vitezei pt calculul vitezei medii
float averageSpeed = 0;
float lastAverageSpeed = 0;
float totalDistanceTraveled = 0;
float tire_dimensions[3] = {13, 13.75, 14.5}; // cele 3 size-uri posibile pentru roti
 
float temp; // temperatura inregistratat de senzorul BMP 280
float pressure;	// presiunea inregistratat de senzorul BMP 280
 
// variabile care imi vor indica ce functii de afisare pe LCD voi apela
bool display = false;
bool displayDirection = false;
 
 
float target_distance = 0;
float t = 0; // contorizeaza timpul cat sta aprins LED-ul cand ating distanta de target
 
 
unsigned long lastDebounceTime = 0;  // Ultima dată când starea butonului a schimbat B2
const unsigned long debounceDelay = 300;  // Debounce time în milisecunde
Functii si structuri de date necesare manipularii datelor citite de senzori

Structurile urmatoare stochează măsurători pe trei axe (x, y, z) pentru giroscop, accelerometru și magnetometru.

struct gyroscope_raw {
  int16_t x, y, z;
} gyroscope;
 
struct accelerometer_raw {
  int16_t x, y, z;
} accelerometer;
 
struct magnetometer_raw {
  int16_t x, y, z;
 
  struct {
    int8_t x, y, z;
  } adjustment;  // asta este pentru calibrare 
} magnetometer;
 
struct temperature_raw {
  int16_t value;
} temperature;
 
struct {
  struct {
    float x, y, z;
  } accelerometer, gyroscope, magnetometer;
 
  float temperature;
} normalized;  // valorile normalizate de la toti senzorii

Aceste functii verifica daca datele sunt gata pentru a fi citite. Se evita întârzierile în procesare și asigura că datele sunt actualizate și gata de utilizare când sunt necesare.

void setMagnetometerAdjustmentValues()  // functie pentru calibrare
{
  uint8_t buff[3];  // pentru valorile de ajustare
 
  I2CwriteByte(MPU9250_MAG_ADDRESS, 0x0A, 0x1F);
  //Modul 0x1F setează magnetometrul pentru a oferi valori pe 16 biți și permite accesul la ROM-ul de fuziune, care este necesar pentru a citi valorile de ajustare
 
  delay(3000);
 
  I2Cread(MPU9250_MAG_ADDRESS, 0x10, 3, buff); // citeste ajustarile
 
 
  magnetometer.adjustment.x = buff[0]; // Adjustare pt axa X
  magnetometer.adjustment.y = buff[1]; // Adjustare pt axa Y
  magnetometer.adjustment.z = buff[2]; // Adjustare pt axa Z
 
  I2CwriteByte(MPU9250_MAG_ADDRESS, 0x0A, 0x10); // Power down
}
 
bool isMagnetometerReady() // asigura ca datele sunt citite doar dupa ce sunt actualizate 
{
  uint8_t isReady; // flag de intrerupere 
 
  I2Cread(MPU9250_MAG_ADDRESS, 0x02, 1, &isReady);
 
  return isReady & 0x01;
}
 
void readRawMagnetometer() // Citeste valorile brute de la magnetometru pe axele x, y, z și le stochează în structura magnetometer
{
  uint8_t buff[7];
 
  // Read output registers:
  // [0x03-0x04] masuratoare axa X
  // [0x05-0x06] masuratoare axa Y
  // [0x07-0x08] masuratoare axa Z
  I2Cread(MPU9250_MAG_ADDRESS, 0x03, 7, buff);
 
  // Magnetometer, creaza valori de 16 bits din date de 8 biti
  magnetometer.x = (buff[1] << 8 | buff[0]);
  magnetometer.y = (buff[3] << 8 | buff[2]);
  magnetometer.z = (buff[5] << 8 | buff[4]);
}
 
bool isImuReady() //  verifică dacă datele de la IMU (Inertial Measurement Unit, care include giroscopul și accelerometrul) sunt gata de citit
{
  uint8_t isReady; // flag de intrerupere
 
  I2Cread(MPU9250_IMU_ADDRESS, 58, 1, &isReady);
 
  return isReady & 0x01;
}
 
void readRawImu() // Citeste datele brute de la accelerometru și giroscop, plus temperatura, din IMU și le stochează în structurile respective
{
  uint8_t buff[14];
 
  // Read output registers:
  // [59-64] Accelerometer
  // [65-66] Temperature
  // [67-72] Gyroscope
  I2Cread(MPU9250_IMU_ADDRESS, 59, 14, buff);
 
  // Accelerometer, create 16 bits values from 8 bits data
  accelerometer.x = (buff[0] << 8 | buff[1]);
  accelerometer.y = (buff[2] << 8 | buff[3]);
  accelerometer.z = (buff[4] << 8 | buff[5]);
 
  // Temperature, create 16 bits values from 8 bits data
  temperature.value = (buff[6] << 8 | buff[7]);
 
  // Gyroscope, create 16 bits values from 8 bits data
  gyroscope.x = (buff[8] << 8 | buff[9]);
  gyroscope.y = (buff[10] << 8 | buff[11]);
  gyroscope.z = (buff[12] << 8 | buff[13]);
}

Functiile de normalizare si calibrare

Normalizarea are rolul de a transforma datele brute citite de senzori in unitati standard. Calibrarea, cum ar fi in cazul setarea valorilor de ajustare pentru magnetometru permit compensarea variațiilor de fabricație și influențelor ambientale, crescând acuratețea și fiabilitatea măsurătorilor.

void normalize(gyroscope_raw gyroscope) // pt a transforma in grade pe secunda folosind factorul de scalare specificat în documentația senzorului
{
  // (MPU datasheet pag 8)
  normalized.gyroscope.x = gyroscope.x / 32.8;
  normalized.gyroscope.y = gyroscope.y / 32.8;
  normalized.gyroscope.z = gyroscope.z / 32.8;
}
 
void normalize(accelerometer_raw accelerometer) // pt a transforma la m/s^2
{
  // factor de scalare  (MPU datasheet pag 9)
  normalized.accelerometer.x = accelerometer.x * G / 16384;
  normalized.accelerometer.y = accelerometer.y * G / 16384;
  normalized.accelerometer.z = accelerometer.z * G / 16384;
}
 
void normalize(temperature_raw temperature)
{
  //Scale Factor (MPU datasheet pag 11) && formula (MPU pag 33)
  normalized.temperature = ((temperature.value - TEMPERATURE_OFFSET) / 333.87) + TEMPERATURE_OFFSET;
}
 
void normalize(magnetometer_raw magnetometer) // calibreaza valorile brute si le ajusteaza
{
  // factor scalar de senzitivitate (MPU datasheet pag 10)
  // 0.6 µT/LSB (14-bit)
  // 0.15µT/LSB (16-bit)
  // Avalori de ajustare (MPU register page 53)
  normalized.magnetometer.x = magnetometer.x * 0.15 * (((magnetometer.adjustment.x - 128) / 256) + 1);
  normalized.magnetometer.y = magnetometer.y * 0.15 * (((magnetometer.adjustment.y - 128) / 256) + 1);
  normalized.magnetometer.z = magnetometer.z * 0.15 * (((magnetometer.adjustment.z - 128) / 256) + 1);
}
Functia setup()

Aceasta functie asigura ca toate dispozitivele sunt configurate corespunzator inainte de inceperea buclei pricipale loop().

  • Se realizeaza activarea comunicatiei I2C pentru a permite comunicarea cu senzorii și pentru a trimite date către consola serială pentru debug sau afișare de informații.
  • Configurarea Senzorilor MPU9250
  • Setează modurile pinilor pentru butoane, LED și senzorul Reed
  • Configureaza Timer1 care sa functioneze in mod CTC, prescaler = 8, generand intrerupere la fiecare 1ms
  • Verificarea și Inițializarea Senzorului BMP280
  • Initializarea vectorului pentru cititiri ale magnetometrului in vederea calcularii punctului cardinal
void setup() {
  Wire.begin();
  Serial.begin(9600);
 
  I2CwriteByte(MPU9250_IMU_ADDRESS, 27, GYRO_FULL_SCALE_1000_DPS); // scalarea completă a giroscopului la 1000 de grade pe secundă
  I2CwriteByte(MPU9250_IMU_ADDRESS, 28, ACC_FULL_SCALE_2G); // scalarea completă a accelerometrului la 2G
  I2CwriteByte(MPU9250_IMU_ADDRESS, 55, 0x02);
  I2CwriteByte(MPU9250_IMU_ADDRESS, 56, 0x01);
  setMagnetometerAdjustmentValues(); //  configurează și citește valorile de calibrare ale magnetometrului
  I2CwriteByte(MPU9250_MAG_ADDRESS, 0x0A, 0x12);
 
  // configurare LCD
  lcd.init();
  lcd.backlight();
  lcd.begin(16, 2); 
 
  // Configurează pinii la care sunt conectate butoanele și LED-ul ca intrări cu rezistență de pull-up internă, respectiv ca ieșire
  pinMode(B1, INPUT_PULLUP);
  pinMode(B2, INPUT_PULLUP);
  pinMode(B3, INPUT_PULLUP);
  pinMode(B4, INPUT_PULLUP);
  pinMode(LED, OUTPUT);
  pinMode(reed, INPUT); // seteaza pinul pt senzorul reed ca intrare
 
  reed_nr = max_reed_nr; // reed_nr urmeaza a fi decrementat 
  circumference = 2 * 3.14 * radius;
 
  cli(); // Dezactivează întreruperile globale în timpul configurării timerului pentru a evita problemele de sincronizare
  // Resetează registrele de control și contorul timerului 1 la 0 pentru configurare
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1 = 0;
  OCR1A = 1999;  // = (1/1000) / ((1/(16*10^6))*8) - 1 valoarea de comparare a timerului  1 pt a genera la fiecare 1ms o intrerupere
  // frecventa ceasului = 16MHz
  TCCR1B |= (1 << WGM12);  // mod CTC 
  TCCR1B |= (1 << CS11);  // prescaler = 8
  TIMSK1 |= (1 << OCIE1A);  // da enable intreruperea de comparare
  sei(); // activeaza intreruperile
 
  if (!bmp.begin(BMP280_I2C_ADDRESS)) { // Verifică dacă senzorul BMP280 este conectat și poate fi inițiat la adresa I2C specificată
    Serial.println("Could not find a valid BMP280 sensor, check wiring!");
    while (1); // Blochează execuția programului dacă senzorul nu este detectat, evitând rularea ulterioară a codului
  }
   for (int i = 0; i < NUM_READINGS; i++) {
        headingReadings[i] = 0;
    }
}
Functia de intrerupere ISR(TIMER1_COMPA_vect)

Functia se executa automat la fiecare 1ms, de fiecare data cand timerul 1 atinge valoarea 1999 si se reseteaza automat la 0 (mod CTC). Comentariile din cod descriu amanuntit logica de implementare, dar voi oferi si o explicatie per ansamblu.

Logica explicata:

  • Se citesc starile senzorului Reed si a celor 4 butoane
  • Apasarea butonului B1 imi va schimba dimensiunea rotii cicland prin vectorul de 3 valori dimensiuni cu ajutorul indicelui tire_index
  • Se actualizeaza circumferinta , se reseteaza distanta target si timerul de control al timpului cat poate sta LED-ul aprins
  • Starea veche a fiecarui buton (old_buttonx_state) trebuie actualizata
  • La apasarea lui B2 incrementez target_distance doar daca a trecut suficient timp de la ultima actualizare, astfel evit incrementarea repetata a target_distance
  • La apasarea lui B3 , setez un flag display de afisare true pe care il voi verifica in functia loop() pentru a stii ce sa afisez pe LCD
  • La apasarea lui B4 , setez un alt flag displayDirection de afisare true pe care il voi verifica in functia loop() pentru a stii ce sa afisez pe LCD
  • Verific apoi daca distanta totala parcursa imi depaseste target distance-ul setat si voi aprinde LED-ul timp de 2s (fapt controlat prin contorul t)
  • Verific valoarea senzorului reed si daca este diferita de 1, adica s-a detectat o rotatie , verific daca a trecut suficient timp de la ultima activare si poate fi considerata noua citire (contorul de debouncing)
  • Daca da, calulez viteza curenta tinand cont sa convertesc in km/h , distanta totala parcursa, incrementez numarul de citiri facute si actualizez viteza medie. Viteza medie este resetata la valoarea vitezei curente dupa 100 de citiri.
  • Apoi resetez cronometru ce masoara timpul dintre 2 rotatii (timer_one_rot) si resetez contorului de debouncing la valoarea maximă pentru urmatoarea masurare (reed_nr = max_reed_nr)
  • Daca nu a trecut suficient timp de la ultima activare a senzorului, decrementez contorul de debouncing
  • Consider ca daca timpul trecut dintre 2 rotatii e mai mare de 2s, atunci bicicleta stationeaza
  • Daca nu, inseamna ca pot masura in continuare timpul dintre 2 rotatii

   Consider ca dificultatea acestui cod ar putea consta in diferentierea lui reed_nr de time_one_rot. 
   timer_one_rot este un cronometru care măsoară intervalul de timp, în milisecunde, între rotații. Fiecare dată când senzorul Reed detectează 
   trecerea unui magnet (indicând o rotație completă), timer_one_rot este resetat la zero, iar timpul până la următoarea detecție este folosit 
   pentru a calcula viteza. In timp ce, variabila reed_nr este folosita ca un contor pentru debouncing. Evita citirile multiple sau false 
   cauzate de zgomotul mecanic sau oscilațiile contactului senzorului reed, care ar putea înregistra mai multe impulsuri pentru o singură 
   trecere a magnetului. O nouă detecție a rotației este acceptată numai după ce a trecut suficient timp de la ultima detecție validă.
   

ISR(TIMER1_COMPA_vect) {
  reedVal = digitalRead(reed); // Citeste starea pinului conectat la senzorul Reed care detecteaza rotatiile rotii bicicletei
  // se citeste starea fiecarui buton 
  int button1 = digitalRead(B1);  
  int button2 = digitalRead(B2);
  int button3 = digitalRead(B3);
  int button4 = digitalRead(B4);
 
  if ((button1 == LOW) && (old_button1_state == HIGH)) { // apasarea butonului 1
    lastSize = tire_dimensions[tire_index]; // salveaza dimensiunea curenta a rotii
    tire_index = (tire_index + 1) % 3;  // Rotește prin dimensiunile disponibile
    radius = tire_dimensions[tire_index];
    circumference = 2 * M_PI * radius; // noua circumferinta 
    target_distance = 0; // se seteaza distanta de target la 0
    t = 0; // timerul pentru led resetat
  } else {
    digitalWrite(LED, LOW);    
  }
  old_button1_state = button1; // actualizarea starii butonului
 
  if ((button2 == LOW) && (old_button2_state == HIGH)) {
    if (millis() - lastDebounceTime > debounceDelay) {
        // Actualizează distanța doar dacă au trecut debounceDelay milisecunde de la ultima schimbare
        target_distance++;
        lastDebounceTime = millis();  // Resetare timp la ultima schimbare
    }
 
  } 
  old_button2_state = button2;
 
 
  if ((button3 == LOW) && (old_button3_state == HIGH)) {
    display = true; // flag verificat in loop care imi indica sa afisez pe lcd temperatura si presiunea
  }
  old_button3_state = button3;
 
  if ((button4 == LOW) && (old_button4_state == HIGH)) {
    displayDirection = true; // flag verificat in loop care imi indica sa afisez pe lcd datele giroscopice si accelerometrice
  }
  old_button4_state = button4;
 
  if (target_distance < totalDistanceTraveled) { // daca am depasit targetul setat aprind ledul 
    if(t < 2000) { // pt a aprinde timp de 2s ledul cand ating distanta target 
      digitalWrite(LED, HIGH);
    }
    t++;
    if(t > 2000) { // led stins dupa 2s
      digitalWrite(LED, LOW);
    }
  } else {
    digitalWrite(LED, LOW);
    t = 0;
  }
 
  if (reedVal != 1) { // s-a detectat o rotatie a rotii
    if (reed_nr == 0) { // a trecut suficient timp de la ultima activare si poate fi considerata noua citire (contorul de debouncing)
      /*
      Calculează viteza curentă. Formula convertește circumferința roții din inch în metri (înmulțind cu 0.0254),
      apoi calculează distanța pe oră împărțind la timpul între două rotații (în secunde) și înmulțind cu 3600 pentru a obține km/h
      */
      speed = (3600 * (float(circumference) * 0.0254)) / float(timer_one_rot);
      totalDistanceTraveled += (circumference * 0.0000254);   
      readings++; // incrementare nr de citiri facute
      averageSpeed = (averageSpeed + speed) / 2;  // actualizare a vitezei medii cu ultima viteza curenta
      if (readings == max_readings) {
        readings = 1;
        averageSpeed = speed; //la 100 de citiri resetez v medie la ultima curenta masurata
      }
      timer_one_rot = 0; // resetare cronometru ce masoara timpul dintre 2 rotatii
      reed_nr = max_reed_nr; // resetarea contorului de debouncing la valoarea maximă pentru urmatoarea masurare
      //  resetarea contorului de debouncing la valoarea maximă pentru a preveni citiri false imediat după o citire validă.
    } else {
      if (reed_nr > 0) { // perioada de debouncing
        reed_nr -= 1;
      }
    }
  } else {
    if (reed_nr > 0) {
      reed_nr -= 1;
    }
  }  
  if (timer_one_rot > 2000) { // considera ca bicicleta e oprita aici 
    speed = 0;
  } else {
    timer_one_rot += 1; // daca nu e oprita masor in continuare timpul intre rotatii
  } 
 
}
Functia void displayNormal()

Funcția afiseaza pe un ecran LCD informații esențiale pentru cicliști, cum ar fi dimensiunea roții, viteza curentă, viteza medie, distanța totală parcursă și distanța țintă. Începe prin a verifica și afișa schimbările dimensiunii roții pentru a asigura acuratețea măsurătorilor, apoi continuă cu actualizarea constantă a vitezei și distanței, oferind ciclistului date necesare pentru monitorizarea performanței.

void displayNormal() {
  /*
  Verifică dacă raza roții curente (radius) este diferită de ultima raza stocată (lastSize).
  Dacă acestea sunt diferite, înseamnă că mărimea roții a fost schimbată și trebuie actualizat afișajul.
  */
  if (radius != lastSize) {
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Wheel Size ");
    lcd.print(radius * 2);    
    delay(700);
    lcd.clear();
    lastSize = radius;
    averageSpeed = 0; // resetare a vitezei medii deoarece calculele anterioare nu mai sunt relevante
  }
 
  lcd.setCursor(0, 0);
  lcd.print(speed);
  if (speed < 10) {
    lcd.setCursor(4, 0);
    lcd.print(" ");
  }  
  lcd.setCursor(6, 0);
  lcd.print("km/h");
  lcd.setCursor(0, 1);
  lcd.print(averageSpeed); 
  lcd.setCursor(6, 1);
  lcd.print("km/h");
  lcd.setCursor(12, 0);
  lcd.print(totalDistanceTraveled);  
  lcd.setCursor(12, 1);
  lcd.print(target_distance);
}
Functia void displayInfoScreen()

Imi afiseaza pe ecran doar la apasarea butonului corespunzator (adica activarea flagului display) temperatura ambientala, presiunea (furnizate de senzorul BMP 280), dar si punctul cardinal spre care ma indrept (pe baza datelor magnetometrice obtinute de la MPU9250 si din functia determinCardinalDirection(averageHeading) pe care o voi detalia in sectiunea urmatoare).

void displayInfoScreen() {
  lcd.clear();
  temp = bmp.readTemperature(); // Citește temperatura actuală de la senzorul BMP280
  pressure = bmp.readPressure(); // Citește presiunea atmosferică de la același senzor BMP280
  lcd.setCursor(0, 0);
  lcd.print("Temp: ");
  lcd.print(temp);
  lcd.print(" C");
  String direction = determinCardinalDirection(averageHeading);
  lcd.print(" ");
  lcd.print(direction);
 
  lcd.setCursor(0, 1);
  lcd.print("Press: ");
  lcd.print(pressure);
  lcd.print(" Pa");
 
  delay(2000);
  display = false; // resetez iar flagul cu false care poate fi activat doar prin apasarea butonului corespunzator
  lcd.clear();
}
Functia String determinCardinalDirection(float heading)

Pe baza unghiului furnizat, funcția clasifică direcția în una dintre categoriile standard: Nord (N), Est (E), Sud (S) si Vest (W).

String determinCardinalDirection(float heading) {
    if (heading >= 315 || heading < 45) {
        return "N"; // Nord
    } else if (heading >= 45 && heading < 135) {
        return "E"; // Est
    } else if (heading >= 135 && heading < 225) {
        return "S"; // Sud
    } else if (heading >= 225 && heading < 315) {
        return "W"; // Vest
    }
    return "Unknown"; // În caz de eroare sau date invalide
 
}
Functia updateHeadingReadings

Functia este folosita pentru a actualiza si calcula punctul cardinal folosind media unui esantion de NUM_READINGS citiri. Se calculeaza un nou heading folosind funcția atan2 pentru a obține unghiul dintre axa y și x al magnetometrului, exprimat în grade. Dacă acest unghi este negativ, i se adaugă 360 de grade pentru a obține o valoare pozitivă. Apoi, se actualizeaza in vectorul de citiri (rotatie circulara) scotand cea mai veche valoare, adaugand noul unghi si se face media stabilizand astfel directia obtinuta si reducand din fluctuatiile senzorului.

void updateHeadingReadings() {
    float newHeading = (atan2(normalized.magnetometer.y, normalized.magnetometer.x)) * 180 / M_PI;
    if (newHeading < 0.0) {
        newHeading += 360.0;
    }
 
    // Incrementare index circular
    headingIndex = (headingIndex + 1) % NUM_READINGS;
 
    // Scoate cea mai veche valoare din suma totală
    totalHeading -= headingReadings[headingIndex];
 
    // Adaugă noua valoare la poziția cea mai veche, acum actualizată
    headingReadings[headingIndex] = newHeading;
    totalHeading += newHeading;
 
    // Calculează media
    averageHeading = totalHeading / NUM_READINGS;
}
Functia void display_Mag_Gyr()

Funcția este destinată să afișeze pe un ecran LCD valorile normalizate pentru giroscop (GYR) și accelerometru (ACC).

void display_Mag_Gyr() {
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("GYR ");
    lcd.print(normalized.gyroscope.x, 1);
    lcd.print(" ");
    lcd.print(normalized.gyroscope.y, 1);
    lcd.print(" ");
    lcd.print(normalized.gyroscope.z, 1);
 
    lcd.setCursor(0, 1);
    lcd.print("ACC ");
    lcd.print(normalized.accelerometer.x, 1);
    lcd.print(" ");
    lcd.print(normalized.accelerometer.y, 1);
    lcd.print(" ");
    lcd.print(normalized.accelerometer.z, 1);
    delay(2000);
    displayDirection = false;
    lcd.clear();
    delay(2000);
}
Functia void loop()

Functia e conceputa pentru a gestiona activitatile ciclice ale dispozitivului. Citeste, proceseaza si normalizeaza perpetuu datele brute ale senzorilor (giroscop, magnetometru si accelerometru) si afiseaza informatii pe LCD in functie de flagul care este true (cele activate de apasarea butoanelor si afisarea default cu viteza, viteza medie, distanta si distanta target). Funcția introduce și o întârziere pentru a echilibra ritmul de executare. Functia updateHeadingReadings() este apelata aici pentru a colecta mereu datele senzorului pentru a obtine media stabila.

void loop() {
 
  if (isImuReady()) {  // verifică dacă unitatea inerțială de măsurare (IMU), care include un giroscop și un accelerometru, este pregătită pentru a furniza date
    readRawImu(); // citeste datele brute
 
    // Normalizarea datelor brute
    normalize(gyroscope);
    normalize(accelerometer);
    normalize(temperature);
  }
 
  if (isMagnetometerReady()) { // Verifică dacă magnetometrul este gata să furnizeze date
    readRawMagnetometer(); // citire date brute
 
    normalize(magnetometer); // normalizarea datelor brute ale magnetometrului
  }
  updateHeadingReadings();
  if ( displayDirection == true){ // Verifică dacă trebuie afișată direcția bazată pe datele magnetometrului și giroscopului
      display_Mag_Gyr();
  }
 
  else if (display) {
    displayInfoScreen(); // verifica daca trebuie sa afiseze temp , presiunea si punctul cardinal
  } else {
    displayNormal(); // afișează defaultpe LCD viteza curentă, viteza medie și distanța totală parcursă si target
  }
  delay(500); // reduce viteza de executare pentru a stabiliza afisajul 500
}

Rezultate

Fara a apasa pe niciun buton, pe ecran imi sunt afisate viteza , viteza medie, distanta parcursa si distanta setata de target.

Butonul 1 imi seteaza dimensiunea rotii, am ledul aprins deoarece distanta target se reseteaza la 0 si eu parcursesem deja o distanta > 0. Dimensiunea rotii poate fi de 26, 27.5 sau 29 inch.

Butonul 2 imi va incrementa distanta target la fiecare apasare.

Butonul 3 imi va afisa temperatura, presiunea si punctul cardinal spre care ma indrept.

Butonul 4 imi va afisa datele giroscopice si magnetometrice.

Videoclip in care se poate urmari functionalitatea proiectului. https://www.youtube.com/watch?v=jS80wkaHZCw

Concluzie

La partea de hardware, ce a fost mai provocator a fost realizarea lipiturilor cu letconul pentru ca a necesitat multa rabdare, partea de conectat si facut cablajul mi-a placut, a fost ca un construit de Lego. Partea fizica cea mai dificila a fost realizarea carcasei, dar totodata am si vrut sa arate ca un produs final finisat si sunt destul de mandra de cum arata.

In ceea ce priveste partea de cod, m-am documentat din mai multe surse pe care le voi referi mai jos despre conceptele folosite (exemplu: interactiunea cu MPU9250, cum functioneaza si cum e construit senzorul). Ceea ce mi-a placut cel mai mult e ca chiar am observat o legatura logica si clara intre partea fizica si cea de software. Am avut o dificultate si m-am straduit mult sa rezolv partea ce priveste afisarea punctului cardinal (desi magnetometrul este calibrat si valorile ajustate, arata niste valori destul de dispersate), planuiesc sa lucrez in continuare la topicul acesta.

In final, produsul final arata bine, are functionalitate practica reala si mi-a placut sa lucrez la creearea lui.

Jurnal

Bibliografie si resurse

pm/prj2024/amocanu/ioana_octavia.tudor.txt · Last modified: 2024/05/27 14:04 by ioana_octavia.tudor
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0