Community project

GPS Speedometer

ESP32
Photo of GPS Speedometer
Generated with AI

Utku Kahraman

Published August 29, 2026

This GPS speedometer reads real-time velocity data from a NEO-6M GPS module and displays it on a TFT screen. The ESP32 processes satellite signals to calculate speed and tracks the maximum velocity reached during operation.

The guide includes a complete wiring diagram showing how to connect the GPS module's power and serial data lines to the ESP32, a full parts list, and ready-to-upload firmware that handles GPS parsing, display rendering, and speed calculations. Assembly takes just a few minutes, and once powered up, the device will acquire a satellite lock and begin showing live speed readings.

Wiring diagram

Wiring diagram for GPS Speedometer

Gather all the parts

QtyComponent
1

NEO-6M GPS Module

NEO-6M

u-blox NEO-6M based GPS receiver module. Outputs NMEA sentences (GGA, RMC, etc.) over UART at 9600 baud by default. Provides latitude, longitude, altitude, speed, and time. The NEO-6M module itself is a 3.3V-class device; many GY-NEO6MV2 breakout boards accept 5V on their VCC header through an onboard regulator, but UART I/O remains 3.3V-domain and must not be driven above 3.6V. Features an on-board patch antenna footprint and an SMA/IPEX connector for external active antenna (preferred for faster lock acquisition). Supply current ~45 mA in acquisition, ~11 mA in tracking.

Assemble it in 4 steps

1. Place the GPS receiver

Put the NEO-6M GPS module where its small square antenna faces upward and is not covered by metal. For the first test, place it near a window or outdoors so it can see the sky.

  • The first satellite lock can take several minutes, especially after the module has been stored without power.
  • Do not put the antenna directly under the ESP32 board or a metal case; blocked sky view prevents a speed reading.

2. Connect GPS power

With the ESP32 unplugged, connect the GPS VCC pin to the board 3V3 pin (power). Connect GPS GND to a board GND pin (ground).

  • Use short jumper wires and make sure the labels on the GPS module match the labels you connect.
  • Do not connect GPS VCC to 5V unless your exact GPS breakout is clearly marked as accepting 5V; the design uses the safe 3.3V connection. Swapped VCC and GND can damage the GPS module.

3. Connect the two data wires

Connect GPS TX to ESP32 GPIO3 / RX0 (data). Connect GPS RX to ESP32 GPIO1 / TX0 (data). Leave the GPS PPS pin unconnected.

  • TX and RX cross over: the GPS pin marked TX goes to the board pin marked RX0, and GPS RX goes to TX0.
  • Keep these two wires clear of VCC and GND. A loose TX wire will leave the display showing SEARCHING.

4. Power up and get a satellite lock

Check every connection once more, then plug the ESP32 board into USB. Put the antenna where it can see open sky; the screen changes from SEARCHING to LOCKED when the GPS receiver has enough satellites.

  • Walk or ride slowly in an open area after it says LOCKED; the large number should then show your speed in km/h.
  • Do not use the displayed speed as a safety-critical instrument while driving; use the vehicle's approved speedometer and obey local laws.

Review all connections

1. Connections between "gps_neo6m" and "ESP32"

Functiongps_neo6mESP32
powerVCC3V3
groundGNDGND
uartTXGPIO 3
uartRXGPIO 1

Deploy the firmware

#include <Arduino.h>
#include <TinyGPS++.h>
#include <Arduino_GFX_Library.h>


// Forward declarations
void drawStaticScreen();
void drawSpeed(float speed);
void drawStatus(bool fixed);
void drawTopSpeed();

constexpr int GPS_RX_PIN = 3;  // GPS TX connects here
constexpr int GPS_TX_PIN = 1;  // GPS RX connects here
constexpr uint32_t GPS_BAUD = 9600;

HardwareSerial gpsSerial(2);
TinyGPSPlus gps;

Arduino_ESP32SPI tftBus(2, 15, 14, 13, GFX_NOT_DEFINED);
Arduino_ILI9341 display(&tftBus, GFX_NOT_DEFINED, 0, false, 240, 320);

float shownSpeed = -1.0f;
float topSpeed = 0.0f;
bool shownFix = false;
uint32_t lastDataMs = 0;

void drawStaticScreen() {
  display.fillScreen(BLACK);
  display.setTextColor(CYAN);
  display.setTextSize(2);
  display.setCursor(44, 22);
  display.print("SPEEDOMETER");

  display.drawRoundRect(12, 58, 216, 142, 12, DARKCYAN);
  display.setTextColor(WHITE);
  display.setTextSize(2);
  display.setCursor(91, 168);
  display.print("km/h");

  display.drawFastHLine(20, 205, 200, DARKGREY);
  display.setTextColor(LIGHTGREY);
  display.setTextSize(2);
  display.setCursor(24, 220);
  display.print("TOP:");
  display.setCursor(24, 250);
  display.print("GPS:");

}

void drawSpeed(float speed) {
  display.fillRoundRect(18, 64, 204, 130, 8, BLACK);
  display.setTextColor(speed < 1.0f ? ORANGE : GREEN);
  display.setTextSize(7);
  char speedText[10];
  snprintf(speedText, sizeof(speedText), "%3.0f", speed);
  int16_t x1, y1;
  uint16_t w, h;
  display.getTextBounds(speedText, 0, 0, &x1, &y1, &w, &h);
  display.setCursor((240 - w) / 2, 104);
  display.print(speedText);
}

void drawStatus(bool fixed) {
  display.fillRect(82, 246, 140, 20, BLACK);
  display.setTextSize(2);
  display.setTextColor(fixed ? GREEN : RED);
  display.setCursor(82, 250);
  display.print(fixed ? "LOCKED" : "SEARCHING");
}

void drawTopSpeed() {
  display.fillRect(82, 216, 130, 22, BLACK);
  display.setTextColor(YELLOW);
  display.setTextSize(2);
  display.setCursor(82, 220);
  display.printf("%.1f km/h", topSpeed);
}

void setup() {
  display.begin();
  display.setRotation(0);
  drawStaticScreen();
  drawSpeed(0.0f);
  drawTopSpeed();
  drawStatus(false);
  gpsSerial.begin(GPS_BAUD, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN);
}

void loop() {
  while (gpsSerial.available() > 0) {
    gps.encode(gpsSerial.read());
  }

  const bool hasRecentFix = gps.location.isValid() && gps.location.age() < 3000;
  if (hasRecentFix != shownFix) {
    shownFix = hasRecentFix;
    drawStatus(shownFix);
  }

  if (gps.speed.isUpdated()) {
    lastDataMs = millis();
    float speedKmh = gps.speed.kmph();
    if (speedKmh < 0.5f) speedKmh = 0.0f;
    if (speedKmh > topSpeed) {
      topSpeed = speedKmh;
      drawTopSpeed();
    }
    if (fabs(speedKmh - shownSpeed) >= 0.5f) {
      shownSpeed = speedKmh;
      drawSpeed(speedKmh);
    }
  }

  if (millis() - lastDataMs > 3500 && shownSpeed != 0.0f) {
    shownSpeed = 0.0f;
    drawSpeed(0.0f);
  }

  delay(5);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik