Skip to content
Free Express · $149+ Cart 0

How to display a speedometer on a 1.54 inch 128x64 OLED?

How to Display a Speedometer on a 1.54 Inch 128x64 OLED

To display a speedometer on a 1.54 inch 128x64 oled display, you need to combine a microcontroller (like an Arduino or ESP32), a GPS module or a simulated speed signal, and a graphics library that can draw arcs, lines, and text on the OLED. The key is to generate a needle that rotates around a center point, with tick marks and labels representing speed values. The 1.54 inch 128x64 oled display uses the SSD1306 or SH1106 driver, typically communicating over SPI or I2C. For a speedometer, SPI is preferred because it offers faster refresh rates (up to 10 MHz), which is critical for smooth needle animation. The display's resolution is 128 pixels wide by 64 pixels tall, giving you a 16:8 aspect ratio, so you’ll need to design the speedometer as a half-circle or a quadrant to fit within the vertical space. A typical speedometer arc spans from 0 to 120 km/h, with major ticks every 20 km/h and minor ticks every 10 km/h. The center of the arc should be placed at pixel coordinates (64, 60) to leave room for the needle pivot and the speed value text below the arc. The arc radius can be set to 50 pixels, which fits within the 64-pixel height, leaving a 10-pixel margin at the top and bottom. The needle itself is a line from the center to the arc edge, with a length of 48 pixels to avoid overlapping the tick marks. The needle angle is calculated as: angle = (speed / max_speed) * 180 degrees, where max_speed is 120 km/h. For example, at 60 km/h, the angle is 90 degrees, pointing straight up. You’ll need to convert this angle to radians for the math functions: angle_rad = (angle - 90) * (PI / 180) because the OLED coordinate system has 0 degrees pointing to the right. The needle endpoint coordinates are: x = 64 + 48 * cos(angle_rad), y = 60 + 48 * sin(angle_rad). To draw the arc, you can use the Adafruit_GFX library’s drawCircle() function but only for the lower half, or you can write a custom function that draws pixels along the arc using Bresenham’s algorithm optimized for 128x64 resolution. The tick marks are short lines perpendicular to the arc. For each major tick at 20 km/h intervals, the angle is 0, 30, 60, 90, 120, 150, and 180 degrees. The inner radius for ticks is 44 pixels, and the outer radius is 50 pixels. So the inner endpoint is: x_inner = 64 + 44 * cos(angle_rad), y_inner = 60 + 44 * sin(angle_rad), and the outer endpoint is: x_outer = 64 + 50 * cos(angle_rad), y_outer = 60 + 50 * sin(angle_rad). For minor ticks, use a radius of 47 to 50 pixels. The speed value text is displayed below the arc, typically at coordinates (64, 55) with center alignment. Use a font size of 2 (16 pixels tall) for the numeric value, and a smaller font for the unit (km/h). The refresh rate of the OLED is around 30 frames per second when using SPI, but the Arduino’s processing speed limits the update rate. On an Arduino Uno (16 MHz), a full redraw of the speedometer takes about 15 milliseconds, which gives you 66 frames per second theoretically, but the OLED’s write speed caps it at 30 fps. On an ESP32 (240 MHz), you can achieve 60 fps with double buffering. The memory buffer for the OLED is 128 * 64 / 8 = 1024 bytes. If you use a framebuffer, you need 1024 bytes of RAM, which is fine for the Uno (2 KB SRAM) but tight for the ESP8266 (80 KB). For the speed input, you can use a GPS module like the NEO-6M, which outputs NMEA sentences at 1 Hz. Parse the $GPGGA or $GPRMC sentence to extract the speed in knots, then convert to km/h (1 knot = 1.852 km/h). The GPS speed update rate is 1 Hz, so the needle will jump every second. To smooth the animation, you can implement a moving average filter over 5 samples: filtered_speed = (filtered_speed * 3 + raw_speed * 2) / 5. This reduces jitter but adds a 0.5-second delay. Alternatively, you can simulate a speed signal using a potentiometer connected to an analog input (0-1023), mapping it to 0-120 km/h. This is useful for testing. The potentiometer’s analog value is read with analogRead() and mapped: speed = map(analogValue, 0, 1023, 0, 120). The needle position is updated only when the speed changes by more than 1 km/h to avoid unnecessary redraws. The display’s contrast can be set via the SSD1306 command 0x81 with a value between 0 and 255. For outdoor visibility, set contrast to 200 (bright). The OLED’s power consumption is about 20 mA at full brightness, so a 500 mAh battery can run it for 25 hours continuously. The SPI wiring is: CS to pin 10, DC to pin 9, RES to pin 8, MOSI to pin 11, SCK to pin 13 on the Arduino Uno. For the ESP32, use pins 5 (CS), 4 (DC), 2 (RES), 23 (MOSI), 18 (SCK). The library initialization code is: Adafruit_SSD1306 display(128, 64, &SPI, DC, RES, CS); then display.begin(SSD1306_SWITCHCAPVCC, 0x3C). The speedometer design can be enhanced with a digital readout in the center of the arc, showing the speed in large font. The digital readout uses the remaining 20 pixels of vertical space below the arc. The arc itself is drawn using a for loop that iterates from 0 to 180 degrees in 1-degree steps, calculating the pixel position and setting it with display.drawPixel(). This is slower than using drawCircle() but gives you control over the arc’s thickness. For a thicker arc, draw 3 concentric arcs with radii 48, 49, and 50 pixels. The tick labels are drawn using display.setCursor() and display.print(). The font used is the default 5x7 pixel font, but you can use custom fonts like the 8x13 font for larger numbers. The speedometer’s background can be black (default) with white elements, or you can invert colors by calling display.invertDisplay(true). The needle is drawn as a line from the center to the arc edge, with a small circle at the pivot point. The pivot circle has a radius of 3 pixels, filled with white. The needle line color is white, and you can add a red tip by drawing a 2-pixel-wide line at the end. To erase the previous needle, you need to redraw the entire background arc and ticks, or you can use the XOR drawing mode. The Adafruit_GFX library does not support XOR natively, so you must clear the display and redraw everything each frame. This is why double buffering is beneficial: you draw to a buffer, then call display.display() to update the OLED in one shot. The buffer update takes about 5 milliseconds, and the display transfer takes 10 milliseconds at 4 MHz SPI clock. For a 128x64 display, the SPI transfer time is: (128 * 64 / 8) * 8 bits / (4 MHz) = 2.048 milliseconds, but the overhead of the SSD1306 commands adds another 2 milliseconds. So total frame time is around 15 milliseconds, giving 66 fps, but the OLED’s internal refresh rate is 30 fps, so you’re limited by the display hardware. The speedometer can be calibrated by adjusting the mapping of the analog input or GPS speed. For example, if the GPS reports speed in knots, multiply by 1.852 to get km/h. If you use a Hall effect sensor on a bike wheel, you need to measure the wheel circumference (e.g., 2.1 meters for a 700c wheel) and calculate speed as: speed = (pulse_count * circumference * 3.6) / (time_interval * 1000). The pulse count is measured using an interrupt on the Arduino. The speedometer update rate is then determined by the wheel rotation frequency. At 30 km/h, a 700c wheel rotates at 30 / (2.1 * 3.6) = 3.97 Hz, so you get a pulse every 0.25 seconds. This is slower than the GPS, so you might need to interpolate between pulses. The OLED’s viewing angle is 160 degrees, so the speedometer is readable from the side. The display’s operating temperature range is -40 to 85 degrees Celsius, making it suitable for automotive use. The SPI interface uses 4 pins (CS, DC, MOSI, SCK) plus power and ground, totaling 6 wires. The I2C version uses only 2 wires (SDA, SCL) but is slower (400 kHz vs 10 MHz). For speedometer animation, SPI is strongly recommended. The display’s driver IC is the SSD1306, which supports hardware acceleration for horizontal and vertical scrolling, but not for arbitrary shapes. So you must implement the needle rotation in software. The math for the needle involves sine and cosine, which are computed using floating-point arithmetic on the Arduino. This takes about 200 microseconds per calculation. To speed it up, you can precompute a lookup table for sine and cosine values for 0 to 180 degrees in 1-degree steps. The table size is 181 entries * 2 bytes per float = 362 bytes, which fits in the Uno’s flash memory. Use the PROGMEM directive to store it in flash. The needle angle is then looked up directly: sin_val = pgm_read_float(&sin_table[angle]), cos_val = pgm_read_float(&cos_table[angle]). This reduces the calculation time to 10 microseconds. The tick marks can also be precomputed and stored in arrays of x,y coordinates for the inner and outer endpoints. This reduces the drawing time from 100 milliseconds to 5 milliseconds. The speedometer’s visual design can include a color gradient by using the OLED’s pixel density. Since the OLED is monochrome, you can simulate shades by using dithering patterns. For example, a 2x2 checkerboard pattern gives 50% gray. This is useful for the background of the arc. The dithering pattern is applied by setting pixels based on their position modulo 2. The arc’s background can be filled with a 25% pattern (every other pixel) to make the tick marks stand out. The needle can be drawn with anti-aliasing by using sub-pixel rendering. This involves drawing the needle line with varying pixel intensities based on the fractional part of the line’s position. Since the OLED is binary, you can use a 2x2 supersampling technique: render the needle at 2x resolution (256x128) and then downscale to 128x64 by averaging 2x2 blocks. This requires a 2 KB buffer, which is double the standard buffer. This is feasible on an ESP32 but not on an Arduino Uno due to RAM limitations. The speedometer’s accuracy depends on the input source. A GPS module has an accuracy of +/- 0.1 m/s (0.36 km/h) under open sky, but it can degrade to 2.5 m/s in urban canyons. A Hall effect sensor is more accurate (+/- 0.1 km/h) but requires calibration. The display’s pixel resolution limits the readability of the needle. At 60 km/h, the needle angle is 90 degrees, and the tip is at (64, 12). The pixel position is precise to within 1 pixel, which corresponds to 120/128 = 0.94 km/h per pixel at the arc edge. So the speedometer’s resolution is about 1 km/h. The tick marks are spaced 20 km/h apart, which is 20 pixels along the arc. This is adequate for a quick glance. The speed value text is displayed with 2 digits, so it shows 0 to 99 km/h. For speeds above 100 km/h, you need 3 digits, which requires a font size of 1 (5x7 pixels) to fit in the 20-pixel-wide area. The text is centered using display.setCursor(64 - (text_width / 2), 55). The text width is calculated as: text_width = number_of_digits * 5 + (number_of_digits - 1) * 1 (for spacing). For 3 digits, it’s 17 pixels, which fits. The unit “km/h” is placed below the number at coordinates (64, 60) with a font size of 1. The speedometer can be extended to show multiple ranges, like a tachometer for RPM. The same design principles apply, but the arc spans 0 to 8000 RPM with redline at 7000 RPM. The redline is indicated by a red segment on the arc, which is simulated by drawing the arc with a different dithering pattern for the last 1000 RPM. The needle’s color can be inverted for the redline zone by using a XOR pattern. The OLED’s update rate is sufficient for a tachometer because RPM changes faster than vehicle speed. The ESP32 can handle updates at 100 fps, which is smooth for a needle. The power consumption of the ESP32 with the OLED is about 80 mA, so a 2000 mAh battery lasts 25 hours. The display’s module typically comes with a 4-pin or 7-pin interface. The 1.54 inch 128x64 oled display from the link uses a 7-pin SPI interface: GND, VCC, D0 (SCK), D1 (MOSI), RES, DC, CS. The VCC is 3.3V, but the module has a built-in voltage regulator that accepts 3.3V to 5V. The logic level is 3.3V, so if you use a 5V Arduino, you need level shifters on the SPI lines. The OLED’s pixel pitch is 0.28 mm, giving a crisp image. The display’s brightness is 100 cd/m² typical, which is readable in direct sunlight if you use a polarizer. The contrast ratio is 2000:1, so the black background is truly black. The speedometer’s visual design can be improved by adding a shadow effect to the needle. This is done by drawing the needle twice: first at a 1-pixel offset with a dimmed pattern (25% dither), then at the exact position with full brightness. This gives a 3D effect. The shadow is drawn at an offset of (1, 1) pixels. The dithering for the shadow is a 2x2 pattern with only one pixel lit. The needle’s pivot point can be a filled circle with a radius of 4 pixels, and a smaller circle inside with a radius of 2 pixels, inverted. This gives a metallic look. The arc’s background can be filled with a gradient from dark to light by using a dithering pattern that increases in density from the bottom to the top. For example, at the bottom of the arc (0 degrees), use 10% dither; at the top (90 degrees), use 50% dither. This creates a visual depth cue. The dithering pattern is calculated as: dither_level = (angle / 180) * 0.5 + 0.1. The pattern is applied using a 2x2 Bayer matrix: [[0, 2], [3, 1]] scaled to the dither level. The pixel is lit if the dither level is greater than the matrix value. This requires a 2x2 pixel block for each arc pixel, which reduces the effective resolution but improves the visual quality. The speedometer’s code structure is as follows: setup() initializes the display and the input source (GPS or analog). loop() reads the speed, calculates the needle angle, clears the buffer, draws the arc, ticks, labels, needle, and digital readout, then calls display.display(). The drawing functions are optimized for speed. The arc is drawn using a for loop that iterates from 0 to 180 degrees. For each degree, it calculates the pixel position and sets it. This is 181 iterations, each taking about 50 microseconds, totaling 9 milliseconds. The ticks are drawn using precomputed arrays, taking 1 millisecond. The needle is drawn using a line function, taking 0.5 milliseconds. The text is drawn using the library’s built-in functions, taking 2 milliseconds. Total draw time is 12.5 milliseconds, plus the buffer transfer time of 10 milliseconds, giving 22.5 milliseconds per frame. This is 44 fps, which is above the OLED’s 30 fps refresh rate, so the display is the bottleneck. The speedometer can be used in a variety of applications, from a bicycle speedometer to a car dashboard. The display’s small size makes it ideal for embedded systems where space is limited. The 1.54 inch 128x64 oled display is a common choice for such projects due to its low cost (around $10) and ease of use. The SPI interface allows for daisy-chaining multiple displays, but for a single speedometer, it’s straightforward. The speedometer’s firmware can be updated over-the-air if you use an ESP32 with WiFi. This allows you to change the speedometer’s design without physical access. The display’s driver IC supports partial updates, which can be used to update only the needle area, reducing the buffer transfer time. Partial updates are enabled by setting the display’s window coordinates. For the needle, the bounding box is a 10x10 pixel area around the needle tip. This reduces the transfer time to 0.2 milliseconds, but the complexity of calculating the bounding box offsets the gain. In practice, full redraws are simpler and fast enough. The speedometer’s accuracy can be verified by comparing it to a GPS speedometer app on a phone. The GPS module’s update rate is 1 Hz