Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
581c6fb
support row based bcm frame buffers
liebman Aug 6, 2026
d36ddbf
move DMA out from hub-75framebuffer
liebman Aug 8, 2026
bef475e
fmt
liebman Aug 8, 2026
9876f3e
added reverse-row-order feature
liebman Aug 8, 2026
13e3163
Calling `swap()` twice without waiting now returns an error
liebman Aug 8, 2026
9c11d85
Circular-mode clear any pending interrupt in swap() to prevent race
liebman Aug 8, 2026
1f4be85
#[ram] a few bcm functions
liebman Aug 8, 2026
b89f39e
enforce single-instance of Hub75 (covers some future case where there…
liebman Aug 8, 2026
b91b225
start and restart now return a new Hub75Error::AlreadyRunning error i…
liebman Aug 8, 2026
394a843
esp32: document enabling out_eof interrupt
liebman Aug 8, 2026
5a66690
clean up linear to use the same pointer delta as circular
liebman Aug 9, 2026
a06285d
assert that frame buffers are in dram
liebman Aug 9, 2026
8ed36ad
make clippy strict and fix warnings
liebman Aug 9, 2026
bb34bdd
run clippy in ci
liebman Aug 9, 2026
a697552
Hub75Swap is now #[must_use]
liebman Aug 9, 2026
8c2d170
lcd_cam: no need for GdmaChannelNum constraint in non-cicular case
liebman Aug 9, 2026
e517ad1
protect dma_descriptor_count from devide by 0 and document panic
liebman Aug 9, 2026
2c1f591
MAX_DMA_CHUNK_SIZE is doc_hidden
liebman Aug 9, 2026
66eca55
use as_mut_ptr() not cast
liebman Aug 9, 2026
85eccf1
update hub75-framebuffer to latest rev on row-bcm branch
liebman Aug 9, 2026
8be569e
update examples, ci and docs
liebman Aug 15, 2026
6cebceb
hub75-framebuffer api update
liebman Aug 19, 2026
79230db
cleanup the way invert-blank works/add invert-oe to invert at the fra…
liebman Aug 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .cargo/audit.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[advisories]
ignore = [
# paste crate is no longer maintained
"RUSTSEC-2024-0436",
]
29 changes: 29 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[alias]

# build
build-esp32 = "build --target xtensa-esp32-none-elf -Zbuild-std=core,alloc --features esp32,esp-hal-unstable,log"
build-esp32-c = "build --target xtensa-esp32-none-elf -Zbuild-std=core,alloc --features esp32,esp-hal-unstable,log,circular-dma"
build-esp32c6 = "build --target riscv32imac-unknown-none-elf -Zbuild-std=core,alloc --features esp32c6,esp-hal-unstable,defmt"
build-esp32c5 = "build --target riscv32imac-unknown-none-elf -Zbuild-std=core,alloc --features esp32c5,esp-hal-unstable,defmt"
build-esp32s3 = "build --target xtensa-esp32s3-none-elf -Zbuild-std=core,alloc --features esp32s3,esp-hal-unstable,defmt"
build-esp32s3-c = "build --target xtensa-esp32s3-none-elf -Zbuild-std=core,alloc --features esp32s3,esp-hal-unstable,defmt,circular-dma"

# clippy
clippy-esp32 = "clippy --target xtensa-esp32-none-elf -Zbuild-std=core,alloc --features esp32,esp-hal-unstable,defmt"
clippy-esp32-c = "clippy --target xtensa-esp32-none-elf -Zbuild-std=core,alloc --features esp32,esp-hal-unstable,log,circular-dma"
clippy-esp32c6 = "clippy --target riscv32imac-unknown-none-elf -Zbuild-std=core,alloc --features esp32c6,esp-hal-unstable,defmt"
clippy-esp32c5 = "clippy --target riscv32imac-unknown-none-elf -Zbuild-std=core,alloc --features esp32c5,esp-hal-unstable,defmt"
clippy-esp32s3 = "clippy --target xtensa-esp32s3-none-elf -Zbuild-std=core,alloc --features esp32s3,esp-hal-unstable,defmt"
clippy-esp32s3-c = "clippy --target xtensa-esp32s3-none-elf -Zbuild-std=core,alloc --features esp32s3,esp-hal-unstable,defmt,circular-dma"

# doc
doc-esp32 = "doc --target xtensa-esp32-none-elf -Zbuild-std=core,alloc --features esp32,esp-hal-unstable,log"
doc-esp32c5 = "doc --target riscv32imac-unknown-none-elf -Zbuild-std=core,alloc --features esp32c5,esp-hal-unstable,defmt"
doc-esp32c6 = "doc --target riscv32imac-unknown-none-elf -Zbuild-std=core,alloc --features esp32c6,esp-hal-unstable,defmt"
doc-esp32s3 = "doc --target xtensa-esp32s3-none-elf -Zbuild-std=core,alloc --features esp32s3,esp-hal-unstable,defmt"

# udeps
udeps-esp32 = "udeps --target xtensa-esp32-none-elf -Zbuild-std=core,alloc --features esp32,esp-hal-unstable,log"
udeps-esp32s3 = "udeps --target xtensa-esp32s3-none-elf -Zbuild-std=core,alloc --features esp32s3,esp-hal-unstable,defmt"
udeps-esp32c5 = "udeps --target riscv32imac-unknown-none-elf -Zbuild-std=core,alloc --features esp32c5,esp-hal-unstable,defmt"
udeps-esp32c6 = "udeps --target riscv32imac-unknown-none-elf -Zbuild-std=core,alloc --features esp32c6,esp-hal-unstable,defmt"
197 changes: 110 additions & 87 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,116 +15,139 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}

env:
CARGO_TARGET_DIR: ${{ github.workspace }}/target
CARGO_TERM_COLOR: always
# Compile-time defmt filter. `trace` keeps every log level compiled in so the
# arguments of trace!/debug!/info!/warn! are all type-checked (a value with no
# `defmt::Format` impl is only a compile error if its level is enabled).
DEFMT_LOG: trace
# espup (used by esp-rs/xtensa-toolchain) queries the GitHub API; provide a
# token to avoid transient rate-limit errors.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

jobs:
build:
name: Build Examples
# Clippy the esp-hub75 library across every chip, logging backend, and
# DMA-mode feature combination.
clippy:
name: Clippy lib (${{ matrix.chip }}, ${{ matrix.logging }}${{ matrix.extra }})
runs-on: ubuntu-latest
env:
RUSTFLAGS: "-D warnings -C link-arg=-Tlinkall.x -C link-arg=-Tdefmt.x"
strategy:
fail-fast: false
matrix:
target: [esp32, esp32s3, esp32c5, esp32c6]
features: [log, defmt]
extra_features: ["", "full-chain-dma,invert-blank,invert-clock", "circular-dma"]
exclude:
- target: esp32c5
extra_features: "circular-dma"
- target: esp32c6
extra_features: "circular-dma"
chip: [esp32, esp32s3, esp32c5, esp32c6]
logging: [log, defmt]
# Leading comma keeps the feature list well-formed when empty.
extra: ["", ",full-chain-dma,invert-blank,invert-clock", ",circular-dma"]
include:
- target: esp32
rust_target: xtensa-esp32-none-elf
toolchain: esp
example: i2s_parallel
cargo_unstable: "-Zbuild-std=core,alloc"
- target: esp32
rust_target: xtensa-esp32-none-elf
toolchain: +esp
example: i2s_parallel_latch
cargo_unstable: "-Zbuild-std=core,alloc"
- target: esp32
- chip: esp32
rust_target: xtensa-esp32-none-elf
toolchain: +esp
example: hello_i2s_parallel
cargo_unstable: "-Zbuild-std=core,alloc"
- target: esp32
rust_target: xtensa-esp32-none-elf
toolchain: +esp
example: esp32-trinity
cargo_unstable: "-Zbuild-std=core,alloc"
- target: esp32
rust_target: xtensa-esp32-none-elf
toolchain: +esp
example: rustacean_i2s
cargo_unstable: "-Zbuild-std=core,alloc"
- target: esp32s3
rust_target: xtensa-esp32s3-none-elf
toolchain: +esp
example: lcd_cam
cargo_unstable: "-Zbuild-std=core,alloc"
- target: esp32s3
- chip: esp32s3
rust_target: xtensa-esp32s3-none-elf
toolchain: +esp
example: lcd_cam_latch
cargo_unstable: "-Zbuild-std=core,alloc"
- target: esp32s3
rust_target: xtensa-esp32s3-none-elf
toolchain: +esp
example: hello_lcd_cam
cargo_unstable: "-Zbuild-std=core,alloc"
- target: esp32s3
rust_target: xtensa-esp32s3-none-elf
toolchain: +esp
example: rustacean_lcd_cam
cargo_unstable: "-Zbuild-std=core,alloc"
- target: esp32c5
rust_target: riscv32imac-unknown-none-elf
toolchain: ""
example: parl_io_latch
cargo_unstable: ""
- target: esp32c6
- chip: esp32c5
rust_target: riscv32imac-unknown-none-elf
toolchain: ""
example: parl_io
cargo_unstable: ""
- target: esp32c6
- chip: esp32c6
rust_target: riscv32imac-unknown-none-elf
toolchain: ""
example: parl_io_latch
cargo_unstable: ""
- target: esp32c6
rust_target: riscv32imac-unknown-none-elf
toolchain: ""
example: rustacean_parl_io
cargo_unstable: ""
exclude:
# circular-dma is a compile error on PARL_IO (ESP32-C5/C6).
- chip: esp32c5
extra: ",circular-dma"
- chip: esp32c6
extra: ",circular-dma"
steps:
- uses: actions/checkout@v4

# Install the Rust toolchain for Xtensa devices:
- name: Install Xtensa toolchain
# The `esp` toolchain covers the Xtensa and RISC-V targets and ships
# rust-src + clippy, so a single install covers every chip.
- name: Install Rust toolchain
uses: esp-rs/xtensa-toolchain@v1.6
with:
version: 1.88.0.0
default: true
version: "1.88.0.0"

# Install the Rust stable toolchain for RISC-V devices:
- name: Install RISC-V toolchain
uses: dtolnay/rust-toolchain@v1
- uses: Swatinem/rust-cache@v2
with:
target: riscv32imc-unknown-none-elf,riscv32imac-unknown-none-elf
toolchain: stable
components: rust-src
cache-all-crates: true
prefix-key: "clippy-lib-${{ matrix.chip }}-${{ matrix.logging }}"

- name: Run clippy
run: |
cargo clippy \
--target ${{ matrix.rust_target }} \
-Zbuild-std=core,alloc \
--features "iram,esp-hal-unstable,${{ matrix.chip }},${{ matrix.logging }}${{ matrix.extra }}" \
-- -D warnings

# Clippy every example crate on each board it supports. Driven by the
# `clippy-*` aliases each example defines in its own `.cargo/config.toml`
# (mirroring `clippy-all.sh`), so new examples/boards are picked up
# automatically and unsupported boards are skipped without an exclude list.
clippy-examples:
name: Clippy examples
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install Rust toolchain
uses: esp-rs/xtensa-toolchain@v1.6
with:
default: true
version: "1.88.0.0"

- uses: Swatinem/rust-cache@v2
with:
prefix-key: "ci-${{ matrix.target }}"
cache-all-crates: true

- name: Build examples
prefix-key: "clippy-examples"

- name: Run clippy
run: |
cargo ${{ matrix.toolchain }} build --target ${{ matrix.rust_target }} --example ${{ matrix.example }} --features "iram,${{ matrix.target }},${{ matrix.features }},${{ matrix.extra_features }}" ${{ matrix.cargo_unstable }}
for dir in examples/*/; do
example="$(basename "$dir")"
for alias in clippy-esp32 clippy-esp32s3 clippy-esp32c6 clippy-trinity clippy-esp32c5; do
if grep -qE "^[[:space:]]*${alias}[[:space:]]*=" "$dir/.cargo/config.toml"; then
board="${alias#clippy-}"
echo "=========================================="
echo "Clippy: examples/${example} (${board})"
echo "=========================================="
(cd "$dir" && cargo "$alias" -- -D warnings)
fi
done
done

# Check the esp-hub75 library formatting.
fmt:
name: Format lib
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# rustfmt ships with the esp toolchain; formatting needs no cross-target
# build, so a single host install is enough.
- name: Install Rust toolchain
uses: esp-rs/xtensa-toolchain@v1.6
with:
default: true
version: "1.88.0.0"

- name: Check formatting
run: cargo fmt --all -- --check

# Check formatting of every example crate. Examples are independent crates
# (not part of a workspace), so each is checked separately; they inherit the
# repo-root rustfmt.toml.
fmt-examples:
name: Format ${{ matrix.example }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
example: [gradient, gradient-embassy, gradient-latched, gradient-quarter, gradient-tiled]
steps:
- uses: actions/checkout@v4

- name: Install Rust toolchain
uses: esp-rs/xtensa-toolchain@v1.6
with:
default: true
version: "1.88.0.0"

- name: Check formatting
run: cargo fmt --all --manifest-path examples/${{ matrix.example }}/Cargo.toml -- --check
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/.cargo
/.devcontainer
/Cargo.lock
/bootloader.bin
Expand All @@ -9,5 +8,3 @@
/rust-toolchain.toml
/target
/.cursorrules
/build-all.sh
/clippy-all.sh
50 changes: 48 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased] - ReleaseDate

### ⚠️ Breaking

* Removed the deprecated free function
`dma_descriptor_count(bcm_chunk_count, bcm_chunk_bytes)` (deprecated since
0.7.0). Use the new generic const fn
`esp_hub75::dma_descriptor_count::<FB>(MAX_DMA_CHUNK_SIZE)` or the
`hub75_dma_descriptors!` macro instead.
* Requires a `hub75-framebuffer` release exposing
`FrameBuffer::BCM_SEGMENT_SHAPES`/`BCM_SEQUENCE_LEN`/`BCM_SEQUENCE_COUNT`.

### Added

* `reverse-row-order` feature (forwards to `hub75-framebuffer`) storing the
rows of every framebuffer layout in reverse scan order, so the DMA stream
renders the last panel row first and row 0 last. Logical coordinates are
unchanged, only the scan order changes.
* `lead-blank-32` and `trail-blank-32` features extending blanking delay options to 32 pixel-clock cycles.
* New generic const fn `esp_hub75::dma_descriptor_count::<FB>(max_chunk)`
computes the required DMA descriptor count from the framebuffer type's
static `FrameBuffer::BCM_SEGMENT_SHAPES`; no framebuffer instance
needed, so descriptor tables stay statically allocated.
* `hub75_dma_descriptors!` now works with any `FrameBuffer` type,
including `tiling::RemappedFrameBuffer` (previously only framebuffer
types with an inherent `dma_descriptor_count()` could be used).
* Compile-time check that a framebuffer's BCM segment count fits the
driver's segment cache (`MAX_SEGMENTS`).
* `invert-oe` feature forwarding to `hub75-framebuffer`, inverting the
output-enable (OE) signal in the generated data stream.

### Changed

* DMA descriptor sizing moved out of `hub75-framebuffer` into this crate:
the framebuffer crate now exposes only the platform-neutral BCM scan
sequence; all ESP DMA descriptor logic lives here.
* `hub75_dma_descriptors!` no longer over-allocates in the default
group-based DMA mode: it now allocates only the largest single transfer
group's descriptors instead of the whole frame's chain (e.g. 33 instead
of 1056 descriptors for a 64×64 row-major 6-plane configuration, saving
~12 KiB of RAM). `full-chain-dma`/`circular-dma` still allocate the full
chain as required.
* `MAX_SEGMENTS` raised from 288 to 320 to cover the true worst case
(32 row-pairs × (8 planes + inter-row gap + trailer)).
* `invert-blank` now applies to 16-bit direct-drive (`Hub75Pins16`)
configurations in addition to 8-bit latched (`Hub75Pins8`); previously the
16-bit blank pin was always inverted in hardware.

## [0.14.0] - 2026-08-02

### ⚠️ Breaking — Major refactor [#49](https://github.com/liebman/esp-hub75/pull/49)
Expand All @@ -18,7 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
each call sent one frame via DMA, returned a `Hub75Transfer` (consuming
`Hub75`), and the display was only alive while the loop ran. Now `new()` /
`new_async()` take an initial `&'static FB` and start an internal ISR that
refreshes the display continuously. To update the image, call `hub75.swap(fb)`
refreshes the display continuously. To update the image, call `hub75.swap(fb)?`
which returns a `Hub75Swap` transfer object; wait on it to reclaim the old
buffer.

Expand All @@ -35,7 +81,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
let hub75 = Hub75::new_async(parl_io, pins, channel, tx_descriptors, freq, &*fb0)?;
loop {
// ... draw into fb1 ...
let mut xfer = hub75.swap(fb1);
let mut xfer = hub75.swap(fb1)?;
xfer.wait_for_done().await;
fb1 = xfer.wait()?;
}
Expand Down
Loading
Loading