Materiały do zajęć z cyklu
"Inżynier przyszłości" pt.:
Mechatronika - jak samodzielnie wykonać drona?
Prowadzący: dr inż. Adam Mańka
Politechnika Śląska
Wydział Transportu i Inżynierii Lotniczej
Przykład 1 – Odczyt z GPS (GNSS).
W przykładzie wykorzystujemy: Arduino UNO + GPS NEO-6M / GY-GPS6MV2
Podłączenie:
Arduino UNO -> GPS NEO-6M, GND->GND, VCC->VCC, D03->TX
DO4->RX – nie podłączamy z uwagi na brak konieczności wysyłania sygnału z Arduino do GPS (dzielnik napięcia)
A. GPS – pobieranie surowych danych w formacie NMEA – bez dodatkowej biblioteki.
//GPS NEO-6M
//Surowe dane w formacie NMEA
//Adam Mańka (adam.manka(at)polsl.pl)
#include
SoftwareSerial gpsSerial(3, 4); // RX, TX
void setup()
{
Serial.begin(9600);
gpsSerial.begin(9600);
Serial.println("Test GPS NEO-6M");
Serial.println("Czekam na dane NMEA...");
}
void loop()
{
while (gpsSerial.available())
{
char c = gpsSerial.read();
Serial.write(c);
}
}
B. GPS – dane po analizie z biblioteką TinyGPSPlus
Uwaga: w tym ćwiczeniu potrzebny jest sam odczyt z GPS więc wystarczy podłączyć jego TX (transmit) do pinu np. 3. Pin RX można zostawić nie podpięty.
//GPS - NEO-6M
//Dane po analizie
//Adam Mańka (adam.manka(at)polsl.pl)
#include
#include
SoftwareSerial gpsSerial(3, 4); // RX, TX
TinyGPSPlus gps;
void setup()
{
Serial.begin(9600);
gpsSerial.begin(9600);
Serial.println("GPS NEO-6M + Arduino Uno");
}
void loop()
{
while (gpsSerial.available())
{
gps.encode(gpsSerial.read());
}
if (gps.location.isUpdated())
{
Serial.print("Lat: ");
Serial.println(gps.location.lat(), 6);
Serial.print("Lon: ");
Serial.println(gps.location.lng(), 6);
Serial.print("Satelity: ");
Serial.println(gps.satellites.value());
Serial.print("Predkosc km/h: ");
Serial.println(gps.speed.kmph());
Serial.println();
}
}
C. GPS – znacznie więcej danych z biblioteką TinyGPSPlus
Uwaga: w tym ćwiczeniu potrzebny jest sam odczyt z GPS więc wystarczy podłączyć jego TX (transmit) do pinu np. 3. Pin RX można zostawić nie podpięty.
//GPS NEO-6M
//Dane po analizie
//Adam Mańka (adam.manka(at)polsl.pl
#include // UART programowy dla GPS
#include // biblioteka do dekodowania NMEA
SoftwareSerial gpsSerial(3, 4); // RX, TX: GPS TX -> D3, GPS RX -> D4 opcjonalnie
TinyGPSPlus gps; // obiekt GPS
const int UTC_OFFSET = 2; // korekta czasu: Polska latem UTC+2
unsigned long lastPrint = 0; // czas ostatniego wydruku
void p2(int v){ if(v < 10) Serial.print('0'); Serial.print(v); } // drukuje 01, 02, 03...
void drukujCzas(){ // data i czas GPS z prosta korekta UTC+2, bez korekty daty
if(!gps.date.isValid() || !gps.time.isValid()){ Serial.print("-"); return; }
int h = gps.time.hour() + UTC_OFFSET; if(h >= 24) h -= 24;
p2(gps.date.day()); Serial.print("."); p2(gps.date.month()); Serial.print("."); Serial.print(gps.date.year()); Serial.print(" ");
p2(h); Serial.print(":"); p2(gps.time.minute()); Serial.print(":"); p2(gps.time.second()); Serial.print(" UTC+2");
}
void setup(){
Serial.begin(9600); // Monitor portu: 9600
gpsSerial.begin(9600); // GPS NEO-6M: zwykle 9600
Serial.println("Politechnika Śląska \nTester modułu GPS NEO-6M \nMechatronika - Adam Mańka (adam.manka@polsl.pl)");
}
void loop(){
while(gpsSerial.available()) gps.encode(gpsSerial.read()); // odczyt znakow NMEA z GPS
if(millis() - lastPrint < 1000) return; lastPrint = millis(); // wydruk co 1 s
Serial.println("\n----------------------------------------");
Serial.print("Data/czas GPS z korekta UTC+2: "); drukujCzas(); Serial.println();
Serial.print("HDOP TinyGPS: "); if(gps.hdop.isValid()) Serial.print(gps.hdop.hdop(),2); else Serial.print("-"); Serial.println(" / glowny wskaznik jakosci pozycji poziomej /");
Serial.println("HDOP: <1.0 bardzo dobrze, 1-2 dobrze, 2-5 srednio, >5 slabo, \"-\" brak wiarygodnego fixa");
Serial.print("Satelity: "); if(gps.satellites.isValid()) Serial.print(gps.satellites.value()); else Serial.print("-"); Serial.println(" / liczba satelitow uzytych lub widzianych przez GPS /");
Serial.print("Szerokosc lat: "); if(gps.location.isValid()) Serial.print(gps.location.lat(),6); else Serial.print("-"); Serial.println();
Serial.print("Dlugosc lon: "); if(gps.location.isValid()) Serial.print(gps.location.lng(),6); else Serial.print("-"); Serial.println();
Serial.print("Wysokosc: "); if(gps.altitude.isValid()) Serial.print(gps.altitude.meters(),2); else Serial.print("-"); Serial.println(" m / wysokosc GPS nad poziomem morza /");
Serial.print("Predkosc: "); if(gps.speed.isValid()) Serial.print(gps.speed.mps(),2); else Serial.print("-"); Serial.println(" m/s");
Serial.print("Kurs ruchu: "); if(gps.course.isValid()) Serial.print(gps.course.deg(),2); else Serial.print("-"); Serial.println(" deg / kierunek ruchu, nie kompas /");
Serial.print("Ramki OK checksum: "); Serial.print(gps.passedChecksum()); Serial.println(" / poprawne ramki NMEA /");
Serial.print("Ramki BAD checksum: "); Serial.print(gps.failedChecksum()); Serial.println(" / uszkodzone lub blednie odebrane ramki NMEA /");
}
Przykład 2 – Odczyt z akcelerometru (IMU).
W przykładzie wykorzystujemy: Arduino UNO + MPU-6050 / GY-521
Podłączenie:
Arduino UNO -> GPS NEO-6M, GND->GND, VCC->VCC, D03->TXDO4->RX – nie podłączamy z uwagi na brak konieczności wysyłania sygnału z Arduino do GPS (dzielnik napięcia)
A. IMU – MPU-6050 – odczyt – oryginalny MPU-6050 (GY-521) – adres 0x68
//Akcelerometr (IMU) MPU - 6050 / GY-521
//Adam Mańka (adam.manka(at)polsl.pl)
// Biblioteka: Adafruit MPU6050
// Połączenie:
// VCC -> 5V
// GND -> GND
// SDA -> A4
// SCL -> A5
// AD0 -> GND albo niepodłączone
// INT -> niepodłączone
#include
#include
#include
#include
Adafruit_MPU6050 mpu;
const float G = 9.80665;
const float RAD_TO_DEG_LOCAL = 57.2957795;
float gyroOffsetX = 0.0;
float gyroOffsetY = 0.0;
float gyroOffsetZ = 0.0;
void calibrateGyro() {
Serial.println("Kalibracja zyroskopu...");
Serial.println("Trzymaj modul nieruchomo.");
const int samples = 200;
float sumX = 0.0;
float sumY = 0.0;
float sumZ = 0.0;
sensors_event_t acc, gyro, temp;
for (int i = 0; i < samples; i++) {
mpu.getEvent(&acc, &gyro, &temp);
// Biblioteka Adafruit zwraca zyroskop w rad/s
sumX += gyro.gyro.x * RAD_TO_DEG_LOCAL;
sumY += gyro.gyro.y * RAD_TO_DEG_LOCAL;
sumZ += gyro.gyro.z * RAD_TO_DEG_LOCAL;
delay(10);
}
gyroOffsetX = sumX / samples;
gyroOffsetY = sumY / samples;
gyroOffsetZ = sumZ / samples;
Serial.println("Kalibracja zakonczona.");
Serial.print("Offset gyro [deg/s]: X=");
Serial.print(gyroOffsetX, 3);
Serial.print(" Y=");
Serial.print(gyroOffsetY, 3);
Serial.print(" Z=");
Serial.println(gyroOffsetZ, 3);
Serial.println();
}
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("Start MPU-6050 / GY-521");
Wire.begin();
Wire.setClock(100000); // bezpieczne I2C dla Arduino Uno R3
if (!mpu.begin(0x68, &Wire)) {
Serial.println("Nie wykryto MPU-6050.");
Serial.println("Sprawdz: VCC, GND, SDA=A4, SCL=A5, AD0=GND.");
while (1) {
delay(100);
}
}
Serial.println("MPU-6050 wykryty.");
// Zakresy pomiarowe dobre na poczatek zajec
mpu.setAccelerometerRange(MPU6050_RANGE_2_G);
mpu.setGyroRange(MPU6050_RANGE_250_DEG);
mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
delay(500);
calibrateGyro();
Serial.println("Odczyt danych:");
Serial.println("ACC w g, GYRO w deg/s, katy w stopniach");
Serial.println();
}
void loop() {
sensors_event_t acc, gyro, temp;
mpu.getEvent(&acc, &gyro, &temp);
// Przyspieszenie z biblioteki jest w m/s^2
float ax_g = acc.acceleration.x / G;
float ay_g = acc.acceleration.y / G;
float az_g = acc.acceleration.z / G;
float accNorm = sqrt(ax_g * ax_g + ay_g * ay_g + az_g * az_g);
// Zyroskop z biblioteki jest w rad/s
float gx_dps = gyro.gyro.x * RAD_TO_DEG_LOCAL - gyroOffsetX;
float gy_dps = gyro.gyro.y * RAD_TO_DEG_LOCAL - gyroOffsetY;
float gz_dps = gyro.gyro.z * RAD_TO_DEG_LOCAL - gyroOffsetZ;
// Proste katy z akcelerometru
// Dzialaja dobrze, gdy modul nie jest silnie przyspieszany dynamicznie
float roll = atan2(ay_g, az_g) * RAD_TO_DEG_LOCAL;
float pitch = atan2(-ax_g, sqrt(ay_g * ay_g + az_g * az_g)) * RAD_TO_DEG_LOCAL;
Serial.print("ACC[g] X=");
Serial.print(ax_g, 3);
Serial.print(" Y=");
Serial.print(ay_g, 3);
Serial.print(" Z=");
Serial.print(az_g, 3);
Serial.print(" | |ACC|=");
Serial.print(accNorm, 3);
Serial.print(" | GYRO[deg/s] X=");
Serial.print(gx_dps, 2);
Serial.print(" Y=");
Serial.print(gy_dps, 2);
Serial.print(" Z=");
Serial.print(gz_dps, 2);
Serial.print(" | Roll=");
Serial.print(roll, 1);
Serial.print(" Pitch=");
Serial.print(pitch, 1);
Serial.print(" | Temp=");
Serial.print(temp.temperature, 1);
Serial.println(" C");
delay(200);
}
B. IMU – MPU-6050 – odczyt (adres 0x68) – do drona
//Akcelerometr (IMU) MPU - 6050 / GY-521
//Adam Mańka (adam.manka(at)polsl.pl)
// Arduino Uno R3 + GY-521 / MPU-6050
// Wersja B - estymacja roll/pitch do zastosowan dronowych
//
// Biblioteka:
// Adafruit MPU6050
// Adafruit Unified Sensor
// Adafruit BusIO
//
// Polaczenie:
// VCC -> 5V
// GND -> GND
// SDA -> A4
// SCL -> A5
// AD0 -> GND
// INT -> niepodlaczone
#include
#include
#include
#include
Adafruit_MPU6050 mpu;
const float G = 9.80665;
const float RAD_TO_DEG_LOCAL = 57.2957795;
// Czestotliwosc pracy petli
const unsigned long LOOP_PERIOD_US = 5000; // 5000 us = 200 Hz
// Filtr komplementarny
// Im wieksze ALPHA, tym mocniej ufamy zyroskopowi.
// Typowe wartosci: 0.96 ... 0.995
const float ALPHA = 0.98;
// Korekcja akcelerometrem tylko gdy modul nie jest silnie przyspieszany dynamicznie
const float ACC_MIN_VALID = 0.75; // g
const float ACC_MAX_VALID = 1.25; // g
// Offsety zyroskopu [deg/s]
float gyroOffsetX = 0.0;
float gyroOffsetY = 0.0;
float gyroOffsetZ = 0.0;
// Estymowane katy [deg]
float roll = 0.0;
float pitch = 0.0;
// Czas
unsigned long lastLoopUs = 0;
// ------------------------------------------------------------
void calibrateGyro() {
Serial.println("Kalibracja zyroskopu - nie ruszaj modulu");
const int samples = 500;
float sumGX = 0.0;
float sumGY = 0.0;
float sumGZ = 0.0;
sensors_event_t acc, gyro, temp;
for (int i = 0; i < samples; i++) {
mpu.getEvent(&acc, &gyro, &temp);
sumGX += gyro.gyro.x * RAD_TO_DEG_LOCAL;
sumGY += gyro.gyro.y * RAD_TO_DEG_LOCAL;
sumGZ += gyro.gyro.z * RAD_TO_DEG_LOCAL;
delay(4); // ok. 250 Hz podczas kalibracji
}
gyroOffsetX = sumGX / samples;
gyroOffsetY = sumGY / samples;
gyroOffsetZ = sumGZ / samples;
Serial.println("Kalibracja zakonczona");
Serial.print("Gyro offset [deg/s]: ");
Serial.print(gyroOffsetX, 3);
Serial.print(", ");
Serial.print(gyroOffsetY, 3);
Serial.print(", ");
Serial.println(gyroOffsetZ, 3);
}
// ------------------------------------------------------------
void initializeAnglesFromAccelerometer() {
sensors_event_t acc, gyro, temp;
mpu.getEvent(&acc, &gyro, &temp);
float ax = acc.acceleration.x / G;
float ay = acc.acceleration.y / G;
float az = acc.acceleration.z / G;
roll = atan2(ay, az) * RAD_TO_DEG_LOCAL;
pitch = atan2(-ax, sqrt(ay * ay + az * az)) * RAD_TO_DEG_LOCAL;
Serial.print("Kat poczatkowy Roll=");
Serial.print(roll, 2);
Serial.print(" Pitch=");
Serial.println(pitch, 2);
}
// ------------------------------------------------------------
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("MPU-6050 / GY-521 - wersja B dronowa");
Wire.begin();
Wire.setClock(400000); // 400 kHz - szybsze I2C, zwykle OK dla krotkich przewodow
if (!mpu.begin(0x68, &Wire)) {
Serial.println("Nie wykryto MPU-6050.");
Serial.println("Sprawdz: VCC, GND, SDA=A4, SCL=A5, AD0=GND.");
while (1) {
delay(100);
}
}
Serial.println("MPU-6050 wykryty.");
// Dla drona lepiej nie ustawiac +/-2g, bo latwo o nasycenie przy manewrach i wibracjach.
mpu.setAccelerometerRange(MPU6050_RANGE_4_G);
// Do prostych testow wystarczy 500 deg/s.
// Przy bardzo dynamicznym dronie mozna dac 1000 albo 2000 deg/s.
mpu.setGyroRange(MPU6050_RANGE_500_DEG);
// Filtr sprzetowy. 21 Hz jest spokojny i odporny na szum.
// Dla szybszej reakcji mozna testowac 44 Hz albo 94 Hz.
mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
delay(500);
calibrateGyro();
initializeAnglesFromAccelerometer();
lastLoopUs = micros();
Serial.println();
Serial.println("t_ms,roll,pitch,gx,gy,gz,accNorm,accValid");
}
// ------------------------------------------------------------
void loop() {
unsigned long nowUs = micros();
if (nowUs - lastLoopUs < LOOP_PERIOD_US) {
return;
}
float dt = (nowUs - lastLoopUs) / 1000000.0;
lastLoopUs = nowUs;
sensors_event_t acc, gyro, temp;
mpu.getEvent(&acc, &gyro, &temp);
// Akcelerometr w g
float ax = acc.acceleration.x / G;
float ay = acc.acceleration.y / G;
float az = acc.acceleration.z / G;
float accNorm = sqrt(ax * ax + ay * ay + az * az);
// Zyroskop w deg/s po kalibracji
float gx = gyro.gyro.x * RAD_TO_DEG_LOCAL - gyroOffsetX;
float gy = gyro.gyro.y * RAD_TO_DEG_LOCAL - gyroOffsetY;
float gz = gyro.gyro.z * RAD_TO_DEG_LOCAL - gyroOffsetZ;
// Kat z samego akcelerometru
float rollAcc = atan2(ay, az) * RAD_TO_DEG_LOCAL;
float pitchAcc = atan2(-ax, sqrt(ay * ay + az * az)) * RAD_TO_DEG_LOCAL;
// Predykcja z zyroskopu
// Uwaga: to uproszczona postac dobra do malych i srednich katow.
roll += gx * dt;
pitch += gy * dt;
// Korekcja akcelerometrem tylko wtedy, gdy |ACC| jest blisko 1 g
bool accValid = (accNorm > ACC_MIN_VALID && accNorm < ACC_MAX_VALID);
if (accValid) {
roll = ALPHA * roll + (1.0 - ALPHA) * rollAcc;
pitch = ALPHA * pitch + (1.0 - ALPHA) * pitchAcc;
}
// Wyjscie CSV do Serial Plotter / logowania / dalszej analizy
Serial.print(millis());
Serial.print(",");
Serial.print(roll, 2);
Serial.print(",");
Serial.print(pitch, 2);
Serial.print(",");
Serial.print(gx, 2);
Serial.print(",");
Serial.print(gy, 2);
Serial.print(",");
Serial.print(gz, 2);
Serial.print(",");
Serial.print(accNorm, 3);
Serial.print(",");
Serial.println(accValid ? 1 : 0);
}
B. IMU – MPU-6050 – odczyt – UWAGA: klon MPU-6050 – adres 0x72
//Akcelerometr (IMU) MPU - 6050 / KLON!
//Adam Mańka (adam.manka(at)polsl.pl)
// Arduino Uno R3 + MPU6050-kompatybilny modul / klon
// Wariant dla modulu z WHO_AM_I = 0x72
//
// Biblioteka:
// MPU6050 by Electronic Cats
//
// Polaczenie:
// VCC -> 5V
// GND -> GND
// SDA -> A4
// SCL -> A5
// AD0 -> GND
// INT -> niepodlaczone
#include
#include
#include
MPU6050 mpu;
const float ACC_SCALE_2G = 16384.0; // LSB/g dla zakresu +/-2g
const float GYRO_SCALE_250 = 131.0; // LSB/(deg/s) dla zakresu +/-250 deg/s
const float RAD_TO_DEG_LOCAL = 57.2957795;
float gyroOffsetX = 0.0;
float gyroOffsetY = 0.0;
float gyroOffsetZ = 0.0;
byte readReg(byte reg) {
Wire.beginTransmission(0x68);
Wire.write(reg);
Wire.endTransmission(false);
Wire.requestFrom(0x68, 1);
if (Wire.available()) {
return Wire.read();
}
return 0xFF;
}
void calibrateGyro() {
Serial.println("Kalibracja zyroskopu...");
Serial.println("Trzymaj modul nieruchomo.");
const int samples = 300;
long sumGX = 0;
long sumGY = 0;
long sumGZ = 0;
int16_t axRaw, ayRaw, azRaw;
int16_t gxRaw, gyRaw, gzRaw;
for (int i = 0; i < samples; i++) {
mpu.getMotion6(&axRaw, &ayRaw, &azRaw, &gxRaw, &gyRaw, &gzRaw);
sumGX += gxRaw;
sumGY += gyRaw;
sumGZ += gzRaw;
delay(5);
}
gyroOffsetX = (sumGX / (float)samples) / GYRO_SCALE_250;
gyroOffsetY = (sumGY / (float)samples) / GYRO_SCALE_250;
gyroOffsetZ = (sumGZ / (float)samples) / GYRO_SCALE_250;
Serial.println("Kalibracja zakonczona.");
Serial.print("Offset gyro [deg/s]: X=");
Serial.print(gyroOffsetX, 3);
Serial.print(" Y=");
Serial.print(gyroOffsetY, 3);
Serial.print(" Z=");
Serial.println(gyroOffsetZ, 3);
Serial.println();
}
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("MPU6050-kompatybilny modul - wariant WHO_AM_I = 0x72");
Wire.begin();
Wire.setClock(100000); // bezpiecznie dla Arduino Uno R3
byte who = readReg(0x75);
Serial.print("WHO_AM_I = 0x");
if (who < 16) Serial.print("0");
Serial.println(who, HEX);
mpu.initialize();
Serial.print("testConnection(): ");
if (mpu.testConnection()) {
Serial.println("OK");
} else {
Serial.println("FAIL - nietypowy identyfikator, ale probujemy dalej");
}
// Konfiguracja jak dla MPU-6050
mpu.setFullScaleAccelRange(MPU6050_ACCEL_FS_2);
mpu.setFullScaleGyroRange(MPU6050_GYRO_FS_250);
mpu.setDLPFMode(MPU6050_DLPF_BW_20);
delay(300);
calibrateGyro();
Serial.println("Odczyt danych:");
Serial.println("ACC[g], |ACC|, GYRO[deg/s], Roll, Pitch, Temp");
Serial.println();
}
void loop() {
int16_t axRaw, ayRaw, azRaw;
int16_t gxRaw, gyRaw, gzRaw;
mpu.getMotion6(&axRaw, &ayRaw, &azRaw, &gxRaw, &gyRaw, &gzRaw);
int16_t tempRaw = mpu.getTemperature();
float ax = axRaw / ACC_SCALE_2G;
float ay = ayRaw / ACC_SCALE_2G;
float az = azRaw / ACC_SCALE_2G;
float gx = gxRaw / GYRO_SCALE_250 - gyroOffsetX;
float gy = gyRaw / GYRO_SCALE_250 - gyroOffsetY;
float gz = gzRaw / GYRO_SCALE_250 - gyroOffsetZ;
float accNorm = sqrt(ax * ax + ay * ay + az * az);
float roll = atan2(ay, az) * RAD_TO_DEG_LOCAL;
float pitch = atan2(-ax, sqrt(ay * ay + az * az)) * RAD_TO_DEG_LOCAL;
// Uwaga: dla tego klona temperatura moze miec inny offset.
// Traktujemy ja raczej jako wartosc kontrolna stabilnosci, nie jako dokladna temperature otoczenia.
float tempC = tempRaw / 340.0 + 36.53;
Serial.print("ACC[g] X=");
Serial.print(ax, 3);
Serial.print(" Y=");
Serial.print(ay, 3);
Serial.print(" Z=");
Serial.print(az, 3);
Serial.print(" | |ACC|=");
Serial.print(accNorm, 3);
Serial.print(" | GYRO[deg/s] X=");
Serial.print(gx, 2);
Serial.print(" Y=");
Serial.print(gy, 2);
Serial.print(" Z=");
Serial.print(gz, 2);
Serial.print(" | Roll=");
Serial.print(roll, 1);
Serial.print(" Pitch=");
Serial.print(pitch, 1);
Serial.print(" | Temp=");
Serial.print(tempC, 1);
Serial.print(" | WHO=0x");
byte who = readReg(0x75);
if (who < 16) Serial.print("0");
Serial.println(who, HEX);
delay(200);
}
Przykład 3 – Odczyt z kompasu (magnetometru)
W przykładzie wykorzystujemy: Arduino Kompas – GY-271 / HW-246
Podłączenie:
Arduino UNO -> GY-271,
5V ->VCC, GND->GY-271 GND, A4 (SDA)-> GY-271 SDA, A5 (SCL)->SCL
DRDY -> nie podłączać
A. KOMPAS (magnetometr) – GY-271 / HW-246
//KOMPAS GY-271 / HW-246
//Adam Mańka (adam.manka(at)polsl.pl)
// AM - GY-271 / HW-246, adres I2C 0x2C
// Program edukacyjny: RAW + gaussy + wstepna kalibracja + azymut
#include // I2C
#include // biblioteka dla QMC5883P / HP5883
#include // atan2()
Adafruit_QMC5883P mag; // obiekt magnetometru
const byte MAG_ADDR = 0x2C; // adres I2C wykryty skanerem
// Wstepne offsety z pierwszego obrotu modulu
const float OFFSET_X = -1543.0; // srodek zakresu X
const float OFFSET_Y = -78.0; // srodek zakresu Y
const float OFFSET_Z = 2460.0; // srodek zakresu Z, tylko informacyjnie
void setup()
{
Serial.begin(9600); // port szeregowy 9600 baud
delay(1000); // pauza na start monitora
Wire.begin(); // I2C dla Arduino Uno / Uno R4
Serial.println("AM - kompas GY-271 / HW-246, wersja edukacyjna");
Serial.println("RAW = dane surowe, Gs = gaussy, CAL = dane po odjeciu offsetu");
Serial.println("Azymut liczony jest z CAL X i CAL Y.");
Serial.println("Uwaga: kalibracja jest tylko wstepna, modul powinien lezec poziomo.");
Serial.println();
if (!mag.begin(MAG_ADDR)) // sprawdzenie, czy czujnik odpowiada
{
Serial.println("BLAD: nie znaleziono magnetometru pod adresem 0x2C.");
while (1);
}
mag.setMode(QMC5883P_MODE_CONTINUOUS); // pomiar ciagly
mag.setODR(QMC5883P_ODR_50HZ); // 50 pomiarow/s
mag.setRange(QMC5883P_RANGE_8G); // zakres +/- 8 gauss
Serial.println("Czujnik wykryty. Obracaj modul powoli na stole.");
Serial.println();
}
void loop()
{
int16_t xRaw, yRaw, zRaw; // surowe dane X/Y/Z
float xG, yG, zG; // dane w gaussach
if (mag.isDataReady()) // czy jest nowy pomiar
{
mag.getRawMagnetic(&xRaw, &yRaw, &zRaw); // odczyt RAW
mag.getGaussField(&xG, &yG, &zG); // odczyt w gaussach
float xCal = xRaw - OFFSET_X; // X po odjeciu offsetu
float yCal = yRaw - OFFSET_Y; // Y po odjeciu offsetu
float zCal = zRaw - OFFSET_Z; // Z po odjeciu offsetu, tylko pokazowo
float azymut = atan2(yCal, xCal) * 180.0 / PI; // kat z CAL Y i CAL X
if (azymut < 0) azymut += 360.0; // zakres 0...360 stopni
Serial.print("RAW X="); Serial.print(xRaw); // surowa os X
Serial.print(" Y="); Serial.print(yRaw); // surowa os Y
Serial.print(" Z="); Serial.print(zRaw); // surowa os Z
Serial.print(" | Gs X="); Serial.print(xG, 3); // pole X w gaussach
Serial.print(" Y="); Serial.print(yG, 3); // pole Y w gaussach
Serial.print(" Z="); Serial.print(zG, 3); // pole Z w gaussach
Serial.print(" | CAL X="); Serial.print(xCal, 0); // X po korekcji
Serial.print(" Y="); Serial.print(yCal, 0); // Y po korekcji
Serial.print(" Z="); Serial.print(zCal, 0); // Z po korekcji
Serial.print(" | Azymut="); Serial.print(azymut, 1); // kierunek z danych CAL
Serial.println(" deg");
}
delay(500); // wolniejszy wydruk dla 9600 baud
}
B. KOMPAS (magnetometr) – GY-271 / HW-246 – Wersja z kalibracją
//KOMPAS GY-271 / HW-246
//Adam Mańka (adam.manka(at)polsl.pl)
// AM - GY-271 / HW-246, adres I2C 0x2C
// Program nr 2: magnetometr z automatyczna kalibracja po starcie
// Czujnik: QMC5883P / HP5883, biblioteka: Adafruit_QMC5883P
#include // I2C
#include // biblioteka dla QMC5883P / HP5883
#include // atan2()
Adafruit_QMC5883P mag; // obiekt magnetometru
const byte MAG_ADDR = 0x2C; // adres I2C wykryty skanerem
const unsigned long CAL_TIME_MS = 30000; // czas kalibracji: 30 sekund
int16_t minX, maxX; // minimalna i maksymalna wartosc X
int16_t minY, maxY; // minimalna i maksymalna wartosc Y
int16_t minZ, maxZ; // minimalna i maksymalna wartosc Z
float offsetX = 0.0; // przesuniecie srodka osi X
float offsetY = 0.0; // przesuniecie srodka osi Y
float offsetZ = 0.0; // przesuniecie srodka osi Z
float scaleX = 1.0; // wspolczynnik skali osi X
float scaleY = 1.0; // wspolczynnik skali osi Y
float scaleZ = 1.0; // wspolczynnik skali osi Z
void setup()
{
Serial.begin(9600); // port szeregowy 9600 baud
delay(1000); // pauza na start monitora portu
Wire.begin(); // I2C dla Arduino Uno / Uno R4
Serial.println();
Serial.println("AM - kompas GY-271 / HW-246, wersja z kalibracja");
Serial.println("RAW = dane surowe, Gs = gaussy, CAL = dane po kalibracji");
Serial.println("Azymut liczony jest z CAL X i CAL Y.");
Serial.println();
if (!mag.begin(MAG_ADDR)) // sprawdzenie, czy czujnik odpowiada
{
Serial.println("BLAD: nie znaleziono magnetometru pod adresem 0x2C.");
while (1);
}
mag.setMode(QMC5883P_MODE_CONTINUOUS); // pomiar ciagly
mag.setODR(QMC5883P_ODR_50HZ); // 50 pomiarow/s
mag.setOSR(QMC5883P_OSR_4); // nadprobkowanie
mag.setDSR(QMC5883P_DSR_2); // dodatkowe usrednianie
mag.setRange(QMC5883P_RANGE_8G); // zakres +/- 8 gauss
mag.setSetResetMode(QMC5883P_SETRESET_ON); // stabilizacja pomiaru
Serial.println("Czujnik wykryty.");
Serial.println("Za chwile rozpocznie sie kalibracja.");
Serial.println();
calibrateMagnetometer(); // automatyczna kalibracja po starcie
Serial.println();
Serial.println("Praca normalna.");
Serial.println("Poloz modul poziomo. Obracaj go i obserwuj Azymut.");
Serial.println();
}
void loop()
{
int16_t xRaw, yRaw, zRaw; // surowe dane X/Y/Z
float xG, yG, zG; // dane w gaussach
if (mag.isDataReady()) // czy jest nowy pomiar
{
bool okRaw = mag.getRawMagnetic(&xRaw, &yRaw, &zRaw); // odczyt RAW
bool okG = mag.getGaussField(&xG, &yG, &zG); // odczyt w gaussach
if (okRaw && okG)
{
float xCal = (xRaw - offsetX) * scaleX; // X po odjeciu offsetu i korekcji skali
float yCal = (yRaw - offsetY) * scaleY; // Y po odjeciu offsetu i korekcji skali
float zCal = (zRaw - offsetZ) * scaleZ; // Z po kalibracji, pokazowo
float azymut = atan2(yCal, xCal) * 180.0 / PI; // kat z osi X/Y po kalibracji
if (azymut < 0) azymut += 360.0; // zakres 0...360 stopni
Serial.print("RAW X="); Serial.print(xRaw); // surowa os X
Serial.print(" Y="); Serial.print(yRaw); // surowa os Y
Serial.print(" Z="); Serial.print(zRaw); // surowa os Z
Serial.print(" | Gs X="); Serial.print(xG, 3); // pole X w gaussach
Serial.print(" Y="); Serial.print(yG, 3); // pole Y w gaussach
Serial.print(" Z="); Serial.print(zG, 3); // pole Z w gaussach
Serial.print(" | CAL X="); Serial.print(xCal, 0); // X po kalibracji
Serial.print(" Y="); Serial.print(yCal, 0); // Y po kalibracji
Serial.print(" Z="); Serial.print(zCal, 0); // Z po kalibracji
Serial.print(" | Azymut="); Serial.print(azymut, 1); // kierunek z CAL X/Y
Serial.println(" deg | po kalibracji, bez kompensacji przechylu");
}
}
delay(700); // wolniejszy wydruk dla 9600 baud
}
void calibrateMagnetometer()
{
minX = 32767; // startowa wartosc minimum X
minY = 32767; // startowa wartosc minimum Y
minZ = 32767; // startowa wartosc minimum Z
maxX = -32768; // startowa wartosc maksimum X
maxY = -32768; // startowa wartosc maksimum Y
maxZ = -32768; // startowa wartosc maksimum Z
Serial.println("KALIBRACJA 30 s");
Serial.println("Obracaj modul powoli poziomo na stole.");
Serial.println("Najlepiej wykonaj pelny obrot 360 stopni kilka razy.");
Serial.println();
unsigned long startTime = millis(); // czas startu kalibracji
unsigned long lastPrint = 0; // czas ostatniego komunikatu
while (millis() - startTime < CAL_TIME_MS)
{
int16_t xRaw, yRaw, zRaw;
if (mag.isDataReady())
{
if (mag.getRawMagnetic(&xRaw, &yRaw, &zRaw))
{
if (xRaw < minX) minX = xRaw; // aktualizacja minimum X
if (xRaw > maxX) maxX = xRaw; // aktualizacja maksimum X
if (yRaw < minY) minY = yRaw; // aktualizacja minimum Y
if (yRaw > maxY) maxY = yRaw; // aktualizacja maksimum Y
if (zRaw < minZ) minZ = zRaw; // aktualizacja minimum Z
if (zRaw > maxZ) maxZ = zRaw; // aktualizacja maksimum Z
}
}
if (millis() - lastPrint > 1000)
{
lastPrint = millis();
int secondsLeft = (CAL_TIME_MS - (millis() - startTime)) / 1000;
Serial.print("Kalibracja, zostalo ");
Serial.print(secondsLeft);
Serial.print(" s | X=");
Serial.print(minX); Serial.print(".."); Serial.print(maxX);
Serial.print(" Y=");
Serial.print(minY); Serial.print(".."); Serial.print(maxY);
Serial.print(" Z=");
Serial.print(minZ); Serial.print(".."); Serial.print(maxZ);
Serial.println();
}
delay(20);
}
offsetX = (maxX + minX) / 2.0; // srodek zakresu X
offsetY = (maxY + minY) / 2.0; // srodek zakresu Y
offsetZ = (maxZ + minZ) / 2.0; // srodek zakresu Z
float radiusX = (maxX - minX) / 2.0; // polowa zakresu X
float radiusY = (maxY - minY) / 2.0; // polowa zakresu Y
float radiusZ = (maxZ - minZ) / 2.0; // polowa zakresu Z
float avgRadiusXY = (radiusX + radiusY) / 2.0; // srednia amplituda X/Y
if (radiusX > 1.0) scaleX = avgRadiusXY / radiusX; else scaleX = 1.0;
if (radiusY > 1.0) scaleY = avgRadiusXY / radiusY; else scaleY = 1.0;
if (radiusZ > 1.0) scaleZ = 1.0; else scaleZ = 1.0; // Z zostawiamy pokazowo
Serial.println();
Serial.println("KONIEC KALIBRACJI");
Serial.print("offsetX="); Serial.print(offsetX, 1);
Serial.print(" offsetY="); Serial.print(offsetY, 1);
Serial.print(" offsetZ="); Serial.println(offsetZ, 1);
Serial.print("scaleX="); Serial.print(scaleX, 3);
Serial.print(" scaleY="); Serial.print(scaleY, 3);
Serial.print(" scaleZ="); Serial.println(scaleZ, 3);
Serial.println();
Serial.println("Te wartosci zostaly policzone automatycznie z min/max.");
}
Przykład 4 – Podłączenie radia RC FS-iA6B (FlySky)
W przykładzie wykorzystujemy: Arduino Radio RC – FS-iA6B (FlySky)
Podłączenie:
Arduino UNO -> odbiornik RC FS-iA6B
GND->iBUS GND (pomiędzy CH4 i CH5)
VCC->iBUS VCC (pomiędzy CH5 i CH6)
D08->iBUS sygnal (pomiędzy CH6 a B/VCC)
//Radio RC FS iA6B - iBUS
//Adam Mańka (adam.manka(at)polsl.pl)
#include
AltSoftSerial ibus;
// Piny LED
const byte LED_PWM = 11; // CH3 -> jasność PWM / alarm
const byte LED_CH5 = 12; // CH5 -> OFF / BLINK / ON / alarm
const byte LED_CH6 = 13; // CH6 -> OFF / ON / alarm
// iBUS
const byte FRAME_SIZE = 32;
const byte CHANNELS = 14;
byte frame[FRAME_SIZE];
byte idx = 0;
uint16_t ch[CHANNELS];
unsigned long ok = 0;
unsigned long err = 0;
unsigned long lastGoodMs = 0;
unsigned long lastPrintMs = 0;
const unsigned long PRINT_MS = 500;
const unsigned long TIMEOUT_MS = 700;
// ------------------------------------------------------------
// Funkcje pomocnicze
// ------------------------------------------------------------
uint16_t rcLimit(uint16_t v)
{
if (v < 1000) return 1000;
if (v > 2000) return 2000;
return v;
}
byte rcToPwm(uint16_t v)
{
v = rcLimit(v);
return map(v, 1000, 2000, 0, 255);
}
byte rc3pos(uint16_t v)
{
if (v < 1300) return 0;
if (v < 1700) return 1;
return 2;
}
bool linkOk()
{
if (ok == 0) return false;
return (millis() - lastGoodMs <= TIMEOUT_MS);
}
// ------------------------------------------------------------
// Dekoder iBUS
// ------------------------------------------------------------
void readByte(byte b)
{
// Początek ramki iBUS: 0x20 0x40
if (idx == 0)
{
if (b == 0x20)
{
frame[0] = b;
idx = 1;
}
return;
}
if (idx == 1)
{
if (b == 0x40)
{
frame[1] = b;
idx = 2;
}
else if (b == 0x20)
{
frame[0] = b;
idx = 1;
}
else
{
idx = 0;
}
return;
}
frame[idx] = b;
idx++;
if (idx < FRAME_SIZE) return;
idx = 0;
// Sprawdzenie sumy kontrolnej
uint16_t sum = 0xFFFF;
for (byte i = 0; i < 30; i++)
{
sum -= frame[i];
}
uint16_t rxSum = frame[30] | ((uint16_t)frame[31] << 8);
if (sum != rxSum)
{
err++;
return;
}
// Odczyt kanałów
for (byte i = 0; i < CHANNELS; i++)
{
ch[i] = frame[2 + i * 2] | ((uint16_t)frame[3 + i * 2] << 8);
}
ok++;
lastGoodMs = millis();
}
// ------------------------------------------------------------
// Odbiór iBUS
// ------------------------------------------------------------
void readIbus()
{
while (ibus.available() > 0)
{
readByte(ibus.read());
}
}
// ------------------------------------------------------------
// Alarm braku ramek iBUS
// ------------------------------------------------------------
void alarmBlink()
{
bool s = (millis() / 250) % 2;
analogWrite(LED_PWM, s ? 255 : 0);
digitalWrite(LED_CH5, s ? HIGH : LOW);
digitalWrite(LED_CH6, s ? HIGH : LOW);
}
// ------------------------------------------------------------
// Sterowanie LED
// ------------------------------------------------------------
void updateLeds()
{
if (!linkOk())
{
alarmBlink();
return;
}
// CH3 -> PWM na D11
analogWrite(LED_PWM, rcToPwm(ch[2]));
// CH5 -> D12: OFF / BLINK / ON
byte p = rc3pos(ch[4]);
if (p == 0)
{
digitalWrite(LED_CH5, LOW);
}
else if (p == 1)
{
digitalWrite(LED_CH5, (millis() / 250) % 2);
}
else
{
digitalWrite(LED_CH5, HIGH);
}
// CH6 -> D13
digitalWrite(LED_CH6, ch[5] > 1500);
}
// ------------------------------------------------------------
// Wydruk diagnostyczny
// ------------------------------------------------------------
void printData()
{
if (!linkOk())
{
Serial.print(F("BRAK iBUS / BRAK RAMEK | OK="));
Serial.print(ok);
Serial.print(F(" ERR="));
Serial.println(err);
return;
}
Serial.print(F("CH1=")); Serial.print(ch[0]);
Serial.print(F(" CH2=")); Serial.print(ch[1]);
Serial.print(F(" CH3=")); Serial.print(ch[2]);
Serial.print(F(" CH4=")); Serial.print(ch[3]);
Serial.print(F(" CH5=")); Serial.print(ch[4]);
Serial.print(F(" CH6=")); Serial.print(ch[5]);
Serial.print(F(" | PWM11="));
Serial.print(rcToPwm(ch[2]));
Serial.print(F(" | OK="));
Serial.print(ok);
Serial.print(F(" ERR="));
Serial.print(err);
Serial.print(F(" AGE="));
Serial.print(millis() - lastGoodMs);
Serial.println(F("ms"));
}
// ------------------------------------------------------------
// SETUP
// ------------------------------------------------------------
void setup()
{
Serial.begin(115200);
ibus.begin(115200);
pinMode(LED_PWM, OUTPUT);
pinMode(LED_CH5, OUTPUT);
pinMode(LED_CH6, OUTPUT);
analogWrite(LED_PWM, 0);
digitalWrite(LED_CH5, LOW);
digitalWrite(LED_CH6, LOW);
for (byte i = 0; i < CHANNELS; i++)
{
ch[i] = 1500;
}
Serial.println(F("FS-iA6B iBUS UNO - alarm braku ramek"));
}
// ------------------------------------------------------------
// LOOP
// ------------------------------------------------------------
void loop()
{
readIbus();
updateLeds();
if (millis() - lastPrintMs >= PRINT_MS)
{
lastPrintMs = millis();
printData();
}
}
Przykład 5 – Podłączenie silników drona BLDC przez ESC (Electronic Speed Controler)
W przykładzie wykorzystujemy: Arduino i ESC 40A – oraz zasilacz laboratoryjny 11V
Podłączenie:
Arduino UNO -> ESC
A. Test silnika drona ESC (bez zadajnika z potencjometru)
//Silnik dronowy BLDC - ESC (bez potencjometru)
//Adam Mańka (adam.manka(at)polsl.pl)
#include
Servo esc;
const int ESC_PIN = 9;
const int THROTTLE_MIN = 1000; // minimum / STOP / uzbrojenie
const int THROTTLE_MAX = 2000; // maksimum ESC
const int TEST_MAX = 1300; // maksimum używane w teście
void setup()
{
Serial.begin(9600);
esc.attach(ESC_PIN);
Serial.println("Test BLDC przez ESC");
Serial.println("1000 us = minimum / STOP");
Serial.println("2000 us = maksimum ESC");
Serial.println("Test ograniczony do 1300 us");
Serial.println("UWAGA: smiglo musi byc zdjete.");
esc.writeMicroseconds(THROTTLE_MIN);
Serial.println("Uzbrajanie ESC: 1000 us przez 3 s");
delay(3000);
}
void loop()
{
for (int us = THROTTLE_MIN; us <= TEST_MAX; us += 10)
{
esc.writeMicroseconds(us);
Serial.println(us);
delay(150);
}
delay(1000);
for (int us = TEST_MAX; us >= THROTTLE_MIN; us -= 10)
{
esc.writeMicroseconds(us);
Serial.println(us);
delay(150);
}
esc.writeMicroseconds(THROTTLE_MIN);
delay(2000);
}
B. Test silnika drona ESC (z zadajnikiem z potencjometru)
//Silnik dronowy BLDC - ESC (z potencjometrem)
//Adam Mańka (adam.manka(at)polsl.pl)
#include
Servo esc;
const int ESC_PIN = 9;
const int POT_PIN = A0;
const int THROTTLE_MIN = 1000; // minimum / STOP
const int THROTTLE_ESC_MAX = 2000; // maksimum ESC
const int THROTTLE_TEST_MAX = 1300; // maksimum w tym teście
unsigned long lastPrint = 0;
void setup()
{
Serial.begin(9600);
esc.attach(ESC_PIN);
Serial.println("Kod B - ESC sterowany potencjometrem");
Serial.println("1000 us = minimum / STOP");
Serial.println("2000 us = maksimum ESC");
Serial.println("Test ograniczony do 1300 us");
Serial.println("Przed zasileniem ESC ustaw potencjometr na minimum.");
Serial.println("UWAGA: smiglo musi byc zdjete.");
}
void loop()
{
int pot = analogRead(POT_PIN); // 0...1023
int throttle = map(pot, 0, 1023, THROTTLE_MIN, THROTTLE_TEST_MAX);
throttle = constrain(throttle, THROTTLE_MIN, THROTTLE_TEST_MAX);
// mała strefa bezpieczeństwa przy samym dole potencjometru
if (pot < 20)
{
throttle = THROTTLE_MIN;
}
esc.writeMicroseconds(throttle);
if (millis() - lastPrint > 300)
{
lastPrint = millis();
Serial.print("POT=");
Serial.print(pot);
Serial.print(" Gaz=");
Serial.print(throttle);
Serial.println(" us");
}
}
Przykład 6 – Wyświetlacz 4×16 (oscyloskop)
Podłączenie:
Arduino UNO -> wyświetlacz 20×4 przez I2C tj. zasilanie oraz A4 i A5
Pomiar napięcia – A0 i GND
// Adam Manka
// LCD I2C 20x4 + mini oscyloskop A0
// Logo Politechniki Slaskiej, potem oscyloskop
#include
#include
LiquidCrystal_I2C lcd(0x27, 20, 4);
const byte AIN = A0;
const int VREF = 5000; // mV
const unsigned long DT = 120; // szybkosc wykresu [ms]
int y[20];
unsigned long t0 = 0;
// Ś
byte ch_S[8] = {
B00010, B01110, B10000, B10000,
B01110, B00001, B00001, B11110
};
// ą
byte ch_a[8] = {
B00000, B00000, B01110, B00001,
B01111, B10001, B01111, B00010
};
// ż
byte ch_z[8] = {
B00100, B00000, B11111, B00010,
B00100, B01000, B11111, B00000
};
// ł
byte ch_l[8] = {
B01100, B00100, B00100, B01100,
B00100, B00100, B01110, B00000
};
// gorna linia
byte ch_top[8] = {
B11111, B00000, B00000, B00000,
B00000, B00000, B00000, B00000
};
void logo()
{
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Politechnika ");
lcd.write(byte(0)); // Ś
lcd.print("l");
lcd.write(byte(1)); // ą
lcd.print("ska");
lcd.setCursor(0, 1);
lcd.print("Wydzia");
lcd.write(byte(3)); // ł
lcd.print(" Transportu");
lcd.setCursor(0, 2);
lcd.print("i In");
lcd.write(byte(2)); // ż
lcd.print("ynierii");
lcd.setCursor(0, 3);
lcd.print("Lotniczej");
delay(5000);
lcd.clear();
}
int adcNaMv(int adc)
{
return (long)adc * VREF / 1023;
}
void pokazNapiecie(int adc)
{
int mv = adcNaMv(adc);
int setne = (mv % 1000) / 10;
lcd.setCursor(0, 0);
lcd.print("U=");
lcd.print(mv / 1000);
lcd.print(".");
if (setne < 10) lcd.print("0");
lcd.print(setne);
lcd.print("V ");
}
void wyslijSerial(int adc)
{
int mv = adcNaMv(adc);
int setne = (mv % 1000) / 10;
Serial.print(mv / 1000);
Serial.print(".");
if (setne < 10) Serial.print("0");
Serial.println(setne);
}
void znakWykresu(byte w)
{
if (w == 0) lcd.write(byte(4)); // wysoka linia
else if (w == 3) lcd.print("_"); // dolna linia
else lcd.print("-"); // linia srodkowa
}
void rysujWiersz(byte w, byte odKolumny)
{
lcd.setCursor(odKolumny, w);
for (byte x = odKolumny; x < 20; x++)
{
if (y[x] == w) znakWykresu(w);
else lcd.print(" ");
}
}
void rysujWykres()
{
// Pierwszy wiersz: od kolumny 9, żeby nie kasować U=2.50V
rysujWiersz(0, 9);
// Pozostale wiersze: cala szerokosc
rysujWiersz(1, 0);
rysujWiersz(2, 0);
rysujWiersz(3, 0);
}
void setup()
{
Serial.begin(9600);
lcd.init();
lcd.backlight();
lcd.createChar(0, ch_S);
lcd.createChar(1, ch_a);
lcd.createChar(2, ch_z);
lcd.createChar(3, ch_l);
lcd.createChar(4, ch_top);
for (byte i = 0; i < 20; i++) y[i] = 3;
logo();
}
void loop()
{
if (millis() - t0 >= DT)
{
t0 = millis();
int adc = analogRead(AIN);
int nowyY = map(adc, 0, 1023, 3, 0);
for (byte i = 0; i < 19; i++) y[i] = y[i + 1];
y[19] = nowyY;
rysujWykres();
pokazNapiecie(adc);
wyslijSerial(adc);
}
}
Przykład 7 – PID – huśtawka aerodynamiczna (2 silniki na osi)
Podłączenie:
Arduino UNO -> czujnik IMU -> L298N
// Adam Mańka (adam.manka(at)polsl.pl
// Rownowaznia aerodynamiczna - PID
// Arduino Uno + L298N + MPU6050
// Wersja dydaktyczna uproszczona
//
// Biblioteka: MPU6050 by Electronic Cats
//
// Komendy Serial Monitor 115200, Newline:
// a - uzbroj silniki
// s - stop
// p4.0 - Kp
// i0.01 - Ki
// d1.0 - Kd
// b100 - bazowy PWM
// m60 - max korekta PID
// sp0 - kat zadany
// sg1 - normalny znak sterowania
// sg-1 - odwroc znak sterowania
// z - wyzeruj aktualny kat
// ? - pokaz komendy
#include
#include
#include
MPU6050 mpu;
// ===== Piny L298N =====
const byte ENA = 5; // PWM - lewy silnik
const byte IN1 = 7;
const byte IN2 = 8;
const byte ENB = 6; // PWM - prawy silnik
const byte IN3 = 9;
const byte IN4 = 10;
// ===== Skale MPU6050 =====
const float ACC_SCALE = 16384.0; // +/-2g
const float GYRO_SCALE = 131.0; // +/-250 deg/s
const float RAD_TO_DEG_LOCAL = 57.2957795;
// ===== PID =====
float Kp = 3.0;
float Ki = 0.0;
float Kd = 0.8;
float setAngle = 0.0; // kat zadany [deg]
float basePWM = 90.0; // podstawowa predkosc silnikow
float maxCorr = 50.0; // maksymalna korekta PID
int controlSign = 1; // jesli ucieka, wpisz sg-1
// ===== Pomiar kata =====
float angle = 0.0;
float lastAngle = 0.0;
float zeroRoll = 0.0;
float gyroOffsetX = 0.0;
const float alpha = 0.96; // filtr komplementarny
const int LOOP_MS = 10; // petla 100 Hz
// ===== Zmienne PID =====
float integral = 0.0;
float pidOut = 0.0;
bool armed = false;
unsigned long lastLoopMs = 0;
unsigned long lastMicros = 0;
unsigned long lastPrintMs = 0;
// ===== Komendy z Serial Monitor =====
char cmd[20];
byte cmdIndex = 0;
// ------------------------------------------------------------
void printCommands()
{
Serial.println();
Serial.println("=== KOMENDY ===");
Serial.println("a - uzbroj silniki");
Serial.println("s - stop");
Serial.println("p4.0 - ustaw Kp");
Serial.println("i0.01 - ustaw Ki");
Serial.println("d1.0 - ustaw Kd");
Serial.println("b100 - bazowy PWM silnikow");
Serial.println("m60 - maksymalna korekta PID");
Serial.println("sp0 - kat zadany");
Serial.println("sg1 - normalny znak sterowania");
Serial.println("sg-1 - odwroc znak sterowania");
Serial.println("z - aktualny kat jako zero");
Serial.println("? - pokaz komendy");
Serial.println();
}
// ------------------------------------------------------------
byte readReg(byte reg)
{
Wire.beginTransmission(0x68);
Wire.write(reg);
Wire.endTransmission(false);
Wire.requestFrom(0x68, 1);
if (Wire.available()) return Wire.read();
return 0xFF;
}
// ------------------------------------------------------------
float calcRollDeg(int16_t ayRaw, int16_t azRaw)
{
float ay = ayRaw / ACC_SCALE;
float az = azRaw / ACC_SCALE;
return atan2(ay, az) * RAD_TO_DEG_LOCAL;
}
// ------------------------------------------------------------
void setMotor(byte en, byte inA, byte inB, int pwm)
{
pwm = constrain(pwm, 0, 255);
if (pwm == 0)
{
digitalWrite(inA, LOW);
digitalWrite(inB, LOW);
analogWrite(en, 0);
}
else
{
digitalWrite(inA, HIGH);
digitalWrite(inB, LOW);
analogWrite(en, pwm);
}
}
// ------------------------------------------------------------
void setMotors(int leftPWM, int rightPWM)
{
setMotor(ENA, IN1, IN2, leftPWM);
setMotor(ENB, IN3, IN4, rightPWM);
}
// ------------------------------------------------------------
void stopMotors()
{
setMotors(0, 0);
}
// ------------------------------------------------------------
void countdownBeforeCalibration()
{
Serial.println("Ustaw rownowaznie poziomo.");
Serial.println("Kalibracja rozpocznie sie za:");
for (int s = 5; s > 0; s--)
{
Serial.print(s);
Serial.println(" s");
delay(1000);
}
Serial.println("Start kalibracji...");
}
// ------------------------------------------------------------
void calibrateMPU()
{
const int calibTimeSec = 5;
long sumGX = 0;
float sumRoll = 0.0;
int samples = 0;
int16_t axRaw, ayRaw, azRaw;
int16_t gxRaw, gyRaw, gzRaw;
unsigned long startTime = millis();
unsigned long endTime = startTime + calibTimeSec * 1000UL;
int lastShownSecond = -1;
while (millis() < endTime)
{
mpu.getMotion6(&axRaw, &ayRaw, &azRaw, &gxRaw, &gyRaw, &gzRaw);
sumGX += gxRaw;
sumRoll += calcRollDeg(ayRaw, azRaw);
samples++;
int secondsLeft = (endTime - millis() + 999) / 1000;
if (secondsLeft != lastShownSecond)
{
lastShownSecond = secondsLeft;
Serial.print("Do konca kalibracji: ");
Serial.print(secondsLeft);
Serial.println(" s");
}
delay(5);
}
gyroOffsetX = (sumGX / (float)samples) / GYRO_SCALE;
zeroRoll = sumRoll / (float)samples;
angle = 0.0;
lastAngle = 0.0;
integral = 0.0;
Serial.println("Kalibracja zakonczona.");
Serial.print("Liczba probek: ");
Serial.println(samples);
Serial.print("gyroOffsetX = ");
Serial.println(gyroOffsetX, 4);
Serial.print("zeroRoll = ");
Serial.println(zeroRoll, 2);
Serial.println();
}
// ------------------------------------------------------------
void printSettings()
{
Serial.print("Kp=");
Serial.print(Kp);
Serial.print(" Ki=");
Serial.print(Ki);
Serial.print(" Kd=");
Serial.print(Kd);
Serial.print(" basePWM=");
Serial.print(basePWM);
Serial.print(" maxCorr=");
Serial.print(maxCorr);
Serial.print(" setAngle=");
Serial.print(setAngle);
Serial.print(" sign=");
Serial.println(controlSign);
}
// ------------------------------------------------------------
void parseCommand(char *c)
{
if (strcmp(c, "a") == 0)
{
armed = true;
integral = 0.0;
lastAngle = angle;
Serial.println("UZBROJONO");
}
else if (strcmp(c, "s") == 0)
{
armed = false;
stopMotors();
integral = 0.0;
Serial.println("STOP");
}
else if (strcmp(c, "z") == 0)
{
zeroRoll += angle;
angle = 0.0;
lastAngle = 0.0;
integral = 0.0;
Serial.println("Aktualny kat ustawiony jako zero.");
}
else if (strncmp(c, "sp", 2) == 0)
{
setAngle = atof(c + 2);
}
else if (strncmp(c, "sg", 2) == 0)
{
int v = atoi(c + 2);
controlSign = (v < 0) ? -1 : 1;
}
else if (c[0] == 'p')
{
Kp = atof(c + 1);
}
else if (c[0] == 'i')
{
Ki = atof(c + 1);
}
else if (c[0] == 'd')
{
Kd = atof(c + 1);
}
else if (c[0] == 'b')
{
basePWM = constrain(atof(c + 1), 0, 255);
}
else if (c[0] == 'm')
{
maxCorr = constrain(atof(c + 1), 0, 255);
}
else if (c[0] == '?')
{
printCommands();
}
printSettings();
}
// ------------------------------------------------------------
void handleSerial()
{
while (Serial.available())
{
char ch = Serial.read();
if (ch == '\n' || ch == '\r')
{
if (cmdIndex > 0)
{
cmd[cmdIndex] = '\0';
parseCommand(cmd);
cmdIndex = 0;
}
}
else
{
if (cmdIndex < sizeof(cmd) - 1)
{
cmd[cmdIndex++] = ch;
}
}
}
}
// ------------------------------------------------------------
void setup()
{
pinMode(ENA, OUTPUT);
pinMode(IN1, OUTPUT);
pinMode(IN2, OUTPUT);
pinMode(ENB, OUTPUT);
pinMode(IN3, OUTPUT);
pinMode(IN4, OUTPUT);
stopMotors();
Serial.begin(115200);
delay(1000);
Serial.println("Rownowaznia aerodynamiczna - PID");
Serial.println("Arduino Uno + L298N + MPU6050");
Wire.begin();
Wire.setClock(100000);
byte who = readReg(0x75);
Serial.print("WHO_AM_I = 0x");
if (who < 16) Serial.print("0");
Serial.println(who, HEX);
mpu.initialize();
Serial.print("testConnection(): ");
if (mpu.testConnection())
{
Serial.println("OK");
}
else
{
Serial.println("FAIL - ale probujemy dalej");
}
mpu.setFullScaleAccelRange(MPU6050_ACCEL_FS_2);
mpu.setFullScaleGyroRange(MPU6050_GYRO_FS_250);
mpu.setDLPFMode(MPU6050_DLPF_BW_20);
printCommands();
countdownBeforeCalibration();
calibrateMPU();
Serial.println("Gotowe.");
Serial.println("Najpierw sprawdz wykres kata bez silnikow.");
Serial.println("Potem wpisz: a");
Serial.println();
lastLoopMs = millis();
lastMicros = micros();
}
// ------------------------------------------------------------
void loop()
{
handleSerial();
unsigned long nowMs = millis();
if (nowMs - lastLoopMs < LOOP_MS) return;
lastLoopMs = nowMs;
unsigned long nowUs = micros();
float dt = (nowUs - lastMicros) / 1000000.0;
lastMicros = nowUs;
if (dt <= 0.0 || dt > 0.1)
{
dt = LOOP_MS / 1000.0;
}
int16_t axRaw, ayRaw, azRaw;
int16_t gxRaw, gyRaw, gzRaw;
mpu.getMotion6(&axRaw, &ayRaw, &azRaw, &gxRaw, &gyRaw, &gzRaw);
float accRoll = calcRollDeg(ayRaw, azRaw) - zeroRoll;
float gyroX = gxRaw / GYRO_SCALE - gyroOffsetX;
// Filtr komplementarny
angle = alpha * (angle + gyroX * dt) + (1.0 - alpha) * accRoll;
float error = setAngle - angle;
float dAngle = (angle - lastAngle) / dt;
if (!armed)
{
pidOut = 0.0;
integral = 0.0;
stopMotors();
}
else
{
if (abs(angle) > 45.0)
{
armed = false;
stopMotors();
Serial.println("STOP: zbyt duzy kat");
return;
}
integral += error * dt;
integral = constrain(integral, -100.0, 100.0);
pidOut = Kp * error + Ki * integral - Kd * dAngle;
pidOut = constrain(pidOut, -maxCorr, maxCorr);
int leftPWM = constrain((int)(basePWM + controlSign * pidOut), 0, 255);
int rightPWM = constrain((int)(basePWM - controlSign * pidOut), 0, 255);
setMotors(leftPWM, rightPWM);
}
lastAngle = angle;
if (nowMs - lastPrintMs >= 100)
{
lastPrintMs = nowMs;
Serial.print("kat:");
Serial.print(angle, 2);
Serial.print(" zad:");
Serial.print(setAngle, 2);
Serial.print(" pid:");
Serial.print(pidOut, 2);
Serial.print(" gx:");
Serial.print(gyroX, 2);
Serial.print(" armed:");
Serial.println(armed ? 1 : 0);
}
}
Adam Mańka - adam.manka(znak)polsl.pl