Embedded

임베디드 레시피 Chapter 1. HW ② 컴퓨터구성

soreDemo 2025. 1. 19. 01:30

1. 논리회로

논리적인 순서로 데이터를 제어해 digital 신호를 input으로 넣었을 때 원하는 output을 만들어내는 회로

 

사람들이 논리회로를 설계하다 보니 7가지 부분집합이 공통적으로 자주 사용되는 것을 파악했습니다. 이를 엮어 규칙으로 만들고 논리회로 소자로 만들었습니다. 각 논리소자를 gate라고 부릅니다.

 

 

예를들어, NOT은 트랜지스터 1개로 AND는 트랜지스터 2개를 직렬로 연결, OR은 2개를 병렬로 연결해서 만들 수 있습니다.

 

원하는 output을 만들 때는 카르노맵(Karnaugh map, K map)이라는 간단한 도구를 이용합니다. Input 개수 3, 4개까지는 충분히 손으로 최적화된 결과를 만들 수 있도록 도와줍니다.


2. 레지스터

2.1. Latch와 Flip Flop

모든 회로를 AND나 OR 같은 논리회로 소자만 사용하다 보니 한계가 드러납니다.

  • 예를 들어, 2-bit adder를 이용해서 자기 자신을 20번 더해야 하는 상황이 있다고 생각해 봅시다.
  • 20번 더해야 하므로 AND, XOR gate를 이용해서 만든 2-bit adder 20개를 직렬로 연결해야 합니다.
  • 만일 중간결괏값을 저장할 수 있는 방법이 있다면, 2-bit adder 1개의 output을 임시저장한 뒤, 그걸 다시 input으로 넣고, 이걸 20번 반복하면 원하는 정답을 낼 수 있습니다.

따라서 Input이 들어오면 output이 나오는 단순한 논리소자를 넘어, 정보를 저장하고 있다가 원하는 시점에 써먹을 수 있는 논리소자가 필요하게 됐습니다. 1-bit 정보를 저장하고 기억할 수 있는 논리소자'latch'라고 하고, 그중에서도 clock 신호에 따라 동기화 돼 동작하는 latch를 Flip Flop (F/F')이라 합니다. 레지스터는 CPU가 적은 양의 데이터 / 연산 중간 결과를 일시적으로 저장하기 위한 고속의 기억회로로 플립플롭의 집합입니다.

가장 간단한 형태의 플립플롭인 SR F/F'입니다. AND와 NOR의 집합으로 나타낼 수도, NAND로도 나타낼 수도 있지만, 중요한 것은 gate의 output이 다시 자기 자신의 input으로 들어오는 피드백 구조라는 점입니다. S, R 같은 플립플롭의 control 신호는 가운데 NAND latch처럼 단독으로 움직일 수도 있고, 왼쪽 AND+NOR F/F'처럼 외부 clock에 맞춰 움직일 수도 있습니다. 우측 진리표를 보면, S와 R이 동시에 0인 경우를 제외하고는 output인 Q의 값이 계속 유지되거나 반댓값으로 바뀌어 유지되는 것을 알 수 있습니다. 이렇듯 논리소자 여러 개를 사용해서 1-bit의 정보를 유지할 수 있습니다. (최소 4개~6개의 트랜지스터를 사용합니다.)

2.2. Clock과 Timing diagram

동기화: 디지털 시스템 속 심장박동인 CPU의 clock에 맞춰 박자도 순서도 맞춰 진행하는 것을 의미합니다.

  • 위 그림의 좌측 회로를 보면, S와 R input에 clock이 연결된 것을 알 수 있습니다.
  • Clock 신호가 S와 R과 AND gate에 엮여있습니다. 따라서 clock이 high일 때만 S 또는 R의 값이 유효한 값을 가지게 됩니다.
  • Clock이 low에서 high로 ↗ 올라갈 때 동기화하는 것을 high edge trigger, 반대로 ↘ 내려갈 때 동기화하는 것을 low edge trigger라고 부릅니다.

Clock은 빠를수록 좋겠지만, 무작정 빨라질 수는 없습니다.

  • 트랜지스터의 물성상 On/Off 되는 데 최소한의 일정 시간 (delay time, 전달지연시간)이 소요됩니다. 신호가 low에서 안정적인 high 신호 (또는 high에서 안정적인 low)가 되기까지 소요되는 시간을 의미합니다.
  • 또한, 처리속도가 다른 회로와 소자들끼리 연결돼 있는 경우에, 올바른 동기화를 위해 시스템은 가장 느린 소자에 clock을 맞춥니다.

MCU 및 소자의 datasheet를 읽다 보면 위와 같은 타이밍도 (Timing diagram)를 만나볼 수 있습니다. 해석하는 방법은 간단합니다.

  • Address: 주소가 전달됩니다. 하나의 뭉텅이 <-->가 하나의 주소를 의미합니다. t_ACC (Address Access Time)은 타깃 주소가 들어간 뒤 유효한 데이터가 나올 때까지의 시간을 의미합니다. 표에 따르면 65ns를 넘으면 안 되네요.
  • CE/: 윗첨자는 바(bar)라고 읽으며, CE (Chip Enable) 핀이 low active라는 것을 의미합니다. 회색으로 표시된 영역은 High 또는 low 둘 다를 의미합니다. t_CE는 명령이 들어간 뒤 유효한 데이터가 나올 때까지의 시간을 의미하며 표에 따르면 65ns를 넘으면 안 됩니다.
  • 오른쪽 표의 t_RC는 read cycle time을 의미하는데, address line에 최소한 저만큼의 시간 동안 주소를 가리키고 있어야 유효한 데이터를 얻을 수 있다는 뜻입니다. 표에 따르면 최소 65ns 동안 타깃 주소를 가리키고 있어야 output에 유효한 데이터가 나옵니다.

이렇듯 datasheet와 타이밍도에는 유효한 데이터를 얻기 위해 어느 핀을 어떤 순서로 어떤 값(Low/High)을 줘야 하고 얼마만큼 시간 동안 유지해야 하는지 상세하게 나와있습니다.


3. Bus

Bus는 장치들이 정보공유를 하기 위해서 공유하는 선들의 집합입니다.

하나의 bus를 다양한 slave들이 공유하므로

  • Digital 신호가 bus를 통해 '이동한다'보다는 특정 시점에 bus를 봤을 때, bus를 '점유하고 있는 어떤 장치의 신호'만이 보입니다.
  • 서로 다른 slave들이 자신이 bus를 쓰겠다고 요청할 때, 순서가 꼬이지 않도록 중재해 주는 역할을 아비터(Arbiter)가 하며 CPU의 CU(Control Unit)가 이러한 신호등 역할을 맡습니다.

4. Memory

임베디드 시스템에서 메모리 선택은 메모리맵, 하드웨어 구성 같은 기본적인 시스템 구성과 성능에 가장 큰 영향력을 미칩니다.

4.1. 메모리 종류

  • RAM: 가만히 놔두면 데이터가 날아가버리는 휘발성 메모리입니다.
    • SRAM: 트랜지스터 6개로 구현된 순수하게 Random access가 가능한 메모리입니다. RAM 중에서 속도는 가장 빠르고 가격도 가장 비싸기 때문에 CPU 가장 인근의 L1~L3 캐시 메모리에 사용됩니다.
    • DRAM: 트랜지스터 1개와 캐패시터 1개 (1T1C) 구조로 구현된 가장 값싼 메모리입니다. 캐패시터에 전하를 채우고 비움으로써 데이터를 기록합니다.
      • SRAM에 비해 구조가 단순하기 때문에 소형화 및 집적도를 높여 용량을 뻥튀기하기 용이합니다.
      • 하지만, 집적도가 높아지며 매우 작아진 캐패시터의 크기와 인근 캐패시터 및 도선끼리 영향을 받으며 캐패시터에서 끊임없이 빠르게 전하가 누설됩니다.
      • 전하 누설 = 데이터 유실이므로 캐패시터가 갖고 있는 전하량을 파악한 후 주기적으로 계속 공급데이터를 유지해줘야만 합니다. 이때 PL172 규격을 따라 일정시간마다 refresh를 진행합니다.
      • CPU와 박자를 맞추면서 데이터를 주고받는 걸 Sychronous DRAM(SDRAM)이라고 합니다. 이때, clock의 rising edge, falling edge 양쪽 모두에서 동작해 데이터를 2배 빠르게 주고받는 메모리를 DDR SDRAM이라고 합니다.
      • DRAM에 대한 자세한 설명
    • PSRAM: 구조적으로는 DRAM이나, HW적으로는 refresh 및 recharge를 자동으로 해주는 회로가 내장돼 charge control이 필요 없어 SRAM처럼 쓸 수 있는 메모리입니다.

  • ROM: 가만히 놔둬도 데이터를 유지하는 비휘발성 메모리입니다. 과거에는 다양한 종류가 있었지만, 다양한 원인으로 1회만 쓸 수 있거나, 굉장히 제한적으로나마 데이터를 수정하는 게 가능한 ROM이 대다수였습니다. 반면 플래시 메모리는 특별한 규칙만 지킨다면, 읽고 쓰고 수정하는 것이 자유롭고 대용량화도 용이하므로 현재는 플래시 메모리만 사용합니다.
    • NOR Flash: 메모리 cell이 병렬로 연결돼 있습니다, word line, bit line을 이용해서 byte 단위로 random access 할 수 있습니다. NAND 플래시보다 구조적으로 더 큽니다. 특정 byte에 바로 접근할 수 있기 때문에 read는 빠르지만, bulk 처리면에서는 NAND보다 뒤처지므로 write, erase는 느립니다.
    • NAND Flash: 메모리 cell이 직렬로 연결돼 있습니다. 따라서 byte 단위로 random access 할 수 없으며, 한 번에 page 단위 (약 0.5KB~4KB)로 읽습니다.

4.2. XIP (eXecution In Program)

메모리 위에서 직접 프로그램을 실행하는 기술을 의미합니다.
Byte/WORD 단위로 random access 가능해야 하므로 모든 RAM 및 NOR 플래시는 XIP가 가능합니다.

대다수 시스템에서 사용하는 NAND 플래시는 XIP가 불가능합니다만, 여전히 부트로더와 스타트업 코드(`start.s`)는 플래시 메모리에 있습니다.

  1. 이를 실행하기 위해서 CPU는 현재 시스템에 연결된 스토리지 종류가 NOR 인지 NAND 인지 파악합니다.
  2. NAND라면, ② NAND 첫 번째 block (다수의 page 집합)을 CPU 내에 있는 IRAM(Internal RAM)으로 읽어 들입니다.
  3. 부트로더는 NAND 두 번째 블록 이후에 들어 있는 startup 코드 프로그램을 메모리로 모두 복사합니다.
  4. 부트로더는 메모리의 시작주소로 jump 합니다.

4.3. 메모리 동작

기본적인 메모리 동작에 대해서 배워보겠습니다. 우선 좌측 그림의 메모리는
Address pin이 8개로 $2^8 = 256$바이트만큼의 크기를 지정할 수 있고,
data pin도 8개로 8-bit 데이터가 출력되며,
읽고 쓰기 동작은 RD핀과 WR핀으로 제어합니다.

 

◈ Read의 경우: RD = `1`, WR = `0`으로 설정한 후, 원하는 주소를 address pin으로 보내면, 조금 뒤에 data pin으로 주소에 해당하는 데이터가 나옵니다.

 

◈ Write의 경우: 이번에는 반대로 RD = `0`, WR = `1`로 설정한 후, 원하는 주소를 address pin으로, 원하는 데이터를 data pin으로 보내면 해당 주소에 데이터가 써집니다.

 


5. CPU

CPU는 간단하게 설명하면, 명령어를 읽는 fetch, 해석하는 decode, 수행하는 execute을 반복하는 논리회로의 집합체입니다. 약속된 신호를 주면, 약속된 동작을 수행하는 단순한 원리로 동작합니다.

 

예시를 통해 CPU 동작과정이 정말 3가지로 이뤄지는지 함께 들여다봅시다.

word a = 1;
word b = 2;
word c;

void add() {
    c = a + b;
    return;
}

주소        어셈블리
0x1000      LOAD 0x2000   ; a를 data register에 load
0x1002      ADD 0x2002    ; Data register에 저장된 값(a)과 b를 더해 data register에 저장.
0x1004      STORE 0x2004  ; Data register에 저장된 값(a + b)를 c에 저장.

전역변수 `a`는 `0x2000`번지에, `b`는 `0x2002`번지 그리고 `c`는 `0x2004`번지에 저장돼 있다고 가정합시다.

  1. '덧셈'을 발견한 컴파일러는 피연산자들을 레지스터로 불러오기, 덧셈하기, 결과 저장하기 어셈블리를 생성합니다.
  2. `LOAD 0x2000`
    1. PC(Program Counter)는 `0x1000`번지를 가리킵니다.
    2. Fetch: 해당 주소에서 명령어를 읽어와 IR(Instruction register)에 저장합니다.
    3. Decode: 명령어를 해석함과 동시에 PC는 다음 명령어 주소를 가리키기 위해 `0x1002`로 증가합니다.
    4. Execute: 해석결과 `LOAD` 명령이고, 피연산자는 `0x2000`번지이므로, 해당 주소에 있는 데이터를 DR(Data register)로 읽어와 저장합니다. (ALU에서 연산할 수도 있으므로 ACC(Accumulator)에도 임시로 저장합니다.)
  3. `ADD 0x2002`
    1. PC는 `0x1002`번지를 가리킵니다.
    2. Fetch: 해당 주소에서 명령어를 읽어와 IR에 저장합니다.
    3. Decode: 명령어를 해석함과 동시에 PC는 다음 명령어 주소를 가리키기 위해 `0x1004`로 증가합니다.
    4. Excute: 해석결과 'ADD' 명령이므로, 정해진 규칙에 따라 ACC에 저장된 값(`a`)과 DR로 읽어온 `0x2002`번지에 있는 값 `b`를 더합니다. 계산결과를 다시 ACC에 저장합니다.
  4. `Store 0x2004`
    1. PC는 `0x1004`번지를 가리킵니다.
    2. Fetch: 해당 주소에서 명령어를 읽어와 IR에 저장합니다.
    3. Decode: 명령어를 해석함과 동시에 PC는 다음 명령어 주소를 가리키기 위해 `0x1006`로 증가합니다.
    4. Execute: 해석결과 `STORE` 명령이므로, 정해진 규칙에 따라 ACC값을 `0x2004`번지에 저장합니다.

정말로 각 명령어가 3가지 세부과정으로 이뤄지는 것을 알 수 있습니다.

 

하지만, 한 가지 명령어를 세 단계로 나눠서 처리하므로 한 단계를 처리하는 동안 다른 명령어는 대기하고 있어야 합니다. Fetch는 AR, Decode는 decoder, Execute는 CU/ DR/ ALU(ACC), 각 단계가 CPU의 서로 다른 부분을 이용하고 간섭하지 않는 것을 알 수 있습니다. 따라서 한 명령어를 Execute 하면서, 다른 명령어는 Decode 하고, 다른 명령어는 Fetch 할 수 있습니다.

파이프라인

이처럼 각 단계를 중첩시켜서 단일 시간에 여러 단계를 동시에 수행하는 기법파이프라인(Pipeline)이라고 합니다.

 

파이프라인 단계를 무작정 많이 늘리면 좋아질 것 같지만, 아쉽게도 정도를 지나치게 될 경우 되려 성능이 떨어집니다.

  • 단계가 많아질수록, 첫 fetch 후 유효한 결괏값이 나올 때까지의 시간이 너무 오래걸립니다. 시스템의 throughput이 낮아집니다.
  • 최적화를 위해 branch pradiction 같은 기법을 사용하는데, 파이프라인 단계가 많아질수록 이 예상 분기에 실패했을 때 지금까지 파이프라인에 담겨있는 내용물을 모조리 쏟아버리고 새로 파이프라인을 시작해야 합니다.
  • 각종 hazard ( Control hazard, Structure hazard, Data hazard  ) 가능성이 커집니다.

따라서 시스템 아키텍처마다 자신의 성능을 최대로 낼 수 있는 pipeline 단계를 선택합니다.

 

마지막으로, 파이프라인에서 눈여겨봐야 할 점은 PC의 위치입니다.

PC는 Execute를 하는 곳의 2개 명령어 밑을 가리킵니다. 주소로 따지면 (32-bit 시스템 기준) +8번지입니다. 파이프라인 단계가 더 많다고 더 밑을 가리키거나 그러지는 않습니다. 겉으로 보이는 파이프 라인은 항상 Fetch - Decode - Execute 3단계입니다. PC는 항상 Execution 하는 곳보다 앞서가고 있다는 개념을 머릿속에 넣어둡시다. 나중에 파이프라인을 사용하는 시스템을 디버깅하는 방법을 설명할 때 유용하게 사용됩니다.