Skip to content
led-ticker

Hardware: Bigsign reference build

The bigsign is led-ticker’s reference build for tall multi-row displays: a Raspberry Pi 5 driving eight P3 32x64 RGB matrix panels arranged in a 2x4 vertical-serpentine layout for a 256x64 canvas. The hi-res font, hi-res emoji, and ScaledCanvas features were all developed against this build, and the dimensions match the bigsign-flavored examples in config/. Different panel counts or layouts will work — the canvas math comes from pixel_mapper_config, not from a hardcoded shape — but the BOM, wiring diagram, and Pi-5 tuning knobs below are what we built and tested against. Drawing logic stays at a 16-tall logical canvas; ScaledCanvas blows it up by a factor of four to fill the panel.

  • Raspberry Pi 5
  • 8x P3 32x64 RGB matrix panels in a 2x4 vertical-serpentine layout = 256x64 logical canvas
  • default_scale = 4 — every logical pixel becomes a 4x4 block on the real panel, content vertically centered
  • Drawing logic stays at “1x scale” with 16-tall content; the wrapper handles the expansion
  • Same Docker image as the smallsign; the rgbmatrix library detects the SoC at runtime
ComponentQuantityNotes
Raspberry Pi 514 GB is plenty. A microSD card and a USB-C power supply for the Pi itself.
P3 32x64 RGB matrix panel8HUB75 panels with the standard 16-pin IDC connector. Matched panels render most consistently across the 2x4 grid.
Adafruit RGB matrix HAT1Sets hardware_mapping = "adafruit-hat" in the config. The bonnet works equivalently.
5V power supply1Sized for eight P3 panels at full white — that’s serious current. A 5V / 30A+ supply with thick busbars is the comfortable headroom pick.
16-pin IDC ribbon cables7+One between each pair of panels in chain order. The serpentine route bends across rows so cables need to reach across the layout.
Power pigtails / busbar1+Whatever your panels expect. Each panel has its own 2-pin power input — a busbar across the back of the frame keeps the wiring tidy.
Shroud / frame1A 2x4 grid of P3 panels is heavy and benefits from a rigid frame. Acrylic diffuser optional.

Total cost (rough): ~$400–600 USD. The 8 P3 panels at $40–60 each are the dominant line item; the Pi 5 + HAT + a 30–60 A 5 V supply with thick busbars round out the build. Frame and shroud costs vary widely depending on whether you fabricate or buy. Prices change — verify before ordering. These were checked in early 2026.

The rgbmatrix library detects the SoC at runtime, so the same Docker image runs on both the smallsign (Pi 4 / BCM2711) and the bigsign (Pi 5 / RP1). No separate build, no per-Pi flag — the Pi-5-specific knobs in the next section are simply ignored on a Pi 4 and vice versa.

Eight panels are tiled into a 2x4 grid (two rows of four). The data chain enters at the bottom-right panel and runs vertically: each column is wired bottom-to-top, with a diagonal jumper from the top of each column to the bottom of the next column to its left. There is no horizontal hop along the bottom row — the chain always climbs a column, then crosses diagonally to the bottom of the next one. All panels are installed upright (no 180° rotations). The pixel_mapper_config Remap string in the config tells the rgbmatrix library how each panel’s chain index maps to its physical (x, y) on the finished canvas.

col 1 col 2 col 3 col 4
top [8] [6] [4] [2]
↑ ↘ ↑ ↘ ↑ ↘ ↑
bot [7] [5] [3] [1] ← data chain enters here

Each [N] is the panel’s position in the chain (1 = first, 8 = last). The arrows show each column climbing bottom-to-top; the arrows show the diagonal jumper from the top of one column to the bottom of the next column to its left. If your top-row panels are physically mounted upside down (some manufacturers ship them that way), swap each top-row n for s in the Remap string — see the comment in the config snippet below.

The minimal [display] block for the bigsign — straight from config/config.bigsign.example.toml:

[display]
rows = 32
cols = 64
chain_length = 8
parallel = 1
# Remap each panel to its physical position. Serpentine chain order, all panels upright.
pixel_mapper_config = "Remap:256,64|192,32n|192,0n|128,32n|128,0n|64,32n|64,0n|0,32n|0,0n"
brightness = 60
gpio_slowdown = 3 # bumped from 2 to pair with RIO mode (default); raise to 4-5 if flicker
hardware_mapping = "adafruit-hat"
default_scale = 4 # 4× scaling fills the 64-tall canvas
# --- Pi 5 performance tuning ---
pwm_bits = 8 # 11 (default) → 8 ≈ 8× faster refresh, slightly worse color
show_refresh_rate = true # log measured refresh Hz to stderr

The 256x64 logical canvas comes from the pixel_mapper_config Remap header (256,64), not from cols * chain_length directly — chain_length = 8 is just the panel count for the rgbmatrix library; the mapper rearranges those panels into the 2x4 grid.

PWM bit depth, default 11. Cutting to 8 buys roughly 8× faster refresh at the cost of a small drop in color depth. On a panel this big the refresh boost is the difference between visible scan-line flicker and a clean image, and the color hit is barely noticeable for text and pixel art. Leave it at 8 unless you’re displaying photos and care about smooth gradients.

Pi-5 only. The RP1 SoC offers two GPIO drive modes:

  • RIO mode (the library default — rp1_pio not set) — faster refresh, higher CPU.
  • PIO mode (rp1_pio = 1) — lower CPU, slower refresh.

The bigsign uses RIO — the default, so nothing to set. The Pi 5 has cores to spare and the bigger panel wants every Hz it can get. Note that RIO mode requires bumping gpio_slowdown (next section). Before June 2026 the knob was called rp1_rio and PIO was the default; an old rp1_rio = 1 line in a config is now ignored (with a startup warning) and RIO applies anyway.

GPIO timing slowdown, must pair with RIO mode (the default). The Pi 4 default of 2 is too aggressive when paired with RIO mode — start at 3, and raise to 4 or 5 if you still see flicker. Higher values trade refresh rate for stability, same tradeoff as on the smallsign just at a higher floor.

content_height cannot exceed 16 at scale = 4

Section titled “content_height cannot exceed 16 at scale = 4”

The hard ceiling is content_height × scale ≤ panel_h_real. For the bigsign at scale = 4 and a 64-tall panel that’s content_height ≤ 16. Pushing above the ceiling makes the wrapper’s vertical offset go negative — the logical canvas is taller than the real panel — and the top + bottom rows of your content silently clip. BDF text is forgiving because cells sit near the vertical center, but hi-res emoji and large hi-res fonts surface the clip immediately (e.g. a hi-res :instagram: icon loses about 4 real px off the bottom). For per-row breathing room use text_y_offset on the widget, not a higher content_height.

The pixel_mapper_config Remap string is sensitive to chain order

Section titled “The pixel_mapper_config Remap string is sensitive to chain order”

Every entry in the string must correspond to a specific panel position in your data chain — get the order wrong (or rotate one panel 180° to match the rest of the row) and you’ll see content split across panels or rendered upside down. The string in config/config.bigsign.example.toml matches a 2x4 serpentine starting at the bottom-right with all panels upright; if your panels are mounted differently, you’ll need to edit each entry’s orientation flag (n, s, e, w). Use panel-map to derive and verify the Remap string from your physical layout instead of hand-editing it.

Slugs like :moon: and :instagram: have a 32x32 hi-res sprite in HIRES_REGISTRY that paints directly to the unwrapped real canvas, bypassing the wrapper’s 4x4 block expansion for sharper detail. On a smallsign at default_scale = 1 the renderer falls back to the 8x8 lo-res sprite automatically — same icon footprint, less detail. Don’t expect hi-res emoji output on the smallsign.

pixel_mapper_config for non-bigsign layouts

Section titled “pixel_mapper_config for non-bigsign layouts”

The Remap string follows the format Remap:WIDTH,HEIGHT|x,yORIENT|... with one entry per panel in chain order. Orientations: n = normal, s = 180°, e = 270°, w = 90°, x = discard. The bigsign’s 2×4 vertical-serpentine chain is documented above. For simpler layouts:

  • 2×2 grid (chain runs along the bottom row first, then top row right-to-left), all panels upright: Remap:128,64|0,32n|64,32n|64,0n|0,0n
  • Single row of 4 panels, all upright, chain enters left: Remap:256,32|0,0n|64,0n|128,0n|192,0n

If your physical layout doesn’t match the chain order from the data cable, every panel in the wrong position needs an entry in the Remap string. See the upstream hzeller/rpi-rgb-led-matrix README — the base for the jamesawesome/rpi-rgb-led-matrix fork led-ticker ships — for the full Remap reference.

Wiring or driver problem? Run panel-test first

Section titled “Wiring or driver problem? Run panel-test first”

If your widgets render with the wrong colors, a garbled bottom half, only one panel lit, or visible flicker, the issue is most likely in the hardware layer (led_rgb_sequence, panel_type, chain_length, gpio_slowdown) — not in your config. The panel-test diagnostic isolates the hardware layer by painting flat R/G/B/W/B colors, so you can verify wiring and driver init before debugging widgets.

A complete working config for this build, including the pixel_mapper_config string and the Pi 5 RP1 tuning this hardware needs. Drop this into config/config.toml and adjust the per-widget content.

Complete config.bigsign.example.toml (256×64 bigsign)
# led-ticker bigsign configuration — 256×64 canvas
#
# 8× P3 32×64 panels in a 2×4 vertical-serpentine layout = 256×64 logical.
# Driven by a Raspberry Pi 5 through an Adafruit RGB Matrix HAT.
#
# Copy this to config.toml on the bigsign Pi and adjust the playlist for your
# content. Full config-options reference:
# https://docs.ledticker.dev/reference/config-options/
[display]
rows = 32
cols = 64
chain_length = 8
parallel = 1
# pixel_mapper_config tells the rgbmatrix library how each panel's position in the
# data chain maps to its physical (x, y) on the finished canvas. The bigsign
# chain enters at the bottom-right panel, climbs each column bottom-to-top,
# then jumps diagonally to the bottom of the next column to its left. All
# panels are installed upright (no 180° rotations). See the chain diagram and
# instructions for deriving your own mapper string at:
# https://docs.ledticker.dev/hardware/bigsign/
#
# Format: Remap:WIDTH,HEIGHT|x,yORIENT|... (one entry per panel in chain order)
# Orientations: n=normal, s=180°, e=270°, w=90°, x=discard.
# If your top-row panels are physically flipped 180°, swap each top-row 'n'
# for 's' (e.g. "192,0n" → "192,0s").
pixel_mapper_config = "Remap:256,64|192,32n|192,0n|128,32n|128,0n|64,32n|64,0n|0,32n|0,0n"
brightness = 60
# default_scale = 4 means every widget draws at the standard 16-tall logical
# canvas, and the ScaledCanvas wrapper expands every pixel to a 4×4 real block
# to fill the 64-tall panel. Widget code never sees the 256×64 panel directly
# — all drawing is in logical-pixel coordinates.
default_scale = 4
hardware_mapping = "adafruit-hat"
# Pi 5 RP1 GPIO tuning.
#
# The library's default Pi 5 backend is RIO (Registered IO) — faster
# refresh, more CPU than PIO. Set rp1_pio = 1 only to trade refresh
# speed for lower CPU.
#
# gpio_slowdown = 3 pairs with RIO mode. The Pi 4 default of 2 is too
# aggressive with RIO; raise to 4–5 if you still see flicker.
#
# pwm_bits = 8 drops from the 11-bit default, buying roughly 8× faster
# refresh at a small color-depth cost. Barely noticeable for text and pixel
# art; only relevant if you're displaying photos with smooth gradients.
gpio_slowdown = 3
pwm_bits = 8
show_refresh_rate = true # log measured refresh Hz to stderr; remove once stable
[title]
delay = 5
# Global transition defaults. Full transition catalogue and per-family tuning
# lives at https://docs.ledticker.dev/transitions/.
[transitions]
default = "push_left"
duration = 0.5
easing = "ease_out"
between_sections = "dissolve"
# --- Playlist ---
# Each [[playlist.section]] displays in order, then the whole playlist loops.
# Sections, modes, and timing knobs: https://docs.ledticker.dev/concepts/sections-and-modes/
[[playlist.section]]
mode = "ticker"
loop_count = 1
# scale inherits from display.default_scale (4)
[playlist.section.title]
type = "message"
text = "#DevOps News"
font_color = "random"
[[playlist.section.widget]]
type = "message"
text = "May the uptime be with you!"
[[playlist.section.widget]]
type = "message"
text = "Always be shipping!"
# --- Countdowns at scale=2 ---
# Dropping to scale=2 gives 32px-tall content centered with 16px black bands
# top and bottom — a visual change of pace between sections.
[[playlist.section]]
mode = "ticker"
loop_count = 2
scale = 2
[playlist.section.title]
type = "message"
text = "Count Downs"
font_color = "random"
[[playlist.section.widget]]
type = "countdown"
text = "Days Until Summer"
countdown_date = 2026-06-20
[[playlist.section.widget]]
type = "countdown"
text = "Days Until Fall"
countdown_date = 2026-09-22
# --- GIF / image playback ---
# The gif widget plays an animated GIF or static image at native physical
# resolution (256×64), using the same fit/text-overlay knobs as the image
# widget. Full reference: https://docs.ledticker.dev/widgets/gif/
#
# `path` resolves relative to this config file's directory. Drop assets
# under `<config_dir>/assets/` and reference by relative path.
#
# [[playlist.section]]
# mode = "slideshow"
# loop_count = 2
#
# [[playlist.section.widget]]
# type = "gif"
# path = "assets/phoenix.gif"
# fit = "pillarbox"