인터럽트·DMA·고속 I/O
화면을 손가락으로 톡 치면, 터치 칩은 1초에 수백 번 좌표를 재다가 "눌렸다"는 사실을 SoC에 알려야 한다. 사진을 찍으면 수십 MB가 저장장치로 흘러가야 하고, 충전기를 꽂으면 USB 선 위로 초당 수십억 비트가 오간다. 그 모든 순간에 CPU가 일일이 장치를 들여다보고 바이트를 하나씩 옮긴다면 CPU는 다른 일을 할 수 없다. 이 장에서는 CPU가 장치와 대화하는 기본 방식(메모리 맵 I/O), 장치가 CPU를 부르는 방법(인터럽트), CPU 대신 데이터를 나르는 일꾼(DMA), 그리고 칩 밖으로 Gb/s를 내보내는 고속 직렬 링크(SerDes)를 차례로 만져 본다.
- 장치 레지스터가 메모리 주소에 매핑되는 원리(MMIO)를 이해하고 SoC 메모리 맵을 읽는다.
- 폴링과 인터럽트의 CPU 사용량·지연을 비교하고, 인터럽트 컨트롤러의 우선순위·중첩·지연을 실험한다.
- DMA와 스캐터-개더 디스크립터, IOMMU가 CPU를 해방하고 메모리를 보호하는 방식을 설명한다.
- I²C 파형을 읽고 I²C·SPI·UART의 차이를 비교한다.
- 병렬 버스가 직렬 SerDes에 밀린 이유, 라인 코딩의 오버헤드, 채널 손실·잡음·지터·이퀄라이저가 아이 다이어그램과 BER에 주는 영향을 확인한다.
장치도 주소를 가진다: 메모리 맵 I/O
CPU가 아는 동작은 사실상 두 가지, 주소에서 읽기(load)와 주소에 쓰기(store)다. 그래서 SoC는 장치를 제어하는 레지스터들에도 주소를 붙인다. CPU가 0x1003_0000번지에 값을 쓰면 그 store는 DRAM이 아니라 NoC(8장)를 타고 GPIO 블록으로 가서 핀의 전압을 바꾼다. 이것이 메모리 맵 I/O(MMIO, Memory-mapped I/O)다. 어느 주소가 어느 블록인지 정한 표를 메모리 맵(Memory map)이라 한다. 아래 지도에서 영역을 눌러 보고, GPIO 레지스터의 비트를 눌러 LED를 켜 보자.
MMIO 주소는 일반 메모리와 다르게 다뤄야 한다. 장치 레지스터는 읽기만 해도 상태가 바뀔 수 있고(예: UART 수신 레지스터를 읽으면 다음 바이트로 넘어간다), 쓰기 순서가 바뀌면 안 된다. 그래서 운영체제는 이 영역을 페이지 테이블에서 캐시 불가·순서 보장(Device memory)으로 표시하고, C 코드에서는 volatile로 컴파일러의 최적화를 막는다. 장치 레지스터 한 번 읽기는 NoC를 왕복해야 하므로 수십~수백 ns가 걸린다. CPU 입장에서는 캐시 적중의 수백 배다.
기다리며 묻기, 아니면 불러 주기: 폴링과 인터럽트
장치에 새 데이터가 왔는지 CPU가 알려면 두 가지 방법이 있다. 폴링(Polling)은 CPU가 주기적으로 장치의 상태 레지스터를 읽어 보는 것이다. 인터럽트(Interrupt)는 장치가 신호선을 올려 CPU를 부르는 것이다. CPU는 하던 일을 멈추고 레지스터를 저장한 뒤, 정해진 인터럽트 처리기(ISR, Interrupt service routine)로 뛰어간다.
그래서 실제 시스템은 둘을 섞는다. 네트워크 드라이버는 패킷이 뜸할 때는 인터럽트로 깨어나고, 폭주가 시작되면 인터럽트를 끄고 폴링으로 바꾼다(Linux NAPI). 장치 쪽에서 여러 이벤트를 모았다가 한 번에 인터럽트를 거는 인터럽트 병합(Interrupt coalescing)도 흔하다. 반대로 지연이 아주 중요한 고속 저장장치(NVMe)에서는 일부러 폴링을 쓰기도 한다.
인터럽트 교통정리: 인터럽트 컨트롤러
SoC에는 인터럽트 신호가 수백 개 있다. 이것을 모두 CPU에 직접 꽂을 수는 없으니, 중간에 인터럽트 컨트롤러를 둔다. Arm 계열에서는 GIC(Generic Interrupt Controller)가 이 일을 한다. GIC는 인터럽트마다 우선순위(숫자가 작을수록 높다)와 목적지 코어를 정해 두고, 대기 중인 것 가운데 가장 높은 것을 CPU에 알린다. CPU가 처리기 안에 있을 때 더 높은 우선순위가 오면 처리기를 멈추고 그것부터 처리하는 것을 중첩(Nesting, 선점)이라 한다. 아래 버튼으로 인터럽트를 직접 걸어 보자.
인터럽트 컨트롤러 타임라인
GIC는 인터럽트를 종류별로 나눈다. 여러 코어가 공유하는 장치 인터럽트(SPI, Shared Peripheral Interrupt — 직렬 버스 SPI와 다른 뜻이다), 코어마다 따로 있는 타이머 같은 인터럽트(PPI), 코어가 다른 코어를 깨우는 소프트웨어 인터럽트(SGI), 그리고 PCIe 장치가 메모리 쓰기로 보내는 메시지 인터럽트(LPI, MSI)다. 하드웨어가 인터럽트를 코어에 알리는 데는 수십~수백 ns면 되지만, 운영체제가 레지스터를 저장하고 처리기를 찾고 스케줄러를 거쳐 사용자 프로그램까지 깨우는 데는 수~수십 µs가 걸린다. 지연을 줄이려면 처리기는 짧게(급한 일만) 하고 나머지는 나중에 스레드에서 하는 상반부·하반부(Top half / bottom half) 구조를 쓴다.
CPU 대신 나르는 일꾼: DMA
저장장치에서 사진 파일 4 MB를 메모리로 가져온다고 하자. CPU가 장치의 데이터 레지스터를 4바이트씩 읽어 메모리에 쓴다면(PIO, Programmed I/O) 100만 번의 느린 MMIO 읽기를 해야 한다. 대신 DMA(Direct Memory Access) 엔진에게 "장치에서 이 주소로 4 MB를 옮겨라"라고 지시만 하면, DMA 엔진이 버스 마스터가 되어 직접 옮기고 끝나면 인터럽트로 알린다. CPU는 그동안 다른 일을 한다.
운영체제가 할당한 버퍼는 가상 주소로는 이어져 있어도 물리 메모리에서는 4 KB 페이지로 흩어져 있다. DMA 엔진은 물리 주소로 일하므로, 조각마다 (주소, 길이, 다음 조각) 정보를 담은 디스크립터(Descriptor)를 사슬로 엮어 건네준다. DMA 엔진은 사슬을 따라가며 조각을 차례로 옮긴다. 이것이 스캐터-개더(Scatter-gather) DMA다.
DMA에는 위험도 있다. DMA 엔진은 아무 물리 주소에나 쓸 수 있으니, 고장 났거나 악의적인 장치(예: 썬더볼트로 꽂은 장치)가 커널 메모리를 덮어쓸 수 있다. 그래서 장치와 메모리 사이에 장치용 MMU, 곧 IOMMU(Arm에서는 SMMU)를 둔다. 장치가 쓰는 주소(IOVA)를 운영체제가 허락한 물리 페이지로만 번역해 주고, 허락하지 않은 주소는 막는다.
센서와 속삭이는 선 두 가닥: I²C, SPI, UART
휴대폰에는 가속도계, 자이로, 조도 센서, 충전 IC, 터치 컨트롤러, 카메라 모듈 설정 포트처럼 데이터는 적지만 수가 많은 칩이 수십 개 있다. 이들에게 고속 링크를 하나씩 줄 수는 없으니 핀을 적게 쓰는 저속 버스로 묶는다. 대표가 I²C(Inter-Integrated Circuit)다. 선은 클럭(SCL)과 데이터(SDA) 두 가닥뿐이고, 여러 장치가 같은 두 선을 나눠 쓴다. 각 장치는 7비트 주소를 가진다.
I²C 선은 저항으로 1(High)에 끌어올려져 있고, 장치는 선을 0으로 끌어내리기만 한다(오픈 드레인). 그래서 둘이 동시에 말해도 단락되지 않는다. 규칙은 이렇다. SCL이 1인 동안 SDA는 바뀌면 안 된다. 단 두 예외가 시작(SCL=1일 때 SDA가 1→0)과 정지(SCL=1일 때 SDA가 0→1)다. 바이트마다 받는 쪽이 SDA를 끌어내려 ACK로 답한다.
| I²C | SPI | UART | |
|---|---|---|---|
| 선 수 | 2 (SCL, SDA) | 4 (SCLK, MOSI, MISO, CS) + 장치마다 CS | 2 (TX, RX), 클럭 없음 |
| 속도 (대략) | 100 k / 400 k / 1 MHz (I3C 12.5 MHz) | 수십 MHz (플래시는 100 MHz 이상, 4선 병렬 Quad SPI) | 115.2 kbaud ~ 수 Mbaud |
| 장치 선택 | 7비트 주소 | 칩 선택(CS) 선 | 점대점 (일대일) |
| 응답 확인 | 바이트마다 ACK | 없음 | 없음 (패리티 선택) |
| 방향 | 반이중 | 전이중 (보내며 동시에 받음) | 전이중 |
| 주 쓰임 | 센서, 충전 IC, 카메라 설정 | 디스플레이 설정, 플래시, 고속 센서 | 디버그 콘솔, 블루투스·GPS 칩 |
읽기 한 번에는 레지스터 주소를 쓰는 단계(시작 + 주소·쓰기 + 레지스터 번호)와 다시 시작해 읽는 단계(주소·읽기 + 6바이트)가 필요하다. 바이트마다 ACK 1비트가 붙는다.
답 보기
쓰기 단계 2바이트, 읽기 단계 7바이트(주소 1 + 데이터 6)로 9바이트 × 9비트 = 81비트에 시작·재시작·정지 몇 비트를 더해 약 85클럭, 400 kHz면 약 212 µs다. 초당 1,000번이면 버스 시간의 약 21%를 쓴다. 센서가 대여섯 개면 I²C 하나로는 모자라기 시작한다. 그래서 고속 센서는 SPI나 I3C를 쓰고, 센서 쪽에 FIFO를 두어 여러 샘플을 한 번에 읽는다.
왜 병렬이 아니라 직렬인가: SerDes
예전 PC의 하드디스크와 프린터는 선 수십 가닥의 두꺼운 병렬 케이블로 연결되었다. 한 번에 여러 비트를 보내니 빠를 것 같지만, 속도를 올리자 문제가 생겼다. 선마다 길이와 전기적 특성이 조금씩 달라 비트들이 도착하는 시각이 어긋난다(스큐). 모든 비트를 하나의 공통 클럭으로 잡으려면, 가장 빠른 선과 가장 느린 선의 차이보다 비트 하나의 시간(UI, Unit interval)이 충분히 길어야 한다.
그래서 고속 I/O는 선 하나(실제로는 잡음에 강한 차동 쌍 한 쌍)에 비트를 몰아 보내고, 받는 쪽에서 신호 자체로부터 클럭을 복원한다. 송신기의 직렬화기와 수신기의 역직렬화기를 합쳐 SerDes(Serializer/Deserializer)라 한다. 더 빠르게 하고 싶으면 이런 레인(Lane)을 여러 개 묶되, 레인마다 따로 클럭을 복원하고 레인 사이의 어긋남은 버퍼로 맞춘다(디스큐).
클럭 복원 회로(CDR)가 일하려면 신호가 자주 바뀌어야 한다. 0이 1,000개 이어지면 언제가 비트 경계인지 알 수 없다. 또 교류 결합 커패시터를 지나는 링크는 1과 0의 수가 평균적으로 같아야(DC 균형) 기준선이 흔들리지 않는다. 이를 위해 데이터를 그대로 보내지 않고 바꿔 보내는 것이 라인 코딩(Line coding)이다. 8b/10b는 8비트를 10비트 부호로 바꿔 같은 비트가 최대 5개까지만 이어지고 DC 균형을 맞춘다. 대신 20%를 버린다. 128b/130b는 데이터를 의사 난수로 뒤섞고(스크램블) 128비트마다 2비트 헤더만 붙여 오버헤드를 1.5%로 줄였다.
아이 다이어그램: 신호가 살아서 도착했는가
칩 밖의 배선(패키지, 기판, 커넥터, 케이블)은 높은 주파수일수록 신호를 크게 깎아 먹는 저역 통과 필터다. 비트가 짧아지면 한 비트의 에너지가 다음 비트들 위로 번져 겹친다. 이를 심볼 간 간섭(ISI, Inter-symbol interference)이라 한다. 여기에 잡음과 지터가 더해진다. 수신 신호를 UI 두 개 길이로 잘라 겹쳐 그리면 눈 모양이 나오는데, 이것이 아이 다이어그램(Eye diagram)이다. 눈이 크게 뜨여 있을수록 0과 1을 구분하기 쉽다.
수신기가 깎인 고주파를 되살리는 방법이 이퀄라이저(Equalizer)다. 송신기의 FFE(Feed-forward equalizer)는 앞뒤 비트를 조금씩 빼서 보내, 채널이 남길 꼬리를 미리 상쇄한다(디엠퍼시스). 아래 시뮬레이터에서 속도와 채널 손실을 올려 눈을 감기게 한 뒤, FFE 탭으로 다시 뜨게 해 보자.
SerDes 아이 다이어그램
PAM4는 한 심볼에 2비트를 싣는다. 같은 비트 속도에서 나이퀴스트 주파수와 레벨 간격이 어떻게 변하는지 생각해 보자.
답 보기
PAM4는 심볼 속도가 절반(16 GBd)이라 나이퀴스트 주파수가 8 GHz로 내려가 채널 손실이 크게 준다. 대신 진폭을 세 칸으로 나누므로 눈 하나의 높이가 1/3(약 −9.5 dB)이 되어 잡음에 약하다. 손실이 큰 채널에서는 PAM4가 이기고, 손실은 작은데 잡음이 큰 경우에는 NRZ가 낫다. PCIe 6.0(64 GT/s)과 최신 이더넷이 PAM4로 간 이유가 손실 때문이다. 그 대신 BER이 나빠지는 것을 FEC(전방 오류 정정)로 메운다.
숫자로 보는 고속 I/O
SB-1의 가장자리에는 저장장치(UFS), 외부 연결(USB), 카메라·디스플레이(MIPI), 그리고 노트북·자동차용 칩이라면 PCIe를 위한 SerDes가 늘어서 있다(1장의 다이 아래쪽 띠). 세대가 바뀔 때마다 레인당 속도가 대략 두 배가 되었고, 그 속도를 위해 라인 코딩과 변조가 바뀌었다.
| 규격 | 레인당 전송률 | 코딩·변조 | 유효 대역폭 (대략) | 연도 |
|---|---|---|---|---|
| PCIe 3.0 | 8 GT/s | 128b/130b, NRZ | 레인당 약 0.98 GB/s | 2010 |
| PCIe 4.0 | 16 GT/s | 128b/130b, NRZ | 레인당 약 1.97 GB/s | 2017 |
| PCIe 5.0 | 32 GT/s | 128b/130b, NRZ | 레인당 약 3.94 GB/s | 2019 |
| PCIe 6.0 | 64 GT/s | PAM4, FLIT + FEC | 레인당 약 7.6 GB/s | 2022 |
| UFS 3.1 (M-PHY HS-G4) | 11.6 Gb/s × 2레인 | 8b/10b | 약 2.3 GB/s (이론) | 2020 |
| UFS 4.0 (M-PHY HS-G5) | 23.3 Gb/s × 2레인 | 128b/129b | 순차 읽기 약 4.2 GB/s | 2022 |
| USB 3.2 Gen 2 | 10 Gb/s | 128b/132b | 약 1.2 GB/s | 2013 |
| USB4 (v1 / v2) | 20 Gb/s × 2 / PAM3 | 128b/132b 등 | 40 / 80 Gb/s 링크 | 2019 / 2022 |
| MIPI D-PHY v2.5 (카메라) | 약 4.5 Gb/s × 4레인 | 소스 동기 클럭 | 약 2 GB/s | 2019 |
| I²C Fast-mode | 400 kb/s | 오픈 드레인 | 수십 kB/s | 1992 |
고속 SerDes는 비트당 수 pJ의 에너지를 쓴다(채널이 길수록 이퀄라이저가 커진다). 16 GT/s × 4레인 링크를 꽉 채우면 SerDes만 수백 mW다. 1장에서 본 "칩 밖은 비싸다"가 여기서도 그대로다. 그래서 링크는 쉴 때 재빨리 저전력 상태(PCIe L1, UFS Hibern8)로 들어가고, 깨어날 때 다시 클럭을 맞추는 시간이 지연에 더해진다(12장).
SerDes 송신기는 수십 GHz의 깨끗한 클럭이 필요하다. 보통 100 MHz 기준 클럭에서 LC-VCO를 쓰는 저지터 PLL로 만들고(14장), 수신기의 CDR은 들어오는 데이터의 천이를 보며 표본 위치를 계속 미세 조정하는 또 하나의 PLL이다. 앞 시뮬레이터의 랜덤 지터가 바로 이 PLL들의 잡음이다.
핵심 정리
- 장치 레지스터는 물리 주소 공간에 매핑되고, CPU는 load/store로 장치를 다룬다(MMIO). 이 영역은 캐시하지 않고 순서를 지킨다.
- 폴링은 이벤트가 잦을 때, 인터럽트는 드물 때 유리하다. 인터럽트 컨트롤러가 우선순위·목적지·중첩을 관리하며, 지연을 줄이려면 처리기를 짧게 한다.
- DMA는 CPU 대신 데이터를 나르고, 스캐터-개더 디스크립터로 흩어진 페이지를 한 번에 처리한다. IOMMU는 장치의 메모리 접근을 허락된 페이지로 제한한다.
- I²C는 선 두 가닥에 주소·ACK를 쓰는 저속 공유 버스, SPI는 칩 선택 선을 쓰는 빠른 전이중 버스, UART는 클럭 없는 점대점 링크다.
- 병렬 버스는 선 사이 스큐에 막혀 직렬 SerDes에 자리를 내줬다. 라인 코딩은 클럭 복원과 DC 균형을 위한 것이며, 채널 손실·잡음·지터가 아이를 닫고 이퀄라이저가 다시 연다. 손실이 커지면 PAM4와 FEC로 간다.
확인 퀴즈
Q1. MMIO 영역을 일반 메모리처럼 캐시하면 안 되는 이유로 가장 알맞은 것은?
Q2. 초당 50만 번 이벤트가 생기는 네트워크 장치에서 인터럽트 대신 폴링(또는 병합)을 쓰는 이유는?
Q3. GIC에서 중첩(선점)을 허용하면 좋아지는 것은?
Q4. 물리적으로 흩어진 4 KB 페이지 여러 개로 된 버퍼를 DMA 한 번으로 보내게 해 주는 것은?
Q5. PCIe 3.0이 8b/10b 대신 128b/130b를 쓰면서 얻은 이득은?
Q6. 아이 다이어그램에서 송신 FFE의 후 탭(디엠퍼시스)을 키우면 일어나는 일은?