Building a 1-to-100 Countdown Display on an 8×64 WS2812B LED Matrix

https://ogbugbu-technologies.com.ng

Introduction

The WS2812B (NeoPixel) programmable LED strip is one of the most popular addressable LED solutions for makers and hobbyists. With 512 individually controllable LEDs arranged in an 8-row by 64-column matrix, you have enough real estate to display large, readable numbers for a countdown from 1 to 100. This project combines the power of the FastLED library

with custom font rendering to create a visually striking countdown timer.


Understanding Your Hardware Setup

The 8×64 WS2812B Matrix Configuration

An 8×64 matrix contains 512 LEDs total. The physical layout typically follows a serpentine (zigzag) pattern—where odd rows run left-to-right and even rows run right-to-left (or vice versa). This matters because the LED data signal flows continuously through the strip, and mapping (x,y) coordinates to the correct LED index requires accounting for this snake-like wiring.

Key Specifications:

  • Total LEDs: 512 (8 rows × 64 columns)
  • Power Draw: ~30mA per LED at full white brightness → 15.4A maximum at 5V
  • Data Pin: Single wire control (typically GPIO 2, 4, or 5 on ESP32)
  • Power Supply: 5V, 20A recommended for full brightness with headroom

Recommended Microcontroller

For driving 512 LEDs with smooth animations, an ESP32 is ideal. It offers:

  • Dual-core 240MHz processor for rendering and WiFi simultaneously
  • Hardware support for precise WS2812B timing via RMT or I2S peripherals
  • Built-in WiFi for NTP time synchronization (if building a real-time clock)

An Arduino Uno/Nano can handle this, but may struggle with complex animations at high refresh rates.


Wiring Diagram

┌─────────────────────────────────────────────────────────┐
│ POWER SUPPLY (5V, 20A) │
│ ┌─────────┐ ┌─────────┐ ┌─────────────────────┐ │
│ │ 5V OUT │───▶│ 5V IN │ │ ESP32 │ │
│ │ GND OUT │───▶│ GND IN │ │ GPIO5 ─────┐ │ │
│ │ │ │ │ │ GND ─────┼──┐ │ │
│ └─────────┘ │ 470Ω │ │ 5V ─────┘ │ │ │
│ │ Resistor│ │ │ │ │
│ │ (Data) │ │ │ │ │
│ └────┬────┘ │ │ │ │
│ │ │ │ │ │
│ ┌────┴────┐ │ │ │ │
│ │ WS2812B │ │ │ │ │
│ │ Matrix │ │ │ │ │
│ │ 8×64 │ │ │ │ │
│ └─────────┘ │ │ │ │
│ └─────────────────┘ │ │
└─────────────────────────────────────────────────────────┘

CRITICAL: Add a 470Ω resistor between ESP32 GPIO and matrix DATA IN.
Add a 1000µF capacitor across the 5V/GND terminals near the matrix.

Power Injection Points: With 512 LEDs, inject power at minimum 3 points (start, middle, and end) to prevent voltage drop and color shifting.


The Complete Arduino Code

Step 1: Install Required Libraries

Install via Arduino IDE Library Manager:

  • FastLED by Daniel Garcia (v3.6+ recommended)

Step 2: Define Your Matrix and Font

Code

#include <FastLED.h>

// ============ CONFIGURATION ============
#define MATRIX_WIDTH      64
#define MATRIX_HEIGHT     8
#define NUM_LEDS          (MATRIX_WIDTH * MATRIX_HEIGHT)  // 512
#define DATA_PIN          5          // GPIO pin connected to matrix
#define LED_TYPE          WS2812B
#define COLOR_ORDER       GRB
#define BRIGHTNESS        64         // 0-255 (keep moderate for power/heat)
#define COUNTDOWN_SPEED   1000       // Milliseconds between numbers

// ============ LED ARRAY ============
CRGB leds[NUM_LEDS];

// ============ SERPENTINE MAPPING ============
// Maps (x, y) coordinates to LED index accounting for zigzag wiring
uint16_t XY(uint8_t x, uint8_t y) {
  // Boundary check
  if (x >= MATRIX_WIDTH || y >= MATRIX_HEIGHT) return 0;
  
  // Serpentine layout: even rows left→right, odd rows right→left
  if (y % 2 == 0) {
    return (y * MATRIX_WIDTH) + x;
  } else {
    return (y * MATRIX_WIDTH) + (MATRIX_WIDTH - 1 - x);
  }
}

// ============ 3×5 DIGIT FONT (Compact, fits well in 8px height) ============
// Each digit is 3 columns wide × 5 rows tall
// We center vertically in the 8-row matrix (rows 1-5 used, rows 0 and 6-7 blank)
const uint8_t DIGIT_3x5[10][5] = {
  // 0
  {0b111, 0b101, 0b101, 0b101, 0b111},
  // 1
  {0b010, 0b010, 0b010, 0b010, 0b010},
  // 2
  {0b111, 0b001, 0b111, 0b100, 0b111},
  // 3
  {0b111, 0b001, 0b111, 0b001, 0b111},
  // 4
  {0b101, 0b101, 0b111, 0b001, 0b001},
  // 5
  {0b111, 0b100, 0b111, 0b001, 0b111},
  // 6
  {0b111, 0b100, 0b111, 0b101, 0b111},
  // 7
  {0b111, 0b001, 0b001, 0b001, 0b001},
  // 8
  {0b111, 0b101, 0b111, 0b101, 0b111},
  // 9
  {0b111, 0b101, 0b111, 0b001, 0b111}
};

// ============ 4×7 DIGIT FONT (Larger, more readable) ============
// Each digit is 4 columns wide × 7 rows tall
// Uses rows 0-6, row 7 blank for spacing
const uint8_t DIGIT_4x7[10][7] = {
  // 0
  {0b0111, 0b1001, 0b1001, 0b1001, 0b1001, 0b1001, 0b0111},
  // 1
  {0b0010, 0b0110, 0b0010, 0b0010, 0b0010, 0b0010, 0b0111},
  // 2
  {0b0111, 0b1001, 0b0001, 0b0010, 0b0100, 0b1000, 0b1111},
  // 3
  {0b1111, 0b0001, 0b0001, 0b0111, 0b0001, 0b0001, 0b1111},
  // 4
  {0b1001, 0b1001, 0b1001, 0b1111, 0b0001, 0b0001, 0b0001},
  // 5
  {0b1111, 0b1000, 0b1000, 0b1111, 0b0001, 0b0001, 0b1111},
  // 6
  {0b0111, 0b1000, 0b1000, 0b1111, 0b1001, 0b1001, 0b0111},
  // 7
  {0b1111, 0b0001, 0b0001, 0b0010, 0b0100, 0b0100, 0b0100},
  // 8
  {0b0111, 0b1001, 0b1001, 0b0111, 0b1001, 0b1001, 0b0111},
  // 9
  {0b0111, 0b1001, 0b1001, 0b0111, 0b0001, 0b0001, 0b0111}
};

// ============ COLOR PALETTE ============
CRGB currentColor = CRGB::Red;      // Starting color
CRGB targetColor = CRGB::Green;     // Ending color

// ============ SETUP ============
void setup() {
  Serial.begin(115200);
  delay(1000); // Safety delay for ESP32 boot
  
  // Initialize FastLED
  FastLED.addLeds<LED_TYPE, DATA_PIN, COLOR_ORDER>(leds, NUM_LEDS);
  FastLED.setBrightness(BRIGHTNESS);
  FastLED.clear();
  FastLED.show();
  
  Serial.println("8x64 WS2812B Countdown Initialized");
  Serial.println("Counting from 100 down to 1...");
}

// ============ DRAW SINGLE DIGIT (3×5 font) ============
void drawDigit3x5(uint8_t digit, uint8_t startX, uint8_t startY, CRGB color) {
  if (digit > 9) return;
  
  for (uint8_t row = 0; row < 5; row++) {
    uint8_t pattern = DIGIT_3x5[digit][row];
    for (uint8_t col = 0; col < 3; col++) {
      // Extract bit (MSB first, left to right)
      if (pattern & (0b100 >> col)) {
        uint8_t x = startX + col;
        uint8_t y = startY + row;
        leds[XY(x, y)] = color;
      }
    }
  }
}

// ============ DRAW SINGLE DIGIT (4×7 font) ============
void drawDigit4x7(uint8_t digit, uint8_t startX, uint8_t startY, CRGB color) {
  if (digit > 9) return;
  
  for (uint8_t row = 0; row < 7; row++) {
    uint8_t pattern = DIGIT_4x7[digit][row];
    for (uint8_t col = 0; col < 4; col++) {
      if (pattern & (0b1000 >> col)) {
        uint8_t x = startX + col;
        uint8_t y = startY + row;
        leds[XY(x, y)] = color;
      }
    }
  }
}

// ============ DRAW NUMBER (1-100) ============
void drawNumber(uint8_t number, CRGB color, bool useLargeFont = true) {
  FastLED.clear();
  
  if (number == 0) number = 1; // Safety
  
  uint8_t tens = number / 10;
  uint8_t ones = number % 10;
  
  if (useLargeFont) {
    // 4×7 font - two digits centered in 64px width
    // Total width: 4 + 1 + 4 = 9 pixels, centered: (64 - 9) / 2 = 27
    uint8_t startX = (MATRIX_WIDTH - 9) / 2;
    uint8_t startY = 0; // Top-aligned in 8px height (7px font + 1px padding)
    
    if (tens > 0) {
      drawDigit4x7(tens, startX, startY, color);
      drawDigit4x7(ones, startX + 5, startY, color); // 4px + 1px spacing
    } else {
      // Single digit - center it
      drawDigit4x7(ones, (MATRIX_WIDTH - 4) / 2, startY, color);
    }
  } else {
    // 3×5 font - more compact
    uint8_t startX = (MATRIX_WIDTH - 7) / 2; // 3 + 1 + 3 = 7
    uint8_t startY = 1; // Centered vertically in 8px (5px font, 3px padding)
    
    if (tens > 0) {
      drawDigit3x5(tens, startX, startY, color);
      drawDigit3x5(ones, startX + 4, startY, color);
    } else {
      drawDigit3x5(ones, (MATRIX_WIDTH - 3) / 2, startY, color);
    }
  }
  
  FastLED.show();
}

// ============ COLOR INTERPOLATION ============
CRGB interpolateColor(CRGB start, CRGB end, float progress) {
  return CRGB(
    start.r + (end.r - start.r) * progress,
    start.g + (end.g - start.g) * progress,
    start.g + (end.b - start.b) * progress
  );
}

// ============ ANIMATED COUNTDOWN ============
void runCountdown() {
  for (int i = 100; i >= 1; i--) {
    // Calculate color progress (0.0 to 1.0)
    float progress = (100.0 - i) / 99.0;
    CRGB color = interpolateColor(currentColor, targetColor, progress);
    
    // Add a pulsing brightness effect
    uint8_t pulse = sin8(millis() / 5); // Gentle pulse
    color.nscale8(192 + pulse / 4);     // 75-100% brightness range
    
    drawNumber(i, color, true);
    
    Serial.print("Countdown: ");
    Serial.println(i);
    
    delay(COUNTDOWN_SPEED);
  }
  
  // Flash "00" or "GO" at the end
  for (int flash = 0; flash < 6; flash++) {
    FastLED.clear();
    FastLED.show();
    delay(200);
    
    // Draw "GO" in green
    CRGB flashColor = (flash % 2 == 0) ? CRGB::Green : CRGB::White;
    // You could add a simple "GO" font here, or just fill with color
    fill_solid(leds, NUM_LEDS, flashColor);
    FastLED.show();
    delay(200);
  }
  
  FastLED.clear();
  FastLED.show();
}

// ============ MAIN LOOP ============
void loop() {
  // Run countdown once, then wait for reset
  runCountdown();
  
  // Idle state - gentle rainbow while waiting
  Serial.println("Countdown complete. Restarting in 5 seconds...");
  delay(5000);
}

Advanced Features: Adding Visual Effects

1. Progress Bar Integration

Add a horizontal progress bar at the bottom row to visualize remaining time:

Code

void drawProgressBar(uint8_t current, uint8_t total) {
  uint8_t filled = map(current, 0, total, 0, MATRIX_WIDTH);
  uint8_t row = MATRIX_HEIGHT - 1; // Bottom row
  
  for (uint8_t x = 0; x < MATRIX_WIDTH; x++) {
    if (x < filled) {
      leds[XY(x, row)] = CRGB::Green;
    } else {
      leds[XY(x, row)] = CRGB::Red;
    }
  }
}

2. Rainbow Color Cycling

Replace the red-to-green interpolation with a full rainbow cycle:

CRGB getRainbowColor(uint8_t number) {
  uint8_t hue = map(number, 1, 100, 0, 255);
  return CHSV(hue, 255, 255);
}

3. Fade Transitions Between Numbers

Add a smooth fade effect when changing numbers:

void fadeTransition(uint8_t fromNum, uint8_t toNum, uint16_t durationMs) {
  uint8_t steps = 30;
  uint16_t stepDelay = durationMs / steps;
  
  for (uint8_t step = 0; step <= steps; step++) {
    float ratio = (float)step / steps;
    
    // Fade out old number
    FastLED.clear();
    drawNumber(fromNum, CRGB::White, true);
    fadeToBlackBy(leds, NUM_LEDS, 255 * ratio);
    
    // Fade in new number (ghost effect)
    CRGB ghostColor = CRGB::White;
    ghostColor.nscale8(255 * ratio);
    drawNumber(toNum, ghostColor, true);
    
    FastLED.show();
    delay(stepDelay);
  }
}

Power Management & Safety

Calculating Power Requirements

ScenarioCurrent DrawRecommended Supply
All LEDs off~10mA (controller only)Any USB port
10% brightness, white~1.5A5V/3A adapter
50% brightness, white~7.7A5V/10A supply
100% brightness, white~15.4A5V/20A supply

Best Practice: Run at 30-50% brightness (BRIGHTNESS 64-128) for most applications. The human eye barely perceives the difference, but power consumption and heat drop dramatically.

Power Injection Strategy

For an 8×64 matrix, inject power at these points:

  1. Start (LED 0)
  2. Middle (LED 256, between rows 3-4)
  3. End (LED 511)

Use 14-16 AWG wire for power distribution, with the data signal running from the controller to LED 0 only.

Troubleshooting Common Issues

IssueLikely CauseSolution
Flickering or random colorsInsufficient powerAdd power injection points; increase supply capacity
First few LEDs work, rest don’tVoltage dropCheck ground continuity; add power at midpoint
Wrong colors (e.g., red shows green)Wrong COLOR_ORDERChange GRB to RGB or BGR in code
Numbers appear mirrored/flippedSerpentine direction wrongSwap the y % 2 logic in XY() function
ESP32 crashes/rebootsPower spike on LED updateAdd 1000µF capacitor; use separate power supply
Slow refresh rateToo many LEDs for MCUUse ESP32 with I2S/RMT driver ; reduce animation complexity

Expanding the Project

WiFi-Connected Countdown Timer

Using an ESP32, sync with NTP servers for accurate real-time countdowns:

#include <WiFi.h>
#include <NTPClient.h>
#include <WiFiUdp.h>

// Connect to WiFi, get time, countdown to specific target time

Sound Integration

Add a DFPlayer Mini or passive buzzer for audio cues at milestones (50, 25, 10, 5, 1).

Multiple Matrix Tiling

Chain multiple 8×64 matrices horizontally using FastLED’s parallel output capabilities on ESP32-S3 (up to 16 strips simultaneously)

to create a massive 8×128 or 8×256 display.


Conclusion

An 8×64 WS2812B matrix provides the perfect canvas for a large, visible countdown display. With 512 LEDs and the FastLED library’s efficient rendering, you can create smooth, colorful countdowns from 100 to 1 with professional-quality visual effects. The serpentine mapping function is the critical piece that translates your logical (x,y) coordinates to the physical LED layout—master this, and you can render anything from numbers to scrolling text to full animations.

Remember: Power management is not optional with this many LEDs. Plan your wiring, use adequate power supplies, and inject power at multiple points to ensure stable, bright, flicker-free operation.


Required Libraries: FastLED by Daniel Garcia (install via Arduino IDE Library Manager)

Recommended Hardware: ESP32 DevKit, 5V/20A power supply, 8×64 WS2812B matrix panel
Difficulty: Intermediate (electronics + coding)

About ogbugbu-technologies.com.ng

View all posts by ogbugbu-technologies.com.ng →

Leave a Reply

Your email address will not be published. Required fields are marked *