SW 개발

[Linux 강좌] 2) 보드를 살려보자 - 마소

. . . 2011. 6. 28. 14:38
반응형

출처 : imaso.co.kr (월간, 마이크로소프트)

이글은 월간 임베의 글을 제가 보기 좋게 편집한 글입니다. 인터넷에 돌아다니는 해당글들이너무 조잡하게 편집되어있어서 보기좋게 편집했습니다. 처음 임베디드 시스템을 접하시는 분들에게 좋은 글입니다.

문제가 된다면 자삭하겠습니다.

보드를 살려보자-1

  • 저 자 : 유영창
  • 출판일 : 2003년11월호

임베디드 시스템에서 하드웨어 설계가 끝나고 제작 후 샘플 보드가 나왔을 때 가장 먼저 하는 것이 보드에 대한 검증이다. 필자가 다니는 회사에서는 이것을 ‘보드 살리기’라는 말로 표현한다. 이 보드 살리기는 운이 좋으면 바로, 운이 나쁘면 언제 끝날지 모르는 피 말리는 시간이 필요하다. 이런 현상이 발생하는 이유는 문제의 원인이 하나가 아니고 여러 가지가 혼재하기 때문이다. 어떤 문제를 해결하는 가장 빠른 방법은 문제의 종류를 구분하는 것이다. 그런데 이 경우에는 이 자체도 매우 어렵다는 것이다. 하드웨어 문제인가? 소프트웨어 문제인가? 때문에 보드 살리기에 필요한 과정을 살펴보고 부트로더와의 관계를 살펴보자.

임베디드 장비에서 맨 처음 부딪치는 문제가 바로 ‘보드 살리기’이다. 즉 초기 설계된 대로 하드웨어가 제대로 동작하는가를 검증하는 과정이다. 처음부터 새로 공부하면서 구현된 아키텍처가 아닌 이미 구현된 아키텍처에 대한 회로를 다시 만들었을 경우라 치더라도 보드를 동작시켜 보면 처음부터 동작하는 경우는 거의(?) 없다. 대부분의 경우 회로 설계 후 초기 테스트 보드가 나오는 과정까지 여러 형태의 실수가 개입하기 때문이다. 이 경우 기본적인 전원이나 동작 조건은 하드웨어적으로 1차 검증이 된다. 필자가 근무하는 회사에서 보드의 하드웨어 초기 점검 사항은 다음과 같다.

  • 주 전원의 정상적인 공급
  • 프로세서 코어 전원의 정상적인 공급
  • 프로세서 클럭의 정상적인 동작
  • JTAG의 동작 및 프로세서 ID 체크
  • ‘이지플래시’ 프로그램과 같은 플래시 라이터를 이용한 정상적인 부트 롬의 쓰기 처리

이 정도의 점검이 끝나면 프로그래머에게 테스트 코드를 요청한다. 기본적인 부팅 코드가 수행될 수 있다고 보기 때문이다. 보드를 시험하기 위한 부팅 코드와 부트로더를 작성하기 위해서 준비해야 할 것은 개발 환경의 구축이다. 이 개발 환경 구축의 방법은 여러 가지가 있겠지만 리눅스 커널을 이용한 임베디드 시스템 개발이 가장 기본이다. 공개된 크로스 컴파일러 환경을 이용하여 구축하는 경우라면 다음과 같이 준비하여야 한다.

  • 리눅스가 동작하는 PC
  • 시리얼 통신 에뮬레이터
  • 플래시 롬 또는 롬 라이터 툴

이외에 준비할 사항이라면 다음과 같은 환경을 추가로 구축해야 한다.

  • TFTP 서버
  • 삼바 환경 설정
  • 윈도우 PC

문제는 리눅스 환경이다. 필자도 임베디드 리눅스 관련 회사에 근무하지만 아무래도 소스 편집 환경은 윈도우가 편하다. 그래서 개발 서버용 리눅스 PC를 따로 두고 대부분의 개발 작업은 윈도우에서 한다(은 필자가 즐겨 사용하는 개발 환경 구성이다). 에서 보듯이 개발 호스트로 리눅스를 사용하는데, 그 이유는 크로스 컴파일러 환경이 리눅스에서 동작하기 때문이다. 따라서 리눅스의 운영체제에 ‘삼바’로 파일을 공유시키고 윈도우에서 편집한다.

그림 1. 필자의 크로스 컴파일러 개발 환경

컴파일을 할 경우에는 X 터미널 에뮬레이터 프로그램을 이용해 리눅스에 명령을 전달한다(필자의 경험에 의하면 국내에서 개발된 XManager라는 프로그램이 가장 편한 것 같다). 주로 전달하는 명령은 컴파일 명령이다. 또한 리눅스 서버는 개발 보드에서 이더넷을 지원할 경우라면 tftp와 같은 파일 전달 서버로써 동작한다. 보드에서 출력되는 시리얼 정보는 ‘token’이라는 공개된 윈도우 프로그램을 주로 이용하고 있다. 이렇게 기본 시스템 환경을 구성하고 나면 이제 가장 골치 아픈 크로스 컴파일 환경을 만들어야 한다. 아무래도 리눅스 커널을 이용한 임베디드 시스템을 구성할 때는 리눅스에 크로스 컴파일 환경을 만드는 것이 유리하기 때문에 이 크로스 컴파일이라는 것에 대해 자세하게 알아보자.

크로스 컴파일 환경 구축

대부분의 프로그래머들은 PC라는 동일한 환경에서 프로그램을 작성한다. PC에서 프로그램을 짜고 컴파일하고, 또 실행 파일도 PC에서 수행한다. 이렇게 동일한 환경에서 동작되는 컴파일러와 이 컴파일러에서 생성된 실행 파일을 동일한 환경에서 수행한다면 이 때의 컴파일러를 ‘네이티브(native) 컴파일러’라고 한다. 이와 반대로 컴파일러가 동작하는 시스템과 컴파일러에 의해서 생성된 실행 파일이 동작하는 시스템이 다를 때 이 컴파일러를 ‘크로스(cross) 컴파일러’라고 한다.

임베디드 시스템에 동작하는 프로그램을 작성한다면 당연히 크로스 컴파일러 환경을 구축해야 한다. 개발 환경은 PC일 것이고 실행 파일이 동작하는 시스템은 다른 CPU 구성을 갖는 임베디드 시스템일 것이기 때문이다. 그렇다고 모든 임베디드 시스템에 동작하는 프로그램들이 꼭 크로스 컴파일 환경을 필요로 하는 것은 아니다.

반면 PC와 같은 구조를 갖는 임베디드 리눅스 시스템을 만든다면 PC의 네이티브 컴파일러에서 만들어진 실행 파일이 그대로 수행될 수 있다. 하지만 임베디드 시스템이 대부분 저렴한 가격대를 요구(PC를 이용한 시스템은 가격이 높아질 수밖에 없다)하거나 특수한 기능을 수행하는 구조를 가지기 때문에 이런 경우는 드문 편이다. 그나마 개발 프로세스를 빠르게 진행하기 위해서 익숙한 PC 구조를 사용하는 것이 요즘 들어 많아지는 추세이기는 하지만 말이다.

어쨌든 크로스 컴파일 환경에서의 개발은 기존 윈도우 개발자 입장에서 보면 매우 열악한 개발 환경이 된다. 돈이 많은 회사야 좋은 개발 툴을 사서 일부 개선하겠지만 그래도 열악하기는 마찬가지다. 이 환경의 가장 단적인 불편 사항은 컴파일이 끝난 후 실행 파일을 즉시 시험하지 못한다는 것이다. 이외에 개발 과정에서 손가락이 무척 바빠지는 것도 빼놓을 수 없는 요인이다. 단순하게 마우스 클릭 한번으로 수행되는 윈도우의 RAD형 컴파일러에 익숙한 사람이 뭔가를 하기 위해서 타이핑하는 것은 무척 힘들기 때문이다.

그래도 이렇게 불편한 리눅스에서 동작하는 크로스 컴파일러를 사용하는 것에는 나름대로 이유가 있다. 지금은 예전과 달리 많은 사람들이 임베디드 시스템 개발에 리눅스를 사용한다는 것쯤은 알고 있다. 하지만 그 이유가 리눅스 커널을 임베디드 시스템에 탑재하기 때문이라고 알고 있다면 이는 잘못된 상식이다. 오히려 임베디드 시스템 개발 환경으로 리눅스를 사용하는 근본적인 이유는 gcc라는 막강한 컴파일러가 있기 때문이라고 할 수 있다.

리눅스만 사용하는 사람이라면 리눅스 프로그램 개발을 위해서 당연히 gcc라는 컴파일러를 사용한다. 그래서 gcc라는 것이 그냥 리눅스용 컴파일러라고 알고 있다. 하지만 gcc는 그렇게 단순한 컴파일러가 아니며, 또한 gcc는 그냥 i386 프로세서에서 동작하는 실행 파일을 만들어내는 컴파일러가 아니다. 현재 존재하는 컴파일러 중에서 가장 많은 프로세서를 지원하는 컴파일러가 바로 gcc이다. gcc 컴파일러 패키지는 이미 전 세계에 동작되는 대부분의 프로세서를 지원하기 위한 준비를 갖추고 있다. 더구나 공짜다. 하지만 대부분의 사람들은 이런 gcc가 어떻게 다른 CPU를 지원하게 할 수 있는지에 대해서는 잘 모른다. 그냥 gcc 명령을 치면 i386 코드가 생성되기 때문이다.

보드를 살려보자-2

  • 저 자 : 유영창
  • 출판일 : 2003년11월호

크로스 컴파일러와 gcc 소스 패키지

gcc에서 i386 이외의 실행 파일을 만들기 위해서는 리눅스에 이미 설치된 gcc란 컴파일러는 소용이 없다. 몇 가지 옵션만 바꾸어서 gcc를 실행한다고 i386 이외의 프로세서를 지원하는 코드가 생성되는 것은 아니다. 애초에 다른 프로세서에서 동작될 수 있는 실행 파일을 만들 수 있도록 gcc를 만들어야 하는 것이다.

그렇다고 기존에 i386에서 동작하는 gcc를 아예 다른 프로세서에서 동작하는 실행 파일을 만드는 gcc로 바꿔버리면 문제가 된다. 그 뒤로는 i386에서 동작하는 어떤 프로그램도 만들 수 없기 때문이다. 그래서 보통은 gcc의 이름을 조금 바꾼다. arm 계열의 프로세서에서 동작하는 실행 프로그램을 만들기 위해서 사용하는 gcc는 arm-linux-gcc라는 이름을 가진다. 이렇게 이름이 다른 두 컴파일러는 전혀 다른 컴파일러가 아닌 똑같은 gcc인 것이다. 단지 gcc는 i386 프로세서에서 수행되는 코드를 만들어 내는 컴파일러이고 arm-linux-gcc라는 것은 arm 프로세서에서 동작되는 코드를 만들어 내는 컴파일러일 뿐이다(관행적으로 네이티브용 컴파일러에는 보통 gcc가 되고 크로스 컴파일러에는 gcc 앞에 접두사를 붙인다). 만약 알파 프로세서로 동작하는 gcc에서 i386 계열의 실행 코드를 만든다면 크로스 컴파일러의 이름이 아마도 i386-linux-gcc가 되지 않을까.

어찌 되었든 둘 다 동일하게 gcc란 패키지에서 파생한 컴파일러인 것이다. 필자가 자꾸 gcc 패키지라는 말을 쓰고 있다. 그런데 이 gcc 패키지라는 것이 무엇일까?

예를 들어 윈도우에서 델파이라는 개발 툴을 구매해서 설치하면 단순하게 델파이 컴파일러만 설치되는 것이 아니다. 개발을 위한 여러 파일들이 같이 설치된다. 이와 동일한 개념으로 gcc 패키지란 컴파일을 하기 위한 이런 저런 파일들이 모두 담긴 것을 말한다. 다만 윈도우에서와 달리 리눅스에서는 패키지들이 소스로 구성되어 있다. 그래서 gcc의 경우는 ‘gcc 소스 패키지’라고 말하기도 한다.

또 많은 개발자들이 착각하는 것 중의 하나가 gcc를 단순하게 C 컴파일러라고 알고 있다는 점이다. 정확하게 말하자면 gcc는 C 컴파일러가 아니다. C++ 소스를 컴파일할 때도 역시 gcc를 사용하고 어셈블러를 컴파일할 때도 gcc를 사용할 수 있다. 그래서 gcc 소스 패키지에는 C 이외의 몇 가지 컴파일러들이 더 들어 있다. 어찌 되었든 gcc 소스 패키지란 gcc 컴파일러를 만들 수 있는 소스로 구성된 것을 말한다.

이 gcc 소스 패키지를 이용하여 원하는 프로세서를 지원하는 크로스(또는 네이티브) 컴파일러를 만드는 것이다. 그런데 아주 모순된 것 중 하나는 gcc를 만들기 위해서는 gcc가 필요하다는 점이다. 왜냐하면 gcc는 소스 패키지이기 때문이다. 하지만 이 점은 별로 문제가 되지 않는다. 대부분의 리눅스에는 gcc 컴파일러가 이미 설치되어 있기 때문이다.

gcc 3.X대의 속설(?)

gcc를 이미 사용하고 있는 많은 사람들이 gcc의 3.X 대에 문제가 있다고 알고 있다. 때문에 이 버전의 gcc 사용을 회피하면서 예전 버전을 고집하는 경향이 있다. 그런데 이 부분은 조금 오해가 있는 것 같다. 문제가 되는 버전은 gcc 3.0이다. 문제가 발생했으니 당연히 해당 버전을 고쳤다. 따라서 3.1대 이후라면 별 문제는 없다. 가장 최근의 버전을 사용하면 이전에 발견된 문제는 지속적으로 해소되는 것이다. 더구나 최근에 사용되는 프로세서를 쓰기 위해서는 예전 버전으로는 한계가 있다. 최신 버전을 써야 지원되는 프로세서가 있기 때문이다. 그러므로 겁먹지 말고 3.X대를 사용하기를 강력하게 권유한다.

크로스 컴파일러 툴 체인

gcc는 컴파일러이다. 당연히 소스를 컴파일할 수 있다. 그런데 gcc만 가지고서는 아무 일도 못한다. gcc는 단순하게 C 프로그램이나 C++ 프로그램을 어셈블러 파일로 만들어줄 뿐이기 때문이다. gcc의 자세한 동작 원리를 알고 싶다면 kldp.org에서 관련 문서를 찾아보기 바란다(지면 관계상 이 원고에서 자세하기 설명하기는 불가능하므로…). 어쨌든 gcc 이외에 필요한 프로그램과 데이터(?)들이 있다. 이런 모든 요소들을 모아 놓은 것을 ‘크로스 컴파일러 툴 체인’이라고 한다. 컴파일러 툴이라면 될 텐데 굳이 체인이라고 붙는 이유는 필자 개인적으로 각 패키지 간에 서로 의존적이라기 때문일 것으로 추측한다. 다음은 크로스 컴파일러 툴 체인의 목록이다.

  • binutils : 어셈블러, 링커, 그리고 라이브러리 관련 실행 파일들 모음
  • kernel : 리눅스 커널(헤더 파일 때문에 필요함)
  • gcc : 컴파일러
  • glibc : 라이브러리와 헤더 파일
  • gdb : 디버거 (툴 체인에 포함시키지 않을 때도 있음)

이 목록의 패키지들은 각각의 버전이 있다. 이 버전은 제각각인데 아쉽게도 이 버전이 다를 때는 상호 호환이 완벽하게 되지 않는다. 이유는 제작자가 모두 다르기 때문이다. 그래서 이 호환되는 버전별로 검증된 것을 모두 골라 놓은 것을 툴 체인이라고 한다.

binutils

바이너리 유틸 패키지이다. 여러 가지 프로그램이 포함되어 있는데 포함되는 내용은 다음과 같다. 주 목적은 실행 파일을 최종적으로 만들고 이에 대한 정보를 제공하는 유틸리티이다.

  • addr2line : 실행 파일의 어드레스에 대한 소스 파일과 라인 번호를 표현해 주는 프로그램이다.
  • ar : 라이브러리를 관리하는 프로그램이다.
  • as : 어셈블러이다.
  • gasp : 어셈블러 매크로 해석기이다.
  • ld : 실행 파일을 만들어 주는 링커이다.
  • nm : 오브젝트 안의 심볼릭을 표시해 주는 프로그램이다.
  • objcopy : 오브젝트 파일을 컨버팅해 주는 프로그램이다.
  • objdump : 오브젝트 파일의 정보를 표시해 준다.
  • ranlib : 라이브러리의 인덱스 파일을 생성한다.
  • readelf : elf 포맷의 파일 헤더 정보를 해석해 준다.
  • size : 오브젝트 파일의 섹션 크기와 포함된 오브젝트의 총 크기를 표시해 준다.
  • strings : 프로그램 내부에 사용되는 초기화 문자열들을 골라 표시해 준다.
  • strip : 오브젝트나 실행 파일의 정보를 선택적으로 제거해 준다.

kernel

크로스 컴파일러를 구성하는 요소 중 glibc를 만들 때 필요한 요소이다. 시스템과 연관된 데이터 타입이나 시스템 콜을 참조하기 위해서 필요한 헤더 파일을 참조하기 때문에 툴 체인에 들어간다. 엄밀히 말하면 kernel의 헤더 파일만 필요하다.

gcc

이것은 실제 컴파일러 패키지이다. 필자가 지금 설명하는 그 프로그램이다. 다음은 패키지에 포함된 대표적인 컴파일러이다

  • gcc
  • cpp
  • g++
  • c++
  • gccbug,gcov

glibc

이 패키지는 커널을 컴파일하거나 부트로더를 컴파일할 때는 필요 없다. 하지만 응용 프로그램을 작성하기 위해서 사용한다면 당연히 이것이 있어야 한다. 이 패키지의 역할은 응용 프로그램에서 사용하는 대부분의 라이브러리와 헤더 파일을 지원하는 것이다. 이 glibc와 비슷한 것 중에 newlib이라는 것이 있다. 이 둘을 구별하자면 glibc는 GNU 라이브러리이다. PC에서 사용하는 대부분의 기본적인 라이브러리를 모두 지원하기 때문에 크기가 매우 크다.

이에 반해 임베디드에서 자주 사용하는 필요한 라이브러리만 모아놓은 것이 newlib이다. 그래서 크기가 매우 작다. 그러나 특별한 경우가 아니라면 필자는 newlib를 별로 추천하지 않는다. 가끔 리눅스용 프로그램을 임베디드 제품에 포팅하다 보면 없는 함수가 있어서 곤란을 겪기 때문이다. 임베디드 장비에서 램이나 플래시 시스템이 16MB 이상이고 여유가 있다면 가급적 glibc를 쓰기를 권유한다.

gdb

이것은 디버거이다. 즉 프로그램을 디버깅할 때 유용한 도구이다. 하지만 윈도우 디버거를 사용하던 사람이라면 매우 불편함을 느낄 수 있다. 왜냐하면 명령형 디버거이기 때문이다. 하지만 없는 것보다는 있는 것이 좋다. printf만 가지고 디버깅하면 매우 힘들기 때문이다. 더욱이 익숙해지면 의외로 막강한 기능에 놀라게 된다. 이 gdb를 굳이 툴 체인에 포함시키지 않는 것은 상호의존적인 관계가 별로 없기 때문이다.

크로스 컴파일 환경 구축 방법

이제 본격적으로 시스템에 사용될 크로스 컴파일러 환경을 구축해 보자. 그런데 여기서 필자의 고민이 생겼다. 크로스 컴파일 환경을 구축하는 방법에는 레드햇 배포판 계열이라면 RPM 패키지를 이용하는 방법이 있고, 그냥 소스 패키지를 이용하는 방법이 있기 때문이다. 초기의 RPM은 그렇지 않았는데 요즘에 들어서 RPM이 배포판을 가리는(?) 경향이 있기 때문이다. 또한 arm용 RPM은 최신 버전으로 구하기가 그리 쉬운 것도 아니다. 하지만 크로스 컴파일러를 소스로 설치하는 것은 다른 기회에 살펴보기로 하고 여기서는 RPM으로 설치하는 것만 다뤄 보겠다. 혹시 소스 설치에 관심이 있다면 www.kelp. or.kr에서 필자가 쓴 문서가 있으므로 한번 찾아보기 바란다.

우선 RPM을 설치하기 위해서는 크로스 컴파일 툴 체인의 RPM 파일을 받아야 한다. 독자들이 이것을 직접 구하기는 그리 쉽지 않으므로 필자가 근무하는 회사의 사이트(www.falinux.com)에서 받자. 자료실의 EZ-X5 디렉토리에 두 가지 종류가 있는데, 와우 리눅스 7.1용과 7.3용이 있다. 그외 배포판은 곧 지원할 예정이지만 최근의 배포판이라면 7.3용을 받으면 될 것이다. 다만 여기서는 7.1을 기준으로 하겠다. 자료실에 포함된 파일의 크로스 컴파일 환경에 포함되는 것은 다음과 같다.

  • 어셈블러 및 로더 기타 툴 : armv5l-linux-binutils-2.13.90.0.16-ez1.i386.rpm
  • 컴파일러 : armv5l-linux-gcc-3.2.1-ez1.i386.rpm
  • 크로스 컴파일 구축을 위한 라이브러리 및 일반 라이브러리 : armv5l-linux-glibc-2.3.1-ez1.i386.rpm

실제 설치에 관련된 문서도 있으므로 설치에 관련된 자세한 방법은 문서를 참조하기로 하고 여기서는 간단하게 설명하겠다. 이후의 작업은 root로 작업을 해야만 한다. 따라서 설치시 반드시 순서를 지켜야 한다.

# rpm -Uvh --nodeps --force armv5l-linux-binutils-2.13.90.0.16-ez1.i386.rpm
# rpm -Uvh --nodeps --force armv5l-linux-gcc-3.2.1-ez1.i386.rpm
# rpm -Uvh --nodeps --force armv5l-linux-glibc-2.3.1-ez1.i386.rpm 

RPM을 풀면 /usr/armv5l-linux 디렉토리 밑에 크로스 컴파일 환경이 설정된다. 이후 실행환경 경로를 잡아줘야 하는데, 설정할 내용은 PATH=$PATH:/usr/armv5l-linux/bin이다. /root의 .bash_profile 파일에 이 내용을 추가한 후 일단 로그아웃한 다음 다시 root로 로그인하면 된다. 아니면 직접 콘솔에 입력하고 export할 수도 있다.

이렇게 크로스 컴파일 환경을 비롯한 개발 환경을 모두 구축하였다면 이제 본격적인 시험용 부트코드를 만들어야 한다. 그런데 이 부트 코드를 만들기 위해 프로그래머는 하드웨어의 핀 동작보다는 내부 동작에 대한 이해를 하고 있어야 한다. 문제는 이 내부 동작에 대한 이해가 프로그래머를 무척 괴롭히는 과정이라는 것이다. 독자들도 부트로더를 만들기 위해서는 전체는 아니겠지만 조금은 알아 두어야 할 것이다.

PXA255 프로세서

PXA255는 ARM의 변종이기는 하지만 프로그래머 입장에서 보면 ARM으로 봐도 무방하며 최근 프로세서의 전형적인 모습을 가지고 있다. 즉 XScale 마이크로 아키텍처 외부에 여러 가지의 주변 디바이스가 통합된 프로세서이다. PXA255의 대략적인 내부 구조는 와 같다. PXA255에서 지원하는 내용을 살펴보면 다음과 같다.

PXA255 프로세서의 내부 블럭도

  • XScale Microarchitecture 100~400MHz Core Clock
  • System memory interface
    • 100MHz SDRAM 4MB to 256MB of SDRAM 16, 64, 128, 256MB DRAM 4 Banks of SDRAM(64MB) SDRAM interface into selfefresh
    • 6 static memory(SRAM, Flash, VLIO)
    • PCMCIA/Compact Flash card Interface
    • LCD Controller
    • Full Function UART
    • Bluetooth UART
    • Standard UART
    • MMC Controller
    • SSP
    • USB Client Controller
    • AC’97 Controller
    • I2C Controller
    • GPIOs
    • Integrated JTAG support

PXA255의 영문 매뉴얼은 책 한권이다. 따라서 이 모든 내용을 다룰 수는 없으므로 여기서는 일단 넘어가자. 자세한 내용이나 설명이 필요한 부분은 그때그때 알아보기로 하고 이제 이지부트가 동작하는 타겟 보드에 대해서 잠깐 살펴보자.

보드를 살려보자 - 3

  • 저 자 : 유영창
  • 출판일 : 2003년11월호

타겟 보드

이전 강좌에서도 독자들에게 양해를 구했듯이 실제 사용 가능한 부트로더를 만들려면 타겟이 분명해야 한다. 여러 프로세서를 지원하는 공개된 부트로더들 역시 환경 설정시에 타겟을 설정하게 하는 것이 그 이유이다. 때문에 필자 회사에서 개발한 EZ-X5 보드를 소개할 수밖에 없는 점을 이해해 주기 바란다. 은 이번 연재를 통해 만들어보게 될 ‘이지부트’라는 부트로더가 실장될 타겟 보드(모델명 EZ-X5)의 외관이다. 실장 내용은 다음과 같다.

  • PXA255 400MHz
  • 32MB SDRAM
  • Boot Flash(NOR FLASH ROM 29LV800)
  • Data Flash(NAND FLASH K9F1208U0A)
  • Ethernet CS8900A
  • Toutch Controller MK712
  • JTAG 변환 회로
  • USB 커넥터
  • 확장 커넥터
  • DEBUG LED
  • LCD 확장 커넥터
  • FFUART/BTUART 2포트 커넥터

EZ-X5 타겟 보드

이 타겟 보드에 실장된 모든 디바이스를 부트로더에서 모두 처리할 필요는 없다. 왜냐 하면 이지부트는 최종적으로 리눅스 커널이 동작된다는 것을 염두에 두었기 때문이다. 실제 이지부트에서 지원하는 디바이스들은 32MB SDRAM, Boot Flash(NOR FLASH ROM 29LV800), Data Flash(NAND FLASH K9F1208U0A), Ethernet CS8900A, DEBUG LED(GPIO) 등이다. 열거된 디바이스들은 부트로더에서 실제 구현할텐데, 상세한 내용은 연재를 진행하면서 필요할 때마다 설명하도록 하겠다. 여기서는 그냥 이런 것이 있구나 하는 정도만 기억하자.

보드 시험을 위한 프로그램

보드를 시험하는 목적은 프로세서가 동작하는가에 대한 검증이다. 이 보드 시험에서 테스트 프로그램의 부트코드가 정상적으로 동작한다면 두 가지의 검증이 되는 것이다. 첫 번째는 보드의 동작이다. 아주 최소한의 동작이기는 하나 보드가 동작하기 시작하면 하드웨어적인 시험을 하기 위한 프로그램의 동작이 가능하기 때문에 이후의 검증 과정이 한결 수월해진다. 두 번째는 크로스 컴파일러의 구축에 대한 검증이다. 부트 코드가 동작한다는 것은 크로스 컴파일 환경에 이상이 없다는 것을 의미하기 때문이다.

서두에서 얘기했지만 하드웨어 검증이 끝나는 최초 시점은 부트 코드가 동작하는 조건이다. 하드웨어 검증만으로는 보드가 살았다고 할 수 없다. 더구나 구축된 크로스 컴파일 환경이 정말 정상적으로 동작되는지 역시 알 수 없는 것이다. 따라서 보드 시험을 위한 프로그램 설계는 아주 간단한 것을 수행하는 것으로 해야 한다. 필자가 즐겨 사용하는 방법은 LED를 점멸시키는 것이다. LED를 점멸시키는 프로그램을 주로 이용하는 이유는 초기 보드 시험시의 조건 때문이다.

프로세서가 동작하는 최소한의 조건은 전원, 클럭, 롬, 프로세서만 있으므로 처음 시험시에는 RAM을 비롯한 주변 디바이스도 달지 않고 시험한다. 주변 칩이 모두 달려 있으면 문제가 발생해도 정확하게 어디가 문제인지 알 수 없기 때문이다. 아무래도 동작을 확인하려면 시각적으로 확인하는 것이 좋기 때문에 디버그용 LED를 달아서 해당 LED를 동작시키는 코드를 작성한다. 이런 시험 코드를 작성하기 위해서 알아야 할 최소한의 내용은 다음과 같다.

  • 리셋시 프로세서 동작
  • PXA255의 GPIO 관련된 내용

또한 프로그램의 제약 조건이 있는데 그것은 스택 구조를 이용하는 방법의 프로그램이 불가능하다는 것이다. 왜냐하면 스택이라고 하는 것은 램을 이용하는 프로그램 방식인데, 앞에서 말했듯이 램을 달지 않고 초기에 시험하기 때문이다. 실제 PXA255와 같이 SDRAM을 사용하는 프로세서는 메모리 환경 설정 레지스터를 프로그래밍하는 것이 다소 복잡하다.

보드에 전원이 인가된 이후에 프로세서가 가장 처음 하는 행동은 리셋이다. 이 리셋에 대하여 알려면 먼저 ARM의 인터럽트에 대한 지식과 PXA255의 리셋에 관련된 지식이 필요하다.

ARM의 인터럽트와 벡터 테이블

ARM에는 FIQ(Fast Interrupt reQuest)와 IRQ(Interrupt ReQuest), Abort, Software Interrupt, Undefined Instruction Trap의 5가지 인터럽트가 있다. 각 인터럽트가 발생하면 ARM은 해당 인터럽트에 대응하는 벡터 테이블의 명령을 수행한다. 은 인터럽트 벡터 테이블이다.

테이블에서 보듯이 각 주소는 4바이트 단위로 수행 명령을 가지게 된다. ARM은 RISC 구조의 간단한 명령 구조를 가지고 있기 때문에 4바이트로 점프 명령이 가능하므로 각 주소에는 실제 인터럽트를 처리하는 루틴으로 점프하는 명령만을 넣는다. 이중 Normal Inter rupt와 Fast Interrupt는 발생 소스가 여러 가지 있으므로 외부 디바이스로 인터럽트 컨트롤러가 간여하게 된다. 실제 PXA255의 인터럽트 중에서 리셋에 관련된 내용을 알아보자.

PXA255의 리셋

PXA255의 리셋 신호의 종류에는 GPIO, 워치독, 하드웨어의 3가지이다. 이중 보드 살리기에 관련된 리셋은 하드웨어 리셋이다. 하드웨어 리셋은 전원이 투입된 후 자동으로 리셋 신호 발생 회로 또는 스위치와 같은 리셋 신호 발생 회로에서 발생된 신호를 nRESET 단자에 일정한 시간 동안 인가함으로로써 발생된다. 하드웨어 리셋 신호는 프로그램으로 금지시킬 수 없다. 또한 내부적인 운영 모드는 리셋에 아무런 영향을 끼칠 수 없다. nRESET 신호는 nRESET_OUT에 연동되어 외부에 리셋 상태를 전달할 수만 있다. 리셋 상태에서는 프로세서의 모든 내용이 초기화된다.

내부 코어와 다비이스들은 사전에 정의된 리셋 초기 상태 값을 유지한다. 이때 외부에서 인가되는 nBATT_FAULT와 nVDD_ FAULT 신호들은 무시된다. 메모리 컨트롤러가 정지되므로 다이나믹 램과 같은 디바이스의 내용은 소실된다. 리셋 상태에 진입한 후에 nRESET 신호 핀에 다시 하이 상태가 일정한 시간 이상 인가되면 리셋 상태가 종료되고 다음과 같은 부팅 순서로 진행된다.

  • 3.6864MHz 오실레이터와 PLL 클럭 제너레이터의 안정 상태를 위해서 잠시 기다린다.
  • nRESET_OUT 신호가 하이 상태가 된다.
  • 정상적인 부팅 순서가 시작되며 디바이스들은 사전에 정의된 상태를 갖는다.

이때의 프로세서는 최초의 리셋 어드레스인 0000-0000번지의 명령을 읽으려고 시도한다. 여기서 한 가지 기억해 두어야 할 것은 하드웨어 리셋이 된 상태는 자동적으로 슈퍼바이저 모드로 전환되고, 내부 코어에 전달되는 클럭은 PXA255가 비록 400MHz로 동작된다 하더라도 3.6864MHz가 그대로 전달된다는 점이다. 즉 동작 속도가 3.6864MHz밖에 안 되어 매우 느리게 동작한다. 또한 메모리 접근 속도도 설정할 수 있는 가장 느린 속도의 접근을 시도한다. 때문에 대부분의 롬 디바이스에 접근이 가능하고 메모리에서 데이터를 읽어올 수 있는 것이다.

인터럽트 벡터 테이블

보드를 살려보자 - 4

  • 저 자 : 유영창
  • 출판일 : 2003년11월호

PXA255의 GPIO

EZ-X5의 LED는 GPIO 2, 3, 4, 5에 연결돼 있다. 따라서 LED 시험을 하기 위해서는 PXA255의 GPIO에 대해 알아봐야 한다. GPIO( General Purpose Input Output)는 일반적인 용도의 입출력 포트를 의미하며, 구조는 와 같다. 이처럼 복잡한 구조를 갖는 이유는 하나의 입출력 핀에 여러 가지 기능을 부여하기 때문이다. 기본적으로 입력과 출력이 가능하며, 하나의 핀은 GPIO로 사용될 수도 있지만 다른 기능으로도 사용이 가능하다.

GPIO의 구조

이 모든 것을 설명하기는 지면 사정상 곤란하므로 시험 코드를 작성하기 위해 필요한 사항에 대해서만 알아보자. 우선 살펴봐야 할 것은 GPIO에 관련된 레지스터이다. PXA255의 GPIO는 다음과 같은 레지스터를 갖는다.

  • 핀의 특성 부여용 레지스터
    • GPDR - GPIO 입출력 방향 설정 레지스터
    • GAFR - GPIO 이외의 기능으로 사용 허가 설정 레지스터
    • GRER - GPIO 입력 상승 에지 검출 허가 레지스터
    • GFER - GPIO 입력 하강 에지 검출 허가 레지스터
  • 데이터 출력 및 검출용 레지스터
    • GPSR - GPIO 출력 셋 레지스터
    • GPCR - GPIO 출력 클리어 레지스터
    • GPLR - GPIO 입력 레벨 검출 레지스터
    • GEDR - GPIO 입력 에지 검출 레지스터

이 레지스터에 접근하기 위해서 한 가지 짚고 넘어가야 할 것이 있다. 인텔의 i386 계열의 프로세서들은 I/O의 주소와 메모리 주소가 분리되어 있다. 하지만 ARM은 이렇게 분리되어 있지 않기 때문에 I/O의 접근과 메모리 접근이 같은 방식으로 수행된다. 즉 I/O는 일반 메모리처럼 접근하도록 프로그래밍한다는 것이다. 또한 PXA255의 코어를 제외한 내부 디바이스들은 이미 주소가 할당되어 있다. 따라서 GPIO를 제어하기 위해서는 GPIO를 제어하는 컨트롤러 레지스터에 접근해야 한다. GPIO의 레지스터 주소는 와 같다.

GPIO의 레지스터 주소

이렇게 상당히 많은 레지스터가 존재하는 이유는 GPIO가 80개 이상이기 때문에 32비트 하나의 레지스터로는 모두 제어할 수 없기 때문이다. EX-X5에서 LED를 제어하는 GPIO는 레지스터를 2, 3, 4, 5번이다. 또한 출력으로만 이용하고 리셋시에는 부가 기능이 해제되기 때문에 관련된 레지스터는 GPDR, GPSR, GPCR이다. 이 레지스터의 주소는 다음과 같다.

  • GPDR0 0x40E0000C
  • GPSR0 0x40E00018
  • GPCR0 0x40E00024

GPDR은 GPIO를 출력으로 설정하는 레지스터인데 GPIO 핀에 해당하는 비트 값이 1로 설정되면 출력이 된다. 리셋시에는 모두 입력 상태를 갖는다. GPSR과 GPCR은 PXA255의 GPIO 출력 제어의 특성 때문에 이렇게 두 개의 레지스터로 나누어진다. 필자의 개인적인 입장으로 보면 PXA255는 각 비트별 제어가 무척 쉽다. 왜냐하면 특정 GPIO의 출력을 하이 상태로 만들 경우 GPSR의 비트 중에 해당 GPIO 비트만 1로 설정해 써넣으면 1이 아닌 다른 GPIO에는 영향을 미치지 않기 때문이다. GPCR은 출력을 로우로 만들어 주며 GPSR과 제어 방식은 동일하다.

LED 제어 프로그램

LED를 제어하기 위한 부트 프로그램을 작성하기 위해서는 ARM 어셈블러를 알아야 한다. 하지만 ARM 명령을 여기서 모두 소개하기는 힘들기 때문에 프로그램의 실제 코딩과 관련된 것만 필요할 때마다 알아보겠다(자세한 내용은 kelp.or.kr의 자료실을 참고하라. ARM7의 명령에 관련된 한글 문서가 있으며, 영문으로 쉽게 구할 수도 있다). 부트코드에 사용되는 ARM 명령은 기본적으로 ARM7이든 ARM9든 XScale이든 모두 다 동일하기 때문에 ARM7의 명령어만 숙지해도 문제가 없을 것이다.

우리가 작성하고자 하는 LED 제어 부트 프로그램은 크게 ▲ start.S(프로그램) ▲ start-ld-script(링크 정보) ▲ Makefile(메이크 파일)의 3가지 파일로 구성되어 있다. 각 파일을 좀더 상세히 알아보자.

보드를 살려보자 - 5

  • 저 자 : 유영창
  • 출판일 : 2003년11월호

start.S 어셈블러

start.S 어셈블러 에서 가장 먼저 보이는 것은 (1)인데, 이렇게 선언되면 이후 코드의 어드레스 영역이나 기타 조건은 링커 스크립트에 선언된 .text라는 선언에 영향을 받는다. 일반적으로는 프로그램 코드 영역을 의미한다.

하드웨어 리셋이 발생하면 ARM은 0x00000000번지의 명령을 수행한다. 이 번지는 링커 스크립트에 의해서 _start 라벨에 맵핑된다. 좀더 정확히 말하면 링커 스크립트에서 .text에 대해 0x00000000번지에 할당되어 있기 때문이고 _start 라벨이 가장 먼저 기술되었기 때문이다. 이 _start가 0x00000000번지이기 때문에 이 라벨 이후에 인터럽트 벡터 테이블이 들어간다. 그래서 각 벡터 테이블이 수행할 명령을 기술한다.

start.S

start.S 에서 (2)의 첫 문장(b reset)은 실제로 수행되는 것이다. 그외 인터럽트가 발생하면 의 마지막에 기술되어 있는 (3)을 통해 error_loop문을 수행한다. 즉 reset을 제외한 모든 인터럽트는 error_loop로 분기하라는 의미이다(ARM의 명령에서 b는 branch의 약자).

이 error_loop는 무한 루프를 돈다. 즉 정상적인 reset 이외에는 그냥 무한 루프를 돌게 만들었다. 하드웨어 리셋에 의해서 reset 라벨로 분기하면 가장 처음 하는 것은 LED와 관계된 GPIO 레지스터의 초기화이다. 즉 가장 먼저 GPIO의 입출력 방향 선택 레지스터에 출력으로 만든다.

우선 ldr이라는 명령을 알아보자(의 (4)). 이 명령은 메모리의 내용을 레지스터로 읽어 오는 명령이다. 이 수행문은 정확히 말하면 두 가지 의미를 가지고 있다. 하나는 PXA_REG_GP_BASE라는 값을 특정한 데이터 영역에 컴파일러가 알아서 기술하고, 그 위치에 해당하는 어드레스를 이용해 r0 레지스터에 값을 가져오는 것이다. 일반적으로 ARM의 상수 값은 12비트 이상을 지정하기 힘들기 때문에 12비트 이상의 상수 값은 이런 식으로 표현한다. 이 문장은 r0 레지스터에 GPIO 레지스터의 가장 선두 주소를 넣는다.

다음에 수행되는 (5)는 r1 레지스터에 DEBUG_LED1의 상수 값을 대입하는 문장이다. 즉 mov는 레지스터의 내용을 다루는데 사용되는 데이터의 이동 명령이다. 이렇게 해서 GPIO 베이스 레지스터 주소를 r0 레지스터에 디버그 LED의 GPIO에 해당하는 비트를 r1에 넣고 (6) 명령을 이용해 GPIO 입출력 레지스터에 출력으로 설정한다. str은 ldr의 반대 명령으로 레지스터의 내용을 메모리에 전송하는 명령이다. 이 문장을 수행한 결과를 풀어 쓰면 다음과 같은 표현이 된다.

  • [ PXA_REG_GP_BASE + PXA_REG_OFFSET_GPDR0 ] ← DEBUG_LED1

이렇게 GPIO의 입출력 방향을 설정한 이후에 (7) 명령을 수행하여 GPIO의 출력을 LOW로 만든다. 이 문장은 GPCR0의 레지스터 중 GPIO를 클리어하는 레지스터에 DEBUG_LED1의 비트를 대입함으로써 LED에 연결된 GPIO의 출력을 LOW로 만드는 것이다. 즉 처음에는 LED를 켠다(일반적으로 하드웨어 회로에서 출력이 LOW가 되면 LED가 켜지게 만든다).

이제 LED를 점멸시키기 위해서는 일정 시간을 대기하고 LED에 연결된 GPIO의 출력을 HIGH로 놓고 다시 일정 시간을 대기한 다음 LED에 연결된 GPIO의 출력을 LOW로 놓는다. 그리고 이것을 계속 반복한다.

이렇게 처리되면 R0에는 GPIO 제어 레지스터의 선두 주소가, R1에는 LED에 해당하는 GPIO의 비트 값이 자리잡게 된다. ARM은 총 15개의 레지스터가 있는데, R13은 주로 스택 포인터로, R14는 분기 후 복귀할 때 복귀될 주소로 이용된다. R15는 현재 수행중인 주소를 관리한다. 따라서 일반적인 목적으로 사용될 수 있는 레지스터는 R0에서 R12까지이다.

이 프로그램에서 이미 R0와 R1을 사용하고 있으므로 대기를 수행하기 위해서 R4 레지스터를 사용하고 있다. 대기를 하기 위한 첫 번째 루틴이 (8)인데 이것은 r4에 WAIT_TIME_NOCACHE 대기 루프 값을 넣고 계속 1씩 감소시킨다. 그 값이 0이 되기 전까지는 이 과정을 반복하여 지연을 만들어내는 것이다. nop라는 명령은 아무런 수행은 하지 않고 단순하게 시간만 소모하는 명령이다. 또 r4 값을 1씩 감소하기 위해서 subs라는 명령을 사용하고 있는데 원래 명령은 sub이고 그 뒤에 붙는 s 문자는 연산 결과가 플래그에 영향을 미치는가를 표시한다. 없으면 연산 결과가 플래그에 영향을 미치지 않기 때문에 그 뒤의 조건문에 영향을 미치지 않는다. subs는 연산 결과가 플래그에 영향을 미치므로 그 뒤에 나오는 조건 분기 명령인 bne에 영향을 미친다. 이 bne의 가장 처음 글자인 b는 앞서 설명했듯이 분기 명령이고, 뒤의 ne는 Not Equal로 ‘0이 아니면’이라는 뜻이다. 즉 연산 결과가 0이 아니면 B20으로 분기하라는 명령이다.

이렇게 일정 시간을 대기한 후에 LED를 끄기 위해서 (9) 문장을 수행한다. 결국 이와 같은 명령을 통해서 LED를 점멸시키는 것이다. 지금까지 LED를 점멸시키는 프로그램 소스를 살펴보았으므로 이를 컴파일하기 위한 내용을 살펴보자.

링커 스크립트

start.S 어셈블러 소스를 보면 알겠지만 여기에는 컴파일되어 실행 파일이 생성된 이후 이 실행 파일의 실제 시작 주소에 대한 정보가 없다. 이것은 컴파일러가 오브젝트 파일만을 만들기 때문이다. 이 오브젝트 파일은 링커(armv5l-linux-ld)에 의해서 실제 수행 가능한 코드로 만들어진다. 그래서 링커는 각 프로그램의 실제 배치와 관련된 주소 정보와 소스의 섹션들을 정렬하는 방법에 대해 기술한 ‘링커 스크립트’라는 파일을 필요로 한다.

start.S는 C 소스가 아니기 때문에 그리 복잡한 정의까지는 필요치 않다. 하지만 의 start-ld-script는 부트로더의 전형적인 구조이므로 그냥 사용하면 될 것이다.

start-ld-script

start-ld-script 에서 첫 행의 OUTPUT_FORMAT이라는 명령은 링커에 의해서 생성되는 실행 파일 포맷이 elf 포맷이고 32비트 리틀 엔디언 형식의 정수 표현을 갖는다는 의미이다. 두 번째 줄의 OUTPUT_ ARCH란 명령은 출력 아키텍처가 arm이라는 의미이다.

링커는 실행 파일의 시작점에 해당하는 라벨 심볼 이름을 기술하게 되어 있는데, 이것을 지정하는 것이 ENTRY이다. 실제로 이 프로그램에서는 리셋에서 시작된다는 것이 정해져 있다(C가 아니기에 무의미하지만 컴파일 경고를 방지하기 위해서 사용하고 있다).

다음은 SECTIONS라는 명령인데, 이 명령을 상세하게 설명하려면 너무 복잡하므로 단순하게 코드나 데이터의 주소와 배열 순서에 대한 정보라고만 기억하자. 이 SECTIONS에서 가장 처음 보이는 (1) 문장은 시작 주소를 지정하는 문장이다. 좀더 정확히 설명하면 .은 현재 위치를, 그 뒤에는 현재 위치에 대입하는 주소를 지정한다. 이 문장이 맨 처음 나왔기 때문에 SECTIONS에 선언된 각 섹션의 가장 선두를 지정하게 되는 것이다.

)2) 문장은 각 코드나 데이터의 섹션간 정렬은 4바이트 단위로 만들라는 것이다. 32비트 코드들은 대개 4바이트 단위로 패치되기 때문에 섹션의 정렬에 따라 4바이트로 끊어지지 않더라도 강제로 맞추라는 의미가 된다. 에서 (3) 문장이 계속 보이는데, 바로 이것이 섹션을 나누는 문장이다. 이 문장은 일반적으로 이렇게 쓴다는 정도만 기억해 두자. 간단히 설명하면 나중에 사용될 이지부트가 C 함수와 혼합되는 링커 스크립트를 만들기 위한 것이다.

이 리눅스 커널에서 사용되는 섹션 구조는 이것보다 상당히 복잡한데 기회가 되면 직접 한번 살펴보기 바란다. 이 링크 스크립트에 대한 것이 궁금하면 역시 kldp.or.kr에서 매우 상세하게 기술된 문서가 있으니 컴파일러 관련 문서를 찾아보기 바란다.

보드를 살려보자 - 6

  • 저 자 : 유영창
  • 출판일 : 2003년11월호

Makefile

리눅스에서 동작하는 gcc는 기본적으로 make 유틸리티를 사용하여 컴파일한다. RAD 툴을 사용하던 프로그래머라면 이 make 유틸리티가 다소 어렵게 느껴질지 모르지만 리눅스 프로그래머거나 기존 펌웨어 프로그래머라면 항상 접하는 유틸리티이다. 리눅스에서 사용되는 make 유틸리티는 매우 유용한 도구이고 개념만 익히고 나면 쉽다는 것을 알 수 있다. make 유틸리티는 기본적으로 Makefile이라는 이름을 갖는 파일을 필요로 한다. 이 Makefile이 바로 컴파일에 관련된 모든 조건을 기록해 놓는 파일이다. 보통 이 Makefile은 구성이 비슷하기 때문에 한번 자신만의 구성을 만들면 이후에 조금씩 수정해서 쓰면 된다. make에 관련된 자세한 사용법은 kldp.or.kr 사이트에 가보거나 시중 서점에 단행본으로 나와 있기 때문에 본격적으로 배우고 싶다면 찾아보기 바란다.

크로스 컴파일을 위한 Makefile은 일반적인 응용 프로그램을 만드는 Makefile과 몇 가지 다른 점이 있는데 먼저 필자가 만든 Makefile을 보고서 꼭 필요한 내용만 의미를 파악해 보자.

Makefile

가장 처음 보이는 의 (1)은 컴파일러로 사용될 수행 프로그램을 기술한 것이다. 여기서 CC는 컴파일러, LD는 링커, OC는 elf 포맷의 실행 파일을 이진 코드로 만들기 위해서 사용되는 프로그램을 정의하고 있다.

start.S가 컴파일되어 이진 파일로 만들어지는 과정을 살펴보면 가장 처음 CC에 의해서 지정된 컴파일러에 의해서 start.S는 start.o로 만들어진다. 이렇게 만들어진 start.o는 LD에 의해서 지정된 링커에 의해서 elf 형태의 ledtest-elf32란 이름을 갖는 실행 파일이 된다. 이것을 그대로 롬으로 쓸 수는 없는데, 이유는 elf 포맷의 실행 파일은 수행 코드와 데이터만 있는 것이 아니고 수행에 필요한 기타 정보들이 기록되어 있기 때문이다. 이런 정보를 제거하기 위해서 OC로 지정된 오브젝트 유틸리티 프로그램을 이용해 모든 정보가 제거된 이진 파일인 ledtest_org를 만든다.

이 과정을 추적해 보자. 먼저 make란 명령을 수행하면 Makefile을 이용하여 각 단계를 수행한다. make 명령은 Makefile에서 (2)와 같이 all:로 표현된 부분을 찾는다. 여기서 all을 만들기 위해서 의존적인 $(PRE_TARGET)를 찾는데, 이것은 PRE_TARGET = ledtest-elf32라는 명령에 의해서 ledtest-elf32로 지정되어 있다. make는 all을 만들기 위해서 ledtest-elf32가 처리되어 있어야 하므로 다시 ledtest-elf32:에 관련된 것을 찾는다.

의 (3)에서 make가 $(PRE_TARGET)은 다시 $(OBJS)에 의존적이라는 것을 발견하고 이것을 처리하는 과정을 수행한다. OBJS = start.o로 선언되어 있으므로 make는 다시 start.o에 대하여 검색한다. 여기서 지금까지 선두에 선언된 (4) 문장으로 진행된다. 이것의 의미는 확장자가 .o로 끝나는 것은 확장자를 제외한 동일한 이름의 확장자가 .S인 파일에 의존적임을 표시한다. 이 문장이 컴파일을 수행하게 해주는 최종 조건인 것이다. 이후 (4) 문장에 의해서 start.S 파일은 start.o로 컴파일되는데 이를 수행하는 문장이 (5)이다. 이 문장을 풀어 쓰면 다음과 같은 매우 긴 문장으로 치환되어 수행된다.

armv5l-linux-gcc -c -nostdinc -I. -I$(TOPDIR)/include -Wall -Wstrict-prototypes -Wno-trigraphs -O2 -fno-strict-aliasing -fno-common -pipe -mapcs-32 -march=armv5 -Wa,-mxscale -mtune=strongarm -mshort-load-bytes -msoft-float -fno-builtin -o start.o start.S

이 문장의 대부분은 컴파일러 옵션인데 중요한 몇 가지만 살펴보자.

-c : 컴파일만 하고 링크는 하지 말라는 의미이다. -nostdinc : 표준 헤더 파일 디렉토리를 참조하지 말라는 의미이다. -march=armv5 : arm 명령을 version 5로 컴파일하라는 의미이다. 여기서는 PXA255가 armv5라는 의미이다. - mxscale : 프로세서 명령 타입이 xscale임을 알려주는 것이다. - mtune=strongarm : SA1100 타입으로 최적화하라는 의미이다. 이것은 xscale의 sa1100와 유사하기 때문이다. - msoft-float : 실수 연산에 대한 처리는 코프로세스를 이용하지 않은 소프트웨어 연산을 처리하라는 의미이다. - fno-builtin : 표준 라이브러리와 링크되지 말고 단독으로 링크하라는 의미이다.

이렇게 해서 start.o가 생성되면 지금까지와 역순으로 각각의 명령이 수행된다. (6) 문장을 풀어 쓰면 다음과 같은 의미가 된다.

armv5l-linux-ld -p -X -T ./start-ld-script -o ledtest-elf32 start.o

여기서 링크 스크립트를 지정하는 것이 ./start-ld-script이다. 이렇게 해서 ledtest-elf32가 얻어지면 (7) 문장을 수행한다. (7)의 첫 번째 문장은 출력 포맷을 2진 파일로 만들고(-O binary) .note 섹션의 심볼릭 정보를 제거하고(-R .note) .comment 섹션의 심볼릭 정보를 제거하고(-R .comment) 스트립하라(-S)는 의미가 된다. 이를 풀어 쓰면 다음과 같다.

armv5l-linux-objcopy -O binary -R .note -R .comment -S ledtest-elf32 ledtest-org

원칙상 여기서 수행을 끝내도 되지만 나중을 위해서 한번 더 변화하는 과정을 거치는데 (7)의 두 번째 문장이 1024바이트 단위로 실제 파일을 만든다(이것은 다음 연재에서 설명하겠다). 이렇게 해서 최종 부트코드 ledtest_x5라는 파일이 만들어진다.

이를 플래시 라이터 툴이나 롬 라이터 툴을 이용해 넣으면 된다. EZ-X5에서는 ‘이지플래시’라는 프로그램을 이용해 프린터 포트와 연결된 JTAG 단자를 통해 써넣는다. 마지막으로 전원을 켰을 때LED가 점멸하면 보드의 초기 부팅 조건에 이상이 없음을 알게 된다.

arm 부트로더에 익숙해지자!

지금까지 살펴본 프로그램은 무척 간단하지만 이 프로그램을 수행시키는 과정은 생각보다 복잡하다. 리눅스를 처음 접하는 독자라면 더욱 어려울 수도 있다. 그러나 이 과정은 임베디드에서 수행되는 부트로더를 만들기 위해서 꼭 거쳐야 하는 첫 번째 관문이다. 앞으로도 많은 관문들이 기다리고 있지만 차근차근 정복하면 그다지 힘들지 않을 것이다. 부디 포기하지 말고 계속 진행하기 바란다. 한번 익숙해지면 그 다음부터는 arm 부트로더를 만드는 작업이 그리 어렵지 않을 것이기 때문이다.

반응형