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
- Upload the slave sketch (using SpenceKonde’s ATTinyCore in Arduino IDE).
- Wire your anemometer and I²C bus as above.
- 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