작은 것부터 큰 것까지 순서대로 register 제어 → memory 제어 → memory controller 제어 → LCD(peripheral device)를 제어하는 방법에 대해서 배워봅시다.
1. Device control
1.1. Device를 control 하는 방법
Device를 control 한다는 것은 임베디드 시스템 엔지니어로서 당연히 마음 설레고 즐거운 일입니다. Device는 보통 peripheral device, MCU 외부에 핀을 통해 달려있는 IC를 의미합니다.
통신방법은 어렵지 않습니다. 정해진 규칙에 따라 특정 핀에 신호를 어느 타이밍에 인가하고 일정시간 유지했다 땜으로써 peripheral device와 통신하는데 이러한 timing, bus interface는 datasheet를 잘 읽으며 파악해야 합니다.
예를 들어 NOR flash의 read instruction 타이밍도가 위와 같이 있다고 가정합시다.
- CS_N (low active, Chip Select) 핀에 low 신호를 인가합니다.
- t(ard) 시간이 지난 뒤 OE_N (low active, Output Enable)핀에 low 신호를 인가합니다.
- t(a) 시간이 지나면 DATA 핀으로 원하는 데이터가 나옵니다.
이렇게 datasheet에 타이밍과 순서를 비롯해 여러 정보가 있으므로 이를 파악하고 따라야 합니다.
1.2. Memory mapped I/O
Memory mapped I/O란, 메모리의 일부 영역을 레지스터 및 외부 I/O을 위한 영역으로 할당해서 별다른 방법을 취하지 않고도 메모리에 Read/Write 함으로써 레지스터/외부 peripheral과 I/O 하는 것을 말합니다. co-processor든, 메모리든, 외부 peripheral이든 모두 메모리에 접근하듯 똑같이 align 된 주소를 통해 접근해서 I/O 할 수 있습니다.
레지스터에 값을 쓴다, 레지스터를 설정한다는 것은 'datasheet를 통해 주소와 각 bit의 용례를 파악한 뒤 특정 bit에 0 또는 1 비트값을 쓴다는 것'을 의미합니다.
이때, 어떤 레지스터의 주소를 알고 있고 특정 bit를 수정하기 위해 직접 주소를 계산한 뒤 접근해서는 안됩니다.
- 무슨 뜻이냐면, `0x1234_5678`에 있는 32-bit 레지스터 A가 있고 9번째 bit를 수정하고 싶을 때,
- 1바이트 뒤인 `0x_1234_5678 + 0x1 = 0x1234_5679`에 접근해서는 안됩니다.
- Address align 관련 exception이 나기 때문입니다. 무조건 1-WORD 단위로 맞춰서 접근하고, offset을 더해서 원하는 bit를 가리킵시다.
예제를 통해서 memory mapped I/O가 실제로 어떻게 일어나는지 알아봅시다.
LCD device의 datasheet를 확인하니 위와 같은 블록 다이어그램이 나왔습니다.
- 데이터버스의 width는 16-bit입니다. 16개의 데이터핀이 나와있습니다.
- WRITE/ READ 핀으로 읽고 쓰기 명령을 줄 수 있습니다.
- 주소핀이 따로 없고 ADS(A[7])핀만 나와있습니다. 이를 토대로 데이터버스는 주소를 쓸 때, 데이터를 쓸 때 모두 사용하며 이를 ADS핀으로 제어하는 것이라고 추정할 수 있습니다.
Address Target Size
0x0000_0000 SDRAM_CS0/ 32MB
0x0800_0000 SDRAM_CS1/ 32MB
.... 생략 ....
0x2000_0000 LCD_CS_N/ 3MB
.... 생략 ....
MCU의 datasheet를 확인하니 위와 같은 메모리맵을 발견했습니다. 메모리의 `0x2000_0000`으로부터 3MB 영역이 LCD device를 위해 할당된 것을 알 수 있습니다. 따라서 저희는 `0x2000_0000~0x20FFF_FFFF`에 접근해 R/W를 함으로써 `LCD_CS`핀이 활성화되며 LCD를 제어할 수 있습니다.
; Command 주기
LDR R0, #0x20000080 ; LCD_CS 활성화 + ADS[7] High = Command
LDR R1, #0xABCD ; 0xABCD라는 command
STR R1, [R0] ; LCD controller에게 Command를 Data Line을 통해서 전달.
; Data 주기
LDR R0, #0x20000000 ; LCD_CS 활성화 + ADS[7] Low = Data
LDR R1, #0x1234 ; 0x1234라는 data
STR R1, [R0] ; LCD controller에게 Data를 Data Line을 통해서 전달.
Datasheets를 통해 알게 된 내용으로 작성한 어셈블리는 위와 같습니다.
- Command를 날릴 때는 A[7]을 High로 줘야 하기 때문에 `0x2000_0000 + 0x80 = 0x2000_0080`을 base 주소로,
- Data를 날릴 때는 A[7]을 Low로 줘야하기 때문에 `0x2000_0000`을 base 주소로 줬습니다.
이를 C언어로 나타내면 아래와 같겠죠? (#define문 MACRO 테크닉은 잠시 뒤에 다룹니다.)
#define Main_LCD_Write_cmd (cmd) (*(volatile word *)(0x200000080) = cmd
#define Main_LCD_Write_data (data) (*(volatile word *)(0x200000000) = data
Main_LCD_Write_cmd (0xABCD);
Main_LCD_Write_data (0x1234);
결국 핵심은 메모리 R/W 하듯이 특정 메모리 주소 영역에 접근하면, 주변장치를 다룰 수 있다는 점입니다.
Wait state
메모리든 peripheral device든 주소와 명령을 주고 유효한 데이터가 나올 때까지는 물리적으로 시간이 소요될 수밖에 없습니다. 속도가 빠른 프로세서는 불가피하게 느린 device들을 위해 기다릴 수 밖에 없습니다. 따라서 프로세서가 얼마나 몇 cycle 기다려줄지 device 연결할 때 레지스터값을 수정해서 설정하는데, 이 값을 wait state라고 합니다. 이 wait state를 문제없이 동작하는 한도 내에서 최소로 설정해야 메모리가 최대 성능을 낼 수 있습니다.
사용하는 NOR Flash의 Read operation 타이밍도를 확인해 보니 원하는 주소를 넘겨준 뒤 유효한 데이터가 나올 때까지 t_RC (Read Cycle Time)이 최소 65ns가 소요됩니다. 이건 물리적으로 어떻게 극복할 수 없고 기다려야 하죠.
NOR Flash와 I/O를 할 MCU의 datasheet를 확인해 보니, 주소와 CE를 넘겼을 때 유효한 데이터가 나올 때까지 기다리는 시간 t_ACSDV (Address and chip select active to data valid)이 최소 (T-21)+WT라고 나와있다고 가정합시다. T는 MCU의 clock 주파수의 주기, W는 wait state 개수입니다. S5PC110은 800MHz이므로 주기는 1.25ns입니다.
$(1.25 - 21) + 1.25W > 65$
$1.25W > 84.5$
$W > 67.6$
따라서 이 MCU에서 이 flash를 사용하려면 Wait state로 최소 68-cycle을 기다려야 합니다. 더 성능이 좋은 플래시메모리를 사용하는 게 좋겠네요. (교재 기준으로 50MHz MCU에서는 3-cycle 기다리면 됩니다.)
2. HW Control
2.1. PLL (Clock)
Chapter 1에서 저희는 PLL(Phase Locked Loop) 회로가 feedback 구조를 통해 원하는 주파수(clock)를 만든다고 배웠고, Chapter 4에서 Reset handler 및 bootloader가 하는 작업 중에 PLL 설정이 있다는 것도 배웠지요.
- 오실레이터로 기준 주파수 TCXO를 인가합니다.
- 펄스-전압변환기와 VCO(Voltage Controlled Oscillator)를 거쳐서 증폭된 주파수가 나옵니다.
- 그러나, 증폭된 주파수 특성상 외부환경(온도, 습도, 회로 길이 등)에 취약해 주파수가 흔들립니다. (100MHz를 원하는데 99~101MHz 이런식으로)
- 출력 주파수를 1/M 한 뒤 다시 피드백해서 입력값(TCXO)과 얼마나 차이가 나는지 파악하면서 보정합니다.
- 출력 주파수는 보정되면서 원하는 주파수가 일정하게 출력됩니다.
이렇게 PLL 회로는 N과 M 값을 조정하며 주파수를 증폭하고 다운샘플링하고 피드백하며 원하는 증폭된 주파수를 일정하게 출력하는 회로를 말합니다. 그리고 이 N, M은 레지스터를 수정하면서 SW적으로 조정할 수 있습니다. MCU의 clock 관련 레지스터를 확인해 보면, 원하는 PLL 출력값을 얻기 위해 어떻게 레지스터를 조정하면 좋은지 datasheet에 나와있습니다.
2.2. GPIO
GPIO(General Perpose I/O) pin들은 사용자가 원하는 대로 I/O 용도로 사용할 수 있는 핀입니다. 저희한테는 I/O를 위한 핀이지만, ARM Core MCU 입장에서 GPIO는 AMBA bus에 연결된 하나의 peripheral device로 인식됩니다. 따라서 메모리에는 GPIO를 위한 영역이 있으며 Memory mapped I/O에 의해 GPIO register들도 이 영역 내 특정 주소에 접근해 control 가능합니다.
우선 CPU는 GPIO를 사용하기 위해 GPIO 레지스터들을 설정해야 합니다.
- Mode: 이 핀을 GPIO로 쓸 건지, alternative functionality 핀으로 쓸건지 결정합니다. Alternative functionality로 설정할 경우 프로세서에 의해 사전약속된 기능으로 사용됩니다.
- 방향: 이 핀을 INPUT으로 쓸건지 OUTPUT으로 쓸건지 결정합니다.
- Command: Read 할 건지 Write 할건지 결정합니다.
- Interrupt: 이 GPIO 핀에 인터럽트를 설정할 건지, 어떤 타이밍(CPU clock의 rising edge 또는 falling edge)에 걸릴 건지 결정합니다.
이렇게 GPIO 레지스터들을 설정하면, 드디어 사용자가 원하는 대로 값을 읽고 쓸 수 있게 됩니다.
2.3 DMA
DMA (Direct Memory Access)는 CPU를 대신해서 데이터를 송수신하는 HW입니다.
대량의 데이터를 타깃 device에게 송수신할 때는 CPU가 계속해서 자신보다 상대적으로 느린 device에게 맞춰서 기다려줘야 하기 때문에 wait state가 잔뜩 쌓이고, 시스템 성능이 전체적으로 낮아지게 됩니다. 따라서 CPU는 DMA에게 '네가 대신해서 처리해'라고 source와 destination 그리고 전송할 바이트수를 전달하며 명령을 내립니다. 메모리-target device 간 대량 데이터 I/O를 DMA가 대신 수행합니다. I/O가 끝났다면 '다 됐어요!' 라며 CPU에게 인터럽트를 통해 알립니다.
DMA는 두 가지 전송모드가 있습니다.
- Single address mode: BR(Bus Request), BG(Bus Grant) 신호로 매 cycle마다 bus 사용권을 얻어서 1-byte를 전송한 뒤 사용권을 반환합니다. 그러므로 전송속도는 느립니다. 하지만, CPU나 peripheral이 언제든지 bus 사용권을 획득할 수 있어서 시스템 안정성은 높습니다.
- Burst address mode: Block transfer이라고도 불리며, CPU로부터 요청된 데이터 전송이 끝날 때까지 멈추지 않고 계속해서 bus를 사용해서 전송합니다. 따라서 전송속도는 빠르지만, 다른 IP들은 bus를 사용할 수 없어 CPU가 기다리는 경우가 생길 수도 있습니다.
대량 I/O에 아주 유용하지만, DMA는 조심해서 사용해야 합니다. 왜냐하면, cache와 밀접한 연관이 있어서 메모리와 캐시메모리 사이의 불일치인 cache incoherence를 발생시킬 수 있기 때문입니다. 바로 밑에서 다룹니다.
2.4. Cache
Cache는 temporal/ spartial locality에 따라 자주 사용할 가능성이 높은 데이터와 그 인접 데이터들을 저장하는 아주 빠르고 작은 SRAM 같은 메모리입니다.
전체 시스템 성능 향상을 위해 cache에 있는 데이터는 프로세서가 바로 써먹은 뒤 수정내역도 cache에만 적용합니다. 그래서 필연적으로 수정/변경내역이 cache에는 있지만 메모리에는 없는, 이른바 일관성이 깨지는 cache incoherence가 발생합니다.
이를 방지하기 위해 cache에 쓸 때는 두 가지 정책 중 하나를 고릅니다.
- Write through: Cache가 갱신되면, memory도 똑같이 갱신합니다. 일관성 문제에서는 벗어나지만, 어쨌든 메모리에 접근하기 때문에 cache를 사용하는 의미가 줄어든다는 단점이 있습니다.
- Write back: Cache만 갱신하고, 갱신된 cache line에는 dirty bit라는 표시를 해줍니다. cache가 가득 찼을 때, dirty bit가 표시된 cache line들을 지우고 memory에 반영합니다.
- 이때, 갱신된 내용을 메모리에 write 하는 과정을 조금이나마 빠르게 하기 위해 마치 DMA처럼 갱신된 데이터를 따로 보관했다가 CPU 대신에 메모리에 전송하는 것을 담당하는 FIFO buffer를 사용합니다.
Cache incoherence를 해결하기 위해 2가지 방법을 사용합니다.
- Cache flush: 이름 그대로 incoherence가 발생했다고 가정하고 그냥 cache를 싹 비우는 방법입니다.
- Cache clean: Dirty bit가 set 된 cache line 또는 block만 메모리에 갱신합니다.
Cache가 가득찼을 때 버려야 할 blockd을 선택하는 방법을 replacement policy라 합니다. 보통 random, round-robin 방식을 사용합니다. CP15의 CR 레지스터의 14번 bit를 MCR, MRC 명령어로 바꿔서 정책을 결정할 수 있습니다.
2.5. MMU (Memory Management Unit)
MMU의 역할은 3가지 입니다.
- 가상주소(virtual address, logical address)와 물리주소(physical address) 사이의 변환
- 물리적으로는 여러 군데 나뉜 주소영역들을 하나의 연속적인 메모리 (가상주소)로 취급
- 메모리 영역에 특성 부여 (RO 영역, Cache/Non-cache 영역 등)
MMU가 제역할을 수행하기 위해서는 필요한 준비물이 2가지 있겠지요.
- 가상주소-물리주소를 번역해주는 테이블 (Translation page table): 메모리 또는 SRAM
- 이 테이블의 주소 (TTB, Translation Table Base Address): 레지스터
MMU의 동작과정을 간략하게 설명하면 다음과 같습니다.
- CPU가 메모리의 어느 주소를 접근하기 위해 가상주소를 이용합니다.
- MMU는 TTB를 통해 page table에 접근합니다.
- MMU는 가상주소에 맵핑되는 물리주소를 찾습니다.
- 메모리 내 해당 물리주소에 접근해 데이터를 가져와 CPU에게 전달합니다.
그렇다면 Page table은 크기가 얼마나 될까요? (32-bit - ARMv4, ARMv5 기준
- 우선 page table의 1개 entry (또는 line)은 물리주소 1MB 영역과 맵핑됩니다. (이를 section이라 부릅니다.)
- 32-bit, 2^32 = 4GB 메모리공간을 그대로 가리킬 수 있어야 하므로 4,096개의 entry가 있어야겠죠?
- 1개 entry당 4-byte일테니 `int page_table[4096]`이라고 생각하면, 기본 page table의 크기는 16KB입니다.
하나의 섹션(1MB)보다 더 작은 단위로 메모리를 나눠서 맵핑할 수도 있습니다.
- Section - Level 1 page table의 1개 entry를 의미하며 1MB 단위로 맵핑됩니다.
- Coarse page table
- 1개의 section을 더 작게 64KB 또는 4KB 단위로 쪼갠 page table입니다. 이를 Level 2 page table이라 합니다.
- 첫 22-bit가 이 level 2 page table의 base 주소를 가리킵니다.
- table의 하위 2-bit가 `01`이면 coarse page table을 가리킨다는 뜻입니다.
- Fine page table
- 1개의 section을 더 작게 1KB 단위로 쪼갠 page table입니다. 역시 Level 2 page table입니다.
- 첫 20-bit가 이 level 2 page table의 base 주소를 가리킵니다.
- table의 하위 2-bit가 `11`이면 fine page table을 가리킨다는 뜻입니다.
Coarse page table에서 각 entry의 하위 2-bit를 보면,
- `01`일 경우 Large page (64KB)입니다. Coarse page table이 이걸로만 구성돼 있다면, 16개의 entry가 있겠지요.
- `10`일 경우 Small page (4KB)입니다. Coarse page table이 이걸로만 구성돼 있다면, 256개의 entry가 있겠지요.
- (Coarse page table에는 tiny page가 없습니다.)
Fine page table에서 각 entry의 하위 2-bit를 보면,
- `01`일 경우 Large page (64KB)입니다. Tiny page table이 이걸로만 구성돼 있다면, 16개의 entry가 있겠지요.
- `10`일 경우 Small page (4KB)입니다.page table이 이걸로만 구성돼 있다면, 256개의 entry가 있겠지요.
- `11`일 경우 Tiny page (1KB)입니다. Tiny page table이 이걸로만 구성돼 있다면, 1024개의 entry가 있겠지요.
위 내용을 하나의 그림으로 정리하면 다음과 같습니다.
예시
실제 MMU의 page table을 보면서 정말로 위와 같은 과정을 거치는지 확인해 봅시다.
본격적으로 들어가기 전에
1. ARMv4, ARMv5 기반 32-bit 아키텍처입니다.
2. 8자리 숫자 `xxxxxxxx`는 각 자리가 16진수이며 각 숫자는 2^28, 2^24, 2^20, 2^16, 2^12, 2^8, 2^4, 2^0를 의미합니다.
3. 크기는
`0x0000_1000` = 2^12 = 4KB
`0x0001_0000` = 2^16 = 64KB
`0x0010_0000` = 2^20 = 1024KB = 1MB 입니다.
ⓐ: 첫 9MB는 비어있네요. 이 가상주소는 어느 물리주소에도 맵핑돼있지 않습니다.
ⓑ: 이 섹션은 동일한 주소의 물리주소 1MB와 맵핑돼 있습니다.
ⓒ: 이번에는 섹션이 아니고 size를 보니 4KB, coarse page table이네요. ⓓ도 size가 64KB 인 것을 보아하니 large page를 갖는 coarse page table인 것 같습니다.
위 그림은 개발자가 보기 쉽도록 깔끔한 형식으로 정리된 내용이므로,
TTB가 가리키는 주소를 덤프 해서 page table 내부가 어떻게 생겼는지 확인해 봅시다.
TTB를 확인하니 `0x00A2_4000`을 가리키고 있었다고 가정하고 덤프 한 내용이 위와 같다고 생각합시다.
- 위에서 ⓐ 섹션 9MB가 비어있었죠? 실제로 1~9번째 엔트리가 아무 내용 없이 비어있네요. 이렇게 아무것도 mapping 되지 않은 영역을 접근하면 abort가 발생합니다.
- 10번째 엔트리 ②는 `0x0090_0D62 = 0000_0000_1001_0000_0000_1101_0110_0010`입니다.
- LSB가 `10`으로 끝나는 것을 보니 section, Level 1 page를 의미합니다.
- MSB 12-bits를 제외한 나머지 bits를 0으로 초기화시키면 물리주소의 base 주소가 됩니다.
- 따라서 `0x0090_0000`으로부터 1MB 영역의 물리주소와 맵핑되는 것을 알 수 있습니다.
- ⓑ 내용과 같은 것을 확인할 수 있습니다.
- 12번째 엔트리 ③은 `0x00A4_1561`입니다.
- LSB가 `01`로 끝나는 것을 보니 coarse page table, Level 2 page입니다.
- MSB 22-bits를 제외한 나머지 bits를 0으로 초기화시키면 coarse page table의 base 주소가 됩니다.
- 따라서 `0x00A4_1400`으로부터 1MB 영역이 coarse page table이 됩니다.
- 첫 엔트리는 비어있고
- 두 번째 엔트리 (2)를 보니 `0x00B0_1FFA`
- LSB가 `10`으로 끝나는 것을 보니 4KB짜리 small page 인 것을 알 수 있습니다.
- `0x00B0_1000`으로부터 4KB 영역이 물리주소와 맵핑됩니다.ㄷ
- 그 뒤로 14개 엔트리가 똑같이 4KB짜리 small page입니다. ⓒ와 같습니다.
- 16번째 엔트리 (3)을 보니 `0x00B1_0FF9`
- LSB가 `01`로 끝나는 것을 보니 64KB짜리 large page입니다.
- `0x00B1_0000`으로부터 64KB 영역이 물리주소와 맵핑됩니다.
- 이후 14개 엔트리가 똑같이 64KB짜리 large page 입니다. ⓓ와 같습니다.
정리하면, 비어있는 4KB 엔트리 + 15개 small page + 15개 large page = 1MB입니다.
1개의 섹션을 더 세밀하게 표현하고있네요.
2.6. TLB (Translation Lookaside Buffer)
TLB는 MMU의 page table에 대한 cache라고 이해하면 됩니다.
Page table이 SRAM에 있든 메모리에 있든 결국 프로세서는 매번 메모리의 데이터를 취득하기 위해서 주소변환을 위해 1번, 데이터 취득을 위해 1번 총 2번 메모리에 접근해야 하기 때문에 느립니다. 메모리 접근횟수를 최소로 만들기 위해서 page table의 cache를 만든 것입니다.
- CPU가 가상주소로 메모리 접근을 요청합니다.
- TLB에 해당 가상주소가 저장돼 있는지 확인합니다.
- 있다면, 바로 물리주소를 반환합니다.
- 없다면, 어쩔 수 없이 MMU의 page table로 가서 물리주소로 변환합니다.
- 그리고 이후 다시 참조할 수도 있으니 TLB로 가져옵니다.
'Embedded' 카테고리의 다른 글
[정리/요약] ARM Cortex-M 프로그래밍 (0) | 2025.02.11 |
---|---|
임베디드 레시피 Chapter 8. Debug (0) | 2025.02.01 |
임베디드 레시피 Chapter 6. RTOS & Kernel (0) | 2025.01.31 |
임베디드 레시피 Chapter 5. SW ② 스택 (0) | 2025.01.29 |
임베디드 레시피 Chapter 4. ARM ② Assembly와 Bootloader (1) | 2025.01.29 |