초심 프로젝트의 첫번째 주제로 ARM Architecture 를 주제로 새롭게 알게된 것을 정리했다. 교재는 다음과 같다.
주교제 | ARM System Developer`s Guide |
부교제 | ARM System-on-Chip Architecture |
ARM 임베디드 시스템
ARM 은 RISC 이다. ARM 은 프로세서내에 하드웨어 디버그 기술을 포함하고 있다. 따라서 소프트웨어 엔지니어들은 프로세서가 코드를 실행하는 동안 내부에서 어떤 일들이 일어나는지를 살펴볼 수 있다.
RISC 의 특징
RISC 는 하드웨어에 의해 수행되는 명령어들의 복잡성을 줄이는 것을 목표로 하고 있는 데, 그 이유는 하드웨어보다는 소프트웨어에 유연성과 기능성을 제공하는 것이 보다 유리하기 때문이다.
- 명령어의 복잡도를 줄여 성능을 향상시켰고,
- 파이프라인을 사용하여 명령어 처리 속도를 높였으며,
- 코어 옆에 데이터를 저장하기 위한 거대한 레지스터군을 제공하고 있으며,
- 로드-스토어 아키텍처를 사용하고 있다.
RISC 특성외에 몇가지 다른 특징들도 포함하고 있다.
- 전력 소모, 실리콘 면적, 코드 사이즈를 줄이기 위해 여러 사이클에 실행되는 명령어들도 만들었다.
- 명령어의 기능을 향상시키기 위해 배럴 시프터(barrel shifter) 를 추가하였다.
- 코드 집적도를 향상시키기 위해 16 비트 Thumb 명령어 군을 사용하였다.
- 명령어들의 조건부 실행을 가능하게 하여 코드의 집적도와 성능을 향상시켰다.
- 디지털 신호 처리를 위해 DSP 확장 명령어를 포함시켰다.
임베디드 시스템에 포함되어 있는 하드웨어 컴포넌트는 다음과 같다.
- ARM 프로세서 : 칩에 집적되어 있다.
- 주변 장치(Peripheral) : 프로그래머는 메모리에 매핑된 레지스터를 통해 주변 장치를 제어한다.
- 컨트롤러 : 메모리나 인터럽트 처럼, 고급 기능들을 지원하기 위해 사용되는 특별한 유형의 주변 장치
- AMBA 버스 : 프로세서와 주변 장치를 연결해주는 역할을 한다.
- 초기화 코드 : 하드웨어 장치들을 초기화해주는 코드를 말한다.
- 운영체제 : 초기화 과정 다음에 읽혀져 실행될 코드를 말한다. 운영체제는 하드웨어 리소스와 인프라스트럭처를 사용하기 위한 일반적인 프로그래밍 환경을 제공한다.
- 디바이스 드라이버 : 주변 장치와의 표준 인터페이스를 제공한다.
- 애플리케이션 : 임베디드 시스템의 특정 태스크를 수행한다.
용어 정리
배럴 시프터
배럴 시프터는 한 개의 연산으로 데이터 워드 내에 있는 다수의 비트를 이동하거나 회전시킬 수 있는 하드웨어 장치이다. 즉 32 비트 배럴 시프터의 경우라면, 1 사이클 안에 좌측이나 우측으로 최대 32 비트씩 이동시킬 수 있다.
ARM 프로세서 개요
로드 스토어 아키텍처에서 로드 명령어은 메모리에서 코어내의 레지스터로 데이터를 복사하고, 스토어 명령어는 레지스터에서 메모리로 데이터를 복사하는 데 사용된다.
범용 레지스터
레지스터 번호 | 설명 |
r0 ~ r12 | 일반 레지스터 |
r13 | 스택 포인터(sp)로 사용되어 왔으며, 현재 프로세서 모드의 스택 맨 위 주소값을 저장한다 |
r14 | 링크 레지스터(lr)로 불리며, 코어가 서브루틴을 호출할 때마다 그 복귀주소를 저장한다 |
r15 | 프로그램 카운터(pc)로, 프로세서가 읽어들인 다음 명령어의 주소를 저장한다 |
cpsr | 현재의 프로그램 상태 레지스터 |
spsr | 이전에 저장된 프로그램 상태 레지스터 |
프로세서 모드
ARM 에서는 총 6개의 특권 모드(abort, FIQ, IRQ, supervisor, system, undefined)와 하나의 일반모드(user), 즉 전체적으로 7개의 모드가 있다.
모드 | 설명 |
abort | 메모리 액세스가 실패했을 경우 |
FIQ, IRQ | ARM 프로세서에서 사용할 수 있는 2가지의 인터럽트 레벨을 위한 모드 |
supervisor | 프로세서에 리셋이 걸렸을 때에 진입하는 모드로, 일반적으로 운영체제 커널이 동작하는 모드 |
system | user 모드의 특수한 버전으로 cpsr 을 완전히 읽고 쓸 수 있다 |
undefined | 프로세서가 정의되지 않은 명령어나 지원되지 않은 명령어를 만났을 때에 진입하는 모드 |
user | 프로그램과 애플리케이션을 위해 사용하는 모드 |
뱅크 레지스터
레지스터 파일 안에 있는 37개의 레지스터 중에서 20개의 레지스터는 매번 프로세서 모드에 따라 숨겨져 있는데, 이 것을 뱅크 레지스터라고 부른다.
프로세서 모드가 변경되었다면, 새로운 모드의 뱅크 레지스터가 기존의 레지스터를 대체한다. 이때 대체하기전의 기존의 레지스터는 그대로 있으며, 어떤 명령어에 의해서도 영향을 받지 않는다.
프로세서의 각 모드는 해당 익셉션이나 인터럽트를 발생시키는 하드웨어에 의하거나 cpsr 을 직접 제어하여 변경할 수 있다. 익셉션과 인터럽트는 현재 작업을 중단시키고 특정위치로 분기한다.
인터럽트가 모드 변경을 일으켰을 때 어떤일이 일어나는가?
- 코어는 user 모드에서 irq 모드로 변경되고, irq 모드는 프로세서 코어에 인터럽트를 발생시키는 외부 장치로 인해 발생된다.
- 레지스터 r13, r14 가 뱅크된다.
- user 레지스터들은 r13_irq 와 r14_irq 로 대치된다. r14_irq 는 복귀할 주소를 포함하고 있으며, r13_irq 는 IRQ 모드를 위한 스택 포인터가 저장된다.
- irq 모드에서만 보여지는 새로운 레지스터, 즉 이전 모드의 cpsr 을 spsr 에서 저장한다. 참고로 user 모드에서는 spsr 을 액세스 하는 것이 불가능하다.
모드 | 약자 | 특권 기능 | 모드비트 [4:0] |
abort | abt | yes | 10111 |
fast interrupt request | fiq | yes | 10001 |
interrupt request | irq | yes | 10010 |
supervisor | svc | yes | 10011 |
system | sys | yes | 11111 |
undefined | und | yes | 11011 |
user | usr | no | 10000 |
기억해야 할 것은 cpsr 을 직접 제어하여 모드 변경을 한 경우에는 cpsr 이 spsr 에 복사되지 않는다는 것이다. cpsr 의 저장은 익셉션이나 인터럽트가 발생할 때에만 발생된다. 전원이 코어에 공급되면 특권모드인 supervisor 모드에서 시작된다.
프로세서 상태 및 명령서 세트
코어의 상태는 어떤 명령어가 실행될 것인지 결정한다.(ARM, Thumb, Jazelle) Thumb 상태에서는 순수한 16비트 Thumb 명령어만이 실행되고, ARM, Thumb, Jazelle 명령어를 섞어서 코딩할 수 없다.
아래 표는 ARM 과 Thumb 명령어의 특징을 나타내고 있다.
ARM(cpsr T=0) | Thumb(cpsr T=1) | |
명령어 크기 | 32비트 | 16비트 |
코어 명령어 | 58 | 30 |
조건부 실행 | 대부분 가능 | 분기 명령어에서만 가능 |
데이터 처리 명령어 | 배럴 시프터와 ALU의 액세스 가능 | 배럴 시프터 명령어와 ALU 명령어가 분리됨 |
프로그램 상태 레지스터 | 특권 모드에서만 읽고 쓰기 가능 | 직접 액세스 불가 |
레지스터 사용 | 15개의 범용 레지스터 + pc | 8 개의 범용 레지스터 + 7개의 상위 레지스터 + pc |
인터럽트 마스크
특정 인터럽트 소스가 프로세서에게 인터럽트 요청을 할 수 없도록 하는 것이다. cpsr 은 인터럽트 마스크 비트 6번과 7번(F와I)의 2개를 가지고 있는데, I 비트가 1이면 IRQ 가 마스크되며, F 비트가 1이면 FIQ 비트가 마스크 된다.
상태 플래그
cpsr 의 상태 플래그(Q,V,C,Z,N)을 확인해서 현재 프로그램의 상태를 확인할 수 있다.
조건부 실행
조건부 실행이란 코어가 어떤 명령어를 실행할 지의 여부를 제어할 수 있음을 의미한다. 대부분의 명령어들은 코어가 그것을 실행할 지의 여부를 결정할 수 있는 조건 인자를 가지고 있는데, 이것은 상태 플래그의 설정값을 기반으로 한다. 명령어를 실행하기전에 코어는 자신이 가지고 있는 조건 인자와 cpsr 의 상태 플래그를 비교한다. 만약 이것이 일치하면 명령어는 실행되고 그렇지 않으면 명령어는 무시된다. 만약 조건 인자가 없다면 AL(항상 실행)로 설정된다.
파이프 라인
ARM7 의 경우는 총 3단계의 파이프라인을 가지고, ARM9 은 5단계, ARM10 은 6단계의 파이프라인을 가진다.
ARM 파이프라인은 어떤 명령어가 실행 단계로 넘어오기 전까지는 그 명령어를 처리하지 않는다. ARM7 의 경우, 네 번째 명령어가 읽혀질 무렵에서야 비로소 하나의 명령어를 실행한다.
실행 단계에서 pc는 항상 명령어 주소에 8 을 더한 값을 가리킨다. 다시 말하면 pc는 실행되고 있는 명령어의 주소보다 2단계 앞선 주소를 가리키고 있는 것이다. 이것은 pc 를 이용하여 상대 오프셋값을 계산할 때에 매우 중요하며, 전체 파이프라인에 대한 구조적인 특징이기도 하다. Thumb 상태에서는 프로세서의 pc 값이 “명령어 주소 + 4” 라는 사실을 기억한다.
파이프라인의 3 가지 특징
- 분기 명령어로 실행되거나 pc 값을 직접 수정하여 분기하는 경우에는 파이프라인이 깨지게 된다.
- ARM10 은 분기 예측 방식을 사용하는데, 이것은 명령어를 실행하기 전에 가능한 분기를 미리 예측하여 새로운 분기 주소를 로드함으로써 파이프라인이 깨지는 상황을 줄여준다.
- 실행 단계에 있는 명령어는 인터럽트가 발생하더라도 그 과정을 완료한다. 파이프라인 안의 다른 명령어드은 무시되어 벡터 테이블 안에 적절한 엔트리 명령어로 파이프라인을 채우게 된다.
ARM 프로세서에서의 익셉션
익셉션이나 인터럽트가 발생하였을 때, 프로세서는 pc 에 특정 메모리 주소값을 넣는다. 이 주소값은 벡터 테이블이라는 특정주소 영역 안에 있는 값이다.
메모리맵 주소 0x00000000 은 벡터 테이블을 위해 32 비트 워드값들로 예약되어 있다.
Reset vector | 전원이 공급될 때 프로세서에 의해 처음으로 실행되는 명령어의 위치. 이 명령어에서 초기화 코드로 분기한다 |
Undefined Instruction vector | 프로세서가 명령어를 분석할 수 없을 때에 사용된다 |
Software Interrupt vector | SWI 명령어를 실행시켰을 때에 호출됨. SWI 명령어는 운영체제 루틴에서 벗어나고자 할 경우에 주로 사용된다 |
Prefetch Abort vector | 프로세서가 정확한 접근권한 없이 어떤 주소에 명령어를 읽어들이려고 할때 발생. 실제 abort 는 파이프라인의 디코드 단계에서 발생한다 |
Data Abort vector | 명령어가 정확한 접근권한 없이 데이터 메모리를 액세스하려고 시도할 때에 발생한다 |
Interrupt Request vector | 프로세서의 현재 흐름을 중단시키기 위해 외부 하드웨어 장치에 의해 사용된다 |
Fast Interrupt Request vector | 더욱 빠른 응답 시간을 요구하는 하드웨어를 위해 할당되어진다 |
다음은 벡터 테이블의 정보를 표로 나타낸 것이다.
익셉션/인터럽트 | 벡터주소 | 상위 벡터주소 |
reset | 0x00000000 | 0xffff0000 |
undefined instruction | 0x00000004 | 0xffff0004 |
software instruction | 0x00000008 | 0xffff0008 |
prefetch abort | 0x0000000c | 0xffff000c |
data abort | 0x00000010 | 0xffff0010 |
reserved | 0x00000014 | 0xffff0014 |
interrupt request | 0x00000018 | 0xffff0018 |
fast interrupt request | 0x0000001c | 0xffff001c |
캐시 메모리와 코프로세서
ARM 코어 주변에 확장시킬 수 있는 하드웨어 장치에는 캐시 및 TCM, 메모리 관리 장치(MMU), 코프로세서 인터페이스의 3가지가 있다.
캐시와 TCM
캐시는 주메모리와 코어사이의 위치한 빠른 메모리 블록으로, 메모리 소자로 부터 보다 효율적인 패치(fetch)를 가능케 해준다.
ARM 은 두 종류의 캐시를 가지고 있다.
- 폰노이만 형태의 코어에 부착되어 있는 형태로, 명령어와 데이터를 하나의 통일된 캐시에 함께 저장한다.
- 하버드 형태의 코어에 부탁되어 있는 형태로, 명령어와 데이터를 위한 캐시가 분리되어 있다.
캐시는 전반적인 성능을 향상시켜주는 반면 예측성을 떨어뜨린다. 실시간 시스템에서 명령어나 데이터를 읽거나 쓰는 데 걸리는 시간이 예측 가능해야 된다. 이를 위해 TPM(Tightly Coupled Memory) 라는 일종의 메모리를 이용하여 구현할 수 있다.
- TCM 이란 코어 가까이에 위치한 빠른 SRAM 으로, 명령어나 데이터를 읽어들이는 데 필요한 클럭 사이클을 보장해준다.
캐시와 TCM 을 접목함으로써 ARM 프로세서는 성능을 향상시키고 예측 가능한 실시간 응답성을 보장한다.
메모리 관리
ARM 코어는 3가지 유형의 메모리 관리 하드웨어를 지원한다.
- 일반 관리 장치(no extensions providing on protection) : 보호 기능을 제공하지 않고, 간단한 임베디드 시스템에 주로 사용된다.
- 제한적인 메모리 보호(MPU) : 메모리 보호는 필요하지만, 복잡한 메모리맵을 사용하지 않는 시스템에서 사용된다.
- 완전한 메모리 보호(MMU) : 메모리를 세밀하게 제어할 수 있는 변환 테이블 세트를 사용한다. 이 테이블은 주메모리에 저장되어 접근권한과 가상 메모리 매핑 기능을 제공한다.
코프로세서
코프로세서는 ARM 프로세서에 부착하여 사용하는 것으로, 명령어 세트를 확장시키거나 값을 설정할 수 있는 레지스터를 제공하여 코어의 처리기능을 확장시켜 준다. 이 새로운 명령어들은 ARM 파이프라인의 디코드 단계에서 처리된다. 만약 디코드 단계에서 코프로세서 명령어를 발견한다면, 이 명령어는 관련 코프로세서로 넘겨진다. 하지만 코프로세서가 존재하지 않거나, 이 명령어를 인식하지 못할 경우에는 undefined instruction 익셉션을 발생한다.
32 비트 ARM 명령어
ARM 은 그 버전에 따라서 지원하는 명령어들이 다르다. 하지만 새로운 버전은 보통 기존 명령어에 새로운 명령어를 추가하는 방식으로, 하위 버전과의 호환성을 보장하고 있다.
데이터 처리 명령어
레지스터 안에서 데이터를 조작하는데 사용된다.
데이터 이동 명령어
가장 간단한 ARM 명령어로서 초기값을 설정하거나 레지스터간에 데이터를 이동하기 위해 사용된다.
명령어 | 설명 |
MOV | 32 비트값을 레지스터로 복사 |
MVN | 32 비트값의 NOT 을 레지스터로 복사 |
r5 = 5 r7 = 7 MOV r7 , r5 r5 = 5 r7 = 5
배럴 시프터
데이터 처리 명령어는 산술 연산 장치(ALU) 에서 처리된다. ARM 명령어의 강력한 특징은 ALU 로 입력되기 전에 배럴 시프터에 의해 2진 단위로 좌우 시프트가 가능하다는 점이다.
r5 = 5 r7 = 7 MOV r7, r5, LSL #2 ; r5 << 2 5 * 4 r5 = 5 r7 = 20
명령어 | 설명 | 결과 | |
LSL | 왼쪽으로 논리 시프트 | x « y | |
LSR | 오른쪽으로 논리 시프트 | (unsigned)x » y | |
ASR | 오른쪽으로 산술 시프트 | (signed)x » y | |
ROR | 오른쪽으로 로테이트 | ((unsigned)x » y) | (x « (32 - y) |
RRX | 오른쪽으로 확장 로테이트 | (c 플래그 « 31 | (unsigned)x » 1) |
다음은 r1 을 1만큼 왼쪽으로 시프트하는 것을 보여주고 있다. 명령어에 S 가 붙어 있으므로 cpsr 안의 C 플래그가 업데이트된다.
cpsr = nzcvqiFt_USER r0 = 0x00000000 r1 = 0x80000004 MOVS r0, r1, LSL #1 cpsr = nzCvqiFt_USER ; 1만큼 왼쪽으로 시프트 되면서 최상위 비트가 캐리지 리턴 됨으로서 cpsr 의 C 플래그가 1로 세팅된다 r0 = 0x00000008 r1 = 0x80000004 ; r1 의 값은 변화가 없다.
산술 명령어
32 비트 signed/unsigned 값의 덧셈과 뺄셈을 구현하기 위해 사용된다.
명령어 | 설명 |
ADC | 캐리를 고려한 32 비트값의 덧셈 |
ADD | 32 비트값의 덧셈 |
RSB | 32 비트값의 뺄셈(반전) |
RSC | 캐리를 고려한 32 비트값의 뺄셈(반전) |
SBC | 캐리를 고려한 32 비트값의 뺄셈 |
SUB | 32 비트값의 뺄셈 |
다음은 간단한 뺄셈 연산을 보여주는 예제이다.
r0 = 0x00000000 r1 = 0x00000002 r2 = 0x00000001 SUB r0, r1, r2 r0 = 0x00000001
RSB 명령어는 상수값 #0 에서 r1 을 뺀 후, 그 결과를 r0 에 저장한다. 다음은 어떤 값을 음수로 만들기 위해 사용하는 방법이다.
r0 = 0x00000000 r1 = 0x00000077 RSB r0, r1, #0 ; Rd = 0x0 - r1 r0 = -r1 = 0xffffff89
SUBS 명령어는 루프문에서 카운터를 감소시킬 때에 사용하면 유용하다. 다음은 r1 에 저장되어 있는 값 1 에서 상수값 1을 뺀 후, 그 결과값 0 을 r1에 저장한다. cpsr 안의 ZC 는 1로 업데이트 된다.
cpsr = nzcvqiFt_USER r1 = 0x00000001 SUBS r1, r1, #1 cpsr = nZCvqiFt_USER r1 = 0x00000000
산술 연산에서의 배럴 시프터 사용
산술 연산이나 논리 연산에서 두 번째 오퍼랜드의 시프트가 가능하다는것은 ARM 명령어 세트에서 매우 강력한 특징이다.
r0 = 0x00000000 r1 = 0x00000005 ADD r0, r1, r1, LSL #1 r0 = 0x0000000f r1 = 0x00000005
논리 명령어
논리 명령어는 2개의 소스 레지스터에 비트 단위로 논리 연산을 수행한다.
명령어 | 설명 |
AND | 32 비트 AND 논리 연산 |
ORR | 32 비트 OR 논리 연산 |
EOR | 32 비트 XOR 논리 연산 |
BIC | 비트 클리어(AND NOT) 논리 연산 |
다음은 비트 클리어를 수행하는 예제이다.
r0 = 0000 r1 = 1111 r2 = 0101 BIC r0, r1, r2 r0 = 1010
레지스터 r2 는 비트 패턴을 포함하고 있는데, r2 에 저장되어 있는 1의 값에 대응되는 r1의 비트들이 0으로 클리어된다. 이 명령어는 상태비트를 0으로 설정할 때에 특히 유용하며 cpsr 에서 인터럽트 마스크를 변경하기 위해 자주 사용된다.
비교 명령어
32 비트값을 가진 레지스터를 비교하고 테스트하기 위해 사용된다. 이 명령어는 결과에 따라 cpsr 플래그 비트를 업데이트하며 다른 레지스터에는 영향을 미치지 않는다. 해당 비트들을 세트한 다음, 조건부 실행을 사용하여 프로그램의 흐름을 변경하는 데 사용된다.
명령어 | 설명 |
CMN | 음수 비교 |
CMP | 양수 비교 |
TEQ | 두 32 비트값이 같은지를 비교 |
TST | 32 비트값의 테스트 비트 |
다음은 CMP 명령어를 이용한 예제이다.
cpsr = nzcvqiFt_USER r0 = 4 r9 = 4 CMP r0, r9 cpsr = nZcvqiFt_USER
비교 명령어는 비교되는 레지스터에는 영향을 끼치지 않고, 오직 cpsr 의 상태 비트만을 업데이트 한다는 것을 반드시 이해하기 바란다.
곱셈 명령어
64 비트 곱셈 명령어는 64 비트를 표현하는 2개의 레지스터를 이용하여 곱셈을 한다. 결과는 하나의 결과 레지스터에 저장하거나 2개의 레지스터를 이용하여 저장한다.
명령어 | 설명 |
MLA | 32 비트 곱셈-덧셈 명령어 |
MUL | 32 비트 곱셈 명령어 |
SMLAL | 64 비트 signed 곱셈-덧셈 명령어 |
SMULL | 64 비트 signed 곱셈 명령어 |
UMLAL | 64 비트 unsigned 곱셈-덧셈 명령어 |
UMULL | 64 비트 unsigned 곱셈 명령어 |
다음은 레지스터 r1 과 r2 를 곱하여 그 결과를 레지스터 r0 에 저장하는 간단한 곱셈 명령어를 보여주고 있다.
r0 = 0x00000000 r1 = 0x00000002 r2 = 0x00000002 MUL r0, r1, r2 ; r0 = r1 * r2 r0 = 0x00000004 r1 = 0x00000002 r2 = 0x00000002
다음은 64 비트 곱셈의 예제로서 레지스터 r2 와 r3 를 곱하여 그 결과를 레지스터 r0 와 r1 에 저장한다.
r0 = 0x00000000 r1 = 0x00000000 r2 = 0xf0000002 r3 = 0x00000002 UMULL r0, r1, r2, r3 r0 = 0xe0000004 ; = RdLO r1 = 0x00000001 ; = RdHi
결과는 32 비트 레지스터에 저장하기에는 너무 크기 때문에, RdLo 와 RdHi 라는 레이블이 붙은 2개의 레지스터에 저장된다. RdLo 에는 64 비트의 결과 중 하위 32 비트의 값이 저장되며, RdHi 에는 64 비트의 값 중 상위 32 비트의 값이 저장된다.
분기 명령어
실행의 흐름을 변경하거나 어떤 루틴을 호출하는 데 사용한다. 프로그램 카운터 pc 가 새로운 주소를 가리키도록 함으로써 실행의 흐름을 바꾸어 준다.
명령어 | 설명 |
B | 분기 |
BL | 서브루틴 호출 분기 명령어 |
BX | ARM/Thumb 모드 전환 분기 명령어 |
BLX | ARM/Thumb 모드 전환 및 서브루틴 호출 분기 명령어 |
다음은 분기 명령어를 이용한 예제이다.
B forward ADD r1, r2, #4 ADD r0, r6, #2 forward ; label SUB r1, r2, #4 backward ; label 무한 루프 ADD r1, r2, #4 SUB r1, r2, #4 ADD r4, r6, r7 B backward
대부분의 어셈블러는 레이블을 사용하여 분기 명령어의 자세한 부분을 감춘다.
링크값을 갖는 분기인 BL 명령어는 B 명령어와 유사하지만 링크 레지스터 lr 에 복귀할 주소를 저장해둔다. 이 명령어는 서브루틴 호출을 수행하는 데 사용된다. 서브루틴에서 복귀하려면 pc 에 링크 레지스터의 값을 넣어주어야 한다.
BL subroutine ; 서브루틴으로 분기 CMP r1, #5 ; r1 과 r5 를 비교 MOVEQ r1, #0 ; (r1 == 5) 이면 r1 = 0 ... subroutine <서브루틴 코드> MOV pc, lr ; pc = lr 을 저장하여 복귀
로드-스토어 명령어
메모리와 프로세서 레지스터 사이에서 데이터를 전송해준다.
단일-레지스터 전송 명령어
이 명령어는 메모리에서 레지스터, 또는 레지스터에서 메모리로 하나의 데이터를 전송하는 데 사용된다.
명령어 | 설명 |
LDR | 메모리에서 레지스터로 한 워드를 읽어들임 |
STR | 레지스터에서 메모리로 바이트나 워드를 저장 |
LDRB | 메모리에서 레지스터로 바이트를 읽어들임 |
STRB | 레지스터에서 메모리로 바이트를 저장 |
LDRH | 메모리에서 레지스터로 하프워드를 읽어들임 |
STRH | 레지스터에서 메모리로 하프워드를 저장 |
LDRSB | 메모리에서 레지스터로 signed 바이트를 읽어들임 |
LDRSH | 메모리에서 레지스터로 signed 하프워드를 읽어들임 |
; 레지스터 r1 이 가리키는 메모리 주소의 내용을 레지스터 r0 에 로드한다. LDR r0, [r1] ; LDR r0, [r1, #0] ; 레지스터 r0 의 내용을 레지스터 r1 이 가리키는 메모리 주소에 저장한다. STR r0, [r1] ; STR r0, [r1, #0]
단일-레지스터 로드-스토어 주소지정방식
ARM 명령어 세트는 메모리의 주소를 계산하기 위해 다양한 모드를 제공한다.
자동인덱스 | 베이스 레지스터로부터의 주소에서 주소 오프셋만큼을 계산한 다음에 새로운 주소값을 가지고 주소 베이스 레지스터를 업데이트한다 |
프리인텍스 | 자동인덱스와 동일하지만, 주소 베이스 레지스터를 업데이트하지는 않는다 |
포스트인덱스 | 주소가 사용된 후 주소 베이스 레지스터를 단지 업데이트만 한다 |
다음은 각각 다른 주소지정방식을 사용한 LDR 명령어의 예이다.
명령어 r0 = r1 += - 자동인덱스 : LDR r0,[r1, #0x4]! Mem32[r1+0x4] 0x4 - 프리인덱스 : LDR r0,[r1, #0x4] Mem32[r1+0x4] 업데이트 안 됨 - 포스트인덱스 : LDR r0,[[r1]],#0x4 Mem32[r1] 0x4
다중-레지스터 전송 명령어
하나의 명령어로 메모리와 프로세서 사이에서 여러 개의 레지스터를 전송할 수 있다. 다중-레지스터 전송 명령어는 메모리에서 여러 블록의 데이터를 전송하거나 문맥과 스택을 저장하고 복구하는 데 단일-전송 레지스터 전송보다 효과적이다.
또한 일반적으로 ARM 은 이 명령어가 실행되고 있는 동안에 다른 명령어가 실행되지 못하게 하기 때문에 인터럽트 지연을 증대시킬 수 있다. 만약 인터럽트가 발생한다면, 다중-레지스터 전송 명령어가 완료될 때까지는 아무런 영향을 미치지 못한다.
LDM | 여러 개의 레지스터를 읽어들임 |
STM | 여러 개의 레지스터를 저장 |
다음은 다중 로드-스토어 명령어를 위한 주소지정방식이다.
주소지정방식 | 설명 | 시작 주소 | 끝 주소 | Rn! |
IA | increment after | Rn | Rn+4*N-4 | Rn+4*N |
IB | increment before | RN+4 | Rn+4*N | Rn+4*N |
DA | decrement after | Rn-4*N+4 | Rn | Rn-4*N |
DB | decrement before | Rn-4*N | Rn-4 | Rn-4*N |
예제를 통해 감을 잡아보자!!
mem32[0x80018] = 0x03 mem32[0x80014] = 0x02 mem32[0x80010] = 0x01 r0 = 0x00080010 r1 = 0x00000000 r2 = 0x00000000 r3 = 0x00000000 LDMIA r0, {r1-r3} r0 = 0x0008001c r1 = 0x00000001 r2 = 0x00000002 r3 = 0x0000003
다음은 베이스 주소가 업데이트될 때 사용되는 LDM-STM 쌍이다. 일반적으로 LDM 을 호출하면 반드시 STM 이 호출된다.
STM | LDM |
STMIA | LDMDB |
STMIB | LDMDA |
STMDA | LDMIB |
STMDB | LDMIA |
예제를 통해 알아보자!!
r0 = 0x00009000 ; 초기값 r1 = 0x00000009 r2 = 0x00000008 r3 = 0x00000007 STMIB r0!, {r1-r3} ; STMIB 호출 MOV r1, #1 MOV r2, #2 MOV r3, #3 r0 = 0x0000900c ; 값이 수정됨 r1 = 0x00000001 r2 = 0x00000002 r3 = 0x00000003 LDMDA r0!, {r1-r3} ; LDMDA 호출 r0 = 0x00009000 ; 초기값으로 복원 r1 = 0x00000009 r2 = 0x00000008 r3 = 0x00000007
만약 베이스 주소를 업데이트하는 스토어 명령어를 사용하고자 한다면, 같은 수의 레지스터를 가진 이와 한 쌍인 로드 명령어는 데이터를 다시 읽어들이고 베이스 주소 포인터를 복구하는 데 사용될 것이다. 이것은 레지스터의 값들을 임시로 저장해두었다가 나중에 복구할 때에 유용하다.
다음은 블록 메모리 복사를 하는 예제이다.
; r9 는 소스 데이터의 시작 위치를 가리킴 ; r10 은 결과 데이터의 시작 위치를 가리킴 ; r11 은 소스의 끝을 가리킴 Loop ; 소스로 부터 32 바이트를 로드하여 r9 포인터에 업데이트함 LDMIA r9!, {r0-r7} ; 목적지로 32 바이트를 저장하고 r10 포인터를 업데이트함 STMIA r10!, {r10-r7} ; 그것들을 저장함 ; 소스의 끝에 도착했는지를 체크함 CMP r9, r11 BNE loop
스택 명령어
ARM 아키텍처는 스택 오퍼레이션을 수행하기 위해 다중 레지스터 전송 명령어를 사용하고 있다. 팝(POP) 동작(스택에서 데이터를 제거하는 일)을 위해 LDM 명령어를 사용하며, 푸시(push) 동작(스택에 데이터를 집어 넣는 일)을 위해서는 STM 명령어를 사용한다.
ascending 스택은 메모리의 상위 주소 방향으로 스택이 자라는 것을 말하며, descending 스택은 메모리의 하위 주소 방향으로 스택이 자라는 것을 의미한다. full 스택(F)을 사용할 경우, 스택 포인터 sp 는 마지막으로 사용된 위치의 주소(예를 들어, sp 는 스택 상에 마지막 아이템을 가리킴)를 가리키고 있다. 반면 empty 스택(E)은 sp 가 처음 사용되지 않을 영역의 주소(예를 들면, 스택의 마지막 아이템 다음의 빈 공간)를 가리키고 있다.
ARM 은 루틴이 어떻게 호출되고 레지스터들이 어떻게 할당될지를 정의하는 ATPCS(ARM-Thumb Procedure Call)을 규정하고 있다. ATPCS 에서 스택은 full descending 스택으로 정의되어 있으므로, LDMFD 와 STMFD 명령어는 팝과 푸시 기능을 각각 제공하고 있다.
r1 = 0x00000002 r4 = 0x00000003 sp = 0x00080014 STMFD sp!, {r1, r4} r1 = 0x00000002 r4 = 0x00000003 sp = 0x0008000c
스택 동작을 위한 어드레싱 방법은 다음 표와 같다.
주소지정방식 | 설명 | 팝 | =LDM | 푸시 | =STM |
FA | full ascending | LDMFA | LDMDA | STMFA | STMIB |
FD | full descending | LDMFD | LDMIA | STMFD | STMDB |
EA | empty ascending | LDMEA | LDMDB | STMEA | STMIA |
ED | empty descending | LDMED | LDMIB | STMED | STMDA |
명령어 수행전
0x80018 | 0x00000001 | |
sp | 0x80014 | 0x00000002 |
0x80010 | Empty | |
0x8000c | Empty |
명령어 수행후
0x80018 | 0x00000001 | |
0x80014 | 0x00000002 | |
0x80010 | 0x00000003 | |
sp | 0x8000c | 0x00000002 |
스택을 처리하기 위해 보존해야 되는 스택 베이스, 스택 포인터, 스택 리미트의 3 개의 인자가 있다. 스택 베이스는 메모리에서 스택의 시작위치를 나타낸다. 스택 포인터는 초기에는 스택 베이스를 가리키지만, 데이터가 스택으로 들어가면 스택 포인터는 메모리의 하단으로 이동하여 스택의 가장 맨 위를 계속 가리키고 있다. 스택 포인터가 스택 리미트를 넘겨가면 스택 오버플로우 에러가 발생된다. 다음은 스택 오버 플로우 에러가 발생하였는지를 체크하기 위한 간단한 코드이다.
SUB sp, sp, #size CMP sp, r10 BLLO_stack_overflow ; 조건
ATPCS 는 레지스터 r10 을 스택 리미트 sl로 정의 하고 있다. 이것은 스택 체크를 할 때에만 사용될 수 있도록 선택할 수 있다. BLLO 명령어는 BL 명령어에 조건 인자 LO를 추가한 형태의 명령어이다. 새로운 데이터가 스택에 들어온 다음, sp 가 r10 보다 작거나, 스택 포인터가 스택 베이스보다 작아질 때에도 스택 오버플로우 에러가 발생한다.
Swap 명령어
메모리의 내용과 레지스터의 내용을 바꾸어준다. swap 명령어를 처리하고 있는 동안은 다른 어떤 명령어나 버스 액세스에 의해 중단될 수 없다. 이를 가리켜 작업이 완료될 때까지 시스템이 “버스를 잡고 있다”고 표현한다.
명령어 | 설명 |
SWP | 메모리와 레지스터간에 워드를 교환 |
SWPB | 메모리와 레지스터간에 바이트를 교환 |
mem32[0x9000] = 0x12345678 r0 = 0x00000000 r1 = 0x11112222 r2 = 0x00009000 SWP r0, r1, [r2] mem32[0x9000] = 0x11112222 r0 = 0x12345678 r1 = 0x11112222 r2 = 0x00009000
이 명령어는 운영체제 안에서 세마포어나 뮤텍스를 구현할 때에 특히 유용하다.
spin MOV r1, =semaphore MOV r2, #1 SWP r3, r2, [r1] ; 완료될 때까지 버스를 잡고 있음 CMP r3, #1 BEQ spin
SWI 명령어
소프트웨어 인터럽트 명령어는 소프트웨어 인터럽트 익셉션을 발생시키는 것으로, 어플리케이션이 운영체제 루틴을 호출하기 위한 매커니즘을 제공한다.
명령어 | 설명 | SWI_number |
SWI | 소프트웨어 인터럽트 | lr_svc = SWI 다음의 명령어의 주소, spsr_svc = cpsr, pc = 벡터 + 0x8, cpsr 모드 = SVC, cpsr I = 1(IRQ 인터럽트 마스크) |
SWI 명령어를 실행할 때, 프로세서는 프로그램 카운터 pc 에 벡터 테이블의 오프셋 0x8 로 설정한다. 이 명령어는 프로세서 모드를 SVC 로 변경시켜 운영체제 루틴이 특권 모드에서 호출될 수 있도록 해준다. 각 SWI 명령어는 관련 SWI 숫자를 포함하고 있는데, 이것은 특별한 함수 호출이나 특징을 나타내는 데 사용된다.
cpsr = nzcVqift_USER pc = 0x00008000 lr = 0x003fffff ; lr = r14 r0 = 0x12 0x00008000 SWI 0x123456 cpsr = nzcVqlft_SVC ; 현재 상태 spsr = nzcVqift_USER ; 이전 상태 pc = 0x00000008 lr = 0x00008004 ; 링크 레지스터 r0 = 0x12
PSR 명령어
psr(Program Status Register)을 직접 제어하기 위한 명령어를 2가지 제공하고 있다. MRS 명령어는 cpsr 이나 spsr 의 내용을 레지스터로 전송해준다.
MSR 명령어는 반대방향, 즉 레지스터의 내용을 cpsr 이나 spsr 로 전송해준다. 이 명령어들은 cpsr이나 spsr 을 읽고 쓰기 위해 사용된다.
명령어 | 설명 | 형식 |
MRS | PSR 레지스터를 범용 레지스터에 복사 | Rd=psr |
MSR | 범용 레지스터를 PSR 레지스터로 이동 | psr[field]=Rm |
MSR | 상수값을 PSR 레지스터로 이동 | psr[field]=immediate |
예제를 통해 알아보자!
cpsr = nzcvqIFt_SVC MRS r1, cpsr BIC r1, r1, #0x80 MRS cpsr_c, r1 cpsr = nzcvqiFt_SVC
위 코드는 먼저 MSR 이 cpsr 을 레지스터 r1 으로 복사한다. BIC 명령어는 r1 의 7번째 비트를 0으로 클리어해준다. 그 다음, 레지스터 r1 을 cpsr 로 복사하는데, 이것은 IRQ 인터럽트를 활성화시켜준다. 이 코드는 cpsr 안에 있는 다른 모든 설정값을 유지하며 control 영역안에 있는 I 비트만을 수정한다. 위 예제는 SVC 모드 안에 있다. user 모드에서는 모든 cpsr 비트를 읽을 수 있지만, 상태 플래그 영역 f 만을 업데이트 할 수 있다.
상수값 로드
32 비트 상수값을 레지스터에 저장하는 ARM 명령어를 제공한다.
명령어 | 설명 | 형식 | |
LDR | 상수값을 레지스터에 저장하는 의사 명령어 | Rd = 32 비트 상수값 | |
ADR | 주소값을 레지스터에 저장하는 의사 명령어 | Rd = 32 비트 상대 주소값 |
다음 예제를 통해 알아보자!
LDR r0, [pc, #const_number-8-{pc}] ... const_number DCD 0xff00ffff
위 예제는 32 비트 상수값 0xff00ffff 를 레지스터 r0 로 읽어들이는 LDR 명령어를 보여주고 있다. 상수값을 로드하기 위해 메모리를 엑세스한다.
이 방법은 시간에 민감한 루틴에서는 매우 큰 손실을 가져온다. 그래서 컴파일러와 어셈블러는 메모리에서 상수값을 로드하는 일을 가능한 피하기 위한 교묘한 테크닉을 사용한다. LDR 같은 명령어는 상수값을 만들어 내기 위해 MOV 나 MVN 명령어를 삽입하거나, 코드내에 데이터 영역인 리터럴 풀(literal pool)로부터 상수값을 읽기 위해 pc 상대 주소를 가지고 있는 LDR 명령어를 만들어 낸다.
ARMv5E 에서 제공하는 명령어
ARMv5E 에서 제공하는 새로운 명령어들은 다음과 같다.
CLZ 명령어
최상위 비트에서 처음으로 1이 나온 비트 사이에 0 이 몇 개나 있는가를 세는 데 사용된다.
r1 = 0b00000000000000000000010000 CLZ r0, r1 r0 = 15 ; 총 15 개
포화 산술 연산
32 비트 연산 중에 오버플로우가 발생할 경우, 표현할 수 있는 가장 큰 값인 0x7fffffff 값이 유지된다. 이로써 오버플로우가 발생하였는가를 확인하기 위한 코드를 추가할 필요가 없게 된다.
명령어 | 포화 연산 |
QADD | Rd = Rn+Rm |
QDADD | Rd = Rn+(Rm*2) |
QSUB | Rd = Rn-Rm |
QDSUB | Rd = Rn-(Rm*2) |
예제를 보자!
cpsr = nzcvqiFt_SVC r0 = 0x00000000 r1 = 0x70000000 r2 = 0x7fffffff QADD r0, r1, r2 cpsr = nzcvQiFt_SVC r0 = 0x7fffffff ; 오버플로우가 발생하지 않음
포화 상태라는 것을 가리키는 Q 비트(cpsr 의 27번째 비트)가 1로 세트된다. Q 비트는 외부에서 0 으로 설정해줄 때까지 1의 상태를 유지한다.
조건부 실행
ARM 명령어는 조건부로 실행될 수 있다. 주어진 조건이나 테스트 상황을 만족할 때에만 명령어가 실행되도록 설정할 수 있다. 조건부 실행 명령어를 이용하면 분기되는 상황을 감소시켜 파이프라인이 깨지는 수를 줄여준다.
디폴트 니모닉은 AL 로 표기하며, “항상 실행하라”는 의미이다.
; z 플래그가 1 인 경우에만, r0 = r1 + r2 ADDEQ r0, r1, r2
위의 예제는 EQ 조건이 추가되어 있는 ADD 명령어에 대해 보여주고 있다. 이 명령어는 cpsr 의 Z(zero) 플래그가 1로 세트되었을 때에만 실행된다.
16 비트 Thumb 명령어
Thumb 은 32 비트 ARM 명령어의 일부를 16 비트 명령어 세트로 인코딩한다. 16 비트 데이터 버스로 이루어진 프로세서에서는 ARM 명령어보다 Thumb 명령어가 더 나은 성능을 보여준다. 따라서 메모리가 제한되어 있는 시스템에서는 Thumb 명령어를 권한다.
평균적으로 Thumb 코드는 동일한 ARM 코드보다 약 30% 정도 적은 공간을 차지한다.
Thumb 모드에서의 레지스터
Thumb 상태에서는 모든 레지스터를 직접 액세스할 수 있는 것은 아니다.
레지스터 | 액세스 |
r0-r7 | 완전 액세스 가능 |
r8-r12 | MOV, ADD, CMP 일 경우에만 액세스 가능 |
r13 sp | 제한적인 액세스 |
r14 lr | 제한적인 액세스 |
r15 pc | 제한적인 액세스 |
cpsr | 간접 액세스 |
spsr | 액세스 불가능 |
위의 표를 보면 cpsr 이나 spsr 을 직접 액세스할 수 있는 명령어가 없다. 즉, Thumb 명령어에는 MRS 나 MSR 에 상응하는 명령어가 없다.
cpsr 이나 spsr 의 값을 변경하기 위해서는 ARM 상태로 변경한 다음, MSR 과 MRS 를 사용하여 값을 바꾸어야 한다. Thumb 에는 코프로세서 명령어도 없다. 역시 같은 방법으로 해결해야 한다.
ARM-Thumb 인터워킹
ARM 코드와 Thumb 코드를 함께 링크하는 방법을 말한다. ARM 루틴에서 Thumb 를 호출하기 위해서 코어는 상태변환을 해야 한다. cpsr 의 T 비트로 확인할 수 있다. BX 와 BLX 분기 명령어는 어떤 루틴으로 분기하는 동안 ARM 과 Thumb 상태를 바꾸어준다. ARM 버전과는 달리, Thumb BX 명령어는 조건부로 실행될 수 없다.
명령어 | 설명 |
BX | ARM/Thumb 상태 변환 분기 명령어 |
BXL | ARM/Thumb 상태 변환 및 서브루틴 호출 분기 명령어 |
Thumb 상태로 변환하려면 최하위 비트를 1로 설정하면 된다. 복귀주소는 BX 명령어에 의해 자동으로 보존되지는 않으므로, 분기하기에 앞서 MOV 명령어를 사용하여 복귀할 주소를 설정해두어야 한다.
; ARM 코드 CODE32 ; 워드 정렬 LDR r0, =thumbCode + 1 ; Thumb 상태로 진입하기 위해 1을 더함 MOV lr,pc ; 복귀할 주소 설정 BX r0 ; Thumb 코드 & 상태로 분기 ; 계속 ; Thumb 코드 CODE16 ; 하프워드 정렬 thumbCode ADD r1, #1 BX lr ; ARM 코드 & 상태로 복귀
BX 명령어는 0번 비트가 상태 변환을 일으키지 않는다면 일반 분기 명령어로 사용할 수도 있다.
BX 명령어 대신 BLX 명령어를 사용하면 Thumb 루틴을 호출하는 것이 단순해진다. 이것은 링크 레지스터 lr 에 복귀할 주소를 저장하기 때문이다.
CODE32 LDR r0, =thumbRoutine + 1 BLX r0 ; Thumb 코드 & 상태로 분기 ; 계속 CODE16 thumbRoutine ADD r1, #1 BX r14 ; ARM 코드 & 상태로 복귀
무조건 분기 명령어
조건 분기 명령어는 Thumb 상태에서 유일하게 조건적으로 실행되는 명령어이다.
명령어 | 설명 |
B | 무조건 분기 명령어 |
BL | 서브루틴 호출 명령어 |
BL 명령어는 조건부 실행이 불가능하며, 분기할 수 있는 범위는 약 +/- 4MB 정도이다.
데이터 처리 명령어
이들 명령어는 ARM 명령어와 유사한 체계를 따른다. 대부분의 Thumb 데이터 처리 명령어는 하위 레지스터에서만 동작하며 cpsr 을 업데이트한다.
상위 레지스터 r8-r14, pc 에서 동작할 수 있는 예외 명령어에는 다음과 같은 것들이 있다.
MOV Rd, Rn ADD Rd, Rm CMP Rn, Rm
상위 레지스터들을 사용할 경우에는 CMP 를 제외한 다른 명령어들은 상태 플래그를 업데이트하지 않는다. 하지만 CMP 명령어는 cpsr 을 항상 업데이트 한다. 예제를 통해 확인해보자!
cpsr = nzcvIFT_SVC r1 = 0x80000000 r2 = 0x10000000 ADD r0, r1, r2 r0 = 0x90000000 cpsr = NzcvIFT_SVC ; cpsr 을 업데이트 됨
단일-레지스터 전송 명령어
메모리에서 레지스터를 읽거나 메모리에 레지스터를 저장하는 LDR 과 STR 명령어를 지원한다. 이들 명령어는 2개의 프리인덱스 주소지정방식(레지스터 오프셋과 상수 오프셋)을 사용한다.
mem32[0x90000] = 0x00000001 mem32[0x90004] = 0x00000002 mem32[0x90008] = 0x00000003 r0 = 0x00000000 r1 = 0x00090000 r4 = 0x00000004 LDR r0, [r1,r4] ; ① 레지스터 오프셋을 이용한 방법 r0 = 0x00000002 r1 = 0x00090000 r4 = 0x00000004 LDR r0, [r1, #0x04] ; ② 상수값 오프셋을 이용한 방법 r0 = 0x00000002
두가지 방법의 유일한 차이점은 첫번째는 레지스터 r4 안의 값에 따른 오프셋을 사용하는 반면, 두 번째 LDR 은 고정된 오프셋을 사용한다는 것이다.
다중-레지스터 전송 명령어
이들은 IA(Increment After) 주소지정방식만을 지원한다. 이 명령어는 실행 후 베이스 레지스터인 Rn 을 항상 업데이트한다. 베이스 레지스터와 레지스터 리스트는 항상 하위 레지스터인 r0 에서 r7 까지로 제한된다.
ARM 명령어 세트와는 달리, 업데이트를 하라는 의미의 기호 ! 는 옵션이 아니다.
스택 명령어
기억할 만한 흥미로운 점은 명령어에 스택 포인터가 없다는 것이다. Thumb 에서 스택 포인터는 레지스터 r13 으로 정해져 있고 sp 는 자동으로 업데이트되기 때문이다. 사용가능한 레지스터는 하위 레지스터인 r0 에서 r7 까지고 제한되어 있다. 스택 명령어는 full descending 스택 동작만을 지원한다.
; 서브루틴 호출 BL ThumbRoutine ; 계속 ThumbRoutine PUSH {r1, lr} MOV r0, #2 POP {r1, pc}
위의 예제에서 링크 레지스터 lr은 레지스터 r1 이 쌓여 있는 스택 위에 쌓인다. 복귀시에는 복귀 주소를 pc 에 로드한 후, 레지스터 r1 을 빼낸다. 이로써 서브루틴에서 복귀하게 된다.
소프트웨어 인터럽트 명령어
Thumb 상태에서 익셉션이나 인터럽트가 발생하면, 프로세서는 그 익셉션을 처리하기 위해 자동으로 ARM 상태로 변경된다. Thumb SWI 명령어는 ARM 과 동일한 기능을 하며 표기법도 같다. 다른 점은 SWI 번호가 0 에서 255 까지로 제한되어 있으며 조건부 실행이 불가능하다는 것이다.
최적화된 C 프로그래밍
여기서는 같은 루틴이면 C 프로그래밍 상에서 어떻게 최적화 시키는가에 대한 방법에 대해 설명하고 있다.
데이터 형의 효과적인 사용
- 레지스터에 저장되는 지역 변수에 대해, 8 비트나 16 비트의 특성을 이용하는 산술 연산이 아니라면 char 나 short 는 사용하지 않도록 한다. 대신 signed int 또는 unsigned int 형을 사용한다. 나눗셈을 하는 경우에는 unsigned 형이 더 빠르다.
- 메모리에 저장되는 배열이나 전역 변수에 대해, 필요한 데이터를 저장할 수 있을 만큼의 가능한 작은 데이터형을 사용하도록 한다. 이것은 메모리를 절약해준다. ARMv4 아키텍처에서는 배열 포인터를 증가시키며 데이터를 액세스하는 것이 효과적이다. LDRH 는 short 형의 배열을 지원하지 않기 때문에 이러한 형의 배열 사용은 피해야 한다.
- 배열의 엔트리 또는 전역 변수를 지역 변수로 읽어들이거나, 지역 변수를 배열에 저장할 때에는 명시적(explicit) 캐스트 연산을 사용하도록 한다. 이러한 캐스트 연산은 메모리 안에 저장되어 있는 narrow 형의 데이터가 더 넓은 형으로 확장되어 레지스터 안에 저장된다는 것을 보여준다. 암시적 캐스트를 사용한 경우에는 “implicit narrowing cast” 라는 경고문을 발생시킨다.
- 명시적이든 암시적이든 narrow 캐스트 연산을 하는 것은 피하도록 한다. 이들은 보통 추가 사이클을 필요로 하기 때문이다. 로드-스토어할 때의 캐스트 연산은 비용이 들지 않는데, 로드-스토어 명령어가 캐스트를 수행하기 때문이다.
- 함수 인자나 리턴값을 위해 char 와 short 형을 사용하지 않도록 한다. 파라미터들의 범위가 더 적더라도 int 형을 사용하면 컴파일러는 불필요한 캐스트 연산을 수행하지 않는다.
효과적인 루프문의 코딩
- 0 으로 다운카운트를 하는 루프를 사용하도록 한다. 그러면 컴파일러는 최종값을 저장하기 위해 레지스터를 할당할 필요가 없으며, 0 과 비교하는 작업에 비용이 들지 않는다.
- 디폴트로 unsigned 루프 카운터를 사용하고, 반복 조건으로 i > 0 보다는 i != 0 을 사용하도록 한다. 그러면 루프 오버헤드는 명령어 2개로 줄어든다.
- 루프가 적어도 한 번 이상 실행된다면 for 문보다는 do-while 문을 사용하도록 한다. 이것은 컴파일러가 루프 카운터가 0 인지 아닌지를 체크할 필요성을 없애준다.
- 루프 오버헤드를 줄여야 하는 중요한 루프문은 언롤링시키도록 한다. 그렇다고 언롤링을 너무 많이 하지는 않도록 한다. 만약 루프 오버헤드가 전체의 일정 비율만큼 작아진다면, 언롤링은 코드 사이즈를 증가시키고 캐시 성능에 좋지 않은 영향을 끼칠 것이다.
- 배열에서 요소(element) 들의 수는 4 나 8 의 배수가 되도록 정렬시키도록 한다. 그러면 배열 요소를 추가해야 할 지에 대해 고민하지 않고 루프를 2, 4, 8 배로 쉽게 언롤링 시킬 수 있다.
루프 언롤링
예를 들어 루프를 한번 수행하는 데, 총 4 번의 사이클을 소요한다면, 10 번의 루프를 돌면 총 40 번의 사이클을 소요하게 된다. 이러한 루프 오버헤드를 루프 언롤링을 이용하여 줄일 수 있다. 언롤링이란 루프문의 몸체를 여러 번 반복하여 적음으로써 같은 비율만큼 반복수를 줄여주는 방법을 말한다.
int checksum(int *data, unsigned int N) { int sum = 0; do { sum += *(data++); sum += *(data++); sum += *(data++); sum += *(data++); // 한번에 4번의 루프를 일일이 적어주고 N -= 4; // 총 4 를 뺀다 }while(N != 0); return sum; }
이것을 컴파일한 결과는 다음과 같다.
checksum MOV r2, #0 ; sum = 0 checksum_loop LDR r3, [r0], #4 ; r3 = *(data++) SUBS r1, r1, #4 ; N -= 4 & 플래그 업데이트 ADD r2, r3, r2 ; sum += r3 LDR r3, [r0], #4 ; r3 = *(data++) ADD r2, r3, r2 LDR r3, [r0], #4 ADD r2, r3, r2 LDR r3, [r0], #4 ADD r2, r3, r2 BNE checksum_loop ; (N != 0) 이면 loop 로 분기 MOV r0, r2 ; r0 = sum MOV pc, r14 ; r0 리턴
루프 오버헤드는 4N 사이클에서 (4N)/4 = N 사이클로 줄어들었다.
ATPCS 에서의 효율적인 레지스터 매핑
- 함수의 내부 루틴에서 사용하는 지역 변수의 수를 12 개로 제한하도록 한다. 컴파일러는 이러한 지역 변수들을 ARM 레지스터에 할당할 수 있어야 한다.
- 변수들이 내부 루프에서 사용되고 있는지를 확인하여 어떤 변수가 중요한지 컴파일러에게 알리도록 한다.
효과적인 함수 호출 방법
- 함수 인자의 수는 4로 제한하도록 한다. 이것은 함수 호출을 보다 효율적으로 만들어 준다. 관련 인자들을 그룹화하려면 구조체를 사용하여 여러 인자들을 패싱하지 말고 구조체 포인터 하나만을 패싱하도록 한다.
- 작은 함수들은 같은 소스 파일안에 이들을 호출하는 함수 앞에 정의하도록 한다. 그러면 컴파일러는 함수 호출을 최적화하거나 작은 함수들을 인라인시킨다.
- 크리티컬한 함수는 __inline 키워드를 이용하여 인라인시키도록 한다.
포인터 앨리어싱
2개의 포인터가 같은 주소를 가리키고 있을 때 이들을 앨리어스(alias)라 부른다. 만약 한 포인터에 값을 적었다면, 그것은 다른 포인터에서 읽은 값에 영향을 주게 될 것이다.
다음은 포인터 앨리어싱을 피하는 방법이다.
- 메모리를 제어하는 공통 표현법을 없애기 위해 컴파일러에 의존하지 않도록 한다. 대신 그 표현 방법을 저장하고 있는 새로운 지역 변수를 만드는 데, 이것은 그 표현이 한번만 사용되도록 보장해준다.
- 지역 변수의 주소값을 사용하는 것을 피하도록 한다. 지역 변수로부터 변수를 액세스하는 것은 비효율적일 수도 있다.
효율적인 구조체 정렬
- 요소의 크기가 점점 증가하는 순으로 구조체 레이아웃을 잡도록 한다. 가장 작은 요소를 가진 구조체에서 시작해서 가장 큰 것을 가진 구조체로 끝낸다.
struct{ char a; char c; short d; int b; }
- 너무 큰 구조체는사용하지 않도록 한다. 대신 더 작은 구조체 계층을 사용한다.
- 이식성을 위해, 구조체 레이아웃이 컴파일러의 영향을 받지 않도록 API 구조체에 패딩을 추가하도록 한다.
- API 안에 enum 형을 사용하지 않도록 한다. enum 형의 크기는 컴파일러에 따라 다르다.
비트 필드
- 가능하면 비트필드를 사용하지 않도록 한다. 대신 마스크값을 정의하는 #define 이나 enum 을 사용한다.
- 논리연산 AND, OR, EOR 와 마스크값을 사용하여 정수형에 비트필드를 테스트, 토글, 세트하도록 한다.
엔디언과 정렬
- 가능하다면 비정렬 데이터는 사용하지 않도록 한다.
- 어떤 바이트 정렬이 될 수 있는 데이터를 위해서 char* 형을 사용하도록 한다. 바이트 단위로 읽은 다음, 논리 연산을 수행하여 데이터를 액세스한다. 그러면 코드는 정렬이나 ARM 엔디안 설정에 영향을 받지 않는다.
- 비정렬 구조체를 빠르게 액세스하기 위해서는 포인터 정렬과 프로세서 엔디안에 따라 다른 함수를 사용하여 구현하도록 한다.
나눗셈
C 라이브러리에서 제공되는 표준 정수 나눗셈 루틴은 20 에서 100 사이클 정도가 소요되는 데, 이것은 ARM 프로세서 제품군이나 조기 종료 여부 그리고 입력 오퍼랜드의 범위에 따라 달라진다. 나눗셈(/)과 나머지(%)는 처리속도가 너무 느리기 때문에 가능하면 사용하지 않는 것이 좋다. 하지만 상수로 나누는 것이나 똑같은 분모로 반복해서 나누는 작업은 효율적으로 처리할 수 있다.
여기서는 곱셈으로 나눗셈을 대체하는 방법과 나눗셈을 가능하면 적게 호출하는 방법에 대해 설명할 것이다.
- 가능하면 나눗셈의 사용을 피하도록 한다. 원형 버퍼 처리를 위해 사용하지 않는다.
- 나눗셈을 피할 수 없다면, 나눗셈 루틴이 나눗셈 n/d 와 나머지 n%d 를 함께 생성한다는 사실을 이용하도록 한다.
- 같은 분모 d 로 반복해서 나누기 위해서는 s = (2^k - 1)/d 를 이용하여 계산하도록 한다.
- unsigned 상수 d 로 unsigned n < 2^n 을 나누기 위해서 n/d 가 (ns) » (N+k) 또는 (ns+s) » (N+k)가 되도록 32비트 unsigned s 와 시프트 k를 찾는다. 이에 대한 선택은 d에 의해 결정된다. signed 나눗셈에도 유사한 결과가 나온다.
인라인 함수 및 인라인 어셈블리
인라인 함수를 사용하면 함수 호출로 인한 오버헤드를 완전히 없앨 수 있다. 게다가 많은 컴파일러들은 C 소스 코드 상에 인라인 어셈블리를 포함할 수 있도록 지원하고 있다. 어셈블리를 포함한 인라인 함수를 사용하며, 보통은 사용할 수 없는 ARM 명령어와 최적화를 지원하는 컴파일러를 얻을 수 있다.
- C 컴파일러가 지원하지 않는 새로운 함수나 원칙을 정의하기 위해 인라인 함수를 사용하도록 한다.
- C 컴파일러가 지원하지 않는 ARM 명령어를 액세스하기 위해 인라인 어셈블리를 사용하도록 한다. 이러한 예로는 코프로세서 명령어나 ARMv5E 명령어가 있다.
이식성 문제
ARM 에서 C 코드로 포팅할 때 직면할 수 있는 문제점에 대한 설명이다.
char 형
ARM 에서 char 형은 unsigned 이다. 하지만 많은 다른 프로세서에서 char 형은 signed 형으로 취급된다. 만약 char 형의 루프 카운터를 사용한다면, 무한 루프에 빠질 수도 있다. 이 것을 해결하기 위해서는 char 형을 signed 형으로 바꿔 주거나, 루프 카운터를 int 형으로 변경한다.
int 형
오랜된 몇몇 아키텍처에서는 16 비트 int 를 사용한다. 거의 찾아보기 힘들지만, 32 비트 int 형으로 변환하는 데 문제가 될 수 있다. int 를 사용하기 전에 먼저 테스트를 해보도록 한다.
비정렬 데이터 포인터
어떤 프로세서에서는 비정렬 주소에서 short 형이나 int 형 값을 로드하는 것을 지원한다. C 프로그램은 char* 를 int* 로 캐스트함으로써 이것들이 비정렬이 되도록 그 포인터들을 조작할 수 있다.
엔디안 가정
엔디안에 의존적인 코드를 없애거나 엔디안에 상관없는 코드로 대체해야 한다.
함수 프로토타입
매개변수를 wide 하게 보내는 컴파일러들은 함수 프로토타입이 정확하지 않더라도 올바른 결과를 가져올 수 있다. 항상 ANSI 프로토타입을 사용하도록 한다.
비트필트의 사용
비트필드내에 비트들의 레이아웃을 설정하는 것은 구현 가능하며, 엔디안에 의존적이다. 만약 C 코드가 비트들이 어떤 순서로 배열되어 있다고 가정하고 있다면 이러한 코드는 이식이 어려울 것이다.
열거형 사용
enum 이 이식 가능할지라도 컴파일러마다 enum 으로 다른 바이트 수만큼을 할당한다. 그러므로 만약 API 구조체에 enums 를 사용한다면 다른 컴파일러간에 크로스링크나 라이브러리를 사용할 수 없다.
인라인 어셈블리
C 코드 상에 인라인 어셈블리를 사용하면 아키텍처간의 포팅이 어려워진다. 따라서 인라인 함수를 쉽게 대체할 수 있는 작은 인라인 함수로 분리해야 한다.
volatile 키워드
ARM 메모리 매핑된 주변장치의 형(type) 정의를 할 때 volatile 키워드를 사용하도록 한다. 이 키워드는 메모리 액세스를 할 때 컴파일러가 최적화시키는 것을 방지해준다. 또한 컴파일러는 정확한 형으로 데이터 액세스를 보장한다.
예를 들어, 만약 volatile short 형으로 메모리 위치를 정의한다면 컴파일러는 16 비트 로드-스토어 명령어인 LDRSH 와 STRH 를 사용하여 액세스할 것이다.
ARM 어셈블리 코드 작성 및 최적화 방안
성능을 극대화 하기 위해서는 크리티컬한 루틴들을 직접 어셈블리 코딩을 통해 최적화 해야한다. 직접 어셈블리 코딩을 하면, C 소스에서는 사용할 수 없었던 다음의 3 가지 최적화 기법을 직접 제어할 수 있다.
- 명령어 스케줄링 : 프로세서가 정지 상태에 있지 않도록 하기 위해 명령어의 순서를 다시 배치하는 것이다. ARM 명령어는 파이프라인 안에 있기 때문에 명령어의 타이밍은 주변 명령어에 의해 영향을 받을 수 있다.
- 레지스터 할당 : 성능 극대화를 위해 변수를 ARM 레지스터나 스택 공간에 할당하는 방법에 대해 결정해야 한다. 이것의 목표는 메모리 액세스의 수를 최소화하는 것이다.
- 조건부 실행 : 다양한 ARM 조건 코드 및 명령어를 사용한다.
ARM9TDMI 의 5 단계 동작 설명
- Fetch : pc 가 가리키는 메모리 주소로 부터 명령어를 읽는다. 이 명령어는 코어로 로드된 후, 코어 파이프라인의 다음 단계로 진행된다.
- Decode : 이전 사이클에서 읽어들인 명령어를 해독한다. 만약 이전 단계에서 입력 오퍼랜드들이 사용되지 않았다면 레지스터 뱅크에서 입력 오퍼랜드들도 읽어들인다.
- ALU : 이전 사이클에서 해독한 명령어를 실행한다. 이 명령어는 pc-8(ARM 상태) 또는 pc-4(Thumb 상태)의 주소에서 읽은 것이다. ALU 는 보통 데이터 처리 연산을 위한 결과값을 계산하거나 로드, 스토어, 분기 연산을 위한 주소값을 계산한다. 어떤 명령어들은 이 단계에서 여러 사이클을 소모할 수 있다. 예를 들어, 다중 로드-스토어 명령어와 레지스터 기반의 시프트 연산은 여러 ALU 사이클을 소모한다.
- LS1 : 로드-스토어 명령어에 의해 규정된 데이터를 읽거나 저장한다. 로드-스토어 명령어가 아니라면 이 단계는 영향을 미치지 않는다.
- LS2 : 바이트 또는 하프워드 로드 명령어에 의해 로드된 데이터를 제로 또는 부호 확장한다. 8 비트 바이트 또는 16 비트 하프워드를 로드하는 명령어가 아니라면 이 단계는 영향을 미치지 않는다.
명령어 스케줄링
- ARM 코어는 파이프라인 아키텍처를 사용하고 있다. 파이프라인은 어떤 명령어의 결과를 여러 사이클이나 지연시킬 수도 있다. 만약 다음 명령어의 소스 오퍼랜드로 이 결과값을 이용한다면 프로세서는 그 값이 준비될 때까지 정지 사이클을 삽입할 것이다.
- 로드 명령어와 곱셈 명령어는 여러 ARM 제품군에서 결과 지연을 발생시킨다.
- 로드 명령어 다음에 생기는 인터로크를 없앨 수 있는 소프트웨어적인 방법으로는 2 가지가 있다. 루프 i 에서 루프 i+1 을 위한 데이터를 로드하도록 프리로드하는 방법이 있고, 루프 i 와 i+1 을 위한 코드를 언롤링하고 인터리빙하는 방법이 있다.
레지스터 할당
- ARM 은 범용으로 사용할 수 있는 레지스터가 14개, 즉 r0 에서 r12, r14 가 있다. 스택 포인터 r13 과 프로그램 카운터 r15 는 범용 데이터를 위해서 사용할 수 없다. 운영체제 인터럽트는 user 모드 r13 이 유효한 스택을 가리키고 있다고 가정하고 있으므로 이 경우에는 r13 을 사용하지 않도록 한다.
- 만약 14개 이상의 지역 변수가 필요하다면 내부 루프에서 바깥쪽으로 동작하도록 그 변수들을 스택에 저장하도록 한다.
- 어셈블리 코딩을 할 경우, 물리적인 레지스터 이름(r0, r1 등)을 그대로 사용하지 말고 레지스터의 이름을 정의하여 사용하도록 한다. 이렇게 하면 레지스터들을 다시 할당하거나 코드를 쉽게 유지할 수 있다.
- 반드시 레지스터를 사용해야만 한다면 같은 레지스터 안에 여러 개의 값들을 저장할 수 있다. 예를 들어, 한 레지스터 안에 루프 카운터와 시프트를 저장할 수 있다. 또한 한 레지스터 안에 여러 픽셀들을 저장할 수도 있다.
조건 분기 명령어의 활용
- 대부분의 if 문은 조건부 실행으로 구현할 수 있다. 이것은 조건 분기를 사용하는 것보다 효율적이다.
- if 문을 그 자체가 조건문이 되는 비교 명령어를 사용해서 유사한 조건들을 논리 AND 와 OR 연산을 통해 구현할 수도 있다.
최적의 루프문 구현
- ARM 은 루프 카운터를 구현하기 위해, 플래그를 설정하기 위한 뺄셈 명령어와 조건 분기 명령어의 2 가지 명령어를 필요로 한다.
- 루프의 성능을 향상시키기 위해 루프를 언롤링시키도록 한다. 단, 지나친 언롤링은 캐시의 성능을 떨어뜨릴 수 있으므로 피한다. 적은 수의 반복문에서 루프를 언롤링하는 것은 비효율적일 수 있다. 이 경우에 대해서는 테스트를 통해 루프의 반복수가 가장 큰 경우에만 루프를 언롤링하도록 할 수 있다.
- 중복된 루프문은 하나의 루프 카운터 레지스터만을 필요로 하는데, 이렇게 하면 다른 용도로 레지스터들을 사용할 수 있으므로 효율성 향상을 가져온다.
- ARM 은 음수 인덱스 루프와 로그 인덱스 루프를 효율적으로 구현할 수 있다.
비트 조작
- ARM 은 논리 연산과 배럴 시프터를 사용하여 효율적으로 비트들을 압축, 복원할 수 있다.
- 비트 스트림을 효율적으로 액세스하기 위해서는 32 비트 레지스터를 비트버퍼로 사용하도록 한다. 비트 버퍼 안에 있는 유효한 비트들의 수를 알아낼 수 있도록 레지스터를 하나 더 사용한다.
- 비트스트림을 효율적으로 해독하기 위해 비트스트림의 다음 N 비트들을 검색할 수 있는 검색 표를 사용하도록 한다. 검색 표는 대부분의 N 비트에서 코드의 길이를 직접 리턴하거나 긴 코드에서는 종료 문자를 리턴한다.
효율적인 switch 문
- 몇몇 작은 N 값에 대해 switch 값이 0 ⇐ x < N 범위 안에 있는지를 확인하도록 한다. 이를 위해서 해시 함수를 사용해야 할 경우도 있다.
- 함수 포인터 테이블을 표시하거나 일정 간격으로 코드의 일부 영역으로 분기하기 위해서 switch 값을 사용하도록 한다. 두 번째 기술은 위치에 영향을 받지 않지만 첫 번째는 영향을 받는다.
비정렬 데이터의 처리
- 성능이 문제가 안 된다면, LDMB 나 STMB 명령어를 사용하여 비정렬 데이터를 액세스하도록 한다. 이러한 접근 방법은 포인터 정렬이나 메모리 시스템에 정해진 엔디안에 상관없이 주어진 엔디안의 데이터를 액세스할 수 있다.
- 만약 성능이 문제가 된다면, 각각 가능한 메모리 정렬 방식을 이용하여 최적화한 여러 개의 루틴으로 구분하여 사용하는 것이 좋다. 이러한 루틴들을 자동으로 만들려면 어셈블러 MACRO 지시어를 사용할 수 있다.
요약
- 프로세서 인터로크(interlock) 또는 정지 상태를 초래하지 않도록 코드를 스케줄링하도록 한다. 로드 명령어와 곱셈 명령어는 한 명령어를 처리하는 데 보통 오랜 시간이 걸리므로 특히 신경써야 한다.
- 14 개의 사용 가능한 범용 레지스터에 가능하면 많은 데이터를 저장하도록 한다. 때로는 한 레지스터에 여러개의 데이터를 압축하여(packed) 저장하는 것도 가능하다.
- 작은 if 문을 위해서는 조건 분기보다는 조건부 데이터 처리 연산을 사용하도록 한다.
- 루프의 최대 성능을 위해서는 0 으로 다운카운트되는 언롤(unrolled) 루프를 사용하도록 한다.
- 비트-압축된 데이터를 묶거나 풀기 위해서는 32 비트 레지스터 버퍼를 사용하도록 한다. 그러면 효율성은 높아지고 메모리 데이터 대역은 줄어든다.
- 효율적인 switch 문을 구현하기 위해서는 분기 테이블과 해시(hash) 함수를 사용하도록 한다.
- 비정렬 데이터를 효율적으로 처리하기 위해서는 다중 루틴을 사용하도록 한다. 입출력 배열의 고유한 메모리 정렬에 맞게 각 루틴을 최적화하고 런타임시에 루틴을 선택한다.
어셈블리 코드를 이용한 원형함수의 최적화
ARM 명령어들은 덧셈, 뺄셈, 곱셈과 같은 간단한 원형함수만을 구현하고 있다. 따라서 나눗셈, 제곱근, 삼각함수와 같은 복잡한 연산을 수행하기 위해서는 소프트웨어 루틴을 사용해야 한다. 이러한 복잡한 연산의 성능을 향상시켜주는 많은 유용한 방법과 알고리즘이 있다.
표준이 되는 방법으로는 다음과 같은 것들이 있다.
- 작은 몫을 계산하기 위해서 2 진 검색(binary search)이나 시행 뺄셈(trial subtraction)을 이용한다.
- 제곱근의 풀이와 역수의 빠른 계산을 위해서 Newton-Raphson 반복을 이용한다.
- exp, log, sin, cos 과 같은 초월함수를 계산하기 위해서 급수 전개와 표 검색을 이용한다.
- 비트 조작을 하기 위해서는 비트를 각각 테스트하기 보다는 배럴 시프트를 사용하여 논리 연산을 수행한다.
- 가짜 난수를 만들어내기 위해서 MAC(Multiply Accumulate) 명령어를 사용한다.
디지털 신호 처리
디지털 신호를 표현하는 방법
신호값을 표현하는 방법을 선택하기 위해서는 다음의 분류를 사용한다.
- 프로토타입 알고리즘을 위해서는 부동소수점 표현 방법을 사용한다. 속도에 민감한 어플리케이션에서는 부동소수점을 사용하지 않는다. 대부분의 ARM 은 부동소수점을 하드웨어적으로 지원하고 있지 않다.
- 적당한 동적 범위를 가지고 있으며 속도에 민감한 DSP 어플리케이션을 위해서는 고정소수점 표현을 사용한다. ARM 코어는 8, 16, 32 비트 고정소수점 DSP 기능을 잘 지원하고 있다.
- 속도와 높은 동적 범위를 요구하는 어플리케이션을 위해서는 블록 부동소수점이나 로그 표현 방법을 사용한다.
ARM 에서의 DSP 소개
다음은 ARM 에서의 DSP 코드 작성을 위한 가이드이다.
- 포화는 추가의 사이클을 소요하기 때문에 포화가 필요하지 않도록 DSP 알고리즘을 설계한다. 포화보다는 확장 정밀도의 산술 연산이나 추가적인 스케일링을 사용한다.
- 로드와 스토어를 최소화하도록 DSP 알고리즘을 설계한다. 데이터를 로드한 다음 가능하면 그 데이터를 많이 사용하는 연산을 수행한다. 몇 가지 출력 결과를 바로 계산하면 이것이 가능하다. 재사용을 증가시킬 수 있는 또 다른 방법은 몇 가지 연산에 집중하는 것이다. 예를 들어, 데이터를 로드하면서 도트 결과와 신호 확장을 동시에 수행할 수 있다.
- 프로세서 인터로크를 피하기 위하여 ARM 어셈블리를 작성한다. 로드와 곱셈 결과는 정지 사이클을 추가하지 않고서는 다음 명령어에서 사용이 불가능하다.
- ARM 에는 범용으로 사용할 수 있는 레지스터가 14 개 있다. r0 에서 r12 까지 그리고 r14 가 그것이다. 내부 루프에서는 레지스터를 14 개 이하로 사용하도록 DSP 알고리즘을 설계한다.
요약
특정 어플리케이션을 위해 언롤링을 하거나 코딩을 하면 이 루틴들은 좀더 개선할 수 있다. 하지만 이 수치는 ARM 시스템에서 실제로 얻을 수 있는 지속적인 DSP 성능에 대한 좋은 아이디어를 제공해야 한다. 벤치마크는 캐시를 가진 코어의 경우 제로-대기-상태(zero-wait-state) 메모리나 캐시 적중을 가정하였을 때 로드, 스토어 루프 오버헤드를 모두 포함하고 있다.
다음은 ARM 필터링 벤치마크를 표로 나타낸것이다.
프로세스 코어 | 16비트 도트곱 | 16비트 블록 FIR 필터 | 32비트 블록 FIR 필터 | 16비트 블록 IIR 필터 |
ARM7TDMI | 7.6 | 5.2 | 9.0 | 22.0 |
ARM9TDMI | 7.0 | 4.8 | 8.3 | 22.0 |
StrongARM | 4.8 | 3.8 | 5.2 | 16.5 |
ARM9E | 2.5 | 1.3 | 4.3 | 8.0 |
XScale | 1.8 | 1.3 | 3.7 | 7.7 |
다음은 ARM FFT 벤치마크 결과이다.
16비트 complex FFT(radix-4) | ARM9TDMI(사이클/FFT) | ARM9E(사이클/FFT) |
64포인트 | 3,524 | 2,480 |
256포인트 | 19,514 | 13,194 |
1,024포인트 | 99,946 | 66,196 |
4,096포인트 | 487,632 | 318,878 |
익셉션과 인터럽트 처리
익셉션은 명령어의 순차적인 실행 과정을 바꾸어주는 역할을 한다.
익셉션으로는 Data Abort, FIQ, IRQ, Prefetch Abort, SWI, Reset, Undefined Instruction 의 7 가지가 있다.
인터럽트란 외부 주변 장치에 의해 발생되는 특별한 유형의 익셉션이다. 인터럽트 지연(interrupt latency)이란 외부 인터럽트 요청 신호가 발생할 때부터 특정 인터럽트 서비스 루틴(ISR)의 첫 번째 명령어를 읽어들일 때까지의 시간 간격을 말한다.
익셉션 핸들링
대부분의 익셉션들은 익셉션이 발생하였을 때 실행되는 소프트웨어 루틴인 익셉션 핸들러라는 관련 소프트웨어를 가지고 있다.
ARM 프로세서 익셉션과 모드
익셉션이 모드를 변경시킬 때, 코어는 자동으로,
- 익셉션 모드의 spsr 에 cpsr 값을 저장한다.
- 익셉션 모드의lr 에 pc 값을 저장한다.
- cpsr 의 모드 비트를 변경하여, 해당 익셉션 모드로 진입한다.
익셉션 | 모드 | 주요 목적 |
Fast Interrupt Request | FIQ | 고속 인터럽트 처리 |
Interrupt Request | IRQ | 일반 인터럽트 처리 |
SWI 와 Reset | SVC | 운영체제를 위한 보호 모드 |
Prefetch Abort 와 Data Abort | abort | 가상 메모리와 메모리 보호 처리 |
Undefined Instruction | undefined | 하드웨어 코프로세서의 소프트웨어 에뮬레이션 |
벡터 테이블
벡터 테이블이란, 익셉션이 발생하였을 때 ARM 코어가 분기하는 주소 테이블을 의미한다. 이들 주소는 일반적으로 여러가지 형식 중 하나의 분기 명령어를 포함하고 있다.
다음은 벡터 테이블과 프로세서 모드를 표로 나타낸 것이다.
익셉션 | 모드 | 벡터 테이블 오프셋 |
Reset | SVC | +0x00 |
Undefined Instruction | UND | +0x04 |
Software Interrupt | SVC | +0x08 |
Prefetch Abort | ABT | +0x0c |
Data Abort | ABT | +0x10 |
Not assigned | - | +0x14 |
IRQ | IRQ | +0x18 |
FIQ | FIQ | +0x1c |
익셉션 우선 순위
익셉션이 동시에 발생할 때, 각각의 우선순위에 따라서 수행이 된다.
익셉션 | 우선순위 | I 비트 | F 비트 |
Reset | 1 | 1 | 1 |
Data abort | 2 | 1 | - |
Fast Interrupt Request | 3 | 1 | 1 |
Interrupt Request | 4 | 1 | - |
Prefetch Abort | 5 | 1 | - |
Software Interrupt | 6 | 1 | - |
Undefined Instruction | 6 | 1 | - |
예를 들어, IRQ 가 발생하여 인터럽트 처리를 하던 도중에 FIQ 가 발생하면, 인터럽트 루틴을 중지시키고 FIQ 인터럽트 루틴을 실행한 후에 IRQ 인터럽트 루틴으로 제어권을 다시 넘겨준다.
Prefetch Abort 익셉션은 메모리로 부터 명령어를 읽어들이려고 시도하다가 실패한 경우에 발생한다. 이 익셉션은 명령어가 파이프라인의 실행(execute) 단계에 있고, 우선 순위가 높은 다른 익셉션이 발생하지 않을 때에 발생한다.
링크레지스터 오프셋
익셉션이 발생하면 링크 레지스터는 현재의 pc 값을 기준으로 특정 주소값으로 설정된다. 예를 들어, IRQ 익셉션이 발생하면, 링크 레지스터 lr 은 마지막으로 실행된 명령어에 8 을 더한 값을 가리킨다.
익셉션 | 주소 | 설명 | |
Reset | - | lr 값이 정의되지 않음 | |
Data Abort | lr - 8 | Data Abort 익셉션을 발생시킨 명령어를 가리킴 | |
FIQ | lllr - 4 | FIQ 핸들러로부터의 복귀주소 | |
IRQ | lr - 4 | IRQ 핸들러로부터의 복귀주소 | |
Prefetch Abort | lr - 4 | Prefetch Abort 익셉션을 발생시킨 명령어를 가리킴 | |
SWI | lr | SWI 명령어 다음 명령어를 가리킴 | |
Undefined Instruction | lr | undefined instruction 다음 명령어를 가리킴 |
인터럽트
인터럽트(IRQ, FIQ)를 활성화 시키는 방법은 다음과 같다.
cpsr 값 | IRQ | FIQ |
전(pre) | nzcvqjIFt_SVC | nzcvqjiFt_SVC |
코드(code) | enable_irq | enable_fiq |
MRS r1, cpsr | MRS r1, cpsr | |
BIC r1, r1, #0x80 | BIC r1, r1, #0x40 | |
MSR cpsr_c, r1 | MSR cpsr_c, r1 | |
후(post) | nzcvqjiFt_SVC | nzcvqjIft_SVC |
다음은 인터럽트(IRQ, FIQ)를 비활성화 시키는 방법이다.
cpsr 값 | IRQ | FIQ |
전(pre) | nzcvqjift_SVC | nzcvqjift_SVC |
코드(code) | disable_irq | disable_fiq |
MRS r1, cpsr | MRS r1, cpsr | |
ORR r1, r1, #0x80 | ORR r1, r1, #0x40 | |
MSR cpsr_c, r1 | MSR cpsr_c, r1 | |
후(post) | nzcvqjIft_SVC | nzcvqjiFt_SVC |
인터럽트 처리 방법
인터럽트 처리 방법으로는 다음과 같은 것들이 있다.
중첩을 허용하지 않는 인터럽트 처리 방법 | 가장 간단한 인터럽트 처리 방법으로, 순차적으로 각각의 인터럽트들을 처리하는 방법 |
중첩을 허용한 인터럽트 처리 방법 | 우선순위 없이 다중 인터럽트를 처리하는 방법 |
재진입 인터럽트 처리 방법 | 우선순위를 적용하여 다중 인터럽트들을 처리하는 방법 |
우선순위를 적용한 간단한 인터럽트 처리 방법 | 우선순위를 적용한 인터럽트들을 처리하는 방법 |
우선순위를 적용한 표준 인터럽트 처리 방법 | 우선순위가 높은 인터럽트를 우선순위가 낮은 인터럽트보다 더 빠른 시간 안에 처리하는 방법 |
우선순위를 적용한 다이렉트 인터럽트 처리 방법 | 우선순위가 높은 인터럽트를 더 짧은 시간에 처리하기 위한 방법으로, 특정 서비스 루틴으로 직접 분기하는 방법 |
우선순위를 적용한 그룹 인터럽트 처리 방법 | 인터럽트들을 여러 개의 인터럽트 우선순위로 묶어서 처리하는 방법 |
중첩이 허용되지 않는 간단한 인터럽트 핸들러
- 각 인터럽트들을 순서대로 하나씩 처리하고 서비스한다.
- 인터럽트가 서비스되고 있는 동안에는 다른 인터럽트를 처리할 수 없기 때문에 인터럽트 지연이 크다.
- 장점 : 구현 및 디버깅이 상대적으로 쉽다.
- 단점 : 다중 우선순위 인터럽트를 가지고 있는 복잡한 임베디드 시스템을 처리하는 데 사용할 수 없다.
중첩이 허용되는 인터럽트 핸들러
IRQ 스택에 데이터가 있는 동안에는 핸들러가 문맥전환을 수행할 수 없기 때문에 문맥 전환을 수행하려면 IRQ 스택을 비워야 한다. IRQ 스택에 저장되어 있는 모든 레지스터들은 태스크의 스택, 보통 SVC 스택으로 전송된다. 이것들은 스택 프레임이라는 스택 상에 할당된 메모리 블록으로 전송된다.
- 우선순위 할당 없이 여러 개의 인터럽트를 처리한다.
- 인터럽트 지연이 중간 이상 정도이다.
- 장점 : 인터럽트 지연을 줄이기 위해 각 인터럽트의 서비스가 완료되기 전에 인터럽트를 활성화시킬 수 있다.
- 단점 : 인터럽트의 우선순위를 다루지 않는다. 더 낮은 우선순위의 인터럽트가 더 높은 우선순위의 인터럽트가 발생하는 것을 막을 수 있다.
재진입 인터럽트 핸들러
- 우선순위가 부여될 수 있는 여러 개의 인터럽트들을 처리한다.
- 인터럽트 지연이 적다.
- 장점 : 다른 우선순위를 가진 인터럽트들을 처리할 수 있다.
- 단점 : 코드가 더욱 복잡해진다.
펌웨어
펌웨어란 하드웨어를 어플리케이션이나 운영체제와 인터페이스해주는 하위 레벨의 코드를 말한다. 부트로더란 운영체제나 어플리케이션을 메모리로 다운로드한 후, 그 소프트웨어로 pc 제어권을 넘겨주는 작업을 수행하는 소프트웨어를 말한다.
다음과 같은 과정에 따라 이미지를 로드하고 부팅시킨다.
- Reset 익셉션을 처리한다.
- 하드웨어 초기화를 한다. 즉, 시스템 레지스터의 베이스 주소를 설정하고 세그먼트 디스플레이 장치를 초기화한다.
- 메모리맵을 변경(리매핑)한다. ROM 주소를 상위 주소로, SRAM 주소를 0x00000000 번지로 바꾸어준다.
- 시리얼 포트의 통신 하드웨어 장치를 초기화한다.
- 부트로더가 이미지를 SRAM 에 로드한 후, pc 의 제어권을 이미지(pc = 0x00000000) 에게 넘겨준다.
임베디드 운영체제
ARM 프로세서에서 실행되는 임베디드 운영체제는 다음과 같은 기본 컴포넌트들로 구성되어 있다.
- 초기화 코드(initialization) 루틴에서는 운영체제에서 사용되는 내부 변수, 데이터 구조체, 하드웨어 장치를 모두 초기화한다.
- 메모리 처리(memory handling) 루틴에서는 커널과 다양한 어플리케이션들이 상주하여 실행될 수 있도록 메모리를 구성한다.
- 모든 인터럽트와 익셉션들은 핸들러(handler)를 필요로 한다. 사용되지 않는 인터럽트와 익셉션이라 할지라도 이를 위한 더미 핸들러를 만들어 놓아야 한다.
- 선점형 운영체제에는 주기 타이머(periodic timer)가 필요하다. 이 타이머는 스케줄러가 호출될 수 있도록 인터럽트를 발생시킨다.
- 스케줄러(scheduler)란 새로 실행될 태스크를 결정하는 알고리즘을 말한다.
- 문맥전환(context switch)이란 현재의 태스크 상태를 저장하고, 새로 실행될 태스크의 상태를 로드하는 것을 의미한다.
여기서는 SLOS(Simple Little Operating System)라는 운영체제에서 다음의 컴포넌트들에 대한 예제를 보여준다.
- 초기화 코드 : 초기화 루틴은 각 프로세서 모드를 위한 스택, 각 어플리케이션들을 위한 PCB(Process Control Blocks), 디바이스 드라이버 등 SLOS 에서 사용되는 모든 함수를 셋업한다.
- 메모리 모델 : SLOS 커널은 메모리의 하위 주소에 위치해 있으며, 각 어플리케이션들은 그 자신을 위한 저장 영역과 스택을 가지고 있다. 마이크로 컨트롤러 시스템 레지스터는 ROM 과 SRAM 으로 부터 멀리 떨어진 위치에 놓여 있다.
- 인터럽트와 익셉션 : SLOS 는 Reset, SWI, IRQ 의 3가지 이벤트만을 사용한다. 다른 사용되지 않는 모든 인터럽트와 익셉션은 더미 핸들러로 만들어져 있다.
- 스케줄러 : SLOS 는 간단한 라운드 로빈 스케줄러로 구현되어 있다.
- 문맥전환 : 현재 태스크의 문맥을 PCB 에 저장한 다음, 다음에 실행될 태스크의 문맥을 PCB 에서 읽어온다.
- 디바이스 드라이버 프레임 워크 : 어플리케이션이 하드웨어를 직접 엑세스하지 못하게 함으로써 운영체제를 보호한다.
캐시
캐시는 프로세서와 주메모리 사이에 위치해 있는 작고 빠른 메모리를 말한다. 이것은 최근에 참조한 시스템 메모리의 일부를 저장하고 있는 일종의 저장 버퍼이다. 프로세서는 시스템의 성능을 향상시키기 위해 가능하면 시스템 메모리보다는 캐시 메모리를 사용하기를 선호한다.
쓰기 버퍼란 프로세서 코어와 주 메모리 사이에 놓여있는 작은 FIFO(First-In-First-Out) 메모리를 말한다. 쓰기버퍼의 목적은 주 메모리에 쓰는 것과 관련된 느린 쓰기 시간으로부터 프로세서 코어와 캐시 메모리를 자유롭게 하는 데 있다.
참조의 지역성 원리란 컴퓨터 소프트웨어 프로그램이 데이터 메모리의 지역적인 부분에서 반복적으로 동작하는 코드의 작은 루프를 자주 실행한다는 것을 의미한다. 이는 캐시를 가진 프로세서 코어를 사용할 때 시스템의 평균 성능이 다소 높아지는 이유이기도 하다.
캐시 아키텍처의 특징을 설명하기 위해 ARM 커뮤니티에서 사용되는 많은 용어들이 있다. 편의를 돕기 위해 아래의 표를 만들었는데, 이는 현재 캐시를 가지고 있는 모든 ARM 코어의 특징들에 대해 설명하고 있다.
코어 | 캐시타입 | 캐시크기(KB) | 캐시라인크기(워드) | 연상 | 위치 | 캐시 락다운 지원 여부 | 쓰기 버퍼 크기(워드) |
ARM720T | 통합 | 8 | 4 | 4-way | 논리캐시 | no | 8 |
ARM740T | 통합 | 4 또는 8 | 4 | 4-way | yes 1/4 | 8 | |
ARM920T | 분할 | 16/16 D+I | 8 | 64-way | 논리캐시 | yes 1/64 | 16 |
ARM922T | 분할 | 8/8 D+I | 8 | 64-way | 논리캐시 | yes 1/64 | 16 |
ARM940T | 분할 | 4/4 D+I | 4 | 64-way | yes 1/64 | 8 | |
ARM926EJS | 분할 | 4-128/4-128 D+I | 8 | 4-way | 논리캐시 | yes 1/4 | 16 |
ARM946EJS | 분할 | 4-128/4-128 D+I | 4 | 4-way | 논리캐시 | yes 1/4 | 4 |
ARM1022E | 분할 | 4-128/4-128 D+I | 8 | 64-way | 논리캐시 | yes 1/64 | 16 |
ARM1026EJS | 분할 | 16/16 D+I | 8 | 4-way | 논리캐시 | yes 1/4 | 8 |
인텔 StrongARM | 분할 | 4-128/4-128 D+I | 4 | 32-way | 논리캐시 | no | 32 |
인텔 XScale | 분할 | 32/32 D+I | 8 | 32-way | 논리캐시 | yes 1/32 | 32 |
캐시 라인(cache line)이란 캐시의 기본 컴포넌트로서 디렉토리 스토어, 데이터 섹션, 상태 정보의 세 부분으로 구성된다. 캐시 태그(cache-tag)란 캐시 라인이 주메모리로부터 어디로 로드되는지를 가리키는 디렉토리 엔트리를 말한다. 캐시 안에는 유효 비트와 더티 비트의 2 가지 상태 비트가 있다. 유효 비트는 관련 캐시 라인이 활성화 메모리를 포함하고 있을 때에 1 로 설정된다. 더티 비트는 캐시가 후기입(writeback) 정책을 사용하고 있으며 새로운 데이터가 캐시 메모리에 쓰여질 때에 활성화 된다.
캐시가 MMU 앞에 놓이느냐 뒤에 놓이느냐에 따라 물리(physical) 캐시 또는 논리(logical) 캐시로 구분된다. 논리 캐시는 프로세서 코어와 MMU 사이에 놓여 가상 주소 공간 안에 있는 코드와 데이터를 참조한다. 물리 캐시는 MMU 와 주메모리 사이에 놓여 물리주소를 사용하여 코드와 데이터를 참조한다.
직접 매핑된 캐시는 주어진 주 메모리 공간에 대해 캐시 안에 하나의 공간만 할당되어 있는 매우 간단한 캐시 아키텍처이다. 직접 매핑 캐시는 스래싱(thrashing)의 지배를 받는다.
여기서 스래싱이란, 캐시 메모리 안에 동일한 위치에 대해 소프트웨어 병목현상이 발생하는 것을 말한다. 스래싱 때문에 캐시 라인을 반복해서 제거하고 로딩하는 작업이 일어난다.
로딩과 제거는 프로그램의 일부를 캐시 라인 안에 있는 동일한 캐시 라인에 매핑되어 있는 주소에서 주 메모리로 이동하는 결과를 야기한다. 스래싱을 줄이기 위해 캐시는 way 라는 더 작은 단위로 쪼개져야 한다. 이 방법을 사용하면 하나의 주 메모리 주소에 대해 캐시 안에 여러개의 저장위치를 제공할 수 있다. 예를 들어 주 메모리안에 있는 어떤 하나의 위치는 캐시 안에 4개의 다른 위치에 매핑될 수 있다. 이 캐시를 세트 연상 캐시라고 부른다.
코어 버스 아키텍처는 캐시 시스템의 설계를 결정한다. 폰노이만 아키텍처는 코드와 데이터를 저장하기 위한 통합 캐시를 사용하고, 하버드 아키텍처는 분할 캐시를 사용한다. 분할 캐시는 명령어를 위한 캐시와 데이터를 위한 캐시가 분리되어 있는 형태를 말한다.
캐시 교체 정책은 캐시 미스가 발생하였을 때 교체를 위해 어떤 캐시를 선택할 지를 결정해준다. 설정된 정책은 캐시 컨트롤러가 캐시 메모리 안에 가능한 설정으로부터 캐시 라인을 선택하기 위해 사용하는 알고리즘을 정의한다. 교체하려고 선택된 캐시 라인을 victim 이라고 한다. ARM 캐시 코어에서 사용 가능한 2 가지 교체 정책에는 의사 랜덤 방식과 라운드 로빈 방식이 있다.
1. 라운드 로빈 방식(또는 주기적 교체) : 단순히 교체될 세트안에 있는 다음 캐시 라인을 선택하는 것을 말한다. 선택 알고리즘은 캐시 컨트롤러가 캐시 라인을 할당할 때마다 증가되는, 순차적으로 증가하는 victim 카운터를 사용한다. victim 카운터가 최대값에 이르면 정의된 베이스값으로 리셋된다.
* 의사 랜덤 교체 방식 : 교체될 세트 안에 있는 다음 캐시 라인을 랜덤하게 선택하는 것을 말한다. 선택 알고리즘은 무작위로 증가하는 victim 카운터를 사용한다. 의사 랜덤 교체 알고리즘에서 컨트롤러는 무작위로 증가값을 선택하고 이를 victim 카운터에 더해서 victim 카운터를 증가시킨다. victim 카운터가 최대값에 이르면 정의된 베이스값으로 리셋된다.
캐시 메모리에 데이터를 쓸 때 사용가능한 방법도 2 가지가 있다. 컨트롤러가 캐시 메모리만 업데이트할 경우에는 후기입 방식이라고 하고, 캐시 컨트롤러가 캐시와 주메모리에 모두 쓸 때에는 연속기입 방식이라고 한다.
1. 연속기입 방식 : 캐시가 적중(hit)을 하면 캐시와 주메모리에 모두 값을 저장한다. 이 방법의 경우 캐시와 주메모리가 항상 일관성을 유지하게 된다. 이 정책하에서는 캐시 컨트롤러는 캐시 메모리에 값을 쓰기위해 주메모리에 쓰는 작업을 수행해야 한다. 주메모리에 값을 저장해야 하기 때문에 연속기입 방식은 후기입 방식보다 느리다.
* 후기입 방식 : 유효한 캐시 데이터 메모리에만 저장하고 주메모리에는 저장하지 않는다. 결과적으로 유효 캐시 라인과 주메모리는 다른 데이터를 포함할 수도 있다. 이 방식의 장점은 서브루틴에 의해 임시 지역변수를 반복적으로 사용할 때에 생긴다. 이 변수들은 원래 임시로만 사용되기 때문에 실제로는 저장할 필요가 없다.
캐시 미스가 발생하였을 때 캐시 컨트롤러가 캐시 라인을 할당하기 위해 사용하는 방법에도 2 가지가 있다. read-allocate 방식이란 데이터를 주메모리에서 읽었을 때 캐시 라인을 할당하고, write-allocate 방식은 주메모리에 쓸 때 캐시 라인을 할당한다.
ARM 에서는 주 메모리의 D 캐시 안에 있는 데이터를 복사한다는 의미로 클린 이라는 용어를 사용한다. 또한 캐시의 내용을 없애버린다는 의미로 플러시라는 용어를 사용한다.
캐시 락다운은 몇 가지 ARM 코어에서 제공되는 특징을 말한다. 락다운 특징은 코드와 데이터를 캐시 쪽에 로드하고 eviction(제거) 로 부터 면제되었다는 것을 표시한다. 락다운 안에 있는 코드나 데이터는 캐시 메모리 안에 저장되어 있기 때문에 보다 빠른 시스템 반응을 제공한다. 캐시 안에 정보를 락(lock) 하는 목적은 캐시 미스 문제를 피하기 위해서다.
메모리 보호 장치(MPU)
메모리를 보호할 수 있는 방법에는 2가지가 있는 데, 첫 번째 방법은 비보호(unprotected) 로 태스크 상호 작용을 위한 규칙들을 관리하기 위해 소프트웨어 제어 루틴을 사용한다.
두 번째 방법은 보호(protected)로 태스크 상호 작용을 관리하기 위해 하드웨어와 소프트웨어를 둘 다 사용하고 있다. 보호 시스템에서는 접근 권한이 침범받았을 경우 하드웨어적으로 abort 를 발생시켜 메모리의 영역을 보호한다. 그러면 소프트웨어는 abort 루틴을 처리하고 메모리 기반의 자원을 제어하도록 반응한다.
ARM MPU 는 시스템 보호를 위한 기본적인 구조로 영역을 사용하고 있다. 영역이란 메모리의 영역과 관련되어 있는 속성값들의 모임을 말한다. 영역은 오버랩될 수 있기 때문에 현재 실행중인 태스크에 의한 원치 않는 액세스로부터 잠들어 있는 태스크의 메모리 영역을 보호하기 위해 배경 영역을 사용할 수 있다.
다양한 영역의 속성을 설정하기 위한 루틴들을 포함하여 MPU 를 초기화하는 데 필요한 여러 단계가 있다.
- CP15:c6 을 사용하여 명령어 및 데이터 영역의 크기와 위치를 설정하는 것이고,
- CP15:c5 를 사용하여 각 영역에 대한 접근권한을 설정하는 것이며,
- 각 영역을 위한 캐시와 쓰기 버퍼 속성을 지정하는 것인데, 캐시를 위해서는 CP15:c2 를, 쓰기 버퍼를 위해서는 CP15:c3 를 사용한다.
- 마지막으로 CP15:c6 을 이용하여 활성화 영역을 인에이블한 다음, CP15:c1 을 사용하여 캐시와 쓰기 버퍼, MPU 를 인에이블하는 것이다.
보호시스템을 정의한 다음에는 보호 시스템을 실행하는 데 필요한 마지막 단계는 태스크 변환을 하는 동안 다음 태스크로 영역 할당을 변경하는 것이다.
메모리 관리 장치(MMU)
MMU 의 핵심기능은 각 태스크들이 그 자신만의 가상 메모리 공간에서 독립적으로 프로그램을 실행할 수 있도록 관리해주는 것이다. 가상 메모리 시스템의 중요한 특징으로는 주소 재배치를 들 수 있다. 주소 재배치란 프로세서 코어가 사용하려는 주소를 주 메모리 안의 다른 주소로 변경해주는 것이다. 이 변환 작업은 MMU 하드웨어에 의해 수행된다.
가상 메모리 시스템에서 가상 메모리는 보통 고정 영역과 동적 영역으로 나누어 진다. 고정 영역에서는 페이지 테이블 안에 매핑되어 있는 변환 데이터가 일반적인 동작이 이루어지는 동안에 변경되지 않는다. 동적 영역에서는 가상 메모리와 물리 메모리 사이에 매핑되어 있는 메모리가 종종 변경된다.
페이지 테이블은 가상 페이지 정보에 대한 설명을 포함하고 있다. 페이지 테이블 엔트리(PTE) 는 가상 메모리 안에 있는 페이지를 물리 메모리 안에 있는 페이지 프레임으로 변환한다. 페이지 테이블 엔트리는 가상 주소에 의해 구조화되어 있으며 한 페이지를 한 페이지 프레임으로 매핑하기 위한 변환 데이터를 포함하고 있다.
ARM MMU 의 기능은 다음과 같다.
- 레벨 1 과 레벨 2 페이지 테이블을 읽고 이것들을 TLB 에 로드한다.
- 최근의 가상 메모리에서 물리 메모리로의 주소 변환값을 TLB 안에 저장한다.
- 가상 주소에서 물리 주소 변환을 수행한다.
- 접근권한을 실행하고 캐시와 쓰기 버퍼를 설정한다.
ARM MMU 에 추가된 주요 특징으로는 고속 문맥전환 확장(FCSE)이 있다. 고속 문맥 전환 확장은 문맥전환중에 캐시나 TLB 를 플러시할 필요가 없기 때문에 멀티 태스크 환경에서 성능을 향상시켜 준다.
작은 가상 메모리 시스템의 동작 예는 멀티태스킹을 지원하기 위해 MMU 를 셋업하는 자세한 방법을 보여주고 있다. 셋업하는 단계는 다음과 같다.
- 가상 메모리의 고정 시스템 소프트웨어에서 사용될 영역을 정의한다.
- 각 태스크들에 대한 가상 메모리맵을 정의한다.
- 그 고정영역과 태스크 영역을 물리 메모리맵에 위치시킨다.
- 페이지 테이블 영역 안에 페이지 테이블을 정의하고 위치시킨다.
- 영역과 페이지 테이블을 생성하고 관리하는 데 필요한 데이터 구조체를 정의한다.
- 페이지 테이블 엔트리를 만들기 위해 정의된 영역 데이터를 사용해서 MMU 를 초기화하고, 그것들을 페이지 테이블에 저장한다.
- 한 태스크에서 다음 태스크로 전환되는 문맥전환 과정을 만든다.
ARM 아키텍처의 미래
ARM 아키텍처는 정적 상태에 머물러 있지 않고, 오늘날의 소비가 기기에서 요구되는 어플리케이션에 맞도록 개발되고 개선되어 왔다. ARMv5TE 아키텍처가 ARM 에 DSP 의 일부를 추가하는 데 매우 성공적이었음에도 불구하고, ARMv6 에서는 또 다시 대규모 멀티프로세서 시스템을 위해 DSP 의 지원을 더욱 확장하고 추가로 지원하였다.
다음의 표는 이 새로운 기술들이 다른 프로세서 코어에 어떻게 매핑되었는가를 보여주고 있다.
프로세서 코어 | 아키텍처 버전 |
ARM1136J-S | ARMv6J |
ARM1156T2-S | ARMv6 + Thumb-2 |
ARM1176JZ-S | ARMv6J + TrustZone |
ARM 은 주요 장점 중의 하나인 코드 밀집도에 지속적인 관심을 보임으로써, 최근에는 Thumb 아키텍처의 확장 버전인 Thumb-2 를 발표하기에 이르렀다. TrustZone 과 관련된 보안에 대한 새로운 관심은 ARM 이 이분야의 선도적인 위치를 차지할 수 있도록 해주었다.
AMBA 버스
ARM 에서 제공하는 AMBA 버스를 사용하면 SoC 설계자 간의 의사소통이 용이하여 SoC 설계시간 및 오류를 단축할 수 있고, IP 의 재사용이 용이해 외부의 AMBA 버스 기반으로 설계된 표준 IP 도입으로 SOC 설계시간을 단축할 수 있다.
AMBA 버스의 구성
고속으로 동작하는 입출력 제어기가 연결되는 버스 인터페이스인 AHB(Advanced High-performnce Bus), ASB(Advanced System Bus) 와 저속의 입출력 제어기가 연결되는 버스인 APB(Advanced Peripheral Bus) 가 있으며, 근래에 발표된 AMBA 버스 사양 3.0 에는 낮은 전력소모와 고속 동작을 지원하는 AXI(Advanced eXtensible Bus) 가 있다.
AHB
특징은 다음과 같다.
- 고속으로 동작
- 파이프라인 동작 지원
- 여러개의 버스 마스터 지원
- 버스트 전송 지원
- 단일 Rising 클럭 에지 사용
AHB 버스는 마스터, 슬레이브, 중재기, 그리고 디코더로 구성되어 있다.
- AHB 마스터 : 읽기 또는 쓰기 동작을 요청하는 주체가 되며, 한 번에 하나의 마스터만 버스를 사용할 수 있다. CPU, DMA 장치등이 마스터가 된다.
- AHB 슬레이브 : 주어진 주소 영역 내에서 마스터의 요청에 따라 읽기 또는 쓰기 동작을 수행하고, 성공여부, 오류 발생여부, 대기 상태와 같은 전송 결과를 마스터에게 보고한다. AHB 슬레이브에는 메모리 제어기, APB 브리지 등이 있다.
- AHB 중재기 : 여러 버스 마스터의 요청을 중재하여 한 번에 하나의 마스터만이 버스를 사용하도록 조정하는 기능을 수행하는 제어 블록이다.
- AHB 디코더 : 마스터의 요청에 따라 전송하고자 하는 슬레이브의 어드레스를 디코드하는 제어 블록으로, AHB 버스에는 하나의 디코더가 있고, 이 하나의 디코더에서 모든 슬레이브를 디코드한다.
ASB
AHB 버스와 마찬가지로 고속으로 동작하는 입출력 제어기를 연결하기 위한 규격이다. 사실 ASB 버스는 AHB 버스보다 먼저 설계된 고속 버스로 ARM 의 내부 버스의 구조에 기본을 두고 있다. ASB 도 고속으로 동작하기 위하여 파이프라인 동작을 지원하고, 여러 개의 버스마스터가 사용될 수 있다. 하지만 ASB 버스는 AHB 버스와 달리 rising-edge 와 falling-edge 클럭을 모두 사용하도록 설계되었다.
APB
APB(Advanced Peripheral Bus) 버스는 브리지와 슬레이브로 구성되어 있다. APB 브리지는 AHB 버스와 연결되어 AHB 에서 구동된 어드레스를 일시적으로 유지하고, APB 슬레이브를 디코드하는 기능을 가지고 있으며, APB 슬레이브를 제어하기 위한 신호를 생성한다. APB 버스는 비교적 속도가 느린 주변 장치를 제어하고 전력 소모를 줄일 수 있도록 구성하기 위하여 간단한 인터페이스 구조를 가지고 있으며, 래치 타입의 어드레스와 제어신호를 가지고 있어 전력소모를 줄일 수 있도록 설계되어 있다.
AXI
AXI(Advanced eXtensible Interface) 버스는 AMBA 버스 사양 3.0 에서 제시된 고속의 버스로, 버스 사이클은 어드레스 구동단계, 제어신호 구동단계와 데이터 구동단계로 분리되어 있으며, 정렬되지 않은 데이터 전송 및 버스트 전송을 지원하고 있다. AXI 버스는 데이터를 읽기 위한 데이터 채널과 데이터를 쓰기 위한 데이터 채널로 분리되어 있고, 고속 동작에 적합하게 설계되어 있으며, 전력소모를 줄이도록 설계되어 있다.
FAQ
새롭게 알게된 점들을 정리했다.
LDR r0, =0x12345678 의 뜻은?
명령어에서 이해가 안갔던 부분은 '=' 이었다.
'=' 은 ARM 어셈블러에서 사용되는 특수 기능으로서 프로그래밍을 좀 더 간편하게 해주는 용도로 쓰인다.
위의 명령어를 처리하기 위해서는 두 번의 오퍼레이션이 필요한데, 그것을 한 번에 처리할 수 있도록 해준다.