суббота, 13 июля 2013 г.

PhantomEx: Программирование видеопамяти

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

Каким образом можно организовать вывод? Программируем мы на C и в C есть функция printf(...) для организации форматированного вывода. Ага, не тут то было! Ничего у нас не выйдет.

Стандартые библиотеки C мы не используем, да и смысл они имеют при работе в среде какой-нибудь работающей операционной системы, так как используют системные вызовы. Так что printf(...) или любого другого его аналога у нас нет.

Следующее что приходит на ум - функции BIOS для вывода символов на экран. Тут мы тоже терпим фиаско - функции BIOS не доступны в защищенном режиме процессора Intel. А мы находимся именно в защищенном режиме, нас туда перевел GRUB2.

Остается только один вариант - использование видеопамяти и создание собственных функций вывода информации на экран


1. Видеопамять глазами программиста


 Видеопамять - это участок памяти, доступной процессору, начинающийся с адреса 0xB8000. Разумеется речь идет о текстовом режиме с разрешением 80x25 символов. Не густо, по современным меркам, но для наших задач этого вполне достаточно.


Приведенная схемка поясняет основные принципы работы видеопамяти. Весь экран представляет собой массив слов (16-разрядное число) в младшем байте каждого из которых хранится код символа (по таблице ASCII, причем исключительно по основной её части), а старший байт - хранит атрибуты символов: цвет символа и цвет фона под ним.

Таблица 3. Кодирование символа в ячейках видеопамяти.

бит Назначение
0 - 7 код ASCII символа в видеопамяти
8 - 11 цвет символа (цвет текста)
12 - 15цвет фона текста

Вывод текста на экран таким образом заключается в обыкновенном копировании необходимых значений в нужные ячейки видеопамяти. Что касается цветов, то их выбор не велик - всего 16 значений.

Таблица 4. Коды цветов в текстовом режиме.

КодЦветКодЦвет
0x00Черный0x08Темно-серый
0x01Синий0x09Светло-синий
0x02Зеленый0x0AСветло-зеленый
0x03Цианид0x0BЦианид светлый
0x04Красный0x0CСветло-красный
0x05Маджента0x0DМаджента светлая
0x06Коричневый0x0EСветло-коричневый
0x07Светло-серый0x0FБелый

Как мы свяжем между собой координаты символа на экране с адресом в видеопамяти? Экран прямоугольный с шириной width = 80 и высотой height = 25 символов. Координаты отсчитываются от левого верхнего угла экрана: x - вправо, y - влево. Очевидно что символы первой строки будут иметь номера с 0 до 79, второй - от 80 до 159 и так далее. Тогда смещение относительно начало видеопамяти определится по формуле

offset = width*y + x

Если по этому адресу записать в память код символа и его атрибуты, то символ тут же отобразится на экране. И, надо сказать, использование видеопамяти самый оптимальный по быстродействию способ вывода на экран.

Ещё нам желательно научиться позиционировать курсор. Для этого необходимо обращаться к контроллеру CRT непосредственно через порты ввода/вывода, это опять таки, единственный для нас способ в защищенном режиме.

Контроллер CRT управляет разверткой и формированием кадров на дисплее. Для обращения к контролеру CRT используются порты с номерами 0x03D4 и 0x03D5. Порт 0x03D4 служит для задания номера регистра CRT к которому происходит обращение, а порт 0x03D5 - для передачи данных в этот регистр.

Позиция курсора, передаваемая в CRT вычисляется точно так же как и смещение в видеопамяти

cur_pos = width*y + x

Старший и младший байты позиции курсора записываются в разные регистры CRT: младший - в регистр с номером 0x0F, а старший в регистр 0x0E.

В принципе теоретических сведений пока достаточно.

2. Пишем вспомогательный код


Наш проект будет иметь довольно большой объем, поэтому сразу необходимо продумать структуру исходных текстов, а так же немного упростить себе жизнь введя некоторые глобальные определения.

Во первых переопределим некоторые стандартные типы. Создадим файл common.h

Листинг 5. Переопределение стандартных типов (файл common.h)

#ifndef        COMMON_H
#define        COMMON_H


  /* 32-битные типы данных */
  typedef    unsigned int      u32int;
  typedef    int               s32int;
  /* 16-битные типы данных */
  typedef    unsigned short    u16int;
  typedef    short             s16int;
  /* 8-битные типы данных */
  typedef    unsigned char     u8int;
  typedef    char              s8int;
 
 
#endif

Для чего нужно это? В принципе, только для того чтобы заменить декларацию беззнаковых типов более короткими именами. По ходу дела на придется определять множество переменных, и было бы неплохо упростить себе жизнь и повысить читаемость нашего кода.

Кроме этого нам необходимы функции для работы с портами ввода/вывода. Добавим в common.h, после переопределения типов, декларацию прототипов таких функций

 /* Send byte in port */
  void outb(u16int port, u8int value);
/* Get byte from port */
  u8int inb(u16int port);
  /* Get word from port */
  u16int inw(u16int port);
 
Эти функции позволят нам обращаться к портам ввода/вывода из кода на C. Добавим реализацию этих функций, для чего создадим файл common.c

Листинг 6. Реализация функций работы с портами ввода/вывода

#include    "common.h"

/*---------------------------------------------------
//    Записать байт в порт
//-------------------------------------------------*/

void outb(u16int port, u8int value)
{
  asm volatile ("outb %1, %0"::"dN"(port),"a"(value));
}

/*---------------------------------------------------
//    Считать байт из порта
//-------------------------------------------------*/

u8int inb(u16int port)
{

  u8int ret;
 
  asm volatile ("inb %1, %0":"=a"(ret):"dN"(port));
 
  return ret;
}

/*----------------------------------------------------
//    Считать слово из порта
//--------------------------------------------------*/

u16int inw(u16int port)
{

  u16int ret;
 
  asm volatile ("inw %1, %0":"=a"(ret):"dN"(port));
 
  return ret;
}

Как видите, здесь неизбежно применение ассемблерного кода, реализованного в виде ассемблерных вставок. Данные статьи не есть руководство по языку C, поэтому с дополнительными вопросами по поводу использования ассемблерных вставок, отсылаю к статье в Википедии. Там достаточно подробно описана методика применения таких конструкций в коде на C. Скажу лишь что при использовании компилятора gcc код ассемблера в этих вставках использует синтаксис AT&T, так что как я и говорил ранее, этот синтаксис придется освоить.

Теперь необходимо модифицировать Makefile, добавив туда новый объектный модуль

Листинг 7. Модификация Makefile для добавления нового кода

SOURCES=init.o main.o common.o

Модуль common.o будет собран из написанных нами исходников. На этом подготовительный этап можно считать завершенным.

3. Программирование текстового видеобуфера


Приступим теперь к реализации функций вывода информации на экран. Для этого нам необходимо написать отдельный модуль. Сначала сотворим заголовочный файл этого модуля, где определим основные константы и прототипы доступных из модуля функций.

Листинг 8. Заголовочный файл модуля работы с видеопамятью
(файл text_framebuffer.h)

#ifndef        TEXT_FRAMEBUFFER_H
#define        TEXT_FRAMEBUFFER_H

/* Адрес начала видеопамяти */
#define        VIDEO_MEMORY    0xB8000

/*    Цвета текстового режима        */
#define        BLACK        0x0
#define        BLUE         0x1
#define        GREEN        0x2
#define        CIAN         0x3
#define        RED          0x4
#define        MAGENA       0x5
#define        BROWN        0x6

#define        LIGHT_GRAY   0x7
#define        DARK_GRAY    0x8
#define        LIGHT_BLUE   0x9
#define        LIGHT_GREEN  0xA
#define        LIGHT_CIAN   0xB
#define        LIGHT_RED    0xC
#define        LIGHT_MAGENA 0xD
#define        LIGHT_BROWN  0xE
#define        WHITE        0xF

#define        SCREEN_WIDTH  80
#define        SCREEN_HEIGHT 25


#include    "common.h"

  /* Установка аппаратного курсора в заданную позицию */
  void    move_cursor(u8int x, u8int y); 
  /*    Установка цвета фона и цвета текста    */
  void    set_bkground_color(u8int color); 
  void    set_text_color(u8int color);
  /* Очистка экрана */
  void    clear(void); 
  /* Печать текстовой строки */
  void    print_text(char* s); 
  /* Вывод числа в 16-ричной форме */
  void    print_hex_value(u32int value); 
  /* Вывод 10-ричного числа */
  void    print_dec_value(u32int value);

#endif

Тут мы определили в виде констант начало адресного пространства видеопамяти, коды используемых в текстовом режиме цветов и прототипы минимального набора функций, обеспечивающих достаточно комфортную работу с монитором.

Определив прототипы приступаем к реализации 

Листинг 9.

#include    "text_framebuffer.h"

/******************************************************/
/*    Глобальные переменные для текстового буфера     */
/******************************************************/

/* Указатель на видеопамять */
u16int* video_memory = (u16int*) VIDEO_MEMORY;

/* Цвет фона        */
u8int    background_color = BLACK;
u8int    default_background_color = BLACK;
/* Цвет текста            */
u8int    text_color = LIGHT_GRAY;
u8int    default_text_color = LIGHT_GRAY;

/* Параметры экрана        */
u8int    width = SCREEN_WIDTH;
u8int    height = SCREEN_HEIGHT;

/* Текущие координаты курсора    */
u8int    cur_x = 0;
u8int    cur_y = 0;

/* таблица цифр систем счисления  */
char    hex_table[16] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};         

/*-------------------------------------------------------
//    Позиционирование курсора на экране
//-----------------------------------------------------*/

void move_cursor(u8int x, u8int y)
{
 
  /* Расчитываем положение курсора для CRT */
  u16int cur_pos = y*width + x;
 
  /* Посылаем в порт 0x03D4 номер регистра CRT */
  outb(0x3D4, 0x0E);

  /* Шлем старший байт положения курсора */
  outb(0x3D5, cur_pos >> 8);
 
  /* Шлем малдший байт положения курсора */
  outb(0x3D4, 0x0F);
  outb(0x3D5, cur_pos);
 
  /* Обновляем текущее положения курсора */
  cur_x = x;
  cur_y = y;
}


Определяем глобальные переменные, в которых будем хранить текущие настройки экрана. Особое внимание обращаю на конструкцию

 /* Указатель на видеопамять */
u16int* video_memory = (u16int*) VIDEO_MEMORY;


Здесь мы создаем указатель на массив из 16-битных значений, описывающих символ и атрибуты символа в конкретной точке экрана. Чтобы этот указатель давал доступ к видеопамяти, его нужно инициализировать значением 0xB8000, которому в файле text_framebuffer.h соответствует константа VIDEO_MEMORY. Приведение к типу u16int* здесь необходимо, поскольку компилятор не считает числовую константу значением указателя. Таким образом, мы получаем массив, значения которого непосредственно отображаются на видеопамять.

Реализуем функцию позиционирования курсора на экране. В ней рассчитываем значение которое побайтно передается в регистры 0x0E и 0x0F контролера CRT. Задание номера регистра осуществляется через порт 0x03D4, передача байтов положения курсора - через порт 0x03D5. 

Следующий фрагмент кода в файле text_framebuffer.c определяет функции задания цвета текста и фона, а так же функцию прокрутки экрана вверх, если выводимый текст перестает там помещаться, так как это делает в текстовом терминале того же Linux.

Листинг 10. Задание цветов  и скроллинг (файл text_framebuffer.c)

/*-------------------------------------------------------
//    Set background color
//-----------------------------------------------------*/
void set_bkground_color(u8int color)
{
  background_color = color;
}

/*-------------------------------------------------------
//    Set text color
//-----------------------------------------------------*/

void set_text_color(u8int color)
{
  text_color = color;
}

/*-------------------------------------------------------
//    Screen scrolling
//-----------------------------------------------------*/

static void scroll(void)
{
  /* Помещаем цвета фона и текста в байт атрибутов */
 u8int attrib_byte = (default_background_color << 4) | (default_text_color & 0x0F);
 
  /* Создаем символ "пробел" и добавляем к нему атрибуты */
  u16int blank = 0x20 | (attrib_byte << 8);
 
  /* Если достигнута последняя строка экрана */
  if (cur_y >= height)
  {
    int i;
   
    /* Сдвигаем все вышележащие строки вверх по видеопамяти */
    for (i = 0*width; i < (height - 1)*width; i++)
    {
      video_memory[i] = video_memory[i+width];
    }
   
    /* Заполняем пробелами последнюю строку */
    for (i = (height - 1)*width; i < height*width; i++)
    {
      video_memory[i] = blank;
    }
       

    /* Ставим курсор в начало последней строки */
    cur_y = 24;
    move_cursor(cur_x, cur_y);
  }
}

Функции задания цвета тривиальны и в пояснениях не нуждаются. В функции скроллинга мы создаем символ "пробел" с атрибутами, соответствующими текущей настройке цветов.

Сначала создается байт атрибутов - ему присваивается значение цвета фона, сдвинутое влево на 4 бита и операций "ИЛИ" оно объединяется со значением цвета текста. При этом на цвет текста для страховки накладывается маска 0x0F гарантированно обращающая в ноль старшую тетраду байта цвета текста, которая ничего не значит и может теоретически содержать "мусор", который  при наложении на тетраду цвета фона мог бы её исказить.

Окончательно формируем наш пробел, в младший байт blank записывая ASCII-код пробела 0x20, а в старший с помощью сдвига на 8 бит влево и наложения "ИЛИ" помещаем байт атрибутов.

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

Теперь всё готово чтобы вывести на экран символ

Листинг 11. Вывод символа на экран (файл text_framebuffer.c)

/*-------------------------------------------------------
//    Выводим символ на экран
//-----------------------------------------------------*/

void put_char(char c)
{

  /* Задаем атрибуты символа согласно текущим цветам */
  u8int attrib_byte = (background_color << 4) | (text_color & 0x0F);
  /* Задаем слово, в тарший байт которого кладем атрибуты */
  u16int attrib_word = attrib_byte << 8;
  /* Эта переменная - указатель на текущую ячейку видеопамяти */
  u16int* location;
 
  /* Начинаем выводить, с учетом характера символа */
  if ( c == 0x08 && cur_x ) /* Отрабатываем символ Backspace */
  {
    cur_x--; /* Сдвигем курсор если он не в начале строки */
  }
  else if (c == 0x09) /* Отрабатываем табуляцию (TAB) */
  {
    cur_x = (cur_x + 8) &~(8-1);
  }
  else if (c == '\r') /* Return - возврат в начало строки */
  {
    cur_x = 0; /* Помещаем курсор в начало строки */
  }
  else if (c == '\n') /* Enter - перевод строки */
  {
    cur_x = 0; /* Курсор в начало строки */
    cur_y++; /* и на строку вниз */
  }
  else /* Прочие символы (большинство значений) */
  {
    /* Расчитываем нужный адрес в видеопамяти */

    location = video_memory + (cur_y*width + cur_x);
    /* Пишем символ в видеопамять накладывая атрибуты */
    *location = c | attrib_word;

    /* Сдвигаем курсор вправо */
    cur_x++;
  }
  

  /* Если мы вылезли за пределы строки, переходим на новую */
  if (cur_x > width)
  {
    cur_x = 0;
    cur_y++;
  }
  /* Скроллинг, если это необходимо */
  scroll();
  /* Обновляем положение аппаратного курсора */
  move_cursor(cur_x, cur_y);   
}


Эта функция выводит на экран символ в режиме телетайпа, с отработкой управляющих символов. Вначале мы формируем атрибуты для выводимого символа, учитывая текущие цвета. Затем выводим символ на экран в зависимости от того что это за символ. Отработка управляющих символов включает только опреации управления положением курсора. За вывод обычного символа на экран отвечает следующий код

/* Расчитываем нужный адрес в видеопамяти */
location = video_memory + (cur_y*width + cur_x);
/* Пишем символ в видеопамять накладывая атрибуты */
*location = c | attrib_word;

/* Сдвигаем курсор вправо */
cur_x++;


здесь мы расчитываем адрес в видеопамяти и инициализируем им указатель location. Теперь по данному адресу мы можем записать символ с атрибутами, применяя к указателю операцию разименования, для обращения к ячейке видеопамяти на которую ссылается указатель.

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

Листинг 12. Вывод на экран строки символов (файл text_framebuffer.c)

/*---------------------------------------------
//    Вывод строки на экран
//-------------------------------------------*/

void print_text(char* s)
{
  int i = 0;
  /* Перебираем все символы строки пока не встретим ноль */
  while (s[i])
  {
    /* Печатаем i-й символ */

    put_char(s[i++]);
  }
}


Просто, не правда ли? Всю основную работу мы проделали при выводе символа на экран, здесь же просто перебираем всю строку вплоть до завершающего нуля (помните что язык C использует строки формата ASCIIZ - символ завершающий строку имеет код равный нулю), и печатаем все попадающиеся нам символы.

Кроме текстовой информации необходимо выводить и числа в разном формате. Числа для этого нужно преобразовывать в строки. В стандартных библиотеках C полно таких функций, но они нам не доступны, поэтому придется реализовывать их самостоятельно.

Листинг 13. Преобразования числа в строку (файл text_framebuffer.c)

/*-----------------------------------------------
// Число в строку с 16-ричной записью
//---------------------------------------------*/

void dec2hex_str(u32int value, char* hex_str)
{
  u32int mod = 0; /* Остаток от деления */
  u32int res = value; /* Результат деления */
  /* Счетчики циклов */ 
  int i = 7; 
  int j = 0;
 
  do
  {
    /* Находим остаток от деления числа на 16*/

    mod = res % 16;
    /* а также делим число на цело на 16*/
    res = res / 16;
    /* Пишем в строку с конца первую цифру числа беря её из таблицы*/
    hex_str[i] = hex_table[mod];
    i--; /* Сдвигаемся по строке влево*/
       
  } while (res >= 16); /* до тех пор пока результат деления */

  /* не станет меньше основания системы счисления (16)*/
  /* Пишем последнюю цифру в строку */
  hex_str[i] = hex_table[res];
  /* Все оставшиеся символы строки деалем нулями (текстовыми!)
  for (j = 0; j < i ; j++)
    hex_str[j] = '0';
  /* Ставим завершающий символ строки */
  hex_str[8] = '\0';
}


/*-----------------------------------------------
// Перевод числа в строку с 10-ричной записью
//---------------------------------------------*/

void dec2dec_str(u32int value, char* dec_str)
{
  u32int mod = 0;
  u32int res = value;
  char tmp_str[256];
 
  int i = 0;
  int j = 0;
 
  do /* Аналогично предыдущей функции переводим число в 10-ричный*/

  /* формат*/
  {
    mod = res % 10;
    res = res / 10;
   
    tmp_str[i] = hex_table[mod];   
    i++;
   
  } while (res >= 10);
 
  /* Пишем последнюю цифру если она не ноль то из таблицы */
  if (res != 0 )
  {
    tmp_str[i] = hex_table[res];
    tmp_str[++i] = '\0';
  }   
  else /* если же ноль - то завершаем строку */
    tmp_str[i] = '\0';
   

  /* Записываем полученную строку в результирующую */
  /* используя обратный порядок символов */
  for (j = 0; j < i; j++)
    dec_str[j] = tmp_str[i-j-1];
  /* Завершаем строку */
  dec_str[i] = '\0';
}


Эти функции преобразуют переданное им число в строку, содержащую запись числа в 16-ричном и 10-ричном формате.

Шестнадцатеричный формат мы будем использовать для компактной записи значений указателей, причем все числа у нас будут выводится восемью цифрами, длина строки будет фиксированной, поэтому в функции dec2hex_str(...) мы сразу пишем цифры от конца строки к её началу. Для приведения в сответсвия значения разряда системы счислений нужной цифре мы создали таблицу hex_table и она подходит для всех используемый при написании программ систем счисления.

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

Теперь мы легко можем вывести числа в нужном формате

Листинг 14. Собственно вывод числа на экран (файл text_framebuffer.c)

/*-----------------------------------------------
// Вывод на экран числа в 16-ричной форме
//---------------------------------------------*/

void print_hex_value(u32int value)
{
  char tmp[8];
 
  dec2hex_str(value, tmp);
 
  print_text("0x");
  print_text(tmp);
}

/*-----------------------------------------------
// Вывод на экран числа в 10-ричной форме
//---------------------------------------------*/

void print_dec_value(u32int value)
{
  char tmp[256];
 
  dec2dec_str(value, tmp);
   
  print_text(tmp);
}


Думаю, что приведенный код в пояснениях не нуждается.

Ну и наконец нам надо озаботится возможностью очистки экрана. Напишем функцию, выполняющую это дейтсвие.

Листинг 15. Очистка экрана (файл text_framebuffer.c)

/*-------------------------------------------------
//   Очистка экрана
//-----------------------------------------------*/
void clear(void)
{
  /* Формируем байт атрибутов */

  u8int    attrib_byte = (default_background_color << 4) |
                         (default_text_color & 0x0F);
  /* Создаем пробел с атрибутами */
  u16int blank = 0x20 | (attrib_byte << 8); 
  /* Создаем два пробела подряд для использования 32-разрядной */

  /* шины данных при копировании в видеопамять */
  u32int wide_blank = (blank << 16) | blank; 
  /* Трансформируем видеобуфер в массив 32-разрядных чисел */
  u32int* wide_buf = (u32int*) video_memory;
  /* Расчитываем размер 32-разрядного видеобуфера */
  int N = (width >> 2)*height;
  

  /* Заполняем видеопамять пробелами */
  int i; 
   
  for (i = 0; i < N; i++)
  {
    wide_buf[i] = wide_blank;
  } 
  /* Устанавливаем курсов в начало экрана */
  move_cursor(0, 0);
}


В этой функции достаточно разумеется заполнить всю видеопамять пробелами с текущими атрибутами. Однако, пересылать 16-битные значения в видеобуфер неоптимально, бедь мы распологаем 32-разрядной шиной данных. Поэтому видеобуфер мы преобразуем к массиву 32-разрядных чисел, в итоге он выходит в 2 раза короче, и пересылаем сразу по два пробела с атрибутами.

Всё! Мы написали весь необходимый нам функционал. Теперь помещаем модуль text_framebuffer в переменную SOURCE в Makefile, чтобы данный модуль был добавлен в ядро. Кроме того, создадим файл main.h куда включим заголовки всех используемых ядром модулей

Листинг 16. Файл заголовков main.h

#ifndef    MAIN_H
#define    MAIN_H

#include    "common.h"
#include    "text_framebuffer.h"

#endif


и уже его мы включим в файле main.c (код ядра должен быть написан аккуратно). Кроме того, добавим в функцию main() немного кода для тестирования новых возможностей

Листинг 17. Тестирование функций вывода на экран (файл main.c)

#include    "main.h"

/*----------------------------------------------
//    Точка входа в ядро
//--------------------------------------------*/

int main(void)
{
 
  int i = 0;
 
  /* Тестируем вывод текста */

  print_text("Hello from myOSkernel!!!\n\n");
  

  /* Тестируем вывод чисел */
  print_text("hex value: ");
  print_hex_value(1000);
  print_text("\n");
 
  print_text("dec value: ");
  print_dec_value(589);
  print_text("\n\n");
  

  /* Тестируем цвета в текстовом режиме */
  /* выводя таблицу доступных цветов на экран */
  print_text("0 1 2 3 4 5 6 7 8 9 A B C D E F\n");
 
  for (i = 0; i < 16; i++)
  {
    set_bkground_color(i);
    print_text("  ");
  }
     
  return 0;
}


Собираем ядро, инсталлируем его, и запускаем виртуальную машину. И получаем вот такое :)


И мы добились своего - теперь у нас есть средства отображения информации на экране.

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