SPI with 2 or 4 data lines?

If "possible" means a nice library, then the answer becomes "no".

It is only "possible" by diving into the low-level hardware register details.
 
I don't know if this is of any use.

Code from the above link, modified for Teensy, might need some more.
C++:
#include <SPI.h>

uint32_t startTime, elapsedTime, old_res;
#define SCALE_FACTOR 0.000152587890625
/* D14 and D15 should be grounded if you use this ad7606 board
 *  https://www.amazon.de/RELAND-SUN-Multi-Channel-Datenerfassungsmodul-Synchronisation/dp/B0B4HKSK8B/ref=sr_1_2?keywords=ad7606&qid=1681803018&sr=8-2
 *  i do not use oversampling in this example
 *  i set range to the ground in this example so the range is +/-5V
*/
#define BUSY 32               // you can use any digital pin   
#define RESET 30              // you can use any digital pin 
#define START_CONVERSION 34   // you can use any digital pin   
#define CHIP_SELECT 10        // SPI CS     // #define CHIP_SELECT 53        // SPI CS     
#define D7_out 12             // SPI MISO there is no need to use MOSI port with the ad7606 #define D7_out 50             // SPI MISO there is no need to use MOSI port with the ad7606
#define RD 13                 // SPI SCLK #define RD 52                 // SPI SCLK
#define RANGE 36               // you can use any digital pin
#define TOTAL_RAW_BYTES 16
SPISettings _spiSettings;

int bytesToRead = TOTAL_RAW_BYTES;
byte raw[TOTAL_RAW_BYTES];
uint16_t parsed[8];


//--------------SETUP-UP---------------------------
void setup()
{
    initial();
    Serial.begin(2000000);
    while (!Serial) {} // wait for usb connection
    SPI.begin();
}
//---------------LOOP--------------------------
void loop()
{
    startTime = micros();
    readData();
    elapsedTime = micros() - startTime;
    if (elapsedTime != old_res)
    {
        Serial.print("Conversion time:\t");
        Serial.print(elapsedTime);
        Serial.println(" microseconds");
        old_res = elapsedTime;
    }
    parseRawBytes();

    for (int i = 0; i < 8; i++) {
        Serial.print((float)parsed[i] * SCALE_FACTOR, 5);
        Serial.print(",");
    }

    Serial.print("\r\n");
}
//-----------------------------------------
void initial()
{
    pinMode(BUSY, INPUT);
    pinMode(RESET, OUTPUT);
    pinMode(START_CONVERSION, OUTPUT);
    pinMode(CHIP_SELECT, OUTPUT);
    pinMode(D7_out, OUTPUT);
    pinMode(RD, OUTPUT);
    digitalWrite(START_CONVERSION, HIGH);
    digitalWrite(CHIP_SELECT, HIGH);
    reset(RESET);
}
//-----------------------------------------
void parseRawBytes() {
    /*
    I figured the problem should be here in the function of parseRawBytes.
    The sizeof(int) is 4 in Portanta and also Giga.
    This results in the value of (sizeof(parsed) / sizeof(int)) is 4 not 8,
    so only the data for the first 4 channels can be obtained.
    I modify the maximum value of loop, eight channels data can be obtained.

        for (int i = 0; i < (sizeof(parsed) / sizeof(int)); i++)
        {
            parsed[i] = (raw[i * 2] << 8) + raw[(i * 2) + 1];
        }
    */

    for (int i = 0; i < 8; i++)
    {
        parsed[i] = (raw[i * 2] << 8) + raw[(i * 2) + 1];
    }
}
//-----------------------------------------
/*reset signal*/
void reset(uint8_t port)
{
    digitalWrite(port, HIGH);
    // delayMicroseconds(1);
    digitalWrite(port, LOW);
    //  delayMicroseconds(1);
}
//-----------------------------------------
void conversionPulse(uint8_t port)
{
    digitalWrite(port, LOW);
    //  delayMicroseconds(1);
    digitalWrite(port, HIGH);
}
//-----------------------------------------
/*
1- start conversion START_CONVERSION HIGH
2- wait until the busy is LOW again
3- put the CS to LOW
4- Read a byte from the SPI
*/
void readData()
{
    conversionPulse(START_CONVERSION);
    while (digitalRead(BUSY) == HIGH) {
        //  delayMicroseconds(1);
    }
    SPI.beginTransaction(_spiSettings);
    digitalWrite(CHIP_SELECT, LOW);
    while (bytesToRead > 0) {
        raw[TOTAL_RAW_BYTES - bytesToRead] = SPI.transfer(0x00);
        bytesToRead--;
    }
    digitalWrite(CHIP_SELECT, HIGH);
    SPI.endTransaction();

    bytesToRead = TOTAL_RAW_BYTES;
}
//-----------------------------------------
/*
  0 +/-  5V
  1 +/- 10V
*/
void setRange(bool range)
{
    pinMode(RANGE, OUTPUT);
    digitalWrite(RANGE, range);
}
//-----------------------------------------
/*OS2 OS1 OS0
oversampling
000 No oversampling Maximum sampling rate is 200KSPS
001  2 times        100 KSPS
010  4 times        50 KSPS
011  8 times        25 KSPS
100 16 times        12.5 KSPS
101 32 times        6.25 KSPS
110 64 times        3.125 KSPS
*/
/*
void setOversampling(uint8_t OS0, uint8_t OS1, uint8_t OS2) {
    pinMode(_OS0, OUTPUT);
    pinMode(_OS1, OUTPUT);
    pinMode(_OS2, OUTPUT);
    digitalWrite(_OS0, bitRead(B001, OS0));
    digitalWrite(_OS1, bitRead(B010, OS1));
    digitalWrite(_OS2, bitRead(B100, OS2));
}
*/
 
Then I better use the 16-bit parallel interface.

Agreed, parallel mode is (probably) simpler. On Teensy 4.1 you can read all 16 bits with a single GPIO register if you connect the data pins to all the "AD_B1_xx" GPIO pins.

1701910871216.png


I spent a few minutes reading (more like lightly skimming) the AD7606B datasheet. This chip has combinations of modes, so I might have misunderstood. It appears to have software modes where you can control more things, and hardware modes where you can't. My initial impression is some of those modes might not be able to work with 2 data pin half duplux SPI, no matter how much work is put into the low-level code.

Especially Figure 74 on page 39 looks worrisome. In this diagram, you can see the SDI signal transmits the register number in the first several cycles and then the chip responds with data on the DOUT(A-D) pin(s) in the remaining cycles. This absolutely can not work with the SPI port in half duplex mode. In half duplex mode, the 2 data pins are either both input or both output for the duration of a transfer. You simply can't get anything in SPI half duplux mode like this diagram where you have 3 signals, 1 from Teensy to the ADC chip and 2 from the ADC chip. In half duplex mode, Teensy's SPI gives you 2 inputs and 0 outputs (or 0 inputs and 2 outputs) but never both input and output during the same transfer.


1701911277619.png


The one possible bright spot I saw was Figure 71 on page 37. That diagram (no SDI signal, data on 2 pins) looks like it might fit with SPI half duplex capability. Maybe? But my (admittedly limited) understanding of quickly reading this datasheet is you lose access to a lot of this chip's features in that hardware mode.

FlexIO might be able to do something like Figure 74, especially if using 3 shifters and some work in software to combine the bits received on 2 separate pins. But if you're not wanting to dive into tricky low-level programming, you definitely would not want to get into trying to write a FlexIO driver! Maybe in some glorious future we'll have drivers for all sorts of exotic ADC chips in the FlexIO_t4 library, but today that seems like a distant pipe dream at best.

Parallel mode really does look like the easiest path with this ADC chip.
 
Last edited:
> Agreed, parallel mode is (probably) simpler. On Teensy 4.1 you can read all 16 bits with a single GPIO register if you connect the data pins to all the "AD_B1_xx" GPIO pins.

What's the name of the register that must be read?

Thanks,
Michael
 
Looking at my excel document I see:
1702241485546.png

So it looks like those 16 pins are all on the upper part of GPIO1 (In default mode)
Or GPIO6 in Fast mode. And startup.c changes all of the pins to High speed mode.


You can probably read all of them in with the PSR register:
like: GPIO6_PSR >> 16
 
Now I'm confused. GPIO1 or GPIO6 or GPIO6_PSR ?
Why shifted 16 bits to the right, if the signal names are AD_B1_xx with xx = 00 to 15 ?
Is this explained somewhere?

Michael
 
Last edited:
Now I'm confused. GPIO1 or GPIO6 or GPIO6_PSR ?
Why shifted 16 bits to the right, if the signal names are AD_B1_xx with xx = 00 to 15 ?
Is this explained somewhere?

Michael
GPIO6

You will need to shift 16 to the right because the sequential port bits start at gpio6 bit 16 to 31.

Here's the pin mapping

Code:
//MXRT pin  |6.31|6.30|6.29|6.28|6.27|6.26|6.25|6.24| |6.23|6.22|6.21|6.20|6.19|6.18|6.17|6.16|
//4.1 pins  |27  |26  |39  |38  |21  |20  |23  |22  | |16  |17  |41  |40  |15  |14  |18  |19  |

If you create a mask you can set the pin directions, clear the before writing and write to them

GPIO6_GDIR
GPIO6_DR_CLEAR
GPIO6_DR_SET
 
Now I'm confused. GPIO1 or GPIO6 or GPIO6_PSR ?

GPIO6_PSR is the hardware register you need.

Names GPIO1 and GPIO6 refer to the entire peripheral which has several registers.

If you can just leave the 16 pins as input mode, I would recommend first configuring all 16 with ordinary pinMode(). There are important low-level details about pin configuration which are handled automatically by pinMode().


Why shifted 16 bits to the right, if the signal names are AD_B1_xx with xx = 00 to 15 ?
Is this explained somewhere?

It is all documented (but not really "explained") in the reference manual (which is on the Teensy 4.1 page under "Technical Info" and you can also get on NXP's website, though they may require registering an account to download it). This manual is huge and a pretty steep learning curve. It documents everything, but has little to no cross referencing of related parts and doesn't really explain why things are the way they are. Fortunately, plenty of us here know this material pretty well.

In the reference manual you'll find a lot of documentation which says these pins are connected to GPIO1. Then elsewhere you can find info about the ability to map a pin to either the slow GPIO (1 to 5) or fast GPIO (6 to 9). But almost all the documentation was written for the earlier RT1052 chip, so a lot of documentation mentions GPIO1 only. Of course, knowing how the RT1062 hardware works, you can know that any pins GPIO1 accesses can alternately be accessed by GPIO6.

Teensy's startup code maps all the pins to the fast GPIO peripherals. So even though the default for non-Teensy boards would be GPIO1, because of the way the Teensy core library is designed, you would access the port with GPIO6 rather than GPIO1.

Not documented in the reference manual is the (likely) story behind *why* the chip has redundant GPIO peripherals. NXP's earlier RT1052 chip had only the slow GPIO. They almost certainly heard feedback from (huge corporation) customers about the slow GPIO performance. In the design of these modern chips, it's much easier to bolt on another peripheral than to dive deep down into the design of the existing ones. And the slowness comes from various buses and bus bridges, which might be IP they licensed from ARM which often comes with restrictions on modifications (which is a smart policy for ARM as it keeps a lot of consistency in the platform and required compiler support). So rather than do anything about the GPIO slowness, they just added another copy of the same GPIO perhipheral but connected it to a special low-latency bus the M7 core provides. And then they added muxes to let you assign each pin to either the slow or fast GPIO.

The main downside to the fast GPIO is DMA can't access anything on the low latency bus (all 0x42xxxxxx addresses). So when a pin needs to accessed by DMA, as the OctoWS2811 library does, the special mux registers are used to assign those pins to the slow GPIO which DMA can access.

Other than DMA, you would normally always want to the fast GPIO. That's why Teensy's startup code assigns all the pins to be used by fast GPIO6 to GPIO9.
 
Last edited:
Back
Top