[41호]최대 흡기상태 측정 모듈
2016 ICT 융합 프로젝트 공모전 입선
최대 흡기상태 측정 모듈
글 | 고려대학교 강병욱, 김도형, 송명근, 원준현, 장재현
심사평
뉴티씨 실제로 병원에서 X-ray 사진을 찍을 때에 좋은 위치에서 찍기 위한 실질적인 아이디어가 반영된 작품으로 판단되며, 좋은 어플리케이션이다. 잘 만들면 실용적으로 적용하는 것도 좋으리라 생각된다. 난이도는 어렵지 않으나, 실제 동작이 완료된 점, 실용적인 점 등은 좋은 점수를 받은 이유이다.
칩센 상세한 관찰로 아이디어가 도출되어 높게 평가한다. 구현이 쉬울 수는 있어도 어려운 게 뛰어난 것은 아니다.
위드로봇 X-ray 검사 시 호흡을 멈추는 상태를 검출한다는 아이디어는 매우 재미있습니다. 단, 이 상태를 검출하기 위해 PSD를 이용하여 측정자의 어깨의 흔들림을 측정하는 부분은 조금 아쉽습니다. 최대 흡기 상태를 정의할 수 있는 다른 지표를 찾고, 그 지표를 검출할 수 있는 시스템을 개발하는 과정이 상세하게 드러나면 좋은 작품이 될 것 같습니다.
작품 개요
다소 생소할 수 있는 이 작품은 방사선사의 고충으로 고안되어 나온 작품으로, X-ray검사 중 가장 많이 촬영되어지는 흉부부위는 다른 부위와 다르게 방사선사와 피검자의 신호 교환으로 촬영 된다. 보통의 흉부선 X-ray 촬영은 아래와 같이 이루어지고 있다.
이 순간 방사선사는 피검자의 호흡 상태를 알지 못한 채로 X-ray 촬영버튼인 shoot 버튼을 누르며 촬영이 종료된다. 방사선사가 피검자에게 이러한 지시를 하는 이유는 피검사자의 최대 흡기 상태, 즉 최대한 숨을 들이마셔 폐의 부피를 가장 크게 하였을 때 횡격막의 양끝이 아래로 잘 내려가 흉강 내 구조물이 엑스선 영상에 이상적으로 나타나기 때문이다.
물론 대부분의 X-ray 사진이 잘 나오는 편이지만, 그렇지 않을 경우 재촬영이 필요하게 되며, 이는 피검자에게 불필요한 방사선 피폭을 안겨주고 시간, 비용적인 문제 역시 따르게 된다. 또한 청력이 약한 노인이나 장애인분들 같은 경우 방사선사와의 신호 소통이 잘 이루어지지 않아 발생되는 문제점들이 생기는데 있어 구체적인 해결방법을 제시하고자 한다.
인체가 호흡을 할 때 흡기상태에서는 쇄골이 위로 올라가고, 횡격막은 내려가며 외관상으로는 가슴이 내밀어지고 어깨가 올라가게 된다. 즉, 가슴둘레가 커지고 어깨 높이가 높아지게 되는 것이다. 실제로 디텍터를 끌어안은 피검자는 가슴은 내밀지 못하므로 어깨가 더욱 상승하는 것을 관찰할 수 있지만 먼 거리에서 X-ray 버튼을 누르는 방사선사의 눈으로는 식별하가 쉽지 않다. 실제로 대부분의 경험 많은 방사선사는 피검자의 호흡 상태를 알아차리는 것보다 오랜 기간 숙달된 감각으로 X-ray 촬영을 한다고 한다.
우리는 인체가 흡기중일 때 어깨가 상승한다는 것을 이용하여, 어깨 높이에 따른 최대 흡기 상태가 측정이 가능하다면, 보다 정확한 측정 방식이 될 것으로 예상하여 보다 객관적으로, 그리고 정량적으로 최대 흡기 상태를 측정 할 수 있도록 본 모듈을 고안하였다.
작품 설명
프로젝트 진행 중 문득 X-ray가 모듈에 영향을 미치지 않을까? 의문점이 들었다. 하지만 잠깐의 조사로 괜찮다는 것을 금방 알 수 있었는데. X-ray는 공항검색, 화물검색, 전자장비 오류 검출 등으로 많은 분야에서 사용되고 있다. 여기서 전자장비의 검출은 X-ray가 전자장비에 영향을 미치지 않는다고 판단할 수 있으므로 다른 장비의 기기를 추가적으로 사용 가능하다는 것을 알 수 있다. 실제로 방사선사의 휴대폰 또한 온종일 X-ray에 노출되지만 오랜 시간 사용가능한 것 역시 확인할 수 있었다. 피검자의 최대 흡기 상태의 측정 방법은 어깨높이를 측정함으로써 간단히, 간접적으로 알 수 있다.
[그림 5]와 같이 만들어진 모듈은 어깨의 상단에 배치된다. 모듈에서는 적외선을 쏘고 반사되어진 빛을 감지하여 시간을 측정하여 그 거리를 가늠하는 것이다. 실제 흉부 X-ray 상황을 가정해 보자. 모듈이 없을 때와 비슷한 상황으로 가겠지만 모듈을 사용한다면 방사선사는 피검자의 호흡 상태를 알 수 있는 상황이 될 것이다.방사선사는 최대 흡기 상태 측정모듈의 LED의 ON/OFF로 피검자의 상태를 알 수 있다. 모듈의 마지막 Green_Led에 불이 들어오면 피검자의 최대 흡기 상태라고 판단하여 shoot 버튼을 누름으로써 보다 정확하게 흉부 X-ray 촬영이 가능하다.
3.1. 주요 동작 및 특징
● 피검자의 유/무 검출
● 피검자의 어깨 높이 측정
● 피검자의 어깨 높이에 따른 LED 변화
적외선 거리 감지기의 Data에 의해서 각색의 LED로 상황을 알려주게 된다. [그림 9]는 이상적인 적외선 센서 그래프로, 피검자 감지를 하면 [그림 7]과 같이 노란색 LED에 불이 들어오게 되고, 호흡을 측정하여 최대 흡기 상태라고 판단되면 [그림 8]과 같이 녹색 LED까지 불이 들어오게 된다.
3.2. 전체 시스템 구성
3.2.1. Hardware
하드웨어 구성은 비교적 간단하다. 거리를 측정할 적외선 감지모듈과 제어할 수 있는 마이크로컨트롤러 보드 그리고 System 상태를 알려주는 LED로 구성되어 있다. 적외선 거리 측정센서 모듈은 SHARP사의 GP2YOAO2YKOF로 20~150cm의 거리 측정이 가능하며 [그림 11]에서의 노란색 구간인 20~30cm의 전압 레벨의 차가 심하여, Data 값 분석에 있어서 보다 명확하게 판단할 수 있다. [그림 11]의 초록색 구간은 피검자 유무 검출 구간으로 사용되었다.
3.2.2. Software
Software System의 전체 구성은 크게 2가지로 나뉘어진다. 노이즈 제거를 위한 Filtering System과 최대 흡기 상태를 찾는 Detecting System이다. Filtering System은 적외선 거리센서로부터 받아오는 Data의 노이즈를 줄여주는 System이고, 최대 흡기 상태 측정은 Detecting System이 담당한다.
가. Filtering System
Filtering System은 [그림 14]와 같이 적외선 거리측정 센서의 Data 값이 연속적인 노이즈로 제어하기 힘들어 소프트웨어 필터링을 추가로 사용하였다. 부속적인 시스템이지만 기존에 사용되는 Filtering과는 조금 차별화를 두고 무엇보다 작은 시스템으로 큰 효과를 보는 후처리방식이며, 노이즈라 판단될 경우 보정치환을 하여 노이즈를 제거하게 된다.
후처리 System이기 때문에 Data를 Buffer에 잠시 묶어두어 노이즈를 찾아내게 된다. 한 번 Filtering 하는데 3개의 Buffer가 필요하다. 현재 5번의 Filtering을 하여 총 11개의 Data_buffer가 필요하며 하나의 Filtering영역에서 노이즈 신호라고 판단될 경우 이전의 정상적인 값을 치환하여 Data 값을 보정하게 된다. 후처리 방식으로 진행되는 Filtering이지만 실제 시간차이는 System이 동작하는 속도로 움직이기 때문에 사람의 체감으로는 잘 구분하지 못한다. Filtering Data는 Detecting System으로 활용된다.
나. Detecting System
Detection System은 크게 네 가지 구간으로 나누어지는데 그 중 첫 번째 상태로 피검자를 감지하지 못한다면 System은 대기상태에 머물게 된다. 두 번째 상태로 피검자를 감지하였다면 이를 System 사용자에게 알려주게 된다. 세 번째 상태로는 최대 흡기 상태 측정상태로 방사선사에게 Shoot을 해도 좋다는 신호를 주는 상태이다. 네 번째 상태는 shoot 버튼을 누르게 되면 대기상태로 돌아간다. 하지만 이 네 번째 상태는 Shoot 버튼을 누른 상태로 가상의 상태를 시험하기 위해 만든 상태이다. 실제 시스템 구성에서는 사용되지 않고 대기상태로 돌아가게 된다.
3.3. 개발 환경(개발 언어, Tool, 사용 시스템 등)
· C언어 개발
· arduino sketch Tool 사용
4. 단계별 제작 과정
· 문제점을 발견하고 모듈을 구상 거리측정센서 및 보드 선정
· 하드웨어 케이스제작 및 부착
· 소프트웨어제작 1달 동안 Filtering 고안 및 Test.
· 2주 Detecting system 제작 및 수치 값 조정
5. 소스코드
/***********************************************************
1. micros() 함수를 이용한 dt 생성!
2. dt를 이용한 미분값 확인!
3. 배열 10개로의 평균값 구하기.
************************************************************/
#include <TimerOne.h>
#define ts 0.001*0.001
#define FlagOn 1
#define FlagOff 0
#define R_LED 9 // Red LED!!
#define Y_LED 8 // Yellow LED!!
#define Y_LED1 10 // Yellow LED!!
#define Y_LED2 12 // Yellow LED!!
#define G_LED 11 // Green LED!!
#define Filtering_y 4
#define Filtering_diff_y 4
#define matrix_SizeFory 99
#define matrix_SizeForDiffy 99
#define Average_Value 10
#define entry_value 0
#define high_filter 0×00
#define low_filter 0×08
#define filter_buffer_number 4 // 필터링 개수 정하는 곳 <<<< 4가 적당한듯
#define Low_number 1
#define DatsFilterBufferNumber 5
float tCount = entry_value;
float tCountPre = entry_value;
unsigned char i = 0, j = 0, k = 0, l = 0;
unsigned char DetectionFlag = 0×00;
unsigned char reference_Flag = 0×00;
unsigned char System_stats = 0×00; // System Flag
int SumCheckFlag = 0;
int Count = 0;
int refer_filter_value = 0;
int band_pass_value = 0;
int sum_y, sum_diff_y, y_filtering , value_filtering, diff_value, integ_value, integ_y, integ_y_filter;
float t;
int diff_y, diff_y_filtering , dt, diff_diff_y; // time, y_value, diff_Y, △t, diff_diff_Y
int ch_t_value = entry_value;
int diff_y_pre = entry_value;
int y_filtering_pre = entry_value;
int filter_value = entry_value;
int pre_filter_value = entry_value;
int diff_filter_value = entry_value;
int mov_avr_value = entry_value;
int reference_value = entry_value; // 사람이 들어왔을시 기준값 저장
int Neates = entry_value;
int Neates_ = entry_value;
int Breath_Check_Time = entry_value;
int detection_H_Check_Time = entry_value;
float Breath_Ch_tCountPre = entry_value;
float detection_H_Check_tCountPre = entry_value;
int y;
int Start_Value = entry_value;
int Replace_Out = entry_value;
int BreathFlag = entry_value;
int TimeCheckValue = entry_value;
int Data_buffer[2 * filter_buffer_number];
int y_filter_buffer[matrix_SizeFory];
int diff_filter_buffer[matrix_SizeForDiffy];
int reference_buffer[0];
int StartF = 1000;
int DatsFilterBuffer[DatsFilterBufferNumber * 2];
int HumenCHeckFlag[DatsFilterBufferNumber * 2];
int out_value = 0;
unsigned int IR_output = entry_value; // ANALOG IN 0 PIN!!
void PinModeSetup()
{
pinMode(R_LED, OUTPUT);
pinMode(Y_LED, OUTPUT);
pinMode(Y_LED1, OUTPUT);
pinMode(Y_LED2, OUTPUT);
pinMode(G_LED, OUTPUT);
digitalWrite(R_LED, HIGH);
digitalWrite(Y_LED, HIGH);
digitalWrite(Y_LED1, HIGH);
digitalWrite(Y_LED2, HIGH);
digitalWrite(G_LED, HIGH);
}
void saveStatesForNextStep() {
tCountPre = tCount;
diff_y_pre = diff_y;
y_filtering_pre = y_filtering;
pre_filter_value = filter_value;
saveDataBufferForNextStep(filter_buffer_number); // Cation!! this funtion is macro
}
void saveDataBufferForNextStep( int filter_num) { // 5번
for (int i = filter_num – 1 ; i >= 0; i–) {
Data_buffer[(2 * i) + 2] = Data_buffer[(2 * i) + 1];
Data_buffer[(2 * i) + 1] = Data_buffer[(2 * i)];
}
}
void RealtimeProcessingForNextStep() {
for (int i = DatsFilterBufferNumber – 1 ; i >= 0; i–) {
DatsFilterBuffer[(2 * i) + 2] = DatsFilterBuffer[(2 * i) + 1];
DatsFilterBuffer[(2 * i) + 1] = DatsFilterBuffer[(2 * i)];
}
}
void printStates(int t) {
Serial.print(t);
Serial.print(“, “);
}
int mov_avr_filter(int y_value) {
sum_y = 0;
y_filter_buffer[0] = y_value;
for (int i = Filtering_y; i >= 0; i–) {
y_filter_buffer[i] = y_filter_buffer[i - 1];
sum_y += y_filter_buffer[i];
}
value_filtering = sum_y / Filtering_y + 1;
return value_filtering;
}
int diff_buffer_filtering(int y_value) {
sum_diff_y = 0;
for (int i = Filtering_diff_y; i >= 0; i–) {
diff_filter_buffer[i + 1] = diff_filter_buffer[i];
sum_diff_y += diff_filter_buffer[i];
}
diff_filter_buffer[0] = y_value;
value_filtering = sum_diff_y / Filtering_diff_y;
return value_filtering;
}
int diff_funtion(int value_pre, int value) {
diff_value = (((value – value_pre) / dt));
return diff_value;
}
int integ_cacul(int value, int value_pre)
{
int tringle = 0;
integ_value = (((value – value_pre) * dt));
return integ_value;
}
/*
compare_funtion 함수 (상 하위 필터 선택, 필터횟수, 과거의 과거의 값, 과거의 값, 현재 값)으로 구성, 필터횟수가 크면 클수록 급격히 변화되는 변화량에 영향을 미칠 수 있음!! 현재의 값과 과거의 값을 비교하여 과거의 값을 교정하는 방식의 필터링. 순간적인 noise에 적합하다.
*/
int compare_function( unsigned char high_low_filter, unsigned char HWNUM, int y, int after_y, int after_after_y) {
unsigned char high_after_flag = 0×00;
unsigned char high_after_after_flag = 0×00;
unsigned char low_after_flag = 0×00;
unsigned char low_after_after_flag = 0×00;
switch (high_low_filter) {
case 0×00 :
if ((y > after_after_y) && (y > after_y)) // y 의심
{
return after_after_y;
}
else {
return y;
}
break;
case 0×08 :
if ((y < after_after_y) && (y < after_y)) // y 의심
{
return after_after_y;
}
else {
return y;
}
break;
case 0×02 : if (high_low_filter == 0×02 && high_after_flag > high_after_after_flag) {} break;
case 0×04 : if (high_low_filter == 0×04 && high_after_flag < high_after_after_flag) {} break;
}
}
int ReplaceFuction(int Pre_Pre_y, int Pre_y, int y) {
if (Pre_Pre_y < Pre_y) {
return Pre_y;
} else {
return Pre_Pre_y;
}
}
void High_Compare_operation() {
for (int i = 0 ; i <= filter_buffer_number ; i++) {
i = 2 * i;
j = (2 * i) + 1;
k = (2 * i) + 2;
Data_buffer[k] = compare_function(high_filter, filter_buffer_number, Data_buffer[k], Data_buffer[j], Data_buffer[i]);
}
}
void Low_Compare_operation() {
for (int i = (filter_buffer_number – (filter_buffer_number – Low_number)) ; i <= filter_buffer_number ; i++) {
i = 2 * i;
j = (2 * i) + 1;
k = (2 * i) + 2;
Data_buffer[k] = compare_function(low_filter, filter_buffer_number, Data_buffer[k], Data_buffer[j], Data_buffer[i]);
}
}
void Time_Count() {
tCount = micros();
t = (float)(tCount) * ts;
dt = (float)(tCount – tCountPre) * ts;
}
void Serial_out_put() {
/* Serial out put*/
// for(int j = 0 ; j <= filter_buffer_number*2 ; j++){
// printStates(Data_buffer[j]);
// }
printStates(Data_buffer[filter_buffer_number * 2]);
printStates(Data_buffer[filter_buffer_number * 2]);
printStates(mov_avr_value);
Serial.print(“\r\n”);
}
void System_opeation() {
switch (System_stats) {
case 0×00 :
digitalWrite(R_LED, LOW);
digitalWrite(Y_LED, HIGH);
digitalWrite(Y_LED1, HIGH);
digitalWrite(Y_LED2, HIGH);
digitalWrite(G_LED, HIGH);
// Serial.println(” System operation… “);
break;
case 0×02 :
digitalWrite(R_LED, LOW);
digitalWrite(Y_LED, LOW);
digitalWrite(Y_LED1, HIGH);
digitalWrite(Y_LED2, HIGH);
digitalWrite(G_LED, HIGH);
// Serial.println(” Dection_humen… Take brethe…”);
break;
case 0×04 :
digitalWrite(R_LED, LOW);
digitalWrite(Y_LED, LOW);
digitalWrite(Y_LED1, LOW);
digitalWrite(Y_LED2, LOW);
digitalWrite(G_LED, LOW);
// Serial.println(” Ok. ready_shot “);
break;
}
}
void System_timer(void)
{
System_opeation();
}
void setup() {
// put your setup code here, to run once:
PinModeSetup();
Serial.begin(115200); // error rate -3.5%
Timer1.initialize(12000); // System_timer to run every 0.2 seconds
Timer1.attachInterrupt(System_timer);
delay(100);
}
void loop() {
Data_buffer[0] = analogRead(A0);
Time_Count();
High_Compare_operation();
// mov_avr_value = mov_avr_filter(Data_buffer[filter_buffer_number*2]);
// Serial_out_put(); //화면 출력 for Test
/* 아래에 검출 이 들어와야함.*/
if (Data_buffer[0] < 265) {
System_stats = 0×00;
}
/****************** System_stats = 0×00 ******************/
if (System_stats == 0×00) {
if ((Data_buffer[filter_buffer_number * 2]) – 50 > 300) {
detection_H_Check_tCountPre = tCount;
System_stats = 0×02;
}
}
/****************** System_stats = 0×02 ******************/
if (System_stats == 0×02) {
Breath_Check_Time = 0;
detection_H_Check_Time = (float)(tCount – detection_H_Check_tCountPre) * ts;
if (detection_H_Check_Time == 1) {
Neates = Data_buffer[filter_buffer_number * 2]; // 첫번째 조건 초기값 : Neates
Neates_ = Data_buffer[filter_buffer_number * 2]; // 두번째 조건 초기값 : Neates_
}
if ((Data_buffer[filter_buffer_number * 2]) > Neates + 2) { //Neates보다 큰 값이 들어오면 Neates는 더 커진다.
Neates = Data_buffer[filter_buffer_number * 2];
Breath_Ch_tCountPre = tCount;
} else { // 위의 조건이 맞지 않으면 Breath_Ch_tCountPre 는 1초 .. 2초 세게된다.
if (Neates < Neates_ + 20) { // Nestes_ 보다 들어오는 값이 작다면 Breath_Ch_tCountPre 변동된다.
Breath_Ch_tCountPre = tCount;
}
Breath_Check_Time = (float)(tCount – Breath_Ch_tCountPre) * ts;
}
if (Breath_Check_Time == 1) { // Breath_Check_Time이 1초가 되면 shoot 상태로 가고 0×02 상태에서 벗어난다.
System_stats = 0×04;
}
}
saveStatesForNextStep(); // 항상 마지막에 위치해야 함
}
[41호] Project H – 스마트홈
2016 ICT 융합 프로젝트 공모전 입선
Project H – 스마트홈
글 | 고려대학교 최윤형
심사평
JK전자 모터로 제어되는 기구부와 집 모형, 마이크로컨트롤러와 안드로이드 앱 개발까지 다양한 분야의 지식이 필요한 작품이네요. 시스템적으로 구현이 어려운 기술들은 아니지만 서로 다른 분야의 기술들을 적절히 잘 활용하여 완성도도 높고, 곧 다가올 사물인터넷 시대의 한 분야인 홈 케어 시스템에서 필요로 하는 요소들을 1개의 작품에 잘 적용하였습니다.
뉴티씨 스마트홈을 주제로 잡고 실제로 스마트한 환경을 구현하려 매우 노력한 흔적이 보여서 보고서 완성도면에서 20점을 주었으며, 전반적으로 좋은 점수를 받았다. 하지만, 아직 실용성이 부족하며, 조금만 더 창의적인 아이디어를 나만의 것으로 소화하여 만들었다면 더 좋은 점수를 받았을 것이다. 작품의 기대효과가 높지는 않으나, 전체적인 완성도나 기술성은 높은 점수를 주었다.
칩센 20년 전부터 나오던 홈오토메이션이 IoT라는 개념으로 다시 나오는 것 같다. 그만큼 주제는 식상하다. 하지만 구현 완성도와 보고서의 내용이 뛰어나고 노력한 흔적이 보인다.
위드로봇 모형으로 집을 만들고 그 안에서 발생할 수 있는 여러 상황을 시뮬레이션하여, 핸드폰으로 각 상황에 알맞게 조치를 취할 수 있음을 보여주는 재미있는 작품입니다. 다양한 센서를 인터페이스하는 측면에서 많은 공부가 되었을 것 같습니다. 작품 전체의 완성도가 높은 수작입니다.
1. 작품 개요
지난해 국내 스마트 홈 시장 규모 6조9천억 원11.8%↑ -부산일보
구글, ‘홈 오토메이션’ 분야 신생 업체 32억 달러에 인수 -연합뉴스
애플, 인텔리전트홈 원격제어…특허 -zdnet korea
뉴스에서 보도된 내용이다. 애플 구글같은 대기업들이 관심을 보이는 분야이고 국내 스마트 홈시장은 매년 늘어갈 것이라는 전망이다. 대기업이 진출한다는 것은 그만큼 가치가 있는 종목이고 홈 오토메이션이라는 것이 중요한 기술이 될 것임을 알 수 있다.
홈 오토메이션은 스마트폰과 블루투스를 이용한 AVR과의 통신으로 모터및 센서 제어를 해보는 것이므로 JAVA를 이용한 APP개발, 또한 블루투스 통신 구현, AVR을 이용한 모터 및 센서 제어 등 기초적인 기술들이 대부분 사용되었다. 그래서 이 프로젝트의 경험을 바탕으로 할 수 있는 일들이 다양해질 것이라고 생각한다.
2. 작품 설명
2.1. 전체 시스템 구성 – 시스템 구성도(Block diagram)
2.2. 주요 동작 및 특징
2.2.1. 조도량에 따라 커튼 열고 닫힘
개요: 아침에는 햇볕이 들어오도록 자동으로 열리고 날씨가 좋지 않거나 밤에는 사생활 보호를 위해 자동으로 닫히도록 한다.
작동: GL5516(조도 센서)를 이용하여 빛의 양을 감지하고 빛의 양이 적으면 커튼을 닫히도록 하고 빛의 양이 많으면 커튼이 열리도록 한다.
2.2.2. 썬루프 원격 조절
개요: 스마트폰을 이용하여 원격으로 썬루프를 조정한다.
작동:
1. 스마트폰으로 썬루프가 열리는 정도를 조절할 수 있게 한다.
2. 날씨가 좋지 않을 시 어두운 정도를 GL5516(조도 센서)를 이용하여 측정하고 조도가 낮아 비가 오기 전 스마트폰으로 명령을 주지 않아도 자동으로 썬루프를 닫는다. 만약 구름이 있어도 썬루프를 열겠다는 신호를 보내면 “Warning: 썬루프를 여시겠습니까?”라는 문구를 띄우고 “예”를 누르면 작동한다.
2.2.3. 방범 시스템
개요 : 밤에 늦게 누군가가 집에 침입하였을 때 빠르게 112에 신고할 수 있다.
작동 :
1. 스마트폰으로 방범 시스템 작동 유무 설정
2. 방범시스템을 작동시키면 GP2D120(광 센서)을 이용해 사람 유무 판단.
3. 사람이 있다고 감지되었을 때 스마트폰으로 감지되었다고 메시지와 진동으로 표시. 그 후 112에 신고하시겠습니까?라는 문구를 띄우고 예를 누르면 112에 연결
2.2.4. 우편함 알림 시스템
개요: 밖에 있는 우편함에 우편이 있는지 없는지 직접 확인을 하지 않아도 센서를 통해 자동적으로 스마트폰 메일 알람 시스템으로 알려준다.
작동:
1. GP2D120(광 센서)를 우편함 아래쪽에서 거리를 잴 수 있도록 배치하고 우편이 놓였을 때 거리 값이 작아지는 것을 이용하여 우편 유무 확인
2. 우편이 있을 때 스마트폰으로 우편이 왔다고 메일 알림으로 표시한다.
2.3. 등각 외관도 및 작품 사진
2.4. 개발 환경(개발 언어, Tool, 사용 시스템 등)
2.5. 세부 구성
기구부
회로부 및 센서부
2.6. 소프트웨어 주요 코드
2.6.1. Atmega128 소프트웨어 주요 코드
==================================================
// Function: 외부 인터럽트를 사용하기 위한 설정
// Descriptions: 하강엣지일때 계속해서 발생
==================================================
DDRD &= ~(_BV(DDD0)|_BV(DDD1)); //INT0(PD0), INT1(PD1) 핀을 디지털 입력 포트로 설정
PORTD |= _BV(PD0)|_BV(PD1); //풀업저항 있는 입력
// 하강 일시 작동 PD0 INT0 PD1 INT1 사용
EICRA &= ~(_BV(ISC00)|_BV(ISC01)|_BV(ISC10)|_BV(ISC11));
// 하강 일시 계속 인터럽트 발생
EIMSK |= _BV(INT0)|_BV(INT1); //개별적 인터럽트 허용
창 리미트스위치를 눌렀을 때 인터럽트가 실행되도록 초기화 설정
==================================================
// Function: adc 사용하기 위한 설정(GP2D120) 거리 센서 0~3.3출력
// Descriptions: PF0(ADC0) ~PF7(ADC7)까지 있음
==================================================
DDRF=0; //풀업저항 없는 입력 사용
PORTF =0;
ADMUX |= _BV(REFS0); ADMUX &= ~_BV(REFS1);
//5v AVCC연결된 전압 AREF에 연결된 커패시터사용
ADCSRA |= _BV(ADPS2)|_BV(ADPS1)|_BV(ADPS0);
//AVR 16000KHz 80 <= X <= 160설정해야함 따라서 분주비 128 설정 125KHz
// ADC 클럭을 결정하는 비트입니다. 분주비가 작으면 변환이 빨리 되지만 사용상 문제가 없는 범위에서 최대한 느리게 변환 하는 것이 노이즈 영향이 줄어든다.
ADCSRA |= _BV(ADEN); //인에이블비트
거리 센서의 아날로그값을 디지털값으로 변화하기 위하여 ADC컨버터 초기값 설정
==================================================
// Function: cds광전도셀에서 받은 op앰프 출력을 입력시킬때 쓰는 핀
// Descriptions: cds셀 입1/0 상태 입력단자. 어두울 때 1 밝을 때 0
==================================================
DDRG &= ~_BV(DDG4);//cds셀 상태 입력단자. 어두울 때 1 밝을 때 0
PORTG &= ~_BV(PG4);//풀업저항 없는 입력
PD4에 CDS셀의 조도 값을 받아들이기 위하여 입력 설정
==================================================
// Function: 연기 센서
// Discriptions: 연기 센서 작동되면 PING2에서 5v 출력이 PING3 에 입력된다.
==================================================
DDRG |= _BV(DDG2); //출력으로 사용 사용된 곳 없음
PORTG |= _BV(PG2);
DDRG &= ~_BV(DDG3); //풀업있는 입력 사용
PORTG |= _BV(PG3);
DDRG &= ~_BV(DDG0)|_BV(DDG1); //리밋스위치용 풀업저항 있는 입력
PORTG |= _BV(PG0)|_BV(PG1);
//사용시 Pxn핀 HIGHT 전압 출력
//PORTx 레지스터의 Pxn 비트를 1로
DDRF |= _BV(DDF2);
PORTF |= _BV(PF2);
PG3에 연기센서값을 받아들이기 위하여 입력 설정
==================================================
// Function: ch 0~7중 하나를 선택해 0~5V로 변환하는 함수
// Descriptions: PF0(ADC0) ~PF7(ADC7)까지 있음
// 예를 들어 ATmega128 ADC의 reference 전압을 2.5V로 만든다면,
// ADC의 resolution은 1/1024
// 2500mV / 1024 = 약 2.44 mV
// 즉, ADC에 0V가 입력되면 0이라는 변환 값이 나오고,
// 2.44mV가 입력되면 1,
// 4.88mV가 입력되면 2, …..
// 2500mV가 입력되면 1023 이라는 ADC 변환 값이 출력됩니다.
==================================================
uint16_t GetAdv(uint8_t ch) //ch 0~7중 하나를 선택해 A/D 변환하는 함수
{
uint16_t Ain;
ADMUX = (ADMUX&0xf8)|(ch&0×7); //하위 3비트로 채널 선택
ADCSRA |= _BV(ADSC); //ADSC에 1을 넣어 S/H후 A/D 변환 시작
while(!(ADCSRA&_BV(ADIF))); //ADSC의 ADIF가 1이 되면 A/D변환 종료
//val = ADC;
//ADC로부터 변환된 값 저장
///참고: 이렇게 하면 실제로는 ADIF가 0이됨
Ain = ((uint32_t)ADC*50 + (1<<9))>>10; //Ain은 실제 전압의 10배
//Ain = ADC *5.0/1023.0;
ADCSRA |= _BV(ADIF);
return Ain;
}
ADC n번을 이용하고 그 값을 10배하여 받아들임 ex) 100값을 받았다면 10V입력
//- Wireless Module를 통해 수신된 데이터 패킷이 있으면 받아서 데이터에 따라 모터 구동
if (uwIsPacket()==UW_PACKET0)
{
if (uwGetData0(&data))
{
mtrDrive0(data);
lswPutLed(0×1); //확인용 LED 점멸
}
t_prev = tm2Getms();
}
수신한 Data 값을 가지고 모터를 PWM 제어
if(data==PROTECTON) //방범기능용
{
protectflag=0;
}
if(data==PROTECTONOFF)
{
protectflag=1;
}
방범 기능 On/Off 기능 수행
==================================================
// Function: DS_SD_002 연기 센서에 0×30 데이터 전송
// Discriptions: PING3 에서 5V입력이 확인되면
==================================================
if(!(PING & _BV(PING3)))
{
if(DS_SD_002_Flag==0){
uartPutc(DS_SD_002);
lswPutLed(0×2); //확인용 LED 점멸
DS_SD_002_Flag=1;
DS_SD_002_t_prev = tm2Getms();
}
}
else if ((tm2Getms()-DS_SD_002_t_prev)>6000) // 일정시간지나면 입력 받아들임
{
DS_SD_002_Flag=0;
}
연기 센서에 연기가 감지되면 그것을 확인하는 코드
==================================================
// Function: cds광전도셀에서 받은 op앰프 출력을 입력시킬때 쓰는 핀
// Discriptions: cds셀 입1/0 상태 입력단자. 어두울 때 1 밝을 때 0
==================================================
if(PING & _BV(PING4))
{
if(CDS_Flag==0){
uartPutc(CDS);
lswPutLed(0×4); //확인용 LED 점멸
CDS_Flag=1;
CDS_t_prev = tm2Getms();
mtrDrive0(0×4); //아니면 ox2 닫기는 left
}
}
CDS 셀에서 AVR로 입력된 값을 확인하여 선루프를 자동으로 열고 닫는다.
==================================================
// Function: adc입력상태를 확인하여 출력 0번은 MAIL
// Descriptions: 0번은 메일함 꼭대기까지 5cm 2.3정도 탐지 거리 7mm 물건이 있을 경우 1cm기준으로 잡는다. 1.8
==================================================
if(GetAdv(myAdc0)>5) //adc0번이 입력 전압이 1.8V보다 작을때
{
if(GP2D120Mail_Flag==0){
uartPutc(MAIL);
lswPutLed(0×8); //확인용 LED 점멸
GP2D120Mail_Flag=1;
GP2D120Mail_t_prev = tm2Getms();
}
}
우편함에서 우편이 감지되면 AVR로 신호를 준다.
if(protectflag==0)
{
lswPutLed(0xaa);
==================================================
// Function: adc 입력 상태를 확인하여 출력 GP2D120
// Descriptions: 추후 감지 범위 바꿔가며 정밀하게 만들 예정
==================================================
if(GetAdv(myAdc1)>20) //adc0번이 입력 전압이 2.5V보다 작을 때 30cm
{
if(GP2D120_1_Flag==0){
uartPutc(GP2D120_0);
lswPutLed(0×81); //확인용 LED 점멸
GP2D120_1_Flag=1;
GP2D120_1_t_prev = tm2Getms();
}
}
==================================================
if(GetAdv(myAdc2)>15) //adc0번이 입력 전압이 2.5V보다 작을 때 우체통
{
if(GP2D120_2_Flag==0){
uartPutc(GP2D120_1);
lswPutLed(0×90); //확인용 LED 점멸
GP2D120_2_Flag=1;
GP2D120_2_t_prev = tm2Getms();
}
}
else if((tm2Getms()-GP2D120_2_t_prev)>5000)
{
GP2D120_2_Flag=0;
}
==================================================
if(GetAdv(myAdc3)>15) //adc0번이 입력 전압이 2.5V보다 작을 때
{
if(GP2D120_3_Flag==0){
uartPutc(GP2D120_2);
lswPutLed(0×84); //확인용 LED 점멸
GP2D120_3_Flag=1;
GP2D120_3_t_prev = tm2Getms();
}
}
==================================================
if(GetAdv(myAdc4)>15) //adc0번이 입력 전압이 2.5V보다 작을 때
{
if(GP2D120_4_Flag==0){
uartPutc(GP2D120_3);
lswPutLed(0×90); //확인용 LED 점멸
GP2D120_4_Flag=1;
GP2D120_4_t_prev = tm2Getms();
}
}
각각의 센서에서 받은 값을 확인하여 방범 기능을 수행한다.
==================================================
// Function: mtrDrive0
// Descriptions: CP-RC가 전송한 Data에 따른 모터 구동
==================================================
void mtrDrive0(uint16_t Data)
{
if (Data&MTR_LEFT)
{
if(!(PING&_BV(PING0)))
{
pwmSet(PWM_CH0, 0);
}
else pwmSet(PWM_CH0, MTR0_HIGH); //CH_0모터 255 고속 값으로
}
else if (Data&MTR_RIGHT)
{
if(!(PING&_BV(PING1)))
{
pwmSet(PWM_CH0, 0);
}
else pwmSet(PWM_CH0, -MTR0_HIGH);
}
else if (Data&MTR_GOLEFTALL)
{
while((PING&_BV(PING0)))
{
pwmSet(PWM_CH0, MTR0_HIGH);//CH_0모터 255 고속 값으로
}
}
else if (Data&MTR_GORIGHTALL)
{
while((PING&_BV(PING1)))
{
pwmSet(PWM_CH0, -MTR0_HIGH);//CH_0모터 255 고속 값으로
}
}
else //모터 정지
{
pwmSet(PWM_CH0, 0);
}
}
void pwmSet(uint8_t ch, int16_t Val)
{
if (Val<-255) Val=-255;
if (Val>255) Val=255;
if (ch==PWM_CH0) // CH0 모터인 경우
{
#if PWM_EN_DIP_REVERSE
if (lswGetSwitch() & LSW_DIP1) Val = -Val;
#endif
if (Val>0) // 정방향이면,
{
PORTE&=~_BV(PE4); // L298 1A1를 LOW로
OCR3A=Val;
}
else
{
PORTE|=_BV(PE4); // L298 1A1을 High로
OCR3A=255+Val;
}
}
}
모터 제어를 위한 PWM 코드
==================================================
// Function: uwGetData0
// Description: 패킷으로부터 2바이트 데이터 추출
==================================================
uint8_t uwGetData0(uint16_t* data)
{
uint8_t sreg;
uint16_t result=0;
if (((uwData[1]+uwData[2])&0x7f)==uwData[3])
{
*data=uwData[1]+((uint16_t)uwData[2]<<8);
result=1;
}
sreg=SREG;
cli();
uwFlag=0;
SREG=sreg;
return result;
}
2바이트를 추출하기 위한 USART 소켓 통신 코드
==================================================
// Function: uwIsPacket
// Description: 패킷 전송 유무 확인, 패킷 전송시 1
==========================
uint8_t uwIsPacket(void)
{
uint8_t sreg, result;
sreg=SREG;
cli();
result=uwFlag;
SREG=sreg;
return result;
}
패킷 전송 유무 확인, 패킷 전송 시 1 리턴
2.6.2. 스마트폰 안드로이드 소프트웨어 주요 코드
if(read()==”cloud”){
Intent intent = new Intent(getApplicationContext(),
Cloudalert.class);
startActivity(intent);
}
if(read()==”fire”){
Intent intent = new Intent(getApplicationContext(),
Firealert.class);
startActivity(intent);
}
Button Call119 = (Button)findViewById(R.id.call119yesbtn);
Call119.setOnClickListener(new OnClickListener(){
public void onClick(View v){
Intent in = new Intent(Intent.ACTION_CALL);
in.setData(Uri.parse(“tel:01090577489″));
startActivity(in);
}
});
if(read()==”protect”){
Intent intent = new Intent(getApplicationContext(),
Protectalert.class);
startActivity(intent);
}
Button Call112 = (Button)findViewById(R.id.call112yesbtn);
Call112.setOnClickListener(new OnClickListener(){
public void onClick(View v){
Intent in = new Intent(Intent.ACTION_CALL);
in.setData(Uri.parse(“tel:01090577489″)); //암시적 인텐트로 전화를 건다. 매니페스트에 추가해줘야함
startActivity(in);
}
});
AVR에서 블루투스로 보내준 데이터에 따라 인텐트 생성하여 알림 창을 띄운다.
private class TransmitThread extends Thread {
int pauseCnt = 0;
byte[] packet0 = new byte[4];
public void run() {
setName(“RepeatThread”);
while (true) {
if (mBtnStatusChanged==true)
{
if (mSerialService.getState() == BluetoothSerialService.STATE_CONNECTED) {
packet0[0] = (byte) PACKET0_START;
packet0[1] = (byte) (mBtnStatus&0xff);
packet0[2] = (byte) (mBtnStatus>>8);
packet0[3] = (byte) ((packet0[1]+packet0[2]) & 0xff);
mSerialService.write(packet0);
}
if (mBtnStatus==0) {
if (++pauseCnt >= 10) {
pauseCnt = 10;
mBtnStatusChanged = false;
}
} else {
pauseCnt = 0;
}
try {
Thread.sleep(TRANSMIT_INTERVAL-1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
AVR에서 스마트폰으로 온 데이터를 받기 위한 통신 쓰레드
private String read() {
int length = buffer.length();
String data = buffer.substring(0, length);
buffer.delete(0, length);
return data;
}
스마트폰에 수신된 데이터를 buffer에 저장하고 읽어오는 함수
public void write(byte[] buffer) {
try {
mmOutStream.write(buffer);
// Share the sent message back to the UI Activity
mHandler.obtainMessage(BluetoothSerial.MESSAGE_WRITE, -1, -1, buffer)
.sendToTarget();
} catch (IOException e) {
Log.e(TAG, “Exception during write”, e);
}
}
스마트폰에서 AVR로 데이터를 보내기 위한 함수
2.7. 스마트폰 제어 화면
2.7.1. 메인 화면
ProjectH 앱을 실행하면 첫 화면에서 방범 시스템 On/Off, 썬루프 닫기/열기 기능을 설정할 수 있다.
썬루프 닫기/열기는 누르고 있는 동안 열고 닫히는 버튼이고 썬루프 전부 닫기 / 전부 열기는 한번 누르면 전부 열고 닫는 버튼이다. 방범 시스템 On/Off기능은 GP2D120 센서로 움직이는 물체를 감지하여 켜고 끌 때 사용하는 버튼이다.
그 외에 사용된 센서는 감지하는 신호를 받아 팝업 창을 띄우게 된다.
2.7.2. 화재알림기능_DS-SD-002(연기 센서)
연기 센서는 제작 업체에 따르면 일반 화장실 크기의 공간에서 담배를 태우면 연기가 감지되도록 설계되었다고 한다. 연기가 감지되었을 때 릴레이의 접점을 이용하여 신호로 전달한다. 연기가 감지되었을 때는 AVR에서 신호를 받아들여 스마트폰으로 위험을 알리는 팝업 창을 띄우고 센서 부분에서 버져가 울리게 된다.
“집에 연기가 감지되었습니다. 119에 신고하시겠습니까?”라는 문구에서 Yes를 누를 경우 바로 119에 전화가 걸리게 되고 아닐 경우 No를 누르면 메인화면으로 돌아가게 된다.
2.7.3. 방범 기능_GP2D120
GP2D120 센서의 거리에 따른 값을 0~3.3V 범위의 아날로그로 받아 AVR에서 0~5V의 디지털 값으로 바꾸어 어느 정도에 거리에 있는 물체들만 감지하도록 하였고, 화재 알림 기능과 동일하게 Yes를 누르면 112에 바로 통화가 가능하며 No를 누르면 빠져나가 메인화면으로 돌아가게 된다. 스마트폰 메인 화면에 Protect On/Off기능으로 켜고 끌 수 있다. 감지되는 위치에 따라 감지 위치 화면을 띄운다.
아래 그래프는 거리에 따른 전압의 출력 값을 나타내는 데이터시트이다. 이것을 정리해보면 두번째 표와 같다.
2.7.4. 우편 기능_GP2D120
우편이 우체통으로 들어왔을 때 센서를 가리게 된다. 그러면 GP2D120 센서의 출력 전압이 0V가 된다. 이 원리를 이용하여 전압이 0V가 되었을 때 AVR에서 스마트폰으로 신호를 전달하게 되고 노티피케이션을 통하여 사용자에게 보여주게 된다.
1) 화면 노티피케이션을 누르면
2) 화면이 뜨고 노티피케이션은 사라진다.
스마트폰에서 메시지가 오면 상단에 메시지 알림이 뜨고 클릭하면 메시지를 보는 동시에 상단 메세지알림이 사라진다. 이와 같이 최대한 메시지와 알림 및 확인 방식을 유사하게 해 직관적으로 알아볼 수 있도록 제작하였다.
2.7.5. 조도감지센서(GL5516)
조도가 자신이 원하는 정도에 기준을 정하기 위해서 OP앰프를 사용했고, -쪽(Reference 전압)으로 들어가는 전압을 조절하기 위하여 10KΩ의 가변저항을 넣었다. 이 회로는 어느 정도 어두워지면 HIGHT 값을 AVR에 전달한다. 우리 시스템에서는 우천 시 집안으로 비나 눈이 들어오는 것을 방지하기 위해 어두워지면 자동으로 창을 닫는 것으로 설정하였다.
위 회로에서 -쪽을 살펴보면 R1이 1.8k, R2가 0~10KΩ이다. 만약 CDS가 밝을 때 5KΩ이 나온다면 +에 들어가는 전압은 5/(10+5), 즉 VCC에 약 1/3 1.6V정도가 된다. 가변 저항의 값은 1.6 =< 가변저항값/(1.8+가변저항값)으로 최소값을 결정해주면 된다.
2.7.6. 썬루프 창 부분 (KGU-3429(DC 12V))
회전운동을 직선운동으로 바꾸어줄 리니어모터를 사용하였다. 메인 화면에서 썬루프 닫기나 썬루프 열기를 누르고 있을 때 닫히거나 열리게 되고, 썬루프 전부 닫기나 전부 열기를 한 번 눌렀을 시 전부 닫히거나 열리게 된다. 리니어모터가 지정된 범위에서만 움직이도록 양 끝단에 리밋스위치를 달아 범위를 제한했다.
3. 단계별 제작 방법
3.1. 모터 기구부 제작
모터가 일직선상으로 직선운동을 원활하게 하기 위해서 직선의 홈을 만들어 그 직선을 따라 이동하도록 제작하였다. 빨간색 부분에 파여진 홈에 모터의 신호에 따라 좌우로 작동을 한다. 파란색 사각형 부분은 리미트센서가 들어가도록 홈을 제작하였다. ‘썬루프 전체 열기’를 실행하면 모터가 이 두 리밋센서 사이에서만 움직이도록 제한하였다.
3.2. 전체 외형 제작
위와 같이 SolidWorks 2010으로 레이저 가공을 위한 1:1 도면을 만들었다. 나사 구멍과 지붕의 기울기를 고려하며 시스템상 충돌이 나지않고 어셈블리가 되도록 하였다. 어셈블리가 되지 않으면 도면을 따라 레이저 프린터 가공을 하였을 때 나사구멍이 맞지 않아 조립이 되지 않으므로, 신중을 가했다.
다음으로는 솔리드웍스의 1:1 도면을 각각 부품에 대하여 CAD 파일로 내보내기를 한다. 내가 사용한 레이저 가공기가 Solid Work는 지원하지 않고 AutoCAD를 통한 1:1 도면 프린트만 가능했기 때문이다. 아래는 실제로 레이저 프린터로 아크릴을 가공했을 때 사용했던 AutoCAD 도면이다.
3.3. 소프트웨어
안드로이드 앱 프로그래밍은 처음이라 참고할 도서를 선정하여 공부하며 따라했다. 대표적인 책으로 ‘Do it 안드로이드 앱 프로그래밍’ 책이 있었고 Bluetooth chat 부분의 통신 부분을 가지고 블루투스 부분을 구현하게 되었다. 블루투스를 구현한 후 AVR에서 스마트폰으로 넘어오는 데이터 값에 따라 서로 다른 화면을 띄워줄 필요가 있었고 그 화면 구성을 인텐트로 각각의 Layout을 지정해주었다. 송수신 데이터 값이 중간에 변경되지 않고 정확하게 전달하기 위해서 public static final int 값으로 지정하고 문자로 지정해줌으로써 쉽게 통신하도록 해주었다. 데이터 송수신을 소켓통신 방법을 사용하고 오류검정코드 packet0[3] = (byte) ((packet0[1]+packet0[2]) & 0xff);를 통해 각각의 비트를 검사하며 데이터의 손실이 일어나도 그 데이터는 버리도록 만들어 통신 신뢰도를 높였다. AVR에서는 ADC 변환, 모터 제어를 위한 PWM, USART, 센서값 받기 등 센서 처리와 통신을 위한 코드를 만들었다.
4. 참조 자료
• Do it 안드로이드 앱 프로그래밍 젤리빈 4.2.2판
• 기적을 부르는 안드로이드 통신 프로그래밍
[41호]프릭스봇 Freaks Bot 만능 메인보드 Review
프릭스봇 Freaks Bot 만능 메인보드 Review
글 | 금강초롱 blog.naver.com/crucian2k3
나만의 독창적인 선풍기를 하나 만들어 보고픈 꿈이 있다고 가정해 봅니다. 단순화시켜서 생각해 보면 모터에 날개만 달면 바람을 일으키는 무언가는 쉽게 만들 수 있을 것 같습니다. 좀 더 그럴싸한 물건으로 탄생되기를 원한다면 리모컨으로 바람의 세기나 방향을 조절하는 기능을 넣을 수도 있을 것입니다.
이번엔 생각을 조금 더 확장시켜 간혹 선풍기에 아이들이 손가락이나 연필 같은 것을 집어넣어 다치거나 하는 등 사고가 나기도 하는데, 이런 문제를 극복해 보기위해 바람이 나오는 것은 충실하되 날개를 본체 아래에 살짝 감춰 봅니다. 이 정도면 모터에 바람개비가 붙어 있는 단순한 선풍기에 비해 꽤나 진보적인 선풍기가 될 것입니다.
그렇습니다. 이미 나와 있는 기술들을 결합하고 자신만의 독창성을 부여시킨다면 좀 더 이색적이고 인류생활에 보탬을 줄 수 있는 그런 물건이 만들어 질 수 있을 것입니다. 어찌보면 인류의 삶 그 자체가 이러한 노력의 과정이자 진행형일 수도 있겠다고 생각해 봅니다. 오늘 소개해 드리는 프릭스봇은 움직이는 뭔가를 조종해 보는데 필요한 어지간한 것들을 몽땅 쑤셔 넣은 마치 ‘스위스 밀리터리 나이프’ 같은 보드라고 봅니다.
사실 아두이노 우노 보드 한장 달랑 들고는 무엇인가를 해보려 할 때 상당한 실망감을 느낄 수도 있습니다. 이는 베이스가 되는 보드 그 이상도 이하도 아니기에 좀 더 그럴싸한 실험을 해보고자 한다면 필연적으로 쉴드 보드를 필요로 합니다.
일전에 필자는 2휠 밸런싱 로봇을 만들기 위해 ‘아두이노 M0’ + ‘스테빌라이저 쉴드’ + ‘블루투스 모듈’을 결합하여 시도해 본 일이 있었습니다. 스테빌라이저 쉴드 보드는 DC 모터 드라이브 회로와 MPU6050 등 6축 자세제어용 칩이 부가된 보드로 상당한 기능집약이 이뤄진 보드에 속합니다. 이렇듯 아무리 사용하기 편리한 아두이노 보드라 하더라도 뭔가를 제작해 보려 하면 쉴드보드를 겹겹이 쌓거나 포트에 주렁주렁 매달아야 하는 것은 자명합니다. 오늘 소개코자 하는 프릭스봇(FreaksBot) 메인보드는 상당히 여러 가지의 센서류, 입출력 장치들을 내장하고 있어 마음먹은 바를 매우 빠르게 실현할 수 있도록 지원을 해주고 있습니다. Freaks를 한국말로 뭐라고 해야 할지 궁리하다가 ‘만능’이라는 이름을 붙여 봤습니다만 다소간의 어색함을 느낍니다.
이번 리뷰는 (주)엔티렉스의 지원을 받아 작성하게 되었습니다.
1. 개봉기
프릭스봇을 제작한 ElecFreaks사가 어떤 곳인지 궁금하여 홈페이지를 둘러보았습니다. 중국판 실리콘밸리라고도 일컷는 심천에 소재한 회사로 여러 가지 측면에서 상당히 흥미로움을 주는 곳 같습니다. 우선 홈페이지(http://www.elecfreaks.com)가 다소 독특하여 리사이즈를 하면 일반홈페이지 스타일로 보여주나 화면사이즈를 변경하면 모바일이나 테블릿에서 볼 때도 어색치 않은 타입으로 자동변형이 일어나도록 만들어져 있는 것을 알 수 있습니다.
이것이 뭐 그리 대단한 기술은 아니지만 소소한 부분까지 사용자를 배려해 놓은 세심함에 대해서는 좋은 점수를 주고 싶습니다.
이 회사는 우리나라 같으면 벤처회사 같은 곳으로 보여지며 자신들을 오픈 디자인 하우스라고 부릅니다. 신속한 프로토타이핑 제품을 개발해 주거나 소량 다품종을 제조하는 일을 한다고 하니 벤처회사들을 지원하는 더 세분화된 벤처회사 같은 인상을 줍니다. 암튼 고리타분, 군살, 비대한 조직 등과는 거리가 먼 특성 있는 곳으로 보여지는 회사답게 회사이름도 Freak(~쟁이, ~꾼)라는 단어를 사용하였고 이
회사에서 발매중인 대표적인 제품중 하나인 이 보드가 아래와 같이 마치 붕어빵봉지를 연상시키는 봉지에 덩그러니 담겨져 있음을 알 수 있습니다.
홈페이지에 올려진 제품 홍보자료도 마치 건빵포대에 인쇄를 한 듯한 형태로써 회사이름에서부터 설명 자료까지 모든 것이 파격의 연속인 그런 회사의 제품입니다.
보드는 4개의 바퀴를 가진 제품과 어울리는 형상을 갖고 있습니다. 일부러 보드사이즈를 키워 다루기 편하도록 디자인 한 것으로 보여집니다.
2. 특징과 용도
일반적으로 아두이노 기반으로 4바퀴를 독립적으로 제어하고자 하는 목적에 최적화된 보드임을 금방 알 수 있습니다.
4바퀴를 독립적으로 제어할 수 있으므로 옴니휠을 사용해 자동차의 조향을 회전속도와 방향 컨트롤을 통해 제어할 수 있으며, 꽃게처럼 옆으로 이동하는 차량로봇도 손쉽게 만들어 볼 수 있습니다. 또한, 4개의 독립된 모터를 제어하는 것이 가능하므로 잘 응용하면 쿼드콥터 같은 비행물체도 어렵지 않게 제작이 가능할 것으로 생각됩니다.
3. 제품 스펙 살펴보기
보드 레이아웃 : http://www.elecfreaks.com/wiki/index.php?title=FreaksBot |
4. 회로 살펴보기
4.1. USB to Serial 부문
오리지널 아두이노 메가 2560에서는 ATmega16U2를 사용하여 컴퓨터와 인터페이싱을 한 후 재차 ATmega2560과 TTL로 연결되는 방식으로 설계가 되어 있습니다만, 프릭스 봇에서는 매우 독특한 방식으로 이 부분을 처리한 것을 알 수 있습니다.
한마디로 1타2피 전법을 사용하고 있는데요, 바로 블루투스 4.0을 커버하는 TI의 CC2540 칩셋에게 USB to Serial 기능까지 부여시킨 것입니다. 이는 최소의 비용으로 최대효과를 내려하는 노력의 산물이 아닌가 합니다. 회로상으로는 블루투스를 통한 업로드도 충분히 가능할 것으로 보입니다.
4.2. LED 주변부
A15에 WS2812 2개를 직렬로 연결하여 상태를 표시를 할 수 있도록 설계를 해 놓았습니다. 이는 RGB로 표현될 수 있으므로 재미있는 상태표시가 가능할 것으로 보입니다. U6은 EEPROM으로 생각됩니다만, 품번 식별이 어렵습니다.
4.3. 전원부
전형적인 Step-down형 전원부 회로이며 외부 입력전원은 VIN으로써 최소 7V는 되어야 안정적으로 5V 생성이 가능합니다. 전원 효율성과 큰 돌입 전류, 노이즈 등이 우려되는바 이러한 상황에서도 안정적으로 전원을 공급하기위해 노력한 흔적들이 보입니다.
USB-B 커넥트를 통해 인입되는 5V와 외부전원에 의해 인가되는 5V 사이에서 안정적인 전원 공급을 위해 컴퍼레이터와 MOSFET을 사용하여 일단 VIN, 즉 외부전원이 우선시 되도록 설계가 되어 있음을 알 수 있습니다.
4.4. 모션센서부
모션센서는 너무도 유명한 MPU6050칩을 사용하고 있습니다.
D21/SCL과 D20/SDA핀을 이용하여 자이로센서와 가속도센서 정보를 얻어오게 됩니다. (ADDR : 0×68, AD0 : Low)
경험에 의하면 이러한 모션센서는 가능한 한 바퀴축과 수평을 이루는 지점에 설치하여야 가장 안정된 신호를 입수할 수 있었습니다.
프릭스봇은 사이즈 특성상 최하단부에 Base로 배치함이 바람직해 보입니다.
4.5. 모터제어부
TB6612칩은 DC모터 2개를 동시에 컨트롤할 수 있습니다.
VIN은 최대 15V까지 받을 수 있으나, 12V이내에서 동작시킬 것을 권장합니다. 출력전류는 평균 1.2A까지 흘릴 수 있으며 피크전류는 최대 3.2A입니다. 외부에서 인가되는 전원을 PWM 신호에 의해 드라이빙하는 회로로 통상 모터드라이버라고 부릅니다.
5. 보드 활용 실전
이 제품은 초심자를 위한 제품으로는 보여지지 않는바, 예제 역시도 아두이노 개발환경에 대해 어느 정도의 이해는 갖추고 있다는 전제하에 어떻게 쓸지는 전적으로 개인이 알아서 할 것을 암시하는 듯하게 리소스페이지가 만들어져 있습니다.
ELECFreaks사에서 예제로 3가지 형태를 제시하고 있으며 각각의 프로그램을 설치해보고 화면상으로 테스트가 가능한 부분에 대해서는 하는데까지 접해 보고자 합니다.
5.1. 메카넘차량
이 데모는 메카넘이라고 하는 독특한 형상의 바퀴를 장착한 후 4바퀴의 방향을 적절히 통제함으로써 전진, 후진, 좌, 우, 동북, 북서, 남서, 남동, 시계 방향 및 반 시계 방향으로 차량을 이동 시킬 수 있게 됩니다.
곁들여 자이로센서와 가속도센서를 이용하거나 블루투스 앱으로 차량을 제어할 수도 있을 것으로 봅니다.
■ 테스트코드 돌려보기
우선 아래 주소에서 zip 파일을 다운받은 후 아두이노 IDE가 설치된 폴더의 libraries 폴더에 복사해 넣습니다.
http://www.elecfreaks.com/estore/download/FreaksMeca_SourceCode.zip
디렉토리의 *.c, *.h파일이 서로 종속적인 관계에 있으므로 위 폴더중 하나라도 누락되면 에러를 일으키게 됩니다.
일단 위와 같은 실험을 해보기 위해서는 매카넘 휠세트가 필요하나 여러 여건상 어려움이 있어 다른 예제인 2휠 균형제어 로봇을 실험해 보기로 합니다.
이 메카넘 데모는 4바퀴의 회전방향을 통제함으로써 전진, 후진뿐만 아니라 좌우 양방향으로도 이동이 되는 재미나는 이동체를 만들어 볼 수 있습니다.
5.2. 2휠 균형제어로봇
흔히 밸런싱 로봇이라고도 부르며, 최근의 드론이 출현하기 전까지만 하더라도 많은 전자공학도들에게 호기심을 불러 일으키는 주제 중 하나였습니다.
균형제어 로봇은 세그웨이라는 제품명으로 유명한 두 바퀴로 균형도 잡고 방향도 전환하는 이동수단의 일종이라고 볼 수 있겠습니다.
프릭스봇 메인보드에는 균형제어로봇을 실험해 보는데 필요한 센서, 구동부를 모두 포함하고 있기에 외부에 모터와 전원부만 연결하면 금방 테스트가 가능합니다.
5.2.1. 프레임 등 기구부 준비
기구부는 전자실험을 진행할 때 늘상 고민거리를 안겨주는 대표적인 부분에 해당합니다.
필자는 디바이스마트에서 발매하는 「Self-Balancing Car Starter Kit」를 구입하여 바퀴, 모터, 프레임 등 기본적인 요소를 완성할 수 있었습니다. 이 키트의 핵심은 DC모터라고 생각합니다.
이 키트에 포함된 JGA25-26CPR DC모터는 DC12V가 정격입력전압이며 1:34의 감속기어가 들어가 있습니다.
무부하 시 126RPM이 나오나 실험을 해본 결과 너무 감속비가 크다는 사실을 확인하여 좀 더 낮은 감속비를 가진 모터를 물색하였습니다.
이렇게 물색하여 마땅한 모터를 발견하였습니다.
이전 모터와 변경한 모터의 스펙을 비교해 보면 아래와 같습니다.
■ 변경한 모터외관
이 스펙 비교표에서 눈여겨 볼 부분이 기어감속비와 무부하 회전속도입니다. 이는 얼마나 빠릿하게 균형을 잡을 수 있는가와 직결된 부분으로 반응속도가 빠르면 빠를수록 균형을 잡는데 유리합니다. 너무 감속비가 작으면 힘이 없게 됩니다.
아래 사진은 프레임에 프릿스봇 메인보드를 안착한 것입니다.
M3커넥터를 좌측편모터로 연결하고 M2커넥터를 우측편모터로 연결하면 됩니다.
아래는 모터와 프릭스봇 메인보드간을 연결하는 케이블 결선도입니다.
3번 핀과 5번 핀에서는 홀센서 출력이 나오며 각 펄스의 상승엣지와 하강엣지를 모두 사용할 때 최고의 각도 분해능을 얻게 됩니다.
엔코더는 1회전당 48Count가 얻어지고 기어비가 20.1이 되므로 48*21 = 979.2개의 펄스가 얻어지게 됩니다. 다음으로 배터리를 수납할 받침대를 보드 위쪽에 위치시킵니다.
할 수만 있다면 배터리를 무게중심이 낮은 곳에 배치하는 것이 좋다고 봅니다.
배터리 팩은 18650타입의 2600mAh 리튬폴리머배터리 3개를 직렬로 연결하여 3.7v*3 = 11.1V 전원공급기를 만들었습니다.
배터리 극성 단자에 스폿용접이 되어 있는 배터리를 사용하면 매우 쉽게 배터리 팩을 만들 수 있습니다. 프릭스봇 VIN단자로 외부전원을 공급할 예정이므로 3.5mm DC 아답터잭을 연결해 두었습니다.
이렇게 하여 조립된 완성본의 외관입니다.
원본 소스코드에 단 한 줄도 변경을 가하지 않았는데도 바로 균형을 잡아냅니다.
필자도 사실은 잘 동작이 될 것인지 반신반의 하였습니다만 밸런싱로봇 예제프로그램과 pololu사 20.4:1 Metal Gear motor가 궁합이 잘 맞은 것 같습니다.
5.2.2. 프로그램
개발환경은 Microsoft Visual Studio상에서 vMicro라고 하는 아두이노 개발환경을 이용하였습니다. 여러개의 파일이 복잡하게 엮이는 프로젝트인 경우 아두이노 오리지널 개발환경은 불편하기 짝이 없습니다.
아래와 같은 환경을 만드는 방법은 필자의 블로그에 올려져 있으므로 참고 바랍니다.
프릭스봇 밸런싱로봇 프로그램은 크게 3개의 *.ino 파일로 구성되어 있습니다.
여러개의 ino 파일을 한 디렉토리에 모아놓고 컴파일하면 자동으로 소스통합 컴파일이 일어납니다.
■ 파일목록
■ BlancdCar.ino
/***************************************************************
*
* FREAKS BOT MAIN 보드를 활용한 균형제어로봇
*
****************************************************************
* ProjectName: BlanceCAR
*
* Dependencies: Arduino1.6.9
*
* Processor: ATmega2560
*
****************************************************************/
#include <avr/io.h>
#include <EEPROM.h>
#include “config.h”
#include “def.h”
#include “ElecfreaksCar.h”
static uint32_t currentTime = 0;
static uint16_t previousTime = 0;
// us(마이크로초) 단위의 숫자로 루프시간을 의미하며 PID측 루틴에서 조절됨
static uint16_t cycleTime = 0;
// 캘리브레이션은 메인루프쪽에서 수행되며 매싸이클마다 0으로 내려가면서 보정이 이뤄짐
uint16_t calibratingA = 0;
uint16_t calibratingG = 0;
// 1G 가속도 측정모드
static uint16_t acc_1G;
static uint16_t acc_25deg;
static int16_t gyroADC[3],accADC[3],accSmooth[3],magADC[3];
int16_t heading; // [-180;+180]
static uint16_t i2c_errors_count = 0;
static ElecfreaksCar elecfreaksCar;
USE_LED;
static struct {
uint8_t currentSet;
int16_t accZero[3];
int16_t magZero[3];
uint8_t checksum; // MUST BE ON LAST POSITION OF STRUCTURE !
} global_conf;
uint8_t SMALL_ANGLES_25 = 0;
uint8_t CALIBRATE_MAG = 0;
int16_t angleTrim[2]={30, 0};
// **************
// gyro+acc IMU
// **************
int16_t gyroData[3] = {0,0,0};
int16_t gyroZero[3] = {0,0,0};
int16_t angle[2] = {0,0}; // absolute angle inclination in multiple of 0.1 degree 180 deg = 1800
void setup() {
Serial.begin(115200);
// pinMode(13, OUTPUT);
// LED_OFF;
initSensors();
previousTime = micros();
// this speeds up analogRead without loosing too much resolution:
// http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1208715493/11
ADCSRA |= _BV(ADPS2);
ADCSRA &= ~_BV(ADPS1);
ADCSRA &= ~_BV(ADPS0);
int val = 0;
for(int i=0; i<sizeof(global_conf); i++){
val = EEPROM.read(i);
*(((uint8_t *)&global_conf)+i) = (uint8_t)val;
}
}
// ******** Main Loop *********
void loop () {
static uint32_t rcTime = 0;
// static unsigned char rcBuf[64];
// uint16_t rcLen = 0;
// if((rcLen = Serial.available()) > 0){
// Serial.readBytes(rcBuf, rcLen);
// elecfreaksCar.recievedData(rcBuf, rcLen);
// }
if (currentTime > rcTime ) { // 50Hz
rcTime = currentTime + 20000;
} else { // not in rc loop
static uint8_t taskOrder=0; // never call all functions in the same loop, to avoid high delay spikes
if(taskOrder>4) taskOrder-=5;
switch (taskOrder) {
case 0:
taskOrder++;
#if MAG
if (Mag_getADC()) break; // max 350 (HMC5883) // only break when we actually did something
#endif
case 1:
taskOrder++;
#if BARO
if (Baro_update() != 0 ) break;
#endif
case 2:
taskOrder++;
case 3:
taskOrder++;
case 4:
taskOrder++;
#if SONAR
Sonar_update();
#endif
break;
}
//Serial.println(taskOrder); //debug
}
computeIMU();
// Measure loop rate just afer reading the sensors
currentTime = micros();
cycleTime = currentTime – previousTime;
previousTime = currentTime;
// 매3초마다 방향정보를 반영한다.
if(currentTime > 3000 && currentTime < 3100){
elecfreaksCar.setHeading(heading);
}
elecfreaksCar.balance((float)cycleTime/1000.0f);
}
6. 총평
이번에 소개한 프릭스봇 메인보드는 4개의 모터를 구동시키는 장치를 제작하는데 매우 적합토록 만들어진 제품임에 부족함이 없을 듯합니다. 원격제어에 유용하게 적용할 수 있도록 블루투스 모듈과 적외선 수신센서까지 실장된 것을 보면 고심한 흔적이 역력합니다.
아쉬움이라고 한다면 첫 번째로 기술문서가 충분히 지원이 되지 않고 있는 점입니다. 제조사의 공식 홈페이지, 위키싸이트 어느 곳에서도 메인보드에 대한 자세한 설명을 찾기가 힘들었습니다.
두 번째로는 튜토리얼이 매우 부족하다는 점입니다.
보드상의 Fullcolor LED, 스피커, 적외선 센서, 블루투스 모듈 등 여러 디바이스들에 대해서 확인해 볼 수 있는 API를 제공하고 이를 통해 진단기능을 수행하는 등의 예제 정도는 충분히 제공도 할 것 같습니다만 2017. 3월초 현재 이러한 정보는 찾을 수가 없습니다. 아마도 출시된 지 얼마 되지 않은 제품임에 따라 아직 충분한 자료를 제공하고 있지 않은 것으로 여겨지며 약간의 시간이 지나면 다양한 예제들이 제시될 것으로 예상해 봅니다.
그러함에도 매우 간편하게 모터로 구동되는 제어장치를 완성할 수 있기에 프로토타이핑 성격의 RC카에 도전코자 한다면 충분히 가치있는 선택이 된다고 봅니다.
이번 리뷰를 작성하며 예제가 충분치 않아 원제조사 측에 도움까지 청해가며 작성을 해보기도 하였고 덩그러니 보드만 설명하자니 아무래도 아쉬움이 많아 실제로 동작되는 2휠 밸런싱로봇까지 제작을 해봤습니다.
원격조정로봇이나 밸런싱로봇 등에 관심이 있는 독자라면 프릿스봇 메인보드는 한번쯤 다뤄 볼 만한 괜찮은 아이템이 아닌가 여겨지며 특히, 학생들을 위한 2휠 균형로봇 제작실험 목적으로 상당히 유용성이 돋보이는 제품이라고 생각해봅니다.
감사합니다.
■ 참고자료
1. 프릭스봇 공식 홈페이지
http://www.elecfreaks.com/estore/freaksbot.html
2. 프릭스봇 위키페이지(기술문서제공)
http://www.elecfreaks.com/wiki/index.php?title=FreaksBot
3. 2휠 균형로봇 관련 예제파일
http://www.elecfreaks.com/wiki/index.php?title=FreaksRobo_-_EN
4. 셀프 밸런싱카 스터터 킷 발매사
http://www.devicemart.co.kr/1272033
5. Pololu DC Motor 홈페이지
https://www.pololu.com/product/3215
[41호]ABT_System(Automatic Bicycle Transmission System)
2016 ICT 융합 프로젝트 공모전 입선
ABT_System(Automatic Bicycle Transmission System)
글 | 단국대학교 김승권, 서진욱, 이규호, 김영태, 최재호, 김재도
심사평
JK전자 최근 자전거를 이용하는 인구가 급격하게 늘어난 것은 사실이다. 대부분이 건강을 위한 운동이 목적일 것이다. 특히 자전거 운동은 런닝보다 무릎관절 등에 무리가 덜 가는 운동으로 알려져 있어 높은 연령층에서도 많이 이용을 하고 있는 것 같다. 관절 건강을 위한 최적의 속도와 기어비를 자동으로 조정해서 패달을 밟는 최적의 강도를 자동으로 설정을 해주는 기능이 있는 자전거라면 많은 수요가 있을 수 있을 것 같다.
뉴티씨 매우 실용적인 작품으로, 자전거가 외부에서 타는 것임을 감안하여 방수나 모터를 좀 더 튼튼한 것으로 사용하여 오랜기간 사용할 수 있도록 했으면 좋았을 것 같다. 충분히 실용화하여 사용할 수 있는 것으로 보이며, 재미있는 작품으로 보인다.
칩센 점점 수요가 늘고 있는 자전거를 이용한 모티브가 돋보입니다. 케이스를 직접 제작하는 열의와 작품의 완성도 또한 높다고 판단됩니다. 세부적으로 제품의 안전과 관련된 부분을 고려하여 보완한다면 상용화된 작품으로도 진행될 여지가 있습니다.
위드로봇 보고서의 내용 만으로는 잘 동작하였는지 판단하기 어렵습니다. 제작한 과정 뿐만 아니라 동작 상에서 발생하는 추가 문제점, 그 추가 문제점의 해결 방안에 대한 의견이 보고서에 첨부되면 더욱 좋겠습니다.
1. 작품 제목
ABT(Automatic Bicycle Transmission)_System
2. 작품 개요
자전거는 1800년대에 최초로 발명된 이래 인류 역사상 가장 멋진 발명품 중 하나이다. 2015년 기준으로 자전거 인구는 1200만명을 넘어섰으며 자동차와는 달리 연료없이 순수 인간의 동력으로만 구동 가능하다는 장점이 있어 친환경적이고 굉장히 경제적이다.
자전거는 본래 단순한 이동수단이었지만 철인 3종경기나 트라이애슬론과 같은 큰 경기가 열릴 정도로 인기가 높아졌다. 점점 더 가볍고 튼튼한 자전거가 개발되고 있으며 최근에는 버튼만으로도 변속이 가능한 자전거가 개발되었다. 변속은 자전거 주행을 할 때 안전과 연관된 부분이기 때문에 현재 달리고 있는 속도와 경사에 맞게, 빠르게 변속을 해야한다. 자전거에 익숙하지 않은 사람들은 변속하는 시점을 잘 모르기 때문에 무리하게 자전거를 탈 경우 무릎손상을 초래할 수도 있다. 현재 시중에 판매되는 자전거 중에서는 편리하고 기능이 많이 들어간 자전거 일수록 매우 비싸다. “비싼 자전거만 좋고 튼튼해야 할까?”라는 편견을 깨고 어느 자전거에나 장착이 가능한, 누구나 쉽게 변속을 할 수 있는, 편리한 자전거를 만들고 싶었다. 그래서 ABT_System을 제작하게 된 것이다.
3. 작품 설명
3.1. 개발환경
· 개발 언어 : C 언어, C++
· 개발 Tool : Arduino Sketch
· 사용 시스템 : 아두이노
현재 시중에 판매되고 있는 전자식 변속기는 크게 내장형 배터리 / 변속, 브레이크 레버 / 앞 변속기 / 뒷 변속기 / 통신케이블 / 배터리로 구성되어 있다. 물론 전자식 변속기를 장착하기 위해서는 장착할 수 있는 모델을 따로 구매해야하기 때문에 금전적인 문제가 상당히 크다. ABT_System을 장착하기 위해서는 자전거에 장착할 부분의 크기만 알면 되기 때문에 장착에 제한이 없다. 또한 구성품이 앞·뒤 변속기, LCD, 배터리, 컨트롤러, 스위치만 있으면 되기 때문에 구성품이 적으며, 장착이 간단하다.
ABT_System에는 LCD가 장착되어 있어 현재 변속기의 위치를 표시하며, 케이던스(1분당 페달을 밟는 횟수)와 속도를 동시에 확인할 수 있다. 바퀴와 페달에 장착된 마그네틱 스위치를 이용해 속도와 케이던스를 실시간으로 측정하며, 6축 가속도 센서를 이용하여 경사를 측정한다. 컨트롤러로 사용한 아두이노 나노를 통해 값을 계산한다. 이렇게 계산한 케이던스와 속도를 기준으로 적절한 기어비를 계산해 아두이노 메가로 기어를 변속하라는 명령을 전송한다. 전송받은 데이터를 기준으로 앞, 뒤 액추에이터 서보는 변속이 가능한 각도로 변경되며, 드레일러를 밀어 기어를 변속하게 된다.
3.2. 주행모드
ABT_System에는 총 3가지 모드가 존재하여 사용자가 원하는대로 변속이 가능하다.
3.2.1. One Finger Mode
One Finger Mode에서는 한 쪽 버튼만으로 변속이 가능하도록 소스코딩을 하였다. 이에 설정한 기어비는 앞, 뒤 변속기의 기어비를 계산하여 최적의 기어비로 변속이 가능하도록 설정하였다.
가장 표준 자전거는 앞 기어는 3개, 뒷 기어는 7개의 기어로 구성되어 있으며, 총 21단으로 변속이 가능하다. 자전거의 기어는 앞, 뒤 기어를 적절히 변속해야 현재 달리고 있는 환경에 따라 원활하게 주행이 가능하다. 예를 들어 앞 기어의 가장 작은 톱니를 1번, 가장 큰 톱니를 3번이라고 가정하고, 뒷 기어의 가장 작은 톱니를 1번, 가장 큰 톱니를 7번이라고 가정하자. 앞 기어가 1번일 때는 경사를 오르거나 저속 주행을 할 때 사용하는 데 뒷 기어는 5, 6, 7번 기어를 사용하는 것이 가장 적절하다. 즉 뒷 기어의 1, 2, 3, 4번은 사용할 확률이 적다는 뜻이다. One Finger Mode에서는 가장 적절한 기어비로 변속을 하는 것을 목적으로 하기 때문에 앞, 뒤 기어의 기어비를 계산하여 11가지의 경우의 수로 변속을 하게 된다.
3.2.2. Manual Mode
Manual Mode는 수동 변속 모드로 자동변속 기능을 필요로 하지 않고 사용자가 원하는 대로 즉각적으로 변속할 수 있는 설정이다. 좌측 버튼으로는 앞 기어의 변속을 담당하며, 우측 버튼으로는 뒷 기어의 변속을 담당한다. LCD에는 현재 변속 위치를 표시하기 때문에 사용자가 변속을 하고 싶을 때는 버튼을 눌러 실시간으로 변속이 가능하도록 설정하였다. 그렇다면 변속을 할 때 변속기의 위치는 고려하지 않을까? 일반 자전거의 변속기는 변속기 속선을 당기는 정도에 따라서 변속기의 위치를 세밀하게 조절이 가능하다. 변속기의 위치를 고려하지 않으면 체인이 대각선으로 연결되어 소음을 발생시키게 되는데 이 때 트리밍(Trimming)을 통해서 변속기의 위치를 조절해줘야 한다. 그래서 Manual Mode에서나, Automatic Mode, One Finger Mode에서도 트리밍이 가능하도록 코딩을 하여 변속을 할 때 소음이 발생하지 않도록 액추에이터가 이동한다. 트리밍을 하지 않으면 변속기의 플레이트가 닳아 손상되며, 체인에도 큰 영향을 미친다.
3.2.3. Automatic Mode
Automatic Mode는 ABT_System에 장착된 기능을 모두 활용하는 모드로 주행각도, 주행속도, 케이던스를 측정하여 현재 페달링 속도와 주행속도, 노면의 기울기를 모두 고려하여 가장 적절한 기어비로 변속을 할 수 있도록 설정하였다. 1차로 기울기, 2차로 주행속도, 3차로 케이던스를 고려한다. 변속을 할 때 가장 중요한 것이 노면의 기울기이다. 주행속도와 케이던스를 먼저 고려하게 되면 노면의 기울기에 따라서 변속이 먼저 이뤄지지 않기 때문에 안정성이 낮아진다.
만약 내리막을 주행하고 있으며 케이던스가 0인 상태에서는 어떻게 될까? 일정 수준의 케이던스를 넘지 않으면 변속을 하지 않으며, 변속을 하던 도중에 페달링을 멈추면 케이던스가 0이 되기 때문에 변속을 하지 않도록 설정하였다. 만약 케이던스가 0이더라도 주행 중이거나 정지하는 경우에는 이전에 유지한 변속 값을 유지하기 때문에 갑자기 변속이 되지 않는다.
3.3. LCD 모듈
3.3.1. 부품리스트
3.3.2. 부품 설명
캐릭터 LCD : 캐릭터 LCD는 그래픽 LCD와는 달리 문자 하나당 40개의 픽셀로 이뤄져 있으며, 아두이노나 Atmega128과 같은 MCU에서는 헤더파일에 기본적인 알파벳 코드가 작성되어 있어 함수를 호출하여 문자를 입력하면 바로 LCD에 글자를 띄울 수 있다.
MC7805CT : MC7805CT는 회로에서 요구하는 전압이 있어 일정 수준 이하로 전압을 내릴 때 사용하는 리니어 레귤레이터이다. 정전압 레귤레이터에는 스위칭 방식인 SMPS, 가장 많이 사용되는 리니어 레귤레이터가 있다. 리니어 레귤레이터는 전위차를 열에너지로 소모하기 때문에 에너지 효율은 좋지 않지만 회로구성이 간단하고 요구전압과 입력전압의 차이가 많이 나지않을 때 사용된다. 모델명의 뒷자리 숫자, 즉 78XX는 변환하고자하는 전압을 의미하며 우리가 사용하는 7805는 5V로 전압을 내려주는 리니어 레귤레이터이다.
아두이노 나노 : 아두이노 나노는 아두이노 프로 미니 다음으로 작은 모델이다. 아두이노 우노나 메가와 같은 모델은 크기가 크기 때문에 한정된 크기에서 MCU를 요구할 때는 적합하지 않다는 단점이 있다. 때문에 아두이노 나노나 프로 미니, 마이크로와 같은 모델을 사용하며, Atmega328을 사용하여 아두이노 우노나 마이크로, 프로 미니와 다른 점이 거의 없다.
마그네틱 센서 : 마그네틱 센서는 비활성 가스를 충전한 유리관 속에 봉입된 스위치를 의미한다. 리드스위치는 다른 스위치와 달리 접점의 동작, 복귀가 빠르고 신뢰도가 높기 때문에 TACT 스위치보다 사용하기 편하다. 자전거에 장착하여 고속으로 회전하는 케이던스와 바퀴의 속도를 측정하기 위해서는 거리측정센서나 근접센서를 사용하기 어렵다. 때문에 자력에 의해 스위치가 On/Off되는 마그네틱 센서를 사용하는 것이 인식률이나 장착에 더 효율적이다.
3.4. 앞·뒤 변속기 / 메인 컨트롤러
3.4.1. 부품리스트
3.4.2. 부품 설명
아두이노 메가 : 아두이노 메가는 Atmega2560을 사용하고 GPIO 포트가 다양하여 확장성이 좋다는 장점이 있다. 아두이노 나노 등을 사용해 소형화할 수 있었는데 아두이노 메가를 사용한 이유는 다음과 같다.
아두이노 메가 이외의 아두이노 보드는 디지털/아날로그 포트, Hardware Serial Port가 적기 때문에 많은 GPIO 포트를 요구할 때는 적합하지 않다. ABT_System은 Hardware Serial Port가 총 3개 필요하다. AX-12A를 구동할 때 Serial 통신으로 Packet을 보내주는데 제작과정에서 SoftwareSerial 클래스를 사용하는 것이 되지 않아 Hardware Serial Port를 사용해야 했다. 그래서 불가피하게 Atmega2560을 선택했으며, 포트 부족 문제를 해결하였다.
AX-12A : AX-12A 액추에이터 서보는 내부에 MCU가 장착되어 수신하는 Packet에 의해 동작한다. Robotics에서 제공하는 데이터시트를 보면 Instruction마다 기능을 다르게 설정할 수 있으며, ABT_System에는 360도 회전하는 주행모드보다는 각도 제어모드가 적합하기 때문에 AX-12A를 선택하였다. 일반 디지털 서보보다 비싼 가격대가 형성되어 있지만 토크, 허용전류, 빠른 응답속도, 정확한 명령 수행으로 인해 위 액추에이터를 선택하였다. 또한 일반 서보는 180도까지 제어가 가능하지만 액추에이터 서보는 330도까지 제어가 가능하기 때문에 더 넓은 제어각도를 처리할 수 있다.
MMA8452 : MMA8452는 3축 가속도센서로 X, Y, Z축에서 기울어진 정도를 측정하여 각도로 표시를 해주기 때문에 기울기를 측정하기에 적합한 모듈이다. 보통 가속도, 자이로센서는 SPI, I2C 통신을 둘 다 지원하기 때문에 사용자가 원하는 대로 제어가 가능하다는 장점이 있다. 자이로 센서와 가속도 센서의 차이점은 측정하는 값에 차이가 있다. 가속도 센서는 X, Y, Z축의 중력가속도 성분을 분해하여 각각 각도로 나타내는 것이 가능하다. 자이로 센서는 각 축의 각속도를 측정하는 것이기 때문에 각도를 구하려면 적분을 해야한다. 때문에 ABT_System을 구현할 때는 가속도 센서를 사용하는 것이 적합하다고 판단하였다. ABT_System에서는 주행 각도를 측정하여 그에 알맞은 기어비로 기어를 변속하게 된다.
4. 단계별 제작과정
4.1. ABT_System 장착 위치별 지름, 세부크기 측정
4.2. 부품별 3D Modeling
4.3. 3D Printing
4.4. 2차, 3차 모델링 수정
4.5. 최종 출력 및 부품배치
4.6. 부품 조립 및 자전거 장착
4.7. 테스트
4.8. 회로도
4.9. 시험주행
5. 소스코드
5.1. FRONT CODE
FRONT CODE
#include <LiquidCrystal.h>
#include <Wire.h>
#include <SFE_MMA8452Q.h>
#include <String.h>
#define r_up 10
#define r_down 11
#define f_up 12
#define f_down 13
// Button Setting pin
#define f_7 1
#define f_6 2
#define s_7 3
#define s_6 4
#define f_5 5
#define t_7 6
#define t_6 7
#define s_5 8
#define f_4 9
#define t_5 10
#define s_4 11
int mode_state=1;
int m_after1, m_after2 = 0;
int mode = 0;
int mode_temp=0;
int in[4] = {};
float in_spd = 0;
unsigned long time,time2, oldtime,oldtime2, delta,delta2;
float spd1,spd2 = 0;
float temp1, temp2 =0;
int swpin=18,swpin2=19, swval=0,swval2=0, oldswval=0,oldswval2=0;
LiquidCrystal lcd(7,6,5,4,3,2);
int after[4], before[4] = {0};
int f_state =1;
int r_state = 1;
int spd, rpm =0;
int s_temp, r_temp = 0;
char f_i2c;
char r_i2c;
char out[5]={};
MMA8452Q accel;
void setup()
{
accel.init();
lcd.begin(16,2);
pinMode(swpin,INPUT);
pinMode(swpin2,INPUT);
pinMode(r_up, INPUT);
pinMode(r_down,INPUT);
pinMode(f_up, INPUT);
pinMode(f_down, INPUT);
Serial.begin(9600);
}
void loop()
{
speed_sens(spd1, spd2);
spd = (int)spd1;
rpm = (int)spd2;
switch(mode)
{
case 0:
{
lcd.setCursor(0,0);
lcd.print(“Select Mode”);
button();
mode = mode_change(in[0],in[2], in[3]);
break;
}
case 1:
{
button();
Mode_1();
lcd_cursor_mode1();
break;
}
case 2:
{
button();
Mode_2();
lcd_cursor_mode2();
break;
}
case 3:
{
button();
Mode_3();
lcd_cursor_mode3();
break;
}
default:
{}
}
f_i2c = trans(f_state);
r_i2c = trans(r_state);
out[0] = f_i2c;
out[1] = ‘+’;
out[2] = r_i2c;
out[3] = ‘\n’;
out[4] = ”;
Serial.print(out);
delay(150);
if(mode_temp != mode)
{
lcd.clear();
}
mode_temp = mode;
s_temp = spd;
r_temp = rpm;
}
void lcd_spd()
{
lcd.print(“SPD:”);
if(spd<10)
{
lcd.setCursor(5,1);
lcd.print(” “);
lcd.setCursor(4,1);
lcd.print(spd);
}
else if(spd>=10)
{
lcd.setCursor(6,1);
lcd.print(” “);
lcd.setCursor(4,1);
lcd.print(spd);
}
lcd.setCursor(7,1);
lcd.print(” RPM:”);
if(rpm>=10)
{
lcd.setCursor(14,1);
lcd.print(” “);
lcd.setCursor(12,1);
lcd.print(rpm);
}
else if(rpm<10)
{
lcd.setCursor(13,1);
lcd.print(” “);
lcd.setCursor(12,1);
lcd.print(rpm);
}
else if(rpm>99)
{
lcd.setCursor(12,1);
lcd.print(rpm);
}
}
void lcd_cursor_mode2()
{
//lcd.clear();
lcd.setCursor(0,0);
lcd.print(“MLM”);
lcd.print(” Fr:”);
lcd.print(f_state);
lcd.print(“, Rr:”);
lcd.print(r_state);
lcd.setCursor(0,1);
lcd_spd();
}
void lcd_cursor_mode1()
{
lcd.setCursor(0,0);
lcd.print(“ATM”);
lcd.print(” Fr:”);
lcd.print(f_state);
lcd.print(“, Rr:”);
lcd.print(r_state);
lcd.setCursor(0,1);
lcd.print(“SPD:”);
lcd.print(spd);
lcd.print(” RPM:”);
lcd.print(rpm);
lcd_spd();
}
void lcd_cursor_mode3()
{
lcd.setCursor(0,0);
lcd.print(“OFM”);
lcd.print(” Fr:”);
lcd.print(f_state);
lcd.print(“, Rr:”);
lcd.print(r_state);
lcd.setCursor(0,1);
lcd.print(“SPD:”);
lcd.print(spd);
lcd.print(” RPM:”);
lcd.print(rpm);
lcd_spd();
}
void button()
{
// int in[4] = {0};
int i = 0;
for(i=0;i<4;i++)
{
in[i] = digitalRead(10+i);
}
if((in[0] == 1) && (in[2] == 1))
{
mode = 0;
}
}
char trans(int state)
{
switch(state)
{
case 1:
{
return ’1′;
}
case 2:
{
return ’2′;
}
case 3:
{
return ’3′;
}
case 4:
{
return ’4′;
}
case 5:
{
return ’5′;
}
case 6:
{
return ’6′;
}
case 7:
{
return ’7′;
}
default:
{
return ’0′;
}
}
}
int mode_change(int in1, int in2, int in3)
{
if(in1 == 1)
{
return 1;
}
if(in2 == 1)
{
return 2;
}
if(in3 == 1)
{
return 3;
}
}
int Mode_1()
{
if(accel.x < 15 && accel.x>=0)
{
if(spd != 0 && rpm < 30)
{
spd = s_temp;
rpm = r_temp;
}
if(spd >=0 && spd<=15)
{
if(rpm<=30)
{
mode_state = 1;
}
else if(rpm >30 && rpm<=60)
{
mode_state = 2;
}
else if(rpm > 60 && rpm<= 90)
{
mode_state = 3;
}
}
else if(spd >15 && spd<=25)
{
if(rpm<=30)
{
mode_state = 4;
}
else if(rpm >30 && rpm<=60)
{
mode_state = 5;
}
else if(rpm > 60 && rpm<= 90)
{
mode_state = 6;
}
else if(rpm >90 && rpm <= 120)
{
mode_state = 7;
}
}
else if(spd > 25)
{
if(rpm<=30)
{
mode_state = 8;
}
else if(rpm >30 && rpm<=60)
{
mode_state = 9;
}
else if(rpm > 60 && rpm<= 90)
{
mode_state = 10;
}
else if(rpm >90 && rpm <= 120)
{
mode_state = 11;
}
}
}
else if(accel.x >15)
{
if(spd >=0 && spd<=15)
{
if(rpm<=30)
{
mode_state = 1;
}
else if(rpm >30 && rpm<=60)
{
mode_state = 2;
}
else if(rpm > 60 && rpm<= 90)
{
mode_state = 3;
}
}
}
else if(accel.x < 0 && accel.x >-15)
{
if(spd >15 && spd<=25)
{
if(rpm<=30)
{
mode_state = 4;
}
else if(rpm >30 && rpm<=60)
{
mode_state = 5;
}
else if(rpm > 60 && rpm<= 90)
{
mode_state = 6;
}
else if(rpm >90 && rpm <= 120)
{
mode_state = 7;
}
}
}
switch(mode_state)
{
case 1:
{
f_state = 1;
r_state = 7;
return 0;
}
case 2:
{
f_state = 1;
r_state = 6;
return 0;
}
case 3:
{
f_state = 1;
r_state = 5;
return 0;
}
case 4:
{
f_state = 2;
r_state = 6;
return 0;
}
case 5:
{
f_state = 2;
r_state = 5;
return 0;
}
case 6:
{
f_state = 2;
r_state = 4;
return 0;
}
case 7:
{
f_state = 2;
r_state = 3;
return 0;
}
case 8:
{
f_state = 3;
r_state = 5;
return 0;
}
case 9:
{
f_state = 3;
r_state = 4;
return 0;
}
case 10:
{
f_state = 3;
r_state = 3;
return 0;
}
case 11:
{
f_state = 3;
r_state = 2;
return 0;
}
default:
{
}
}
}
void Mode_2()
{
int j=0;
if(after[0] != 1)
{
if(in[2] == 1 && r_state < 7)
{
r_state += 1;
}
}
if(after[1] != 1)
{
if(in[3] == 1 && r_state >1 )
{
r_state -= 1;
}
}
if(after[2] != 1)
{
if(in[0] == 1 && f_state < 3)
{
f_state += 1;
}
}
if(after[3] != 1)
{
if(in[1] == 1 && f_state > 1)
{
f_state -= 1;
}
}
for(j=0;j<4;j++)
{
before[j] = in[j];
after[j] = before[j];
}
}
int Mode_3()
{
int Read1 = digitalRead(r_up);
int Read2 = digitalRead(r_down);
if(m_after1 != 1)
{
if((mode_state < 21) && (in[2] == 1))
{
mode_state += 1;
}
}
if(m_after2 != 1)
{
if((mode_state > 1) && (in[3] == 1))
{
mode_state -= 1;
}
}
m_after1 = in[2];
m_after2 = in[3];
switch(mode_state)
{
case 1:
{
f_state = 1;
r_state = 7;
return 0;
}
case 2:
{
f_state = 1;
r_state = 6;
return 0;
}
case 3:
{
f_state = 1;
r_state = 5;
return 0;
}
case 4:
{
f_state = 2;
r_state = 6;
return 0;
}
case 5:
{
f_state = 2;
r_state = 5;
return 0;
}
case 6:
{
f_state = 2;
r_state = 4;
return 0;
}
case 7:
{
f_state = 2;
r_state = 4;
return 0;
}
case 8:
{
f_state = 3;
r_state = 5;
return 0;
}
case 9:
{
f_state = 3;
r_state = 4;
return 0;
}
case 10:
{
f_state = 3;
r_state = 3;
return 0;
}
case 11:
{
f_state = 3;
r_state = 2;
return 0;
}
default:
{
}
}
}
void speed_sens(float in1, float in2)
{
accel.read();
swval = digitalRead(swpin);
swval2 = digitalRead(swpin2);
if(((oldswval2 == LOW) && (swval2 == HIGH)))
{
time = millis();
delta = millis() – oldtime;
//26″ * 25.4 * 3.141592 = 2.074m, 0.002174 km;
// * 3600 = 7446.4 km/s
in1 = 7600 / (float)delta;
oldtime = time;
// Serial.print((int)spd + “+” + (int)spd2);
}
if((oldswval == LOW) && (swval == HIGH))
{
time2 = millis();
delta2 = millis() – oldtime2;
in2 = ((1.0 / (float)delta2) * 60000);
oldtime2 = time2;
}
// if((spd != 0 || spd2 != 0) && ((temp1 != spd )|| (temp2 != spd2)))
//{
// sprintf(buf, “%3ld+%3ld”, (long)spd, (long)spd2);
// Serial.println(buf);
// Serial.print(buf);
//}
oldswval = swval;
oldswval2 = swval2;
temp1 = in1;
temp2 = in2;
}
5.2. REAR CODE
REAR CODE
#include <String.h>
char Recv[5]={};
byte rearD = 0×02;
byte frontD = 0×01;
int frontAngle, rearAngle = 0;
char front_n,rear_n = ’1′;
char temp_f, temp_r = ’1′;
unsigned long time,time2, oldtime,oldtime2, delta,delta2;
float spd,spd2 = 0;
float temp1, temp2 =0;
int swpin=8,swpin2=9, swval=0,swval2=0, oldswval=0,oldswval2=0;
char buf[7] = {0};
void setup() {
pinMode(2, OUTPUT);
pinMode(swpin,INPUT);
pinMode(swpin2,INPUT);
digitalWrite(2,LOW);
Serial.begin(9600);
Serial1.begin(1000000);
Serial2.begin(1000000);
}
//400 1단 , 540 7단
void loop() {
int i=0;
while(Serial.available())
{
char data = SW.read();
Recv[i] = data;
i++;
}
change_value_f();
change_value_r();
Packet1(0×01,frontAngle);
Packet2(0×01,rearAngle);
swval = digitalRead(swpin);
swval2 = digitalRead(swpin2);
if(((oldswval2 == LOW) && (swval2 == HIGH)))
{
time = millis();
delta = millis() – oldtime;
//26″ * 25.4 * 3.141592 = 2.074m, 0.002174 km;
// * 3600 = 7446.4 km/s
spd = 7600 / (float)delta;
oldtime = time;
}
if((oldswval == LOW) && (swval == HIGH))
{
time2 = millis();
delta2 = millis() – oldtime2;
spd2 = ((1.0 / (float)delta2) * 60000);
oldtime2 = time2;
}
if((spd != 0 || spd2 != 0) && ((temp1 != spd )|| (temp2 != spd2)))
{
sprintf(buf, “%3ld+%3ld”, (long)spd, (long)spd2);
Serial.print(buf);
}
oldswval = swval;
oldswval2 = swval2;
temp1 = spd;
temp2 = spd2;
delay(50);
}
void Packet1(unsigned char ID, int Angle)
{
unsigned char Check_Sum, Header = 0xFF;
Serial1.write(Header);
Serial1.write(Header);
Serial1.write(ID); // Motor ID
Serial1.write(0×05); // Data legnth
Serial1.write(0×03); // Instruction
Serial1.write(0x1E); // Goal Position
Serial1.write(Angle); // Set Goal Position
Serial1.write(Angle>>8); //Clear Goal position 8bit
Check_Sum = ~(ID + 0×05 + 0×03 + 0x1E + Angle + (Angle>>8)); // Send Data Error Check
Serial1.write(Check_Sum);
}
void change_value_r()
{
if(rear_n != Recv[2])
{
rearAngle = rear_transferid(Recv[2]);
}
else
{
rearAngle = rear_transferid(rear_n);
}
//temp_r = rear_n;
rear_n = Recv[2];
// Recv[2] = temp_r;
}
void change_value_f()
{
if(front_n != Recv[0])
{
frontAngle = front_transferid(Recv[0]);
}
else
{
frontAngle = front_transferid(front_n);
}
temp_f = front_n;
front_n = Recv[0];
// Recv[0] = temp_f;
}
void Packet2(unsigned char ID, int Angle)
{
unsigned char Check_Sum, Header = 0xFF;
Serial2.write(Header);
Serial2.write(Header);
Serial2.write(ID); // Motor ID
Serial2.write(0×05); // Data legnth
Serial2.write(0×03); // Instruction
Serial2.write(0x1E); // Goal Position
Serial2.write(Angle); // Set Goal Position
Serial2.write(Angle>>8); //Clear Goal position 8bit
Check_Sum = ~(ID + 0×05 + 0×03 + 0x1E + Angle + (Angle>>8)); // Send Data Error Check
Serial2.write(Check_Sum);
}
void Default(unsigned char ID1)
{
Packet1(ID1, 512);
delay(250);
}
int rear_transferid(char a)
{
switch(a){
case ’1′: {return 440;}
case ’2′: {return 500;}
case ’3′: {return 520;}
case ’4′: {return 540;}
case ’5′: {return 580;}
case ’6′: {return 610;}
case ’7′: {return 700;}
default : {return 480;}
}
}
int front_transferid(char a)
{
switch(a){
case ’1′: {return 250;}
case ’2′: {return 530;}
case ’3′: {return 690;}
default : {}
}
}
void First_front()
{
Packet1(0×01,512);
}
void Second_front()
{
Packet1(0×01,1023);
delay(500);
Packet1(0×01,750);
}
void Third_front()
{
Packet1(0×01,1023);
}
[41호]Silence Quadcopter (Rotor’s Noise Canceling Quadcopter)
2016 ICT 융합 프로젝트 공모전 입선
Silence Quadcopter(Rotor’s Noise Canceling Quadcopter)
글 | 단국대학교 정의동, 최진우
심사평
JK전자 현재 무인 항공기(드론)는 성능(기능), 안정성, 배터리 지속력에 초점이 맞추어져 개발이 진행되고 있는 것이 사실이다. 언젠가는 무인 항공기가 어느 정도 성능을 갖추고 실생활에 사용되기 시작하면 소음에 대한 이슈도 분명히 대두될 것이다. 본 작품에서 구현하고 있는 반대 주파수를 이용해서 소음을 감쇠하는 방법 외에도 모터, 프러펠러 개선 등의 다양한 방법으로 소음을 줄이려는 시도가 있을것이다.
뉴티씨 매우 좋은 작품이며, 특히 DSP를 활용해 FFT 및 IFFT 등을 통하여 원하는 주파수를 찾고 역으로 주파수를 발생시켜 상쇄시키는, 예전에 공중전화 부스 등에서 적용되었던 아이디어를 개선해 현재의 실용적인 부분에서 적용한 점이 돋보인다. 드론의 중요성이 커지고있는 이 때에, 조용한 드론을 만들어야 하는 필요성도 매우 높아질 것으로 예상되는 바, 이 때문에 좋은 점수를 받았으며, 실제 일부 성공한 점 등도 좋은 점수를 받았다. 앞으로 더 연구에 매진하여 만점짜리 결과가 나오기를 바란다.
칩센 재미있는 주제에 노이즈캔슬링 원리를 잘 적용하였다. 다른 분야에도 시도를 해보면 좋을 것 같다.
위드로봇 드론의 뜻이 원래 시끄럽게 붕붕거리다는 뜻인데, 이 소리를 없애는데 관심을 가진 재미있는 아이디어가 돋보이는 작품입니다. 노이즈 캔슬레이션 알고리즘을 좀 더 깊이 공부하고, 이를 구현했더라면 더 나은 결과가 나왔을 것 같습니다. 또한 정량적인 평가를 위해 노이즈를 측정할 수 있는 방법이 별도로 필요해 보입니다. 창의성과 기대효과 측면에 높은 점수를 드립니다.
1. 작품제목
작품의 제목은 Silence Quadcopter(Rotor’s Noise Canceling Quadcopter)입니다. 제목을 해석해 보자면 조용한 쿼드콥터 즉, 로터의 소음을 없애(로터 소리의 역 주파수의 소리를 내보내어 상쇄시켜) 로터의 소리를 최소화시킨 쿼드콥터라는 뜻입니다.
2. 작품 개요
현재 드론(쿼드콥터, X콥터, UAV 등의 총칭) 분야는 중국, 미국 등의 방대한 투자 및 개발에 힘입어 더 이상의 발전은 없을 것이라 생각할 정도로 성장해 왔습니다. 하지만 그에 반해 몇몇 작지 않은 문제점들도 발견되었습니다. 그 중에서 크게 네 가지를 꼽아 보았습니다.
첫째, 안전 문제가 있습니다. 드론을 접해본 사람이라면 누구나 프로펠러의 위험성을 느낄 수 있습니다. 그리하여 만약, 드론이 비행 도중 주변의 물체 또는 사람 등과 부딪힐 경우 큰 사고로 번질 위험이 있습니다.
둘째, 최근 들어 크게 대두가 되고 있는 보안 및 사생활 문제가 있습니다. 드론 촬영을 하는 경우, 허가받지 않은 장소 촬영 또는 허가받지 않은 상태에서의 다른 사람 촬영 등은 사생활 침해 및 초상권 침해 등 법적인 문제를 야기할 수 있습니다. 또한 드론 관련 법안이 제대로 나와있지 않은 현재로서는 큰 문제를 야기할 수 있습니다.
셋째, 소음 문제가 있습니다. 드론을 접해 보았거나 드론에 가까이 가본 사람이라면 누구나 느낄 수 있는 것이 소리가 너무 크다는 것입니다. 드론산업 및 드론 취미가 많이 급증하고 있는 현재, 이 소음문제는 앞으로 더욱 커질 전망입니다.
넷째, 배터리 문제가 있습니다. 드론의 가장 큰 문제 중 하나로 생각되는 것이 바로 배터리 문제입니다. 완충 기준 거의 대부분의 드론들은 비행시간이 30분을 넘기지 못하고 있습니다.
이러한 네 가지 문제를 해결하기 위해, 안전 문제는 물체 감지 및 정확한 제어 등으로 해결하고 있는 추세이고, 보안 및 사생활 문제는 법안이 만들어지고 있으며, 배터리 문제는 기업들의 배터리 개발 등으로 해결하고는 있지만 한계가 있을 것으로 보입니다. 반면 소음문제는 아직 전혀 해결되지 않고 있습니다.
그래서 소음 문제를 해결하기 위해 생각해낸 것이 ‘로터(모터 + 프로펠러) 소리의 반대되는 주파수로 상쇄시킴으로서 소음 문제를 해결하자’ 라는 것이고, 배터리 문제를 해결하기 위해 ‘모터에 자가 동력장치(주로 자전거 바퀴에 많이 설치되어 야간조명 등에 이용됨)를 설치하여 비행과 동시에 충전이 가능하게 설계를 하자’ 라는 것입니다.
처음 프로젝트 목표는 소음 및 배터리 문제, 이 두 가지를 해결하자는 것이었지만 자가 동력장치를 추가하면 결국에는 그 만큼의 모터 출력을 더 요구하게 되어 즉, 에너지 보존법칙에 의해 충전되는 양 이상의 에너지를 필요로 하기 때문에 효율성이 떨어지게 되어 배터리 문제는 좀 더 고민을 해보고, 우선 소음 문제부터 해결하기로 목표를 수정하였습니다.
3. 작품 설명
3.1 주요 동작 및 특징
이 작품은 기존의 쿼드콥터에 로터의 소음을 없애는 기능을 추가시킨 쿼드콥터 입니다. 그래서 이 쿼드콥터는 크게 두 부분으로 나눌 수 있습니다. 하나는 제어부, 다른 하나는 센서부입니다.
우선 전체적인 하드웨어를 살펴보면 CPU + 제어부(모터, 프로펠러, ESC) + 센서부(마이크센서, 사운드센서, 증폭기, 핀 확장 소자, DAC 변환 소자) + 배터리 + 프레임으로 이루어져 있습니다.
소프트웨어를 살펴보면, 제어부 소스는 소스가 매우 짧고 간결하게 짜여 있다는 것을 알 수 있습니다. 외부 인터럽트를 이용해 주기적으로 모터 제어를 할 수 있게 하였는데, 모터 제어는 PID 제어 방식을 이용하였습니다. PID 오픈 소스들을 살펴보면 너무 자료가 많고, 다 다르고, 소스의 크기 또한 작지 않아 최대한 간편히 코딩을 해 보았습니다.
기본적인 개념인 P = 비례, I – 적분, D – 미분을 이용하여 PID 값 = (현재각도 * P게인값) + {이전 적분 누적값 + (I게인값 * 현재각도 * 0.01)} + {D게인값 * (현재각도 – 이전각도)} 이라는 공식을 세우고 그에 맞게 소스를 구현했습니다. 그리고 Roll 축, Pitch 축의 각도 값은 10개를 평균내 사용하였는데 그 이유는 최대한 쓰레기 값을 버리고, 최대한 정밀한 값을 사용하게 하기 위해서입니다. 예를 들어 10개의 각도값이 차례로 10 12 13 15 16 16 18 195 19 20 일때 18에서 195로 넘어가는 각도값의 차이는 177입니다. 하지만 10개의 각도를 평균내 사용하면 33.4 – 18 = 15.4로 오류를 최소한으로 줄일 수 있게 됩니다. P, I, D의 각 게인값 들은 직접 시소 테스트, 호버링 테스트 등을 하며 맞추었고, 외부 인터럽트의 주기는 가장 적합한 주기 네 가지 1ms, 2ms, 10ms, 20ms를 구상하여 테스트를 해 그 중에서도 가장 적합한 주기 20ms를 찾았습니다.
IMU 세 축 Yaw 축, Roll 축, Pitch 축 중 Pitch 축, Roll 축은 PID제어를 사용하였고, Yaw 축은 PI제어를 사용하였는데 Yaw 축만 다른 두 축과는 다르게 PI 제어를 사용한 이유는 Roll, Pitch 제어로 맞춘 평행을 Yaw 축 제어를 하면서 너무 많은 값이 더해지게 될 수 있는데, 이를 아래 예시를 통해 설명하겠습니다.
ex) 1motor_pwm = input_pwm + Pitch_pid_result + Yaw_pid_result 일 때 input_pwm은 공통이고, Pitch_pid_result는 맞추었다면 그 상태에서 Yaw_pid_result의 값이 너무 커지거나 너무 작아지게 되면 전체 pwm인 1motor_pwm의 값의 변화가 너무 커지게 됩니다. 그 결과 전체적인 자세제어에 문제가 생기게 되어 비행하는데 영항을 끼치게 되므로 영향을 최대한 덜 미치면서 방향 제어를 할 수 있는 제어 방식을 추구하다 보니 PI 제어가 가장 적합하다 생각하였습니다. 또한, Yaw 축 소스만은 Roll, Pitch 소스와는 다른 부분인
if(yawcnt == 1) yawerr = g_iq17yaw;
else ;
라는 부분이 있는데 이는 초기 yaw 각도 값이 너무 크거나 작을 경우 모터 출력에 영향을 주게 되어 그것을 막기 위해 초기 에러 각도 값을 지정해 주어 각도에다 그 에러 값을 빼주어 현재 각도 값을 산출해 이용했습니다.
PID 게인 값을 맞춘 과정은 따로 설명을 하지 않았는데, 0부터 0.1 또는 0.01씩을 올려가며 자세 안정의 수렴, 발산 등을 맞추어 준 것입니다.
그리고 쿼드콥터의 형태는 +형태로 하였습니다. +형태, x형태 둘 중 어떤 방식으로 할까 고민하다가 보다 테스트하기 간편하고, 그러면서 자세제어가 정확한 +형태로 하였습니다. +형태는 4개의 로터에 Yaw 축 제어가 공통으로 들어가 있는 상태에서 각각 대칭으로 한 쌍은 Pitch 축 제어, 또 다른 한 쌍은 Roll 축 제어를 하게 하여 각 모터 당 Yaw 축을 제외하고 하나의 축만을 제어해 테스트에 용이하였고, 한 축을 하나의 모터로만 제어하는 것에 비해 자세제어가 안정적으로 잘 되었습니다.
센서부 소스는 제어부 소스에 비해 소스의 길이가 상당히 길다는 점을 알 수 있습니다. 우선 센서부도 제어부와 마찬가지로 외부 인터럽트를 써서 센서 adc 값을 받아들이고, FFT 변환을 하고, 변환된 값의 반대되는 값을 보냅니다. FFT 변환 소스는 paulbourke.net에 있는 DFT – FFT 이론을 바탕으로 짜여진 것이고, 그 소스를 가져와 수정해 사용했습니다.
FFT는 Fast Fourier Transform 즉, 고속 푸리에 변환으로서 DFT (Discrete Fourier Transform – 이산 푸리에 변환)의 고속 모드라고 생각하면 될 것 같습니다. FFT에 대해 좀 더 자세히 설명을 해 보자면 FFT는 시간영역에서 변화하는 데이터 값을 주파수 영역에서의 데이터 값으로 변환시켜주는 알고리즘입니다. 즉, 보는 관점을 시간영역에서 주파수 영역으로 바꿔주어 값을 사용하는 것입니다.
그럼 왜 관점을 시간관점에서 주파수관점으로 바꿔서 계산을 하느냐하면, 우선 소리란 물체의 진동에 의하여 생긴 음파가 귀청을 울리어 귀에 들리는 것이라고 사전에 정의되어 있습니다. 즉, 소리는 진동수에 의해 크기가 결정되고, 진폭에 의해 세기가 결정됩니다. 그렇기 때문에 단위를 시간에서 주파수로 바꾸어준 후 그 값에 반대되는 값을 계산하고, 이를 다시 시간단위로 바꾼 후에 DAC변환을 통해 소리로 출력을 해야 하는 것입니다.
전체적인 신호처리 과정을 대략적으로 설명하자면, MIC센서를 이용한 아날로그값 받기 ▶ DAC변환소자를 이용한 디지털값으로 변환 ▶ 변환된 디지털값을 FFT 및 RFFT(Reverse FFT)처리 ▶ IFFT(Inverse FFT)처리 ▶ 디지털값 출력 ▶ DAC컨버터 구축 ▶ 아날로그값 변환 및 출력 ▶ 사운드센서로 소리 출력입니다. 여기서 RFFT는 말 그대로 FFT를 역 변환 해주는 것이고, IFFT는 FFT의 반대 즉, 시간 영역에서 주파수 영역으로 바뀌었던 데이터를 반대로 주파수 영역에서 시간 영역으로 바꿔주는 것을 의미합니다. 소리를 출력하여 로터의 소음과 상쇄시킨 결과 완벽히 소음이 줄지는 않았으나, 누가 봐도 소음이 줄었다는 것을 느낄 수 있을 정도까지는 되었습니다. 그래서 현재는 조금씩 주기, 출력 시간을 조정해가며 테스트하고 있습니다.
3.2 전체 시스템 구성
3.3 개발 환경(개발언어, Tool, 사용시스템 등)
언어 – C언어
개발 Tool에서 쓰이는 언어가 C언어이기 때문에, 자연스레 C언어를 이용하여 코딩 및 소스구현을 하게 되었습니다.
Tool – Source Insight (Dsp전용 디버깅 프로그램)
DSP를 다룰 수 있는 툴을 찾다보니 Source Insight가 주변에 사용해 보았던 사람이 많아 쉽게 접근할 수 있었습니다.
시스템 – DSP(tms320f2809, 제조사 : Texas Instrument)
DSP를 선정하게 된 이유는 많은 핀들을 보유함과 동시에 PWM이 잘 되고 처리속도가 빠른며 싼 값에 구할 수 있는 MCU를 찾다보니 DSP가 라즈베리파이, 아두이노, ATmega, 망고보드, cortex 시리즈 등 여러 MCU 중에서 가장 적합하다고 생각해서 메인 MCU로 선정을 하게 되었습니다.
시스템 – 라즈베리파이2(Model B)
라즈베리파이를 선정하게 된 이유는 900MHz의 빠른 속도와, 1GB의 넉넉한 램 용량, 저렴한 가격 및 많은 GPIO 핀을 보유하고 있어서입니다. 물론 신호처리에 최적화된 DSP에 비해서는 살짝 부족하고 ADC 핀도 없어 불편한 감이 있기는 하지만, 모터 제어를 하는 동시에 신호처리를 하기 위해서는 두 개 이상의 MCU를 써야하는데 DSP를 메인 MCU로 쓰고 있기 때문에 서브 MCU로는 괜찮다고 생각된 라즈베리 파이를 선정하였습니다.
4. 단계별 제작 과정
4.1. 계획 세우기 및 사전조사
프로젝트 주제를 정하고 그에 대한 사전조사를 인터넷 및 관련 사람들을 대상으로 조사하여 문제점, 주요특징, 앞으로의 전망 등을 충분히 조사하였습니다.
4.2. 해당 작품 공부 및 제작 준비
쿼드콥터라는 것을 처음 해본만큼, 먼저 제작을 해 보았던 선배님들이나 RC 동호회 사람들, 인터넷 등을 이용해 작품 공부를 하였고, 필요한 부품 및 기자재 등을 준비하였습니다.
4.3. 하드웨어 제작
기본적인 하드웨어(대각선길이-28cm)를 제작하였고, 블루투스 모듈(HC-06), 9축 센서(EBIMU-9DOF), BLDC모터(제조사-hobbyking), 변속기(제조사-hobbywing), MCU(DSP-tms320f2809) 등 각종 센서, 모터, cpu 등을 테스트 해 보았습니다.
4.4. 시소테스트, 호버링 등 구동테스트
구동을 하기 위해 제어기(PID 제어 이용)를 설계하여 직접 코딩 및 테스트하여 각의 게인값들을 맞추고, 9축센서와 CPU 및 PC를 연동시켜 각 축(Roll, Pitch, Yaw)에 대한 제어가 잘 되는지 시소테스트(Roll, Pitch 축)와 줄 매달아놓고 테스트(Yaw 축), 그리고 호버링 테스트를 하면서 확인 및 수정 하였습니다.
4.5. 마이크센서와 라즈베리파이와 스피커센서를 거친 신호처리
로터(모터 + 프로펠러) 근처에 스피커센서를 두어 로터의 소리의 값을 받아와 라즈베리파이의 ADC핀(ADC변환소자 이용)에 연결 및 디지털 데이터를 뽑아냈고, FFT(Fast Fourier Transform) 알고리즘을 거쳐 변환 데이터를 걸러낸 후 그에 반대되는 위상을 가진 역 주파수를 계산 및 출력해 냈으며, DAC과정(CPU의 GPIO핀과 핀 확장소자인 74HC595과 증폭기인 UILM358을 이용)을 거쳐 스피커 센서로 소리를 내보내기 위한 센서소스를 직접 짜고, 테스트 및 쿼드콥터에 설치하여 구동테스트를 하였습니다.
4.6. 안드로이드 연동 및 전체 구동테스트
작품에 안드로이드를 올려 PC와 쿼드콥터와의 통신 및 컨트롤이 아닌 핸드폰 등 스마트기기와의 통신 및 컨트롤을 계획하여 앱을 만들고 기능들을 추가하여 편의성을 더할 예정이고, 안드로이드 스튜디오를 이용해 어플리케이션을 만들 생각입니다. 또한, 적외선센서를 이용해 장애물 감지 및 피해가는 알고리즘을 구성할 것이고, 고도센서를 이용해 보다 정확한 자세제어를 할 수 있게 할 것입니다.
5. 기타(회로도, 소스코드, 참고문헌 등)
5.1. 회로도
5.2. 소스코드
5.2.1. 제어 소스코드
##############################################################
// FILE : Motor.c
// TITLE : Motor c file.
// Author : Jeong Eui Dong
// Company : Maze & Hz
############################################################
// $Release Date: 2016.02.12 $
############################################################
#define _MOTOR_
#include “DSP280x_Device.h”
#include “DSP280x_Examples.h” // DSP280x Examples Include File
#include “Main.h”
#include “Motor.h”
#pragma CODE_SECTION(control_ISR , “ramfuncs2″);
//motor interrupt motor pid , Kalmanfilter 구상하기.
interrupt void control_ISR( void )
{
StopCpuTimer0();
//TxPrintf(“motor_check\n”);
yawcnt ++ ;
i++;
// g_iq17roll = imu roll, g_iq17pitch = imu pitch , g_iq17yaw = imu yaw
//g_iq17errsum = g_iq17errsum + g_iq17ki * 0.01 * g_iq17angles1;
// g_iq17pidresult = g_iq17kp * 오차 + g_iq17ki * g_iq17errsum + kd * (g_iq17angles1 – g_iq17angles2); // 오차 = ki * 0.01 * angles1
/////////////////////////////////////////////////////////////////////
//roll, pitch축 제어는 PID제어 사용
Rollerr = _IQ17(-3.0);
Rollnowangle = g_iq17roll – Rollerr; //Roll
for(i=0; i<9; i++)
Rollangle[i+1] = Rollangle[i];
Rollangle[0] = Rollnowangle;
Rollanglesum = Rollangle[0] + Rollangle[1] + Rollangle[2] + Rollangle[3] + Rollangle[4] + Rollangle[5] + Rollangle[6] + Rollangle[7] + Rollangle[8] + Rollangle[9];
Rollangleresult = _IQ17mpy(Rollanglesum, _IQ17(0.1));
Rollerrsum = Rollerrsum + _IQ17mpy(Rollki, _IQ17mpy(0.01, Rollangleresult));
Rollpidresult = _IQ17mpy(Rollkp, Rollangleresult) + Rollerrsum + _IQ17mpy(Rollkd, (Rollangleresult – Rollpreangle));
Rollhalf = _IQ17mpy(Rollpidresult,_IQ17(0.5));
Rollpreangle = Rollangleresult;
Pitcherr = _IQ17(1.0);
Pitchnowangle = g_iq17pitch – Pitcherr; //Pitch
for(i=0; i<9; i++)
Pitchangle[i+1] = Pitchangle[i];
Pitchangle[0] = Pitchnowangle;
Pitchanglesum = Pitchangle[0] + Pitchangle[1] + Pitchangle[2] + Pitchangle[3] + Pitchangle[4] + Pitchangle[5] + Pitchangle[6] + Pitchangle[7] + Pitchangle[8] + Pitchangle[9];
Pitchangleresult = _IQ17mpy(Pitchanglesum, _IQ17(0.1));
Pitcherrsum = Pitcherrsum + _IQ17mpy(Pitchki, _IQ17mpy(0.01, Pitchangleresult));
Pitchpidresult = _IQ17mpy(Pitchkp, Pitchangleresult) + Pitcherrsum + _IQ17mpy(Pitchkd, (Pitchangleresult – Pitchpreangle));
Pitchhalf = _IQ17mpy(Pitchpidresult,_IQ17(0.5));
Pitchpreangle = Pitchangleresult;
// yaw축 제어는 다른방식 사용 ▶ PI제어 사용 ▶ 최대한 roll, pitch 제어에 영항을 미쳐서는 안된다.
if(yawcnt == 1) Yawerr = g_iq17yaw; //Yaw
else ;
Yawnowangle = g_iq17yaw – Yawerr;
if(Yawnowangle >= _IQ17(180))
Yawnowangle = Yawnowangle – _IQ17(360);
if(Yawnowangle < _IQ17(-180))
Yawnowangle = Yawnowangle + _IQ17(360);
Yawerrsum = Yawerrsum + _IQ17mpy(Yawki, _IQ17mpy(0.01, Yawnowangle));
//Yawpidresult = _IQ17mpy(Yawkp, Yawnowangle) + Yawerrsum + _IQ17mpy(Yawkd, (Yawnowangle – Yawpreangle));
Yawpidresult = _IQ17mpy(Yawkp, Yawnowangle) + Yawerrsum;
Yawpreangle = Yawnowangle;
//운동방향 – 전진 : front high , 후진 : back high , 우측 : right high , 좌측 : left high , 고도상승 : all high , 고도하강 : all low
//1번 모터 시계 ▶ 반대 red – yellow
Bldc1pwmRegs.CMPA.half.CMPA = ( ( g_int32pwm_output + ( Pitchpidresult >> 17 ) + ( Yawpidresult >> 17 )) > 5000 )? (Uint16)5000 : (Uint16)(g_int32pwm_output + ( Pitchpidresult >> 17 ) + ( Yawpidresult >> 17 ) + frontgo); // -
//2번 모터 반시계
Bldc2pwmRegs.CMPA.half.CMPA = ( ( g_int32pwm_output – ( Rollpidresult >> 17 ) – ( Yawpidresult >> 17 )) > 5000 )? (Uint16)5000 : (Uint16) ( g_int32pwm_output – ( Rollpidresult >> 17 ) – ( Yawpidresult >> 17 ) + rightgo); // +
//3번 모터 시계 -> 반대 red – yellow
EPwm5Regs.CMPA.half.CMPA = ( ( g_int32pwm_output – ( Pitchpidresult >> 17 ) + ( Yawpidresult >> 17 )) > 5000 )? (Uint16)5000 : (Uint16)( g_int32pwm_output – ( Pitchpidresult >> 17 ) + ( Yawpidresult >> 17 ) + backgo); // -
//4번 모터 반시계
EPwm6Regs.CMPA.half.CMPA = ( ( g_int32pwm_output + ( Rollpidresult >> 17 ) – ( Yawpidresult >> 17 )) > 5000 )? (Uint16)5000 : (Uint16)( g_int32pwm_output + ( Rollpidresult >> 17 ) – ( Yawpidresult >> 17 ) + leftgo); // +
//////////////////////// X-Copter //////////////////////////////
// 4번 ▶ 1번, 3번 ▶ 4번, 2번 ▶ 3번, 1번 ▶ 2번
/*
//1번 모터 반시계 ▶ 반대 red – yellow 연결 Dir : Front
Bldc2pwmRegs.CMPA.half.CMPA = ( ( g_int32pwm_output – ( Rollhalf >> 17 ) – ( Yawpidresult >> 17 ) + ( Pitchhalf >> 17 )) > 5000 )? (Uint16)5000 : (Uint16) ( g_int32pwm_output – ( Rollhalf >> 17 ) + ( Yawpidresult >> 17 ) + ( Pitchhalf >> 17 ) + frontgo ); // +
//2번 모터 시계 Dir : Right
// EPwm5Regs.CMPA.half.CMPA = ( ( g_int32pwm_output – ( Pitchhalf >> 17 ) + ( Yawpidresult >> 17 ) – ( Rollhalf >> 17 )) > 5000 )? (Uint16)5000 : (Uint16)( g_int32pwm_output – ( Pitchhalf >> 17 ) – ( Yawpidresult >> 17 ) – ( Rollhalf >> 17 ) + rightgo ); // +
//3번 모터 반시계 ▶ 반대 red – yellow 연결 Dir : Back
EPwm6Regs.CMPA.half.CMPA = ( ( g_int32pwm_output + ( Rollhalf >> 17 ) – ( Yawpidresult >> 17 ) – ( Pitchhalf >> 17 )) > 5000 )? (Uint16)5000 : (Uint16)( g_int32pwm_output + ( Rollhalf >> 17 ) + ( Yawpidresult >> 17 ) – ( Pitchhalf >> 17 ) + backgo ); // -
// 4번 모터 시계 Dir : Left
// Bldc1pwmRegs.CMPA.half.CMPA = ( ( g_int32pwm_output + ( Pitchhalf >> 17 ) + ( Yawpidresult >> 17 ) + ( Rollhalf >> 17 )) > 5000 )? (Uint16)5000 : (Uint16)(g_int32pwm_output + ( Pitchhalf >> 17 ) – ( Yawpidresult >> 17 ) + ( Rollhalf >> 17 ) + leftgo ); // -
*/
//test motor pwm
// EPwm5Regs.CMPA.half.CMPA = g_int32pwm_output;
}
5.2.2. 센서 소스코드
//라즈베리파이에서는 소스 복사가 안되어 DSP로 테스트한 소스를 올렸다./////##############################################################
// FILE : Sensor.c
// TITLE : Senser c file.
// Author : Jeong Eui Dong
// Company : Maze & Hz
//############################################################
// $Release Date: 2016.02.12 $
//############################################################
#include “DSP280x_Device.h”
#include “DSP280x_Examples.h” // DSP280x Examples Include File
#include “Main.h”
#include “Sensor.h”
#define PI 3.14159264
void FFT_Algorithm(volatile int16 dir,volatile int16 m,volatile float32 *Real,volatile float32 *Imagine);
void Initsensor_ADC( void )
{
memset( ( void * )&Sound1real, 0, sizeof( Sound1real ) );
memset( ( void * )&Sound2real, 0, sizeof( Sound2real ) );
memset( ( void * )&Sound3real, 0, sizeof( Sound3real ) );
memset( ( void * )&Sound4real, 0, sizeof( Sound4real ) );
memset( ( void * )&Sound1change, 0, sizeof( Sound1change ) );
memset( ( void * )&Sound2change, 0, sizeof( Sound2change ) );
memset( ( void * )&Sound3change, 0, sizeof( Sound3change ) );
memset( ( void * )&Sound4change, 0, sizeof( Sound4change ) );
memset( ( void * )&Sound1imagine, 0, sizeof( Sound1imagine ) );
memset( ( void * )&Sound2imagine, 0, sizeof( Sound2imagine ) );
memset( ( void * )&Sound3imagine, 0, sizeof( Sound3imagine ) );
memset( ( void * )&Sound4imagine, 0, sizeof( Sound4imagine ) );
memset( ( void * )&Soundcheck, 0, sizeof( Soundcheck) );
}
interrupt void sensor_ADC( void )
{
static Uint16 tick = 0;
static Uint16 check = 0;
static Uint16 cnt = 0;
static _iq17 iqSound1real = _IQ17(0), iqSound2real = _IQ17(0), iqSound3real = _IQ17(0), iqSound4real = _IQ17(0);
static _iq17 iqSound1imagine = _IQ17(0), iqSound2imagine = _IQ17(0), iqSound3imagine = _IQ17(0), iqSound4imagine = _IQ17(0);
static _iq17 iqSound1change = _IQ17(0), iqSound2change = _IQ17(0), iqSound3change = _IQ17(0), iqSound4change = _IQ17(0);
StopCpuTimer2();
PieCtrlRegs.PIEACK.all = PIEACK_GROUP1;
//TxPrintf(“adc_check\n”);
CpuTimer0Regs.PRD.all=100*7;
check++;
CpuTimer0Regs.PRD.all=100*30;
AdcRegs.ADCTRL2.bit.SOC_SEQ1 = 1;
Soundcheck[0] = AdcMirror.ADCRESULT0;
CpuTimer0Regs.PRD.all=100*30;
AdcRegs.ADCTRL2.bit.SOC_SEQ1 = 1;
Soundcheck[2] = AdcMirror.ADCRESULT2;
CpuTimer0Regs.PRD.all=100*30;
AdcRegs.ADCTRL2.bit.SOC_SEQ1 = 1;
Soundcheck[1] = AdcMirror.ADCRESULT1;
CpuTimer0Regs.PRD.all=100*30;
AdcRegs.ADCTRL2.bit.SOC_SEQ1 = 1;
Soundcheck[3]= AdcMirror.ADCRESULT3;
AdcRegs.ADCTRL2.bit.RST_SEQ1 = 1;
Sound1real[Adcsound] = (float32)Soundcheck[0];
Sound2real[Adcsound] = (float32)(Soundcheck[2] >> 1);
Sound3real[Adcsound] = (float32)(Soundcheck[1] >> 1);
Sound4real[Adcsound] = (float32)(Soundcheck[3] >> 1);
if(Sound1real[Adcsound] < 0) Sound1real[Adcsound] = (float32)0;
if(Sound2real[Adcsound] < 0) Sound2real[Adcsound] = (float32)0;
if(Sound3real[Adcsound] < 0) Sound3real[Adcsound] = (float32)0;
if(Sound4real[Adcsound] < 0) Sound4real[Adcsound] = (float32)0;
if(Adcsound == 128)
{
FFT_Algorithm((int16)1, (int16)8, Sound1real, Sound1imagine);
FFT_Algorithm((int16)1, (int16)8, Sound2real, Sound2imagine);
FFT_Algorithm((int16)1, (int16)8, Sound3real, Sound3imagine);
FFT_Algorithm((int16)1, (int16)8, Sound4real, Sound4imagine);
for(cnt = 0; cnt < Adcsound; cnt++)
{
iqSound1real = _IQ17(Sound1real[cnt]);
iqSound2real = _IQ17(Sound2real[cnt]);
iqSound3real = _IQ17(Sound3real[cnt]);
iqSound4real = _IQ17(Sound4real[cnt]);
iqSound1imagine = _IQ17(Sound1imagine[cnt]);
iqSound2imagine = _IQ17(Sound2imagine[cnt]);
iqSound3imagine = _IQ17(Sound3imagine[cnt]);
iqSound4imagine = _IQ17(Sound4imagine[cnt]);
iqSound1change = _IQsqrt(_IQmpy(iqSound1real, iqSound1real) + _IQmpy(iqSound1imagine, iqSound1imagine));
iiqSound2change = _IQsqrt(_IQmpy(iqSound2real, iqSound2real) + _IQmpy(iqSound2imagine, iqSound2imagine));
iiqSound3change = _IQsqrt(_IQmpy(iqSound3real, iqSound3real) + _IQmpy(iqSound3imagine, iqSound3imagine));
iiqSound4change = _IQsqrt(_IQmpy(iqSound4real, iqSound4real) + _IQmpy(iqSound4imagine, iqSound4imagine));
iSound1change[cnt] = _IQtoF(iqSound1change);
iSound2change[cnt] = _IQtoF(iqSound2change);
iSound3change[cnt] = _IQtoF(iqSound3change);
iSound4change[cnt] = _IQtoF(iqSound4change);
}
Max_arr = Sound1change[0];
for(cnt = 1; cnt < Adcsound; cnt++)
{
if(Max_arr < Sound1change[cnt]) Max_arr = Sound1change[cnt];
else;
if(Max_arr < Sound2change[cnt]) Max_arr = Sound2change[cnt];
else;
if(Max_arr < Sound3change[cnt]) Max_arr = Sound3change[cnt];
else;
if(Max_arr < Sound4change[cnt]) Max_arr = Sound4change[cnt];
else;
}
for(tick = 0; tick < 128; tick++)
{
Sound1real[tick] = Sound1real[tick+1];
Sound2real[tick] = Sound2real[tick+1];
Sound3real[tick] = Sound3real[tick+1];
Sound4real[tick] = Sound4real[tick+1];
}
Adcsound = 127;
Sound1real[128] = 0;
Sound2real[128] = 0;
Sound3real[128] = 0;
Sound4real[128] = 0;
}
Adcsound++;
//AdcRegs.ADCST.bit.INT_SEQ1_CLR = 1;
StopCpuTimer0();
StartCpuTimer2();
}
void FFT_Algorithm(volatile int16 dir,volatile int16 m,volatile float32 *Real,volatile float32 *Imagine)
{
/* Calculate the number of points */
static float32 tx = 0, ty = 0, i2 = 0, k = 0, l1 = 0, l2 = 0, u1 = 0, u2 = 0;
static float32 t1 = 0, t2 = 0;
static int16 i1 = 0, b = 0, l = 0, n = 1, j = 0, i = 0;
static _iq17 iqn = _IQ17(1), iqReal = _IQ17(0), iqImagine = _IQ17(0), iqT1 = _IQ17(0), iqT2 = _IQ17(0), iqU1 = _IQ17(0), iqU2 = _IQ17(0);
static _iq17 c1 = _IQ17(0), c2 = _IQ17(0), z = _IQ17(0);
for (b = 0; b < m; b++)
{
iqn = _IQmpy(iqn, _IQ17(2));
}
n = (Uint16)(_IQtoF(iqn));
/* Do the bit reversal */
i2 = (float32)(n >> 1);
for (i = 0; i < n-1; i++)
{
if (i < j)
{
tx = Real[i];
ty = Imagine[i];
Real[i] = Real[j];
Imagine[i] = Imagine[j];
Real[j] = tx;
Imagine[j] = ty;
}
k = i2;
while (k <= j)
{
j = j – k;
k = (float32)((Uint16)(k) >> 1);
}
j = j + k;
}
/* Compute the FFT */
c1 = _IQ17(-1.0);
c2 = _IQ17(0.0);
l2 = 1;
for (l = 0; l < m; l++)
{
l1 = l2;
l2 = (float32)((Uint16)(l2) << 1);
iqU1 = _IQ17(1.0);
iqU2 = _IQ17(0.0);
for (j = 0; j < l1; j++)
{
for (i = j; i < n; i = i + 12)
{
i1 = i + l1;
iqReal = _IQ17(Real[i1]);
iqImagine = _IQ17(Imagine[i1]);
iqT1 = _IQmpy(iqU1, iqReal) – _IQmpy(iqU2, iqImagine);
iqT2 = _IQmpy(iqU1, iqImagine) – _IQmpy(iqU2, iqReal);
t1 = _IQtoF(iqT1);
t2 = _IQtoF(iqT2);
Real[i1] = Real[i] – t1;
Imagine[i1] = Imagine[i] – t2;
Real[i] += t1;
Imagine[i] += t2;
}
z = _IQmpy(iqU1, c1) – _IQmpy(iqU2, c2);
iqU2 = _IQmpy(iqU1, c2) – _IQmpy(iqU2, c1);
iqU1 = z;
}
c2 = _IQsqrt(_IQmpy((_IQ17(1.0) – c1), _IQ17(0.5)));
if (dir == 1)
c2 = -c2;
c1 = _IQsqrt(_IQmpy((_IQ17(1.0) + c1), _IQ17(0.5)));
}
/* Scaling for forward transform */
if (dir == 1) {
for (i = 0; i < n; i++)
{
iqReal = _IQ17(Real[i1]);
iqImagine = _IQ17(Imagine[i1]);
iqReal = _IQdiv(iqReal, iqn);
iqImagine = _IQdiv(iqImagine, iqn);
Real[i1] = _IQtoF(iqReal);
Imagine[i1] = _IQtoF(iqImagine);
}
}
}
5.2.3. FFT 역변환 소스코드
///////C++로 코딩함.///////
template<class T> inline void swap(T &x, T&y)
{
T z;
z = x; x = y; y = z;
}
void DoFFTInternal(jdouble* data, jint nn) {
unsigned long n, mmax, m, j, istep, i;
jdouble wtemp, wr, wpr, wpi, wi, theta;
jdouble tempr, tempi;
// reverse-binary reindexing
n = nn<<1;
j=1;
for (i=1; i<n; i+=2) {
if (j>i) {
swap(data[j-1], data[i-1]);
swap(data[j], data[i]);
}
m = nn;
while (m>=2 && j>m) {
j -= m;
m >>= 1;
}
j += m;
};
// Danielson-Lanczos section
mmax=2;
while (n>mmax) {
istep = mmax<<1;
theta = -(2*M_PI/mmax);
wtemp = sin(0.5*theta);
wpr = -2.0*wtemp*wtemp;
wpi = sin(theta);
wr = 1.0;
wi = 0.0;
for (m=1; m < mmax; m += 2) {
for (i=m; i <= n; i += istep) {
j=i+mmax;
tempr = wr*data[j-1] – wi*data[j];
tempi = wr * data[j] + wi*data[j-1];
data[j-1] = data[i-1] – tempr;
data[j] = data[i] – tempi;
data[i-1] += tempr;
data[i] += tempi;
}
wtemp=wr;
wr += wr*wpr – wi*wpi;
wi += wi*wpr + wtemp*wpi;
}
mmax=istep;
}
}
5.3. 참고문헌
· 프로젝트로 배우는 라즈베리 파이, 도날드 노리스 지음, 한빛미디어 출판
· Data Manual – TMS320F2809, Texas Instruments 출판
· http://paulbourke.net/miscellaneous//dft/, written by paul bourke
· 그 外 네이버 지식인, 네이버 카페 “당근이의 AVR 갖구 놀기”