내가 새로이 알게된 임베디드에 관련한 사항들을 정리해 둔 곳이다.
volatile 는 왜 사용할까
일단 여러가지 이유가 있겠지만, 이해를 돕기 위해서 예를 들어 설명하겠다.
32 bit 를 read 하려고 한다. 그런데 버스 size 가 8 bit 라면, 총 4 번을 엑세스해야 할 것이다.
각 엑세스 마다, chip seletor 시그널이 뜰 것이다. 하지만, 실제로 신호 선에 오실로스코프를 이용해서 확인해보면 한번만 뜰 때가 있다.
왜 이럴까? 바로 컴파일시에 컴파일러가 너무 똑똑한(?) 자동으로 나머지 4 번의 시그널을 1 번으로 merge 를 시켜버린 것이다.
이런 컴파일러의 행동을 막기위해서는 volatile 을 사용해야 한다.
대용량의 메모리를 할당받고 싶다면
리눅스에서 1 page 이하(4 KByte)의 크기를 할당받고 싶다면,
- kmalloc(b-size, GFP_KERNEL), kfree(virt)
위와 같이 하면 된다.
1 page 이상의 크기를 할당받고 싶다면,
- kmalloc(b-size, GFP_DMA);
- void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp);
- dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t handle);
다음의 예제를 보자!
dma_addr_t dma; // DMA 용 물리주소 void *virt = dma_alloc_coherent(pdev->dev, 1M, &dma, GFP_KERNEL);
마지막으로 더 큰 메모리를 할당하고 싶다면 어떻게 할까?
만일 현재 128M 의 메모리를 가지고 있고 그 중에서 64M 를 할당하고 싶다면, 커널에는 64M 만 있다고 알려주고 나머지 64M 는 메모리 매핑을 해서 사용한다. 실제로 이런식으로 DVR 에서 많이 사용된다. 일종의 트릭이라고 할 수 있겠다.
pci 에 관한 괜찮은 책
PCI 인터페이스에 대해서 공부해야 한다면, 다음의 책을 추천한다. 마땅히 PCI 에 대한 책이 없는 요즘에 그래도 유일한 국내서라고 할 수 있겠다.
에서 'PCI 버스 해설과 인터페이스 카드 설계' 가 그것이다. 이 사이트에는 이 책 말고도 시중에서는 찾아보기 힘든 표준 스펙에 대한 것들이 나와있다. 물론 이런 책들은 시중 서점에서는 절대 구할 수 없다. 오로지 여기서만 구입할 수 있다.
리눅스 커널에서 각 디바이스 별 함수의 명세를 알고 싶다면
리눅스는 워낙 크기가 방대하기 때문에, 어디서 부터 어디까지 봐야 할지 난감할 때가 많다.
이럴 때는 리눅스 커널 안에 있는 Documentation 라는 디렉토리를 보자! 각 디바이스 별로 문서를 찾을 수 있다.
포인터를 이용한 데이터 접근
C 에서 변수를 쓸 때, 보통은 메모리의 어디에 위치하는지 신경쓰지 않는다. 그저 선언을 통해 변수를 만들고(변수를 위한 메모리 할당), 식별자를 통해 그 내용을 다룬다. 하지만 화면에 텍스트를 표시하려면 포인터를 써야 한다.
C 에서 포인터는 주소가 들어 있는 변수다. 포인터를 선언하는 데이터 형 이름은 없다. 대신 식별자 앞에 '*' 를 붙여 포인터임을 선언한다.
char *p; // "p" 는 char 에 대한 포인터다
선언할 때 포인터가 가리키는 대상의 타입(과 크기)을 명시한다. 선언하면 메모리가 할당되지만, 사용하기 전에 다음과 같이 초기화해야 한다.
p = (char *)(0xB8000 + 2 * (80 * row + col));
물론 초기값은 다음과 같이, 선언할 때 지정해도 된다.
char *p = (char *)(0xB8000 + 2 * (80 * row + col));
캐스팅하지 않으면, 마지막 보기는 주소를 담는 변수에 int 타입 데이터를 저장하려고 하게 된다. 주소와 int 가 같은 수의 비트를 차지할지는 몰라도 컴파일러는 여전히 '사과와 오렌지' 를 섞어 쓰고 있다는 경고를 출력할 것이다.
포인터가 초기화 되면, 역참조(de-reference) 연산자인 '*' 를 붙여서 데이터에 접근할 수 있다.
*p = 'A'; // 'A' 를 화면에 표시한다
p 를 char 를 가리키는 포인터로 선언했기 때문에, 역참조된 포인터(*p) 는 char 타입 변수를 쓰는 곳에는 어디든 대입 연산자의 좌변이든 우변이든 쓸 수 있다.
ifdef 사용하기
유연성(?) 있는 프로그램을 만들기 위한 방법 중에서 ifdef 을 이용한 방법에 대해서 설명하겠다.
먼저 이해를 돕기위해 아래 예제 코드를 보자!
/* * DEBUG DEFINITIONS */ #ifdef __DEBUG__ #define DbgPrint CrtPrintf #else int DbgPrint(const char *fmt, ...) { return -1; } #endif
위의 예제 코드를 보면 'DEBUG' 가 선언되었는지 아닌지에 따라서 DbgPrint 에 대한 Define 이 다르게 된다는 것을 알 수 있다.
'DEBUG' 가 선언되지 않았을 때는 아무 동작 없이 -1 을 동작하도록 선언되어 있고, 'DEBUG' 를 선언한 상태에서는 CrtPrintf 함수를 호출하도록 되어 있다.
이것을 응용한다면, 특정 모드(?) 에 따라서 유연하게 프로그램을 변경가능하게끔 할 수 있다.
하드웨어 이것만은 알아두자
RTS0
핀 이름 위에 막대가 있는 것은 신호가 Active Low 방식으로 동작함을 의미한다. Active Low 라는 것은 논리 수준 0 이 신호의 기능을 활성화 시키고, 논리 수준 1은 기능을 비활성화 시킨다는 것을 의미한다. 이와 반대로 동작하는 것을 active high 라고 한다.
Active low 기능은 '/' 나 '~' 를 신호 이름 앞이나 뒤에 표시하기도 한다. 어떤 부품 제조사들은 active-low 신호를 신호명 앞에 'n' 을 붙여서 nRESET처럼 표시하기도 한다.
NC
어떤 핀과도 연결되지 않은 핀을 뜻한다.
RD/wr(read write bar)
신호가 1로 지정되면 읽기가, 0으로 지정되면 쓰기 작업이 수행된다.
데이터 북을 통해서 확인해야 하는 것들
- 프로세서가 리셋된 후 어떤 주소로 점프하는가?
- 리셋될 때, 프로세서 레지스터와 주변기기의 상태는 무엇인가?
- 주변기기의 레지스터를 프로그램 하는 올바른 순서는?
- 인터럽트 벡터(vector) 테이블은 어디에 있어야 하는가? 메모리의 특정 주소에 위치하여야만 하는가? 그렇지 않다면 프로세서는 어떻게 그 위치를 알 수 있을까?
- 인터럽트 벡터 테이블의 형식은 어떤가? ISR 함수를 가르키는 포인터들의 테이블 인가?
- 프로세서에서 자체 발생시키는 특수 인터럽트(트랩(Trap)이라 부르기도 한다)가 있는가?이 인터럽트를 다룰 ISR을 작성해 주어야 하는가?
- 전체 혹은 개별적으로 인터럽트를 활성화 시키거나 비활성화 시키는 방법은 무엇인가?
- 어떻게 인터럽트를 수신했다는 것은 알리고 클리어하나?
하드웨어 초기화 시 확인할 것들
- 프로세서와 ROM 이 맞는 전압으로 연결되었는지 확인한다.
- 클록 신호가 동작하는지 확인한다. 프로세서는 클록 없이는 아무 일도 하지 않는다.
- 프로세서가 정확하게 리셋에서 빠져나오는지 점검한다. 프로세서가 로직 분석기(logic analyzer)를 사용해서 꺼낸 주소를 점검한다. 프로세서가 개발자가 예상한 위치에서 첫 명령어를 불러오려고 하는지를 확인하는 것이다.
- 워치독 타이머가 프로세서를 리셋하지 않는지 확인한다.
- 프로세서의 입력 핀들이 high 나 low 로 연결되었는지 확인한다. 이 부분은 특히 인터럽트 핀의 경우 중요하다. 플로팅(floating) 상태의 입력 핀은 프로세서에 모든 종류의 혼란을 일으킬 수 있다.
C 코드가 수행되기전에 이뤄져야 할 동작들
- C 프로그램을 위한 시작코드는 연속된 다음의 동작들로 이루어진다.
- 모든 인터럽트를 금지시킨다.
- 초기화된 데이터를 롬에서 램으로 복사한다.
- 초기화 되지 않은 자료 영역을 0 으로 초기화 한다.
- 스택 영역을 할당하고 초기화한다.
- 프로세서의 스택 포인터를 초기화 한다.
- main 함수를 호출한다.
1번 핀 찾기
그림에서 보듯이 조그만 네모 패드는 1번 핀을 위해 가끔 사용된다. 반면에 다른 패드는 대부분 원형이다. 이것은 일반적으로 IC 가 DIP(Dual Inline Package) 내에 있는 경우이다. 1번 핀의 다른 식별자는 1번 핀 옆의 원형 실크 스크린이다.
IC 제조사 또한 원형 식별자를 1번 핀 옆에 두거나 IC의 맨 위쪽에 호(arc) 형태의 식별자를 두어서 1번 핀을 식별한다. 이 경우 1번 핀은 이 식별자의 왼쪽에 위치한다. 더 작은 칩들의 경우 1번핀 옆의 모서리를 깎아낸다. 핀 번호는 1번 핀부터 칩의 반시계 방향으로 증가된다.
어떤 경우에는 이들 식별자의 조합이 사용될 수도 있다.
GCC 컴파일 시, 중간 과정 파일들 생성하기
디버깅을 위해서 컴파일 과정에서 생성되는 파일이 필요할 때가 있다.
각 단계마다 생성되는 파일을 옵션을 통해서 추가할 수 있지만, 한번에 모든 과정의 파일들을 생성시키려면 다음과 같이 실행한다.
#gcc -v -save-temps -o test test.c #ls test test.c test.i test.o test.s
- test.i : 전처리 수행 후의 결과 파일
- test.s : 어셈블 수행 후의 결과 파일
- test.o : 링킹 후의 결과 파일
- test : 실행 가능한 결과 파일
임베디드 리눅스 부팅시간 줄이기
리눅스는 특정 아키텍처에 최적화된 커널이 아니기 때문에, 임베디드 장비에 포팅되는 리눅스의 임베디드 장비의 특징에 따라 다르긴 하겠지만 결코 빠른편이라고는 할 수 없다. 여기서는 임베디드 리눅스 부팅시간을 단축시킬 수 있는 방법에 대해서 설명해보겠다.
임베디드 시스템에서 가장 먼저 구동되는 프로그램인 부트로더는 일반적으로 수십 밀리초에서 수 초가 소요되며, 커널은 수십 초에서 수 분이 소요된다. 커널 부팅이 완료된 후, 루트 파일 시스템이 마운트되고 init 프로세스로부터 시작하는 사용자 영역의 기본적인 초기화에 수십 초에서 수 분이 소용된다. 이후 각각의 시스템에 특화된 애플리케이션이 동작한다.
부팅시간을 줄이려는 시도는 어떤 커널과 루트 파일 시스템을 선택하느냐를 고려하면서 시작된다.
부팅 시간을 줄이기 위해 리눅스 커널의 핵심부분에 수정을 가하는 경우 시스템 호환성과 기능성을 저해하며, 리눅스 커널의 설정을 조정함으로써 부팅 시간을 크게 줄일 수 있다.
파일 시스템 중에서 cramfs 는 플래시 파일 시스템 가운데 구동 시간이 가장 짧은 것으로 알려져 있다. 그러나 읽기만 가능하고 쓰기가 가능하지 않다는 점과 파일 시스템 크기가 256MB 로 제한되어 있다는 점이다.
시스템에서 플래시 메모리에 쓰기 기능을 요구하는 경우, 루트 파일 시스템으로 cramfs 를 사용해 부팅이 완료된 후 플래시 메모리 쓰기 기능이 요구되는 시점에서 읽기, 쓰기가 가능한 추가적인 파일 시스템을 마운트하고 사용하는 것을 고려해볼 수 있다.
또한 부팅시에 프로그램 수행을 위한 메모리 사용량과 초기화에 필요한 시간을 최소화하기 위해 busybox 와 glibc(또는 uclibc) 를 사용한다.
일반적인 임베디드 시스템은 플래시 메모리 사용량을 제한하기 위해, 압축된 커널, 파일 시스템 이미지를 플래시 메모리에 저장해 부팅 시점에서 압축을 풀고 RAM 으로 복사한다. 이때 압축을 푸는 과정과 RAM 으로 복사하는 과정에서 많은 시간이 소요된다.
압축을 푸는 시간을 줄이기 위해 압축을 하지 않은 이미지를 사용하는 방법이 있으며, RAM 으로 복사하는 시간을 줄이기 위해 이미지 가운데 수정이 필요없는 부분은 플래시 메모리에 위치시키고 수정이 필요한 부분(예:변수값)만을 RAM 으로 복사해서 사용하는 XIP 기법이 활용된다.
XIP 는 부팅 속도를 향상시키는 반면, 시스템 동작 시 프로그램이 RAM 보다 느린 플래시에서 동작하기 때문에 성능저하를 가져올 수 있음을 유의해야 한다.
커널에서 XIP 를 사용하는 기법을 커널 XIP 라고 하며, 애플리케이션 영역에서 XIP 를 사용하는 기법을 애플리케이션 XIP 라고 한다.
다음은 XIP 를 위한 커널 설정이다.
===== 1. 커널 XIP 를 위한 설정 ===== Kernel Execute-In-Place from ROM(XIP_KERNEL) XIP Kernel Physical Location(0x35400000) Add a U-Boot Header to XIP Kernel(XIP_UBOOT_HDR) ===== 2. 애플리케이션 XIP 를 위한 설정 ===== Use Linear Addressing for CramFs(CRAMFS_LINEAR) Support XIP on Linear CramFs(CRAMFS_LINEAR_XIP) Root file System on Linear CramFs(ROOT_CRAMFS_LINEAR)
설정이 끝나면 애플리케이션 XIP 로 동작하기 원하는 애플리케이션의 모드를 변경해준다(예: chmod +t busybox).
sticky bit(스티키 비트)를 설정하면, 실행 가능한 화일의 2진 이미지가 메모리에 한번 읽혀 들어올 때 그대로 남아 있게 함으로써 시스템의 부담을 적게한다.
UNIX 시스템에서는, 화일에 특별한 모드인 sticky 모드를 부여해서 그 프로그램이 처음으로 실행되면 그 실행가능 이미지는 스왑 공간에 계속 남아 있게 함으로서 다음 실행때에 좀더 빠른 실행을 가능하게 한다. sticky 모드를 제어하는 i-node의 비트를 sticky 비트라고 부른다.
슈퍼유저만이 chmod 커맨드를 사용해서 화일에 sticky 모드를 부여할 수 있다.
sticky 비트의 8진수 값은 1000이다. sticky 비트가 설정되어 있을 경우 커맨드 “ls -l”를 사용해서 화일 사용권을 표시해 보면 사용권의 마지막 문자(“other”의 실행권 x 혹은 -)가 t 혹은 T로 표시된다.
또한 부팅 시점에 초기화가 필요한 기능을 비활성화 해주면 부팅 시간을 줄일 수 있다. 네트워크 기능과 각종 디바이스를 비롯해 사용하지 않는 기능을 제거한다. 부팅 시점에는 사용하지 않지만 부팅 완료 후 필요에 의해 사용하는 기능이 있으면, 커널 모듈로 컴파일 한다. 'modprobe' 나 'insmod' 명령어를 이용해서 활성화 시킬 수 있다.
커널 초기화 마지막 단계에서 커널은 루트 파일 시스템의 'init' 프로그램을 실행한다.
'init' 프로그램은 /etc/inittab 설정파일을 시작으로 많은 스크립트 파일을 통해 초기화 과정을 수행한다.
여기서 필요없는 기능과 추후에 동작시켜도 되는 기능은 이 과정에서 동작하지 않도록 스크립트를 수정한다.
부팅 시점에 꼭 구동되어야 하는 기능 가운데 시간을 많이 소요하는 기능은 스크립트 대신 프로그램을 작성해 동작시키면 스크립트로 실행할 때보다 실행 시간을 줄일 수 있다.
부트로더 설정에 의한 부팅시간 줄이기
부팅 중 콘솔을 통해 많은 메세지가 출력된다. 콘솔은 매우 느린 디바이스이며 부팅 지연 요소가 된다.
부트로더 옵션 'quiet' 는 부팅 중 메세지 출력을 막아주는 옵션이며, 부팅이 완료된 후에는 정상적으로 콘솔 입출력을 사용할 수 있다.
보고밉스(BogoMIPS)는 커널이나 각 디바이스 드라이버에서 프로세서 속도와 관계없이 일정한 시간을 구하기 위해 제안된 초당 연산 횟수로 커널 내부적으로는 LPJ 라는 이름으로 다루어진다. 부팅 시 LPJ 를 계산하는데 수십 ~ 수백 밀리초가 소요된다.
하지만, 부트로더에서 LPJ 값을 미리 지정해주면 부팅 시 계산 없이 부트로더에서 지정한 값을 사용하게 되며, 이를 통해 수십 ~ 수백 밀리초의 부팅 시간을 줄일 수 있다. 아래는 부팅 옵션의 예다.
#setenv bootargs root=/dev/null ro nointrd rootfstype=cramfs init=/sbin/init mem=128M console=ttyAMA0 rootflags=physaddr=0x35980000 lpj=523264 quiet
실제로 위에서 언급한 것들을 적용시킨 결과, 약 30초 정도의 부팅시간이 1 ~ 2 초 정도로 줄어들었다.
부팅 시간을 좀 더 줄이기 위해서
부트로더는 통상적으로 수백 밀리초에서 수 초를 소요한다. 커널과 파일시스템 부팅에 수십 초가 소요되는 경우 부트로더에서 사용되는 시간이 큰 문제가 되지는 않지만, 커널과 파일시스템의 최적화를 통해 1 ~ 2 초 내에 부팅이 되면 부트로더에서 사용하는 수 초도 큰 부담이다.
따라서 부트로더의 디바이스 검색, 초기화, 테스트 관련 코드를 수정하게 되면 부팅 시간을 줄일 수 있다.
통상적인 임베디드 시스템은 init 프로세스로부터 시작하는 사용자 영역의 초기화 마지막에 시스템 특화 애플리케이션(예: GUI, Network Daemon 등)을 수행한다. 최근 임베디드 시스템의 경우 애플리케이션의 기능이 다양해지면서 초기화에 소요되는 시간도 점점 길어지고 있다. 이 부분의 개선도 고려될 필요가 있다.
하드웨어 부분을 고려해 보자. 예를 들어, 본문의 테스트에서 사용된 플래시 메모리는 싱글 워드 방식(Single Word Mode)만을 지원하는 구형 플래시 메모리이다. 따라서 플래시 메모리만 버스트 모드(Burst Mode)로 교체해 주어도 부팅 시간이 크게 감소된다.
부팅시간 감소를 위해서는 CPU, 메인 메모리의 성능과 상호 인터페이스 속도도 검토할 필요가 있다.
최신 툴체인(toolchain) 설치하기
임베디드 개발에 있어서 툴체인은 없어서는 안 될 존재이다. 가장 좋은 방법이라고 하면, 각각 패키지들을 손수 빌드하여 만드는 것이겠지만, 버전마다 예상치 못한 에러가 발생할 수 있기 때문에 바이너리 형태로 제공하는 것을 사용할 것을 개인적으로 추천한다.
일반적으로 툴체인의 버전은 PC 상의 툴체인 버전보다 낮다. 상황에 따라서는 최신의 툴체인이 최선의 방법은 아닐 것이다.
하지만, 최신의 툴체인을 사용하는 것은 그만큼의 장점을 가지기 때문에 여기서는 손쉽게 툴체인을 구하는 방법에 대해서 설명하겠다.
http://www.codesourcery.com/gnu_toolchains/arm/download.html 에 접속한다.
왼쪽에 'Lite Edition' 에서 'ARM Toolchains' 를 선택한다. 다음 페이지에서 오른쪽 상단에 'DOWNLOADS' 를 선택한다.
다운로드 할 툴체인에 대한 정보를 선택하는 화면이 뜰 것이다. 여기서 약간의 설명을 하자면, 다음과 같다.
- ARM EABI : glibc 라이브러리를 포함하지 않는 툴체인
- ARM GNU/Linux : 리눅스에서 제공하는 glibc 라이브러리를 포함하는 툴체인
- Source Code : 선택하지 않으면, 바이너리 형태의 툴체인을 받을 수 있고, 선택 할 시에는 툴체인의 소스코드를 다운로드 할 수 있다.
설치하는 방법은 다음과 같다.
#bzid -d arm-2008q1-126-arm-none-eabi-i686-pc-linux-gnu.tar.bz2 #tar xf arm-2008q1-126-arm-none-eabi-i686-pc-linux-gnu.tar -C /usr/local
~/.bash_profile 에다가 다음을 추가한다.
# GCC-4.2.3 PATH=$PATH:/usr/local/arm-2008q1/bin
최신 툴체인(toolchain) 직접 빌드하여 설치하기
앞에서는 이미 만들어진 바이너리 형태의 툴체인을 설치하는 방법이라면, 이번에 설명할 방법은 직접 컴파일하여 툴체인을 만드는 방법이다.
http://www.kegel.com/crosstool/ 에 가면 필요한 파일을 다운로드 받을 수 있다. 현재 최신버전은 http://kegel.com/crosstool/crosstool-0.43.tar.gz (tarball) 이다.
빌드 방법은 스크립트 파일을 실행해서 필요한 패키지들을 다운받아 빌드하여 만드는 방법이다. 스크립트 파일 실행만으로 만들 수 있기 때문에 간편하다.
사용 방법은 http://www.kegel.com/crosstool/current/doc/crosstool-howto.html 에 잘 나와 있다.
빌드시 주의할 점은 타겟 아키텍쳐와 빌드가 가능한지 여부를 확인해야 하는 것이다.
http://www.kegel.com/crosstool/crosstool-0.43/buildlogs/ 에 보면, 각 gcc 버전과 glibc 버전, 그리고 아키텍쳐에 따라 빌드 성공 여부가 나와있다.
여기서는 반드시 'ok' 라고 표시된 패키지를 선택해야 한다.
빌드 방법을 간단히 설명하겠다. root 계정이 아닌 일반 사용자 계정으로 해야 한다.
#su - fat81 $tar crosstool-0.43.tar.gz $TARBALLS_DIR=$HOME/downloads # where it will save source tarballs $RESULT_TOP=/opt/crosstool # where it will install the tools $GCC_LANGUAGES="c,c++" # which languages it will make compilers for $cd crosstool $eval `cat arm.dat gcc-3.4.0-glibc-2.3.2.dat` sh all.sh --notest ...
에러없이 컴파일 되었다면, result 디렉토리 아래에 툴체인이 생성되었을 것이다.