Final Project¶
Project Overview¶
Idea¶
My project is inspired by the adafruit Animated LED Sand project. There is a full documentation on how to build this project on the adafruit website.
Here is a preview of what i aim to make.
Potential Constraints¶
Unlike the Animated LED Sand project I do not have access to the following parts:-
- Adafruit LIS3DH Triple-Axis Accelerometer
- Adafruit 15x7 CharliePlex LED Matrix
Therefore I need to find replacements for those parts and redesgin my project around them. This in return forces me to create a new circuit, design and code to adapt to the new parts. I also aim to make improvements to the original idea through the redesign process.
Bill of Materials¶
- Adafruit Feather nrf52840 Sense
- Wires
- MAX7219
- 8 x 8 LED Matrix Display
- Viynl Paper
- PLA Filament
-
Spray Paint
-
(Access to a 3D Printer and Viynl Cutter)
3D Printing & Design¶
Design¶
I first planned out a rough idea of how my design will look like.
I got the idea to use snap fits to join my parts together from this video.
I followed the tutorial to create a container made of three parts that uses snap fits.
This is how the snaps look like form the inside. The tutorial provided me with a good base for my design, all I need to do is adjust the top and bottom parts to better match my project goals.
I modified the user parameters using the measurements I’ve gathered in the sketch.
Since I will be using an led matrix, I created a square hole on the top part of the container.
I created a round bottom to allow the container to move around. These movements will also move the LED sand by using the accelometer in the adafruit.
I added a hole to charge the electronics and i positioned it at the bottom of the container.
This is how the parts look put together. The first image below is an inside view of how it looks like and the second is the outside view.
Printing¶
Ultimate Cura is a 3D printing software. I imported the parts I designed on the software then I used the following printing settings.
Ultimate Cura has a preview feature that shows how much time is needed to print a 3D model. Based on the settings I entered it takes roughly eight and a half hours to fully print the parts I need.
After the printing process is done I sanded the parts to give them a smooth surface.
I used silver spray paint to decorate the container.
Electronics¶
Sensor Testing¶
The adafruit feather sense has a builtin Accelerometer and Gyro sensor that can replace the Adafruit LIS3DH Triple-Axis. The built in sensor uses the Adafruit LSM6DS Librabry.
As a first test I wanted to see if the sensor actually works, I used the example project provided by the library to test this.
Based on the serial monitor the sensors seem to work.
Adapting Circuit to Input & Output Devices¶
At first I tried to use the sensor to move a square on an OLED display.
I later realized that an OLED display will need more programming work to achieve the same results, as the functions I need are not supported by the OLED library.
I instead decided to use an 8x8 LED dot matrix that was availble in storage. I used this guide to setup and test the dot matrix circuit.
The following circuit uses a multiplexer to control the 16 available pins of the dot matrix using only 3 pins of the adafruit.
Circuit Connections¶
I transfered the circuit to a veroboard. Here’s a short video of this process.
Prototype Code¶
I ran out of time so i could only implement a simple verion of the sand simulation.
#include <Adafruit_LSM6DS33.h>
#include <LedControl.h>
LedControl ledMatrix = LedControl(12,11,10,1);
// For SPI mode, we need a CS pin
#define LSM_CS 10
// For software-SPI mode we need SCK/MOSI/MISO pins
#define LSM_SCK 13
#define LSM_MISO 12
#define LSM_MOSI 11
int nx,ny =0;
int x,y ;
int ax,ay ;
Adafruit_LSM6DS33 lsm6ds33;
void setup(void) {
Serial.begin(115200);
Serial.println("Adafruit LSM6DS33 test!");
if (!lsm6ds33.begin_I2C()) {
// if (!lsm6ds33.begin_SPI(LSM_CS)) {
// if (!lsm6ds33.begin_SPI(LSM_CS, LSM_SCK, LSM_MISO, LSM_MOSI)) {
Serial.println("Failed to find LSM6DS33 chip");
while (1) {
delay(10);
}
}
Serial.println("LSM6DS33 Found!");
lsm6ds33.configInt1(false, false, true); // accelerometer DRDY on INT1
lsm6ds33.configInt2(false, true, false); // gyro DRDY on INT2
ledMatrix.shutdown(0,false); // Wake up! 0= index of first device;
ledMatrix.setIntensity(0,2);
delay(500);
}
void loop() {
// /* Get a new normalized sensor event */
sensors_event_t accel;
sensors_event_t gyro;
sensors_event_t temp;
lsm6ds33.getEvent(&accel, &gyro, &temp);
ledMatrix.clearDisplay(0);
ax = (int)accel.acceleration.x;
ay = (int)accel.acceleration.y;
Serial.print(x);
delay(500);
if(ax > accel.acceleration.x ){
if (x>=0 && x<8){ x++;}
}else if (ax < accel.acceleration.x ){
if (x<=8 && x>0){ x--;}
}
if(ay > accel.acceleration.y ){
if (y>=0 && y<8){ y++;}
}else if (ay < accel.acceleration.y ){
if (y<=8 && y>0){ y--;}
}
ledMatrix.setLed(0,x,y,true);
ledMatrix.setLed(0,x+1,y,true);
ledMatrix.setLed(0,x+2,y,true);
ledMatrix.setLed(0,x,y+1,true);
ledMatrix.setLed(0,x,y+2,true);
ledMatrix.setLed(0,x-2,y,true);
ledMatrix.setLed(0,x-3,y,true);
ledMatrix.setLed(0,x+3,y+1,true);
ledMatrix.setLed(0,x,y,true);
ledMatrix.setLed(0,x+3,y,true);
ledMatrix.setLed(0,x+5,y,true);
ledMatrix.setLed(0,x,y+5,true);
ledMatrix.setLed(0,x+3,y+1,true);
delay(1000);
}
It’s not great but it can be improved later on.
Prototype Issues¶
Electronic Issues¶
Issue | Solution | |
---|---|---|
1 | Needs to be connected to the laptop to work | Add battery module to the adafruit |
2 | Electronics move around in the container | Support electronics with bolts and mechanical supports |
3 | Code is slow and unresponsive | Rewrite code |
Design Issues¶
Issue | Solution | |
---|---|---|
1 | Container height is too big | reduce the height of the design to fit |
2 | USB hole prevents the conatiner from being closed | Move the USB hole up |
3 | Bottom part of the container is too flat and does not move correctly | Round the bottom part in the desgin |
Improvements¶
Design¶
To improve the design I first created a geometric representation of the electronics I am using. I tried my best to match the dimentions of the electronics. This helps me better plan out how they will look inside the container.
I made some adjustments to the initial design to improve the fuctionality and stability of the container.
I additionally reduced the height of the conatiner by 2 cm to make it more compact. This is how it looks like from the outside.
I only needed to 3D print the parts that I modified, I made sure that the new parts fit in what I have made before.
You can download the fusion360 design file from here.
Electronics¶
Secure Electronics¶
I used 2.5 mm screws to secure the electronics in place
New Improved Code¶
To improve the code I first looked solutions on the interent. I found a similar project using different libraries than what I was using. I adapted the code to my project by reading the two libraries (ledConrol.h & Charliplexing.h) and replacing the code availble with what works for my project.
Electronics Test¶
This code is used to test that the accelometer and led matrix are working correctly.
#include <SPI.h>
#include <LedControl.h>
#include <Adafruit_LSM6DS33.h>
#include <Wire.h>
LedControl lc88=LedControl(12,11,10,1);
// For SPI mode, we need a CS pin
#define LSM_CS 10
// For software-SPI mode we need SCK/MOSI/MISO pins
#define LSM_SCK 13
#define LSM_MISO 12
#define LSM_MOSI 11
Adafruit_LSM6DS33 lsm6ds33;
void setup(void) {
Serial.begin(115200);
while (!Serial)
delay(10); // will pause Zero, Leonardo, etc until serial console opens
Serial.println("Adafruit LSM6DS33 test!");
if (!lsm6ds33.begin_I2C()) {
// if (!lsm6ds33.begin_SPI(LSM_CS)) {
// if (!lsm6ds33.begin_SPI(LSM_CS, LSM_SCK, LSM_MISO, LSM_MOSI)) {
Serial.println("Failed to find LSM6DS33 chip");
while (1) {
delay(10);
}
}
Serial.println("LSM6DS33 Found!");
lsm6ds33.setAccelRange(LSM6DS_ACCEL_RANGE_4_G);
lsm6ds33.setGyroRange(LSM6DS_GYRO_RANGE_2000_DPS);
lsm6ds33.configInt1(false, false, true); // accelerometer DRDY on INT1
lsm6ds33.configInt2(false, true, false); // gyro DRDY on INT2
lc88.shutdown(0,false); // Wake up! 0= index of first device;
lc88.setIntensity(0,2);
lc88.clearDisplay(0);
delay(500);
}
void loop() {
// /* Get a new normalized sensor event */
sensors_event_t accel;
sensors_event_t gyro;
sensors_event_t temp;
lsm6ds33.getEvent(&accel, &gyro, &temp);
/* Display the results (acceleration is measured in m/s^2) */
Serial.print("\t\tAccel X: ");
Serial.print(accel.acceleration.x);
Serial.print(" \tY: ");
Serial.print(accel.acceleration.y);
Serial.print(" \tZ: ");
Serial.print(accel.acceleration.z);
Serial.println(" m/s^2 ");
delay(100);
for(int row=0; row<=7; row++){
lc88.setLed(0,row,0,true);
delay(250);
}
for(int col=0; col<=7; col++){
lc88.setLed(0,0,col,true);
delay(250);
}
delay(500);
lc88.clearDisplay(0);
delay(2000);
}
LED Sand Code¶
#include <SPI.h>
#include <LedControl.h>
#include <Adafruit_LSM6DS33.h>
#include <Wire.h>
LedControl lc88=LedControl(12,11,10,1);
// For SPI mode, we need a CS pin
#define LSM_CS 10
// For software-SPI mode we need SCK/MOSI/MISO pins
#define LSM_SCK 13
#define LSM_MISO 12
#define LSM_MOSI 11
#define N_GRAINS 16 // Number of grains of sand
#define WIDTH 8 // Display width in pixels
#define HEIGHT 8 // Display height in pixels
#define MAX_FPS 100 // Maximum redraw rate, frames/second
// The 'sand' grains exist in an integer coordinate space that's 256X
// the scale of the pixel grid, allowing them to move and interact at
// less than whole-pixel increments.
#define MAX_X (WIDTH * 256 - 1) // Maximum X coordinate in grain space
#define MAX_Y (HEIGHT * 256 - 1) // Maximum Y coordinate
struct Grain {
int16_t x, y; // Position
int16_t vx, vy; // Velocity
} grain[N_GRAINS];
uint32_t prevTime = 0; // Used for frames-per-second throttle
uint8_t img[WIDTH * HEIGHT]; // Internal 'map' of pixels
Adafruit_LSM6DS33 lsm6ds33;
void setup(void) {
Serial.begin(115200);
Serial.println("Adafruit LSM6DS33 test!");
if (!lsm6ds33.begin_I2C()) {
// if (!lsm6ds33.begin_SPI(LSM_CS)) {
// if (!lsm6ds33.begin_SPI(LSM_CS, LSM_SCK, LSM_MISO, LSM_MOSI)) {
Serial.println("Failed to find LSM6DS33 chip");
while (1) {
delay(10);
}
}
Serial.println("LSM6DS33 Found!");
uint8_t i, j, bytes;
lsm6ds33.setAccelRange(LSM6DS_ACCEL_RANGE_4_G);
lsm6ds33.setGyroRange(LSM6DS_GYRO_RANGE_2000_DPS);
lsm6ds33.configInt1(false, false, true); // accelerometer DRDY on INT1
lsm6ds33.configInt2(false, true, false); // gyro DRDY on INT2
memset(img, 0, sizeof(img)); // Clear the img[] array
for(i=0; i<N_GRAINS; i++) { // For each sand grain...
do {
grain[i].x = random(WIDTH * 256); // Assign random position within
grain[i].y = random(HEIGHT * 256); // the 'grain' coordinate space
// Check if corresponding pixel position is already occupied...
for(j=0; (j<i) && (((grain[i].x / 256) != (grain[j].x / 256)) ||
((grain[i].y / 256) != (grain[j].y / 256))); j++);
} while(j < i); // Keep retrying until a clear spot is found
img[(grain[i].y / 256) * WIDTH + (grain[i].x / 256)] = 1; // Mark it
grain[i].vx = grain[i].vy = 0; // Initial velocity is zero
}
//Serial.print("Setup done...grains:\n\r");
//for(i=0; i<N_GRAINS; i++) {
// Serial.print(grain[i].x);
// Serial.print(" ");
// Serial.print(grain[i].y);
// Serial.print("\n\r");
// }
lc88.shutdown(0,false); // Wake up! 0= index of first device;
lc88.setIntensity(0,2);
lc88.clearDisplay(0);
delay(500);
}
void loop() {
// /* Get a new normalized sensor event */
uint32_t t;
while(((t = micros()) - prevTime) < (1000000L / MAX_FPS));
prevTime = t;
sensors_event_t accel;
sensors_event_t gyro;
sensors_event_t temp;
lsm6ds33.getEvent(&accel, &gyro, &temp);
/* Display the results (acceleration is measured in m/s^2) */
int16_t ax = ((int)accel.acceleration.y ) / 8, // Transform accelerometer axes
ay = ((int)accel.acceleration.x ) / 8, // to grain coordinate space
az = abs((int)accel.acceleration.z ) / 64; // Random motion factor
az = (az >= 3) ? 1 : 4 - az; // Clip & invert
ax -= az; // Subtract motion factor from X, Y
ay -= az;
int16_t az2 = az * 2 + 1;
// Serial.print("Accel read: ");
// Serial.print(ax);
// Serial.print(" ");
// Serial.print(ay);
// Serial.print(" ");
// Serial.print(az);
// Serial.print("\n\r");
////
// Serial.print("Accel read: ");
// Serial.print((int)accel.acceleration.x/8 );
// Serial.print(" ");
// Serial.print((int)accel.acceleration.y/8);
// Serial.print(" ");
// Serial.print( abs((int)accel.acceleration.z ) / 64);
// Serial.print("\n\r");
// ...and apply 2D accel vector to grain velocities...
int32_t v2; // Velocity squared
float v; // Absolute velocity
for(int i=0; i<N_GRAINS; i++) {
grain[i].vx += ax + random(az2); // A little randomness makes
grain[i].vy += ay + random(az2); // tall stacks topple better!
// Terminal velocity (in any direction) is 256 units -- equal to
// 1 pixel -- which keeps moving grains from passing through each other
// and other such mayhem. Though it takes some extra math, velocity is
// clipped as a 2D vector (not separately-limited X & Y) so that
// diagonal movement isn't faster
v2 = (int32_t)grain[i].vx*grain[i].vx+(int32_t)grain[i].vy*grain[i].vy;
if(v2 > 65536) { // If v^2 > 65536, then v > 256
v = sqrt((float)v2); // Velocity vector magnitude
grain[i].vx = (int)(256.0*(float)grain[i].vx/v); // Maintain heading
grain[i].vy = (int)(256.0*(float)grain[i].vy/v); // Limit magnitude
}
}
//Serial.print("2d vector applied, updating position...\n\r");
// ...then update position of each grain, one at a time, checking for
// collisions and having them react. This really seems like it shouldn't
// work, as only one grain is considered at a time while the rest are
// regarded as stationary. Yet this naive algorithm, taking many not-
// technically-quite-correct steps, and repeated quickly enough,
// visually integrates into something that somewhat resembles physics.
// (I'd initially tried implementing this as a bunch of concurrent and
// "realistic" elastic collisions among circular grains, but the
// calculations and volument of code quickly got out of hand for both
// the tiny 8-bit AVR microcontroller and my tiny dinosaur brain.)
uint8_t i, bytes, oldidx, newidx, delta;
int16_t newx, newy;
for(i=0; i<N_GRAINS; i++) {
newx = grain[i].x + grain[i].vx; // New position in grain space
newy = grain[i].y + grain[i].vy;
if(newx > MAX_X) { // If grain would go out of bounds
newx = MAX_X; // keep it inside, and
grain[i].vx /= -2; // give a slight bounce off the wall
} else if(newx < 0) {
newx = 0;
grain[i].vx /= -2;
}
if(newy > MAX_Y) {
newy = MAX_Y;
grain[i].vy /= -2;
} else if(newy < 0) {
newy = 0;
grain[i].vy /= -2;
}
oldidx = (grain[i].y/256) * WIDTH + (grain[i].x/256); // Prior pixel #
newidx = (newy /256) * WIDTH + (newx /256); // New pixel #
if((oldidx != newidx) && // If grain is moving to a new pixel...
img[newidx]) { // but if that pixel is already occupied...
delta = abs(newidx - oldidx); // What direction when blocked?
if(delta == 1) { // 1 pixel left or right)
newx = grain[i].x; // Cancel X motion
grain[i].vx /= -2; // and bounce X velocity (Y is OK)
newidx = oldidx; // No pixel change
} else if(delta == WIDTH) { // 1 pixel up or down
newy = grain[i].y; // Cancel Y motion
grain[i].vy /= -2; // and bounce Y velocity (X is OK)
newidx = oldidx; // No pixel change
} else { // Diagonal intersection is more tricky...
// Try skidding along just one axis of motion if possible (start w/
// faster axis). Because we've already established that diagonal
// (both-axis) motion is occurring, moving on either axis alone WILL
// change the pixel index, no need to check that again.
if((abs(grain[i].vx) - abs(grain[i].vy)) >= 0) { // X axis is faster
newidx = (grain[i].y / 256) * WIDTH + (newx / 256);
if(!img[newidx]) { // That pixel's free! Take it! But...
newy = grain[i].y; // Cancel Y motion
grain[i].vy /= -2; // and bounce Y velocity
} else { // X pixel is taken, so try Y...
newidx = (newy / 256) * WIDTH + (grain[i].x / 256);
if(!img[newidx]) { // Pixel is free, take it, but first...
newx = grain[i].x; // Cancel X motion
grain[i].vx /= -2; // and bounce X velocity
} else { // Both spots are occupied
newx = grain[i].x; // Cancel X & Y motion
newy = grain[i].y;
grain[i].vx /= -2; // Bounce X & Y velocity
grain[i].vy /= -2;
newidx = oldidx; // Not moving
}
}
} else { // Y axis is faster, start there
newidx = (newy / 256) * WIDTH + (grain[i].x / 256);
if(!img[newidx]) { // Pixel's free! Take it! But...
newx = grain[i].x; // Cancel X motion
grain[i].vy /= -2; // and bounce X velocity
} else { // Y pixel is taken, so try X...
newidx = (grain[i].y / 256) * WIDTH + (newx / 256);
if(!img[newidx]) { // Pixel is free, take it, but first...
newy = grain[i].y; // Cancel Y motion
grain[i].vy /= -2; // and bounce Y velocity
} else { // Both spots are occupied
newx = grain[i].x; // Cancel X & Y motion
newy = grain[i].y;
grain[i].vx /= -2; // Bounce X & Y velocity
grain[i].vy /= -2;
newidx = oldidx; // Not moving
}
}
}
}
}
//Serial.print("Updating grain position...\n\r");
grain[i].x = newx; // Update grain position
grain[i].y = newy;
img[oldidx] = 0; // Clear old spot (might be same as new, that's OK)
img[newidx] = 1; // Set new spot
}
// Update pixel data in LED driver
//Serial.print("Now updating pixels...\n\r");
for(i=0; i<WIDTH*HEIGHT; i++) {
// bool ledSwitch = ;
lc88.setLed(0,i % WIDTH, i / WIDTH, img[i]);
}
}
Code Demo¶
Heres a demo of how the new code works. As you can see it is clearly faster and more responsive than the previous version.
Protability (Adding battery)¶
To improve the portability of the device, I connected a lithium Ion Battery. Now it can run without using a wire.
Decoration¶
I noticed that there were gaps on the bottom of the container due to it not being printed correctly by the 3D printer.
I used a glue gun to fill the gaps, this wasn’t the best solution because glue has a unsmooth surface that can’t be sanded off.
I choose to cover the glue up with vynil stickers. This is the design I made and it can be downloaded from here.
I first stuck the name sticker on the side of the container. I did this before spray painting so that I can get a nice silver name after removing the sticker.
I covered the electronics with tape to prevent the black paint form damaging them.
In order to spray all sides of my project I hanged it using a screw driver and supports.
The paint took roughly 30 minutes to dry, I came back and sprayed it again to give it a thicker layer of paint.
After ensuring the paint is dry, I removed the name sticker and tape.
I stuck the remaining stickers around the container until I was satisfied with how it looked.
Final Product¶
This how my LED sand project turned out at the end. I made a lot of mistakes along the way but I am very happy of the results !!