Communicate via SPI 
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:

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
ActorClass Application {
Structure {
// ...
ActorRef spi5: minihil.platform.adapters.spi_custom.ASPI5Adapter
ActorRef spi6: minihil.platform.adapters.spi_custom.ASPI6Adapter
}
}
![]() |
Add the appropriate imports for the adapters |
![]() |
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
ActorClass Application {
// ...
ActorRef spi6_tester: ASPI6Tester
ActorRef spi6: ASPI6Adapter
Binding spi6_tester.ctrl and spi6.ctrl
Binding spi6_tester.comm and spi6.fct
}
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 {
// ...
}
}
}
}
![]() |
Port for controlling the SPI adapter |
![]() |
SPI tester instance |
![]() |
Connect the communication and control ports |
![]() |
Instantiate the communication and control ports |
![]() |
Create buffers for transmitting and receiving with data for transmitting pre-determined, bufsize >= frames to be sent |
![]() |
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):
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.
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;
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("------------------------");
'''
}
}
}
}
![]() |
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
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]);
logger.log("------------------------");
'''
}
}
}
}
}
![]() |
Import SPI5 adapter |
![]() |
Instantiate actors and connect ports for SPI5 adapter and tester |
![]() |
Chane data to be transferred |
![]() |
Change starting wait timing, so master is configured first |
![]() |
Set isMaster = false |
Running the example
Once you have flashed and started the target you should see output like this in your terminal.
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("------------------------");
'''
}
}
}
}
}