Skip to main content

device_envoy_rp/
cyd.rs

1#![cfg_attr(
2    feature = "doc-images",
3    doc = ::embed_doc_image::embed_image!(
4        "linkage_blaze_gallery",
5        "docs/assets/linkage_blaze_gallery.png"
6    )
7)]
8//! Raspberry Pi Pico support for standalone Cheap Yellow Display (CYD) modules.
9//!
10//! These modules combine a 320×240 ILI9341 display with XPT2046 resistive
11//! touch and connect to a Raspberry Pi Pico 1 or 2 over [SPI](crate#glossary).
12//! After construction, applications use the portable display and
13//! calibrated-touch interfaces from
14//! [`device_envoy_core::cyd`](https://docs.rs/device-envoy-core/latest/device_envoy_core/cyd/).
15//! The ESP32, WebAssembly, and in-memory implementations share these interfaces.
16//!
17//! ## See CYD in action
18//!
19//! [Linkage Blaze] demonstrates animated clocks, mechanisms, and figures built
20//! with Device Envoy's portable CYD display APIs. Explore the examples in the
21//! [interactive Linkage Blaze gallery].
22#![cfg_attr(
23    feature = "doc-images",
24    doc = "\n[![Linkage Blaze gallery showing CYD applications][linkage_blaze_gallery]][interactive Linkage Blaze gallery]\n"
25)]
26//!
27//! [Linkage Blaze]: https://github.com/CarlKCarlK/linkage-blaze
28//! [interactive Linkage Blaze gallery]: https://carlkcarlk.github.io/linkage-blaze/demos/
29//!
30//! The portable [Core CYD documentation] provides the shared
31//! [application example], [drawing-strategy guide], [implementation overview],
32//! and [Linkage Blaze gallery].
33//!
34//! [Core CYD documentation]: https://docs.rs/device-envoy-core/latest/device_envoy_core/cyd/
35//! [application example]: https://docs.rs/device-envoy-core/latest/device_envoy_core/cyd/#application-example
36//! [drawing-strategy guide]: https://docs.rs/device-envoy-core/latest/device_envoy_core/cyd/#choose-a-drawing-strategy
37//! [implementation overview]: https://docs.rs/device-envoy-core/latest/device_envoy_core/cyd/#implementations-1
38//! [Linkage Blaze gallery]: https://docs.rs/device-envoy-core/latest/device_envoy_core/cyd/#see-cyd-in-action
39//!
40//! Your drawing strategy determines the pixel-buffer capacity selected through
41//! [`CydRp::new_static`]: use [`CydRp::SCREEN_PIXELS`] for full-screen frames,
42//! the largest region's pixel count for regional frames,
43//! [`tiling::TileGrid::max_tile_pixel_count`] for tiled drawing, or `0` for
44//! immediate operations and contiguous streaming.
45//!
46//! [Choose a constructor](#choose-a-constructor) explains how to construct a
47//! Raspberry Pi Pico device. The
48//! [Raspberry Pi Pico CYD touch-paint example](https://github.com/CarlKCarlK/device-envoy/blob/main/crates/device-envoy-examples-rp/examples/cyd_touch_paint.rs)
49//! puts both stages together in a complete program.
50//! ## Choose a constructor
51//!
52//! Construction depends on how many [SPI peripherals](crate#glossary) the CYD
53//! should use and whether the application needs touch:
54//!
55//! - [`CydRp`] uses two SPI peripherals: `SPI0` for the display and `SPI1` for
56//!   touch. Choose it when both are available. Construction also loads or runs
57//!   touch calibration.
58//! - [`CydRpOneSpi`] uses one SPI peripheral for both the display and touch.
59//!   Choose it when the wiring or application requires the other peripheral.
60//!   Device Envoy coordinates access internally.
61//! - [`CydDisplayRp`] uses one SPI peripheral for the display and omits touch.
62//!   Choose it when the application does not need touch.
63//!
64//! The [`CydRp::new`], [`CydRpOneSpi::new`], and [`CydDisplayRp::new`] examples
65//! show the constructor arguments for each choice. After construction, shared
66//! application code can use the [`Cyd`] trait without naming an RP type.
67
68mod buffer;
69mod display;
70mod one_spi;
71mod text;
72#[path = "cyd/touch.rs"]
73mod touch_driver;
74
75use core::{convert::Infallible, fmt};
76
77use device_envoy_core::button::Button;
78use device_envoy_core::cyd::backend;
79use device_envoy_core::cyd::{
80    SCREEN_PIXELS,
81    backend::{CalibrationConfig, RawTouchEvent, TouchUncalibrated},
82    display::CydFrame,
83    touch::TouchEvent,
84};
85use device_envoy_core::pixel_target::PixelTarget;
86// The device abstraction and its neutral support types live in
87// `device-envoy-core::cyd`; re-export the public surface from this device crate.
88pub use device_envoy_core::cyd::{
89    Cyd, CydDisplay, CydTouch,
90    display::{Orientation, tiling},
91    touch,
92};
93use embassy_rp::Peri;
94use embassy_rp::gpio::Pin;
95use embassy_rp::peripherals::{SPI0, SPI1};
96use embassy_rp::spi::{ClkPin, MisoPin, MosiPin};
97use embedded_graphics::{
98    Pixel,
99    mono_font::MonoFont,
100    pixelcolor::{IntoStorage, Rgb565, Rgb888},
101    prelude::{Dimensions, DrawTarget, OriginDimensions, Point, Size},
102    primitives::Rectangle,
103};
104use embedded_hal::spi::SpiDevice;
105use static_cell::StaticCell;
106
107use buffer::DynPixelBuffer;
108use buffer::{PixelBuffer, RegionView};
109pub use display::DEFAULT_DISPLAY_SPI_HZ;
110pub use one_spi::{CydRpOneSpi, CydRpOneSpiStatic};
111pub use text::DEFAULT_FONT;
112use touch_driver::TOUCH_SPI_HZ;
113
114use crate::flash_block::FlashBlockRp;
115use display::CydDisplayRp as CydDisplayRpDevice;
116use touch_driver::CydTouchRp as CydTouchRpDevice;
117
118/// An owned CYD-family RP display component.
119///
120/// `D` is the underlying `embedded-hal` SPI device type; it defaults to an
121/// exclusively-owned SPI peripheral. Shared-bus backends (see
122/// [`CydRpOneSpi`]) instantiate this with a shared-bus device instead.
123///
124/// Start with the [`cyd`](mod@crate::cyd) module example. The display-only
125/// constructor and static-storage pattern are shown by the
126/// [`CydDisplayRp::new`] example.
127pub struct CydDisplayRp<D: SpiDevice<u8> = display::CydDisplaySpiDevice> {
128    display: CydDisplayRpDevice<D>,
129    orientation: Orientation,
130    // Every CydRp owns exactly one draw buffer. Apps that don't draw through it
131    // pass a zero-sized buffer (e.g. `CydStaticRp<0>`).
132    pixel_buffer: &'static mut dyn DynPixelBuffer,
133    // Default drawing style. Background clears the device at construction and
134    // fills every new frame; foreground color and font drive `CydFrameRp::write_text`.
135    // The `Rgb565` versions are precomputed so the hot drawing paths skip the
136    // per-call conversion.
137    background_color: Rgb888,
138    foreground_color: Rgb888,
139    background565: Rgb565,
140    foreground565: Rgb565,
141    font: &'static MonoFont<'static>,
142}
143
144/// An owned uncalibrated CYD touch component for RP boards.
145///
146/// `D` is the underlying `embedded-hal` SPI device type; see
147/// [`CydDisplayRp`] for the shared-bus rationale.
148pub(crate) struct CydTouchUncalibratedRp<D = touch_driver::CydTouchSpiDevice> {
149    touch: CydTouchRpDevice<D>,
150}
151
152/// An owned calibrated CYD-family RP touch component.
153///
154/// Start with the [`cyd`](mod@crate::cyd) module example. Construction
155/// is covered by [`CydRp::new`]; applications then call the calibrated
156/// [`CydTouch::try_read`] operation.
157pub struct CydTouchRp<D = touch_driver::CydTouchSpiDevice> {
158    raw: CydTouchUncalibratedRp<D>,
159    calibration_config: CalibrationConfig,
160    orientation: Orientation,
161}
162
163/// An RP CYD device containing a display and calibrated touch input.
164///
165/// [`CydRp::new_static`] creates the pixel buffer storage passed to
166/// [`CydRp::new`], which constructs the hardware and loads or performs touch
167/// calibration. See the [`cyd`](mod@crate::cyd) module example for normal
168/// drawing and touch input.
169pub struct CydRp {
170    /// The display component.
171    pub display: CydDisplayRp,
172    /// The calibrated touch component.
173    pub touch: CydTouchRp,
174}
175
176/// An uncalibrated CYD RP bundle.
177pub(crate) struct CydRpUncalibrated {
178    /// The owned display component.
179    pub display: CydDisplayRp,
180    /// The owned uncalibrated touch component.
181    pub touch: CydTouchUncalibratedRp,
182}
183
184/// Static storage for a [`CydRp`]-owned pixel buffer.
185///
186/// `PIXEL_COUNT` is an RGB565 pixel count, not a byte count. Choose its capacity
187/// through [`CydRp::new_static`]. Declare the storage at file scope:
188///
189/// ```rust,no_run
190/// #![no_std]
191/// #![no_main]
192/// use device_envoy_rp::cyd::{CydRp, CydStaticRp};
193/// static CYD_STATIC: CydStaticRp<{ CydRp::SCREEN_PIXELS }> = CydRp::new_static();
194/// # #[panic_handler]
195/// # fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} }
196/// ```
197pub struct CydStaticRp<const PIXEL_COUNT: usize> {
198    pixel_buffer: StaticCell<PixelBuffer<PIXEL_COUNT>>,
199}
200
201impl<const PIXEL_COUNT: usize> CydStaticRp<PIXEL_COUNT> {
202    /// Internal constructor. Apps create storage via [`CydRp::new_static`] so all
203    /// construction goes through the `CydRp` device abstraction.
204    pub(crate) const fn new() -> Self {
205        assert!(
206            PIXEL_COUNT <= SCREEN_PIXELS,
207            "PIXEL_COUNT must not exceed SCREEN_PIXELS"
208        );
209        Self {
210            pixel_buffer: StaticCell::new(),
211        }
212    }
213}
214
215/// The RP implementation of
216/// [`CydFrame`](https://docs.rs/device-envoy-core/latest/device_envoy_core/cyd/display/trait.CydFrame.html).
217///
218/// Frames are returned by [`CydDisplay::frame_mut`]. See the portable
219/// [`CydFrame`](https://docs.rs/device-envoy-core/latest/device_envoy_core/cyd/display/trait.CydFrame.html)
220/// documentation for normal drawing.
221pub struct CydFrameRp<'a, D: SpiDevice<u8> = display::CydDisplaySpiDevice> {
222    display: &'a mut CydDisplayRpDevice<D>,
223    view: RegionView<'a>,
224    // Where this frame presents and how large it is: set from the `Rectangle`
225    // passed to `frame_mut`, so `flush` needs no separate position argument.
226    rectangle: Rectangle,
227    // Default foreground color and font, copied from the owning `CydDisplayRp`, so
228    // `write_text` can render with the device default style.
229    pub(crate) background565: Rgb565,
230    pub(crate) foreground565: Rgb565,
231    pub(crate) font: &'static MonoFont<'static>,
232}
233
234impl<'a, D: SpiDevice<u8>> CydFrameRp<'a, D> {
235    /// Fill the frame with an explicit color.
236    ///
237    /// See the portable
238    /// [`CydFrame::fill`](https://docs.rs/device-envoy-core/latest/device_envoy_core/cyd/display/trait.CydFrame.html#tymethod.fill)
239    /// documentation.
240    pub fn fill(&mut self, color: Rgb565) -> &mut Self {
241        self.view.fill(color);
242        self
243    }
244
245    /// The buffered frame region's width in pixels.
246    #[must_use]
247    pub fn width(&self) -> usize {
248        self.view.width()
249    }
250
251    /// The buffered frame region's height in pixels.
252    #[must_use]
253    pub fn height(&self) -> usize {
254        self.view.height()
255    }
256
257    /// Borrow the buffered frame region's raw RGB565 pixels in row-major order.
258    pub fn raw_pixels_mut(&mut self) -> &mut [u16] {
259        self.view.raw_pixels_mut()
260    }
261
262    /// Present this frame's pixels at its rectangle's top-left (set by
263    /// [`CydDisplay::frame_mut`]).
264    ///
265    /// This inherent method synchronously writes the buffered rectangle over
266    /// SPI. In generic
267    /// code, call [`CydFrame::flush`](https://docs.rs/device-envoy-core/latest/device_envoy_core/cyd/display/trait.CydFrame.html#tymethod.flush)
268    /// and await its future instead.
269    pub fn flush(&mut self) -> Result<(), Error> {
270        Ok(self.display.flush_buffer(
271            self.view.size().width as usize,
272            self.view.size().height as usize,
273            self.view.raw_pixels(),
274            self.rectangle.top_left,
275        )?)
276    }
277
278    fn local_x(&self, x: i32) -> Option<usize> {
279        usize::try_from(x.checked_sub(self.rectangle.top_left.x)?).ok()
280    }
281
282    fn local_y(&self, y: i32) -> Option<usize> {
283        usize::try_from(y.checked_sub(self.rectangle.top_left.y)?).ok()
284    }
285}
286
287impl<D: SpiDevice<u8>> DrawTarget for CydFrameRp<'_, D> {
288    type Color = Rgb565;
289    type Error = Infallible;
290
291    fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
292        self.fill(color);
293        Ok(())
294    }
295
296    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
297    where
298        I: IntoIterator<Item = Pixel<Self::Color>>,
299    {
300        for Pixel(point, color) in pixels {
301            let Some(local_x) = self.local_x(point.x) else {
302                continue;
303            };
304            let Some(local_y) = self.local_y(point.y) else {
305                continue;
306            };
307            if local_x < self.view.width() && local_y < self.view.height() {
308                let index = local_y * self.view.width() + local_x;
309                self.raw_pixels_mut()[index] = color.into_storage();
310            }
311        }
312        Ok(())
313    }
314}
315
316impl<D: SpiDevice<u8>> Dimensions for CydFrameRp<'_, D> {
317    fn bounding_box(&self) -> Rectangle {
318        self.rectangle
319    }
320}
321
322impl<D: SpiDevice<u8>> PixelTarget for CydFrameRp<'_, D> {
323    fn width(&self) -> usize {
324        usize::try_from(self.rectangle.top_left.x)
325            .expect("frame top-left x must be non-negative")
326            .checked_add(self.width())
327            .expect("frame width must fit in usize")
328    }
329
330    fn height(&self) -> usize {
331        usize::try_from(self.rectangle.top_left.y)
332            .expect("frame top-left y must be non-negative")
333            .checked_add(self.height())
334            .expect("frame height must fit in usize")
335    }
336
337    fn put_pixel(&mut self, x: usize, y: usize, color: Rgb888) {
338        let Some(local_x) = self.local_x(x as i32) else {
339            return;
340        };
341        let Some(local_y) = self.local_y(y as i32) else {
342            return;
343        };
344        if local_x >= self.view.width() || local_y >= self.view.height() {
345            return;
346        }
347        let stride = self.view.width();
348        self.raw_pixels_mut()[local_y * stride + local_x] = Rgb565::from(color).into_storage();
349    }
350
351    /// The frame buffer already stores RGB565, so a decoded image pixel can be
352    /// written verbatim with no RGB888 round-trip.
353    fn put_pixel_565(&mut self, x: usize, y: usize, rgb565: u16) {
354        let Some(local_x) = self.local_x(x as i32) else {
355            return;
356        };
357        let Some(local_y) = self.local_y(y as i32) else {
358            return;
359        };
360        if local_x >= self.view.width() || local_y >= self.view.height() {
361            return;
362        }
363        let stride = self.view.width();
364        self.raw_pixels_mut()[local_y * stride + local_x] = rgb565;
365    }
366}
367
368#[derive(Debug)]
369/// Error from a CYD RP display or touch operation.
370///
371/// See the [`CydRp::new`] constructor example, which propagates this error.
372pub enum Error {
373    /// The display panel could not be initialized.
374    /// See the [`CydRp::new`] constructor example.
375    InitDisplay,
376    /// A frame could not be flushed to the display.
377    /// See the [`CydRp::new`] constructor example.
378    FlushFrameBuffer,
379    /// Changing the display orientation failed.
380    /// See the [`CydRp::new`] constructor example.
381    SetOrientation,
382}
383
384impl<D: SpiDevice<u8>> CydDisplayRp<D> {
385    fn set_orientation(&mut self, orientation: Orientation) -> Result<(), Error> {
386        self.display.set_orientation(orientation)?;
387        self.orientation = orientation;
388        Ok(())
389    }
390
391    fn from_display_device(
392        mut display: CydDisplayRpDevice<D>,
393        orientation: Orientation,
394        background_color: Rgb888,
395        foreground_color: Rgb888,
396        font: &'static MonoFont<'static>,
397        pixel_buffer: &'static mut dyn DynPixelBuffer,
398    ) -> Result<Self, Error> {
399        let background565 = rgb565(background_color);
400        display.fill(background565)?;
401
402        Ok(Self {
403            display,
404            orientation,
405            pixel_buffer,
406            background_color,
407            foreground_color,
408            background565,
409            foreground565: rgb565(foreground_color),
410            font,
411        })
412    }
413
414    /// Construct a display component from an already-built SPI device.
415    ///
416    /// Used by shared-bus backends (see [`CydRpOneSpi`]) that build their own
417    /// `SpiDevice` instead of owning an exclusive SPI peripheral.
418    #[expect(clippy::too_many_arguments, reason = "mirrors CydDisplayRp::new")]
419    pub(crate) fn new_from_device<Dc, Rst, Backlight>(
420        spi_device: D,
421        dc_pin: Peri<'static, Dc>,
422        rst_pin: Peri<'static, Rst>,
423        backlight_pin: Peri<'static, Backlight>,
424        orientation: Orientation,
425        background_color: Rgb888,
426        foreground_color: Rgb888,
427        font: &'static MonoFont<'static>,
428        pixel_buffer: &'static mut dyn DynPixelBuffer,
429    ) -> Result<Self, Error>
430    where
431        Dc: Pin,
432        Rst: Pin,
433        Backlight: Pin,
434    {
435        let display = CydDisplayRpDevice::new_from_device(
436            spi_device,
437            dc_pin,
438            rst_pin,
439            backlight_pin,
440            orientation,
441        )?;
442        Self::from_display_device(
443            display,
444            orientation,
445            background_color,
446            foreground_color,
447            font,
448            pixel_buffer,
449        )
450    }
451}
452
453impl CydDisplayRp<display::CydDisplaySpiDevice> {
454    /// Construct a display-only CYD display component that owns its draw buffer.
455    ///
456    /// Choosing the pixel buffer capacity is the most important construction
457    /// decision: `statics` determines both static RAM use and the largest
458    /// buffered region. See [`CydRp::new_static`] for the sizing choices.
459    ///
460    /// ```rust,no_run
461    /// # #![no_std]
462    /// # #![no_main]
463    /// use device_envoy_rp::{Result, cyd::{CydDisplay, CydDisplayRp, CydRp, DEFAULT_DISPLAY_SPI_HZ, DEFAULT_FONT, Orientation}};
464    /// use embedded_graphics::{pixelcolor::Rgb888, prelude::RgbColor};
465    /// # #[panic_handler]
466    /// # fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} }
467    /// async fn construct(p: embassy_rp::Peripherals) -> Result<()> {
468    ///     static CYD_STATIC: device_envoy_rp::cyd::CydStaticRp<0> = CydRp::new_static();
469    ///     let display = CydDisplayRp::new(&CYD_STATIC, p.SPI0, p.PIN_18, p.PIN_19, p.PIN_16,
470    ///         p.PIN_17, p.PIN_20, p.PIN_21, p.PIN_22, DEFAULT_DISPLAY_SPI_HZ,
471    ///         Orientation::Landscape, Rgb888::BLACK, Rgb888::WHITE, &DEFAULT_FONT)?;
472    ///     assert_eq!(display.screen_size(), Orientation::Landscape.size());
473    ///     Ok(())
474    /// }
475    /// ```
476    #[expect(
477        clippy::too_many_arguments,
478        reason = "mirrors CydEsp's constructor shape"
479    )]
480    pub fn new<const PIXEL_COUNT: usize, Sck, Mosi, Miso, Cs, Dc, Rst, Backlight>(
481        statics: &'static CydStaticRp<PIXEL_COUNT>,
482        display_spi: Peri<'static, SPI0>,
483        display_sck_pin: Peri<'static, Sck>,
484        display_mosi_pin: Peri<'static, Mosi>,
485        display_miso_pin: Peri<'static, Miso>,
486        display_cs_pin: Peri<'static, Cs>,
487        display_dc_pin: Peri<'static, Dc>,
488        display_rst_pin: Peri<'static, Rst>,
489        display_backlight_pin: Peri<'static, Backlight>,
490        display_spi_hz: u32,
491        orientation: Orientation,
492        background_color: Rgb888,
493        foreground_color: Rgb888,
494        font: &'static MonoFont<'static>,
495    ) -> Result<Self, Error>
496    where
497        Sck: Pin + ClkPin<SPI0>,
498        Mosi: Pin + MosiPin<SPI0>,
499        Miso: Pin + MisoPin<SPI0>,
500        Cs: Pin,
501        Dc: Pin,
502        Rst: Pin,
503        Backlight: Pin,
504    {
505        let pixel_buffer = PixelBuffer::init_static(&statics.pixel_buffer);
506        let display = CydDisplayRpDevice::new(
507            display_spi,
508            display_sck_pin,
509            display_mosi_pin,
510            display_miso_pin,
511            display_cs_pin,
512            display_dc_pin,
513            display_rst_pin,
514            display_backlight_pin,
515            display_spi_hz,
516            orientation,
517        )?;
518        Self::from_display_device(
519            display,
520            orientation,
521            background_color,
522            foreground_color,
523            font,
524            pixel_buffer,
525        )
526    }
527}
528
529impl<D: SpiDevice<u8>> CydTouchUncalibratedRp<D> {
530    /// Construct an uncalibrated touch component from an already-built SPI device.
531    ///
532    /// Used by shared-bus backends (see [`CydRpOneSpi`]) that build their own
533    /// `SpiDevice` instead of owning an exclusive SPI peripheral.
534    pub(crate) fn from_device<Irq: Pin>(
535        touch_spi_device: D,
536        touch_irq_pin: Peri<'static, Irq>,
537    ) -> Self {
538        Self {
539            touch: CydTouchRpDevice::from_device(touch_spi_device, touch_irq_pin),
540        }
541    }
542}
543
544impl CydTouchUncalibratedRp<touch_driver::CydTouchSpiDevice> {
545    /// Construct an uncalibrated touch component.
546    #[expect(clippy::too_many_arguments, reason = "mirrors CydDisplayRp::new")]
547    pub(crate) fn new<TouchSck, TouchMosi, TouchMiso, TouchCs, TouchIrq>(
548        touch_spi: Peri<'static, SPI1>,
549        touch_sck_pin: Peri<'static, TouchSck>,
550        touch_mosi_pin: Peri<'static, TouchMosi>,
551        touch_miso_pin: Peri<'static, TouchMiso>,
552        touch_cs_pin: Peri<'static, TouchCs>,
553        touch_irq_pin: Peri<'static, TouchIrq>,
554    ) -> Result<Self, Error>
555    where
556        TouchSck: Pin + ClkPin<SPI1>,
557        TouchMosi: Pin + MosiPin<SPI1>,
558        TouchMiso: Pin + MisoPin<SPI1>,
559        TouchCs: Pin,
560        TouchIrq: Pin,
561    {
562        Ok(Self {
563            touch: CydTouchRpDevice::new(
564                touch_spi,
565                touch_sck_pin,
566                touch_mosi_pin,
567                touch_miso_pin,
568                touch_cs_pin,
569                touch_irq_pin,
570            )?,
571        })
572    }
573}
574
575impl CydRp {
576    /// Total pixel count of the CYD panel — fixed hardware, independent of orientation.
577    ///
578    /// Used by the [`CydStaticRp`] storage example.
579    pub const SCREEN_PIXELS: usize = SCREEN_PIXELS;
580
581    /// Create static storage for a CYD pixel buffer.
582    ///
583    /// Choose any `PIXEL_COUNT` from zero through [`CydRp::SCREEN_PIXELS`].
584    ///
585    /// - `0` allocates no pixel buffer, so only
586    ///   [immediate operations](CydDisplay::fill_rectangle) and
587    ///   [contiguous streaming](CydDisplay::fill_contiguous) are available.
588    /// - A regional buffer can be sized for the largest rectangle requested
589    ///   through [`CydDisplay::frame_mut`].
590    /// - For tiled drawing, size the buffer to
591    ///   [`tiling::TileGrid::max_tile_pixel_count`], then pass the grid to
592    ///   [`CydDisplay::for_each_tile`]. Only one tile is buffered at a time.
593    /// - [`CydRp::SCREEN_PIXELS`] allocates a full-screen buffer and is usually
594    ///   the most convenient choice when enough RAM is available.
595    ///
596    /// Attempting to create a frame or tile larger than the allocated buffer
597    /// panics.
598    #[must_use]
599    pub const fn new_static<const PIXEL_COUNT: usize>() -> CydStaticRp<PIXEL_COUNT> {
600        CydStaticRp::new()
601    }
602
603    /// Construct a ready-to-use CYD.
604    ///
605    /// The display and touch controllers use separate SPI peripherals. The
606    /// supplied flash block stores touch calibration, and
607    /// `recalibration_button` requests interactive recalibration.
608    ///
609    /// Choosing the pixel buffer capacity is the most important construction
610    /// decision: `statics` determines both static RAM use and the largest
611    /// buffered region. See [`CydRp::new_static`] for the sizing choices.
612    ///
613    /// Use [`CydRpOneSpi`] for boards where display and touch share one SPI
614    /// peripheral.
615    ///
616    /// This example focuses on the board-specific construction. For the normal
617    /// draw/flush/read loop, start with the [`cyd`](mod@crate::cyd) module
618    /// example. For complete startup and wiring in a real program, see the
619    /// [checked Raspberry Pi Pico CYD touch-paint example](https://github.com/CarlKCarlK/device-envoy/blob/main/crates/device-envoy-examples-rp/examples/cyd_touch_paint.rs).
620    ///
621    /// ```rust,no_run
622    /// # #![no_std]
623    /// # #![no_main]
624    /// # #[panic_handler]
625    /// # fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} }
626    /// # use device_envoy_rp::{Result, button::{ButtonRp, PressedTo}, cyd::{CydRp, CydStaticRp, DEFAULT_DISPLAY_SPI_HZ, DEFAULT_FONT, Orientation}, flash_block::FlashBlockRp};
627    /// # use embedded_graphics::{pixelcolor::Rgb888, prelude::RgbColor};
628    /// async fn construct(p: embassy_rp::Peripherals) -> Result<()> {
629    ///     let [mut calibration_flash] = FlashBlockRp::new_array::<1>(p.FLASH)?;
630    ///     let mut recalibration_button = ButtonRp::new(p.PIN_15, PressedTo::Ground);
631    ///     static CYD_STATIC: CydStaticRp<{ CydRp::SCREEN_PIXELS }> = CydRp::new_static();
632    ///
633    ///     let cyd = CydRp::new(
634    ///         &CYD_STATIC,
635    ///
636    ///         // Display SPI and pins:
637    ///         p.SPI0,
638    ///         p.PIN_18,
639    ///         p.PIN_19,
640    ///         p.PIN_16,
641    ///         p.PIN_17,
642    ///         p.PIN_20,
643    ///         p.PIN_21,
644    ///         p.PIN_22,
645    ///         DEFAULT_DISPLAY_SPI_HZ,
646    ///
647    ///         // Presentation:
648    ///         Orientation::Landscape,
649    ///         Rgb888::BLACK,
650    ///         Rgb888::WHITE,
651    ///         &DEFAULT_FONT,
652    ///
653    ///         // Touch SPI and pins:
654    ///         p.SPI1,
655    ///         p.PIN_10,
656    ///         p.PIN_11,
657    ///         p.PIN_12,
658    ///         p.PIN_13,
659    ///         p.PIN_14,
660    ///
661    ///         // Calibration storage and recalibration button:
662    ///         &mut calibration_flash,
663    ///         &mut recalibration_button,
664    ///     )
665    ///     .await?;
666    ///
667    ///     Ok(())
668    /// }
669    /// ```
670    #[expect(clippy::too_many_arguments, reason = "mirrors CydDisplayRp::new")]
671    pub async fn new<
672        const PIXEL_COUNT: usize,
673        Sck,
674        Mosi,
675        Miso,
676        Cs,
677        Dc,
678        Rst,
679        Backlight,
680        TouchSck,
681        TouchMosi,
682        TouchMiso,
683        TouchCs,
684        TouchIrq,
685    >(
686        statics: &'static CydStaticRp<PIXEL_COUNT>,
687        display_spi: Peri<'static, SPI0>,
688        display_sck_pin: Peri<'static, Sck>,
689        display_mosi_pin: Peri<'static, Mosi>,
690        display_miso_pin: Peri<'static, Miso>,
691        display_cs_pin: Peri<'static, Cs>,
692        display_dc_pin: Peri<'static, Dc>,
693        display_rst_pin: Peri<'static, Rst>,
694        display_backlight_pin: Peri<'static, Backlight>,
695        display_spi_hz: u32,
696        orientation: Orientation,
697        background_color: Rgb888,
698        foreground_color: Rgb888,
699        font: &'static MonoFont<'static>,
700        touch_spi: Peri<'static, SPI1>,
701        touch_sck_pin: Peri<'static, TouchSck>,
702        touch_mosi_pin: Peri<'static, TouchMosi>,
703        touch_miso_pin: Peri<'static, TouchMiso>,
704        touch_cs_pin: Peri<'static, TouchCs>,
705        touch_irq_pin: Peri<'static, TouchIrq>,
706        calibration_flash_block: &mut FlashBlockRp,
707        recalibration_button: &mut impl Button,
708    ) -> crate::Result<Self>
709    where
710        Sck: Pin + ClkPin<SPI0>,
711        Mosi: Pin + MosiPin<SPI0>,
712        Miso: Pin + MisoPin<SPI0>,
713        Cs: Pin,
714        Dc: Pin,
715        Rst: Pin,
716        Backlight: Pin,
717        TouchSck: Pin + ClkPin<SPI1>,
718        TouchMosi: Pin + MosiPin<SPI1>,
719        TouchMiso: Pin + MisoPin<SPI1>,
720        TouchCs: Pin,
721        TouchIrq: Pin,
722    {
723        let CydRpUncalibrated { mut display, touch } = CydRpUncalibrated::new(
724            statics,
725            display_spi,
726            display_sck_pin,
727            display_mosi_pin,
728            display_miso_pin,
729            display_cs_pin,
730            display_dc_pin,
731            display_rst_pin,
732            display_backlight_pin,
733            display_spi_hz,
734            orientation,
735            background_color,
736            foreground_color,
737            font,
738            touch_spi,
739            touch_sck_pin,
740            touch_mosi_pin,
741            touch_miso_pin,
742            touch_cs_pin,
743            touch_irq_pin,
744        )?;
745        let touch = backend::ensure_calibration(
746            &mut display,
747            touch,
748            calibration_flash_block,
749            recalibration_button,
750            None,
751            orientation,
752        )
753        .await
754        .map_err(|error| match error {
755            backend::Error::Device(cyd_error) => crate::Error::from(cyd_error),
756            backend::Error::Flash(flash_error) => flash_error,
757        })?;
758        display.set_orientation(orientation)?;
759        Ok(Self { display, touch })
760    }
761}
762
763impl Cyd for CydRp {
764    type Error = Error;
765    type Display = CydDisplayRp;
766    type Touch = CydTouchRp;
767
768    fn parts(&mut self) -> (&mut Self::Display, &mut Self::Touch) {
769        (&mut self.display, &mut self.touch)
770    }
771
772    fn orientation(&self) -> Orientation {
773        self.display.orientation
774    }
775}
776
777impl CydRpUncalibrated {
778    /// Construct an uncalibrated CYD bundle from display and touch parts.
779    #[expect(clippy::too_many_arguments, reason = "mirrors CydDisplayRp::new")]
780    pub(crate) fn new<
781        const PIXEL_COUNT: usize,
782        Sck,
783        Mosi,
784        Miso,
785        Cs,
786        Dc,
787        Rst,
788        Backlight,
789        TouchSck,
790        TouchMosi,
791        TouchMiso,
792        TouchCs,
793        TouchIrq,
794    >(
795        statics: &'static CydStaticRp<PIXEL_COUNT>,
796        display_spi: Peri<'static, SPI0>,
797        display_sck_pin: Peri<'static, Sck>,
798        display_mosi_pin: Peri<'static, Mosi>,
799        display_miso_pin: Peri<'static, Miso>,
800        display_cs_pin: Peri<'static, Cs>,
801        display_dc_pin: Peri<'static, Dc>,
802        display_rst_pin: Peri<'static, Rst>,
803        display_backlight_pin: Peri<'static, Backlight>,
804        display_spi_hz: u32,
805        _orientation: Orientation,
806        background_color: Rgb888,
807        foreground_color: Rgb888,
808        font: &'static MonoFont<'static>,
809        touch_spi: Peri<'static, SPI1>,
810        touch_sck_pin: Peri<'static, TouchSck>,
811        touch_mosi_pin: Peri<'static, TouchMosi>,
812        touch_miso_pin: Peri<'static, TouchMiso>,
813        touch_cs_pin: Peri<'static, TouchCs>,
814        touch_irq_pin: Peri<'static, TouchIrq>,
815    ) -> Result<Self, Error>
816    where
817        Sck: Pin + ClkPin<SPI0>,
818        Mosi: Pin + MosiPin<SPI0>,
819        Miso: Pin + MisoPin<SPI0>,
820        Cs: Pin,
821        Dc: Pin,
822        Rst: Pin,
823        Backlight: Pin,
824        TouchSck: Pin + ClkPin<SPI1>,
825        TouchMosi: Pin + MosiPin<SPI1>,
826        TouchMiso: Pin + MisoPin<SPI1>,
827        TouchCs: Pin,
828        TouchIrq: Pin,
829    {
830        Ok(Self {
831            display: CydDisplayRp::new(
832                statics,
833                display_spi,
834                display_sck_pin,
835                display_mosi_pin,
836                display_miso_pin,
837                display_cs_pin,
838                display_dc_pin,
839                display_rst_pin,
840                display_backlight_pin,
841                display_spi_hz,
842                Orientation::Landscape,
843                background_color,
844                foreground_color,
845                font,
846            )?,
847            touch: CydTouchUncalibratedRp::new(
848                touch_spi,
849                touch_sck_pin,
850                touch_mosi_pin,
851                touch_miso_pin,
852                touch_cs_pin,
853                touch_irq_pin,
854            )?,
855        })
856    }
857}
858
859fn rgb565(color: Rgb888) -> Rgb565 {
860    Rgb565::from(color)
861}
862
863impl<D: SpiDevice<u8>> fmt::Debug for CydDisplayRp<D> {
864    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
865        formatter
866            .debug_struct("CydDisplayRp")
867            .field("orientation", &self.orientation)
868            .finish_non_exhaustive()
869    }
870}
871
872impl<D> fmt::Debug for CydTouchUncalibratedRp<D> {
873    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
874        formatter
875            .debug_struct("CydTouchUncalibratedRp")
876            .finish_non_exhaustive()
877    }
878}
879
880impl<D> fmt::Debug for CydTouchRp<D> {
881    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
882        formatter
883            .debug_struct("CydTouchRp")
884            .field("calibration_config", &self.calibration_config)
885            .field("orientation", &self.orientation)
886            .finish_non_exhaustive()
887    }
888}
889
890impl fmt::Debug for CydRp {
891    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
892        formatter
893            .debug_struct("CydRp")
894            .field("orientation", &self.display.orientation)
895            .finish_non_exhaustive()
896    }
897}
898
899impl fmt::Debug for CydRpUncalibrated {
900    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
901        formatter
902            .debug_struct("CydRpUncalibrated")
903            .field("orientation", &self.display.orientation)
904            .finish_non_exhaustive()
905    }
906}
907
908impl<D: SpiDevice<u8>> backend::DisplayBackend for CydDisplayRp<D> {
909    type Error = Error;
910    type Frame<'a>
911        = CydFrameRp<'a, D>
912    where
913        Self: 'a;
914
915    fn create_frame_mut(&mut self, rectangle: Rectangle) -> Self::Frame<'_> {
916        self.display.make_frame(
917            &mut *self.pixel_buffer,
918            rectangle,
919            self.background565,
920            self.foreground565,
921            self.font,
922        )
923    }
924}
925
926impl<D: SpiDevice<u8>> CydDisplay for CydDisplayRp<D> {
927    #[inline]
928    fn screen_size(&self) -> Size {
929        self.display.size()
930    }
931
932    fn background_color(&self) -> Rgb888 {
933        self.background_color
934    }
935
936    fn foreground_color(&self) -> Rgb888 {
937        self.foreground_color
938    }
939
940    fn background_565(&self) -> Rgb565 {
941        self.background565
942    }
943
944    fn foreground_565(&self) -> Rgb565 {
945        self.foreground565
946    }
947
948    #[inline]
949    fn fill_rectangle(&mut self, rectangle: Rectangle, color: Rgb565) -> Result<(), Error> {
950        Ok(self.display.fill_rectangle(rectangle, color)?)
951    }
952
953    #[inline]
954    fn fill_contiguous<I>(&mut self, rectangle: Rectangle, pixels: I) -> Result<(), Error>
955    where
956        I: IntoIterator<Item = Rgb565>,
957    {
958        Ok(self.display.fill_contiguous(rectangle, pixels)?)
959    }
960}
961
962impl<D: SpiDevice<u8>> TouchUncalibrated for CydTouchUncalibratedRp<D> {
963    type Error = Error;
964    type Calibrated = CydTouchRp<D>;
965
966    fn read_raw_touch_event(&mut self) -> Result<Option<RawTouchEvent>, Self::Error> {
967        Ok(self.touch.read_raw_touch_event())
968    }
969
970    fn calibrate(
971        self,
972        calibration_config: CalibrationConfig,
973        orientation: Orientation,
974    ) -> Self::Calibrated {
975        CydTouchRp {
976            raw: self,
977            calibration_config,
978            orientation,
979        }
980    }
981}
982
983impl<D: SpiDevice<u8>> CydTouch for CydTouchRp<D> {
984    type Error = Error;
985
986    fn try_read(&mut self) -> Result<Option<TouchEvent>, Error> {
987        Ok(self
988            .raw
989            .touch
990            .read_raw_touch_event()
991            .map(|raw_touch_event| match raw_touch_event {
992                RawTouchEvent::Down { raw_x, raw_y } => {
993                    let (x, y) = self.calibration_config.map_raw_to_screen(raw_x, raw_y);
994                    TouchEvent::Down {
995                        point: self
996                            .orientation
997                            .map_landscape_point(Point::new(x as i32, y as i32)),
998                    }
999                }
1000                RawTouchEvent::Move { raw_x, raw_y } => {
1001                    let (x, y) = self.calibration_config.map_raw_to_screen(raw_x, raw_y);
1002                    TouchEvent::Move {
1003                        point: self
1004                            .orientation
1005                            .map_landscape_point(Point::new(x as i32, y as i32)),
1006                    }
1007                }
1008                RawTouchEvent::Up => TouchEvent::Up,
1009            }))
1010    }
1011}
1012
1013impl<D: SpiDevice<u8>> CydFrame for CydFrameRp<'_, D> {
1014    type Error = Error;
1015
1016    fn rectangle(&self) -> Rectangle {
1017        self.rectangle
1018    }
1019
1020    fn fill(&mut self, color: Rgb565) -> &mut Self {
1021        CydFrameRp::fill(self, color)
1022    }
1023
1024    fn clear(&mut self) -> &mut Self {
1025        self.fill(self.background565)
1026    }
1027
1028    fn write_text(&mut self, text: &str) -> &mut Self {
1029        CydFrameRp::write_text(self, text)
1030    }
1031
1032    fn copy_from_565(&mut self, src: &[u16]) -> device_envoy_core::Result<()> {
1033        let dst = self.raw_pixels_mut();
1034        if dst.len() != src.len() {
1035            return Err(device_envoy_core::Error::CopySize {
1036                src_len: src.len(),
1037                frame_len: dst.len(),
1038            });
1039        }
1040        dst.copy_from_slice(src);
1041        Ok(())
1042    }
1043
1044    // Flushing the panel over SPI is synchronous, so this future resolves on its
1045    // first poll. The `async fn` is the device-agnostic frame boundary the
1046    // render loop awaits; on the MCU it adds no suspension.
1047    async fn flush(&mut self) -> Result<(), Error> {
1048        CydFrameRp::flush(self)
1049    }
1050}