SPI3 App Lab Brick Proof of Concept for UNO Q

Following the SPI3 discussions, I spent some time experimenting with SPI3 communication between the STM32U585 MCU and the QRB2210 MPU on the UNO Q.

The result is a small Proof of Concept demonstrating:

  • A custom Arduino App Lab brick
  • Access to /dev/spidev0.0 from a dedicated Docker container
  • SPI3 communication between MCU and MPU
  • Transmission of floating-point values
  • A simple Python API usable from App Lab

Repository:

This is an experimental project and certainly not production-ready, but it may be useful to anyone interested in exploring SPI3 on the UNO Q.

Feedback and suggestions are welcome.

I have successfully imported your project into my App Lab and run it. I can see floating-point values being printed on the Python console. Later, I will try sending known float values from the MCU to verify that the MPU receives exactly what is being transmitted.

There is no doubt that the sketch and script you have created are beyond my current experience level. Nevertheless, I will try to learn a few things from your application through experimentation and questions.

In the meantime, would it be possible for you to provide the smallest possible sketch that continuously writes the value 13.57 to the SPI3 port at 1-second intervals? I will then execute CLI commands on the Linux side to read the incoming data and display it.

My immediate goal is to confirm that I can access the communication link established between the SPI3 interface on the MCU side and the /dev/spidev0.0 device on the MPU side. After that I will see how you have implemented the SPI3 using higer level code.

Q1:
There is a SPIPeripheral.h Library -- is it readily available or you have created it?

Q2:
In the sketch, you have the following code which I believe uses the Router Bridge. but you have executed the Monitor.begin() code and not the Bridge.begin() code. Are you sure that the bridge as been properly intialized as I have not seen the stated message (Begin SPI3 Test....) on Serial Monitor.

 Monitor.println("Begin SPI3 Test....");

Thhank you.

@philippe86220

Thank you for putting this together - I have been struggling to get it working!!!!

I noticed that you are using HTTP to send/receive data between the MPU and MCU I have been trying to use it direct without using HTTP - wonder if that is even possible?

That was created by @facchinm in response to one of the issues/prs on SPI3 - when I find the link I will post it.

EDIT: here is the link I mentioned: unoq: enable spi peripheral interface by facchinm · Pull Request #383 · arduino/ArduinoCore-zephyr

and to the original discussion on getting SPI3 test case working:
How to install python packages on the Arduino Q - UNO Family / UNO Q - Arduino Forum

@GolamMostafa Thank you for testing the project.

Regarding SPIPeripheral.h, I did not create it myself. I found it in the SPI3 discussion and used it as the basis for my experiments. I still need to investigate its exact origin and will update the repository if I identify the original source.

I have created a simplified sketch that continuously transmits a single float value (13.57) instead of an array of random values.

#include <stdint.h>
#include <string.h>

#include "SPIPeripheral.h"

#define HEADER_BYTE 0xA5

SPIPeripheralClass<1024> spi;

uint8_t buffer[12];

void setup() {

  Monitor.begin();
  delay(5000);

  spi.begin();

  Monitor.println("SPI3 fixed float test");
}

void loop() {

  uint8_t packet[2 + sizeof(float)];

  spi.depopulate(*buffer, 12);

  if (buffer[0] == 0x0B) {

    float value = 13.57f;

    packet[0] = HEADER_BYTE;
    packet[1] = 1;

    memcpy(
      &packet[2],
      &value,
      sizeof(float)
    );

    spi.populate(
      packet,
      sizeof(packet)
    );

    spi.ready();
  }
}

The purpose of this simplified version is to validate the MCU ↔ MPU communication path independently of the App Lab brick and the higher-level code.

If the SPI frame is decoded correctly on the Linux side, the received value should be approximately:

13.57

This should make it easier to verify that the SPI3 link between the STM32U585 MCU and /dev/spidev0.0 is working correctly before moving on to more advanced examples.

I will test your sketch.

Thank you.

@Merlin513

Thank you very much for the clarification and for the links.

I will update the README to mention that SPIPeripheral.h comes from @facchinm's work on the SPI peripheral interface.

Regarding HTTP: in my project, HTTP is not used for the SPI3 communication itself. SPI3 communication is handled by spidev inside the dedicated Docker container. The HTTP layer is only used internally as a simple bridge between the App Lab Python code and the SPI service running in that container.

So the actual data path is:

MCU -> SPI3 -> /dev/spidev0.0 -> spi_service.py -> HTTP -> App Lab Python API

I used this approach because the App Lab main Python environment did not have direct access to spidev in my tests. The dedicated container made it easier to install python3-spidev and expose /dev/spidev0.0.

A more direct approach may be possible, but it would require the main App Lab environment to have both access to /dev/spidev0.0 and the required Python spidev module available.

I have one simple question:

The SPI Port of the header of UNO Q works fine with SPI.h Library. Could not this library be used to handle the SPI3 Port in the similar by executing following codes:?

#include<SPI.h>

SPI3.begin();
byte n = SPI3.transfer(0x33);

That is actually a very interesting question.

My understanding is that SPI3 is not being used in the same way as the external SPI interface exposed through the UNO Q header.

With the standard SPI library, the MCU communicates with external SPI devices connected to the board headers:

SPI.begin(); SPI.transfer(...);

In the SPI3 proof of concept, the STM32U585 MCU communicates with the QRB2210 MPU through an internal SPI link, which appears to be why the dedicated SPIPeripheral.h layer was introduced.

So the question is probably not whether SPI3 is physically capable of transferring bytes using an API similar to SPI.transfer(), but whether the current software stack exposes SPI3 through the standard Arduino SPI API or through a dedicated peripheral interface.

I don't know enough about the internals of SPIPeripheral.h yet to answer that with certainty, but I suspect that it exists precisely because SPI3 is being used differently from the external SPI interface.

Did you forget to include Bridge.begin() line in the setup() function of the original sketch?

Tank you very for providing those links in your post #3.

Thank you for your question.

I investigated this a bit further.

You are correct that Monitor appears to be tied to the RouterBridge infrastructure. Looking at the RouterBridge sources, I found that Monitor is implemented as a BridgeMonitor object:

class BridgeMonitor : public Stream

and is constructed from the global Bridge object:

explicit BridgeMonitor(BridgeClass& bridge)
inline BridgeMonitor<> Monitor(Bridge);

So there is definitely a relationship between Monitor and Bridge.

I also performed a simple test in App Lab. With:

void setup() {
    Monitor.begin();
    delay(2000);
    Monitor.println("setup...");
}

the message was not always visible.

However, increasing the delay to 5 seconds:

void setup() {
    Monitor.begin();
    delay(5000);
    Monitor.println("setup...");
}

made the message appear correctly.

This suggests that the Monitor channel may not be fully ready immediately after startup.

Regarding Bridge.begin(), I did not explicitly call it in my SPI3 proof-of-concept. Nevertheless, RouterBridge-related libraries are automatically added by App Lab during compilation, even for very simple applications that do not explicitly include RouterBridge.

Because of that, I currently suspect that part of the Bridge infrastructure is initialized automatically by the App Lab runtime, although I have not yet verified exactly how this is done internally.

In any case, the SPI3 communication itself does not use RouterBridge. In this proof-of-concept, Monitor was only used for debug output. The actual data path is:

MCU → SPI3 → /dev/spidev0.0 → Python service on the MPU

using SPIPeripheral.h on the MCU side and spidev on the MPU side.

So the missing "Begin SPI3 Test..." message does not necessarily indicate that SPI3 was not working; it may simply have been printed before the Monitor channel became available.

Any message from MCU goes over the Router Bridge to the MPU and then to the Serial Monitor under the control of Linux Machine which must be ready/initialized before the MCU proceeds to execute the Monitor/Serial.print() command. Therefore, I think that we should have mandatory Bride/Monitor.begin() code in the setup() of MCU which you have done and wait until the Linux/Bridge at the MPU side is fully ready. To satisfy this requirement, I would like to put the following lines in the shetch and sctipt.

Sketch side

#include<Arduino_RouterBridge.h>

bool status = false;
String value = "";
unsigned long prMillis = millis();

void setup()
{
   Bridge.begin();
   Monitor.begin();

   do
   {
      RpcCall rpc = Bridge.call("is _linux_ready");
      status = rpc.result(value);
   }
   while((status != true) && (millis() - prMillis) < 5000);
   if(status == true)
   {
      Monitor.print(value);//Linux is ready
   }
   else
   {
      while(true) //error at Linux side
      {
        digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN);
        delay(1000);
     }
  }
}

Script

def  isLinuxReady():
   return "Linux is ready"

Bridge("is_linux_ready", isLinuxReady);

Thank you for the detailed suggestion.

I agree with you regarding Monitor.print(): since Monitor uses the RouterBridge/Bridge infrastructure, the Linux side must be ready before early debug messages printed from setup() can be reliably displayed.

I also confirmed this experimentally: with a short delay after Monitor.begin(), a message printed once from setup() may not appear, while with a longer delay, for example 5 seconds, it does appear.

However, in the SPI3 proof of concept, Monitor is only used for debug output. It is not part of the SPI3 data transfer.

The SPI3 communication itself uses:

SPIPeripheralClass<1024> spi;
spi.begin();

and the data path is:

MCU -> SPI3 -> /dev/spidev0.0 -> Python service on the MPU

not:

MCU -> RouterBridge -> Python

So I think your proposed Bridge.call("is_linux_ready") mechanism is useful if the sketch depends on RouterBridge communication or on reliable early Monitor.print() output.

But for the minimal SPI3 test, I would prefer to keep RouterBridge out of the data path in order to demonstrate that SPI3 works independently.

Also, in my App Lab brick, the readiness check is currently done on the Python side by waiting until the SPI service responds over HTTP. That verifies that the Docker service using /dev/spidev0.0 is ready.

So I agree that the debug output should be cleaned up or delayed, but I would not make RouterBridge mandatory for the SPI3-only proof of concept.

You are absolutely correct in syaing that SPI transaction does not depend on Router Brige though it is need for printing debugging message.

Noe: Your README file in the repository is an excellent write-up. I would like to post it in this thread for quick availability to the readers.

Work-flow description of the application: (taken from the repository of @philippe86220)

Arduino UNO Q - SPI3 App Lab Brick (Proof Of Concept)

Introduction

This project is a Proof of Concept (POC) demonstrating the use of the SPI3 link between the STM32U585 MCU and the Qualcomm QRB2210 MPU on the Arduino UNO Q through a custom Arduino App Lab brick.

The goal is to demonstrate that it is possible to:

  • Access /dev/spidev0.0 from an App Lab brick.
  • Exchange data between the MCU and the MPU using SPI3.
  • Transfer arrays of floating-point values.
  • Expose the received data to an App Lab application through a simple Python API.

This project is experimental and intended to explore the internal architecture of the UNO Q.


Architecture

STM32U585 (MCU)
       |
       | SPI3
       |
QRB2210 Linux (MPU)
       |
   /dev/spidev0.0
       |
   spi_service.py
       |
      HTTP
       |
    SPI3 Brick
       |
     App Lab

Operation

MCU Side

The Arduino sketch:

  • Generates an array of floating-point values.
  • Builds an SPI frame.
  • Adds a synchronization header.
  • Sends the data to the MPU.

Frame structure:

Byte 0  : 0xA5 (signature)
Byte 1  : number of floats
Byte 2+ : float data

MPU Side

The App Lab brick:

  • Accesses /dev/spidev0.0 through python3-spidev.
  • Receives SPI frames.
  • Verifies the 0xA5 header.
  • Converts received bytes back into floating-point values.
  • Exposes the data through a simple Python API.

Example:

from spi3 import SPI3 spi = SPI3() if spi.begin(): value = spi.read_float() print(value) values = spi.read_floats(16) print(values)


Demonstration Data Source

For demonstration purposes, the MCU generates an array of random floating-point values.
The generated values are packed into an SPI frame and transmitted to the MPU through SPI3.

In a real-world application, these values could easily be replaced by:

  • Analog readings (A0, A1, etc.)
  • Sensor measurements
  • GPS data
  • IMU data
  • LiDAR samples
  • Any other application-specific payload

The purpose of this project is not the data source itself, but the demonstration of SPI3 communication between the MCU and MPU on the Arduino UNO Q.


Validated Features

  • SPI3 access from an App Lab brick.
  • Access to /dev/spidev0.0.
  • MCU → MPU communication.
  • Transfer of float arrays.
  • Decoding on the Linux side.
  • Data exposure inside App Lab.
  • Use of a dedicated Docker container.

Known Limitations

This project is an experimental prototype.

Current limitations:

  • Synchronization between MCU and MPU can be improved.
  • The brick keeps the last valid frame.
  • Minimal protocol implementation.
  • No CRC or integrity checking.

These areas can be improved in future versions.


Educational Purpose

This repository is not intended to become an official library.

Its primary purpose is to demonstrate:

  • How to create a custom App Lab brick.
  • How to use a dedicated Docker container.
  • How to access Linux devices from that container.
  • How to use SPI3 between the MCU and MPU on the UNO Q.

Acknowledgements

This project was created as part of a personal exploration of the Arduino UNO Q platform.

A significant part of the investigation, debugging, experimentation, and design process was carried out with the assistance of ChatGPT.

Status

Experimental Project (POC)

Successfully tested on Arduino UNO Q.


Credits

This proof-of-concept is based on the SPI3 peripheral support work by @facchinm:

  • ArduinoCore-zephyr PR #383
  • Original SPI3 discussion on the Arduino Forum

The App Lab brick and MPU-side proof-of-concept were developed from these foundations.

@philippe86220

This time about the tile of your thread as I could not clearly apprehed the meaing of the title inspite of reading for > 5 times though I have got the meaning later on after reading your md file.

The Title:
SPI3 App Lab Brick Proof of Concept for UNO Q

The above title has the followig four noun words (as I understand):
SPI3 (ok)
App Lab Brick : please clarify
Proof of Concept : please clarify
UNO Q (ok)

And if required/needed, you may consider rephrasing the title.

Thank you.

"Bricks" are reusable components of Arduino Apps:

https://docs.arduino.cc/software/app-lab/tutorials/bricks/

The App @philippe86220 shared here utilizes a specific type of Brick called a "custom Brick", which is a recently introduced feature:

Bricks are similar to the Arduino libraries we can leverage to create sketches more efficiently and easily.

"Proof of concept" is a common phrase:

https://en.wiktionary.org/wiki/proof_of_concept

A short and/or incomplete realization of a certain method or idea to demonstrate its feasibility.

I have just updated the repository README with two new sections explaining how the Custom Brick works internally and why an HTTP service is used.

The goal is to make the architecture easier to understand, especially for readers who are discovering App Lab Custom Bricks for the first time.

:backhand_index_pointing_right:

Why does this custom Brick use a Docker service and an HTTP API?

A common question is:

Why not access /dev/spidev0.0 directly from main.py?

The answer is that Arduino App Lab applications run in a managed environment that does not necessarily have direct access to all Linux devices, system packages, or kernel interfaces.

In this proof of concept:

MCU
  ↓
SPI3
  ↓
/dev/spidev0.0
  ↓
Docker service
  ↓
HTTP API
  ↓
App Lab Python code

The custom Brick creates a dedicated Linux service running inside a container.

This service:

  • has access to /dev/spidev0.0
  • installs and uses python3-spidev
  • communicates directly with the SPI3 interface
  • exposes a simple HTTP API

The App Lab Python code does not need to know how SPI transactions are performed. It simply calls:

spi = SPI3()

value = spi.read_float()
values = spi.read_floats()

The Brick acts as an abstraction layer between the App Lab application and the Linux SPI device.

This approach provides several advantages:

  • keeps the App code simple
  • isolates Linux-specific dependencies
  • makes the functionality reusable across multiple projects
  • follows the philosophy of App Lab Bricks as reusable building blocks

The same architecture could be used for many other Linux resources, such as:

  • ALSA audio devices
  • GPS receivers
  • serial ports
  • I2C devices
  • network services
  • custom hardware interfaces

The SPI3 proof of concept demonstrates one practical example of this pattern.


Why use an HTTP server?

Some readers may wonder why this project uses an HTTP server.

The answer is simple:

The HTTP server is not used to create a website.

It is only used as a communication channel between two Python programs running on the Linux MPU.

Think of it this way:

App Lab Python
        |
        | "Please give me the SPI3 values"
        v
SPI3 Service
        |
        | Reads /dev/spidev0.0
        v
SPI3 Hardware

The HTTP request is simply a message sent from one program to another.

For example:

spi.read_floats()

internally becomes:

GET /values

The SPI3 service receives this request, reads the SPI3 interface, and sends the result back.

The App Lab application then receives the values and continues normally.

Why use HTTP?

Because it is:

  • simple
  • reliable
  • easy to debug
  • supported by standard Python libraries

The HTTP server is only running locally inside the UNO Q Linux environment.

No Internet connection is required.

You can think of it as a "messenger" between the App Lab application and the Linux service that has access to the SPI3 device.


What do you mean -- the app is not running in you UNO Q?

I think the dedicated topic created by @Merlin513 will provide the context that will allow you to understand it: