본문 바로가기
Computer Architecture

PCIe Enumeration

by hyeyoo 2023. 11. 30.
※ 이 블로그의 글은 글쓴이가 공부하면서 정리하여 쓴 글입니다.
※ 최대한 내용을 검토하면서 글을 쓰지만 틀린 내용이 있을 수 있습니다.
※ 만약 틀린 부분이 있다면 댓글로 알려주세요.

Introduction

펌웨어와 운영체제는 컴퓨터에 어떤 PCI(e) 디바이스들이 있는지 어떻게 발견할까?

 

이전 글에서는 ACPI MCFG 테이블과 PCI Function의 BDF 번호를 알면 특정 Function의 Configuration Space의 위치를 알 수 있다고 했다. 그런데 어떤 디바이스들이 컴퓨터에 연결되었는지를 먼저 알아야 Configuration Space를 읽든 말든 할 수 있다. 펌웨어와 운영체제는 부팅 직후 컴퓨터에 어떤 디바이스들이 있는지 알지 못한다. 부팅 직후 디바이스를 하나씩 발견하는 과정을 Enumeration이라고 한다.

PCI Endpoint (Type 0) and Bridge (Type 1)

Enumeration을 더 설명하기 전에 우리는 브리지에 관해 더 알아볼 필요가 있다. PCI Configuration Space의 "표준 레지스터"들 (첫 64바이트)을 살펴보자.

PCI Standard Registers (First 64 Bytes) (https://en.wikipedia.org/wiki/PCI_configuration_space  의 수정본)

PCI에서 Function은 엔드포인트거 혹은 브리지다. 둘을 구분하는 방법은 PCI Configuration Space의 Header Type 필드를 확인하는 것이다.

Header Type에서 "Configuration Header Format"이 0이라면 엔드포인트, 1이라면 브리지다. 위 그림을 보면 엔드포인트냐 브리지냐에 따라서 중간에 있는 (10h ~ 30h) 레지스터들의 기능과 역할이 달라진다.

PCIe 스위치 (좌), 루트 컴플렉스 (우)

"왜 종류가 엔드포인트랑 브리지밖에 없지?"라고 생각할 수 있는데,  앞선 글 에서 살펴봤듯 루트 컴플렉스와 PCIe 스위치의 업스트림 포트, 다운스트림 포트는 모두 Configuration Space 상에서 Configuration Header Format = 1인 브리지로 표현된다.

Primary, Secondary, Subordinate Bus Number

Type 1 Configuration Header (https://www.macnica.co.jp/en/business/semiconductor/articles/microchip/140352/)

어떤 Function이 브리지라면, Configuration Space에 "Primary Bus Number"와 "Subordinate Bus Number", "Subordinate Bus Number" 레지스터가 존재한다. PCI에서 브리지는 업스트림 버스와 다운스트림 버스를 연결한다. 아래 그림을 예로 살펴보자.

호스트 브리지의 버스는 항상 번호가 0이다. (이 사실이 나중에 Enumeration에서도 사용되니 참고하자.) 버스 0에는 브리지가 연결되어있는데, 이 브리지는 업스트림 버스인 버스 0을 다운스트림 버스인 버스 1과 연결하는 브리지다. 마찬가지로 버스 1에는 브리지가 연결되어있고, 이 브리지는 버스 1과 버스 2를 연결한다.

이렇듯 호스트 브리지를 제외한 브리지들은 모두 업스트림 버스를 버스 브리지와 연결한다. 브리지의 Type 1 Configuration Header에서 "Primary Bus Number"는 업스트림 버스 번호이고, "Secondary Bus Number"는 다운스트림 버스의 번호다. 그리고 "Subordinate Bus Number"는 다운스트림에 연결된 버스들 중, 번호가 가장 큰 것을 의미한다.

다시말해 [Secondary Bus Number, Subordinate Bus Number]는 브리지가 Configuration Space에 대한 요청을 포워딩 해야하는 범위이다. 만약 호스트가 브리지에 [Secondary Bus Number, Subordinate Bus Number] 사이에 있는 버스에 대한 Configuration Space 읽기/쓰기 요청을 보낸다면, 브리지는 다운스트림으로 해당 요청을 포워딩한다.

Checking if a device exists or not

어떤 디바이스가 존재하는가, 아닌가는 디바이스의 Function 0 Configuration Space의 표준 레지스터에서 Vendor ID를 읽음으로써 확인할 수 있다. 디바이스가 존재하며 && 디바이스가 응답을 보낼 준비가 되었다면 정상적인 Vendor ID 값이 반환된다. 만약 디바이스가 존재하지 않는다면 Vendor ID는 FFFFh이다. FFFFh는 디바이스가 존재하지 않는다는 것을 의미하며, 예약된 값이므로 존재하는 디바이스는 이 값을 반환할 수 없다. 만약 디바이스가 존재하지만 아직 응답을 보낼 준비가 되지 않았다면 디바이스가 별도의 방법 (글의 단순함을 위해 디테일은 생략) 으로 소프트웨어에 알려줄 수 있다. 이 경우 소프트웨어는 일정 시간 기다린 후 다시 Vendor ID를 읽는다.

Enumeration

앞서 Primary / Secondary / Subordinate Bus Number를 알아본 이유는 Enumeration 과정에서 이 번호들을 설정해주어야 하기 때문이다. [1]에 따르면 "펌웨어가 이미 설정했다고 가정하고 운영체제는 브리지를 따로 설정하지 않아도 된다"는 뉘앙스로 말하는데, 이게 UEFI나 ACPI 스펙 상에서 보장이 되는 사항인지는 모르겠다. 여기서는 그냥 펌웨어가 Enumeration을 한다고 가정하고 설명한다. 예시를 따라가며 Enumeration이 어떻게 진행되는지 살펴보자.

호스트 브리지의 Secondary Bus Number는 항상 0이다. Enumeration을 하기 전에 펌웨어는 이 사실 말고는 아무것도 아는 게 없다. 펌웨어의 Subordinate 번호가 무엇인지는 Enumeration을 해봐야 알 수 있는데, 우선 255로 설정한 다음 나중에 다시 갱신한다. 아직 버스 0에 무엇이 연결되었는지는 모른다.

버스 0에 어떤 디바이스들이 연결되었는지 확인하려면 각 디바이스의 Function 0의 Configuration Space에서 Vendor ID를 읽는다. 즉, BDF (0, 0, 0), (0, 1, 0), (0, 2, 0), ..., (0, 7, 0)의 Vendor ID를 읽어본다.

만약 디바이스가 존재한다면 (Vendor ID != FFFFh) Header Type을 확인해서 이게 브리지인지 엔드포인트인지 확인한다.  여기서는 BDF (0, 0, 0)의 1이고, 이걸 브리지 A라고 하자. 펌웨어는 브리지의 Primary Bus Number를 0으로 설정하고, Secondary Bus Number는 번호를 할당해주어야 한다. 단순하게 1로 부여하자. 호스트 브리지에서와 마찬가지로 마찬가지로 Subordinate Bus Number는 일단 255로 설정한다.

버스 0의 디바이스 0에서 브리지를 발견했다면, BDF (0, 1, 0)을 탐색하기 전에 버스 1의 디바이스들을 탐색해야 한다. 왜냐하면 PCI가 Depth-First Search 방식으로 Enumeration을 수행할 것을 명시하기 때문이다.

만약 브리지가 아니라 엔드포인트였다면, Header Type 필드의 비트 7 (Multi-function bit)을 읽어서 둘 이상의 function을 갖는 multi-function 디바이스인지 확인한다음 다른 Function들도 존재하는지 확인해야 한다. PCI에서 Function 0은 필수지만 다른 Function들은 순서대로 존재하지 않을 수 있다. (예를 들어 어떤 디바이스는 Function 0, 5, 7을 구현할 수도 있다.)

버스 1에서도 똑같이 BDF (1, 0, 0)부터 (1, 7, 0)까지의 Vendor ID를 읽어본다. 하지만 모두 Vendor ID가 FFFFh였다. 따라서 버스 1에는 연결된 디바이스가 없다. 이제 브리지 A의 다운스트림에 있는 버스 중 번호가 가장 큰 것이 1번임을 알았기 때문에, Subordinate Bus Number를 1로 설정한다.

다시 버스 0으로 돌아가서 BDF (0, 1, 0)부터 탐색을 시작한다. Vendor ID가 유효한 값이 나왔고, Header Type을 확인하니 브리지라서 BDF (0, 2, 0) 대신 버스 2를 탐색한다.

버스 2의 BDF (2, 0, 0)부터 (2, 7, 0) 까지의 Vendor ID를 읽어본다. 하지만 모두 Vendor ID가 FFFFh다. 따라서 버스 2에는 연결된 디바이스가 없다. 이제 브리지 B의 다운스트림에 있는 버스 중 번호가 가장 큰 것이 1번임을 알았기 때문에, Subordinate Bus Number를 1로 설정한다.

다시 버스 0으로 돌아와서 BDF (0, 2, 0)부터 탐색을 시작한다. 하지만 모두 Vendor ID가 FFFFh다. 따라서 버스 0에는 브리지 A, B를 제외하고 더 이상 연결된 디바이스가 없다. 이제 호스트 브리지의 다운스트림에 있는 버스 중 번호가 가장 큰 것이 2번임을 알았기 때문에, Subordinate Bus Number를 2로 설정한다.

Enumeration 결과 시스템에는 총 2개의 디바이스가 있고, 두 디바이스는 모두 버스 0에 연결되어 있으며 [Secondary Bus Number, Subordinate Bus Number]가 각각 [1, 1], [2, 2]다.

참고 문서

[1] https://wiki.osdev.org/PCI#Enumerating_PCI_Buses

 

PCI - OSDev Wiki

The PCI Bus The PCI (Peripheral Component Interconnect) bus was defined to establish a high performance and low cost local bus that would remain through several generations of products. By combining a transparent upgrade path from 132 MB/s (32-bit at 33 MH

wiki.osdev.org

'Computer Architecture' 카테고리의 다른 글

PCIe Configuration Space  (0) 2023.11.30
An Introduction to PCI Express  (0) 2023.11.15
PIPT / VIVT / VIPT 캐시와 flush_dcache_folio()  (0) 2022.06.25
다시 정리하는 NUMA  (0) 2022.01.21
LEGv8 ISA - 특징과 명령어 포맷  (0) 2021.12.02

댓글