SW 개발

[Linux 강좌] 3) 부트로더의 기본구현 -마소-

. . . 2012. 5. 11. 15:54
반응형

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

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

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

...

출판일 : 2003년 12월호

부트로더의 주 임무는 커널이 동작할 수 있는 환경을 구성하고 커널을 시작하는 것이다. 정의는 간단하지만 프로세서를 정상적으로 동작시켜야 하기 때문에 그리 만만한 일이 아니다. PXA255 프로세서에서 동작하는 이지부트 프로그램은 두 부분으로 나눠져 있다. 최소한의 환경 설정을 하는 start 프로그램 부분이 있으며, 커널이 부팅하도록 지원하고 개발의 편리함을 제공하는 main 프로그램이 있다. start 프로그램은 모두 ARM 어셈블러로 구성돼 있고 스택이 없기 때문에 다중 호출도 하지 못하는 가장 열악한 환경의 프로그램이다. 이번 호에는 이지부트의 시작점인 start.S를 집중적으로 살펴보고자 한다.

필자는 제이닷디앤티의 기술이사로 근무 중이다. 원래는 윈도우 시스템 환경의 시스템 프로그래머였다가 그 악명 높은 파란 화면에 질려서 리눅서로 전향(?)했다. 리눅스를 이용해 공장 자동화에 적용시키려다 임베디드 리눅스에 처음 발을 들여놓게 됐다. 하지만 아직도 리눅스의 vi 에디터에 적응하지 못하는 절름발이 리눅서이기도 하다.

부트로더의 기본구현

부트로더의 주 임무는 커널이 동작할 수 있는 환경을 구성하고 커널을 시작하는 것이다. 정의는 간단하지만 프로세서를 정상적으로 동작시켜야 하기 때문에 그리 만만한 일이 아니다. PXA255 프로세서에서 동작하는 이지부트 프로그램은 두 부분으로 나눠져 있다. 최소한의 환경 설정을 하는 start 프로그램 부분이 있으며, 커널이 부팅하도록 지원하고 개발의 편리함을 제공하는 main 프로그램이 있다. start 프로그램은 모두 ARM 어셈블러로 구성돼 있고 스택이 없기 때문에 다중 호출도 하지 못하는 가장 열악한 환경의 프로그램이다. 이번 호에는 이지부트의 시작점인 start.S를 집중적으로 살펴보고자 한다.

공개된 부트로더를 살펴보면 주로 두 개의 프로그램으로 나누어진다. 하나는 부팅할 때 동작하는 프로그램이고, 또 하나는 프로세서의 램 컨트롤러 환경 설정이 끝난 후에 동작하는 프로그램이다. 또한 대부분의 부트로더 프로그램은 롬(ROM)에서 동작하지 않고 램(RAM)에서 동작한다.

왜 이렇게 동작하는 구조를 가질까? 이것은 필자가 공개된 부트로더를 처음 보면서 가졌던 의문이다. 롬에서 프로그램이 동작하면 메모리의 사용 효율도 높일 수 있을 것 같은데 굳이 낭비를 하면서까지 램에서 프로그램을 동작시키는 이유가 무엇인가가 의문의 핵심이었다. 하지만 직접 EZ-X5에서 동작하는 부트로더를 만들어가면서 이 질문에 대한 답을 모두 얻었다. 결론부터 이야기하면 이유는 다음의 세 가지이다.

  • 롬은 처리 속도가 느리다.
  • 플래시 롬은 해당 장치에 쓰기를 시도하는 순간 롬으로서의 역할을 하지 못한다.
  • 어셈블러로 작성된 프로그램과 C 함수로 작성되는 프로그램에 차이가 있다.

플래시 롬의 특성

요즘 나오는 32비트 프로세서들은 처리 속도가 최소한 50MHz 이상이다. 시중에서 가장 많이 사용되고 있는 프로세서들은 200MHz가 기본이다. 그런데 롬이나 플래시 롬들은 150nsec(나노 초) 정도의 접근 속도를 갖는다. 이것은 최근 프로세서들의 동작 속도에 비하면 현저하게 느린 속도이다. 프로그램을 수행하기 위해서는 롬에서 프로그램 코드를 프로세서로 가져와야 하는데 롬이나 플래시 롬들은 150나노 초 정도의 시간이 소요되기 때문에 프로세서가 캐시를 사용한다고 하더라도 수행 속도는 롬의 처리 속도에 좌우되어 버린다.

반면 롬과는 달리 SRAM이나 SDRAM의 처리 속도는 고속이다. 더욱이 SDRAM의 동작 속도는 100MHz 정도이다. 두 메모리 장치의 속도 차이는 한 사이클만 보면 대단하지 않을 것 같지만 수만 번의 연산을 반복하면 그 차이는 어마어마해진다. 그래서 프로그램의 처리 속도를 올리기 위해서는 롬에서 프로그램을 수행하는 것보다 램에서 수행하는 것이 더 낫다.

지금도 가끔 쓰이지만 예전에 프로그램 롬으로 대표적인 것이 EEPROM이었다. 이 롬은 가격적인 이점 때문에 8비트 프로세서에서 지속적으로 사용되기는 하지만 사용 가능한 공간도 그리 크지 않고 프로그램을 써넣는 장비가 따로 필요한 점 등 무척 불편하기 때문에 최근에는 잘 사용되지 않고 있다. 이와 달리 플래시 롬은 용량도 상당히 크고 써넣는 기능을 소프트웨어로 간단하게 구현할 수 있는 데다 프로그램을 롬에 써넣는 장비가 따로 필요 없기 때문에 점점 많이 사용되는 추세이다(가장 초기에 동작하는 프로그램을 써넣을 경우에는 장비가 필요하거나 JTAG을 이용한 라이팅 프로그램이 필요하다).

하지만 이 플래시 롬이 롬으로서의 역할을 하는 것은 단순하게 읽기만을 수행할 때이다. 플래시 롬에 쓰기를 시도하게 되면 그 순간 플래시 롬은 롬으로서의 역할을 하지 못한다. 그래서 플래시 롬에서 동작하는 프로그램은 자신이 동작하는 플래시 롬에 쓰기를 시도할 수 없다. 쓰기를 시도하는 순간 프로그램은 멈추게 된다. 이런 특성 때문에 플래시 롬에서 동작하는 부트로더는 다운로드 기능을 구현하지 못한다.

꼭 플래시 롬에서 동작하는 프로그램을 제작해야 하는 경우에는 플래시에 쓰는 기능을 수행하는 루틴만 잠시 램으로 옮겨서 수행한 후 플래시에 원하는 데이터를 써넣고 다시 플래시 롬을 롬으로 동작하는 변환 처리를 한 다음 원래의 루틴으로 동작하게 하는 방법을 사용한다. 그러나 이런 식으로 구현하게 되면 생각보다 여러 가지 경우를 고려해야 한다. 가장 대표적인 고려 사항이 복귀 루틴의 고정 처리이다. 복귀된 위치가 다른 루틴으로 대치되어 버리면 문제가 발생할 경우가 있기 때문이다(플래시에 대한 이야기는 이후에 다시 자세히 하게 될 것이므로 여기서는 이 정도로 이해하고 넘어가자).

어셈블러 구현 루틴과 C 함수 구현 루틴의 차이

부트로더를 어떤 언어로 구현하는가는 프로그래머가 어떤 언어를 더 자유롭게 구현하는가에 달린 문제이다. 꼭 C 언어로 부트로더를 구현할 이유는 없다. 하지만 부트로더를 C 언어로 구현하면 어느 정도 아키텍처에 의존적인 부분을 줄여서 작성할 수 있다. 필자는 ARM에서 사용한 부트로더인 ‘이지부트’를 i386EX에도 적용한 적이 있다. 이때 ARM에서 사용된 C로 작성된 부분 중 초기화 부분 이외에는 그대로 사용이 가능했다.

초 기에 부트로더를 만들기 위해서는 다른 부트로더의 소스에서 루틴을 가져오거나 커널 소스에 있는 루틴을 가져오게 된다. 이렇게 다른 소스를 가져와 사용할 때는 아무래도 어셈블러보다 C 언어가 유리하다. 결국 C 언어로 부트로더를 작성하는 것이 여러모로 유리하기 때문에 대부분의 공개된 부트로더는 C 언어로 사용된다. 그런데 C 언어는 함수 호출 구조상 스택(stack)이라는 것이 필요하다. 그러나 스택은 롬 시스템에서는 원초적으로 사용이 불가능하다. 따라서 C 언어로 작성된 프로그램은 램에서 동작하여야 한다. 부트로더는 이런 언어적인 차이도 롬으로 동작하는 것과 램에서 동작하는 것으로 나누어야 하는 이유가 된다.

이 런 이유로 EZ-X5에서 동작하는 이지부트 프로그램은 start 프로그램과 main 프로그램으로 나누어져 있다. start 프로그램은 어셈블러로 작성되어 있고 아주 기초적인 부분을 수행한다. main은 C 언어로 작성되어 있고 start 프로그램에서 구현한 것 외의 나머지 대부분을 구현한다. 각각 만들어진 두 개의 프로그램 소스는 컴파일되고 실행 이미지가 따로 만들어진다. 이후에 하나의 이미지로 합쳐져 플래시 롬에 써넣게 된다. 이렇게 합쳐진 프로그램 중에서 리셋 후에 처음 동작하는 것을 이지부트에서는 start란 프로그램이 담당하고, 스택이 설정되어 C 함수 호출이 가능한 램 공간에서 수행되는 대부분의 작업은 main 프로그램이 담당한다.

이지부트가 어떻게 동작하는지 을 가지고 설명하겠다. 그림에서 보듯이 EZ-X5의 플래시 롬은 0x00000000에 위치한다. start 프로그램은 2KB 이내로 매우 작은 크기를 갖고 있다. main 프로그램은 그 나머지를 차지한다. 플래시 롬에는 이 두 개의 프로그램이 연속되어 있는 하나의 이미지로 만들어져 배치된다. 이 이미지는 리셋 후 start 프로그램에 의해서 의 (1)에서 보듯이 램의 0xA0F00000에 그대로 복사된다. 램은 0xA000000부터 시작하는데 나중에 배치되는 커널을 고려해서 램의 뒤쪽에 부트로더를 둔다.

![이지부트의 동작 방식]https://t1.daumcdn.net/cfile/tistory/127DC6334FACB7990D

main 프로그램은 수행 시작 위치가 0xA0F00800로 고정된다. 이 위치는 램에 같이 복사된 2KB를 차지하는 start 이미지 바로 뒤쪽이다(예전에 어떤 사람이 필자에게 2KB를 제외한 크기를 램에서 복사하지 않는가 물어본 적이 있다. 하지만 이렇게 하는 특별한 이유가 있는 것은 아니다. 단지 필자가 게으르기 때문인데, 2KB를 제외하면 실제 크기를 계산해서 프로그래밍해야 하지만 그대로 하는 것이 정신건강(?)에 유리하기 때문이다). 부트로더는 부팅이 끝나면 그대로 사라지는 프로그램이기 때문에 부득이 램의 크기가 작은 시스템이 아니라면 굳이 메모리를 아낄 필요는 없다.

롬 에서 동작하는 start 프로그램은 초기화를 마치고 main으로 이동해야 하는데 두 프로그램은 같은 프로그램이 아니기 때문에 소스상의 심볼릭 참조가 불가능하다. 즉 소스에서 어떤 함수로 이동하라는 식의 기술이 곤란하다. 그래서 절대 위치로 이동하는 명령을 사용하는데 이동 위치가 의 (2)와 같이 0xA0F00800이 된다.

이지부트의 디렉토리 구조

이지부트 소스에서 start와 main의 두 프로그램은 상호 유기적인 관계가 있기 때문에 디렉토리를 하나로 작성하고 하부 디렉토리에 따로 관리한다. 하나의 디렉토리 안에 모든 소스를 관리하면 나중에 무척 힘들게 되고 Makefile이나 링크 스크립트 작성이 어렵다. 그래서 Makefile이나 링크 스크립트의 관리적인 측면에서 따로 나누는 것이 유리하다.

우선 이지부트의 하부 디렉토리 구조를 살펴보자. 이지부트가 존재하는 디렉토리가 ezboot라는 이름을 갖는다고 가정하면 하부 디렉토리의 모습은 다음과 같다.

  • ezboot/image/ : 최종적으로 생성되는 부트로더 이미지가 들어가는 디렉토리
  • ezboot/include/ : 공통적인 헤더 파일 디렉토리
  • ezboot/main/ : 부트로더의 주 구현 소스가 있는 디렉토리
  • ezboot/start/ : 초기 부팅용이 들어가는 디렉토리

이 렇게 관리하면 작성되는 Makefile은 3개가 된다. 각각 전체 컴파일을 위한 Makefile, start를 위한 Makefile, main을 위한 Makefile이다. 가장 먼저 전체 컴파일을 위한 Makefile을 보자().

ezboot/Makefile

이 Makefile의 각 문장을 살펴보면 의 (1) 문장은 하부 디렉토리의 Makefile을 처리할 때 설정된 변수 값들이 전달되게 하는 문장이다. (2) 문장은 DIRS 변수에 디렉토리 목록이 지정되도록 해주는 문장이다. 이렇게 지정된 DIRS 변수는 (6) 문장에 의해서 디렉토리를 탐색하는 데 사용되고 지정된 디렉토리에 존재하는 Makefile들을 처리한다. (3) 문장에 의해서 start 프로그램의 링크 옵션과 링크 스크립트 파일을 결정한다. 여기서는 start/start-ld-script가 start 프로그램의 링크 옵션이 된다.

)4) 문장은 C 언어를 처리하기 위한 링크 옵션이다. start 옵션은 지난 호에서 설명한 것과 동일하지만 C 언어에서는 옵션이 조금 다르다. 먼저 기억해야 할 것은 C로 작성하는 부트로더의 main 프로그램은 리눅스 운영체제에서 동작하는 C 프로그램과는 다르다는 것이다. 리눅스에서 동작하는 C 프로그램은 암묵적으로 공유 라이브러리와 표준 헤더 파일을 참조하는 것을 전제 조건으로 작성되며, 이 라이브러리들은 운영체제에 영향을 받는다. 그러나 부트로더는 운영체제 위에서 동작하는 것이 아니고 스스로가 아주 작은 운영체제이기 때문에 이런 표준 라이브러리와 헤더 파일을 사용하면 안 된다. 더욱이 C 프로그램은 스타트업 코드를 가지고 있는데, 이 코드 역시 운영체제의 로더 프로그램에 영향을 받는다.

부트로더는 로더 프로그램에 의해 구동되는 것이 아니라 바이너리 파일을 직접 플래시 롬에 기록하는 방식이기 때문에 일반적인 C 프로그램처럼 스타트업 코드에 의해서 호출되는 main 함수를 가지는 구조로 구성되면 안 되는 것이다. 이런 이유로 부트로더를 위한 옵션은 달라져야 하는데 이지부트의 main을 처리하기 위한 옵션은 다음과 같다.

  • -static : 공유 라이브러리를 포함하지 않고 모든 라이브러리는 실행 코드에 포함한다.
  • -nostdlib : 표준 라이브러리를 사용하지 않는다.
  • -nostartfiles : 스타트업 코드를 포함하지 않는다.
  • -nodefaultlibs : 디폴트 라이브러리를 사용하지 않는다.

이 렇게 C에 관련된 옵션을 설정하고 나서 의 (5) 문장에 의해서 최종적으로 만들어질 부트 이미지의 이름을 결정한다. 앞에서 이지부트 이미지는 두 개의 프로그램이 합쳐져서 만들어진다고 설명했는데, 이 과정을 좀더 자세하게 알아보자.

두 개의 프로그램 소스가 포함된 각각의 디렉토리, 즉 start/와 main/에 있는 Makefile에 의해 두 개의 프로그램 소스가 컴파일되고 링크되면 최종적으로 start/start_org와 main/main_org가 만들어진다. 두 프로그램 중에서 첫 번째 이미지는 2KB 이하의 크기를 갖는다. 그래서 강제로 start/start_org를 2KB 크기로 맞추기 위해 (7) 문장을 수행한다(지면 관계상 dd에 대해서는 설명하지 않겠다. 여러 가지로 이용 가능한 매우 유용한 프로그램이니 펌웨어를 작성하거나 임베디드 리눅스를 하려면 필히 익히기를 당부한다).

start 프로그램을 2KB로 맞추는 이유는 나중에 부트로더 이미지가 롬에서 램으로 복사되었을 때 쉽게 main의 시작 위치로 갈 수 있기 때문이다. 이렇게 2KB로 맞추어진 start 프로그램 뒤에 main 프로그램을 붙이는 것이 (8) 문장이다. 이 문장이 수행되면 image/ ezboot.x5라는 파일이 생긴다.

(9) 문장은 관리를 편리하게 하기 위한 문장이다. 보통 부트로더가 컴파일된 후 이미지가 생기면 tftp 프로토콜을 이용해 그 이미지를 타겟 보드에 다운로드하게 된다. 이것을 매번 tftp 서비스 디렉토리에 복사하면 귀찮기 때문에 이 문장은 컴파일이 정상적으로 수행되면 생성된 부트로더 이미지를 tftp 서비스 디렉토리에 자동으로 복사해 주는 것이다.

(10) 문장은 모든 내용을 다시 컴파일할 때나 소스만 다른 쪽에 옮길 경우를 위해서 컴파일 과정에서 생성된 모든 파일을 지우는 것이다. make clean이라는 명령으로 동작한다. (11) 문장은 디렉토리에 있는 소스간 의존성에 관련된 내용을 Makefile에 기록한다. 이것은 하나의 파일을 수정했을 때, 특히 헤더 파일을 수정했을 때 연관된 소스들이 자동으로 재컴파일 대상이 되도록 하기 위한 것이다. make dep이라는 명령으로 동작한다. 이 Makefile을 사용해서 컴파일하는 경우는 두 가지로 나누는데, 가장 처음이라면 다음과 같이 수행한다.

make dep; make clean; make all

이 런 과정을 거친 뒤에 특정 소스만 수정해서 컴파일할 때는 그냥 make만 치면 된다. 이제 전체를 컴파일하는 Makefile을 보았으므로 실제 소스를 컴파일하는 start/Makefile과 main/Makefile을 살펴보자(, ).

start/Makefile

main/Makefile

start 프로그램을 위한 Makefile은 이전 강좌에 사용한 것과 유사하다. main 프로그램을 위한 Makefile도 마찬가지로 거의 유사하다. 단지 main 프로그램을 위한 Makefile은 C 언어를 컴파일하기 위한 구조로 구성된다는 차이만 있다(Makefile에 대해서는 더이상 언급하지 않겠다. 이전 연재의 설명으로도 이 Makefile에 대한 이해가 가능할 것으로 생각되기 때문이다).

한 가지 주의할 점은 각각의 디렉토리에서 make 명령은 수행하지 말아야 한다는 것이다. 기본 설정 변수들이 상위 디렉토리의 Makefile에서 설정한 값을 사용하기 때문에 하부 디렉토리에서 직접 make 명령을 사용하면 오류가 발생한다.

링크 스크립트 재정리

실행 파일을 만들기 위한 링크 스크립트들은 이전 연재에서 설명한 것과 유사하다. 차이가 있다면 시작 주소 값이 다른 정도이다.

링 크 스크립트를 보면 start 프로그램은 의 (1) 문장에 의해서 시작 주소가 플래시 롬이 위치한 번지부터 프로그램의 코드가 할당된다. 프로그램 진입 라벨은 (2) 문장에 의해 결정된다. main 프로그램은 의 (3) 문장에 의해서 시작 주소가 SDRAM의 뒤쪽 번지부터 프로그램의 코드가 할당된다. 프로그램의 진입 라벨은 (4)에 의해서 결정된다.

start/start-id-script

main/main-id-script

ENTRY 라는 명령은 실제로 프로그램을 코딩하는 입장에서는 실제 이동 위치에 대한 번지 값을 주고 이동하기 때문에 무의미하지만 컴파일 경고를 막기 위해서 사용된다. 이렇게 기본적인 프로그램을 컴파일하기 위한 환경 파일이 만들어졌다면 실제로 동작하는 부트로더를 만들어야 한다. 가장 먼저 만드는 것은 리셋이 발생한 후 수행되는 초기화 코드, 즉 start 프로그램이다. 이 프로그램이 이번 강좌에서 중점적으로 다룰 소스이다.

start 프로그램의 구현

start 프로그램은 지난 호에 제작한 보드 살리기용 프로그램과 매우 다르게 구성된다. PXA255 프로세서가 제대로 동작하기 위한 설정도 해야 하고 주변 메모리나 컨트롤러를 사용하기 위해서 각 컨트롤러의 레지스터 설정도 해야 하기 때문이다. 이지부트에서 start 프로그램이 하는 것은 다음과 같다.

  • 프로세서의 슈퍼 바이저 모드 진입 및 인터럽트 금지
  • 프로세서 속도 설정
  • 프로세서 명령 캐시 설정
  • GPIO의 설정
  • 메모리 컨트롤러 설정 및 시험
  • 부트로더 이미지 복사
  • 스택 설정
  • main 프로그램으로 이동

start 의 메인 소스는 start.S이다. 이것은 순수하게 어셈블러로만 구현되고 있다. 이 프로그램이 동작하는 시점에는 스택이 없기 때문에 방법이 없다. 우선 start의 메인 소스를 살펴보고 각각에 대하여 분석해 보자

프로세서 샘플 소스의 활용

대부분의 프로그래머는 부트로더를 제작할 때 초기 프로세서 설정 부분은 해당 프로세서의 동작 방식을 정확히 이해하고 작성하기보다 프로세서 제작사에서 제공하는 테스트 프로그램 소스를 분석해서 적용한다(인텔의 경우 자사의 PXA255 프로세서에 대해 ‘러벅 보드’라는 개발용 보드에 ‘엔젤’이라는 부트로더를 제공한다). 따라서 초기 프로세서의 설정 조건에 대한 부분은 이것만한 참조 소스가 없다. 단지 흠이라면 모든 조건을 시험할 수 있게 만들어진 프로그램이기 때문에 소스가 복잡하다는 점이다.

이지부트 역시 엔젤을 참조해서 만들어진 프로그램이다. 그래서 프로세서 초기화 부분은 거의 그대로 참조했다. 초기 설정 부분을 엔젤처럼 蒡뵉瞞?하는지는 매뉴얼에 어느 정도 나와 있지만 실제로는 별로 도움이 되지 않는다. 엔젤과 다르게 설정하면 보드는 동작하지 않기 때문이다. 그래서 대부분의 경우 그대로 참조하게 된다.

엔젤 소스를 참고하기는 했지만 필자가 이지부트를 만들 때 주로 참고했던 소스는 u-Boot라는 부트로더 소스이다. 이 부트로더 소스에서는 PXA255 초기화 부분이 무척 단순하다. 만약 관심 있는 독자라면 소스포지(http://sourceforge.net) 사이트를 방문해서 u-Boot를 찾아보기 바란다.

리셋과 인터럽트 핸들러 처리

본격적으로 설명에 들어가기에 앞서 이지부트의 세 가지 단점을 먼저 말해야 할 것 같다. 이지부트가 가지는 단점 중 첫 번째는 인터럽트 핸들러를 제공하지 않는다는 것이다. 두 번째 단점은 MMU를 처리하지 않는다는 것, 그리고 세 번째 단점은 전원 관리에 따른 리셋 부분을 처리하지 않는다는 것이다.

이지부트가 동작하는 EZ-X5는 휴대폰과 같이 전원에 민감한 기기를 위한 것이 아니고 FA나 일반 컨트롤러의 개념으로 사용하도록 고안된 보드이다. 많은 분들이 학습용으로 구매하는 것은 아마도 필자가 다니는 회사의 문서와 입소문 때문인 것으로 보인다. 이런 EZ-X5에 동작하고, 주 목적이 리눅스 커널을 로딩하는 기능이기 때문에 사용되지 않는 기능은 과감하게 제거했다. 필자와 부트로더를 만든 ‘푸지’라는 별명을 가진 개발팀장의 게으름도 큰 몫을 한 것 역시 부인하기는 어렵다. 그래서 앞에서 단점으로 얘기한 내용을 처리하지 않았다.

이지부트는 리셋을 제외한 인터럽트가 발생하면 에러 핸들링 루틴이 수행된다. 에러 핸들링은 두 가지로 나누어 처리된다. 첫 번째는 data_abort 인터럽트에 대한 에러 처리, 즉 프로세서가 주변의 메모리에서 데이터를 가져올 때 실패할 경우인데, 이는 SDRAM을 초기화한 이후에 발생한다. 스테틱 메모리 계열인 플래시 롬에서 데이터를 가져올 때는 실패에 대한 처리가 불가능하다. 이에 반해 SDRAM은 데이터의 이상 유무를 SDRAM 제어 컨트롤러가 체크하기 때문에 메모리 설정에 문제가 있거나 하드웨어 자체에 문제가 있으면 data_abort 인터럽트가 발생한다. 이런 이유로 data_abort 인터럽트는 따로 에러 핸들링을 한다. 이 부분을 따로 처리하는 이유는 보드가 동작할 때 메모리 설정 부분이 가장 문제가 되기 때문이다. 이런 에러를 처리하는 data_abort 인터럽트를 처리하기 위한 문장이 의 (16)이다.

두 번째 에러 처리 인터럽트는 data_abort를 제외한 인터럽트 핸들러이다. 이지부트는 처음에 외부 인터럽트를 금지하고 시작하기 때문에 발생할 수 있는 인터럽트는 인터럽트 금지에 영향을 받지 않는 인터럽트들이다. 이런 인터럽트로는 정상적이지 않은 명령 수행에 관한 인터럽트와 명령 프리패치 에러에 대한 인터럽트가 있다. 이에 대한 인터럽트 핸들러의 에러 처리는 (17) 문장이 담당한다.

두 개의 인터럽트 처리의 차이는 표시되는 LED의 점멸 위치이다. 이 값은 헤더 파일인 ez_x5.h에 선언되어 있는데, 3개의 DEBUG LED를 조합해서 이진 숫자에 대해 표시하고 있다. 초기의 디버깅은 이 LED의 상태를 보고 원인을 파악하게 된다.

  • define DEBUG_START 1 // 디버깅 초기 표시

  • define DEBUG_READY_MEMTEST 2 // 메모리 테스트 준비 표시

  • define DEBUG_MEM_OK 3 // 메모리 정상 표시

  • define DEBUG_MEM_ERROR 4 // 메모리 에러 표시

  • define DEBUG_JUMP_C 5 // C로 점프하기 전의 표시

  • define DEBUG_DATA_ABORT 6 // 데이터 에러 표시

  • define DEBUG_OTHER_EXCEPT 7 // 기타 에러 표시

리셋이 발생한 후 가장 먼저 처리하는 것은 슈퍼 바이저 모드로 프로세서 모드를 전환하는 것이고, 인터럽트를 프로세서 레벨에서 금지시키는 것이다. 특히 인터럽트 컨트롤러를 설정해서 외부 인터럽트를 금지시킬 수 있지만 모든 인터럽트를 금지할 수 없으므로 CPSR 레지스터의 인터럽트 금지 플러그를 활성화해 금지시킨다. 이를 처리하는 문장이 의 (5)이다. CPSR 레지스터는 와 같은 형식으로 정의되어 있다.

start/start.S

Program Status Register Format

프로세서 속도와 명령 캐시 설정

프로세서의 속도를 설정하는 부분은 조심해야 하는데, 먼저 PXA255의 매뉴얼을 보면 조합 가능한 상태를 볼 수 있다. 은 이에 해당하는 조합 가능한 표이다. 에서 이지부트는 기본적으로 사각표의 값으로 설정된다. 설정을 결정하는 값은 ez_x5.h에 다음과 같이 선언돼 있다.

// PXBus=196 MEM=99.5 SDRAM=99.5
#define PXA_CCCR_CPU_398M (CCCR_BF_L_X27 | CCCR_BF_M_X4 | CCCR_BF_N_RUN_X10 )
#define CPU_SPEED PXA_CCCR_CPU_398M

프로세서의 속도 설정 조합

이 값들은 의 (6) 문장에서 이용하여 프로세서 클럭을 설정한다. 이 값들은 과 같이 값을 대입하는데, 즉 L 값은 27, M 값은 4, 그리고 RUN 모드는 1.0으로 동작시키는 조건을 표현한 것이다. 선언된 각각의 값은 공급되는 클럭에 대한 배수이다. 그러므로 이 선언대로 설정할 경우 동작되는 속도는 CPU에 공급되는 클럭인 3.6864MHz을 가정할 때 주어진 값을 계산하면 3.6864×27×4=398.1312가 나온다. 즉 398.1312MHz로 CPU가 동작하게 되는 것이다.

PXA255 는 오버클럭킹을 시도해도 동작하므로 이 표를 적당히 조절하면 500MHz 이상으로도 동작하게 할 수도 있다. 단 오버클럭을 할 경우엔 발열이 많으므로 온도에 대한 대책을 세워주는 것이 좋다. 이렇게 클럭을 설정한 이후에는 터보 모드를 활성화시키는데, 이것은 의 (6)-1 문장 루틴이 수행한다. 이 문장은 가급적 그대로 사용하는 것이 좋다.

클 럭이 설정되었다면 프로세서 동작을 빠르게 하기 위해서 명령 캐시를 동작시켜야 한다. 명령 캐시를 동작시키는 것과 동작시키지 않는 것의 차이가 크기 때문에 명령 처리에 상당한 영향을 미치므로 필히 동작시켜야 한다. 명령 캐시를 동작시키는 것이 의 (7) 문장이며, 이 문장 역시 그대로 사용하여야 한다. 명령 캐시는 프로세서에 연결된 코프로세서를 제어하는 것인데, 코프로세서에 제어를 하면 항상 의 (3) 문장의 매크로를 수행하여야 한다.

PXA255 에는 명령 캐시 이외에 데이터 캐시도 있다. 하지만 대부분의 부트로더는 데이터 캐시를 활성화하지 않는다. 이유는 I/O 영역을 접근할 때 데이터 캐시가 있으면 잘못된 처리가 발생할 수 있기 때문이다. 또한 SDRAM을 검사하기 위해서도 캐시가 동작되면 항상 정상적인 결과만 되돌릴 수 있다. 그러나 부트로더가 MMU를 활성화시킨다면 특정 영역의 데이터 캐시는 동작하지 않게 할 수 있다. 이런 경우라면 데이터 캐시를 사용해도 문제는 없다. 그러나 이지부트는 MMU를 처리하지 않으므로 데이터 캐시는 동작시키지 않아야 한다.

프로세서의 GPIO 설정

PXA255 의 특징은 대부분의 핀들이 중복된 기능을 가지고 있다는 것이다. 핀의 중복된 기능 중 하나는 GPIO의 기능이고, 또 다른 하나는 다른 컨트롤러의 기능이다. 그러므로 사용하지 않는 컨트롤러와 연결된 핀들은 GPIO로 전환이 가능하므로 사용 목적에 따라서 프로세서의 핀들을 효율적으로 사용할 수 있다.

하 지만 이런 장점이 있는 반면 nCS와 같은 칩 선택 핀(chip-select-pin)도 GPIO로 사용 가능하기 때문에 다른 프로세서와 달리 메모리 컨트롤러를 설정하기 이전에 반드시 GPIO를 먼저 설정해야 한다. 다른 프로세서는 메모리 컨트롤러 설정이 GPIO의 설정에 큰 영향은 없는데, 유독 PXA255는 메모리 컨트롤러 설정 이전에 GPIO에 대한 설정을 하지 않으면 메모리 컨트롤러의 설정이 적용되지 않는다(이는 필자가 PXA255를 위한 부트로더를 만들 때 상당히 고생했던 부분이다. 항상 매뉴얼을 찬찬히 읽는 버릇을 가져야 한다는 뼈아픈 교훈도 얻었다).

GPIO를 담당하는 소스는 이지부트에서 start/gpio.S이다. 이 소스 중 초기화와 관련된 핵심 내용은 과 같다.

start/start.S

소스를 자세하게 설명하기에는 지면이 부족하므로 중요한 사항을 설명하면 설정 순서는 에 열거된 순서를 지켜주어야 한다는 것이다. 특히 GPIO의 기본 설정이 끝난 후에 PSSR 레지스터를 설정해야 한다. 그 외에 하는 것은 디버깅을 위한 LED를 설정하는 것으로, 이는 보드별로 달라질 수 있는 부분이므로 그리 중요하지 않다.

메모리 컨트롤러 설정/시험

32 비트 원칩 프로세서의 부트로더를 만들면서 가장 어려운 것이 메모리 컨트롤러의 설정이다. 특히 SDRAM을 설정하는 부분은 임베디드 초보자에게는 접근하기가 무척 어렵다(여기서 자세하게 설명하는 것 역시 힘들다. 기본적으로 SDRAM에 대한 이해가 선행되어야 하기 때문이다. 필자 역시 이 분야는 전문가가 아니고 필자와 같이 근무하는 푸지 팀장에게 많은 부분을 의존하고 있다).

따라서 SDRAM 컨트롤러의 동작 방식에 대한 설명보다 PXA255의 메모리 컨트롤러에 대한 것을 위주로 설명하겠다. 그나마 다행인 것은 국내에서 사용되는 대부분의 SDRAM이 삼성과 하이닉스에서 나온 것이므로 다른 사람이 만든 설정 값을 그대로 이용해도 큰 무리가 없다는 점이다. 외국에서도 램은 대부분 국내 것을 사용하므로 마찬가지다. 이 자리를 빌어서 우리나라의 기술 발전에 다시 한번 박수를 보낸다.

메모리 컨트롤러는 세 가지로 나눠진다. 스테틱 메모리 컨트롤러, DRAM 계통의 메모리 컨트롤러, PCMCIA 메모리 컨트롤러이다. 여기서는 스테틱 메모리 컨트롤러와 DRAM 계통 중 특히 SDRAM에 대한 것만을 설명하고자 한다.

메모리 컨트롤러를 설정하는 부분은 의 (10) 문장이다. 이 문장은 메모리 설정 루틴을 호출한다. 실제 메모리 설정 부분은 start/memory.S 소스에 존재한다. 이 소스에서 수행하는 것은 메모리 설정과 메모리 시험이다. 지면 관계상 소스는 설명하지 않고 기본적인 개념만 소개하겠다.

스테틱 메모리 컨트롤러

스 테틱 메모리 컨트롤러는 주로 SRAM이나 ROM, 그리고 주변 장치들을 제어하기 위한 버스 타이밍을 설정하는 컨트롤러이다. 특히 ARM은 I/O 버스 제어용 명령이 따로 존재하지 않고 일반 메모리 공간을 접근하는 방식으로 I/O를 사용한다. 그래서 스테틱 메모리 컨트롤러 설정은 PXA255용 부트로더를 제작하는 사람은 정확하게 숙지하고 있어야 한다.

PXA255 는 칩 선택 핀이 총 6개 있다. 각 핀의 이름은 nCS0, nCS1, nCS2, nCS3, nCS4, nCS5이다. 이 핀들을 제어하는 레지스터는 총 3개인데, MSC0, MSC1, MSC2가 있다. 그래서 nCS 두 개를 한 쌍으로 MSC가 관리한다.

nCS0,nCS1 --> MSC0 : 0x4800 0008
nCS2,nCS3 --> MSC1 : 0x4800 000C
nCS4,nCS5 --> MSC2 : 0x4800 0010

내부적인 선언은 각 레지스터가 동일하다. 는 MSC 레지스터의 설정 표이다. 설명되는 내용은 Noburst나 Variable Latency일 경우이다. MSC0인 경우 상위 16과 하위 16이 각각 nCS1, nCS0를 제어한다. MSC1, MSC2 역시 동일하다. 의 각각의 명칭은 의 타이밍도를 보면 이해될 수 있을 것이다.

MSC 레지스터 설정의 타이밍도

스 테틱 메모리 레지스터의 설정 값도 역시 ez_x5.h 헤더 파일에 있다. 은 플래시 메모리와 NAND가 연결되어 있는 nCS0와 nCS1용 MSC0 레지스터의 설정 예이다. 이 소스에서 RDF는 클럭 속도에 영향을 받는다. 이 헤더 파일에서 주로 수정하는 값은 버스 폭을 조정하는 MSC_CSX_RBW 파라미터와 읽기·쓰기 신호의 유지 시간인 MSC_CSX_RDF 값이다.

nCS0와 nCS1용 nCS0 레지스터의 설정 예

SDRAM 메모리 컨트롤러

SDRAM 메모리 컨트롤러는 앞서 언급했듯이 주로 레지스터에 대한 설명을 쓰겠다. SDRAM 값의 설정 역시 ez_x5.h에서 하는데, 주로 수정하는 값은 클럭 속도나 SDRAM의 버스 폭 정도이다. SDRAM에 관련된 레지스터는 다음과 같다. 각 레지스터별 필드의 의미는 , , 로 알 수 있다.

  • MDCNFG : SDRAM Configuration Register [0x4800 0000]
  • MDMRS : SDRAM Mode Register Set Configuration Register [0x4800 0040]
  • MDREFR : SDRAM Refresh Control Register [0x4800 0004]

MDCNFG

MDMRS

MDREFR

메모리 검증 테스트

메모리 설정을 하고 난 이후에는 의 (11) 문장을 수행하여 메모리가 정상인가를 확인한다. 이지부트가 확인하는 메모리 영역은 이지부트의 메인 부분이 올라가는 공간만이다. 그 나머지는 확인하지 않는다. 전체 메모리를 확인하지 않는 이유는 SDRAM의 일부분만 고장나는 경우는 매우 드문 데다 부팅 속도를 빠르게 하기 위해서이다. 결국 신뢰성과 속도의 갈림길에서 속도에 손을 들어준 것이다.

메모리 시험 방식은 32비트 버스 폭으로 0x55555555 값을 쓴 후 바로 0xAAAAAAAA를 쓴다. 이것은 버스를 순간적으로 비트로 바꾸어 테스트함으로써 데이터 버스의 빠른 변화에 문제가 없는가를 확인하는 것이다. 이렇게 0xAAAAAAAA 값을 시험하고자 하는 영역에 해당하는 메모리 전체에 쓴 후에 다시 읽어서 정상인가를 확인한다. 이렇게 시험이 끝나면 다시 거꾸로 시험한다. 즉 0xAAAAAAAA를 먼저 쓴 후 바로 0x55555555를 쓴다. 역시 전체에 기록한 후 다시 읽어서 정상인가를 확인한다.

메모리 시험 방식은 이지부트에서 사용하는 방법 이외에 여러 가지가 있다. 하지만 모두 완벽하게 시험하기는 쉽지 않은데 완벽한 시험에는 시간이 걸리기 때문에 부팅 속도를 위해서 아주 기본적인 버스에 대한 것만 시험하게 된다.

부트로더 이미지 복사

이 렇게 메모리에 대한 검증이 끝나면 (12) 문장을 수행하여 이지부트 메인이 올라갈 공간을 0으로 채우고 플래시에 있는 부트로더 내용을 그대로 램으로 옮긴다. 메모리를 클리어하는 것은 프로그램의 버그가 있을 때 동작에 영향을 줄이기 위한 것이기 때문에 부팅 속도에 문제가 생기면 안 해도 되는 부분이다.

스택 설정

이제 램도 정상적으로 처리 가능하므로 스택을 설정하여 서브루틴을 여러 번 호출할 수 있도록 한다. 스택을 설정하기 위해서 의 (13) 문장이 수행된다. ARM에서는 스택에 데이터를 넣었을 때 스택 포인터의 이동 방향이 정해져 있지 않다. 즉 하위 어드레스 쪽으로 감소할 수도 있고 상위 어드레스 쪽으로 증가할 수도 있다. 그러나 gcc에서 C 함수의 스택은 감소하도록 컴파일된다. main은 C 언어로 작성되므로 스택의 초기 위치는 부트로더 프로그램이 사용하기로 결정된 공간의 마지막 부분에 설정된다. 한 가지 조심할 점은 스택 포인터는 데이터가 넣어진 후 변경되기 때문에 실제 마지막 주소 위치에서 4바이트 크기를 빼주어야 한다는 것이다. 의 (13) 문장 중 빼기 명령이 있는 이유가 바로 이 때문이다.

스택을 설정한 이후에 의 (14) 문장을 보면 0xA0000000에 0을 넣고 있는데 이것은 이지부트인가를 확인하기 위한 일부 처리인데 중요한 의미는 아니므로 신경쓰지 말기 바란다. 이제 마지막으로 복사된 메인 프로그램으로 이동해야 한다. start 프로그램과 main 프로그램은 별개의 프로그램이다. 즉 각각 컴파일되고 링크되는 과정을 거친다. 하나의 이미지는 데이터로써 합쳐져 있을 뿐이다. 이렇게 다른 프로그램일 경우에는 상호 심볼릭 참조가 불가능하므로 두 개의 프로그램 사이에 수행 위치를 변경할 경우에는 절대 주소를 이용해 이동하여야 한다. 그런데 ARM에서는 강제 이동 명령이 따로 존재하지 않는다. 따라서 PC 레지스터에 직접 값을 대입하면 된다. 의 (15) 문장이 메인 프로그램으로 이동하는 것인데 더하는 과정을 둔 것은 메모리 관리를 편하게 하기 위함이다.

다음 연재도 기본 구현!

원 래 부트로더의 기본 구현은 1회 원고 분량으로 쓰려고 계획했었다. 이는 처음 원고를 계획할 때 필자의 착오에 기인한 것이다. 당초 계획과 달리 부트로더의 기본 구현에 한 차례 더 연재를 할애하게 된 점에 대해 독자들의 양해를 구한다. 또 한 가지 더 있는데 처음 계획은 부트로더 소스를 하나씩 분석하려고 계획했으나 지면 관계상 소스를 모두 소개하는 것이 어렵다는 것을 느꼈다. 그래서 원리 구현과 매뉴얼 소개 중심으로 진행한 점 역시 널리 이해해주기 바란다.

마 지막으 로 start.S 소스 부분은 어셈블러로만 작성되고 프로세서의 특성을 먼저 이해해야 하는 부분이 많아서 어렵게 느끼는 독자가 많으리라 생각된다. 부트로더를 자신이 직접 처음부터 작성해야 하는 경우라면 어쩔 수 없지만 공개된 부트로더를 수정해야 하는 독자라면 이번 연재는 그냥 그런 것이려니 하고 넘어가기 바란다. 다음 연재부터는 부담이 적은 C 언어로 설명하기 때문이다.

반응형