Expand description
§BSB - A1: Input and Output
Goal: Implement output (on the CGA Text Mode and the Serial Console) and input (via Keyboard).
Regarding basic debugging in StuBS, output functions are pretty essential.
Rust comes with excellent debug formatting for most built-in types.
Any custom type can also be made printable by implementing the Debug
trait.
use core::fmt;
#[derive(Debug)]
struct MyType {
foo: usize,
}
// --- or ---
struct OtherType {
bar: usize,
}
impl fmt::Debug for OtherType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "OtherType {{ bar: {} }}", self.bar)
}
}
// --- usage ---
let t = MyType { foo: 4 };
println!("{:?}", t); // <- macro is part of the template
Usually, Rust provides the println!
macro as part of its standard library, but for our bare-metal OS, we must implement our own macros (println!
, debug!
and serial!
).
As Rust macros are a bit tedious, these are part of the template.
However, the technical side of writing and displaying the printed stings on the screen or serial console is far more interesting.
Naturally, this is your first assignment.
§Learning Objectives
- Getting to know the development environment
- Refreshing the knowledge of the programming language Rust
- Hardware programming (CGA text mode and keyboard)
§Output on CGA Text Mode
The Color Graphics Adapter (CGA) supports multiple modes for displaying text or pixel-based graphics. For simplicity, we focus on the text mode with 25 lines and 80 characters per line.
We configure this mode in our multiboot header so that the bootloader sets it up automatically. Printing to the screen and configuring the text cursor requires communication with the CGA device.
0 80 Columns 79
0 +---------------------------->+
| char, attr, char, attr, ... |
25 rows | char, attr, ... |
v ... |
24 +-----------------------------+
On x86, there are three ways to communicate with devices:
- IO
Ports
: x86 has specific instructions (in
andout
) to write/read from a device. - IO Memory: Specific memory addresses might correspond to devices. Writing/reading from them triggers device communication instead of normal memory accesses.
- (Interrupts: They will be part of the next assignment.)
The frame buffer of the CGA screen is memory-mapped (IO Memory) at 0xb8000
, and we can print to the screen by writing into this memory region.
However, this buffer does not contain any pixels.
Instead, it contains the characters (and colors), which are then rendered by the CGA device.
Each character cell consists of two bytes:
The first one is an index into the CGA font, selecting the character, and the second byte specifies the foreground and background colors.
The CGA font is compatible with most ASCII characters, meaning you can print regular strings.
These are the supported CGA characters:
0x0 -------------------------- 0x1f
| |
0x00: ☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼
0x20: !"#$%&'()*+,-./0123456789:;<=>?
0x40: @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
0x60: `abcdefghijklmnopqrstuvwxyz{|}~⌂
0x80: ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒ
0xa0: áíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐
0xc0: └┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀
0xe0: αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■
Any ASCII character can be printed with
b'A'
, other characters must use the hex representationb'\x01'
for☺
.
This example writes a red “H” in the upper left corner:
const BUFFER: *mut u8 = 0xb8000 as _;
unsafe {
BUFFER.add(0).write_volatile(b'H');
BUFFER.add(1).write_volatile(0x04); // red on black
}
The style attributes of a character have the following format:
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Blink | Background | Foreground |
Background (bits 0-3) and foreground (bits 4-6) define the respective colors, and the blink bit (7) toggles whether the character blinks. The bitfield-struct crate might prove helpful when defining these attributes.
The colors for the background can be one of the following, while the foreground can only have the colors 1-7.
0 | black | 4 | red | 8 | dark gray | 12 | light gray | |||
1 | blue | 5 | magenta | 9 | light blue | 13 | light magenta | |||
2 | green | 6 | brown | 10 | light green | 14 | yellow | |||
3 | cyan | 7 | light gray | 11 | light cyan | 15 | white |
For the println!
macro, we need a writer object that implements the Write
trait.
This trait or interface requires a single method: write_str()
, which takes a string and writes it to the CGA buffer.
The Window
struct in the template is the main abstraction for the CGA and implements the Write
trait.
Implement all of its functions, including the Write
trait and write_str()
function.
Note that the write_str()
takes a UTF-8 &str
as input, which you have to convert to bytes before writing it to the CGA buffer.
Just ignore non-ASCII characters.
If you encounter newlines or reach the end of a line, the input should wrap and scroll up.
§Hardware Cursor
The CGA screen can have a single hardware cursor. This cursor can be set using IO Ports. In total, there are 18 CGA registers, which are 8 bytes large. These registers are multiplexed by an index port, as shown below:
...
14: Cursor Address High -----+
15: Cursor Address Low ----+ |
... | |
+-----------+
+------> \ /
| +-------+
| |
Index Port Data Port
(0x3d4) (0x3d5)
This means that updating the Cursor address requires four port writes.
First, write 14 to the index port (0x3d4
) to select the high address, then write the high address to the data port (0x3d5
), and then repeat this with the low address.
§Serial Console
Similarly to the CGA screen, the VT100-compatible Serial
console should also implement the Write
trait and write_str()
function.
In contrast to the CGA screen, we have to configure the serial console ourselves.
The serial console contains six IO ports for data sending and configuration, which are defined and described in the template (Serial
).
The init function should set up the COM1 console with the following configuration.
- 115200 Baud rate (bits per second)
- 8-bit word length (size of a symbol)
- 1 stop bit (end of a symbol)
- No parity (error detection)
This init function should be called very early in main()
on core 0.
Optionally you can implement the ANSI escape codes for foreground and background colors. These can be send over the serial console and your terminal interprets them accordingly.
§Keyboard Input
In addition to text output, input via keyboard should also be supported (whereby the test of input is not possible without output).
For this purpose, you should complete the PS2Controller
.
The keycodes can be parsed with the pc_keyboard
crate.
The PS2Controller::event()
function should return the next key event.
For this, the key has to be read from the keyboard buffer via the following IO ports and then passed to pc_keyboard::Keyboard::add_byte()
and pc_keyboard::Keyboard::process_keyevent()
.
Keep in mind that the keyboard can also send mouse events. They have to be skipped.
+-8042--------------+
+--------------+ | |
<---| Control Port |<-------+-- Status Register |
--->| 0x64 |---, | |
+--------------+ `---+-> Commands |
| |
+--------------+ ,---+-- Output Buffer -,|
<---| Data Port |<--` | :==== Keyboard
--->| 0x60 |--------+-> Input Buffer --`|
+--------------+ | |
+-------------------+
Your test application should repeatedly query pressed keys from the keyboard and print their ASCII values using println!
.
It is sufficient to run the test application on the first core (aka BSP, boot processor) – the other cores (aka APs, application processors) should only do output using the debug macro to verify their functionality.
If multiple cores perform concurrent output via KOUT, you will end up in an alphabetical jumble. Try to locate the root cause of this issue (you will be able to fix it in subsequent assignments).