Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SPI #245

Open
soundanalogous opened this issue Nov 9, 2015 · 7 comments
Open

Add SPI #245

soundanalogous opened this issue Nov 9, 2015 · 7 comments

Comments

@soundanalogous
Copy link
Member

I had started work on the interface a while back: firmata/protocol#27.

And a thread here: firmata/protocol#26.

The tricky part is there are a lot of edge cases to cover. A decision needs to be made on where to draw the line regarding what is in and what is out in terms of support. Ideally this can be done in a way that the interface can scale if it becomes apparent that more functionality is needed.

@overdamped
Copy link

Has anyone attempted implementation of this yet? If not, I have a definite and immediate need that I'd like to try to implement the proposal towards.

@soundanalogous
Copy link
Member Author

There is an implementation. Still needs a few tweaks but would be great to have people start testing it to see where or if it falls short. See this thread: #341.

The implementation is in this branch: https://github.com/firmata/arduino/tree/spi-alpha.

There were a couple of changes I had not yet made in the spi-alpha branch yet that were discussed here: firmata/protocol#88.

@overdamped
Copy link

Excellent! I'll give it a spin and give some feedback. Thanks!

@soundanalogous
Copy link
Member Author

@overdamped I've updated the SPI implementation in the spi-alpha branch to match the firmata SPI protocol proposal.

Please try this version and provide any feedback you may have. I have an example client-side nodeJS implementation as well in the firmata.js spi-alpha branch.

I have only been able to test on an old LIS3LV02DQ accelerometer as that is the only SPI component I have. I'd love to see this tested on SPI components require sending and receiving more than 64 bytes at a time (this will test the multi-packet transfer case). Because Firmata sends data in 7-bit chunks, this means a 32 byte data packet would require 64 bytes to send. A SPI LCD screen is probably a good test case.

The part of the Firmata SPI proposal that I still think is kinda ugly, but don't have a better solution for is the [csPinControl] byte required with every SPI_TRANSFER, SPI_WRITE, or SPI_READ message. This is to allow the firmware to control the CS pin while still allowing multi-packet or streaming transfers (necessary if the packet length exceeds the transport (Serial, TCP, BLE) buffer size, which is 64 bytes for Serial for most architectures). However, this is the most efficient method I've found, the main advantage is that most simple SPI transfers (transfer, write or read) only require a single Firmata message. The 2 less efficient alternatives are:

Alternate 1: Requiring the user to frame every transfer with SPI_BEGIN_TRANSACTION and SPI_END_TRANSACTION messages (requires sending 3 Firmata messages for even simple transfers). In this case the SPI_BEGIN_TRANSACTION and SPI_END_TRANSACTION would handle toggling the CS pin appropriately. It would probably be the best approach semantically, but is not as efficient as what is currently proposed and implemented.

Alternate 2: Require the user to manually control the CS pin. This would also require sending 3 Firmata messages even for simple transfers.

@zfields FYI

@soundanalogous
Copy link
Member Author

soundanalogous commented Sep 24, 2017

I figure it's easier to understand if I attempt to explain the 3 different options for CS pin handling with a concrete example. To read the x, y, and z axis data from the LIS3LV02DQ accelerometer, the following steps are required:

  1. Pull the CS pin low
  2. Write a multi-byte command to setup the read
  3. Read the 6 data bytes (2 bytes for each axis)
  4. Set the CS pin back to high

There are at least 3 different ways this could be implemented in Firmata.

Option 1 (current implementation)
2 Firmata messages required to read the accelerometer data:

  1. SPI_WRITE(channel, CS_START_ONLY, numWordsToWrite, data[]) // CS pin is pulled low and stays low
  2. SPI_READ(channel, CS_END_ONLY, numWordsToRead) // CS pin is set back to HIGH

In option 1 SPI_BEGIN_TRANSACTION only needs to be sent once per device or when switching to another device on the same SPI bus.

// client-side example
board.spiBeginTransaction(deviceId, options);
...
function readAccelData() {
  // only requires 2 separate messages, but requires the user to understand 
  // how to use the pinControl options correctly and is thus more error prone
  board.spiWrite([MULTIBYTE_READ], {pinControl: CS_START_ONLY});
  board.spiRead(NUM_BYTES_TO_READ, {pinControl: CS_END_ONLY}, function(data) {
    // parse and act on the accelerometer data...
  });
}

Option 2 (frame every transfer as a transaction)
4 Firmata messages are required to read the accel data:

  1. SPI_BEGIN_TRANSACTION(params...) // CS pin is pulled low and stays low
  2. SPI_WRITE(channel, numWordsToWrite, data[])
  3. SPI_READ(channel, numWordsToRead)
  4. SPI_END_TRANSACTION // CS pin is set back to HIGH
// client-side example
function readAccelData() {
  // requires 4 separate messages, but it's clean
  board.spiBeginTransaction(deviceId, options);
  board.spiWrite([MULTIBYTE_READ]);
  board.spiRead(NUM_BYTES_TO_READ, function(data) {
    // parse and act on the accelerometer data...
  });
  board.spiEndTransaction();
}

Option 3 (manual control of CS pin)
4 Firmata messages are required to read the accel data:

  1. DIGITAL_MESSAGE to pull CS pin LOW
  2. SPI_WRITE(channel, numWordsToWrite, data[])
  3. SPI_READ(channel, numWordsToRead)
  4. DIGITAL_MESSAGE to set CS pin back to HIGH

In option 3 SPI_BEGIN_TRANSACTION only needs to be sent once per device or when switching to another device on the same SPI bus.

// client-side example
board.spiBeginTransaction(deviceId, options);
...
function readAccelData() {
  // also requires 4 separate messages, but requires manually handling CS pins for every transfer
  board.digitalWrite(csPin, LOW);
  board.spiWrite([MULTIBYTE_READ]);
  board.spiRead(NUM_BYTES_TO_READ, function(data) {
    // parse and act on the accelerometer data...
  });
  board.digitalWrite(csPin, HIGH);
}

@soundanalogous
Copy link
Member Author

A client library could also provide helper functions to mask some of the complexity in managing the CS pin. For example:

// example client library helper function to provide a higher level of abstraction
function readRegister(register, numBytesToRead, callback) {
  board.spiWrite(register, {pinControl: CS_START_ONLY});
  board.spiRead(numBytesToRead, {pinControl: CS_END_ONLY}, callback);
}

This enables our LIS3LV02DQ accelerometer example to use more familiar syntax:

board.spiBegin();

var deviceId = 9; // must be unique per device per application
board.spiBeginTransaction(deviceId, {
  bitOrder: MSBFIRST,
  dataMode: board.SPI_DATA_MODES.MODE0,
  maxClockSpeed: 2500000, // 2.5 Mhz
  csPin: 2,
  csActiveState: board.SPI_CS_ACTIVE_STATE.LOW
});

// Device on, 40hz, normal mode, all axis' enabled
board.spiWrite([register.CTRL_REG1, 0x87]);
// +/- 2g resolution
board.spiWrite([register.CTRL_REG2, 0x40]);

// setup multi-byte read
var readAddress = register.READ | READ_BIT | MULTI_BYTE_BIT;
board.readRegister(readAddress, BYTES_TO_READ, function(data) {
  var x = (data[1] << 8) | data[0];
  var y = (data[3] << 8) | data[2];
  var z = (data[5] << 8) | data[4];
  console.log("X: " + x + " Y: " + y + " Z: " + z);
});

board.spiEndTransaction();

@soundanalogous
Copy link
Member Author

soundanalogous commented Sep 30, 2017

Also, as a convenience when using more than one device on a single SPI bus, SPI_BEGIN_TRANSACTION could allow everything after the 4th byte (deviceId | channel) to be optional. That way only the first SPI_BEGIN_TRANSACTION for a specific device would need to send the full configuration, but subsequent messages would only need to send the deviceId, which would be used to lookup the settings for that device in stored in the firmware.

// Bytes 0 - 10 required the first time this message is sent for a specific SPI device
0:  START_SYSEX
1:  SPI_DATA              (0x68)
2:  SPI_BEGIN_TRANSACTION (0x01)
3:  deviceId | channel    (bits 2-6: deviceId, bits 0-1: channel)
4:  dataMode | bitOrder   (bits 1-2: dataMode (0-3), bit 0: bitOrder)
5:  maxSpeed              (bits 0 - 6)
6:  maxSpeed              (bits 7 - 14)
7:  maxSpeed              (bits 15 - 21)
8:  maxSpeed              (bits 22 - 28)
9:  maxSpeed              (bits 29 - 31)
10: wordSize              (0 = DEFAULT = 8-bits, 1 = 1-bit, 2 = 2 bits, etc)
11: csPin                 [optional] (0-127) The chip select pin number
12: csPinOptions          (required if csPin specified)
                          bit 0: CS_ACTIVE_STATE (0 = Active LOW (default)
                                                  1 = Active HIGH)
                          bit 1: CS_TOGGLE (0 = toggle at end of transfer (default)
                                            1 = toggle between words)
11|13: END_SYSEX
// subsequent messages only need to specify the deviceId and channel
0:  START_SYSEX
1:  SPI_DATA              (0x68)
2:  SPI_BEGIN_TRANSACTION (0x01)
3:  deviceId | channel    (bits 2-6: deviceId, bits 0-1: channel)
4:  END_SYSEX

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants