WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Conversation

@ahdinosaur
Copy link
Owner

@ahdinosaur ahdinosaur commented Oct 8, 2025

πŸ‘©β€πŸ”¬ πŸ§ͺ πŸ’₯

If we can off-load sending bits to the LEDs using hardware, then we can prepare the bits for the next frame while the current frame is being sent.

This pull request is a big step towards making this possible.

One realization to make this work is that yes the generic_const_exprs feature would make no-alloc much nicer, BUT IN THE MEANTIME we can achieve the same outcomes (with a worse user experience) by using generic constants without expressions. Since you can't do constant calculations using generics (e.g. we can't calculate the buffer size needed using other generic types we already have, such as the type of LED which has a constant function for this), the user must calculate them on their side and pass in to your type arguments. We can use all our builder pattern skills to make this as ergonomic as possible.

Changes:

  • Change Driver (and DriverAsync) trait interface
pub trait Driver {
    /// The error type that may be returned by the driver.
    type Error;

    /// The color type accepted by the driver.
    type Color;

    /// The word of the frame buffer.
    type Word;

    /// Encodes an update frame buffer for the LED hardware.
    ///
    /// # Type Parameters
    ///
    /// * `PIXEL_COUNT` - Number of pixels in frame
    /// * `FRAME_BUFFER_SIZE` - Length of encoded frame buffer, in words.
    /// * `Pixels` - Iterator of colors for each pixel
    /// * `Color` - Type of each pixel
    ///
    /// # Arguments
    ///
    /// * `pixels` - Iterator of colors for each pixel
    /// * `brightness` - Global brightness scaling factor (0.0 to 1.0)
    /// * `correction` - Color correction factors
    ///
    /// # Returns
    ///
    /// Result with frame buffer
    fn encode<const PIXEL_COUNT: usize, const FRAME_BUFFER_SIZE: usize, Pixels, Color>(
        &mut self,
        pixels: Pixels,
        brightness: f32,
        correction: ColorCorrection,
    ) -> Vec<Self::Word, FRAME_BUFFER_SIZE>
    where
        Pixels: IntoIterator<Item = Color>,
        Self::Color: FromColor<Color>;

    /// Writes frame buffer to the LED hardware.
    ///
    /// # Type Parameters
    ///
    /// * `FRAME_BUFFER_SIZE` - Length of encoded frame buffer, in words.
    ///
    /// # Arguments
    ///
    /// * `frame` - Frame buffer
    ///
    /// # Returns
    ///
    /// Result indicating success or an error
    fn write<const FRAME_BUFFER_SIZE: usize>(
        &mut self,
        frame: Vec<Self::Word, FRAME_BUFFER_SIZE>,
        brightness: f32,
        correction: ColorCorrection,
    ) -> Result<(), Self::Error>;

    /// Shows a frame on the LED hardware.
    ///
    /// # Type Parameters
    ///
    /// * `PIXEL_COUNT` - Number of pixels in frame
    /// * `FRAME_BUFFER_SIZE` - Length of encoded frame buffer, in words.
    /// * `Pixels` - Iterator of colors for each pixel
    /// * `Color` - Type of each pixel
    ///
    /// # Arguments
    ///
    /// * `pixels` - Iterator of colors for each pixel
    /// * `brightness` - Global brightness scaling factor (0.0 to 1.0)
    /// * `correction` - Color correction factors
    ///
    /// # Returns
    ///
    /// Result indicating success or an error
    fn show<const PIXEL_COUNT: usize, const FRAME_BUFFER_SIZE: usize, I, C>(
        &mut self,
        pixels: I,
        brightness: f32,
        correction: ColorCorrection,
    ) -> Result<(), Self::Error>
    where
        I: IntoIterator<Item = C>,
        Self::Color: FromColor<C>,
    {
        let frame_buffer =
            self.encode::<PIXEL_COUNT, FRAME_BUFFER_SIZE, _, _>(pixels, brightness, correction);
        self.write(frame_buffer, brightness, correction)
    }
}
  • Until the generic_const_exprs feature is stable, we aren't able to use associated constants, const functions, or expressions in the Blinksy code to calculate constants at compile-time. Instead, we must receive pre-calculated constants from the user as a generic. The best we can do is make it easy as possible by providing good types, traits, and const functions for the user to use.
  • To build a Control, you now need to provide a FRAME_BUFFER_SIZE constant.
    • So for example, to build a frame buffer to drive Ws2812 LEDs:
      .with_frame_buffer_size::<{ Ws2812::frame_buffer_size(Layout::PIXEL_COUNT) }>()
  • Re-factor clockless and clocked drivers into common pattern
    • There is a generic driver for each type: ClocklessDriver and ClockedDriver.
    • You construct the generic driver by combining an Led with a Writer, both of that type.
    • All drivers and all writers are made through builders.
    • So for example: the clockless RMT driver for a WS2812 LED:
let driver = ClocklessDriver::default()
    .with_led::<Ws2812>()
    .with_writer(ClocklessRmt::default()
        .with_led::<Ws2812>()
        .with_rmt_buffer_size::<{ rmt_buffer_size::<Ws2812>(Layout::PIXEL_COUNT) }>()
        .with_channel(/* rmt channel */)
        .with_pin(/* rmt pin */)
        .build()
    );
  • Another example: the clocked SPI driver for an APA102 LED:
let driver = ClockedDriver::default()
    .with_led::<Apa102>()
    .with_writer(/* spi bus */)
  • Clockless and clocked writers are also constructed through builders.
  • Move LED definitions to crate::leds module, remove Led suffix from structs.

@ahdinosaur
Copy link
Owner Author

Logic analyzer results:

  • just gledopto-ws2812-strip
image
  • just gledopto-ws2812-strip-embassy
image
  • just gledopto-apa102-grid
image image
  • just gledopto-apa102-grid-embassy
image image

@ahdinosaur
Copy link
Owner Author

diff --git a/esp/Cargo.toml b/esp/Cargo.toml
index 06d54e1..7b97237 100644
--- a/esp/Cargo.toml
+++ b/esp/Cargo.toml
@@ -26,5 +26,5 @@ debug = 2
 debug-assertions = false
 incremental = false
 lto = 'fat'
-opt-level = 's'
+opt-level = 3
 overflow-checks = false

This alone helps:

  • just gledopto-ws2812-strip
image

@ahdinosaur ahdinosaur merged commit 30f4d58 into main Oct 14, 2025
6 checks passed
@ahdinosaur ahdinosaur changed the title Experiments with a framebuffer Re-architect to pre-calculate a buffer for each frame Oct 14, 2025
ahdinosaur added a commit that referenced this pull request Oct 15, 2025
Now if you have a small enough number of clockless (i.e. WS2812) LEDs where a whole frame can fit within an RMT buffer, then the LED output will have no gaps. Closes #86. 

- Upgrade to `[email protected]`
- Use `esp_hal::rmt::CHANNEL_RAM_SIZE` instead of hard-coded 64
  - On async, the timing gap must be too much as the cube doesn't work, so reduce to a single LED (8 * channels_count + 1), but even that seems to be wrong in a weird way.
- Fix bug with RMT driver
  - If you set memsize, then an RMT write would only write that much data.
  - Was introduced and fixed in #90, but then accidentally re-introduced before merge.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants