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