[한컴MDS] 서보모터 조이스틱 제어 펌웨어 프로젝트 후기

대학시절, 임베디드에 진심이었던 시절이 있었습니다.
임베디드 개발 동아리에 들어가 'Atmega128',  'Coretex m4' , 'RaspBerryPi'  등 여러 제어보드를 다뤄보기도 하고, 해커톤에 참가해 수상도 하고 졸업하고 나서도 한컴 MDS 에서 진행한, 요즘으로 말하면 임베디드 부트캠프도 다녔는데  '한컴 MDS 임베디드 개발자 양성과정' 에서 진행한 프로젝트에 대해서 정리해 보려고 합니다.


한이음 ICT 해커톤 공모전 도전기

https://www.owl-dev.me/blog/37


펌웨어 프로젝트 소스

https://github.com/jinsujj/joystick-firmware



소프트웨어 개발 과정 중에서, 하드웨어를 제어하기 위한 소프트웨어를 흔히 "임베디드 소프트웨어 (Embedded Software)" 라 칭합니다.

그리고 임베디드 소프트웨어로는 운영체제 없이 제어하는 '펌웨어(Firmware)' 와 주로 Linux O/S 에서 동작하는 "커널 모듈 ( Kernel Module)", 디바이스 드라이버(Device Driver) 로 분류됩니다.


교육 중에 프로젝트를 구상해야 하는 시기에, 평소 들고다니는 하드웨어 보관 상자에, 언젠가 써먹겠지 하고 쟁여두던 서보모터와, 조이스틱 하드웨어 모듈이 있어서, 교육 마지막 프로젝트 과제에서, 이들을 이용해 펌웨어 제작을 하게 되었습니다. 






이번 프로젝트는 S3C2450 칩에서 제공하는 여러 기능중에,

아날로그 신호를 디지털로 변환해주는 ADC (Analog Digital Converter) 레지스터펄스폭 단위로 서보모터를 제어하는 PWM ( Pulse with modulation) 레지스터,그리고 타이머를 체킹하는 WDT (Watch Dog Timer) 레지스터를 활용해서 구현했습니다.




S3C2450 - SoC 칩 ( 타겟 보드 )


S3C2450 보드 칩 사용자 매뉴얼 ( DataSheet )


(* 아래 2개 동일한 PDF 문서 URL 입니다)

https://www.manualslib.com/download/1308248/Samsung-S3c2416.html

http://www.fdi.ucm.es/profesor/mendias/psyd/docs/S3C2450.pdf




'서보모터'와, '조이스틱', '부저' 센서 모듈과, 타겟 보드 SoC DataSheet 을 활용해, 펌웨어 파일을 만들고 S3C2450 칩에 Linux O/S 를 올려서 S3C2450 SoC 칩이 위 하드웨어들을 제어하는 방식을 갖게 됩니다. 



전체 소스는 Github Repo 에서 확인이  가능하며 하드웨어를 제어하기 위한 레지스터 비트 맵핑은 어렵지만, 소스 로직 자체는 간단합니다. 핵심이 되는 소스로는,  펌웨어 동작 시작 관련 Main.c , 아날로그 디지털 컨버터 관련 addUse.c, 조이스틱 관련 Joystic.c 로 구성됩니다.


참고로 리눅스용 C 컴파일러는 GCC (GNU Compiler Collection) 라 불리며, 아래 단계를 거쳐 빌드 됩니다.


전처리 단계: C 파일을 gcc 컴파일러로 컴파일 할 경우 전처리 단계가 진행되는데, 그 결과로 i 파일을 만들어냅니다.


gcc -E main.c -o main.i

컴파일 단계: 전처리된 파일로 컴파일을 진행하여 어셈블리어로 된 파일인 s. 파일을 생성합니다. cli 를 사용합니다.

gcc -S main.c

어셈블 단계: 어셈블리어로 쓰여진 s 파일을 컴퓨터가 이해할 수 있는 기계어로 된 파일인 o 파일로 변환합니다


gcc -c main.c

링크 단계: 라이브러리 함수와 여러 오브젝트 파일을 연결해서 실행파일은 a.out 을 생성합니다. 


gcc main.c -o main






Main.c 


#include "my_lib.h" #include "option.h" #include "2450addr.h" void Main(void) { int i, j; /* Memory Management Unit Init */ MMU_Init(); /* LED GPIO Register Init */ Led_Port_Init(); /* Testing LED */ for(i=0;i<16;i++) { Led_Display(i); for(j=0;j<0x1ffff;j++); } /* UART Register Init */ Uart_Init(115200); /* Testing UART */ Uart_Send_String("\nHello ARM !!!\n\n"); /* Srart JoyStick Control */ JoyStick(); for(;;); // endless loop }


펌웨어 프로그램 실행 시, main 이 되는 부분으로, 처음 MMU_Init 으로 타겟보드의 메모리를 초기화 해줍니다. 

*MMU_Init() 함수 구현은, libc.c 파일에 명시 되어 있으며 해당 파일은 S3C2450 SoC 의 라이브러리를 모아둔 파일입니다.


Led_Port_Init() 함수 구현은  마찬가지로 lib.c 파일에 명시되어 있고 아래 처럼 구현되어 있습니다. 


lib.c


/* * ===================================================================== * LED Display Librarues * ===================================================================== */ void Led_Port_Init(void) { /* * LED On : Active Low * Initialize GPGDAT[7:4] : high * Setup GPGCON[15:8] : 01-> GPG7~4 Output mode * GPGUP pull up function disable * */ /* YOUR CODE HERE */ rGPGDAT |= (0xf<<4); rGPGCON &= ~(0xff<<8); rGPGCON |= (0x55<<8); rGPGUDP &= 0xf<<4; } void Led_Display(int data) { /* * LED On : Active high * LED Off: Active low * GPGDAT[7:4] */ rGPGDAT |= (0xf<<4); if(data & 0x01) rGPGDAT &= ~(0x1<<7); if(data & 0x02) rGPGDAT &= ~(0x1<<6); if(data & 0x04) rGPGDAT &= ~(0x1<<5); if(data & 0x08) rGPGDAT &= ~(0x1<<4); }


LED 의 경우에는 S3C2450 칩의 I/O Port 중에서 G 포트를 사용하도록 메모리를 할당 해줍니다.  ( DataSheet 11 Page )

데이터 시트를 찾는 과정을 간략하게 트레이스하면 아래와 같습니다. 





G port 의 'GPGCON', 'GPGDAT', 'GPGUDP' 레지스터의 정보 (메모리 주소, R/W 정보) 확인.  ( DataSheet 82 Page ) 




'GPGCON', 'GPGDAT', 'GPGUDP' 레지스터의 각 Bit 정보를 확인합니다.  ( DataSheet 299 Page ) 







    • ADC (Analog Digital Conveter) 레지스터의 경우도 마찬가지로 메모리 맵핑을 해줍니다  ( DataSheet 577 Page ) 

      각 비트는 C 언어로 비트연산을 통해 각 메모리에 설정을 해줍니다.

      좀더 구체적으로는 Interrupt, polling 및, 각 클럭에 대한 계산 로직까지 고려해야 하지만 핵심은 아래와 같습니다 


      • 14 번 비트에  prescaler 정보를 Enable(1) 로 설정 해줍니다.  (1 <<14)
      • 6 번 비트는 39 의 값을 prescaler 값으로 넣어줍니다   ( 39 == 100111)
      • 2 번 비트는 Normal operation 모드로 설정 해줍니다 ( 0 << 2)
      • 1 번 비트는 read operation 모드로 설정 해줍니다 (1 <<1)
      • 0 번 비트는 A/D 컨버터가 시작하도록 설정 해줍니다 (1)






조이스틱은 이 펌웨어의 핵심 동작 로직으로 먼저 사용할 함수를 미리 선언해 둡니다. 

절차적 지향 프로그래밍에서 자주 사용하던 방법으로, 객체 지향 프로그래밍에 빗대어 설명하자면 인터페이스 선언과 비슷합니다.




다음은 const 값 같은 부저 음계에 대한 값을 선언해 둡니다. 

임베디드의 세계에서는, Soc 타겟보드에 종속적으로, 개발하려는 SoC 칩에 대한 메모리 주소를 상세하게 알고있어야 하는 단점이 있습니다. 



이부분은 서보모터와, 부저에 대한 초기 세팅으로, 기본적으로 Port B 레지스터를 사용하되,  서보모터의 x 축은 GPB0, y 축은 GPB2  그리고 부저는 GPB1 을 사용합니다.  





참고로 서보모터는 PWM ( Pulse with Modulation) 을 사용하기 위해서 타이머를 사용해야 하는데, 타이머 out 핀이 Port B 레지스터에 물려있어서,  PortB 레지스터를 사용했습니다.( DataSheet 42 Page ) 




PWM Timer 관련해서는 'TOUT0', 'TOU1' 'TOU2', 'TOU3' 이렇게 4개의  PWM 신호를 받을 수 있습니다.  ( DataSheet 286 Page ) 





서보모터를 제어하는 로직으로 x 축, y 축 모두  Timer4 번 레지스터를 활용했습니다.

동작로직은  while 문을 돌려서, nput 모드로 조이스틱 모듈로 input 받고,  output 모드로 서보모터 모듈로 output 합니다. 

각 동작 사이에는 Delay 시간을 걸어주었습니다. 


rGPBDAT &= ~(0x1 <<0)    // 00 = input Mode

rGPBDAT |= (0x1 <<0)    // 01 = output Mode 



* 참고로 rTCFG0, rTCFG1 을 동작에 의미없는... 삭제했어야할 코드입니다.






motor  x 와 동일합니다.




motor x 와 비슷합니다. 




Delay 를 걸어주기 위해서 WatchDog Timer 레지스트리를 사용합니다.

구체적인 설정에 대한 설명은 위에 읽는 방법에 대해 기술하여 생략합니다.  다만 PCLK 는 CPU 가 가지고 있는 3Ghz 와 같은 1초에 몇번  Switching 을 할 수 있는지를 나타내는 지표로, FPGA 와 동일하게, 이 클럭을 16 분의1, 128분의1 등으로 줄여서, 타이머 클럭으로 사용합니다. 









ADC (Analog Digital Converter) 관련해서 AIN8(XM), AIN9(XP) 2개의 채널을 활용해서 아날로그 신호를 디지털로 변환합니다.

이 신호는 모터 x 축과, y축을 활용하는 구간에서 적용됩니다. 

 










Joystic.c


#include "2450addr.h" #include "option.h" //#include "macro.h" void Start_Init(void); void motorX(int tone, int duration); void motorY(int tone, int duration); void DelayForPlay2(unsigned short time); #define BASE10 10000 #define TONE_BEEP 1000 #define DURATION_5SEC 5000 #define DURATION_1SEC 1000 #define C1 523 // Do #define C1_ 554 #define D1 587 // Re #define D1_ 622 #define E1 659 // Mi #define F1 699 // Pa #define F1_ 740 #define G1 784 // Sol #define G1_ 831 #define A1 880 // La #define A1_ 932 #define B1 988 // Si #define C2 C1*2 // Do #define C2_ C1_*2 #define D2 D1*2 // Re #define D2_ D1_*2 #define E2 E1*2 // Mi #define F2 F1*2 // Pa #define F2_ F1_*2 #define G2 G1*2 // Sol #define G2_ G1_*2 #define A2 A1*2 // La #define A2_ A1_*2 #define B2 B1*2 // Si void Start_Init(void) { //tout 0 (X Motor) rGPBCON &= ~(0x3 << 0); rGPBCON |= 0x1<<0; //output //tout2 (Y Motor) rGPBCON &= ~(0x3 <<4); rGPBCON |= 0x1<<4; //output //tout3 (Y Motor) *예비 rGPBCON &= ~(0x3 <<6); rGPBCON |= 0x1<<6; //output //tout 1 (Buzzer) rGPBCON &= ~(0x3 <<2); rGPBCON |= 0x1<<2; //off } void motorX(int tone, int duration) { rTCFG0 = (0xff<<8)|(0); rTCFG1 = (0<<20)|(3<<16); rTCNTB4 = 16.113*duration; rTCON &=~ (1<<22); //Timer 4 auto reload off rTCON |= (1<<21); //Timer 4 manual update on rTCON &= ~(1<<21); //Timer 4 manual update off rTCON |= (1<<20); //Timer 4 start/stop while(rTCNTO4 !=0) { rGPBDAT &= ~(0x1<<0); //Motor_x //rGPBDAT &= ~(0x1 <<2); //Motor_y //rGPBDAT &= ~(0x1 <<3); //pwm_pin DelayForPlay2(BASE10/tone); rGPBDAT |= (0x1<<0); //rGPBDAT |= (0x1 <<2); //rGPBDAT |= (0x1 <<3); DelayForPlay2(BASE10/tone); } rTCON &= ~(1<<20); } void motorY(int tone, int duration) { rTCFG0 = (0xff<<8)|(0); rTCFG1 = (0<<20)|(3<<16); rTCNTB4 = 16.113*duration; rTCON &=~ (1<<22); //Timer 4 auto reload off rTCON |= (1<<21); //Timer 4 manual update on rTCON &= ~(1<<21); //Timer 4 manual update off rTCON |= (1<<20); //Timer 4 start/stop /* rTCNTB3 = 16.113*duration; //타이머 4번과 타이머 3번간의 성능 차이가 존재한다. rTCON &=~ (1<<19); //Timer 3 auto reload off rTCON |= (1<<17); //Timer 3 manual update on rTCON &= ~(1<<17); //Timer 3 manual update off rTCON |= (1<<16); //Timer 3 start/stop */ while(rTCNTO4 !=0) { rGPBDAT &= ~(0x1 <<2); //Motor Y DelayForPlay2(BASE10/tone); rGPBDAT |= (0x1 <<2); DelayForPlay2(BASE10/tone); } rTCON &= ~(1<<20); } void Buzzer(int tone, int duration) { rTCFG0 = (0xff<<8)|(0); rTCFG1 = (0<<20)|(3<<16); rTCNTB4 = 16.113*duration; rTCON &=~ (1<<22); //Timer 4 auto reload off rTCON |= (1<<21); //Timer 4 manual update on rTCON &= ~(1<<21); //Timer 4 manual update off rTCON |= (1<<20); //Timer 4 start/stop while(rTCNTO4 !=0) { rGPBDAT &= ~(0x1<<1); //Buzzer DelayForPlay2(BASE10/tone); rGPBDAT |= (0x1<<1); DelayForPlay2(BASE10/tone); } rTCON &= ~(1<<20); } void DelayForPlay2(unsigned short time) // resolution=0.1ms { /* Prescaler value : 39 */ /* Clock Select : 128 */ rWTCON=(37<<8)|(3<<3); // resolution=0.1ms rWTDAT=time+10; // Using WDT rWTCNT=time+10; rWTCON|=(1<<5); while(rWTCNT>10); rWTCON = 0; } void JoyStick(void) { int VarRegist=0; int realValue=0; Start_Init(); Timer_Init();https://www.owl-dev.me/blog/60l ADC_init(); Buzzer(C1,100); Buzzer(D1,100); motorX(500,100); motorY(500,100); while(1) { rADCMUX=0x9; VarRegist = (rADCDAT0&0x3ff); realValue = (int)((VarRegist)*(655.0/1024.0)+245); motorY(realValue,100); //(245~900) Uart_Printf("VarRegist_Y =%d\n",realValue); rADCMUX=0x8; VarRegist = (rADCDAT0&0x3ff); //ADCDAT 레지스터의 data 값만 extract realValue = (int)((VarRegist)*(860.0/1024.0)+90); //(90~950) motorX(realValue,100); Uart_Printf("VarRegist_X =%d\n\n\n",realValue); /* 이론상 0~1024 -> 35~1000 이므로 VarRegist*(965/1024) +35 realValue = (int)(VarRegist*(float)(965.0/1024.0)+50); */ } }





[Reference] 

https://clearwater92.tistory.com/39