Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Руководство по созданию простой UNIX-подобной ОС

3. Экран

Оригинал: "3. The Screen"
Автор: James Molloy
Дата публикации: 2008
Перевод: Н.Ромоданов
Дата перевода: январь 2012 г.

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

3.1. Теория

Ваше ядро с помощью GRUB загружается в текстовый режим. То есть, для него выделяется фреймбуфер или кадровый буфер (область памяти), с помощью которого осуществляется управление экраном символов (но не пикселей) размером в 80 символов в ширину и 25 символов в высоту. Это тот режим, в котором ваше ядро будет работать до тех пор, пока вы не окунетесь в мир VESA (который не рассматривается в данном руководстве).

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

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

(y * 80 + x) * 2

Важно отметить, что множитель '* 2' указан только потому, что каждый элемент занимает 2 байта (16 бит). Например, если вы индексируете массив 16-битных значений, то индексом будет просто у * 80 + x.

Формат слова

В кодировке ASCII (в текстовом режиме кодировка Unicode не поддерживается) для представления символов используются 8 бит. У нас остается еще 8 бит, которые не используются. Аппаратура VGA использует их для назначения цвета тексту и цвета фону (по 4 бит каждый). На рисунке показано деление на части этого 16-разрядного значения.

4 бита для цветового кода дает нам 15 возможных цветов, которые мы можем отобразить::

0:черный, 1:синий, 2:зеленый, 3:циан, 4:красный, 5:мажента, 6:коричневый, 7:светло серый, 8:темно серый, 9:светло синий, 10:светло зеленый, 11:светлый циан, 12:светло красный, 13:светлый мажента, 14: светло коричневый, 15: белый.

У контроллера VGA также есть несколько портов главной шины ввода/вывода, которые можно использовать для того, чтобы отправить контроллеру конкретные указания. (Среди прочего) есть регистр управления с адресом 0x3D4 и регистр данных с адресом 0x3D5. Мы будем использовать эти порты для того, чтобы передать в контроллер указание обновить позицию курсора (мигающий символ подчеркивания указывающий место, где должен появиться следующий символ).

3.2. Практика

3.2.1. Прежде всего

Во-первых, нам нужно еще несколько обычно используемых глобальных функций. В файлах common.c и common.h есть функции записи данных в шину ввода/вывода и чтения их из шины, а также некоторые определения типов, что позволит вам проще писать переносимый код. Эти файлы также являются идеальным местом для размещения таких функций, как memcpy/memset т.д. Я оставляю их вам для реализации! :)

// common.h -- Определения типов данных и некоторых глобальных функций.
// Из руководства по разработке ядра James Molloy

#ifndef COMMON_H
#define COMMON_H

// Несколько замечательных типов, обеспечивающих межплатформенную стандартизацию размеров.
// Это определения для 32-битной платформы X86.

typedef unsigned int   u32int;
typedef          int   s32int;
typedef unsigned short u16int;
typedef          short s16int;
typedef unsigned char  u8int;
typedef          char  s8int;

void outb(u16int port, u8int value);
u8int inb(u16int port);
u16int inw(u16int port);

#endif
// common.c -- Определения некоторых глобальных функций.
// Из руководства по разработке ядра James Molloy

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

3.2.2. Код монитора

Пример заголовочного файла:

// monitor.h -- Определение интерфейса для monitor.h
// Из руководства по разработке ядра James Molloy

#ifndef MONITOR_H
#define MONITOR_H

#include "common.h"

// Write a single character out to the screen.
void monitor_put(char c);

// Clear the screen to all black.
void monitor_clear();

// Output a null-terminated ASCII string to the monitor.
void monitor_write(char *c);

#endif // MONITOR_H
3.2.2.1. Перемещение курсора

Чтобы переместить аппаратно реализованный курсор, мы должны, прежде всего, рассчитать линейное смещение координат х и у курсора. Мы делаем это с помощью приведенного выше уравнения. Далее, мы должны передать это смещение в контроллер VGA. По ряду причин, он принимает 16-битное значение местоположения курсора, которое должно быть представлено в виде двух байтов. Для того, чтобы сообщить колнтроллеру, что мы посылаем ему старший байт, мы посылаем в порт контроллера (0x3D4) команду 14 , а затем отправляем этот байт в порт 0x3D5. Затем мы все повторяем с младшим байтом, но отправляем команду 15..

// Обновляем аппаратный курсор.
static void move_cursor()
{
   // The screen is 80 characters wide...
   u16int cursorLocation = cursor_y * 80 + cursor_x;
   outb(0x3D4, 14);                  // Сообщаем плате VGA о том, что мы посылаем старший байт курсора.
   outb(0x3D5, cursorLocation >> 8); // Посылаем старший байт курсора.
   outb(0x3D4, 15);                  // Сообщаем плате VGA о том, что мы посылаем младший байт курсора.
   outb(0x3D5, cursorLocation);      // Посылаем младший байт курсора.
} 
3.2.2.2. Скроллинг экрана

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

// Скроллинг текста на экране на одну строку вверх.
static void scroll()
{

   // Берем символ пробела с атрибутами цвета, заданными по умолчанию.
   u8int attributeByte = (0 /*black*/ << 4) | (15 /*white*/ & 0x0F);
   u16int blank = 0x20 /* space */ | (attributeByte << 8);

   // Строка 25 находится внизу, это означает, что нам нужно выполнить скроллинг вверх
   if(cursor_y >= 25)
   {
       // Перемещаем кусок текста, отображаемого в текущий момент, 
       // обратно в буфер, сдвинув его на одну строку 
       int i;
       for (i = 0*80; i < 24*80; i++)
       {
           video_memory[i] = video_memory[i+80];
       }

       // Последняя строка должна теперь быть пустой. Это осуществляется 
       // записью в нее 80 символов пробела.
       for (i = 24*80; i < 25*80; i++)
       {
           video_memory[i] = blank;
       }
       // Теперь курсор должен находиться на последней строке.
       cursor_y = 24;
   }
} 
3.2.2.3. Записываем символ на экран

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

// Writes a single character out to the screen.
void monitor_put(char c)
{
   // Цвет фона - черный (0), цвет текста - белый (15).
   u8int backColour = 0;
   u8int foreColour = 15;

   // Байт атрибута состоит из двух полубайтов - младший является цветом, 
   // используемым для отображения текста, а старший - фоновым цветом.
   u8int  attributeByte = (backColour << 4) | (foreColour & 0x0F);
   // Байт атрибута представляет собой 8 старших битов слова, которое мы должны 
   // послать на плату VGA.

   u16int attribute = attributeByte << 8;
   u16int *location;

   // Обработка символа backspace - перемещаем курсор на одну позицию обратно
   if (c == 0x08 && cursor_x)
   {
       cursor_x--;
   }

   // Обработка символа табуляции - увеличиваем координату X курсора,
   // но так, чтобы она делилась на 8
   else if (c == 0x09)
   {
       cursor_x = (cursor_x+8) & ~(8-1);
   }

   // Обрабатываем возврат каретки
   else if (c == '\r')
   {
       cursor_x = 0;
   }

   // Обрабатываем переход на новую строку - перемещаем курсор обратно 
   // влево и увеличиваем номер строки
   else if (c == '\n')
   {
       cursor_x = 0;
       cursor_y++;
   }
   // Обрабатываем другие символы, которые можно выводить на экран.
   else if(c >= ' ')
   {
       location = video_memory + (cursor_y*80 + cursor_x);
       *location = c | attribute;
       cursor_x++;
   }

   // Проверяем, нужно ли нам добавлять новую строку из-за того, что
   // мы достигли конца экрана.
   if (cursor_x >= 80)
   {
       cursor_x = 0;
       cursor_y ++;
   }

   // Скроллинг экрана, если это необходимо.
   scroll();
   // Перемещение аппаратного курсора.
   move_cursor();
} 

Видите? Это очень просто! Ниже показано как на практике выполнить запись:

location = video_memory + (cursor_y*80 + cursor_x);
*location = c | attribute;
  • Устанавливаем указатель 'location' так, чтобы он указывал на линейный адрес слова, соответствующего текущей позиции курсора (смотри уравнение, приведенное выше).
  • Задаем значение для 'location' – выполняем операцию логического ИЛИ между значением символа и значением 'attribute'. Помните, что мы выше сдвинули 'attribute' на 8 бит влево, так что в действительности мы просто в 'attribute' в качестве младшего байта устанавливаем значение c.
3.2.2.4. Очищаем экран

Очистка экрана также осуществляется просто. Просто заполните его пробелами:

// Очистка экрана - записываем пробелы во фреймбуфер.
void monitor_clear()
{
   // Устанавливаем в байте атрибутов значения цветов, используемые по умолчанию
   u8int attributeByte = (0 /*black*/ << 4) | (15 /*white*/ & 0x0F);
   u16int blank = 0x20 /* space */ | (attributeByte << 8);

   int i;
   for (i = 0; i < 80*25; i++)
   {
       video_memory[i] = blank;
   }

   // Перемещаем аппаратный курсор в начало.
   cursor_x = 0;
   cursor_y = 0;
   move_cursor();
} 
3.2.2.5. Записываем строку
// Выдаем на монитор строку ASCII, завершающуюся символом null.
void monitor_write(char *c)
{
   int i = 0;
   while (c[i])
   {
       monitor_put(c[i++]);
   }
} 

3.3. Итог

Если собрать весь этот код вместе, вы сможете в наш файл main.c добавить следующие пару строк:

monitor_clear();
monitor_write("Hello, world!");

И, вуаля, функция вывода текста! Не плохо для пары минут работы, а?

3.4. Расширения

Помимо реализации memcpy/memset/strlen/strcmp и т.д., есть еще несколько функций, которые сделают вашу жизнь немного проще..

void monitor_write_hex(u32int n)
{
   // TODO: Реализуйте эту функцию самостоятельно!
}

void monitor_write_dec(u32int n)
{
   // TODO: Реализуйте эту функцию самостоятельно!
} 

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

Вы также можете изучит код из файла linux0.1 - реализацию vsprintf, которая достаточно качественная. Вы можете скопировать эту функцию, затем использовать ее для реализации функции printf(), которая, когда дело дойдет до отладки, сделает вашу жизнь существенно проще.

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


Назад К началу Вперед