쿼드콥터 제작기 13 - 테스트비행과 절반의 성공

Posted by dw0rdptr
2016. 3. 19. 03:43 IoT/QuadCopter

PID게인조정을 끝내고 테스트비행을 하러 집앞 학교 운동장에서 테스트를 했다.

참고로 비행제한구역이라 일몰전 150m 고도 이하로 비행했으므로 항공법에 위배되지는 않는다.


왼쪽영상이 드론에 달아놓은 캠이고, 오른쪽이 직접 찍은 영상


드디어 어느정도 날 수 있다! 작년 가을 첫 비행테스트를 했을 때 뜨지도 못하고 바닥에서 굴렀던 걸 생각하면 훨씬 나아졌다.


그런데.. DMP의 샘플링레이트가 100hz밖에 안돼 자세제어 주기가 3ms에 훨씬 못미치는 11ms정도가 되어버린다.

때문에 자세변화에 모터출력이 바로 반응하지 못해 조금씩 옆으로 치우져지는 문제가 생겨버렸다.

자세를 빠르게 못잡으니 조종이 제대로 안돼 오래 날리지는 못했고 거기에 조종경험은 거의 없다시피 해서 조종미숙으로 하드랜딩..

보완할 부분이 많아보인다.



1. DMP를 다시 상보필터로

  - 역시 뭐든 편하게 하려하면 안되나보다. DMP의 샘플링레이트를 200hz로 올릴 수 있긴 했는데 노이즈가 엄청 심해져 포기..

   다시 상보필터를 써야겠다.


2. 하드웨어적으로 방진

  - 아무래도 진동에는 상대적으로 더 취약한 상보필터니까 댐퍼나 센서위치를 바꾸거나 해서 진동을 줄여야 할것같다.

    클럭이 높은 두에나 메가로 바꿔 Low Pass Filter를 다시 적용해 보는것도 생각중


3. 프레임 교체

  - 테스트를 많이 거친 프레임이다 보니 여기저기 부러진곳도 있고 상태가 말이아니라 하비킹에서 새로 주문해야 할듯




이번에도 못뜨면 때려칠까 했는데 이건 못뜬것도아니고.. 그렇다고 완벽하게 성공도 아닌것같아 일단은 문제점만 분석해두고 보류하려고 한다.

11월에 수능보고 살아있으면 다시 진행해야지

그래도 가끔 블로그 들어오긴 하니까 궁금한점 댓글로 물어보시면 아는 선에서 최대한 알려드립니다. 포스팅은 11월에 다시..


쿼드콥터 제작기 12 - 보드간 I2C통신

Posted by dw0rdptr
2016. 3. 16. 15:08 IoT/QuadCopter

이전글에서 송신기의 PPM 신호를 받아오는데에서 문제가생겨 아두이노보드를 서브와 메인으로 나눠 두개를 사용하기로 했다. 


메인 아두이노(Arduino Uno)

- mpu6050 센서에서 각도값 도출

- 서브아두이노에서 받아온 제어목표값과 센서데이터를 이용, PID제어

- 모터출력 담당



서브 아두이노(Arduino Nano) 

- RC조종기의 PPM신호 해석 

- PPM에서 해석한 PWM신호에서 목표 스로틀과 YAW, PITCH, ROLL 각도를 map함수로 스케일링 후 메인 아두이노로 전달

 * 스로틀범위는 830~2000으로 잡았다. PID제어를생각해 ESC신호의 최대범위보단 조금 낮춰두는게 안전할 것 같아서 최대 스로틀은 2000으로 제한했다.

   PITCH와 ROLL은 -30~30으로 잡아두었다. 단위는 목표각도니까 degree. YAW는 대충 -20~20으로 맞췄다.



RC Receiver -> 서브아두이노(Nano)에서 신호해석, 스케일링 -> 메인아두이노(Uno)로 전달


나노에서 우노로 보드간 통신을 하는방법은 I2C, UART, SPI가있다. 그 중 간단하게 구현가능한 I2C를 사용했다.

MPU6050에서 데이터를 얻어올 때에도 사용하는 I2C에 대해 간단하게 설명하면


- I2C는 필립스에서 개발한 직렬 컴퓨터 버스이며, 마더보드, 임베디드 시스템, 휴대전화 등에 저속의 주변기기를 연결하기 위해 사용

- 1:N연결을 지원하고 Master-Slave 모드로 통신

- 슬레이브 선택을 위해 소프트웨어적인 주소를 사용하므로 연결하는 장치가 늘어나도 사용하는 핀이 증가하지 않는다

- 데이터 송수신을 위한 SDA와 동기화 클록을 위한 SCL 핀 2개만 사용.(2-Wire)

- UART나 SPI와 비교할 때 속도의 한계가 있으므로 사용에는 제한 그러나 아두이노에서는 Wire라이브러리에서 슬레이브 모드를 지원하고, UART와 달리 1:N통신이 지원되므로 비교적 간단하게 다양한 기능 구현이 가능



I2C통신을위한 핀은 위 그림처럼 우노와 나노 각각의 SCL, SDA핀을 연결해준다. 참고로 A5핀이 SCL, A4번핀이 SDA이다. 

리시버핀 연결은 전글을 참고. 전원은 한쪽보드에서만 공급하고 5v-Vin, GND-GND 연결하면 나머지보드도 전원공급이 된다.



Receiver.ino

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#include <PinChangeInt.h>
#include "Wire.h"
 
#define RC_Ch1 3
#define RC_Ch2 4
#define RC_Ch3 5
#define RC_Ch4 6
 
#define ROLL_MIN -30
#define ROLL_MAX 30
#define PITCH_MIN -30
#define PITCH_MAX 30
#define YAW_MIN -20
#define YAW_MAX 20
 
#define ESC_MIN 800
#define ESC_MAX 2200 
 
 
volatile unsigned long LastCh1 = micros();
volatile unsigned long LastCh2 = micros();
volatile unsigned long LastCh3 = micros();
volatile unsigned long LastCh4 = micros();
 
 
 
 
volatile float Ch1;
volatile float Ch2;
volatile float Ch3;
volatile float Ch4;
 
uint16_t Throttle=0;
int8_t TargetROLL, TargetPITCH, Yaw;
 
void setup() {
  Wire.begin(4);
  Wire.onRequest(Send);
 
  //Serial.begin(9600);
 
  PCintPort::attachInterrupt(RC_Ch1, Ch1_Interrupt, CHANGE);
  PCintPort::attachInterrupt(RC_Ch2, Ch2_Interrupt, CHANGE);
  PCintPort::attachInterrupt(RC_Ch3, Ch3_Interrupt, CHANGE);
  PCintPort::attachInterrupt(RC_Ch4, Ch4_Interrupt, CHANGE);
 
}
 
void loop() {
 
    TargetROLL = map(Ch1, 11001900, ROLL_MIN, ROLL_MAX);
    TargetPITCH = map(Ch2, 11001900, PITCH_MIN, PITCH_MAX);
    Throttle = map(Ch3, 11301800, ESC_MIN, ESC_MAX);
    Yaw = map(Ch4, 11001800, YAW_MIN, YAW_MAX);
 
     
}
 
 
void Ch1_Interrupt() {
    Ch1 = micros() - LastCh1;
  LastCh1 = micros();
}
 
void Ch2_Interrupt() {
    Ch2 = micros() - LastCh2;
  LastCh2 = micros();
}
 
void Ch3_Interrupt() {
    Ch3 = micros() - LastCh3;
  LastCh3 = micros();
}
 
void Ch4_Interrupt() {
    Ch4 = micros() - LastCh4;
  LastCh4 = micros();
}
 
 
void Send() {
 
  byte bufferArr[5];
  
  bufferArr[0= (Throttle >> 8) &0xFF;
  bufferArr[1= Throttle & 0xFF;
  bufferArr[2= TargetROLL & 0xFF;
  bufferArr[3= TargetPITCH & 0xFF;
  bufferArr[4= Yaw & 0xFF;
  
  Wire.write(bufferArr,5);
  
}
 
cs


서브아두이노(Nano)의 전체 코드이다.

우선 인터럽트로 RC신호를 해석해 Throttle, TargetROLL, TargetPITCH, Yaw제어목표값을 map함수로 제어에 쓸수 있게 스케일링하였다.


I2C로 데이터를 넘기는 과정을 간단하게 설명하면

Wire.begin(4); -> 4번 슬레이브장치로 등록

Wire.onRequest(Send); -> 마스터장치(Uno)에서 데이터를 요구하면 Send함수 실행


Send함수에서 bufferArr[5] 바이트배열 버퍼를 선언 후 구한 Throttle, TargetROLL, TargetPITCH, Yaw값을 버퍼에 저장 (Throttle은 16bit, 2byte 차지하므로 한번 밀어준다.)


Wire.write(bufferArr,5); -> 마스터장치로 bufferArr 5byte 전송


Quad_main.ino

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include "I2Cdev.h"
#include "Wire.h"
 
 
#define ROLL_MIN -30
#define ROLL_MAX 30
#define PITCH_MIN -30
#define PITCH_MAX 30
#define YAW_MIN -20
#define YAW_MAX 20
 
#define ESC_MIN 800
#define ESC_MAX 2200 
 
#define SamplingTime 0.01
 
 
uint16_t Throttle,  LastThrottle=0;
int8_t TargetROLL;
int8_t TargetPITCH;
int8_t TargetYAW;
int8_t LastTargetROLL, LastTargetPITCH,LastYAW;
 
byte a,b,c,d,e;
 
void setup() {
  Wire.begin();
  TWBR = 24;
 
}
 
void loop() {
    RC_Command();
}
 
 
 
void RC_Command() {
 
  Wire.requestFrom(45);
 
  
  a = Wire.read();
  b = Wire.read();
  c = Wire.read();
  d = Wire.read();
  e = Wire.read();
  
  Throttle = a;
  Throttle = (Throttle << 8| b;
  TargetROLL = c;
  TargetPITCH = d;
  TargetYAW = e;
 
  
    if (Throttle < ESC_MIN || Throttle > ESC_MAX)
      Throttle = LastThrottle;
 
    if (TargetROLL < ROLL_MIN || TargetROLL > ROLL_MAX)
      TargetROLL = LastTargetROLL;
 
    if (TargetPITCH < PITCH_MIN || TargetPITCH > PITCH_MAX)
      TargetPITCH = LastTargetPITCH;
 
    if (TargetYAW < YAW_MIN || TargetYAW > YAW_MAX)
     TargetYAW = LastYAW;
      
    if (TargetROLL >= -3 && TargetROLL <= 3 )
      TargetROLL = 0;
    
    if (TargetPITCH >= -3 && TargetPITCH <= 3 )
      TargetPITCH = 0;
 
    if(TargetYAW >= -3 && TargetYAW <= 3)
      TargetYAW = 0;
 
 
 
    LastThrottle = Throttle;
    LastTargetROLL = TargetROLL;
    LastTargetPITCH = TargetPITCH;
    LastYAW = TargetYAW;
 
    
  }
  
 
cs
d

메인아두이노(Uno)코드중 서브아두이노에서 데이터를 요청하는 부분이다.

RC_Command 함수를 보면 

Wire.requestFrom(4, 5); -> 슬레이브장치 4번에서 5byte의 데이터 요청.


a = Wire.read();

b = Wire.read();

c = Wire.read();

d = Wire.read();

e = Wire.read();   -> byte형변수 5개 선언. I2C통신은 한번에 1byte씩 받으므로 Wire.read();로 읽은 바이트데이터를 a부터 e까지 저장해준다.


Throttle = a;

Throttle = (Throttle << 8) | b;

TargetROLL = c;

TargetPITCH = d;

TargetYAW = e;            -> 각각의 변수에 대입해주면 된다


if문으로 가끔 크게 튀는 노이즈를 제거해주었고 조종기를 건들지않았을때 YAW PITCH ROLL이 0이되도록 트림을 맞춰주면 조종기설정은 끝

PID게인도 거의 맞춰가는 것 같고 다음글은 비행테스트가 될 것 같다. 


쿼드콥터 제작기 11 - PPM과 아두이노 Interrupt

Posted by dw0rdptr
2016. 3. 1. 22:37 IoT/QuadCopter

hobbyking에서 리시버와 조종기를 구매했다. 모델명은 HK-T6A V2 Transmitter&Receiver 2.4Ghz

송신기에서는 PWM신호를 직접 전달하는데 6채널 송신기의 경우 채널별로 에일러론(CH1), 엘리베이터(CH2), 쓰로틀(CH3), 러더(CH4) 외 두가지 PWM 신호, 총 6가지 PWM신호를 포함하게 되는 PPM 신호를 송신한다. 수신기에서는 송신된 무선전파를 수신하여 PPM신호을 검출한다. 검출한 PPM신호를 MCU(컨트롤러 보드)로 전달하고, MCU에서는 PPM신호에서 개별 채널의 PWM신호를 뽑아내는 것이 기본적인 RC PPM신호의 사용이다.

PWM이 Pulse Width Modulation(펄스 폭 변조)라면, PPM은 Pulse Positon Modulation(펄스 위치 변조)인 것이다.


   표준 RC 수신기 block diagram

출처 : http://skymixer.net/electronics/84-rc-receivers/78-rc-ppm-signal

위 그림에서 PPM Frame Decoder가 MCU, 즉 아두이노라고 생각하면 된다. 

그림처럼 아두이노(MCU)와 수신기를 연결해야 하는데 그전에 먼저 송신기와 수신기를 바인딩 시켜야 한다. 이건 제품마다 다르니 제품명을 검색해서 찾으면 된다.


위처럼 5v와 GND핀에 연결, 각 채널 시그널핀은 디지털핀에 연결하면 아두이노와 수신기 연결은 끝.

이제 문제는 아두이노에서 어떻게 PPM신호를 해석해 PWM신호를 뽑아내냐는 것인데..이는 아두이노의 Interrupt로 간단하게 구현 가능하다.


인터럽트에 대해 간단히 설명을 하자면, 지정된 핀의 상태가 원하는 조건과 일치하면 미리 등록한 인터럽트 callback 함수, 즉 ISR (Interrupt Service Routines 인터럽트 서비스 루틴)을 자동으로 호출해주는 기능이다. 이때 실행중이던 loop() 함수 안의 루틴은 인터럽트 콜백 함수가 끝날 때 까지 대기상태가 된다.

쉽게말해 어떤 핀의 상태가 특정한 조건이 되면 loop()를 멈추고 그동안 사용자가 등록한 함수(ISR)를 실행하고 등록한 함수가 끝나면 다시 loop()함수로 되돌아오는게 인터럽트이다.


다음 단계를 통해 PPM신호의 개별 채널을 감지하고 개별 채널의 PWM ON(High)시간을 측정할 수 있다.


1. 상승에지 감지(Rising Edge Sense)를 하면 인터럽트 서비스 루틴이 실행됨

2. Timer1의 카운터값을 기록하고, 센싱모드를 하강에지감지(Falling edge sense)로 바꾼다.

3. 인터럽트 서비스 루틴을 빠져 나간다.

4. 하강에지 감지(Falling Edge Sense)를 하면 인터럽트 서비스 루틴이 실행된다.(위에서 하강에지감지 모드로 바꾸었기 때문)

5. Timer1의 카운터값을 기록하고, 센싱모드를 상승에지감지(Rising Edge sense)로 바꾼다.

6. 펄스의 ON시간을 계산한다.(펄스의 ON 시간 계산하기: 두가지 경우가 있음)

// 경우1: 하강에지 시각이 상승에지보다 후(뒤)일때 : ON시간 = (후 - 전)

// 경우2: 하강에지 시각이 상승에지보다 전(앞)일때 : ON시간 = (카운터최대값 +전 - 후)

7. 인터럽트 서비스 루틴을 빠져 나간다.

8. 상승에지 인터럽트 감지하면, 위의 1번부터 차례대로 진행.(무한 반복)


출처 : AVR과 RC PPM 신호 (3편) - PPM 신호감지를 위한 AVR을 소스코드

좀더 자세하게 알고 싶으면 위 출처의 링크를 보면 된다.


어찌저찌 구현을 했는데..

예상치 못한 문제가 생겨버렸다. RC신호를 받으려고 인터럽트 서비스 루틴을 실행하는 중에는 PID제어값, 특히 D제어값이 이상하게 튀어버린다. 시소테스트를 하는 도중 틱 틱 소리가 났었는데 D제어값이 튄것이 그 이유였다.


500~600이상 D제어 값이 튀어버린다.


그래서 여기저기 도움을 청하고 구글링을 하다 내린 결론은 아두이노가 RC 신호 수신 대기시간동안은 PID제어를 못한다는 것이었다.

그래서 서브 아두이노 보드를 추가해 수신기를 연결하고 RC PPM 신호를 받아 해석한 각 PWM 신호 범위를 필요에 맞게 스케일링 한 뒤 보드간 통신으로 PID제어와 모터 출력을 담당하는 메인 아두이노 보드로 넘겨주는 방법으로 해결했다.


자세한 방법은 다음포스팅에~