Long contiguous SPI transfers (T4x)

DrM

Well-known member
Hi,

I am contemplating using an ADC with a "daisy chain" capability. There are now quite a few of these; as examples. see the ADS 8881 for 18 bits and the ADS 8861 for 16 bits.

How this works: To form the daisy chain, the clocks and convert pins are all (each) driven together, the SDO of the first ADC is connected to the SDI of the next, and so on, and the SDO of the last goes to the MCU SPI interface. Each ADC tacks the bits it receives onto the frame that it sends to the next. The last ADC sends a frame with length equal to the number of ADC times the number of bits from each ADC.

For example, with two 18 bit ADCs, the transfer to the MCU is a frame of length 36 bits. For four 16 bit ADCs, the transfers size would be 64 bits. Finally, for performance, these transfers need to be contiguous, i.e. a single frame of 36 bits or 64 bits in these examples.

Now the SPI question:

The I.MXRT106x processors seem to be capable of contiguous SPI transfers up to 4096 bits long. (See I.MRXT1060 Reference Manual, rev 3, TCR on page 2885, and bit field FRAMESZ on oage 2888).

But the transmit and receive data registers are 32 bits. (The TDR is page 2889 and the RDR is on page 2890).

I assume the way one transfers more than 32 bits is to loop over writing the TDR and reading the RDR. In a 36 bit transfer, the second read is ready after 4 bits?

For purposes of illustration, I am thinking of something like the following.

Comments? Suggestions?

Thank you

C-like:
void setSpectialTransferSize(uint8_t nbits) {
  uint32_t tcr = port().TCR;
  port().TCR = (tcr & 0xfffff000) | LPSPI_TCR_FRAMESZ(nbits-1);
}

//   For transfers of size "nbits" <= 16
uint16_t specialTransfer16(uint16_t data) {
  int n = 0;
  port().TDR = data;        // output 16 bit data.
  while ((port().RSR & LPSPI_RSR_RXEMPTY)) ;    // wait while the RSR fifo is empty...
  return port().RDR;
}

//   For transfers of size "nbits" <= 32
uint32_t specialTransfer32(uint32_t data) {
  port().TDR = data;
  while ((port().RSR & LPSPI_RSR_RXEMPTY));
  return port().RDR;
}

//   For transfers of size "nbits" <= 64
uint64_t specialTransfer64(uint64_t data) {
  uint64_t d64;

  port().TDR = data>>32;
  while ((port().RSR & LPSPI_RSR_RXEMPTY));
  d64 = port().RDR;
  d64 <<= 32
 
  port().TDR = data&0xFFFFFFFF;
  while ((port().RSR & LPSPI_RSR_RXEMPTY));
  d64 |= port().RDR;

  return d64;
}
 
The TDR and RDR registers are actually FIFOs; words written to TDR are added to the end of the transmission FIFO and words read from RDR are removed from the top of the received FIFO. I believe on the Teensy 4.x, the FIFOs both have a capacity of 16 32-bit words. Every entry occupies a whole word, e.g. if you write an 8-bit value to TDR it will be zero-extended and take up one entire word in the FIFO.
 
Thank you. So the first bits are in the lower bits of the word?

In other words, for a 36 bit transfer (two 18 bit datum), I read 32 bits and then 4 bits. But the code is going to look like reading a 32 bit register twice.

The bits I need are the 18 high bits from the first 32 bit word, then the lower 14 bits shifted left 14 and the low 4 bits from the second word.

Here is my naive first attempt at some code, hacking from the earlier thread where we talked about contiquous 16 bit transfers.


Does this look right?

BTW, I really appreciate the help. It is a huge time saver compared to trying to guess way through this with trial and error and random noise coming from the ADCs.




Code:
IMXRT_LPSPI_t *lpspi = &IMXRT_LPSPI4_S;

uint16_t spi_nbits = 8;

static inline uint16_t get_framesz( ) {
  return (lpspi->TCR & 0x00000fff) + 1;
}

static inline void set_framesz( uint16_t nbits ) {
  lpspi->TCR = (lpspi->TCR & 0xfffff000) | LPSPI_TCR_FRAMESZ(nbits-1);
  spi_nbits = nbits
}

static inline uint16_t retreive16( ) {
  lpspi->TDR = 0xFFFF;                       // output 16 bit data
  while (lpspi->RSR & LPSPI_RSR_RXEMPTY) {}  // wait while RSR fifo is empty
  return lpspi->RDR;                         // return data read
}

static inline uint32_t retrieve32( ) {
  lpspi->TDR = 0xFFFFFFFF;                   // output 32 bit data
  while (lpspi->RSR & LPSPI_RSR_RXEMPTY) {}  // wait while RSR fifo is empty
  return lpspi->RDR;                         // return data read
}

static inline uint32_t retrieve18( ) {
  return transfer32();
}

static inline void retrieve36( uint32_t *rcvd ) {
  uint32_t u32a;
  uint32_t u32b;

  u32a = retrieve32();
  u32b = retrieve32();

  rcvd[0] = u332a >> 14;
  rcvd[1] = (u32a & 0xFFF7) << 14 | u32b & 0xF;
}
 
Actrually trhat is a goof, i need to do it like this, (writing too many bits will start another transfer)

Code:
IMXRT_LPSPI_t *lpspi = &IMXRT_LPSPI4_S;

uint16_t spi_nbits = 8;

static inline uint16_t get_framesz( ) {
  return (lpspi->TCR & 0x00000fff) + 1;
}

static inline void set_framesz( uint16_t nbits ) {
  lpspi->TCR = (lpspi->TCR & 0xfffff000) | LPSPI_TCR_FRAMESZ(nbits-1);
  spi_nbits = nbits
}

static inline uint16_t retreive16( ) {
  lpspi->TDR = 0xFFFF;                       // output 16 bit data
  while (lpspi->RSR & LPSPI_RSR_RXEMPTY) {}  // wait while RSR fifo is empty
  return lpspi->RDR;                         // return data read
}

static inline uint32_t retrieve32( ) {
  lpspi->TDR = 0xFFFFFFFF;                   // output 32 bit data
  while (lpspi->RSR & LPSPI_RSR_RXEMPTY) {}  // wait while RSR fifo is empty
  return lpspi->RDR;                         // return data read
}

static inline uint32_t retrieve18( ) {
  lpspi->TDR = 0xFFFF7;                   // output 32 bit data
  while (lpspi->RSR & LPSPI_RSR_RXEMPTY) {}  // wait while RSR fifo is empty
  return lpspi->RDR;                         // return data read
}

static inline void retrieve36( uint32_t *rcvd ) {
  uint32_t u32a;
  uint32_t u32b;

  lpspi->TDR = 0xFFFFFFFF;                   // output 32 bit data
  while (lpspi->RSR & LPSPI_RSR_RXEMPTY) {}  // wait while RSR fifo is empty
  u32a = lpspi->RDR;                         // return data read

  lpspi->TDR = 0xF;                   // output 4 bits
  while (lpspi->RSR & LPSPI_RSR_RXEMPTY) {}  // wait while RSR fifo is empty
  u32b = lpspi->RDR;                         // return data read

  rcvd[0] = u332a >> 14;
  rcvd[1] = (u32a & 0xFFF7) << 14 | u32b & 0xF;
}
 
Like this:

Code:
static inline void retrieve36( uint32_t *rcvd ) {
  uint32_t u32a;
  uint32_t u32b;

  lpspi->TDR = 0xFFFFFFFF;                   // output 32 bit data
  lpspi->TDR = 0xF;                   // output 4 bits

  while (lpspi->RSR & LPSPI_RSR_RXEMPTY) {}  // wait while RSR fifo is empty
  u32a = lpspi->RDR;                         // return data read

  while (lpspi->RSR & LPSPI_RSR_RXEMPTY) {}  // wait while RSR fifo is empty
  u32b = lpspi->RDR;                         // return data read

  rcvd[0] = u332a >> 14;
  rcvd[1] = (u32a & 0xFFF7) << 14 | u32b & 0xF;
}
 
Thank you. So the first bits are in the lower bits of the word?
SPI is always big-endian. As I understand it the SPI machinery knows the number of bits to transfer IIRC so it won't get it wrong - just put 8 bit values in and 8-bit transfers should DTRT.
 
Here is my starting point for code, it compiles and runs.

The question is, how to unpack the 18 bit words?

Code:
IMXRT_LPSPI_t *lpspi = &IMXRT_LPSPI4_S;

uint16_t spi_nbits = 8;

static inline uint16_t get_framesz( ) {
  return (lpspi->TCR & 0x00000fff) + 1;
}

static inline void set_framesz( uint16_t nbits ) {
  lpspi->TCR = (lpspi->TCR & 0xfffff000) | LPSPI_TCR_FRAMESZ(nbits-1);
  spi_nbits = nbits;
}

static inline uint16_t retrieve16( ) {
  lpspi->TDR = 0xFFFF;                       // output 16 bit data
  while (lpspi->RSR & LPSPI_RSR_RXEMPTY) {}  // wait while RSR fifo is empty
  return lpspi->RDR;                         // return data read
}

static inline uint32_t retrieve32( ) {
  lpspi->TDR = 0xFFFFFFFF;                   // output 32 bit data
  while (lpspi->RSR & LPSPI_RSR_RXEMPTY) {}  // wait while RSR fifo is empty
  return lpspi->RDR;                         // return data read
}

static inline uint64_t retrieve64( ) {
  uint64_t u64;

  lpspi->TDR = 0xFFFFFFFF;                   // output 32 bit data
  lpspi->TDR = 0xFFFFFFFF;                   // output 32 bit data
 
  while (lpspi->RSR & LPSPI_RSR_RXEMPTY) {}  // wait while RSR fifo is empty
  u64 = lpspi->RDR;
  u64 <<= 32;

  while (lpspi->RSR & LPSPI_RSR_RXEMPTY) {}  // wait while RSR fifo is empty
  u64 |= lpspi->RDR;
  u64 <<= 32;
 
  return u64;                                // return data read
}

static inline uint32_t retrieve18( ) {
  lpspi->TDR = 0xFFFF7;                   // output 32 bit data
  while (lpspi->RSR & LPSPI_RSR_RXEMPTY) {}  // wait while RSR fifo is empty
  return lpspi->RDR;                         // return data read
}

static inline void retrieve36( uint32_t *rcvd ) {
  uint32_t u32a;
  uint32_t u32b;

  lpspi->TDR = 0xFFFFFFFF;                   // output 32 bit data
  lpspi->TDR = 0xFFFFFFFF;                   // output 32 bit data

  while (lpspi->RSR & LPSPI_RSR_RXEMPTY) {}  // wait while RSR fifo is empty
  u32a = lpspi->RDR;                         // return data read

  while (lpspi->RSR & LPSPI_RSR_RXEMPTY) {}  // wait while RSR fifo is empty
  u32b = lpspi->RDR;                         // return data read

  Serial.println( "retrieve36:");
  Serial.print( u32a, HEX); 
  Serial.print( " ");
  Serial.println( u32b, HEX);

  /* ----
  ???
  rcvd[1] =  (u32b & 0xF) | (u32a & 0xFFF7) << 4;  // 4 bits in the second word
  rcvd[0] = u32a >> 14;
  */ 
}

static inline void retrieve54( uint32_t *rcvd ) {
  uint32_t u32a;
  uint32_t u32b;

  lpspi->TDR = 0xFFFFFFFF;                   // output 32 bit data
  lpspi->TDR = 0xFFFFFFFF;                   // output 32 bit data

  while (lpspi->RSR & LPSPI_RSR_RXEMPTY) {}  // wait while RSR fifo is empty
  u32a = lpspi->RDR;                         // return data read

  while (lpspi->RSR & LPSPI_RSR_RXEMPTY) {}  // wait while RSR fifo is empty
  u32b = lpspi->RDR;                         // return data read

  Serial.println( "retrieve36: ");
  Serial.print( u32a, HEX); 
  Serial.print( " ");
  Serial.println( u32b, HEX);

  /* ----
  ???
   */ 
}

static inline void retrieve72( uint32_t *rcvd ) {
  uint32_t u32a;
  uint32_t u32b;
  uint32_t u32c;

  lpspi->TDR = 0xFFFFFFFF;                   // output 32 bit data
  lpspi->TDR = 0xFFFFFFFF;                   // output 32 bit data
  lpspi->TDR = 0xFFFFFFFF;                   // output 32 bit data

  while (lpspi->RSR & LPSPI_RSR_RXEMPTY) {}  // wait while RSR fifo is empty
  u32a = lpspi->RDR;                         // return data read

  while (lpspi->RSR & LPSPI_RSR_RXEMPTY) {}  // wait while RSR fifo is empty
  u32b = lpspi->RDR;                         // return data read

  while (lpspi->RSR & LPSPI_RSR_RXEMPTY) {}  // wait while RSR fifo is empty
  u32c = lpspi->RDR;                         // return data read

  Serial.print( "retrieve72: ");
  Serial.print( u32a, HEX); 
  Serial.print( " ");
  Serial.print( u32b, HEX);
  Serial.print( " ");
  Serial.println( u32c, HEX);

  /* ----
  ???



  */
 
}
 
The ADC produces 18 bit signed numbers. You will use 32 bit signed integers. When right shifting uint32 using >>14 the sign bit no longer will be sign bit.
Use >>>14 instead.
 
Sorry, wondering if you could use some form of KISS principle...
Store each 18 bit value into a 32bit variable. You could have an array of them... 2 or 4 of them...
You could then simply push each of the values on to the TDR and receive them back from the RDR

If you wish for lower bits being transferred over SPI. Change the FRAMESIZE field of the TCR to have the value 17, and it will only
transfer 18 bits out of the 32 you put into each of the TDR and RDR will again only return the 18 bits. into your 32 bit value... No need to
muck with packing and the like...

EDIT: forgot to mention, that the ILI9486 driver sort of does this for SPI as they dont support RGB565 over SPI, the do support RGB666 which they store as 24 bits... So if I have memory enough for frame buffer (T4.1 with PSRAM) or SDRAM board, I can put frame buffer out into other memory
and also then do DMA out of this where 32 bits go into TDR and 24 bits go out over the buss...
 
That won't work because as mentioned in the opening post the ADCs are daisy chained together. A transfer of 18 bits will only receive a value from the ADC at the end (or front, depending on how you look at it) of the chain.
 
That won't work because as mentioned in the opening post the ADCs are daisy chained together. A transfer of 18 bits will only receive a value from the ADC at the end (or front, depending on how you look at it) of the chain.
I could easily be wrong, but how does the ADC chip know if my side of the SPI bus is storing the 18*4=72 bits into 3 uint32_t values or into 4?

That is, on the display: it could care less if I did: 3 SPI.transfer() calls to output the 24 bits (or receive), or if I instead tell it to output or receive 24 bits, in which case any transfer size between 17 and 32, it expects you to pass it 32 bits... (more important with the DMA setup...)

That is if enabled in the display code, each pixel is stored as 32 bites (uint32_t). The code to output the frame buffer to the
screen, just does:
Code:
            maybeUpdateTCR(_tcr_dc_not_assert | LPSPI_TCR_FRAMESZ(23) | LPSPI_TCR_CONT);
            while (pftbft < pfbtft_end) {
                _pimxrt_spi->TDR = *pftbft++;
                _pending_rx_count++;    //
                waitFifoNotFull();
            }
            maybeUpdateTCR(_tcr_dc_not_assert | LPSPI_TCR_FRAMESZ(23));
            _pimxrt_spi->TDR = *pftbft;
            _pending_rx_count++;    //
            waitTransmitComplete();

            #else
And it outputs the 24 bits per pixel
 
Hi Paul, yes that is the chip.

Attached is the schematic and a functioning sketch, and a simple python script that talks to it and logs the data to a file. The code is very preliminary, just simple and direct for testing.

The readout from one board looks okay, the voltages around the board are what they are supposed to be, and everything is cool as it should be.

But there are two problems. The first is an aside to what we have been talking about, and I think the first came from fab, but i will mention it anyway. I am only seeing dc levels and noise from the microphone (on either side of R1, R2, C2, C3). The DC levels are as listed in the datasheet for the microphone. The remainer of the path seems as it would be for those inputs. So, i think they somehow lost the instructions for handling the microphones.

Anyway, fab toasting the microphones should not effect experimenting with the ADC.

The other thing is that when I jumper the last SDI in the chain low, per the datasheet, the readout goes to zeroes. I am not done diagnosing that. I might suspect it is too much for the LDO in the Teensy, but it seems like it should be okay.


Anwyway, with those stories from the trences, I think the code in the sketch is more or less as gleaned from the conversations above and perhaps something along those line could be the basis of an extended interface in the SPI library.
 

Attachments

  • Microphone_ADS8887_rev0.pdf
    227.4 KB · Views: 18
  • Microphone_ADS8887_Controller_241213.zip
    14.4 KB · Views: 15
  • SimpleDataLoggerPython.zip
    221.1 KB · Views: 14
I could easily be wrong, but how does the ADC chip know if my side of the SPI bus is storing the 18*4=72 bits into 3 uint32_t values or into 4?
Because the transfer "begins" when the CS line goes low. That triggers the ADCs to place their sampled value into their shift registers ready for reading. So every time CS triggers it resets the reading position back to the beginning of the chain.

When the chips are chained together they basically become one big shift register that has to be read in a single transfer.
 
Okay, here it is, the corrected sketch.

The open question; What should the mode setting be in 3-wire mode, and in daisy-chain mode? Any advice?

Thank you
 

Attachments

  • Microphone_ADS8887_Controller_241213.zip
    15.6 KB · Views: 17
Back
Top