Page 1 of 1

How to print with color using 0xb800 in Assembly? (VGA)

Posted: Wed Aug 09, 2017 7:45 am
by stevewoods1986
Hello.

I've made a simple kernel (or second sector booted by INT 13h if preferred) that prints Hello World! on the screen. I want to add color to every character that prints. I am using the 0xb800 VGA address.

Code: Select all

BITS 16
org 0x7c00

jmp Main

Main:
mov dx, 0xb800
mov es, dx

mov si, msg
mov cx, 0

Print:
lodsb
cmp al, 0
je Done

mov di, cx
mov byte [es:di], 0x1f ; color that is expected to work.
mov byte [es:di], al ; this prints the actual character

inc cx
inc cx

jmp Print

Done:
ret

msg db 'Hello World!', 90, 0 ; I put in NOP operand because of a little problem... ignore this
By the way, when I print 13 (0x0D or carriage return), 10 (0x0A or line feed) to make a new line, it only shows characters. It doesn't actually do a new line. Why is this? I just see a music symbol (that's supposed to be 0x0E) and a triangle, not a new line.

Any help is appreciated.

Thanks
Steve.

Re: How to print with color using 0xb800 in Assembly? (VGA)

Posted: Wed Aug 09, 2017 8:09 am
by onlyonemac
The colour should be located at the memory location after the character that you want it to apply to. Your code is doing

Code: Select all

mov di, cx
mov byte [es:di], 0x1f ; color that is expected to work.
mov byte [es:di], al ; this prints the actual character

inc cx
inc cx
which places the colour in memory at the location where the character is supposed to be, and then places the character instead where it's supposed to go. Your mistake is that you're putting both bytes (the character and the colour) at the same location.

What you actually need to do is

Code: Select all

; place character
mov di, cx
mov byte [es:di], al
inc cx

; set colour
mov di, cx
mov byte [es:di], 0x1f
inc cx
Personally however I prefer to set them both at the same time.

Code: Select all

mov di, cx
mov ah, 0x1f ; put the colour in the upper byte of ax
mov word [es:di], ax ; set the character and the colour at the same time
inc cx
inc cx
This uses the useful feature of the x86 architecture that the ah and al registers combine to form ax, so you can set them separately and then access them together. Also because the x86 architecture is little-endian, the lower byte of ax (which is al, the character) will be placed first in memory, followed by the upper byte which is the colour and is placed after the character in memory where it should be.

Re: How to print with color using 0xb800 in Assembly? (VGA)

Posted: Wed Aug 09, 2017 8:22 am
by iansjack
CR and LF are not characters (well, they are - the symbols that you see, but that's not what you want) but logical concepts. If you want a new line you just start writing characters to the appropriate address (0xb8000 for the first line, 0xb80a0 for the next line, etc.).

Re: How to print with color using 0xb800 in Assembly? (VGA)

Posted: Wed Aug 09, 2017 8:26 am
by onlyonemac
The reason why your carriage return and line feed characters are displaying wrong is because the VGA doesn't understand text flow, it's just a grid of characters. Think of it like this (ignoring colour):

Code: Select all

[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
When you put the text "hello world" on the screen, it becomes this:

Code: Select all

[h][e][l][l][o][ ][w][o][r][l]
[d][ ][ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
The text wraps because each "box" is a memory location and the first line of boxes comes first in memory, followed by the second line of boxes, and so on.

Suppose you change the text to "hello\nworld" ("\n" refers to the line feed character). Your character grid will become:

Code: Select all

[h][e][l][l][o][*][w][o][r][l]
[d][ ][ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
where "*" is the line feed character. The VGA doesn't put the text on the next line, because the characters are in the memory locations for the previous line. Control characters (most commonly carriage return, line feed, tab, and delete) don't mean anything to it. This means that you have to handle these characters yourself, by changing the memory address at which you put the characters to "skip" to the next line:

Code: Select all

[h][e][l][l][o][ ][ ][ ][ ][ ]
[w][o][r][l][d][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
Notice that there is a row of blank "boxes" after the word "hello" continuing to the end of the line, before the word "world" appears. In the computer's memory, which is a single straight line, this appears as:

Code: Select all

[h][e][l][l][o][ ][ ][ ][ ][ ][w][o][r][l][d][ ][ ][ ][ ][ ]...
The reason why you get music notes and triangles is because, since the VGA doesn't understand these control characters, they're used to display extra symbols. All of the character codes from 0x00 to 0x1F (0x20 is a space) are used like this. A similar thing is done with the characters from 0x7F (delete) upwards, and you can find a table of all of the available characters here.

Also note that some platforms use both a carriage return and a line feed to represent a newline, and others use just a line feed. When you display this, you want to display only a single new line. My preferred way to handle this is to output a new line whenever the line feed character occurs in the source string and to ignore the carriage return without either outputting it to the screen or changing the address where the next character will be located. This will handle both platforms that use a carriage return and a line feed and those that use just a line feed, and you don't need to detect the sequence of a carriage return followed by a line feed and can simply handle each character on its own.

Re: How to print with color using 0xb800 in Assembly? (VGA)

Posted: Wed Aug 09, 2017 9:27 am
by Octacone
onlyonemac wrote: The reason why you get music notes and triangles is because, since the VGA doesn't understand these control characters, they're used to display extra symbols. All of the character codes from 0x00 to 0x1F (0x20 is a space) are used like this. A similar thing is done with the characters from 0x7F (delete) upwards, and you can find a table of all of the available characters here.
Here is a more "detailed" version of it: https://en.wikipedia.org/wiki/Code_page_437
I use this one all the time.

Re: How to print with color using 0xb800 in Assembly? (VGA)

Posted: Wed Aug 09, 2017 10:11 am
by stevewoods1986
onlyonemac wrote:The colour should be located at the memory location after the character that you want it to apply to. Your code is doing

Code: Select all

mov di, cx
mov byte [es:di], 0x1f ; color that is expected to work.
mov byte [es:di], al ; this prints the actual character

inc cx
inc cx
which places the colour in memory at the location where the character is supposed to be, and then places the character instead where it's supposed to go. Your mistake is that you're putting both bytes (the character and the colour) at the same location.

What you actually need to do is

Code: Select all

; place character
mov di, cx
mov byte [es:di], al
inc cx

; set colour
mov di, cx
mov byte [es:di], 0x1f
inc cx
Personally however I prefer to set them both at the same time.

Code: Select all

mov di, cx
mov ah, 0x1f ; put the colour in the upper byte of ax
mov word [es:di], ax ; set the character and the colour at the same time
inc cx
inc cx
This uses the useful feature of the x86 architecture that the ah and al registers combine to form ax, so you can set them separately and then access them together. Also because the x86 architecture is little-endian, the lower byte of ax (which is al, the character) will be placed first in memory, followed by the upper byte which is the colour and is placed after the character in memory where it should be.
Thank you for solving my problem. I tried putting it after and before but I didn't do the other instructions. 0x0741 is 0x4107 in memory because of little endian! (see Section 1.3.1 of https://software.intel.com/sites/defaul ... -vol-1.pdf).

I also checked out http://wiki.osdev.org/Printing_to_Screen to check if I was doing everything right.

Anyway, this solved my problem! Thank you so much, onlyonemac.

Re: How to print with color using 0xb800 in Assembly? (VGA)

Posted: Wed Aug 09, 2017 10:16 am
by stevewoods1986
iansjack wrote:CR and LF are not characters (well, they are - the symbols that you see, but that's not what you want) but logical concepts. If you want a new line you just start writing characters to the appropriate address (0xb8000 for the first line, 0xb80a0 for the next line, etc.).
OK. Thank you. By the way, is that to do with 80*25 text mode?

Hello World!.................................................................... (thanks to Python for the multiple dots).
Goodbye World!

Re: How to print with color using 0xb800 in Assembly? (VGA)

Posted: Thu Aug 10, 2017 10:09 am
by Schol-R-LEA
It applies to any text mode, actually. The text video buffer is basically a single-dimensional array of character-attribute pairs (shown here as a FASM struc):

Code: Select all

struc text_cell char attrib
{
    . db char,
    . db attrib 
}
but each text mode's row size overlays it to effectively form a two-dimensional array.

This means that the best way to handle the insertion point - and the cursor in general - is to have a pair of integer variables for the x and y positions in that grid. For now, these could be globals, but you will probably want to make a struc for those which can be set up for a given text mode as needed.

EDIT: Sorry for cutting it off so abruptly, I had to leave for a medical appointment. I have a moment to post this now, but I can make further suggestions later if you like. I will need to read up on the virtual and rept directives to give a deeper answer.

Re: How to print with color using 0xb800 in Assembly? (VGA)

Posted: Fri Aug 11, 2017 1:33 pm
by onlyonemac
Schol-R-LEA wrote:This means that the best way to handle the insertion point - and the cursor in general - is to have a pair of integer variables for the x and y positions in that grid.
I second this. You'll probably want to write a module like this:

Code: Select all

#define VGA_BASE_ADDRESS (0xB8000)
#define ROWS (25)
#define COLS (80)

int cursor_x = 0;
int cursor_y = 0;

// main function to put characters on the screen
void console_print_character(char character, unsigned char colour)
{
    if (character >= 0x20 && character <= 0x7E)    // printable characters
    {
        // put character on screen
        ((unsigned char*) VGA_BASE_ADDRESS)[(cursor_x + (cursor_y * COLS)) * 2] = character;
        ((unsigned char*) VGA_BASE_ADDRESS)[(cursor_x + (cursor_y * COLS)) * 2 + 1] = colour;

        // move cursor
        cursor_x++;
        if (cursor_x == COLS)
        {
            cursor_x = 0;
            cursor_y++;
            if (cursor_y == ROWS)
            {
                console_clear_screen();    // TODO: make this scroll the screen upwards rather than clear it completely
            }
        }
    }
    else if (character == 0x0A)    // line feed
    {
        // move cursor to next line
        cursor_x = 0;
        cursor_y++;
        if (cursor_y == ROWS)
        {
            console_clear_screen();    // TODO: make this scroll the screen upwards rather than clear it completely
        }
    }
   else if (character == 0x7F)   // delete
    {
        // move cursor back
        cursor_x--;
        if (cursor_x < 0)
        {
            cursor_y--;
            if (cursor_y < 0)
            {
                cursor_y == 0;
            }
        }

        // delete character
        ((unsigned char*) VGA_BASE_ADDRESS)[(cursor_x + (cursor_y * COLS)) * 2] = 0x20;    // space, which is also used to produce a "blank" character
        ((unsigned char*) VGA_BASE_ADDRESS)[(cursor_x + (cursor_y * COLS)) * 2 + 1] = 0;
    }
}

// print a string
void console_print_string(char* string)
{
    int index = 0;
    while (string[index] != 0)
    {
        console_print_character(string[index]);
    }
}

// clear the screen
void console_clear_screen()
{
    for (int y = 0; y < ROWS; y++)
    {
        for (int x = 0; x < COLS; x++)
        {
            ((unsigned char*) VGA_BASE_ADDRESS)[(cursor_x + (cursor_y * COLS)) * 2] = 0x20;    // space, which is also used to produce a "blank" character
            ((unsigned char*) VGA_BASE_ADDRESS)[(cursor_x + (cursor_y * COLS)) * 2 + 1] = 0;
        }
    }
    cursor_x = 0;
    cursor_y = 0;
}
This will give you basic console output, with new lines and deleting behaving as expected. You can later add support for tabs and scrolling the screen. You can also write another function that turns a number into text and prints it to the screen.

You could adapt this module to assembly, but for tasks like this it's usually better to use a higher-level language like C.