mirror of
https://github.com/phil-opp/blog_os.git
synced 2025-12-16 22:37:49 +00:00
Add draft about printing to screen
This commit is contained in:
180
_drafts/printing-to-screen.md
Normal file
180
_drafts/printing-to-screen.md
Normal file
@@ -0,0 +1,180 @@
|
||||
---
|
||||
layout: post
|
||||
title: 'Printing to Screen'
|
||||
category: 'rust-os'
|
||||
---
|
||||
TODO Introduction
|
||||
|
||||
## The VGA Text Buffer
|
||||
The text buffer starts at physical address `0xb8000` and contains the characters displayed on screen. It has 80 rows and 25 columns. Each screen character has the following format:
|
||||
|
||||
Bit(s) | Value
|
||||
------ | ----------------
|
||||
0-7 | ASCII code point
|
||||
8-11 | Foreground color
|
||||
12-14 | Background color
|
||||
15 | Blink
|
||||
The following colors are available:
|
||||
|
||||
Number | Color | Number + Bright Bit | Bright Color
|
||||
------ | ---------- | ------------------- | -------------
|
||||
0x0 | Black | 0x8 | Dark Gray
|
||||
0x1 | Blue | 0x9 | Light Blue
|
||||
0x2 | Green | 0xa | Light Green
|
||||
0x3 | Cyan | 0xb | Light Cyan
|
||||
0x4 | Red | 0xc | Light Red
|
||||
0x5 | Magenta | 0xd | Light Magenta
|
||||
0x6 | Brown | 0xe | Yellow
|
||||
0x7 | Light Gray | 0xf | White
|
||||
Bit 4 is the _bright bit_, which is unavailable in background color as it's the blink bit. But it's possible to disable blinking through a [BIOS function][disable blinking] and use the full 16 colors as background.
|
||||
|
||||
[disable blinking]: http://www.ctyme.com/intr/rb-0117.htm
|
||||
|
||||
## Creating a Rust Module
|
||||
Let's create the Rust module `vga_buffer`. Therefor we create a file named `src/vga_buffer.rs` and add a `mod vga_buffer` line to `src/lib.rs`. Now we can create an enum for the colors ([full file](#TODO)):
|
||||
|
||||
```rust
|
||||
#[repr(u8)]
|
||||
pub enum Color {
|
||||
Black = 0,
|
||||
Blue = 1,
|
||||
...
|
||||
Yellow = 14,
|
||||
White = 15,
|
||||
}
|
||||
|
||||
struct ColorCode(u8);
|
||||
|
||||
impl ColorCode {
|
||||
const fn new(foreground: Color, background: Color) -> ColorCode {
|
||||
ColorCode((background as u8) << 4 | (foreground as u8))
|
||||
}
|
||||
}
|
||||
```
|
||||
We use the `repr(u8)` attribute to represent each variant of `Color` as an `u8`. The `ColorCode` contains the full color byte, containing foreground and background color. The `new` function is a [const function] to allow it in static initializers. It's unstable so we need to add the `const_fn` feature in `src/lib.rs`. We ignore the blink bit here to keep the code short. Now we can represent a screen character and the text buffer:
|
||||
|
||||
```rust
|
||||
struct ScreenChar {
|
||||
ascii_character: u8,
|
||||
color_code: ColorCode,
|
||||
}
|
||||
|
||||
const BUFFER_HEIGHT: usize = 25;
|
||||
const BUFFER_WIDTH: usize = 80;
|
||||
|
||||
struct Buffer {
|
||||
chars: [[ScreenChar; BUFFER_WIDTH]; BUFFER_HEIGHT],
|
||||
}
|
||||
```
|
||||
Now we can represent the actual screen writer:
|
||||
|
||||
```rust
|
||||
pub struct Writer {
|
||||
column_position: usize,
|
||||
color_code: ColorCode,
|
||||
}
|
||||
|
||||
impl Writer {
|
||||
pub const fn new(foreground: Color, background: Color) -> Writer {
|
||||
Writer {
|
||||
column_position: 0,
|
||||
color_code: ColorCode::new(foreground, background),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
It's `pub` because it's part of the public interface and `const` to allow it in static initializers. The plan is to write always to the last line and shift lines up when a line is full (or on `\n`). So we just need to store the current column position and the current `ColorCode`.
|
||||
|
||||
### Writing to screen
|
||||
Now we can create a `write_byte` function:
|
||||
|
||||
```rust
|
||||
impl Writer {
|
||||
pub fn write_byte(&mut self, byte: u8) {
|
||||
const NEWLINE: u8 = '\n' as u8;
|
||||
const BUFFER: *mut Buffer = 0xb8000 as *mut _;
|
||||
|
||||
match byte {
|
||||
NEWLINE => {}, // TODO
|
||||
byte => {
|
||||
if self.column_position >= BUFFER_WIDTH {} //TODO
|
||||
let buffer = unsafe{&mut *BUFFER};
|
||||
let row = BUFFER_HEIGHT - 1;
|
||||
let col = self.column_position;
|
||||
|
||||
buffer.chars[row][col] = ScreenChar {
|
||||
ascii_character: byte,
|
||||
color_code: self.color_code,
|
||||
};
|
||||
self.column_position += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Let's break it down:
|
||||
|
||||
- We take `&mut self` as we modify the column position.
|
||||
- The unsafe block is needed to convert the raw pointer to a reference because Rust doesn't know if the pointer is valid.
|
||||
- We write a new `ScreenChar` to the current field in the buffer and increase the column position.
|
||||
|
||||
Now we can test it in `main`:
|
||||
|
||||
```rust
|
||||
pub extern fn main() {
|
||||
use vga_buffer::{Writer, Color};
|
||||
...
|
||||
let mut writer = Writer::new(Color::Blue, Color::LightGreen);
|
||||
writer.write_byte(b'H');
|
||||
}
|
||||
```
|
||||
The `b'H'` is a [byte character] and represents the ASCII code point for `H`. It should be printed in the _lower_ left corner of the screen on `make run`.
|
||||
|
||||
To print whole strings, we can convert them to bytes[^utf8-problems] and print them one-by-one:
|
||||
|
||||
```rust
|
||||
pub fn write_str(&mut self, s: &str) {
|
||||
for byte in s.bytes() {
|
||||
self.write_byte(byte)
|
||||
}
|
||||
}
|
||||
```
|
||||
[byte character]: https://doc.rust-lang.org/reference.html#characters-and-strings
|
||||
[^utf8-problems]: This approach works well for strings that contain only ASCII characters. For other Unicode characters, however, we get weird symbols on the screen. But they can't be printed in the VGA text buffer anyway.
|
||||
|
||||
### Support Formatting Macros
|
||||
It would be nice to support Rust's formatting macros, too. That way, we can easily print different types like integers or floats. To support them, we need to implement the [core::fmt::Write] trait. The only required method of this trait is `write_str` and looks quite similar to our `write_str` method. We just need to move it into an `impl ::core::fmt::Write for Writer` block and add a return type:
|
||||
|
||||
```rust
|
||||
impl ::core::fmt::Write for Writer {
|
||||
pub fn write_str(&mut self, s: &str) -> ::core::fmt::Result {
|
||||
for byte in s.bytes() {
|
||||
self.write_byte(byte)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
```
|
||||
The `Ok(())` is just the `Ok` Result containing the `()` type.
|
||||
|
||||
Now we can use Rust's built-in `write!`/`writeln!` formatting macros:
|
||||
|
||||
```rust
|
||||
...
|
||||
let mut writer = Writer::new(Color::Blue, Color::LightGreen);
|
||||
writer.write_byte(b'H');
|
||||
writer.write_str("ello! ");
|
||||
write!(writer, "The numbers are {} and {}", 42, 1.0/3.0);
|
||||
```
|
||||
Now you should see a `Hello! The numbers are 42 and 0.3333333333333333` in strange colors at the bottom of the screen.
|
||||
|
||||
[core::fmt::Write]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html
|
||||
|
||||
### Newlines
|
||||
Right now, we just ignore newlines and characters that don't fit into the line anymore. Instead we want to move every character one line up (the top line gets deleted) and start at the beginning of the last line again. To do this, we add a `new_line` method to `Writer`:
|
||||
|
||||
```rust
|
||||
```
|
||||
Blablabla
|
||||
|
||||
Now we just need to call in the 2 cases marked with `//TODO` and our writer supports newlines.
|
||||
Reference in New Issue
Block a user