I'm trying to transfer my EC sensor onto ATtiny, but having problems with the interrupt that does not trigger for some reason. The exact same code does work on an ATmega328 (my Pro Mini clone).
Basically what this code does: it charges up a capacitor, then discharges it through the liquid to be probed, and the moment the charge on the capacitor becomes low enough the CAPPOS pin goes from HIGH to LOW, triggering an interrupt. The time it takes between starting the discharge process and the pin triggered is measured using TCNT1 (without prescaler) and is a measure for the resistance of the liquid.
This I first developed on an ESP8266, which works great.
Typical discharge times are in the 1-1000 microseconds range, which is why I'm timing by counting clock cycles.
Then I tried to convert it to ATtiny (running it on a 85 for now, later it will be a 25 as the code is small enough), and it didn't work.
The same code I then ran on the ATmega328 - copy/pasting the readEC() function, using the same pins (PB1, PB3 and PB4) and using the general Pin Change interrupt rather than the more specific interrupts available on ATmega. This worked right away, proving the code is fine.
When again testing on the ATtiny, it didn't run. Every single measurement timed out - but checking the port immediately after the while loop showed turned low, though without triggering the interrupt. Testing the port inside the loop and breaking out of it when it turned LOW also worked, but due to the large overhead of reading micros() the timing is too imprecise.
I have no idea why the interrupt does not trigger - I can trigger it with a pull-up resistor and a jumper wire to GND just fine. So the pin is good. The TinyWireS library is also not the problem - I tested the interrupt with and without this library included (and the callbacks set up to prevent the optimiser kicking it all out - the library itself also relies on interrupts) and there was no difference.
Circuit diagram - without R1 and J3 (that's for an NTC probe - that part works fine):
Code - stripped to the bare basics, the only I2C communication possible is a scan by the I2CScanner (which correctly reports the address) and the command 0x01 which starts an EC reading, and returns the discharge time in the form of three bytes. Those bytes I've used before to get debug info such as number of interrupts triggered.
/*
I2C library used:
https://github.com/nadavmatalon/TinyWireS
*/
#include <TinyWireS.h>
#define CAPPOS PB1 // digital pin for cap and EC probe
#define CAPNEG PB4 // digital pin for cap and analog in for NTC probe.
#define ECPIN PB3 // digital pin for EC probe
#define CHARGEDELAY 40 // Time in microseconds it takes for the cap to charge; at least 5x RC.
// // 22 nF & 330R resistor RC = 7.25 us, times 5 = 36.3 us.
#define EC_TIMEOUT 2000 // Timeout for the EC measurement in microseconds.
// Time scale to go from clock pulses to microseconds.
// 1 MHz clock: 1 = 2^0 pulses per microsecond, TIMESCALE 0.
// 8 MHz clock: 8 = 2^3 pulses per microsecond, TIMESCALE 3.
// 16 MHz clock: 16 = 2^4 pulses per microsecond, TIMESCALE 4.
#if (F_CPU == 1000000)
#define TIMESCALE 0
#elif (F_CPU == 8000000)
#define TIMESCALE 3
#elif (F_CPU == 16000000)
#define TIMESCALE 4
#else
#error Unknown CPU clock speed, TIMESCALE undefined.
#endif
#define I2C_DEFAULT 0x4c // The default used if no address found in EEPROM.
// The available I2C commands.
#define READ_EC 0x00 // Read the EC probe - produce 2 bytes of data (the cycle count).
// Other I2C paramenters.
#define MAX_TRANSMISSION 3 // No more than three bytes in a single I2C transmission for this application, ever.
#define I2C_EEPROM 63 // EEPROM address where our I2C slave address is stored.
uint8_t data[MAX_TRANSMISSION]; // Stores the incoming or outgoing I2C data.
// Advance declaration of callback functions.
void wireRequest(void);
void wireReceive(void);
volatile uint16_t dischargeCycles = 0;
void setup() {
// TinyDebugSerial tinySerial= TinyDebugSerial();
// tinySerial.begin(38400);
TCCR1A = 0; // clear control register A
TCCR1 = 0; // clear timer control register
TCCR1 |= (1 << CS10); // Set to no prescaler
// Set up I2C interface and callback functions.
TinyWireS.begin(I2C_DEFAULT);
TinyWireS.onRequest(wireRequest);
TinyWireS.onReceive(wireReceive);
// Enable pin change interrupts.
GIMSK |= (1 << PCIE);
}
void loop() {
// Nothing to do here - everything is controlled by I2C commands.
}
/*
Take a single reading from the EC probe.
*/
void readEC() {
uint32_t totalCycles = 0;
for (uint8_t i = 0; i < 64; i++) {
// Stage 1: charge the cap, positive cycle.
DDRB |= (1 << CAPPOS); // CAPPOS output.
DDRB |= (1 << CAPNEG); // CAPNEG output.
DDRB &= ~(1 << ECPIN); // ECPIN input.
PORTB |= (1 << CAPPOS); // CAPPOS HIGH: Charge the cap.
PORTB &= ~(1 << CAPNEG); // CAPNEG LOW.
PORTB &= ~(1 << ECPIN); // ECPIN pull up resistor off.
TCNT1 = 0;
while (TCNT1 < (CHARGEDELAY << TIMESCALE)) {};// Wait for cap to charge.
// Stage 2: measure positive discharge cycle by measuring the number of clock cycles it takes
// for pin CAPPOS to change from HIGH to LOW.
dischargeCycles = 0;
TCNT1 = 0; // Reset the timer.
DDRB &= ~(1 << CAPPOS); // CAPPOS input.
DDRB |= (1 << ECPIN); // ECPIN output.
PORTB &= ~(1 << CAPPOS); // CAPPOS pull up resistor off.
PORTB &= ~(1 << ECPIN); // ECPIN LOW.
PCMSK |= (1 << CAPPOS); // Set up the pin change interrupt on CAPPOS.
while ((TCNT1 >> TIMESCALE) < EC_TIMEOUT) {
if (dischargeCycles)
break;
}
PCMSK &= ~(1 << CAPPOS); // Clear the pin change interrupt on CAPPOS.
totalCycles += dischargeCycles;
// Stage 3: charge the cap, negative cycle.
DDRB &= ~(1 << ECPIN); // ECPIN input
DDRB |= (1 << CAPPOS); // CAPPOS output
PORTB &= ~(1 << CAPPOS); // CAPPOS LOW
PORTB |= (1 << CAPNEG); // CAPNEG HIGH: Charge the cap
TCNT1 = 0;
while (TCNT1 < (CHARGEDELAY << TIMESCALE)) {};
// Stage 4: discharge the cap, compenstation cycle.
DDRB &= ~(1 << CAPPOS); // CAPPOS input
DDRB |= (1 << ECPIN); // ECPIN output
PORTB |= (1 << ECPIN); // ECPIN HIGH
// delay based on dischargeCycles
TCNT1 = 0;
if (dischargeCycles) {
while (TCNT1 < dischargeCycles) {};
}
else {
while (TCNT1 < (EC_TIMEOUT << TIMESCALE)) {};
}
}
// Disconnect EC probe: all pins to INPUT.
DDRB &= ~(1 << CAPPOS); // CAPPOS input
DDRB &= ~(1 << CAPNEG); // CAPNEG input
DDRB &= ~(1 << ECPIN); // ECPIN input
PORTB &= ~(1 << CAPNEG); // CAPNEG pull up resistor off.
PORTB &= ~(1 << ECPIN); // ECPIN pull up resistor off.
uint16_t averageCycles = (totalCycles >> 6);
// Return the actual dischargeCycles in data[1] and data[2], useful for debugging and used for calibration.
data[0] = 0;
data[1] = (averageCycles >> 8) & 0xFF;
data[2] = (averageCycles >> 0) & 0xFF;
return;
}
ISR(PCINT0_vect) {
dischargeCycles = TCNT1;
PCMSK &= ~(1 << CAPPOS); // Clear the pin change interrupt on CAPPOS.
}
void wireReceive(uint8_t n) {
// Make sure we don't try to read more bytes than that fit in our data structure.
if (n > MAX_TRANSMISSION)
n = MAX_TRANSMISSION;
for (uint8_t i = 0; i < n; i++) {
if (TinyWireS.available())
data[i] = TinyWireS.read();
else
break;
}
// Clear the buffer.
// There should not be more data in the buffer than the already received bytes, unless the master
// is misconfigured and sends more data than it should.
while (TinyWireS.available())
TinyWireS.read();
// Series of if/else if is slightly smaller than switch/case statements.
if (data[0] == READ_EC)
readEC();
// Any invalid commands are silently ignored.
}
void wireRequest() {
TinyWireS.write(data[0]);
TinyWireS.write(data[1]);
TinyWireS.write(data[2]);
}


