How to display a speedometer on a 0.96 inch OLED?
To display a speedometer on a 0.96 inch OLED, you need to drive a 128x64 pixel monochrome display with a microcontroller like an Arduino or ESP32, using I2C communication to render real-time speed data as an analog gauge. The key is to map speed values (e.g., 0–120 km/h) to arc angles on the screen, drawing tick marks, a needle, and text using graphics libraries such as Adafruit_SSD1306 or u8g2. A typical setup involves connecting the 0.96 inch 128x64 i2c oled display to your board: VCC to 3.3V or 5V (check your module), GND to ground, SDA to the I2C data pin (A4 on Arduino Uno, GPIO 21 on ESP32), and SCL to the I2C clock pin (A5 on Uno, GPIO 22 on ESP32). The display runs at 100 kHz or 400 kHz I2C speed, drawing about 20 mA during active use. To create the speedometer, you’ll precompute the arc: for a 120 km/h gauge, each km/h corresponds to roughly 2.4 degrees if you use a 270-degree sweep (from 135° to 405° in polar coordinates). The pixel coordinates for the arc center should be around (64, 56) to leave room for the speed label below. Tick marks every 10 km/h require drawing 13 lines from the arc inward, each 4 pixels long, using Bresenham’s line algorithm. The needle is a line from the center to the arc edge, updated at 30 Hz to avoid flicker. For real-world speed data, you’d feed pulses from a hall effect sensor on a wheel (e.g., 4 pulses per revolution) into a microcontroller interrupt, calculating speed as (pulse count / time interval) * wheel circumference. A 0.66-meter wheel circumference at 60 km/h gives 16.7 Hz pulse frequency. The OLED’s 128x64 resolution limits detail: you can fit about 20 characters in a 6x8 pixel font on one line, so the speed value (e.g., “85 km/h”) takes 6 characters, leaving room for a small “MPH” toggle. Contrast is adjustable via the SSD1306’s internal charge pump, with a default 0x7F register value. Power consumption totals around 30 mA for the display plus 50 mA for the microcontroller, so a 500 mAh LiPo lasts about 10 hours. For accuracy, the I2C bus length should stay under 50 cm to avoid signal degradation, and pull-up resistors (4.7 kΩ) are needed if not on the module. The display’s refresh rate is about 10–15 frames per second when drawing full bitmaps, but for a speedometer, you only update the needle and text, achieving 30+ FPS with double buffering. The SSD1306 controller has 128x64 bits of SRAM, so you can pre-store gauge arcs as bitmaps in PROGMEM to reduce computation. A typical gauge bitmap for a 128x64 screen takes 1 KB (128 * 64 / 8), and you can overlay the needle using XOR drawing to avoid clearing the whole screen. For the arc, you’ll generate 270-degree sweep points using sin/cos tables: for angle θ from 135° to 405°, x = 64 + 40 * cos(θ), y = 56 + 40 * sin(θ), where 40 is the radius in pixels. The tick marks extend from radius 40 to 36, with major ticks (every 30°) at radius 40 to 34. The speed label at the bottom (y = 60) displays the numeric value in 16x32 pixel font, requiring 4 characters max. To handle non-linear speed inputs, you can use a moving average filter over 5 samples to smooth the needle. The I2C address is typically 0x3C or 0x3D, configurable by soldering a resistor on the module. For a bike speedometer, you’d calibrate by measuring wheel diameter: a 26-inch wheel (0.66 m circumference) at 100 rpm gives 6.6 m/s or 23.8 km/h. The OLED’s viewing angle is 160°, but brightness drops at extreme angles. Operating temperature ranges from -30°C to 70°C, so it works outdoors. The display module weighs about 6 grams, making it suitable for handheld or dash-mounted projects. For the gauge design, you can add a red zone (e.g., above 100 km/h) by drawing thicker arcs in that range. The SSD1306 supports page addressing mode, but for speed, use horizontal addressing to write bytes sequentially. A full screen clear takes 1 ms at 400 kHz I2C. To display the speedometer, you’ll initialize the display with ssd1306.begin(SSD1306_SWITCHCAPVCC, 0x3C), then in the loop, read speed, convert to angle, clear the needle area, draw the new needle, and update the text. The needle’s tip is at (64 + 38 * cos(θ), 56 + 38 * sin(θ)), and the pivot is a 3-pixel circle at (64, 56). For aesthetics, use a 1-pixel wide line for the needle, and a 2-pixel wide arc for the gauge border. The speed value font can be from the Adafruit_GFX library, which includes 5x7, 8x8, and 16x32 fonts. For 16x32, each character takes 32 bytes, so “85” uses 64 bytes, fitting in the 1 KB buffer. The gauge arc itself, as a bitmap, takes 128 * 64 / 8 = 1024 bytes, but you only need to store the arc pixels, which are about 270 * 2 = 540 bytes if you compress. However, for simplicity, precompute the arc as a byte array of 1024 bytes and store in PROGMEM. The needle update requires XOR drawing: first XOR the old needle to erase, then XOR the new needle. This avoids clearing the arc. The speed text update can be done by writing directly to the buffer at the bottom row. For real-time performance, use a timer interrupt to sample speed every 100 ms, and update the display every 50 ms. The ESP32’s dual core can handle this: core 0 runs the display update, core 1 handles sensor interrupts. The I2C bus on ESP32 uses GPIO 21 (SDA) and 22 (SCL) by default, with a clock stretch timeout of 1 ms. For the speed sensor, a Hall effect sensor like A3144 gives a digital pulse per magnet pass. With 4 magnets on the wheel, each revolution gives 4 pulses. At 60 km/h with a 0.66 m wheel, the wheel rotates at 25.25 Hz, giving 101 pulses per second. The microcontroller counts pulses in a 100 ms window, so 10 pulses correspond to 10 km/h. The formula: speed (km/h) = (pulse count / 4) * wheel circumference (m) * 3.6 / time (s). For a 100 ms window, speed = (pulses / 4) * 0.66 * 3.6 / 0.1 = pulses * 5.94. So 10 pulses give 59.4 km/h. Calibration requires measuring actual wheel circumference with a tape measure. The OLED’s pixel pitch is 0.21 mm, giving a 26.88 mm x 13.44 mm active area. The 128x64 resolution means each pixel is 0.21 mm square, so the gauge arc of radius 40 pixels is 8.4 mm. This is small, so the speedometer is best viewed from 30–50 cm. For readability, use high contrast: set the display’s contrast register to 0xCF for maximum brightness. The SSD1306 also has a pre-charge period setting (0x22) and VCOMH deselect level (0x40), which can be tweaked for better contrast. The display’s lifetime is about 50,000 hours for the OLED panel, but the driver IC lasts longer. For a vehicle speedometer, you need to handle vibration: mount the display on foam tape. The I2C protocol is robust, but long wires (over 1 m) may need shielded cable. The pull-up resistors on the module are typically 4.7 kΩ, but for longer buses, use 2.2 kΩ. The speedometer code can be written in Arduino IDE with libraries: Wire.h, Adafruit_SSD1306.h, and Adafruit_GFX.h. A typical initialization: display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.clearDisplay(); display.setTextSize(2); display.setTextColor(SSD1306_WHITE); Then in the loop: int speed = readSpeed(); int angle = map(speed, 0, 120, 135, 405); drawNeedle(angle); display.setCursor(40, 56); display.print(speed); display.display(); The drawNeedle function uses line drawing: display.drawLine(64, 56, 64 + 38 * cos(angle * PI / 180), 56 + 38 * sin(angle * PI / 180), SSD1306_WHITE). For the gauge arc, draw a series of arcs: for (int i = 135; i <= 405; i += 5) { int x1 = 64 + 40 * cos(i * PI / 180); int y1 = 56 + 40 * sin(i * PI / 180); int x2 = 64 + 40 * cos((i+5) * PI / 180); int y2 = 56 + 40 * sin((i+5) * PI / 180); display.drawLine(x1, y1, x2, y2, SSD1306_WHITE); } This draws a continuous arc. For tick marks, at every 30 degrees (i.e., every 10 km/h), draw a line from radius 40 to 34. The major ticks at 0, 30, 60, 90, 120 km/h can be thicker (2 pixels). The speed label can be centered: display.setCursor(64 - (digitCount * 8), 56). For a 3-digit speed, use 24 pixels width. The display’s buffer is 1024 bytes, so you can pre-store the gauge background as a bitmap and send it once. For example, create a 128x64 array of bytes, set arc pixels to 1, and use display.drawBitmap(0, 0, gauge_bmp, 128, 64, SSD1306_WHITE). This reduces drawing time. The bitmap can be generated with a Python script using Pillow: from PIL import Image; img = Image.new('1', (128, 64)); draw = ImageDraw.Draw(img); draw.arc([24, 16, 104, 96], 135, 405, fill=1); img.save('gauge.bmp'). Then convert to byte array using a tool like LCD Image Converter. The needle update then only requires XOR drawing the needle line. For the speed value, use a 16x32 font: display.setFont(&FreeSansBold32pt7b); display.setCursor(30, 56); display.print(speed); This font is in the Adafruit_GFX library. The speedometer can also show units: display.setCursor(90, 56); display.print("km/h"); in 8x8 font. For a 0–120 km/h gauge, the arc covers 270 degrees, with 0 km/h at 135° (bottom left) and 120 km/h at 405° (bottom right). The needle rotates clockwise. The angle per km/h is 2.25 degrees. So for 60 km/h, angle = 135 + 60 * 2.25 = 270°, which is straight up. The needle length is 38 pixels, so the tip is at (64 + 38 * cos(270°), 56 + 38 * sin(270°)) = (64, 56 – 38) = (64, 18). The pivot is at (64, 56). The gauge arc has a radius of 40 pixels, so the arc top is at y = 56 – 40 = 16. The display’s top row is y=0, so the arc fits within the screen. The bottom of the screen (y=56 to 63) is used for the speed label. The display’s height is 64 pixels, so y=0 to 63. The gauge center at y=56 leaves 8 pixels for the label. For a 16-pixel tall font, you need y=48 to 63, so adjust center to y=48. Then arc radius = 40, top at y=8, bottom at y=88 (off screen), but the arc only goes from 135° to 405°, so the bottom part is not drawn. Better center at y=40, radius=32, top at y=8, bottom at y=72 (off screen). The label at y=56. This fits. So final gauge center: (64, 40), radius=32. The arc from 135° to 405°: x = 64 + 32 * cos(θ), y = 40 + 32 * sin(θ). The needle length = 30 pixels. The speed label at y=56. The tick marks from radius 32 to 28. This gives a clean look. For the red zone, from 100 to 120 km/h, draw the arc in inverse color (black on white) or thicker. The display’s default color is white on black, but you can invert with display.invertDisplay(true). For the speedometer, use white on black for normal, and invert the red zone arc. Alternatively, draw the red zone with a different pattern: for angles from 360° to 405°, draw a dashed line. The speedometer can also show a digital readout: display.setTextSize(3); display.setCursor(20, 52); display.print(speed); This uses 24x32 pixel characters. For 3 digits, that’s 72 pixels wide, fitting in 128. The units “km/h” in text size 1 (6x8) at (100, 56). The needle update frequency: 20 Hz is smooth enough. To avoid tearing, use double buffering: draw to an off-screen buffer, then display.drawBitmap(). The Adafruit_SSD1306 library has a buffer that you can modify directly: display.clearDisplay(); then set pixels in display.buffer. For example, to set a pixel: display.buffer[x + (y/8)*128] |= 1 << (y%8). This is faster than using drawPixel. For the arc, precompute the pixel coordinates and set them in the buffer once. For the needle, clear the old needle by XORing the same pixels, then set the new needle. The XOR method: for each pixel in the needle line, toggle the bit in the buffer. This requires storing the old needle coordinates. Use an array of 30 points (for 30 pixel needle) and store the previous line. Then XOR the old, XOR the new. The speed text can be written by clearing the text area (e.g., rows 56–63, columns 20–100) and then setting the new text. Use display.setTextSize(3); display.setCursor(20, 56); display.print(speed); This writes to the buffer. Finally, call display.display() to send the buffer to the OLED. The I2C transfer of 1024 bytes at 400 kHz takes about 2.5 ms (1024 * 10 bits / 400000 = 25.6 ms? Actually, each byte is 8 bits plus start/ack, so 10 bits per byte, 10240 bits / 400000 = 25.6 ms. That’s too slow for 30 FPS. But you don’t need to send the whole buffer each time. The display supports page addressing, so you can update only the rows that changed. For the needle, only rows 0 to 56 (since needle is from y=10 to 40) need updating. That’s about 40 rows * 128 bytes = 5120 bits, but you can send only the pages (8 rows each) that contain changes. The needle affects pages 1 to 5 (rows 8 to 47). That’s 5 pages * 128 bytes = 640 bytes, transfer time 640 * 10 / 400000 = 16 ms. Still slow. For 30 FPS, you need 33 ms per frame, so 16 ms is okay. But if you also update the text, add another page. So total ~20 ms per frame, giving 50 FPS. However, the microcontroller’s computation time also adds. Use an ESP32 at 240 MHz to handle this. The Arduino Uno at 16 MHz may struggle. For the Uno, use a lower update rate (10 FPS) and precompute everything. The speedometer can also display a trip distance using a separate counter. The OLED’s small size limits the amount of data, so keep it simple. For a professional look, add a bezel: draw a circle around the gauge using drawCircle(64, 40, 34, WHITE). The gauge arc is inside. The speed values (0, 20, 40, 60, 80, 100, 120) can be printed along the arc using small font (5x7) at the tick marks. For example, at 135° (0 km/h), print “0” at (x, y) offset by 10 pixels from the arc. Use display.setCursor(x-3, y-4); display.print("0"); This requires calculating positions for each label. The label positions: for angle θ, x = 64 + 36 * cos(θ), y = 40 + 36 * sin(θ). Then adjust for text centering. For 0 km/h at 135°, x = 64 + 36 * cos(135°) = 64 – 25.46 = 38.54, y = 40 + 36 * sin(135°) = 40 + 25.46 = 65.46. But y=65 is off screen (max 63), so reduce radius to 30 for labels. Use radius=30: x = 64