
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:
- 4 bytes
utcHundredths - 4 bytes
lat_fp - 4 bytes
lon_fp - 2 bytes
hdop_x100 - 1 byte
satsUsed
- 4 bytes
loop() & parseGGA()
- Accumulate NMEA in
lineBufuntil ‘\n’. If it contains “GGA,” callparseGGA(): parseGGA()steps:- Tokenize by commas.
- Read time hhmmss.ss → compute
utcHundredths. - Read raw lat (ddmm.mmmm), N/S, raw lon (dddmm.mmmm), E/W → convert to decimal degrees → micro-degrees into
lat_fp,lon_fp. - Read
satsUsedandhdop_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()
Wire.requestFrom(0x42,15)– If- Read 15 bytes into
uint8_t buf[15]. - 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]; - Convert & print:
- Time:
hh = utcHundredths/(3600*100), etc. - Lat =
lat_fp/1e6, Lon =lon_fp/1e6 - HDOP =
hdop_x100/100.0 - Satellites =
satsUsed
- Time:
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.