Thread: Teensy 4.0 and SPI - problems with number of transactions in an interrupt rou

Status
Not open for further replies.

rt54321

Member
[Reposted from General forum - this is a technical support question]
Greetings All!

I'm using the Teensy 4.0 with an external ADC (the ADS8861, which is a 16 bit, SPI output ADC). I'm in the testing phase, and I'm getting erroneous outputs on the SPI data and clock lines.

The yellow trace is the "conversion start" pin. I'm triggering ADC conversions at 10kHz. The blue trace is the SPI clock line. My complete code is here:


Code:
 /* RGB Analog Example, Teensyduino Tutorial #2
   http://www.pjrc.com/teensy/tutorial2.html

   This example code is in the public domain.
*/
#include <SPI.h>

#define HWSERIAL Serial1

const int adcTriggerPin = 2; //PWM to the ADC ("start conversion" pin)
const int adcReadyPin = 3;  //Interrupt from the ADC data (rising edge is trigger that ADC is done)

//On the teensy, we also have:
//MISO: pin 12
//CLK: pin 13


volatile unsigned int interrupt_counter;
volatile uint32_t  ADCbyte1, ADCbyte2;
volatile int32_t  ADCresult;
volatile uint8_t  print_flag;
int  loop_counter;

/************************************
SPI pins:
pin 12 = MISO
pin 13 = SCLK
************************************/

// set up the speed, mode and endianness of SPI devices:
SPISettings settingsADC(10000000, MSBFIRST, SPI_MODE2);  //SPI_MODE2?

void setup()   {                
  Serial.begin(9600);


  pinMode(adcTriggerPin,OUTPUT);
  analogWriteFrequency( adcTriggerPin, 10000);  
 
  pinMode(adcReadyPin, INPUT);
   
  delay(1000);

  // initialize SPI:
  SPI.begin();
 
 
 
  /* Interrupt attacments:      (pin, callback func, edge or change) */
  attachInterrupt(adcReadyPin, ADC_ready, RISING);

  analogWrite(adcTriggerPin, 150); //Start the ADC converstion trigger
}

void loop()                    
{
   
        loop_counter++;
        Serial.print("Loops: ");
        Serial.println(loop_counter);      
        Serial.print("ADCbyte1: ");
        Serial.println(ADCbyte1);
        Serial.print("ADCbyte2: ");
        Serial.println(ADCbyte2);        
        Serial.print("ADC result: ");
        Serial.println(ADCresult);
        Serial.print("Interrupt counter: ");
        Serial.println(interrupt_counter);
    delay(1000);
}



void ADC_ready(void)
{
    noInterrupts(); //Disable interrupts    
    //ALL variables in here must be volatile
    interrupt_counter++;

    for (volatile int time_waster = 0; time_waster < 100; time_waster++)
    {
      volatile int j = time_waster + 1;    
    }
   
    SPI.beginTransaction(settingsADC);
    ADCbyte1 = SPI.transfer(0);
    ADCbyte2 = SPI.transfer(0);
    ADCresult = (int32_t)((ADCbyte1 << 8) + ADCbyte2);    // * 5.0f / 32768.0f;
    SPI.endTransaction();
    interrupts(); //Enable interrupts
}

When I debug my ADC result, I get an erroneous value of ADCresult = 65535. Here's the question: In my interrupt, I call on 2 SPI.transfer() transactions (8 bits + 8 bits = 16 bits in total, for the ADC result). In the oscilloscope capture, there are 3 blocks of SPI clocks (the blue trace). This tells me that 3 SPI transactions are happening, even though I only called for 2. How can this happen? I'm not sure why my SPI is "triggering" too many times.

DS0007.png
 
the attachInterrupt() is fine with a pin number for Teensy 3/4.

if i run your sketch with nothing attached to SPI and pin 2 jumpered to pin 3, it works for me. On scope I see only 2 SPI transfers each time the PWM pin rises. Your scope image shows SPI tranfers taking most of the high pulse time of the PWM. On my T4, the SPI transfers takes only a small fraction (1.9us) of the high pulse time.

running T4 with 1.8.8 with 1.47. what versions are you using?
 
Last edited:
Greetings All - thank you for your help!

To answer the suggestions so far:

1. adding the digitalPinToInterrupt did not seem to make any difference
2. I'm using a Teensy 4.0 with Arduino 1.8.9, and the Teensy 1.4.7 driver

This is a zoomed-in screenshot using the following code in the interrupt (only 1 SPI transaction):
Code:
    noInterrupts(); //Disable interrupts    
    for (volatile int time_waster = 0; time_waster < 5; time_waster++)
    {
      volatile int j = time_waster + 1;     
    }  
    SPI.beginTransaction(settingsADC);
    ADCbyte1 = SPI.transfer(0);
    //ADCbyte2 = SPI.transfer(0);  //Second SPI transaction commented out
    ADCresult = (int32_t)((ADCbyte1 << 8) + ADCbyte2);    // * 5.0f / 32768.0f;
    SPI.endTransaction();
    interrupts(); //Enable interrupts
DS0008.png


And this is a zoomed-in screenshot with both SPI transactions in the interrupt code (notice there are actually 4 SPI transactions in the screenshot, blue trace):
Code:
    noInterrupts(); //Disable interrupts    
    SPI.beginTransaction(settingsADC);
    ADCbyte1 = SPI.transfer(0);
    ADCbyte2 = SPI.transfer(0);
    ADCresult = (int32_t)((ADCbyte1 << 8) + ADCbyte2);    // * 5.0f / 32768.0f;
    SPI.endTransaction();
    interrupts(); //Enable interrupts

DS0009.png

Using only 1 SPI transaction appears to perform the correct SPI functionality. I'm not sure why calling 2 successive SPI transactions is causing this behavior.
 
Though not effecting my "connectionless test", you are missing manipulation of the SPI CS pin. Do you have a teensy pin connected to your ADC's CS pin? The ADC sensor is expecting CS to be HIGH in idle, and that you set it LOW before the first SPI.transfer(), and set it HIGH after the last SPI.transfer(). Typically, one uses pin 10 for CS (digitalMode(10,OUTPUT) ). You could use your PWM pin, but SPI would be enabled on FALLING edge of PWM. Or get rid of your PWM, and use IntervalTimer with 100 us period to invoke your ADC_read() with explicit CS manipulation.

The ADS8861 data sheet says it uses SPI_MODE0 if the SPI clock is less than 30mhz. You have SPI clock configured for 10mhz.

if you have only one SPI device, you can call SPI.beginTransaction() once in setup()
 
Last edited:
Greetings All - I certainly appreciate your ongoing help with this issue!

Okay, I switched to SPI mode0, and got the following result (still too many packets being transmitted):
DS0010.png

The ADS8861 is a reduced count ADC. Accordingly, it doesn't have a chip select CS pin - it is always assumed to be ready to transmit/receive SPI data. Once I get this one going, I intend on daisy chaining 4 of them with a single SPI master (the teensy 4.0)
 
I found a more recent data sheet http://www.ti.com/lit/ds/symlink/ads8861.pdf that better matches what you are describing. This data sheet doesn't say anything about best SPI mode, but it does mention CS for either 3-wire SPI or 4-wire SPI. I don't have the device, so I can't really test. But if you are seeing more SPI transactions than you expect, then the interrupt is firing again. The data sheet suggests the "data ready" comes from sensor's DOUT pin (high to low). The trouble with that is once you start the SPI CLK you wll get more DOUT transitions which may queue up as additional interrupts. You may need to study that with the scope. Perhaps jumper pin 2 (PWM) to pin 3. When PWM rises, ADC will start, and your attachInterrupt handler can delayMicroseconds(10) or so, to allow the ADC to complete, and then initiate the two SPI transfers. With pin 3 only attached to pin 2, you'll only get one interrupt per PWM cycle.
 
Also for a quick sanity test, you might try something like:

Code:
void ADC_ready(void)
{
    noInterrupts(); //Disable interrupts    
    //ALL variables in here must be volatile
    interrupt_counter++;

    for (volatile int time_waster = 0; time_waster < 100; time_waster++)
    {
      volatile int j = time_waster + 1;    
    }
   
    SPI.beginTransaction(settingsADC);
    ADCbyte1 = SPI.transfer(0);
    ADCbyte2 = SPI.transfer(0);
    ADCresult = (int32_t)((ADCbyte1 << 8) + ADCbyte2);    // * 5.0f / 32768.0f;
    SPI.endTransaction();
    interrupts(); //Enable interrupts
   [COLOR="#FF0000"] asm("dsb");[/COLOR]
}
Sometimes the state of interrupts and the like don't update... And ISR gets called twice... The dsb tells the system to recompute it or the like...

Not sure why you are disabling interrupts while within the interrupt? Especially doing a thing that takes some time like doing SPI...
 
Thank you manitou and KurtE! I will try the asm command soon.
--> The reason that I disable interrupts inside the interrupt routine is because of what manitou said in his last post: I'm using the ADS8861 in a daisy chain mode with multiple devices. As such, the DOUT pin of the ADC also acts as the "conversion done" pin.

On my setup, DOUT is tied to both adcReady (pin 3), and MISO (pin 12).

The first low-to-high transition on DOUT signals to the micro that the ADC is done converting (pin 3 interrupt), so I disable interrupts on the pin, and then use "normal" SPI to get the data.
 
Just in the hopes that this is useful, extracted from the datasheet manitou linked to:

The ADS8861 is a 10-pin chip, an 16-bit 1 MSPS ADC:
          ┌──────┐
      REF │ 1 10 │ DVDD
     AVDD │ 2  9 │ DIN
     AINP │ 3  8 │ SCLK
     AINN │ 4  7 │ DOUT
      GND │ 5  6 │ CONVST
          └──────┘
with two operatonal modes:

  1. 3-wire interface mode
    DIN is always high.
    At idle, CONVST is low.
    Conversion starts when CONVST goes high for at least 10ns.
    Conversion takes 500 to 710 ns.
    If CONVST is high for at least 710ns, the most significant bit of the conversion is available on DOUT 12.3ns after CONVST is pulled low.
    SCLK maximum frequency is 66.6 MHz, and should have 50% duty cycle (45% to 55%).
    Read DOUT on each rising edge of SCLK.
    It takes 13.2ns from the falling edge of SCLK for the next bit of the conversion to be available on DOUT.
    Read 16 bits from DOUT in this manner.
    There must be at least 20 ns between the last falling SCLK, before CONVST is pulled high for the next conversion.
  2. 4-wire interface mode
    At idle, DIN is low and CONVST is low.
    Conversion starts by pulling DIN high for at least 10ns. At least 7.5ns later from the rising edge, CONVST is pulled high.
    Conversion takes 500 to 710 ns.
    If DIN is high for at least 710ns, the most significant bit of the conversion is available on DOUT 12.3ns after DIN is pulled low.
    SCLK maximum frequency is 66.6 MHz, and should have 50% duty cycle (45% to 55%).
    Read DOUT on each rising edge of SCLK.
    It takes 13.2ns from the falling edge of SCLK for the next bit of the conversion to be available on DOUT.
    Read 16 bits from DOUT in this manner.
    13.2ns or longer after the last falling SCLK edge, pull CONVST low if this was an one-shot conversion;
    for continuous conversions, keep CONVST high, and pull DIN high to start a new conversion anytime after the last falling SCLK edge.

At 600 MHz, each clock cycle is 1.667ns (= 1000 ns / freq MHz), so you'll want at least 5-cycle (8.3ns) and 8-cycle (13.3ns) delays. Longer delays should be OK according to the datasheet. SPI mode 0 is correct.

The datasheet explicitly says that at idle, DOUT is three-stated, so you cannot rely on its transition to trigger the SPI transfer.

Instead, use a 0.71µs (426 cycles at 600 MHz) or longer delay or a timer, after starting the conversion. Then, after pulling CONVST(3-wire)/DIN(4-wire) down, wait at least 12.3ns (8 cycles at 600 MHz), then transfer two bytes over SPI using mode 0. Note that only DOUT and SCLK are used for the SPI transfers.

For example, using the three-wire mode, where DIN is tied to DVDD:
  • At idle, CONVST is low.
  • Trigger a conversion by pulling CONVST high.
  • Wait at least 710ns.
  • Pull CONVST low.
  • Wait at least 12.3ns.
  • Transfer two bytes using SPI mode 0 (connecting SCK on AD8861 to SCK (pin 13) on Teensy 4.0, and DOUT on AD8861 to DIN/MISO (pin 12) on Teensy 4.0).
I'm not yet familiar enough with Teensy 4.0 to say how to best interrupt-ize this, but first, I would make sure the procedural approach above works.

Then, I'd probably try a timer interrupt based approach, with the interrupt being a three-state state machine, stealing Paul's code from SPI/SPI.cpp:SPIClass::transfer() to do the SPI transfers. (Instead of busy-waiting in a while (!(SPSR & _BV(SPIF))) ; loop, I'd rearm the next timer interrupt to just after the transfer should be complete.) The 13ns or so delays I'd do using volatile asm("nop"); (or "nop\n\tnop" or "nop\n\tnop\n\tnop" , depending on how many no-operations I might need); I don't think the interrupt handler would run for more than say 50ns or so at 600 MHz.
Another option would be to use the SPI transfers, with the completion as a trigger, and use a seven-state machine, where the first five are dummy transfers, and the last two contain the actual data. Other than starting the next SPI transfer, first and fifth states might have those ~13ns delays. (At something like 60 MHz SPI clock rate, this might actually reach 1 MSPS.)

From the top of my head -- but remember, I'm just a hobbyist, nothing compared to KurtE or manitou, so I'm probably wrong -- I would not expect a full 1 MSPS capability; but I suspect something like 600 kSPS is doable, although it might require some inline assembly (mostly to get those small delays long enough but not too long, perhaps to spruce up the interrupt handler if it affects other code).
 
Last edited:
Status
Not open for further replies.
Back
Top