FastArduino v1.10
C++ library to build fast but small Arduino/AVR projects
|
There are plenty of devices of all kinds, based on SPI interface, that you may want to connect to your Arduino or a board you created with an AVR ATmega or ATtiny MCU.
If you want to learn more about SPI concepts and vocabulary, you can find further information on Wikipedia.
Unfortunately, FastArduino obviously cannot provide specific support for all existing SPI devices.
However, based on a given device datasheet, it can be quite easy to add a FastArduino driver for any SPI device.
FastArduino provides all the necessary classes and methods for you to implement such a specific driver.
The following sections describe the FastArduino API for SPI device driver implementation, and list the steps to successfully implement such a driver.
The generic support for SPI device driver in FastArduino is quite simple, it is entirely embedded in 2 classes:
The important class here is spi::SPIDevice
, this is a template class (with many parameters discussed later) which all actual SPI device drivers shall derive from.
The abstract base class AbstractSPIDevice
contains most methods used in transferring (both ways) content to/from a device on the SPI bus. It exists solely for the sake of code size: it factors all methods that do not depend on any spi::SPIDevice
template parameter, into a non-template class so that generated code is not repeated for each device driver. All its important methods are available directly from spi::SPIDevice
and its children, as protected
methods.
As you can see in the following diagrams, the drivers for SPI devices currently supported by FastArduino directly derive from spi::SPIDevice
, sometimes enforcing template parameter values:
Hence, creating a new driver for an SPI device is as simple as:
spi::SPIDevice
subclass; let's call it MySPIDevice
in the rest of this page.public
API on this MySPIDevice
class, based on actual device features we want to useprotected
API methods inherited from spi::SPIDevice
The spi::SPIDevice
template class is instantiated through the following template parameters:
board::DigitalPin
is the most important parameter of the template; it defines on which digital pin of the MCU the targeted device "chip select" pin shall be connected; in MySPIDevice
, this shall remain a template parameter because you never know in advance how the device will be connected to the MCU across different projects.MySPIDevice
class definition.Subclassing spi::SPIDevice
gives MySPIDevice
access to all low-level protected
methods:
spi::SPIDevice.start_transfer()
: any SPI transfer to the slave SPI device must start with this call. Such a transfer must end by calling spi::SPIDevice.end_transfer()
.spi::SPIDevice.transfer(uint8_t)
: send one byte to the slave SPI device and return the byte that was received during transmission (SPI is a full-duplex protocol where master and slave can send data at the same time).spi::SPIDevice.transfer(uint8_t*, uint16_t)
: send a packet of bytes to the slave SPI device, and receive all bytes simultaneously transmitted by that device.spi::SPIDevice.transfer(const uint8_t*, uint16_t)
: send a packet of bytes to the slave SPI device, but trash any bytes simultaneously transmitted by that device.spi::SPIDevice.end_transfer()
: finish the current transfer to the slave SPI device, that was initiated with spi::SPIDevice.start_transfer()
.Any feature implementation in MySPIDevice
will always consist in a sequence of calls to the methods above, like:
Most SPI devices use codes to perform various features, either write-only (sending values to the device) or read-only (asking the device for some values).
In the sections below, we will sometimes refer to a simple SPI device, the MCP3008, an 8-channel Analog-Digital Converter, which communication protocol is super simple (because the number of features for such a chip is quite limited).
In general, before developing a full-fledged driver for an SPI device, you need to learn how to use that device.
Based on the device datasheet, you first learn how to manipulate the device through the SPI bus.
For better understanding, you generally use a debugging example that helps demonstrate how the device works.
One easy way to develop such a debugging sample is to create a program with just one source code file containing:
#include
directivesPublicDevice
class that derives from spi::SPIDevice
but declares main()
as a friend
, which allows direct calls, from main()
, to protected
API of spi::SPIDevice
, for easy testingPublicDevice
instance, from main()
and trace results to a console, through UARTFastArduino includes such a debugging sample in examples/spi/SPIDeviceProto
example, copied hereafter:
This example demonstrates how to simply test the MCP3008 ADC chip. It is made of several parts:
Those lines include a few headers necessary (or just useful) to debug an SPI device.
Then a buffer is defined for tracing through UART and the necessary UART ISR is registered.
Any specificity of the tested SPI device is defined as a constant in the next code section. These constants will be used for template parameters later on.
Here we set the UNO pin to be connected to the CS pin of the tested SPI device, then we define the proper SPI clock rate for this device, based on CPU frequency and device maximum SPI frequency.
This is where we define a utility class to debug our SPI interface to the tested device. PublicDevice
class does nothing but making all protected methods callable from main()
, so that we can directly perform our code tests in main()
, without thinking much about proper API design now.
This is the main()
function where it all happens. First we initialize the MCU and the UART for tracing.
Here we simply initialize SPI function on the UNO.
We then declare the device
variable that we will use for testing our SPI device.
Then we start an infinite loop that will read data from the SPI device and trace it:
In this code snippet, result1
and result2
each contain a part of the expected result (analog channel read on MCP3008), these must be used to calculate the actual value (based on the datasheet):
At this level, you have already been able to debug how the device works and you have a good overview of what features you want to provide to developers (and to yourself as the first of all) who will want to use this device.
An easy way is to provide an API that maps every feature found in the datasheet to its dedicated method. This is what we would call a low-level API; that is the minimum your driver should provide.
Additionally MySPIDevice
might implement a higher level API, based on the low-level one, but this is not mandatory; actually, this is not even advised generally, as this high-level API might be implemented in a distinct class. Using a separate class for high-level API allows other developers to develop their own high-level API without having to use yours if it does not fit their needs.
It is often advised to add begin()
and end()
methods to MySPIDevice
when it makes sense. begin()
would initialize the device before usage (most devices will require special setup before use).
In the MCP3008 example, the API could be rather simple; for instance, we could:
enum
for the selection of channel to read (including single-ended Vs. differential input modes)MCP3008Device
as device driver keeping only CS
as template parameteruint16_t read_channel()
We proceed with the MCP3008 example. When implementing the API, you must scrupulously follow the device datasheet for every method!
Here is a simple implementation attempt for MCP3008 driver:
Note the implementation of read_channel()
which is mainly the same as in the debugging example described earlier.
Of course, the MCP3008 is a very simple device which is easy to interact with through SPI, but there are many SPI devices with more complex capabilities (cameras, B&W and color display controllers, RF transmitters...) For those devices, the number of features can be large and this would result in dozens or even hundreds of API methods!
ATtiny MCU provides some support (through its USI feature) for SPI but it is quite limited in comparison to ATmega devices; hence FastArduino SPI support for ATtiny chips has similar limitations:
spi::DataOrder
supported is spi::DataOrder::MSB_FIRST
spi::Mode
s supported are spi::Mode::MODE_0
and spi::Mode::MODE_1
spi::ClockRate
parameter is not used in spi::SPIDevice
implementation, hence the maximum clock rate is always used, and is roughly equal to CPU frequency / 7
, hence typically a bit more than 1MHz with common clock frequency used in ATtiny boards (internal 8MHz RC clock); this might be a problem for devices supporting only 1MHz SPI.These limitations might prevent proper support, on ATtiny MCU, of some SPI devices.
If your device is in this situation, then you should add compile error checks (through static_assert()
, or #if
and #error
) in your SPI device driver header file, so that it cannot compile for these unsupported ATtiny targets.
Bravo! You successfully added FastArduino support, in your own project, for a specific SPI device!
The last mile would now consist in adding your valuable work to FastArduino library! You do not have to, of course, but this would be a good way to:
However, like for a marathon, the last mile can be difficult! In order to run this last mile, you will have to:
public
methods, and advised for protected
ones.examples/spi
directory; examples must be kept simple but still demonstrate the API usage; example circuits (connection pins) shall be described. These examples can be further used as "tests" before new releases of FastArduino.Important condition: in order to accept merging a PR to FastArduino, I must be able to check it by myself, hence I need to first have the new supported device available on my workbench; I will gladly buy one (or a few) if it is affordable and easy to find.