diff --git a/chips/x86_q35/src/lib.rs b/chips/x86_q35/src/lib.rs index 457c582a4f..5b162671ad 100644 --- a/chips/x86_q35/src/lib.rs +++ b/chips/x86_q35/src/lib.rs @@ -24,6 +24,7 @@ mod pic; pub mod pit; pub mod serial; - pub mod vga; +pub mod vga_textscreen; + pub mod vga_uart_driver; diff --git a/chips/x86_q35/src/vga.rs b/chips/x86_q35/src/vga.rs index 88f7ff39ab..90ad67cd5c 100644 --- a/chips/x86_q35/src/vga.rs +++ b/chips/x86_q35/src/vga.rs @@ -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`. //! +//! 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. //! @@ -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; @@ -130,14 +132,20 @@ 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 @@ -145,10 +153,8 @@ pub enum VgaMode { 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; TEXT_BUFFER_WIDTH * TEXT_BUFFER_HEIGHT]> = unsafe { StaticRef::new(TEXT_BUFFER_ADDR as *const _) }; @@ -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 --- @@ -308,9 +293,8 @@ 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 { col: Cell, row: Cell, /// Current VGA text attribute byte for newly written characters. @@ -318,14 +302,88 @@ pub struct Vga { /// 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, + // UTF-8 decode state + utf8_rem: Cell, // remaining continuation bytes + utf8_acc: Cell, // codepoint acc + _mode: PhantomData, } -impl Vga { +impl Vga { 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 { + 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 { + // 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 } } @@ -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 { @@ -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 { @@ -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 @@ -459,7 +566,7 @@ 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 @@ -467,3 +574,6 @@ pub(crate) fn new_text_console(_page_dir_ptr: &mut x86::registers::bits32::pagin cell.set(blank); } } + +/// Convenience alias: concrete text-mode VGA writer +pub type VgaText = Vga; diff --git a/chips/x86_q35/src/vga_textscreen.rs b/chips/x86_q35/src/vga_textscreen.rs new file mode 100644 index 0000000000..24a9d03137 --- /dev/null +++ b/chips/x86_q35/src/vga_textscreen.rs @@ -0,0 +1,147 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright Tock Contributors 2025. + +//! VgaTextScreen - chip-side adapter that implements the kernel `TextScreen` HIL +//! by wrapping the existing VGA text-mode writer +//! +//! - `print()` writes bytes to VGA immediately, the completes via a deferred call +//! - Other commands are completed synchronously +//! +//! This file intentionally contains **no** UART concepts. It is purely a TextScreen + +use crate::vga::{TextMode, TextModeCap, Vga, TEXT_BUFFER_HEIGHT, TEXT_BUFFER_WIDTH}; +use core::cell::Cell; +use kernel::deferred_call::{DeferredCall, DeferredCallClient}; +use kernel::hil::text_screen::{TextScreen as HilTextScreen, TextScreenClient}; +use kernel::utilities::cells::{OptionalCell, TakeCell}; +use kernel::ErrorCode; + +// Generic adapter. We only implement the HIL for M: TextModeCap +pub struct VgaTextScreenImpl<'a, M: TextModeCap> { + vga: Vga, + client: OptionalCell<&'a dyn TextScreenClient>, + + // deferred completion for print() + dcall: DeferredCall, + tx_buf: TakeCell<'static, [u8]>, + tx_len: Cell, + busy: Cell, +} + +impl VgaTextScreenImpl<'_, M> { + pub fn new() -> Self { + Self { + vga: Vga::::new(), + client: OptionalCell::empty(), + dcall: DeferredCall::new(), + tx_buf: TakeCell::empty(), + tx_len: Cell::new(0), + busy: Cell::new(false), + } + } + + /// (Optional) helpers the board may want to expose later. + pub fn clear_screen(&self) { + self.vga.clear(); + } +} + +impl<'a, M: TextModeCap> HilTextScreen<'a> for VgaTextScreenImpl<'a, M> { + fn set_client(&self, client: Option<&'a dyn TextScreenClient>) { + match client { + Some(c) => self.client.set(c), + None => { + let _ = self.client.take(); + } + } + } + fn get_size(&self) -> (usize, usize) { + (TEXT_BUFFER_WIDTH, TEXT_BUFFER_HEIGHT) + } + + fn print( + &self, + buffer: &'static mut [u8], + len: usize, + ) -> Result<(), (ErrorCode, &'static mut [u8])> { + if self.busy.get() { + return Err((ErrorCode::BUSY, buffer)); + } + + // Consume as many as asked or available + let write_len = core::cmp::min(len, buffer.len()); + + // Render immediately (simple byte writer handles \n/\r/tab inside Vga) + for &b in &buffer[..write_len] { + self.vga.write_byte(b); + } + + // Schedule completion split-phase + self.tx_len.set(write_len); + self.tx_buf.replace(buffer); + self.busy.set(true); + self.dcall.set(); + Ok(()) + } + + fn set_cursor(&self, x: usize, y: usize) -> Result<(), ErrorCode> { + // Forward to the writer; it bounds-checks and updates the HW cursor. + self.vga.set_cursor(x, y); + self.client.map(|c| c.command_complete(Ok(()))); + Ok(()) + } + + fn hide_cursor(&self) -> Result<(), ErrorCode> { + self.client.map(|c| c.command_complete(Ok(()))); + Ok(()) + } + + fn show_cursor(&self) -> Result<(), ErrorCode> { + self.client.map(|c| c.command_complete(Ok(()))); + Ok(()) + } + + fn blink_cursor_on(&self) -> Result<(), ErrorCode> { + self.client.map(|c| c.command_complete(Ok(()))); + Ok(()) + } + + fn blink_cursor_off(&self) -> Result<(), ErrorCode> { + self.client.map(|c| c.command_complete(Ok(()))); + Ok(()) + } + + fn display_on(&self) -> Result<(), ErrorCode> { + self.client.map(|c| c.command_complete(Ok(()))); + Ok(()) + } + + fn display_off(&self) -> Result<(), ErrorCode> { + self.client.map(|c| c.command_complete(Ok(()))); + Ok(()) + } + + fn clear(&self) -> Result<(), ErrorCode> { + self.vga.clear(); + self.client.map(|c| c.command_complete(Ok(()))); + Ok(()) + } +} + +impl DeferredCallClient for VgaTextScreenImpl<'_, M> { + fn handle_deferred_call(&self) { + if let Some(buf) = self.tx_buf.take() { + let len = self.tx_len.get(); + self.busy.set(false); + self.client.map(|c| c.write_complete(buf, len, Ok(()))); + } + } + + fn register(&'static self) { + self.dcall.register(self); + } +} + +/// Public, concrete type the board uses: text-only screen. +pub type VgaTextScreen<'a> = VgaTextScreenImpl<'a, TextMode>; diff --git a/chips/x86_q35/src/vga_uart_driver.rs b/chips/x86_q35/src/vga_uart_driver.rs index 662c055e1f..4cb62a7b7e 100644 --- a/chips/x86_q35/src/vga_uart_driver.rs +++ b/chips/x86_q35/src/vga_uart_driver.rs @@ -14,17 +14,17 @@ //! - **Receive / abort / re-configure** operations just return //! `ErrorCode::NOSUPPORT` — VGA is output-only. -use crate::vga::Vga; +use crate::vga::{TextMode, Vga}; use core::{cell::Cell, cmp}; use kernel::deferred_call::{DeferredCall, DeferredCallClient}; use kernel::hil::uart::{Configure, Parameters, Receive, ReceiveClient, Transmit, TransmitClient}; +use kernel::utilities::cells::OptionalCell; use kernel::utilities::cells::TakeCell; use kernel::ErrorCode; -use tock_cells::optional_cell::OptionalCell; /// UART-compatible wrapper around the VGA text writer. pub struct VgaText<'a> { - vga_buffer: Vga, + vga_buffer: Vga, tx_client: OptionalCell<&'a dyn TransmitClient>, rx_client: OptionalCell<&'a dyn ReceiveClient>, deferred_call: DeferredCall, @@ -35,7 +35,7 @@ pub struct VgaText<'a> { impl VgaText<'_> { pub fn new() -> Self { Self { - vga_buffer: Vga::new(), + vga_buffer: Vga::::new(), tx_client: OptionalCell::empty(), rx_client: OptionalCell::empty(), deferred_call: DeferredCall::new(),