Simple Cup Anemometer on ATTiny85 (I2C)

This ATTiny85-based I²C-slave sketch turns a simple cup-anemometer into a self-contained, configurable wind-speed sensor. By default it assumes each magnet-pass pulse equals 0.3 m/s wind speed, but you can re-calibrate in the field over I²C. It provides:

• Real-time pulse counting with software debounce (2 ms dead-time)
• One-second sampling windows and a 60-second ring buffer for history
• Three metrics updated every second and reported on I²C:
– Last: wind speed (m/s) in the most recent 1 s interval
– Avg: average wind speed (m/s) over the last N s (default 60 s)
– Gust: maximum 1 s wind speed (m/s) over the last N s

All values are sent as unsigned 16-bit “centi-m/s” (hundredths of m/s). Settings are stored in EEPROM and survive power cycles:

• I²C address (1 byte)
• Calibration constant (2 bytes)—default 300 → 0.3 m/s per pulse/s
• History window size N in seconds (1 byte, 1…60; default 60 s)

Hardware Wiring

ATTiny85 DIP-8 (flat face toward you, pins count CCW):
• Pin 8 VCC → +3.3 V or +5 V
• Pin 4 GND → 0 V
• Pin 5 PB0 → SDA ←→ pull-up (4.7 kΩ) → VCC
• Pin 7 PB2 → SCL ←→ pull-up (4.7 kΩ) → VCC
• Pin 2 PB3 → anemometer reed switch → GND

Software Usage

  1. Upload the slave sketch (using SpenceKonde’s ATTinyCore in Arduino IDE).
  2. Wire your anemometer and I²C bus as above.
  3. On power-up ATtiny85 reads its settings from EEPROM (or writes defaults) and begins sampling.

I²C Master Protocol

• Read data:

Wire.requestFrom(addr, (uint8_t)6);
uint16_t last = (Wire.read()<<8)|Wire.read();
uint16_t avg  = (Wire.read()<<8)|Wire.read();
uint16_t gust = (Wire.read()<<8)|Wire.read();
// Convert to m/s: value/100.0

Bytes: [Last MSB][Last LSB][Avg MSB][Avg LSB][Gust MSB][Gust LSB]

• Set I²C address:

Wire.beginTransmission(addr);
Wire.write(0xA0);
Wire.write(new_addr);
Wire.endTransmission();

• Set calibration (new K in m/s per pulse/s):

uint16_t K_int = round(K * 1000);
Wire.beginTransmission(addr);
Wire.write(0xA1);
Wire.write(K_int & 0xFF);
Wire.write(K_int >> 8);
Wire.endTransmission();

• Set history window N (1…60 s):

Wire.beginTransmission(addr);
Wire.write(0xA2);
Wire.write(N);
Wire.endTransmission();
// ATtiny85 will save N to EEPROM and reset itself

Example Master Sketch

#include <Wire.h>
const uint8_t SLAVE = 0x42;  // default address
void setup() {
  Serial.begin(115200);
  Wire.begin();
}
void loop() {
  // Read and print every second
  Wire.requestFrom(SLAVE, (uint8_t)6);
  if (Wire.available() == 6) {
    uint16_t last = (Wire.read() << 8) | Wire.read();
    uint16_t avg  = (Wire.read() << 8) | Wire.read();
    uint16_t gust = (Wire.read() << 8) | Wire.read();
    Serial.print("Last: ");
    Serial.print(last / 100.0, 2);
    Serial.print(" m/s  Avg: ");
    Serial.print(avg  / 100.0, 2);
    Serial.print(" m/s  Gust: ");
    Serial.print(gust / 100.0, 2);
    Serial.println(" m/s");
  }
  delay(1000);
}

Source Attiny85

#include <TinyWireS.h>
#include <EEPROM.h>
#include <avr/wdt.h>

#define EEPROM_I2C_ADDR_LOC  0   // EEPROM byte 0: I2C address
#define EEPROM_CAL_LOC       1   // EEPROM bytes 1–2: calibration K_int
#define EEPROM_WINDOW_LOC    3   // EEPROM byte 3: history window size

#define CMD_SET_ADDR    0xA0
#define CMD_SET_CAL     0xA1
#define CMD_SET_WINDOW  0xA2

// defaults
const uint8_t  DEFAULT_I2C_ADDR = 0x42;
const uint16_t DEFAULT_K_INT    = 300;   // 0.3 m/s per pulse/s → 300 mm/s·pps⁻¹
const uint8_t  BUFFER_LEN       = 60;

uint8_t  i2c_addr;
uint16_t K_int;          // mm/s per pps (integer)
float    K;              // m/s per pps (float)

uint16_t buffer[BUFFER_LEN];
uint8_t  buf_head   = 0;
bool     buf_full   = false;
uint8_t  window_size;

volatile uint32_t pulseCount, lastPulseTime;
uint16_t last_centi, avg_centi, gust_centi;
uint32_t next_ms;

const uint32_t SAMPLE_MS   = 1000;
const uint32_t DEBOUNCE_US = 2000;

void setup() {
  // — Load or init I²C address —
  uint8_t e = EEPROM.read(EEPROM_I2C_ADDR_LOC);
  if (e >= 8 && e <= 0x77) {
    i2c_addr = e;
  } else {
    i2c_addr = DEFAULT_I2C_ADDR;
    EEPROM.write(EEPROM_I2C_ADDR_LOC, i2c_addr);
  }

  // — Load or init calibration constant —
  uint16_t raw = EEPROM.read(EEPROM_CAL_LOC)
               | (EEPROM.read(EEPROM_CAL_LOC+1) << 8);
  if (raw == 0x0000 || raw == 0xFFFF) {
    raw = DEFAULT_K_INT;
    EEPROM.write(EEPROM_CAL_LOC,     raw & 0xFF);
    EEPROM.write(EEPROM_CAL_LOC + 1, raw >> 8);
  }
  K_int = raw;
  K     = K_int / 1000.0f;

  // — Load or init window size —
  uint8_t w = EEPROM.read(EEPROM_WINDOW_LOC);
  if (w < 1 || w > BUFFER_LEN) {
    w = BUFFER_LEN;
    EEPROM.write(EEPROM_WINDOW_LOC, w);
  }
  window_size = w;

  // — Init I²C slave callbacks —
  TinyWireS.begin(i2c_addr);
  TinyWireS.onRequest(requestEvent);
  TinyWireS.onReceive(receiveEvent);

  // — Configure PB3 for pin‐change interrupt (reed switch) —
  pinMode(3, INPUT_PULLUP);
  GIMSK |= _BV(PCIE);
  PCMSK |= _BV(PCINT3);
  sei();

  // init counters & timing
  pulseCount    = 0;
  lastPulseTime = 0;
  next_ms       = millis() + SAMPLE_MS;
}

void loop() {
  TinyWireS_stop_check();  // handle I2C in background

  if (millis() >= next_ms) {
    // grab & reset pulse count
    noInterrupts();
      uint32_t cnt = pulseCount;
      pulseCount = 0;
    interrupts();

    // compute centi-m/s for this 1 s window
    float pps = cnt * (1000.0 / SAMPLE_MS);
    float mps = pps * K;
    uint16_t c = (uint16_t)round(mps * 100.0);

    // push into ring buffer
    buffer[buf_head++] = c;
    if (buf_head >= BUFFER_LEN) {
      buf_head = 0;
      buf_full = true;
    }

    // compute avg & gust over last window_size samples
    uint8_t valid = buf_full ? BUFFER_LEN : buf_head;
    uint8_t len   = (valid < window_size ? valid : window_size);
    uint8_t idx0  = (buf_head + BUFFER_LEN - len) % BUFFER_LEN;

    uint32_t sum = 0;
    uint16_t mx  = 0;
    for (uint8_t i = 0; i < len; i++) {
      uint16_t v = buffer[(idx0 + i) % BUFFER_LEN];
      sum += v;
      if (v > mx) mx = v;
    }
    last_centi = c;
    avg_centi  = (len ? sum / len : 0);
    gust_centi = mx;

    next_ms += SAMPLE_MS;
  }
}

ISR(PCINT0_vect) {
  uint32_t t = micros();
  if (t - lastPulseTime < DEBOUNCE_US) return;
  lastPulseTime = t;
  if ((PINB & _BV(PB3)) == 0) {
    pulseCount++;
  }
}

void requestEvent() {
  // send Last, Avg, Gust (MSB then LSB each)
  TinyWireS.write(last_centi >> 8);
  TinyWireS.write(last_centi & 0xFF);
  TinyWireS.write(avg_centi  >> 8);
  TinyWireS.write(avg_centi  & 0xFF);
  TinyWireS.write(gust_centi >> 8);
  TinyWireS.write(gust_centi & 0xFF);
}

void receiveEvent(uint8_t howMany) {
  while (TinyWireS.available()) {
    uint8_t cmd = TinyWireS.read();
    // Set I²C address
    if (cmd == CMD_SET_ADDR && TinyWireS.available()) {
      uint8_t a = TinyWireS.read();
      if (a >= 8 && a <= 0x77) {
        EEPROM.write(EEPROM_I2C_ADDR_LOC, a);
        TinyWireS.begin(a);
      }
    }
    // Set calibration constant
    else if (cmd == CMD_SET_CAL && TinyWireS.available() >= 2) {
      uint8_t lo = TinyWireS.read();
      uint8_t hi = TinyWireS.read();
      uint16_t rawNew = (hi << 8) | lo;
      if (rawNew >= 100 && rawNew <= 10000) {
        EEPROM.write(EEPROM_CAL_LOC,     lo);
        EEPROM.write(EEPROM_CAL_LOC + 1, hi);
        K_int = rawNew;
        K     = K_int / 1000.0f;
      }
    }
    // Set history-window size and reset
    else if (cmd == CMD_SET_WINDOW && TinyWireS.available()) {
      uint8_t w = TinyWireS.read();
      w = constrain(w, 1, BUFFER_LEN);
      EEPROM.write(EEPROM_WINDOW_LOC, w);
      // watchdog reset
      wdt_enable(WDTO_15MS);
      for (;;);
    }
  }
}

Calibration Script

https://www.ctvrtky.info/wp-content/uploads/2025/08/Anemometer-Calibration.html

Leave a Reply