Understanding text mode

We've called this a "VGA driver" so far, but it's a bit more specific than that: VGA has both graphical and text modes. We're going to be doing text mode, as it's simpler than graphics, and we need to get something going in order to see output.

Memory mapping

The first thing to understand about how VGA's text mode works is that it's "memory mapped." This means that you interact with it by writing to a chunk of memory.

We haven't talked too much about memory yet, and we'll talk about it much more in the future; one of a kernel's many jobs is to manage memory. In general, for the kind of hardware we're writing, memory is linear, that is, the first memory address starts at 0, the next one at 1, and so on and so on.

Like all things, this is fundamentally an abstraction: in the future, when we have programs running on our OS, they will think they're using linear memory even when they're not! That's getting a bit ahead of ourselves, though...

We can refer to a memory location by its particular number, and read values from it, or store values in it. Its number is called its "address," and the amount of memory we can give an address to is called "addressable memory."

How much is that? Well, it depends on the amount of RAM that's installed in our computer. Eventually, our kernel will learn how to ask how much memory is available, but for now, we're using so little memory that we're just going to assume that it's all there and works okay. You have to walk before you can run!

Furthermore, memory addresses are usually written in hexadecimal. The usual system of numbers humans used is "decimal", or "base 10." Hexadecimal is "base 16." If you haven't worked with hexadecimal numbers before, you should check out Appendix B. If you don't want to, you don't have to: just know that hexadecimal numbers have the letters a through f in them, and you should be able to follow along at first. You'll eventually want to come back to this and learn it, though, as we'll be using them more and more as time goes on.

So, applying this to VGA text mode: there's a block of memory located at the address 0xb8000, and it consists of four thousand bytes. In our case, a byte is eight bits, each consisting of a zero or a one. Why is it at this address? Because that's what the specification says. I'm sure there's a justification, but it doesn't really matter for our purposes: that's what it says, so that's what we do.

With this understanding, this line of code on the previous page may make some sense:


# #![allow(unused_variables)]
#fn main() {
let slice = unsafe { core::slice::from_raw_parts_mut(0xb8000 as *mut u8, 4000) };
#}

This creates a &mut [u8], a mutable "slice" of bytes. from_raw_parts_mut takes a *mut u8, a "raw pointer", and a length, and creates a slice of that length starting from that pointer. To make a raw pointer, we can write out the memory address, and then cast with as. The address starts with 0x because it's written in hexadecimal.

Finally, this is very unsafe, in a Rust sense: we're creating a slice to an arbitrary spot in memory, and are gonna start writing values to it. We know that this is okay, because we're a kernel and we know the specification. Rust doesn't know anything about VGA, and so can't check that this code is correct. We can, and that's exactly what unsafe is there for.

Writing to the map

Now that we have a slice, we can use [] to index it, and read or write from any index. The next bit of code looks like this:


# #![allow(unused_variables)]
#fn main() {
slice[0] = b'h';
slice[1] = 0x02;
#}

It repeats in this pattern over and over, in twos.

We're writing things to the first and second locations in our slice. But why do we write these specific things? Well, the first one is called the character byte, and the second is called the attribute byte.

The character byte

The first byte is called the "character byte" because well, it's a character! More specifically, you can put ASCII characters in here. But more importantly, you don't write the character itself, you write its numeric value. That's what the b'' is doing; instead of an 'h', which would be a char in Rust, and therefore be four bytes (thanks to Unicode), we want the ASCII version. Rust provides the b'' construct to make getting this value easy; we can write b'h', but actually get a u8 instead of a char.

The attribute byte

The second byte is the "attribute" byte, and it sets two different things: the foreground color, and the background color. Here's the list of possible colors and their numeric values:

namevalue
Black0x0
Blue0x1
Green0x2
Cyan0x3
Red0x4
Magenta0x5
Brown0x6
Gray0x7
DarkGray0x8
BrightBlue0x9
BrightGreen0xA
BrightCyan0xB
BrightRed0xC
BrightMagenta0xD
Yellow0xE
White0xF

So, these are individual colors, but how do you choose both? Let's look closely at that line again:


# #![allow(unused_variables)]
#fn main() {
slice[1] = 0x02;
#}

See how there's both a 0 and a 2? The first value is 0, the background color, which is black. The second is 2, the foreground color, which is green. This is an example of why hexadecimal is useful; we can look at 0x02 and say "oh, black and green" because of the 0 and the 2.

Conclusion

To get a feeling for all of this, try to change the foreground colors, background colors, and letters. Write some letters in different colors than other letters. It's up to you! Once you feel comfortable with this, you can move on to the next section. Even though you can memorize these numbers, you don't have to: we'll create a nicer implementation that makes this much more readable.