Guest User

SpiDevice

a guest
Nov 16th, 2025
53
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C++ 14.46 KB | Source Code | 0 0
  1. #pragma once
  2. #include <Arduino.h>
  3. #include <SPI.h>
  4. #include <stdexcept>
  5.  
  6.  
  7.  
  8. /**
  9.     A template for classes which communicate with an SPI device.
  10.     Intended to cover the basics and pitfalls, providing a clean and easy to understand example.
  11.  
  12.  
  13.     @note Transactions
  14.         Transactions are necessary once more than a single device is operating on the same SPI
  15.         interface. Each device might use a different configuration for transmitting data.
  16.         Transactions ensure that this configuration is consistent during transmission.
  17.         Not using transactions under such circumstances may lead to unexpected/erratic results.
  18.  
  19.         However, an open transaction will prevent other devices on the same SPI interface from being
  20.         read from and/or written to. It also disables any interrupt registered via
  21.         `SPI.usingInterrupt()` for the duration of the transaction.
  22.  
  23.         In general it is good practice to keep your transactions short.
  24.         It is recommended you use the `spi*Transaction` methods (spiReadTransaction,
  25.         spiWriteTransaction, spiTransferTransaction) for simple communication, since they guarantee
  26.         ending the transaction.
  27.         For more complex cases use `spiTransaction()` with a lambda. This method also guarantees
  28.         the transaction is ended after.
  29.         If you must, you can resort to manually starting and ending transactions using
  30.         `spiBeginTransaction()` and `spiEndTransaction()`.
  31.  
  32.  
  33.     @note Chip Select
  34.         On SPI, every connected device has a dedicated Chip Select (CS) pin, which is used to indicate
  35.         the device whether traffic on the SPI is intended for it or not.
  36.         When the CS is HIGH, the device is supposed to ignore all traffic on the SPI.
  37.         When the CS is LOW, traffic on the SPI is intended for that device.
  38.         This class automatically handles setting the CS pin to the correct state.
  39.  
  40.  
  41.     @note Method Naming
  42.         You will find this class slightly deviates from common SPI method naming. It uses the
  43.         following convention:
  44.         * spiWrite* - methods which exclusively write to the device
  45.         * spiRead* - methods which exclusively read from the device
  46.         * spiTransfer* - duplex methods which write AND read to/from the device (in this order)
  47.  
  48.  
  49.     @example Usage
  50.         // Implement your SpiDevice as a subclass of SpiDevice with proper speed, bit order and mode settings
  51.         class MySpiDevice : public SpiDevice<20000000, MSBFIRST, SPI_MODE0>{}
  52.  
  53.         // Provide the chip select (CS) pin your device uses
  54.         // Any pin capable of digital output should do
  55.         // NOTE: you MUST replace `REPLACE_WITH_PIN_NUMBER` with the number or identifier of the
  56.         //       exclusive CS pin your SPI device uses.
  57.         constexpr uint8_t MY_DEVICE_CHIP_SELECT_PIN = REPLACE_WITH_PIN_NUMBER;
  58.  
  59.         // Declare an instance of your SPI device
  60.         MySpiDevice myDevice(MY_DEVICE_CHIP_SELECT_PIN);
  61.  
  62.         void setup() {
  63.             myDevice.init();
  64.         }
  65.  
  66.         void loop() {
  67.             uint8_t  data8       = 123;
  68.             uint16_t data16      = 12345;
  69.             uint8_t  dataBytes[] = "Hello World";
  70.             uint8_t  result8;
  71.             uint16_t result16;
  72.             uint8_t  resultBytes[20];
  73.  
  74.  
  75.             // OPTION 1:
  76.             // Write data automatically wrapped in a transaction
  77.             result8 = myDevice.spiTransferTransaction(data8); // or result16/data16
  78.             // other devices are free to use SPI here
  79.             myDevice.spiWriteTransaction(dataBytes, sizeof(dataBytes));
  80.             // other devices are free to use SPI here too
  81.  
  82.  
  83.             // OPTION 2:
  84.             // explicitely start and end a transaction
  85.             myDevice.spiTransaction([](auto &d) {
  86.                 d.spiWriteTransaction(dataBytes, sizeof(dataBytes)); // any number and type of transfers
  87.             });
  88.             // other devices are free to use SPI starting here
  89.  
  90.  
  91.             // OPTION 3:
  92.             // explicitely start and end a transaction
  93.             myDevice.spiBeginTransaction();
  94.             while(someCondition) {
  95.                 myDevice.spiWrite(data); // any number of transfers, any type of transfer
  96.             }
  97.             // before this call, NO OTHER DEVICE should use SPI, as it might need
  98.             // different transaction settings and by that mess with yours.
  99.             myDevice.spiEndTransaction();
  100.  
  101.             // optional, once entirely done with SPI, you can also end() it
  102.             // this just makes sure, the CS pin is set to HIGH and SPI.end() is invoked.
  103.             myDevice.spiEnd();
  104.         }
  105.  
  106.     @note Further Reading
  107.         * Arduino SPI documentation: https://docs.arduino.cc/language-reference/en/functions/communication/SPI/
  108.         * Arduino SPI Guideline: https://docs.arduino.cc/learn/communication/spi/
  109. **/
  110. template<uint32_t SPI_SPEED_MAXIMUM, uint8_t SPI_DATA_ORDER, uint8_t SPI_DATA_MODE>
  111. class SpiDevice {
  112. protected:
  113.     // whether a transaction is currently active
  114.     bool inTransaction = false;
  115.  
  116.  
  117.  
  118.     // Chip Select pin - must be LOW when communicating with the device, HIGH otherwise
  119.     const uint8_t _pinCs;
  120.  
  121.  
  122.     // The communication settings used by the device
  123.     const SPISettings _spi_settings;
  124.  
  125.  
  126.  
  127.     // The SPI interface to use, the default global `SPI` is usually fine. But you can pass in
  128.     // a custom one if you have multiple SPI interfaces.
  129.     SPIClass &_spi;
  130.  
  131.  
  132.  
  133. public:
  134.     /**
  135.         Standard Constructor
  136.  
  137.         @argument [uint8_t]
  138.             pinCs The dedicated Chip Select pin used by this SPI device
  139.         @argument [SPIClass] spi
  140.             The SPI interface to use. Defaults to the global `SPI` instance.
  141.             Provide this argument if you use multiple SPI interfaces.
  142.     **/
  143.     SpiDevice(uint8_t pinCs, SPIClass &spi=SPI) :
  144.         _pinCs(pinCs),
  145.         _spi(spi) {}
  146.  
  147.  
  148.  
  149.     /**
  150.         Initialize the SPI device and set up pins and the SPI interface.
  151.         You MUST invoke this method in the setup() function.
  152.         Make sure ALL devices are initialized before starting any transmissions, this is to make
  153.         sure ONLY the device you intend to talk to is listening.
  154.         Otherwise the CS pin of an uninitialized SPI device might be coincidentally LOW, leading to
  155.         unexpected/erratic results.
  156.     **/
  157.     void init() const {
  158.         // Calling SPI.begin() multiple times is safe, but omitting it is not.
  159.         // Therefore we make sure it is definitively called before any trancations.
  160.         _spi.begin();
  161.  
  162.         // set the pinMode for the chip select pin to output
  163.         ::pinMode(_pinCs, OUTPUT);
  164.         ::digitalWrite(_pinCs, HIGH); // default to disabling communication with device
  165.     }
  166.  
  167.  
  168.     uint8_t pinCs() const {
  169.         return _pinCs;
  170.     }
  171.  
  172.  
  173.  
  174.     /**
  175.         TODO
  176.         Behaves like spiRead(), but automatically wraps the transfer in spiBeginTransaction() and
  177.         spiEndTransaction().
  178.  
  179.         @see spiRead()
  180.     **/
  181.     uint8_t* spiReadTransaction(uint8_t* dst, size_t len) const {
  182.         spiBeginTransaction();
  183.         spiRead(dst, len);
  184.         spiEndTransaction();
  185.  
  186.         return dst;
  187.     }
  188.  
  189.  
  190.  
  191.     /**
  192.         Behaves like spiWrite(), but automatically wraps the transfer in spiBeginTransaction() and
  193.         spiEndTransaction().
  194.  
  195.         @see spiWrite()
  196.     **/
  197.     void spiWriteTransaction(const uint8_t *data, size_t len) const {
  198.         spiBeginTransaction();
  199.         spiWrite(data, len);
  200.         spiEndTransaction();
  201.     }
  202.  
  203.  
  204.  
  205.     /**
  206.         Behaves like spiTransfer(), but automatically wraps the transfer in spiBeginTransaction() and
  207.         spiEndTransaction().
  208.  
  209.         @see spiTransfer()
  210.     **/
  211.     uint8_t spiTransferTransaction(uint8_t byte) const {
  212.         spiBeginTransaction();
  213.         uint8_t result = spiTransfer(byte);
  214.         spiEndTransaction();
  215.  
  216.         return result;
  217.     }
  218.  
  219.  
  220.  
  221.     /**
  222.         Behaves like spiTransfer(), but automatically wraps the transfer in spiBeginTransaction() and
  223.         spiEndTransaction().
  224.  
  225.         @see spiTransfer()
  226.     **/
  227.     uint16_t spiTransferTransaction(uint16_t bytes) const {
  228.         spiBeginTransaction();
  229.         uint16_t result = spiTransfer(bytes);
  230.         spiEndTransaction();
  231.  
  232.         return result;
  233.     }
  234.  
  235.  
  236.  
  237.     /**
  238.         A safe way to perform multiple transfers, ensuring proper transactions.
  239.  
  240.         @return The return value of the provided callback.
  241.  
  242.         @example Usage
  243.             myDevice.spiTransaction([](auto &d) {
  244.                 d.spiTransfer(data); // any number and type of transfers
  245.             });
  246.     **/
  247.     template<class Func>
  248.     auto spiTransaction(Func&& callback) const {
  249.         class Ender {
  250.             const SpiDevice &d;
  251.         public:
  252.             Ender(const SpiDevice &dev) : d(dev) {}
  253.             ~Ender() { d.spiEndTransaction(); }
  254.         } ender(*this);
  255.  
  256.         spiBeginTransaction();
  257.         return callback(*this);
  258.     }
  259.  
  260.  
  261.  
  262.  
  263.     /**
  264.         Begins a transaction.
  265.         You can't start a new transaction without ending a previously started one.
  266.  
  267.         @see Class documentation note on transactions
  268.         @see spiEndTransaction() - Ends the transaction started with spiBeginTransaction()
  269.         @see spiTransaction() - A better way to ensure integrity with multiple writes
  270.         @see spiWrite() - After invoking spiBeginTransaction(), you can communicate with your device using spiWrite()
  271.         @see spiWriteTransaction() - An alternative where you don't need
  272.     **/
  273.     void spiBeginTransaction() {
  274.         if (inTransaction) throw std::runtime_error("Already in a transaction");
  275.         inTransaction = true;
  276.         _spi.beginTransaction(_spi_settings);
  277.  
  278.         // CS must be set LOW _after_ beginTransaction(), since beginTransaction() may change
  279.         // SPI mode/clock. If CS is low before this, the device sees mode changes mid-frame.
  280.         ::digitalWrite(_pinCs, LOW);
  281.     }
  282.  
  283.  
  284.  
  285.     /**
  286.         Ends a transaction started with spiBeginTransaction().
  287.         You SHOULD call this method once you're done reading from and/or writing to your SPI device.
  288.  
  289.         @see Class documentation note on transactions
  290.     **/
  291.     void spiEndTransaction() {
  292.         ::digitalWrite(_pinCs, HIGH);
  293.  
  294.         _spi.endTransaction();
  295.         inTransaction = false;
  296.     }
  297.  
  298.  
  299.  
  300.     /**
  301.         Reads `len` bytes from the SPI device, writes it into dst and returns the dst pointer.
  302.  
  303.         @note
  304.             This method WILL write a single null byte (0x00) to the SPI device before reading.
  305.  
  306.         @note
  307.             This method does NOT on its own begin/end a transaction. Therefore when using this
  308.             method, you MUST ensure proper transaction handling.
  309.  
  310.         @see Class documentation note on transactions
  311.     **/
  312.     uint8_t* spiRead(uint8_t* dst, size_t len) const {
  313.         #if defined(ESP32)
  314.             _spi.transferBytes(nullptr, dst, len); // ESP32 supports null write buffer
  315.         #elif defined(__AVR__)
  316.             for (size_t i = 0; i < len; i++) dst[i] = _spi.transfer(0x00);
  317.         #else
  318.             for (size_t i = 0; i < len; i++) dst[i] = _spi.transfer(0x00);
  319.         #endif
  320.  
  321.         return dst;
  322.     }
  323.  
  324.  
  325.     /**
  326.         Sends `len` bytes to the SPI device.
  327.  
  328.         @note
  329.             This method does NOT on its own begin/end a transaction. Therefore when using this
  330.             method, you MUST ensure proper transaction handling.
  331.  
  332.         @see Class documentation note on transactions
  333.     **/
  334.     void spiWrite(const uint8_t *data, size_t len) const {
  335.         #if defined(ESP32)
  336.             _spi.writeBytes(data, len); // ESP32 has transferBytes(write, read, len)
  337.         #elif defined(__AVR__)
  338.             _spi.transfer((void*)data, (uint16_t)len); // AVR SPI supports transfer(buffer, size)
  339.         #else
  340.             for (size_t i = 0; i < len; i++) _spi.transfer(data[i]);
  341.         #endif
  342.     }
  343.  
  344.  
  345.  
  346.  
  347.     /**
  348.         Sends and receives a single byte to and from the SPI device.
  349.  
  350.         @note
  351.             This method does NOT on its own begin/end a transaction. Therefore when using this
  352.             method, you MUST ensure proper transaction handling.
  353.  
  354.         @see Class documentation note on transactions
  355.     **/
  356.     uint8_t spiTransfer(uint8_t byte) const {
  357.         return _spi.transfer(byte);
  358.     }
  359.  
  360.  
  361.  
  362.     /**
  363.         Sends and receives two bytes to and from the SPI device.
  364.  
  365.         @note
  366.             This method does NOT on its own begin/end a transaction. Therefore when using this
  367.             method, you MUST ensure proper transaction handling.
  368.  
  369.         @see Class documentation note on transactions
  370.     **/
  371.     uint16_t spiTransfer(uint16_t bytes) const {
  372.         return _spi.transfer(bytes);
  373.     }
  374.  
  375.  
  376.  
  377.     /**
  378.         Writes `len` bytes to the SPI device, then reads `len` bytes it, writing the read bytes
  379.         into `rx` and returning the pointer to `rx`.
  380.  
  381.         @note
  382.             This method does NOT on its own begin/end a transaction. Therefore when using this
  383.             method, you MUST ensure proper transaction handling.
  384.  
  385.         @see Class documentation note on transactions
  386.     **/
  387.     uint8_t* spiTransfer(const uint8_t* tx, uint8_t* rx, size_t len) const {
  388.         #if defined(ESP32)
  389.             _spi.transferBytes((uint8_t*)tx, rx, len);
  390.         #elif defined(__AVR__)
  391.             for (size_t i = 0; i < len; i++) rx[i] = _spi.transfer(tx[i]);
  392.         #else
  393.             for (size_t i = 0; i < len; i++) rx[i] = _spi.transfer(tx[i]);
  394.         #endif
  395.  
  396.         return rx;
  397.     }
  398.  
  399.  
  400.  
  401.     /**
  402.         Ends the usage of the SPI interface and sets the chip select pin HIGH (see class documentation).
  403.  
  404.         @note
  405.             If you use this, you MUST NOT communicate with any device on this SPI interface.
  406.             If you want to still communicate with devices again after invoking spiEnd(), you first
  407.             MUST either call init() again or manually invoke begin() on the SPI interface itself.
  408.  
  409.         TODO: figure out under which circumstances invoking this method is advisable. Figure out whether the remark regarding SPI.begin() after .end() is correct.
  410.     **/
  411.     void spiEnd() const {
  412.         _spi.end();
  413.         ::digitalWrite(_pinCs, HIGH);
  414.     }
  415.  
  416.  
  417.  
  418.     /**
  419.         @return [SPIClass] The SPI interface used by this device.
  420.     **/
  421.     SPIClass& spi() const {
  422.         return _spi;
  423.     }
  424.  
  425.  
  426.  
  427.     /**
  428.         @return SPISettings The SPI settings used by this device
  429.     **/
  430.     const SPISettings& spiSettings() const {
  431.         return _spi_settings;
  432.     }
  433. };
  434.  
Advertisement
Add Comment
Please, Sign In to add comment