Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #pragma once
- #include <Arduino.h>
- #include <SPI.h>
- #include <stdexcept>
- /**
- A template for classes which communicate with an SPI device.
- Intended to cover the basics and pitfalls, providing a clean and easy to understand example.
- @note Transactions
- Transactions are necessary once more than a single device is operating on the same SPI
- interface. Each device might use a different configuration for transmitting data.
- Transactions ensure that this configuration is consistent during transmission.
- Not using transactions under such circumstances may lead to unexpected/erratic results.
- However, an open transaction will prevent other devices on the same SPI interface from being
- read from and/or written to. It also disables any interrupt registered via
- `SPI.usingInterrupt()` for the duration of the transaction.
- In general it is good practice to keep your transactions short.
- It is recommended you use the `spi*Transaction` methods (spiReadTransaction,
- spiWriteTransaction, spiTransferTransaction) for simple communication, since they guarantee
- ending the transaction.
- For more complex cases use `spiTransaction()` with a lambda. This method also guarantees
- the transaction is ended after.
- If you must, you can resort to manually starting and ending transactions using
- `spiBeginTransaction()` and `spiEndTransaction()`.
- @note Chip Select
- On SPI, every connected device has a dedicated Chip Select (CS) pin, which is used to indicate
- the device whether traffic on the SPI is intended for it or not.
- When the CS is HIGH, the device is supposed to ignore all traffic on the SPI.
- When the CS is LOW, traffic on the SPI is intended for that device.
- This class automatically handles setting the CS pin to the correct state.
- @note Method Naming
- You will find this class slightly deviates from common SPI method naming. It uses the
- following convention:
- * spiWrite* - methods which exclusively write to the device
- * spiRead* - methods which exclusively read from the device
- * spiTransfer* - duplex methods which write AND read to/from the device (in this order)
- @example Usage
- // Implement your SpiDevice as a subclass of SpiDevice with proper speed, bit order and mode settings
- class MySpiDevice : public SpiDevice<20000000, MSBFIRST, SPI_MODE0>{}
- // Provide the chip select (CS) pin your device uses
- // Any pin capable of digital output should do
- // NOTE: you MUST replace `REPLACE_WITH_PIN_NUMBER` with the number or identifier of the
- // exclusive CS pin your SPI device uses.
- constexpr uint8_t MY_DEVICE_CHIP_SELECT_PIN = REPLACE_WITH_PIN_NUMBER;
- // Declare an instance of your SPI device
- MySpiDevice myDevice(MY_DEVICE_CHIP_SELECT_PIN);
- void setup() {
- myDevice.init();
- }
- void loop() {
- uint8_t data8 = 123;
- uint16_t data16 = 12345;
- uint8_t dataBytes[] = "Hello World";
- uint8_t result8;
- uint16_t result16;
- uint8_t resultBytes[20];
- // OPTION 1:
- // Write data automatically wrapped in a transaction
- result8 = myDevice.spiTransferTransaction(data8); // or result16/data16
- // other devices are free to use SPI here
- myDevice.spiWriteTransaction(dataBytes, sizeof(dataBytes));
- // other devices are free to use SPI here too
- // OPTION 2:
- // explicitely start and end a transaction
- myDevice.spiTransaction([](auto &d) {
- d.spiWriteTransaction(dataBytes, sizeof(dataBytes)); // any number and type of transfers
- });
- // other devices are free to use SPI starting here
- // OPTION 3:
- // explicitely start and end a transaction
- myDevice.spiBeginTransaction();
- while(someCondition) {
- myDevice.spiWrite(data); // any number of transfers, any type of transfer
- }
- // before this call, NO OTHER DEVICE should use SPI, as it might need
- // different transaction settings and by that mess with yours.
- myDevice.spiEndTransaction();
- // optional, once entirely done with SPI, you can also end() it
- // this just makes sure, the CS pin is set to HIGH and SPI.end() is invoked.
- myDevice.spiEnd();
- }
- @note Further Reading
- * Arduino SPI documentation: https://docs.arduino.cc/language-reference/en/functions/communication/SPI/
- * Arduino SPI Guideline: https://docs.arduino.cc/learn/communication/spi/
- **/
- template<uint32_t SPI_SPEED_MAXIMUM, uint8_t SPI_DATA_ORDER, uint8_t SPI_DATA_MODE>
- class SpiDevice {
- protected:
- // whether a transaction is currently active
- bool inTransaction = false;
- // Chip Select pin - must be LOW when communicating with the device, HIGH otherwise
- const uint8_t _pinCs;
- // The communication settings used by the device
- const SPISettings _spi_settings;
- // The SPI interface to use, the default global `SPI` is usually fine. But you can pass in
- // a custom one if you have multiple SPI interfaces.
- SPIClass &_spi;
- public:
- /**
- Standard Constructor
- @argument [uint8_t]
- pinCs The dedicated Chip Select pin used by this SPI device
- @argument [SPIClass] spi
- The SPI interface to use. Defaults to the global `SPI` instance.
- Provide this argument if you use multiple SPI interfaces.
- **/
- SpiDevice(uint8_t pinCs, SPIClass &spi=SPI) :
- _pinCs(pinCs),
- _spi(spi) {}
- /**
- Initialize the SPI device and set up pins and the SPI interface.
- You MUST invoke this method in the setup() function.
- Make sure ALL devices are initialized before starting any transmissions, this is to make
- sure ONLY the device you intend to talk to is listening.
- Otherwise the CS pin of an uninitialized SPI device might be coincidentally LOW, leading to
- unexpected/erratic results.
- **/
- void init() const {
- // Calling SPI.begin() multiple times is safe, but omitting it is not.
- // Therefore we make sure it is definitively called before any trancations.
- _spi.begin();
- // set the pinMode for the chip select pin to output
- ::pinMode(_pinCs, OUTPUT);
- ::digitalWrite(_pinCs, HIGH); // default to disabling communication with device
- }
- uint8_t pinCs() const {
- return _pinCs;
- }
- /**
- TODO
- Behaves like spiRead(), but automatically wraps the transfer in spiBeginTransaction() and
- spiEndTransaction().
- @see spiRead()
- **/
- uint8_t* spiReadTransaction(uint8_t* dst, size_t len) const {
- spiBeginTransaction();
- spiRead(dst, len);
- spiEndTransaction();
- return dst;
- }
- /**
- Behaves like spiWrite(), but automatically wraps the transfer in spiBeginTransaction() and
- spiEndTransaction().
- @see spiWrite()
- **/
- void spiWriteTransaction(const uint8_t *data, size_t len) const {
- spiBeginTransaction();
- spiWrite(data, len);
- spiEndTransaction();
- }
- /**
- Behaves like spiTransfer(), but automatically wraps the transfer in spiBeginTransaction() and
- spiEndTransaction().
- @see spiTransfer()
- **/
- uint8_t spiTransferTransaction(uint8_t byte) const {
- spiBeginTransaction();
- uint8_t result = spiTransfer(byte);
- spiEndTransaction();
- return result;
- }
- /**
- Behaves like spiTransfer(), but automatically wraps the transfer in spiBeginTransaction() and
- spiEndTransaction().
- @see spiTransfer()
- **/
- uint16_t spiTransferTransaction(uint16_t bytes) const {
- spiBeginTransaction();
- uint16_t result = spiTransfer(bytes);
- spiEndTransaction();
- return result;
- }
- /**
- A safe way to perform multiple transfers, ensuring proper transactions.
- @return The return value of the provided callback.
- @example Usage
- myDevice.spiTransaction([](auto &d) {
- d.spiTransfer(data); // any number and type of transfers
- });
- **/
- template<class Func>
- auto spiTransaction(Func&& callback) const {
- class Ender {
- const SpiDevice &d;
- public:
- Ender(const SpiDevice &dev) : d(dev) {}
- ~Ender() { d.spiEndTransaction(); }
- } ender(*this);
- spiBeginTransaction();
- return callback(*this);
- }
- /**
- Begins a transaction.
- You can't start a new transaction without ending a previously started one.
- @see Class documentation note on transactions
- @see spiEndTransaction() - Ends the transaction started with spiBeginTransaction()
- @see spiTransaction() - A better way to ensure integrity with multiple writes
- @see spiWrite() - After invoking spiBeginTransaction(), you can communicate with your device using spiWrite()
- @see spiWriteTransaction() - An alternative where you don't need
- **/
- void spiBeginTransaction() {
- if (inTransaction) throw std::runtime_error("Already in a transaction");
- inTransaction = true;
- _spi.beginTransaction(_spi_settings);
- // CS must be set LOW _after_ beginTransaction(), since beginTransaction() may change
- // SPI mode/clock. If CS is low before this, the device sees mode changes mid-frame.
- ::digitalWrite(_pinCs, LOW);
- }
- /**
- Ends a transaction started with spiBeginTransaction().
- You SHOULD call this method once you're done reading from and/or writing to your SPI device.
- @see Class documentation note on transactions
- **/
- void spiEndTransaction() {
- ::digitalWrite(_pinCs, HIGH);
- _spi.endTransaction();
- inTransaction = false;
- }
- /**
- Reads `len` bytes from the SPI device, writes it into dst and returns the dst pointer.
- @note
- This method WILL write a single null byte (0x00) to the SPI device before reading.
- @note
- This method does NOT on its own begin/end a transaction. Therefore when using this
- method, you MUST ensure proper transaction handling.
- @see Class documentation note on transactions
- **/
- uint8_t* spiRead(uint8_t* dst, size_t len) const {
- #if defined(ESP32)
- _spi.transferBytes(nullptr, dst, len); // ESP32 supports null write buffer
- #elif defined(__AVR__)
- for (size_t i = 0; i < len; i++) dst[i] = _spi.transfer(0x00);
- #else
- for (size_t i = 0; i < len; i++) dst[i] = _spi.transfer(0x00);
- #endif
- return dst;
- }
- /**
- Sends `len` bytes to the SPI device.
- @note
- This method does NOT on its own begin/end a transaction. Therefore when using this
- method, you MUST ensure proper transaction handling.
- @see Class documentation note on transactions
- **/
- void spiWrite(const uint8_t *data, size_t len) const {
- #if defined(ESP32)
- _spi.writeBytes(data, len); // ESP32 has transferBytes(write, read, len)
- #elif defined(__AVR__)
- _spi.transfer((void*)data, (uint16_t)len); // AVR SPI supports transfer(buffer, size)
- #else
- for (size_t i = 0; i < len; i++) _spi.transfer(data[i]);
- #endif
- }
- /**
- Sends and receives a single byte to and from the SPI device.
- @note
- This method does NOT on its own begin/end a transaction. Therefore when using this
- method, you MUST ensure proper transaction handling.
- @see Class documentation note on transactions
- **/
- uint8_t spiTransfer(uint8_t byte) const {
- return _spi.transfer(byte);
- }
- /**
- Sends and receives two bytes to and from the SPI device.
- @note
- This method does NOT on its own begin/end a transaction. Therefore when using this
- method, you MUST ensure proper transaction handling.
- @see Class documentation note on transactions
- **/
- uint16_t spiTransfer(uint16_t bytes) const {
- return _spi.transfer(bytes);
- }
- /**
- Writes `len` bytes to the SPI device, then reads `len` bytes it, writing the read bytes
- into `rx` and returning the pointer to `rx`.
- @note
- This method does NOT on its own begin/end a transaction. Therefore when using this
- method, you MUST ensure proper transaction handling.
- @see Class documentation note on transactions
- **/
- uint8_t* spiTransfer(const uint8_t* tx, uint8_t* rx, size_t len) const {
- #if defined(ESP32)
- _spi.transferBytes((uint8_t*)tx, rx, len);
- #elif defined(__AVR__)
- for (size_t i = 0; i < len; i++) rx[i] = _spi.transfer(tx[i]);
- #else
- for (size_t i = 0; i < len; i++) rx[i] = _spi.transfer(tx[i]);
- #endif
- return rx;
- }
- /**
- Ends the usage of the SPI interface and sets the chip select pin HIGH (see class documentation).
- @note
- If you use this, you MUST NOT communicate with any device on this SPI interface.
- If you want to still communicate with devices again after invoking spiEnd(), you first
- MUST either call init() again or manually invoke begin() on the SPI interface itself.
- TODO: figure out under which circumstances invoking this method is advisable. Figure out whether the remark regarding SPI.begin() after .end() is correct.
- **/
- void spiEnd() const {
- _spi.end();
- ::digitalWrite(_pinCs, HIGH);
- }
- /**
- @return [SPIClass] The SPI interface used by this device.
- **/
- SPIClass& spi() const {
- return _spi;
- }
- /**
- @return SPISettings The SPI settings used by this device
- **/
- const SPISettings& spiSettings() const {
- return _spi_settings;
- }
- };
Advertisement
Add Comment
Please, Sign In to add comment