Mercator: An ESP32-based spherical persistence-of-vision display

A spinning ring of DotStar LEDs creates a programmable globe.

Matt Welsh
10 min readNov 29, 2020

I’ve been working on this project for the last few months, and finally got to the point where I can finally declare it done. Mercator is a spherical persistence-of-vision display based on a ring of DotStar programmable LEDs and the Adafruit HUZZAH32 ESP32 dev board. As the LEDs spin around, the pattern changes to display pretty much anything:

The whole project is open source and the source code and design files can be found here. In this post I’ll walk through the design and explain it in some detail.

Do a Google search for “persistence of vision globe” and you’ll come up with a bunch of similar projects on the Internet, including some impressive YouTube videos. I wanted to build something based on my favorite dev board (the AdaFruit HUZZAH32), and using primarily 3D printed and/or lasercut physical components (rather than, say, CNC milled aluminum). Mercator is the result.

Parts

Here’s the stuff one would need to buy to build one of these:

  • Adafruit DotStar LED strip, with 144 LEDs/meter. I cut the strip and use a half meter (72 LEDs in total).
  • Adafruit HUZZAH32 dev board, based on the ESP32 processor. I program this using the Arduino IDE. This board drives the LED strip and controls the motor speed.
  • DC motor. I use this big honking 30W motor that runs off of a 12V power supply. This is no doubt ridiculously overpowered but I wanted something with a large shaft that I could mate with.
  • Two ball bearings, such as these, to hold the base and top of the LED ring steady while allowing free rotation.
  • Since the LED strip is rotating but the electronics are housed in the base, I use a slip ring which allows for electrical connections to be maintained between the two. You need one with at least four wires, such as this one from Adafruit.
  • The motor drives the LED ring using an XL timing belt, such as this one. There is a 3D printed pulley connected to the motor shaft which mates with the teeth of the belt.
  • US5881LUA hall effect sensor. This is used to detect the rotation of the LED ring, which has a small magnet attached to the base (more on this below).
  • Three 10kΩ potentiometers, one each to control the rotation speed, display rotation, and LED brightness.
  • A power switch and a pushbutton. The switch simply controls power to the motor, while the button is used to start the motor and switch between display modes.
  • I used a custom PCB to connect all of the parts, the schematic and Eagle files for which are in the Mercator GitHub repo. One could just as easily use a breadboard.
  • You’ll also need a 1kΩ resistor, two 10μF capacitors, a 7805 voltage regulator, a RFP30N06LE power MOSFET (or similar), and a 1N4004 diode. My PCB design assumes a 12V, 1A power supply connected to the PCB using a 2.1mm DC barrel jack. My PCB uses 2.5mm pitch connectors (JST XH connectors being my favorite) to connect to the other external components, such as the LEDs, motor, pots, etc.

Physical design

There are three main components to Mercator: The base, the clear acrylic shield, and the 3D printed LED ring. The base is laser cut out of 3mm thick wood. The shield cut from 3mm clear acrylic. The shield is there for two reasons: first, to prevent injuries (in case one of my kids decided to stick their hands in front of the spinning ring), but also to hold the top axis of the LED ring upright. I use two 12mm shaft diameter ball bearings, one mounted in the top face of the shield, and another in the top face of the base, to hold the LED ring. The shield has notches that align with matching holes in the top face of the base to hold it in place, so it is easy to lift the shield off if needed.

The case and 3D printed components were designed in Fusion360.

The design files for each component of the base and shield are included in the Mercator GitHub repo.

The centerpiece of the design is the 3D printed LED ring, which I also designed in Fusion360. It is essentially a hollow vertical shaft with a solid ring for mounting the LED strip. Holes in the side of the shaft and on the base of the ring allow wires to pass through. I cut the LED strip into two halves of 36 LEDs each, connecting the two halves with a few short lengths of wire soldered on to each end. The two halves of the strip are mounted to the ring using foam adhesive tape. One end of the LED strip is connected to four wires from the slip ring, which run up through the middle of the hollow shaft and emerge from small holes near the base.

The base of the LED ring is an XL timing belt-compatible pulley. The timing belt mates with an identical 3D-printed pulley mounted on the shaft of the motor and held in place with two set screws. The LED ring is simply pushed into the ball bearing mounted in the base. It is not glued so it is easy to lift the LED ring off of the base if needed.

To allow for the belt tension to be adjusted, the top face of the base has a slot allowing the motor to be positioned closer to or farther from the LED ring. The motor hangs down from the top face of the base and is secured using two screws.

The front panel has the power switch, start button, and three pots attached to it. Magnets hold the front panel to the front of the base, so it can be easily removed to get access to the electronics inside.

Detecting rotation

A key aspect of the design is detecting when the LED ring is in a certain rotational position, so we can adjust the speed at which to update the LED strip to display the desired pattern. For this I use a US5881LUA Hall effect sensor which detects the presence or absence of a magnetic field passing by its face. Small magnets are mounted on the base of the LED ring and the Hall sensor positioned to be a few millimeters away from the end of the magnet. The Hall sensor is connected to an input pin on the microcontroller which goes low when the magnet passes by.

Close-up of Mercator’s mechanics. On the left is the motor shaft with 3D printed XL timing belt pulley attached using set screws. The timing belt connects to a similar pulley on the base of the LED ring, on the right. Mounted to the LED ring is a small magnet, which rotates with the ring. The Hall effect sensor (small black component) is a few mm from the end of the magnet, and mounted to an L-bracket attached to the base using a ziptie.

Electronics

The electronics for this project are fairly straightforward. The ESP32 dev board controls everything and is attached to the LED strip, potentiometer inputs, Hall effect sensor, and the start button. The HUZZAH32 board is powered via a micro-USB cable connected to a standard 5V USB wall wart. The motor uses a separate 12V/1A power supply.

Originally, I was intending to power both the motor and the ESP32 from a common 12V power supply, using a 7805 linear regulator to provide 5V to the HUZZAH32 board. However, the current draw of the ESP32 and the LED strip together are substantial (especially when all LEDs are on) which generates a great deal of heat in the 7805. I ended up only using the 7805 to provide 5V to the LED inside of the start button.

Schematic for the Mercator connector board. See the GitHub repo for the full design files.

The speed of the motor is controlled using a PWM signal on one of the ESP32’s GPIO pins, which is connected to the gate of the RFP30N06LE power MOSFET. This allows the ESP32 to modulate the power to the motor, and hence its speed, by setting the PWM rate.

I designed a custom PCB and had boards made by jlcpcb.com ($20 or so gets you five boards, all in, including shipping!), but this is not really necessary and a breadboard or protoboard could be used instead. The HUZZAH32 mounts to the top side of this board using Molex connectors, and I use JST XH connectors for the other board-to-wire connections.

The fully-populated Mercator PCB.

Software

Okay, with the hardware and electronics out of the way, let’s get to the software side of things. The ESP32 is programmed using the Arduino SDK and makes use of the Adafruit Dotstar library to control the LED strip. Here’s a direct link to the program source.

The Mercator software supports rectangular input images of resolution 72 x 36 pixels. The 36px height is due to the number of LEDs that fit on the ring (72 LEDs in total, 36 on each side). For a good persistence-of-vision effect, we want to run the motor as fast as possible, but no faster than is needed to draw an entire 360-degree “sweep” of an input image per revolution. Through experimentation, I found that I could update the LED strip about 36 times per revolution while maintaining a reasonable motor speed and good visual effect. This equates to 72 columns in the input image, since the “front” and “back” of the LED strip can simultaneously display two columns that are 180 degrees out of phase. (In retrospect, I could have additionally leveraged the fact that the front and back pixel strips need not be perfectly aligned to virtually double the vertical resolution, but the current design does not do this.)

A little Python script reads an input image, scales it to 72x36, and writes it out as a C header file, three bytes per pixel (one byte each for red, green, and blue). The Mercator code includes one or more of these header files, allowing multiple images to be stored in the program binary and cycled through when the user presses the front panel button.

You can pack a surprisingly large amount of detail into a 72x36px image.

Apart from static image files, I also added a version of Conway’s Game of Life, where each pixel is shaded according to how many generations it has been “alive”.

The program works by calculating the revolution time of the LED ring based on detections from the Hall effect sensor. This value is smoothed using an EWMA filter and the time for each column of the input image is calculated as 1/36th of this interval. On each time step, the program decides if it’s time to display the next column, and if so, updates the front and back segments of the LED ring accordingly. Because the front and back LED segments are slightly offset, a vertical shift is applied to the back pixel segment relative to the front.

The program also reads the values of the three pots and uses them to control the motor speed, LED strip brightness, and horizontal rotational speed of the image accordingly. Pushing the front panel button cycles through the different images as well as the Game of Life simulation.

Demo

Taking pictures and videos of Mercator is frustrating, because the typical phone camera cannot capture the persistence-of-vision effect observed by the human eye. Here’s a GIF made from an iPhone video of the globe as it is spinning:

It looks much better than this in person, of course, and the “banding” effect seen in the above video is an artifact of the camera, not visible to the human eye.

To capture a decent photograph, I had to use a long-exposure camera app, which produces okay results, but still somewhat artificially washed out:

That’s no moon.

Lessons learned

As usual with my projects, this one took me a lot longer than I anticipated and, after some initial success with the physical design, the project sat on my workbench collecting dust for a while before I got around to writing the software. A few things that tripped me up and which I would have done differently if I started from scratch:

More LEDs: I happened to have this length of LED strip on hand and the size is right for a desktop display, but in retrospect, the vertical resolution is not that great. As mentioned previously I could double the vertical resolution by leveraging an offset between the front and back pixel strips. Another approach would be to have multiple LED rings at different angles, which would increase both apparent vertical as well as actual horizontal resolution of the display.

Eliminating wobble: The above design suffers from a bit of visible jitter in the horizontal axis as the ring spins around. I believe this is due to the use of a timing belt where the engagement of the teeth on the belt and the notches on the pulleys on either end are not consistent through the entire revolution. Direct drive of the ring via the motor shaft would probably fix this problem.

Hope you enjoyed this writeup and drop me a line if you have any thoughts or suggestions!

--

--

Matt Welsh

AI and Systems hacker. Formerly at Fixie.ai, OctoML, Google, Apple, Harvard CS prof. I like big models and I cannot lie.