Envision a scenario where you command a trio of microcontrollers, each independently equipped with its own pair of setup and loop functions. These microcontrollers are not just idling around; each one is dedicated to a particular task or is engaged in critical time-sensitive computations.
Today's insight delves into the remarkable world of the TC275 Microcontroller, also affectionately known as the ShieldBuddy. This isn't your average, everyday Arduino-compatible board. Think of the ShieldBuddy as a Herculean contender in the arena of microcontrollers, a powerhouse adept at juggling multiple processes, particularly excelling at managing them concurrently.
This is the juncture where you should strap in and recline comfortably, as this guide promises to be an enlightening journey into the intricacies of multicore programming on the TC275 ShieldBuddy.
Have a look at the Pinout below
What you'll need:
- A PC with Windows Vista or later
- The Aurix free toolchain (Click Here!)
- Legacy Arduino IDE (Click Here!)
- The Arduino Development add-in for the Arduino IDE (Click Here!)
Unzip this to a temporary directory using the zip password “ShieldBuddy”. Run the installer and use the password “ShieldBuddy” to copy the IDE onto your PC.
Once you've installed all the necessary software (packages), you can use the ShieldBuddy basically as if it were any other Arduino board. The big difference is that the ShieldBuddy has three cores instead of just one. Each core can perform independent processes from what the other cores are performing: processor core 0 runs setup and loop, while cores 1 and 2 run setup1/setup2 and loop1/loop2. It's important to not try and use the same resource (for example output pin) of the ShieldBuddy with two different processor cores simultaneously, or else your program might not work properly. The three processor cores are pretty much the same, but cores 1 and 2 are a little bit faster (about 20%) than core 0. You can use the same programming language for all three cores.
CodeThe standard Arduino IDE has been extended to allow all 3 cores to be used. Anybody used to the default Arduino sketch might notice though that in addition to the familiar setup() and loop(), there is now a setup1(), loop1() and setup2(), loop2(). These new functions are for CPU cores 1 and 2 respectively. So while core 0 can be used as on any ordinary Arduino, you could now get to run three applications simultaneously.
void setup() {
/* Setup Function for Core 0 */
}
void loop() {
/* Loop Function for Core 0 */
}
void setup1() {
/* Setup Function for Core 1 */
}
void loop1() {
/* Loop Function for Core 1 */
}
void setup2() {
/* Setup Function for Core 2 */
}
void loop2() {
/* Loop Function for Core 2 */
}
Multicore Programming"With Great Power Comes Great Responsibilities" - Uncle BenIndeed the three cores exist individually, however, they all control a single memory space, so they are all able to access any address without restriction. This does however include all the peripherals and importantly all FLASH and RAM areas. (If you find this to be a bit vague, don't worry. You'll understand more in the following subsection.)
Using the Serial MonitorImagine three painters individually drawing a lovely painting on the same canvas. While they could perfectly draw at the same time if they use different pens and draw on different parts of the canvas, they might also need to coordinate the use of a sharpie from time to time, where an artist would wait until the other is finished using the sharpie so that they could use it.
Communicating with the serial monitor would be the same way here. If all three cores want to say something at the same time, they cannot just use a Serial.print() at the same time; some mechanism has to be implemented so that all three cores don't try to use the same serial port at the same time because if that happens, the characters will be eventually be serially printed but there is no guarantee that they will be sent in order.
This is why it is better to use the SerialASC.print() function in combination with a resource-blocking flag.
This is implemented for you already and you could just always copy the following code snippet.
while(Htx_LockResource(&SerialASC.PortInUse) == Htx_RESOURCE_BUSY){;}
SerialASC.print("WHATEVER TEXT YOU WANT TO SERIALLY SEND ;) ");
Htx_UnlockResource(&SerialASC.PortInUse);
This code snippet checks to see if any other core is using the serial port. If the port is in use it goes into this while loop and just waits until the port is not used anymore. As soon as the port is not used anymore, the Htx_LockResource(&SerialASC.PortInUse) function locks the serial port. So that this core could print whatever it wants.
The Htx_UnlockResource(&SerialASC.PortInUse); function unlocks the serial port so that the port can be used by other cores.
Using Interrupts to Coordinate and Communicate Between CoresAnother way to regulate the usage of resources is to trigger interrupts so that one core can explicitly convey a message to another. This means that core 0 can trigger an interrupt in say core 1. That interrupt might tell core 1 that a resource is now free or perhaps tell it to go and read a global variable that core 0 has just updated.
Defining the Interrupt Service Routine
The first step is to create the function that runs when the interrupt is triggered (also known as the Interrupt Service Routine or just ISR for short).
void nameOfChoiceOfISR(void)
{
SerialASC.print("\n\rHello from Core ");SerialASC.print(GetCpuCoreID());
}
Attaching the ISR to the interrupt
Attaching the previously created ISR to the corresponding core that it will be running on.
CreateCore0Interrupt(nameOfChoiceOfISR);
Triggering the Interrupt
Putting the function that triggers the interrupt within the code, so that the ISR runs on its corresponding core as soon as this function is called.
InterruptCore0();
Example of an interrupt being triggered
In the following code snippet, we create three ISR functions each of which runs on a different core. We then attach these ISRs to the core interrupts and then call on those interrupts every 100 ms within the loop of core 0.
void Core0IntService(void)
{
SerialASC.print("\n\rHello from Core ");SerialASC.print(GetCpuCoreID());
}
void Core1IntService(void)
{
SerialASC.print("\n\rHello from Core ");SerialASC.print(GetCpuCoreID());
}
void Core2IntService(void)
{
SerialASC.print("\n\rHello from Core ");SerialASC.print(GetCpuCoreID());
}
void setup() {
// put your setup code here, to run once:
SerialASC.begin(9600);
/* Create an interrupt in core 0 */
CreateCore0Interrupt(Core0IntService);
/* Create an interrupt in core 1 */
CreateCore1Interrupt(Core1IntService);
/* Create an interrupt in core 2 */
CreateCore2Interrupt(Core2IntService);
}
void loop() {
/* Trigger The interrupt service routine on Core 0 */
InterruptCore0();
delay(100);
/* Trigger The interrupt service routine on Core 1 */
InterruptCore1();
delay(100);
/* Trigger The interrupt service routine on Core 2 */
InterruptCore2();
delay(100);
}
void setup1() {
}
void loop1() {
}
void setup2() {
}
void loop2() {
}
Enabling and Disabling Interrupts
It is possible to disable all interrupts using:
noInterrupts();
This will also stop the delay() and other timer-related functions.
Interrupts can be re-enabled using:
interrupts();
Timers
The STM0 (system timer 0) is used to provide the basis for all the Arduino timing functions such as delay(), millis(), micros(), etc. The system timer has a 10ns tick time which means that every 10ns an internal interrupt is triggered and the timer advances by one tick. Of course, 10 ns is a very fast tick rate, but you could create your timer-based interrupt in each of the three cores.
For core 0 you are limited to one timer interrupt but for the remaining two cores you have up to two timer interrupts per core. Leaving you with a maximum of 5 timer interrupts.
When setting up the timer you'll have to decide:
- Type of Interrupt: the timer could either be continuously constantly running every tick period if it's a ContinuousTimerInterrupt. The timer could also be run once and never again through as a OneShotTimerInterrupt.
- Tick Period of Interrupt: since the smallest tick period is 10 ns, we use 10 ns as a unit. So if you want to run a timer interrupt that triggers after 50 microseconds, the input would be 5000.
- The function to be called back: the function that will be run by the specified core in the case of an interrupt running.
All these inputs would be entered in any of the following functions:
- For Core 0
CreateTimerInterrupt(TYPE_OF_TIMER, TICK_PERIOD_X_10NS, NAME_OF_CALLBACK_FUNCTION);
- For Core 1 Timer Interrupt 1
CreateTimerInterrupt0_Core1(TYPE_OF_TIMER, TICK_PERIOD_X_10NS, NAME_OF_CALLBACK_FUNCTION);
- For Core 1 Timer Interrupt 2
CreateTimerInterrupt1_Core1(TYPE_OF_TIMER, TICK_PERIOD_X_10NS, NAME_OF_CALLBACK_FUNCTION);
- For Core 2 Timer Interrupt 1
CreateTimerInterrupt0_Core2(TYPE_OF_TIMER, TICK_PERIOD_X_10NS, NAME_OF_CALLBACK_FUNCTION);
- For Core 2 Timer Interrupt 2
CreateTimerInterrupt0_Core2(TYPE_OF_TIMER, TICK_PERIOD_X_10NS, NAME_OF_CALLBACK_FUNCTION);
Example Code
The following code snippet runs a continuous triggering timer on core 0 and a one-shot timer on core 1. Where the interrupt that runs on core 0 prints "CONTINUOUS TIMER" on the screen while the other one prints "ONE SHOT TIMER".
void timerInterruptCore0(void)
{
SerialASC.print("CONTINUOUS TIMER\n");
}
void timerInterruptCore1(void)
{
SerialASC.print("ONE SHOT TIMER\n");
}
void setup() {
SerialASC.begin(9600);
CreateTimerInterrupt(ContinuousTimerInterrupt, 200000000, timerInterruptCore0);
}
void loop() {
}
void setup1() {
CreateTimerInterrupt0_Core1(OneShotTimerInterrupt, 500000000, timerInterruptCore1);
}
void loop1() {
}
void setup2() {
}
void loop2() {
}
As you can see in the screenshot above the continuous timer interrupt runs once every 2 seconds for as long as the microcontroller runs. The one-shot timer runs only once 5 seconds after the timer initialization.
Custom Pulse Width Modulation
Similar to the Arduino, the ShieldBuddy TC275 uses Pulse Width Modulation to generate analog voltages. The PWM frequency is only around 1kHz on the Arduino. The ShieldBuddy frequency is 390kHz when using an 8-bit resolution. While this is great for AC waveform generation, audio applications, etc., it can be too high for power devices used for motor control.
But have no fear if you want to make your ShieldBuddy board just like a "basic" Arduino, then just add
useArduinoPwmFreq();
to your code that this will set the PWM frequency to 1.5kHz so that the shields would work without a problem.
If you would like to use PWM with a frequency between 6Hz (minimum) and 390kHz (maximum), you can utilize the useCustomPwmFreq() function.
For example, the following snippet changes the PWM frequency to 4kHz
useCustomPwmFreq(4000);
That's all Folks!
Thanks for tuning in for this protip. You should now be capable of harnessing the power of your cores ;)
For more information on the TC275 Shieldbuddy feel free to check out the user manual by clicking here!
Comments