This sketch turns an ATtiny (or Arduino) into:
• a rain‐gauge collector (counting bucket‐tips via a reed switch)
• a temperature sensor (NTC thermistor)
• a heater‐controller (PWM output)
• an I²C-slave interface to report rain and temperature data
You can choose, at compile time, whether each rain tip is converted to millimetres by
- supplying a bucket volume (mL) and collection area (m²), or
- hard-coding a fixed mm-per-tip value
via the single switch at the top of the sketch:
• set #define USE_AREA_CALC 1
–– sketch computes

• set #define USE_AREA_CALC 0
–– sketch uses your FIXED_MM_PER_PULSE
I²C registers (master → slave via Wire.requestFrom):
• reg 0: rain in current 1-min window (hundredths mm)
• reg 2: rain in last 1-min window
• reg 4: rain in last 10 min (sum of ten 1-min windows)
• reg 6: temperature, in tenths °C (signed 16-bit)
• reg 8: heater PWM duty (0–255)
#include <Arduino.h>
#include <Wire.h>
// ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
// CONFIGURATION SWITCH:
// if 1 → compute MM_PER_PULSE from bucket volume + area
// if 0 → use a fixed MM_PER_PULSE value directly
#define USE_AREA_CALC 1
// ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
// PIN ASSIGNMENTS (Arduino numbering)
#define RAIN_PIN 4 // PB4 (physical pin 3)
#define NTC_PIN A3 // PB3/ADC3 (physical pin 2)
#define HEATER_PWM_PIN 1 // PB1/OC1A (physical pin 6)
// I²C slave address
#define SLAVE_ADDR 0x20
// RAIN CALIBRATION
#if USE_AREA_CALC
// compute mm per pulse from bucket volume and collecting area
const float TIP_VOLUME_mL = 0.2; // mL per tip
const float AREA_M2 = 0.01; // m²
const float MM_PER_PULSE = TIP_VOLUME_mL / (1000.0 * AREA_M2);
#else
// hard-code the rainfall depth per tip (in millimeters)
// e.g. if each tip = 0.5 mm of rain:
const float FIXED_MM_PER_PULSE = 0.5;
const float MM_PER_PULSE = FIXED_MM_PER_PULSE;
#endif
// NTC β‐formula constants
const float R_REF = 10000.0, T0_K = 298.15, BETA = 3950.0, R0 = 10000.0;
// HEATER CURVE (°C)
const float T_FULL = 15.0; // ≤ → 100% duty
const float T_ZERO = 25.0; // ≥ → 0% duty
// TIMING
const uint32_t INTERVAL_MS = 60000UL; // 1 min
const uint32_t DEBOUNCE_MS = 50UL; // ms
// STATE
volatile uint16_t rainTips = 0; // count in current minute
uint16_t rainCounts[10] = {0}; // last 10 one-min counts
uint8_t bufIndex = 0;
// for debounce & edge detect
volatile uint32_t lastTipMs = 0;
volatile bool lastPinState = LOW;
// I²C‐REQUEST handler prototype
void onRequest();
// RAIN‐TIP INTERRUPT (debounced, rising-edge only)
ISR(PCINT0_vect) {
uint32_t now = millis();
bool cur = digitalRead(RAIN_PIN);
// ignore if still bouncing
if (now - lastTipMs < DEBOUNCE_MS) {
lastPinState = cur;
return;
}
// count only LOW→HIGH
if (cur == HIGH && lastPinState == LOW) {
rainTips++;
lastTipMs = now;
}
lastPinState = cur;
}
void onRequest() {
uint8_t reg = Wire.read();
switch (reg) {
case 0: { // current minute
uint16_t v = uint16_t(rainTips * MM_PER_PULSE * 100.0 + 0.5);
Wire.write(lowByte(v));
Wire.write(highByte(v));
break;
}
case 2: { // last minute
uint16_t tips = rainCounts[(bufIndex + 9) % 10];
uint16_t v = uint16_t(tips * MM_PER_PULSE * 100.0 + 0.5);
Wire.write(lowByte(v));
Wire.write(highByte(v));
break;
}
case 4: { // last 10 minutes
uint32_t sum = 0;
for (uint8_t i = 0; i < 10; i++) sum += rainCounts[i];
uint16_t v = uint16_t(sum * MM_PER_PULSE * 100.0 + 0.5);
Wire.write(lowByte(v));
Wire.write(highByte(v));
break;
}
case 6: { // temperature
extern int16_t lastTemp10;
uint16_t t10 = uint16_t(lastTemp10);
Wire.write(lowByte(t10));
Wire.write(highByte(t10));
break;
}
case 8: // PWM duty
extern uint8_t lastOCR;
Wire.write(lastOCR);
break;
default:
Wire.write((uint8_t)0);
}
}
int16_t lastTemp10 = 0; // tenths °C
uint8_t lastOCR = 0; // 0–255 PWM duty
uint32_t lastInterval;
void setup() {
// rain pin + interrupt
pinMode(RAIN_PIN, INPUT_PULLUP);
GIMSK = _BV(PCIE);
PCMSK = _BV(PCINT4);
// heater PWM pin
pinMode(HEATER_PWM_PIN, OUTPUT);
analogWrite(HEATER_PWM_PIN, 0);
// I²C slave
Wire.begin(SLAVE_ADDR);
Wire.onRequest(onRequest);
lastInterval = millis();
}
void loop() {
uint32_t now = millis();
// every minute, shift buffer
if (now - lastInterval >= INTERVAL_MS) {
lastInterval += INTERVAL_MS;
rainCounts[bufIndex] = rainTips;
bufIndex = (bufIndex + 1) % 10;
rainTips = 0;
}
// read NTC via analogRead()
int raw = analogRead(NTC_PIN);
float vR = float(raw) / 1023.0;
float r = R_REF * (1.0 / vR - 1.0);
float invT = 1.0 / T0_K + log(r / R0) / BETA;
float tK = 1.0 / invT;
float tC = tK - 273.15;
lastTemp10 = int16_t(round(tC * 10.0));
// compute linear PWM duty
float dutyF;
if (tC <= T_FULL) dutyF = 1.0;
else if (tC >= T_ZERO) dutyF = 0.0;
else dutyF = (T_ZERO - tC) / (T_ZERO - T_FULL);
lastOCR = uint8_t(dutyF * 255.0 + 0.5);
analogWrite(HEATER_PWM_PIN, lastOCR);
}
WIRING
Below is a wiring guide for the SLAVE unit (your rain-gauge + NTC + heater controller).
- Power
• VCC → +5 V (or +3.3 V if your ATtiny/Arduino runs at 3.3 V)
• GND → common ground - I²C bus (shared with the master)
• SDA → MCU SDA pin (ATtiny85 PB0 / Arduino A4)
• SCL → MCU SCL pin (ATtiny85 PB2 / Arduino A5)
• Pull-ups: 4.7 kΩ resistors from SDA→VCC and SCL→VCC - Rain-gauge “bucket‐tip” input
• RAIN_PIN (PB4 / Arduino D4) ←→ one side of your reed switch or optocoupler output
• Other side of reed switch ←→ GND
• Internal pull-up is enabled in software, so no external pull-up resistor is needed on PB4 - NTC thermistor
• Connect NTC between NTC_PIN (PB3 / Arduino A3) and GND
• Connect R_REF (10 kΩ reference resistor) between NTC_PIN and VCC
• This forms a voltage divider: V(NTC_PIN) = VCC × R_NTC/(R_NTC + R_REF) - Heater PWM output
• HEATER_PWM_PIN (PB1 / Arduino D1) → gate of an N-channel MOSFET (or transistor)
• MOSFET source → GND; MOSFET drain → low side of heater element
• Other side of heater element → VCC (or separate heater supply, same ground)
• Add a flyback/free-wheeling diode if heater is inductive
In simplest form:
HEATER_PWM_PIN → [gate MOSFET]
MOSFET DRAIN → HEATER(–)
HEATER(+) → +VCC
MOSFET SOURCE → GND
- Summary table
Pin Type MCU Connect to
────── ───────── ───── ─────────────────────────
VCC power VCC +5 V (or +3.3 V)
GND power GND ground
SDA I²C data PB0/A4 master SDA, +4.7 k pull-up
SCL I²C clock PB2/A5 master SCL, +4.7 k pull-up
RAIN_PIN digital PB4/D4 reed switch → GND
NTC_PIN analog PB3/A3 NTC → GND; 10 kΩ → VCC
HEATER_PWM PWM PB1/D1 gate of MOSFET→heater→VCC
With this wiring in place, upload the slave sketch. The internal pull-up on RAIN_PIN cleanly biases the rain-gauge input high until the reed switch closes to GND, triggering the interrupt. The NTC divider on A3 measures temperature, and the PWM pin drives your heater via the MOSFET.
MASTER SKETCH WITH PULSE TESTER
Below is an enhanced “master” sketch that:
- Generates simulated rain‐gauge pulses on a digital pin (so you can wire that pin to the slave’s RAIN_PIN).
- Polls the slave once per second to read actual rain and temperature data.
- Prints both the slave’s measured rain and the master’s expected rain (based on the pulse rate you chose).

Wire-up:
• Connect Master D2 → Slave RAIN_PIN (with a pull-up on the slave)—this lets the master drive the reed-switch input.
• I²C SDA, SCL shared between master and slave as usual.
#include <Wire.h>
#define SLAVE_ADDR 0x20
// pulse generator on D2 → slave's rain pin
const uint8_t PULSE_PIN = 2;
// how many pulses per minute you want to generate:
const uint16_t PULSES_PER_MIN = 30;
// and how many mm each pulse should represent (match your slave's MM_PER_PULSE):
const float MM_PER_PULSE_SIM = 0.5;
// derived timing in ms between rising edges:
const float INTERVAL_MS_PER_PULSE = 60000.0 / float(PULSES_PER_MIN);
uint32_t lastPulseTime = 0;
bool pulseState = LOW;
//———————————————————————————————————————————————————————————————
// helper to read a 16-bit little-endian register from the slave
uint16_t readReg16(uint8_t reg) {
Wire.beginTransmission(SLAVE_ADDR);
Wire.write(reg);
Wire.endTransmission(false);
Wire.requestFrom(SLAVE_ADDR, uint8_t(2));
uint8_t lo = Wire.read();
uint8_t hi = Wire.read();
return uint16_t(lo) | (uint16_t(hi) << 8);
}
void setup() {
Serial.begin(115200);
Wire.begin(); // join I²C as master
// set up the pulse-generator pin
pinMode(PULSE_PIN, OUTPUT);
digitalWrite(PULSE_PIN, LOW);
lastPulseTime = millis();
}
void loop() {
uint32_t now = millis();
// ————————————— Pulse generator —————————————
// toggle D2 at half the interval to form a LOW→HIGH transition each INTERVAL_MS_PER_PULSE
if (now - lastPulseTime >= INTERVAL_MS_PER_PULSE / 2) {
pulseState = !pulseState;
digitalWrite(PULSE_PIN, pulseState);
lastPulseTime += INTERVAL_MS_PER_PULSE / 2;
}
// ———————————— Poll the slave every 1 s ————————————
static uint32_t lastPoll = 0;
if (now - lastPoll >= 1000) {
lastPoll = now;
// read 16-bit regs
uint16_t rawNow = readReg16(0); // hundredths mm
uint16_t rawLast = readReg16(2);
uint16_t raw10min = readReg16(4);
uint16_t rawTemp = readReg16(6); // tenths °C
// read PWM duty:
Wire.beginTransmission(SLAVE_ADDR);
Wire.write(8);
Wire.endTransmission(false);
Wire.requestFrom(SLAVE_ADDR, uint8_t(1));
uint8_t rawPWM = Wire.read();
// convert
float rainNow = rawNow / 100.0; // mm in current 1-min
float rainLast = rawLast / 100.0; // mm last 1-min
float rain10min = raw10min / 100.0; // mm last 10-min
float tempC = int16_t(rawTemp) / 10.0; // °C
float pwmDuty = rawPWM / 255.0 * 100.0; // % duty
// expected rain (master’s generator) in mm/min
float expectedRainPerMin = PULSES_PER_MIN * MM_PER_PULSE_SIM;
// print results
Serial.print("Generated pulses: ");
Serial.print(PULSES_PER_MIN); Serial.print(" p/min → ");
Serial.print(expectedRainPerMin,2); Serial.println(" mm/min");
Serial.print(" Slave now: "); Serial.print(rainNow,2); Serial.print(" mm/min, ");
Serial.print("last: "); Serial.print(rainLast,2); Serial.print(" mm, ");
Serial.print("10-min: "); Serial.print(rain10min,2); Serial.println(" mm");
Serial.print(" Temp: "); Serial.print(tempC,1); Serial.print(" °C, ");
Serial.print("Heater PWM: "); Serial.print(pwmDuty,1); Serial.println(" %");
Serial.println();
}
}
How it works
- In
loop(), we flipPULSE_PINevery half‐interval so that each LOW→HIGH edge occurs everyINTERVAL_MS_PER_PULSE = 60 000 ms / PULSES_PER_MIN. - Those edges trigger the slave’s rain‐tip interrupt just as if a physical bucket tipped.
- Once per second we read registers 0,2,4,6,8 from the slave and convert raw units into mm, °C, and %PWM.
- We also compute “expected” rain in mm/min =
PULSES_PER_MIN × MM_PER_PULSE_SIMso you can compare generator vs. slave measurement.
Using the master’s settings in the example sketch:
• PULSES_PER_MIN = 30 pulses/min
• MM_PER_PULSE_SIM = 0.5 mm/pulse
Therefore the master “generator” produces:
– Pulses per minute: 30 p/min
– Rainfall per pulse: 0.5 mm/pulse
– ⇒ Expected rain rate:
30 p/min × 0.5 mm/p = 15 mm/min
Broken down further:
• 15 mm per minute
• 0.25 mm per second
If you change either parameter, just recompute:
expected rain (mm/min) = PULSES_PER_MIN × MM_PER_PULSE_SIM