SPI Communication

Basic Operation

In Serial Peripheral Interface (SPI) Communication there is a master device and one or more slave devices. The microcontroller can be configured as either a master or a slave device.

The serial clock (SCK) signal is generated by the master. The clock line is also referred to as the SCLK or CLK line. The master receives data from the slave on the Master-Input-Slave-Output (MISO) line. The master sends data to the slave on the Master-Output-Slave-Input (MOSI) line. At any one time there is communication between the master and one slave. The master communicates with a slave by pulling the slave-select (SS) line of the slave low. The slave-select (SS) pin is sometimes also referred to as the chip-select (CS) pin. At the end of the communication the master pulls the SS line high.

The following SPI specifications can be used:

SPI specifications.

Only the master device specifies "SELECT" and "DESELECT". "READ" is used by input channels, "WRITE" by output channels, and data can be used by both input and output channels.

The specifications for the SPI devices that the microcontroller needs to communicate with can be given in the "SPI" option as "SPI"->{(Subscript[name, 1])->{(Subscript[spec, 11]),(Subscript[spec, 12]),…},(Subscript[name, 2])->{(Subscript[spec, 21]),(Subscript[spec, 22]),…},…}. The (Subscript[name, i]) of the SPI device can take the following values:

Possible names of SPI devices connected to the microcontroller.

The microcontroller's SPI channels:

Input and output SPI channels.

In an input channel, the suboption "Conversion" specifies how the data coming in needs to be converted and assigned to the input.

In an output channel, the suboption "Conversion" specifies how the output needs to be converted and sent out.

Example

The example is going to use an Arduino in master mode that both sends and receives data over the SPI bus. We will have an Arduino in slave mode that also sends and receives data. Thus the example covers both receiving and transmitting in both master and slave modes.

A potentiometer and a LED is connected to both the master and the slave. The master transmits its potentiometer value to the slave. The slave set the brightness of its LED based on this value. The master receives the potentiometer value of the slave and set the brightness of its LED using that value.

Load the package.

Needs["MicrocontrollerKit`"]

The system to embed is the same for both the master and the slave.

sys=StateSpaceModel[{{}, {}, {}, {{1, 0}, {0, 1}}}, SamplingPeriod ->1, 
SystemsModelLabels->{{"Pot.", "SPI"},{"SPI","LED"},{}}]

Pin 9 or any other pin on the master can be used to select the slave. There must be no change in the other connections.

When the slave select (SS) line goes low, how does the slave know if the master is sending or requesting data to be sent? To disambiguate this, the first byte after the SS goes low is used to indicate if the master is going to read from the slave or write to the slave.

The exchange sequence to read from and write to a master device:

readFromSlave="0x01";
writeToSlave="0x02";

The potentiometer reads a value between 0 and 5V. We are going to convert that to a value between 0 and 255 and send it over the SPI bus.

The conversion of the value that is read by the potentiometer before it is output.

obConversion[byte_]:={CDeclare["double","outValue"],CAssign[byte,COperator[Times,{"outValue",COperator[Divide,{255,5}]}]]}

ToCCodeString[obConversion["byte"]]

When the microcontroller receives the data byte which is in the range from 0 to 255, we are going to convert it to a value between 0 and 5 before setting it as the LED's brightness.

The conversion of the incoming SPI byte value before it is sent the LED.

ibConversion[byte_]:={CDeclare["uint8_t",CAssign["inValue",byte]],COperator[Times,{5.0,COperator[Divide,{"inValue",255.0}]}]}

ToCCodeString[ibConversion["byte"]]

There is no real need to convert from the 5 value range to the 255 range and then back again. It is done here to demonstrate how to specify the conversion, so that it can be adapted as needed.

Next let's specify the exchange sequence that the slave will see on the SPI bus.  If it sees a writeToSlave byte it needs to read the next byte, and if it sees a readFromSlave byte, in loads the byte to the SPI bus for the master to read.

The exchange sequence to read from and write to a master device:

(Subscript[exSequence, s])="Master"->{{writeToSlave,"READ"},{readFromSlave,"WRITE"}};

The master talks to one slave whose slave select pin is connected to pin 9 on the master. To read or write, the master first selects the slave, sets up to either read or write data, does the operation, and deselects the slave.

The exchange sequence for the master device:

(Subscript[exSequence, m])="9"->{{"SELECT",readFromSlave,"READ", "DESELECT"},{"SELECT",writeToSlave,"WRITE", "DESELECT"}};

Now we can specify up the input and output channels of the slave and embed its code.

The input channels of the slave:

(Subscript[ics, s])={{"A3", "Analog"},{"11", "SPI","Conversion"->ibConversion}};

The output channels of the slave:

(Subscript[ocs, s])={{"12","SPI", "Conversion"->obConversion},{"3","PWM"}};

Embed to the slave.

MicrocontrollerEmbedCode[sys,
<|"Target"->"ArduinoUno","Inputs"->(Subscript[ics, s]),"Outputs"->(Subscript[ocs, s])|>,"/dev/cu.usbmodem141201",<|"SPI"->(Subscript[exSequence, s])|>]

Next we move on to the master.

The input channels of the master:

(Subscript[ics, m])={{"A3", "Analog"},{"12", "SPI","SlaveSelect"->"9","Conversion"->ibConversion}};

The output channels of the master:

(Subscript[ocs, m])={{"11","SPI", "SlaveSelect"->"9","Conversion"->obConversion},{"3","PWM"}};

Embed the master code on a second Arduino to start communication with the slave:

MicrocontrollerEmbedCode[sys,
<|"Target"->"ArduinoUno","Inputs"->(Subscript[ics, m]),"Outputs"->(Subscript[ocs, m])|>,"/dev/cu.usbmodem141101",<|"SPI"->(Subscript[exSequence, m])|>]