8. Экранные библиотеки и работа с видеопамятью.

Терминал в UNIX с точки зрения программ - это файл. Он представляет собой два устройства: при записи write() в этот файл осуществляется вывод на экран; при чтении read()-ом из этого файла - читается информация с клавиатуры.

Современные терминалы в определенном смысле являются устройствами прямого доступа:

Традиционные терминалы являются самостоятельными устройствами, общающимися с компьютером через линию связи. Протокол* - общения образует систему команд терминала и может быть различен для терминалов разных моделей. Поэтому библиотека работы с традиционным терминалом должна решать следующие проблемы:

В UNIX эти задачи решает стандартная библиотека curses (а только первую задачу более простая библиотека termcap). Для настройки на систему команд конкретного дисплея эти библиотеки считывают описание системы команд, хранящееся в файле /etc/termcap. Кроме них бывают и другие экранные библиотеки, а также существуют иные способы работы с экраном (через видеопамять, см. ниже).

В задачах данного раздела вам придется пользоваться библиотекой curses. При компиляции программ эта библиотека подключается при помощи указания ключа -lcurses, как в следующем примере:

    cc progr.c -Ox -o progr -lcurses -lm
Здесь подключаются две библиотеки:

В начале своей программы вы должны написать директиву

    #include <curses.h>
подключающую файл /usr/include/curses.h, в котором описаны форматы данных, используемых библиотекой curses, некоторые предопределенные константы и.т.п. (это надо, чтобы ваша программа пользовалась именно этими стандартными соглашениями). Посмотрите в этот файл!

Когда вы пользуетесь curses-ом, вы НЕ должны пользоваться функциями стандартной библиотеки stdio для непосредственного вывода на экран; так вы не должны пользоваться функциями printf, putchar. Это происходит потому, что curses хранит в памяти процесса копию содержимого экрана, и если вы выводите что-либо на экран терминала обходя функции библиотеки curses, то реальное содержимое экрана и позиция курсора на нем перестают соответствовать хранимым в памяти, и библиотека curses начнет выводить неправильное изображение.

       ПРОГРАММА
        |  |
        | CURSES---копия экрана
        | printw,addch,move
        |  |
        V  V
     библиотека STDIO --printf,putchar----> экран
Таким образом, curses является дополнительным "слоем" между вашей программой и стандартным выводом и игнорировать этот слой не следует.

Напомним, что изображение, создаваемое при помощи библиотеки curses, сначала формируется в памяти программы без выполнения каких-либо операций с экраном дисплея (т.е. все функции wmove, waddch, waddstr, wprintw изменяют только ОБРАЗЫ окон в памяти, а на экране ничего не происходит!). И лишь только ПОСЛЕ того, как вы вызовете функцию refresh() ("обновить"), все изменения происшедшие в окнах будут отображены на экране дисплея (такое одновременное обновление всех изменившихся частей экрана позволяет провести ряд оптимизаций). Если вы забудете сделать refresh - экран останется неизменным. Обычно эту функцию вызывают перед тем, как запросить у пользователя какой-либо ввод с клавиатуры, чтобы пользователь увидел текущую "свежую" картинку. Хранение содержимого окон в памяти программы позволяет ей считывать содержимое окон, тогда как большинство обычных терминалов не способны выдать в компьютер содержимое какой-либо области экрана.

Общение с терминалом через линию связи (или вообще через последовательный протокол) является довольно медленным. На персональных компьютерах существует другой способ работы с экраном: через прямой доступ в так называемую "видеопамять" - специальную область памяти компьютера, содержимое которой аппаратно отображается на экране консоли. Работа с экраном превращается для программиста в работу с этим массивом байт (запись/чтение). Программы, пользующиеся этим способом, просты и работают очень быстро (ибо доступ к памяти черезвычайно быстр, и сделанные в ней изменения "проявляются" на экране почти мгновенно). Недостаток таких программ - привязанность к конкретному типу машины. Эти программы немобильны и не могут работать ни на обычных терминалах (подключаемых к линии связи), ни на машинах с другой структурой видеопамяти.

Выбор между "традиционной" работой с экраном и прямым доступом (фактически - между мобильностью и скоростью) - вопрос принципиальный, тем не менее принятие решения зависит только от вас. Видеопамять IBM PC в текстовом режиме 80x25 16 цветов имеет следующую структуру:

       struct symbol{       /* IBM PC family           */
            char chr;       /* код символа             */
            char attr;      /* атрибуты символа (цвет) */
       } mem[ 25 ] [ 80 ];  /* 25 строк по 80 символов */
Структура байта атрибутов:
     ------------------------------------------
     | 7   | 6 | 5 | 4 | 3         | 2 | 1 | 0 | # бита
     ------------------|-----------------------
     |blink| R | G | B | intensity | r | g | b | цвет
     ------------------|-----------------------
    background (фон)  | foreground (цвет букв)
R - red (красный) G - green (зеленый) B - blue (синий)
blink - мерцание букв (не фона!)
intensity - повышенная яркость

Координатная система на экране: верхний левый угол экрана имеет координаты (0,0), ось X горизонтальна, ось Y вертикальна и направлена сверху вниз.

Цвет символа получается смешиванием 3х цветов: красного, зеленого и синего (электронно-лучевая трубка дисплея имеет 3 электронные пушки, отвечающие этим цветам). Кроме того, допустимы более яркие цвета. 4 бита задают комбинацию 3х основных цветов и повышенной яркости. Образуется 2**4=16 цветов:

                 I R G B   номер цвета
    BLACK        0 0 0 0    0    черный
    BLUE         0 0 0 1    1    синий
    GREEN        0 0 1 0    2    зеленый
    CYAN         0 0 1 1    3    циановый (серо-голубой)
    RED          0 1 0 0    4    красный
    MAGENTA      0 1 0 1    5    малиновый
    BROWN        0 1 1 0    6    коричневый
    LIGHTGRAY    0 1 1 1    7    светло-серый (темно-белый)
    DARKGRAY     1 0 0 0    8    темно-серый
    LIGHTBLUE    1 0 0 1    9    светло-синий
    LIGHTGREEN   1 0 1 0   10    светло-зеленый
    LIGHTCYAN    1 0 1 1   11    светло-циановый
    LIGHTRED     1 1 0 0   12    ярко-красный
    LIGHTMAGENTA 1 1 0 1   13    ярко-малиновый
    YELLOW       1 1 1 0   14    желтый
    WHITE        1 1 1 1   15    (ярко)-белый

Физический адрес видеопамяти IBM PC в цветном алфавитно-цифровом режиме (80x25, 16 цветов) равен 0xB800:0x0000. В MS DOS указатель на эту память можно получить при помощи макроса make far pointer: MK_FP (это должен быть far или huge указатель!). В XENIX** - указатель получается при помощи системного вызова ioctl, причем система предоставит вам виртуальный адрес, ибо привелегия работы с физическими адресами в UNIX принадлежит только системе. Работу с экраном в XENIX вы можете увидеть в примере "осыпающиеся буквы".

8.1.

    /*#! /bin/cc  fall.c -o fall -lx
     *      "Осыпающиеся буквы".
     *      Использование видеопамяти IBM PC в ОС XENIX.
     *      Данная программа иллюстрирует доступ к экрану
     *      персонального компьютера как к массиву байт;
     *      все изменения в массиве немедленно отображаются на экране.
     *          Функция nap() находится в библиотеке -lx
     *      Показана также работа с портами IBM PC при помощи ioctl().
     */
    #include <stdio.h>
    #include <fcntl.h>         /* O_RDWR */
    #include <signal.h>
    #include <ctype.h>
    #include <sys/types.h>
    #include <sys/at_ansi.h>
    #include <sys/kd.h>        /* for System V/4 and Interactive UNIX only */
    /*#include <sys/machdep.h>    for XENIX and SCO UNIX only */
    #include <sys/sysmacros.h>
    #ifdef M_I386
    # define far    /* на 32-битной машине far не требуется */
    #endif
    char far *screen;   /* видеопамять как массив байт */
                        /* far - "длинный" (32-битный) адрес(segment,offset) */
    int     segm;       /* сегмент с видеопамятью */
    #define COLS  80        /* число колонок на экране */
    #define LINES 25        /* число строк */
    #define DELAY 20        /* задержка (миллисекунд) */
    int  ega;               /* дескриптор для доступа к драйверу EGA */
    /* структура для обмена с портами */
    static struct port_io_struct    PORT[ 4 /* не более 4 за раз */] = {
           /* операция       номер порта   данные */
           /*   .dir           .port        .data */
     /* Переустановить flip/flop:
      * заставить порт 0x3C0 ожидать пары адрес/значение
      * при последовательной записи байтов в этот порт.
      */
        {   IN_ON_PORT,     0x3DA,       -1               },
           /* IN-чтение */
     /* Теперь 3c0 ожидает пары адрес/значение */
        {   OUT_ON_PORT,    0x3C0,       -1 /* адрес */   },
        {   OUT_ON_PORT,    0x3C0,       -1 /* значение*/ },
           /* OUT-запись */
     /* переинициализировать дисплей, установив бит #5 порта 3c0 */
        {   OUT_ON_PORT,    0x3C0,       0x20             }
    };
    void closescr(nsig){             /* конец работы */
            setbgcolor(0);  /* установить черный фон экрана */
            exit(0);
    }
    /* получение доступа к видеопамяти адаптера VGA/EGA/CGA */
    void openscr () {
        static struct videodev {
            char *dev; int mapmode;
        } vd[] = {
            { "/dev/vga", MAPVGA },
            { "/dev/ega", MAPEGA },
            { "/dev/cga", MAPCGA },
            { NULL,       -1     }
        }, *v; /* устройство для доступа к видеоадаптеру */
        for(v=vd; v->dev;v++ )
            if((ega = open (v->dev, O_RDWR)) >= 0 ) goto ok;
        fprintf( stderr, "Can't open video adapter\n" );
        exit(1);
    ok:
        /* fprintf(stderr, "Adapter:%s\n", v->dev); */
        /* получить адрес видеопамяти и доступ к ней */
    #ifdef M_I386
        screen = (char *) ioctl (ega, v->mapmode, 0);
    #else
        segm = ioctl (ega, v->mapmode, 0);
        screen = sotofar (segm, 0); /* (segment,offset) to far pointer */
    #endif
        signal( SIGINT, closescr );
    }
    /* макросы для доступа к байтам "символ" и "атрибуты"
     * в координатах (x,y) экрана.
     */
    #define GET(x,y)        screen[ ((x) + (y) * COLS ) * 2 ]
    #define PUT(x,y, c)     screen[ ((x) + (y) * COLS ) * 2 ]  = (c)
    #define GETATTR(x,y)        screen[ ((x) + (y) * COLS ) * 2 + 1 ]
    #define PUTATTR(x,y, a)     screen[ ((x) + (y) * COLS ) * 2 + 1 ] = (a)
    /* символ изображается как черный пробел ? */
    #define white(c,a) ((isspace(c) || c==0) && (attr & 0160)==0)
    /* установить цвет фона экрана */
    void setbgcolor( color ){
         PORT[1].data = 0;  /* регистр номер 0 палитры содержит цвет фона */
         /* всего в палитре 16 регистров (0x00...0xFF) */
         PORT[2].data = color ;
         /* новое значение цвета, составленное как битовая маска
          *  RGBrgb  (r- красный, g- зеленый, b- синий, RGB- дополнительные
          *  тусклые цвета)
          */
         /* выполнить обмены с портами */
         if( ioctl( ega, EGAIO, PORT ) < 0 ){
               fprintf( stderr, "Can't out port\n" );
               perror( "out" );
         }
    }
    void main(ac, av) char **av;{
            void fall();
            openscr();
            if( ac == 1 ){
                setbgcolor(020);    /* темно-зеленый фон экрана */
                fall();             /* осыпание букв */
            } else {
                if(*av[1] == 'g')
    /* Установить режим адаптера graphics 640x350 16-colors */
                     ioctl( ega, SW_CG640x350, NULL);
    /* Если вы хотите получить адрес видеопамяти в графическом режиме,
     * вы должны СНАЧАЛА включить этот режим,
     * ЗАТЕМ сделать screen=ioctl(ega, v->mapmode, NULL);
     * и ЕЩЕ РАЗ сделать включение графического режима.
     */
    /* Установить режим адаптера text 80x25 16-colors       */
                else ioctl( ega, SW_ENHC80x25, NULL);
            }
            closescr(0);
    }
    /* осыпать буквы вниз */
    void fall(){
            register i, j;
            int rest;
            int nextcol;
            int n;
            int changed = 1;        /* не 0, если еще не все буквы опали */
            char mask [ COLS ];
            while( changed ){
               changed = 0;
               for( i = 0 ; i < COLS ; i++ )
                    mask[ i ] = 0;
               for( i = 0 ; i < COLS ; i++ ){
                    rest = COLS - i;   /* осталось осыпать колонок */
                    nextcol = rand() % rest;
                    j = 0;  /* индекс в mask */
                    n = 0;  /* счетчик */
                    for(;;){
                            if( mask[j] == 0 ){
                                    if( n == nextcol ) break;
                                    n++;
                            } j++;
                    }
                    changed += fallColumn( j );
                    mask[j] = 1;
               }
            }
    }
    /* осыпать буквы в одном столбце */
    int fallColumn( x ){
            register int y;
            char ch, attr;
            int firstspace = (-1);
            int firstnospace = (-1);
    Again:
            /* find the falled array */
            for( y=LINES-1; y >= 0 ; y-- ){
                    ch = GET( x, y );
                    attr = GETATTR( x,y );
                    if( white(ch, attr)){
                        firstspace = y;
                        goto FindNoSpace;
                    }
            }
    AllNoSpaces:
            return 0;       /* ничего не изменилось */
    FindNoSpace:            /* найти не пробел */
            for( ; y >= 0 ; y-- ){
                    ch = GET( x, y );
                    attr = GETATTR( x, y );
                    if( !white(ch, attr)){
                            firstnospace = y;
                            goto Fall;
                    }
            }
    AllSpaces:       /* в данном столбце все упало */
            return 0;
    Fall:
            /* "уронить" букву */
            for( y = firstnospace ; y < firstspace ; y++ ){
                    /* переместить символ на экране на одну позицию вниз */
                    ch   = GET( x, y );
                    attr = GETATTR( x, y );
                    PUT( x, y, 0 );
                    PUTATTR( x, y, 0 );
                    PUT( x, y+1 , ch );
                    PUTATTR( x, y+1, attr );
                    nap( DELAY );   /* подождать DELAY миллисекунд */
            }
            return 1;               /* что-то изменилось */
    }

* - Под протоколом в программировании подразумевают ряд соглашений двух сторон (сервера и клиентов; двух машин в сети (кстати, термин для обозначения машины в сети "host" или "site")) о формате (правилах оформления) и смысле данных в передаваемых друг другу сообщениях. Аналогия из жизни - человеческие речь и язык. Речь всех людей состоит из одних и тех же звуков и может быть записана одними и теми же буквами (а данные - байтами). Но если два человека говорят на разных языках - т.е. поразному конструируют фразы и интерпретируют звуки - они не поймут друг друга!

** - XENIX - (произносится "зиникс") версия UNIX для IBM PC, первоначально разработанная фирмой Microsoft и поставляемая фирмой Santa Cruz Operation (SCO).

© Copyright А. Богатырев, 1992-95
Си в UNIX

Назад | Содержание | Вперед