Serial data goes high when clock low?

nsaspook

Joined Aug 27, 2009
16,330
I've been looking closely at SPI but haven't seen a 2 wire SPI reference anywhere. In my case its only a master and a single slave so the chip select really isn't needed. So i'm left with a clock and a single data line between the master and slave. I know my implementation sends data both ways. What does the SPI protocol look like for sending data both ways on a single data line?
Sure you can send data both ways. I don't use resistors in this example because I set the TRIS mode on the SPI TX port pin for input or output.
This slave device uses a bi-directional data line for SPI.
https://www.parallax.com/product/lsm9ds1-9-axis-imu-module/

Example code modified for PIC32MK:
C:
/**
* @file LSM9DS1_SPIwrite.c
*
* @author Matthew Matz
*
* @version 0.5
*
* @copyright
* Copyright (C) Parallax, Inc. 2016. All Rights MIT Licensed.
*
* @brief This Propeller C library was created for the Parallax 9-axis IMU Sensor, based
* on the STMicroelectronics LSM9DS1 inertial motion sensor chip.
*/


#include "lsm9ds1.h"
#include "config/pic32mk_mcj_curiosity_pro/peripheral/gpio/plib_gpio.h"
#include "config/pic32mk_mcj_curiosity_pro/peripheral/spi/spi_master/plib_spi2_master.h"

extern int __pinM, __pinSDIO, __pinSCL;

/*
* PIC32: changed to use hardware SPI module and plib_gpio pin functions
*/
void imu_SPIwriteByte(unsigned char csPin, unsigned char subAddress, unsigned char data)
{
    uint8_t tmp = subAddress & 0x3F;

    SPI2CONbits.DISSDO = 0; // half-duplex switching, SPI TX ON, PORT pin mode OFF
    if (csPin == __pinM)
        csPin_m_Clear();
    else
        csPin_ag_Clear();

    SPI2_Write(&tmp, 1);
    SPI2_Write(&data, 1);
    if (csPin == __pinM)
        csPin_m_Set();
    else
        csPin_ag_Set();

}

void imu_SPIreadBytes(unsigned char csPin, unsigned char subAddress, unsigned char *dest, unsigned char count)
{
    int i;

    // To indicate a read, set bit 0 (msb) of first byte to 1
    unsigned char rAddress = 0x80 | (subAddress & 0x3F);

    // Mag SPI port is different. If we're reading multiple bytes,
    // set bit 1 to 1. The remaining six bytes are the address to be read
    if ((csPin == __pinM) && count > 1) rAddress |= 0x40;

    SPI2CONbits.DISSDO = 0; // half-duplex switching, SPI TX ON
    if (csPin == __pinM)
        csPin_m_Clear();
    else
        csPin_ag_Clear();

    SPI2_Write(&rAddress, 1);
    SPI2CONbits.DISSDO = 1;
    for (i = 0; i < count; i++) {
        SPI2_Read(&dest[i], 1); // half-duplex switching, SPI TX OFF, PORT pin mode input
    }
    if (csPin == __pinM)
        csPin_m_Set();
    else
        csPin_ag_Set();

}
/*
* Based on the Arduino Library for the LSM9SD1 by Jim Lindblom of Sparkfun Electronics
*/
C:
#include "definitions.h"                // SYS function prototypes
#include "../../Utility/libsimpletools/simpletools.h"
#include "lsm9ds1.h"
#include "imu.h"
#include "../../eadog.h"

int __pinAG, __pinM, __pinSDIO, __pinSCL;
char __autoCalc = 0;

int imu_init(int pinSCL, int pinSDIO, int pinAG, int pinM)
{
    char buffer[STR_BUF_SIZE];

    __pinAG = pinAG;
    __pinM = pinM;
    __pinSDIO = pinSDIO;
    __pinSCL = pinSCL;

    // Set both the Accel/Gyro and Mag to 3-wire SPI mode
    /*
     * We don't use SPI bit-banging for 3-wire because it's possible to setup a hardware 3-wire mode on the PIC32 module
     * on the pic32mk hardware SPI module jumper mosi to miso for half-duplex 3-wire mode and config for SPI MODE 3
     * set the TRIS mode on the SPI TX port pin for input so we can turn-off the transmitter voltage during SPI receive
     * on the bidirectional SPI line
     *
     * SPIxCON: SPI CONTROL REGISTER
     * bit 12 DISSDO: Disable SDOx pin bit(4)
     * 1 = SDOx pin is not used by the module. Pin is controlled by associated PORT register
     * 0 = SDOx pin is controlled by the module
     */
    TRISCbits.TRISC7 = 1; // SPI2 TX port set to port control as a input so we don't affect received data when in port mode
    imu_SPIwriteByte(__pinAG, CTRL_REG8, 0b00001100);
    imu_SPIwriteByte(__pinM, CTRL_REG3_M, 0b10000100);

    // To verify communication, we can read from the WHO_AM_I register of
    // each device. Store those in a variable so we can return them.
    char xgTest = 0, mTest = 0;
    imu_SPIreadBytes(__pinM, WHO_AM_I_M, (unsigned char *) &mTest, 1); // Read the gyro WHO_AM_I
    imu_SPIreadBytes(__pinAG, WHO_AM_I_XG, (unsigned char *) &xgTest, 1); // Read the accel/mag WHO_AM_I
    int whoAmICombined = (xgTest << 8) | mTest;

    if (whoAmICombined != ((WHO_AM_I_AG_RSP << 8) | WHO_AM_I_M_RSP)) return 0;

    //Init Gyro
    imu_SPIwriteByte(__pinAG, CTRL_REG1_G, 0xC0);
    imu_SPIwriteByte(__pinAG, CTRL_REG2_G, 0x00);
    imu_SPIwriteByte(__pinAG, CTRL_REG3_G, 0x00);
    imu_SPIwriteByte(__pinAG, CTRL_REG4, 0x38);
    imu_SPIwriteByte(__pinAG, ORIENT_CFG_G, 0x00);

    //Init Accel
    imu_SPIwriteByte(__pinAG, CTRL_REG5_XL, 0x38);
    imu_SPIwriteByte(__pinAG, CTRL_REG6_XL, 0xC0);
    imu_SPIwriteByte(__pinAG, CTRL_REG7_XL, 0x00);

    //Init Mag
    imu_SPIwriteByte(__pinM, CTRL_REG2_M, 0x00);
    imu_SPIwriteByte(__pinM, CTRL_REG4_M, 0x0C);
    imu_SPIwriteByte(__pinM, CTRL_REG5_M, 0x00);

    //Set Scales
    imu_setGyroScale(2000); // 2000
    imu_setAccelScale(16);  // 16
    imu_setMagScale(16);   // 16

    //Look for calibrations in NVRAM
    char biasStamp[8] = {0};
    char mBiasStored[8] = {0};
    char aBiasStored[8] = {0};
    char gBiasStored[8] = {0};
    get_nvram_str(0, biasStamp);
    get_nvram_str(7, mBiasStored);
    get_nvram_str(14, aBiasStored);
    get_nvram_str(21, gBiasStored);

    if (strncmp(biasStamp, "LSM9DS1", 7) == 0) {
        sprintf(buffer, "LSM9DS1 cal header found");
        eaDogM_WriteStringAtPos(8, 0, buffer);
        OledUpdate();
        if ((mBiasStored[0] = 'm')) {
            int mxB = (mBiasStored[1] << 8) | mBiasStored[2];
            int myB = (mBiasStored[3] << 8) | mBiasStored[4];
            int mzB = (mBiasStored[5] << 8) | mBiasStored[6];

            imu_setMagCalibration(mxB, myB, mzB);
        }

        if ((aBiasStored[0] = 'a')) {
            int axB = (aBiasStored[1] << 8) | aBiasStored[2];
            int ayB = (aBiasStored[3] << 8) | aBiasStored[4];
            int azB = (aBiasStored[5] << 8) | aBiasStored[6];

            imu_setAccelCalibration(axB, ayB, azB);
        }

        if ((gBiasStored[0] = 'g')) {
            int gxB = (gBiasStored[1] << 8) | gBiasStored[2];
            int gyB = (gBiasStored[3] << 8) | gBiasStored[4];
            int gzB = (gBiasStored[5] << 8) | gBiasStored[6];

            imu_setGyroCalibration(gxB, gyB, gzB);
        }
    } else {
        sprintf(buffer, "No LSM9DS1 cal header found %s", biasStamp);
        eaDogM_WriteStringAtPos(8, 0, buffer);
        OledUpdate();
    }



    // Once everything is initialized, return the WHO_AM_I registers we read:
    return whoAmICombined;
}
Code:
void imu_readAccel(int *ax, int *ay, int *az)
{
  unsigned char temp[6]; // We'll read six bytes from the accelerometer into temp 
  short tempX, tempY, tempZ;
 
  imu_SPIreadBytes(__pinAG, OUT_X_L_XL, temp, 6); // Read 6 bytes, beginning at OUT_X_L_XL
  tempX = (temp[1] << 8) | temp[0]; // Store x-axis values into ax
  tempY = (temp[3] << 8) | temp[2]; // Store y-axis values into ay
  tempZ = (temp[5] << 8) | temp[4]; // Store z-axis values into az
 
  *ax = (int) tempX;
  *ay = (int) tempY;
  *az = (int) tempZ;

  if (__autoCalc & 0b10)
  {
    *ax -= __aBiasRaw[X_AXIS];
    *ay -= __aBiasRaw[Y_AXIS];
    *az -= __aBiasRaw[Z_AXIS];
  }
}
1712253611317.png
Proto shield board on the PIC32MK DEV board.
1712253632490.png
1712253655333.png
I don't know if your device uses a similar method.
 
Last edited:

Thread Starter

DaveInPA

Joined Apr 3, 2024
12
Sure you can send data both ways. I don't use resistors in this example because I set the TRIS mode on the SPI TX port pin for input or output.
This slave device uses a bi-directional data line for SPI.
https://www.parallax.com/product/lsm9ds1-9-axis-imu-module/

Example code modified for PIC32MK:
C:
/**
* @file LSM9DS1_SPIwrite.c
*
* @author Matthew Matz
*
* @version 0.5
*
* @copyright
* Copyright (C) Parallax, Inc. 2016. All Rights MIT Licensed.
*
* @brief This Propeller C library was created for the Parallax 9-axis IMU Sensor, based
* on the STMicroelectronics LSM9DS1 inertial motion sensor chip.
*/


#include "lsm9ds1.h"
#include "config/pic32mk_mcj_curiosity_pro/peripheral/gpio/plib_gpio.h"
#include "config/pic32mk_mcj_curiosity_pro/peripheral/spi/spi_master/plib_spi2_master.h"

extern int __pinM, __pinSDIO, __pinSCL;

/*
* PIC32: changed to use hardware SPI module and plib_gpio pin functions
*/
void imu_SPIwriteByte(unsigned char csPin, unsigned char subAddress, unsigned char data)
{
    uint8_t tmp = subAddress & 0x3F;

    SPI2CONbits.DISSDO = 0; // half-duplex switching, SPI TX ON, PORT pin mode OFF
    if (csPin == __pinM)
        csPin_m_Clear();
    else
        csPin_ag_Clear();

    SPI2_Write(&tmp, 1);
    SPI2_Write(&data, 1);
    if (csPin == __pinM)
        csPin_m_Set();
    else
        csPin_ag_Set();

}

void imu_SPIreadBytes(unsigned char csPin, unsigned char subAddress, unsigned char *dest, unsigned char count)
{
    int i;

    // To indicate a read, set bit 0 (msb) of first byte to 1
    unsigned char rAddress = 0x80 | (subAddress & 0x3F);

    // Mag SPI port is different. If we're reading multiple bytes,
    // set bit 1 to 1. The remaining six bytes are the address to be read
    if ((csPin == __pinM) && count > 1) rAddress |= 0x40;

    SPI2CONbits.DISSDO = 0; // half-duplex switching, SPI TX ON
    if (csPin == __pinM)
        csPin_m_Clear();
    else
        csPin_ag_Clear();

    SPI2_Write(&rAddress, 1);
    SPI2CONbits.DISSDO = 1;
    for (i = 0; i < count; i++) {
        SPI2_Read(&dest[i], 1); // half-duplex switching, SPI TX OFF, PORT pin mode input
    }
    if (csPin == __pinM)
        csPin_m_Set();
    else
        csPin_ag_Set();

}
/*
* Based on the Arduino Library for the LSM9SD1 by Jim Lindblom of Sparkfun Electronics
*/
C:
#include "definitions.h"                // SYS function prototypes
#include "../../Utility/libsimpletools/simpletools.h"
#include "lsm9ds1.h"
#include "imu.h"
#include "../../eadog.h"

int __pinAG, __pinM, __pinSDIO, __pinSCL;
char __autoCalc = 0;

int imu_init(int pinSCL, int pinSDIO, int pinAG, int pinM)
{
    char buffer[STR_BUF_SIZE];

    __pinAG = pinAG;
    __pinM = pinM;
    __pinSDIO = pinSDIO;
    __pinSCL = pinSCL;

    // Set both the Accel/Gyro and Mag to 3-wire SPI mode
    /*
     * We don't use SPI bit-banging for 3-wire because it's possible to setup a hardware 3-wire mode on the PIC32 module
     * on the pic32mk hardware SPI module jumper mosi to miso for half-duplex 3-wire mode and config for SPI MODE 3
     * set the TRIS mode on the SPI TX port pin for input so we can turn-off the transmitter voltage during SPI receive
     * on the bidirectional SPI line
     *
     * SPIxCON: SPI CONTROL REGISTER
     * bit 12 DISSDO: Disable SDOx pin bit(4)
     * 1 = SDOx pin is not used by the module. Pin is controlled by associated PORT register
     * 0 = SDOx pin is controlled by the module
     */
    TRISCbits.TRISC7 = 1; // SPI2 TX port set to port control as a input so we don't affect received data when in port mode
    imu_SPIwriteByte(__pinAG, CTRL_REG8, 0b00001100);
    imu_SPIwriteByte(__pinM, CTRL_REG3_M, 0b10000100);

    // To verify communication, we can read from the WHO_AM_I register of
    // each device. Store those in a variable so we can return them.
    char xgTest = 0, mTest = 0;
    imu_SPIreadBytes(__pinM, WHO_AM_I_M, (unsigned char *) &mTest, 1); // Read the gyro WHO_AM_I
    imu_SPIreadBytes(__pinAG, WHO_AM_I_XG, (unsigned char *) &xgTest, 1); // Read the accel/mag WHO_AM_I
    int whoAmICombined = (xgTest << 8) | mTest;

    if (whoAmICombined != ((WHO_AM_I_AG_RSP << 8) | WHO_AM_I_M_RSP)) return 0;

    //Init Gyro
    imu_SPIwriteByte(__pinAG, CTRL_REG1_G, 0xC0);
    imu_SPIwriteByte(__pinAG, CTRL_REG2_G, 0x00);
    imu_SPIwriteByte(__pinAG, CTRL_REG3_G, 0x00);
    imu_SPIwriteByte(__pinAG, CTRL_REG4, 0x38);
    imu_SPIwriteByte(__pinAG, ORIENT_CFG_G, 0x00);

    //Init Accel
    imu_SPIwriteByte(__pinAG, CTRL_REG5_XL, 0x38);
    imu_SPIwriteByte(__pinAG, CTRL_REG6_XL, 0xC0);
    imu_SPIwriteByte(__pinAG, CTRL_REG7_XL, 0x00);

    //Init Mag
    imu_SPIwriteByte(__pinM, CTRL_REG2_M, 0x00);
    imu_SPIwriteByte(__pinM, CTRL_REG4_M, 0x0C);
    imu_SPIwriteByte(__pinM, CTRL_REG5_M, 0x00);

    //Set Scales
    imu_setGyroScale(2000); // 2000
    imu_setAccelScale(16);  // 16
    imu_setMagScale(16);   // 16

    //Look for calibrations in NVRAM
    char biasStamp[8] = {0};
    char mBiasStored[8] = {0};
    char aBiasStored[8] = {0};
    char gBiasStored[8] = {0};
    get_nvram_str(0, biasStamp);
    get_nvram_str(7, mBiasStored);
    get_nvram_str(14, aBiasStored);
    get_nvram_str(21, gBiasStored);

    if (strncmp(biasStamp, "LSM9DS1", 7) == 0) {
        sprintf(buffer, "LSM9DS1 cal header found");
        eaDogM_WriteStringAtPos(8, 0, buffer);
        OledUpdate();
        if ((mBiasStored[0] = 'm')) {
            int mxB = (mBiasStored[1] << 8) | mBiasStored[2];
            int myB = (mBiasStored[3] << 8) | mBiasStored[4];
            int mzB = (mBiasStored[5] << 8) | mBiasStored[6];

            imu_setMagCalibration(mxB, myB, mzB);
        }

        if ((aBiasStored[0] = 'a')) {
            int axB = (aBiasStored[1] << 8) | aBiasStored[2];
            int ayB = (aBiasStored[3] << 8) | aBiasStored[4];
            int azB = (aBiasStored[5] << 8) | aBiasStored[6];

            imu_setAccelCalibration(axB, ayB, azB);
        }

        if ((gBiasStored[0] = 'g')) {
            int gxB = (gBiasStored[1] << 8) | gBiasStored[2];
            int gyB = (gBiasStored[3] << 8) | gBiasStored[4];
            int gzB = (gBiasStored[5] << 8) | gBiasStored[6];

            imu_setGyroCalibration(gxB, gyB, gzB);
        }
    } else {
        sprintf(buffer, "No LSM9DS1 cal header found %s", biasStamp);
        eaDogM_WriteStringAtPos(8, 0, buffer);
        OledUpdate();
    }



    // Once everything is initialized, return the WHO_AM_I registers we read:
    return whoAmICombined;
}
Code:
void imu_readAccel(int *ax, int *ay, int *az)
{
  unsigned char temp[6]; // We'll read six bytes from the accelerometer into temp
  short tempX, tempY, tempZ;

  imu_SPIreadBytes(__pinAG, OUT_X_L_XL, temp, 6); // Read 6 bytes, beginning at OUT_X_L_XL
  tempX = (temp[1] << 8) | temp[0]; // Store x-axis values into ax
  tempY = (temp[3] << 8) | temp[2]; // Store y-axis values into ay
  tempZ = (temp[5] << 8) | temp[4]; // Store z-axis values into az

  *ax = (int) tempX;
  *ay = (int) tempY;
  *az = (int) tempZ;

  if (__autoCalc & 0b10)
  {
    *ax -= __aBiasRaw[X_AXIS];
    *ay -= __aBiasRaw[Y_AXIS];
    *az -= __aBiasRaw[Z_AXIS];
  }
}
View attachment 319138
Proto shield board on the PIC32MK DEV board.
View attachment 319139
View attachment 319140
I don't know if your device uses a similar method.

Thanks! Can you tell me which scope channels map to clock vs. data please. I'm wondering how your SPI implementation indicates the direction and how you coordinate that. I've been searching internet for 2-wire SPI implementations and not finding any examples...
 

nsaspook

Joined Aug 27, 2009
16,330
Thanks! Can you tell me which scope channels map to clock vs. data please. I'm wondering how your SPI implementation indicates the direction and how you coordinate that. I've been searching internet for 2-wire SPI implementations and not finding any examples...
1 data, 2 clock, 4 AG_CS on the IMU. The software knows when the slave will send data back because the master sends specific commands (bits) to the slave with a expected data sequence in return from the slave as the master sends additional clocks to the slave for that data.
 

phonetagger

Joined Aug 2, 2022
2
It occurs to me that the comms protocol could be entirely proprietary and even bit-banged. If so, the data line might be shared between different devices in order to keep pin count very low, and only clock line differentiates between devices. The firmware could be multithreaded, in which case comms could be interleaved between devices even on a bit-by-bit basis; each thread only needs to disable interrupts between the point that it sets the data line and the point it toggles the clock line; after that, it re-enables interrupts and a different thread cuts in & sends another bit to the other device using the same data line and a different clock line. Just a thought.
 

Thread Starter

DaveInPA

Joined Apr 3, 2024
12
It occurs to me that the comms protocol could be entirely proprietary and even bit-banged. If so, the data line might be shared between different devices in order to keep pin count very low, and only clock line differentiates between devices. The firmware could be multithreaded, in which case comms could be interleaved between devices even on a bit-by-bit basis; each thread only needs to disable interrupts between the point that it sets the data line and the point it toggles the clock line; after that, it re-enables interrupts and a different thread cuts in & sends another bit to the other device using the same data line and a different clock line. Just a thought.
I definitely think its proprietary and bit-banged as the MCUs on both side only support UART and 8-bit serial. No mention of SPI, IC2, etc. Its just a single device on the receiving side that has 3 alpha-numeric leds, 2 status indicator LEDs, and 2 push buttons. Still a mystery what is happening during the low clock cycles but i'm going to ignore for now. I'm now thinking i can treat this as SPI however since its just data being sent on clock pulses. Will be easier i bet to use ESP SPI functions rather than my own interrupts, etc.
 

sailorjoe

Joined Jun 4, 2013
365
A quick examination of the data table posted by the OP indicates that every time the data line is H before the clock, the previous data line at clock is also an H. This implies that there is a shift register that is pushing bits out just before the clock goes low to high, and the prior bit is still on the line while it comes out of high impedance and before the next bit is actually shifted out just before the clock. If this is true, the pre-clock data can be ignored.
Just my first impression; needs to be verified.
 

Thread Starter

DaveInPA

Joined Apr 3, 2024
12
The difference in rise and fall times is concerning. Hard to guess what the data is really supposed to look like. NRZ?
You may be looking at subsequent posts where I show the signal when the cable is not terminated. The first post has the signal with it terminated and being pulled down. Or are you saying the rise times on those signals look problematic?

I’ve been trying to figure out what impacts rise and fall times. Is it the transmission line capacitance and resistance, the terminating resistor, the impedance of the receiving McU, or all the above…
 
Top