Evan Rust
Published © GPL3+

Creating Images Using One LED

Use the concepts in light painting to create a machine that can recreate bitmaps with a long-exposure camera using a single LED.

AdvancedFull instructions provided4 hours16,577
Creating Images Using One LED

Things used in this project

Hardware components

NEMA17 Stepper Motor
×2
Driver DRV8825 for Stepper Motors for Theremino System
Driver DRV8825 for Stepper Motors for Theremino System
×2
SD card reader
×1
Arduino Mega 2560
Arduino Mega 2560
×1
Various timing belts and v-wheels
×1

Software apps and online services

Arduino IDE
Arduino IDE
GIMP
Fusion 360
Autodesk Fusion 360

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)

Story

Read more

Custom parts and enclosures

X Axis End Stop

X Axis End Stop- Stepper End

X Axis Link

X Axis Glider (Print 2)

Y Axis Link

Y Axis Glider (Print 2)

Schematics

Stepper Wiring

Code

Light Painting Program

C/C++
//Bitmap reading function partially from Adafruit

#include <SD.h>
#include <SPI.h>
#include "DRV8825.h"

#define MOTOR_STEPS 200
#define RPM 150
#define MICROSTEPS 4

//pin definitions
#define STEPPER_X_DIR 7
#define STEPPER_X_STEP 6
#define STEPPER_X_EN 8
#define STEPPER_Y_DIR 4
#define STEPPER_Y_STEP 5
#define STEPPER_Y_EN 12

#define X 0
#define Y 1

#define X_DIR_FLAG -1 //1 or -1 to flip direction
#define Y_DIR_FLAG 1 //1 or -1 to flip direction

#define STEPS_PER_MM (3.75 * MICROSTEPS) //steps needed to move 1mm
#define SPACE_BETWEEN_POSITIONS 5 //5mm per move

#define R A0
#define G A1
#define B A2

#define SD_CS 22

int currentPositions[] = {0, 0};

DRV8825 stepperX(MOTOR_STEPS, STEPPER_X_DIR, STEPPER_X_STEP, STEPPER_X_EN);
DRV8825 stepperY(MOTOR_STEPS, STEPPER_Y_DIR, STEPPER_Y_STEP, STEPPER_Y_EN);

void setup() {
  Serial.begin(115200);
  init_steppers();
  SD.begin(SD_CS);
  createBitmaps();
  stepperX.disable();
  stepperY.disable();
  while(1);
}

void loop() {
}

void createBitmaps(){
  File dir = SD.open("bitmaps");
  while(true){
    File bitmap = dir.openNextFile();
    if(!bitmap){
      break;
    }
    paintBitmap(bitmap);
    delay(15000);
  }
  
}

#define BUFFPIXEL 20
void paintBitmap(File bmpFile){
  int bmpWidth, bmpHeight;
  uint8_t bmpDepth;
  uint32_t bmpImageOffset;
  uint32_t rowSize;               // Not always = bmpWidth; may have padding
  uint8_t  sdbuffer[3 * BUFFPIXEL]; // pixel buffer (R+G+B per pixel)
  uint8_t  buffidx = sizeof(sdbuffer); // Current position in sdbuffer
  boolean  goodBmp = false;       // Set to true on valid header parse
  boolean  flip = true;        // BMP is stored bottom-to-top
  int      w, h, row, col;
  uint8_t  r, g, b;
  uint32_t pos = 0, startTime = millis();

  Serial.println();
  Serial.print("Loading image '");
  Serial.print(bmpFile.name());
  Serial.println('\'');

  // Open requested file on SD card

  // Parse BMP header
  if (read16(bmpFile) == 0x4D42) { // BMP signature
    Serial.print("File size: "); Serial.println(read32(bmpFile));
    (void)read32(bmpFile); // Read & ignore creator bytes
    bmpImageOffset = read32(bmpFile); // Start of image data
    Serial.print("Image Offset: "); Serial.println(bmpImageOffset, DEC);
    // Read DIB header
    Serial.print("Header size: "); Serial.println(read32(bmpFile));
    bmpWidth = read32(bmpFile);
    bmpHeight = read32(bmpFile);
    if (read16(bmpFile) == 1) { // # planes -- must be '1'
      bmpDepth = read16(bmpFile); // bits per pixel
      Serial.print("Bit Depth: "); Serial.println(bmpDepth);
      if ((bmpDepth == 24) && (read32(bmpFile) == 0)) { // 0 = uncompressed

        goodBmp = true; // Supported BMP format -- proceed!
        Serial.print("Image size: ");
        Serial.print(bmpWidth);
        Serial.print('x');
        Serial.println(bmpHeight);

        // BMP rows are padded (if needed) to 4-byte boundary
        rowSize = (bmpWidth * 3 + 3) & ~3;

        // If bmpHeight is negative, image is in top-down order.
        // This is not canon but has been observed in the wild.
        if (bmpHeight < 0) {
          bmpHeight = -bmpHeight;
          flip = false;
        }

        // Crop area to be loaded
        w = bmpWidth;
        h = bmpHeight;

        if(bmpWidth*bmpHeight>290){ //Too large
          Serial.println("File is too large to be printed.");
          return;
        }
        
        for(uint8_t i=0; i<5;i++){
          analogWrite(R, 150);
          delay(500);
          analogWrite(R, 0);
          delay(500);
        }
        for (row = 0; row<h; row++) { // For each scanline...
          moveToPosition(0, row);
          // Seek to start of scan line.  It might seem labor-
          // intensive to be doing this on every line, but this
          // method covers a lot of gritty details like cropping
          // and scanline padding.  Also, the seek only takes
          // place if the file position actually needs to change
          // (avoids a lot of cluster math in SD library).
          if (flip) // Bitmap is stored bottom-to-top order (normal BMP)
            pos = bmpImageOffset + (bmpHeight - 1 - row) * rowSize;
          else     // Bitmap is stored top-to-bottom
            pos = bmpImageOffset + row * rowSize;
          if (bmpFile.position() != pos) { // Need seek?
            bmpFile.seek(pos);
            buffidx = sizeof(sdbuffer); // Force buffer reload
          }

          // optimize by setting pins now
          for (col = 0; col<w; col++) { // For each pixel...
                          // Time to read more pixel data?
            if (buffidx >= sizeof(sdbuffer)) { // Indeed
              bmpFile.read(sdbuffer, sizeof(sdbuffer));
              buffidx = 0; // Set index to beginning
            }

            // Convert pixel from BMP to TFT format, push to display
            b = sdbuffer[buffidx++];
            g = sdbuffer[buffidx++];
            r = sdbuffer[buffidx++];

            moveToPosition(col, row);
            activateLED(r,g,b);
            // optimized!
            //tft.pushColor(tft.Color565(r,g,b));
          } // end pixel
          analogWrite(R, 0);
          analogWrite(G, 0);
          analogWrite(B, 0);
        } // end scanline
        Serial.print("Loaded in ");
        Serial.print(millis() - startTime);
        Serial.println(" ms");
      } // end goodBmp
    }
  }

  bmpFile.close();
  moveToPosition(0,0);
  if (!goodBmp) Serial.println("BMP format not recognized.");
}

uint16_t read16(File f) {
  uint16_t result;
  ((uint8_t *)&result)[0] = f.read(); // LSB
  ((uint8_t *)&result)[1] = f.read(); // MSB
  return result;
}

uint32_t read32(File f) {
  uint32_t result;
  ((uint8_t *)&result)[0] = f.read(); // LSB
  ((uint8_t *)&result)[1] = f.read();
  ((uint8_t *)&result)[2] = f.read();
  ((uint8_t *)&result)[3] = f.read(); // MSB
  return result;
}

void activateLED(int r, int g, int b){
  Serial.print(F("LED has value of: "));
  Serial.print(r);
  Serial.print(", ");
  Serial.print(g);
  Serial.print(", ");
  Serial.println(b);
  analogWrite(R, r);
  analogWrite(G, g);
  analogWrite(B, b);
}

void moveToPosition(int x, int y){
  int newPosX = (x-currentPositions[X])*STEPS_PER_MM*X_DIR_FLAG*SPACE_BETWEEN_POSITIONS;
  int newPosY = (y-currentPositions[Y])*STEPS_PER_MM*Y_DIR_FLAG*SPACE_BETWEEN_POSITIONS;
  stepperX.move(newPosX);
  stepperY.move(newPosY);
  currentPositions[X] = x;
  currentPositions[Y] = y;
  Serial.print("Stepper positions: "); Serial.print(currentPositions[X]); Serial.print(", "); Serial.println(currentPositions[Y]);
}

void init_steppers(){
  stepperX.begin(RPM);
  stepperX.setEnableActiveState(LOW);
  stepperX.enable();
  stepperX.setMicrostep(MICROSTEPS);
  stepperY.begin(RPM);
  stepperY.setEnableActiveState(LOW);
  stepperY.enable();
  stepperY.setMicrostep(MICROSTEPS);
}

Credits

Evan Rust

Evan Rust

120 projects • 1052 followers
IoT, web, and embedded systems enthusiast. Contact me for product reviews or custom project requests.

Comments