1

I'm using an MPC73831 - a LiPo charging IC - in my Arduino project. It has a status pin to allow interface to a microcontroller, which uses a tri-state operation to convey three possible states of charging. I don't have the IC and battery yet, but I wanted to write a quick program to test a generic tri-state pin for HI, LO, and HiZ while I'm waiting for it to arrive.

The algorithm I wrote is very simple:

Set pinTest to input, no pullup.
Is pinTest high?
    Yes - pinTriState is HIGH, done.
Set pinTest to input with pullup.
Is pinTest high?
    Yes - pinTriState is HiZ, internal pullup causes high reading.
    No - pinTriState is LOW, 'cancelling out' internal pullup.

However, I'm not getting the expected results when using the internal pullup resistor - my board is showing correct analysis for HI and LO, but HiZ returns a mixture of HiZ and HIGH. I have tried this on an Uno, a Pro-Micro (32u4) and Uno->Micro and Micro->Uno. All give the same spurious results. I am baffled, therefore some advice would be appreciated. The full code is as below. I have left in code which randomises the time pinTriState stays at in a given state to keep pinTest guessing, but it's ultimately only an elaborate way of cycling through the three states.

#define pinTest 2
#define pinTriState 3

int state = 0;
char stateCh = '?';

int sect1;
int sect2;
int sectMax;

void setup()
{
    Serial.begin(9600);
    randomSeed(analogRead(A0));
    sect1 = random(1, 20);
    sect2 = random(sect1 + 1, random(sect1 + 2, sect1 + 20));
    sectMax = random(sect2 + 1, random(sect2 + 2, sect2 + 20));
}

void loop()
{
    // PART (1) Code to toggle three states of a tri-state pin
    if (state == 0)
    {
        pinMode(pinTriState, OUTPUT);
        digitalWrite(pinTriState, HIGH);
        stateCh = 'H';
    }
    if (state == sect1)
    {
        pinMode(pinTriState, OUTPUT);
        digitalWrite(pinTriState, LOW);
        stateCh = 'L'
    }
    if (state == sect2)
    {
        pinMode(pinTriState, INPUT);
        stateCh = 'Z';
    }

    // PART (2) Code to determine state of tri-state pin
    Serial.print(stateCh);     // Shows what it *should* be
    pinMode(pinTest, INPUT);
    if (digitalRead(pinTest))
        Serial.println(" - HIGH");
    else
    {
        pinMode(pinTest, INPUT_PULLUP);
        if (digitalRead(pinTest))
            Serial.println(" - HiZ");
        else
            Serial.println(" - LOW");
    }

    (state == sectMax ? state = 0 : state++);
}

The necessary pins are connected with a single wire. If I upload the code to two boards I remove PART (2) on the 'triState' board and PART (1) on the 'testing' board. In this case, stateCh has no function.

For a single-board setup, I get the following output:

H - HIGH
H - HIGH
...
L - LOW
L - LOW
...
Z - HiZ        <-- the first Z is correct 95% of the time
Z - HIGH
Z - HIGH       <-- the remaining Zs are a mixture, biased heavily towards HIGH.
Z - HiZ
...
H - HIGH       <-- H always gives HIGH
...

It makes no difference if I change the operating order of pinState. I've thrown in delay(100); between almost every other line, again to no avail.

I tried with a 22k, a 33k, and a 47k external pullup resistor. The results are perfect. However, this requires an extra Arduino pin to make sure that the pullup is only active when necessary. Part (2) becomes:

// PART (2) Code to determine state of tri-state pin
Serial.print(stateCh);     // Shows what it *should* be
pinMode(pinTest, INPUT);
pinMode(pinPullup, INPUT);    // connected to pinTest via resistor
if (digitalRead(pinTest))
    Serial.println(" - HIGH");
else
{
    //pinMode(pinTest, INPUT_PULLUP);   <-- not anymore
    pinMode(pinPullup, OUTPUT);
    digitalWrite(pinPullup, HIGH);    // pulls pinTest to Vdd
    if (digitalRead(pinTest))
        Serial.println(" - HiZ");
    else
        Serial.println(" - LOW");
}

Using the extra pin to provide the pullup is a real pain in the ar...duino. I'm cripplingly short of spare pins and I don't see why this is not resolvable with internal pullups. Help?!

  • "but HiZ returns a mixture of HiZ and HIGH". That's exactly what you expect for an input pin with no pull-up, and a high-impedance (or no) connection. I would be surprised if it didn't alternate between LOW and HIGH somewhat randomly. – Nick Gammon Jun 28 '15 at 1:20
  • @NickGammon: But the input is pulled up - pinMode(pinTest, INPUT_PULLUP) is the first line in the else block. And that doesn't answer why it works with an external pull-up resistor that's turned on at the right time in leiu of the internal pull-up. – CharlieHanson Jun 28 '15 at 1:27
  • Yes, but setting the pin to input with no pull-up or pull-down (your first test) gives meaningless results. See my code example in the amended reply. It works. – Nick Gammon Jun 28 '15 at 2:54
  • I understand your point know. However, that doesn't explain why using an external pullup doesn't exhibit the same problem - bearing in mind that the 'up' leg of the resistor is switched between a HiZ and HIGH source, i.e. it is switched in and out the same as the internal one would be. Please note that I'm not disagreeing - I'm happy that I can solve this with a simple pull-down. Is it that the leakage of pinPullup happens to be biased towards Vcc, keeping the resistor slightly pulled up at all times? Why never pulled down by a leakage? – CharlieHanson Jun 28 '15 at 8:21
  • A pin which is input connected to a high-impedance source is undefined. So I won't try to explain why undefined is more often high than low. Maybe right now an adjacent pin pulls it high. In another scenario it might be pulled low. Your external pull-up solution is, if I remember correctly not at all the same thing because the pin always has a defined state. – Nick Gammon Jun 28 '15 at 9:13
4

The internal pull-up is around 50k. So, install an external pull-down (to ground) of a somewhat higher value, like 1M.

Now:

Go to input mode, no pull-up. Get a reading. Turn on the internal pull-up. This should overpower the pull-down. If the reading changes, it was tri-state. Otherwise, if the reading doesn't change the external device is overpowering both pull-up and pull-down and you have the actual reading.

You may need a slight delay to allow the low-value pull-down to change the pin state.


Amended code to demonstrate:

// PART (2) Code to determine state of tri-state pin
Serial.print(stateCh);     // Shows what it *should* be

pinMode(pinTest, INPUT);
delayMicroseconds (100);
byte firstReading = digitalRead(pinTest);
pinMode(pinTest, INPUT_PULLUP);
delayMicroseconds (10);
byte secondReading =  digitalRead(pinTest);

if (firstReading != secondReading)
  Serial.println(" - HiZ");
else if (secondReading == HIGH)
  Serial.println(" - HIGH");
else
  Serial.println(" - LOW");

(Only showing the part that detects the pin state, the rest is the same).

Output:

H - HIGH
L - LOW
Z - HiZ
Z - HiZ
Z - HiZ
H - HIGH
H - HIGH
H - HIGH
H - HIGH
H - HIGH
H - HIGH
H - HIGH
L - LOW
Z - HiZ
Z - HiZ
Z - HiZ
H - HIGH
H - HIGH
H - HIGH
H - HIGH
H - HIGH
H - HIGH
H - HIGH
L - LOW
Z - HiZ
Z - HiZ
Z - HiZ
H - HIGH
H - HIGH
H - HIGH

As you can see, working perfectly. I needed a longer delay after turning the internal pullup off because the fall time was around 100 µs. This actually sounds reasonable, because the 1 megohm resistor will take time to discharge the capacitance on the pin.

So:

  • 10 µs delay after turning the internal pull-up on

  • 100 µs delay after turning the internal pull-up off


Theory for the delays

The Atmega328 pin is specified as having a maximum capacitance of 10 pF (10e-12). It is considered charged/discharged after 5 time-constants, where T = R.C.

Thus for an internal pull-up resistance of 50k and a capacitance of 10 pF, charging should take:

50e3 * 10e-12 * 5 = 2.5e-006 (0.0000025) -> 2.5 µs

And to discharge through 1M (1e6) ohms, discharging should take:

1e6 * 10e-12 * 5 = 5e-005 (0.00005) -> 50 µs

My observed results were about 2.5 µS for the charge, but more like 100 µs for the discharge, however I probably had considerable stray capacitance in the wires used in the test circuit.

  • Testing shows that with a 1 megohm pull-down resistor, it takes around 5 µS for the pull-up to change the pin's state. So a delayMicroseconds (10) or thereabouts should be plenty, to allow the pin time to react to the HiZ situation. – Nick Gammon Jun 28 '15 at 1:41
  • However discharging the pin through the 1 megohm pull-down takes somewhat longer. 100 µS or more. So the code above shows the longer delay after turning off the internal pull-up. The sensible thing to do would be to turn the internal pull-up off right after testing it, then it could be discharging while the code did something else. – Nick Gammon Jun 28 '15 at 3:05
3

Another option is to use an analog input pin instead of a digital input pin.

Create a balanced voltage divider using equal high value resistors (say 100KΩ each). Attach the mid-point to the ADC pin, and one end to each of Vcc and GND. The incoming status signal goes to the ADC pin:

enter image description here

Perform an analogRead() on the pin. You will get one of 3 results:

  • Close to 0 = LOW
  • Close to 1024 = HIGH
  • Close to 512 = HiZ

When the pin is in HiZ the resistors will hold the voltage at around 50% of Vcc. When the pin goes HIGH it will pull the pin up and override the resistors, giving a high value reading. Similarly, when the pin goes low, the resistors will be overridden and a low value reading will be given.

  • +1; This would have been my solution if I had a spare ADC input. Alas, the Pro-Micro isn't blessed in the pins department. – CharlieHanson Jun 28 '15 at 22:09
1

Nick Gammon's answer seems the most straightforward solution to your problem, given that you cannot spare an analog input. It has however the drawback of needing a delay of ≈ 100 µs. This may not be an issue, but I just figured out a scheme that allows a faster read, and may be of interest to some: instead of pull-ups or pull-downs, put just a 10 kΩ resistor in series between the MPC73831's output and you Arduino input. When you want to read the state:

  1. Take a first reading with the pin in INPUT mode:
    • if you read HIGH, then your MPC is either HIGH or HI_Z
    • if you read LOW, then it's either LOW or HI_Z
  2. Now, switch the pin to OUTPUT, write the opposite of what you just read, switch back to input and take a second reading:
    • if you read what you just wrote, then your MPC is in the HI_Z state
    • if you read the same as before, then it's either HIGH or LOW.

Here is the implementation of this scheme:

#define HI_Z 2

/*
 * Read a 3-state pin.
 * Put a 10 kOhm resistor between the output and input pins.
 */
int read3state(int pin)
{
    int reading1 = digitalRead(pin);
    pinMode(pin, OUTPUT);
    digitalWrite(pin, !reading1);
    pinMode(pin, INPUT);
    int reading2 = digitalRead(pin);
    if (reading1 == reading2)
        return reading1;
    else
        return HI_Z;
}

I have tested this a few 104 times with an Arduino sending to itself a pseudo-random sequence of LOW, HIGH and HI_Z. It works reliably for me.

Caveat: This scheme relies on the pin being capacitively coupled to GND or Vcc, and this coupling being stronger than couplings with other signals in your circuit. If you fear this might not be the case, you can put something like 100 pF from the pin to ground, but then you should better wait a couple of microseconds before taking the second reading.

0

For the 1M pulldown solution, turning off the internal pullup at the end of the pin check routine MIGHT give enough time for the pin to discharge, and it might not if the code can conditionally do very little else between pin checks. If you want to leave out the 100 us delay, take care to ensure that your code will always do something that takes sufficient cycles for the pin to discharge between checks.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.