High Speed digitalRead for weird data interface.

I'm using the AD7768 in a project (http://www.analog.com/en/products/analog-to-digital-converters/ad7768.html). I would like to read data from it using an Arduino DUE (84MHz ARM SAM3).

This chip acts sort of like a SPI master, except it sends out the data on eight separate lines and pulses DRDY instead of using a framing signal. The clock coming in is at 4MHz, so nominally I have 21 opcodes to work with per clock.

I have connected the input clock to pin 30, and am using pins 14-21 for the data line inputs. Data is clocked in on a falling edge of the clock. DRDY is supposed to fall when a result is about to be clocked out.

To try to make this run fast enough, I implemented an interrupt on the falling edge of DRDY, and then do this (removed all but one input channel both for my testing and for clarity):

boolean ADCDataBuffer[8][32];

void ADCDataHandler () {
  noInterrupts();
  while(digitalRead(30));
  ADCDataBuffer[0][31] = digitalRead(21);
  while(!digitalRead(30));
  while(digitalRead(30));
  ADCDataBuffer[0][30] = digitalRead(21);
...

and I continue in that manner in a rolled out polling system inside the interrupt with interrupts disabled after you enter it.

In between reads I'm sending the data to my computer over USB, but the data it reports to have received is not showing the correct bits. I scoped them, so I'm confident the pin is seeing the correct data. From scoping the input clock signal, it also looks quite clean with no ringing that might cause repeated triggers.

I'm rather stuck using those pins, unfortunately. I thought about trying to align the data inputs to a real physical port, but it wasn't possible.

I did some additional timing debugging by recording micros() at the beginning and end of the interrupt. If I just do:

void ADCDataHandler() {
  starttime = micros();
  ADCDataReady = true;
  endtime = micros();

and when I print endtime-starttime I get 1-2us (!?!).

Simply reading only the first 8 bits for a single channel bit by bit with the while statements delaying each bit gives a 64us delay inside the interrupt.

If I just add while(digitalRead(30)) between starttime = micros() and ADCDataReady = true it reports 2-3us.

If I remove the while(digitalRead(30)) delay and simply have digitalRead(21) with no assignment to a variable, it is also 2-3us. If I add in the assignment with "ADCDataBuffer[0][31] = digitalRead(21)" instead, the time is similar.

Does anyone have any ideas? Simply reading a digital pin seems to be unbelievably slow, microseconds! From reading other forum posts, it sounds like it takes nearly 5us to read a digital pin on the AVR architecture too!

What is going on under the hood here? I can only assume that digitalRead is doing a whole bunch of hidden stuff because this seems like it should be a one opcode pin read and maybe a few opcodes of overhead putting the data into RAM. I'll probably end up having to drop the clock speed to make it usable, but this is so far away from fast enough... I'd have to drop the frequency down to ~3kHz to keep up at the reported interrupt run times.

The pins are configured as inputs, without pullups, btw.

have a look at the Due SAM3X8E processor Parallel input/Output Controller (PIO) Section 31 of SAM3X8E manual.
what USB protocol are you using to transfer the data to the PC?

Aha, thanks, that's much closer.

Now I'm doing:

void ADCDataHandler () {
  while((d31 = REG_PIOD_PDSR) & (1<<9));
  while(!(REG_PIOD_PDSR & (1<<9)));
  while((d30 = REG_PIOD_PDSR) & (1<<9));
  while(!(REG_PIOD_PDSR & (1<<9)));
  while((d29 = REG_PIOD_PDSR) & (1<<9));
  while(!(REG_PIOD_PDSR & (1<<9)));
...
}

and so on to make this a lot faster. I tried to give it code to minimize computation by checking the port with the while() and simultaneously storing it so that it just stores the last value prior to the edge rather than after. Not technically correct, but should be okay if it works.

Unfortunately still a bit too slow, I think the inverting logic and bit mask on the check for the clock going back high before checking is a problem. I think I can see the final 13-14 bits, but they jitter a lot. Removing the checks for the clock going back high got 24 bits recorded, but I don't think they're trustworthy either.

I think maybe if I drop my oscillator down by a less extreme amount it may work now. It seems like there is a substantial overhead in using the attachInterrupt system, so that might be a limiting factor. I found a few references to setting it up and the datasheet has a section on it that will probably take a while to parse, but if anyone knows of a more getting-started kind of guide I'd love to see it.

Oh, for the USB protocol I'm using the Native USB port and the SerialUSB.println() function.

There is an extended delay between collecting data, and I simply monitor for a flag saying the packet has been updated. It doesn't seem to be resource limiting anything.

If I understand correctly how works your ADC device, I would wire this device to the DUE this way:

  • Connect the 8 bits of data to D0/D1/..../D7 (see Graynomad pinout diagram). All 8 bits are on PIOD !

To read the 8 bits, write: ConversionData = (uint8_t) PIOD->PIO_PDSR;

  • Connect DRDY signal to any digital pin, e.g. pin 23 (A0)

And declare a callback function : attachInterrupt( 23, ConversionFunction, Falling);

void ConversionFunction() {

// Read ConversionData
}

  • Connect the input clock of the ADC device (4 MHz ?) to pin 2 (TIOA0)

Program Timer Counter 0 channel 0 to produce pulses every 4 MHz on TIOA0

Thanks!

Unfortunately I have no ability to move the data lines, but that would have been best I agree. Simply reading with REG_PIOD_PDSR (so getting only two data channels and the clock) is still too slow, but it's pretty close to fast enough I think.

Where is the "PIOD->PIO_PDSR" syntax coming from? I couldn't find the actual hardware register map definitions anywhere. I fumbled around a bit and eventually found that REG_PIOD_PDSR is defined and works. It would be useful to know where to find Arduino defined register names for future reference, especially when I'm trying to figure out how to manually configure interrupt registers too. I am a little worried that your syntax will result in more clock cycles used if it's truly checking where that pointer goes every time. Maybe it just compiles to the register address though in the assembly, that seems pretty likely if the compiler is any good.

I had originally hoped to use edge detection on the clock line for my interrupts so that the interrupt does nothing but record the values during that clock. However, it seems that the ISR call has too much overhead and doesn't hit the interrupt code fast enough to work (the data is no longer valid so long after the edge event). Plus that ADC is just constantly outputting clock pulses so it effectively froze the Due. The datasheet was ambiguous about this "feature" but no configuration I can set makes it only output clocks when actually transmitting data. It must just be a clock divider off the master clock to the ADC with no enable/disable logic.

I did read in a few places that the Arduino attachInterrupt system has far more overhead than defining the interrupt manually in the registers. I don't know why, nor do I know why digitalRead is 100x slower than just reading the port register. I figured it was just setting the interrupt registers properly and putting the function in the right spot, but it must be doing other stuff when it gets called first.

It seems like the configuration to manually set up interrupts (using C15 as my DRDY line) is just:

REG_PIOC_ESR = (1<<15); // Set edge event detection on C15.
REG_PIOC_FELLSR = (1<<15); // Configure the edge for the falling edge on C15 (Due Pin 48).
REG_PIOC_IER = (1<<15); // Start interrupts.

Surprisingly, the arduino compiler recognizes irq_initialize_vectors() and Enable_global_interrupt() and the ISR() macro for defining the interrupt. Must be part of GCC libraries I guess, I thought those were specific to the Atmel Studio environment and I'm using the Arduino IDE. I'm a bit worried that I'm going to end up accidentally mixing and matching macros and patterns from different sources, I'd rather just learn the lowest overhead method to do it and adopt that method from now on exclusively.

One reference looks a lot like mine and follows with NVIC_EnableIRQ(). But I think this may be from yet a third system called CMSIS (Interrupts and Exceptions (NVIC) and http://asf.atmel.com/docs/latest/sam3x/html/group__interrupt__group.html). Unfortunately, that seems to be looking for an 'IRQn_Type {aka IRQn}' for an argument which I have never heard of and the reference to IRQn_Type looks to be for a different target chip. In an example they call it with TC0_IRQn as the argument, but I don't know where this comes from. The define in the documentation doesn't list anything that looks correct.

I also found this documentation for the asf (http://asf.atmel.com/docs/latest/sam3x/html/sam_pio_quickstart_use_case_2.html), so maybe the correct code is:

pmc_enable_periph_clk(ID_PIOC);
pio_handler_set(PIOC, ID_PIOC, PIO_PC15, PIO_IT_FALL_EDGE, handler);
pio_enable_interrupt(PIOC, PIOC_PC15);
NVIC_EnableIRQ(PIOC, PIO_PC15);

but these functions aren't defined in the Arduino IDE. NVIC_EnableIRQ is defined, but the pio and pmc functions are not and the NVIC function has a different number and type of expected arguments so there's clearly some conflicting stuff.

Currently I'm trying to understand how to tell the SAM3X where to actually jump on an interrupt. I haven't done this on an ARM chip before, so I'm a little confused. The Atmel asf documentation suggested that if I just use ISR() { } to define my interrupt function it will work, but I'm not clear on what the best practice is and those are clearly incomplete instructions. Is there only one ISR that gets called no matter which interrupt is triggered? I keep looking for a register to set to the memory location of the interrupt function call, but this is all AVR style thinking and is probably the wrong way to think about it.

I'm also worried that if I do it the Atmel way, I'll break stuff like millis() that also rely on interrupts.

ard_newbie:

  • Connect the input clock of the ADC device (4 MHz ?) to pin 2 (TIOA0)

Program Timer Counter 0 channel 0 to produce pulses every 4 MHz on TIOA0

To avoid confusion, the peripheral is generating the clock pulses rather than the Due. This is hugely annoying, but it is what it is. So I actually have to read the clock pin to tell whether it's safe to read data, which can't be done with an edge interrupt because of the interrupt overhead. That's what the while((d31 = REG_PIOD_PDSR) & (1<<9)) was intended for -- polling the clock pin once I'm inside the ISR because there's no other option to read it fast enough.

There is a lot of confusion.....

If your 8 input datalines are on PIOD0/.../PIOD7, reading the 8 with a single code line :
ConversionData = PIOD->PIO_PDSR;
takes 8 clock cycles. You get a byte in 8 clock cycles = 95.2 ns is it too slow ?

Depending on the frequency of the availability of data bytes, you can use an attachInterrupt() or an input capture thru a Timer Counter to detect a DRDY falling edge or a polling method in loop(). To speed up attachInterrupt(), use a PIO pin with the smallest possible number, e.g. PIOA0.

FYI, attachInterrupt is coded in Winterrupt.c.

A discussion about attachInterrupt() speed, see reply #3:
http://forum.arduino.cc/index.php?topic=499177.0

You will find header files in:
https://android.googlesource.com/platform/external/arduino-ide/+/f876b2abdebd02acfa4ba21e607327be4f9668d4/hardware/arduino/sam/system/CMSIS/Device/ATMEL/sam3xa/include/component

Thanks again for the reply!

Side question: I agree that the comparison and assignments and other things would take a significant number of instructions, but does simply reading a PIO_PDSR register really take 8 clock cycles? I thought I was just copying a 32-bit wide register to another one.

If I run the external ADC at full speed, the first clock is coming to the Due about 250ns after the falling edge of DRDY. I should have about 21 instructions to tolerate before I'll miss the first bit, but I'm not sure how long it takes to hit the interrupt or if the Arduino approach to the interrupts has any extra overhead that I can eliminate.

digitalRead had a shocking amount of overhead, after all, and that seems so simple that it didn't even occur to me that it would be slow. Is attachInterrupt() better optimized?

I'm still a bit lost by how the ARM interrupts are handled by the chip. The interrupt section of the datasheet goes over how to configure the registers (which is where I got the register names for falling edge detection), but I can't seem to find a more verbose introduction.

Is it one of those architectures where there's a global interrupt flag that goes to a vector table with addresses for the handler functions for each interrupt, branches to those addresses, and then returns to the saved program counter? Or are the interrupts multiplexed so that you have to manually check which specific interrupt was triggered? I get the impression they're multiplexed and grouped together by peripheral.

If so, I have only one external interrupt. Maybe I can go faster by not testing which pin interrupt was triggered or something? Sorry for the basic questions, the datasheet is just assuming my knowledge coming in is a bit higher than it is since this is my first ARM. It's easier than I expected, but I'm having trouble reasoning about the interrupt setup.

I'm having a weirdly hard time finding Winterrupt.c, for some reason I can't find the file on my computer but I'll try to find the correct version online and see if that clarifies things.