GPS I²C Bridge on ATTiny85 (part 1)

Phase 1: Prototype & Test on Arduino

  1. Hardware Setup
    – GPS module wired to Arduino’s SoftwareSerial pins (e.g. D3=RX, D4=TX).
    – USB-serial connection from Arduino to PC.
  2. Load & Verify Sketch
    – Upload the existing sketch that parses RMC/GGA/GSV and emits JSON at 115 200 baud.
    – Open the Serial Monitor to confirm well-formed JSON lines every 10 s.
  3. Test Web Client
    – Serve the HTML/JS page locally or open it in a Chromium browser (with Web Serial API).
    – Click “Connect to Arduino,” verify map, sky-view and info panel update correctly.
  4. Stability Checks
    – Log for several minutes, ensure no parser crashes or buffer overruns.
    – Confirm JSON timing, sequence of fields, and client responsiveness under different GPS conditions.

Phase 2: Port Core Parsing to ATtiny85

  1. Toolchain & Bootloader
    – Install ATtiny85 support in your Arduino IDE (e.g. “ATtinyCore”).
    – Burn a suitable bootloader (e.g. Micronucleus or Optiboot-Tiny).
  2. Reduce Sketch to Essentials
    – Remove Serial and JSON-output code.
    – Replace SoftwareSerial (requires ~2 kB flash) with one-pin USI-based UART RX only, or TinySoftwareSerial.
    – Keep only the minimal parsing routines for RMC, GGA, GSV.
  3. Memory & Flash Optimization
    – Move all constant strings ("$GPRMC,", format strings) into PROGMEM.
    – Use integer arithmetic where possible (e.g. fixed-point for lat/lon).
    – Limit maximum satellites (e.g. 12 instead of 32) to shrink RAM usage.
  4. Verify on ATtiny
    – Temporarily re-enable a simple Serial-to-SoftwareSerial bridge on Arduino as USB-serial proxy.
    – Connect GPS → ATtiny UART RX, ATtiny’s debug TX → Arduino RX.
    – Confirm that your parsing still produces correct intermediate values (e.g. blink LEDs or toggle a pin for sanity).

Phase 3: Add I²C Slave Interface

  1. Define Register Map
    – Decide fixed addresses for timestamp, lat×10⁷, lon×10⁷, alt×100, HDOP×100, total_sats, sat blocks.
  2. Implement USI-TWI Slave
    – Use AVR’s USI hardware in slave mode at address 0x42 (or your choice).
    – On USI_OVF_vect, handle master’s read requests by returning the requested register bytes.
  3. Data-Ready Notification (Optional)
    – Reserve an ATtiny GPIO (e.g. PB2) as an open-drain “DATA_RDY” output.
    – In parsing code, when a fresh fix is fully assembled, pulse DATA_RDY low for a few µs.
  4. Test I²C Bridge
    – On a standard Arduino UNO (I²C master), write a small sketch that:
    • Polls the DATA_RDY pin via attachInterrupt() or polls registers every 10 s.
    • Reads back timestamp, lat/lon, HDOP and prints them to Serial.
    – Validate the values against a known-good source (e.g. your original JSON client).

Phase 4: Finalize & Optimize

  1. Power Management
    – Implement sleep_mode() between sentence parsing loops to save power.
    – Optionally control GPS module’s PWR_EN pin to fully switch it off between fixes.
  2. Flash & Fuse Settings
    – Set ATtiny clock to 8 MHz internal (or 16 MHz if supported) for reliable USI timing.
    – Configure Brown-Out Detector (BOD) for safe EEPROM/I²C operation.
  3. Documentation & Packaging
    – Create a concise datasheet: pinout, I²C address, register map, example host code.
    – Offer the bridge as a small PCB or DIP-module for easy integration.

With these four phases—prototype on Arduino, port parsers to ATtiny, add I²C, and finalize—you’ll end up with a compact, low-power ATtiny85 GPS-to-I²C bridge that any I²C master can use without dealing with UART or NMEA parsing.

Part 1: Arduino Sketch (NMEA → JSON)

First the Arduino sketch, then the HTML/JS client—so you can see exactly how data flows from GPS hardware all the way to map+sky‐view in the browser.

1.1. Libraries & Pins

• SoftwareSerial on pins 3 (RX) & 4 (TX) at 9 600 baud to talk to the GPS module.
• TimeLib to keep track of a Unix timestamp once we parse a valid date/time.

#include <SoftwareSerial.h>
#include <TimeLib.h>
#define RX_PIN 3
#define TX_PIN 4
#define BAUD   9600
SoftwareSerial gps(RX_PIN, TX_PIN);

1.2. Data Buffers & Flags

– lineBuf[] & linePos accumulate incoming bytes until a full NMEA line (\n) arrives.
– Booleans haveDatehaveGGAhaveGSV track which pieces we’ve collected this cycle.
– Parsed fields:
• Time+date → set in TimeLib (now())
• lat_ddlon_ddalt_mhdop from GGA
• An array sats[] of up to 32 satellites from GSV (PRN, elevation, azimuth, SNR)

1.3. setup()

Serial.begin(115200);   // output JSON at 115 200 baud
gps.begin(BAUD);        // GPS speaks at 9 600 baud

1.4. loop()

  1. Read & dispatch lines
    – While gps.available(), read one char at a time.
    – On '\n', terminate lineBuf and call dispatch(lineBuf).
  2. When we have date + GGA + GSV
    – If all flags are true and 10 000 ms elapsed since last JSON, call emitJSON().
    – Reset haveGGAhaveGSV, and satellite counter for next cycle.

1.5. dispatch(const char *s)

Looks at s[3..5] (characters 4–6) to pick the parser:

  • "RMC" → parseRMC()
  • "GGA" → parseGGA()
  • "GSV" → parseGSV()

1.6. Parsing RMC (Time & Date)

// Skip "$GPRMC," then read hhmmss.ss into tbuf[]
// Convert tbuf to int hh, mm, ss
// Check status ‘A’ = valid fix
// Skip 6 commas to reach date field ddmmyy in dbuf[]
// Convert to day, month, year
setTime(hh, mm, ss, day, month, year);
haveDate = true;

1.7. Parsing GGA (Position, HDOP, Altitude)

// Skip "$GPGGA," & time field
rawLat = atof();   // e.g. 3723.2475  (DDMM.MMMM)
ns     = 'N' or 'S'
rawLon = atof();   // e.g. 12258.3416 (DDDMM.MMMM)
ew     = 'E' or 'W'
fix    = digit
if fix < 1 return; // no valid fix
hdop   = atof();
alt_m  = atof();

// Convert raw DDMM.MMMM → decimal degrees:
dlat = int(rawLat/100);
lat_dd = dlat + (rawLat - dlat*100)/60.0; apply sign for S
(same for lon_dd)
haveGGA = true;

1.8. Parsing GSV (Satellite Info)

// Skip "$GPGSV," + total sentences + sentence index
totalSats = atoi();  // number of sats in view
Then up to 4 satellites per sentence:
  S.prn = atoi();
  S.el  = atoi();
  S.az  = atoi();
  S.snr = atoi();
satCount++;
haveGSV = true;

1.9. Emitting JSON

Every 10 s, once we’ve got date, GGA, GSV:

uint32_t unixTs = now();           // epoch seconds
float acc_m    = hdop * 5.0;       // rough accuracy
Serial.print("{");
Serial.print("\"ts\":");        Serial.print(unixTs);      Serial.print(",");
Serial.print("\"lat\":");       Serial.print(lat_dd,6);    Serial.print(",");
Serial.print("\"lon\":");       Serial.print(lon_dd,6);    Serial.print(",");
Serial.print("\"alt_m\":");     Serial.print(alt_m,1);      Serial.print(",");
Serial.print("\"hdop\":");      Serial.print(hdop,2);       Serial.print(",");
Serial.print("\"acc_m\":");     Serial.print(acc_m,1);      Serial.print(",");
Serial.print("\"total_sats\":");Serial.print(totalSats);   Serial.print(",");
Serial.print("\"sats\":[");
for each satellite i:
  Serial.print("{\"prn\":"); Serial.print(sats[i].prn);
  … print el,az,snr …
  Serial.print("}");
  if not last, print comma
Serial.println("]}");

Part 2: HTML + JavaScript Client

2.1. Page Structure & Styles

<button id="connect">Connect to Arduino</button>
<canvas id="mapCanvas"></canvas>    <!-- slippy map -->
<div id="marker"></div>             <!-- red center marker -->
<div id="info">No data</div>        <!-- text status panel -->
<div id="sky"><canvas></canvas></div> <!-- sky‐view plot -->

CSS absolutely positions everything full‐screen, with map in background, marker centered, info bottom‐left, sky bottom‐right.

2.2. Map Rendering (OpenStreetMap Tiles)

  1. Coordinate ↔ Tile
    • lon2x/lat2y convert geo → fractional tile coordinates at zoom level.
  2. Tile Cache & Fetch
    • ensureTile(z,x,y) fetches and caches PNG from tile.openstreetmap.org/z/x/y.png.
  3. Drawing
    • In drawMap(), figure out which tiles cover the canvas, draw each cached tile at correct offset.
    • If accuracy_m > 0, draw a translucent blue circle around the center to represent horizontal accuracy.

2.3. Sky‐View Plot

  • A polar “sky” circle of radius R.
  • For each satellite { el, az, prn }, compute
    r = (90−el)/90 * R
    θ = az in radians
    x = cx + r·sin(θ), y = cy − r·cos(θ)
  • Draw a small filled circle and label with PRN.

2.4. Web Serial Connection

connectBtn.onclick = async () => {
  // Request port, open at 115200
  // Create TextDecoderStream to split incoming by newline
  // reader.read() yields lines; parse JSON
  const obj = JSON.parse(line);
  update(obj);
};

2.5. Data Update Routine

function update(data) {
  if (data.acc_m) accuracy_m = data.acc_m;
  if (data.lat && data.lon) {
    centerLat = data.lat;
    centerLon = data.lon;
    scheduleDraw();
  }
  if (Array.isArray(data.sats)) {
    satsData = data.sats;
    drawSky();
  }
  if (data.ts) {
    // format HH:MM:SS from epoch seconds
    info.textContent = `Time: … UTC  Lat: …  Lon: … Alt: … HDOP: … Sats: …`;
  }
}

How It All Flows

  1. GPS module → raw NMEA sentences at 9 600 baud →
  2. Arduino sketch parses RMC/GGA/GSV → every 10 s emits one JSON line at 115 200 baud →
  3. Browser (Web Serial API) reads JSON → calls update() →
  4. Map canvas recenters and draws tiles + accuracy circle →
  5. Sky canvas redraws satellite positions →
  6. Info panel shows time, lat/lon, altitude, HDOP, satellite count.

With this breakdown, you can see exactly what data structures and timing each component uses—and you’re now ready to port the parsers to ATTiny85 and swap the UART→USB link for an I²C interface in Phase 2. Let me know where you’d like to dig in next!

Leave a Reply