Il bus PCI
(Peripheral Component Interconnect) è uno standard per i bus introdotto da Intel.
La maggior parte dei computer dell’epoca (come oggi) erano espandibili, ovvero potevano essere inserite delle schede di espansione indipendenti da IBM per aggiungere funzionalità al sistema.
Tuttavia questa possibilità generò dei problemi, specialmente in un mercato dove non era presente il coordinamento tra i vari produttori di schede. Poteva infatti capitare che due schede diverse facessero riferimento agli stessi indirizzi di memoria, e che quindi quegli indirizzamenti fossero sovrapposti. Questo problema si propagava anche in come venivano scritti i driver, che non erano quindi più in grado di distinguere se la scheda desiderata era effettivamente inserita, se si trattava di una scheda diversa oppure se non ci fosse installato nulla.
Dal momento che il PC AT
era costruito con parti standard, il suo bus venne chiamato ISA (Industry Standard Architecture).
Questo bus era però molto limitato e descriveva tutti i limiti del PC AT
.
Nel 1992 la Intel propose lo standard Peripheral Component Interconnect, all’epoca a 32bit
, valido per qualsiasi tipo di processore, non solo Intel.
Nei computer moderni si utilizza un’evoluzione del PCI
, ovvero il PCIexpress
, molto più complesso ma comunque compatibile con il precedente. Purtroppo non vedremo il PCIexpress
in questo corso.
Lo standard PCI
stabilisce:
Lo standard definisce inoltre tre spazi di indirizzamento: spazio di memoria, spazio di I/O e spazio di configurazione.
I primi due spazi sono completamente analoghi a quelli che già conosciamo e sono quelli che il software deve utilizzare per dialogare con le periferiche connesse al bus.
Per garantire il vincolo che i registri delle periferiche non occupino indirizzi sovrapposti si introducono due regole:
Le periferiche che rispettano lo standard non possono scegliere autonomamente gli indirizzi dei propri registri, ma devono contenere dei comparatori programmabili in modo che questi indirizzi siano impostati dal software della macchina.
Il software può impostare questi comparatori all’avvio del sistema, tramite il nuovo spazio di configurazione.
Lo spazio di configurazione è fatto in modo da poter accedere a dei registri di configurazione che ogni periferica deve avere per rispettare lo _standard_ in modo univoco e senza conflitti.
Tramite questi registri il software di avvio (PCI BIOS
) può scoprire quali periferiche sono connesse al bus.
Non solo, capisce anche di quanti indirizzi hanno bisogno e programma di conseguenza i comparatori affinché non ci siano sovrapposizioni.
La struttura che permette al bus di essere indipendente dal processore è quella del ponte ospite-PCI (detto North-Gate), un dispositivo che fa da tramite tra il bus locale (quello dove risiede la CPU) e il bus PCI.
Lo standard PCI
non prevede infatti un unico bus, bensì un albero di bus, che ha come radice il bus collegato al ponte ospite-PCI. Ogni bus è collegato all’albero tramite ponti PCI-PCI.
Per ogni albero si possono avere fino ad un massimo di 256 bus. Ogni bus può inoltre collegare fino a 32 dispositivi, ciascuno contenente da 1 a 8 funzioni (audio, video, …).
Alcune funzioni possono persino fare da ponte verso altri tipi di bus, formando ponti PCI-ISA
, ponti PCI-ATA
, ponti PCI-USB
, …
Esempio di architettura con bus PCI
.
Le operazioni (lettura, scrittura, …) sul bus PCI
sono dette transazioni.
Le transazioni sono iniziate da un dispositivo iniziatore che cerca di operare su un altro dispositivo obiettivo, e seguono il clock
.
Lo standard permette a qualsiasi dispositivo di essere iniziatore o obiettivo in transazioni diverse. Questo meccanismo permette il meccanismo di accesso diretto alla memoria di cui parleremo in seguito.
Immaginiamo per ora (per semplicità) che l’unico iniziatore sia il ponte ospite-PCI, che inizia le transazioni sul bus PCI
per tradurre le operazioni analoghe avviate sul bus locale.
Le transazioni si svolgono in più fasi:
I principali collegamenti del bus sono i seguenti: (#
indica gli attivi bassi)
Nome | Iniziatore | Obiettivo | Ruolo |
---|---|---|---|
FRAME# |
uscita | ingresso | Delimita l’inizio e il termine di ogni transazione |
DEVSEL# |
ingresso | uscita | Segnale attivato dall’obiettivo che riconosce uno dei propri indirizzi durante l’indirizzamento |
C[3:0] |
uscita | ingresso | Codificano il tipo di operazione nell’indirizzamento (C[3:0] ) |
BE#[3:0] |
uscita | ingresso | Fungono da byte-enabler nelle fasi di trasferimento (BE#[3:0] ) |
AD[31:0] |
ingresso/uscita | ingresso/uscita | Codificano l’indirizzo nella fase di indirizzamento. Trasportano i dati nelle fasi di trasferimento dati |
TRDY# |
ingresso | uscita | (target ready), Handshake nelle fasi di scambio dati |
IRDY# |
uscita | ingresso | (initiator ready), Handshake nelle fasi di scambio dati |
STOP# |
ingresso | uscita | Serve a terminare prematuramente una transazione |
CLK |
ingresso | ingresso | Segnale di clock , tutti i dispositivi campionano i loro segnali in ingresso sul suo fronte di salita. È molto più lento di quello della CPU (all’inizio era 33MHz poi è passato a 64MHz ) |
Alcuni esempi di temporizzazioni:
Lettura con 4 fasi di trasferimento dati.
Scrittura con 4 fasi di trasferimento dati.
Il collegamento segue il seguente protocollo:
C[3:0]
e AD[31:0]
con il tipo di operazione e l’indirizzo iniziale del trasferimentoFRAME#
per segnalare l’inizio di una transazioneDEVSEL#
clock
, l’inizializzatore termina l’operazione con un erroreNelle operazioni di lettura:
AD[31:0]
e segnala la loro validità tramite la linea TRDY#
.IRDY#
.TRDY#
e quando lo trova attivo campiona AD[31:0]
e una volta pronto attiva IRDY#
.Nelle operazioni di scrittura invece avviene l’opposto:
AD[31:0]
e BE#[3:0]
per poi attivare IRDY#
quando è pronto,TRDY#
.IRDY#
, e quando lo trova attivo campiona AD[31:0]
e BE#[3:0]
e una volta pronto attiva TRDY#
.Ogni fase dati trasferisce al più 4Byte
allineati naturalmente.
Ciascuna fase si conclude quando sia IRDY#
che TRDY#
sono attivi sullo stesso fronte di salita del clock.
Il riutilizzo delle stesse linee per scopi diversi riduce i costi a scapito però della velocità di trasferimento. Fortunatamente, la possibilità di eseguire più fasi dati con una singola fase di indirizzamento ci fa recuperare un po’ di velocità. È infatti sufficiente che l’iniziatore mantenga FRAME#
attivo quando lo è anche IRDY#
.
L’obiettivo può inoltre attivare STOP#
per terminare forzatamente la transazione.
Lo standard PCI
prevede uno spazio di configurazione di 4 Byte
(64 parole) per ogni funzione. In questo spazio ciascuna funzione rende accessibili i propri registri di configurazione.
Un indirizzo nello spazio di configurazione è composto da tre parti:
bus|8bit
: codice identificativo del busdevice|5bit
: dipende dalla posizione dove una scheda è fisicamente inseritafunction|3bit
: indica la funzione del deviceIl bus più vicino alla CPU ha il codice 0
, gli altri vengono assegnati in maniera dinamica.
Le funzioni rendono accessibili i loro registri di configurazione secondo lo schema sulla destra.
Non tutti i registri devono essere sempre presenti, ma, tranne per il ponte ospite-PCI, tutte le funzioni devono avere i seguenti registri:
Vendor ID
: (lettura) un codice che identifica il produttore della funzione.
È assegnato ad ogni produttore da una autorità centrale (PCI SIG
).
L’Intel ha come ID: 8086
Device ID
: (lettura) un codice che identifica la funzione.
È assegnato dal produttore
Command
: (lettura/scrittura) permette di abilitare o disabilitare varie capacità della funzione (vedere sotto);
Status
: (lettura) descrive alcune capacità della funzione, più lo stato di alcuni eventi, per lo più legati al verificarsi di errori
Revision ID
: (lettura) numero di revisione della funzione, assegnato dal produttore;
Class Code
: (lettura) un codice che identifica in modo generico il tipo di funzione;
Header Type
: (lettura) tipo di header; deve essere 0
per tutte le funzioni che non siano ponti PCI-PCI o ponti PCI-CardBus.
In particolare, del registro Command
ci interessano solo i bit 0
, 1
e 2
. Questi bit si occupano di:
I/O
(0
) e di memoria
(1
).2
)Questi bit valgono di default zero, ed è il software di inizializzazione che deve settarli a 1 dopo aver assegnato alla funzione gli indirizzi necessari nei corrispondenti spazi, qualora questi corrispondano.
Se ad esempio una funzione non ha registri nello spazio di I/O
il suo bit 0
non sarà scrivibile.
I registri con sfondo grigio sono obbligatori.
Nella nostra macchina il ponte ospite-PCI è collegato come se fosse una periferica che esprime dei registri nello spazio di I/O
:
CAP
(Configuration Address Port) (indirizzo 0xCF8
): permette di accedere allo spazio di configurazione, inserendo le 3 informazioni fondamentali e l’offset di parola ({bus, device, funzione, offset}
). Il ponte si preparerà quindi alla transazioneCDP
(Configuration Data Port) (indirizzo 0xCFC
): permette di accedere alla parola selezionata tramite CAP
.
Se l’operazione è di lettura e nessun dispositivo risponde alla transazione il ponte restituisce il valore 0xFFFF
al processore.Questi due registri possono essere usati per accedere allo spazio di configurazione in modo simile a come facciamo per la memoria video.
Per recuperare il risultato dell’operazione IN/OUT
si utilizza CDP
.
Lo scopo principale della fase di configurazione dei dispositivi è quello di assegnare indirizzi univoci negli spazi di I/O
e/o di memoria
.
Una volta che CAP
è stato impostato, il ponte ospite-PCI tradurrà le letture e le scritture in CDP
in corrispondenti transizioni nello spazio di configurazione.
Prima di impostare CAP
però il ponte attiverà l’uscita IDSEL
, che è un entrata presente in ogni slot di espansione che è necessario attivare per poter trasmettere il numero della funzione e l’offset della parola che contiene il registro (oltre ai byte enable relativi al registro).
Un vincolo presente in tutti i dispositivi è che la funzione 0
deve essere sempre presente. In questo modo si permette allo spazio di configurazione di essere accessibile già all’avvio del sistema.
Il costruttore di un blocco, oltre a deciderne la locazione, l’organizzazione interna e i blocchi di indirizzi all’interno dei quali rendere disponibili i registri specifici della funzione, deve prevedere un BAR
Base Address Register nello spazio di configurazione.
BAR
di memoria, $b \ge 4$
BAR
di I/O, $b \ge 2$
La dimensione del blocco è $2^b$ dove $b$ è il numero di bit non scrivibili utilizzati per fornire al software di inizializzazione indicazioni sul tipo di blocco e sulla sua dimensione.
In particolare il bit 0
serve a capire se il blocco è pensato per lo spazio di I/O
(1
) o di memoria
(0
).
Si può scoprire il suo valore provando a scrivere tutti 1
nel BAR
e rileggendo il risultato, per vedere quanti sono stati effettivamente scritti.
I blocchi possono essere assegnati solo a regioni allineate naturalmente alla loro dimensione.
Il software di inizializzazione sceglierà quindi la regione e ne scriverà il numero nei bit scrivibili del BAR
, così che contenga l’indirizzo di partenza del blocco.
Dopo aver assegnato una regione il software di inizializzazione setterà il bit 0
o 1
del registro Command
.
Da quel momento in poi la funzione risponderà alle transazioni i cui indirizzi cadono in quella regione.
Nella macchina QEMU
l’inizializzazione è fatta dal PCI BIOS
, e viene completata prima che il vostro software parta.
Noi ci limiteremo ad accettare le impostazioni del BIOS
, perciò tutto quello che ci serve è sapere quali indirizzi il BIOS
ha assegnato ai vari blocchi, e possiamo farlo leggendo i BAR
.
I dispositivi PCI
possono generare richieste di interruzioni, ma lo standard non prevede dettagli a riguardo.
Ogni dispositivo ha fino a quattro piedini in uscita per le richieste di interruzione: INTA#
, INTB#
, INTC#
e INTD#
.
Ogni funzione può quindi essere collegata ad al più uno di questi piedini. Inoltre, funzioni dello stesso dispositivo possono essere collegate allo stesso piedino.
Il piedino utilizzato deve essere leggibile dal registro di configurazione chiamato Intr. Pin
, grande 1Byte
e di sola lettura.
Se vale 0
indica che la funzione non genera interruzione, 1
che utilizza INTA#
, 2
che utilizza INTB#
e così via…
Chi scrive il software però vorrebbe sapere a quale piedino dell’APIC
ciascuna funzione è collegata, informazione che dipende però dal progettista in quanto non standardizzata.
Questo deve quindi garantire che il BIOS
scriva qualche informazione nel registro Intr. line
(1Byte
, lettura/scrittura) affinché il software possa ricavare l’informazione.
Nel nostro caso il BIOS
scrive in Intr. line
proprio il numero del piedino dell’APIC
, quindi possiamo limitarci a leggere questo registro.
PC AT
Nella nostra macchina QEMU
la struttura del bus è quella a destra.
Abbiamo un unico bus PCI
e due ponti:
ISA
dove si trovano le vecchie periferiche del PC AT
, come tastiera e timerATA
, dove si trovano gli HDIn questa architettura, comune nei primi anni 2000, il bus ospite-PCI veniva chiamato North Bridge e spesso conteneva altre funzioni, come quella di controllare una porizone della memoria.
Inoltre i ponti PCI-ISA e PCI-ATA erano spesso inglobati in un altro dispositivo che svolgeva tutte queste funzioni, chiamato South Bridge
I numero all’interno delle funzioni PCI
rappresentano VendorID:DeviceID