x86_q35/
vga.rs

1// Licensed under the Apache License, Version 2.0 or the MIT License.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3// Copyright Tock Contributors 2025.
4
5//! Minimal VGA peripheral implementation for the Tock x86_q35 chip crate.
6//!
7//! Supports classic 80×25 text mode out-of-the-box and exposes a stub for
8//! setting planar 16-colour graphics modes (640×480 and 800×600).  These
9//! extra modes will be filled in later once the driver is integrated with a
10//! future framebuffer capsule.
11//!
12//!
13//! NOTE!!!
14//!
15//! This file compiles and provides working text-
16//! mode console support so the board can swap from the UART mux to a VGA
17//! console.  Graphical modes are *disabled at runtime* until a framebuffer
18//! capsule implementation lands.  The low-level register writes for 640×480 and 800×600 are
19//! nonetheless laid out so they can be enabled by flipping a constant.
20//!
21//! VGA peripheral driver for the x86_q35 chip.
22//!
23//! The driver currently focuses on **text mode** (colour attribute buffer at
24//! 0xB8000).  It also defines [`VgaMode`] and an [`init`] routine that writes
25//! the necessary CRT controller registers for text mode and two common planar
26//! 16-colour modes.  Other code (e.g. the board crate) can query the selected
27//! mode via `kernel::config::CONFIG.vga_mode` and decide whether to route the
28//! `ProcessConsole` to this driver or to the legacy serial mux.
29
30use core::cell::Cell;
31use kernel::utilities::StaticRef;
32use tock_cells::volatile_cell::VolatileCell;
33
34/// Write an 8-bit value to an I/O Port.
35/// Read an 8-bit value from an I/O port.
36use x86::registers::io::{inb, outb};
37
38// 16 classic VGA colors (matches text-mode palette indices 0–15)
39#[repr(u8)]
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub enum Color {
42    Black = 0x0,
43    Blue = 0x1,
44    Green = 0x2,
45    Cyan = 0x3,
46    Red = 0x4,
47    Magenta = 0x5,
48    Brown = 0x6,
49    LightGray = 0x7,
50    DarkGray = 0x8,
51    LightBlue = 0x9,
52    LightGreen = 0xA,
53    LightCyan = 0xB,
54    LightRed = 0xC,
55    Pink = 0xD,
56    Yellow = 0xE,
57    White = 0xF,
58}
59
60/// Packed VGA attribute byte for text mode:
61/// bits 0–3 = foreground color; 4–6 = background color; 7 = blink/bright (mode-dependent).
62#[repr(transparent)]
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub struct ColorCode(u8);
65
66impl ColorCode {
67    /// Build a color code from fg/bg/blink.
68    /// Note: with Attribute Controller mode bit 7 = blink enabled, bit 7 here blinks.
69    /// If blink is disabled in the controller, bit 7 acts as "bright background".
70    pub const fn new(fg: Color, bg: Color, blink: bool) -> Self {
71        let mut b = (fg as u8) & 0x0F;
72        b |= ((bg as u8) & 0x07) << 4;
73        if blink {
74            b |= 1 << 7;
75        }
76        Self(b)
77    }
78
79    /// Raw byte as written to the high byte of the cell.
80    #[inline(always)]
81    pub const fn as_u8(self) -> u8 {
82        self.0
83    }
84
85    /// Construct directly from a packed byte (for interop/tests).
86    #[inline(always)]
87    pub const fn from_u8(b: u8) -> Self {
88        Self(b)
89    }
90
91    /// Extractors (handy for debugging/tools)
92    #[inline(always)]
93    pub const fn fg(self) -> Color {
94        match self.0 & 0x0F {
95            0x0 => Color::Black,
96            0x1 => Color::Blue,
97            0x2 => Color::Green,
98            0x3 => Color::Cyan,
99            0x4 => Color::Red,
100            0x5 => Color::Magenta,
101            0x6 => Color::Brown,
102            0x7 => Color::LightGray,
103            0x8 => Color::DarkGray,
104            0x9 => Color::LightBlue,
105            0xA => Color::LightGreen,
106            0xB => Color::LightCyan,
107            0xC => Color::LightRed,
108            0xD => Color::Pink,
109            0xE => Color::Yellow,
110            _ => Color::White, // 0xF
111        }
112    }
113    /// Note: background uses only 3 bits (0..=7). Bright backgrounds require
114    /// disabling blink in the Attribute Controller and repurposing bit 7.
115    #[inline(always)]
116    pub const fn bg(self) -> Color {
117        match (self.0 >> 4) & 0x07 {
118            0x0 => Color::Black,
119            0x1 => Color::Blue,
120            0x2 => Color::Green,
121            0x3 => Color::Cyan,
122            0x4 => Color::Red,
123            0x5 => Color::Magenta,
124            0x6 => Color::Brown,
125            _ => Color::LightGray, // 0x7
126        }
127    }
128    pub const fn blink(self) -> bool {
129        (self.0 & 0x80) != 0
130    }
131}
132
133/// All VGA modes supported by the x86_q35 chip crate.
134#[non_exhaustive]
135#[derive(Clone, Copy, Debug, PartialEq, Eq)]
136pub enum VgaMode {
137    Text80x25,
138    Graphics640x480_16,
139    Graphics800x600_16,
140}
141
142// Constants for memory-mapped text mode buffer
143
144// VGA physical Address
145
146const TEXT_BUFFER_ADDR: usize = 0xB8000;
147// Buffer dimensions
148const TEXT_BUFFER_WIDTH: usize = 80;
149const TEXT_BUFFER_HEIGHT: usize = 25;
150/// Physical address where QEMU exposes the linear-frame-buffer BAR.
151const LFB_PHYS_BASE: u32 = 0xE0_00_0000;
152
153const VGA_CELLS: StaticRef<[VolatileCell<u16>; TEXT_BUFFER_WIDTH * TEXT_BUFFER_HEIGHT]> =
154    unsafe { StaticRef::new(TEXT_BUFFER_ADDR as *const _) };
155
156/// `TextBuf` is a thin, zero-cost view over the 80×25 VGA text buffer at 0xB8000.
157/// It wraps the `StaticRef<[VolatileCell<u16>; N]>` so code can:
158/// - iterate all cells (`iter()`) and by rows (`rows()`),
159/// - use bracket indexing `TEXT[(row, col)]` (panics on OOB in debug),
160/// - or use safe lookup `get(row, col) -> Option<&VolatileCell<u16>>`.
161///
162/// All accesses go through `VolatileCell`, so reads/writes are *volatile*.
163struct TextBuf {
164    cells: StaticRef<[VolatileCell<u16>; TEXT_BUFFER_WIDTH * TEXT_BUFFER_HEIGHT]>,
165}
166
167impl TextBuf {
168    #[inline(always)]
169    const fn new() -> Self {
170        Self { cells: VGA_CELLS }
171    }
172
173    /// Iterate all cells (row-major).
174    #[inline(always)]
175    fn iter(&self) -> core::slice::Iter<'_, VolatileCell<u16>> {
176        self.cells[..].iter()
177    }
178
179    /// Iterate rows as fixed-size chunks.
180    #[inline(always)]
181    fn rows(&self) -> core::slice::ChunksExact<'_, VolatileCell<u16>> {
182        self.cells[..].chunks_exact(TEXT_BUFFER_WIDTH)
183    }
184
185    /// Expose an Option for row, col for the user
186    #[inline(always)]
187    pub fn get(&self, row: usize, col: usize) -> Option<&VolatileCell<u16>> {
188        if row < TEXT_BUFFER_HEIGHT && col < TEXT_BUFFER_WIDTH {
189            Some(&self.cells[row * TEXT_BUFFER_WIDTH + col])
190        } else {
191            None
192        }
193    }
194}
195impl<'a> IntoIterator for &'a TextBuf {
196    type Item = &'a VolatileCell<u16>;
197    type IntoIter = core::slice::Iter<'a, VolatileCell<u16>>;
198    #[inline(always)]
199    fn into_iter(self) -> Self::IntoIter {
200        self.iter()
201    }
202}
203
204impl core::ops::Index<usize> for TextBuf {
205    type Output = VolatileCell<u16>;
206    #[inline(always)]
207    fn index(&self, i: usize) -> &Self::Output {
208        &self.cells[i]
209    }
210}
211
212impl core::ops::Index<(usize, usize)> for TextBuf {
213    type Output = VolatileCell<u16>;
214    #[inline(always)]
215    fn index(&self, (row, col): (usize, usize)) -> &Self::Output {
216        &self.cells[row * TEXT_BUFFER_WIDTH + col] // panics on OOB
217    }
218}
219
220/// Low-level VGA controller (global mode/programming).
221/// This configures the hardware; it is not tied to a particular `Vga` writer.
222pub struct VgaDevice;
223
224impl VgaDevice {
225    /// Global, row-major, bracket-indexable view.
226    const TEXT: TextBuf = TextBuf::new();
227    /// Program the requested mode on the VGA controller.
228    pub fn set_mode(mode: VgaMode) {
229        match mode {
230            VgaMode::Text80x25 => Self::program_text_mode(),
231            VgaMode::Graphics640x480_16 => panic!("VGA 640×480 mode not implemented"),
232            VgaMode::Graphics800x600_16 => panic!("VGA 800×600 mode not implemented"),
233        }
234    }
235
236    /// Only needed for graphics modes (linear framebuffer @ LFB_PHYS_BASE).
237    pub fn map_for_mode(mode: VgaMode, page_dir: &mut x86::registers::bits32::paging::PD) {
238        use x86::registers::bits32::paging::{PAddr, PDEntry, PDFlags, PDFLAGS};
239
240        if matches!(
241            mode,
242            VgaMode::Graphics640x480_16 | VgaMode::Graphics800x600_16
243        ) {
244            let pde_idx = (LFB_PHYS_BASE >> 22) as usize;
245            let pa = PAddr::from(LFB_PHYS_BASE);
246            let mut flags = PDFlags::new(0);
247            flags.write(PDFLAGS::P::SET + PDFLAGS::RW::SET + PDFLAGS::PS::SET);
248            page_dir[pde_idx] = PDEntry::new(pa, flags);
249        }
250    }
251
252    // --- private ---
253
254    fn program_text_mode() {
255        // (content moved verbatim from old `init_text_mode`)
256        unsafe {
257            // Select CRTC register index 0x11 (cursor start register) and reset its value to 0
258            outb(0x3D4, 0x11);
259            outb(0x3D5, 0x00);
260
261            // Read the Attribute Controller’s status register to reset its internal flip-flop
262            inb(0x3DA);
263        }
264
265        // Program the 21 Attribute Controller registers:
266        //   0x00–0x0F are the 16 palette entries,
267        //   0x10 = mode control (graphics off, blink on),
268        //   0x12 = color plane enable mask.
269        for (idx, val) in [
270            (0x00, 0x00u8), // palette 0: black
271            (0x01, 0x01),   // palette 1: blue
272            (0x02, 0x02),   // palette 2: green
273            (0x03, 0x03),   // palette 3: cyan
274            (0x04, 0x04),   // palette 4: red
275            (0x05, 0x05),   // palette 5: magenta
276            (0x06, 0x14),   // palette 6: brown
277            (0x07, 0x07),   // palette 7: light grey
278            (0x08, 0x38),   // palette 8: dark grey
279            (0x09, 0x39),   // palette 9: light blue
280            (0x0A, 0x3A),   // palette A: light green
281            (0x0B, 0x3B),   // palette B: light cyan
282            (0x0C, 0x3C),   // palette C: light red
283            (0x0D, 0x3D),   // palette D: light magenta
284            (0x0E, 0x3E),   // palette E: yellow
285            (0x0F, 0x3F),   // palette F: white
286            (0x10, 0x0C),   // mode control: text mode, blink attribute on
287            (0x12, 0x0F),   // enable all 4 color planes
288        ]
289        .iter()
290        .copied()
291        {
292            unsafe {
293                // Write the register index to the Attribute Controller
294                outb(0x3C0, idx);
295                // Write the corresponding value
296                outb(0x3C0, val);
297            }
298        }
299
300        // Reset the flip-flop again before enabling video output
301        unsafe {
302            inb(0x3DA);
303
304            // Turn video output back on (set bit 5 of the Attribute Controller’s 0x20 register)
305            outb(0x3C0, 0x20);
306        }
307    }
308}
309
310// Public API - the VGA struct providing text console implementation
311
312/// Simple text-mode VGA console.
313pub struct Vga {
314    col: Cell<usize>,
315    row: Cell<usize>,
316    /// Current VGA text attribute byte for newly written characters.
317    /// Layout (text mode):
318    /// bits 0–3 = fg (0–15), 4–6 = bg (0–7), 7 = blink/bright (mode-dependent).
319    /// Kept packed to match hardware and allow a single 16-bit volatile store per glyph.
320    attr: Cell<u8>,
321}
322impl Vga {
323    pub const fn new() -> Self {
324        Self {
325            col: Cell::new(0),
326            row: Cell::new(0),
327            // default: LightGray on Black, no blink
328            attr: Cell::new(ColorCode::new(Color::LightGray, Color::Black, false).as_u8()),
329        }
330    }
331
332    fn update_hw_cursor(&self) {
333        let pos = (self.row.get() * TEXT_BUFFER_WIDTH + self.col.get()) as u16;
334        unsafe {
335            outb(0x3D4, 0x0F);
336            outb(0x3D5, (pos & 0xFF) as u8);
337            outb(0x3D4, 0x0E);
338            outb(0x3D5, (pos >> 8) as u8);
339        }
340    }
341
342    fn scroll_up(&self) {
343        let blank = ((self.attr.get() as u16) << 8) | b' ' as u16;
344
345        // move rows 1..H up by one
346        let rows = VgaDevice::TEXT.rows();
347        let src_rows = rows.clone().skip(1);
348        let dst_rows = rows;
349        for (src_row, dst_row) in src_rows.zip(dst_rows) {
350            for (src, dst) in src_row.iter().zip(dst_row.iter()) {
351                dst.set(src.get()); // volatile read + write
352            }
353        }
354
355        // clear last row
356        if let Some(last_row) = VgaDevice::TEXT.rows().last() {
357            for cell in last_row {
358                cell.set(blank);
359            }
360        }
361
362        self.row.set(TEXT_BUFFER_HEIGHT - 1);
363        self.col.set(0);
364    }
365
366    pub fn set_cursor(&self, col: usize, row: usize) {
367        if VgaDevice::TEXT.get(row, col).is_some() {
368            self.col.set(col);
369            self.row.set(row);
370            self.update_hw_cursor();
371        }
372    }
373
374    /// Set the current attribute from a typed ColorCode.
375    #[inline(always)]
376    pub fn set_color_code(&self, code: ColorCode) {
377        self.attr.set(code.as_u8());
378    }
379
380    /// Set fg/bg/blink with typed colors.
381    #[inline(always)]
382    pub fn set_colors(&self, fg: Color, bg: Color, blink: bool) {
383        self.set_color_code(ColorCode::new(fg, bg, blink));
384    }
385
386    /// Read back the current color code (typed).
387    #[inline(always)]
388    pub fn color_code(&self) -> ColorCode {
389        ColorCode::from_u8(self.attr.get())
390    }
391
392    pub fn clear(&self) {
393        let blank = ((self.attr.get() as u16) << 8) | b' ' as u16;
394        for cell in &VgaDevice::TEXT {
395            cell.set(blank);
396        }
397        self.col.set(0);
398        self.row.set(0);
399        self.update_hw_cursor();
400    }
401
402    pub fn write_byte(&self, byte: u8) {
403        match byte {
404            b'\n' => {
405                self.col.set(0);
406                self.row.set(self.row.get() + 1);
407            }
408            b'\r' => {
409                self.col.set(0);
410            }
411            b => {
412                let val = ((self.attr.get() as u16) << 8) | b as u16;
413
414                // safe write; if somehow OOB, scroll and retry once
415                if let Some(cell) = VgaDevice::TEXT.get(self.row.get(), self.col.get()) {
416                    cell.set(val);
417                } else {
418                    self.scroll_up();
419                    if let Some(cell) = VgaDevice::TEXT.get(self.row.get(), self.col.get()) {
420                        cell.set(val);
421                    }
422                }
423
424                // advance cursor, wrap at end-of-line
425                let mut col = self.col.get() + 1;
426                let mut row = self.row.get();
427                if col >= TEXT_BUFFER_WIDTH {
428                    col = 0;
429                    row += 1;
430                }
431                self.col.set(col);
432                self.row.set(row);
433            }
434        }
435
436        // scroll if we ran off the last row (covers '\n' path too)
437        if self.row.get() >= TEXT_BUFFER_HEIGHT {
438            self.scroll_up();
439        }
440
441        self.update_hw_cursor();
442    }
443}
444
445const _: () = {
446    // Exhaustively touch every current VgaMode variant
447    match VgaMode::Text80x25 {
448        VgaMode::Text80x25 => (),
449        VgaMode::Graphics640x480_16 => (),
450        VgaMode::Graphics800x600_16 => (),
451    }
452};
453
454// stub for future graphic options implementation
455pub fn framebuffer() -> Option<(*mut u8, usize)> {
456    None
457}
458
459/// Initialise 80×25 text mode and start with a clean screen.
460pub(crate) fn new_text_console(_page_dir_ptr: &mut x86::registers::bits32::paging::PD) {
461    // Program 80×25 text mode
462    VgaDevice::set_mode(VgaMode::Text80x25);
463
464    // Wipe the BIOS banner so the kernel starts on a blank page.
465    let blank: u16 = 0x0720; // white-on-black space
466    for cell in &VgaDevice::TEXT {
467        cell.set(blank);
468    }
469}