Il frame buffer

Una descrizione di alto livello del driver di frame buffer

di Alessandro Rubini

Riprodotto con il permesso di Linux Magazine, Edizioni Master.

Per la maggior parte degli utenti dei elaboratori, la visualizzazione di informazioni su uno schermo bidimensionale è una funzionalità fondamentale della macchina. Questa visione è chiaramente rafforzata dal fatto che il sistema operativo più diffuso al mondo si chiami banalmente «finestre», in quanto non funziona su macchine non dotate di interfaccia grafica.

In realtà esiste un altro modo di vedere il mondo, un modo che considera la gestione grafica una parte accessoria dell'operatività dell'elaboratore, che pertanto deve essere esclusa dal nucleo del sistema operativo per essere gestita, dove utile, nel solo spazio utente.

L'impostazione adottata ormai da molti anni da parte di GNU/Linux è una specie di compromesso: la visualizzazione grafica rimane dominio delle applicazioni, ma il kernel deve aiutare le applicazioni nella gestione dell'hardware. Questo aiuto si chiama frame buffer, un'interfaccia offerta da un device driver del nucleo verso le funzionalità dell'hardware grafico sottostante.

La memoria video

In tutti gli elaboratori, la memoria video è un banco di memoria RAM che contiene la rappresentazione numerica dei vari elementi dello schermo. Tale memoria può risiedere nella memoria di sistema oppure essere fisicamente parte del circuito elettronico di visualizzazione (normalmente chiamato scheda video, anche quando non risiede su una scheda separata).

In quasi tutti gli elaboratori, gli elementi di immagine sono allineati riga per riga, dall'alto in basso. Una notevole eccezione a questa regola è descritta nel riquadro 1.

Gli elementi di immagine che risiedono nella memoria video sono normalmente i pixel (termine liberamente tratto da "PICTure ELement"), ma in certi casi possono anche essere interi caratteri. In questo caso, il valore di un byte nella memoria video rappresenta un carattere testuale, che verrà trasformato nell'immagine corrispondente da inviare allo schermo da parte della circuiteria video; a questo byte spesso se ne affianca un secondo, che specifica gli attributi (per esempio il colore) del carattere testuale.

All'inizio del mondo, tutte le macchine includevano una modalità testuale di rappresentazione. Poi, talvolta per limitare i costi dell'hardware e talvolta per la palese inutilità dell'interfaccia a caratteri, la gestione testuale a livello fisico è stata abbandonata. In questi casi la memoria video contiene una rappresentazione puramente grafica dell'informazione, così che ogni rappresentazione testuale deve essere disegnata su un canovaccio grafico. Ad oggi il PC rimane l'eccezione più significativa a questa tendenza verso l'abbandono del modo testo (ma si veda il riquadro 2).

Storicamente, in ambiente GNU/Linux, il kernel ha sempre offerto un'astrazione logica verso la modalità video a caratteri. A fronte di questo, l'accesso alla modalità grafica veniva lasciato completamente in mano all'applicazione, che doveva conoscere autonomamente tutti i dettagli relativi al dispositivo grafico in uso sulla specifica macchina, usufruendo di /dev/mem come tramite per poter leggere e scrivere la memoria video. Tipicamente, le applicazioni che generavano output grafico erano il server X11 oppure programmi autonomi che sfruttavano la libreria svgalib, un buon modo per vedere il risultato del lavoro di TeX senza il carico computazionale di un server X.

Altri tempi.

Il frame buffer

Nonostante l'ostilità di Linus Torvalds verso l'introduzione di codice relativo alla grafica all'interno del nucleo, la necessità si supportare altre piattaforme lo ha portato ad un ripensamento, ed il concetto di frame buffer è stato inserito in Linux-2.2, integrando il lavoro fatto da chi aveva portato GNU/Linux su piattaforma Motorola 68000: macchine Macintosh, Amiga, Atari.

Poiché queste macchine, a differenza del PC, non dispongono di una gestione hardware dello schermo di testo, il terminale testuale deve essere emulato a livello di sistema operativo, per ottenere lo stesso tipo di comportamento verso l'utente. L'astrazione software, perciò, riproduce il comportamento dei terminali testuali (/dev/ttyN, ma anche /dev/vcsN e /dev/vcsaN) trasformando in modo del tutto trasparente il testo ASCII in un rappresentazione grafica dei caratteri, l'unica forma rappresentabile nella memoria video.

In questo contesto, una volta realizzata la gestione della memoria grafica per l'utilizzo da parte del kernel stesso, nulla vieta di offire l'accesso diretto alla memoria video in /dev, tramite un dispositivo chiamato frame buffer, con il nome da sempre usato per ogni memoria di transito (buffer) associata ad un'immagine rettangolare (frame, riquadro). Il file speciale per accedere alla memoria video della prima periferica grafica si chiama /dev/fb0, ma è possibile gestirne più di una, se l'hardware lo permette.

Il driver di frame buffer, perciò, permette di leggere e scrivere direttamente la memoria video senza passare da /dev/mem. Questo, tra l'altro, abilita una gestione dei permessi più flessibile rispetto a quella tradizionale, secondo la quale ogni applicazione grafica deve essere eseguita con i permessi dell'amministratore, in quanto /dev/mem non può essere aperto all'accesso degli utenti non privilegiati.

In aggiunta a lettura e scrittura, il frame buffer implementa una serie di comandi all'interno della chiamata di sistema ioctl, per fornire informazioni allo spazio utente ed esaudire richieste di vario tipo, come cambiamenti di configurazione.

La rappresentazione del pinguino in alto a sinistra durante l'avvio del sistema è un effetto collaterale, pressochè gratuito, della gestione della memoria grafica da parte del kernel. La memoria occupata dall'immagine viene liberata dopo l'avvio del sistema, insieme a tutta la memoria usata dalle procedure di inizializzazione.

Un esempio di moderno elaboratore elettronico che usufruisce del frame buffer è mostrato in figura 1, dove la macchina appare svestits e accesa. Si tratta di un sistema basato su processore ARM, con uno schermo a cristalli liquidi e una scheda Ethernet posticcia. Le macchine come questa, normalmente usate nei palmari o nei telefoni cellulari, non dispongono di una modo testo né si basano sullo standard VESA o altre cose tipiche del mondo x86, per cui ogni accesso allo schermo viene mediato dal driver di frame buffer.

Tra le altre cose, avere accesso diretto alla memoria video tramite un file speciale in /dev vuol dire poter disegnare sullo schermo senza dover conoscere i dettagli dell'hardware video. Come succede per ogni altra periferica visibile in /dev, la conoscenza dei dettagli implementativi rimane confinata nel kernel e le applicazioni sfruttano un'interfaccia uniforme. Così, un comando come "cat /dev/zero > /dev/fb0" pulirà lo schermo qualsiasi sia il dispositivo grafico sottostante. Allo stesso modo, l'eseguibile fbset sfrutta i comandi della chiamata di sistema ioctl per descrivere all'utente la configuazione corrente dello schermo o per cambiarla, indipendentemente dal tipo di schermo o di scheda video in uso.

Per esempio, questi comandi sono stati invocati sulla macchina della figura 1:

	bash# /addons/fbset 

mode "240x320-57" # D: 4.540 MHz, H: 18.760 kHz, V: 57.196 Hz geometry 240 320 240 320 16 timings 220264 1 1 6 1 0 1 hsync high rgba 5/11,6/5,5/0,0/0 endmode

bash# cat /dev/fb0 | wc -c 153600

Sfruttando l'accesso diretto in memoria video, è banale prendere una fotografia dello schermo o disegnarci sopra. L'unica cosa che occorre sapere è come sono disposti i pixel in memoria, ma tutte le periferiche in uso oggi dispongono l'immagine in righe consecutive. La disposizione dell'informazione nel singolo pixel, invece, viene comunicata dal dispositivo stesso, come mostrato da fbset nella riga rgba.

Il programma fbread565 (listato 1), per esempio, crea un'immagine in formato PPM a partire dal contenuto corrente dello schermo, assumendo che il formato dei pixel sia RGB565 (16 bit per pixel); questa assunzione è stata presa solo per mantenere il codice conciso e chiaro, in quanto un'implementazione più flessibile risulterebbe notevolmente più lunga. Il programma richiede la dimensione dello schermo tramite ioctl per poi recuperare i dati tramite mmap o tramite lseek/read, in base all'esistenza o meno di una vriabile di ambiente. In figura 2 è rappresentato l'output di fbread565 dopo aver eseguito i due seguenti comandi sulla console della macchina:

     bash# cat /dev/urandom > /dev/fb0
     bash# ls > /dev/tty1

La figura mostra chiaramente la sovrapposizione dell'informazione testuale (output di ls) sull'informazione grafica precedente, in questo caso i colori casuali estratti da /dev/urandom.

L'operazione opposta, visualizzare un'immagine su un frame buffer, risulta altrettanto facile: il listato 2 mostra fbwrite565.c, un programma che riceve da standard input un'immagine in formato PPM e la scrive sul frame buffer, assumendo ancora una volta di avere pixel di tipo RGB565. La figura 3 mostra una fotografia riportata sullo schermo in questo modo.

La console e i terminali

Un ruolo normalmente svolto dallo schermo di sistema è la visualizzazione dei messaggi di avvio del kernel. È possibile e talvolta molto utile redirigere la console di sistema su porta seriale, ma per i normali utenti è preferibile la visualizzazione dei messaggi sulla periferica video.

In mancanza di un sistema di console testuale, il kernel deve essere dotato, nella sua immagine eseguibile, di tutto il codice per simulare un modo testo all'interno di un sistema puramente grafico. Tale codice, in Linux-2.6, si trova in drivers/video/console. La directory include sia il supporto per la normale interfaccia testuale VGA sia quello per la console grafica, oltre ad alcuni font tra i quali scegliere, durante la compilazione del kernel, quale usare per la rappresentazione dei caratteri.

Il file dummycon.c, nella stessa directory, è particolarmente interessante perché realizza una console "stupida", da usarsi temporaneamente all'avvio del sistema, prima dell'inizializzazione di una console più completa. Si tratta di un file molto breve e comprensibile: il suo compito è definire una struttura di metodi, chiamata struct consw da "console switch", che non faccia assolutamente niente. La struttura definisce come reindirizzare le operazioni di basso livello relative alla console testuale in base al tipo di dispositivo di visualizzazione in uso; l'implementazione "stupida" è un segnaposto per permettere al sistema di lavorare anche in mancanza di un dispositivo reale su cui scrivere.

Si noti che tale switch viene utilizzato per la visualizzazione dei messaggi del kernel solo quando si stia usando la console terminale virtuale, mentre non ha effetto in caso di console seriale. Naturalmente, struct consw definisce in ogni caso come operare sul terminale di testo, anche quando tale dispositivo non funga da console di sistema. Per esempio, la macchina ARM mostrata in precedenza usa la console seriale, ma è comunque in grado di di visualizzare il testo, grazie al codice di drivers/video/console/fbcon.c, come mostrato in figura 2. L'utilizzo ambiguo del termine "console" per riferirsi sia al canale dove vengono consegnati i messaggi di sistema sia all'accoppiata tastiera/monitor, è un'eredità storica che risale alle prime versioni del nucleo, quando le due entità erano ancora indivisibili.

Normalmente, la prima console attivata durante la procedura di avvio è la console testuale VGA, o quella dummy se la VGA non è disponibile. Solo succesivamente, quando tutto il sistema del frame buffer è stato inizializzato, è possibile rimpiazzare le funzionalità iniziali con quelle della console definitiva.

Per esempio, questo è quello che si può rilevare dopo aver avviato un PC con la versione 2.6.11.3, compilata con il supporto per il frame buffer e il font 4x6:

	colera% dmesg | grep Console
	Console: colour VGA+ 80x50
	Console: switching to colour frame buffer device 160x80
	colera% sudo fbset

mode "640x480-60" # D: 25.176 MHz, H: 31.469 kHz, V: 59.942 Hz geometry 640 480 640 13107 8 timings 39721 40 24 32 11 96 2 accel true rgba 8/0,8/0,8/0,0/0 endmode

In questo caso, si è passati da una modalità testuale di 80x50 caratteri ad una modalità grafica di 640x480 pixel, che si traduce in 160x80 caratteri grazie al font di 4x6 pixel -- che ammetto essere poco leggibile.

La struttura consw, cui abbiamo già accennato, è riportata integralmente nel riquadro 4. Si tratta di una struttura che ospita molti metodi di basso livello, in quanto i meccanismi per la gestione dei terminali virtuali devono occuparsi non solo della visualizzazione del testo, ma anche dello scroll, del cursore, del cambio di terminale virtuale e della modifica del font (che, tra l'altro, può essere diverso da un terminale virutale all'altro).

I meccanismi di più basso livello

Il prossimo mese vedremo in dettaglio le strutture dati relative ad un driver di frame buffer e le funzionalità offerte dal kernel, unitamente ad un esempio completo di driver "parassita" che si appoggia su un frame buffer esistente.



Figura 1
un prototipo nudo



Figura 2
il contenuto di /dev/fb0



Figura 3
effetto di fbwrite565


Riquadro 1 - la memoria video dell'Apple2

L'Apple 2 è, o era, un elaboratore basato su un processore 6502 con frequenza di clock di 1MHz. La memoria RAM prodotta negli anni '80, la cosiddetta DRAM (RAM dinamica), deve essere rinfrescata ogni manciata di millisecondi, tramite un ciclo di lettura. Senza tale lettura, la memoria perde il suo contenuto.

Come tutte le macchine professionali della sua era, l'Apple 2 è dotato di una modalità grafica testuale, tramite la quale un byte in memoria video determina il contenuto di una cella di 8x8 pixel sullo schermo, tramite un font di caratteri contenuto in memoria non volatile.

A differenza dei suoi coetanei, però, la disposizione in memoria dei codici ASCII della schermata video non è sequenziale. Le 24 righe sono divise in tre gruppi da 8 e in memoria si succedono, nell'ordine, le prime righe di ogni gruppo (1, 9, 17), alcuni byte inutilizzati, poi le seconde di ogni gruppo (2, 10, 18), altri byte inutilizzati, e così via. La disposizione della memoria grafica è simile.

Questa disposizione è stata scelta dal progettista in modo da poter sfruttare la lettura periodica della memoria video (necessaria per la generazione del segnale verso il monitor) al fine di rinfrescare la memoria RAM. Si è preferito in questo caso richiedere ai programmi di fare un'operazione in più al fine di ottimizzare la progettazione hardware di sistema. Inoltre, pilotando la memoria a frequenza doppia rispetto al processore, le letture periodiche della memoria video non influiscono minimamente sulle prestazioni dell'elaborazione dati.

Altre macchine dello stesso periodo offrono una disposizione sequenziale delle righe della memoria video ma devono fermare in continuazione il clock del processore per poter generare il segnale video.

Che io sappia, nessuna altra macchina adotta usa un simile riordinamento delle righe. Persistono altre stranezze nella disposizione dei dati in memoria, ma oggi nello scrivere programmi come fbread ed fbwrite si può assumere che i pixel siano disposti in memoria sequenzialmente da sinistra a destra e dall'alto in basso.


Riquadro 2 - il modo testo e il processore Geode

Ogni macchina di tipo PC (x86) deve presentarsi al mondo come un IBM PC del 1980 (e se contiene alcune estensioni successive deve normalmente offrire esattamente le stesse funzionalità offerte dai suoi predecessori). Ogni macchina che si discosti non è un PC. Un effetto di questa regola è la necessità imprescindibile per un PC moderno di offrire un modo testo VGA, per esempio perché Il BIOS si appoggia su questo modo testo e non si può pensare seriamente di eliminare il BIOS da un PC. Il modo testo, però, è completamente superfluo una volta avviata la macchina.

Per questo motivo, alcuni produttori di cosiddetti system on chip (SOC), integrando la periferica video nel processore hanno deciso di eliminare il modo testo, sostituendolo con una emulazione software, che risulta più economica da vari punti di vista -- spesso lo stesso lavoro di emulazione viene fatto per tastiera e mouse PS/2.

Questa emulazione, però, non può essere fatta dal sistema operativo, in quanto deve essere già attiva quando l'utente configura il proprio BIOS. I costruttori che realizzano tale emulazione spesso nemmeno ne documentano l'esistenza, per motivi commerciali di immagine. Il processore PC-compatibile "Geode" è un bell'esempio di questo approccio: si tratta di un processore uscito qualche anno fa, dai consumi molto modesti e relativamente veloce nel calcolo.

Il Geode è stato adottato come soluzione ideale per molti problemi, compresi quelli che richiedono sistemi operativi hard real time, come RTAI, salvo mostrare latenze nella risposta di due-tre ordini di grandezza superiori ai 486 o ai pentium. Il motivo di questo comportamento inaccettabile è stato infine ricondotto al lampeggiamento del cursore sullo schermo testuale. I sistemi in tempo reale non hanno normalmente un monitor e nessuno si preoccupa di toccare la configurazione della periferica video; in questo caso però l'emulazione del modo testo viene effettuata dal processore stesso tramite un'interruzione nascosta ad alta priorità, per cui il lampeggiamento di un cursore che nessuno vede rende inutilizzabile la macchine per un compito di acuisizione dati, almeno finchè qualcuno non attiva un modo grafico sulla macchina.


Riquadro 3 - struct consw
struct consw {
    struct module *owner;
    const char *(*con_startup)(void);
    void    (*con_init)(struct vc_data *, int);
    void    (*con_deinit)(struct vc_data *);
    void    (*con_clear)(struct vc_data *, int, int, int, int);
    void    (*con_putc)(struct vc_data *, int, int, int);
    void    (*con_putcs)(struct vc_data *, const unsigned short *, int, int, int);
    void    (*con_cursor)(struct vc_data *, int);
    int     (*con_scroll)(struct vc_data *, int, int, int, int);
    void    (*con_bmove)(struct vc_data *, int, int, int, int, int, int);
    int     (*con_switch)(struct vc_data *);
    int     (*con_blank)(struct vc_data *, int, int);
    int     (*con_font_set)(struct vc_data *, struct console_font *, unsigned);
    int     (*con_font_get)(struct vc_data *, struct console_font *);
    int     (*con_font_default)(struct vc_data *, struct console_font *, char *);
    int     (*con_font_copy)(struct vc_data *, int);
    int     (*con_resize)(struct vc_data *, unsigned int, unsigned int);
    int     (*con_set_palette)(struct vc_data *, unsigned char *);
    int     (*con_scrolldelta)(struct vc_data *, int);
    int     (*con_set_origin)(struct vc_data *);
    void    (*con_save_screen)(struct vc_data *);
    u8      (*con_build_attr)(struct vc_data *, u8, u8, u8, u8, u8);
    void    (*con_invert_region)(struct vc_data *, u16 *, int);
    u16    *(*con_screen_pos)(struct vc_data *, int);
    unsigned long (*con_getxy)(struct vc_data *, unsigned long, int *, int *);
};


Riquadro 4 - approfondimenti

Tutto il codice di frame buffer si trova in drivers/video/, mentre la gestione della monitor di sistema sta in drivers/video/console. La console propriamente detta viene gestita da poche funzioni in kernel/printk.c, mentre tutte le console seriali stanno nella directory drivers/serial.

La directory Documentation/fb contiene vari file che documentano le specificità di alcuni driver di basso livello ma anche l'architettura generale del sistema (internals.txt).

I sorgenti dei programmi di esempio sono disponibili come http://www.linux.it/kerneldocs/fb/src.tar.gz


Listato 1 - fbread565

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <fcntl.h>
#include <errno.h>

#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/mman.h>

#include <linux/fb.h>

struct fb_var_screeninfo var;
struct fb_fix_screeninfo fix;

int main(int argc, char **argv)
{
    int i, j, x, y;
    int fd;
    unsigned char *addr;
    int usemmap = (int)getenv("USEMMAP");

    if (argc > 1) exit(1);

    fd = open("/dev/fb0", O_RDWR);
    if (fd < 0) exit(2);

    if (ioctl(fd, FBIOGET_VSCREENINFO, &var)) exit(3);
    if (ioctl(fd, FBIOGET_FSCREENINFO, &fix)) exit(4);

    addr = mmap(0, fix.line_length * var.yres, PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == (unsigned char *)-1) exit(5);

    x = var.xres; y = var.yres;
    printf("P6\n%i %i \n255\n", x, y);

    for (j=0; j<y; j++) {
	unsigned short thispix;
	for (i=0; i<x; i++) {
	    int index;
	    index = j * fix.line_length + i*2;
	    if (usemmap) {
		thispix = *(unsigned short *)(addr + index);
	    } else {
		lseek(fd, index, SEEK_SET);
		read (fd, &thispix, 2);
	    }
	    putchar((thispix & 0xf800) >> 8);
	    putchar((thispix & 0x07e0) >> 3);
	    putchar((thispix & 0x001f) << 3);
	}
    }
    exit(0);
}    


Listato 2 - fbwrite565

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <fcntl.h>
#include <errno.h>

#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/mman.h>

#include <linux/fb.h>

struct fb_var_screeninfo var;
struct fb_fix_screeninfo fix;

int main(int argc, char **argv)
{
    int i, j, x, y, z;
    int fd;
    char head[256]="";
    char s[80];
    unsigned char pix[3];
    unsigned char *addr;
    int usemmap = (int)getenv("USEMMAP");

    if (argc > 1) exit(1);

    while(1) {
	/* parse ppm header */
        if (!fgets(s, 80, stdin)) exit(2);
        if (s[0] == '#') continue;
        if (strlen(head) + strlen(s) > 255) exit(3);
        strcat(head, s);
        if (head[0]!='P' || head[1]!='6') exit(4);
        if (sscanf(head, "P6 %i %i %i", &x, &y, &z) == 3)
            break;
    }

    fd = open("/dev/fb0", O_RDWR);
    if (fd < 0) exit(5);

    if (ioctl(fd, FBIOGET_VSCREENINFO, &var)) exit(6);
    if (ioctl(fd, FBIOGET_FSCREENINFO, &fix)) exit(7);

    addr = mmap(0, fix.line_length * var.yres, PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == (unsigned char *)-1) exit(8);

    for (j=0; j<y; j++) {
	unsigned short thispix;
	for (i=0; i<x; i++) {
	    int index;
	    fread(pix, 1, 3, stdin);
	    if (i >= var.xres)
		continue;
	    if (j >= var.yres)
		continue;
	    index = j * fix.line_length + i*2;
	    thispix = (((short)pix[0]<<8) & 0xf800)
		| (((short)pix[1]<<3) & 0x07e0)
		| ((short)pix[2]>>3);
	    if (usemmap) {
		*(unsigned short *)(addr + index) = thispix;
	    } else {
		lseek(fd, index, SEEK_SET);
		write (fd, &thispix, 2);
	    }
	}
    }
    exit(0);
}