The SPI (Serial Peripheral Interface) bus is a synchronous communication bus typically used to transfer data between a microcontroller and an external device (eg sensor, actuator, memory, SD card, ...). Being synchronous, unlike the typical asynchronous serial communication (UART), it uses a clock signal to ensure the perfect synchronism in the transmission and reception between the two counterparts known as master and slave.

Bus description

Overall, the SPI bus is characterized by the following signals :

  • SCLK (Serial CLocK) : clock for synchronization in the data exchange;
  • SS (Slave Select) : signal for enabling the slave (receiver);
  • MOSI (MasterOut / SlaveIn) : data line used for the transmission from master to slave;
  • MISO (MasterIn / SlaveOut) : data line used for the transmission from slave to master;

Excluding the SS signal, which can be handled separately, the bus should be considered a 3-wires bus.

SPI_single_slave

The master is responsible for generating the clock signal used by the slave. The latter uses this signal to identify the instants of time in which to sample the data present on the MOSI line (data to be received) or in which to set a logic level (0/1) on the line MISO (data to be transmitted); sampling can be configured on the rising edge or falling edge of the clock (clock phase, CPHA) as well as the "active" state of the clock itself can be set high or low (clock polarity, CPOL).

SPI_timing_diagram2

In practice, for each clock pulse, generated by the master, the slave does two things :

  • samples the signal on the MOSI line for acquire data in reception;
  • sets a logic level (0/1) on the MISO line to send data;

All this makes the SPI bus full duplex so that the transmission and reception can take place simultaneously; typically however many devices (slaves) work in half duplex mode.

This architecture allows you to implement the SPI devices with a simple shift register internally. At each clock pulse in the reception mode, the bit read on the MOSI line is transferred to the register and then shifted; vice versa, in the case of transmission, with clock pulse, a shift register operation is made and the bit is set on the MISO line.

SPI_8-bit_circular_transfer

The SS signal is used by the master to enable the slave with which to start a communication session. In fact, the SPI bus is thought to have only one master and one or more slaves, each of which can be activated with a dedicated signal. Typically, the SS signal is high when the slave is disconnected from the bus, it is set to the low value (active low) from the master, when it wants to communicate with a specific slave. At the end of the communication, the signal is returned to the high value.

SPI_three_slaves

It is necessary a SS signal for each slave present on the bus, and this entails the need for an increasing number of pins on the master of connected devices increases. In many cases this is not practical and you are using the cascade connection "daisy chain", using a single SS for all slaves that are connected to each other through the data lines (MISO line of a slave goes into MOSI line of the next slave ).

SPI_three_slaves_daisy_chained

In this mode, the data transmitted from the master is propagated in cascade to all slaves in clock ticks later, this means that if we have N slaves then N sequences of 8 clock pulses are needed  to transfer an entire byte to all slaves.

.Net Micro Framework : the supported classes for the bus

The .Net Micro Framework, according to the logic of abstraction that characterizes it, provides the SPI class (Microsoft.SPOT.Hardware namespace, assembly Microsoft.SPOT.Hardware.dll) in order to use this type of bus with any device that supports it. To start using this feature, you must configure the SPI port to be used by the nested class SPI.Configuration, this configuration allows you to set:

  • ChipSelect_Port : the pin (Cpu.Pin enum) which will be used as SS (Slave Select). It is possible to set the value GPIO_NONE if you prefer to drive this pin directly without leaving the burden to the SPI class;
  • ChipSelect_ActiveState : the active state of the chip select. Typically, the devices have an SPI chip select "active low", it is necessary to set the logic level 0 (false) to turn on and communicate with the device;
  • ChipSelect_SetupTime : it is the time that must elapse from the time when the chip select is activated and the clock signal is transmitted on its line. This parameter is closely related to the device with which you are communicating (see datasheet), because it is the time it takes for the device to "see" that has been activated and that the master wants to talk to him;
  • ChipSelect_HoldTime : it is the time that must elapse between the end of the read/write transaction and the instant when the chip select is inactive. In practice, it serves to complete the operation for the slave then be switched off (in this case also depends on the device and is to be found in the datasheet);
  • Clock_IdleState : it indicates the clock idle condition on the line when the slave has not been activated; typically it is known as the the clock polarity;
  • Clock_Edge : it indicates the rising edge or falling edge at which the data on the communication line (MOSI or MISO) is sampled, it is typically known as the clock phase;
  • Clock_Rate : it is the clock frequency;
  • SPI_mod : it represents the SPI.SPI_module enum that indicates the physical SPI port of the CPU to use;

All the above configuration parameters are always closely tied to the device with which you want to communicate and to be found in the datasheet. Regarding the SPI_mod parameter, you can find it in the documentation of the master (typically the CPU of our board) to identify how the OEM has exposed the SPI ports available through the HAL of the .Net Micro Framework.

An SPI.Configuration instance is needed as a parameter to the constructor of the SPI class to be able to immediately begin using the bus.

   1: SPI.SPI_module spiModule = SPI.SPI_module.SPI1;
   2:  
   3: SPI.Configuration spiCfg = new SPI.Configuration(Cpu.Pin.GPIO_NONE,     // chip select pin
   4:                                                  SPI_CS_ACTIVE_STATE,   // chip select active state
   5:                                                  SPI_CS_SETUP_TIME,     // chip select setup time
   6:                                                  SPI_CS_HOLD_TIME,      // chip select hold time
   7:                                                  SPI_CLK_IDLE_STATE,    // clock idle state
   8:                                                  SPI_CLK_EDGE,          // clock edge
   9:                                                  SPI_CLK_RATE,          // clock rate (Khz)
  10:                                                  spiModule);            // spi module used
  11:  
  12: SPI spi = new SPI(spiCfg);
  13:  
  14: OutputPort nssPort = new OutputPort(Cpu.Pin.GPIO_Pin0, true);

In the example above, it is preferred to manage the signal SS in an autonomous way by the use of a OutputPort to move a corresponding pin.

Once there is an instance of the SPI class, the main methods used are only two :

  • Write() : it allows you to perform a data transfer from the master to the slave. Provides two overloads to allow the operation to blocks of 8 or 16 bits (a byte array or ushort);
  • WriteRead() : it allows you to perform a data transfer from master to slave and vice versa. This operation occurs simultaneously being the SPI full duplex; even in this case it is possible to transfer blocks of 8 or 16 bits;

The Write() method is conceptually simple, because the class takes care of moving the clock signal on the line transferring the data in the array that receives as a parameter. For the WriteRead() method, it is needed a clarification: the array (initially empty) where will put the data received from the slave must have the same size of the array that contains the data to be transmitted. This equality is necessary for the intrinsic feature of the SPI bus on which at every clock pulse is transmitted one bit of the send buffer is scanned and a bit for the receive buffer.

   1: byte write = new byte[CMD_SIZE];
   2:  
   3: // prepare write buffer ...
   4:  
   5: // send frame
   6: nssPort.Write(false);
   7: spi.Write(write);
   8: nssPort.Write(true);

In the above example, the SS signal is set to false before executing the Write() method via the SPI class to enable the slave to receive the data; it is reset to false at the end of the transmission.

We can observe that there is no Read() method ! How so? If we want only to read from the slave without having to send anything. Typically SPI devices always provide a command from having to transmit and then begin to receive the data , then in most cases we find ourselves having to use the WriteRead() method. It is true that the slave must first receive a command to be able to analyze , perform the operation , and respond with a data , so it is impossible that at the cloco pulses to transmit command , the master begins to receive also the answer. In many cases , the Write() method is used to transmit the command and it is followed by a WriteRead() to read the response. In the latter case , what should we write on the bus if we are only interested in receiving ? Well the answer is simple ... we send "dummy" bytes ! In practice , we use the WriteRead() method to read a data from the slave due to the fact that the SPI class automatically generates the clock pulses for receiving; not having to transmit anything, we set the MOSI line in an idle state (high or low, using the byte 0xFF or 0X00 ) or a "dummy" byte , provided by the datasheet of this device does not give "noise" to the slave.

   1: // dummy bytes from master to force clock a reading from slave
   2: byte write = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
   3: byte read = new byte[5];
   4:  
   5: nssPort.Write(false);
   6: // write dummy bytes to read data
   7: spi.WriteRead(write, read);
   8: nssPort.Write(true);

Deep into the HAL

According to the .Net Micro Framework architecture, each OEM must implement a layer (HAL and PAL) to serve as a "bridge" between the (CLR managed code to high-level) and the particular underlying hardware.

We take as reference the Netduino board (generation 1), which has as a Atmel AT91 microcontroller .By downloading the source code of the firmware (they are open source) from the official web site, we can find the implementation code in managed C # class (SPI Framework \ Core \ Native_Hardware \ SPI.cs) in which the GetSpiPins() method is invoked on the HardwareProvider class current instance, by providing the identifier of the SPI module, this method returns the pins related to SCK, MISO and MOSI. If we have specified a pin for chip select in the constructor, it also creates an instance OutputPort for this (in practice is the operation that we would do if we wanted to drive the SS in autonomy and we passed GPIO_NONE to the constructor of the SPI.

   1: public SPI(Configuration config)
   2: {
   3:     HardwareProvider hwProvider = HardwareProvider.HwProvider;
   4:  
   5:     if (hwProvider != null)
   6:     {
   7:         Cpu.Pin msk;
   8:         Cpu.Pin miso;
   9:         Cpu.Pin mosi;
  10:  
  11:         hwProvider.GetSpiPins(config.SPI_mod, out msk, out miso, out mosi);
  12:  
  13:         if (msk != Cpu.Pin.GPIO_NONE)
  14:         {
  15:             Port.ReservePin(msk, true);
  16:         }
  17:  
  18:         if (miso != Cpu.Pin.GPIO_NONE)
  19:         {
  20:             Port.ReservePin(miso, true);
  21:         }
  22:  
  23:         if (mosi != Cpu.Pin.GPIO_NONE)
  24:         {
  25:             Port.ReservePin(mosi, true);
  26:         }
  27:     }
  28:  
  29:     if (config.ChipSelect_Port != Cpu.Pin.GPIO_NONE)
  30:     {
  31:         m_cs = new OutputPort(config.ChipSelect_Port, !config.ChipSelect_ActiveState);
  32:     }
  33:  
  34:     m_config = config;
  35:     m_disposed = false;
  36: }

After a series of invocations to cascade through the CLR to the implementation of the HAL, the GetPins() method is invoked on the AT91_SPI_Driver class(DeviceCode \ Targets \ Native \ AT91 \ DeviceCode \ AT91_SPI \ AT91__SPI.cpp) that returns the identifiers of pins of the processor associated with the SPI port requested. It is observed that the Netduino allows you to use only the port labeled 0, associated with the digital pins 11, 12 e 13 respectively for MOSI, MISO and SCLK.

   1: void AT91_SPI_Driver::GetPins(UINT32 spi_mod, GPIO_PIN &msk, GPIO_PIN &miso, GPIO_PIN &mosi)
   2: {
   3:     NATIVE_PROFILE_HAL_PROCESSOR_SPI();
   4:  
   5:     switch(spi_mod)
   6:     {
   7:     case 0:
   8:         msk = AT91_SPI0_SCLK;
   9:         miso = AT91_SPI0_MISO;
  10:         mosi = AT91_SPI0_MOSI;
  11:  
  12:         break;
  13: #if (AT91C_MAX_SPI == 2)
  14:     case 1:
  15:         msk = AT91_SPI1_SCLK;
  16:         miso = AT91_SPI1_MISO;
  17:         mosi = AT91_SPI1_MOSI;
  18:  
  19:         break;
  20: #endif        
  21:     default:
  22:         break;
  23:  
  24:     }
  25: }

In the case of Netduino board (generation 2) that has a STM32 processor , the function of reading the pin SPI is CPU_SPI_GetPins() (DeviceCode \ Targets \ Native \ STM32 \ DeviceCode \ STM32_SPI \ STM32_SPI_functions.cpp) that is invoked whenever it is starts and stops transmission to the slave.

   1: void CPU_SPI_GetPins( UINT32 spi_mod, GPIO_PIN& msk, GPIO_PIN& miso, GPIO_PIN& mosi )
   2: {
   3:     NATIVE_PROFILE_HAL_PROCESSOR_SPI();
   4:     if (spi_mod == 0) {
   5: #if defined(PLATFORM_ARM_Netduino2) || defined(PLATFORM_ARM_NetduinoPlus2) || defined(PLATFORM_ARM_NetduinoShieldBase)
   6:         msk  = SPI2_SCLK_Pin;
   7:         miso = SPI2_MISO_Pin;
   8:         mosi = SPI2_MOSI_Pin;
   9: #else
  10:         msk  = SPI1_SCLK_Pin;
  11:         miso = SPI1_MISO_Pin;
  12:         mosi = SPI1_MOSI_Pin;
  13: #endif
  14:     } else if (spi_mod == 1) {
  15: #if defined(PLATFORM_ARM_Netduino2) || defined(PLATFORM_ARM_NetduinoPlus2) || defined(PLATFORM_ARM_NetduinoShieldBase)
  16:         msk  = SPI1_SCLK_Pin;
  17:         miso = SPI1_MISO_Pin;
  18:         mosi = SPI1_MOSI_Pin;
  19: #else
  20:         msk  = SPI2_SCLK_Pin;
  21:         miso = SPI2_MISO_Pin;
  22:         mosi = SPI2_MOSI_Pin;
  23: #endif
  24:     } else {
  25:         msk  = SPI3_SCLK_Pin;
  26:         miso = SPI3_MISO_Pin;
  27:         mosi = SPI3_MOSI_Pin;
  28:     }
  29: }

An important aspect to note is that the SPI Netduino shift and transmits the output data in MSB first mode, which starts the transmission from the most significant bit (Most Significant Bit). In the case that the device expects to receive the bits in the reverse order (LSB Least Significant Bit), you must reverse the order of the bits before starting transmission.

Conclusion

The SPI bus has the advantage of being full duplex but especially to work at very high speeds. In addition, the hardware implementation of a device that supports it is relatively simple. The main disadvantages are the need for more pins (for SS), and the absence of a hardware flow control than that of a acknoweledge from the slave (which should be implemented at the software level and higher protocol).

The .NET Micro Framework enables you to use this bus very easily with a single class and two main methods. In this way, it is possible to develop a managed driver to be able to communicate with any SPI device.

Very soon I will write a post on a managed driver that I am developing for an NFC chip from NXP using the SPI bus (it supports also I2C and HSU), to see the potential of this wonderful framework!