Per trasferire una serie di informazioni alla memoria abbiamo visto fino ad ora due modalità:
Per entrambe le modalità è previsto il coinvolgimento della CPU, che dovrà eseguire prima una lettura (registro pronti, RAM $\to$ CPU) e poi una scrittura (CPU $\to$ IO
), comportando due scambi dati sul bus
La modalità DMA
(Direct Memory Access) prevede invece che sia direttamente il dispositivo ad eseguire le operazioni di lettura o scrittura necessarie sulla RAM senza coinvolgere la CPU.
Per fare ciò si dotano i dispositivi di un software particolare che eseguirà in autonomia un trasferimento dati dal dispositivo verso un buffer in RAM (ingresso/lettura) o viceversa (uscita/scrittura).
Supponiamo che il buffer si trovi all’indirizzo b
e sia grande n
byte (comunicati al dispositivo), occupando quindi gli indirizzi [b, b+n)
. Una volta ottenuti questi dati il dispositivo si preoccuperà di eseguire autonomamente le operazioni in RAM.
Ovviamente il dispositivo deve essere dotato di:
sommatore
che gli permetta di calcolare da solo gli indirizzi necessari a partire da b
contatore
che decrementi n
ogni volta che è stato completato un trasferimento.Quando tutti i byte sono stati trasferiti il dispositivo segnalerà l’informazione settando opportunamente un suo registro di stato (tipicamente generando un’interruzione).
Struttura DMA
dal punto di vista hardware (rimuovendo temporaneamente cache, MMU e bus PCI)
L’accesso alla RAM è arbitrato tramite i collegamenti HOLD
/HOLDA
fra dispositivo e CPU, che fanno da handshake.
Infatti poiché comunicano tutti sullo stesso bus vogliamo che la comunicazione sia esclusiva, per evitare corse e più dispositivi che inviano dati in contemporanea.
La comunicazione si sviluppa così:
alta impedenza
HOLD
;HOLDA
;HOLD
;HOLDA
, attiva i suoi piedini e riprende il suo normale funzionamento.In pratica. la CPU dà la precedenza al DMA
nell’accesso al bus. La tecnica è chiamata “cycle stealing”, in quanto il DMA
ruba cicli di bus alla CPU.
Questo porta un rallentamento nell’esecuzione delle istruzioni, poiché molte rischiano di andare in attesa per un tempo indeterminato nel caso in cui il DMA
fosse sempre in accesso alla RAM.
Tuttavia il meccanismo resta vantaggioso in almeno tre casi:
Se la CPU è più lenta della RAM. Era comune negli home computer degli anni ‘80, dove il DMA
era utilizzato per trasferire i dati di una schermata dalla RAM alla scheda video mentre la CPU eseguiva il programma che preparava la schermata successiva. Oggi questo caso è impensabile.
Se il trasferimento a controllo di programma non è abbstanza veloce per il dispositivo
Se il dispositivo deve trasferire i dati con più urgenza di quanto permesso dal meccanismo delle interruzioni
Gli utlimi due scenari possono verificarsi anche oggi, basta pensare ad alcune schede di rete che possono ricevere o inviare decine di milioni di pacchetti al seconda a velocità di 200 Gbps
.
Se inseriamo la cache notiamo subito che i segnali HOLD
/HOLDA
si collegano ora al controllore cache, in quanto è lui ad essere collegato al bus
e non più la CPU.
Viene quindi introdotto un grande vantaggio:
La CPU, statisticamente, può ora eseguire più istruzioni. Infatti parte delle istruzioi in memoria saranno probabilmente salvate proprio in cache e non richiederanno un’accesso alla RAM.
Questo fatto provoca anche delle complicazioni.
Questi problemi nascono dal fatto che le operazioni del DMA
potrebbero coinvolgere parti di RAM che erano state precedentemente copiate in cache.
Vediamo come gestire le comunicazioni a seconda che la cache segua le politica write-through
o la politica write-back
.
write-through
È il caso più semplice poiché all’inizio del trasferimento tutte le cacheline
eventualmente presenti contengono lo stesso valore delle corrispondenti cacheline
in RAM.
Questo implica che non ci sono problemi nel caso di operazione di uscita su DMA
(lettura), poiché i dati in RAM sono aggiornati coerentemente con le modifiche salvate nelle cacheline
.
Nel caso di operazione di ingresso in DMA
(scrittura) invece dobbiamo assicurarci che tutte le cacheline
coinvolte nel trasferimento vengano rimosse dalla cache, o che siano perlomeno aggiornate.
Esistono due metodi per poter risolvere questo problema, uno al livello hardware e uno a livello software.
Nei processori Intel la soluzione è risolta in hardware
.
Si fa in modo che il controllore cache osservi tutte le possibili sorgenti di scritture in RAM attraverso il bus condiviso, processo chiamato di snooping.
Se le linee di controllo identificano un operazione di scrittura, il controllore può usare il contenuto delle linee di indirizzo per eseguire una normale ricerca in cache, e nel caso di hit
invalidare in autonomia la corrispondente cacheline
.
Se il controllore cache ricevesse in ingresso anche le linee di dati, allora potrebbe addirittura aggiornare la cacheline
invece di invalidarla. Questa operazione si chiama snarfing e non prevista nei processori Intel.
Nei sistemi ARM
il problema è invece delegato al software, tramite istruzioni dedicate che permettono alla CPU di interagire direttamente con il controllore cache e invalidarne le cacheline
.
Il software dovrà quindi eseguire tutte le istruzioni specificando l’intervallo [b, b+n)
(allineato opportunamente alle cacheline) subito dopo che il trasferimento sia terminato, così da poter manualmente invalidare gli indirizzi.
write-back
In questa politica le scritture della CPU vengono mantenute soltanto in cache e effettuate in maniera sincrona in secondi momenti (come quando la cacheline dirty
verrebbe sovrascritta).
Le cacheline ~dirty
invece continuano a contenere le stesse informazioni della RAM.
Questa politica comporta un problema sia nelle operazioni di uscita su DMA
, poiché il buffer di lettura IN RAM potrebbe contenere memoria non aggiornata, sia per le operazioni di entrata in DMA
, dove la faccenda è più complessa.
Infatti, nelle scritture, il DMA
potrebbe andare a modificare solo una parte della cacheline dirty
, perciò la mera invalidazione porterebbe a perdere le modifiche effettuate sulle parti di cacheline
non comprese nel buffer.
Concentriamoci intanto sull’uscita, ovvero le letture.
Per risolvere possiamo utilizzare la tecinca di snooping, ma in questo caso il controllore cache deve implementare lo snarfing per le cacheline dirty. Nei processori dove lo snarfing non è previsto il protocollo di accesso alla RAM viene modificato in modo che si svolga in più fasi, la prima comune a tutti:
DMA
comunica al controllore cache gli indirizzi a cui vuole accedere e quest’ultimo risponde con il segnale di hit
/miss
e l’eventuale stato del bit dirty
.Vi possono essere quindi più seconde fasi:
miss || (hit && ~dirty)
: allora si procede normalmente con l’accesso in RAM come visto per il write-through
hit && dirty
: il DMA
interrompe la richiesta, passando il controllo alla cache, che effettua la write-back
in RAM.
Si può poi proseguire in due modi:
write-back
, controllore cache restituisce il controllo al DMA
che reinizializza la lettura dei dati ottenendo adesso dati corretti dalla RAM
DMA
fa snooping con snarfing.Anche per le operazioni di entrata (scrittura) la struttura è simile. Il primo passo è il medesimo delle letture, successivamente anche qui ci possono essere più seconde fasi:
miss
: si procede normalmente con l’accesso in RAM
hit && ~dirty
: si invalida la cacheline
e si prosegue normalmente
hit && dirty
: si possono percorrere più strade:
DMA
sovrascrivesse un’intera cacheline
è sufficente invalidarla e scrivere direttamente in RAM (Write and Invalidate)DMA
lascia il controllo al controllore cache per fargli eseguire il write-back
in RAM, quindi riesegue la sua operazione di scritturacacheline dirty
solamente al `DMA`, invalidando successivamente la propria copia.
Il DMA
lavora quindi sui dati forniti dal controllore cache, e scrivendo in RAM l’intera cacheline
aggiornataNella soluzione interamente software, la politica più comune è quella di esegiuire sempre il write-back
di un certo intervallo di indirizzi (quelli del buffer) prima di avviare il trasferimento, invalidandoli successivamente.
Questa operazione è detta di invalidazione senza write back.
cacheline
Nel caso della scrittura su intere cacheline
la struttura del buffer [b, b+n)
vista dalla cache sarà la seguente:
[b, finePrimaCacheline)
cacheline
[inizioUltimaCacheline, n)
Per le sezioni intermedie è possibile fare Write and Invalidate mentre, per quelle iniziali e finali il DMA
potrà utilizzare normali scritture con snooping.
Anche nel caso puramente software la sovrascrittura di intere cacheline
può essere ottimizzata, grazie all’istruzione di invalidazione senza write-back.
Questa istruzione deve comunque essere effettuata all’inizio del trasferimento, per evitare il write-back
di eventuali cacheline dirty
sovrascrivendo porzioni di memoria che erano state già modificate dal DMA
e che non volevamo modificate.
MMU
La DMA
può utilizzare soltanto indirizzi fisici, infatti non interagisce con la MMU
.
Tuttavia il software utilizza soltanto indirizzi virtuali [b, b+n)
.
Sono quindi necessari i seguenti accorgimenti per integrare DMA
e MMU
:
DMA
andrà comunicato l’indirizzo fisico f(b)
e non quello virtuale b
[b, b+n)
attraversa più pagine non tradotte in frame contigui, il trasferimento deve essere spezzato in più trasferimenti in modo che ciascuno di essi coinvolga solo _frame contigui_.Considerando il punto 1., comunicando b
il DMA
lo utilizzerebbe come fisico, accedendo a parti di memoria che non centrano niente con il buffer (tranne nei rari casi dove b = f(b)
).
Per il punto 2. ipotizziamo invece il caso sulla destra.
Se comunicassimo f(b)
ed n
, il DMA
scriverà su [f(b), f(b) + n)
, invadendo F2
con effetti disastrosi.
Quello che vogliamo noi è invece l’intervallo fisico [f(b), f(b+n))
.
Per poterlo modificare opportunamente il trasferimento, in questo caso, deve essere spezzato in due parti:
[f(b), fineF1)
[inizioF3, f(b)+n)
.Per il punto 3. immaginando quindi di trovarci in un sistema multiprocesso che realizzi swap-in/out dei processi per poter eseguire più processi di quanti ne possano entrare in RAM.
Supponiamo quindi che un processo P1
avvii un trasferimento in DMA
attraverso un buffer privato. È quindi necessario che P1
non venga mai _swappato_, altrimenti in quegli indirizzi subentrerebbe un processo P2
che vedrebbe la sua memoria privata modificata.
Un’esempio di architettura con bus PCI
è quella nell’immagine sulla destra.
In questo caso, tra le periferiche, l’unica collegata al bus principale è il ponte ospite-PCI
. In questa architettura, è proprio lui a pilotare i fili HOLD
/HOLDA
collagati alla cache.
Avevamo già detto quando abbiamo visto il bus PCI
che diversi dispositivi possono agire da bus master
ed effettuare trasferimento dati.
Vediamo quindi in particolare come i bus master
effettuano trasferimenti da e verso la RAM.
Per essere precisi in realtà i bus master
inizializzano il trasferimento verso il ponte
non direttamente verso la RAM. È infatti il ponte
che poi reindirizza i dati alla RAM.
Poiché diversi dispositivi possono agire da bus master
dobbiamo prevederne un coordinamento, affinché non possano entrare in comunicazione tutti insieme.
Introduciamo quindi un arbitro
, un ulteriore dispositivo (spesso integrato nel ponte
stesso) che gestisce tramite handshake tutte le richieste di trasferimento.
Quando un bus master
vuole iniziare una richiesta invia il segnale di REQ
all’arbitro
.
Appena è il suo turno l’arbitro
invia un segnale chiamato di grant GNT
o segnale di acknowledge ACK
.
Per ottimizzare i tempi, l’arbitraggio può verificarsi mentre è ancora in corso una precedente transazione.
Infatti il dispositivo che ottiene GNT
, prima di iniziare la propria transazione, necessita comunque che sia FRAME#
che IRDY#
siano disattivati, ovvero che il bus sia libero. Perciò rimarrà in attesa finché la precedente operazione non sarà terminata.
I dispositivi si metteranno quindi autonomamente in coda e partiranno opportunamente e verrano eseguiti immediatamente al termine della transazione in corso.
Le informazioni vengono quindi passate attraverso l’arbitro
al ponte
, che poi si occuperà di trasferirle al destinatario sul bus principale
.
Nonostante il trasferimento avvenga quindi in asincrono, il ponte
invia comunque un segnale di trasferimento completato al bus master
che si occupa delle operazioni nel momento della ricezione in locale delle informazioni.
Questo permette infatti di ottimizzare i tempi, facendo iniziare un nuovo ciclo di ricezione/scrittura dati, anche quando in realtà le informazioni sono ancora contenute solamente in locale al ponte
.
Questa tipo di gestione delle informazioni viene chiamata bufferizzazione.
Il problema di interazione tra bus mastering
e interruzioni sorge proprio per via della bufferizzazione.
Il bus master
invia infatti EOI
all’APIC
nel momento in cui il ponte
gli comunica che ha ricevuto l’ultimo pacchetto di dati.
Non è però detto che le informazioni siano ancora state caricate in RAM quando il processore gestirà l’interruzione sollevata dall’APIC
.
Esistono diverse soluzioni a questo problema, una puramente software può essere provare a leggere un registro della periferica che si è occupata del trasferimento.
Questa lettura verrà infatti accodata alle altre in attesa sul ponte
che si riferiscono al medesimo dispositivo.
Il ponte
quindi prima terminerà il trasferimento dei dati in RAM, e solo successivamente riuscirà a compiere la lettura del registro.
Tramite questa lettura, che di per se non ci interssa nel contenuto, siamo invece sicuri che la scrittura in RAM è stata completata, e che quindi possiamo andare a recuperare i dati.
La Intel ha invece proposto una soluzione hardware che collega il ponte
all’APIC
attraverso un handshake.
L’APIC
, quando riscontra una richiesta di interruzione, prima di inviare il segnale alla CPU richiede un segnale di ACK
al ponte
.
Quest’ultimo lo fornirà solamente quando ha trasferito tutti i dati che contiene fino a quel momento.
Un ultima soluzione moderna prevede invece che le richieste di interruzione non viaggino su linee separate, ma siano inoltrate come speciali transazioni sul bus PCI
stesso, sotto forma di scritture a particolari indirizzi chiamati Message Signaled Interrupts. Poiché prima che la richiesta di interruzione arrivi, il buffer del ponte
dovrà essere svuotato dai precedenti contenuti, si risolve anche in questo modo il problema delle corse.
Vediamo un esempio pratico di un dispositivo in grado di effettuare DMA
, ovvero l’HD
.
Dobbiamo però considerare che l’HD
che abbiamo a disposizione operava il DMA
sul vecchio PC AT
. Dobbiamo quindi fornirgli un controllore DMA aggiuntivo che si occupi di effettuare i controlli descritti fin’ora per poter trasferire i dati.
Nei calcolatori con bus PCI
il controllore DMA non è più presente, poiché il suo ruolo è stato delegato ad un ponte PCI-SATA
.
Il ponte PCI-SATA
si comporta infatti da bus master
sostituendo a tutti gli effetti il controllore DMA.
L’HD
si preoccupa quindi di comunicare con il ponte
come se questo fosse il controllore. Il ponte
dal suo canto, trasferisce i dati sul PCI
in meniera coerente con le regole del bus.
Tutte le operazioni che sono descritte in seguito sono presenti nelle specifiche del nucleo, in particolare nella sezione 3.1
.
Le specifiche della programmazione dei Bus Master IDE Controller
si può trovare a questo link.
L’interfaccia implementa un meccanismo di scatter/gather che permette il trasferimento di grandi blocchi che dovranno essere sparsi/raccolti dalla memoria, utilizzando di fatto buffer discontigui. Grazie a questo meccanismo è possibile diminuire il numero di interrupt al sistema.
Per fornire informazioni sul buffer al controllore è necessario creare un PRD
(Physical Region Descriptor) che ha la seguente forma:
La funzione bus master IDE
utilizza 16Byte
dello spazio di I/O
, accessibili come Byte
, Word
o Dword
.
I registri a disposizione sono i seguenti:
Offset | Registro | Diritti di Accesso |
---|---|---|
0x00 |
Registro di comando Bus Master IDE Primario | R/W |
0x01 |
Specifico del dispositivo | |
0x02 |
Registro di stato Bus Master IDE Primario | RWC |
0x03 |
Specifico del dispositivo | |
0x04 - 0x07 |
Indirizzo tabella PRD Bus Master IDE Primario | R/W |
0x08 |
Registro di comando Bus Master IDE Secondario | R/W |
0x09 |
Specifico del dispositivo | |
0x0A |
Registro di stato Bus Master IDE Secondario | RWC |
0x0B |
Specifico del dispositivo | |
0x0C - 0x0F |
Indirizzo tabella PRD Bus Master IDE Secondario | R/W |
(R/W
sono diritti in lettura e scrittura. RWC
sono diritti in lettura e azzeramento del contenuto)
La specifica ci fornisce anche le regole che il dispositivo deve seguire:
Bus Master IDE Command Register
base + 0x00
base + 0x08
0x00
R/W
8bits
Bit | Descrizione |
---|---|
7:4 |
Riservati. Devono restituire 0 in lettura. |
3 |
Controllo di lettura o scrittura: 0 per le letture, 1 per scritture. |
2:1 |
Riservati. Devono restituire 0 in lettura. |
0 |
Avvia/Arresta Bus Master: Portato ad 1 per abilitare il funzionamento del bus master. |
Bus Master IDE Status Register
base + 0x02
base + 0x0A
0x00
R/W
, Clear
8bits
Bit | Descrizione |
---|---|
7 |
Solo simplex: Indica se i canali primario e secondario possono operare contemporaneamente (0 ) o no (1 ). |
6 |
Drive 1 DMA Capable: Indica che il drive 1 è capace di trasferimenti DMA e che il controller è ottimizzato. |
5 |
Drive 0 DMA Capable: Indica che il drive 0 è capace di trasferimenti DMA e che il controller è ottimizzato. |
4:3 |
Riservati. Devono restituire 0 in lettura. |
2 |
Interrupt: Indica che il dispositivo IDE ha sollevato un’interruzione. Si azzera scrivendo 1 . |
1 |
Errore: Indica un errore nel trasferimento dati. Si azzera scrivendo 1 . |
0 |
Bus Master IDE Active: Indica che il bus master è attivo. Si azzera al termine del trasferimento o scrivendo 0 . |
Descriptor Table Pointer Register
base + 0x04
base + 0x0C
0x00000000
R/W
32bits
Bit | Descrizione |
---|---|
31:2 |
Indirizzo base della Descriptor Table , corrispondono a A[31:2] |
1:0 |
Riservati |
La Descriptor Table
deve essere allineata a Dword
, e non deve superare il limite di 64KB
in memoria.
Esaminiamo quindi un esempio di lettura di una porzione dell’HD
:
namespace bm{
ioaddr iBMCMD; // Command Register
ioaddr iBMSTR; // Status Register
ioaddr iBMDTPR; // Descriptor Table Pointer Register
//....
}
using namespace bm;
int main (){
natb nn = BUFSIZE / 512;
natb lba = 0;
natb bus = 0, dev = 0, fun = 0;
vid::clear(0x0f);
if(!bm::find(bus,dev,fun)){
printf("bm non trovato!\n");
pause();
return 0;
}
printf("PCI−ATA at %2x:%2x:%2x\n", bus, dev, fun);
bm::init(bus, dev, fun);
// ...
Dobbiamo innanzitutto trovare il ponte tra i dispositivi PCI
installati.
Dalle specifiche ricaviamo che i primi due byte del Class Code del ponte devono valere 0x0101
(sezione 5 punto 1).
Il Class Code è il campo di 3Byte
all’offset 9
dello spazio di configurazione PCI
.
La funzine bm::find()
cerca dunque il primo dispositivo che contenga 0x0101
nella word
all’offset 10
.
bool bm::find(natb& bus, natb& dev, natb& fun) {
natb code[] = { 0xff, 0x01, 0x01 };
do {
if (pci::find_class(bus, dev, fun, code) && (code[0] & (1U << 7)))
return true;
} while (pci::next(bus, dev, fun));
return false;
}
Le specifiche ci dicono inoltre che l’indirizzo base dei registri del ponte è controllato dalla BAR
, che si trova all’offset 36
(sezione 5 punto 2).
Inoltre, sempre nelle specifiche è indicato che i registri si trovano nello spazio di I/O
agli offset 0
, 2
e 4
rispetto alla base.
void init(natb bus, natb dev, natb fun) {
natl base = pci::read_confl(bus, dev, fun, 0x20);
// Azzeriamo il bit meno significativo poiché siamo nello spazio di `I/O`.
base &= ~0x1;
iBMCMD = (ioaddr)(base + 0x00);
iBMSTR = (ioaddr)(base + 0x02);
iBMDTPR = (ioaddr)(base + 0x04);
// Settiamo i bit 0 e 2 nel registro `Command` per abilitare il ponte
// per le transazioni di I/O e a operare in bus mastering
natw cmd = pci::read_confw(bus, dev, fun, 4);
pci::write_confw(bus, dev, fun, 4, cmd | 0x5);
}
Otteniamo così la fase e possiamo ottenere gli indirizzi dei tre registri sommandovi gli offset.
Vediamo il continuo del codice:
//...
apic::set_VECT(14, HD_VECT) ;
gate_init(HD_VECT, a_bmide);
apic::set_MIRQ(14, false);
//...
Nelle prime due righe associamo la funzione a_bmide()
al piedino 14
dell’APIC
, tramite il tipo HD_VECT
.
Lo scopo di questa funzione è di invocare c_bmide()
:
extern "C" void c_bmide(){
done = true;
bm::ack();
hd::ack_intr();
apic_send_EOI();
}
void ack() {
natb work = inputb(iBMCMD);
work &= 0xFE;
outputb(work, iBMCMD);
inputb(iBMSTR);
}
Il driver dovrà quindi solamente farci sapere quando l’operazione è conclusa ponendo la variabile globale done
a true
, dichiarando diverse variabili:
volatile bool done = false;
extern char vv[];
const natl BUFSIZE = 65536;
extern natl prd[];
extern "C" void a_bmide();
Il contenuto degli nn
settori deve essere scritto in vv
, dichiarato in Assembler
per motivi che vediamo dopo.
#include <libce.h>
.text
.global a_bmide
.extern c_bmide
a_bmide:
salva_registri
call c_bmide
carica_registri
iretq
.data
.balign 4
.global prd
prd: .fill 16384, 4
.balign 4
.global vv
vv: .fill 65536, 1
Per verificare che il contenuto di vv
cambierà senza che il nostro programma vi ci scriva direttamente, lo inizializziamo con i caratteri '-'
.
//...
for(int i = 0; i < BUFSIZE; i++)
vv[i]='−';
printf("primi 80 caratteri di vv:\n");
for(int i = 0; i < 80; i++)
charwrite(vv[i]);
printf("ultimi 80 caratteri di vv:\n");
for(int i = BUFSIZE − 80; i < BUFSIZE; i++)
charwrite(vv[i]);
//...
Quando l’operazione di Bus Mastering
sarà conclusa dovremo trovare che questi caratteri sono stati sostituiti con i byte letti dall’HD
della macchina virtuale, con un numero sufficente di caratteri '@'
.
Per scrivere nell’HD
i 64k
caratteri che poi dovranno essere letti si utilizza il seguente comando:
perl -e 'print "@"x65536' | dd of=~/CE/share/hd.img conv=notrunc
Il comando prima della pipe stampa i caratteri, quello dopo li scrive nel file a partire dall’inizio e senza cambiarne le dimensioni.
//...
prd[0] = reinterpret_cast<natq>(vv) ;
prd[1] = 0x80000000 | ((nn * 512) & 0xFFFF);
bm::prepare(reinterpret_cast<natq>(prd), false);
hd::enable_intr();
hd::start_cmd(lba, nn, READ_DMA);
bm::start();
printf("aspetto l'interrupt ...\n");
while(!done)
;
printf("primi 80 caratteri di vv:\n");
for(int i = 0; i < 80; i++)
charwrite(vv[i]);
printf("ultimi 80 caratteri di vv:\n");
for(int i = BUFSIZE − 80; i < BUFSIZE; i++)
charwrite(vv[i]);
pause();
}
Il resto del programma segue abbastanza da vicino lo schema suggerito nella sezione 3.1
delle specifiche:
Prepara la tabella dei PRD
bm::prepare()
scriviamo l’indirizzo di partenza nel registro chiamato BMDTPR
, scegliendo il trasferimento verso la memoria, azzerando i bit di Interrupt
e Error
nel registro di stato:
void bm::prepare(natq prd, bool write){
outputl(prd, iBMDTPR);
natb work = inputb(iBMCMD);
if(write)
work &= ~0x8;
else
work |= 0x8;
outputb(work, iBMCMD);
work = inputb(iBMSTR);
work |= 0x6;
outputb(work, iBMSTR);
}
Programmiamo il controllore dell’HD
per il trasferimento in DMA
usando la stessa funzione hd::start_cmd()
già utilizzata per i normali trasferimenti di lettura e scrittura, con la differenza che nel registro CMD
abilitiamo il controllore a inviare ricieste di interruzione
bm::start()
avviamo anche il ponte, ponendo a 1
lo Start Bit
nel registro BMCCMD
(2.1
)
void bm::start(){
natb work = inputb(iBMCMD);
work |= 1;
outputb(work, iBMCMD);
}
Le azioni in questo punto sono svolte dal ponte e dal controllore
Aspettiamo quindi che arrivi l’interruzione
c_bmide()
tramite bm::ack()
.
Inoltre il driver disabilita ulteriori richieste di interruzione da parte del controllore (anche se non strettamente necessario).Alla fine della sezione 1.2
delle specifiche troviamo una nota:
Le regioni di memoria specificate tramite i
PRD
non devono trovarsi a cavallo dei confini di64KiB
.
Quello che accade se si attraversano questi confini può avere esiti disastrosi.
Infatti, il sommatore del ponte PCI-ATA
potrebbe essere di soli 16bit
, e quindi, dato l’indirizzo 0x1122FFFF
, passerà poi all’indirizzo 0x11220000
invece che a 0x11230000
.
Nell’esempio precedente per vedere questo comportamento è sufficente modificare le seguenti righe:
nn = BUFSIZE / 12;
// ...
// Prima dell'attesa
char *buf = (char *)((natq)&vv & 0xFFFFFFFFFFFF0000);
printf("80 byte all'indirizzo %p\n", buf);
for (int i = 0; i < 80; i++)
vid::char_write(buf[i]);
// ...
// Dopo l'attesa
printf("80 byte all'indirizzo %p\n", buf);
for (int i = 0; i < 80; i++)
vid::char_write(buf[i]);
Abbiamo sostanzialmente due modi per risolvere il problema:
assembly
la riga .balign 65536
prima della dichiarazione di vv:
Immaginiamo di avere il buffer vv
di dimensione n
. vv
potrebbe iniziare in una pagina, senza riempirla tutta.
Le successive sezioni del buffer completano le pagine nelle quali si trovano.
Potrebbe inoltre capitare che l’ultima sezione termini non in corrispondenza della fine di una pagina, ma anch’essa nel mezzo.
Per fare ciò:
ioaddr iBMPTR, ///< Indirizzo del buffer di destinazione
iBMLEN, ///< Numero di byte da trasferire
iCMD; ///< Registro di comando
paddr f, g;
char* vv;
int totali, rimanenti;
natl mutex = sem_ini(1);
// Suppongo che chi scrive i frame faccia una `sem_signal`
// quando il frame è stato caricato
natl sync = sem_ini(0);
char* tmp = vv;
while(totali > 0){
f = trasforma(tmp);
if(tmp & 0x1){
flog(ERROR_LOG, "indirizzo fisico di una pagina di vv dispari");
return 0;
}
// g primo indirizzo della pagina successiva
g = limit(f)
// Dimensione della porzione da trasmettere
rimanenti = g - f;
// Se siamo alla fine
if(rimanenti > totali){
rimanenti = totali;
}
sem_wait(mutex);
sem_wait(sync);
tmp = g;
totali -= rimanenti;
outputl(f, c->iBMPTR);
outputl(rimanenti, c->iBMLEN);
outputl(1, c->iCMD);
sem_signal(mutex);
}