날마다 새롭게 또 날마다 새롭게

임베디드 프로그래밍 C코드 최적화 정리 본문

프로그래밍/Embeded S/W

임베디드 프로그래밍 C코드 최적화 정리

아무유 2013. 2. 2. 16:31

1. 임베디드 소프트웨어 개발이란

- 임베디드 시스템 : 특수 목적을 가지고 제작되는 시스템 (전자레인지)으로 필요한 기능 구현에 최적화 되어 있다.

- 임베디드 시스템 구현 : 특별한 설정 없이 편하게 개발할 수 있는 PC와는 다르게 임베디드 시스템은 개발 환경이 모두 달라서 컴파일러에 필요한 정보를 모두 설정해 주거나 프로그램에서 직접 처리해야 한다.

- 임베디드 소프트웨어 개발방법

- 개발환경의 종류

- 네이티브 개발환경 : 개발환경과 실행환경이 같다. (PC용 소프트웨어)

- 크로스 개발환경 : 개발환경과 실행환경이 다르다. (임베디드 소프트웨어)

- 크로스 개발환경의 구성

- 호스트 시스템 : 소프트웨어를 개발하는 환경 (PC) 

- 타겟 시스템 : 소프트웨어가 실행되는 시스템 (임베디드 시스템)

- 툴체인 : 크로스 컴파일러, 크로스 어셈블러, 링커/로케이터 등으로 구성된 소프트웨어 개발도구

- 임베디드 개발에 필요한 케이블 종류

- 시리얼 케이블 : UART(Universal Asynchronous Receive/Transmitter) 범용 비동기화 송수신기를 사용한 통신으로 시스템 콘솔에서 통신 내용을 확인할 수 있다. 타겟 시스템 모니터링과 디버깅이 가능하다.

- JTAG 케이블 : 타겟 시스템에 프로그램 다운로드와 플래시 메모리에 프로그램을 탑재할 수 있다. 디버깅 기능도 가능하다.

- 이더넷/USB 케이블 : 타겟 시스템에 개발한 소프트웨어를 탑재할 수 있다. 하지만 추가 프로그램이 필요하다.

- 임베디드 소프트웨어 실행

- 스타트업 코드 : 하드웨어를 초기화하고 운영체제 프로그램을 RAM으로 로딩하여 실행하는 역할을 한다.

- C 프로그램에서 사용할 스택과 힙을 생성하고 초기화 하는 등 C프로그램이 작동하기 위한 초기화 작업을 수행한다.

2. 프로그램에서 하드웨어 제어하는 방법

- 마이크로프로세서는 주변장치를 메모리로 인식한다.

- 하드웨어 접근 방법 : 입출력 포트에서 사용할 메모리를 지정하고, 지정된 메모리에 값을 읽거나 쓰면 된다.

- 데이터 시트 : 장치를 사용하기 위해 어떤 레지스터를 등록하고 어떤 값을 써야하는지에 대한 정보를 제공한다.

- 레지스터의 종류

- CON 레지스터 : 포트를 사용하기 위한 설정 값들이 저장되는 레지스터

- DAT 레지스터 : 입출력 장치로부터 데이터를 읽고 쓸 수 있는 레지스터

- 포트의 종류

- PA(output-only) : 출력전용 포트로 읽기가 필요하지 않은 장치를 위해 사용하는 포트이다.

- char 포인터 : 임베디드에서 char는 1 바이트 처리를 위한 데이터 타입이다.

- 하드웨어 제어 방법 : 사용할 포트를 어떤 모드로 사용할 지 설정하고, 프로그램에서 데이터 레지스터에 값을 쓰거나 읽어오면 된다.

ex) LED port adress(PA) = 0x3000_0000,    PA[n]=0, LED On, 

PA[5]=LED3, PA[6]=LED2, PA[7]=LED1

LED의 포트 어드레스에 접근하기 위한 명령어

char *p = (char*)0x30000000;


if LED2만 끄려고 한다,

char *p = (char*)0x30000000;

*p = 0;             // 레지스터 클리어, 모든 LED켜짐

*p = 0x1<<6;    // LED2만 끔

 이 코드의 문제점은 해당 비트만 수정하지 않고 다른 비트의 값을 변경한 것이다. 모든 메모리를 초기화할 경우 다른 포트를 위한 설정 값이나 데이터들이 모두 삭제될 수 있다.

- 하드웨어 접근 시 유의사항 : 특정 비트만 원하는 값으로 수정하도록 한다.

- 특정 비트 연산 종류

- 설정 : 특정 비트를 1로 설정하려면 지정된 위치의 비트를 1과 (OR) 연산한다.

ex) a |= 0x1 << 5;

- 클리어 : 특정 비트를 클리어하려면 지정된 비트를 0과 (AND) 연산한다.

ex) a &= ~(0x1 << 5);

- 반전 : 반전시키고자 하는 비트는 1과 ^ 연산한다. ^연산은 두 피연산자가 같으면 0으로 다르면 1로 설정한다.

ex) a ^= 0x1<<5;

- 비트 검사 : 특정 비트가 0인지 1인지를 검사하는 방법은 다음과 같다.

ex) a & (0x1<<5)

- 비트 추출 : 특정 비트만 추출하는 방법

ex) 예를 들어 [6:4] 비트만 추축한다면 a & 0x7이다.

- 비트 연산의 활용 예

start에 정수를 더한 결과가 4의 배수가 아니라면 그보다 큰 가장 가까운 4의 배수로 늘려주는 연산

start += x;

start += 0x3;

start &= ~(0x3);

무조건 3을 더하고 마지막 2비트를 클리어한다.

- 비트 연산이 자주 사용되므로 이를 매크로 함수로 만들어 사용한다.

- 매크로 사용 시 주의점 : 치환함정을 주의한다.

ex) #define ADD(X) X+X

k = -ADD(5);

는 -10이 아니라 0이 출력된다. 올바르게 작동하기 위해서는 #define ADD(X) (X+X)로 묶어준다.

 매크로는 각 인자를 괄호로 묶어주고,, 전체 수식 또한 괄호로 묶어주어야 한다.

3. 메모리 직접 접근 방법 : 포인터 없이 메모리에 접근하는 방법 (임베디드 환경에서 주로 사용함)

char *p = (char*)0x30000000;를 메모리 직접 접근방법으로 변환하면 다음과 같다.

*(volatile unsigned int*)0x30000000 = 100;

- 그리로 이것을 매크로를 사용하여 다음과 같이 선언할 수 있다.

#define PA    (*(volatile unsigned char*)0x30000000)

4. volatile을 사용하는 이유

- 하드웨어에 의해서 변경되는 값들은 캐시에 즉각적으로 반영되지 않는다. 그러므로 데이터를 캐시로부터 읽어오지 말고 주 메모리에서 직접 읽어오도록 해야한다.

- 컴파일러 최적화가 임의로 코드를 변경하는 것을 막아준다.(데드코드인 경우 컴파일러가 최적화하면서 이를 삭제한다.)

5. main() 호출과정 : 시스템의 동작은 크게 초기화 작업과 프로그램 실행으로 나누어 볼 수 있다.

- 리눅스 운영체제에서의 호출과정

1) 쉘에서 프로그램 수행 

2) 프로세스 복사 : 기존의 프로세스를 복사하고, 복사된 프로세스의 기존 실행 컨텍스트를 버리고 새로운 실행 컨텍스트로 변경한다. (fork())

3) execve() : sys_execve(). 사용자 모드에서 커널모드로 모드를 전환한다. 

4) do_execve() : open_exec()가 파일 정보를 읽어서 적합한 binary handler를 실행시킨다.

5) flush_old_exec() : 기존 프로세스 정보를 삭제한다. 현재 실행 중인 프로그램 정보를 "current" 정보로 설정한다.

6) 새로운 프로세스에서 사용할 메모리 레이아웃 설정. text, data, bss, stack 등의 세그먼트를 선형 메모리에 맵핑.

7) 동적 링커 메모리에 로딩한다.

8) "current" 프로세스가 실행된다. start_thread()를 실행한다.

9) sys_execve()가 종료된다 : 커널 모드에서 사용자 모드로의 전환을 의미한다.

10) reschedule() : 운영체제의 스케줄러가 스케줄링을 다시 한다. 이 때 문맥교환(context switch)가 발생한다.

11) _dl_sysdep_start() : 환경변수 검사, 공유 오브젝트 맵핑, 동적 링커 처리 종료 후 _start코드를 실행한다.

12) main() 호출한다.

- 운영체제가 없는 경우 호출과정

- 부트코드 : 프로그램이 작동하기 전 하드웨어를 초기화한다.

- 스타트업 코드 : C 프로그램이 작동될 수 있는 환경을 만들어 준다.

1) 엔트리 포인트 지정과 예외 처리 벡터 설정

엔트리 포인트는 프로그램의 시작을 알리는 역할을 한다.

2) 인터럽트 사용불가로 설정한다. 

(Watch Dog Timer : 일정 시간 동안 시스템에서 아무런 반응이 없다면 시스템을 재시작 한다.)

3) 시스템 클록 설정 : S3C2440의 경우에는 3개의 클락으로 구성된다.

FCLK : CPU에서 사용하는 클로. 가장 빠르다.

HCLK : 고속 버스(AHB)의 클록. 고속 입출력 장치에서 사용한다.

PCLK : 저속 버스(APB)의 클록. 저속 입축력 장치에서 사용한다.

3) rw_data를 ROM에서 RAM으로 복사한다.

4) ZI(bss)영역을 0으로 클리어한다.

5) 모드별 stack을 생성한다.

ARM 모드 종류 : User/System, Supervisor, FIQ, IRQ, Abort, Undefined

6) 힙을 생성한다.

7) 인터럽트 사용가능으로 설정한다.

8) main() 호출한다.


 


Comments