Table of Contents

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:

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

Hardware Design

Lista Piese:
Schema circuitului:

Descrierea legaturilor:

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

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.

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

Buton 1 → seteaza dimensiunea rotii

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

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

Button 4 → trigger sa afisez datele giroscopice si accelerometrice

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

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:

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().

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:

   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

https://www.optimusdigital.ro/ro/senzori-senzori-inertiali/1671-modul-10dof-mpu9250-i-bmp280-accelerometru-giroscop-magnetometru-i-barometru-digital-gy.html?search_query=mpu&results=435

https://www.epitran.it/ebayDrive/datasheet/20.pdf

https://www.youtube.com/watch?v=250Lzc0WHIg&list=PLIcgpRPBfC5U9chF8or-npb-BaCgIstxk&index=3

https://www.youtube.com/watch?v=wazPfdGBeZA

https://www.youtube.com/watch?v=mzwovYcozvI

https://forum.arduino.cc/t/solved-issue-mpu9250-sensor-cant-get-correct-data-from-magnetometer/644845/7