TSOS/src/kernel/drivers/vga/screen.c

277 lines
7.7 KiB
C

/*
Copyright 2022 Mattia Giambirtone & Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Implementation of a simple text-only VGA driver
#include "kernel/drivers/vga/screen.h"
#include "kernel/types.h"
#include "kernel/util.h"
#include <stdarg.h>
u16 getCursorOffset(void);
void setCursorOffset(u16 offset);
u16 putchar(char ch, u8 row, u8 col, char attr);
u16 getOffset(u8 row, u8 col);
u8 getRow(u16 offset);
u8 getColumn(u16 offset);
void printc(char c);
// Public API below
void kprint(const char* message) {
/*
Prints a null-terminated string to the VGA
text buffer
*/
i32 offset = getCursorOffset();
i32 row = getRow(offset), col = getColumn(offset);
while (*message) {
offset = putchar(*(message++), row, col, 0);
// We compute the new row and column for the
// next iteration. Hopefully the compiler inlines
// these two calls
row = getRow(offset);
col = getColumn(offset);
}
setCursorOffset(getOffset(row, col));
}
void kprintf(const char *fmt, ...) {
/*
A simple reimplementation of printf in
kernel space. The following basic format
specifiers can be used:
- %s -> string
- %d -> signed number
- %u -> unsigned number
- %c -> character
*/
va_list args;
va_start(args, fmt);
char* sbuf; /* Used to print strings */
char ibuf[12]; /* Used as input to itoa. 12 is the number of
digits of 2 ** 32 + 2 (for the sign and null byte)*/
u32 offset = getCursorOffset();
u32 row = getRow(offset), col = getColumn(offset);
memset(ibuf, '0', 12);
while (*fmt) {
offset = putchar(*(fmt++), row, col, 0);
row = getRow(offset);
col = getColumn(offset);
}
va_end(args);
}
void inline kprintln(const char* message) {
/*
Identical to kprint, but calls
kprint("\n") afterwards
*/
kprint(message);
kprint("\n");
}
// Private API below
u16 putchar(char ch, u8 row, u8 col, char attr) {
/*
Writes a single character to the VGA text buffer
at the given row and column and returns the new
location of the cursor. If the attr byte is zero,
its value is the result of the expansion of the
LIGHT_GREY_ON_BLACK macro. If both row and col
are equal to -1, the cursor location is retrieved
from the VGA controller using I/O ports. The following
escape sequences are handled:
- '\n' -> Goes to the next line
- '\t' -> Prints VGA_TABSIZE spaces
- '\r' -> Resets the cursor's column, but not the row
Upon error, a negative value is returned; The possible
errors are codes are
- -1 -> Column out of bounds
- -2 -> Row out of bounds
*/
if (!attr) {
// No color data provided? We supply our own
attr = LIGHT_GREY_ON_BLACK;
}
if (col >= MAX_COLS) {
return -1;
}
if (row >= MAX_ROWS) {
return -2;
}
u16 offset = (col >= 0 && row >= 0)? getOffset(row, col): getCursorOffset();
switch (ch) {
// Note the difference between a carriage return (which
// only brings the write head back at 0 without changing
// the row) and the newline (which increases the row as well)
case '\r':
offset = getOffset(getRow(offset), 0);
break;
case '\n':
offset = getOffset(getRow(offset) + 1, 0);
break;
case '\t':
for (i32 i = 0; i < VGA_TABSIZE; i++) {
VMEM_BUF[offset] = ' ';
VMEM_BUF[offset + 1] = attr;
offset += 2;
}
break;
default:
VMEM_BUF[offset] = ch;
VMEM_BUF[offset + 1] = attr;
offset += 2;
break;
}
// We check if we reached the end of the screen, in which
// case we scroll
if (offset >= SCREEN_SIZE * 2) {
for (u8 i = 1; i < MAX_ROWS; i++) {
// This loop will take the bytes of row i and copy
// them to row i - 1, effectively erasing the first
// one and causing the text on the screen to scroll!
copystr((char*)(getOffset(i, 0) + VMEM_ADDRESS),
(char*)(getOffset(i - 1, 0) + VMEM_ADDRESS),
MAX_COLS * 2);
}
// We empty the last line
char* last = (char*)(getOffset(MAX_ROWS - 1, 0) + VMEM_ADDRESS);
for (u8 i = 0; i < MAX_COLS * 2; i++) {
last[i] = 0;
}
offset -= 2 * MAX_COLS;
}
setCursorOffset(offset);
return offset;
}
u16 getCursorOffset(void) {
/*
Returns the current offset of
the VGA text cursor
*/
// We request the high byte of the
// cursor position by writing to the
// VGA control port. We can then read
// the result on the VGA data port.
// Memory-mapped I/O is great, isn't it?
writeByte(REG_SCREEN_CTRL, 14);
// And we read it
u16 offset = readByte(REG_SCREEN_DATA);
// Since we requested the high byte
// (i.e. the 8 most significant bits)
// we now want to move them by 8 places.
// Conveniently, that's exactly what a
// left shift is for :)
offset <<= 8;
// Now we request the low byte of the cursor
// position
writeByte(REG_SCREEN_CTRL, 15);
offset += readByte(REG_SCREEN_DATA);
return offset * 2; // Times 2 because VGA cells are 2 bytes long
}
void setCursorOffset(u16 offset) {
/*
Sets the offset of the VGA text
cursor to the desired value. Note
that, this being a low level wrapper
around I/O ports, no bounds checking
is performed: the caller should make
sure that the offset fits within the
80x25 grid of the VGA screen
*/
// We now divide by 2 because while we count
// in bytes, the VGA controller counts in cells
offset /= 2;
// This function is almost identical to getCursorOffset,
// except we're now writing on the bus instead of reading
// from it, so the VGA controller updates the cursor position
// We write the high bits first
writeByte(REG_SCREEN_CTRL, 14);
writeByte(REG_SCREEN_DATA, HIGH8(offset));
// Then the low bits
writeByte(REG_SCREEN_CTRL, 15);
writeByte(REG_SCREEN_DATA, LOW8(offset));
}
void clearScreen(void) {
/*
Clears the screen and resets
the cursor position to 0, 0
*/
for (i32 i = 0; i < SCREEN_SIZE; i++) {
VMEM_BUF[i * 2] = ' '; // The screen is actually never "empty", just filled with spaces
VMEM_BUF[i * 2 + 1] = LIGHT_GREY_ON_BLACK;
}
setCursorOffset(getOffset(0, 0));
}
u16 inline getOffset(u8 row, u8 col) {
/*
Converts a column, row pair into
an absolute offset into the VGA
text buffer
*/
// We multiply by 2 because each VGA
// character cell is 2 bytes long
return (row * MAX_COLS + col) * 2;
}
u8 inline getRow(u16 offset) {
/*
Converts an absolute offset into
the VGA text buffer into a row
*/
// Conversely, we divide by two
// when converting back from the
// raw offset to columns/rows
return offset / (2 * MAX_COLS);
}
u8 inline getColumn(u16 offset) {
/*
Converts an absolute offset into
the VGA text buffer into a column
*/
return (offset - (getRow(offset) * 2 * MAX_COLS)) / 2;
}