드론 제작과 운용 예제(MPU9250과 GPS)
무선조종장치(RC device) 2025. 6. 16. 01:19 |반응형
아래 예제는 저서 '드론 제작과 운용' 에서 설명한 코딩 예제입니다.
코드 파일.hwpx
0.05MB
#include <Wire.h>
#include <MPU9250_asukiaaa.h>
#include "HT_SSD1306Wire.h"
#include "HT_TinyGPS++.h"
// 핀 정의 (예시, 실제 연결된 핀 번호로 변경 필요)
#define SDA_PIN 41
#define SCL_PIN 42
#define CALIB_SEC 20 // 캘리브레이션 지속 시간 설정 (20초)
// I2C를 사용하여 OLED 디스플레이 초기화, RST_OLED도 반드시 적어주어야함
static SSD1306Wire display(0x3c, 400000, SDA_OLED, SCL_OLED, GEOMETRY_128_64, RST_OLED);
float accelXOffset = 0, accelYOffset = 0, accelZOffset = 0;
float gyroXOffset = 0, gyroYOffset = 0, gyroZOffset = 0;
TwoWire myWire = TwoWire(1);
MPU9250_asukiaaa mySensor;
TinyGPSPlus gps; // GPS 객체 생성
void VextON(void) {
// 외부 전원 켜기 (예: 센서 또는 주변 장치)
pinMode(Vext, OUTPUT);
digitalWrite(Vext, LOW); // Vext 핀을 LOW로 설정하여 전원 공급
}
void VextOFF(void) {
// 외부 전원 끄기
pinMode(Vext, OUTPUT);
digitalWrite(Vext, HIGH); // Vext 핀을 HIGH로 설정하여 전원 차단
}
void calibrateAccel() {
float sumX = 0, sumY = 0, sumZ = 0;
int samples = 500;
for (int i = 0; i < samples; i++) {
mySensor.accelUpdate();
sumX += mySensor.accelX();
sumY += mySensor.accelY();
sumZ += mySensor.accelZ();
delay(10);
}
accelXOffset = sumX / samples;
accelYOffset = sumY / samples;
accelZOffset = sumZ / samples;
Serial.print("accelXOffset : "); Serial.println(accelXOffset);
Serial.print("accelYOffset : "); Serial.println(accelYOffset);
Serial.print("accelZOffset : "); Serial.println(accelZOffset);
}
void calibrateGyro() {
float sumX = 0, sumY = 0, sumZ = 0;
int samples = 500;
for (int i = 0; i < samples; i++) {
mySensor.gyroUpdate();
sumX += mySensor.gyroX();
sumY += mySensor.gyroY();
sumZ += mySensor.gyroZ();
delay(10);
}
// 아래 자이로 옵셋은 수평 상태에서 모두 0이 나오는 것이 이상적
gyroXOffset = sumX / samples;
gyroYOffset = sumY / samples;
gyroZOffset = sumZ / samples;
Serial.print("gyroXOffset : "); Serial.println(gyroXOffset);
Serial.print("gyroYOffset : "); Serial.println(gyroYOffset);
Serial.print("gyroZOffset : "); Serial.println(gyroZOffset);
}
void setMagMinMaxAndSetOffset(MPU9250_asukiaaa* sensor, int seconds) {
unsigned long calibStartAt = millis(); // 캘리브레이션 시작 시간 기록
float magX, magXMin, magXMax, magY, magYMin, magYMax, magZ, magZMin, magZMax;
// 초기 최소/최대값 설정
sensor->magUpdate();
magXMin = magXMax = sensor->magX();
magYMin = magYMax = sensor->magY();
magZMin = magZMax = sensor->magZ();
// 캘리브레이션 루프
while(millis() - calibStartAt < (unsigned long) seconds * 1000) {
delay(100); // 데이터 갱신 간격
sensor->magUpdate();
magX = sensor->magX();
magY = sensor->magY();
magZ = sensor->magZ();
// 최소/최대값 업데이트
if(magX > magXMax) magXMax = magX;
if(magY > magYMax) magYMax = magY;
if(magZ > magZMax) magZMax = magZ;
if(magX < magXMin) magXMin = magX;
if(magY < magYMin) magYMin = magY;
if(magZ < magZMin) magZMin = magZ;
}
// 오프셋 설정 (최소/최대값의 중간값 기준)
sensor->magXOffset = - (magXMax + magXMin) / 2;
sensor->magYOffset = - (magYMax + magYMin) / 2;
sensor->magZOffset = - (magZMax + magZMin) / 2;
// 오프셋값 출력
Serial.println("Magnetometer Offsets:");
Serial.print("X Offset: "); Serial.println(mySensor.magXOffset);
Serial.print("Y Offset: "); Serial.println(mySensor.magYOffset);
Serial.print("Z Offset: "); Serial.println(mySensor.magZOffset);
}
void calculateTiltCompensatedHeading() {
const int MEASUREMENTS = 20;
float sumRollX = 0.0, sumRollY = 0.0;
float sumPitchX = 0.0, sumPitchY = 0.0;
float sumYawX = 0.0, sumYawY = 0.0;
unsigned long prevMillis = millis();
for (int i = 0; i < MEASUREMENTS;) {
unsigned long currentMillis = millis();
// 0.005초(5ms) 경과 확인
if (currentMillis - prevMillis >= 5) {
prevMillis = currentMillis;
mySensor.accelUpdate();
float ax = mySensor.accelY();
float ay = -mySensor.accelX();
float az = mySensor.accelZ();
// 자이로 데이터 업데이트
mySensor.gyroUpdate();
float gx = mySensor.gyroY();
float gy = -mySensor.gyroX();
float gz = mySensor.gyroZ();
// 지자기계 데이터 업데이트
mySensor.magUpdate();
float mx = mySensor.magY();
float my = -mySensor.magX();
float mz = mySensor.magZ();
// 2) 롤, 피치 라디안 계산
float rollRad = atan2(-ax, az);
// 0~2π 범위로 만들기
if (rollRad < 0) rollRad += 2.0f * PI;
float pitchRad = atan2(-ay, sqrt(ax*ax + az*az));
// 아래 if문 등은 원래 코드에서 pitch 조정하는 부분을 상황에 맞게 변환
// 예: pitch가 음수면 2π 더하는 식으로 0~2π 만들 수도 있음
if (pitchRad < 0) pitchRad += 2.0f * PI;
// 3) 야우(heading) 라디안 계산
float yawRad = atan2(my, mx);
// atan2() 결과가 -π ~ +π 이므로, 0~2π 맞추려면
if (yawRad < 0) yawRad += 2.0f * PI;
// (선택) 샘플별 오프셋/특별 보정이 꼭 필요하다면,
// 여기서 yawRad에 +-, rollRad에 +- 하는 식으로 처리 가능.
// 4) (cos, sin)으로 변환하여 누적
sumRollX += cos(rollRad);
sumRollY += sin(rollRad);
sumPitchX += cos(pitchRad);
sumPitchY += sin(pitchRad);
sumYawX += cos(yawRad);
sumYawY += sin(yawRad);
i++; // 카운트 증가
}
}
// -- 모든 샘플을 누적한 뒤 평균 백터 계산 --
// 롤
float avgRollRad = atan2(sumRollY, sumRollX);
float avgRollDeg = avgRollRad * 180.0 / PI;
if (avgRollDeg < 0) avgRollDeg += 360.0;
float rollOffset = 171.7; // 수평일 때 172도인 문제를 보정
avgRollDeg -= rollOffset;
if (avgRollDeg < 0) {
avgRollDeg += 360.0;
}
else if (avgRollDeg >= 360.0) {
avgRollDeg -= 360.0;
}
// 피치
float avgPitchRad = atan2(sumPitchY, sumPitchX);
float avgPitchDeg = avgPitchRad * 180.0 / PI;
if (avgPitchDeg < 0) avgPitchDeg += 360.0;
float pitchOffset = 350.2; // 수평일 때 피치가 350.2도인 문제를 보정
avgPitchDeg -= pitchOffset;
if (avgPitchDeg < 0) {
avgPitchDeg += 360.0;
}
else if (avgPitchDeg >= 360.0) {
avgPitchDeg -= 360.0;
}
// 요
float avgYawRad = atan2(sumYawX, sumYawY);
float avgYawDeg = avgYawRad * 180.0 / PI;
if (avgYawDeg < 0) avgYawDeg += 360.0;
float yawOffset = 270; // 270도가 북쪽인 문제를 보정
avgYawDeg -= yawOffset;
if (avgYawDeg < 0) {
avgYawDeg += 360.0;
}
else if (avgYawDeg >= 360.0) {
avgYawDeg -= 360.0;
}
// 결과 출력
Serial.print("Roll: "); Serial.print(avgRollDeg);
Serial.print(" Pitch: "); Serial.print(avgPitchDeg);
Serial.print(" Yaw: "); Serial.println(avgYawDeg);
// 디스플레이에 출력
char rollStr[32];
snprintf(rollStr, sizeof(rollStr), "%.1f°", avgRollDeg);
display.drawString(30, 15, "Roll: ");
display.drawString(60, 15, rollStr);
char pitchStr[32];
snprintf(pitchStr, sizeof(pitchStr), "%.1f°", avgPitchDeg);
display.drawString(95, 15, "Pitch: ");
display.drawString(125, 15, pitchStr);
char yawStr[32];
snprintf(yawStr, sizeof(yawStr), "%.0f°", avgYawDeg);
display.drawString(30, 30, "Yaw: ");
display.drawString(60, 30, yawStr);
display.display();
}
void setup() {
Serial.begin(115200);
Serial1.begin(9600, SERIAL_8N1, 34, 33); // GPS 모듈과 통신 시작
// I2C 설정 및 센서 초기화
myWire.begin(SDA_PIN, SCL_PIN, 400000);
mySensor.setWire(&myWire);
mySensor.beginAccel();
mySensor.beginMag(MAG_MODE_CONTINUOUS_100HZ);
mySensor.beginGyro();
VextON(); // 외부 전원 켜기
delay(100); // 전원이 안정될 때까지 짧은 지연
display.init(); // 디스플레이 초기화
display.setFont(ArialMT_Plain_10); // 디스플레이의 기본 폰트 설정
// DWARF 로고 송출
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
display.drawString(display.getWidth()/2, 24, "DWARF");
display.display();
delay(3000);
display.clear();
display.display();
// 가속도 및 자이로 캘리브레이션 준비 메시지 표시
display.setFont(ArialMT_Plain_10);
display.drawString(display.getWidth() / 2, 5, "Start scanning");
display.drawString(display.getWidth() / 2, 20, "values of Accel & Gyro");
display.drawString(display.getWidth() / 2, 35, "to get offset values.");
display.drawString(display.getWidth() / 2, 50, "DO NOT TOUCH OR MOVE");
display.display();
// 가속도 및 자이로 캘리브레이션
calibrateAccel();
calibrateGyro();
display.clear();
display.display();
display.setFont(ArialMT_Plain_16);
display.drawString(display.getWidth() / 2, 10, "Calibration");
display.drawString(display.getWidth() / 2, 30, "1/2 finished");
display.display();
delay(2000);
display.clear();
display.display();
display.setFont(ArialMT_Plain_10);
display.drawString(display.getWidth()/2, 5, "Now, Rotate your device");
display.drawString(display.getWidth()/2, 20, "360 degrees");
display.drawString(display.getWidth()/2, 35, "in every direction for");
display.drawString(display.getWidth()/2, 50, String(CALIB_SEC) + " seconds.");
display.display();
// 방위각 변화를 감지하여 캘리브레이션 시작
mySensor.magUpdate();
float initialDirection = mySensor.magHorizDirection();
unsigned long startTime = millis(); // startTime 선언
bool significantChange = false;
while(millis() - startTime < (unsigned long)(CALIB_SEC * 1000)) {
delay(100); // 0.1초 간격으로 방위각 측정
mySensor.magUpdate();
float currentDirection = mySensor.magHorizDirection();
if(abs(currentDirection - initialDirection) > 15.0) {
significantChange = true; // 15도 이상의 변화 감지
break;
}
}
if(significantChange) {
setMagMinMaxAndSetOffset(&mySensor, CALIB_SEC); // 캘리브레이션 함수 호출
// 캘리브레이션 완료 메시지 표시
display.clear();
display.display();
display.setFont(ArialMT_Plain_16);
display.drawString(display.getWidth() / 2, 10, "Calibration");
display.drawString(display.getWidth() / 2, 30, "Finished.");
Serial.println("Calibration Finished.");
display.display();
delay(4000);
display.clear();
display.display();
}
else {
// 방위각 변화가 없을 경우 기본 오프셋값 설정
mySensor.magXOffset = -81;
mySensor.magYOffset = 47;
mySensor.magZOffset = 81;
display.clear();
display.display();
display.setFont(ArialMT_Plain_10);
display.drawString(display.getWidth()/2, 5, "No significant direction");
display.drawString(display.getWidth()/2, 20, "change detected.");
display.drawString(display.getWidth()/2, 35, "Using default offsets.");
display.display();
delay(4000);
display.clear();
display.display();
Serial.println("No significant direction change detected. Using default offsets:");
}
display.clear();
display.display();
Serial.println("Initialization complete.");
}
void loop() {
// GPS 데이터 수신
unsigned long start = millis(); // 현재 시간 기록
static unsigned long lastUpdate = 0;
if (millis() - lastUpdate > 100) { // 마지막 업데이트 이후 100ms가 지났는지 확인
lastUpdate = millis(); // 마지막 업데이트 시간 갱신
display.clear(); // 디스플레이 내용 초기화
display.setTextAlignment(TEXT_ALIGN_LEFT);
display.setFont(ArialMT_Plain_10);
// GPS 위치 데이터 확인 및 출력
/*
char gpsInfo[64];
snprintf(gpsInfo, sizeof(gpsInfo), "Lat: %.6f\nLng: %.6f\nAlt: %.2f m",
gps.location.lat(), // 위도 정보 가져오기
gps.location.lng(), // 경도 정보 가져오기
gps.altitude.meters()); // 고도 정보 가져오기
display.drawString(0, 45, String(gpsInfo)); // GPS 정보를 디스플레이에 그리기
*/
if (gps.location.isValid()) { // GPS 위치 데이터가 유효한지 확인
display.drawString(0, 0, "GPS OK");
Serial.println("GPS OK");
}
else {
display.drawString(0, 0, "GPS Data Invalid"); // GPS 데이터가 유효하지 않은 경우
Serial.println("GPS Data Invalid");
}
// 프로그램 시작 이후 경과 시간을 시:분:초 형식으로 표시
unsigned long seconds = millis() / 1000; // 초 단위로 경과 시간 계산
unsigned int hours = seconds / 3600; // 시간 계산
unsigned int minutes = (seconds % 3600) / 60; // 분 계산
unsigned int secs = seconds % 60; // 초 계산
char timeString[9];
sprintf(timeString, "%02u:%02u:%02u", hours, minutes, secs); // 시간 형식 문자열 생성
display.setTextAlignment(TEXT_ALIGN_RIGHT); // 오른쪽 정렬
display.setFont(ArialMT_Plain_10);
display.drawString(128, 0, String(timeString)); // 디스플레이에 경과 시간 표시
// 방향호출
calculateTiltCompensatedHeading();
delay(100); // 100ms 대기
}
}
반응형
'무선조종장치(RC device)' 카테고리의 다른 글
드론 제작과 운용 4장 연막탄 코딩 (0) | 2025.07.02 |
---|---|
픽스호크 PID제어 및 오토튠 및 메뉴얼튠 방법 Qgroundcontrol (0) | 2023.07.11 |
YI GONG 9ch RC 트랙터와 트럭 알리직구 후기 (0) | 2023.05.21 |
부모와 함께하는 초중고 제주 꿈나무 드론레이싱대회 (0) | 2023.05.04 |
Turbo racing C64 Drift RC CAR 1:76 초소형 비례 제어 rc카 리뷰 (0) | 2023.04.05 |