GPS I²C Bridge on ATTiny85 (part 3)

GPS I²C Bridge on ATtiny85 with Optional Power Control (Part 3)

In Phase 3 we extend the I²C-slave GPS bridge from Part 2 by:

  • Porting the sketch to TinyWireS (USI-TWI) on an ATtiny85
  • Adding an optional GPS power-switch controlled via I²C register 9

Hardware

  • Microcontroller: ATtiny85
  • GPS module: TTL-level NMEA TX/RX
  • I²C pull-ups: 4.7 kΩ on SDA (A4) and SCL (A5)
  • Optional low-side power switch:
    • Logic-level N-channel MOSFET (e.g. 2N7000) or NPN transistor (e.g. 2N2222)
    • Gate/base resistor: 100 Ω (MOSFET) or 4.7 kΩ (BJT)
    • Gate/base pull-down: 100 kΩ

Pin Assignments

ATtiny85 PinFunction
D3GPS TX → SoftwareSerial RX
D4GPS RX ← SoftwareSerial TX
A4 (SDA)I²C data (slave @ 0x42)
A5 (SCL)I²C clock
D6GPS_PWR_PIN → gate/base of MOSFET/BJT (optional)

I²C “Registers” and Data Packet

On each I²C request, the ATtiny85 sends one of the following registers (little-endian):

IndexFieldTypeBytesUnits / Description
0utcHundredthsuint32_t4hhmmss.ss → total hundredths
1dateDDMMYYuint32_t4DDMMYY as integer
2lat_fpint32_t4Latitude × 1 000 000 (micro-degrees)
3lon_fpint32_t4Longitude × 1 000 000
4speed_x100uint16_t2Speed (knots)×100
5course_x100uint16_t2Course (deg)×100
6alt_cmint32_t4Altitude (meters)×100
7hdop_x100uint16_t2HDOP×100
8satsUseduint8_t1Number of satellites
9GPS_PWR stateuint8_t10=OFF, 1=ON (read/write)

Slave Sketch

#include <Wire.h>           // USIWire (or TinyWireS‐shim)
#include <SoftwareSerial.h>
#include <TinyGPSPlus.h>

#define I2C_ADDR       0x42
#define GPS_RX_PIN     3     // ← GPS TX
#define GPS_TX_PIN     4     // → GPS RX
#define GPS_PWR_PIN    6     // power‐enable for GPS

SoftwareSerial gpsSerial(GPS_RX_PIN, GPS_TX_PIN);
TinyGPSPlus gps;

// last register index
volatile uint8_t regIndex = 0;

// parsed GPS values
uint32_t utcHundredths, dateDDMMYY;
int32_t  lat_fp, lon_fp, alt_cm;
uint16_t speed_x100, course_x100, hdop_x100;
uint8_t  satsUsed;

// called when master does Wire.write(regIndex[, value])
void receiveEvent(int howMany) {
  if (Wire.available()) {
    uint8_t r = Wire.read();
    regIndex = r;
    // if master is writing to register 9, read the next byte value
    if (r == 9 && Wire.available()) {
      uint8_t v = Wire.read();
      // v==1 → ON, v==0 → OFF
      digitalWrite(GPS_PWR_PIN, v ? HIGH : LOW);
    }
  }
}

// called when master does Wire.requestFrom()
void requestEvent() {
  // update fields only on a full valid fix
  if (gps.time.isValid()    &&
      gps.date.isValid()    &&
      gps.location.isValid()&&
      gps.hdop.isValid()    &&
      gps.speed.isValid()   &&
      gps.course.isValid()  &&
      gps.altitude.isValid()&&
      gps.satellites.isValid())
  {
    utcHundredths =
      (uint32_t(gps.time.hour())   * 3600UL +
       uint32_t(gps.time.minute()) *   60UL +
       uint32_t(gps.time.second())) * 100UL +
      uint32_t(gps.time.centisecond());

    dateDDMMYY =
      uint32_t(gps.date.day())   * 10000UL +
      uint32_t(gps.date.month()) *   100UL +
      uint32_t(gps.date.year() % 100);

    lat_fp      = int32_t(gps.location.lat()    * 1e6);
    lon_fp      = int32_t(gps.location.lng()    * 1e6);
    speed_x100  = uint16_t(gps.speed.knots()    * 100.0);
    course_x100 = uint16_t(gps.course.deg()     * 100.0);
    alt_cm      = int32_t(gps.altitude.meters() * 100.0);
    hdop_x100   = uint16_t(gps.hdop.hdop()      * 100.0);
    satsUsed    = gps.satellites.value();
  }

  // respond to master based on regIndex
  uint8_t *p;
  switch (regIndex) {
    case 0:
      p = (uint8_t*)&utcHundredths; Wire.write(p, 4); break;
    case 1:
      p = (uint8_t*)&dateDDMMYY;   Wire.write(p, 4); break;
    case 2:
      p = (uint8_t*)&lat_fp;       Wire.write(p, 4); break;
    case 3:
      p = (uint8_t*)&lon_fp;       Wire.write(p, 4); break;
    case 4:
      p = (uint8_t*)&speed_x100;   Wire.write(p, 2); break;
    case 5:
      p = (uint8_t*)&course_x100;  Wire.write(p, 2); break;
    case 6:
      p = (uint8_t*)&alt_cm;       Wire.write(p, 4); break;
    case 7:
      p = (uint8_t*)&hdop_x100;    Wire.write(p, 2); break;
    case 8:
      Wire.write(&satsUsed, 1);    break;
    case 9:
      { // report current GPS_PWR_PIN state
        uint8_t st = digitalRead(GPS_PWR_PIN);
        Wire.write(&st, 1);
      }
      break;
    default:
      // nothing
      break;
  }
}

void setup() {
  // GPS power‐control pin
  pinMode(GPS_PWR_PIN, OUTPUT);
  // by default keep GPS ON
  digitalWrite(GPS_PWR_PIN, HIGH);

  gpsSerial.begin(9600);
  Wire.begin(I2C_ADDR);
  Wire.onReceive(receiveEvent);
  Wire.onRequest(requestEvent);
}

void loop() {
  while (gpsSerial.available()) {
    gps.encode(gpsSerial.read());
  }
  // no other tasks
}


Master Usage

  1. Write the desired register index:
    Wire.beginTransmission(0x42); Wire.write(regIndex); Wire.endTransmission();
  2. Request N bytes (matching that register’s length) and read them:
    Wire.requestFrom(0x42, N); …
  3. Reconstruct the little-endian value and print or use in your code.
  4. To toggle GPS power, write register 9 with 0 (OFF) or 1 (ON).

Master sketch

#include <Wire.h>

#define GPS_ADDR 0x42

enum {
  R_UTC,
  R_DATE,
  R_LAT,
  R_LON,
  R_SPEED,
  R_COURSE,
  R_ALT,
  R_HDOP,
  R_SATS,
  R_COUNT
};

void setup() {
  Serial.begin(115200);
  while (!Serial);
  Wire.begin();
}

void loop() {
  // Read all nine registers
  uint32_t utc      = readU32(R_UTC);
  uint32_t date     = readU32(R_DATE);
  int32_t  lat_fp   = readS32(R_LAT);
  int32_t  lon_fp   = readS32(R_LON);
  uint16_t speed    = readU16(R_SPEED);
  uint16_t course   = readU16(R_COURSE);
  int32_t  alt_cm   = readS32(R_ALT);
  uint16_t hdop     = readU16(R_HDOP);
  uint8_t  sats     = readU8(R_SATS);

  // Convert & print
  Serial.print("Time: ");
  printTime(utc);
  Serial.print("Date: ");
  printDate(date);
  Serial.print("Lat: "); Serial.println(lat_fp/1e6,6);
  Serial.print("Lon: "); Serial.println(lon_fp/1e6,6);
  Serial.print("Speed: "); Serial.println(speed/100.0,2);
  Serial.print("Course: "); Serial.println(course/100.0,2);
  Serial.print("Alt: "); Serial.println(alt_cm/100.0,2);
  Serial.print("HDOP: "); Serial.println(hdop/100.0,2);
  Serial.print("Sats: "); Serial.println(sats);
  Serial.println("-------------------------");
  delay(1000);
}

uint32_t readU32(uint8_t reg) {
  Wire.beginTransmission(GPS_ADDR);
  Wire.write(reg);
  Wire.endTransmission();
  Wire.requestFrom(GPS_ADDR, (uint8_t)4);
  uint32_t v=0;
  for(int i=0;i<4;i++) v |= (uint32_t)Wire.read() << (8*i);
  return v;
}

int32_t readS32(uint8_t reg) { return (int32_t)readU32(reg); }

uint16_t readU16(uint8_t reg) {
  Wire.beginTransmission(GPS_ADDR);
  Wire.write(reg);
  Wire.endTransmission();
  Wire.requestFrom(GPS_ADDR, (uint8_t)2);
  uint16_t v= Wire.read();
  v |= (uint16_t)Wire.read()<<8;
  return v;
}

uint8_t readU8(uint8_t reg) {
  Wire.beginTransmission(GPS_ADDR);
  Wire.write(reg);
  Wire.endTransmission();
  Wire.requestFrom(GPS_ADDR, (uint8_t)1);
  return Wire.read();
}

void printTime(uint32_t ch) {
  uint8_t cs= ch%100;
  uint32_t s= ch/100;
  uint8_t ss= s%60, mm= (s/60)%60, hh= s/3600;
  if(hh<10) Serial.print('0'); Serial.print(hh); Serial.print(':');
  if(mm<10) Serial.print('0'); Serial.print(mm); Serial.print(':');
  if(ss<10) Serial.print('0'); Serial.print(ss); Serial.print('.');
  if(cs<10) Serial.print('0'); Serial.println(cs);
}

void printDate(uint32_t d) {
  uint8_t DD= d/10000, MM= (d/100)%100, YY= d%100;
  if(DD<10) Serial.print('0'); Serial.print(DD); Serial.print('/');
  if(MM<10) Serial.print('0'); Serial.print(MM); Serial.print('/');
  if(YY<10) Serial.print('0'); Serial.println(YY);
}

Conclusion

This ATtiny85 + TinyWireS sketch provides a fully self-contained GPS-to-I²C bridge. It delivers compact, fixed-point GPS fixes on demand and optionally allows the master to power-cycle the GPS module via a low-side MOSFET or BJT—no USB/UART needed on the host side.

Leave a Reply