Introduction: Led Matrix, Circuit-sculpture Style. ATTiny85 With Max7221 Led Driver, With External MCU Connector

About: Software Engineer Musician (Bass Guitar) Maker & Tinkerer

I like the game of patience that is building circuit-sculpture style circuits.

I enjoy the planning, the trials and errors that often make the end result quite different from the plan.


I'm also a fan of the so-called SuperComputer Panel, that BigClive has on some of his youtube videos, and I recently made an instructable with various software implementations of this blinking led panel.


So I decided to combine both.

I wanted the led matrix to be autonomous and I had an ATTiny85 laying around. This is more than enough to run the software while being...tiny!


I'm sharing the build below, should anyone be interested to build it.

Supplies

You'll need:

  1. 1mm brass rod and a bit of 0.5mm brass rod
  2. 64 5mm Red Leds
  3. 1 piece of 6 pin DuPont female connector
  4. 1 Max7221
  5. 6 diodes (pretty much any kind)
  6. 4 10KOhms resistors
  7. 1 micro sliding switch
  8. 1 Attiny 85
  9. 1 100nF capacitor
  10. 1 10 uF (or any value, it's not critical) electrolythic capacitor
  11. 1 piece of wood for the base
  12. a 3D printer to print the led jigs and matrix holders

And... a lot of patience!

Step 1: Schematic, Code and Design Planning

The ATTiny85 is driving the Max7221 via SPI, using the LedControl library.

I wanted to be able to use the matrix with an external more powerful MCU too, so I designed the SPI and CS connections so that they could be shared.

This required diodes between the output of the MCUs and the inputs of the Max7221, as well as pull-down resistors on the Max7221 inputs).

The micro slider switch is used to power-off the ATTiny85 when an external MCU is used.


The sketch running on the ATTiny85 is adapted from my previous "SuperComputer Panel" Instructable, to comply with the limited memory resources of the ATTiny85.


There are plenty of tutorials online on how to program an ATTiny85 with the Arduino IDE.

I'm now using this excellent MiniCore Arduino core.

Something you must take care of with this build:

  1. Select the board config ATTiny85 with Internal Clock @ 8 MHz
  2. Use the "Burn Bootloader" command, in order to set the internal fuses for the ATTiny to run at 8MHz. Otherwise the sketch won't run (well, it will run but the LED won't light-up).


I'm using an USBAsp programmer, cheap and efficient, but you may also use an Arduino as ISP.


The software will be available here very soon!


Now that the software part is done, here comes the fun: building this thing.


I usually don't plan very much on paper, I draw a rough daft and go from here.

This time I drew an accurate representation of the matrix and integrated circuit, respecting their dimensions, so that I could plan ahead the numerous connections.


I wanted the vertical wires to be...vertical and evenly spaced, on a plane perpendicular to the matrix.

I drew the wires going to the matrix colums trying to plan their path to their pins. This was mainly to determine the adequate spacing between the bottom of the matrix and the top of the crcruits.

I even tried to plan the depth for wire-crossing, but ended-up doing things differently, because I was unable to shape the wires as accurately as planned.


Then it was time to sequence the build tasks (so that I don't have to un-solder things when I had to do when making my Christmas star).

Step 2: Solder the Circuits

First thing first: have a working led driver.

So I soldered the circuits assembly, planning the connections so that the left end is the positive power rails and the right one, the negative one.

I could do without the 10KOhms variable resistor (it has little effect on the brightness setting), but I kept it anyway (I didn't have a higher value one at hand, I might replace it some day).


As usual when building things (or programming, I'm a software engineer), test early and often!


So I wired a LED between one line and column outputs and had the pleasure of seing it blink.

Step 3: Build the Led Matrix

I first 3D-printed a jig for the actual matrix, in order to be able to have neatly aligned LED.

Then I printed a little jig to bend each LED wires the same way: the positive and negative pins are spaced about 3mm to allow room for safe row and column crossing.


It took a bit of time but it went pretty well.

Of course I tested each connection after soldering.

Step 4: Build the Base

The base is made of scavenged wood.

I drilled two 1.5mm holes spaced by 63mm (this is the space between the first and last column of the matrix).

I then 3D printed 2 brackets to hold the matrix on the 1.5mm wires that are also used as power lines.

Step 5: Build the Matrix

I started by soldering the circuits to the vertical power lines.

I made sure I had enough space upwards for the column wires.


I then wired one column and one row, trying to be faithful to my initial plan.

Then I tested that the led was blinking as expected.


Then I wired the columns.


These 1mm wires are stiff, and impossible to bend after soldering wihtout risking damaging the connections.

So it's a game of patience to shape each wire accordingly before actually soldering.

I used a couple of pliers and a wire cutter.


After wiring all columns I did a new test to make sure that all the leds of the top row were blinking as expected.


Then I wired the other rows, trying as much as possible to have the vertical wires on the same plane.

I'm not that satisfied with the way a few wires are routed to the Max7221, but I can live with it.


What a joy when the whole matrix is blinking as expected!


Now was the time to wire the power supply connector

Step 6: Power Supply Connector

The matrix is powered by a micro-usb breakout board, or by an external MCU.

I decided to have the connectors at each end of the base, and have the power wires run accros it, soldered to their respective vertical power wires.

I 3D printed a little chock to help maintain the wires parallel while soldering.


The external MCU connctor is a female DuPont connector cut to length with a Dremmel.

I'm using a 6 pin connector so that I can have a 'key' on the connector to prevent any reverse-polarity when plugging an external MCU.

The connectors are glued to the wooden base.

Step 7: External MCU Connector and Connections

The MOSI, CLK and CS pins of the Max7221 must be routed to the external MCU connector, via diodes.

I used 0.5mm brass wire, this was much easier, although not easy at all!

I wanted these wires to be parallel, nicely aligned at the center of the matrix.

They also need to go to the external MCU connector.

After many trials and errors I first shaped and soldered the wires going to the Max7221, helped by 2 little 3D-printed brackets to hold the 3 wires together.

Of course I melted the bottom bracket...

I also used a bit of UV-Resin to hold the base of the wires on the wooden base.


Then I shaped and soldered the horizontal wires going to the external MCU connector.

Step 8: ESP32 Adapter

I wanted to be able to use a MCU such as an ESP32 with my LED Matrix.

I designed a little adapter to be able to easily plug any ESP32.


Then I found the great https://docs.arduino.cc/libraries/md_max72xx/ library to drive a Max72xx on ESP32.

It even comes with a neat sketch that provieds a minimal web page to be used to display any message.

I modified the MD_MAX72XX_Message_ESP32 sketch to use WIFI in Access Point mode (private protal) instead of Station mode, so that I can easily use it anywhere without needing to reconfigure its wifi settings.


For now the upload of the ESP32 sketch is failing so until I can upload it it's available in the next section.


All in all, I'm quite satisfied with the result.

Step 9: ESP32 Code

For now the upload of the ESP32 sketch is failing so until I can upload it, here it is:


// ESP32 as Access Point with private portal and LED matrix IP display
// Uses MD_MAX72XX library to scroll the AP IP on the display.
// Users can connect to the ESP32's AP and send messages via a web interface.

#include <WiFi.h>
#include <WiFiServer.h>
#include <MD_MAX72xx.h>

#define PRINT_CALLBACK 0
#define DEBUG 1
#define LED_HEARTBEAT 0

#if DEBUG
#define PRINT(s, v) { Serial.print(F(s)); Serial.print(v); }
#define PRINTS(s) { Serial.print(F(s)); }
#else
#define PRINT(s, v)
#define PRINTS(s)
#endif

#if LED_HEARTBEAT
#define HB_LED D2
#define HB_LED_TIME 500 // in milliseconds
#endif

// Define the number of devices we have in the chain and the hardware interface
#define HARDWARE_TYPE MD_MAX72XX::PAROLA_HW
#define MAX_DEVICES 1
// GPIO pins
#define CLK_PIN 18 // or SCK
#define DATA_PIN 23 // or MOSI
#define CS_PIN 17 // or SS
// SPI hardware interface
MD_MAX72XX mx = MD_MAX72XX(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);

// AP credentials
const char ap_ssid[] = "ESP32_Matrix";
const char ap_password[] = "password";

// WiFi Server object and parameters
WiFiServer server(80);

// Global message buffers shared by Wifi and Scrolling functions
const uint8_t MESG_SIZE = 255;
const uint8_t CHAR_SPACING = 1;
const uint8_t SCROLL_DELAY = 75;
char curMessage[MESG_SIZE];
char newMessage[MESG_SIZE];
bool newMessageAvailable = false;

const char WebResponse[] = "HTTP/1.1 200 OK\nContent-Type: text/html\n\n";
const char WebPage[] =
"<!DOCTYPE html>" \
"<html>" \
"<head>" \
"<title>ESP32 LED Matrix Portal</title>" \
"<script>" \
"function SendText() {" \
" var msg = document.getElementById('msg').value;" \
" var xhr = new XMLHttpRequest();" \
" xhr.open('GET', '/set?msg=' + encodeURIComponent(msg), true);" \
" xhr.send();" \
"}" \
"</script>" \
"</head>" \
"<body>" \
"<h1>ESP32 LED Matrix</h1>" \
"<p>Enter a message to scroll on the LED matrix:</p>" \
"<input type='text' id='msg' placeholder='Your message here' maxlength='255'>" \
"<button onclick='SendText()'>Send</button>" \
"</body>" \
"</html>";

void handleWiFi(void)
{
WiFiClient client = server.available();
if (client) {
PRINTS("\nNew client connected");
String currentLine = "";
while (client.connected()) {
if (client.available()) {
char c = client.read();
if (c == '\n') {
if (currentLine.length() == 0) {
// Send the web page
client.print(WebResponse);
client.print(WebPage);
break;
} else {
// Check for GET request with message
if (currentLine.startsWith("GET /set?msg=")) {
int msgStart = currentLine.indexOf("msg=") + 4;
int msgEnd = currentLine.indexOf(" ", msgStart);
String msg = currentLine.substring(msgStart, msgEnd);
msg.replace("%20", " ");
msg.toCharArray(newMessage, MESG_SIZE);
newMessageAvailable = true;
PRINT("\nNew message: ", newMessage);
}
currentLine = "";
}
} else if (c != '\r') {
currentLine += c;
}
}
}
client.stop();
PRINTS("\nClient disconnected");
}
}

void scrollDataSink(uint8_t dev, MD_MAX72XX::transformType_t t, uint8_t col)
{
#if PRINT_CALLBACK
Serial.print("\n cb ");
Serial.print(dev);
Serial.print(' ');
Serial.print(t);
Serial.print(' ');
Serial.println(col);
#endif
}

uint8_t scrollDataSource(uint8_t dev, MD_MAX72XX::transformType_t t)
{
static enum { S_IDLE, S_NEXT_CHAR, S_SHOW_CHAR, S_SHOW_SPACE } state = S_IDLE;
static char *p;
static uint16_t curLen, showLen;
static uint8_t cBuf[8];
uint8_t colData = 0;

switch (state)
{
case S_IDLE:
PRINTS("\nS_IDLE");
p = curMessage;
if (newMessageAvailable) {
PRINT("\nNew message - ", newMessage);
strcpy(curMessage, newMessage);
newMessageAvailable = false;
}
state = S_NEXT_CHAR;
break;
case S_NEXT_CHAR:
PRINT("\nS_NEXT_CHAR ", *p);
if (*p == '\0')
state = S_IDLE;
else {
showLen = mx.getChar(*p++, sizeof(cBuf) / sizeof(cBuf[0]), cBuf);
curLen = 0;
state = S_SHOW_CHAR;
}
break;
case S_SHOW_CHAR:
PRINTS("\nS_SHOW_CHAR");
colData = cBuf[curLen++];
if (curLen < showLen)
break;
showLen = (*p != '\0' ? CHAR_SPACING : (MAX_DEVICES*8)/2);
curLen = 0;
state = S_SHOW_SPACE;
case S_SHOW_SPACE:
PRINT("\nS_SHOW_SPACE: ", curLen);
PRINT("/", showLen);
curLen++;
if (curLen == showLen)
state = S_NEXT_CHAR;
break;
default:
state = S_IDLE;
}
return(colData);
}

void scrollText(void)
{
static uint32_t prevTime = 0;
if (millis() - prevTime >= SCROLL_DELAY) {
mx.transform(MD_MAX72XX::TSR);
prevTime = millis();
}
}

void setup(void)
{
#if DEBUG
Serial.begin(115200);
delay(1000);
PRINTS("\n[ESP32 AP LED Matrix Portal]\nConnect to 'ESP32-AP' and send messages from your browser.");
#endif

#if LED_HEARTBEAT
pinMode(HB_LED, OUTPUT);
digitalWrite(HB_LED, LOW);
#endif

// Display initialization
PRINTS("\nInitializing Display");
mx.begin();
mx.setShiftDataInCallback(scrollDataSource);
mx.setShiftDataOutCallback(scrollDataSink);
curMessage[0] = newMessage[0] = '\0';

// Start ESP32 as Access Point
PRINTS("\nStarting Access Point");
WiFi.softAP(ap_ssid, ap_password);
IPAddress myIP = WiFi.softAPIP();
PRINT("\nAP IP: ", myIP);

// Start the server
PRINTS("\nStarting Server");
server.begin();

// Set up first message as the AP IP address
sprintf(curMessage, "%d.%d.%d.%d", myIP[0], myIP[1], myIP[2], myIP[3]);
PRINT("\nAP IP: ", curMessage);
}

void loop(void)
{
#if LED_HEARTBEAT
static uint32_t timeLast = 0;
if (millis() - timeLast >= HB_LED_TIME) {
digitalWrite(HB_LED, digitalRead(HB_LED) == LOW ? HIGH : LOW);
timeLast = millis();
}
#endif
handleWiFi();
scrollText();
}