SPIDriver
Classes in the file shared/drivers/spi/SPIDriver.h
provide both low and high level access to an SPI Bus, in order to perform read, write and full duplex transactions with slaves on the bus.
Formerly, these function were provided by the infamous BusTemplate.h
.
SPIDriver allow for sensors working with different bus configurations (baud rate, polarity, phase etc) to work togheter on the same bus, by appropriately reconfiguring the bus before each transaction.
General architecture
SPIBusInterface
Low level access to the SPI bus is provided trough the interface SPIBusInterfaces, a virtual class the provides all the methods needed to perform operations on the bus:
-
configure(...)
Configures the bus with the provided parameters -
select(...)
/deselect(...)
asserts and clears the Chip Select (CS) pin -
write(...)
writes bytes on the bus -
read(...)
reads bytes from the bus -
transfer(...)
performs a full duplex transaction, writing the provided bytes and returning the received bytes
Interface implementations (SPIBus)
This interface is implemented primarly by the SPIBus
class, which performs the real operations on a SPI bus, using the SPI peripheral of the STM32F4 micro.
The interface may be implemented by other classes, for example MockSpiBus
, that implements a "fake" SPI bus used for unit testing.
The various drivers must access the an SPI bus exclusively through the interface. By doing this, the implementation of the driver is completely transparent to the use of a real SPI bus (SPIBus
), a mock one (MockSPIBus
) or other implementations: all can be used interchangeably without needing to change a single line of code in the driver.
SPITransaction
While SPIBusInterface
provides low level access to the bus (the user must manually set the CS pin and manually configure the bus at each transaction), the class SPITransaction
does all of this automatically, thus providing an high level interface that hides some of the implementation details of the SPI protocol. Because of this, SPITransaction
is much simpler (and less verbose) to use and is in fact the suggested way of operating on the bus.
SPIBusConfig struct
Contains all the configuration parameters of the bus. Multiple slaves on the same bus may use different bus parameters.
SPISlave struct
Aggregates all the objects needed by a driver to access a bus.
Examples
High level acccess through SPITransaction
In this example, SPIDriver is used to write and read 1 byte from two sensors. The bus is configured differently for each sensor.
// Creates an SPIBus object using the SPI5 peripheral
// The SPIBus object is shared among all slaves on the bus
SPIBus bus(SPI5);
GpioPin cs_s1(GPIOC_BASE, 1); // Chip select pin for slave 1 (GPIO C1)
GpioPin cs_s2(GPIOC_BASE, 1); // Chip select pin for slave 2 (GPIO C2)
SPIBusConfig cfg_s1; // Bus configuration for slave 1
SPIBusConfig cfg_s2; // Bus configuration for slave 1
int main()
{
cfg_s1.br = SPIBaudRate::DIV_64; // Divide the peripheral clock by 64 (slave 1)
// Slave 1 defaults to cpol = 0
cfg_s2.br = SPIBaudRate::DIV_128; // Divide the peripheral clock by 128 (slave 2)
cfg_s2.cpol = 1; // Set clock polarity to 1 (slave 2)
for(;;)
{
// Operations with slave 1
{
SPITransaction t1{bus, cs_s1, cfg_s1}; // Start a transaction for slave 1
// The bus is immediately configured here
t1.write(0x25, 69); // Writes 69 in the 0x25 register
uint8_t val = t1.read(0x26); // Reads the 0x26 register
printf("Sensor 1: %d\n", (int)val);
// End of scope: the SPITransaction is terminated here.
}
// Operations with slave 2
{
SPITransaction t2{bus, cs_s2, cfg_s2}; // Start a transaction for slave 2
// The bus is immediately reconfigured here using the parameters for the second slave
t2.write(0x45, 123); // Writes in the 0x45 register
uint8_t val = t2.read(0x70); // Reads the 0x70 register
printf("Sensor 2: %d\n", (int)val);
// End of scope: the SPITransaction is terminated here.
}
}
return 0;
}
Low level acccess through SPIBusInterface
In this example, exactly the same operations are performed, but directly using SPIBusInterface
instead of SPITransaction
// Creates an SPIBus object using the SPI5 peripheral
// The SPIBus object is shared among all slaves on the bus
SPIBus bus(SPI5);
GpioPin cs_s1(GPIOC_BASE, 1); // Chip select pin for slave 1 (GPIO C1)
GpioPin cs_s2(GPIOC_BASE, 1); // Chip select pin for slave 2 (GPIO C2)
SPIBusConfig cfg_s1; // Bus configuration for slave 1
SPIBusConfig cfg_s2; // Bus configuration for slave 1
int main()
{
cfg_s1.br = SPIBaudRate::DIV_64; // Divide the peripheral clock by 64 (slave 1)
// Slave 1 defaults to cpol = 0
cfg_s2.br = SPIBaudRate::DIV_128; // Divide the peripheral clock by 128 (slave 2)
cfg_s2.cpol = 1; // Set clock polarity to 1 (slave 2)
for(;;)
{
// Operations with slave 1
uint8_t reg = 0x25;
uint8_t data = 69;
bus.configure(cfg_s1); // Configure the bus for slave 1
// Write one byte
bus.select(cs_s1); // Assert CS
bus.write(®, 1); // Write the register address
bus.write(&data, 1); // Write the data to be put in the register
bus.deselect(cs_s1); // Clear CS
// Read one byte
reg = 0x26 | 0x80; // Specify the register we want to read, set the MSB to 1 to signal a read operation
uint8_t val;
bus.select(cs_s1);
bus.write(®, 1); // Write register address
bus.read(&val, 1); // Read 1 byte into val
bus.deselect(cs_s1);
printf("Sensor 1: %d\n", (int)val);
// Operation with slave 2
reg = 0x45;
data = 123;
bus.configure(cfg_s2); // Configure the bus for slave 2
// Write one byte
bus.select(cs_s2); // Assert CS
bus.write(®, 1); // Write the register address
bus.write(&data, 1); // Write the data to be put in the register
bus.deselect(cs_s2); // Clear CS
// Read one byte
reg = 0x70 | 0x80; // Specify the register we want to read, set the MSB to 1 to signal a read operation
uint8_t val;
bus.select(cs_s2);
bus.write(®, 1);
bus.read(&val, 1);
bus.deselect(cs_s2);
printf("Sensor 2: %d\n", (int)val);
}
return 0;
}
Notes
- The
SPIBus
object must be shared by all slaves on the same bus. Do not create multiple instances of the classSPIBus
for each sensor. - But each slave can have different configurations
- SPITransactions must be scoped (if not, in the first example, the bus may be configured for slave 2 before the operations with slave 1 are performed)
- Access through
SPIBusInterface
(second example) is discouraged due to its complexity and because it is error-prone (easy to forget to configure / select / deselect). But it may be necessary in some cases (sensors with non-standard SPI implementation) - Operations on the same bus must be synchronized between multiple threads. It is strongly recommended to perform operations from a single thread to avoid synchronization problems.
Further examples
See tests/drivers/test-l3gd20.cpp
and shared/Sensors/L3GD20.h
for a real life example of using SPIDriver to sample a gyroscope.