Introduction: Huge POV Display
Ever wanted to see your name in lights? In this instructable we will show you how!
We will show you how to make a big Persistence Of Vision Display, or POV Display for short. It is basically a row of lights, LEDs in our case, which turns around very fast to show an image.
Furthermore, we wanted to go big, so we made a display of 1 meter; ideal for displaying huge logo's!
But what is this Persistence of vision thing? Wikipedia explains it as follows: Persistence of vision refers to the optical illusion that occurs when visual perception of an object does not cease for some time after the rays of light proceeding from it have ceased to enter the eye. The illusion has also been described as "retinal persistence" "persistence of impressions" or simply "persistence" and other variations.
Or in normal terms: Our eyes are too slow to see the LEDs line by line, and will see it as a circle of continuous light. So by changing the colors at the right time, we can make images; how cool is that?!
We made this display for a project at the university. We are in the first master year of electrical engineering at the VUB in Brussels, Belgium; that is also why its logo is the cover photo of this instructable!
The project is a bit more advanced, and we will dive into all details and design considerations. But if you simply want to reproduce it, you can focus on the results only ;)
We will start by building the rotating arm which holds the LED strip, and by making the enclosure that holds the arm. Next up are the electronics and the code. Let's get started!
Step 1: Bill of Materials
We will start this project by going through the materials. The parts list is not very large, but it will require access to some advanced tools.
- Plexiglass / Acrylic
- M3 nuts & bolts
- M3 threaded rod
- Motor coupling
- Switch housing
- Small magnet
- SPI Ledstrip
- Gyroscope: LSM9DS1
- DC motor
- ATX powersupply
- Carbon Brushes
- Power switch
- Fuse + holder
- FTDI breakout board
PCB (all SMD components)
- ATMEGA328P microcontroller
- 5 V linear regulator
- 3.3 V linear regulator
- 5 V buck converter
- Push button
Step 2: Designing the Rotating Arm
The rotating arm is the part on which the LED strip will be mounted. This is the part that actually turns, so it should be light, yet sturdy. It is made out of clear plexiglass, such that it is not too noticeable.
For the size we wanted a length of 1 meter, which we made thicker towards the center to ensure good solidity. We designed all parts in Autodesk Inventor, which allowed us to do dynamical analysis. Our design proved to have very little deflection when excited.
We also calculated the moments of inertia, which we will use in the next step of the design. Ofcourse, you can simply download the included file if you don't want to do the simulations yourself.
Later, we will also install the PCB on here, with all the components to drive the LED strip. But for now, we can already mount the motor coupling.
Step 3: Choosing a Motor
The next step in this project is choosing the right components. The most important one is the motor, since this will need to turn the LED strip fast enough.
We could have tried a bunch of motors to find a suitable one, but that's not a good way of going about things. We will find a suitable motor based on calculations, which is much more fun!
The first step is to determine the rotation speed needed to achieve the persistence of vision. This can already be achieved at around 4 rotations per second, or 320 rpm. This is the first requirement for our motor.
The next requirement is that it should have enough torque. The torque is important, since it will influence the speed at which the rotation arm accelerates; it should come to full speed in a reasonable amount of time.
T = I a
* T is the torque of the motor
* I is the moment of inertia of the arm
* a is the angular acceleration
To get the acceleration time, we can integrate this formula
int(T dt) = int(I a dt)
T t = I v (where v = angular velocity of the motor = 320 rpm)
t = I v / T
For a torque of about 4500 gcm (0.44 Nm), we get an acceptable time of about 14 seconds.
Conclusion: our motor needs at least 320 rpm and 4500 gcm. We found one from Como drills (940D271) that meets these requirements.
Step 4: Making the Case
Since a large spinning piece of plexiglass needs to be mounted on a solid base, the design of the case was quite important. It is basically a box which holds the motor and the powersupply. The powersupply is incorporated in the project to add enough weight to the base. We used an old ATX PSU, as it can provide enough power, and is quite heavy.
Another challenge was getting power to the rotor. We didn't want to use batteries here (the LED strip can draw about 3 Amps), so we decided to use carbon brushes instead. This is the same principle that is applied in brushed DC motors. The brushes mate with concentric copper rings on the PCB.
After designing the case - of which all files are included - we lasercut the pieces and assembled them. The assembly is very easy, since it is designed to fit together like a puzzle.
When the motor and brushes are installed in their corresponding cutouts, we can do the wiring. Simply run 12 V and GND to the motor (via a fuse), and another 12 V and GND line to the brushes.
We will need two more wires from the powersupply: the green wire and another ground wire (all others can be cut short). Connect these together with a switch in between, the powersupply will turn on when they are connected.
Step 5: Making the PCB
Now we move on to the PCB; it consists of 3 main parts:
- The slip rings
The middle of the PCB consists of 2 concentric rings which line up with the carbon brushes in the case. They are simply 2 tracks of exposed copper.
- Buck converter
The led strip runs at 5 V. However, we choose to transfer the power to the rotor at 12 V instead, and step it down to 5 V on the PCB. This is done to cope with voltage dips when the contact between the PCB and the brushes is not optimal. Moreover, at a higher voltage, we need less current, which is also beneficial.
Thus, we added a buck converter to step down the 12 V to 5 V quite efficiently. We wanted a 1000 uF capacitor, but since this was too tall, we split it in 10 100 uF caps instead. The limited height drove many of the PCB design choices.
The brain of the display is located on the last part of the PCB. First, we have 2 voltage regulators for 5 and 3.3 V. Since we don't need much power here, they can be of the linear type.
The microcontroller is an ATMEGA328P, the same chip as on many Arduino boards. It is connected to the LSM9DS1 accelerometer/gyroscope/magnetometer (via header). Despite the fact that the ATMEGA328P runs on 5 V, while the LSM9DS1 runs on 3.3 V, we don't need level shifters. This is explained in Adafruit's hookup guide for their breakout board (https://learn.sparkfun.com/tutorials/lsm9ds1-break... :
No level shifters even though the Arduino’s I/O pins are 5V? Well, I2C is a funny interface: pins aren’t directly pulled high by a GPIO, instead an open-drain MOSFET relies on pull-up resistors to create a logic high. Because the breakout board pull-up resistors are stronger (less resistance) than the Arduino’s internal pull-ups, the voltage on the logic pins will be much closer to 3.3V (though a little higher) than 5V – within a tolerable range. Just make sure you power the breakout off of the Arduino’s 3.3V power rail!
The ATMEGA328P can be programmed by an FTDI breakout board, which is connected through a header.
With the electronics done, we can move on to the programming part!
Step 6: The Code
For the code, we could have used the Arduino environment together with some libraries. This requires quite some overhead, and thus space on the microcontroller. Since we want to display images which can take quite some room, we wanted to avoid this overhead and go as barebones as possible. Therefore, we wrote our own libraries and used pure C code in Atmel Studio.
To make things clear, a flow diagram of the code is added. While in theory most of the concepts are relatively easy, in practice it's sometimes quite different. Sensors are not perfect: they are susceptible to noise, vibrations,... and take a finite time to measure. Execution time of code is also finite, while everything has to go as smooth as possible in real time. These are some of the things which we have to take into account while writing the code.
After initialising all the sensors, communication protocols and timers, we first wait until our motor has reached a steady state velocity. This is simply done by measuring each T2 seconds a velocity and comparing it with a previous measurement. Now that the velocity is relatively stable, we can synchronise the refresh rate of the LED strip to this velocity. Because a clear measurement of the velocity (degrees/s) is therefore needed, a running average over a few measurements is taken to suppress the noise influence. Now we can easily calculate the time T1 (s) that the LED strip needs to travel a certain angle (degrees).
From now on, every T1 seconds, an interrupt will be carried out that will send new data corresponding with this change in angle to the LED strip. Note that for more precision, the value of T1 is recalculated every T2 seconds. The last task remaining is to make sure the orientation of the image to be displayed is correct. This is done by constantly measuring the magnetic field strength and comparing it with a threshold that will only be exceeded when the strip passes the magnet. When this happens, we know we have rotated 360 degrees and can start redrawing the image. This threshold is an important parameter which can trade of certain performances. When too low, it will detect the magnet often too much which results in a constant (slight) shaking of the image. When too high, it will miss the magnet sometimes and the image will slightly drift away and then abruptly correct for this.
In the next sections we will explain all the important aspects of the code in more detail, but here's an overview:
- Description of the SPI protocol for the LED strip
- Description of the I2C protocol for the LSM9DS1
- Using the LSM9DS1
- Converting images
Step 7: The Code - SPI
The first part of the code we will take a look at, is how to control the LED strip. In this project, we used the APA 102C RGB full color strip. Simply sending some binary data to this device allows us to choose which LED's will be turned on and which color they will display. To know what information the strip expects us to send, we will - you guessed it - have to take a look at the data sheet. This tells us that the following data structure will have to be send:
1. A start frame of 32 bits of zeros
2. For each LED in the strip: a LED frame of 32 bits
This consists of one byte to control the brightness and one byte for each RGB color (in the order BGR !)
3. An end frame of (at least) 32 bits of ones
Note that this number of ones is only true for a strip of 64 LED's and should be equal to one half of the number of LED's your strip consists of.
Now that we know what data to send, the question of course remains how to send it. Enter the Serial Peripheral Interface, SPI for short. SPI is a very handy synchronous communication protocol that will allow our microcontroller to speak to the strip. Since it works synchronous, an extra data line (the clock) will be used. This is, as the name suggests, simply a square wave and will tell the receiver when to sample the data line. Looking at the datasheet of the strip once again, we can see that the sampling will be done on the rising edge of the clock signal. When using the SPI protocol, we usually talk about a master (the microcontroller) and slave (the strip) device. The master will generate the clock signal and send data to the slave via a data line called MOSI (Master Out Slave In). The slave can talk back via the MISO data line (Master In Slave Out). When multiple slaves are connected, only one can send/receieve data at a time. This is dictated by a last line, SS (Slave Select).
In our case it is not so complicated however, since we will only use the clock and MOSI lines. With the clock and data lines of the microcontroller connected to the strip (PB5 to CKI and PB3 to SDI) the only thing remaining to do is to configure the SPI on the controller and hopefully see some colors! Setting up and using the SPI is luckily relatively easy. First of all, two status registers (SPCR and SPSR) have to be initialised. Now we can simply put our data we want to send byte by byte in the data register (SPDR) and the microcontroller will deliver it nice and easy to the strip!
What if your microcontroller doesn't have the hardware to support SPI, you need another separate SPI bus or simply want to take matters into your own hands? Don't worry, since SPI can easily be bit banged (= implemented in software). The only thing you will have to do is to generate the correct waveforms at the correct moments yourself. The most important drawback however is the slower speed and thus was not used during this project. The code can still be found in our SPI library if you want to take a look at it though!
Step 8: The Code - I2C
Another protocol, why not use only SPI? Since we want the highest update speed possible for the led strip, it's best to use another protocol for communication with the accelerometer / gyroscope. This other protocol is I2C, and luckily for us, it is supported by the chip.
We also decided to handle the I2C library ourselves. So let's go through a quick overview of it's operation first.
I2C, or Two Wire Interface, makes use of only - you guessed it - 2 wires: SDA (data) and SCL (clock). In contrary to SPI, the slave devices don't need a chip select signal, but are identified with an address. Since these addresses are 7 bits, it is possible to use 128 (!) devices with only these 2 lines.
The code starts by initializing the I2C by setting the frequency to 100 kHz. We can now send and receive bytes.
- We start by sending a start sequence to let the slaves know they should listen.
- We send the address of the slave we want to write. The address consists of the chip address (7 bits), and 1 bit indicating it's a write (0).
- We send the address of the register we want to write to.
- We send the data.
- Send a stop sequence to indicate the end of the communication.
Reading data: this starts by writing the register(s) we want to read
- We start by sending a start sequence to let the slaves know they should listen.
- We send the address of the slave we want to read. The address consists of the chip address (7 bits), and 1 bit indicating it's a write (0).
- We send the address of the register we want to read.
- We resend a start, a so called repeated start.
- We send the address of the slave we want to read. The address consists of the chip address (7 bits), and 1 bit indicating it's a read (1).
- The chip now sends it's data.
- If we want more data, we reply with an acknowledgement (the chip will then send it's next register), otherwise we reply with NOT ACK.
- Send a stop sequence to indicate the end of the communication
Step 9: The Code - the Sensor
The sensor we used is the LSM9DS1. It has a built-in acceleromter, gyroscope and magnetometer, of which we will use the gyrsocope and magnetometer. The gyroscope will calculate the rotation speed, and the magnetometer will detect the point our new image should start. Sparkfun has an awesome library for this chip, but hey, we don't use off-shelf-solutions, do we?
We'll check the datasheet to find all the register settings that we need to configure the chip. It is very important to note that the gyroscope/accelerometer and the magnetometer are actually different chips, which are put together in a package. This means that they have another I2C address: 0x6B for the gyro, 0x1E for the magnetometer.
The first thing we need to use the gyroscope, is configuring its control registers. We find them from page 45.
The first register is CTRL_REG_1 (0x10): we want the highest data rate, and a resolution of 2000 dps (degrees/second); this corresponds to 5 rotations/second, ideal for our display!
The other control registers can be left at their default values.
To check if the gyroscope has finished its data acquisition, we can read the status register (0x27). Here, we can check the GDA (Gyroscope Data Available) bit, which is 1 when there is new unread data.
For reading the data, we need to consult the OUT registers (OUT_X_G, OUT_Y_G and OUT_Z_G). Since our chip is mounted such that the rotation is in the Z direction. Therefore, we can read out only register OUT_Z_G (0x1C - 0x1D). The value of this register needs to be converted to actual dps units, by multiplying it with the right value, given in table 3 of the datasheet.
For the magnetometer we do something similar. We start with CTRL_REG1_M (0x20) on page 63. We enable the x and y axis, and choose the highest data rate.
The next one is CTRL_REG3_M (0x22), where we choose continuous conversion mode.
The other ones can again be left on the default settings.
The magnetometer data available can be checked the same way, by reading the status register (0x27).
The data can be read from OUT_X_M, OUT_Y_M and OUT_Z_M (0x28, 0x2A and 0X2C respectively). We will read all of them at once by accessing the x register and doing a continuous read. The values should again be converted with table 3, with the use of the 4 gauss scale (standard setting).
We can now poll the sensor to check if it has new data available and read it.
Step 10: The Code - Using Images
Now that we are able to control the LED strip, it is time to take it one step further. Remember that the goal is to be able to display whatever image only by rotating the strip. Therefore it is a natural step to convert the image to a polar representation. For this, an external script was written in Python, since its Image library is very handy to work with. Unfortunately, this library is only supported in Python 2.5 or lower. Any other language can of course also be used to accomplish the same result.
Before we start to convert our image to a more suitable representation, it is important to think about the (limited) memory of our controller for a moment. Every pixel of our image has an 8 bit value for its red, green and blue color value. For 60 pixels/LED's we would already need to store 180 bytes in memory (3 bytes/LED). If we do this for every 3 degrees, this would result in 22 kB (180 * 120), which is already around 70% of the total memory of the microcontroller (32 kB)! To be able to store more pixels, the color resolution will have to decrease. In other words, we will store the 3 RGB values in only 1 byte now, and extract the different values by using some masking techniques.
The conversion to a polar representation is relatively straightforward. Given a value of the radial distance (r) and angle (theta), the conversion to cartesian coordinates (x', y') can easily be made. Since the origin is located in the middle, but the coordinate system of an image has its origin in the top left corner, another change of coordinates (x, y) has to be made. Note that these values are most of the time not integers and thus do not directly correspond to a pixel. A lot of different interpolation methods can be used, but the most simple one (nearest neighbour) was opted for.
Now the color of a pixel is given for a certain r and theta, the next step consists in storing this value in a matrix where the position will correspond to the LED that will have to display this value. When theta varies from 0 to 180 degrees, the values of the right side of the strip are found. From 180 to 360 degrees, the left side of the strip is found. After rotating 180 degrees, the strip is found in the same position, but the LED's are now located the other way around. In other words, the same information can be send to the strip, but in another order. With 60 LED's and a resolution of 3 degrees, a 60x60 image can be displayed and stored in an 60 by 180/3 matrix.
But we can do better than this, can't we? By using a simple trick, an 120x120 image can be displayed with only 60 LED's. The only thing that has to be done is to introduce an offset on our strip. This can be seen as if the LED's on the left of the strip correspond to the even pixels, while the ones on the right fill in the odd pixels. When the strip is now rotated 180 degrees, the strip is found in the same position, but the LED's themselves lie on a spot that was previously empty. Since after 180 degrees other information has to be send, the matrix is now 60 by 360/3.
Step 11: Succes!
After putting everything together, doing a lot of testing and debugging, we can proudly say that the project was a successful one. As can be seen from the pictures above, the 120x120 logo is accurately represented by our 60 LED's! While persistance of vision may look like something strange and difficult to grasp, we can clearly see that it works. But this project was more than a simple proof of concept, since it also learned us (and hopefully you) a lot about microcontrollers, sensors, communication protocols and the practical side of things in general.
We hope you liked the project, since we sure had a lot of fun - and some headaches - while making it! :)
If you did, please consider to vote for us in the lights contest, we would highly appreciate it! https://www.instructables.com/contest/lights2017/...
Participated in the
Lights Contest 2017