Skip to main content

esp_hal/mipi_dsi/
dpi.rs

1//! MIPI DSI video-mode (DPI) driver.
2
3use core::{
4    marker::PhantomData,
5    pin::Pin,
6    sync::atomic,
7    task::{Context, Poll},
8};
9
10use esp_sync::NonReentrantMutex;
11
12use crate::{
13    asynch::AtomicWaker,
14    dma::aligned::InternalMemory,
15    interrupt,
16    mipi_dsi::{
17        ConfigError,
18        MipiDsi,
19        vdma::{VdmaChannel, VdmaLinkItem},
20    },
21    peripherals::{Interrupt, MIPI_DSI_BRIDGE, MIPI_DSI_HOST, VDMA},
22    soc::clocks::{ClockTree, MipiDsiDpiClkConfig, MipiDsiInstance},
23    system::Cpu,
24};
25
26/// Channel index used by the static VDMA block-done ISR.
27///
28/// Set before the channel is started; read by the ISR which cannot capture
29/// runtime state.  A single MIPI-DSI instance is the only VDMA user, so
30/// this value is stable for the lifetime of `DsiDpi`.
31static VDMA_ISR_CHANNEL: atomic::AtomicU8 = atomic::AtomicU8::new(0);
32
33const MAX_FBS: usize = 3;
34/// LLIs in the circular DMA ring.  The ISR re-arms each one immediately after
35/// the DMA consumes it, so the ring never suspends regardless of ring depth.
36/// Two is sufficient; keeping it small reduces ISR overhead.
37const NUM_LLIS: usize = 2;
38const DMA_BURST_LEN: u32 = 256;
39const FIFO_EMPTY_THRESHOLD: u32 = 1024 - DMA_BURST_LEN;
40
41// ── Static link-list storage ──────────────────────────────────────────────────
42
43static LLI_STORAGE: NonReentrantMutex<InternalMemory<[VdmaLinkItem; NUM_LLIS]>> =
44    NonReentrantMutex::new(InternalMemory::new(
45        [const { VdmaLinkItem::zeroed() }; NUM_LLIS],
46    ));
47
48// ── Public types ──────────────────────────────────────────────────────────────
49
50// DPI pixel clock source is represented by the generated `MipiDsiDpiClkSclk`
51// enum from the clock tree. Re-export it so callers don't need to reach into
52// `soc::clocks` directly.
53pub use crate::soc::clocks::MipiDsiDpiClkSclk as DpiClockSource;
54
55/// Input/output pixel color format.
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57#[cfg_attr(feature = "defmt", derive(defmt::Format))]
58pub enum ColorFormat {
59    /// RGB 888, 24 bpp.
60    Rgb888,
61    /// RGB 565, 16 bpp.
62    Rgb565,
63}
64
65impl ColorFormat {
66    pub(crate) fn bits_per_pixel(self) -> u32 {
67        match self {
68            Self::Rgb888 => 24,
69            Self::Rgb565 => 16,
70        }
71    }
72
73    fn raw_type(self) -> u8 {
74        match self {
75            Self::Rgb888 => 0,
76            Self::Rgb565 => 2,
77        }
78    }
79
80    fn dpi_type(self) -> u8 {
81        match self {
82            Self::Rgb888 => 0,
83            Self::Rgb565 => 2,
84        }
85    }
86
87    /// `dpi_color_coding` register value (DSI host).
88    fn host_color_coding(self) -> u8 {
89        match self {
90            Self::Rgb888 => 5, // 24-bit
91            Self::Rgb565 => 0, // 16-bit config 1
92        }
93    }
94}
95
96/// Video frame timing (pixel/line counts).
97#[derive(Clone, Copy, Debug)]
98pub struct FrameTiming {
99    /// Active pixels per line.
100    pub h_active: u32,
101    /// Horizontal sync pulse width (pixels).
102    pub hsw: u32,
103    /// Horizontal back porch (pixels).
104    pub hbp: u32,
105    /// Horizontal front porch (pixels).
106    pub hfp: u32,
107    /// Active lines per frame.
108    pub v_active: u32,
109    /// Vertical sync pulse width (lines).
110    pub vsw: u32,
111    /// Vertical back porch (lines).
112    pub vbp: u32,
113    /// Vertical front porch (lines).
114    pub vfp: u32,
115}
116
117/// DPI video-mode configuration.
118#[derive(Clone, Copy, Debug)]
119pub struct DpiConfig {
120    /// DSI virtual channel (0–3).
121    pub virtual_channel: u8,
122    /// Desired pixel clock in MHz.
123    pub pixel_clock_mhz: f32,
124    /// Pixel clock source (XTAL, PLL_F240M, or PLL_F160M).
125    pub dpi_clk_src: DpiClockSource,
126    /// Input pixel format (from frame buffer).
127    pub in_color_format: ColorFormat,
128    /// Output pixel format (sent over DSI lanes).
129    pub out_color_format: ColorFormat,
130    /// Frame timing parameters.
131    pub timing: FrameTiming,
132}
133
134// ── VDMA block-done ISR ───────────────────────────────────────────────────────
135
136/// Fired by the DW-GDMA at the end of every frame block transfer.
137///
138/// Re-arms the LLI that the DMA just consumed (sets `LLI_VALID = 1` and
139/// flushes the cache line to SRAM) so the DMA can use it again when the ring
140/// comes back around.  This keeps the channel running indefinitely without
141/// software ever needing to restart it.
142#[crate::ram]
143#[crate::handler]
144fn vdma_block_done_isr() {
145    let channel_id = VDMA_ISR_CHANNEL.load(atomic::Ordering::Relaxed) as usize;
146    let ch = VDMA::regs().ch(channel_id);
147    ch.intclear0().write(|w| unsafe { w.bits(0xFFFFFFFF) });
148
149    LLI_STORAGE.with(|storage| {
150        let mut storage = storage.get_mut();
151        for lli in storage.iter() {
152            lli.rearm();
153        }
154
155        storage.writeback();
156    });
157}
158
159// ── Async waker ───────────────────────────────────────────────────────────────
160
161static VSYNC_WAKER: AtomicWaker = AtomicWaker::new();
162
163#[crate::handler]
164fn dsi_bridge_isr() {
165    let bridge = MIPI_DSI_BRIDGE::regs();
166    let st = bridge.int_st().read();
167    if st.vsync().bit_is_set() {
168        bridge.int_ena().modify(|_, w| w.vsync().clear_bit());
169        VSYNC_WAKER.wake();
170    }
171}
172
173// ── DsiDpi ────────────────────────────────────────────────────────────────────
174
175/// Video-mode (DPI) streaming handle.
176///
177/// Consumes the [`MipiDsi`] bus and drives the DSI host in video mode,
178/// continuously streaming frame buffers via VDMA.
179pub struct DsiDpi<'d> {
180    _guard: crate::mipi_dsi::DphyGuard<'d>,
181    fb_ptrs: [*mut u8; MAX_FBS],
182    fb_size: usize,
183    num_fbs: usize,
184    current_fb: usize,
185    _phantom: PhantomData<&'d mut [u8]>,
186}
187
188impl Drop for DsiDpi<'_> {
189    fn drop(&mut self) {
190        // Disable the block-done interrupt before shutting down.
191        let channel_id = self._guard.vdma_channel_id as usize;
192        let ch = VDMA::regs().ch(channel_id);
193        unsafe {
194            ch.intsignal_enable0().write_with_zero(|w| w);
195            ch.intstatus_enable0().write_with_zero(|w| w);
196        }
197        interrupt::disable(Cpu::current(), Interrupt::DMA);
198
199        // Disable the VDMA channel itself.
200        let shift = channel_id as u32;
201        let val: u32 = 0x0100 << shift; // ch_en_we only (clears ch_en)
202        unsafe { VDMA::regs().chen0().write(|w| w.bits(val)) };
203
204        MIPI_DSI_BRIDGE::regs()
205            .dpi_misc_config()
206            .modify(|_, w| w.dpi_en().clear_bit());
207        ClockTree::with(|clocks| MipiDsiInstance::MipiDsi.release_dpi_clk(clocks));
208        // _guard is dropped next, which calls dphy_power_down() and releases PHY clocks.
209    }
210}
211
212impl<'d> DsiDpi<'d> {
213    pub(crate) fn new(
214        bus: MipiDsi<'d>,
215        config: DpiConfig,
216        framebuffers: &[&'d mut [u8]],
217    ) -> Result<Self, ConfigError> {
218        let num_fbs = framebuffers.len();
219        debug_assert!((1..=MAX_FBS).contains(&num_fbs));
220        let fb_size = framebuffers[0].len();
221
222        // ── DPI clock ──────────────────────────────────────────────────────
223        // Compute the divider from the source frequency and the desired pixel
224        // clock, then configure and enable through the clock tree so that the
225        // upstream PLL reference count is maintained correctly.
226        let (_dpi_clk_config, real_dpi_mhz) = ClockTree::with(|clocks| {
227            // Obtain source frequency (div_num = 0 → no division applied yet).
228            let src_hz = MipiDsiInstance::dpi_clk_config_frequency(
229                clocks,
230                MipiDsiDpiClkConfig::new(config.dpi_clk_src, 0),
231            );
232            let src_mhz = src_hz as f32 / 1_000_000.0;
233            let div = ((src_mhz / config.pixel_clock_mhz) + 0.5) as u32;
234            let div = div.max(1);
235            let cfg = MipiDsiDpiClkConfig::new(config.dpi_clk_src, div - 1);
236            MipiDsiInstance::MipiDsi.configure_dpi_clk(clocks, cfg);
237            MipiDsiInstance::MipiDsi.request_dpi_clk(clocks);
238            (cfg, src_mhz / div as f32)
239        });
240
241        let host = MIPI_DSI_HOST::regs();
242        let bridge = MIPI_DSI_BRIDGE::regs();
243
244        // ── Host: virtual channel & color coding ───────────────────────────
245        host.dpi_vcid()
246            .modify(|_, w| unsafe { w.dpi_vcid().bits(config.virtual_channel) });
247        host.dpi_color_coding().modify(|_, w| unsafe {
248            w.dpi_color_coding()
249                .bits(config.out_color_format.host_color_coding())
250        });
251
252        // All DPI signals active-high (no inversion).
253        host.dpi_cfg_pol().write(|w| unsafe { w.bits(0) });
254
255        // Burst mode with sync pulses.  LP transitions are allowed only in
256        // blanking intervals; lp_vact_en and frame_bta_ack_en are left clear:
257        // - lp_vact_en: going LP during the active pixel burst corrupts video.
258        // - frame_bta_ack_en: requesting a per-frame BTA stalls the host if the panel does not
259        //   respond, freezing the video stream.
260        host.vid_mode_cfg().modify(|_, w| unsafe {
261            w.vid_mode_type().bits(2); // burst with sync pulses
262            w.lp_vsa_en().set_bit();
263            w.lp_vbp_en().set_bit();
264            w.lp_vfp_en().set_bit();
265            w.lp_hbp_en().set_bit();
266            w.lp_hfp_en().set_bit();
267            w.lp_cmd_en().set_bit()
268        });
269
270        let t = &config.timing;
271        host.vid_pkt_size()
272            .modify(|_, w| unsafe { w.vid_pkt_size().bits(t.h_active as u16) });
273        host.vid_num_chunks()
274            .modify(|_, w| unsafe { w.vid_num_chunks().bits(0) });
275        host.vid_null_size()
276            .modify(|_, w| unsafe { w.vid_null_size().bits(0) });
277
278        // ── Host: horizontal timing (lane byte clock cycles) ───────────────
279        let ratio = bus.lane_bit_rate_mbps / config.pixel_clock_mhz / 8.0;
280        let htotal = t.hsw + t.hbp + t.h_active + t.hfp;
281
282        let host_hsw = fround_u32(t.hsw as f32 * ratio);
283        let host_hbp = fround_u32(t.hbp as f32 * ratio);
284        let host_act = fround_u32(t.h_active as f32 * ratio);
285        let host_hfp = fround_u32(t.hfp as f32 * ratio);
286        let host_htotal = fround_u32(htotal as f32 * ratio);
287        let comp = host_htotal as i32 - (host_hsw + host_hbp + host_act + host_hfp) as i32;
288        let host_act = (host_act as i32 + comp).max(0) as u32;
289
290        host.vid_hsa_time()
291            .modify(|_, w| unsafe { w.vid_hsa_time().bits(host_hsw as u16) });
292        host.vid_hbp_time()
293            .modify(|_, w| unsafe { w.vid_hbp_time().bits(host_hbp as u16) });
294        host.vid_hline_time().modify(|_, w| unsafe {
295            w.vid_hline_time()
296                .bits((host_hsw + host_hbp + host_act + host_hfp) as u16)
297        });
298
299        // ── Host: vertical timing ──────────────────────────────────────────
300        host.vid_vsa_lines()
301            .modify(|_, w| unsafe { w.vsa_lines().bits(t.vsw as u16) });
302        host.vid_vbp_lines()
303            .modify(|_, w| unsafe { w.vbp_lines().bits(t.vbp as u16) });
304        host.vid_vactive_lines()
305            .modify(|_, w| unsafe { w.v_active_lines().bits(t.v_active as u16) });
306        host.vid_vfp_lines()
307            .modify(|_, w| unsafe { w.vfp_lines().bits(t.vfp as u16) });
308
309        // ── Bridge: timing (HFP compensated for actual DPI clock) ─────────
310        let brg_hfp = {
311            let c = fround_u32(real_dpi_mhz / config.pixel_clock_mhz * htotal as f32) as i32
312                - htotal as i32;
313            (t.hfp as i32 + c).max(0) as u32
314        };
315        bridge.dpi_h_cfg0().modify(|_, w| unsafe {
316            w.htotal()
317                .bits((t.hsw + t.hbp + t.h_active + brg_hfp) as u16);
318            w.hdisp().bits(t.h_active as u16)
319        });
320        bridge.dpi_h_cfg1().modify(|_, w| unsafe {
321            w.hsync().bits(t.hsw as u16);
322            w.hbank().bits(t.hbp as u16)
323        });
324        bridge.dpi_v_cfg0().modify(|_, w| unsafe {
325            w.vtotal().bits((t.vsw + t.vbp + t.v_active + t.vfp) as u16);
326            w.vdisp().bits(t.v_active as u16)
327        });
328        bridge.dpi_v_cfg1().modify(|_, w| unsafe {
329            w.vsync().bits(t.vsw as u16);
330            w.vbank().bits(t.vbp as u16)
331        });
332
333        // ── Bridge: pixel format & DMA ─────────────────────────────────────
334        let total_bits = t.h_active * t.v_active * config.in_color_format.bits_per_pixel();
335        bridge.raw_num_cfg().modify(|_, w| unsafe {
336            w.raw_num_total().bits(total_bits.div_ceil(64));
337            w.unalign_64bit_en().bit(!total_bits.is_multiple_of(64));
338            w.raw_num_total_set().set_bit()
339        });
340        bridge
341            .dpi_misc_config()
342            .modify(|_, w| unsafe { w.fifo_underrun_discard_vcnt().bits(t.h_active as u16) });
343        bridge.pixel_type().modify(|_, w| unsafe {
344            w.raw_type().bits(config.in_color_format.raw_type());
345            w.dpi_type().bits(config.out_color_format.dpi_type());
346            w.data_in_type().clear_bit()
347        });
348        bridge.dma_flow_ctrl().modify(|_, w| unsafe {
349            w.dsi_dma_flow_controller().clear_bit();
350            w.dma_flow_multiblk_num().bits(1)
351        });
352        bridge
353            .dma_frame_interval()
354            .modify(|_, w| w.dma_multiblk_en().clear_bit());
355        bridge
356            .dma_req_cfg()
357            .modify(|_, w| unsafe { w.dma_burst_len().bits(DMA_BURST_LEN as u16) });
358        bridge.raw_buf_almost_empty_thrd().modify(|_, w| unsafe {
359            w.dsi_raw_buf_almost_empty_thrd()
360                .bits(FIFO_EMPTY_THRESHOLD as u16)
361        });
362
363        bridge.en().modify(|_, w| w.dsi_en().set_bit());
364        bridge
365            .dpi_config_update()
366            .write(|w| w.dpi_config_update().set_bit());
367
368        interrupt::bind_handler(Interrupt::DSI_BRIDGE, dsi_bridge_isr);
369
370        // Flush all frame buffers from CPU cache → PSRAM.
371        for fb in framebuffers.iter() {
372            unsafe {
373                crate::soc::cache_writeback_addr(fb.as_ptr() as u32, fb_size as u32);
374            }
375        }
376
377        // Build ring: LLI[i] → LLI[(i+1) % NUM_LLIS].
378        let fb_ptrs = core::array::from_fn::<_, MAX_FBS, _>(|i| {
379            if i < num_fbs {
380                framebuffers[i].as_ptr().cast_mut()
381            } else {
382                core::ptr::null_mut::<u8>()
383            }
384        });
385
386        let vdma_channel_id = bus.guard.vdma_channel_id;
387        VDMA_ISR_CHANNEL.store(vdma_channel_id, atomic::Ordering::Relaxed);
388
389        LLI_STORAGE.with(|storage| {
390            let mut storage = storage.get_mut();
391            for i in 0..NUM_LLIS {
392                let next = &raw const storage[(i + 1) % NUM_LLIS];
393                storage[i].configure(fb_ptrs[0] as u32, fb_size, next);
394            }
395
396            // Flush the LLI descriptors from CPU cache → SRAM so the DMA sees them.
397            storage.writeback();
398
399            VdmaChannel::new(vdma_channel_id).start(&storage[0]);
400        });
401
402        // ── VDMA block-done interrupt ──────────────────────────────────────
403        // Enable the block-transfer-done signal so the ISR fires after every
404        // frame block and re-arms the consumed LLI.
405        {
406            let ch = VDMA::regs().ch(vdma_channel_id as usize);
407            ch.intstatus_enable0()
408                .write(|w| w.ch1_enable_block_tfr_done_intstat().set_bit());
409            ch.intsignal_enable0()
410                .write(|w| w.ch1_enable_block_tfr_done_intsignal().set_bit());
411        }
412        interrupt::bind_handler(Interrupt::DMA, vdma_block_done_isr);
413
414        // ── Enable video mode ──────────────────────────────────────────────
415        host.mode_cfg()
416            .modify(|_, w| w.cmd_video_mode().clear_bit());
417        bridge.dpi_misc_config().modify(|_, w| w.dpi_en().set_bit());
418        bridge
419            .dpi_config_update()
420            .write(|w| w.dpi_config_update().set_bit());
421
422        let MipiDsi { guard, .. } = bus;
423        Ok(Self {
424            _guard: guard,
425            fb_ptrs,
426            fb_size,
427            num_fbs,
428            current_fb: 0,
429            _phantom: PhantomData,
430        })
431    }
432
433    /// Returns a mutable slice to the back (not-currently-displayed) frame buffer.
434    ///
435    /// With a single frame buffer this is the same buffer the DMA may be
436    /// reading — the caller is responsible for synchronisation in that case.
437    pub fn framebuffer_mut(&mut self) -> &mut [u8] {
438        let back = (self.current_fb + 1) % self.num_fbs;
439        unsafe { core::slice::from_raw_parts_mut(self.fb_ptrs[back], self.fb_size) }
440    }
441
442    /// Flip the back buffer to the display.
443    ///
444    /// Flushes the rendered back buffer from CPU cache to PSRAM, then updates
445    /// the source address in every LLI so the DMA switches to the new pixels
446    /// on its next block boundary.
447    ///
448    /// `LLI_VALID` re-arming is handled entirely by the `vdma_block_done_isr`
449    /// interrupt handler, which fires after every frame block and keeps the
450    /// DMA channel running indefinitely without software restarts.
451    pub fn commit(&mut self) {
452        let back = (self.current_fb + 1) % self.num_fbs;
453
454        // Flush back buffer: CPU cache → PSRAM.
455        unsafe {
456            crate::soc::cache_writeback_addr(self.fb_ptrs[back] as u32, self.fb_size as u32);
457        }
458
459        // Update sar_lo in every LLI to point at the new front buffer.
460        // The DMA latches sar_lo at the start of each block, so an in-progress
461        // block is unaffected; the new pixels appear on the next block boundary.
462        let src = self.fb_ptrs[back] as u32;
463        LLI_STORAGE.with(|storage| {
464            let storage = storage.get_mut();
465            for lli in storage.iter() {
466                lli.set_source(src);
467            }
468        });
469
470        // The ISR will flush the cache, no need to do it here.
471
472        self.current_fb = back;
473    }
474
475    /// Block until the DSI bridge signals the start of the next vertical blank.
476    ///
477    /// Use this to pace rendering to the display refresh rate.  Any vsync event
478    /// that is already pending (i.e. fired while the CPU was busy rendering)
479    /// will be returned immediately.
480    pub fn wait_for_vsync(&mut self) {
481        let bridge = MIPI_DSI_BRIDGE::regs();
482        while !bridge.int_raw().read().vsync().bit_is_set() {}
483        bridge.int_clr().write(|w| w.vsync().clear_bit_by_one());
484    }
485
486    /// Async: yield until the next vsync event.
487    pub fn wait_for_vsync_async(&mut self) -> impl Future<Output = ()> {
488        VsyncFuture { _dpi: self }
489    }
490}
491
492#[must_use = "futures do nothing unless you `.await` or poll them"]
493struct VsyncFuture<'a, 'b> {
494    _dpi: &'a mut DsiDpi<'b>,
495}
496
497impl Future for VsyncFuture<'_, '_> {
498    type Output = ();
499
500    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
501        let bridge = MIPI_DSI_BRIDGE::regs();
502        if bridge.int_raw().read().vsync().bit_is_set() {
503            bridge.int_clr().write(|w| w.vsync().clear_bit_by_one());
504            Poll::Ready(())
505        } else {
506            VSYNC_WAKER.register(cx.waker());
507            bridge.int_ena().modify(|_, w| w.vsync().set_bit());
508            Poll::Pending
509        }
510    }
511}
512
513impl Drop for VsyncFuture<'_, '_> {
514    fn drop(&mut self) {
515        let bridge = MIPI_DSI_BRIDGE::regs();
516        bridge.int_ena().modify(|_, w| w.vsync().clear_bit());
517    }
518}
519
520// ── Helpers ───────────────────────────────────────────────────────────────────
521
522#[inline]
523fn fround_u32(x: f32) -> u32 {
524    (x + 0.5) as u32
525}