Maker.io main logo

Return to The Matrix with the Metro RP2350

2025-06-10 | By Adafruit Industries

License: See Original Project Displays LCD / TFT Arduino

Courtesy of Adafruit

Guide by Anne Barela and Tim C

Overview

adafruit_metro_large_4-3

Return to The Matrix with this project recreation. The falling streams of green characters are ‎strange yet calming. ‎

From Quora, the significance of The Matrix computer screen‎

  1. ‎Representation of Reality: The screen symbolizes the digital nature of the Matrix ‎itself. It reflects how the simulated world is constructed and manipulated by the ‎machines controlling humanity.‎

  2. Alternative Perception: Characters like Neo and Morpheus use the screen to perceive ‎the underlying code of the Matrix. It represents the ability to see beyond the illusion ‎of the real world and understand the truth of their existence.‎

  3. Coding Language: The characters are able to read the screen because it displays the ‎Matrix's code, which is a visual representation of the program that constructs their ‎reality. This code is often depicted in green characters on a black background, ‎reminiscent of classic computer interfaces.‎

Or, it just makes a cool visualization.‎

This project displays high resolution video generated by an Adafruit Metro RP2350. The ‎HSTX bus outputs DVI video which can be shown on an HDMI monitor. The Adafruit-DVI-‎HSTX library makes the project easy to write programs like this in Arduino, while displayio ‎makes it easy in CircuitPython. No soldering required.‎

Parts

You likely have an HDMI cable and monitor. If not, you can look to get these:

Preparing the Metro RP2350‎

HSTX Connection to DVI

metro_1

Get the HSTX cable. Any length Adafruit sells is fine. CAREFULLY lift the dark grey bar up on ‎the Metro, insert the cable silver side down, blue side up, then put the bar CAREFULLY ‎down, ensuring it locks in. If it feels like it doesn't want to go, do not force it.‎

Do the same with the other end on the DVI breakout. Note that the DVI breakout will be ‎inverted/upside down when compared to the Metro - this is normal for these boards and the ‎Adafruit cables.‎

That's it - no soldering, easy!‎

Arduino Version

metro_2

The first version uses Arduino. If you'd just like to run the program without using Arduino, ‎you can download the .UF2 file below in the green box. Save the file to your computer. Plug ‎the Metro RP2350 into your computer with a known good USB cable (data + power, not a ‎charge only cable).‎

Hold down the BOOT/BOOTSEL button (highlighted in red above), and while continuing to ‎hold it (don't let go!), press and release the reset button (highlighted in blue ‎above). Continue to hold the BOOT/BOOTSEL button until the RP2350 drive appears on ‎your computer! Copy the .UF2 file you saved previously to the RP2350 drive and The Matrix ‎should appear if you have the display connected.‎

matrix.uf2‎

Arduino

Please refer to the Arduino IDE setup in the Adafruit Metro RP2350 guide.‎

Adafruit Metro RP2350‎

By Tim C

‎Arduino IDE Setup

View Guide

guide_2a

You will want to add the library Adafruit DVI HSTX (version 1.10 or later, likely the latest ‎version) to your Arduino environment. Select Sketch - > Include Library -> Manage ‎Libraries... Search for "Adafruit DVI HSTX" by Jeff Epler. You will want version 1.1.0 or later, ‎likely the newest version. Click Install and accept installing libraries that Adafruit DVI HSTX ‎is dependent on. Those are the only libraries which need to be loaded.‎

libraries_3a

For the main program, get the code by clicking the "Download Project Bundle" button ‎below. Extract the file Metro_HSTX_Matrix.ino from the zip archive. Load it into the Arduino ‎IDE.‎

Download File

Copy Code
// SPDX-FileCopyrightText: 2021 Anne Barela for Adafruit Industries
//
// SPDX-License-Identifier: MIT
//
// Based on Adafruit-DVI-HSTX library code written by Jeff Epler 
// and use of Claude 3.7 Sonnet on 3/2/2025
// https://claude.site/artifacts/cf022b66-50c3-43eb-b334-17fbf0ed791c

#include <Adafruit_dvhstx.h>

// Display configuration for text mode in Adafruit-DVI-HSTX
const int SCREEN_WIDTH = 91;
const int SCREEN_HEIGHT = 30;

// Animation speed (lower = faster)
// Adjust this value to change the speed of the animation
const int ANIMATION_SPEED = 70; // milliseconds between updates

// Initialize display for Adafruit Metro RP2350
DVHSTXText display({14, 18, 16, 12});  // Adafruit Metro HSTX Pinout

// Define structures for character streams
struct CharStream {
  int x;          // X position
  int y;          // Y position (head of the stream)
  int length;     // Length of the stream
  int speed;      // How many frames to wait before moving
  int countdown;  // Counter for movement
  bool active;    // Whether this stream is currently active
  char chars[30]; // Characters in the stream
};

// Array of character streams - increased for higher density
// To fill 60-75% of the screen width (91 chars), we need around 55-68 active streams
CharStream streams[250]; // Allow for decent density

// Stream creation rate (higher = more frequent new streams)
const int STREAM_CREATION_CHANCE = 65; // % chance per frame to create new stream

// Initial streams to create at startup
const int INITIAL_STREAMS = 30;

// Random characters that appear in the streams
const char matrixChars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+[]{}|;:,.<>?/\\";
const int numMatrixChars = sizeof(matrixChars) - 1;

// Function declarations
void initStreams();
void updateStreams();
void drawStream(CharStream &stream);
void createNewStream();
char getRandomChar();

void setup() {
  // Initialize the display
  display.begin();
  display.clear();
  
  // Seed the random number generator
  randomSeed(analogRead(A0));
  
  // Initialize all streams
  initStreams();
}

void loop() {
  // Update and draw all streams
  updateStreams();
  
  // Randomly create new streams at a higher rate
  if (random(100) < STREAM_CREATION_CHANCE) {
    createNewStream();
  }
  
  // Control animation speed
  delay(ANIMATION_SPEED);
}

void initStreams() {
  // Initialize all streams as inactive
  for (int i = 0; i < sizeof(streams) / sizeof(streams[0]); i++) {
    streams[i].active = false;
  }
  
  // Create more initial streams for immediate visual impact
  for (int i = 0; i < INITIAL_STREAMS; i++) {
    createNewStream();
  }
}

void createNewStream() {
  // Find an inactive stream
  for (int i = 0; i < sizeof(streams) / sizeof(streams[0]); i++) {
    if (!streams[i].active) {
      // Initialize the stream
      streams[i].x = random(SCREEN_WIDTH);
      streams[i].y = random(5) - 5; // Start above the screen
      streams[i].length = random(5, 20);
      streams[i].speed = random(1, 4);
      streams[i].countdown = streams[i].speed;
      streams[i].active = true;
      
      // Fill with random characters
      for (int j = 0; j < streams[i].length; j++) {
        streams[i].chars[j] = getRandomChar();
      }
      
      return;
    }
  }
}

void updateStreams() {
  display.clear();
  
  // Count active streams (for debugging if needed)
  int activeCount = 0;
  
  for (int i = 0; i < sizeof(streams) / sizeof(streams[0]); i++) {
    if (streams[i].active) {
      activeCount++;
      streams[i].countdown--;
      
      // Time to move the stream down
      if (streams[i].countdown <= 0) {
        streams[i].y++;
        streams[i].countdown = streams[i].speed;
        
        // Change a random character in the stream
        int randomIndex = random(streams[i].length);
        streams[i].chars[randomIndex] = getRandomChar();
      }
      
      // Draw the stream
      drawStream(streams[i]);
      
      // Check if the stream has moved completely off the screen
      if (streams[i].y - streams[i].length > SCREEN_HEIGHT) {
        streams[i].active = false;
      }
    }
  }
}

void drawStream(CharStream &stream) {
  for (int i = 0; i < stream.length; i++) {
    int y = stream.y - i;
    
    // Only draw if the character is on screen
    if (y >= 0 && y < SCREEN_HEIGHT) {
      display.setCursor(stream.x, y);
      
      // Set different colors/intensities based on position in the stream
      if (i == 0) {
        // Head of the stream is white (brightest)
        display.setColor(TextColor::TEXT_WHITE, TextColor::BG_BLACK, TextColor::ATTR_NORMAL_INTEN);
      } else if (i < 3) {
        // First few characters are bright green
        display.setColor(TextColor::TEXT_GREEN, TextColor::BG_BLACK, TextColor::ATTR_NORMAL_INTEN);
      } else if (i < 6) {
        // Next few are medium green
        display.setColor(TextColor::TEXT_GREEN, TextColor::BG_BLACK, TextColor::ATTR_LOW_INTEN);
      } else {
        // The rest are dim green
        display.setColor(TextColor::TEXT_GREEN, TextColor::BG_BLACK, TextColor::ATTR_V_LOW_INTEN);
      }
      
      // Draw the character
      display.write(stream.chars[i]);
    }
  }
  
  // Occasionally change a character in the stream
  if (random(100) < 25) { // 25% chance
    int idx = random(stream.length);
    stream.chars[idx] = getRandomChar();
  }
}

char getRandomChar() {
  return matrixChars[random(numMatrixChars)];
}

View on GitHub

Select Adafruit Metro RP2350 as the board in the box in the toolbar. ‎

Plug your Metro into your computer via a known good USB data + power cable (not the tiny ‎power-only cables that come with battery packs). The Metro should show up as a new ‎serial port. Select that serial port under Tools -> Port.‎

Click the arrow key -> on the toolbar to compile the program and upload it to the Metro.‎

Changing the Code

You can change how many streams are on the screen at the same time by changing ‎the STREAM_CREATION_CHANCE variable. It should be from 10 (lowest) to 99 (highest).‎

Using Pico 2 or Fruit Jam instead of Metro RP2350‎

The HSTX pins for Metro RP2350 are defined as:‎

DVHSTXText display({14, 18, 16, 12});‎

But for other boards you may need to use

DVHSTXText display({12, 14, 16, 18});‎

It's all depending how you wired your HSTX bus - the wire pairs can be swapped IF your code ‎knows it. It would help if the Pico 2 had an HSTX connector on top. Oh well.‎

CircuitPython Version

This code was written for CircuitPython 9.2.x and not 10.0.0. Please use CircuitPython 9.x ‎until a dual code version is available.‎

With some help from the friendly robot Claude.ai, Tim ported the Matrix rain Arduino code ‎to a CircuitPython.‎

All of the same functions from the Arduino version of the code have counterparts in the ‎CircuitPython version. In order to render glyphs, a TileGrid is used along with a ‎spritesheet containing some Japanese Katakana characters from Unifoundry, as well as ‎the letters used to spell "Adafruit", "jam" and "matrix".‎

All characters are rendered mirrored horizontally by using the flip_x property of ‎the TileGrid as was done in the movie graphics.‎

For coloring the glyphs, a TilePaletteMapper is used. This class is a special kind ‎of pixel_shader that allows us to re-map color indexes for individual tiles within a TileGrid. ‎The code uses 16 shades of green in the shader_palette. As it renders the falling glyphs, ‎their colors are set by adjusting the mapping at their x,y location within the grid to a color ‎index for a green that has brightness relative to their position in the stream.‎

programming_circuitpython_matrix_rain

Project Setup

Are you new to using CircuitPython? No worries, there is a full getting-started guide here.‎

Plug the device into your computer with a known good USB cable (not a charge-only cable). ‎The device will appear to your computer in File Explorer or Finder (depending on your ‎operating system) as a flash drive named CIRCUITPY. If the drive does not appear, you ‎can install CircuitPython on your device and then return here.‎

Download the project files with the Download Project Bundle button below. Unzip the file ‎and copy/paste the code.py and other project files to your CIRCUITPY drive using File ‎Explorer or Finder (depending on your operating system).‎

‎Download Project Bundle

Copy Code
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
# SPDX-License-Identifier: MIT
"""
Matrix rain visual effect

Largely ported from Arduino version in Metro_HSTX_Matrix to
CircuitPython by claude with some additional tweaking to the
colors and refresh functionality.
"""
import sys
import random
import time
import displayio
import supervisor
from displayio import Group, TileGrid
from tilepalettemapper import TilePaletteMapper
from adafruit_fruitjam.peripherals import request_display_config
import adafruit_imageload


# use the built-in HSTX display
request_display_config(320, 240)
display = supervisor.runtime.display

# screen size in tiles, tiles are 16x16
SCREEN_WIDTH = display.width // 16
SCREEN_HEIGHT = display.height // 16

# disable auto_refresh, we'll call refresh() after each frame
display.auto_refresh = False

# group to hold visual elements
main_group = Group()

# show the group on the display
display.root_group = main_group

# Color gradient list from white to dark green
COLORS = [
    0xFFFFFF,
    0x88FF88,
    0x00FF00,
    0x00DD00,
    0x00BB00,
    0x009900,
    0x007700,
    0x006600,
    0x005500,
    0x005500,
    0x003300,
    0x003300,
    0x002200,
    0x002200,
    0x001100,
    0x001100,
]

# Palette to use with the mapper. Has 1 extra color
# so it can have black at index 0
shader_palette = displayio.Palette(len(COLORS) + 1)
# set black at index 0
shader_palette[0] = 0x000000

# set the colors from the gradient above in the
# remaining indexes
for i in range(0, len(COLORS)):
    shader_palette[i + 1] = COLORS[i]

# mapper to change colors of tiles within the grid
if sys.implementation.version[0] == 9:
    grid_color_shader = TilePaletteMapper(
        shader_palette, 2, SCREEN_WIDTH, SCREEN_HEIGHT
    )
elif sys.implementation.version[0] >= 10:
    grid_color_shader = TilePaletteMapper(shader_palette, 2)

# load the spritesheet
katakana_bmp, katakana_pixelshader = adafruit_imageload.load("matrix_characters.bmp")

# how many characters are in the sprite sheet
char_count = katakana_bmp.width // 16

# grid to display characters within
display_text_grid = TileGrid(
    bitmap=katakana_bmp,
    width=SCREEN_WIDTH,
    height=SCREEN_HEIGHT,
    tile_height=16,
    tile_width=16,
    pixel_shader=grid_color_shader,
)

# flip x to get backwards characters
display_text_grid.flip_x = True

# add the text grid to main_group, so it will be visible on the display
main_group.append(display_text_grid)


# Define structures for character streams
class CharStream:
    def __init__(self):
        self.x = 0  # X position
        self.y = 0  # Y position (head of the stream)
        self.length = 0  # Length of the stream
        self.speed = 0  # How many frames to wait before moving
        self.countdown = 0  # Counter for movement
        self.active = False  # Whether this stream is currently active
        self.chars = [" "] * 30  # Characters in the stream


# Array of character streams
streams = [CharStream() for _ in range(250)]

# Stream creation rate (higher = more frequent new streams)
STREAM_CREATION_CHANCE = 65  # % chance per frame to create new stream

# Initial streams to create at startup
INITIAL_STREAMS = 30


def init_streams():
    """Initialize all streams as inactive"""
    for _ in range(len(streams)):
        streams[_].active = False

    # Create initial streams for immediate visual impact
    for _ in range(INITIAL_STREAMS):
        create_new_stream()


def create_new_stream():
    """Create a new active stream"""
    # Find an inactive stream
    for _ in range(len(streams)):
        if not streams[_].active:
            # Initialize the stream
            streams[_].x = random.randint(0, SCREEN_WIDTH - 1)
            streams[_].y = random.randint(-5, -1)  # Start above the screen
            streams[_].length = random.randint(5, 20)
            streams[_].speed = random.randint(0, 3)
            streams[_].countdown = streams[_].speed
            streams[_].active = True

            # Fill with random characters
            for j in range(streams[_].length):
                # streams[i].chars[j] = get_random_char()
                streams[_].chars[j] = random.randrange(0, char_count)
            return


def update_streams():
    """Update and draw all streams"""
    # Clear the display (we'll implement this by looping through display grid)
    for x in range(SCREEN_WIDTH):
        for y in range(SCREEN_HEIGHT):
            display_text_grid[x, y] = 0  # Clear character

    # Count active streams (for debugging if needed)
    active_count = 0

    for _ in range(len(streams)):
        if streams[_].active:
            active_count += 1
            streams[_].countdown -= 1

            # Time to move the stream down
            if streams[_].countdown <= 0:
                streams[_].y += 1
                streams[_].countdown = streams[_].speed

                # Change a random character in the stream
                random_index = random.randint(0, streams[_].length - 1)
                # streams[i].chars[random_index] = get_random_char()
                streams[_].chars[random_index] = random.randrange(0, char_count)

            # Draw the stream
            draw_stream(streams[_])

            # Check if the stream has moved completely off the screen
            if streams[_].y - streams[_].length > SCREEN_HEIGHT:
                streams[_].active = False


def draw_stream(stream):
    """Draw a single character stream"""
    for _ in range(stream.length):
        y = stream.y - _

        # Only draw if the character is on screen
        if 0 <= y < SCREEN_HEIGHT and 0 <= stream.x < SCREEN_WIDTH:
            # Set the character
            display_text_grid[stream.x, y] = stream.chars[_]

            if _ + 1 < len(COLORS):
                grid_color_shader[stream.x, y] = [0, _ + 1]
            else:
                grid_color_shader[stream.x, y] = [0, len(COLORS) - 1]
    # Occasionally change a character in the stream
    if random.randint(0, 99) < 25:  # 25% chance
        idx = random.randint(0, stream.length - 1)
        stream.chars[idx] = random.randrange(0, 112)


def setup():
    """Initialize the system"""
    # Seed the random number generator
    random.seed(int(time.monotonic() * 1000))

    # Initialize all streams
    init_streams()


def loop():
    """Main program loop"""
    # Update and draw all streams
    update_streams()

    # Randomly create new streams at a higher rate
    if random.randint(0, 99) < STREAM_CREATION_CHANCE:
        create_new_stream()

    display.refresh()
    available = supervisor.runtime.serial_bytes_available
    if available:
        c = sys.stdin.read(available)
        if c.lower() == "q":
            supervisor.reload()


# Main program
setup()
while True:
    loop()

View on GitHub

Usage

usage_4

Plug the Metro into a HDMI monitor via an HDMI cable.‎

Power the Metro RP2350 either via USB C (5 volts) or the barrel power connection (5.5 to 17 ‎volts DC, center positive). ‎

The Matrix animation will automatically start!‎

adafruit_metro_full_small

adafruit_metro_large_4-3

Changing the Number of Streams

‎‎See the Arduino and CircuitPython pages for the variable you'll want to change to get more ‎or fewer streams.‎

Mfr Part # 6003
METRO RP2350 WITHOUT PSRAM
Adafruit Industries LLC
$204.86
View More Details
Mfr Part # 6055
ADAFRUIT RP2350 22-PIN FPC HSTX
Adafruit Industries LLC
Mfr Part # 6036
22-PIN 0.5MM PITCH FPC FLEX CABL
Adafruit Industries LLC
Mfr Part # 4474
CABLE A PLUG TO C PLUG 3'
Adafruit Industries LLC
Mfr Part # 608
CABLE M-M HDMI-A 1M
Adafruit Industries LLC
Mfr Part # SC0940
RASPBERRY PI MONITOR (RED/WHITE)
Raspberry Pi
$821.10
View More Details
Add all DigiKey Parts to Cart
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.