Skip to content

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 !!

Demonstration Video


Last update: May 7, 2023