/* 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" i32 getCursorOffset(void); void setCursorOffset(i32 offset); i32 putchar(byte ch, i32 col, i32 row, byte attr); i32 getOffset(i32 col, i32 row); i32 getRow(i32 offset); i32 getColumn(i32 offset); // Public API below void kprintAt(char* message, i32 col, i32 row) { /* Prints a null-terminated string to the VGA text buffer at the specified row and column. The cursor's position is not updated. If both col and row are negative, the current cursor location is used */ i32 offset; if (col >= 0 && row >= 0) { offset = getOffset(col, row); } else { offset = getCursorOffset(); row = getRow(offset); col = getColumn(offset); } for (i32 i = 0; message[i]; i++) { offset = putchar(message[i], col, row, 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); } } void inline kprint(char* message) { /* Prints a null-terminated string to the VGA text buffer */ kprintAt(message, -1, -1); } void inline kprintln(char* message) { /* Identical to kprint, but calls kprint("\n") afterwards */ kprint(message); kprint("\n"); } // Private API below i32 putchar(byte ch, i32 col, i32 row, byte 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; } i32 offset = (col >= 0 && row >= 0)? getOffset(col, row): 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(0, getRow(offset)); break; case '\n': offset = getOffset(0, getRow(offset) + 1); 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 (i32 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(0, i) + VMEM_ADDRESS), (char*)(getOffset(0, i - 1) + VMEM_ADDRESS), MAX_COLS * 2); } // We empty the last line char* last = (char*)(getOffset(0, MAX_ROWS - 1) + VMEM_ADDRESS); for (i32 i = 0; i < MAX_COLS * 2; i++) { last[i] = 0; } offset -= 2 * MAX_COLS; } setCursorOffset(offset); return offset; } i32 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 i32 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(i32 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, (byte)(offset >> 8)); // Then the low bits writeByte(REG_SCREEN_CTRL, 15); writeByte(REG_SCREEN_DATA, (byte)(offset & 0xff)); // These bitwise tricks *seem* like black // magic, but they're quite simple: shifting // the offset by 8 bits moves the high bits 8 // positions down (so we lose the low bits and // the high bits now fit into a single byte, which // is what writeByte wants). Then we get rid of the // high bits (0xff is 11110000) with a bitwise and, // leaving us with only the low bits set which we // then feed to the I/O port again; The reason this // works is because 0xff (which is smaller than our // offset), is zero-extended from the beginning with // zeroes, so when we perform the operation the high // bits are cancelled out } 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)); } i32 inline getOffset(i32 col, i32 row) { /* 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; } i32 inline getRow(i32 offset) { /* Converts an absolute offset into the VGA text buffer into a row */ return offset / (2 * MAX_COLS); } i32 inline getColumn(i32 offset) { /* Converts an absolute offset into the VGA text buffer into a column */ return (offset - (getRow(offset) * 2 * MAX_COLS)) / 2; }