Looking at pulled-up-pin Rise Times with an Oscilloscope

This article describes a technique for looking at rise times and switching-threshhold voltages on Arduino input pins that have been pulled up, via an internal or external pull-up.

The technique can be adapted to look at other Arduino features with a scope, but time-resolution will drop in most other cases, because they are likely to use more cycles per step. The method here allows measurement of pin-switch times with two cycles resolution, which is 0.125 microsecond (us) on a 16MHz Arduino. Measurement-loop time is 10 us, so the measurement loop repeats 100000 times per second.

Note: When testing digital input pins as outlined here, do not attach any power sources (such as other pins) to the pins under test (ie, any of the port A pins for the included sketch). At the start of each measurement cycle, the code briefly sets each test pin to be a low output, which will cause a short circuit (and chip damage) if a non-zero voltage is attached. It is ok to ground a pin or to attach a passive R-L-C circuit or a high-impedance source.

This article is organized in several sections: 1, physics and hardware notes; 2, the rise-time measurement method; 3, code listings; and 4, a brief bibliography, with links.

Section 1. Physics and other background notes

Section 1 has four parts:

 1.1 A pulled-up AVR pin charges up before turning on
 1.2 Regarding internal I/O pin pull-up resistance
 1.3 Regarding pin threshhold voltage
 1.4 Format of in-line assembly code in the sketch

1.1 A pulled-up AVR pin charges up before turning on

When an input to an Arduino pin that was held low is switched to a pulled-up input pin, it takes time for the pin to go high due to parasitic stray or probe capacitance that must be charged before the pin can register as high. The basic circuit of an Atmel AVR pin with capacitance C and pull-up R is sketched below. Pxx is the internal pin connection, which goes high when its switching-voltage threshhold is reached, some number of nanoseconds or microseconds after the pull-up is attached.

When a capacitor charges through a resistor, the voltage across it increases with time t as V*(1-exp(-t/tau)), where V is voltage applied to the resistor and tau=R*C is a time constant. Tau is in seconds when R is in ohms and C in farads. Pxx in the figure will change from low to high sometime within R*C seconds of when voltage is applied through R, because in the first tau seconds of charging, the voltage across C increases to 63.2% of V and because Atmel AVR input pins definitely register high when more than 0.6*Vcc is applied. See Wikipedia article RC time constant for more information.

Here is a concrete example. Suppose that pull-up resistance Rp is about 40 kΩ; that Pxx goes high at about Vt=0.5*Vcc; that C is about 10 pF (10 picofarads, or 0.01 nanofarad, or 10⁻¹¹ farads). Then RC = 40000 * 10⁻¹¹ = 4* 10⁻⁷ seconds = 0.4 us (400 ns).

If t and Vt [the threshhold voltage] and either R or C are known, C or R can be computed via C = -t/(R*ln(1-Vt/V)) or R = -t/(C*ln(1-Vt/V)).

1.2 Regarding internal I/O pin pull-up resistance

An AVR I/O pin pull-up resistor is a FET circuit that under program control can connect the input pin via a nominal resistance to Vcc, allowing the pin to pull up to Vcc if not otherwise influenced.

Table 30-1 and Table 31.1 in ATmega datasheets (see Bibliography) say that internal I/O pin pull-up resistance is 20 kΩ min, 50 kΩ max. I measured a pin resistance at about 40 kΩ, as shown below.

Let Rp=pull-up resistance, Rs=standard resistance, Rm=DVM input resistance, Vp = voltage reading when pin D of voltmeter is connected to Vcc, and Vs = voltage reading when pin D of voltmeter is connected to ground. By the voltage divider formula, if Rm is infinite then Vp = Vcc * (Rp/(Rp+Rs), from which Rp = Rs*Vp/(Vcc-Vp). With pin 11 of an Atmega2650 set via pinMode(11, INPUT_PULLUP); I read Vp = 1 V, Vcc = 4.89 V, giving Rp = 39 kΩ.

  The board was powered via USB when I measured Vcc = 4.89 V.
  Vcc was 4.98 V when scope photos were taken.

Because Rm of my meter is not infinite, I also measured Vs, and by formula Rp = Rs*(Vcc-Vs)/Vs computed Rp = 43 kΩ. When Rm is in parallel with Rp, Rp will be underestimated; when Rm is in parallel with Rs, Rp will be overestimated. Conclusion: Rp on pin 11 of this Atmega2650 under these conditions is about 40 kΩ.

1.3. Regarding pin threshhold voltage

The time delay between when a pull-up is connected to a pin and when the pin goes high depends on the pull-up resistance; the pull-up voltage; capacitance at the pin; and the threshhold level at which the pin registers high. Other factors may apply (capacitor type and initial charge, rate of change of signal, hysteresis, cross-talk, etc) but won't be discussed here.

Under most conditions, Atmel AVR input pins are guaranteed to register high when more than 0.6*Vcc is applied, and to register low when less than 0.3*Vcc is applied. [See Table 30-1 or Table 31.1 in ATmega datasheets per Bibliography.] Those datasheets don't spell out how input pins behave when between those two points.

In my experiments, it appears the port A pins on my Atmega2650 go high at about 0.5*Vcc, ie between 2.5 and 2.6 V with Vcc=4.98 V.

1.4 Format of in-line assembly code in the sketch

Inline assembly code in this sketch is formatted to comply with gcc rules for inline assembly. For more information see part 7 of the AVR libc user manual per Bibliography. The general form of an asm statement is

  asm(<string constant with code> : <outputs> : <inputs> : <clobbers>);

In C, if several string constants appear separated by white space, their concatenation is treated as a single string constant. I used white space for cosmetic formatting to make the assembly code readable. The sequence \n\t in the code string tells C to end the current line of assembly and start the next line with a tab.

Section 2. The rise-time measurement method

As mentioned above, this method runs a measurement loop that is 10 us long and repeats 100000 times per second. The loop starts by pulling the pins of output port A low for a microsecond, then returns A to being pulled-high inputs. In the next nine microseconds of each loop pass, the program copies pins of A to port C, about five dozen times.

That is, the program spends most of its time reading register A and writing it to port C. The program also uses two bits of port B to create scope trigger signals. Cycle-by-cycle details of the measurement loop are given in comments in the program. This method allows observation of switching threshholds with reasonable accuracy, and measurement of pin-switch times with two cycles resolution (0.125 us on a 16MHz Arduino).

Here are three photos of scope displays using this sketch. The first photo shows 1.9 loop passes. The lower trace is from a 1X scope probe and apparently significant capacitance; as seen in the upper trace, PC1 went high 4 us after PA1 was pulled high.

The second photo shows 4.5 loop passes. The lower trace illustrates the 1-microsecond-wide high-going pulse on PB1 that can be used as a scope trigger. Although it isn't shown, I used the low-going pulse on PB0 as the main trigger for all the scope photos.

The third photo shows half a pass, with a 10X scope probe attached to PA1. PC1 (lower trace) went high 1 us after pull-ups were applied, indicating only one-fourth as much stray, parasitic, or probe capacitance than in the first photo's case.

Section 3.

Listing of sketch, risetimeScoping.ino

(:source lang=c)

 /* risetimeScoping -- Make a scope loop for looking at rise times on
 pulled-up pins.  JI Waldby - 17 Feb 2015

 This sketch makes the pins of port C be outputs with values copied
 from the pin register of port A.  90% of the time pins of A are pulled
 high.  That is, we have a 10-microsecond-long loop in which A is held
 low for 1 microsecond, then pull-ups are set, and for 9 microseconds A
 inputs are copied to C outputs every 2 cycles.

 The assembly code does the following steps, at indicated cycle #'s in
 the loop:

 Cycle#   Op
   0      Raise PB0, drop PB1 (scope sync signals)
   1      Let port A pins be low inputs (instead of high inputs)
   2	 Let port A pins be low outputs
  3-15    nop's to fill to 1 us   
  16      Raise PB1, drop PB0
  17      Let port A pins be low inputs
  18      Let port A pins be high inputs (pulled-up)
  19...156 (Repeat next two steps to fill up rest of 10 us loop time)
          Read port A into R17
          Write R17 to port C
  158     Branch to step 0
  159     "

 The general idea is to issue scope sync signals on PB0 and PB1, then
 pull-up the A pins so they start rising from low to high.  When the
 capacitively-delayed signal on an A pin gets high enough, it will read
 as high; that reading will be copied a cycle later to the C port.

 Note, perhaps we should also disable millis() and microseconds()
 timing in setup() but this first version doesn't do so.
 void setup() {
   // Pin 11 may be usable for static pulled-up test-lead signal
   pinMode(11, INPUT_PULLUP);
   DDRA = 0;   // make the pins of A be inputs.
   DDRB = 3;   // make PB0 and PB1 be outputs and other B pins be inputs.
   DDRC = ~0;  // put the pins of port C as outputs
   PORTA = 0;  // make the pins of A be low.
   PORTB = ~3; // init B pins
 #define dup3(t)  t t t
 #define dup13(t) t t t t t  t t t t t  t t t
 #define dup23(t) t t t t t  t t t t t  t t t t t  t t t t t  t t t

 void loop() {
   asm volatile (
      ".equ aPINA,0   \n\t"   // Tell asm values of some symbols
      ".equ aDDRA,1   \n\t"
      ".equ aPORTA,2  \n\t"
      ".equ aPORTB,5  \n\t"
      ".equ aPORTC,8  \n\t"
      "ldi r20,0   \n\t"    // To set PA for all inputs, or to zero PA
      "ldi r21,253 \n\t"    // To Raise PB0, Drop PB1
      "ldi r22,254 \n\t"    // To Drop PB0, Raise PB1
      "ldi r23,255 \n"      // To set PA for all outputs

      "LU1:      \n\t"	 // Loop:   [Intended to be 160 cycles, 10 us, long]
      "out aPORTB,r22 \n\t" //  0  Toggle PB0,PB1 for scope sync signal
      "out aPORTA,r20 \n\t" //  1  Let port A pins be low
      "out aDDRA,r23  \n\t" //  2  Let port A pins be low outputs
      dup13("nop     \n\t") //     Delay to fill out first microsecond
      "out aPORTB,r21 \n\t" // 16  Toggle PB0,PB1 for scope sync signal
      "out aDDRA,r20  \n\t" // 17  Let port A pins be low inputs
      "out aPORTA,r23 \n\t" // 18  Let port A pins be high inputs
      // Now repeat "in r17,aPORTA" and "out aPORTC,r17" to read port A
      // into R17 and write R17 to port C enough times to fill out the loop.
      // We need T-8-N cycles, if T=160=total for loop and N=#nops=13+1
      dup23(dup3("in  r17,aPINA\n\tout aPORTC,r17\n\t"))
      "nop       \n\t"      // 157    fill out odd cycle       
      "rjmp LU1  \n\t"      // 158,9  Jump to step 0
      :   // : Outputs
      :   // : Inputs
      : "r17","r20","r21","r22","r23" // : Clobbers

Section 4: Bibliography

A brief guide to background material, with links

Gnu ARM inline-assembly quick reference pdf -- Explains more about using inline assembly in GNU tools

nongnu.org's AVR libc site

The AVR libc user manual in which part 7 (pp. 38-50) describes inline asm statements

An alternate (old) ref for gcc inline assembly can be found by web-search for GCCAVRInlAsmCB.pdf

RC time constant -- Wikipedia article about RC time constants, with some calculated examples

Voltage Divider -- Wikipedia article about voltage divider circuits, as mentioned in section 1.2 regarding internal I/O pin pull-up resistance.

ATmega datasheets, 650-page guide, "Atmel-8271-8-bit-AVR-Microcontroller-ATmega48A-48PA-88A-88PA-168A-168PA-328-328P_datasheet_Complete"

ATmega datasheets -- 447-page guide for ATmega640, ATmega1280, ATmega1281, ATmega2560, ATmega2561