In this tutorial, we’ll get into the basics of the display by building a simple Larson Scanner using the AS1115 LED Driver and bar graph.
Background
AS1115 Display Driver Overview
I2C Display Add-on Datasheet
I2C Basics
Changing the I2C Library
AS1115 Datasheet
I2C Display Setup
The Display only requires six connections: 5V, GND, SCL, SDA as well as a connection for the multiplexer and the piezo speaker. It was designed to work directly with the I2C and SPI Education Shield, and as because of that, it does not have I2C pull up resistors on board. Depending on what you’re connecting to, it may be necessary to provide those resistors as well as series resistors on the SCL and SDA connections. For general connections to an Arduino or other development board, pull up resistors of 4.7K should be sufficient. If you need to use series resistors, 100Ω is fine.
For this tutorial, I’ve connected the I2C Display Add-on to an Arduino Uno using a breadboard, jumper wires and 4.7K pull up resistors.
AS1115 Larson Scanner
A Larson Scanner, for those who aren’t familiar with it, is a simple display that has LEDs that chase back and forth, left to right to left. If you think of the nose of the car KITT from Knight Rider, or the eyes of the Cylons from the 80s version of Battlestar Galactica, that’s what a Larson Scanner is. In fact, it’s called a “Larson Scanner” because the creator of those two shows specifically, was Glen A. Larson.
So what we’re going to do, is have the top and bottom row of green LEDs act as a Larson Scanner on the I2C Display. And just because we don’t want to ignore the seven segment display, we’ll count how many times we’ve scanned and display it numerically.
AS1115 Larson Scanner Code Plan
As usual, we’ll want to start with a pretty descriptive set of declarations to reference the various registers of the AS1115, so that we’re not guessing at what our code does. There is a bit of configuration necessary to get the chip running as well, so a function to perform chip initialization will be necessary.
We’re going to want to update the display quite frequently, and since we’re updating two different things, the number display and the bar graph, it would get ugly fast if we put all of that into the main loop of our sketch. So we’ll want to create a generic display update function. Any glitches that occur won’t be buried in data, but displayed pretty visually, so if any I2C errors occur, we’ll want to capture those as well.
Declarations
First things first… load the library by putting #include <I2C.h> right at the top of your sketch. This is a common mistake to make and will cause all kinds of compile havoc if you forget it.
We need to create a description of the I2C address of our AS1115, and then each of the registers we’ll need to work with in this sketch, including the digit registers.
Finally we need to create a global variable to hold the amount of errors that might be occurring, and a simple global byte we can use to hold the returned error code (if any) from the I2C commands we’ll be using. There’s also the little variable that will hold how many times we’ve scanned left-right-left with the bar graph, so we can display it on the 7SEG.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <I2C.h> const byte AS1115_I2C = 0x00; // Display Manager IC I2C Address const byte REGISTER_SHUTDOWN_MODE = 0x0C; // Manages the shutdown features of the AS1115 const byte REGISTER_DECODE = 0x09; // Determines which digits are decoded const byte REGISTER_SCAN_LIMIT = 0x0B; // Determines which digits are enabled const byte REGISTER_INTENSITY = 0x0A; // Determins how bright the display is globally const byte DIGIT_0 = 0x01; // Left most seven segment digit const byte DIGIT_1 = 0x02; // Second seven segment digit const byte DIGIT_2 = 0x03; // Third seven segment digit const byte DIGIT_3 = 0x04; // Right most seven segment digit const byte BTM_RED = 0x05; // Bottom Red bar graph multiplexed with L1/L2/L3 const byte BTM_GRN = 0x06; // Bottom Green bar graph const byte TOP_RED = 0x07; // Top Red bar graph const byte TOP_GRN = 0x08; // Top Green bar graph long errorCount = 0; // A variable for holding how many errors occur byte errorStatus = 0; // A variable to hold the I2C command error code int scanCycles = 0; // A variable to hold how many times the scanner cycles |
Error Checking
We’re going to use a very simple error checking system that will simply increment the amount of errors occurring on the I2C bus and display that count on the Serial monitor. That way, if the display acts glitchy, we can open up the monitor and see just how bad it is, then start taking corrective action.
The way this works, is by wrapping every I2C command with an error checker. We’ll take errorStatus and set it equal to the returned error code from each I2C command. Then we’ll quickly check the value… if it’s zero, we do nothing, if it’s anything else, we’ll call the error handler.
1 2 |
errorStatus = I2c.write (SomeChip, SomeRegister, SomeValue); if (errorStatus != 0) errorHandler(); |
The error handling function will increment the error count, then display it, along with the error code that was loaded into errorStatus (which is a global variable, so easy to access from any function in the sketch). As a good measure, we’ll reset errorStatus to 0 before we exit the handler, so we’re always in a known state when beginning an I2C command.
1 2 3 4 5 6 7 8 9 10 |
void errorHandler() { errorCount++; Serial.print ("Error Code: 0x"); Serial.print (errorStatus, HEX); Serial.print (" Error Count Is Now: "); Serial.println (errorCount); errorStatus = 0; } |
Setup
The Setup function is going to be pretty simple. First things first, get the Serial system initialized. Then Analog 0, which controls the multiplexing SPDT IC on the Display, needs to be driven high. That will enable the bottom row red LEDs and disable the L1/L2/L3 display on the 7SEG.
Because we’re using the customizable I2C library, and not the Wire library, we need to configure our I2C settings quickly. We need to initialize the buffers, disable internal pull up resistors, set our bus speed to 100kHz, and prepare a timeout value to keep our sketch from jamming if something fails to update the microcontroller appropriately.
Finally, we’ll make a call to the chip initialization function that will configure the AS1115 for Larson Scanner operation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
void setup() { pinMode (A0, OUTPUT); // Configure the Display multiplexer pin as output digitalWrite (A0, HIGH); // Enable the bottom row red LED, not L1/L2/L3 I2c.begin (); // Initialize the I2C library I2c.pullup (0); // Disable the internal pullup resistors I2c.setSpeed (0); // Enable 100kHz I2C Bus Speed I2c.timeOut (250); // Set a 250ms timeout before the bus resets Serial.begin(9600); init_AS1115(); } |
The chip initialization needs to perform at least one thing, which is to bring the chip out of the default shutdown state. Beyond that, we have to think through how many digits we want to display (scan limit), what format we intend to use to control them (decode mode), and how bright we want to display them (intensity), .
The scan limit doesn’t allow enable / disable of individual digits, but rather 0 or 0 and 1 or 0 and 1 and 2, etc. We need to show everything since we’re going to be using the top green LED bar graph, so we’ll need to write a 0x07 to REGISTER_SCAN_LIMIT.
Our decode method is a little tricky. We want to be able to send simple values to the 7SEG digits, but want to control the LEDs of the bar graph individually. That means Digits 0 through 3 need to use the decode fonts, but the bar graph needs a different setting. Each digit (and we’re thinking of each bar graph as a separate digit as well) is represented by a corresponding bit in REGISTER_DECODE, bit 0 = digit 0, bit 1 = digit 1, etc. Setting the bit means decode, clearing the bit means no decode. Using that logic, we need to write B00001111, hex value 0x0F to the register.
The intensity level is entirely subjective so it’s up to you how bright you want to make everything. Writing 0x00 to REGISTER_INTENSITY is the least bright and 0x0F is the most bright. You can just increase or reduce this value as you want. Remember you can also adjust each digits brightness individually, but we’re not going to cover that in this tutorial.
Finally, we need to bring the chip out of shutdown, by writing 0x01 to REGISTER_SHUTDOWN_MODE.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
void init_AS1115() { errorStatus = I2c.write(AS1115_I2C, REGISTER_SCAN_LIMIT, 0x07); // Enable Digits 0:6 if (errorStatus != 0) errorHandler(); errorStatus = I2c.write(AS1115_I2C, REGISTER_DECODE, 0x0F); // Set digits 0-3 to decode as fonts if (errorStatus != 0) errorHandler(); errorStatus = I2c.write(AS1115_I2C, REGISTER_INTENSITY, 0x08); // Set the intensity level; 0x00 = LOW, 0x0F = HIGH if (errorStatus != 0) errorHandler(); errorStatus = I2c.write(AS1115_I2C, REGISTER_SHUTDOWN_MODE, 0x01); // Begin normal operation if (errorStatus != 0) errorHandler(); } |
Main Loop
To keep everything clean, the main loop is just going to call the AS1115 Larson Scanner function, and the scan cycle count display function.
1 2 3 4 5 6 |
void loop() { cycleLarsonScanner(); updateScanCount(); } |
cycleLarsonScanner
This isn’t going to be the fanciest of Larson Scanners, but merely a demo to get the board up and running for you. So the bit won’t accelerate / decelerate or fade or any of the real cool things that people can get it to do. What it will do is just chase a bit back and forth across both the green bar graphs.
To do that, we’ll need to simple for loops. The first one will send the bit to the left, by shifting the value 0x01 the amount of places to the left dictated by how far we are through the first loop.
The second one will send the bit to the right, by shifting the value 0x04 the amount of places to the right dictated by how far we are through the second loop.
The only tricky part of this, is not re-displaying the bits at the ends by duplicating them in the two loops. That’s why the first loop will go from bit 0 to bit 6 and the second loop goes from bit 7 to bit 1.
A delay has to be included to allow our eyes to see the actual movement. To slow and it looks clunky, too fast and it just blinks. I set a speed of 40 for the delay and that’s nice and zippy, but not too zippy.
At the end of the function, we increment how many times we’ve cycled through the scanner, until we get to 9999 when we reset it to zero, so we don’t start displaying funky characters on the display (it would overflow into the decode font characters you had selected).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
void cycleLarsonScanner() { byte scannerSpeed = 40; // Send the bit to the left for (int i = 0; i < 7; i ++) { errorStatus = I2c.write(AS1115_I2C, TOP_GRN, 0x01 << i); if (errorStatus != 0) errorHandler(); errorStatus = I2c.write(AS1115_I2C, BTM_GRN, 0x01 << i); if (errorStatus != 0) errorHandler(); delay(scannerSpeed); } // Send the bit to the right for (int i = 0; i < 7; i ++) { errorStatus = I2c.write(AS1115_I2C, TOP_GRN, 0x80 >> i); if (errorStatus != 0) errorHandler(); errorStatus = I2c.write(AS1115_I2C, BTM_GRN, 0x80 >> i); if (errorStatus != 0) errorHandler(); delay(scannerSpeed); } // increment the scanCycles variable, unless it's already at 9999, then reset it if (scanCycles == 9999) scanCycles = 0; scanCycles++; } |
updateScanCount
This one is very simple. Since we’re decoding the values we send to the first four digits of the display, all we have to do is send the actual value we want to show, right to the digits register. To do that, we just have to break a four digit number into it’s constituent places by using some division and some modulo math.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void updateScanCount() { errorStatus = I2c.write(AS1115_I2C, DIGIT_0, scanCycles / 1000); if (errorStatus != 0) errorHandler(); errorStatus = I2c.write(AS1115_I2C, DIGIT_1, scanCycles % 1000 / 100); if (errorStatus != 0) errorHandler(); errorStatus = I2c.write(AS1115_I2C, DIGIT_2, scanCycles % 1000 % 100 / 10); if (errorStatus != 0) errorHandler(); errorStatus = I2c.write(AS1115_I2C, DIGIT_3, scanCycles % 1000 % 100 % 10); if (errorStatus != 0) errorHandler(); } |
Full Sketch
And finally, here’s the entire sketch fully assembled…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
/* A sketch that displays a simple Larson Scanner using the green bar graph LEDs on the I2C Display Add-on board, and displays a running count of how many times the scan has cycled through. https://rheingoldheavy.com/i2c-display-add-on-tutorial-02-larson-scanner/ */ #include <I2C.h> const byte AS1115_I2C = 0x00; // Display Manager IC I2C Address const byte REGISTER_SHUTDOWN_MODE = 0x0C; // Manages the shutdown features of the AS1115 const byte REGISTER_DECODE = 0x09; // Determines which digits are decoded const byte REGISTER_SCAN_LIMIT = 0x0B; // Determines which digits are enabled const byte REGISTER_INTENSITY = 0x0A; // Determins how bright the display is globally const byte DIGIT_0 = 0x01; // Left most seven segment digit const byte DIGIT_1 = 0x02; // Second seven segment digit const byte DIGIT_2 = 0x03; // Third seven segment digit const byte DIGIT_3 = 0x04; // Right most seven segment digit const byte BTM_RED = 0x05; // Bottom Red bar graph multiplexed with L1/L2/L3 const byte BTM_GRN = 0x06; // Bottom Green bar graph const byte TOP_RED = 0x07; // Top Red bar graph const byte TOP_GRN = 0x08; // Top Green bar graph long errorCount = 0; // A variable for holding how many errors occur byte errorStatus = 0; // A variable to hold the I2C command error code int scanCycles = 0; // A variable to hold how many times the scanner cycles void setup() { pinMode (A0, OUTPUT); // Configure the Display multiplexer pin as output digitalWrite (A0, HIGH); // Enable the bottom row red LED, not L1/L2/L3 I2c.begin (); // Initialize the I2C library I2c.pullup (0); // Disable the internal pullup resistors I2c.setSpeed (0); // Enable 100kHz I2C Bus Speed I2c.timeOut (250); // Set a 250ms timeout before the bus resets Serial.begin(9600); init_AS1115(); } void loop() { cycleLarsonScanner(); updateScanCount(); } void errorHandler() { errorCount++; Serial.print ("Error Code: 0x"); Serial.print (errorStatus, HEX); Serial.print (" Error Count Is Now: "); Serial.println (errorCount); errorStatus = 0; } void init_AS1115() { errorStatus = I2c.write(AS1115_I2C, REGISTER_SCAN_LIMIT, 0x07); // Enable Digits 0:6 if (errorStatus != 0) errorHandler(); errorStatus = I2c.write(AS1115_I2C, REGISTER_DECODE, 0x0F); // Set digits 0-3 to decode as fonts if (errorStatus != 0) errorHandler(); errorStatus = I2c.write(AS1115_I2C, REGISTER_INTENSITY, 0x08); // Set the intensity level; 0x00 = LOW, 0x0F = HIGH if (errorStatus != 0) errorHandler(); errorStatus = I2c.write(AS1115_I2C, REGISTER_SHUTDOWN_MODE, 0x01); // Begin normal operation if (errorStatus != 0) errorHandler(); } void cycleLarsonScanner() { byte scannerSpeed = 40; // Send the bit to the left for (int i = 0; i < 7; i ++) { errorStatus = I2c.write(AS1115_I2C, TOP_GRN, 0x01 << i); if (errorStatus != 0) errorHandler(); errorStatus = I2c.write(AS1115_I2C, BTM_GRN, 0x01 << i); if (errorStatus != 0) errorHandler(); delay(scannerSpeed); } // Send the bit to the right for (int i = 0; i < 7; i ++) { errorStatus = I2c.write(AS1115_I2C, TOP_GRN, 0x80 >> i); if (errorStatus != 0) errorHandler(); errorStatus = I2c.write(AS1115_I2C, BTM_GRN, 0x80 >> i); if (errorStatus != 0) errorHandler(); delay(scannerSpeed); } // increment the scanCycles variable, unless it's already at 9999, then reset it if (scanCycles == 9999) scanCycles = 0; scanCycles++; } void updateScanCount() { errorStatus = I2c.write(AS1115_I2C, DIGIT_0, scanCycles / 1000); if (errorStatus != 0) errorHandler(); errorStatus = I2c.write(AS1115_I2C, DIGIT_1, scanCycles % 1000 / 100); if (errorStatus != 0) errorHandler(); errorStatus = I2c.write(AS1115_I2C, DIGIT_2, scanCycles % 1000 % 100 / 10); if (errorStatus != 0) errorHandler(); errorStatus = I2c.write(AS1115_I2C, DIGIT_3, scanCycles % 1000 % 100 % 10); if (errorStatus != 0) errorHandler(); } |
And here’s a link to an HTML 5 video of the Larson Scanner in action (which for reasons of javacript conficlt that are beyond my capabilities to solve, I cannot embed, unfortunately): Rheingold Heavy I2C Display Add-on Larson Scanner