From 8b432d310407d41c87a1b8338cb350ee45a19e6d Mon Sep 17 00:00:00 2001 From: domnudragota Date: Mon, 1 Sep 2025 13:24:26 +0300 Subject: [PATCH 01/16] feature: add init skeleton for keyboard, implements ps2client and provide API for keyevent/keyboardClient --- chips/x86_q35/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/chips/x86_q35/src/lib.rs b/chips/x86_q35/src/lib.rs index 39f7062b10..306141778f 100644 --- a/chips/x86_q35/src/lib.rs +++ b/chips/x86_q35/src/lib.rs @@ -30,3 +30,4 @@ mod cmd_fifo; pub mod keyboard; pub mod vga; pub mod vga_uart_driver; +pub mod keyboard; From 1ef553f43f4a891629bcd74fa6419709230c1ecb Mon Sep 17 00:00:00 2001 From: domnudragota Date: Mon, 1 Sep 2025 13:24:26 +0300 Subject: [PATCH 02/16] feature: add init skeleton for keyboard, implements ps2client and provide API for keyevent/keyboardClient --- chips/x86_q35/src/chip.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/chips/x86_q35/src/chip.rs b/chips/x86_q35/src/chip.rs index 992fbd4c3c..b01129cfeb 100644 --- a/chips/x86_q35/src/chip.rs +++ b/chips/x86_q35/src/chip.rs @@ -18,6 +18,7 @@ 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; +use crate::keyboard::Keyboard; /// Interrupt constants for legacy PC peripherals mod interrupt { From 56b38de89609393cdc3a3142f2dc90d9d8e160fd Mon Sep 17 00:00:00 2001 From: domnudragota Date: Thu, 4 Sep 2025 13:22:09 +0300 Subject: [PATCH 03/16] feature: add UART Receive path + FIFO, deferred RX pump + TX consume --- chips/x86_q35/src/vga_uart_driver.rs | 168 +++++++++++++++++++++++---- 1 file changed, 148 insertions(+), 20 deletions(-) diff --git a/chips/x86_q35/src/vga_uart_driver.rs b/chips/x86_q35/src/vga_uart_driver.rs index 662c055e1f..ba4eca6f11 100644 --- a/chips/x86_q35/src/vga_uart_driver.rs +++ b/chips/x86_q35/src/vga_uart_driver.rs @@ -2,34 +2,55 @@ // 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. +//! VgaUart` — a **synchronous, write-only (TX)** backend //! -//! ## Key design points +//! Implements `hil:uart::Transmit` and `hil::uart::Receive` +//! can read keyboard input injected by the keyboard-console capsule. //! -//! - 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. +//! RX policy +//! - Bytes are injected via `inject_input_byte(b)` (called by the capsule) +//! - If a receive is active, we copy into the caller/s buffer and complete +//! on newline or when full +//! - If no receive is active, bytes go to a small `type-Ahead FIFO1` (drops oldest when full) +//! - Work is done in a deferred call (IRQ is kept small) +//! +//! Note for TX: +//! `transmit_buffer` reports the consumed length : length(min(requested, buf.len())). use crate::vga::Vga; -use core::{cell::Cell, cmp}; +use core::{ + cell::{Cell, RefCell}, + cmp, +}; use kernel::deferred_call::{DeferredCall, DeferredCallClient}; -use kernel::hil::uart::{Configure, Parameters, Receive, ReceiveClient, Transmit, TransmitClient}; +use kernel::hil::uart::{ + Configure, Error as UartError, Parameters, Receive, ReceiveClient, Transmit, TransmitClient, +}; use kernel::utilities::cells::TakeCell; use kernel::ErrorCode; use tock_cells::optional_cell::OptionalCell; +const RX_FIFO_SIZE: usize = 64; + /// UART-compatible wrapper around the VGA text writer. pub struct VgaText<'a> { + // TX side 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, + // number of actual bytes consumed (not the requested length) + pending_len_consumed: Cell, + // RX side + rx_client: OptionalCell<&'a dyn ReceiveClient>, + rx_buf: TakeCell<'static, [u8]>, + rx_wanted: Cell, + rx_len: Cell, + // small FIFO (drop oldest on overflow) + rx_fifo: RefCell<[u8; RX_FIFO_SIZE]>, + rx_head: Cell, + rx_tail: Cell, + rx_count: Cell, } impl VgaText<'_> { @@ -40,24 +61,110 @@ impl VgaText<'_> { rx_client: OptionalCell::empty(), deferred_call: DeferredCall::new(), pending_buf: TakeCell::empty(), - pending_len: Cell::new(0), + pending_len_consumed: Cell::new(0), + 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), } } - fn fire_tx_callback(&self, buf: &'static mut [u8], len: usize) { + // TX helpers + fn fire_tx_callback(&self, buf: &'static mut [u8], len_consumed: usize) { self.tx_client.map(|client| { - client.transmitted_buffer(buf, len, Ok(())); + client.transmitted_buffer(buf, len_consumed, Ok(())); }); } + + // RX helpers + fn fifo_push(&self, b: u8) { + // drop-oldest on overflow + if self.rx_count.get() == RX_FIFO_SIZE { + // overwrite oldest: advance tail, decrement count stays at full + self.rx_tail.set((self.rx_tail.get() + 1) % RX_FIFO_SIZE); + self.rx_count.set(self.rx_count.get()) //stays full + } else { + self.rx_count.set(self.rx_count.get() + 1); + } + let head = self.rx_head.get(); + self.rx_fifo.borrow_mut()[head] = b; + self.rx_head.set((head + 1) % RX_FIFO_SIZE); + } + fn fifo_pop(&self) -> Option { + if self.rx_count.get() == 0 { + return None; + } + let tail = self.rx_tail.get(); + let b = self.rx_fifo.borrow()[tail]; + self.rx_tail.set((tail + 1) % RX_FIFO_SIZE); + self.rx_count.set(self.rx_count.get() - 1); + Some(b) + } + + fn pump_rx(&self) { + // Move bytes from FIFO into an active RX buffer + // Complete when newline is copied or wanted bytes threshold reached + let mut should_complete = false; + + // we need the current length after the borrow ends + 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 { + match self.fifo_pop() { + Some(b) => { + buf[len] = b; + len += 1; + if b == b'\n' { + // include the newline in the completion + break; + } + } + None => break, // Nothing more available + } + } + 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); + self.rx_client.map(|client| { + client.received_buffer(buf, len, Ok(()), UartError::None); + }); + } + } + } + /// Called by the keyboard_console capsule to feed a keystroke + /// Safe to call from deferred context only + pub fn inject_input_byte(&self, b: u8) { + self.fifo_push(b); + // Defer delivery into the active RX buffer + self.deferred_call.set(); + } } // 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(); + let len = self.pending_len_consumed.get(); self.fire_tx_callback(buf, len); } + self.pump_rx(); } fn register(&'static self) { @@ -81,7 +188,7 @@ impl<'a> Transmit<'a> for VgaText<'a> { self.vga_buffer.write_byte(byte); } self.pending_buf.replace(buffer); - self.pending_len.set(len); + self.pending_len_consumed.set(write_len); self.deferred_call.set(); Ok(()) } @@ -104,9 +211,30 @@ impl<'a> Receive<'a> for VgaText<'a> { fn receive_buffer( &self, buffer: &'static mut [u8], - _len: usize, + len: usize, ) -> Result<(), (ErrorCode, &'static mut [u8])> { - Err((ErrorCode::NOSUPPORT, buffer)) + // Only one receive allowed + let busy = { + let mut present = false; + self.rx_buf.map(|_| { + present = true; + }); + present + }; + if busy { + 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 place immediately from FIFO + self.deferred_call.set(); + Ok(()) } fn receive_word(&self) -> Result<(), ErrorCode> { From 6e71b5a9694ffbcaaa206746b3b11721030a0dad Mon Sep 17 00:00:00 2001 From: domnudragota Date: Thu, 4 Sep 2025 13:59:39 +0300 Subject: [PATCH 04/16] feature: add keyboardconsole glue capsule and wire it to keyboard-vgatext --- chips/x86_q35/src/keyboard_console.rs | 30 +++++++++++++++++++++++++++ chips/x86_q35/src/lib.rs | 1 + 2 files changed, 31 insertions(+) create mode 100644 chips/x86_q35/src/keyboard_console.rs diff --git a/chips/x86_q35/src/keyboard_console.rs b/chips/x86_q35/src/keyboard_console.rs new file mode 100644 index 0000000000..8d983e4f58 --- /dev/null +++ b/chips/x86_q35/src/keyboard_console.rs @@ -0,0 +1,30 @@ +// Licensed under the Apache License, Version 2.0 or the MIT License. +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright Tock Contributors 2025. + +//! Keyboard/Console glue capsule +//! +//! Bridges ASCII keystrokes from the PS/2 keyboard device +//! into the VGA-backed +//! UART-like driver (VGAText) so ProcessConsole can receive input + +use crate::keyboard::AsciiClient; +use crate::vga_uart_driver::VgaText; + +// Forward bytes into VGAText's RX path +pub struct KeyboardConsole<'a> { + vga: &'a VgaText<'a>, +} + +impl<'a> KeyboardConsole<'a> { + pub const fn new(vga: &'a VgaText<'a>) -> Self { + Self { vga } + } +} + +impl AsciiClient for KeyboardConsole<'_> { + fn put_byte(&self, b: u8) { + // Newline completion is handled inside VgaText + self.vga.inject_input_byte(b); + } +} diff --git a/chips/x86_q35/src/lib.rs b/chips/x86_q35/src/lib.rs index 306141778f..2e4d96def3 100644 --- a/chips/x86_q35/src/lib.rs +++ b/chips/x86_q35/src/lib.rs @@ -28,6 +28,7 @@ pub mod serial; mod cmd_fifo; pub mod keyboard; +pub mod keyboard_console; pub mod vga; pub mod vga_uart_driver; pub mod keyboard; From 21b1d92a6e036ff47037b32f542a2374bff59297 Mon Sep 17 00:00:00 2001 From: domnudragota Date: Thu, 4 Sep 2025 14:11:11 +0300 Subject: [PATCH 05/16] rebase ps2-keyboard-console-capsule from ps2-keyboard-v2 --- chips/x86_q35/src/keyboard.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/chips/x86_q35/src/keyboard.rs b/chips/x86_q35/src/keyboard.rs index 28bd883ecc..b19d66c297 100644 --- a/chips/x86_q35/src/keyboard.rs +++ b/chips/x86_q35/src/keyboard.rs @@ -182,8 +182,12 @@ impl<'a> Keyboard<'a> { /// 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. + 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]); } /// Not used by the current ASCII-only console path. Subject to change. From e8b6d4532fc9ad8b55c6d67fc649b60c875e36b2 Mon Sep 17 00:00:00 2001 From: domnudragota Date: Thu, 4 Sep 2025 14:28:09 +0300 Subject: [PATCH 06/16] workflow: test PC on VGA output, add more encoding support for VGA --- chips/x86_q35/src/vga.rs | 47 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/chips/x86_q35/src/vga.rs b/chips/x86_q35/src/vga.rs index 88f7ff39ab..2646f9b3ca 100644 --- a/chips/x86_q35/src/vga.rs +++ b/chips/x86_q35/src/vga.rs @@ -402,12 +402,57 @@ impl Vga { pub fn write_byte(&self, byte: u8) { match byte { b'\n' => { + // line feed self.col.set(0); self.row.set(self.row.get() + 1); } b'\r' => { + // carriage return self.col.set(0); } + b'\t' => { + // advance to next 8-column tab stop by inserting spaces + 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 { + // wrapped to next line + self.col.set(0); + self.row.set(self.row.get() + 1); + } else { + self.col.set(end_col); + } + } + 0x08 => { + // backspace: move left (wrapping to previous line) and erase the cell + 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); + } else { + // at (0,0): nothing to do + } + 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 => { + // form feed: clear screen + self.clear(); + } + 0x07 => { + // bell: no-op + } b => { let val = ((self.attr.get() as u16) << 8) | b as u16; @@ -433,7 +478,7 @@ impl Vga { } } - // scroll if we ran off the last row (covers '\n' path too) + // scroll if we ran off the last row (covers '\n' and tab wrap) if self.row.get() >= TEXT_BUFFER_HEIGHT { self.scroll_up(); } From f2d70ef45fdab1aea05980b5ce51d9f3d1e26c48 Mon Sep 17 00:00:00 2001 From: domnudragota Date: Thu, 4 Sep 2025 14:48:04 +0300 Subject: [PATCH 07/16] fix: add glyph support for kernel command table --- chips/x86_q35/src/vga.rs | 120 +++++++++++++++++++++++++++++++++------ 1 file changed, 102 insertions(+), 18 deletions(-) diff --git a/chips/x86_q35/src/vga.rs b/chips/x86_q35/src/vga.rs index 2646f9b3ca..dc45149e97 100644 --- a/chips/x86_q35/src/vga.rs +++ b/chips/x86_q35/src/vga.rs @@ -318,6 +318,9 @@ 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 } impl Vga { pub const fn new() -> Self { @@ -326,6 +329,75 @@ impl Vga { 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), + } + } + + #[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 } } @@ -402,29 +474,23 @@ impl Vga { pub fn write_byte(&self, byte: u8) { match byte { b'\n' => { - // line feed self.col.set(0); self.row.set(self.row.get() + 1); } b'\r' => { - // carriage return self.col.set(0); } b'\t' => { - // advance to next 8-column tab stop by inserting spaces 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 { - // wrapped to next line self.col.set(0); self.row.set(self.row.get() + 1); } else { @@ -432,14 +498,11 @@ impl Vga { } } 0x08 => { - // backspace: move left (wrapping to previous line) and erase the cell 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); - } else { - // at (0,0): nothing to do } let space = ((self.attr.get() as u16) << 8) | (b' ' as u16); if let Some(cell) = VgaDevice::TEXT.get(self.row.get(), self.col.get()) { @@ -447,16 +510,42 @@ impl Vga { } } 0x0C => { - // form feed: clear screen self.clear(); } 0x07 => { - // bell: no-op + // 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 { @@ -465,8 +554,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 { @@ -478,15 +565,12 @@ impl Vga { } } - // scroll if we ran off the last row (covers '\n' and tab wrap) if self.row.get() >= TEXT_BUFFER_HEIGHT { self.scroll_up(); } - self.update_hw_cursor(); } } - const _: () = { // Exhaustively touch every current VgaMode variant match VgaMode::Text80x25 { From fb75f33e85c5677bb16d920ac6ba07d3bd7ba45b Mon Sep 17 00:00:00 2001 From: domnudragota Date: Thu, 4 Sep 2025 15:11:31 +0300 Subject: [PATCH 08/16] fix: ran cargo fmt + makeprepush again --- chips/x86_q35/src/vga.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chips/x86_q35/src/vga.rs b/chips/x86_q35/src/vga.rs index dc45149e97..b91e89f83b 100644 --- a/chips/x86_q35/src/vga.rs +++ b/chips/x86_q35/src/vga.rs @@ -397,7 +397,7 @@ impl Vga { 0x25BC => Some(b'v'), _ if ch < 0x80 => Some(ch as u8), // plain ASCII - _ => None, // drop everything else + _ => None, // drop everything else } } From 578038b7ffb2a3306995d2580e791ce507ed2180 Mon Sep 17 00:00:00 2001 From: domnudragota Date: Thu, 11 Sep 2025 15:03:13 +0300 Subject: [PATCH 09/16] fix: removed rebase artifacts --- chips/x86_q35/src/chip.rs | 1 - chips/x86_q35/src/lib.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/chips/x86_q35/src/chip.rs b/chips/x86_q35/src/chip.rs index b01129cfeb..992fbd4c3c 100644 --- a/chips/x86_q35/src/chip.rs +++ b/chips/x86_q35/src/chip.rs @@ -18,7 +18,6 @@ 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; -use crate::keyboard::Keyboard; /// Interrupt constants for legacy PC peripherals mod interrupt { diff --git a/chips/x86_q35/src/lib.rs b/chips/x86_q35/src/lib.rs index 2e4d96def3..549d8ba7d6 100644 --- a/chips/x86_q35/src/lib.rs +++ b/chips/x86_q35/src/lib.rs @@ -27,8 +27,8 @@ pub mod ps2; pub mod serial; mod cmd_fifo; + pub mod keyboard; pub mod keyboard_console; pub mod vga; pub mod vga_uart_driver; -pub mod keyboard; From 0b0fc876a7461017eb61a509fde3641e0a2c4a47 Mon Sep 17 00:00:00 2001 From: domnudragota Date: Mon, 15 Sep 2025 14:14:31 +0300 Subject: [PATCH 10/16] capsules/core: add TextConsoleUart - UART over TextScreen --- capsules/core/src/lib.rs | 1 + capsules/core/src/text_screen_uart.rs | 287 ++++++++++++++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 capsules/core/src/text_screen_uart.rs diff --git a/capsules/core/src/lib.rs b/capsules/core/src/lib.rs index 3528b00f03..7bd5093acd 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; +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..4c6cd457e0 --- /dev/null +++ b/capsules/core/src/text_screen_uart.rs @@ -0,0 +1,287 @@ +// 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. + +// ONLY FOR NOW, WILL BE REMOVED LATER +#[allow(dead_code)] +use core::cell::{Cell, RefCell}; +use core::cmp; + +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, + + // 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]>, + // ONLY FOR NOW, WILL BE REMOVED LATER + #[allow(dead_code)] + 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' +} + +// ONLY FOR NOW, WILL BE REMOVED LATER +#[allow(dead_code)] +impl<'a, S: HilTextScreen<'a>> TextConsoleUart<'a, S> { + pub fn new(screen: &'a S) -> Self { + Self { + screen, + 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), + } + } + + /// 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 (from keyboard glue). 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(()) + } +} From 670db49b57d09cc9fcdd428f0c5426ce4b058023 Mon Sep 17 00:00:00 2001 From: domnudragota Date: Mon, 15 Sep 2025 14:26:43 +0300 Subject: [PATCH 11/16] document TextScreen UART adapter in README --- capsules/core/README.md | 5 +++++ 1 file changed, 5 insertions(+) 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 ------------------------------ From db60143d81adc33596f8122f295e26a57fbddaa1 Mon Sep 17 00:00:00 2001 From: domnudragota Date: Mon, 15 Sep 2025 15:18:35 +0300 Subject: [PATCH 12/16] feature: add VgaTextScreen adapter implementing kernel::hil::text_screen for VGA, no UART coupling --- chips/x86_q35/src/lib.rs | 1 + chips/x86_q35/src/vga.rs | 4 +- chips/x86_q35/src/vga_textscreen.rs | 154 ++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 chips/x86_q35/src/vga_textscreen.rs diff --git a/chips/x86_q35/src/lib.rs b/chips/x86_q35/src/lib.rs index 549d8ba7d6..dff64f1549 100644 --- a/chips/x86_q35/src/lib.rs +++ b/chips/x86_q35/src/lib.rs @@ -31,4 +31,5 @@ mod cmd_fifo; pub mod keyboard; pub mod keyboard_console; pub mod vga; +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 b91e89f83b..edb11aed6f 100644 --- a/chips/x86_q35/src/vga.rs +++ b/chips/x86_q35/src/vga.rs @@ -145,8 +145,8 @@ pub enum VgaMode { const TEXT_BUFFER_ADDR: usize = 0xB8000; // Buffer dimensions -const TEXT_BUFFER_WIDTH: usize = 80; -const TEXT_BUFFER_HEIGHT: usize = 25; +pub(crate) const TEXT_BUFFER_WIDTH: usize = 80; +pub(crate) const TEXT_BUFFER_HEIGHT: usize = 25; /// Physical address where QEMU exposes the linear-frame-buffer BAR. const LFB_PHYS_BASE: u32 = 0xE0_00_0000; diff --git a/chips/x86_q35/src/vga_textscreen.rs b/chips/x86_q35/src/vga_textscreen.rs new file mode 100644 index 0000000000..41af0baf4c --- /dev/null +++ b/chips/x86_q35/src/vga_textscreen.rs @@ -0,0 +1,154 @@ +// 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::{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; + +pub struct VgaTextScreen<'a> { + vga: Vga, + client: OptionalCell<&'a dyn TextScreenClient>, + + // deferred completion for print() + dcall: DeferredCall, + tx_buf: TakeCell<'static, [u8]>, + tx_len: Cell, + busy: Cell, +} + +impl VgaTextScreen<'_> { + #[allow(dead_code)] + // WILL BE REMOVED IN THE FUTURE + 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), + } + } + + #[allow(dead_code)] + // WILL BE REMOVED IN THE FUTURE + /// (Optional) helpers the board may want to expose later. + pub fn clear_screen(&self) { + self.vga.clear(); + } + + // Keep the board wiring simple + + #[allow(dead_code)] + // WILL BE REMOVED IN THE FUTURE + pub fn register_deferred_call(&'static self) { + self.dcall.register(self); + } +} + +impl<'a> HilTextScreen<'a> for VgaTextScreen<'a> { + 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> { + // Minimal first pass: acknowledge immediately. + 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 VgaTextScreen<'_> { + 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); + } +} From adff4cbb8ff087ed9f1f2a7465d287488eea3350 Mon Sep 17 00:00:00 2001 From: domnudragota Date: Tue, 16 Sep 2025 13:25:52 +0300 Subject: [PATCH 13/16] feature: wired capsule in main.rs, removed old glue --- boards/qemu_i486_q35/src/main.rs | 43 ++++- capsules/core/src/lib.rs | 2 +- chips/x86_q35/src/keyboard.rs | 7 +- chips/x86_q35/src/keyboard_console.rs | 30 --- chips/x86_q35/src/lib.rs | 5 +- chips/x86_q35/src/vga_textscreen.rs | 7 - chips/x86_q35/src/vga_uart_driver.rs | 254 -------------------------- 7 files changed, 46 insertions(+), 302 deletions(-) delete mode 100644 chips/x86_q35/src/keyboard_console.rs delete mode 100644 chips/x86_q35/src/vga_uart_driver.rs diff --git a/boards/qemu_i486_q35/src/main.rs b/boards/qemu_i486_q35/src/main.rs index 8cd4e18a33..536bdcaf17 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::text_screen::TextScreen as HilTextScreen; use kernel::ipc::IPC; use kernel::platform::chip::Chip; use kernel::platform::scheduler_timer::VirtualSchedulerTimer; @@ -29,7 +32,9 @@ use kernel::syscall::SyscallDriver; use kernel::{create_capability, static_init}; use x86::registers::bits32::paging::{PDEntry, PTEntry, PD, PT}; use x86::registers::irq; +use x86_q35::keyboard::AsciiClient; use x86_q35::pit::{Pit, RELOAD_1KHZ}; +use x86_q35::vga_textscreen::VgaTextScreen; use x86_q35::{Pc, PcComponent}; mod multiboot; @@ -92,6 +97,17 @@ pub struct QemuI386Q35Platform { &'static VirtualSchedulerTimer>>, } +struct KbdToUart<'a, S: HilTextScreen<'a>> { + uart: &'a TextConsoleUart<'a, S>, +} + +impl<'a, S: HilTextScreen<'a>> AsciiClient for KbdToUart<'a, S> { + #[inline] + fn put_byte(&self, b: u8) { + self.uart.inject_byte(b); + } +} + impl SyscallDriverLookup for QemuI386Q35Platform { fn with_driver(&self, driver_num: usize, f: F) -> R where @@ -177,8 +193,31 @@ 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) + ); + + let kbd_to_uart = static_init!( + KbdToUart<'static, VgaTextScreen<'static>>, + KbdToUart { + uart: vga_text_uart + } + ); + + chip.keyboard.set_ascii_client(kbd_to_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 diff --git a/capsules/core/src/lib.rs b/capsules/core/src/lib.rs index 7bd5093acd..437fbe4258 100644 --- a/capsules/core/src/lib.rs +++ b/capsules/core/src/lib.rs @@ -26,5 +26,5 @@ pub mod process_console; pub mod rng; pub mod spi_controller; pub mod spi_peripheral; -mod text_screen_uart; +pub mod text_screen_uart; pub mod virtualizers; diff --git a/chips/x86_q35/src/keyboard.rs b/chips/x86_q35/src/keyboard.rs index b19d66c297..3d589c05f8 100644 --- a/chips/x86_q35/src/keyboard.rs +++ b/chips/x86_q35/src/keyboard.rs @@ -43,10 +43,7 @@ pub struct KeyEvent { pub extended: bool, } -/// Optional byte send for ASCII output (useful for a later capsule) -/// Not used by the current ASCII-only console path. Subject to change. -/// Called from the keyboard's deferred (bottom-half) context; keep it non-blocking. -pub(crate) trait AsciiClient { +pub trait AsciiClient { fn put_byte(&self, b: u8); } @@ -193,7 +190,7 @@ impl<'a> Keyboard<'a> { /// Not used by the current ASCII-only console path. Subject to change. /// Will expand when writing a capsule for PC #[allow(dead_code)] - pub(crate) fn set_ascii_client(&self, c: &'static dyn AsciiClient) { + pub fn set_ascii_client(&self, c: &'static dyn AsciiClient) { self.ascii.set(c); } diff --git a/chips/x86_q35/src/keyboard_console.rs b/chips/x86_q35/src/keyboard_console.rs deleted file mode 100644 index 8d983e4f58..0000000000 --- a/chips/x86_q35/src/keyboard_console.rs +++ /dev/null @@ -1,30 +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. - -//! Keyboard/Console glue capsule -//! -//! Bridges ASCII keystrokes from the PS/2 keyboard device -//! into the VGA-backed -//! UART-like driver (VGAText) so ProcessConsole can receive input - -use crate::keyboard::AsciiClient; -use crate::vga_uart_driver::VgaText; - -// Forward bytes into VGAText's RX path -pub struct KeyboardConsole<'a> { - vga: &'a VgaText<'a>, -} - -impl<'a> KeyboardConsole<'a> { - pub const fn new(vga: &'a VgaText<'a>) -> Self { - Self { vga } - } -} - -impl AsciiClient for KeyboardConsole<'_> { - fn put_byte(&self, b: u8) { - // Newline completion is handled inside VgaText - self.vga.inject_input_byte(b); - } -} diff --git a/chips/x86_q35/src/lib.rs b/chips/x86_q35/src/lib.rs index dff64f1549..8a05ee267d 100644 --- a/chips/x86_q35/src/lib.rs +++ b/chips/x86_q35/src/lib.rs @@ -29,7 +29,6 @@ pub mod serial; mod cmd_fifo; pub mod keyboard; -pub mod keyboard_console; + pub mod vga; -mod vga_textscreen; -pub mod vga_uart_driver; +pub mod vga_textscreen; diff --git a/chips/x86_q35/src/vga_textscreen.rs b/chips/x86_q35/src/vga_textscreen.rs index 41af0baf4c..86266f296a 100644 --- a/chips/x86_q35/src/vga_textscreen.rs +++ b/chips/x86_q35/src/vga_textscreen.rs @@ -29,8 +29,6 @@ pub struct VgaTextScreen<'a> { } impl VgaTextScreen<'_> { - #[allow(dead_code)] - // WILL BE REMOVED IN THE FUTURE pub fn new() -> Self { Self { vga: Vga::new(), @@ -42,17 +40,12 @@ impl VgaTextScreen<'_> { } } - #[allow(dead_code)] - // WILL BE REMOVED IN THE FUTURE /// (Optional) helpers the board may want to expose later. pub fn clear_screen(&self) { self.vga.clear(); } // Keep the board wiring simple - - #[allow(dead_code)] - // WILL BE REMOVED IN THE FUTURE pub fn register_deferred_call(&'static self) { self.dcall.register(self); } 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 ba4eca6f11..0000000000 --- a/chips/x86_q35/src/vga_uart_driver.rs +++ /dev/null @@ -1,254 +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 (TX)** backend -//! -//! Implements `hil:uart::Transmit` and `hil::uart::Receive` -//! can read keyboard input injected by the keyboard-console capsule. -//! -//! RX policy -//! - Bytes are injected via `inject_input_byte(b)` (called by the capsule) -//! - If a receive is active, we copy into the caller/s buffer and complete -//! on newline or when full -//! - If no receive is active, bytes go to a small `type-Ahead FIFO1` (drops oldest when full) -//! - Work is done in a deferred call (IRQ is kept small) -//! -//! Note for TX: -//! `transmit_buffer` reports the consumed length : length(min(requested, buf.len())). - -use crate::vga::Vga; -use core::{ - cell::{Cell, RefCell}, - cmp, -}; -use kernel::deferred_call::{DeferredCall, DeferredCallClient}; -use kernel::hil::uart::{ - Configure, Error as UartError, Parameters, Receive, ReceiveClient, Transmit, TransmitClient, -}; -use kernel::utilities::cells::TakeCell; -use kernel::ErrorCode; -use tock_cells::optional_cell::OptionalCell; - -const RX_FIFO_SIZE: usize = 64; - -/// UART-compatible wrapper around the VGA text writer. -pub struct VgaText<'a> { - // TX side - vga_buffer: Vga, - tx_client: OptionalCell<&'a dyn TransmitClient>, - deferred_call: DeferredCall, - pending_buf: TakeCell<'static, [u8]>, - // number of actual bytes consumed (not the requested length) - pending_len_consumed: Cell, - // RX side - rx_client: OptionalCell<&'a dyn ReceiveClient>, - rx_buf: TakeCell<'static, [u8]>, - rx_wanted: Cell, - rx_len: Cell, - // small FIFO (drop oldest on overflow) - rx_fifo: RefCell<[u8; RX_FIFO_SIZE]>, - rx_head: Cell, - rx_tail: Cell, - rx_count: 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_consumed: Cell::new(0), - 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), - } - } - - // TX helpers - fn fire_tx_callback(&self, buf: &'static mut [u8], len_consumed: usize) { - self.tx_client.map(|client| { - client.transmitted_buffer(buf, len_consumed, Ok(())); - }); - } - - // RX helpers - fn fifo_push(&self, b: u8) { - // drop-oldest on overflow - if self.rx_count.get() == RX_FIFO_SIZE { - // overwrite oldest: advance tail, decrement count stays at full - self.rx_tail.set((self.rx_tail.get() + 1) % RX_FIFO_SIZE); - self.rx_count.set(self.rx_count.get()) //stays full - } else { - self.rx_count.set(self.rx_count.get() + 1); - } - let head = self.rx_head.get(); - self.rx_fifo.borrow_mut()[head] = b; - self.rx_head.set((head + 1) % RX_FIFO_SIZE); - } - fn fifo_pop(&self) -> Option { - if self.rx_count.get() == 0 { - return None; - } - let tail = self.rx_tail.get(); - let b = self.rx_fifo.borrow()[tail]; - self.rx_tail.set((tail + 1) % RX_FIFO_SIZE); - self.rx_count.set(self.rx_count.get() - 1); - Some(b) - } - - fn pump_rx(&self) { - // Move bytes from FIFO into an active RX buffer - // Complete when newline is copied or wanted bytes threshold reached - let mut should_complete = false; - - // we need the current length after the borrow ends - 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 { - match self.fifo_pop() { - Some(b) => { - buf[len] = b; - len += 1; - if b == b'\n' { - // include the newline in the completion - break; - } - } - None => break, // Nothing more available - } - } - 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); - self.rx_client.map(|client| { - client.received_buffer(buf, len, Ok(()), UartError::None); - }); - } - } - } - /// Called by the keyboard_console capsule to feed a keystroke - /// Safe to call from deferred context only - pub fn inject_input_byte(&self, b: u8) { - self.fifo_push(b); - // Defer delivery into the active RX buffer - self.deferred_call.set(); - } -} - -// DeferredCallClient implementation -impl DeferredCallClient for VgaText<'_> { - fn handle_deferred_call(&self) { - if let Some(buf) = self.pending_buf.take() { - let len = self.pending_len_consumed.get(); - self.fire_tx_callback(buf, len); - } - self.pump_rx(); - } - - 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_consumed.set(write_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])> { - // Only one receive allowed - let busy = { - let mut present = false; - self.rx_buf.map(|_| { - present = true; - }); - present - }; - if busy { - 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 place immediately from FIFO - self.deferred_call.set(); - Ok(()) - } - - 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(()) - } -} From 29558bd707aa5be282cca739c29490be47661e79 Mon Sep 17 00:00:00 2001 From: domnudragota Date: Tue, 16 Sep 2025 15:11:35 +0300 Subject: [PATCH 14/16] feature: gate VGA to text mode via sealed capability --- chips/x86_q35/src/vga.rs | 87 +++++++++++------------------ chips/x86_q35/src/vga_textscreen.rs | 28 +++++----- 2 files changed, 48 insertions(+), 67 deletions(-) diff --git a/chips/x86_q35/src/vga.rs b/chips/x86_q35/src/vga.rs index edb11aed6f..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 @@ -147,8 +155,6 @@ const TEXT_BUFFER_ADDR: usize = 0xB8000; // Buffer dimensions pub(crate) const TEXT_BUFFER_WIDTH: usize = 80; pub(crate) const TEXT_BUFFER_HEIGHT: usize = 25; -/// Physical address where QEMU exposes the linear-frame-buffer BAR. -const LFB_PHYS_BASE: u32 = 0xE0_00_0000; 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. @@ -321,8 +305,9 @@ pub struct Vga { // 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), @@ -331,6 +316,7 @@ impl Vga { attr: Cell::new(ColorCode::new(Color::LightGray, Color::Black, false).as_u8()), utf8_rem: Cell::new(0), utf8_acc: Cell::new(0), + _mode: PhantomData, } } @@ -571,14 +557,6 @@ impl Vga { 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)> { @@ -588,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 @@ -596,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 index 86266f296a..24a9d03137 100644 --- a/chips/x86_q35/src/vga_textscreen.rs +++ b/chips/x86_q35/src/vga_textscreen.rs @@ -10,15 +10,16 @@ //! //! This file intentionally contains **no** UART concepts. It is purely a TextScreen -use crate::vga::{Vga, TEXT_BUFFER_HEIGHT, TEXT_BUFFER_WIDTH}; +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; -pub struct VgaTextScreen<'a> { - vga: Vga, +// 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() @@ -28,10 +29,10 @@ pub struct VgaTextScreen<'a> { busy: Cell, } -impl VgaTextScreen<'_> { +impl VgaTextScreenImpl<'_, M> { pub fn new() -> Self { Self { - vga: Vga::new(), + vga: Vga::::new(), client: OptionalCell::empty(), dcall: DeferredCall::new(), tx_buf: TakeCell::empty(), @@ -44,14 +45,9 @@ impl VgaTextScreen<'_> { pub fn clear_screen(&self) { self.vga.clear(); } - - // Keep the board wiring simple - pub fn register_deferred_call(&'static self) { - self.dcall.register(self); - } } -impl<'a> HilTextScreen<'a> for VgaTextScreen<'a> { +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), @@ -89,8 +85,9 @@ impl<'a> HilTextScreen<'a> for VgaTextScreen<'a> { Ok(()) } - fn set_cursor(&self, _x: usize, _y: usize) -> Result<(), ErrorCode> { - // Minimal first pass: acknowledge immediately. + 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(()) } @@ -132,7 +129,7 @@ impl<'a> HilTextScreen<'a> for VgaTextScreen<'a> { } } -impl DeferredCallClient for VgaTextScreen<'_> { +impl DeferredCallClient for VgaTextScreenImpl<'_, M> { fn handle_deferred_call(&self) { if let Some(buf) = self.tx_buf.take() { let len = self.tx_len.get(); @@ -145,3 +142,6 @@ impl DeferredCallClient for VgaTextScreen<'_> { self.dcall.register(self); } } + +/// Public, concrete type the board uses: text-only screen. +pub type VgaTextScreen<'a> = VgaTextScreenImpl<'a, TextMode>; From 5c74b7a27fea341aa98e371ff7162f931f7c2d6f Mon Sep 17 00:00:00 2001 From: domnudragota Date: Wed, 17 Sep 2025 15:49:46 +0300 Subject: [PATCH 15/16] fix: fix old remnants from rebase --- boards/qemu_i486_q35/src/main.rs | 4 ++-- chips/x86_q35/src/chip.rs | 9 --------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/boards/qemu_i486_q35/src/main.rs b/boards/qemu_i486_q35/src/main.rs index 536bdcaf17..689a927d54 100644 --- a/boards/qemu_i486_q35/src/main.rs +++ b/boards/qemu_i486_q35/src/main.rs @@ -221,7 +221,7 @@ unsafe extern "cdecl" fn main() { .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. @@ -275,7 +275,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/chips/x86_q35/src/chip.rs b/chips/x86_q35/src/chip.rs index 992fbd4c3c..273264d965 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; @@ -307,7 +299,6 @@ impl Component for PcComponent<'static, I> { com3, com4, pit, - vga, ps2, keyboard, syscall, From 3163314163b6a06f91583b8dbd4d6b7fa0050b58 Mon Sep 17 00:00:00 2001 From: domnudragota Date: Thu, 18 Sep 2025 14:05:41 +0300 Subject: [PATCH 16/16] feature: PS/2 split + Keyboard HIL; TextScreen-backed UART; VGA wiring --- boards/qemu_i486_q35/src/main.rs | 25 +---- capsules/core/src/text_screen_uart.rs | 140 ++++++++++++++++++++++++-- chips/x86_q35/src/keyboard.rs | 14 ++- chips/x86_q35/src/ps2.rs | 30 ------ 4 files changed, 149 insertions(+), 60 deletions(-) diff --git a/boards/qemu_i486_q35/src/main.rs b/boards/qemu_i486_q35/src/main.rs index 689a927d54..4293af0115 100644 --- a/boards/qemu_i486_q35/src/main.rs +++ b/boards/qemu_i486_q35/src/main.rs @@ -21,7 +21,7 @@ use kernel::component::Component; use kernel::debug; use kernel::deferred_call::DeferredCallClient; use kernel::hil; -use kernel::hil::text_screen::TextScreen as HilTextScreen; +use kernel::hil::keyboard::Keyboard; use kernel::ipc::IPC; use kernel::platform::chip::Chip; use kernel::platform::scheduler_timer::VirtualSchedulerTimer; @@ -32,7 +32,6 @@ use kernel::syscall::SyscallDriver; use kernel::{create_capability, static_init}; use x86::registers::bits32::paging::{PDEntry, PTEntry, PD, PT}; use x86::registers::irq; -use x86_q35::keyboard::AsciiClient; use x86_q35::pit::{Pit, RELOAD_1KHZ}; use x86_q35::vga_textscreen::VgaTextScreen; use x86_q35::{Pc, PcComponent}; @@ -97,17 +96,6 @@ pub struct QemuI386Q35Platform { &'static VirtualSchedulerTimer>>, } -struct KbdToUart<'a, S: HilTextScreen<'a>> { - uart: &'a TextConsoleUart<'a, S>, -} - -impl<'a, S: HilTextScreen<'a>> AsciiClient for KbdToUart<'a, S> { - #[inline] - fn put_byte(&self, b: u8) { - self.uart.inject_byte(b); - } -} - impl SyscallDriverLookup for QemuI386Q35Platform { fn with_driver(&self, driver_num: usize, f: F) -> R where @@ -203,14 +191,9 @@ unsafe extern "cdecl" fn main() { TextConsoleUart::new(vga_screen) ); - let kbd_to_uart = static_init!( - KbdToUart<'static, VgaTextScreen<'static>>, - KbdToUart { - uart: vga_text_uart - } - ); - - chip.keyboard.set_ascii_client(kbd_to_uart); + // 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(); diff --git a/capsules/core/src/text_screen_uart.rs b/capsules/core/src/text_screen_uart.rs index 4c6cd457e0..fbb7b0b15a 100644 --- a/capsules/core/src/text_screen_uart.rs +++ b/capsules/core/src/text_screen_uart.rs @@ -8,10 +8,9 @@ //! `TextScreenClient::write_complete()`. RX: producers call `inject_byte()`; //! bytes are buffered and completes on `\n` or when full. -// ONLY FOR NOW, WILL BE REMOVED LATER -#[allow(dead_code)] 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}; @@ -28,6 +27,10 @@ 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>, @@ -43,8 +46,6 @@ pub struct TextConsoleUart<'a, S: HilTextScreen<'a>> { // Type-ahead FIFO (drop-oldest on overflow rx_fifo: RefCell<[u8; RX_FIFO_SIZE]>, - // ONLY FOR NOW, WILL BE REMOVED LATER - #[allow(dead_code)] rx_head: Cell, rx_tail: Cell, rx_count: Cell, @@ -58,12 +59,13 @@ pub struct TextConsoleUart<'a, S: HilTextScreen<'a>> { skip_next_lf: Cell, // if we just wrote '\n' because we saw '\r' } -// ONLY FOR NOW, WILL BE REMOVED LATER -#[allow(dead_code)] 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(), @@ -84,13 +86,129 @@ impl<'a, S: HilTextScreen<'a>> TextConsoleUart<'a, S> { } } + #[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 (from keyboard glue). Safe from deferred context + /// Feed one input byte. Safe from deferred context pub fn inject_byte(&self, b: u8) { self.fifo_push(b); self.dcall.set(); @@ -285,3 +403,11 @@ impl<'a, S: HilTextScreen<'a>> Configure for TextConsoleUart<'a, S> { 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/keyboard.rs b/chips/x86_q35/src/keyboard.rs index 7393925efa..b6e98b4c5c 100644 --- a/chips/x86_q35/src/keyboard.rs +++ b/chips/x86_q35/src/keyboard.rs @@ -404,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/ps2.rs b/chips/x86_q35/src/ps2.rs index 5faf4a71d4..662abaa333 100644 --- a/chips/x86_q35/src/ps2.rs +++ b/chips/x86_q35/src/ps2.rs @@ -293,36 +293,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) -> Ps2Result<()> { - 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).