hero

Guide to GIGA R1 Dual Cores

Learn how to access and control the M4 and M7 cores on the GIGA R1, and how to communicate between them using RPC.

The GIGA R1's STM32H747XI has two cores, the M4 and the M7. Each core can be programmed individually, with M7 acting as the main processor, and the M4 as a co-processor.

The M7 is referred to as the main processor due its superior hardware features, as well as it is required to run to boot the M4 core (you boot the M4 from within the M7).

These two cores can run applications in parallel, for example, running a servo motor on one core, and a display on another, without blocking each other. In a single core, such operations would slow down the program, resulting in lesser performance.

The M4 and M7 cores are programmed with separate sketches, using the same serial port. In the Arduino IDE, you can select the core you want to program, and then upload the sketch you want to run on that specific core.

Goals

In this guide you will discover:

  • How to configure and program the M4/M7 cores and conventional approaches to do so.
  • How to boot the M4 core.
  • How to communicate between the cores via Remote Call Procedures (RPC).
  • Useful examples based on the dual core & RPC features.
  • The
    RPC
    library API.

Hardware & Software Needed

*For instructions on how to install the GIGA Core, follow the Getting Started with GIGA R1 guide.

Programming M4/M7

Programming the cores is done via the Arduino IDE, in a special interface that appears only when you select the Arduino GIGA R1 board from the board menu.

Partitioning The Flash Memory

To allocate memory for the M4, the flash memory can be partitioned. This is done by navigating to Tools > Flash Split in the IDE.

Flash partitioning in the IDE.
Flash partitioning in the IDE.

  • 2MB M7 + M4 in SDRAM (default) - this option is the default configuration, which is for programming the M7 only. This allocates no memory to the M4.
  • 1.5MB M7 + 0.5MB M4 - useful when larger amount of memory is required on the M7.
  • 1MB M7 + 1MB M4 - useful when you need to balance the memory equally between the M4 and M7 cores.

It is required to use option 2 or 3 if you intend to program the M4 via the IDE, as the default option provides no memory allocation for the M4.

Target Core

To select the core you want to program, navigate to Tools > Target Core in the IDE.

Flash partitioning in the IDE.
Flash partitioning in the IDE.

Here you can choose between:

  • Main Core - this is the M7 core, the main processor on the board.
  • M4 Co-processor - this is the M4 core, the co-processor on the board.

Uploading

As both cores share the same serial port, choosing the Flash Split + Target Core is required so that the program is uploaded to the correct core.

Uploading is no different than to any other Arduino board: simply click the upload button and wait for it to finish.

Booting M4 Core

The M4 core does not boot by itself as it requires interaction from the M7 core. This boot function is built into the

RPC
library, and needs to be included in the sketch uploaded to the M7:

1#include <RPC.h>
2
3void setup() {
4 RPC.begin(); //boots M4
5}
6void loop(){
7}

Once the M4 is booted from the M7, both cores will run in parallel, much like two Arduinos sharing the same board.

Writing Over Existing Sketch

Uploading new sketches works the same as a typical upload procedure. The new sketch will overwrite the current sketch running on the core you upload to.

Limitations

The M7 and M4 cores are two separate cores, and when initialized, they will continue to execute their corresponding program.

In this section you will find some known limitations of using the two parallel cores.

Booting M4

As mentioned in the previous section, the M4 requires to be booted from the M7, by using the

RPC.begin()
method. If this is not included, the M4 will not boot.

Serial Communication

Serial communication is not available by default on the M4 core. A work around for this is by sending data using the

RPC
library, and printing it from the M7 core. To achieve this, see the following examples:

M4 Sketch

1#include <RPC.h>
2
3void setup() {
4 RPC.begin();
5}
6
7void loop() {
8 // put your main code here, to run repeatedly:
9 RPC.println("Hello World!");
10}

M7 Sketch

1#include <RPC.h>
2
3void setup() {
4 // put your setup code here, to run once:
5 Serial.begin(9600);
6 RPC.begin();
7}
8
9void loop() {
10 String buffer = "";
11 while (RPC.available()) {
12 buffer += (char)RPC.read();
13 }
14
15 if (buffer.length() > 0) {
16 Serial.print(buffer);
17 }
18}

Note that both of these sketches needs to be uploaded to their corresponding cores.

Methods of Programming

Programming the M4 and M7 cores is straightforward, but can be complicated to track. Having a strategy for how you want to build your dual core applications is key.

In this section we introduce the "single" and "multiple" sketch approach, and the pros and cons of each method.

Single Sketch Approach

The single sketch approach means writing a single sketch that is uploaded to both cores each time a change is made. In the sketch, we can keep track of what each core does, simply by querying the core used with a simple function:

1String currentCPU() {
2 if (HAL_GetCurrentCPUID() == CM7_CPUID) {
3 return "M7";
4 } else {
5 return "M4";
6 }
7}

With this function, we check whether the M4 or M7 is running, and we can write code for each of the core like this:

1if (currentCPU() == "M4") {
2 //run M4 code
3 }
4
5 if (currentCPU() == "M7") {
6 //run M7 code
7 }

The pros of using this approach is that you can write all code in a single file, therefore, revisioning code, as well as the provisioning is easier to manage.

The cons of using this approach is that you will run out of program memory faster. You will also upload code to the cores that will never execute (the M7 code will not execute on M4 and vice versa).

Multiple Sketch Approach

The multiple sketch approach means developing two separate sketches, one for each core. This does not require the use of the

HAL_GetCurrentCPUID()
to retrieve the core you are using, you can instead just write sketches as you would normally do.

The pros of using this approach is that the code you write is optimized only for one core, and this saves a lot of program memory.

The cons is to manage the versions becomes harder, and while flashing the board, you'd need to keep track on which version is uploaded to which core. It is easier to upload to the wrong core by accident using this approach, but you have more optimized code.

When writing multiple sketches, there are some things to consider to make your development experience easier:

  • Name your sketches with either
    _M4
    or
    _M7
    suffix or prefix. This will make it easier if the code is intended to be shared with others.
  • Consider having a starting sequence (e.g. the blue LED blinking 3 times), whenever a core is initialized.
  • Always include
    RPC.begin()
    on your M7 core sketch.

Remote Call Procedures (RPC)

RPC is a method that allows programs to make requests to programs located elsewhere. It is based on the client-server model (also referred to as caller/callee), where the client makes a request to the server.

An RPC is a synchronous operation, and while a request is being made from the caller to another system, the operation is suspended. On return of the results, the operation is resumed.

The server side then performs the subroutine on request, and suspends any other operation as well. After it sends the result to the client, it resumes its operation, while waiting for another request.

Request routine.
Request routine.

RPCs in the Arduino Environment

At the moment, only a limited amount of boards supports RPC, as in this context, it is designed to be a communication line between two cores. The GIGA R1 is one of them.

What makes this implementation possible is the

RPC
library (see API section), which utilises the rpclib C++ library as well as functions from the Stream class.

The library makes it possible to set up either of the M4/M7 cores as a server/client, where remote calls can be made between them. This is done by "binding" a function to a name on the server side, and calling that function from the client side.

On the server side, it could look like this:

1//server side, for example M7
2int addFunction(int a, int b){
3 return a + b;
4}
5
6RPC.bind("addFunction", addFunction);

On the client side, it could look like this:

1int x,y = 10;
2
3RPC.call("addFunction", x, y);

When

call()
is used, a request is sent, it is processed on the server side, and returned. The
x
and
y
variables are used as arguments, and the result returned should be 20 (10+10).

Communication between M7 and M4 core.
Communication between M7 and M4 core.

RPC Examples

In this section, you will find a series of examples that is based on the

RPC
library.

RPC Serial

The

Serial.print()
command only works on the M7 core. In order to print values on the M4, we need to:

  • Use
    RPC.println()
    on the M4. This will print the values to the RPC1 stream.
  • Use
    RPC.available()
    and
    RPC.read()
    .

M4 Sketch:

1#include <RPC.h>
2
3void setup() {
4RPC.begin();
5}
6
7void loop() {
8RPC.println("Printed from M4 core");
9delay(1000);
10}

M7 Sketch:

1#include <RPC.h>
2
3void setup() {
4Serial.begin(9600);
5RPC.begin();
6}
7
8void loop() {
9 String buffer = "";
10 while (RPC.available()) {
11 buffer += (char)RPC.read(); // Fill the buffer with characters
12 }
13 if (buffer.length() > 0) {
14 Serial.print(buffer);
15 }
16}

RPC Sensor

This example demonstrates how to request a sensor reading from one core to the other, using:

  • M4 as a client.
  • M7 as a server.

M4 Sketch:

1#include "Arduino.h"
2#include "RPC.h"
3
4using namespace rtos;
5
6Thread sensorThread;
7
8void setup() {
9 RPC.begin();
10 Serial.begin(115200);
11
12 /*
13 Starts a new thread that loops the requestReading() function
14 */
15 sensorThread.start(requestReading);
16}
17
18void loop() {
19}
20
21/*
22This thread calls the sensorThread() function remotely
23every second. Result is printed to the RPC1 stream.
24*/
25void requestReading() {
26 while (true) {
27 delay(1000);
28 auto result = RPC.call("sensorRead").as<int>();
29 RPC.println("Result is " + String(result));
30 }
31}

M7 Sketch:

1#include "Arduino.h"
2#include "RPC.h"
3
4void setup() {
5 RPC.begin();
6 Serial.begin(115200);
7
8 //Bind the sensorRead() function on the M7
9 RPC.bind("sensorRead", sensorRead);
10}
11
12void loop() {
13 // On M7, let's print everything that is received over the RPC1 stream interface
14 // Buffer it, otherwise all characters will be interleaved by other prints
15 String buffer = "";
16 while (RPC.available()) {
17 buffer += (char)RPC.read(); // Fill the buffer with characters
18 }
19 if (buffer.length() > 0) {
20 Serial.print(buffer);
21 }
22}
23
24/*
25Function on the M7 that returns an analog reading (A0)
26*/
27int sensorRead() {
28 int result = analogRead(A0);
29 return result;
30}

RPC Servo Motor

This example demonstrates how to request a servo motor on another core to move to a specific angle, using:

  • M4 as a client.
  • M7 as a server.

Each example is written as a single sketch intended to be uploaded to both cores.

M4 sketch:

1#include "Arduino.h"
2#include "RPC.h"
3
4using namespace rtos;
5
6Thread servoThread;
7
8void setup() {
9 RPC.begin();
10 Serial.begin(115200);
11
12 /*
13 Starts a new thread that loops the requestServoMove() function
14 */
15 servoThread.start(requestServoMove);
16}
17
18void loop() {
19}
20
21/*
22This thread calls the servoMove() function remotely
23every second, passing the angle variable (0-180).
24*/
25void requestServoMove() {
26 while (true) {
27 //Read a pot meter
28 int rawAnalog = analogRead(A0);
29
30 //Map value to 180
31 int angle = map(rawAnalog, 0, 1023, 0, 180);
32
33 delay(1000);
34 auto result = RPC.call("servoMove", angle).as<int>();
35 RPC.println("Servo angle is: " + String(result));
36 }
37}

M7 sketch:

1#include "Arduino.h"
2#include "RPC.h"
3#include <Servo.h>
4
5Servo myservo;
6
7void setup() {
8 RPC.begin();
9 myservo.attach(5); //attach servo to pin 5
10
11 Serial.begin(115200);
12
13 //Bind the servoMove() function on the M7
14 RPC.bind("servoMove", servoMove);
15}
16
17void loop() {
18 // On M7, let's print everything that is received over the RPC1 stream interface
19 // Buffer it, otherwise all characters will be interleaved by other prints
20 String buffer = "";
21 while (RPC.available()) {
22 buffer += (char)RPC.read(); // Fill the buffer with characters
23 }
24 if (buffer.length() > 0) {
25 Serial.print(buffer);
26 }
27}
28
29/*
30Function on the M7 that returns an analog reading (A0)
31*/
32int servoMove(int angle) {
33 myservo.write(angle);
34 delay(10);
35 return angle;
36 /*
37 After the operation is done, return angle to the client.
38 The value passed to this function does not change, but this
39 verifies it has been passed correctly.
40 */
41}

RPC Library API

The

RPC
library is based on the rpclib C++ library which provides a client and server implementation. In addition, it provides a method for communication between the M4 and M7 cores.

This library is included in the GIGA core, so it is automatically installed with the core. To use this library, you need to include

RPC.h
:

1#include <RPC.h>

RPC.begin()

Initializes the library. This function also boots the M4 core.

Syntax

1RPC.begin()

Returns

  • 1
    on success.
  • 0
    on failure.

RPC.bind()

Used on the server side to bind a name to a function, and makes it possible for remotely calling it from another system.

Syntax

1RPC.bind("this_function", thisfunction)

Parameters

  • "name_of_func"
    - name given for the function to be called from the client side.
  • name_of_func
    - name of the function on the server side.

Returns

  • None.

RPC.call()

Used on the client side to call a function with optional parameters.

1RPC.call("this_function", int args)

Parameters

  • "name_of_func"
    - the name of the function declared on the server side.
  • args
    - arguments to be passed to the function.

Returns

  • Result of the function if arguments are passed.

RPC Serial API

The RPC Serial methods are also included in the

RPC
library, and uses methods from the Stream base class, and is similar to the Serial class.

As the

Serial
class is only available on the M7 core, the M4 core uses
RPC
library to print data, where the M7 can read the data and print it to a computer.

RPC.println()

Prints data to a serial port. This is used on the M4 core to send data to the M7.

Syntax

1RPC.println(val);

Parameters

  • The value to print. Can be any data type, but not multiple (e.g. string + integer in the same call).

Returns

  • Number of bytes used. E.g. printing ("hello") returns 7. As hello (5) + new line (2) = 7.

RPC.available()

Get the number of available bytes to read from the M4.

Syntax

1RPC.available();

Parameters

  • None.

Returns

  • The number of bytes available to read.
  • -1
    if there is none.

RPC.read()

Reads the first available byte from the M4.

Syntax

1RPC.read();

Parameters

  • None.

Returns

  • The first available byte from the M4.
  • -1
    if there is none.

Contribute to Arduino

Join the community and suggest improvements to this article via GitHub. Make sure to read out contribution policy before making your pull request.

Missing something?

Check out our store and get what you need to follow this tutorial.

Suggest Changes

The content on docs.arduino.cc is facilitated through a public GitHub repository. You can read more on how to contribute in the contribution policy.