diff --git a/boards/qemu_i486_q35/src/main.rs b/boards/qemu_i486_q35/src/main.rs index 8cd4e18a33..4293af0115 100644 --- a/boards/qemu_i486_q35/src/main.rs +++ b/boards/qemu_i486_q35/src/main.rs @@ -11,6 +11,7 @@ use capsules_core::alarm; use capsules_core::console::{self, Console}; +use capsules_core::text_screen_uart::TextConsoleUart; use capsules_core::virtualizers::virtual_alarm::{MuxAlarm, VirtualMuxAlarm}; use components::console::ConsoleComponent; use components::debug_writer::DebugWriterComponent; @@ -18,7 +19,9 @@ use core::ptr; use kernel::capabilities; use kernel::component::Component; use kernel::debug; +use kernel::deferred_call::DeferredCallClient; use kernel::hil; +use kernel::hil::keyboard::Keyboard; use kernel::ipc::IPC; use kernel::platform::chip::Chip; use kernel::platform::scheduler_timer::VirtualSchedulerTimer; @@ -30,6 +33,7 @@ use kernel::{create_capability, static_init}; use x86::registers::bits32::paging::{PDEntry, PTEntry, PD, PT}; use x86::registers::irq; use x86_q35::pit::{Pit, RELOAD_1KHZ}; +use x86_q35::vga_textscreen::VgaTextScreen; use x86_q35::{Pc, PcComponent}; mod multiboot; @@ -177,12 +181,30 @@ unsafe extern "cdecl" fn main() { let uart_mux = components::console::UartMuxComponent::new(chip.com1, 115_200) .finalize(components::uart_mux_component_static!()); - // Alternative for VGA - let vga_uart_mux = components::console::UartMuxComponent::new(chip.vga, 115_200) + // VGA path: TextScreen (chip) + UART capsule over it + let vga_screen = static_init!(VgaTextScreen<'static>, VgaTextScreen::new()); + // VgaTextScreen completes print() via deferred call + vga_screen.register(); + + let vga_text_uart = static_init!( + TextConsoleUart<'static, VgaTextScreen<'static>>, + TextConsoleUart::new(vga_screen) + ); + + // Wire the PS/2 keyboard into the text console via the Keyboard HIL. + chip.keyboard.init_device(); + chip.keyboard.set_client(vga_text_uart); + + // TextConsoleUart pumps RX via deferred call and needs screen callback + vga_text_uart.register(); + vga_text_uart.set_as_screen_client(); + + // Build a UartMux on top of the TextConsoleUart so PC can attach + let vga_uart_mux = components::console::UartMuxComponent::new(vga_text_uart, 115_200) .finalize(components::uart_mux_component_static!()); // Debug output uses VGA when available, otherwise COM1 - let debug_uart_device = vga_uart_mux; + let debug_uart_device = uart_mux; // Create a shared virtualization mux layer on top of a single hardware // alarm. @@ -236,7 +258,7 @@ unsafe extern "cdecl" fn main() { // For now the ProcessConsole (interactive shell) is wired to COM1 so the user can // type commands over the serial port. Once keyboard input is implemented // we can switch `console_uart_device` to `vga_uart_mux`. - let console_uart_device = uart_mux; + let console_uart_device = vga_uart_mux; // Initialize the kernel's process console. let pconsole = components::process_console::ProcessConsoleComponent::new( diff --git a/capsules/core/README.md b/capsules/core/README.md index a632da6954..1b07bda7dc 100644 --- a/capsules/core/README.md +++ b/capsules/core/README.md @@ -55,6 +55,11 @@ various elements of Tock. low-level debugging tasks, such as debugging toolchain and relocation issues. - **[Process Console](src/process_console.rs)**: Provide a UART console to inspect the status of process and stop/start them. +- **[TextScreen UART](src/text_screen_uart.rs)**: UART HIL adapter that runs over a + `TextScreen`, allowing boards to drive `ProcessConsole` (and other UART clients) + on displays like VGA without changing those clients. TX forwards to `print()`, + RX buffers keystrokes and completes on newline/full. Not a userspace syscall driver. + Virtualized Hardware Resources ------------------------------ diff --git a/capsules/core/src/lib.rs b/capsules/core/src/lib.rs index 3528b00f03..437fbe4258 100644 --- a/capsules/core/src/lib.rs +++ b/capsules/core/src/lib.rs @@ -26,4 +26,5 @@ pub mod process_console; pub mod rng; pub mod spi_controller; pub mod spi_peripheral; +pub mod text_screen_uart; pub mod virtualizers; diff --git a/capsules/core/src/text_screen_uart.rs b/capsules/core/src/text_screen_uart.rs new file mode 100644 index 0000000000..fbb7b0b15a --- /dev/null +++ b/capsules/core/src/text_screen_uart.rs @@ -0,0 +1,413 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright Tock Contributors 2025. + +//! UART over a generic TextScreen (kernel::hil::text_screen). +//! +//! TX: forwards `transmit_buffer()` to `screen.print(..)` and completes via +//! `TextScreenClient::write_complete()`. RX: producers call `inject_byte()`; +//! bytes are buffered and completes on `\n` or when full. + +use core::cell::{Cell, RefCell}; +use core::cmp; +use kernel::hil::keyboard::KeyboardClient as HilKeyboardClient; + +use kernel::deferred_call::{DeferredCall, DeferredCallClient}; +use kernel::hil::text_screen::{TextScreen as HilTextScreen, TextScreenClient}; +use kernel::hil::uart::{ + Configure, Error as UartError, Parameters, Receive, ReceiveClient, Transmit, TransmitClient, +}; +use kernel::utilities::cells::{OptionalCell, TakeCell}; +use kernel::ErrorCode; + +const RX_FIFO_SIZE: usize = 64; + +// UART interface over a generic text screen +pub struct TextConsoleUart<'a, S: HilTextScreen<'a>> { + // where we output + screen: &'a S, + + kbd_shift_l: Cell, + kbd_shift_r: Cell, + kbd_caps: Cell, + + // UART clients + tx_client: OptionalCell<&'a dyn TransmitClient>, + rx_client: OptionalCell<&'a dyn ReceiveClient>, + + // TX state: in-flight write (owned by screen until write_complete()) + tx_busy: Cell, + // we shouldn't need a deferred call for TX because TextScreen + + // RX state + rx_buf: TakeCell<'static, [u8]>, + rx_wanted: Cell, + rx_len: Cell, + + // Type-ahead FIFO (drop-oldest on overflow + rx_fifo: RefCell<[u8; RX_FIFO_SIZE]>, + rx_head: Cell, + rx_tail: Cell, + rx_count: Cell, + + // deferred call to pump RX outside producer context + dcall: DeferredCall, + + // Odd take + // ProcessConsole typically treats submit as \n. + // Users may send \r (Enter on some keyboards) or \r\n + skip_next_lf: Cell, // if we just wrote '\n' because we saw '\r' +} + +impl<'a, S: HilTextScreen<'a>> TextConsoleUart<'a, S> { + pub fn new(screen: &'a S) -> Self { + Self { + screen, + kbd_shift_l: Cell::new(false), + kbd_shift_r: Cell::new(false), + kbd_caps: Cell::new(false), + tx_client: OptionalCell::empty(), + rx_client: OptionalCell::empty(), + + tx_busy: Cell::new(false), + + rx_buf: TakeCell::empty(), + rx_wanted: Cell::new(0), + rx_len: Cell::new(0), + + rx_fifo: RefCell::new([0; RX_FIFO_SIZE]), + rx_head: Cell::new(0), + rx_tail: Cell::new(0), + rx_count: Cell::new(0), + + dcall: DeferredCall::new(), + + skip_next_lf: Cell::new(false), + } + } + + #[inline(always)] + fn shift_active(&self) -> bool { + self.kbd_shift_l.get() || self.kbd_shift_r.get() + } + + // ADD + fn map_keycode_to_ascii(&self, code: u16, pressed: bool) -> Option { + if !pressed { + return None; + } + + // Update and ignore modifier keys here + match code { + 42 => { + // LEFTSHIFT + self.kbd_shift_l.set(true); + return None; + } + 54 => { + // RIGHTSHIFT + self.kbd_shift_r.set(true); + return None; + } + 58 => { + // CAPSLOCK toggles on press + self.kbd_caps.set(!self.kbd_caps.get()); + return None; + } + _ => {} + } + + // Basics + match code { + 28 => return Some(b'\n'), // ENTER + 14 => return Some(0x08), // BACKSPACE + 15 => return Some(b'\t'), // TAB + 57 => return Some(b' '), // SPACE + _ => {} + } + + // Digits row 1..0 + if (2..=11).contains(&code) { + let normal = [b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'0']; + let shifted = [b'!', b'@', b'#', b'$', b'%', b'^', b'&', b'*', b'(', b')']; + let idx = (code - 2) as usize; + return Some(if self.shift_active() { + shifted[idx] + } else { + normal[idx] + }); + } + + // Letters (Linux keycode set) + let letter = match code { + 30 => b'a', + 48 => b'b', + 46 => b'c', + 32 => b'd', + 18 => b'e', + 33 => b'f', + 34 => b'g', + 35 => b'h', + 23 => b'i', + 36 => b'j', + 37 => b'k', + 38 => b'l', + 50 => b'm', + 49 => b'n', + 24 => b'o', + 25 => b'p', + 16 => b'q', + 19 => b'r', + 31 => b's', + 20 => b't', + 22 => b'u', + 47 => b'v', + 17 => b'w', + 45 => b'x', + 21 => b'y', + 44 => b'z', + _ => 0, + }; + if letter != 0 { + let upper = self.shift_active() ^ self.kbd_caps.get(); + return Some(if upper { + letter.to_ascii_uppercase() + } else { + letter + }); + } + + // (Optional) extend with punctuation if needed later. + + None + } + pub fn inject_key_event(&self, code: u16, pressed: bool) { + // Track release of shift + if !pressed { + match code { + 42 => { + self.kbd_shift_l.set(false); + return; + } // LShift up + 54 => { + self.kbd_shift_r.set(false); + return; + } // RShift up + _ => {} + } + } + + if let Some(b) = self.map_keycode_to_ascii(code, pressed) { + self.inject_byte(b); + } + } + + /// Must be called by the board after construction to receive screen callbacks + /// (Call: screen.set_client(Some(capsule)) ) + pub fn set_as_screen_client(&'static self) { + self.screen.set_client(Some(self)); + } + + /// Feed one input byte. Safe from deferred context + pub fn inject_byte(&self, b: u8) { + self.fifo_push(b); + self.dcall.set(); + } + + // RX helpers + fn fifo_push(&self, b: u8) { + if self.rx_count.get() == RX_FIFO_SIZE { + // drop-oldest + self.rx_tail.set((self.rx_tail.get() + 1) % RX_FIFO_SIZE); + // count stays at full + } else { + self.rx_count.set(self.rx_count.get() + 1); + } + let h = self.rx_head.get(); + self.rx_fifo.borrow_mut()[h] = b; + self.rx_head.set((h + 1) % RX_FIFO_SIZE); + } + + fn fifo_pop(&self) -> Option { + if self.rx_count.get() == 0 { + return None; + } + let t = self.rx_tail.get(); + let b = self.rx_fifo.borrow()[t]; + self.rx_tail.set((t + 1) % RX_FIFO_SIZE); + self.rx_count.set(self.rx_count.get() - 1); + Some(b) + } + + fn pump_rx(&self) { + let mut should_complete = false; + let mut new_len = self.rx_len.get(); + + self.rx_buf.map(|buf| { + let wanted = self.rx_wanted.get(); + let mut len = self.rx_len.get(); + + while len < wanted { + // Pull next byte from FIFO + let mut b = match self.fifo_pop() { + Some(b) => b, + None => break, + }; + + // CR/LF normalization + // If previous byte was a '\r' we turned into '\n', skip a following real '\n' + if b == b'\n' && self.skip_next_lf.replace(false) { + continue; + } + // Map lone '\r' to '\n' and remember to drop a following '\n' (CRLF) + if b == b'\r' { + b = b'\n'; + self.skip_next_lf.set(true); + } + + buf[len] = b; + len += 1; + + if b == b'\n' { + // include newline and complete early + break; + } + } + + new_len = len; + + let last_is_newline = len > 0 && buf[len - 1] == b'\n'; + if len == wanted || last_is_newline { + should_complete = true; + } + }); + + self.rx_len.set(new_len); + + if should_complete { + if let Some(buf) = self.rx_buf.take() { + let len = self.rx_len.get(); + self.rx_len.set(0); + self.rx_wanted.set(0); + + // Move the 'static buffer into the callback + self.rx_client.map(move |client| { + client.received_buffer(buf, len, Ok(()), UartError::None); + }); + } + } + } +} + +// Deferred Call Client +impl<'a, S: HilTextScreen<'a>> DeferredCallClient for TextConsoleUart<'a, S> { + fn handle_deferred_call(&self) { + self.pump_rx(); + } + + fn register(&'static self) { + self.dcall.register(self); + } +} + +// UART Transmit + +impl<'a, S: HilTextScreen<'a>> Transmit<'a> for TextConsoleUart<'a, S> { + fn set_transmit_client(&self, client: &'a dyn TransmitClient) { + self.tx_client.set(client); + } + + fn transmit_buffer( + &self, + buffer: &'static mut [u8], + len: usize, + ) -> Result<(), (ErrorCode, &'static mut [u8])> { + if self.tx_busy.get() { + return Err((ErrorCode::BUSY, buffer)); + } + + let write_len = cmp::min(len, buffer.len()); + self.tx_busy.set(true); + match self.screen.print(buffer, write_len) { + Ok(()) => Ok(()), + Err((e, buf)) => { + self.tx_busy.set(false); + Err((e, buf)) + } + } + } + + fn transmit_word(&self, _word: u32) -> Result<(), ErrorCode> { + Err(ErrorCode::NOSUPPORT) + } + + fn transmit_abort(&self) -> Result<(), ErrorCode> { + Err(ErrorCode::NOSUPPORT) + } +} + +// UART Receive + +impl<'a, S: HilTextScreen<'a>> Receive<'a> for TextConsoleUart<'a, S> { + fn set_receive_client(&self, client: &'a dyn ReceiveClient) { + self.rx_client.set(client); + } + + fn receive_buffer( + &self, + buffer: &'static mut [u8], + len: usize, + ) -> Result<(), (ErrorCode, &'static mut [u8])> { + if self.rx_buf.is_some() { + return Err((ErrorCode::BUSY, buffer)); + } + let wanted = cmp::min(len, buffer.len()); + if wanted == 0 { + return Err((ErrorCode::SIZE, buffer)); + } + + self.rx_len.set(0); + self.rx_wanted.set(wanted); + self.rx_buf.replace(buffer); + + // Try to consume immediately from FIFO + self.dcall.set(); + Ok(()) + } + + fn receive_word(&self) -> Result<(), ErrorCode> { + Err(ErrorCode::NOSUPPORT) + } + + fn receive_abort(&self) -> Result<(), ErrorCode> { + Err(ErrorCode::NOSUPPORT) + } +} + +// UART over TextScreen: Screen Client +impl<'a, S: HilTextScreen<'a>> TextScreenClient for TextConsoleUart<'a, S> { + fn command_complete(&self, _r: Result<(), ErrorCode>) { + // We don't currently expose cursor/display commands via UART + } + + fn write_complete(&self, buffer: &'static mut [u8], len: usize, r: Result<(), ErrorCode>) { + self.tx_busy.set(false); + self.tx_client.map(|client| { + client.transmitted_buffer(buffer, len, r); + }); + } +} + +impl<'a, S: HilTextScreen<'a>> Configure for TextConsoleUart<'a, S> { + fn configure(&self, _params: Parameters) -> Result<(), ErrorCode> { + Ok(()) + } +} + +impl<'a, S: HilTextScreen<'a>> HilKeyboardClient for TextConsoleUart<'a, S> { + fn keys_pressed(&self, events: &[(u16, bool)], _r: Result<(), ErrorCode>) { + for &(code, pressed) in events { + self.inject_key_event(code, pressed); + } + } +} diff --git a/chips/x86_q35/src/chip.rs b/chips/x86_q35/src/chip.rs index 804e618311..decd1f4fc8 100644 --- a/chips/x86_q35/src/chip.rs +++ b/chips/x86_q35/src/chip.rs @@ -17,7 +17,6 @@ use crate::keyboard::Keyboard; use crate::pic::PIC1_OFFSET; use crate::pit::{Pit, RELOAD_1KHZ}; use crate::serial::{SerialPort, SerialPortComponent, COM1_BASE, COM2_BASE, COM3_BASE, COM4_BASE}; -use crate::vga_uart_driver::VgaText; /// Interrupt constants for legacy PC peripherals mod interrupt { @@ -76,9 +75,6 @@ pub struct Pc<'a, I: InterruptService + 'a, const PR: u16 = RELOAD_1KHZ> { /// Legacy PIT timer pub pit: Pit<'a, PR>, - /// Vga - pub vga: &'a VgaText<'a>, - /// PS/2 Controller pub ps2: &'a crate::ps2::Ps2Controller, @@ -274,10 +270,6 @@ impl Component for PcComponent<'static, I> { let pit = unsafe { Pit::new() }; - let vga = unsafe { static_init!(VgaText, VgaText::new()) }; - - kernel::deferred_call::DeferredCallClient::register(vga); - let paging = unsafe { let pd_addr = core::ptr::from_ref(self.pd) as usize; let pt_addr = core::ptr::from_ref(self.pt) as usize; @@ -313,7 +305,6 @@ impl Component for PcComponent<'static, I> { com3, com4, pit, - vga, ps2, keyboard, syscall, diff --git a/chips/x86_q35/src/keyboard.rs b/chips/x86_q35/src/keyboard.rs index 5e1d966ee5..6377daf9e7 100644 --- a/chips/x86_q35/src/keyboard.rs +++ b/chips/x86_q35/src/keyboard.rs @@ -216,12 +216,17 @@ impl<'a> Keyboard<'a> { } } - /// Device-level init hook. No-op for now since the `init_early()` - /// already runs in the controller. After capsule lands, we will enqueue: - /// F5 (disable scan) -> FF (reset; expect FA then AA) - F0 02 (Set-2) - F4 (enable). /// This will use the command engine (ACK/RESEND) and can run with IRQ1 enabled. + /// + /// Device init sequence (runs in bottom-half via the command engine): + /// F5 (disable scan) - FF (reset; expect FA then AA) - F0 02 (Set-2) - F4 (enable scan) + /// The command engine handles ACK(FA)/RESEND(FE); BAT(AA) is ignored by decode path. + pub fn init_device(&self) { - // TODO + let _ = self.enqueue_command(&[0xF5]); + let _ = self.enqueue_command(&[0xFF]); + let _ = self.enqueue_command(&[0xF0, 0x02]); + let _ = self.enqueue_command(&[0xF4]); } /// Command engine public API @@ -399,8 +404,18 @@ impl Ps2Client for Keyboard<'_> { } } } - // No command in flight: ignore device-only responses that aren’t keystrokes. - if byte == RESP_ACK || byte == RESP_RESEND || byte == RESP_BAT_OK { + // No command in flight: ignore device-only responses that aren’t keystrokes, + // but use BAT(AA) as a chance to reset local decode/modifier state. + if byte == RESP_ACK || byte == RESP_RESEND { + return; + } + if byte == RESP_BAT_OK { + self.got_e0.set(false); + self.got_f0.set(false); + self.swallow_e1.set(0); + self.shift_l.set(false); + self.shift_r.set(false); + self.caps.set(false); return; } self.decode_byte(byte); diff --git a/chips/x86_q35/src/lib.rs b/chips/x86_q35/src/lib.rs index 39f7062b10..8a05ee267d 100644 --- a/chips/x86_q35/src/lib.rs +++ b/chips/x86_q35/src/lib.rs @@ -27,6 +27,8 @@ pub mod ps2; pub mod serial; mod cmd_fifo; + pub mod keyboard; + pub mod vga; -pub mod vga_uart_driver; +pub mod vga_textscreen; diff --git a/chips/x86_q35/src/ps2.rs b/chips/x86_q35/src/ps2.rs index ae3eca5897..89534134d5 100644 --- a/chips/x86_q35/src/ps2.rs +++ b/chips/x86_q35/src/ps2.rs @@ -290,36 +290,6 @@ impl Ps2Controller { r } - /// Keyboard device commands (port 0x60) - /// Thin wrappers over `send_with_ack`. Kept as methods so they can use the - /// controller’s counters/state. - - /// Send a device command and wait for ACK (0xFA). - /// Retries on RESEND (0xFE) up to `tries - 1` times and increments `self.resends`. - /// Returns: - /// `Ok(())` on ACK - /// `Err(Ps2Error::AckError)` if RESEND persists after all retries - /// `Err(Ps2Error::UnexpectedResponse(_))` for any other response byte - fn send_with_ack_limit(&self, byte: u8, tries: u8, limit: usize) -> Result<(), Ps2Error> { - let mut attempts = 0; - loop { - attempts += 1; - write_data_with(byte, limit)?; - match read_data_with(limit)? { - 0xFA => return Ok(()), - 0xFE if attempts < tries => { - self.resends.set(self.resends.get().wrapping_add(1)); - continue; - } - 0xFE => { - self.resends.set(self.resends.get().wrapping_add(1)); - return Err(Ps2Error::AckError); - } - other => return Err(Ps2Error::UnexpectedResponse(other)), - } - } - } - /// Pure controller + device bring-up. /// No logging, no PIC masking/unmasking, no CPU-IRQ enabling. (hopefully) /// Called by PcComponent::finalize() (chip layer). @@ -378,22 +348,10 @@ impl Ps2Controller { self.tally_timeout(write_command_with(0xAE, spins))?; // device sequence (keyboard) - self.tally_timeout(self.send_with_ack_limit(0xF5, 3, spins))?; // F5 disable scan - self.tally_timeout(self.send_with_ack_limit(0xFF, 3, spins))?; // FF reset - self.tally_timeout({ - match read_data_with(spins)? { - 0xAA => Ok(()), // BAT passed - other => Err(Ps2Error::UnexpectedResponse(other)), - } - })?; - self.tally_timeout(self.send_with_ack_limit(0xF0, 3, spins))?; // select set - self.tally_timeout(self.send_with_ack_limit(0x02, 3, spins))?; // set-2 self.tally_timeout(update_config_with(spins, |mut c| { c.modify(CONFIG::IRQ1::SET); c }))?; - self.tally_timeout(self.send_with_ack_limit(0xF4, 3, spins))?; // enable scan - Ok(()) } 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 deleted file mode 100644 index 662c055e1f..0000000000 --- a/chips/x86_q35/src/vga_uart_driver.rs +++ /dev/null @@ -1,126 +0,0 @@ -// Licensed under the Apache License, Version 2.0 or the MIT License. -// SPDX-License-Identifier: Apache-2.0 OR MIT -// Copyright Tock Contributors 2025. - -//! VgaUart` — a **synchronous, write-only** façade that lets capsules -//! use a `hil::uart::Uart`-style interface while we actually write to -//! the VGA text buffer, not a serial port. -//! -//! ## Key design points -//! -//! - Implements the *minimum subset* of `Transmit` required by `MuxUart`. -//! All writes copy bytes to the global `VGA_TEXT` then schedule a deferred -//! call to invoke the transmit callback (split-phase contract). -//! - **Receive / abort / re-configure** operations just return -//! `ErrorCode::NOSUPPORT` — VGA is output-only. - -use crate::vga::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::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, - tx_client: OptionalCell<&'a dyn TransmitClient>, - rx_client: OptionalCell<&'a dyn ReceiveClient>, - deferred_call: DeferredCall, - pending_buf: TakeCell<'static, [u8]>, - pending_len: Cell, -} - -impl VgaText<'_> { - pub fn new() -> Self { - Self { - vga_buffer: Vga::new(), - tx_client: OptionalCell::empty(), - rx_client: OptionalCell::empty(), - deferred_call: DeferredCall::new(), - pending_buf: TakeCell::empty(), - pending_len: Cell::new(0), - } - } - - fn fire_tx_callback(&self, buf: &'static mut [u8], len: usize) { - self.tx_client.map(|client| { - client.transmitted_buffer(buf, len, Ok(())); - }); - } -} - -// DeferredCallClient implementation -impl DeferredCallClient for VgaText<'_> { - fn handle_deferred_call(&self) { - if let Some(buf) = self.pending_buf.take() { - let len = self.pending_len.get(); - self.fire_tx_callback(buf, len); - } - } - - fn register(&'static self) { - self.deferred_call.register(self); - } -} - -// Transmit for Vga -impl<'a> Transmit<'a> for VgaText<'a> { - fn set_transmit_client(&self, client: &'a dyn TransmitClient) { - self.tx_client.set(client); - } - - fn transmit_buffer( - &self, - buffer: &'static mut [u8], - len: usize, - ) -> Result<(), (ErrorCode, &'static mut [u8])> { - let write_len = cmp::min(len, buffer.len()); - for &byte in &buffer[..write_len] { - self.vga_buffer.write_byte(byte); - } - self.pending_buf.replace(buffer); - self.pending_len.set(len); - self.deferred_call.set(); - Ok(()) - } - - fn transmit_word(&self, _word: u32) -> Result<(), ErrorCode> { - Err(ErrorCode::NOSUPPORT) - } - - fn transmit_abort(&self) -> Result<(), ErrorCode> { - Err(ErrorCode::NOSUPPORT) - } -} - -// Receive for Vga -impl<'a> Receive<'a> for VgaText<'a> { - fn set_receive_client(&self, client: &'a dyn ReceiveClient) { - self.rx_client.set(client); - } - - fn receive_buffer( - &self, - buffer: &'static mut [u8], - _len: usize, - ) -> Result<(), (ErrorCode, &'static mut [u8])> { - Err((ErrorCode::NOSUPPORT, buffer)) - } - - fn receive_word(&self) -> Result<(), ErrorCode> { - Err(ErrorCode::NOSUPPORT) - } - - fn receive_abort(&self) -> Result<(), ErrorCode> { - Err(ErrorCode::NOSUPPORT) - } -} - -// Configure for Vga -impl Configure for VgaText<'_> { - fn configure(&self, _params: Parameters) -> Result<(), ErrorCode> { - Ok(()) - } -}