Implementazione del frame buffer

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.

Le strutture dati del frame buffer

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,
};

Il modulo smallfb

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



Figura 1
smallfb all'interno di una sessione X

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.

Interazione tra smallfb e la grafica ospite

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.

Listato 3 - fbanta


#!/usr/bin/wish
wm geometry . 240x320+0+0
wm overrideredirect . 1
bind . <Visibility> "raise ."
bind . <Expose> "break"

Le accelerazioni grafiche

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.

Riquadro 2 - Approfondimenti

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.