사람의 손으로 하지 않고
기계로만 악기를 연주 할 수 있을까?
하는 마음에서 시작하여
세컨드 기타의 역할을 충분히 담당할 수 있을 만한 기타를 만들어보고 싶어
만들게 되었다.
구현하는 것을 분석한 결과
무인 기타 연주기를 만들기 위해 해결해야하는 문제가 2가지가 있었다.
- 어떻게 박자에 맞게 작동시키는가.
- 실제로 작동을 어떻게 시키는가.
전자의 경우에는 리눅스에서 있는 타이머를 활용하였고,
후자의 경우에는 GPIO를 빠른 시간에 기동하고
pwm도 하드웨어적으로 구현가능한
아두이노를 사용하기로 하였다.
(다만, 통신의 문제때문에 3.3v IO를 가지는 아두이노 듀에를 사용하였다.)
전체적인 구조 스케치는 다음과 같다.
라즈베리파이에서는 악보를 분석하여 timer 작동주기마다 아두이노로 작동 데이터를 보내게 된다.
아두이노는 받은 데이터를 분석하여 실제로 작동하게 된다.
라즈베리 파이에서 이 프로젝트에서는
6,8,10번핀만 사용한다. 모든 액추에이팅 부분은 아두이노에서 처리한다.
<회로, 구동부>
위의 그림은 실제 결선이 어떠한 방식으로 될지 스케치한 그림이다.
7.4v 배터리를 직접 솔레노이드에 연결하고 MOSFET으로 제어를 하게 된다.
(3.3v로 구동시킬 수 있는 MOSFET을 찾아야 한다 데이터 시트를 참고할 것,
필자의 경우 몇번의 실수 끝에 IRL540 을 찾아 사용했다.)
제어 신호는 아두이노로부터 받으며 솔레노이드는 총 6*3 = 18개가 된다.
(NMOS의 경우 +단이 아닌 -단에 붙어서 작동한다.
+ ---> 솔레노이드 ---> MOSFET ----> -
와 같이 이어진다.)
제어신호의 float을 방지하기 위해 GND와 연결한 Pulldown register를 장착하였다.
(실제 회로에서는 4.7k 저항)
위 그림은 위에서 언급한 것들을 실제로 구현한 회로이다.
회로가 만들어 졌다면 이제 실제 구동하는 부분을 제작할 차례이다.
코드를 누르는 부분은 솔레노이드로 만들었다.
푸쉬형 솔레노이드로 리턴 스프링과 C-링을 대체하여 마개(노란색)을 제작하여 붙이고
연장 로드(회색 포맥스)를 401록타이트를 이용하여 붙였다.
솔레노이드가 가하는 힘이 강하지 않기 때문에 Deformation의 우려가 적고
공간이 협소하겨 제대로 된 마운트를 제작하기 힘들기 때문에
강력 양면테이프를 이용하여 솔레노이드를 부착하였다.
서보모터의 경우 결선을
+ 는 레귤레이터에서나오는 5v전원을
- 는 그라운드
Signal은 아두이노의 PWM포트에 연결하였다.
아두이노의 기본 pwm은 490Hz 에서 1000Hz이고 아날로그 서보의 PWM주기는 50Hz이므로
50Hz의 PWM을 생성해주는 서보 라이브러리를 사용하였다.
스트로크 부분 제작(프로토타입)
서보모터와 포맥스, 피크를 이용하여
줄을 튕길 수 있게 만들었다.
서보모터를 낮게 달면 줄을 튕기면서 생기는 반동때문에
6줄이 제대로 안쳐지는 경우가 많아 위와 같이 만들었다.
스트로크 부분 제작(보완)
프로토타입에서는 다운스트로크나 업스트로크가 2번 연속해서 나왔을 때에
제대로 연주가 불가능했다.
때문에 피크를 줄에서 뗄수 있는 서보모터를 하나 더 장착하여 작동시키고
연장로드를 약하게( 포맥스->플라스틱) 하여 잘 작동시키게 하였다.
기타를 연주할 때
>솔레노이드의 작동때문에
> 스트로크 때문에
기타의 소리를 죽이는 뮤트부분의 개발이 필요했고,
서보모터와 포맥스, 스펀지(검은색)을 이용하여 그 부분을 만들었다.
<라즈베리파이>
/* 20 Jul 2014
* Univ. of seoul M.I.E
* Min-je Lee, Geon-hee Joe
* Team 깽판
*
*/
/* DEFINE MACRO */
#define NTSEC 1000000000
#define MAX_LENGTH 100
#define MAX_STROKE_OFFSET 7
#define test 1
// print macro
#define absolute 1 //절대시간으로 표시
#define print_stroke 1
#define print_code 1
#define print_echo 0
#define print_time 1
// debug
#define timewrite 0
#define use_code 1
#define use_stroke 1
// sched
#define schedfifo 0
/* HEADER INCLUDE */
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <unistd.h>
#include <errno.h>
#include <wiringPi.h>
#include <wiringSerial.h>
#include <sched.h>
/* GLOBAL VARIABLE */
// Timer
timer_t code; // 코드 타이머
timer_t stroke; // 스트로크 타이머
struct timespec tmp0; // 프로그램 시작시간
struct timespec tmpn; // 시리얼 받은 시간
// code and stroke
char Code[MAX_LENGTH][3]; // 코드를 읽어들여 저장하는 변수
int converted_code[MAX_LENGTH]; // 코드를 바이너리 형태로 저장하는 병수
unsigned int code_count =0; // 현재 연주하는 코드 카운트
unsigned int length=0; // 파일에서 읽어들인 전체 코드의 길이
const char code_array[30][3] ={ // 코드 운지 prefix 배열
{0b00010000, 0b01001000, 0b10000010}, // C
{0b00000010, 0b01000101, 0b10000000}, // D
{0b00011000, 0b01000100, 0b10000000}, // E
{0b00011000, 0b01000100, 0b10100011}, // F
{0b00100001, 0b01010000, 0b10000000}, // G
{0b00000000, 0b01001110, 0b10000000}, // A
{0b00000000, 0b01000000, 0b10000000}, // B need more nodes
{0b00000000, 0b01000000, 0b10000000},
{0b00000000, 0b01000000, 0b10000000},
{0b00000000, 0b01000000, 0b10000000},
{0b00010000, 0b01000000, 0b10001010}, // Cm
{0b00000010, 0b01000100, 0b10000001}, // Dm
{0b00000000, 0b01011000, 0b10000000}, // Em
{0b00011000, 0b01000000, 0b10100111}, // Fm
{0b00000000, 0b01000000, 0b10000000}, // Gm need more nodes
{0b00000000, 0b01001100, 0b10000010}, // Am
{0b00000000, 0b01000000, 0b10000000}, // Bm need more nodes
/// 하위 비트 얇은 줄
{0b00000111, 0b01000111, 0b10000111},
{0b00111000, 0b01111000, 0b10111000},
{0b00101010, 0b01101010, 0b10101010},
{0b00010100, 0b01001000, 0b10000010}, // C7
{0b00000000, 0b01000101, 0b10000010}, // D7
{0b00000010, 0b01011000, 0b10000100}, // E7
{0b00000000, 0b01000000, 0b10000000}, // F7 need more nodes
{0b00100000, 0b01010000, 0b10000001}, // G7
{0b00000000, 0b01001010, 0b10000000}, // A7
{0b00000000, 0b01010101, 0b10001000}, // B7
{0b00111111, 0b01000000, 0b10000000},
{0b00000000, 0b01111111, 0b10000000},
{0b00000000, 0b01000000, 0b10111111}
};
/* STROKE INSTRUCTION SET
// 0xC1 : 다운스트로크
// 0xC2 : 업스트로크
// 0xC4 : 스트로크 온
// 0xC8 : 스트로크 오프
// 0xD0 : 뮤트?
// 0xE0 : 뮤트 해제
// 0xFF : 곡 종료
*/
unsigned int stroke_array[] ={ // 스트로크 연주 리듬의 어레이
0b11100001,
0b11000100,
0b11000010,
0b11001000,
0b11000001,
0b11000100,
0b11000010,
0b11011000,
0b11100001,
0b11000100,
0b11000010,
0b11001000,
0b11000001,
0b11000010,
0b11000000,
0b11010000,
};
int max_stroke_offset= MAX_STROKE_OFFSET; // 최대 스트로크 오프셋
int stroke_offset = MAX_STROKE_OFFSET; // 스트로크 현재 보고 있는 리듬의 오프셋
/*File descriptor for Serial connection*/
int fd;
/* FUNCTION DEFINE */
static void timer_handler( int sig, siginfo_t *si, void *uc );
static int makeTimer( char *name, timer_t *timerID, long long sec, long long nsec, long long delay ) ;
void run_code();
void run_stroke();
void convert_Code(int i);
/* MAIN CODE */
int main (int argc,char *argv[])
{
/* VARIABLE DEFINE */
// tmp count variable
int i =0;
//time variable
long long sec =1, nsec =0; // code second, nanosecond
long long s_sec =1, s_nsec =0; // stroke second, nanosecond
unsigned long long beat;
// input variable
int BPM = 1;
int stay = 2;
// Read file pointer
FILE* fp;
// Write file pointer
FILE* fpw;
// sched
#if schedfifo
printf("Using FIFO sched\n");
struct sched_param sp;
memset( &sp, 0, sizeof(sp) );
sp.sched_priority = 99;
sched_setscheduler( 0, SCHED_FIFO, &sp );
#endif
/* INIT */
// argumentcheck
if(argc!=3)
{
printf("INPUT MESSAGE TYPE\n PROGRAM FILE BPM\n");
return -1;
}
// Serial init
if ((fd = serialOpen ("/dev/ttyAMA0", 115200)) < 0)
{
fprintf (stderr, "Unable to open serial device: %s\n", strerror (errno)) ;
return -1 ;
}
// calc max stroke offset
max_stroke_offset = sizeof(stroke_array)/sizeof(*stroke_array);
stroke_offset = max_stroke_offset;
printf("STROKE OFFSET = %d\n",stroke_offset);
// 악보 해석 및 컨버팅
/* Delay calc */
if(BPM!=0)
{
BPM = atoi(argv[2]);
beat = NTSEC*60.0/BPM;
sec = (beat*stay)/NTSEC;
nsec = (beat*stay)%NTSEC;
s_sec = (beat*4/(max_stroke_offset))/NTSEC;
s_nsec = (beat*4/(max_stroke_offset))%NTSEC;
printf("BPM = %d\n",BPM);
printf("Delay between beat(ns) = %lld\n" ,beat);
printf("Delay between code = %lld.%lld\n" ,sec,nsec);
printf("Delay between STROKE = %lld.%lld\n" ,s_sec,s_nsec);
}
/* FILE read */
if((fp = fopen(argv[1],"r")) == NULL)
{
printf("FILE ERROR! CHECK FILE EXIST! \n");
return 0;
}
/* FILE parsing */
while(!feof(fp))
{
fscanf(fp,"%s",Code[length]);
printf("%d\t%s\n",length,Code[length]);
length++;
}
printf("Whole Code length : %d\n",length);
/* FILE converting & save in array */
for(i=0;i<length;i++)
{
convert_Code(i);
}
fclose(fp);
/* FILE Writeing open */
#if timewrite
if((fpw = fopen(argv[1],"a")) == NULL)
{
printf("FILE ERROR! CHECK FILE EXIST! \n");
return 0;
}
#endif
/* Make timer (in signal) */
printf("MAKE TIMER \n");
clock_gettime(CLOCK_REALTIME, &tmp0);
#if use_code
makeTimer("First Timer", &code, sec, nsec,0);
#endif
#if use_stroke
makeTimer("Second Timer", &stroke, s_sec, s_nsec,7*s_nsec);
#endif
//makeTimer2("Second Timer", &stroke, sec, nsec);
/* Do busy work. */
while (1)
{
while(serialDataAvail(fd))
{
#if print_echo
clock_gettime(CLOCK_REALTIME, &tmpn);
printf("%9ld.%9ld\n ",tmpn.tv_sec,tmpn.tv_nsec);
printf("%x\n",serialGetchar(fd));
fflush(stdout);
#endif
}
}
/* 추가 명령 대기 */
close(fd);
return 0;
}
static int makeTimer( char *name, timer_t *timerID, long long sec, long long nsec, long long delay )
{
struct sigevent te;
struct itimerspec its;
struct sigaction sa;
int sigNo = SIGRTMIN;
/* Set up signal handler. */
sa.sa_flags = SA_SIGINFO;
sa.sa_sigaction = timer_handler;
sigemptyset(&sa.sa_mask);
if (sigaction(sigNo, &sa, NULL) == -1)
{
printf("sigaction error\n");
return -1;
}
/* Set and enable alarm */
te.sigev_notify = SIGEV_SIGNAL;
te.sigev_signo = sigNo;
te.sigev_value.sival_ptr = timerID;
timer_create(CLOCK_REALTIME, &te, timerID);
its.it_interval.tv_sec = sec;
its.it_interval.tv_nsec = nsec ;
its.it_value.tv_sec = sec+(nsec+delay)/NTSEC;
its.it_value.tv_nsec = (nsec+delay)%NTSEC;
printf("start time : %ld,%ld\n",its.it_value.tv_sec,its.it_value.tv_nsec);
timer_settime(*timerID, 0, &its, NULL);
return 0;
}
static void timer_handler( int sig, siginfo_t *si, void *uc )
{
timer_t *tidp;
tidp = si->si_value.sival_ptr;
struct timespec tmp;
#if print_time
clock_gettime(CLOCK_REALTIME, &tmp);
#if absolute
printf("%9ld.%9ld ",tmp.tv_sec,tmp.tv_nsec);
#else
printf("%9ld.%9ld ",tmp.tv_sec-tmp0.tv_sec, tmp.tv_nsec-tmp0.tv_nsec);
#endif
#endif
#if use_code
if ( *tidp == code )
{
#if print_time
printf("CODE \t");
#endif
run_code();
/* 코드 바꿀 내용 작성 */
}
#endif
#if use_stroke
if ( *tidp == stroke )
{
#if print_time
printf("STROKE \t");
#endif
run_stroke();
}
#endif
#if print_time
printf("\n");
#endif
}
int stop()
{
#if use_code
if (timer_delete (code) < 0)
{
fprintf (stderr, "timer delete error\n");
return -1;
}
#endif
#if use_stroke
if (timer_delete (stroke) < 0)
{
fprintf (stderr, "timer delete error\n");
return -1;
}
#endif
}
void convert_Code(int i)
{
// CODE to char*3
printf("CONVERT CODE : %d\t%s\t",i,Code[i]);
if(strcmp(Code[i]," ")==0)
{
converted_code[i] = 9;
}
else if(strcmp(Code[i],"C")==0)
{
converted_code[i] = 0;
}
else if(strcmp(Code[i],"D")==0)
{
converted_code[i] = 1;
}
else if(strcmp(Code[i],"E")==0)
{
converted_code[i] = 2;
}
else if(strcmp(Code[i],"F")==0)
{
converted_code[i] = 3;
}
else if(strcmp(Code[i],"G")==0)
{
converted_code[i] = 4;
}
else if(strcmp(Code[i],"A")==0)
{
converted_code[i] = 5;
}
else if(strcmp(Code[i],"B")==0)
{
converted_code[i] = 6;
}
else if(strcmp(Code[i],"Cm")==0)
{
converted_code[i] = 10;
}
else if(strcmp(Code[i],"Dm")==0)
{
converted_code[i] = 11;
}
else if(strcmp(Code[i],"Em")==0)
{
converted_code[i] = 12;
}
else if(strcmp(Code[i],"Fm")==0)
{
converted_code[i] = 13;
}
else if(strcmp(Code[i],"Gm")==0)
{
converted_code[i] = 14;
}
else if(strcmp(Code[i],"Am")==0)
{
converted_code[i] = 15;
}
else if(strcmp(Code[i],"Bm")==0)
{
converted_code[i] = 16;
}
else if(strcmp(Code[i],"C7")==0)
{
converted_code[i] = 20;
}
else if(strcmp(Code[i],"D7")==0)
{
converted_code[i] = 21;
}
else if(strcmp(Code[i],"E7")==0)
{
converted_code[i] = 22;
}
else if(strcmp(Code[i],"F7")==0)
{
converted_code[i] = 23;
}
else if(strcmp(Code[i],"G7")==0)
{
converted_code[i] = 24;
}
else if(strcmp(Code[i],"A7")==0)
{
converted_code[i] = 25;
}
else if(strcmp(Code[i],"B7")==0)
{
converted_code[i] = 26;
}
else if(strcmp(Code[i],"1")==0)
{
converted_code[i] = 17;
}
else if(strcmp(Code[i],"2")==0)
{
converted_code[i] = 18;
}
else if(strcmp(Code[i],"3")==0)
{
converted_code[i] = 19;
}
else if(strcmp(Code[i],"4")==0)
{
converted_code[i] = 27;
}
else if(strcmp(Code[i],"5")==0)
{
converted_code[i] = 28;
}
else if(strcmp(Code[i],"6")==0)
{
converted_code[i] = 29;
}
else if(strcmp(Code[i],"0")==0)
{
converted_code[i] = 9;
}
else
{
printf("NOT COORESPOND CODE\n");
converted_code[i] = 9;
}
printf("\t%d\n",converted_code[i]);
}
void run_code()
{
int i=0;
#if print_code
printf("current code : %d\n",code_count);
#endif
for(i=0;i<3;i++)
{
#if print_code
printf("%x ",code_array[converted_code[code_count]][i]);
#endif
serialPutchar(fd,code_array[converted_code[code_count]][i]);
}
code_count++;
if(code_count>=length)
{
printf("\n\nSTOP\n\n");
stop();
serialPutchar(fd,(0xFF));
exit(1);
}
}
void run_stroke()
{
if(stroke_offset==max_stroke_offset)
stroke_offset=0;
#if print_stroke
printf("\t\tstroke %x\toffset\t%d",stroke_array[stroke_offset],stroke_offset);
#endif
serialPutchar(fd,(stroke_array[stroke_offset]));
stroke_offset++;
}
라즈베리파이 부분의 코드는 위와 같다.
라즈베리파이에서 하는 역할은 위의 그림을 다시 상기시켜보면
1. 음악을 가지고 와서 저장
2. 음악의 속도에 맞는 속도로 타이머 생성
3. 각각의 코드에 맞는 시그널 생성, 전송이다.
위의 과정을 자세히 풀어서 생각하면 필요한 것들이 생겨난다.
1. 음악을 가지고 와서 저장
파일 Open 텍스트값 Read, Code저장
파일 Close
( 파일 입출력은 시스템콜을 사용하기 때문에 속도가 더뎌 메모리상에 변수를 저장해 놓는다.)
2. 음악의 속도에 맞는 속도로 타이머 생성
BPM 입력받음( 아규먼트로)
BPM의 단위( Beat / min)을 ( second / Beat)로 변환
위의 시간에 맞는 타이머를 생성
3. 각각의 코드에 맞는 시그널 생성, 전송이다.
시리얼 통신 채널 생성
코드를 바이너리 형태로 변환
위의 내용이 라즈베리파이 코드 내에 구현되어 있다.
물론 개발 단계 자체가 폭포수 개발이라 코드가 깔끔하지 않고 너저분한 점이 있지만,
코드 내용 자체가 어렵지 않으므로 충분히 이해 할 수 있을 것이라 생각한다.
*아마 가장 이해하기 힘든 부분이 아두이노로 보내는 바이너리 코드 부분일 것이라 생각해서
추가 설명을 달아둔다.
입력 받는 텍스트의 경우
C E Em F G G C....
와 같은 포맷으로 입력 된다.
그 포맷을 띄어쓰기 단위로 저장하여(3바이트 배열) 값을 strcmp로 적절한 값에 매칭하여 숫자로 저장한다.
각 코드는 각 노드에 따라 3바이트로 저장되고 이 코드는 코드가 변할 때마다 날아가게 된다.
그리고 바이트의 순서에 따른 문제를 회피하고자 각 바이트는 독립적인 명령을 가진다.
전송되는 바이트의 구조는 다음과 같다.
상위비트 2개는 이 제어코드가 코드를 제어할 것인지 스트로크를 제어할 것인지를 알려준다.
예를 들어
0b 01 010101 이라면
2번 노드의 1번줄, 3번줄, 5번줄을 누르고, 2,4,6번 줄을 떼라는 명령이 된다.
스트로크의 경우 조금 다르게 되는데
3가지의 기능을 구현한다. Mute기능과 stroke on/off stroke up/down이다.
사실 6비트가 아닌 3비트로도 구현이 가능하지만, 가시적으로 잘 보기 위해서 위와 같이 코딩했다.
예를 들어서
0b11 10 01 10 이라면
뮤트 off
스트로크 on
스트로크 up 이 된다.
코드 평가
많은 사람들이 간과하는 것중 하나는
리눅스란 운영체제는 잘 돌아가는 것처럼 느껴지지만
실시간성이 전혀 보장되지 않는다는 것이다.
(OS가 없는 아두이노에서는 타이머를 만들면 그 시간에 거의 맞춰서 일어날 수밖에 없지만
라즈베리파이에서는 그렇지 않다. 같은 시간에 일어날 수도 있다.)
때문에 커널의 변경이 없는 한도에서 최대한 퍼포먼스를 높이기 위해 SCHED FIFO를 사용했다.
물론 이 기능을 제대로 쓰기 위해서는 시스템콜(printf)를 모두 없애야 한다.
프로그램에서 타이머의 편차에 영향을 끼치는 요소는 다음과 같다.
- OS의 스케줄링 방식에 의해 실제 일어나는 시간이 달라짐
- 일어나는 시간 편차 + 계산시간 편차 + 시리얼 전송시간 편차가 라즈베리파이 부분에서 생성
- 아두이노는 풀링 방식으로 받고 있고, 풀링하는 시간동안의 편차 발생
- 액추에이터 변경함수를 콜하고 실제 구동하는 데까지 생기는 편차
이 오차들을 모두 합했을 때에 아래 오실로스코프를 확인해보았을 때(printf를 넣으면 매우 오차가 커진다. 시스템콜이 시간이 많이 들어서...) 0.02ms = 20us 정도를 가진다.
sched_fifo on
sched fifo off
주기 시간의 편차가 약간 줄어들었음을 알 수 있다.
만약 더 실시간성이 중요한 프로젝트를 진행한다면 Xenomai를 설치(http://naudhizb.tistory.com/280)하고
제노마이 API를 이용해 코딩한다면 매우 정확한 시간과 execution time을 얻을 수 있다.
실제 연주 결과는 다음과 같다.
< 아두이노 >
아두이노는 위에서 언급한 제어 코드를 받아 풀링 방식으로 무한루프를 돌면서 제어를 하게 된다.
코드는 다음과 같다.
/* LICENSE */
/*Include*/
#include <Servo.h>
/* DEFINE MACRO */
#define TEST_PIN1 53
#define TEST_PIN2 52
// NODE PIN DEFINE
#define NODE_1_1 22
#define NODE_1_2 24
#define NODE_1_3 26
#define NODE_1_4 28
#define NODE_1_5 30
#define NODE_1_6 32
#define NODE_2_1 23
#define NODE_2_2 25
#define NODE_2_3 27
#define NODE_2_4 29
#define NODE_2_5 31
#define NODE_2_6 33
#define NODE_3_1 34
#define NODE_3_2 36
#define NODE_3_3 38
#define NODE_3_4 40
#define NODE_3_5 42
#define NODE_3_6 44
// STROKE and MUTE DEFINE
#define STROKE_1 10
#define STROKE_2 8
#define STROKE_3 11
// MOTION DEFINE
// big thin
#define STROKE_UP 81
#define STROKE_DOWN 120
// small up
#define STROKE_ON 138
#define STROKE_OFF 147
//
#define MUTE_ON 60
#define MUTE_OFF 70
#define NODE_ON 1
#define NODE_OFF 0
/* VARIABLE DECLARATION */
unsigned char stage =0;
/*
스테이지 변수 ** 만약 아두이노에서 모든 상황을 컨트롤 할 때에 **
0 - 초기화 전
1 - 초기화, 입력 대기, Reset
2 - 프로젝트 RECV, Stopped
3 - 실행중, Running
*/
unsigned long timer = 0; // 현재 시간
unsigned char buffer =0xC0; // 받은 메세지의 내용
unsigned char buffer_stroke =0x00;
unsigned int counter =0;
unsigned char temp=0x01;
/* FUNCTION DECLARATION */
void set_code(unsigned char node, unsigned char string);
void set_stroke();
/*
void reset();
void start();
void stop();
*/
int mservo1 = STROKE_1;
int mservo2 = STROKE_2;
int mservo3 = STROKE_3;
Servo myservo1;
Servo myservo2;
Servo myservo3;
int pos = 0;
void setup()
{
// SERIAL INIT
Serial1.begin(115200);
Serial.begin(115200);
// TEST PIN INIT
pinMode(TEST_PIN1,OUTPUT);
digitalWrite(TEST_PIN1,LOW);
pinMode(TEST_PIN2,OUTPUT);
digitalWrite(TEST_PIN2,LOW);
// On board LED INIT
pinMode(13,OUTPUT);
digitalWrite(13,LOW);
// NODE INIT
pinMode(NODE_1_1,OUTPUT);
pinMode(NODE_1_2,OUTPUT);
pinMode(NODE_1_3,OUTPUT);
pinMode(NODE_1_4,OUTPUT);
pinMode(NODE_1_5,OUTPUT);
pinMode(NODE_1_6,OUTPUT);
pinMode(NODE_2_1,OUTPUT);
pinMode(NODE_2_2,OUTPUT);
pinMode(NODE_2_3,OUTPUT);
pinMode(NODE_2_4,OUTPUT);
pinMode(NODE_2_5,OUTPUT);
pinMode(NODE_2_6,OUTPUT);
pinMode(NODE_3_1,OUTPUT);
pinMode(NODE_3_2,OUTPUT);
pinMode(NODE_3_3,OUTPUT);
pinMode(NODE_3_4,OUTPUT);
pinMode(NODE_3_5,OUTPUT);
pinMode(NODE_3_6,OUTPUT);
digitalWrite(NODE_1_1,LOW);
digitalWrite(NODE_1_2,LOW);
digitalWrite(NODE_1_3,LOW);
digitalWrite(NODE_1_4,LOW);
digitalWrite(NODE_1_5,LOW);
digitalWrite(NODE_1_6,LOW);
digitalWrite(NODE_2_1,LOW);
digitalWrite(NODE_2_2,LOW);
digitalWrite(NODE_2_3,LOW);
digitalWrite(NODE_2_4,LOW);
digitalWrite(NODE_2_5,LOW);
digitalWrite(NODE_2_6,LOW);
digitalWrite(NODE_3_1,LOW);
digitalWrite(NODE_3_2,LOW);
digitalWrite(NODE_3_3,LOW);
digitalWrite(NODE_3_4,LOW);
digitalWrite(NODE_3_5,LOW);
digitalWrite(NODE_3_6,LOW);
// STROKE INIT
pinMode(STROKE_1,OUTPUT);
myservo1.attach(mservo1);
myservo1.write(STROKE_UP);
pinMode(STROKE_2,OUTPUT);
myservo2.attach(mservo2);
myservo2.write(STROKE_ON);
pinMode(STROKE_3,OUTPUT);
myservo3.attach(mservo3);
myservo3.write(MUTE_OFF);
Serial1.println("INIT COMPLETE");
}
/*
제어 코드 구조
0b00 000000 1번 노드의 6줄 컨트롤
0b01 000000 2번 노드의 6줄 컨트롤
0b10 000000 3번 노드의 6줄 컨트롤
0b11 000000 스트로크 및 추가명령어(2^6-2개 가능)
풀링 방식으로 속도 최대화
*/
void flag()
{
if(temp%2==0)
digitalWrite(TEST_PIN1,HIGH);
if(temp%2==1)
digitalWrite(TEST_PIN1,LOW);
temp++;
}
void loop()
{
if(Serial1.available())
{
// Serial1.write(0x11);
buffer = Serial1.read();
if((buffer&0xFF)==0xFF)
{
Serial.println("end");
set_code(0x00,0x00);
set_code(0x01,0x00);
set_code(0x10,0x00);
}
else if((buffer&0xC0)==0xC0)
{
counter++;
if((buffer&0xC1)==0xC1)
myservo1.write(STROKE_DOWN);
if((buffer&0xC2)==0xC2)
myservo1.write(STROKE_UP);
if((buffer&0xC4)==0xC4)
myservo2.write(STROKE_ON);
if((buffer&0xC8)==0xC8)
myservo2.write(STROKE_OFF);
if((buffer&0xD0)==0xD0)
myservo3.write(MUTE_ON);
if((buffer&0xE0)==0xE0)
myservo3.write(MUTE_OFF);
}
else
{
set_code((buffer&0xC0)>>6,(buffer));
}
Serial1.write(buffer);
digitalWrite(13,counter%2);
}
}
void set_code(unsigned char node, unsigned char string)
{
if(node==0)
{
digitalWrite(NODE_1_1,(string&0x01)>>0);
digitalWrite(NODE_1_2,(string&0x02)>>1);
digitalWrite(NODE_1_3,(string&0x04)>>2);
digitalWrite(NODE_1_4,(string&0x08)>>3);
digitalWrite(NODE_1_5,(string&0x10)>>4);
digitalWrite(NODE_1_6,(string&0x20)>>5);
}
if(node==1)
{
digitalWrite(NODE_2_1,(string&0x01)>>0);
digitalWrite(NODE_2_2,(string&0x02)>>1);
digitalWrite(NODE_2_3,(string&0x04)>>2);
digitalWrite(NODE_2_4,(string&0x08)>>3);
digitalWrite(NODE_2_5,(string&0x10)>>4);
digitalWrite(NODE_2_6,(string&0x20)>>5);
}
if(node==2)
{
digitalWrite(NODE_3_1,(string&0x01)>>0);
digitalWrite(NODE_3_2,(string&0x02)>>1);
digitalWrite(NODE_3_3,(string&0x04)>>2);
digitalWrite(NODE_3_4,(string&0x08)>>3);
digitalWrite(NODE_3_5,(string&0x10)>>4);
digitalWrite(NODE_3_6,(string&0x20)>>5);
}
}
아두이노는 코드자체가 어렵지 않고 이해하기 쉽기 때문에 따로 설명할 필요는 없을 듯 하다.
참고로 서보를 사용하기 위해 50Hz의 PWM을 만들어내는 서보 라이브러리를 사용하였다.
사실 아두이노의 레지스터 맵까지 사용하면 라즈베리파이에서 사용 하는 것보다 훨씬 정확한 타이머를 만들 수 있지만, 개발시간의 제약으로 그렇게 하지 못한점이 아쉬웠다.
회로적 지식이 더 있다면(mux를 이용한 IO확장) 더욱 재밋는 프로젝트를 진행할 수 있을 것이라고 생각한다.
개선할 수 있는 점
- RTOS커널을 이용한 실시간성 향상 / 아두이노 코드 개발을 통한 실시간성 향상
- 버퍼나 멀티플렉서를 이용한 코드 부분 IO연장
- 솔레노이드 이외의 액추에이터 구성
Reference
Reference.hwp
raspberryPi_B.pdf
irl540npbf.pdf
MCSMO-0630S06STD.pdf
Thanks to
Team 깽판
조건희
김의태
이영섭