Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion chips/x86_q35/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ mod pic;
pub mod pit;

pub mod serial;

pub mod vga;
pub mod vga_textscreen;

pub mod vga_uart_driver;
234 changes: 172 additions & 62 deletions chips/x86_q35/src/vga.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,22 @@
// SPDX-License-Identifier: Apache-2.0 OR MIT
// Copyright Tock Contributors 2025.

//! Minimal VGA peripheral implementation for the Tock x86_q35 chip crate.
//! Minimal VGA peripheral implementation for the x86_q35 chip.
//!
//! Supports classic 80×25 text mode out-of-the-box and exposes a stub for
//! setting planar 16-colour graphics modes (640×480 and 800×600). These
//! extra modes will be filled in later once the driver is integrated with a
//! future framebuffer capsule.
//! This module exposes a **text-mode** VGA writer only. Mode selection is
//! enforced at the type level via a capability trait (`TextModeCap`), and we
//! provide a concrete alias `VgaText = Vga<TextMode>`.
//!
//! Graphics modes are intentionally not present yet; adding them will mean
//! introducing a new capability type (e.g., `GraphicsMode`) and a separate
//! adapter, so code that expects text-only APIs cannot accidentally compile
//! against graphics.
//!
//! NOTE!!!
//!
//! This file compiles and provides working text-
//! mode console support so the board can swap from the UART mux to a VGA
//! console. Graphical modes are *disabled at runtime* until a framebuffer
//! capsule implementation lands. The low-level register writes for 640×480 and 800×600 are
//! nonetheless laid out so they can be enabled by flipping a constant.
//! console.
//!
//! VGA peripheral driver for the x86_q35 chip.
//!
Expand All @@ -28,6 +29,7 @@
//! `ProcessConsole` to this driver or to the legacy serial mux.

use core::cell::Cell;
use core::marker::PhantomData;
use kernel::utilities::StaticRef;
use tock_cells::volatile_cell::VolatileCell;

Expand Down Expand Up @@ -130,25 +132,29 @@ impl ColorCode {
}
}

/// All VGA modes supported by the x86_q35 chip crate.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VgaMode {
Text80x25,
Graphics640x480_16,
Graphics800x600_16,
// Capability marker traits (type-level gating)
mod screen_cap {
// Sealed so only this module creates capabilities.
pub trait Sealed {}

/// Capability: this instance supports the text-plane writer.
pub trait TextModeCap: Sealed {}

/// The only mode we expose right now.
pub struct TextMode;
impl Sealed for TextMode {}
impl TextModeCap for TextMode {}
}
pub use screen_cap::{TextMode, TextModeCap};

// Constants for memory-mapped text mode buffer

// VGA physical Address

const TEXT_BUFFER_ADDR: usize = 0xB8000;
// Buffer dimensions
const TEXT_BUFFER_WIDTH: usize = 80;
const TEXT_BUFFER_HEIGHT: usize = 25;
/// Physical address where QEMU exposes the linear-frame-buffer BAR.
const LFB_PHYS_BASE: u32 = 0xE0_00_0000;
pub(crate) const TEXT_BUFFER_WIDTH: usize = 80;
pub(crate) const TEXT_BUFFER_HEIGHT: usize = 25;

const VGA_CELLS: StaticRef<[VolatileCell<u16>; TEXT_BUFFER_WIDTH * TEXT_BUFFER_HEIGHT]> =
unsafe { StaticRef::new(TEXT_BUFFER_ADDR as *const _) };
Expand Down Expand Up @@ -224,30 +230,9 @@ pub struct VgaDevice;
impl VgaDevice {
/// Global, row-major, bracket-indexable view.
const TEXT: TextBuf = TextBuf::new();
/// Program the requested mode on the VGA controller.
pub fn set_mode(mode: VgaMode) {
match mode {
VgaMode::Text80x25 => Self::program_text_mode(),
VgaMode::Graphics640x480_16 => panic!("VGA 640×480 mode not implemented"),
VgaMode::Graphics800x600_16 => panic!("VGA 800×600 mode not implemented"),
}
}

/// Only needed for graphics modes (linear framebuffer @ LFB_PHYS_BASE).
pub fn map_for_mode(mode: VgaMode, page_dir: &mut x86::registers::bits32::paging::PD) {
use x86::registers::bits32::paging::{PAddr, PDEntry, PDFlags, PDFLAGS};

if matches!(
mode,
VgaMode::Graphics640x480_16 | VgaMode::Graphics800x600_16
) {
let pde_idx = (LFB_PHYS_BASE >> 22) as usize;
let pa = PAddr::from(LFB_PHYS_BASE);
let mut flags = PDFlags::new(0);
flags.write(PDFLAGS::P::SET + PDFLAGS::RW::SET + PDFLAGS::PS::SET);
page_dir[pde_idx] = PDEntry::new(pa, flags);
}
}
// We only expose programming of the text controller here.
// (No graphics mode type/impl exists yet by design.)

// --- private ---

Expand Down Expand Up @@ -308,24 +293,97 @@ impl VgaDevice {
}

// Public API - the VGA struct providing text console implementation

/// Simple text-mode VGA console.
pub struct Vga {
// Generic over capability M. We only implement methods for M: TextModeCap.
pub struct Vga<M> {
col: Cell<usize>,
row: Cell<usize>,
/// Current VGA text attribute byte for newly written characters.
/// Layout (text mode):
/// bits 0–3 = fg (0–15), 4–6 = bg (0–7), 7 = blink/bright (mode-dependent).
/// Kept packed to match hardware and allow a single 16-bit volatile store per glyph.
attr: Cell<u8>,
// UTF-8 decode state
utf8_rem: Cell<u8>, // remaining continuation bytes
utf8_acc: Cell<u32>, // codepoint acc
_mode: PhantomData<M>,
}
impl Vga {
impl<M: TextModeCap> Vga<M> {
pub const fn new() -> Self {
Self {
col: Cell::new(0),
row: Cell::new(0),
// default: LightGray on Black, no blink
attr: Cell::new(ColorCode::new(Color::LightGray, Color::Black, false).as_u8()),
utf8_rem: Cell::new(0),
utf8_acc: Cell::new(0),
_mode: PhantomData,
}
}

#[inline(always)]
fn utf8_feed(&self, b: u8) -> Option<u32> {
let rem = self.utf8_rem.get();
if rem == 0 {
if b < 0x80 {
Some(b as u32)
} else if (0xC2..=0xDF).contains(&b) {
self.utf8_acc.set((b & 0x1F) as u32);
self.utf8_rem.set(1);
None
} else if (0xE0..=0xEF).contains(&b) {
self.utf8_acc.set((b & 0x0F) as u32);
self.utf8_rem.set(2);
None
} else if (0xF0..=0xF4).contains(&b) {
self.utf8_acc.set((b & 0x07) as u32);
self.utf8_rem.set(3);
None
} else {
// invalid starter
None
}
} else {
if (b & 0xC0) != 0x80 {
// invalid continuation, reset
self.utf8_rem.set(0);
return None;
}
let acc = (self.utf8_acc.get() << 6) | (b as u32 & 0x3F);
let rem2 = rem - 1;
self.utf8_acc.set(acc);
self.utf8_rem.set(rem2);
if rem2 == 0 {
Some(acc)
} else {
None
}
}
}

#[inline(always)]
fn unicode_to_cp437_or_ascii(ch: u32) -> Option<u8> {
// common box drawing used by PC tables
match ch {
0x2500 => Some(0xC4),
0x2502 => Some(0xB3),
0x250C => Some(0xDA),
0x2510 => Some(0xBF),
0x2514 => Some(0xC0),
0x2518 => Some(0xD9),
0x252C => Some(0xC2),
0x2534 => Some(0xC1),
0x251C => Some(0xC3),
0x2524 => Some(0xB4),
0x253C => Some(0xC5),

// fallbacks for common punctuation
0x2013 | 0x2014 => Some(b'-'),
0x2018 | 0x2019 | 0x201C | 0x201D => Some(b'"'),
0x2026 => Some(b'.'),
0x25BC => Some(b'v'),

_ if ch < 0x80 => Some(ch as u8), // plain ASCII
_ => None, // drop everything else
}
}

Expand Down Expand Up @@ -408,10 +466,72 @@ impl Vga {
b'\r' => {
self.col.set(0);
}
b'\t' => {
let col = self.col.get();
let next_tab = ((col / 8) + 1) * 8;
let end_col = core::cmp::min(next_tab, TEXT_BUFFER_WIDTH);
let space = ((self.attr.get() as u16) << 8) | (b' ' as u16);
for c in col..end_col {
if let Some(cell) = VgaDevice::TEXT.get(self.row.get(), c) {
cell.set(space);
}
}
if end_col == TEXT_BUFFER_WIDTH {
self.col.set(0);
self.row.set(self.row.get() + 1);
} else {
self.col.set(end_col);
}
}
0x08 => {
if self.col.get() > 0 {
self.col.set(self.col.get() - 1);
} else if self.row.get() > 0 {
self.row.set(self.row.get() - 1);
self.col.set(TEXT_BUFFER_WIDTH - 1);
}
let space = ((self.attr.get() as u16) << 8) | (b' ' as u16);
if let Some(cell) = VgaDevice::TEXT.get(self.row.get(), self.col.get()) {
cell.set(space);
}
}
0x0C => {
self.clear();
}
0x07 => {
// bell: ignore
}
b if b >= 0x80 => {
if let Some(ch) = self.utf8_feed(b) {
if let Some(cp) = Self::unicode_to_cp437_or_ascii(ch) {
// print this single-byte glyph
let val = ((self.attr.get() as u16) << 8) | cp as u16;
if let Some(cell) = VgaDevice::TEXT.get(self.row.get(), self.col.get()) {
cell.set(val);
} else {
self.scroll_up();
if let Some(cell) = VgaDevice::TEXT.get(self.row.get(), self.col.get())
{
cell.set(val);
}
}
// advance cursor, wrap at end-of-line
let mut col = self.col.get() + 1;
let mut row = self.row.get();
if col >= TEXT_BUFFER_WIDTH {
col = 0;
row += 1;
}
self.col.set(col);
self.row.set(row);
}
}
}
b if b < 0x20 || b == 0x7F => {
// ignore other control bytes
}
b => {
let val = ((self.attr.get() as u16) << 8) | b as u16;

// safe write; if somehow OOB, scroll and retry once
if let Some(cell) = VgaDevice::TEXT.get(self.row.get(), self.col.get()) {
cell.set(val);
} else {
Expand All @@ -420,8 +540,6 @@ impl Vga {
cell.set(val);
}
}

// advance cursor, wrap at end-of-line
let mut col = self.col.get() + 1;
let mut row = self.row.get();
if col >= TEXT_BUFFER_WIDTH {
Expand All @@ -433,24 +551,13 @@ impl Vga {
}
}

// scroll if we ran off the last row (covers '\n' path too)
if self.row.get() >= TEXT_BUFFER_HEIGHT {
self.scroll_up();
}

self.update_hw_cursor();
}
}

const _: () = {
// Exhaustively touch every current VgaMode variant
match VgaMode::Text80x25 {
VgaMode::Text80x25 => (),
VgaMode::Graphics640x480_16 => (),
VgaMode::Graphics800x600_16 => (),
}
};

// stub for future graphic options implementation
pub fn framebuffer() -> Option<(*mut u8, usize)> {
None
Expand All @@ -459,11 +566,14 @@ pub fn framebuffer() -> Option<(*mut u8, usize)> {
/// Initialise 80×25 text mode and start with a clean screen.
pub(crate) fn new_text_console(_page_dir_ptr: &mut x86::registers::bits32::paging::PD) {
// Program 80×25 text mode
VgaDevice::set_mode(VgaMode::Text80x25);
VgaDevice::program_text_mode();

// Wipe the BIOS banner so the kernel starts on a blank page.
let blank: u16 = 0x0720; // white-on-black space
for cell in &VgaDevice::TEXT {
cell.set(blank);
}
}

/// Convenience alias: concrete text-mode VGA writer
pub type VgaText = Vga<TextMode>;
Loading