Community project

Car Multi-Use Display

Raspberry Pi Pico
Photo of Car Multi-Use Display
Generated with AI

Anthony Drakes

Published October 20, 2026

This car display system transforms a Raspberry Pi Pico into a multi-function dashboard that shows GPS position and speed, precise time from an RTC module, and environmental data from a BME280 sensor. The 3.5-inch ILI9488 TFT screen provides a bright, readable interface powered safely from the vehicle's 12 V accessory circuit through a regulated 5 V supply.

The guide includes a complete wiring diagram showing how to connect the display, GPS receiver, real-time clock, and climate sensor to the Pico, a full parts list with automotive-grade power specifications, and the complete firmware with color-coded UI elements. Assembly steps cover creating a safe 5 V power rail from the car's electrical system, bench testing all components before installation, and integrating the display into your vehicle.

Wiring diagram

Gather all the parts

QtyComponent
1

ILI9488 3.5 inch SPI TFT Display

3.5 inch 480x320 TFT LCD module using the ILI9488 controller over 4-wire SPI. It uses MOSI, SCK, CS, DC, and RESET for the display, with optional MISO/readback, PWM backlight control, and XPT2046 resistive touch pins on touch variants. Common ESP32 projects use TFT_eSPI with the ILI9488 driver selected.

1

u-blox NEO-M8N GPS Module

u-blox NEO-M8N GNSS receiver module. Outputs NMEA data over UART by default and may expose PPS on some breakouts. Use 3.3V logic for MCU UART pins.

1

Adafruit DS3231 Precision RTC Breakout

DS3231 precision real-time clock breakout with integrated TCXO and crystal for accurate timekeeping over I2C.

1

BME280

Bosch BME280 environmental sensor on the exact Adafruit 2652 breakout. Supports I2C via SCK/SCL and SDI/SDA, optional SDO address select, and SPI pins when needed.

1

Fused 12 V accessory power tap

An add-a-fuse lead that safely takes switched power from the car's fuse box for the dashboard.

1

Automotive 12 V to 5 V USB buck converter

A car-rated step-down converter that makes stable 5 V power for the Pico dashboard.

Assemble it in 7 steps

1. Keep the Android unit as its own system

Leave the Android head unit, its Nissan harness, CAN-bus box, camera lead, GPS antenna, and RCA loom connected only to one another. Mount the new Pico dashboard in a separate location where it does not block airbags, vents, or the driver’s view.

  • The original 9-inch unit remains responsible for music, CarPlay, Android Auto, camera, and vehicle-radio features.
  • Do not connect any Pico wire to the Android unit’s CAN-bus box or unknown radio-harness wire; a wrong connection can damage either unit or interfere with vehicle electronics.

2. Make the safe 5 V car supply

With the car switched off, fit fuse_tap_1 into a verified switched accessory-fuse position, using the correct add-a-fuse orientation and a small fuse sized for the dashboard converter. Connect its ground lead to clean bare vehicle metal. Connect fuse_tap_1 VOUT to converter_1 IN+ (fused 12 V power), and converter_1 IN- to fuse_tap_1 GND (ground).

  • Use a car-rated converter with a stable 5 V USB output. A switched accessory position turns the dashboard off when the vehicle is off.
  • Never connect 12 V directly to the Pico or display — 12 V can permanently damage them.
  • Disconnect the vehicle battery negative terminal before working at the fuse box, and do not use an airbag, braking, steering, or engine-control fuse position.

3. Power the Pico safely

Connect converter_1 OUT+ to the Pico W USB power input or its 5 V supply pin (power), and connect converter_1 OUT- to a Pico GND pin (ground). Check with a multimeter that the converter output is close to 5 V before plugging the Pico in.

  • Use a short, good-quality USB lead if your converter has a USB socket; it is the simplest protected way to power the Pico.
  • Make sure the converter positive and negative outputs are not swapped — swapped power can damage the Pico and every connected module.

4. Wire the dashboard screen

Connect display_1 VCC to Pico 3V3 (power), GND to Pico GND (ground), CS to GP17 (screen select), SCK to GP18 (screen clock), MOSI to GP19 (screen data), DC to GP20 (screen command/data), and RESET to GP21 (screen reset). Leave the optional MISO, touch, and backlight-control pins unconnected for this first build.

  • Use short wires for the screen clock and data connections; this helps the picture stay stable in a vehicle.
  • Only use a 3.3 V-compatible ILI9488 screen module. A display board that drives its data pins at 5 V can damage the Pico’s 3.3 V pins.

5. Wire the GPS receiver

Place gps_1 where its small antenna can face the sky, such as high on the dashboard behind non-metal trim. Connect VCC to Pico 3V3 (power), GND to Pico GND (ground), and TX to Pico GP1 (GPS data). Leave RX and PPS unconnected.

  • The first satellite lock can take several minutes outdoors; metal, tinted windshields, and garages reduce reception.
  • Do not reuse the Android unit’s GPS antenna unless its connector and electrical requirements are proven to match the NEO-M8N module.

6. Wire the clock and climate sensor

Connect rtc_1 VIN to Pico 3V3 (power), GND to Pico GND (ground), SDA to GP4 (shared data), and SCL to GP5 (shared clock). Connect climate_1 VCC to Pico 3V3 (power), GND to Pico GND (ground), SDA to GP4 (shared data), and SCL to GP5 (shared clock). Fit the RTC’s backup coin cell if your board has a holder.

  • Keep the BME280 away from direct sun, heater ducts, and the Pico regulator so it measures cabin air rather than hot electronics.
  • Make sure VCC and GND are not swapped on either small board — swapped power can damage the sensor or clock module.

7. Test on the bench before fitting it in the car

Power the dashboard from the regulated 5 V output while the modules are still easy to reach. Confirm that the screen shows the dashboard, then take the GPS antenna outdoors and wait for the speed and position area to change from searching to fixed.

  • Once GPS time is received, the clock board keeps time while the car is parked, so the dashboard remains useful without any internet connection.
  • Secure every module and wire so vibration cannot pull a wire loose or let a bare connection touch vehicle metal.

Review all connections

1. Connections between "display_1" and "Raspberry Pi Pico"

Functiondisplay_1Raspberry Pi Pico
powerVCC3V3
groundGNDGND
spiCSGPIO 17
spiSCKGPIO 18
spiMOSIGPIO 19
digitalDCGPIO 20
digitalRESETGPIO 21

2. Connections between "gps_1" and "Raspberry Pi Pico"

Functiongps_1Raspberry Pi Pico
powerVCC3V3
groundGNDGND
uartTXGPIO 1

3. Connections between "rtc_1" and "Raspberry Pi Pico"

Functionrtc_1Raspberry Pi Pico
powerVIN3V3
groundGNDGND
i2cSDAGPIO 4
i2cSCLGPIO 5

4. Connections between "climate_1" and "Raspberry Pi Pico"

Functionclimate_1Raspberry Pi Pico
powerVCC3V3
groundGNDGND
i2cSDAGPIO 4
i2cSCLGPIO 5

5. Connections between "fuse_tap_1" and "Raspberry Pi Pico"

Functionfuse_tap_1Raspberry Pi Pico
powerVIN → verified switched 12 V accessory fuse positionEXT
powerVOUT → Automotive 12 V to 5 V USB buck converter IN+EXT
groundGND → clean vehicle chassis groundEXT

6. Connections between "converter_1" and "Raspberry Pi Pico"

Functionconverter_1Raspberry Pi Pico
groundIN- → Fused 12 V accessory power tap GNDEXT
powerOUT+5V
groundOUT-GND

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include <TinyGPS++.h>
#include <RTClib.h>
#include <Adafruit_BME280.h>
#include <Arduino_GFX_Library.h>

constexpr int TFT_CS_PIN = 17;
constexpr int TFT_SCK_PIN = 18;
constexpr int TFT_MOSI_PIN = 19;
constexpr int TFT_DC_PIN = 20;
constexpr int TFT_RST_PIN = 21;
constexpr int GPS_RX_PIN = 1;
constexpr int I2C_SDA_PIN = 4;
constexpr int I2C_SCL_PIN = 5;

constexpr uint16_t COLOR_BG = 0x0841;
constexpr uint16_t COLOR_PANEL = 0x18C3;
constexpr uint16_t COLOR_TEXT = 0xFFFF;
constexpr uint16_t COLOR_MUTED = 0xBDF7;
constexpr uint16_t COLOR_ACCENT = 0x07FF;
constexpr uint16_t COLOR_GOOD = 0x07E0;
constexpr uint16_t COLOR_WARN = 0xFD20;

Arduino_DataBus *displayBus = new Arduino_HWSPI(TFT_DC_PIN, TFT_CS_PIN, &SPI, true);
Arduino_GFX *display = new Arduino_ILI9488(displayBus, TFT_RST_PIN, 1, true);
TinyGPSPlus gps;
RTC_DS3231 rtc;
Adafruit_BME280 bme;

bool rtcFound = false;
bool bmeFound = false;
bool rtcSyncedFromGps = false;
unsigned long lastRefreshMs = 0;
unsigned long lastSensorMs = 0;
float temperatureC = NAN;
float humidity = NAN;
float pressureHpa = NAN;
String lastSpeed;
String lastTime;
String lastPosition;
String lastClimate;
String lastFix;

void drawLabel(const char *label, int16_t x, int16_t y) {
  display->setTextSize(1);
  display->setTextColor(COLOR_MUTED);
  display->setCursor(x, y);
  display->print(label);
}

void drawValue(const String &value, int16_t x, int16_t y, uint8_t size, uint16_t color) {
  display->setTextSize(size);
  display->setTextColor(color);
  display->setCursor(x, y);
  display->print(value);
}

void drawFrame() {
  display->fillScreen(COLOR_BG);
  display->fillRect(8, 8, 464, 58, COLOR_PANEL);
  display->fillRect(8, 76, 224, 236, COLOR_PANEL);
  display->fillRect(240, 76, 232, 112, COLOR_PANEL);
  display->fillRect(240, 196, 232, 116, COLOR_PANEL);
  drawLabel("OFFLINE CAR DASHBOARD", 20, 18);
  drawLabel("TIME (UTC)", 20, 39);
  drawLabel("GPS SPEED", 22, 89);
  drawLabel("GPS POSITION", 252, 89);
  drawLabel("CABIN / BAROMETER", 252, 209);
}

String twoDigits(int value) {
  return value < 10 ? "0" + String(value) : String(value);
}

void updateCell(String &previous, const String &next, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t size, uint16_t color) {
  if (previous == next) return;
  display->fillRect(x, y, w, h, COLOR_PANEL);
  drawValue(next, x, y, size, color);
  previous = next;
}

void readGps() {
  while (Serial1.available() > 0) {
    gps.encode(Serial1.read());
  }
}

String clockText() {
  if (gps.time.isValid() && gps.date.isValid()) {
    return twoDigits(gps.time.hour()) + ":" + twoDigits(gps.time.minute()) + ":" + twoDigits(gps.time.second()) + "  " + twoDigits(gps.date.day()) + "/" + twoDigits(gps.date.month()) + "/" + String(gps.date.year());
  }
  if (rtcFound) {
    DateTime now = rtc.now();
    return twoDigits(now.hour()) + ":" + twoDigits(now.minute()) + ":" + twoDigits(now.second()) + "  " + twoDigits(now.day()) + "/" + twoDigits(now.month()) + "/" + String(now.year());
  }
  return "WAITING FOR TIME";
}

void refreshDashboard() {
  String speed = "NO FIX";
  String position = "Waiting for satellites";
  String fix = "GPS: searching";
  if (gps.location.isValid()) {
    speed = String(gps.speed.kmph(), 0) + " km/h";
    position = String(gps.location.lat(), 5) + ", " + String(gps.location.lng(), 5);
    fix = "GPS: fixed  " + String(gps.satellites.isValid() ? gps.satellites.value() : 0) + " sats";
  }
  if (!rtcSyncedFromGps && rtcFound && gps.date.isValid() && gps.time.isValid()) {
    rtc.adjust(DateTime(gps.date.year(), gps.date.month(), gps.date.day(), gps.time.hour(), gps.time.minute(), gps.time.second()));
    rtcSyncedFromGps = true;
  }
  String climate = "Sensor not found";
  if (bmeFound && !isnan(temperatureC)) {
    climate = String(temperatureC, 1) + " C   " + String(humidity, 0) + " %\n" + String(pressureHpa, 0) + " hPa";
  }
  updateCell(lastTime, clockText(), 118, 34, 340, 26, 2, COLOR_TEXT);
  updateCell(lastSpeed, speed, 24, 120, 200, 56, 4, gps.location.isValid() ? COLOR_GOOD : COLOR_WARN);
  updateCell(lastFix, fix, 24, 190, 200, 24, 1, COLOR_MUTED);
  updateCell(lastPosition, position, 252, 117, 212, 52, 2, COLOR_ACCENT);
  if (lastClimate != climate) {
    display->fillRect(252, 236, 212, 65, COLOR_PANEL);
    display->setTextSize(2);
    display->setTextColor(bmeFound ? COLOR_TEXT : COLOR_WARN);
    display->setCursor(252, 236);
    display->print(climate);
    lastClimate = climate;
  }
}

void setup() {
  Wire.setSDA(I2C_SDA_PIN);
  Wire.setSCL(I2C_SCL_PIN);
  Wire.begin();
  Serial1.setRX(GPS_RX_PIN);
  Serial1.begin(9600);
  display->begin();
  display->setRotation(1);
  drawFrame();
  rtcFound = rtc.begin();
  bmeFound = bme.begin(0x77, &Wire);
  refreshDashboard();
}

void loop() {
  readGps();
  unsigned long now = millis();
  if (bmeFound && now - lastSensorMs >= 2000) {
    temperatureC = bme.readTemperature();
    humidity = bme.readHumidity();
    pressureHpa = bme.readPressure() / 100.0F;
    lastSensorMs = now;
  }
  if (now - lastRefreshMs >= 500) {
    refreshDashboard();
    lastRefreshMs = now;
  }
  delay(10);
}

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