GPS I²C Bridge on ATTiny85 (part 2)

In this phase we replace the JSON-over-USB link with a compact I²C interface. One Arduino (or ATtiny85) becomes an I²C slave that parses NMEA GGA; the master polls it once per second and prints human-readable output.

2.1 Slave: GPS → I²C

Hardware

  • GPS module → SoftwareSerial RX/TX (e.g. D3=RX, D4=TX)
  • I²C pins A4 (SDA), A5 (SCL) set as slave @ 0x42

Key Variables

  • uint32_t utcHundredths (hhmmss.ss → total hundredths)
  • int32_t lat_fp, lon_fp (decimal degrees ×1 000 000)
  • uint16_t hdop_x100 (HDOP ×100)
  • uint8_t satsUsed

onI2CRequest()

  • Called by Wire when the master does Wire.requestFrom(0x42,15).
  • Packs and sends 15 bytes in little-endian order:
    1. 4 bytes utcHundredths
    2. 4 bytes lat_fp
    3. 4 bytes lon_fp
    4. 2 bytes hdop_x100
    5. 1 byte satsUsed

loop() & parseGGA()

  • Accumulate NMEA in lineBuf until ‘\n’. If it contains “GGA,” call parseGGA():
  • parseGGA() steps:
    1. Tokenize by commas.
    2. Read time hhmmss.ss → compute utcHundredths.
    3. Read raw lat (ddmm.mmmm), N/S, raw lon (dddmm.mmmm), E/W → convert to decimal degrees → micro-degrees into lat_fplon_fp.
    4. Read satsUsed and hdop_x100.
#include <Arduino.h>
#include <Wire.h>
#include <SoftwareSerial.h>

#define I2C_ADDR   0x42
#define MAXLINE    64

// GPS on SoftwareSerial (RX=3, TX=4)
SoftwareSerial gps(3, 4);

// Line buffer for incoming NMEA
char    lineBuf[MAXLINE];
uint8_t linePos = 0;

// Parsed values:
volatile uint32_t utcHundredths = 0;  // hhmmss.ss → total hundredths
volatile int32_t  lat_fp         = 0; // micro‐degrees
volatile int32_t  lon_fp         = 0; // micro‐degrees
volatile uint16_t hdop_x100      = 0; // HDOP ×100
volatile uint8_t  satsUsed       = 0; // # satellites in fix

// I²C request handler: always send the full 15‐byte packet
void onI2CRequest() {
  uint8_t *p;
  // [utcHundredths:4]
  p = (uint8_t*)&utcHundredths;
  for (uint8_t i = 0; i < 4; i++) Wire.write(p[i]);
  // [lat_fp:4]
  p = (uint8_t*)&lat_fp;
  for (uint8_t i = 0; i < 4; i++) Wire.write(p[i]);
  // [lon_fp:4]
  p = (uint8_t*)&lon_fp;
  for (uint8_t i = 0; i < 4; i++) Wire.write(p[i]);
  // [hdop_x100:2]
  p = (uint8_t*)&hdop_x100;
  for (uint8_t i = 0; i < 2; i++) Wire.write(p[i]);
  // [satsUsed:1]
  Wire.write(satsUsed);
}

void setup() {
  // I²C slave
  Wire.begin(I2C_ADDR);
  Wire.onRequest(onI2CRequest);

  // GPS serial
  gps.begin(9600);
}

// Parse a GGA sentence in lineBuf
void parseGGA() {
  // strtok will modify lineBuf
  char *tok = strtok(lineBuf, ",");        // "$xxGGA"
  tok = strtok(NULL, ","); if (!tok) return;
  // Time hhmmss.ss
  float t = atof(tok);
  uint32_t hh = uint32_t(t / 10000);
  uint32_t mm = uint32_t((t - hh * 10000) / 100);
  uint32_t ssf = uint32_t((t - hh * 10000 - mm * 100) * 100 + 0.5);
  utcHundredths = (hh * 3600UL + mm * 60UL) * 100UL + ssf;

  // Latitude ddmm.mmmm + N/S
  tok = strtok(NULL, ","); if (!tok) return;
  float rawLat = atof(tok);
  tok = strtok(NULL, ","); if (!tok) return;
  char ns = tok[0];

  // Longitude dddmm.mmmm + E/W
  tok = strtok(NULL, ","); if (!tok) return;
  float rawLon = atof(tok);
  tok = strtok(NULL, ","); if (!tok) return;
  char ew = tok[0];

  // Fix quality (skip) + satsUsed
  strtok(NULL, ",");
  tok = strtok(NULL, ","); if (!tok) return;
  satsUsed = uint8_t(atoi(tok));

  // HDOP
  tok = strtok(NULL, ","); if (!tok) return;
  hdop_x100 = uint16_t(atof(tok) * 100 + 0.5f);

  // Convert to decimal degrees
  float d1 = int(rawLat / 100);
  float m1 = rawLat - d1 * 100;
  float lat = d1 + m1 / 60.0f;
  if (ns == 'S') lat = -lat;

  float d2 = int(rawLon / 100);
  float m2 = rawLon - d2 * 100;
  float lon = d2 + m2 / 60.0f;
  if (ew == 'W') lon = -lon;

  // Store in micro‐degrees
  lat_fp = int32_t(lat * 1e6 + (lat >= 0 ? 0.5 : -0.5));
  lon_fp = int32_t(lon * 1e6 + (lon >= 0 ? 0.5 : -0.5));
}

void loop() {
  // Read incoming GPS bytes
  while (gps.available()) {
    char c = gps.read();
    if (c == '\r') continue;
    if (c == '\n') {
      lineBuf[linePos] = '\0';
      if (strstr(lineBuf, "GGA,")) {
        parseGGA();
      }
      linePos = 0;
    }
    else if (linePos < MAXLINE - 1) {
      lineBuf[linePos++] = c;
    }
  }
}

Upload this to your slave Arduino or ATtiny85. It’ll parse GGA continuously and, on each I²C request, dump the latest fix in 15 bytes.

2.2 Master: I²C → Serial Print

Hardware

  • Another Arduino (e.g. UNO) as I²C master
  • Connect SDA/SCL to the slave; pull-ups on UNO suffice

loop()

  1. Wire.requestFrom(0x42,15) – If
  2. Read 15 bytes into uint8_t buf[15].
  3. Reconstruct values (little-endian): utcHundredths = buf[0] | buf[1]< lat_fp = buf[4..7], lon_fp = buf[8..11] hdop_x100 = buf[12] | buf[13]< satsUsed = buf[14];
  4. Convert & print:
    • Time: hh = utcHundredths/(3600*100), etc.
    • Lat = lat_fp/1e6, Lon = lon_fp/1e6
    • HDOP = hdop_x100/100.0
    • Satellites = satsUsed

Example Output

Time UTC = 12:34:56  
Lat = 37.387123  
Lon = -122.057890  
HDOP = 0.85  
Satellites used = 8  

With these two sketches you now have a fully working GPS-to-I²C bridge. In Phase 3 you’ll port the slave code into an ATtiny85 using USI-TWI, and eliminate any USB or UART dependency on the master side.

Leave a Reply