In this part of our series on PCB art, we program the ATmega328 MCU embedded on the PCB using the ISP header.

The final step in creating the Maker Pro robot PCB badge is programming the onboard ATmega328p microcontroller to run the board. 

We've done a lot with our custom-made PCB badge to get to this point! If you haven't read the other steps necessary for creating PCB art, take a moment to check them out:

Now, onto programming the ATmega328. There are several ways to program embedded microcontrollers; for this tutorial, we will use an in-circuit programming (ISP) header included on the robot’s right arm near the microcontroller.

program the ATmega328 using the ISP header located near the microcontroller

We can program the ATmega328 using this ISP header located near the microcontroller.

How to Prepare the Programmer

To program the microcontroller, we need a USB ISP AVR programmer, which is a tool used to flash the bootloader and program code to the ATmega328p. These tools can be obtained from any number of sources. 

The only requirement for this project is that your USB AVR programmer includes a cable or adapter for using a six-pin ISP header like the one on the robot’s arm.

USB ASP programmer

This USB ASP programmer has an adapter that allows it to interface with a six-pin programming header.

For the sake of aesthetics, we did not add any pins to the ISP header on the Maker Pro robot PCB. Instead, in order to connect the USB ASP programmer to the board, we will use a nifty little part called a pogo pin. A pogo pin is a spring-loaded pin that can be used to make temporary electrical connections.

Pogo Pin Demo

Take six pogo pins and insert them into the six-pin header on the USB ASP programmer. This little mechanism will allow us to temporarily connect a computer to the Maker Pro robot PCB for programming.

Programming_ATmega328_SH_MP_image4.jpg

The pogo pins allow the USB ASP programmer to be temporarily connected to the PCB without requiring headers to be soldered to the board.

Download the Code

Next up, download the code for the project and open it (or copy/paste) with the Arduino IDE.

        /* 
   Project:  Maker Pro robot PCB badge
   Published on Maker Pro
   Author:  Scott Hatfield (Toglefritz)  
*/

// NEOPIXEL SETUP //

// Include the Adafruit NeoPixel library
#include <Adafruit_NeoPixel.h>

#define PIN 6   // The LEDs are all controlled from pin 6

// There are a total of five LEDs on the Maker Pro robot badge: one on the antenna,
// one on each eye, and two on the wrench
#define NUMPIXELS 5   // There are five LEDs     

// Set up the NeoPixel library
Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);

// The LEDs are all in a single 'strip' with the index on the strip used to activate
// each LED individually
#define LEYE 0
#define REYE 1
#define WTOP 2
#define WBOT 3
#define ANT 4

// Create some color variables to use with the LEDs
uint32_t red = pixels.Color(255, 0, 0);
uint32_t off = pixels.Color(0, 0, 0);

// BUTTONS SETUP //

#define NEXT 15
#define PREV 7

// EINK DISPLAY SETUP //

// Include the libarries necessary to control the display
#include <Adafruit_GFX.h>    // Core graphics library
#include "Adafruit_EPD.h"    // Hardware-specific library
#include <SD.h>

// eInk display connections

#define EPD_CS     10
#define EPD_DC      9
#define SRAM_CS     8
#define EPD_RESET   5 
#define EPD_BUSY    -1 // Do not use a pin (will wait a fixed delay)
#define SD_CS       4

// Set up the display
Adafruit_SSD1608 display(200, 200, EPD_DC, EPD_RESET, EPD_CS, SRAM_CS, EPD_BUSY);
#define BUFFPIXEL 20


// SKETCH VARIABLES  //

long progTime = 0;    // A variable for tracking the time since the program started running via millis() method

// Antenna LED variables
int prevBlink = 0;   // A variable for tracking the last time the antenna LED blinked
long blinkInterval = 2000;    // The LED on the top of the antenna blinks on a random interval between two and seven seconds
const int blinkDuration = 200;    //  This variable determines how long each antenna blink lasts

// Eye LED variables
int eyeR = 0;   // The eye LED red channel
int eyeG = 204;   // The eye LED green channel
int eyeB = 193;   // The eye LED blue channel

// Wrench LED variables
int mode = 1;   // A variable for tracking what animation to use for the wrench LEDs (1 = rainbow cycle, 2 = color flash, 3 = split rainbow)
float wrenchRainbowColor = 1.00;    // A variable for the color for the wrench LEDs
int wrenchFlashColor = 1;   // A variable for the color for the wrench LEDs 
int flashInterval = 1000;   // The interval for flashing the LEDs
int prevFlash = 0;    // The time the LEDs last flashed
bool flashState = true;    // A boolearn for turning the LEDs on and off

// eInk display variables
int prevDispChange = 0;   // A variable for tracking the last time the image on the eInk display was changed
int totalQRimages = 3;    // The total number of QR code images stored on the SD card
int imageSelector = 1;  // An image used to select which image to display

void setup() {
  // Set up the LEDs
  pixels.begin();   // Initialize NeoPixel object
  pixels.clear();   // Set all pixel colors to 'off' to start
  pixels.setBrightness(50);   // The LEDs do not need to be super bright

  // Set pin modes for the buttons
  pinMode(NEXT, INPUT);
  pinMode(PREV, INPUT);

  // Initialize the SD card
  SD.begin(SD_CS);

  display.begin();
  // Reset the display
  display.clearBuffer();
  // Rotate the display to show the QR codes the right side up
  display.setRotation(3);  

  // Display the first image
  bmpDraw("/qrcode1.bmp",0,0);
}

void loop() {
  // Most of the LED-related functions are timing based. Therefore, on each loop, get the current program time.  
  progTime = millis(); 

  // ANTENNA LED //
  
  // The LED on the antenna blinks at random intervals
  // If it has been at least the blink interval time since the last antenna blink, then blink the antenna
  if(progTime - prevBlink > blinkInterval) {
    // Turn the antenna LED on
    pixels.setPixelColor(ANT, red);  
    // Update the previous blink time
    prevBlink = millis();
    // Get a new blink interval
    blinkInterval = random(2000, 7000);   // Get a random interval between five and seven seconds
  }
  // If the antenna LED has been on for at least the blink duration, turn it back off again
  else if(progTime - prevBlink > blinkDuration) {
    pixels.setPixelColor(ANT, off);
  }

  // EYE LEDS //
  
  // The eye LEDs glow a random color that changes each time the NEXT or PREV buttons are pressed
  pixels.setPixelColor(LEYE, eyeR, eyeG, eyeB);
  pixels.setPixelColor(REYE, eyeR, eyeG, eyeB);

  // WRENCH LEDS //

  // The wrench LEDs have three possible modes that change each time the NEXT or PREV buttons are pressed
  // If mode = 1, do rainbow cycle animation
  if(mode == 1) {
    if(wrenchRainbowColor < 255) {  
      wrenchRainbowColor = wrenchRainbowColor + 0.01;
    }
    else {
      wrenchRainbowColor = 0;
    }
    
    pixels.setPixelColor(WTOP, Wheel(wrenchRainbowColor));  
    pixels.setPixelColor(WBOT, Wheel(wrenchRainbowColor));    
  }
  
  // If mode = 2, do color alternate flash
  else if(mode == 2) {    
    // If flashState is true, turn on the LEDs
    if(flashState) {
      pixels.setPixelColor(WTOP, Wheel(wrenchFlashColor));
      pixels.setPixelColor(WBOT, Wheel(wrenchFlashColor));         
    }
    // If flashState is false, turn off the LEDs
    else {
      pixels.setPixelColor(WBOT, off);
      pixels.setPixelColor(WTOP, off);
    }

    // If it has been at lesat the cycle interval since the last cycle, swtich the LEDs and increment the color
    if(progTime - prevFlash > flashInterval) {
      flashState = !flashState;   // Switch the LED state
      prevFlash = millis();   // Update the previous flash time

      // If the cycle color is under or equal to 250, increase the color by a value of 5
      if(wrenchFlashColor <= 250) {
        wrenchFlashColor = wrenchFlashColor + 5;
      }
      // Otherwise, set the color back to zero
      else {
        wrenchFlashColor = 0;
      }
    }
  }

  // Otherwise, if mode = 3, do split rainbow animation
  else {
    if(wrenchRainbowColor < 255) {  
      wrenchRainbowColor = wrenchRainbowColor + 0.01;
    }
    else {
      wrenchRainbowColor = 0;
    }
    
    pixels.setPixelColor(WTOP, Wheel(wrenchRainbowColor));  
    pixels.setPixelColor(WBOT, Wheel(wrenchRainbowColor + 127));  
  }
  
  // Update the LEDs
  pixels.show();

  // BUTTONS //

  // If either button is pressed, update the LEDs
  if(digitalRead(NEXT) == HIGH || digitalRead(PREV) == HIGH) {
    // Change the eye color
    int colorSelect = random(0,10);

    switch(colorSelect) {
      case 0:
        eyeR = 0;
        eyeG = 204;
        eyeB = 193;
      break;
      case 1:
        eyeR = 11;
        eyeG = 118;
        eyeB = 217;
      break;
      case 2:
        eyeR = 11;
        eyeG = 180;
        eyeB = 227;
      break;
      case 3:
        eyeR = 11;
        eyeG = 227;
        eyeB = 155;
      break;
      case 4:
        eyeR = 11;
        eyeG = 217;
        eyeB = 90;
      break;
      case 5:
        eyeR = 213;
        eyeG = 21;
        eyeB = 178;
      break;
      case 6:
        eyeR = 153;
        eyeG = 128;
        eyeB = 15;
      break;
      case 7:
        eyeR = 255;
        eyeG = 37;
        eyeB = 25;
      break;
      case 8:
        eyeR = 204;
        eyeG = 70;
        eyeB = 0;
      break;
      case 9:
        eyeR = 232;
        eyeG = 12;
        eyeB = 84;
      break;
    }

    // Change the wrench LED mode
    if(mode < 3) {
      mode++;
    }
    else {
      mode = 1;
    }

    // The eInk display must be limited to updating at most once ever 180 seconds to avoid permenant 
    // damage to the display. Therefore, before changing the image, check to make certain that at least
    // 180 seconds have passed since the last change.
    if(progTime - prevDispChange > 180000) {
    // If it was the next button that was pressed, go the next image
    if(digitalRead(NEXT) == HIGH) {        
      if(imageSelector < 3) {      
        imageSelector++;    // Increment the image selector
      }
      else {
        imageSelector = 1;
      }
    }
    // Otherwise, if the previous button was pressed, go the the previous image
    else {
      if(imageSelector > 1) {
        imageSelector--;    // Deincrement the image selector
      }
      else {
        imageSelector = 3;
      }
    }

    switch (imageSelector) {
      case 1:
        bmpDraw("/qrcode1.bmp",0,0);
        break;
      case 2:
        bmpDraw("/qrcode2.bmp",0,0);
        break;
      case 3:
        bmpDraw("/qrcode3.bmp",0,0);
        break;
     }

    // Update the previous display change time
    prevDispChange = millis();
  }
}
}

// Wheel() is a helper function to get colors from single values between 0 and 255  
// Input a value 0 to 255 to get a color value.
// The colours are a transition r - g - b - back to r.
uint32_t Wheel(byte WheelPos) {
    WheelPos = 255 - WheelPos;
    if(WheelPos < 85) {
        return pixels.Color(255 - WheelPos * 3, 0, WheelPos * 3);
    }
    else if(WheelPos < 170) {
        WheelPos -= 85;
        return pixels.Color(0, WheelPos * 3, 255 - WheelPos * 3);
    }
    else {
        WheelPos -= 170;
        return pixels.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
    }
}

// This function opens a Windows Bitmap (BMP) file and
// displays it at the given coordinates.  It's sped up
// by reading many pixels worth of data at a time
// (rather than pixel by pixel).  Increasing the buffer
// size takes more of the Arduino's precious RAM but
// makes loading a little faster.  20 pixels seems a
// good balance.
void bmpDraw(char *filename, int16_t x, int16_t y) {

  display.clearBuffer();

  File     bmpFile;
  int      bmpWidth, bmpHeight;   // W+H in pixels
  uint8_t  bmpDepth;              // Bit depth (currently must be 24)
  uint32_t bmpImageoffset;        // Start of image data in file
  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, x2, y2, bx1, by1;
  uint8_t  r, g, b;
  uint32_t pos = 0, startTime = millis();

  if((x >= display.width()) || (y >= display.height())) return;

  // Open requested file on SD card
  if ((bmpFile = SD.open(filename)) == (int)NULL) {
    Serial.print(F("File not found"));
    return;
  }

  // Parse BMP header
  if(read16(bmpFile) == 0x4D42) { // BMP signature
    Serial.print(F("File size: ")); Serial.println(read32(bmpFile));
    (void)read32(bmpFile); // Read & ignore creator bytes
    bmpImageoffset = read32(bmpFile); // Start of image data
    Serial.print(F("Image Offset: ")); Serial.println(bmpImageoffset, DEC);
    // Read DIB header
    Serial.print(F("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(F("Bit Depth: ")); Serial.println(bmpDepth);
      if((bmpDepth == 24) && (read32(bmpFile) == 0)) { // 0 = uncompressed

        goodBmp = true; // Supported BMP format -- proceed!
        Serial.print(F("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
        x2 = x + bmpWidth  - 1; // Lower-right corner
        y2 = y + bmpHeight - 1;
        if((x2 >= 0) && (y2 >= 0)) { // On screen?
          w = bmpWidth; // Width/height of section to load/display
          h = bmpHeight;
          bx1 = by1 = 0; // UL coordinate in BMP file
  
          for (row=0; row<h; row++) { // For each scanline...
  
            // 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 + by1)) * rowSize;
            else     // Bitmap is stored top-to-bottom
              pos = bmpImageoffset + (row + by1) * rowSize;
            pos += bx1 * 3; // Factor in starting column (bx1)
            if(bmpFile.position() != pos) { // Need seek?
              bmpFile.seek(pos);
              buffidx = sizeof(sdbuffer); // Force buffer reload
            }
            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 EPD format, push to display
              b = sdbuffer[buffidx++];
              g = sdbuffer[buffidx++];
              r = sdbuffer[buffidx++];

              uint8_t c = 0;
              if ((r < 0x80) && (g < 0x80) && (b < 0x80)) {
                 c = EPD_BLACK; // try to infer black
              } else if ((r >= 0x80) && (g >= 0x80) && (b >= 0x80)) {
                 c = EPD_WHITE;
              } else if (r >= 0x80) {
                c = EPD_RED; //try to infer red color
              }
              
              display.writePixel(col, row, c);
            } // end pixel
          } // end scanline
        } // end onscreen
        display.display();
        Serial.print(F("Loaded in "));
        Serial.print(millis() - startTime);
        Serial.println(" ms");
      } // end goodBmp
    }
  }

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

// These read 16- and 32-bit types from the SD card file.
// BMP data is stored little-endian, Arduino is little-endian too.
// May need to reverse subscript order if porting elsewhere.

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;
}
    

The code is heavily commented on if you want to understand exactly how it works but the overall functionality is not too complex. There are essentially two major parts: the code that controls the LEDs, and the code that manages the eInk display.

Controlling the Five LEDs

A chunk of the code is dedicated to controlling the five LEDs on the PCB. The robot’s eye LEDs choose a random color to glow each time the NEXT or PREV buttons are pressed. 

The LED at the top of the antenna blinks red at random intervals. The two LEDs on the wrench also change each time the QR code changes; they can cycle colors, flow through a rainbow of colors, or individually flow through a rainbow of colors.

Wrench LED Animation 1 Video

Wrench LED Animation 2 Video

Displaying QR Codes 

The other major functionality is displaying QR codes on the eInk display. The QR codes themselves are loaded from image files stored on the SD card installed on the back of the eInk display (more on that later). 

The QR code allows others to scan the code and view your articles on Maker Pro. On the robot’s legs, there is a NEXT and a PREVIOUS button to change the QR code. In order to avoid damage to the eInk display, the maximum frequency with which the QR codes can be changed is once every three minutes.

Flash the Bootloader

Before code can be uploaded to the ATmega328p onboard the Maker Pro robot PCB, we will need to flash the Arduino bootloader onto the chip. 

The Arduino bootloader is a little program that runs on the microcontroller that allows it to understand code being uploaded from the Arduino IDE. When you purchase an Arduino board, it comes pre-flashed with the Arduino bootloader. In this case, though, our ATmega328p is blank. Before we can upload code, we will need to put the bootloader onto the chip, a process called burning the bootloader. This will only need to be done once.

First, we need to set up the Arduino IDE to connect with the Maker Pro robot badge. First, from the Tools > Board: menu, select Arduino Nano. We make this selection because the Arduino Nano also uses an ATmega328p. 

Programming_ATmega328_SH_MP_image2.png

From the Tools menu, under Board:, select Arduino Nano.

Then, in Tools > Programmer: select USBasp.

Programming_ATmega328_SH_MP_image5.png

From the Tools menu, under Programmer:, select USBasp.

Now, to flash the Arduino bootloader onto the ATmega328p, we first need to connect the USB ASP programmer to the Maker Pro robot PCB. 

If you take a look at the USB ASP programming tool into which we placed the pogo pins, you will find a pin labeled RST. The USB ASP programming pins should be oriented so that this RST pin connects to the pin just above the ISP label on the board’s ISP header, as indicated in the photo below.

Correct placement of the USB ASP programming pins.

Correct placement of the USB ASP programming pins.

Then, with the USB ASP programmer connected to the Maker Pro robot PCB via pogo pins, select Tools > Burn Bootloader in Arduino IDE. 

Burning the bootloader should only take a moment and you will get a success message at the bottom of the IDE once the process is complete. Just make certain to keep the USB ASP programmer in place until the bootloader is finished burning onto the microcontroller or you could risk bricking the chip.

Programming_ATmega328_SH_MP_image7.png

With the USB ASP programmer in place, select Tools > Burn Bootloader.

Upload the Code

Once the bootloader has been successfully flashed onto the ATmega328p, we can upload the actual code that will run the badge. 

Normally, you would simply press the Upload button in the Arduino IDE toolbar, however, this functionality requires a USB to serial adapter on the PCB, which our Maker Pro robot badge lacks. Instead, with the USB ASP programmer still connected to the board, go to Sketch > Upload Using Programmer.

Programming_ATmega328_SH_MP_image6.png

To upload code using the USB ASP programmer, select Sketch > Upload Using Programmer.

Just like when you upload code via a USB connection, as you do with Arduino board, the Arduino IDE will verify and compile the sketch, then display a “Done uploading” message when the processes finish.

Review of Progress and Next Steps

Directly programming an ATmega328 microcontroller is a bit different from programming an Arduino board — even one using the same microcontroller. There are several ways to program microcontrollers after they have been assembled into a PCB. 

For this project, we used one of the most common methods of programming microcontrollers as part of the electronics manufacturing process: using an ISP header on the Maker Pro robot PCB.

At this point, there is only one task left to finish up the project: creating the QR codes that will be displayed on the eInk module.

Scott Hatfield
Hello, my name is Scott. I like to take big, complicated projects and break them down into simple steps that anybody can understand.