Communicate via SPI Advanced

Motivation

It may be desirable to communicate with a device under test using SPI, for example to share measured voltages.
This tutorial will show you how to configure the miniHIL as both and SPI master and slave
and how to implement a correct communication to transmit and receieve data.

Pre-requisites

For the miniHIL to communicate with itself, we need to connect the master and slave’s MOSI, MISO, CLK and NSS pins. For this
tutorial, these are the correct pins:

MOSI: PG14 → PJ10 MISO: PG12 → PJ11 CLK: PG13 → PK0 NSS: PG8 → PF6

Please connect them like in the image below:

SPI Wiring

To prepare the SW create a new project as described in the "Creating a new project" How-To.

Finally, to be able to see some output, make sure you can log as described in here. Note that in this case you only need the USB connection. Log messages will be generated by the SPI tester below.

Instantiate the SPI in software

To communicate using SPI, two actors are needed: One that represents the SPI and one that represents the testing code. Both of them need to be instantiated and connected to each other.
In this example we will create two instances of each, one for the master and one for the slave.

As a container we will use the Application contained in MiniHILApplication.room. So open the file in your newly created project and go to the Structure Section of the Application actor.
There, create an instance of the ASPI5Adapter and ASPI6Adapter as shown below.

RoomModel MiniHilProject {
	//...
	import minihil.platform.adapters.spi_custom.ASPI6Adapter 1

	ActorClass Application {
		Structure {
			//	  ...
			ActorRef spi5: minihil.platform.adapters.spi_custom.ASPI5Adapter
			ActorRef spi6: minihil.platform.adapters.spi_custom.ASPI6Adapter 2
		}
	}
1 Add the appropriate imports for the adapters
2 Instantiate both adapters

The ASPI5Adapter and ASPI6Adapter are abstractions for the miniHIL hardware, freeing you from the specifics of pin assignment and such.
If you are interested in details set the cursor on ASPI6Adapter and press F3.

Creating a test actor

We will now create the test actor in the MiniHilProject ROOM model. This is mostly the same for both master and slave, except for some slight parameter and timing adjustments that will be highlighted below.
First we begin with the master by creating a hull for it and connecting it to the adapter like below.

RoomModel MiniHilProject {
	//...
	import etrice.api.timer.PTimer
	import etrice.api.logger.PLogger

	import minihil.platform.adapters.spi_custom.ASPI6Adapter
	import minihil.platform.adapters.spi_custom.PSPICustomCtrl
	import minihil.platform.adapters.spi_custom.PSPICustomCommunication 1

	ActorClass Application {
		//	  ...
        ActorRef spi6_tester: ASPI6Tester 2
        ActorRef spi6: ASPI6Adapter
        Binding spi6_tester.ctrl and spi6.ctrl
        Binding spi6_tester.comm and spi6.fct 3
	}

	ActorClass ASPI6Tester {
		Interface {
			conjugated Port ctrl: PSPICustomCtrl
			conjugated Port comm: PSPICustomCommunication 4
		}
		Structure {
			usercode3 '''
				static uint8_t __attribute__((section(".dmaMemSection_D3"))) rxBuff[6];		// must be in dmaMemSection_D3
				static uint8_t __attribute__((section(".dmaMemSection_D3"))) txBuff[6] = {'H', 'E', 'L', 'L', 'O', 0x00};	// must be in dmaMemSection_D3 5
			'''
			external Port ctrl
			external Port comm
			SAP timer: PTimer
			SAP logger: PLogger 6
		}
		Behavior {
            // ...
			}
		}
	}
}
1 Port for controlling the SPI adapter
2 SPI tester instance
3 Connect the communication and control ports
4 Instantiate the communication and control ports
5 Create buffers for transmitting and receiving with data for transmitting pre-determined, bufsize >= frames to be sent
6 Timer and logger for periodic actions and terminal outputs

With this you should see the following inside the structure diagram of the Application actor (use ALT-S to open it):

SPIStructure
SPI Example Structure

Adding behavior to the tester

To actually begin a communication, we need to define the actor’s behavior as a state machine. Our state machine will begin by configuring the SPI adapter.
It is required to send a setFrameSize and setSpeed message at the beginning so that reception can work correctly. Afterwards we transmit and receive data
periodically every second, alternating between communicating and waiting. The data transfer itself is configured creating a DSPICustomCommunicationCommand struct and passing it to the
comm.startCommunication() command. This is also where we set the amount of data to be sent, whether we’re receiving, transmitting or both and whether we are the
master in the communication. How this looks can be seen in the code snippet below and results in the following state machine.

SPI Tester Behavior
UART Tester FSM
ActorClass ASPI6Tester {
    //...
    Behavior {

        StateMachine {
            State wait
            State configure
            State configure_size
            State running
            State sending_data
            Transition init0: initial -> wait {
                action '''
                    timer.startTimeout(100);

                '''
            }
            Transition tr0: wait -> configure {
                triggers {
                    <timeout: timer>
                }
                action '''
                    ctrl.setFrameSize(8);	// number of bits sent per frame
                '''
            }
            Transition tr1: configure -> configure_size {
                triggers {
                    <done: ctrl>
                }
                action '''ctrl.setSpeed(10000000);'''	// SPI clock frequency
            }
            Transition tr2: configure_size -> running {
                triggers {
                    <setSpeedCompleted: ctrl>
                }
                action '''timer.startTimer(1000);'''	// how often to transmit
            }
            Transition tr3: running -> sending_data {
                triggers {
                    <timeout: timer>
                }
                action '''
                    DSPICustomCommunicationCommand d; 1
                        d.communicationCommandId = 0xAB;
                        d.enableTX = true;
                        d.enableRX = true;
                        d.numDataFrames = 6;			// number of bytes to send and receive, same for master and slave
                        d.readBufferPtr = rxBuff;		// receiving buffer
                        d.writeBufferPtr = txBuff;		// transmitting buffer
                        d.isMaster = true;				// true if SPI master
                        d.masterInterDataIdleness = 8;	// minimum time delay inserted between two consecutive data frames in master mode
                        comm.startCommunication(&d);	// begin a single transmit/receive
                '''
            }
            Transition tr4: sending_data -> running {
                triggers {
                    <communicationCommandDone: comm>
                }
                action '''
                    logger.logF("SPI6 %x, %x, %x, %x, %x, %x", rxBuff[0], rxBuff[1], rxBuff[2], rxBuff[3], rxBuff[4], rxBuff[5]);
                    logger.logF("SPI6 message: %s", &rxBuff[0]);
                    logger.log("------------------------");
                '''
            }
        }
    }
}
1 Parameters for the SPI communication

Adding an SPI slave

With the currently implemented code, we would be able to communicate with a device under test using SPI. However, in order for the communication in this example to work,
we need to add the SPI slave. This involves only minor differences in the used adapter, DSPICustomCommunicationCommand parameters, transmitted data and the timing.
These differences are marked in the code below, which should be added to your MiniHilProject ROOM model.

RoomModel MiniHilProject {
	import etrice.api.timer.PTimer
	import etrice.api.logger.PLogger

	// SPI imports
	import minihil.platform.adapters.spi_custom.ASPI5Adapter 1
	import minihil.platform.adapters.spi_custom.ASPI6Adapter
	import minihil.platform.adapters.spi_custom.PSPICustomCtrl
	import minihil.platform.adapters.spi_custom.PSPICustomCommunication

    ActorClass Application {
		Structure {
			// SPI main and test actors
			ActorRef spi5: ASPI5Adapter
			ActorRef spi5_tester: ASPI5Tester
			Binding spi5_tester.ctrl and spi5.ctrl
			Binding spi5_tester.comm and spi5.fct 2
			ActorRef spi6_tester: ASPI6Tester
			ActorRef spi6: ASPI6Adapter
			Binding spi6_tester.ctrl and spi6.ctrl
			Binding spi6_tester.comm and spi6.fct
        }
    }

	ActorClass ASPI5Tester {
		Interface {
			conjugated Port ctrl: PSPICustomCtrl
			conjugated Port comm: PSPICustomCommunication
		}
		Structure {
			usercode3 '''
				static uint8_t __attribute__((section(".dmaMemSection_D3"))) rxBuff[6];		// must be in dmaMemSection_D3
				static uint8_t __attribute__((section(".dmaMemSection_D3"))) txBuff[6] = {'W', 'O', 'R', 'L', 'D', 0x00};	// must be in dmaMemSection_D3 3
			'''
			external Port ctrl
			external Port comm
			SAP timer: PTimer
			SAP logger: PLogger
		}
		Behavior {

			StateMachine {
				State wait
				State configure
				State configure_size
				State running
				State wait_For_cmd_accepted
				State sending_data
				Transition init0: initial -> wait {
					action '''
						timer.startTimeout(1000); 4

					'''
				}
				Transition tr0: wait -> configure {
					triggers {
						<timeout: timer>
					}
					action '''
						ctrl.setFrameSize(8);	// number of bits sent per frame
					'''
				}
				Transition tr1: configure -> configure_size {
					triggers {
						<done: ctrl>
					}
					action '''ctrl.setSpeed(10000000);'''	// SPI clock frequency
				}
				Transition tr2: configure_size -> running {
					triggers {
						<setSpeedCompleted: ctrl>
					}
					action '''timer.startTimer(1000);'''	// how often to transmit
				}
				Transition tr3: running -> wait_For_cmd_accepted {
					triggers {
						<timeout: timer>
					}
					action '''
						DSPICustomCommunicationCommand d;
							d.communicationCommandId = 0xAB;
							d.enableTX = true;
							d.enableRX = true;
							d.numDataFrames = 6;			// number of bytes to send and receive, same for master and slave
							d.readBufferPtr = rxBuff;		// receiving buffer
							d.writeBufferPtr = txBuff;		// transmitting buffer
							d.isMaster = false;				// true if SPI master   5
							d.masterInterDataIdleness = 8;	// minimum time delay inserted between two consecutive data frames in master mode
							comm.startCommunication(&d);	// begin a single transmit/receive
					'''
				}
				Transition tr4: wait_For_cmd_accepted -> sending_data {
					triggers {
						<commandAccepted: comm>
					}
				}
				Transition tr5: sending_data -> running {
					triggers {
						<communicationCommandDone: comm>
					}
					action '''
						logger.logF("SPI5 %x, %x, %x, %x, %x, %x", rxBuff[0], rxBuff[1], rxBuff[2], rxBuff[3], rxBuff[4], rxBuff[5]);
						logger.logF("SPI5 message: %s", &rxBuff[0]);
						logger.log("------------------------");
					'''
				}
			}
		}
	}
}
1 Import SPI5 adapter
2 Instantiate actors and connect ports for SPI5 adapter and tester
3 Chane data to be transferred
4 Change starting wait timing, so master is configured first
5 Set isMaster = false

Running the example

Once you have flashed and started the target you should see output like this in your terminal.

SPI example output
SPI6 57, 4f, 52, 4c, 44, 0
SPI6 message: WORLD
------------------------
SPI5 48, 45, 4c, 4c, 4f, 0
SPI5 message: HELLO
------------------------
SPI6 57, 4f, 52, 4c, 44, 0
SPI6 message: WORLD
------------------------
SPI5 48, 45, 4c, 4c, 4f, 0
SPI5 message: HELLO
------------------------

Summary

  • Instantiate an SPI adapter from the miniHIL library

  • Create your own SPI application

  • Connect it to the SPI adapter

See Also

Complete example file

RoomModel MiniHilProject {
	import etrice.api.timer.PTimer
	import etrice.api.logger.PLogger

	// SPI imports
	import minihil.platform.adapters.spi_custom.ASPI5Adapter
	import minihil.platform.adapters.spi_custom.ASPI6Adapter
	import minihil.platform.adapters.spi_custom.PSPICustomCtrl
	import minihil.platform.adapters.spi_custom.PSPICustomCommunication

    ActorClass Application {
		Structure {
			// SPI main and test actors
			ActorRef spi5: ASPI5Adapter
			ActorRef spi5_tester: ASPI5Tester
			Binding spi5_tester.ctrl and spi5.ctrl
			Binding spi5_tester.comm and spi5.fct
			ActorRef spi6_tester: ASPI6Tester
			ActorRef spi6: ASPI6Adapter
			Binding spi6_tester.ctrl and spi6.ctrl
			Binding spi6_tester.comm and spi6.fct
        }
    }

	ActorClass ASPI5Tester {
		Interface {
			conjugated Port ctrl: PSPICustomCtrl
			conjugated Port comm: PSPICustomCommunication
		}
		Structure {
			usercode3 '''
				static uint8_t __attribute__((section(".dmaMemSection_D3"))) rxBuff[6];		// must be in dmaMemSection_D3
				static uint8_t __attribute__((section(".dmaMemSection_D3"))) txBuff[6] = {'W', 'O', 'R', 'L', 'D', 0x00};	// must be in dmaMemSection_D3
			'''
			external Port ctrl
			external Port comm
			SAP timer: PTimer
			SAP logger: PLogger
		}
		Behavior {

			StateMachine {
				State wait
				State configure
				State configure_size
				State running
				State wait_For_cmd_accepted
				State sending_data
				Transition init0: initial -> wait {
					action '''
						timer.startTimeout(1000);

					'''
				}
				Transition tr0: wait -> configure {
					triggers {
						<timeout: timer>
					}
					action '''
						ctrl.setFrameSize(8);	// number of bits sent per frame
					'''
				}
				Transition tr1: configure -> configure_size {
					triggers {
						<done: ctrl>
					}
					action '''ctrl.setSpeed(10000000);'''	// SPI clock frequency
				}
				Transition tr2: configure_size -> running {
					triggers {
						<setSpeedCompleted: ctrl>
					}
					action '''timer.startTimer(1000);'''	// how often to transmit
				}
				Transition tr3: running -> wait_For_cmd_accepted {
					triggers {
						<timeout: timer>
					}
					action '''
						DSPICustomCommunicationCommand d;
							d.communicationCommandId = 0xAB;
							d.enableTX = true;
							d.enableRX = true;
							d.numDataFrames = 6;			// number of bytes to send and receive, same for master and slave
							d.readBufferPtr = rxBuff;		// receiving buffer
							d.writeBufferPtr = txBuff;		// transmitting buffer
							d.isMaster = false;				// true if SPI master
							d.masterInterDataIdleness = 8;	// minimum time delay inserted between two consecutive data frames in master mode
							comm.startCommunication(&d);	// begin a single transmit/receive
					'''
				}
				Transition tr4: wait_For_cmd_accepted -> sending_data {
					triggers {
						<commandAccepted: comm>
					}
				}
				Transition tr5: sending_data -> running {
					triggers {
						<communicationCommandDone: comm>
					}
					action '''
						logger.logF("SPI5 %x, %x, %x, %x, %x, %x", rxBuff[0], rxBuff[1], rxBuff[2], rxBuff[3], rxBuff[4], rxBuff[5]);
						logger.logF("SPI5 message: %s", &rxBuff[0]);
					'''
				}
			}
		}
	}

	ActorClass ASPI6Tester {
		Interface {
			conjugated Port ctrl: PSPICustomCtrl
			conjugated Port comm: PSPICustomCommunication
		}
		Structure {
			usercode3 '''
				static uint8_t __attribute__((section(".dmaMemSection_D3"))) rxBuff[6];		// must be in dmaMemSection_D3
				static uint8_t __attribute__((section(".dmaMemSection_D3"))) txBuff[6] = {'H', 'E', 'L', 'L', 'O', 0x00};	// must be in dmaMemSection_D3
			'''
			external Port ctrl
			external Port comm
			SAP timer: PTimer
			SAP logger: PLogger
		}
		Behavior {

			StateMachine {
				State wait
				State configure
				State configure_size
				State running
				State sending_data
				Transition init0: initial -> wait {
					action '''
						timer.startTimeout(100);

					'''
				}
				Transition tr0: wait -> configure {
					triggers {
						<timeout: timer>
					}
					action '''
						ctrl.setFrameSize(8);	// number of bits sent per frame
					'''
				}
				Transition tr1: configure -> configure_size {
					triggers {
						<done: ctrl>
					}
					action '''ctrl.setSpeed(10000000);'''	// SPI clock frequency
				}
				Transition tr2: configure_size -> running {
					triggers {
						<setSpeedCompleted: ctrl>
					}
					action '''timer.startTimer(1000);'''	// how often to transmit
				}
				Transition tr3: running -> sending_data {
					triggers {
						<timeout: timer>
					}
					action '''
						DSPICustomCommunicationCommand d;
							d.communicationCommandId = 0xAB;
							d.enableTX = true;
							d.enableRX = true;
							d.numDataFrames = 6;			// number of bytes to send and receive, same for master and slave
							d.readBufferPtr = rxBuff;		// receiving buffer
							d.writeBufferPtr = txBuff;		// transmitting buffer
							d.isMaster = true;				// true if SPI master
							d.masterInterDataIdleness = 8;	// minimum time delay inserted between two consecutive data frames in master mode
							comm.startCommunication(&d);	// begin a single transmit/receive
					'''
				}
				Transition tr4: sending_data -> running {
					triggers {
						<communicationCommandDone: comm>
					}
					action '''
						logger.logF("SPI6 %x, %x, %x, %x, %x, %x", rxBuff[0], rxBuff[1], rxBuff[2], rxBuff[3], rxBuff[4], rxBuff[5]);
						logger.logF("SPI6 message: %s", &rxBuff[0]);
						logger.log("------------------------");
					'''
				}
			}
		}
	}
}