Maker Advent Calendar Day #11: OMG OLED!
Welcome to day eleven of your 12 Projects of Codemas Advent Calendar. Today we're playing with a really fun and useful component that you'll use again and again in your projects - a mini I2C OLED display!
Whilst only teeny-tiny, these little displays are great for showing data from your project such as sensor readings, scores, pin status, alerts and other useful information. We're going to combine it with some components from earlier boxes to show you just how handy these are.
Box #11 Contents
In this box you will find:
- 1x Pre-soldered 0.96" OLED I2C display (128x32)
- 4x Male to male jumper wires
Today we're going to display data on our mini I2C OLED display. They're relatively easy to code but they do require the installation of a library (which we import at the start of our program) but we'll guide you through this.
What is an OLED?
The display in your box is an OLED display. OLED stands for Organic Light-Emitting Diode. You may have seen this mentioned on advertisements for TVs as many models use this technology.
It's a type of digital display technology that uses LEDs and layers of thin organic film between two electrodes. When electrical current is applied, the display emits light. Our code tells the display where to show light and when.
What is I2C?
I2C, sometimes referred to as IIC, stands for Inter-Integrated Circuit. It's another type of communication protocol (remember 1-wire from day #8?) which allows multiple I2C devices to communicate to a controller like our Pico.
I2C requires just two wires to communicate (along with 3.3V and GND for our display) and has benefits over some other communication options - but we won't bore you with that just now as it's not going to be relevant until you're much further along on your maker journey.
To use I2C we need to import it in our code, which we'll show you in just two ticks!
Construct the Circuit
First, disconnect your Raspberry Pi Pico from your PC, ensuring it's powered down.
Today we're finally removing the LEDs and buzzer as 'input' components (like buttons and sensors) are more relevant and useful with this display. If you haven't already, remove everything so that you're back to a clean breadboard with just your Pico fitted, like this:
Next, fit one of your buttons and the OLED display into the breadboard like we have below (make sure you leave a gap above both for us to add our jumper wires).
You may also want to peel off the display's protective film at this stage:
Next we'll add our 3.3V connections.
Run a jumper wire from the 3.3V pin (physical pin 36) to the upper red channel, then connect the left button pin to the same red channel, and the second OLED pin labelled 'VCC' to the same channel as well, like below:
Next is our GND pin. Connect the first OLED pin labelled 'GND' to the nearest GND pin on physical pin 18, like we have below:
Now for the three GPIO pins which will talk to our Pico:
- Connect the right button leg to GPIO8 (Physical pin 11)
- Connect the 3rd OLED pin labelled 'SCL' to GPIO1 (Physical pin 2)
- Connect the 4th OLED pin labelled 'SDA' to GPIO0 (Physical pin 1)
We'll be using the light sensor from box #6 in our examples today, so we need to wire that up as well. We'll use our mini breadboard for this.
Fit the light sensor and the 10k resistor into the breadboard as we have below. The long leg of the light sensor needs to go to the left:
Now to wire up the light sensor to our Pico:
- Connect the left resistor leg to the GND connection on physical pin 28
- Connect the left light sensor leg (between the leg and the resistor) to GPIO26 (Physical pin 31)
- Connect the right light sensor leg to 3.3V on physical pin 36
Now turn your breadboards 90 degrees so that the OLED is facing you. This will make it easier to view and read the text we show on it.
Install Code Libraries
Plug your Pico back into your computer's USB port and make sure it's recognised then let's install what we need to make this display work.
Our display needs a code library package that isn't included in MicroPython by default, so we need to install that before we can start using this display. We will then import this library when we write our code
Luckily this is very easy to do and all handled within Thonny, so let's get it installed. The following may vary slightly depending on the type of computer you're using.
Important: If the steps below don't work for you and you see certificate errors or other similar messages, you may need to install the library manually.
To do this:
- Go to this link
- Select all of the code on that page (use Ctrl+A) and copy it over to Thonny
- In Thonny, select File > Save as and choose Raspberry Pi Pico as the destination
- Call the file ssd1306.py
- Select OK
- IMPORTANT - You must now close this ssd1306.py file (use the little X on that tab) then open a new one for your code (File > New) otherwise you'll be overwriting that file and making a big mess of it :)
In Thonny, from the top menu bar select Tools > Manage packages. You should see something similar to the box below :
In the search bar, enter "micropython-ssd1306" then select the search button. You should see results like we have below, including the micropython-ssd1306 module we need at the top of the list:
Select that line to go to the information screen for that module, then select the 'Install' button at the bottom:
Once the package has installed it will show on your installed packages list on the left. It's now ready to use!
Activity 1: Simple Text Display
We'll start by showing a single line of text on the display to kick things off. Of course, it'll be the traditional "Hello World!"...
I2C and Display Setup
To use this display, we need to import I2C as well as the library we have just installed. You'll see this in the first few lines along with the other imports as always.
We then need to set up I2C and the display which you'll see on line 7. This includes the GPIO pins we're using (GPIO0 and GPIO1) which are the SDA and SCL pins, used for I2C connections. After this line we always have to wait around 1 second otherwise I2C can get all stroppy and fall over!
Next we define the display size (in pixels) and the type of driver chip it uses. Our display uses an SSD1306 driver and is 128x32 pixels, which is reflected on line 13. That's all the setup out of the way.
When we want to display something on the display, we always go through the same process:
- Clear the display
- Define what content we want to show on the display
- Push the content to the display
If we don't clear the display every time, it will write the new content on top of the old content, which makes a big mess of jumbled-up characters. Not good!
Our example shows us clearing the display with display.fill(0), then sending "Hello World!" with display.text("Hello World!",0,0), and finally pushing this to the display with display.show().
We'll cover what the the 0,0 arguments do in the next activity, but for now, copy this over to Thonny and see it for yourself!
# Imports from machine import Pin, I2C from ssd1306 import SSD1306_I2C import time # Set up I2C and the pins we're using for it i2c=I2C(0,sda=Pin(0), scl=Pin(1), freq=400000) # Short delay to stop I2C falling over time.sleep(1) # Define the display and size (128x32) display = SSD1306_I2C(128, 32, i2c) # Clear the display first display.fill(0) # Write a line of text to the display display.text("Hello World!",0,0) # Update the display display.show()
Activity 2: Multiple Lines of Text
Let's go one step further, adding more lines and changing the position of the text, but first let's cover those arguments...
You'll notice there are some arguments (0,0) after the text we push to the display:
The first argument determines how many pixels across the display the content should start (the x-axis) and the second argument determines how many pixels from the top the content should start (the y-axis) going downwards.
Our display is 128x32 pixels, so we have 128 pixels running across on the x-axis, and 32 pixels top to bottom on the y-axis.
In our example below, we've managed to squeeze three lines of text on by setting each one at a different height, which just takes a bit of trial and error. The first line is at 0, the next line starts at 12 and the final line starts at 24. If we add more lines they won't be fully visible.
We've also moved line 2 over to the right by entering 50 for the first argument, again, just with trial and error.
Give the example below a try, and then have a play with the argument values to see the changes for yourself.
# Imports from machine import Pin, I2C from ssd1306 import SSD1306_I2C import time # Set up I2C and the pins we're using for it i2c=I2C(0,sda=Pin(0), scl=Pin(1), freq=400000) # Short delay to stop I2C falling over time.sleep(1) # Define the display and size (128x32) display = SSD1306_I2C(128, 32, i2c) # Write three lines to the display display.text("Line 1",0,0) display.text("Line 2",50,12) display.text("Line 3",0,24) # Update the display display.show()
Activity 3: The Endless Counter!
Small OLED displays are great for displaying data, and they can refresh quickly enough to show data changes at a very fast rate.
Let's create an endless (well, kind of!) counter to show another way of using these displays. When you come to make your own projects, things like this will be very useful.
We don't use any physical inputs here, just a counter in our code that increases by +1 every loop.
A new problem - our counter is a number (an integer) but our fussy little display will only show text (a string), so we need to convert our counter to a string every time we run the loop.
Converting Integers to Strings
We convert the counter integer to a text string using the following line: display.text((str(counter)),0,24). So instead of entering text in brackets like we did in the previous activity, we use (str(counter) to turn our counter variable into a string for our display.
It's otherwise quite straightforward, adding +1 to our counter at the end of every loop to continually increase it. You can play with the time delay within the while loop to see just how fast it can run:
# Imports from machine import Pin, I2C from ssd1306 import SSD1306_I2C import time # Set up I2C and the pins we're using for it i2c=I2C(0,sda=Pin(0), scl=Pin(1), freq=400000) # Short delay to stop I2C falling over time.sleep(1) # Define the display and size (128x32) display = SSD1306_I2C(128, 32, i2c) counter = 0 # Start our counter at zero while True: # Loop forever display.fill(0) # Clear the display print(counter) # Print the current count # Show the counter on the display # The display library expects strings only # Counter is a number (integer) so we convert it to text (a string) with 'str' display.text("The Endless",0,0) display.text("Counter!",0,12) display.text((str(counter)),0,24) # Update the display display.show() # Short delay time.sleep(0.1) # Add 1 to our counter counter += 1
Activity 4: The Naughty or Nice Game!
We're now going to create a game that decides your festive fate! Let's make a Naughty or Nice game, which will tell you if you're on the good list this year or not (don't worry kids, it's just a bit of fun - we're sure you've all been good this year!).
This project shows 'Naughty' and 'Nice' with a marker that very quickly switches between the two. When you click the button, it stops on whatever option the marker is on at that moment - your aim is to land on 'Nice'.
Our example below uses all of our usual ingredients (imports, display setup, button pins...) and creates a state variable, which we've used before. We use this variable to switch between the two if statements within our while loop. But why do we want to do that?
We want our display to constantly move the > marker between 'Naughty' and 'Nice'. To do that we have two if statements which are triggered by our state variable being either 0 or 1. Each if statement starts by showing the text with the marker at a different position.
If the state is 0, the text pushed to the display shows the marker on 'Naughty'. If the state is 1, the marker is on 'Nice'. Each if statement also changes the state, so that on the next loop, the other if statement will trigger - which makes the marker jump between the two.
Each of the if statements then has a nested if statement inside it which checks if the button is pressed at that very moment. If it is, the code inside it updates the display to confirm which option was selected, followed by a 2 second pause:
# Imports from machine import Pin, I2C from ssd1306 import SSD1306_I2C import time # Set up the button button = Pin(8, Pin.IN, Pin.PULL_DOWN) # Set up I2C and the pins we're using for it i2c=I2C(0,sda=Pin(0), scl=Pin(1), freq=400000) # Short delay to stop I2C falling over time.sleep(1) # Define the display and size (128x32) display = SSD1306_I2C(128, 32, i2c) # Create variables state = 0 while True: # Loop forever time.sleep(0.1) # Delay before changing marker if state == 0: # If state is 0 display.fill(0) # Clear the display display.text("Naughty or Nice?",0,0) # Line 1 display.text(">Naughty Nice",0,24) # Line 3 display.show() # Update the display state = 1 # State change if button.value() == 1: # If button clicked display.fill(0) # Clear the display display.text("Oh no!",0,0) # Line 1 display.text(">Naughty Nice",0,24) # Line 3 display.show() # Update the display time.sleep(2) # Delay elif state == 1: # If state is 1 display.fill(0) # Clear the display display.text("Naughty or Nice?",0,0) # Line 1 display.text(" Naughty >Nice",0,24) # Line 3 display.show() # Update the display state = 0 # State change if button.value() == 1: # If button clicked display.fill(0) # Clear the display display.text("Yay!",0,0) # Line 1 display.text(" Naughty >Nice",0,24) # Line 3 display.show() # Update the display time.sleep(2) # Delay
Activity 5: Show me the light!
Showing sensor readings on your OLED is quite possibly one of the most useful and fun ways to put the display to good use.
We're going to run an example where we continually check the light sensor reading and push this to the display - it can even show the % symbol!
The example below includes similar code from the light sensor box (day #6) and the same OLED display code and approach we've taken above, so most of this should make sense if you've been following along.
The same initial OLED imports and setup code is present, and we've added the light sensor pin setup back in, linking it to GPIO26 along with the required ADC import.
We then start a while loop which takes a reading from the sensor on line 25. Now, this line does a lot of things at once so let's break it down:
- It creates a variable called 'light'
- It takes a sensor reading
- It turns the reading into a percentage
- It rounds the reading percentage to one decimal place.
We did something similar on day #6 but this time we're doing everything in a single line of code. We've avoided this so far as it can be a little overwhelming, but we're on day #11 so we think you'll be OK!
We then push this data to our display. We use the first line of the display to show some simple text, and the second line to show the reading. Just like the previous examples, we have to convert our 'light' variable to a string to allow the display to use it. We also add the % symbol to the end.
Copy the code below over to Thonny and give it a go:
# Imports from machine import Pin, I2C, ADC from ssd1306 import SSD1306_I2C import time # Set up I2C and the pins we're using for it i2c=I2C(0,sda=Pin(0), scl=Pin(1), freq=400000) # Short delay to stop I2C falling over time.sleep(1) # Define the display and size (128x32) display = SSD1306_I2C(128, 32, i2c) # Define pin for our sensor lightsensor = ADC(Pin(26)) while True: # Delay time.sleep(0.5) # Read sensor value, turn it into a percentage, round to 1 decimal # Store this in a variable called 'light' light = round((lightsensor.read_u16())/65535*100,1) # Print the light reading percentage print(light) # Clear the display display.fill(0) # Write two lines to the display # The line turns our light variable into a string, and adds '%' to the end display.text("Light level:",0,0) display.text((str(light) + "%"),0,12) # Update the display display.show()
Day #11 Complete!
Aren't OLED displays just bags of fun? We've only just scratched the surface as well! There are lots of clever ways you can use these kinds of displays with graphics, fonts and other tricks, but we wanted to keep our activities relatively simple for beginners.
You'll find a ton of resources and other examples on the internet that you can use - we even have our own graphics tutorial which can apply to these displays too (with a few tweaks to the code).
Let's recap, what did we learn on day #11? Today we:
- Learnt how to wire an OLED to our Raspberry Pi Pico
- Introduced the I2C communication protocol
- Leant how to install packages in Thonny
- Learnt how to code an I2C OLED display, including:
- How to write text to the display
- How to write multiple lines of text
- How to alter the position of text
- How to display sensor data on OLED displays
- A few little tricks like markers!
- Re-used knowledge and components from previous boxes, such as:
- State variables
- Converting integers to strings
- Nested if statements
- Light sensors
- ...and more!
Ok folks, we're nearly at the end of our twelve days :( You can remove the circuit from today as tomorrow is an entirely different kind of component - we can't wait! See you tomorrow...
We used Fritzing to create the breadboard wiring diagram images for this page.