The Arduino GIGA R1 WiFi contains two independent processor cores inside its STM32H747XI:
|
1 2 3 4 5 6 7 8 9 10 |
Cortex-M7 → up to 480 MHz → main processor Cortex-M4 → up to 240 MHz → co-processor |
Unlike a normal dual-core microcontroller where one sketch automatically spreads work across both CPUs, the GIGA treats the cores much more like:
|
1 2 3 4 5 |
two Arduino processors inside one MCU |
Each core can run its own sketch.
The two applications can execute in parallel and communicate through Arduino’s:
|
1 2 3 4 |
RPC library |
This makes it possible to place time-critical control on one core while the other handles networking, graphics, storage or application logic.
GIGA Dual-Core Architecture
The STM32H747 contains:
| Core | Role | Maximum clock |
|---|---|---|
| Cortex-M7 | Main processor | 480 MHz |
| Cortex-M4 | Co-processor | 240 MHz |
The M7 is the more powerful core and is the processor that boots first.
The M4 does not automatically start when the board powers up.
It must be started by the M7.
The Most Important Rule: M7 Boots M4
Arduino’s RPC library includes the M4 boot sequence.
The M7 sketch normally needs:
|
1 2 3 4 5 6 7 8 9 10 11 |
#include <RPC.h> void setup() { RPC.begin(); } void loop() { } |
Calling:
|
1 2 3 4 |
RPC.begin(); |
on the M7 starts the M4 firmware.
Without that boot step, uploading a valid M4 sketch is not enough: the M4 remains disabled.
The M7 Cannot Be Disabled
The M7 is:
|
1 2 3 4 5 6 |
the main processor and always boots by default |
Arduino’s current dual-core guide does not provide a normal option to disable it and run only the M4.
You can give the M7 a minimal application, but the M7 still has to participate in the boot process.
You Need Two Sketches
For a normal dual-core application, create:
|
1 2 3 4 5 |
project_M7.ino project_M4.ino |
and treat them as two separate programs.
For example:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
M7 → user interface → Wi-Fi → logging → graphics M4 → sensor acquisition → motor control → timing-sensitive tasks |
This is often easier to reason about than trying to place all application logic in one very large sketch.
Install the GIGA Board Package
Before using both cores, install the current Arduino GIGA board package in the Arduino IDE.
Once the GIGA R1 WiFi is selected, additional board menu options become available under:
|
1 2 3 4 |
Tools |
including:
|
1 2 3 4 5 |
Flash Split Target Core |
Flash Split Must Be Configured
The internal STM32H747 Flash is:
|
1 2 3 4 |
2 MB |
and Arduino lets you divide it between the two cores.
The current IDE provides three main Flash configurations.
Default
|
1 2 3 4 |
2 MB M7 + M4 in SDRAM |
This is the normal default and effectively allocates the internal Flash to the M7.
Arduino’s guide notes that this default configuration is intended for programming the M7 only through the normal IDE dual-core workflow.
1.5 MB / 0.5 MB
|
1 2 3 4 5 |
1.5 MB M7 0.5 MB M4 |
This is useful when:
- the M7 application is relatively large;
- the M4 application is comparatively small.
1 MB / 1 MB
|
1 2 3 4 5 |
1 MB M7 1 MB M4 |
This is useful when both applications need similar program space.
To Upload an M4 Sketch, Allocate Flash to M4
Arduino’s current guide explicitly states that when programming the M4 through the IDE, use either:
|
1 2 3 4 |
1.5 MB M7 + 0.5 MB M4 |
or:
|
1 2 3 4 |
1 MB M7 + 1 MB M4 |
because the default does not provide the normal Flash allocation required for an IDE-uploaded M4 application.
Select the Target Core
Before uploading, open:
|
1 2 3 4 5 |
Tools → Target Core |
and choose:
|
1 2 3 4 5 6 7 8 |
Main Core → Cortex-M7 M4 Co-processor → Cortex-M4 |
The same physical USB programming connection is used for both.
Typical Upload Workflow
A practical workflow is:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
1. Select suitable Flash Split. 2. Open project_M4.ino. 3. Tools → Target Core → M4 Co-processor. 4. Upload. 5. Open project_M7.ino. 6. Tools → Target Core → Main Core. 7. Upload. 8. Ensure M7 calls RPC.begin(). |
After reset:
|
1 2 3 4 5 6 7 |
M7 boots → RPC.begin() → M4 boots → both applications execute |
Uploading One Core Does Not Merge the Sketches
The programs remain separate.
If you upload a new M4 sketch:
|
1 2 3 4 5 |
old M4 application → overwritten |
while the M7 application remains its own image.
The same applies in reverse.
Identify Which Core Is Running
The RPC library provides:
|
1 2 3 4 |
RPC.cpu_id() |
which returns:
|
1 2 3 4 |
CM7_CPUID |
or:
|
1 2 3 4 |
CM4_CPUID |
This is useful if you intentionally upload a common codebase to both cores.
Example: Identify M7 or M4
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <RPC.h> void setup() { RPC.begin(); if (RPC.cpu_id() == CM7_CPUID) { // Running on M7 } else { // Running on M4 } } void loop() { } |
In larger projects, separate M7/M4 sketches are often clearer, but runtime core identification can still be useful.
What Can the M4 Access?
Arduino currently documents M4 support for:
- I2C;
- SPI;
- UART;
- CAN;
- DAC;
- ADC;
- Bluetooth LE through ArduinoBLE.
So the M4 is not merely a tiny background CPU.
It can directly control a large part of the GIGA hardware.
What the M4 Does Not Directly Support
Arduino currently lists these important limitations:
|
1 2 3 4 5 6 |
Wi-Fi Arduino Cloud sketches USB Serial |
as unavailable directly from the M4 in the normal workflow.
The M7 is therefore the natural place for:
- Wi-Fi;
- cloud communication;
- USB Serial Monitor output.
M4 Serial Output Must Normally Pass Through M7
If you write:
|
1 2 3 4 |
Serial.println() |
on M4 expecting the normal USB Serial Monitor, it will not behave like M7 USB serial.
The usual pattern is:
|
1 2 3 4 5 6 7 8 9 |
M4 → RPC.println() M7 → RPC.read() → Serial.print() |
Simple M4 Serial Example
Upload this to the M4:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <RPC.h> void setup() { RPC.begin(); } void loop() { RPC.println("Hello from M4"); delay(1000); } |
M7 USB Bridge Example
Upload this to the M7:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <RPC.h> void setup() { Serial.begin(115200); RPC.begin(); } void loop() { while (RPC.available()) { Serial.write(RPC.read()); } } |
The data path becomes:
|
1 2 3 4 5 6 7 8 |
M4 → RPC stream → M7 → USB Serial → PC |
What Is RPC?
RPC means:
|
1 2 3 4 |
Remote Procedure Call |
It allows one core to invoke a function implemented by the other core.
The basic model is:
|
1 2 3 4 5 6 7 8 9 10 |
server core → binds a function name client core → calls that function name → passes parameters → receives result |
RPC.bind()
Suppose the M4 owns a sensor.
The M4 can expose a function:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <RPC.h> int readSensor() { return analogRead(A0); } void setup() { RPC.begin(); RPC.bind("readSensor", readSensor); } void loop() { } |
The name:
|
1 2 3 4 |
"readSensor" |
becomes callable from the other core.
RPC.call()
The M7 can request the value:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <RPC.h> void setup() { Serial.begin(115200); RPC.begin(); } void loop() { int value = RPC.call("readSensor").as<int>(); Serial.println(value); delay(1000); } |
The M7 does not directly read the sensor in this architecture.
Instead:
|
1 2 3 4 5 6 7 |
M7 → asks M4 → M4 performs read → result returned to M7 |
RPC Calls Are Synchronous
This matters for real-time design.
Arduino describes a normal:
|
1 2 3 4 |
RPC.call() |
as synchronous.
The calling core waits until the remote operation returns.
So this:
|
1 2 3 4 |
RPC.call("slowFunction") |
can block the caller if:
- the remote function takes a long time;
- the other core is busy;
- the remote code blocks on hardware.
Do Not Put Long Blocking Work Behind Every RPC Call
A better architecture is often:
|
1 2 3 4 5 6 7 8 |
RPC → request / configuration / status local core → performs continuous real-time work |
For example:
|
1 2 3 4 5 6 7 8 9 |
M7 RPC command: "setMotorSpeed(1200)" M4: updates target continues running motor loop locally |
This is much better than making M7 repeatedly call an RPC function for every motor-control pulse.
Stream Data Through RPC
The RPC object also behaves similarly to an Arduino Stream.
Useful methods include:
|
1 2 3 4 5 6 |
RPC.println() RPC.available() RPC.read() |
This is convenient for:
- debug text;
- status messages;
- simple byte streams.
Do Not Let Both Cores Control the Same Pin
This is one of the most important GIGA dual-core rules.
The M7 and M4 share physical hardware.
If both try to drive:
|
1 2 3 4 |
D27 |
for example, Arduino warns that pin priority is not guaranteed.
Do not assume:
|
1 2 3 4 5 |
M7 is faster therefore M7 wins |
Arduino’s documentation explicitly says access can effectively be random.
Assign Pin Ownership
A much safer architecture is:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
M7 owns: D0-D21 Wi-Fi USB display M4 owns: D22-D40 ADC channels motor control CAN |
The exact split does not matter.
What matters is that each hardware resource has one clear owner.
Do Not Use the Same Peripheral from Both Cores
The same rule applies to peripherals.
If the M7 starts a CAN application and the M4 then reconfigures the same CAN peripheral, the two programs can interfere.
Arduino explicitly warns against sharing the same:
- pin;
- bus;
- peripheral;
- hardware block;
without deliberate low-level coordination.
Peripheral Ownership Table
For a real project, create something like:
| Resource | Owner |
|---|---|
| Wi-Fi | M7 |
| USB Serial | M7 |
| Display | M7 |
| Motor PWM | M4 |
| Encoder | M4 |
| ADC sensors | M4 |
| CAN | M4 |
This prevents accidental reinitialisation and timing conflicts.
A Good Dual-Core Architecture
For a machine controller:
M7
|
1 2 3 4 5 6 7 8 9 10 |
Wi-Fi web interface logging USB display configuration high-level state machine |
M4
|
1 2 3 4 5 6 7 8 9 |
motor control encoder reading ADC acquisition fast digital I/O safety interlocks timing-sensitive loops |
RPC
|
1 2 3 4 5 6 7 8 9 |
start stop target speed sensor summary fault state position |
This is where dual-core becomes genuinely useful rather than simply a specification.
Why Not Just Use Mbed Threads on M7?
The M7 can already run multiple threads under Mbed OS.
So dual-core is not necessary every time you want:
|
1 2 3 4 |
two things happening at once |
Use the M4 when you benefit from:
- hardware isolation;
- a separate timing domain;
- keeping real-time control away from network/UI work;
- separate firmware ownership;
- a genuinely independent application loop.
Dual-Core Does Not Automatically Double Performance
Moving arbitrary functions to M4 does not automatically make the application:
|
1 2 3 4 |
480 + 240 = 720 MHz |
in any useful simple sense.
Performance depends on:
- whether tasks are parallel;
- memory access;
- peripheral conflicts;
- RPC overhead;
- synchronisation;
- how work is divided.
Best Workloads for M4
The M4 is well suited to repetitive, isolated tasks such as:
- reading sensors at fixed intervals;
- PID loops;
- motor control;
- encoder decoding;
- CAN processing;
- audio/control housekeeping;
- background monitoring.
Best Workloads for M7
The M7 is the natural home for:
- Wi-Fi;
- Arduino Cloud;
- USB communication;
- large displays;
- graphics;
- filesystems;
- heavy numerical processing;
- main application logic.
Common Mistake 1: Uploading M4 Without Changing Flash Split
If the board remains on:
|
1 2 3 4 |
2 MB M7 + M4 in SDRAM |
the normal IDE workflow does not allocate internal Flash for an M4 sketch.
Select:
|
1 2 3 4 |
1.5 MB M7 + 0.5 MB M4 |
or:
|
1 2 3 4 |
1 MB M7 + 1 MB M4 |
Common Mistake 2: Forgetting to Change Target Core
The IDE does not magically know which sketch is intended for which processor.
Before every upload, verify:
|
1 2 3 4 5 |
Tools → Target Core |
Using filenames such as:
|
1 2 3 4 5 |
controller_M7.ino realtime_M4.ino |
helps prevent mistakes.
Common Mistake 3: M4 Sketch Uploaded but Never Runs
The M4 needs to be booted by M7.
Make sure the M7 contains:
|
1 2 3 4 5 6 |
#include <RPC.h> RPC.begin(); |
Common Mistake 4: Printing Serial Directly from M4
Normal USB Serial belongs to the M7 path.
For M4 diagnostics use:
|
1 2 3 4 |
RPC.println() |
and forward the data through M7.
Common Mistake 5: Both Cores Initialise the Same Bus
For example:
|
1 2 3 4 5 6 7 8 |
M7 → CAN.begin() M4 → CAN.begin() |
is not a clean way to share CAN.
Choose one owner and use RPC to exchange the information needed by the other core.
Common Mistake 6: Using RPC for High-Frequency Tight Loops
RPC is convenient, but it is not a substitute for local real-time execution.
Bad architecture:
|
1 2 3 4 5 6 7 |
M7 → RPC call → M4 toggles motor pin → repeat thousands of times per second |
Better architecture:
|
1 2 3 4 5 6 7 8 9 |
M7 → RPC set target M4 → local motor-control loop → reports status occasionally |
Run Arduino and MicroPython Together
One of GIGA’s more unusual capabilities is running:
|
1 2 3 4 5 6 |
MicroPython on M7 + Arduino sketch on M4 |
and communicating between them through:
|
1 2 3 4 |
msgpackrpc / RPC |
Current MicroPython RPC Restrictions
Arduino’s current dual-core guide documents several restrictions.
When using MicroPython on M7 with an Arduino M4 application:
|
1 2 3 4 |
Arduino sketch runs on M4 |
and the supported Flash-based workflow uses:
|
1 2 3 4 |
1.5 MB M7 + 0.5 MB M4 |
Arduino also notes that the SDRAM-target M4 firmware path is not currently supported in that MicroPython RPC workflow.
MicroPython Can Boot the M4
On M7 MicroPython:
|
1 2 3 4 5 6 7 8 9 10 |
import msgpackrpc rpc = msgpackrpc.MsgPackRPC() rpc.start( firmware=0x08180000 ) |
starts the M4 firmware at the documented Flash address for the 1.5 MB / 0.5 MB partition.
MicroPython Can Call M4 Functions
If the M4 binds:
|
1 2 3 4 |
RPC.bind("led", led); |
MicroPython can call it through:
|
1 2 3 4 |
rpc.call("led", True) |
This creates interesting architectures where:
- Python handles high-level experimentation;
- M4 Arduino code handles deterministic hardware control.
RPC Can Work in Either Direction
Do not think of:
|
1 2 3 4 5 |
M7 = server M4 = client |
as a fixed requirement.
Either core can expose bound functions and either can initiate remote calls.
Choose the direction based on who owns the resource.
Example Architecture: Sensor Acquisition on M4
M4
|
1 2 3 4 5 6 7 |
read ADC filter samples calculate average bind getSensorValue() |
M7
|
1 2 3 4 5 6 7 |
call getSensorValue() publish MQTT update display log to storage |
This cleanly keeps the measurement loop independent from network traffic.
Example Architecture: Motor Control on M4
M4 owns
|
1 2 3 4 5 6 7 8 |
PWM encoder limit switches PID fault logic |
M7 owns
|
1 2 3 4 5 6 7 8 |
UI Wi-Fi commands logging configuration |
RPC commands
|
1 2 3 4 5 6 7 8 |
setSpeed() moveTo() stop() getPosition() getFault() |
This is an excellent use of the two cores because the motor loop continues even if the M7 is busy drawing graphics or handling a web request.
Example Architecture: Display on One Core
You can also move a display workload to one processor while the other handles control.
But the same ownership rule applies:
|
1 2 3 4 5 6 7 8 |
one core → owns display bus other core → sends data/state through RPC |
Do not let both cores initialise and update the same display controller independently.
Debugging Dual-Core Applications
Dual-core bugs can be harder than normal Arduino bugs because two programs are running simultaneously.
Useful techniques include:
- blink a different RGB LED colour on each core during boot;
- prefix debug messages with
M7:orM4:; - keep a written peripheral ownership table;
- test each core independently before enabling RPC;
- add one RPC command at a time;
- avoid dynamic resource ownership.
Give Each Core a Boot Signature
For example:
|
1 2 3 4 5 6 7 8 |
M7 → blink BLUE three times M4 → blink GREEN three times |
This immediately tells you whether:
- both firmware images are present;
- M7 successfully booted M4;
- the correct core was uploaded.
Keep M4 Firmware Simple First
When starting a dual-core project, begin with:
|
1 2 3 4 5 6 |
M4: blink LED RPC.println("M4 alive") |
Only after that works should you add:
- CAN;
- ADC;
- motor control;
- timers;
- multiple threads.
Quick Setup Reference
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
1. Install GIGA board package. 2. Select Arduino GIGA R1 WiFi. 3. Tools → Flash Split: 1.5 MB M7 + 0.5 MB M4 or 1 MB M7 + 1 MB M4 4. Tools → Target Core: M4 Co-processor 5. Upload M4 sketch. 6. Tools → Target Core: Main Core 7. Upload M7 sketch. 8. M7 sketch calls: RPC.begin(); 9. M7 boots. 10. M7 boots M4. 11. Both sketches run in parallel. |
RPC API Quick Reference
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <RPC.h> RPC.begin(); → initialise RPC → M7 also boots M4 RPC.bind("name", function); → expose function to other core RPC.call("name", args); → call remote function RPC.call("name", args).as<int>(); → call and retrieve typed result RPC.println(value); → send stream data RPC.available(); → bytes waiting RPC.read(); → read byte RPC.cpu_id(); → CM7_CPUID or CM4_CPUID |
Final Recommendation
Do not use GIGA’s dual-core processor simply because two cores are available.
Use it when your project naturally separates into:
|
1 2 3 4 5 6 |
high-level / connected workload + real-time / hardware workload |
The most effective pattern is usually:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
M7 → Wi-Fi → USB → display → logging → application state M4 → sensors → motors → ADC → CAN → deterministic control RPC → commands → configuration → status → results |
Keep one owner per hardware peripheral, avoid having both cores drive the same pin or bus, and remember that ordinary RPC calls are synchronous.
With those rules in place, the GIGA R1 WiFi behaves less like one complicated dual-core Arduino and more like two cooperating embedded controllers on the same chip.
For the full board mapping, see our Arduino GIGA R1 WiFi pinout guide. For a generational comparison with Arduino’s earlier large ARM board, see Arduino Due vs GIGA R1 WiFi.