empowering creative people

Hello World with the OLED FeatherWing

The OLED Featherwing by Adafruit is a particularly interesting Wing; it gives you a 128x32 monochrome OLED display right on top of your feather board. There's also three buttons to use that are internally connected to your MCU and accessible via the IDE. When I first saw them, I thought these buttons would be fantastic for user interfaces you've got the options to need to navigate a menu system. But I wanted to have a little bit of fun at the very least. Today we are going to be using a Basic Proto M0 Feather Board alongside the OLED Wing to make our a Feather-Snake game! I'm not sure if it's been done before, but why not do it again?

You'll need the OLED FeatherWing and a Feather Board, we're using the Basic Proto M0 Feather in our example, the only thing you'll need to be aware of with different boards is the pins that the buttons are attached to.

The framework of the Snake game we will create comes from GitHub user BauerPower, who initially created the snake logic for a 128x64 OLED display. We'll take a look at the usage of the 128x32 OLED FeatherWing primarily, but you'll have your own working Snake game at the end of the tutorial!

The original game was made using one button for snake direction control, so we will map another button to move our snake in the opposite direction. It also uses hardware interrupts with changing logic to control when the snake turns, I've chosen to utilise the pull-up resistors for the buttons on the Wing and use Falling logic instead. A few tweaks to the debounce functions and border limits on the FeatherWing will be all that we need to make our Snake game port across and work perfectly!

Find BauerPower's Original Snake Sketch here.

There are three essential libraries we need to use to control the OLED display:

  1. The GFX Library from Adafruit
  2. SSD1306 Display Library from Adafruit
  3. Wire library (included in Arduino IDE)

The GFX library is used to render pixels, fonts, shapes and bitmaps to any of the Adafruit displays, whereas the SSD1306 initializes your sketch with all the I2C commands that control cursor position, display control, etc. Finally, the wire library will allow the FeatherWing OLED to use the SDA and SCL pins with the Pin headers included for I2C communication. All we need to do is include those libraries and initialize our display with the i2C address "0x38" in the sketch.

To enable our display we need to begin communication and clear the display:

1. Before your Setup function, define your display as:

Adafruit_SSD1306 display = Adafruit_SSD1306(); //Now you can write to this screen using display.[draw function]

2. Now we can include these functions into our setup to initialize the screen

display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // initialize with the I2C address 0x3C (for the 128x32)
display.clearDisplay(); // Clear the display

To write words to the screen, we can then use a couple of display functions that allow us to choose where and what to write:

display.setTextSize(1); //Set our text size, size 1 correlates to 8pt font
display.setTextColor(WHITE); //We're using a Monochrome OLED so color is irrelevant, pixels are binary.
display.setCursor(0,0); //Start position for the font to appear
display.println("Font to display"); //Will display text on new line
display.display();

text-to-display-oled

First things first, we can turn a single pixel on if we want to. To do so, you use the function DrawPixel (go figure). To define your pixel, you will need to select the X and Y coordinate that your pixel occupies.

display.drawPixel(X-Coordinate, Y-coordinate, Color); 

oled-pixel-display

If we wanted to draw a line, however, we need to provide the start and end pixels to our sketch.

display.drawLine( Start-X, Start-Y, End-X, End-Y, Color);

drawLine-example

The drawLine function does angular calculations to fill in perpendicular grid squares, so they match a line shape. This isn't the best/fastest method to draw a line if it is a straight vertical or horizontal line. The drawFastVLine (and HLine) functions are great for this purpose exactly.

display.drawFastVLine(X-Start, Y-Start, Pixel-length, Color);

horizontal-fast-line-oled

Pixel becomes Line; Line becomes shape! To draw the border of a rectangle shape, we simply drawRect. If we want to fill it in, we fillRect.

display.drawRect(X-Start, Y-Start, Width, Height, Color);

drawRect-oled-example

For a circle, we need a centre point and radius, we can also fill circles or just draw their outline:

display.drawCircle(X-Center, Y-Center, Radius, Color);

drawCircle-display-oled

There are a few other graphics we can render pretty simply with Adafruit's Library, but our Snake game is very basic and only utilises the rectangle/pixel options. I'm not going to be able to walk you through, step-by-step how the snake game works although I've played around with enough to have a decent grasp on it and included some comments to help you digest it. The interesting points for me were the Switch statements used to control clockwise/counter-clockwise turning! Check it all out in more detail below!

#include 
#include 
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

Adafruit_SSD1306 display = Adafruit_SSD1306();

#define BUTTON_A 9
#define BUTTON_B 6
#define BUTTON_C 5
#define LED      13

const int D_NORTH = 1;
const int D_EAST = 2;
const int D_SOUTH = 3;
const int D_WEST = 4;

long score = 0;

byte snakePixelX[20];
byte snakePixelY[20];

int snakeX = 10; //Start X position of snake
int snakeY = 30; //Start Y Position of snake
int snakeLength = 1;

volatile int snakeDir = D_EAST;

volatile int button_A_state = HIGH;
volatile int button_C_state = HIGH;

int minX = 0;
int minY = 0;
int maxX = 128;
int maxY = 32;

int foodX = 0;
int foodY = 0;

long debouncing_time = 250; //Debouncing Time in Milliseconds
volatile unsigned long last_millis;

/*Interrupt Logic*/
void setupButton() {
  pinMode(BUTTON_A, INPUT_PULLUP);
  pinMode(BUTTON_C, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(BUTTON_A), debounceChangeDirectionA, FALLING);
  attachInterrupt(digitalPinToInterrupt(BUTTON_C), debounceChangeDirectionC, FALLING);
}

/* Debounce Logic */
void debounceChangeDirectionA() {
  if((millis() - last_millis) > debouncing_time) {
    changeDirectionA();
    last_millis = millis();
    button_A_state = !button_A_state;  
  }
}

void debounceChangeDirectionC() {
  if((millis() - last_millis) > debouncing_time) {
    changeDirectionC();
    last_millis = millis();
    button_C_state = !button_C_state;
  }
}

                                                                     
/*Movement Logic*/
void changeDirectionA() {
  switch(snakeDir) {
    case D_NORTH:
      snakeDir = D_EAST;
      break;
    case D_EAST:
      snakeDir = D_SOUTH;
      break;
    case D_SOUTH:
      snakeDir = D_WEST;
      break;
    case D_WEST:
      snakeDir = D_NORTH;
      break;
  }
}

void changeDirectionC() {
  switch(snakeDir) {
    case D_NORTH:
      snakeDir = D_WEST;
      break;
    case D_WEST:
      snakeDir = D_SOUTH;
      break;
    case D_SOUTH:
      snakeDir = D_EAST;
      break;
    case D_EAST:
      snakeDir = D_NORTH;
      break;
  }
}
/* Screen initialise */
void setupScreen()   {
  // by default, we'll generate the high voltage from the 3.3v line internally! (neat!)
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);  // initialize with the I2C addr 0x3D (for the 128x64)
  // init done
  display.clearDisplay();

  minX = 0;
  minY = 0;
  maxX = display.width()-1;
  maxY = display.height()-1;

  renderScore();
}

void setup()   {
  setupButton();
  setupScreen();
  dropFood();
  digitalWrite(BUTTON_A, button_A_state);
  digitalWrite(BUTTON_C, button_C_state);
}

bool outOfBounds() {
  return snakeX <= minX || snakeY <= minY || snakeX >= maxX || snakeY >= maxY;
}

void gameOver() {
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(WHITE);
  display.setCursor(5, 5);
  display.println("GAME OVER :(");
  display.display();

  score = 0;
  snakeLength = 1;
  snakeX = display.width() / 2;
  snakeY = display.height() / 2;

  snakePixelX[snakeLength-1] = snakeX;
  snakePixelY[snakeLength-1] = snakeY;

  snakeDir = D_EAST;

  delay(2000);

  display.clearDisplay();
  renderScore();
}

// Drop food on random location
void dropFood() {
  foodX = random(minX+1, maxX-1);
  foodY = random(minY+1, maxY-1);
}

bool collectFood() {
  if (snakeX == foodX && snakeY == foodY) {
    score += 10;

    renderScore();

    dropFood();

    return true;
  } else {
    return false;
  }
}

void renderScore() {
  // TOP
  display.drawLine(0, 0, display.width()-1, 0, WHITE);
  // LEFT
  display.drawLine(0, 0, 0, display.height()-1, WHITE);
  // RIGHT
  display.drawLine(display.width()-1, 0, display.width()-1, display.height()-1, WHITE);
  // BOTTOM
  display.drawLine(0, display.height()-1, display.width()-1, display.height()-1, WHITE);
}

bool crashedIntoSelf() {
  for(byte i = 4; i < snakeLength; i++) {
      if (snakeX == snakePixelX[i] && snakeY == snakePixelY[i]) {
        return true;
      }
  }

  return false;
}


void drawScreen() {
  bool foodCollected = false;

    // Clear the buffer.
  display.clearDisplay();

  display.drawRect(foodX, foodY, 2, 2, WHITE);
  foodCollected  = collectFood();

  // Check snake position
  if (outOfBounds() || crashedIntoSelf()) {
    gameOver();
  }

  // Render the snake
  for(int i = 0; i < snakeLength; i++) {
    display.drawPixel(snakePixelX[i], snakePixelY[i], WHITE);
  }

  // Move pixel values
  for(int i = snakeLength; i > 0; i--) {
    snakePixelX[i] = snakePixelX[i-1];
    snakePixelY[i] = snakePixelY[i-1];
  }

if (foodCollected) {
    snakeLength += 1;
    snakePixelX[snakeLength-1] = snakeX;
    snakePixelY[snakeLength-1] = snakeY;
  }

  switch(snakeDir) {
    case D_NORTH:
      snakeY -= 1;
      break;
    case D_EAST:
      snakeX += 1;
      break;
    case D_SOUTH:
      snakeY += 1;
      break;
    case D_WEST:
      snakeX -= 1;
      break;
  }

  snakePixelX[0] = snakeX;
  snakePixelY[0] = snakeY;
  renderScore();
  display.display();
}


void loop() {

  drawScreen();

}

If you've followed our tutorial on initializing a feather board to your IDE, you'll be a step ahead right now. You can go ahead and upload your code to your board and play some Snake! If you haven't, you'll need to follow the basic steps of downloading and installing the drivers for your Feather. If the Snake turns more than once in rapid succession, you'll need to increase your DebounceDelay value to compensate for the bounce from the push buttons!

Go ahead and upload the code to your Feather, if you've followed our code example, the top button will turn your snake clockwise and the bottom button counter clockwise. It's great fun for at least 30 seconds until you realise your hands are not made for the tiny push buttons on the FeatherWing! Oh well, the entire endeavour was a great mid-week project for me! Hopefully, you've got more of an idea of how to connect your OLED FeatherWing to your Feather now, the idea of displaying data is a handy one to understand! If you notice anything out of place or have recommendations for improvement feel free to let me know in the comments below! Thanks for taking the time to read our tutorial today.

The OLED Featherwing by Adafruit is a particularly interesting Wing; it gives you a 128x32 monochrome OLED display right on t...

Have a question? Ask the Author of this guide today!