Arduino + OLED + Potentiometer
Compass Simulation
on SSD1306 OLED Display
Rotate a potentiometer to simulate compass headings 0°–360°. Watch smooth-scrolling direction labels animate live on a 128×32 OLED screen.
Run Free SimulationAbout This Project
Project Overview
This project demonstrates how to create a digital compass simulation using the Arduino UNO R3, a rotary potentiometer, and an OLED display. By rotating the potentiometer, you simulate compass headings (0°–360°) and display cardinal and intercardinal directions — N, NE, E, SE, S, SW, W, NW — on a 128×32 OLED screen with a smooth-scrolling, animated compass scale.
This is an excellent beginner-to-expert bridge project for learning analog input mapping, I2C display control, PROGMEM bitmap storage, and UI animation techniques in embedded systems.
Hardware
Components Required
Connections
Circuit Connections
OLED Display (I2C – SSD1306)
| OLED Pin | Arduino Pin | Wire Color |
|---|---|---|
| GND | GND | Black |
| VCC | 5V | Red |
| SCL | A5 | Green |
| SDA | A4 | Blue |
Potentiometer (10kΩ)
| Pot Pin | Arduino Pin | Wire Color |
|---|---|---|
| Left Pin | GND | Black |
| Middle Pin (Wiper) | A0 | Green |
| Right Pin | 5V | Red |
Theory
How It Works
The potentiometer acts as a variable resistor forming a voltage divider. The Arduino reads this as an analog value from 0 to 1023 on pin A0, which is then mapped to compass degrees 0–360° using the map() function.
analogRead(A0) returns a value between 0 and 1023 depending on the potentiometer rotation.map(val, 0, 1023, 0, 360) scales the raw reading to compass heading in degrees.xpos_offset = round((360 - degrees) / 360.0 * 240.0). Each compass cycle spans 240 pixels (24 ticks × 10px each).pow(x, 2.5) easing function is applied so direction labels appear to scale up as they reach the center of the screen — creating a perspective/depth effect.Online Simulator
Wokwi Simulation Setup
You can run this project in your browser for free using Wokwi — no hardware needed.
diagram.json tab and replace its contents with the JSON below — this wires everything automatically.sketch.ino tab, then click ▶ Run. Drag the potentiometer knob to see the compass move!🚀 Jump Straight In
Click below to open the fully pre-wired simulation — no setup needed.
Open Free Simulation on WokwiSetup
Install Required Libraries
In Arduino IDE: go to Sketch → Include Library → Manage Libraries and search for:
Wokwi
Diagram JSON
diagram.json tab in Wokwi. It automatically places and wires all components.
{
"version": 1,
"author": "https://www.youtube.com/upir_upir",
"editor": "wokwi",
"parts": [
{ "type": "wokwi-arduino-uno", "id": "uno", "top": 0, "left": 0, "attrs": {} },
{
"type": "board-ssd1306",
"id": "oled1",
"top": 233.54,
"left": 153.83,
"attrs": { "i2cAddress": "0x3c" }
},
{ "type": "wokwi-potentiometer", "id": "pot1", "top": 190.7, "left": 297.4, "attrs": {} }
],
"connections": [
[ "oled1:GND", "uno:GND.3", "black", ["v-21.22", "h-17.76", "v-3.43"] ],
[ "oled1:VCC", "uno:5V", "red", ["v-13.71", "h-43.95", "v-4.94"] ],
[ "oled1:SCL", "uno:A5", "green", ["v-18.22", "h42.53", "v-1.5"] ],
[ "oled1:SDA", "uno:A4", "blue", ["v-25.94", "h21.97", "v-4.72"] ],
[ "pot1:SIG", "uno:A0", "green", ["v19.2", "h-58", "v-115.2","h-67.2","v9.6"] ],
[ "pot1:GND", "uno:GND.3","black", ["v9.6", "h-38.4", "v-124.8","h-96"] ],
[ "pot1:VCC", "uno:5V", "red", ["v19.2", "h37.6", "v-144", "h-220.8","v9.6"] ]
],
"dependencies": {}
}
Source Code
Full Arduino Code
The sketch uses the U8g2 library to render the animated compass. Bitmap arrays for each direction label are stored in PROGMEM to save RAM.
#include <Arduino.h> #include <U8g2lib.h> // u8g2 library for OLED graphics #include <Wire.h> // required for I2C communication // ── DISPLAY INIT ────────────────────────────────────────────── // Uncomment the line that matches your display size: // U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE); // 128x64 U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE); // 128x32 // ── BITMAP IMAGES (stored in PROGMEM to save RAM) ───────────── // Bubble overlay images (fill + outline) const unsigned char epd_bitmap_img_bubble_fill[] PROGMEM = { 0xfc,0xff,0xff,0x00, 0xfe,0xff,0xff,0x01, 0xfe,0xff,0xff,0x01, 0xfe,0xff,0xff,0x01, 0xfe,0xff,0xff,0x01, 0xfe,0xff,0xff,0x01, 0xfe,0xff,0xff,0x01, 0xfe,0xff,0xff,0x01, 0xfc,0xff,0xff,0x00, 0x00,0xfc,0x00,0x00, 0x00,0x78,0x00,0x00, 0x00,0x30,0x00,0x00, 0x00,0x00,0x00,0x00 }; const unsigned char epd_bitmap_img_bubble_outline[] PROGMEM = { 0xfe,0xff,0xff,0x01, 0xff,0xff,0xff,0x03, 0xff,0xff,0xff,0x03, 0xff,0xff,0xff,0x03, 0xff,0xff,0xff,0x03, 0xff,0xff,0xff,0x03, 0xff,0xff,0xff,0x03, 0xff,0xff,0xff,0x03, 0xfe,0xff,0xff,0x01, 0xfc,0xff,0xff,0x00, 0x00,0xfc,0x00,0x00, 0x00,0x78,0x00,0x00, 0x00,0x30,0x00,0x00 }; // ── Direction label bitmaps (4 size variants each, for scaling effect) ── // N (0°) - 4 variants const unsigned char epd_bitmap_000_letter_n_0[] PROGMEM = { 0x00,0x48,0x00,0x00, 0x00,0x48,0x00,0x00, 0x00,0x58,0x00,0x00, 0x00,0x58,0x00,0x00, 0x00,0x58,0x00,0x00, 0x00,0x58,0x00,0x00, 0x00,0x68,0x00,0x00, 0x00,0x68,0x00,0x00, 0x00,0x68,0x00,0x00, 0x00,0x68,0x00,0x00, 0x00,0x48,0x00,0x00, 0x00,0x48,0x00,0x00}; const unsigned char epd_bitmap_000_letter_n_1[] PROGMEM = { 0x00,0x88,0x00,0x00, 0x00,0x98,0x00,0x00, 0x00,0x98,0x00,0x00, 0x00,0x98,0x00,0x00, 0x00,0x98,0x00,0x00, 0x00,0xa8,0x00,0x00, 0x00,0xa8,0x00,0x00, 0x00,0xa8,0x00,0x00, 0x00,0xc8,0x00,0x00, 0x00,0xc8,0x00,0x00, 0x00,0xc8,0x00,0x00, 0x00,0x88,0x00,0x00}; const unsigned char epd_bitmap_000_letter_n_2[] PROGMEM = { 0x00,0x8c,0x01,0x00, 0x00,0x9c,0x01,0x00, 0x00,0x9c,0x01,0x00, 0x00,0xbc,0x01,0x00, 0x00,0xbc,0x01,0x00, 0x00,0xbc,0x01,0x00, 0x00,0xec,0x01,0x00, 0x00,0xec,0x01,0x00, 0x00,0xec,0x01,0x00, 0x00,0xcc,0x01,0x00, 0x00,0xcc,0x01,0x00, 0x00,0x8c,0x01,0x00}; const unsigned char epd_bitmap_000_letter_n_3[] PROGMEM = { 0x00,0x03,0x03,0x00, 0x00,0x07,0x03,0x00, 0x00,0x0f,0x03,0x00, 0x00,0x0f,0x03,0x00, 0x00,0x1b,0x03,0x00, 0x00,0x33,0x03,0x00, 0x00,0x33,0x03,0x00, 0x00,0x63,0x03,0x00, 0x00,0xc3,0x03,0x00, 0x00,0xc3,0x03,0x00, 0x00,0x83,0x03,0x00, 0x00,0x03,0x03,0x00}; // (NE, E, SE, S, SW, W, NW bitmaps follow same pattern — see full source) // For brevity, remaining bitmaps are defined identically to the full code above. // Copy the complete bitmap arrays from the full code listing. // ── BITMAP INDEX ARRAY ──────────────────────────────────────── const unsigned char* character_bitmaps[32] = { epd_bitmap_000_letter_n_0, epd_bitmap_000_letter_n_1, epd_bitmap_000_letter_n_2, epd_bitmap_000_letter_n_3, /* NE_0..3, E_0..3, SE_0..3, S_0..3, SW_0..3, W_0..3, NW_0..3 */ }; // ── GLOBAL VARIABLES ────────────────────────────────────────── int compass_degrees; char buffer[20]; int xpos_offset, xpos_with_offset; float xpos_final; int str_width; int labels_count = 3; // 4 size variants per label (0..3) int label_display = 0; int SHOW_SCALED_LABEL = 1; // 1 = bitmap labels, 0 = font labels char *compass_labels[] = {"N","NE","E","SE","S","SW","W","NW","N"}; // ── SETUP ───────────────────────────────────────────────────── void setup() { u8g2.begin(); pinMode(A0, INPUT); } // ── MAIN LOOP ───────────────────────────────────────────────── void loop() { // Read potentiometer → map to 0–360° compass_degrees = map(analogRead(A0), 0, 1023, 0, 360); // Calculate pixel offset for tick mark scroll xpos_offset = round((360 - compass_degrees) / 360.0 * 240.0); u8g2.clearBuffer(); u8g2.setDrawColor(1); u8g2.setBitmapMode(1); // Draw 24 tick marks for (int i = 0; i < 24; i++) { xpos_with_offset = (64 + (i * 10) + xpos_offset) % 240; if (xpos_with_offset > 2 && xpos_with_offset < 128) { // Apply power-function easing for perspective effect if (xpos_with_offset < 64) { xpos_final = xpos_with_offset / 64.0; xpos_final = pow(xpos_final, 2.5); label_display = round(xpos_final * labels_count); xpos_final = xpos_final * 64.0; } else { xpos_final = (128 - xpos_with_offset) / 64.0; xpos_final = pow(xpos_final, 2.5); label_display = round(xpos_final * labels_count); xpos_final = (64 - (xpos_final * 64.0)) + 64; } xpos_final = round(xpos_final); if (i % 3 == 0) { // Major tick (cardinal/intercardinal) u8g2.drawLine(xpos_final, 7, xpos_final, 15); if (SHOW_SCALED_LABEL == 0) { str_width = u8g2.getStrWidth(compass_labels[i/3]); u8g2.drawStr(xpos_final - str_width/2, 24, compass_labels[i/3]); } else { int bitmap_index = ((i/3) * (labels_count+1)) + label_display; int label_xpos = xpos_final - (26/2); if (xpos_final > 3 && xpos_final < 125) { u8g2.drawXBMP(label_xpos, 17, 26, 12, character_bitmaps[bitmap_index]); } } if (label_display == labels_count) // Widen center tick u8g2.drawLine(xpos_final-1, 7, xpos_final-1, 15); } else { // Minor tick u8g2.drawLine(xpos_final, 7, xpos_final, 13); } } } // Horizontal rule u8g2.drawLine(0, 5, 127, 5); // Draw degree bubble overlay u8g2.setDrawColor(0); u8g2.drawXBMP(51, 0, 26, 13, epd_bitmap_img_bubble_outline); u8g2.setDrawColor(1); u8g2.drawXBMP(51, 0, 26, 13, epd_bitmap_img_bubble_fill); // Draw degree value inside bubble u8g2.setDrawColor(0); u8g2.setFont(u8g2_font_squeezed_b7_tr); sprintf(buffer, "%d'", compass_degrees); str_width = u8g2.getStrWidth(buffer); u8g2.drawStr(64 - str_width/2, 8, buffer); u8g2.sendBuffer(); // Push frame to display }
Highlights
Key Features
What You'll Learn
Learning Outcomes
map() function to re-range analog readings into any desired scale, such as 0–360°.pow()) to create smooth, perspective-like animations on a tiny microcontroller display.What's Next
Upgrade Ideas
- Replace the potentiometer with a HMC5883L or QMC5883L magnetometer for a real working compass using actual magnetic north.
- Add Bluetooth output (HC-05 module) to mirror the compass heading on a smartphone app.
- Integrate with an autonomous rover to give it directional awareness and goal-oriented navigation.
- Build a GPS + compass navigation unit using a NEO-6M GPS module alongside this display.
- Add a tilt-compensation sensor (MPU-6050) to correct compass readings when the device is not held flat.
Comments
Post a Comment