Difference between revisions of "Using SPI with shift registers"

From HalfgeekKB
Jump to navigation Jump to search
 
Line 72: Line 72:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
===Sharing the MISO line with other SPI devices===
 +
 +
Connecting the serial output Q7 directly to the MISO line of the master is only allowable if no other devices on the bus share the MISO line. This is because a legitimate SPI device is expected to tri-state its MISO line when the device is not selected in order to avoid bus contention.
 +
 +
* Even though obviously it's the principle of the thing, for production purposes you might consider substituting a different device that already does what you need.
 +
** The 74HC589 is similar to (but not pin-compatible with) the '165, but with a tri-state MISO line. It would replace a '165 and probably be capable of chaining with other '165 devices. It costs only a tiny bit more than a '165 and substantially less than a single '165 plus a separate buffer. Some will find issue with the fact that there apparently only one supplier (ON Semiconductor).
 +
** Full I/O expanders such as the MCP23S08 (8-bit) and MCP23S17 (16-bit) exist that use the SPI bus at full speed. Such an I/O expander is substantially more expensive than the equivalent shift registers (at current prices an MCP23S17 is 2:1 the combined '165 and '595 it would approximately replace) and a bit more complicated to use, but in exchange provide luxury I/O ports with the works, including direction control, configurable input pullups, and per-port interrupt on change.
 +
*** Such I/O expanders are also available for I2C bus instead (see also MCP23008, MCP23017).
 +
* A tri-state buffer can be placed between Q7 and MISO that is switched by the /SS line.
 +
** The 74HC125 contains four buffers, each with its own /OE pin. This could be used to make multiple '165-like devices electrically compatible with SPI.
 +
** The 74HC1G125 is a single-gate version of the 74HC125, suitable for a single '165-like device. However, currently, it costs a little more than the ordinary '125.
 +
** The 74HC595 has its own serial output Q7 that is switched by its /OE pin. Chaining with a '595 in this way would require a dummy byte to be shifted through before the input could be read, and the '595 would probably not be usable for output by the application because its /OE would need to be set to disabled most of the time.
 +
** A 74HC589-based device (see above) at the head of the chain before any actual '165-like devices would take care of the issue.
 +
* If you control all devices on the bus, you may be able to convert MISO on all devices on the line to open-drain by adding (an inverter and) a MOSFET to the device output. In the case of a '165, the /Q7 output can be used instead of the Q7 to preserve the polarity.
  
 
==74HC595 and similar==
 
==74HC595 and similar==

Latest revision as of 06:24, 2 March 2018

74HC165 and similar

// 74HC165 connection via SPI

#include <SPI.h>

//  UNO   SPI   '165
//  9*    n/a   storage clock (ST_CP, /PL, DIP #1)
//  10*   NSS   shift clock inhibit (CLK INH, /CE, DIP #15)
//  11    MOSI  n/c
//  12    MISO  serial data output (Q7, DIP #9)
//  13    SCK   shift clock (SH_CP, CP, DIP #2)

// Note that the storage clock and the NSS can be anywhere. The UNO pin 10
// must be OUTPUT for the SPI master to work (the "NSS" functionality of the
// pin is applicable for SPI *in slave mode* when it is an input), but it
// need not be the actual select pin.

const uint8_t IM_STO = 9;
const uint8_t IM_NSS = 10;

const unsigned long IM_MAX_SPEED = 4000000; // A guess; could probably do way faster
const uint8_t IM_BIT_ORDER = MSBFIRST;
const uint8_t IM_SPI_MODE = SPI_MODE0;

void setup() {
  pinMode(IM_STO, OUTPUT);
  digitalWrite(IM_STO, true);
  pinMode(IM_NSS, OUTPUT);
  digitalWrite(IM_NSS, true);

  Serial.begin(115200);
  Serial.println("start");

  SPI.begin();
}

void sendStorePulse() {
  digitalWrite(IM_STO, false);
  delayMicroseconds(5);
  digitalWrite(IM_STO, true);
  delayMicroseconds(5);
}

uint8_t loadBySpi() {
  // Select device and begin transaction
  digitalWrite(IM_NSS, false);
  SPI.beginTransaction(SPISettings(IM_MAX_SPEED, IM_BIT_ORDER, IM_SPI_MODE));

  // Perform transfer
  uint8_t receivedData = SPI.transfer(0x00);

  // End transaction and deselect device
  SPI.endTransaction();
  digitalWrite(IM_NSS, true);

  return receivedData;
}

void loop() {
  sendStorePulse();
  uint8_t reading = loadBySpi();

  Serial.print("SPI: ");
  Serial.print(reading, BIN);
  Serial.println();

  delay(1000);
}

Sharing the MISO line with other SPI devices

Connecting the serial output Q7 directly to the MISO line of the master is only allowable if no other devices on the bus share the MISO line. This is because a legitimate SPI device is expected to tri-state its MISO line when the device is not selected in order to avoid bus contention.

  • Even though obviously it's the principle of the thing, for production purposes you might consider substituting a different device that already does what you need.
    • The 74HC589 is similar to (but not pin-compatible with) the '165, but with a tri-state MISO line. It would replace a '165 and probably be capable of chaining with other '165 devices. It costs only a tiny bit more than a '165 and substantially less than a single '165 plus a separate buffer. Some will find issue with the fact that there apparently only one supplier (ON Semiconductor).
    • Full I/O expanders such as the MCP23S08 (8-bit) and MCP23S17 (16-bit) exist that use the SPI bus at full speed. Such an I/O expander is substantially more expensive than the equivalent shift registers (at current prices an MCP23S17 is 2:1 the combined '165 and '595 it would approximately replace) and a bit more complicated to use, but in exchange provide luxury I/O ports with the works, including direction control, configurable input pullups, and per-port interrupt on change.
      • Such I/O expanders are also available for I2C bus instead (see also MCP23008, MCP23017).
  • A tri-state buffer can be placed between Q7 and MISO that is switched by the /SS line.
    • The 74HC125 contains four buffers, each with its own /OE pin. This could be used to make multiple '165-like devices electrically compatible with SPI.
    • The 74HC1G125 is a single-gate version of the 74HC125, suitable for a single '165-like device. However, currently, it costs a little more than the ordinary '125.
    • The 74HC595 has its own serial output Q7 that is switched by its /OE pin. Chaining with a '595 in this way would require a dummy byte to be shifted through before the input could be read, and the '595 would probably not be usable for output by the application because its /OE would need to be set to disabled most of the time.
    • A 74HC589-based device (see above) at the head of the chain before any actual '165-like devices would take care of the issue.
  • If you control all devices on the bus, you may be able to convert MISO on all devices on the line to open-drain by adding (an inverter and) a MOSFET to the device output. In the case of a '165, the /Q7 output can be used instead of the Q7 to preserve the polarity.

74HC595 and similar

#include <SPI.h>

//  UNO   SPI   '595
//  9*    n/a   output disable (/OE, DIP #13)
//  10*   NSS   storage clock (ST_CP, DIP #12)
//  11    MOSI  serial data in (DS, DIP #14)
//  12    MISO  n/c
//  13    SCK   shift clock (SH_CP, CP, DIP #11)

const uint8_t OM_BLK = 9;
const uint8_t OM_STO = 10;

const unsigned long OM_MAX_SPEED = 4000000; // A guess
const uint8_t OM_BIT_ORDER = LSBFIRST;
const uint8_t OM_SPI_MODE = SPI_MODE0;

void setup() {
    pinMode(OM_BLK, OUTPUT);
    digitalWrite(OM_BLK, true);
    pinMode(OM_STO, OUTPUT);
    digitalWrite(OM_STO, true);

    SPI.begin();
}

void sendBySpi(uint8_t value) {
  digitalWrite(OM_STO, false);
  SPI.beginTransaction(SPISettings(OM_MAX_SPEED, OM_BIT_ORDER, OM_SPI_MODE));
  SPI.transfer(value);
  SPI.endTransaction();
  digitalWrite(OM_STO, true);
  digitalWrite(OM_BLK, false);
}

uint8_t c = 0;

void loop() {
  c = (uint8_t)((c + 1) & 0xFF);
  sendBySpi(c);
  delay(100);
}

'165 and '595 in same transaction

The '165 and '595 devices are similar enough that they can be combined as a single conceptual SPI device. One of the select lines from each device can even be combined.

#include <SPI.h>
 
// Where
//   IM means "input module" (e.g. 74HC165) and
//   OM means "output module" (e.g. 74HC595):
 
//  UNO   SPI   Pin description
//  8*          OM output disable ('595 /OE, DIP #13)
//  9*          OM storage clock ('595 ST_CP, DIP #12)
//              IM shift clock inhibit ('165 CLK INH, /CE, DIP #15)
//  10*   *     IM storage clock ('165 ST_CP, /PL, DIP #1)
//  11    MOSI  IM serial data in ('165 DS, DIP #14)
//  12    MISO  OM serial data output ('595 Q7, DIP #9)
//  13    SCK   OM shift clock ('595 SH_CP, CP, DIP #11)
//              IM shift clock ('165 SH_CP, CP, DIP #2)
 
// The functions on UNO #8, #9, and #10 can be moved to any output pins. As
// documented elsewhere, UNO #10 must be set to input mode in order for the
// SPI peripheral to operate in master mode, but need not actually be used
// as a slave select while operating as master.
 
// In many applications the OM output disable can be tied low instead of
// being mapped to a signal, in which case it can be ignored by the program.

// Alternatively, IM storage clock may be tied to OM output disable, in
// which case the outputs are turned off while the inputs are being read
// out. (Though I can't think of a situation where that might be useful at
// the moment.)
 
const uint8_t OM_BLK = 8;
const uint8_t IOM_NSS = 9; // This is OM's STO and IM's DIS
const uint8_t IM_STO = 10;
 
const unsigned long IOM_MAX_SPEED = 4000000;
const uint8_t IOM_BIT_ORDER = LSBFIRST;
const uint8_t IOM_SPI_MODE = SPI_MODE0;
 
inline void setOutput(uint8_t pin, bool state = true) {
  pinMode(pin, OUTPUT);
  digitalWrite(pin, state);
}
 
void setup() {
  setOutput(IOM_NSS);
  setOutput(OM_BLK);
  setOutput(IM_STO);
 
  Serial.begin(115200);
  Serial.println("Starting SPI");
 
  SPI.begin();
}
 
uint8_t exchangeBySpi(uint8_t outValue) {
  digitalWrite(IM_STO, true);
  //delayMicroseconds(5);
 
  digitalWrite(IOM_NSS, false);
  SPI.beginTransaction(SPISettings(IOM_MAX_SPEED, IOM_BIT_ORDER, IOM_SPI_MODE));
  uint8_t inValue = SPI.transfer(outValue);
  SPI.endTransaction();
  digitalWrite(IOM_NSS, true);
   
  digitalWrite(IM_STO, false);
  //delayMicroseconds(5);

  digitalWrite(OM_BLK, false);
  
  return inValue;
}
 
// Writes out the previous reading and reads in a new reading.
// (The output follows the input after a short delay.)
 
uint8_t nextOutValue = 0;
 
void loop() {
  nextOutValue = exchangeBySpi(nextOutValue);
  delay(100);
}