diff --git a/gateware/src/rs/hal/src/dma_display.rs b/gateware/src/rs/hal/src/dma_display.rs deleted file mode 100644 index f27eb72e..00000000 --- a/gateware/src/rs/hal/src/dma_display.rs +++ /dev/null @@ -1,48 +0,0 @@ -#[macro_export] -macro_rules! impl_dma_display { - ($( - $DMA_DISPLAYX:ident, $H_ACTIVE:expr, $V_ACTIVE:expr, $VIDEO_ROTATE_90: expr - )+) => { - $( - struct $DMA_DISPLAYX { - framebuffer_base: *mut u32, - } - - impl OriginDimensions for $DMA_DISPLAYX { - fn size(&self) -> Size { - Size::new($H_ACTIVE, $V_ACTIVE) - } - } - - impl DrawTarget for $DMA_DISPLAYX { - type Color = Gray8; - type Error = core::convert::Infallible; - fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> - where - I: IntoIterator>, - { - let xs: u32 = $H_ACTIVE; - let ys: u32 = $V_ACTIVE; - for Pixel(coord, color) in pixels.into_iter() { - if let Ok((x @ 0..=$H_ACTIVE, - y @ 0..=$V_ACTIVE)) = coord.try_into() { - let xf = if $VIDEO_ROTATE_90 {$V_ACTIVE - y} else {x}; - let yf = if $VIDEO_ROTATE_90 {x} else {y}; - // Calculate the index in the framebuffer. - let index: u32 = (xf + yf * $H_ACTIVE) / 4; - unsafe { - // TODO: support anything other than Gray8 - let mut px = self.framebuffer_base.offset( - index as isize).read_volatile(); - px &= !(0xFFu32 << (8*(xf%4))); - self.framebuffer_base.offset(index as isize).write_volatile( - px | ((color.luma() as u32) << (8*(xf%4)))); - } - } - } - Ok(()) - } - } - )+ - } -} diff --git a/gateware/src/rs/hal/src/dma_framebuffer.rs b/gateware/src/rs/hal/src/dma_framebuffer.rs new file mode 100644 index 00000000..7f49fa1f --- /dev/null +++ b/gateware/src/rs/hal/src/dma_framebuffer.rs @@ -0,0 +1,138 @@ +pub struct DVIModeline { + pub h_active: u16, + pub h_sync_start: u16, + pub h_sync_end: u16, + pub h_total: u16, + pub h_sync_invert: bool, + pub v_active: u16, + pub v_sync_start: u16, + pub v_sync_end: u16, + pub v_total: u16, + pub v_sync_invert: bool, + pub pixel_clk_mhz: f32, +} + +pub trait DMAFramebuffer { + fn update_fb_base(&mut self, fb_base: u32); + fn set_palette_rgb(&mut self, intensity: u8, hue: u8, r: u8, g: u8, b: u8); +} + +#[macro_export] +macro_rules! impl_dma_framebuffer { + ($( + $DMA_FRAMEBUFFERX:ident: $PACFRAMEBUFFERX:ty, + )+) => { + $( + use tiliqua_hal::dma_framebuffer::DVIModeline; + + struct $DMA_FRAMEBUFFERX { + registers: $PACFRAMEBUFFERX, + mode: DVIModeline, + framebuffer_base: *mut u32, + rotate_90: bool, + } + + impl $DMA_FRAMEBUFFERX { + fn new(registers: $PACFRAMEBUFFERX, fb_base: usize, + mode: DVIModeline, rotate_90: bool) -> Self { + registers.flags().write(|w| unsafe { + w.enable().bit(false) + }); + registers.fb_base().write(|w| unsafe { + w.fb_base().bits(fb_base as u32) + }); + registers.h_timing().write(|w| unsafe { + w.h_active().bits(mode.h_active); + w.h_sync_start().bits(mode.h_sync_start) + } ); + registers.h_timing2().write(|w| unsafe { + w.h_sync_end().bits(mode.h_sync_end); + w.h_total().bits(mode.h_total) + } ); + registers.v_timing().write(|w| unsafe { + w.v_active().bits(mode.v_active); + w.v_sync_start().bits(mode.v_sync_start) + } ); + registers.v_timing2().write(|w| unsafe { + w.v_sync_end().bits(mode.v_sync_end); + w.v_total().bits(mode.v_total) + } ); + registers.hv_timing().write(|w| unsafe { + w.h_sync_invert().bit(mode.h_sync_invert); + w.v_sync_invert().bit(mode.h_sync_invert); + w.active_pixels().bits( + mode.h_active as u32 * mode.v_active as u32) + } ); + registers.flags().write(|w| unsafe { + w.enable().bit(true) + }); + Self { + registers, + mode, + framebuffer_base: fb_base as *mut u32, + rotate_90 + } + } + + } + + impl hal::dma_framebuffer::DMAFramebuffer for $DMA_FRAMEBUFFERX { + fn update_fb_base(&mut self, fb_base: u32) { + self.registers.fb_base().write(|w| unsafe { + w.fb_base().bits(fb_base) + }); + self.framebuffer_base = fb_base as *mut u32 + } + + fn set_palette_rgb(&mut self, intensity: u8, hue: u8, r: u8, g: u8, b: u8) { + /* wait until last coefficient written */ + while self.registers.palette_busy().read().bits() == 1 { } + self.registers.palette().write(|w| unsafe { + w.position().bits(((intensity&0xF) << 4) | (hue&0xF)); + w.red() .bits(r); + w.green() .bits(g); + w.blue() .bits(b) + } ); + } + } + + impl OriginDimensions for $DMA_FRAMEBUFFERX { + fn size(&self) -> Size { + Size::new(self.mode.h_active as u32, + self.mode.v_active as u32) + } + } + + impl DrawTarget for $DMA_FRAMEBUFFERX { + type Color = Gray8; + type Error = core::convert::Infallible; + fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> + where + I: IntoIterator>, + { + let h_active = self.size().width; + let v_active = self.size().height; + for Pixel(coord, color) in pixels.into_iter() { + if let Ok((x, y)) = coord.try_into() { + if x >= 0 && x < h_active && y >= 0 && y < v_active { + let xf: u32 = if self.rotate_90 {v_active - y} else {x}; + let yf: u32 = if self.rotate_90 {x} else {y}; + // Calculate the index in the framebuffer. + let index: u32 = (xf + yf * h_active) / 4; + unsafe { + // TODO: support anything other than Gray8 + let mut px = self.framebuffer_base.offset( + index as isize).read_volatile(); + px &= !(0xFFu32 << (8*(xf%4))); + self.framebuffer_base.offset(index as isize).write_volatile( + px | ((color.luma() as u32) << (8*(xf%4)))); + } + } + } + } + Ok(()) + } + } + )+ + } +} diff --git a/gateware/src/rs/hal/src/lib.rs b/gateware/src/rs/hal/src/lib.rs index 1e67a948..f84f8989 100644 --- a/gateware/src/rs/hal/src/lib.rs +++ b/gateware/src/rs/hal/src/lib.rs @@ -7,7 +7,7 @@ extern crate std; // modules -pub mod dma_display; +pub mod dma_framebuffer; pub mod encoder; pub mod i2c; pub mod pca9635; diff --git a/gateware/src/rs/hal/src/video.rs b/gateware/src/rs/hal/src/video.rs index 8063cdee..c31f23e3 100644 --- a/gateware/src/rs/hal/src/video.rs +++ b/gateware/src/rs/hal/src/video.rs @@ -1,5 +1,4 @@ pub trait Video { - fn set_palette_rgb(&mut self, intensity: u8, hue: u8, r: u8, g: u8, b: u8); fn set_persist(&mut self, value: u16); fn set_decay(&mut self, value: u8); } @@ -22,17 +21,6 @@ macro_rules! impl_video { } impl hal::video::Video for $VIDEOX { - fn set_palette_rgb(&mut self, intensity: u8, hue: u8, r: u8, g: u8, b: u8) { - /* wait until last coefficient written */ - while self.registers.palette_busy().read().bits() == 1 { } - self.registers.palette().write(|w| unsafe { - w.position().bits(((intensity&0xF) << 4) | (hue&0xF)); - w.red() .bits(r); - w.green() .bits(g); - w.blue() .bits(b) - } ); - } - fn set_persist(&mut self, value: u16) { self.registers.persist().write(|w| unsafe { w.persist().bits(value) } ); } diff --git a/gateware/src/rs/lib/src/edid.rs b/gateware/src/rs/lib/src/edid.rs new file mode 100644 index 00000000..7cd8dc3f --- /dev/null +++ b/gateware/src/rs/lib/src/edid.rs @@ -0,0 +1,294 @@ +/// Tiny EDID parser, only handles the header and detailed timing descriptor. +/// Does not handle extension blocks. This should be enough for most small embedded monitors. + +/// Main EDID structure representing the first 128 bytes of an EDID block +#[derive(Debug)] +pub struct Edid { + // Header (bytes 0-19) + pub header: EdidHeader, + // Detailed timing descriptors (bytes 54-125) + pub descriptors: [Descriptor; 4], + // Extension flag (byte 126) + pub extensions: u8, + // Checksum (byte 127) + pub checksum: u8, +} + +/// EDID Header information (bytes 0-19) +#[derive(Debug)] +pub struct EdidHeader { + // Fixed header pattern (bytes 0-7) + pub pattern: [u8; 8], + // Manufacturer ID (bytes 8-9) + pub manufacturer_id: [u8; 2], + // Manufacturer product code (bytes 10-11) + pub product_code: u16, + // Serial number (bytes 12-15) + pub serial_number: u32, + // Week of manufacture (byte 16) + pub manufacture_week: u8, + // Year of manufacture (byte 17) + pub manufacture_year: u8, + // EDID version & revision (bytes 18-19) + pub version: u8, + pub revision: u8, +} + +/// Detailed timing descriptors (18 bytes each) +/// For simplicity, we're just storing the raw data for now +#[derive(Debug, Copy, Clone)] +pub struct RawDescriptor { + pub data: [u8; 18], +} + +/// Descriptor types for the 18-byte descriptor blocks +#[derive(Debug, Copy, Clone)] +pub enum Descriptor { + DetailedTiming(DetailedTimingDescriptor), + RawDescriptor([u8; 18]), +} + +/// Detailed timing descriptor (used when pixel clock != 0) +#[derive(Debug, Copy, Clone)] +pub struct DetailedTimingDescriptor { + pub pixel_clock_khz: u32, // in 10 kHz units + pub horizontal_active: u16, + pub horizontal_blanking: u16, + pub vertical_active: u16, + pub vertical_blanking: u16, + pub horizontal_sync_offset: u16, + pub horizontal_sync_pulse_width: u16, + pub vertical_sync_offset: u16, + pub vertical_sync_pulse_width: u16, + pub horizontal_image_size_mm: u16, + pub vertical_image_size_mm: u16, + pub horizontal_border: u8, + pub vertical_border: u8, + pub features: TimingFeatures, +} + +/// Timing features bitmap +#[derive(Debug, Copy, Clone)] +pub struct TimingFeatures { + pub interlaced: bool, + pub stereo_mode: StereoMode, + pub sync_type: SyncType, +} + +/// Stereo mode options +#[derive(Debug, Copy, Clone)] +pub enum StereoMode { + None, + FieldSequentialRight, + FieldSequentialLeft, + InterleavedRightEven, + InterleavedLeftEven, + FourWayInterleaved, + SideBySideInterleaved, +} + +/// Sync type options +#[derive(Debug, Copy, Clone)] +pub enum SyncType { + Analog { + bipolar: bool, + serration: bool, + sync_on_rgb: bool, + }, + DigitalComposite { + serration: bool, + hsync_positive: bool, + }, + DigitalSeparate { + vsync_positive: bool, + hsync_positive: bool, + }, +} + +impl Edid { + /// Parse the EDID from raw bytes + pub fn parse(edid_data: &[u8; 128]) -> Result { + // Verify checksum + let mut checksum: u8 = 0; + for &byte in edid_data.iter() { + checksum = checksum.wrapping_add(byte); + } + if checksum != 0 { + return Err(EdidError::InvalidChecksum); + } + // Parse EDID header + let header = EdidHeader { + pattern: [ + edid_data[0], edid_data[1], edid_data[2], edid_data[3], + edid_data[4], edid_data[5], edid_data[6], edid_data[7], + ], + manufacturer_id: [edid_data[8], edid_data[9]], + product_code: u16::from_le_bytes([edid_data[10], edid_data[11]]), + serial_number: u32::from_le_bytes([ + edid_data[12], edid_data[13], edid_data[14], edid_data[15], + ]), + manufacture_week: edid_data[16], + manufacture_year: edid_data[17], + version: edid_data[18], + revision: edid_data[19], + }; + // Verify header pattern + if header.pattern != [0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00] { + return Err(EdidError::InvalidHeaderPattern); + } + let mut descriptors = [Descriptor::RawDescriptor([0; 18]); 4]; + for i in 0..4 { + let offset = 54 + i * 18; + let mut data = [0; 18]; + data.copy_from_slice(&edid_data[offset..offset + 18]); + // Parse the descriptor based on its format + descriptors[i] = Self::parse_descriptor(&data); + } + + Ok(Edid { + header, + descriptors, + extensions: edid_data[126], + checksum: edid_data[127], + }) + } + + /// Parse a descriptor block + fn parse_descriptor(data: &[u8; 18]) -> Descriptor { + // Check if it's a detailed timing descriptor (pixel clock != 0) + let pixel_clock = u16::from_le_bytes([data[0], data[1]]); + if pixel_clock != 0 { + // Detailed timing descriptor + return Descriptor::DetailedTiming(Self::parse_detailed_timing(data)); + } + // It's not a detailed timing descriptor, store raw data + Descriptor::RawDescriptor(*data) + } + + /// Parse a detailed timing descriptor + fn parse_detailed_timing(data: &[u8; 18]) -> DetailedTimingDescriptor { + let pixel_clock = u16::from_le_bytes([data[0], data[1]]) as u32 * 10; // 10 kHz units + // + // Horizontal active/blanking + let h_active = ((data[4] as u16 & 0xF0) << 4) | data[2] as u16; + let h_blanking = ((data[4] as u16 & 0x0F) << 8) | data[3] as u16; + + // Vertical active/blanking + let v_active = ((data[7] as u16 & 0xF0) << 4) | data[5] as u16; + let v_blanking = ((data[7] as u16 & 0x0F) << 8) | data[6] as u16; + + // Sync offsets and pulse widths + let h_sync_offset = ((data[11] as u16 & 0xC0) << 2) | data[8] as u16; + let h_sync_pulse_width = ((data[11] as u16 & 0x30) << 4) | data[9] as u16; + let v_sync_offset = ((data[11] as u16 & 0x0C) << 2) | ((data[10] as u16 & 0xF0) >> 4); + let v_sync_pulse_width = ((data[11] as u16 & 0x03) << 4) | (data[10] as u16 & 0x0F); + + // Image size + let h_image_size = ((data[14] as u16 & 0xF0) << 4) | data[12] as u16; + let v_image_size = ((data[14] as u16 & 0x0F) << 8) | data[13] as u16; + + // Borders + let h_border = data[15]; + let v_border = data[16]; + + // Features + let interlaced = (data[17] & 0x80) != 0; + + // Stereo mode + let stereo_bits = ((data[17] & 0x60) >> 4) | (data[17] & 0x01); + let stereo_mode = match stereo_bits { + 0x00 | 0x01 => StereoMode::None, + 0x02 => StereoMode::FieldSequentialRight, + 0x04 => StereoMode::FieldSequentialLeft, + 0x03 => StereoMode::InterleavedRightEven, + 0x05 => StereoMode::InterleavedLeftEven, + 0x06 => StereoMode::FourWayInterleaved, + 0x07 => StereoMode::SideBySideInterleaved, + _ => StereoMode::None, // Should not happen + }; + + // Sync type + let sync_type = match (data[17] >> 3) & 0x03 { + 0x00 => SyncType::Analog { + bipolar: (data[17] & 0x04) != 0, + serration: (data[17] & 0x02) != 0, + sync_on_rgb: (data[17] & 0x01) != 0, + }, + 0x02 => SyncType::DigitalComposite { + serration: (data[17] & 0x04) != 0, + hsync_positive: (data[17] & 0x02) != 0, + }, + 0x03 => SyncType::DigitalSeparate { + vsync_positive: (data[17] & 0x04) != 0, + hsync_positive: (data[17] & 0x02) != 0, + }, + _ => SyncType::Analog { + bipolar: false, + serration: false, + sync_on_rgb: false, + }, + }; + + DetailedTimingDescriptor { + pixel_clock_khz: pixel_clock, + horizontal_active: h_active, + horizontal_blanking: h_blanking, + vertical_active: v_active, + vertical_blanking: v_blanking, + horizontal_sync_offset: h_sync_offset, + horizontal_sync_pulse_width: h_sync_pulse_width, + vertical_sync_offset: v_sync_offset, + vertical_sync_pulse_width: v_sync_pulse_width, + horizontal_image_size_mm: h_image_size, + vertical_image_size_mm: v_image_size, + horizontal_border: h_border, + vertical_border: v_border, + features: TimingFeatures { + interlaced, + stereo_mode, + sync_type, + }, + } + } +} + +/// Error type for EDID parsing +#[derive(Debug)] +pub enum EdidError { + InvalidChecksum, + InvalidHeaderPattern, +} + +// A simple example of how to use the parser +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_edid_parse() { + // Example EDID data from Tiliqua screen + let edid_data = [ + 0x0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0, + 0xff, 0xff, 0x32, 0x31, 0x45, 0x6, 0x0, 0x0, + 0xc, 0x1c, 0x1, 0x3, 0x80, 0xf, 0xa, 0x78, + 0xa, 0xd, 0xc9, 0xa0, 0x57, 0x47, 0x98, 0x27, + 0x12, 0x48, 0x4c, 0x0, 0x0, 0x0, 0x1, 0xc1, + 0x1, 0x1, 0x1, 0xc1, 0x1, 0x1, 0x1, 0x1, + 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x9b, 0xe, + 0xd0, 0x64, 0x20, 0xd0, 0x28, 0x20, 0x28, 0x14, + 0x84, 0x4, 0xd0, 0xd0, 0x22, 0x0, 0x0, 0x1e, + 0x9c, 0xe, 0xd0, 0x64, 0x20, 0xd0, 0x28, 0x20, + 0x14, 0x28, 0x48, 0x1, 0x5, 0x28, 0x0, 0x20, + 0x20, 0x20, 0x0, 0x0, 0x0, 0xfa, 0x0, 0xa, + 0x20, 0x20, 0x20, 0x20, 0x2, 0x0, 0x20, 0x20, + 0x20, 0x20, 0x20, 0xa, 0x0, 0x0, 0x0, 0xfc, + 0x0, 0x5a, 0x4c, 0x37, 0x32, 0x30, 0x58, 0x37, + 0x32, 0x30, 0xa, 0x20, 0x20, 0x20, 0x1, 0x62, + ]; + + let edid = Edid::parse(&edid_data); + match edid { + Ok(data) => println!("Successfully parsed EDID: {:#?}", data), + Err(e) => panic!("Failed to parse EDID: {:?}", e), + } + } +} diff --git a/gateware/src/rs/lib/src/lib.rs b/gateware/src/rs/lib/src/lib.rs index fa34b3c2..28b44d6f 100644 --- a/gateware/src/rs/lib/src/lib.rs +++ b/gateware/src/rs/lib/src/lib.rs @@ -10,3 +10,4 @@ pub mod ui; pub mod dsp; pub mod midi; pub mod calibration; +pub mod edid; diff --git a/gateware/src/rs/lib/src/palette.rs b/gateware/src/rs/lib/src/palette.rs index 9daffd39..85e27bed 100644 --- a/gateware/src/rs/lib/src/palette.rs +++ b/gateware/src/rs/lib/src/palette.rs @@ -1,4 +1,4 @@ -use tiliqua_hal::video::Video; +use tiliqua_hal::dma_framebuffer::DMAFramebuffer; use strum_macros::{EnumIter, IntoStaticStr}; @@ -100,7 +100,7 @@ impl ColorPalette { } } - pub fn write_to_hardware(&self, video: &mut impl Video) { + pub fn write_to_hardware(&self, video: &mut impl DMAFramebuffer) { for i in 0..PX_INTENSITY_MAX { for h in 0..PX_HUE_MAX { let rgb = self.compute_color(i, h); diff --git a/gateware/src/tiliqua/dma_framebuffer.py b/gateware/src/tiliqua/dma_framebuffer.py index 6566bcf4..d4e3785d 100644 --- a/gateware/src/tiliqua/dma_framebuffer.py +++ b/gateware/src/tiliqua/dma_framebuffer.py @@ -16,7 +16,7 @@ from amaranth.utils import exact_log2 from amaranth.lib.memory import Memory -from amaranth_soc import wishbone +from amaranth_soc import wishbone, csr from tiliqua import sim, dvi, palette from tiliqua.dvi_modeline import DVIModeline @@ -75,9 +75,11 @@ def __init__(self, *, fb_base_default=0, addr_width=22, def elaborate(self, platform) -> Module: m = Module() + """ if self.fixed_modeline is not None: for member in self.timings.signature.members: m.d.comb += getattr(self.timings, member).eq(getattr(self.fixed_modeline, member)) + """ m.submodules.palette = self.palette m.submodules.fifo = fifo = AsyncFIFOBuffered( @@ -109,10 +111,16 @@ def elaborate(self, platform) -> Module: fb_size_words = (self.timings.active_pixels * self.bytes_per_pixel) // 4 + + tgen_en = Signal() + m.submodules.en_ff = FFSynchronizer( + i=tgen_en, o=dvi_tgen.enable, o_domain="dvi") # Read to FIFO in sync domain with m.FSM() as fsm: with m.State('OFF'): with m.If(self.enable): + # TODO FFsync + m.d.sync += tgen_en.eq(1) m.next = 'BURST' with m.State('BURST'): m.d.comb += [ @@ -198,3 +206,118 @@ def elaborate(self, platform) -> Module: ] return m + +class Peripheral(wiring.Component): + """ + CSR peripheral for tweaking framebuffer timing/palette parameters from an SoC. + Timing values follow the same format as DVIModeline in dvi_modeline.py. + """ + class HTimingReg(csr.Register, access="w"): + h_active: csr.Field(csr.action.W, unsigned(16)) + h_sync_start: csr.Field(csr.action.W, unsigned(16)) + + class HTimingReg2(csr.Register, access="w"): + h_sync_end: csr.Field(csr.action.W, unsigned(16)) + h_total: csr.Field(csr.action.W, unsigned(16)) + + class VTimingReg(csr.Register, access="w"): + v_active: csr.Field(csr.action.W, unsigned(16)) + v_sync_start: csr.Field(csr.action.W, unsigned(16)) + + class VTimingReg2(csr.Register, access="w"): + v_sync_end: csr.Field(csr.action.W, unsigned(16)) + v_total: csr.Field(csr.action.W, unsigned(16)) + + class HVTimingReg(csr.Register, access="w"): + h_sync_invert: csr.Field(csr.action.W, unsigned(1)) + v_sync_invert: csr.Field(csr.action.W, unsigned(1)) + active_pixels: csr.Field(csr.action.W, unsigned(30)) + + class FlagsReg(csr.Register, access="w"): + enable: csr.Field(csr.action.W, unsigned(1)) + + class PaletteReg(csr.Register, access="w"): + position: csr.Field(csr.action.W, unsigned(8)) + red: csr.Field(csr.action.W, unsigned(8)) + green: csr.Field(csr.action.W, unsigned(8)) + blue: csr.Field(csr.action.W, unsigned(8)) + + class PaletteBusyReg(csr.Register, access="r"): + busy: csr.Field(csr.action.R, unsigned(1)) + + class FBBaseReg(csr.Register, access="w"): + fb_base: csr.Field(csr.action.W, unsigned(32)) + + def __init__(self, fb): + self.fb = fb + + regs = csr.Builder(addr_width=6, data_width=8) + + self._h_timing = regs.add("h_timing", self.HTimingReg(), offset=0x00) + self._h_timing2 = regs.add("h_timing2", self.HTimingReg2(), offset=0x04) + self._v_timing = regs.add("v_timing", self.VTimingReg(), offset=0x08) + self._v_timing2 = regs.add("v_timing2", self.VTimingReg2(), offset=0x0C) + self._hv_timing = regs.add("hv_timing", self.HVTimingReg(), offset=0x10) + self._flags = regs.add("flags", self.FlagsReg(), offset=0x14) + self._palette = regs.add("palette", self.PaletteReg(), offset=0x18) + self._palette_busy = regs.add("palette_busy", self.PaletteBusyReg(), offset=0x1C) + self._fb_base = regs.add("fb_base", self.FBBaseReg(), offset=0x20) + + self._bridge = csr.Bridge(regs.as_memory_map()) + + super().__init__({ + "bus": In(csr.Signature(addr_width=regs.addr_width, data_width=regs.data_width)), + }) + + self.bus.memory_map = self._bridge.bus.memory_map + + def elaborate(self, platform) -> Module: + m = Module() + + m.submodules.bridge = self._bridge + + wiring.connect(m, wiring.flipped(self.bus), self._bridge.bus) + + with m.If(self._h_timing.element.w_stb): + m.d.sync += self.fb.timings.h_active.eq(self._h_timing.f.h_active.w_data) + m.d.sync += self.fb.timings.h_sync_start.eq(self._h_timing.f.h_sync_start.w_data) + with m.If(self._h_timing2.element.w_stb): + m.d.sync += self.fb.timings.h_sync_end.eq(self._h_timing2.f.h_sync_end.w_data) + m.d.sync += self.fb.timings.h_total.eq(self._h_timing2.f.h_total.w_data) + with m.If(self._v_timing.element.w_stb): + m.d.sync += self.fb.timings.v_active.eq(self._v_timing.f.v_active.w_data) + m.d.sync += self.fb.timings.v_sync_start.eq(self._v_timing.f.v_sync_start.w_data) + with m.If(self._v_timing2.element.w_stb): + m.d.sync += self.fb.timings.v_sync_end.eq(self._v_timing2.f.v_sync_end.w_data) + m.d.sync += self.fb.timings.v_total.eq(self._v_timing2.f.v_total.w_data) + with m.If(self._hv_timing.element.w_stb): + m.d.sync += self.fb.timings.h_sync_invert.eq(self._hv_timing.f.h_sync_invert.w_data) + m.d.sync += self.fb.timings.v_sync_invert.eq(self._hv_timing.f.v_sync_invert.w_data) + m.d.sync += self.fb.timings.active_pixels.eq(self._hv_timing.f.active_pixels.w_data) + with m.If(self._flags.f.enable.w_stb): + m.d.sync += self.fb.enable.eq(self._flags.f.enable.w_data) + with m.If(self._fb_base.f.fb_base.w_stb): + m.d.sync += self.fb.fb_base.eq(self._fb_base.f.fb_base.w_data) + + # palette update logic + palette_busy = Signal() + m.d.comb += self._palette_busy.f.busy.r_data.eq(palette_busy) + + with m.If(self._palette.element.w_stb & ~palette_busy): + m.d.sync += [ + palette_busy .eq(1), + self.fb.palette.update.valid .eq(1), + self.fb.palette.update.payload.position .eq(self._palette.f.position.w_data), + self.fb.palette.update.payload.red .eq(self._palette.f.red.w_data), + self.fb.palette.update.payload.green .eq(self._palette.f.green.w_data), + self.fb.palette.update.payload.blue .eq(self._palette.f.blue.w_data), + ] + + with m.If(palette_busy & self.fb.palette.update.ready): + # coefficient has been written + m.d.sync += [ + palette_busy.eq(0), + self.fb.palette.update.valid.eq(0), + ] + + return m diff --git a/gateware/src/tiliqua/dvi.py b/gateware/src/tiliqua/dvi.py index b8f909bd..db41513e 100644 --- a/gateware/src/tiliqua/dvi.py +++ b/gateware/src/tiliqua/dvi.py @@ -9,6 +9,7 @@ from amaranth.lib.wiring import In, Out from tiliqua import sim, tmds +from tiliqua.dvi_modeline import DVIModeline class DVITimingGen(wiring.Component): @@ -50,6 +51,7 @@ def __init__(self): def __init__(self): super().__init__({ + "enable": In(1), "timings": In(self.TimingProperties()), # Control signals without inversion applied. # Useful for driving logic external to this core (e.g. do something @@ -78,7 +80,7 @@ def elaborate(self, platform) -> Module: self.x = Signal(signed(12)) self.y = Signal(signed(12)) - with m.If(ResetSignal("dvi")): + with m.If(ResetSignal("dvi") | ~self.enable): m.d.dvi += [ self.x.eq(self.h_reset), self.y.eq(self.v_reset), diff --git a/gateware/src/tiliqua/tiliqua_platform.py b/gateware/src/tiliqua/tiliqua_platform.py index 2bc87775..bdc41c59 100644 --- a/gateware/src/tiliqua/tiliqua_platform.py +++ b/gateware/src/tiliqua/tiliqua_platform.py @@ -322,10 +322,28 @@ class _TiliquaR4Mobo: Attrs(IO_TYPE="LVCMOS33"))), # Motherboard PCBA I2C bus. Includes: + # # - address 0x05: PCA9635 LED driver # - address 0x47: TUSB322I USB-C controller # - address 0x50: DVI EDID EEPROM (through 3V3 <-> 5V translator) # - address 0x60: SI5351A external PLL + # + # WARN: mobo i2c bus speed should never exceed 100kHz if you want to reliably + # be able to read the EDID of external monitors. I have seen cases where going + # above 100kHz causes the monitor to ACK packets that were not destined for + # its EEPROM (instead for other i2c peripherals on i2c0) - this is dangerous and + # could unintentionally write to the monitor EEPROM (!!!) + # + # There are some slave addresses on the DDC channel that are reserved according + # to the standard. As far as I can tell, we have no collisions. Reference: + # + # VESA Display Data Channel Command Interface (DDC/CI) + # Standard Version 1.1, October 29, 2004 + # + # Reserved for VCP: 0x6E + # Reserved in table 4: 0xF0 - 0xFF + # Reserved in table 5: 0x12, 0x14, 0x16, 0x80, 0x40, 0xA0 + # Resource("i2c", 0, Subsignal("sda", Pins("51", dir="io", conn=("m2", 0))), Subsignal("scl", Pins("53", dir="io", conn=("m2", 0))), diff --git a/gateware/src/tiliqua/tiliqua_soc.py b/gateware/src/tiliqua/tiliqua_soc.py index c3b5fe69..6b777411 100644 --- a/gateware/src/tiliqua/tiliqua_soc.py +++ b/gateware/src/tiliqua/tiliqua_soc.py @@ -65,15 +65,6 @@ class PersistReg(csr.Register, access="w"): class DecayReg(csr.Register, access="w"): decay: csr.Field(csr.action.W, unsigned(8)) - class PaletteReg(csr.Register, access="w"): - position: csr.Field(csr.action.W, unsigned(8)) - red: csr.Field(csr.action.W, unsigned(8)) - green: csr.Field(csr.action.W, unsigned(8)) - blue: csr.Field(csr.action.W, unsigned(8)) - - class PaletteBusyReg(csr.Register, access="r"): - busy: csr.Field(csr.action.R, unsigned(1)) - def __init__(self, fb, bus_dma): self.en = Signal() self.fb = fb @@ -84,8 +75,6 @@ def __init__(self, fb, bus_dma): self._persist = regs.add("persist", self.PersistReg(), offset=0x0) self._decay = regs.add("decay", self.DecayReg(), offset=0x4) - self._palette = regs.add("palette", self.PaletteReg(), offset=0x8) - self._palette_busy = regs.add("palette_busy", self.PaletteBusyReg(), offset=0xC) self._bridge = csr.Bridge(regs.as_memory_map()) @@ -101,7 +90,7 @@ def elaborate(self, platform): connect(m, flipped(self.bus), self._bridge.bus) - m.d.comb += self.persist.enable.eq(self.en) + m.d.comb += self.persist.enable.eq(self.fb.enable) with m.If(self._persist.f.persist.w_stb): m.d.sync += self.persist.holdoff.eq(self._persist.f.persist.w_data) @@ -109,26 +98,6 @@ def elaborate(self, platform): with m.If(self._decay.f.decay.w_stb): m.d.sync += self.persist.decay.eq(self._decay.f.decay.w_data) - # palette update logic - palette_busy = Signal() - m.d.comb += self._palette_busy.f.busy.r_data.eq(palette_busy) - - with m.If(self._palette.element.w_stb & ~palette_busy): - m.d.sync += [ - palette_busy .eq(1), - self.fb.palette.update.valid .eq(1), - self.fb.palette.update.payload.position .eq(self._palette.f.position.w_data), - self.fb.palette.update.payload.red .eq(self._palette.f.red.w_data), - self.fb.palette.update.payload.green .eq(self._palette.f.green.w_data), - self.fb.palette.update.payload.blue .eq(self._palette.f.blue.w_data), - ] - - with m.If(palette_busy & self.fb.palette.update.ready): - # coefficient has been written - m.d.sync += [ - palette_busy.eq(0), - self.fb.palette.update.valid.eq(0), - ] return m @@ -170,6 +139,7 @@ def __init__(self, *, firmware_bin_path, default_modeline, ui_name, ui_sha, plat self.pmod0_periph_base = 0x00000700 self.dtr0_base = 0x00000800 self.video_periph_base = 0x00000900 + self.framebuffer_periph_base = 0x00000A00 # Some settings depend on whether code is in block RAM or SPI flash self.fw_location = fw_location @@ -251,7 +221,9 @@ def __init__(self, *, firmware_bin_path, default_modeline, ui_name, ui_sha, plat # mobo i2c self.i2c0 = i2c.Peripheral() - self.i2c_stream0 = i2c.I2CStreamer(period_cyc=256) + # XXX: 100kHz bus speed. DO NOT INCREASE THIS. See comment on this bus in + # tiliqua_platform.py for more details. + self.i2c_stream0 = i2c.I2CStreamer(period_cyc=600) self.csr_decoder.add(self.i2c0.bus, addr=self.i2c0_base, name="i2c0") # eurorack-pmod i2c @@ -281,6 +253,10 @@ def __init__(self, *, firmware_bin_path, default_modeline, ui_name, ui_sha, plat bus_dma=self.psram_periph) self.csr_decoder.add(self.video_periph.bus, addr=self.video_periph_base, name="video_periph") + self.framebuffer_periph = dma_framebuffer.Peripheral(fb=self.fb) + self.csr_decoder.add( + self.framebuffer_periph.bus, addr=self.framebuffer_periph_base, name="framebuffer_periph") + self.permit_bus_traffic = Signal() self.extra_rust_constants = [] @@ -369,6 +345,7 @@ def elaborate(self, platform): # video PHY m.submodules.fb = self.fb + m.submodules.framebuffer_periph = self.framebuffer_periph # video periph / persist m.submodules.video_periph = self.video_periph @@ -407,13 +384,10 @@ def elaborate(self, platform): # Memory controller hangs if we start making requests to it straight away. on_delay = Signal(32) with m.If(on_delay < 0xFF): - m.d.comb += self.cpu.ext_reset.eq(1) - with m.If(on_delay < 0xFFFF): m.d.sync += on_delay.eq(on_delay+1) + m.d.comb += self.cpu.ext_reset.eq(1) with m.Else(): - m.d.sync += self.permit_bus_traffic.eq(1) - m.d.sync += self.fb.enable.eq(1) - m.d.sync += self.video_periph.en.eq(1) + m.d.comb += self.cpu.ext_reset.eq(0) return m diff --git a/gateware/src/top/bootloader/fw/src/main.rs b/gateware/src/top/bootloader/fw/src/main.rs index 8437b7a2..f1c3c895 100644 --- a/gateware/src/top/bootloader/fw/src/main.rs +++ b/gateware/src/top/bootloader/fw/src/main.rs @@ -30,13 +30,15 @@ use embedded_graphics::{ prelude::*, primitives::{PrimitiveStyleBuilder, Line}, text::{Alignment, Text}, + geometry::OriginDimensions, }; use tiliqua_fw::options::*; use hal::pca9635::Pca9635Driver; -hal::impl_dma_display!(DMADisplay, H_ACTIVE, V_ACTIVE, - VIDEO_ROTATE_90); +tiliqua_hal::impl_dma_framebuffer! { + DMAFramebuffer0: tiliqua_pac::FRAMEBUFFER_PERIPH, +} pub const TIMER0_ISR_PERIOD_MS: u32 = 10; @@ -109,12 +111,14 @@ impl App { fn print_rebooting(d: &mut D, rng: &mut fastrand::Rng) where - D: DrawTarget, + D: DrawTarget + OriginDimensions, { let style = MonoTextStyle::new(&FONT_9X15_BOLD, Gray8::WHITE); + let h_active = d.size().width as i32; + let v_active = d.size().height as i32; Text::with_alignment( "REBOOTING", - Point::new(rng.i32(0..H_ACTIVE as i32), rng.i32(0..V_ACTIVE as i32)), + Point::new(rng.i32(0..h_active), rng.i32(0..v_active)), style, Alignment::Center, ) @@ -127,48 +131,50 @@ fn draw_summary(d: &mut D, startup_report: &String<256>, or: i32, ot: i32, hue: u8) where - D: DrawTarget, + D: DrawTarget + OriginDimensions, { + let h_active = d.size().width as i32; + let v_active = d.size().height as i32; let norm = MonoTextStyle::new(&FONT_9X15, Gray8::new(0xB0 + hue)); if let Some(bitstream) = bitstream_manifest { Text::with_alignment( "brief:".into(), - Point::new((H_ACTIVE/2 - 10) as i32 + or, (V_ACTIVE/2+20) as i32 + ot), + Point::new((h_active/2 - 10) as i32 + or, (v_active/2+20) as i32 + ot), norm, Alignment::Right, ) .draw(d).ok(); Text::with_alignment( &bitstream.brief, - Point::new((H_ACTIVE/2) as i32 + or, (V_ACTIVE/2+20) as i32 + ot), + Point::new((h_active/2) as i32 + or, (v_active/2+20) as i32 + ot), norm, Alignment::Left, ) .draw(d).ok(); Text::with_alignment( "video:".into(), - Point::new((H_ACTIVE/2 - 10) as i32 + or, (V_ACTIVE/2+40) as i32 + ot), + Point::new((h_active/2 - 10) as i32 + or, (v_active/2+40) as i32 + ot), norm, Alignment::Right, ) .draw(d).ok(); Text::with_alignment( &bitstream.video, - Point::new((H_ACTIVE/2) as i32 + or, (V_ACTIVE/2+40) as i32 + ot), + Point::new((h_active/2) as i32 + or, (v_active/2+40) as i32 + ot), norm, Alignment::Left, ) .draw(d).ok(); Text::with_alignment( "sha:".into(), - Point::new((H_ACTIVE/2 - 10) as i32 + or, (V_ACTIVE/2+60) as i32 + ot), + Point::new((h_active/2 - 10) as i32 + or, (v_active/2+60) as i32 + ot), norm, Alignment::Right, ) .draw(d).ok(); Text::with_alignment( &bitstream.sha, - Point::new((H_ACTIVE/2) as i32 + or, (V_ACTIVE/2+60) as i32 + ot), + Point::new((h_active/2) as i32 + or, (v_active/2+60) as i32 + ot), norm, Alignment::Left, ) @@ -177,14 +183,14 @@ where if let Some(error_string) = &error { Text::with_alignment( "error:".into(), - Point::new((H_ACTIVE/2 - 10) as i32 + or, (V_ACTIVE/2+80) as i32 + ot), + Point::new((h_active/2 - 10) as i32 + or, (v_active/2+80) as i32 + ot), norm, Alignment::Right, ) .draw(d).ok(); Text::with_alignment( &error_string, - Point::new((H_ACTIVE/2) as i32 + or, (V_ACTIVE/2+80) as i32 + ot), + Point::new((h_active/2) as i32 + or, (v_active/2+80) as i32 + ot), norm, Alignment::Left, ) @@ -192,7 +198,7 @@ where } Text::with_alignment( &startup_report, - Point::new((H_ACTIVE/2) as i32 + or, (V_ACTIVE/2-70) as i32 + ot), + Point::new((h_active/2) as i32 + or, (v_active/2-70) as i32 + ot), norm, Alignment::Center, ) @@ -419,6 +425,17 @@ where } } +fn edid_test(i2cdev: &mut I2c0) -> Result { + info!("Read EDID..."); + let mut edid: [u8; 128] = [0; 128]; + const EDID_ADDR: u8 = 0x50; + for i in 0..16 { + i2cdev.transaction(EDID_ADDR, &mut [Operation::Write(&[(i*8) as u8]), + Operation::Read(&mut edid[i*8..i*8+8])]).ok(); + } + edid::Edid::parse(&edid) +} + #[entry] fn main() -> ! { let peripherals = pac::Peripherals::take().unwrap(); @@ -446,6 +463,52 @@ fn main() -> ! { } } + // Determine display modeline + let edid = { + let mut i2cdev0 = I2c0::new(unsafe { pac::I2C0::steal() } ); + use embedded_hal::delay::DelayNs; + timer.delay_ms(10); + edid_test(&mut i2cdev0) + }; + + info!("edid: {:?}", edid); + + // Default rotation and modeline + let mut video_rotate_90 = false; + let mut modeline = DVIModeline { + h_active : 1280, + h_sync_start : 1390, + h_sync_end : 1430, + h_total : 1650, + h_sync_invert : false, + v_active : 720, + v_sync_start : 725, + v_sync_end : 730, + v_total : 750, + v_sync_invert : false, + pixel_clk_mhz : 74.25, + }; + + // If connected to 720x720 screen, use native resolution. + if let Ok(edid_parsed) = edid { + if edid_parsed.header.product_code == 0x3132 { + video_rotate_90 = true; + modeline = DVIModeline { + h_active : 720, + h_sync_start : 760, + h_sync_end : 780, + h_total : 820, + h_sync_invert : false, + v_active : 720, + v_sync_start : 744, + v_sync_end : 748, + v_total : 760, + v_sync_invert : false, + pixel_clk_mhz : 37.40, + }; + } + } + // Setup external PLL let maybe_external_pll = if HW_REV_MAJOR >= 4 { @@ -453,7 +516,7 @@ fn main() -> ! { let mut si5351drv = Si5351Device::new_adafruit_module(i2cdev_mobo_pll); configure_external_pll(&ExternalPLLConfig{ clk0_hz: CLOCK_AUDIO_HZ, - clk1_hz: Some(CLOCK_DVI_HZ), + clk1_hz: Some((modeline.pixel_clk_mhz*1e6) as u32), spread_spectrum: Some(0.01), }, &mut si5351drv).unwrap(); Some(si5351drv) @@ -511,9 +574,17 @@ fn main() -> ! { let mut logo_coord_ix = 0u32; let mut rng = fastrand::Rng::with_seed(0); - let mut display = DMADisplay { - framebuffer_base: PSRAM_FB_BASE as *mut u32, - }; + + let mut display = DMAFramebuffer0::new( + peripherals.FRAMEBUFFER_PERIPH, + PSRAM_FB_BASE, + modeline, + video_rotate_90, + ); + + write!(startup_report, "display: {}x{}\r\n", + display.size().width, display.size().height); + video.set_persist(1024); let stroke = PrimitiveStyleBuilder::new() @@ -521,12 +592,15 @@ fn main() -> ! { .stroke_width(1) .build(); - palette::ColorPalette::default().write_to_hardware(&mut video); + palette::ColorPalette::default().write_to_hardware(&mut display); log::info!("{}", startup_report); loop { + let h_active = display.size().width; + let v_active = display.size().height; + // Always mute the CODEC to stop pops on flashing while in the bootloader. pmod.mute(true); @@ -536,14 +610,14 @@ fn main() -> ! { app.borrow_ref(cs).error_n.clone()) }); - draw::draw_options(&mut display, &opts, 100, V_ACTIVE/2-50, 0).ok(); - draw::draw_name(&mut display, H_ACTIVE/2, V_ACTIVE-50, 0, UI_NAME, UI_SHA).ok(); + draw::draw_options(&mut display, &opts, 100, v_active/2-50, 0).ok(); + draw::draw_name(&mut display, h_active/2, v_active-50, 0, UI_NAME, UI_SHA).ok(); if let Some(n) = opts.tracker.selected { draw_summary(&mut display, &manifests[n], &error_n[n], &startup_report, -20, -18, 0); if manifests[n].is_some() { - Line::new(Point::new(255, (V_ACTIVE/2 - 55 + (n as u32)*18) as i32), - Point::new((H_ACTIVE/2-90) as i32, (V_ACTIVE/2+8) as i32)) + Line::new(Point::new(255, (v_active/2 - 55 + (n as u32)*18) as i32), + Point::new((h_active/2-90) as i32, (v_active/2+8) as i32)) .into_styled(stroke) .draw(&mut display).ok(); } @@ -551,7 +625,7 @@ fn main() -> ! { for _ in 0..5 { let _ = draw::draw_boot_logo(&mut display, - (H_ACTIVE/2) as i32, + (h_active/2) as i32, 150 as i32, logo_coord_ix); logo_coord_ix += 1;