di Alessandro Rubini
Riprodotto con il permesso di Linux Magazine, Edizioni Master.
Il codice che implementa l'astrazione di frame buffer
Dopo aver discusso, nel precedente articolo, l'accesso ad una periferica di frame buffer da parte dello spazio utente, questo pezzo descrive l'implementazione di un driver di frame buffer in Linux-2.6.12-rc2.
Il problema più ricorrente affrontato dagli sviluppatori del kernel è quello dell'astrazione di una funzionalità dalle caratteristiche dell'hardware sottostante tramite la definizione di strutture dati che permettano di descrivere le singole istanze implementative. Una buona organizzazione delle strutture dati è d'altronde alla base della buona programmazione, come già sottolineava Frederick Brooks nel '75 in "The Mythical Man-Month".
Un driver di frame buffer, perciò, si riduce principalmente ad una struttura dati che descriva l'hardware che si vuole pilotare: le dimensioni dello schermo, la disposizione dei pixel in memoria video, l'indirizzo fisico della memoria video.
Per presentare al kernel la propria periferica video, occorre compilare tre strutture dati di lavoro e una di gestione.
Le strutture di lavoro sono struct fb_ops
, che contiene puntatori
a funzioni definite nel driver stesso, come tutte le strutture dati il
cui nome finisce in _ops
oppure _operations
;
struct fb_var_screeninfo
, che definisce
le caratteristiche del dispositivo
o del modo video configurabili dall'utente (cioè "variabili");
struct fb_fix_screeninfo
che definisce le caratteristiche
di dispositivo e modo video che sono immutabili ("fisse").
In realtà, alcuni campi della struttura fix
non sono realmente
fissi, perché vengono modificati dal driver quando si passa da un
modo video ad un altro (per esempio, passando da un modo in bianco e
nero ad un modo a colori); così non tutti i campi della struttura
var
possono essere modificati, infatti uno dei metodi nella
struttura ops
si chiama fb_check_var
proprio per verificare
che la struttura passata dallo spazio utente descriva una situazione
accettabile.
La struttura di più alto livello, struct fb_info
, contiene un
puntare a struct fb_ops
e una istanza di ognuna delle
strutture fix
e var
. È consuetudine, per migliorare
la leggibilità del codice, definire le tre strutture indipendentemente
per poi assegnare i campi fix
e var
di struct sb_info
al momento del caricamento del modulo.
Tutte le strutture dati relative ad un frame buffer sono definite in
<linux/fb.h>
, dove ogni campo è brevemente commentato. Il
listato 1 mostra le strutture dati definte da smallfb, un modulo di
esempio che implementa un frame buffer per Linux-2.6. Le strutture
implementate sono minimali, per limitare al massimo la complessità
dell'esempio.
Listato 1 - strutture dati di smallfb
static struct fb_ops sf_ops = {
.fb_check_var = NULL, /* no check: we are lazy */
.fb_set_par = NULL, /* no set par: we are lazy */
.fb_setcolreg = NULL, /* no colormaps */
.fb_pan_display = NULL, /* no pan support */
.fb_mmap = NULL /*sf_mmap*/,
/* these are mandatory and implemented elsewhere */
.fb_fillrect = cfb_fillrect,
.fb_copyarea = cfb_copyarea,
.fb_imageblit = cfb_imageblit,
/* we have no cursor support */
.fb_cursor = soft_cursor,
};
struct fb_var_screeninfo sf_var __initdata = {
.xres = SF_WIDTH,
.yres = SF_HEIGHT,
.xres_virtual = SF_WIDTH,
.yres_virtual = SF_HEIGHT,
.red = {16,8,},
.green = { 8,8,},
.blue = { 0,8,},
.bits_per_pixel = 8 * SF_BPP,
.vmode = FB_VMODE_NONINTERLACED,
};
struct fb_fix_screeninfo sf_fix __initdata = {
.id = "small-FB",
.type = FB_TYPE_PACKED_PIXELS,
.visual = FB_VISUAL_TRUECOLOR,
.line_length = SF_STRIDE * SF_BPP,
.smem_start = SF_BASE,
.smem_len = SF_HEIGHT * SF_STRIDE * SF_BPP,
.accel = FB_ACCEL_NONE,
};
/* description of this framebuffer */
struct fb_info sf_info = {
.fbops = &sf_ops,
.screen_base = 0 /* to be compiled at load time */,
.screen_size = SF_STRIDE * SF_HEIGHT * SF_BPP,
.state = FBINFO_STATE_RUNNING,
};
In un dispositivo embedded, come quello mostrato il mese scorso, il ruolo del driver di frame buffer risulta in genere abbastanza semplice: nella fase di inizializzazione il driver deve configurare i registri del microprocessore per attivare l'uscita video appropriata e poi tutto funziona autonomamente. Le applicazioni dovranno poi leggere e scrivere i pixel usando /dev/fb0, ma non potranno cambiare la modalità video né altre caratteristiche del driver o della modalità di uscita dell'informazione.
Nel mondo PC, invece, normalmente le applicazioni in spazio utente possono cambiare la modalità video durante il funzionamento della macchina. Questo avviene in base al tipo di monitor collegato alla macchina, in base alle preferenze dell'utente o anche solo in base allo specifico modello di scheda video, tra quelle supporte dal driver, che si sta usando.
Per evitare questa complessità e ricondurci invece ad una situazione più semplice, con un solo modo video, il codice di smallfb si appoggia sulla modalità grafica di X11. Se da un lato questo vincola a caricare il modulo solo dopo aver avviato X, dall'altro la semplificazione del codice è impareggiabile: il listato 2 contiene tutto il codice necessario a far girare il modulo, con la sola eccezione delle strutture dati, già mostrate nel listato 1.
Il modulo smallfb, nella forma mostrata, si appoggia su un modo grafico a 24 bit per pixel, di risoluzione 1024x768. L'indirizzo di partenza della memoria video, 0xf8200000, è stato ricavato osservando le mappe di meoria del processo X, come descritto nel riquadro 1.
Dopo aver caricato il modulo sulla macchina, si può verifcare
la sua presenza in /proc/fb
, ed ottenere le sue caratteristiche
tramite il comando fbset.
lama% cat /proc/fb 0 small-FB lama% fbset -fb /dev/fb0 mode "240x320" geometry 240 320 240 320 24 timings 0 0 0 0 0 0 0 rgba 8/16,8/8,8/0,0/0 endmode
Si noti come nel caso mostrato il sistema non contiene alcun altro dispositivo di tipo frame buffer; in un sistema con un driver di frame buffer per la scheda video, il modulo smallfb si troverebbe su /dev/fb1 invece che su /dev/fb0. Inoltre, si nota come i parametri di temporizzazione video non siano stati definiti nelle strutture dati.
La dimensione, di 240x320 pixel è tipicamente quella di un elaboratore
palmare. L'area di pixel corrispondente a smallfb viene rappresenta
sullo schermo della macchina ospite nell'angolo in alto a sinistra, ma
nulla vieta di usare un'altra posizione, cambiando l'indirizzo di
partenza. L'allineamento verticale tra righe successive è stato
realizzato tramite la definizione della macro SF_STRIDE
(nel
listato 2), che viene usato nel listato 1 per definire il campo
line_length
in struct fb_fix_screeninfo
.
Il campo line_length
è stato concepito proprio al fine di
supportare aree di memoria video in cui la riga visualizzata non
occupi tutta la memoria compresa tra l'inizio di una riga
e l'inizio della successiva.
Per esempio, è comune per gli schermi di 240 pixel di allineare
le righe ad una distanza di 256 pixel una dall'altra (quindi
512 byte, se si tratta di display a colori
con 16 bit per pixel).
Se da un lato questo allinemaneto porta ad uno spreco di memoria
pari a 16 pixel ogni 240, il calcolo
dell'indirizzo di memoria associatao ad un pixel di coordinate date
risulta semplificato, permettendo di lavorare per campi di bit.
Nell caso di smallfb, l'uso di line_length
permette
l'allineamento del frame buffer virtuale all'interno di quello fisico.
La figura 1 mostra la schermata di X11 dopo aver invocato il programma fbwrite888, parente stretto di fbwrite565 presentato il mese scorso. In questo caso si è dato in pasto ad fbwrite una delle fotografie usate nell'articolo precedente,find ridotta a 240x320.
Si noti che i byte aggiuntivi tra una riga del frame buffer e la successiva, generalmente non vengono utilizzati in alcun modo e possono essere letti o sovrascritti senza effetti collaterali. Per questo motivo, ma anche per evitare inutili complicazioni nel codice del kernel, i metodi read e write del driver di frame buffer non saltano le aree di fine-riga, ma più semplicemente aprono una finestra sulla memoria fisica comprensiva dei pixel non visualizzati.
Una semplice verifica di questo comportamento può essere la copia di /dev/zero o /dev/urandom su smallfb (cioè /dev/fb0 o /dev/fb1); tale copia sovrascriverà il contenuto delle prime 320 righe dello schemo di X, per tutta la loro lunghezza. Oppure si può più semplicemente verificare la dimensione di smallfb:
lama% wc -c /dev/fb0; expr 1024 \* 3 \* 320 983040 /dev/fb0 lama% expr 1024 '*' 3 '*' 320 983040
Listato 2 - il codice di smallfb
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/fb.h>
#include <linux/init.h>
#include <asm/uaccess.h>
#define SF_WIDTH 240
#define SF_HEIGHT 320
#define SF_BPP 3 /* bytes per pixel */
#define SF_STRIDE 1024
#define SF_BASE 0xf8200000
/* init and cleanup */
int __init sf_init(void)
{
int ret;
char __iomem *addr;
sf_info.fix = sf_fix;
sf_info.var = sf_var;
addr = ioremap(SF_BASE, sf_info.screen_size);
if (!addr) return -ENOMEM;
sf_info.screen_base = addr;
ret = register_framebuffer(&sf_info);
if (ret)
iounmap(addr);
else
printk("small-FB registered\n");
return ret;
}
void __exit sf_exit(void)
{
unregister_framebuffer(&sf_info);
iounmap(sf_info.screen_base);
printk("small-FB unregistered\n");
}
module_init(sf_init);
module_exit(sf_exit);
Riquadro 1 - le mappe di memoria
Il kenrel esporta, per ogni processo, le informazioni sulle mappe di memoria virtuale del processo stesso. Nel caso del server X tali mappe includono una finestra verso la schermata grafica.
Se il server grafico si appoggia direttamente sull'hardware, senza
usare il dispositivo fb
del kernel, tale accesso viene
effettuato tramite /dev/mem, usando come offset
l'indirizzo fisico del frame buffer della scheda video.
Questo, per esempio, e` quanto trovo sul mio portatile, relativamente
al processo X:
lama% cat /proc/2369/maps
00000000-000a0000 rwxs 00000000 00:07 65537 /SYSV00000001 (deleted)
000a0000-000c0000 rwxs 000a0000 00:0e 1717 /dev/mem
000c0000-000f0000 rwxs 000c0000 00:0e 1717 /dev/mem
000f0000-00100000 r-xs 000f0000 00:0e 1717 /dev/mem
08048000-081c2000 r-xp 00000000 03:02 555815 /usr/X11R6/bin/XFree86
081c2000-081f4000 rwxp 00179000 03:02 555815 /usr/X11R6/bin/XFree86
081f4000-0890b000 rwxp 081f4000 00:00 0 [heap]
b735c000-b73fc000 rwxs 00000000 00:07 65537 /SYSV00000001 (deleted)
b73fc000-b740c000 rwxs 000a0000 00:0e 1717 /dev/mem
b740c000-b7c0c000 rwxs f8200000 00:0e 1717 /dev/mem
b7c0c000-b7e0c000 rwxs f8000000 00:0e 1717 /dev/mem
b7e0c000-b7e5e000 rwxp b7e0c000 00:00 0
b7e5e000-b7f88000 r-xp 00000000 03:01 272016 /lib/tls/libc-2.3.2.so
b7f88000-b7f91000 rwxp 00129000 03:01 272016 /lib/tls/libc-2.3.2.so
b7f91000-b7f93000 rwxp b7f91000 00:00 0
b7f93000-b7f9b000 r-xp 00000000 03:01 160101 /lib/libgcc_s.so.1
b7f9b000-b7f9c000 rwxp 00007000 03:01 160101 /lib/libgcc_s.so.1
b7f9c000-b7f9e000 r-xp 00000000 03:01 272018 /lib/tls/libdl-2.3.2.so
b7f9e000-b7f9f000 rwxp 00002000 03:01 272018 /lib/tls/libdl-2.3.2.so
b7f9f000-b7fc0000 r-xp 00000000 03:01 272019 /lib/tls/libm-2.3.2.so
b7fc0000-b7fc1000 rwxp 00020000 03:01 272019 /lib/tls/libm-2.3.2.so
b7fc1000-b7fc2000 rwxp b7fc1000 00:00 0
b7fc2000-b7fd3000 r-xp 00000000 03:02 326497 /usr/lib/libz.so.1.2.2
b7fd3000-b7fd4000 rwxp 00010000 03:02 326497 /usr/lib/libz.so.1.2.2
b7fe9000-b7fea000 rwxp b7fe9000 00:00 0
b7fea000-b8000000 r-xp 00000000 03:01 160019 /lib/ld-2.3.2.so
b8000000-b8001000 rwxp 00015000 03:01 160019 /lib/ld-2.3.2.so
bffeb000-c0000000 rwxp bffeb000 00:00 0 [stack]
ffffe000-fffff000 ---p 00000000 00:00 0 [vdso]
Si noti, guardando la quarta colonna del file, come /dev/mem
viene "visto" (tramite la chiamata di sistema mmap) a partire
da indirizzi differenti: alcuni intervalli di indirizzi bassi (il BIOS
della scheda video e quello di sistema) e gli indirizzi
0xf8000000
ed 0xf8200000
. Uno dei due
corrisponde sicuramente alla memoria della scheda grafica, come si
puo` verificare usando lspci -v. Nel mio caso
l'indirizzo da usare si e` rivelato essere 0xf8200000
.
Un modulo come smallfb naturalmente ha solo un ruolo esemplificativo riguardo al codice del kernel, in quanto non è possibile usare il frame buffer associato come se fosse il display principale di sistema.
Se da un lato X11 continuerà a modificare i pixel di tutto lo schermo reale in base alle sue necessità, dall'altro riscontriamo una serie di difficoltà dovute all gestione dei terminali virtuali e della console di sistema.
Per risolvere il primo problema è possibile aprire una finestra fantasma di 240x320 pixel che rimanga sempre sopra le altre e non abbia alcun bordo aggiunto dal gestore di finestre. Per esempio, il programma fbanta, mostrato nel listato 3, è una prima approssimazione verso questo obiettivo anche se mostra serie limitazioni: le altre finestre possono comunque coprire la nostra immagine, anche se per un attimo solo, mentre l'aggiornamento dello sfondo grigio di wish copre proprio l'immagine che volevamo preservare.
Il secondo problema, relativo ai terminali virtuali, impedisce di far girare una sessione di X11 o altri ambienti grafici sul nostro frame buffer.
Nel driver di terminale virtuale (drivers/char/vt.c
e drivers/char/vt_ioctl.c), il
kernel permette ad un processo di prendere il controllo di uno
specifico terminale e di gestire l'entrata e l'uscita da tale
terminale virtuale. Questo meccanismo si chiama controllo da
parte di un processo (process control nei sorgenti) per lo specifico
terminale. Per esempio, in una normale installazione il sistema fa
girare 6 istanze di getty in modo testo (terminali da 1 a 6);
il terminale 7 viene poi gestito dal server X, prima sotto il controllo di
un display manager (xdm
, gdm
o kdm
) e, dopo
l'autenticazione, come sessione grafica di uno specifico utente. Il
server X all'avvio, dopo aver aperto il terminale 7, ne assume il
controllo in modo da poter gestire le transizioni da e verso tale
terminale. In questo modo X11 può essere l'unica applicazione che
manipola i registri della scheda video, controllando sia l'ingresso
nel terminale controllato (per esempio quando si usa Alt-F7 da un
terminale testuale) sia l'uscita da esso (per esempio quando si usa
Ctrl-Alt-F1 dalla sessione grafica).
I comandi di ioctl che implementano questi meccanismi sono
VT_ACTIVATE
, VT_WAITACTIVE
e VT_RELDISP
, a cui viene
affiancato VT_OPENQRY
usato per chiedere al sistema quale sia
il primo terminale non ancora in uso -- motivo per cui X normalmente
gira sul terminale 7
Questa infrastruttura di controllo di processo per i terminali virtuali è fondamentale per un corretto funzionamento degli applicativi grafici anche nei casi in cui non serva riprogrammare la periferica video, come i sistemi embedded, in cui il frame buffer non cambia mai di risoluzione. Poiché un cambio di terminale comporta il rinfresco completo della schermata, l'applicativo grafico deve poter controllare questi eventi in quanto il kernel non può procedere autonomamente al rinfresco dell'immagine. Diversa e` la situazione per i terminali in modo testo, perche` in questo caso la schermata viene gestita dal codice del kernel, che puo` effettuare autonomamente la commutazione da un terminale all'altro.
È proprio l'infrastruttura appena descritta che impedisce di far
girare un server X o un altro applicativo grafico evoluto su
smallfb: tale applicativo chiederà di aprire un nuovo terminale
(VT_OPENQRY
), renderlo il terminale corrente (VT_ACTIVATE
) e
assumerne il controllo (VT_SETMODE
). In una installazione di
default, VT_OPENQRY
assegnerà il terminale 8 al nuovo processo, e
VT_ACTIVATE
, il cui codice invoca change_console()
, manderà
un segnale al server X per segnalare l'uscita dal terminale 7. A questo punto X
riprogrammerà la scheda video per la modalità testo in cui si
trovava prima di prenderne il controllo, e il nostro frame buffer
parassita scomparirà insieme alla schermata grafica. Una volta
acquisito il terminale 8, l'applicativo andrà a scrivere sulla
memoria di smallfb che però non è più visualizzata sullo schermo
e probabilmente svolge un altro ruolo nella VGA testuale. L'effetto
finale dipende dalla specifica istanza di VGA in uso e non sono da
escludersi guai grossi nel sottosistema video o nel kernel in
generale.
Naturalmente la situazione è meno problematica per chi stia usando un frame buffer anche per la modalità testo, con la stessa configurazione hardware usata da X, ma comunque sconsiglierei di toccare alcunchè nella propria scheda video mentre smallfb è caricato.
#!/usr/bin/wish
wm geometry . 240x320+0+0
wm overrideredirect . 1
bind . <Visibility> "raise ."
bind . <Expose> "break"
Normalmente una schermata grafica viene usata più per mostrare finestre piene di bottoni e immagini in movimento che per riempire lo schermo un pixel alla volta come abbiamo fatto noi con fbwrite. Questa constatazione, unita all'ubiquità dell'astrazione frame buffer, che troviamo tanto sui cellulari in bianco e nero quanto sulle macchine da tavolo con risoluzione di 1600x1200 pixel a colori, fa comprendere come qualche scorciatoia nell'esecuzione di operazioni ripetitive potrebbe essere benefica per le prestazioni del sistema grafico.
I tipi di accelerazione offerti dalle varie periferiche grafiche sono alquanto variabili nei dettagli, ma le forme più comuni consistono nel riempimento di rettangoli, nella copia di aree all'interno dello schermo e nella visualizzazione di immagini preconfezionate in memoria. Queste tre forme di accelerazione, perciò, vengono sempre offerte dal device driver di frame buffer, indipendentemente dalla presenza o meno di qualche aiuto da parte dell'hardware. Se l'hardware non implementa queste funzionalità, il driver può appoggiarsi su implementazioni software generiche già implementate nel kernel; questo è per esempio il percorso scelto in smallfb, dove i puntatori alle funzioni cfb_fillrect, cfb_copyarea, cfb_imageblit si riferiscono proprio a queste funzioni generiche (dove cfb sta per color frame buffer).
In questo modo, ogni applicazione che debba svolgere uno di questi compiti può appoggiarsi al codice accelerato, senza bisogno di scegliere il suo comportamento in base alle caratteristiche dell'hardware sottostante. Allo stesso modo, di questi tre metodi accelerati di frame buffer usufruisce il codice della console testuale (frame buffer console), qualunque sia il dispositivo sottostante.
Altre forme di accelerazione vengono implementate dagli specifici
driver e comunicate allo spazio utente tramite un identificativo
numerico, il cui nome simbolico inizia con FB_ACCEL_
(nel caso di
smallfb, FB_ACCEL_NONE
). Tale identificativo fa parte di
struct fb_fix_screeninfo
e viene recuperato dallo spazio utente
tramite il comando ioctl FBIOGET_FSCREENINFO
, lo stesso
usato in fbwrite, insieme FBIOGET_VSCREENINFO
, per sapere
le caratteristiche del frame buffer in uso.
Documentation/fb/internals.txt, già indicato il mese scorso, descrive la struttura interna di un driver di frame buffer.
drivers/video/skeletonfb.c è uno scheletro di driver. Non è possibile usarlo in pratica come abbiamo fatto con smallfb ma è molto ben commentato e copre anche argomenti come la selezione tra diversi tipi di pixel, che sono stati lasciati completamente fuori sia da smallfb sia dalla discussione.
drivers/video/vfb.c è un driver realmente funzionante, che implementa un frame buffer virtuale. La sua virtualità sta nel non essere associato ad alcun dispositivo di visualizzazione, ma vive in memoria virtuale (allocata con vmalloc); il contenuto dello schermo virtuale può essere estratto con read o mmap. È una buona palestra per sperimentare codice relativo al frame buffer qsia in spazio kernel sia in spazio utente.
Il modulo smallfb e` disponibile, insieme con fbwrite per display
a 16 e a 24 bit, in http://www.linux.it/kerneldocs/fb2/src.tar.gz
.