[SOLVED] ATtiny85 ADC read problem

Hi all,

I’m trying to use an ATtiny85 to read two sensors: an EC sensor and an NTC. The ATtiny is interfaced with an Arduino using I2C. The EC sensor needs three pins, the NTC one pin, the I2C two, for a total of six and I have only five (I don’t want to start using the RESET as output as it makes programming much harder). So the NTC shares one pin with the EC sensor - this should work as that one pin is used as output only when measuring the EC, and as input to measure the NTC. The external components shouldn’t affect one another.
When using as output, the port an easily source/sink the current needed to pull it completely high or low, and charge the capacitor as needed.
When not measuring the EC, all three pins are set to INPUT - basically to disconnect the sensor. Then the cap will just cause a bit of stray capacitance on the ADC pin. It may take a few milliseconds to settle its value, with a signal that changes very slowly that’s no problem.
Schematic:


Now this works fine, until I do an EC reading. After that the value read from the ADC jumps quite a bit higher.
The normal reading is about 454 for my current room temperature (28-29°C), but after reading the EC the reading jumps to 520 and stays there until I power cycle the ATtiny, and it goes back to normal.

Of interest: the ADC does continue to change value as the probe heats up (using my body heat) or cools down (to room temperature), and the “high” value is constant and predictable. This makes me believe there’s a systemic issue, I just have no idea what it could be.

Attached is the actual sketch as it is running on the ATtiny (it’s too big to paste inline). The readEC() function doesn’t work - I have not tried to debug this yet. Of note, this is now on an ATtiny85, I hope to move to an ATtiny45 (the sketch is some 3300 bytes now - that floating point math adds a lot to the size!).

The schematic as shown above I soldered on a piece of protoboard, with the ATtiny85 as DIP-8 placed in a socket. The 10k RESET resistor I did not include for prototyping.

EC_ATtiny.ino (12.5 KB)

Happy to report I manage to solve it. I suddenly had a bright idea, and that was it.

The cause of the problem: the internal pull up was activated on CapNeg.

When a pin is set to HIGH (and the corresponding bit in PORTB set), then upon changing the direction of the pin from by setting its bit in DDRB, the bit in PORTB does not get reset. This means that automatically the input pull-up gets activated. This resistor (should be 20-30k in value) is in effect parallel to the 10k pull-up, reducing the total value to about 7k, and that perfectly explains the values I saw on the ADC.

Solution: reset those bits in DDRB.

Come to think of it, it makes sense. I'm sure the pinMode() function takes care of that when setting a pin to input.

Another thing that I noticed when trying to measure the resistance (to make sure my pull-up was correct) is that the internal pull-up also acts as diode: when measuring one way I got the expected 10k, in the other direction 7-8k. That is also consistent observations I read about the value of this resistor, which appears to go up when Vcc goes down - a resistor in series with a diode would show exactly this thanks to the fixed voltage drop over the diode.

I get a compile error EC_ATtiny:80: error: 'TCCR1A' was not declared in this scope, otherwise everything else OK.
Thanks for posting anyhow

Interesting, as it's the exact code I can compile successfully for my ATtiny using the Arduino IDE. No complaints about the TCCR1A register.

Hello

A bit more information on my setup
IDE is 1.8.4
Using ATtiny Universal core (Spence Konde)
Board = ATiny25/45/85
Timer 1 Clock : CPU
Chip ATtiny85
Clock 8MHz internal
BOD disabled
LTO disabled
Programmer Parallel Programmer

TCCR1A=0; // clear control register A
TCCR1 = 0; // clear timer control register
TCCR1 |= (1 << CS10); // Set to no prescaler

change TCCR1A=0; to TCCR0A=0; compiles

I freely admit to being no expert at this level, but using this core has behaved up until now.
I would be interested to hear what you think as I’m quite keen to use the Tiny’s more.

Sorry, no idea why that difference. I've been following some tutorials I found online, without deep understanding of the timers so far.

I'm also having big problem with interrupts: they just don't register. I strongly suspect the TinyWireS library from interfering, most likely that one also uses interrupts. I have to read that library and see what happens.

So I tried to rewrite that part, but no luck. The EC probe times out all the time it seems, and TCNT1 is not read properly if at all.

And to make matters worse one of my two ATtiny85 chips has stopped working, it programs fine but does not respond to the I2C any more. The other one does work... That was another hour or so of serious frustration. These things are not easy to work with for lack of much of a user interface, including the Serial console and the need of plugging in the programmer and switching for the reader time and again. Lots of work for every little code change.

The next stage of my experimenting is going to be on my Pro Mini clone, much easier to work with, using only the timers/interrupts that are available on the ATtiny as well. I mostly want to understand the pin change interrupt - but will need to check out the TinyWireS library as well to make them cooperate.

Hello
I was wondering if you were using a different ATtiny core.
The ATtiny 841 has I2C on it, but in SOT only. Spence Konde's (DrAzzy's) site is worth a look as he has a board with the 841 on it (sold on Tindie). I got it to work fairly easily.
Also have a look at Nick Gammon's site (Gammon.au or similar) and he has some nice articles on ATtiny85 ppin change interrupts which work well, as you might expect.
Technoblogy is also a nicely put together site with some very useful examples of using low level code, thoroughly recommend.
I would agree that they aren't easy to work with, but very satisfying when they do.
Have you also had a look at HighTech Low Tech (or similar name, MIT) with some good basic stuff on ATtiny85.
Cheers

I’m using the Arduino IDE, and downloaded added this core file to it: https://raw.githubusercontent.com/damellis/attiny/ide-1.6.x-boards-manager/package_damellis_attiny_index.json
No idea if there are others out there, but it shouldn’t make a difference for register names.

Some major progress made: I got my EC measuring code to work on my Pro Mini (ATmega328)! So much easier with Serial and no need to plug wires to reprogram the thing and so… Now all that’s left is checking ATtiny register names and adding some #define tags and it should compile on both platforms.

The PCICR is key to making it work. sei() and cli() don’t appear to do anything. I also had to explicitly enable and disable the interrupt in PCMSK0 to only trigger when needed.

Using an interrupt is necessary to get any reasonable resolution: I couldn’t get better than 61 clock ticks when doing the pin test inside the time-out loop. This time-out in turn is necessary to prevent it from sitting there forever if the EC probe is not connected.

Anyway, here the unpolished ATmega code for the EC probe.

#include <EEPROM.h>

#define CAPPOS PB1                            // Digital pin 9 for CapPos.
#define CAPNEG PB4                            // Digital pin 12 for CapNeg.
#define ECPIN PB3                             // Digital pin 11 for EC probe

//#define NTC_ADC 2                             // The ADC that's linked to the CAPNEG pin.
#define CHARGEDELAY 30                        // Time in microseconds it takes for the cap to charge; at least 5x RC.
#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

volatile uint32_t dischargeCycles;

void setup() {
  TCCR1A = 0;                                 //  clear control register A

// ATtiny:
//  TCCR1 = 0;                                  //  clear timer control register
//  TCCR1 |= (1 << CS10);                       //  Set to no prescaler

// ATmega:
  TCCR1B = 0;                                 //  clear timer control register
  TCCR1B |= (1 << CS10);                      //  Set to no prescaler

  // Enable pin change interrupts.
  // ATtiny:
//  GIMSK |= (1 << PCIE);

  // ATmega328 (Arduino):
  PCICR |= (1 << PCIE0);                      // set PCIE0 to enable PCMSK0 scan

  Serial.begin(115200);
}

void loop() {

  readEC();
  Serial.println();
  delay(2000);
 
}

/*
   Take a single reading from the EC probe.
*/
void readEC() {

  uint32_t totalCycles = 0;
  uint8_t timeouts = 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.
    delayMicroseconds(CHARGEDELAY);               // wait for cap to charge.

    // Stage 2: measure positive discharge cycle.
    dischargeCycles = 0;
    DDRB &= ~(1 << CAPPOS);                       // CAPPOS input.
    DDRB |= (1 << ECPIN);                         // ECPIN output.
    PORTB &= ~(1 << CAPPOS);                      // CAPPOS pull up resistor off.
    PORTB &= ~(1 << ECPIN);                       // ECPIN LOW.
    TCNT1 = 0;                                    // Start the timer.

    uint32_t startMicros = micros();
    PCMSK0 |= (1 << CAPPOS);          // Set PCINT1 to trigger an interrupt on state change 
    while (micros() - startMicros < EC_TIMEOUT) {
      if (dischargeCycles) {
        break;
      }
    }
    PCMSK0 &= ~(1 << CAPPOS);          // Clear the pin change interrupt on CAPPOS.
    totalCycles += dischargeCycles;
    if (dischargeCycles == 0)
      timeouts++;
        
    // 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
    PORTB &= ~(1 << ECPIN);                       // ECPIN pull up resistor off.
    delayMicroseconds(CHARGEDELAY);               //  wait for cap to charge

    // Stage 4: discharge the cap, compenstation cycle.
    DDRB &= ~(1 << CAPPOS);                       // CAPPOS input
    DDRB |= (1 << ECPIN);                         // ECPIN output
    PORTB &= ~(1 << CAPPOS);                      // CAPPOS pull up resistor off.
    PORTB |= (1 << ECPIN);                        // ECPIN HIGH

    // delay based on dischargeCycles
    if (dischargeCycles)
      delayMicroseconds(dischargeCycles >> TIMESCALE);
    else
      delayMicroseconds(EC_TIMEOUT);
  }

  // Disconnect EC probe: all pins to INPUT.
  DDRB &= ~(1 << CAPPOS);                         // CAPPOS input
  DDRB &= ~(1 << CAPNEG);                         // CAPNEG input
  DDRB &= ~(1 << ECPIN);                          // ECPIN input
  PORTB &= ~(1 << CAPPOS);                        // CAPPOS pull up resistor off.
  PORTB &= ~(1 << CAPNEG);                        // CAPNEG pull up resistor off.
  PORTB &= ~(1 << ECPIN);                         // ECPIN pull up resistor off.

  uint16_t averageCycles = (totalCycles >> 6);

  Serial.print("Timeouts: ");
  Serial.print(timeouts);
  Serial.print(", total cycles: ");
  Serial.print(totalCycles);
  Serial.print(", average cycles: ");
  Serial.println(averageCycles);

  return;
}

ISR(PCINT0_vect) {
  dischargeCycles = TCNT1;
}

Hello
I'll run the new code.
I suspect my compile problem was possibly down to the ATtiny core differences, but I stand to be corrected on that.
Nice to get it running anyhow.
If I get a moment, I'll change the ATtiny core to the one you are using to see if it's that.
Again I might be wrong, but I was recommended to use the Spence Konde core instead of the original DMellis version.
Signing off for now

The latest code will not compile on ATtiny as it uses ATmega register names, needs some amendments - but I'll also have to add back the I2C parts to have at least a means of getting data back out of the ATtiny :slight_smile: