Multi Core Contention Prevention

RP2040 has Dual Cortex M0+ processor cores which can run upto 133MHz independent of each other. However, the second core (core 1) is asleep on boot and needs to be waken up by a function call from the first core (core 0). This program demonstrates the prevention of contention between core 0 and core 1 for a shared memory space using spin locks. The demonstration of contention can be found here. The resources for the project include the C SDK User Guide, the RP2040 Datasheet and Prof. Hunter's website.


The complete code

/*
 * Parth Sarthi Sharma (pss242@cornell.edu)
 * Code based on examples from Raspberry Pi Foundation.
 * This code is a demonstration of contention prevention between the
 * two cores. The code wakes up core 1 from its slumber and increments a
 * common variable on each core using the spin lock mechanism to prevent contention.
 */
#include <stdio.h> //The standard C library
#include "pico/stdlib.h" //Standard library for Pico
#include "pico/time.h" //The pico time library
#include "pico/multicore.h" //The pico multicore library
#include "hardware/gpio.h" //The hardware GPIO library
#include "hardware/timer.h" //The hardware timer library
#include "hardware/sync.h" //The hardware sync library

#define BUTTON 5 //The pushbutton

int spinNum; //The spin lock number
spin_lock_t *spinLock; //The spinlock object that will be associated with spinNum

int i = 0; //The number to be incremented
int core0 = 0, core1 = 0; //Counters for each core

void core1_entry() { //The program running on core 1
    while(1){ //While eternity
        spin_lock_unsafe_blocking(spinLock); //Acquire the spin lock without disabling interrupts
        if(i < 100000000){ //If i is less than 100000000
            i++; //Increment i
            core1++; //Increment core 1 
        }
        spin_unlock_unsafe(spinLock); //Release the spin lock without re-enabling interrupts
    }
}

int main(){ //The program running on core 0
    stdio_init_all(); //Initialize all of the present standard stdio types that are linked into the binary

    gpio_init(BUTTON); //Initialize the pushbutton pin
    gpio_set_dir(BUTTON, GPIO_IN); //Initialize the pushbutton pin to be input

    spinNum = spin_lock_claim_unused(true); //Claim a free spin lock. If true the function will panic if none are available
    spinLock = spin_lock_init(spinNum); //Initialise a spin lock

    multicore_launch_core1(core1_entry); //Reset core1 and enter the core1_entry function on core 1 using the default core 1 stack

    uint64_t startTime = get_absolute_time(); //Fetch the time at which the CPU starts executing the program
    printf("Start Time: %lld.\n", startTime); //Print out the time start time
    while(1){ //While eternity
        spin_lock_unsafe_blocking(spinLock); //Acquire the spin lock without disabling interrupts
        if(i < 100000000){ //As long as i is less than 100000000
            i++; //Increment i
            core0++; //Increment core 0
        }
        else if(i == 100000000){ //As soon as i hits 100000000
            uint64_t timeTaken = get_absolute_time() - startTime; //Get the time it took to finish the job
            printf("Time Taken: %lld.\n", timeTaken); //Print out the time it took to finish the job            
            printf("Core0: %d, core1: %d\n", core0, core1); //Print out the number of increments on core 0 and core 1
            i++; //Increment i to stop printing
        }
        else if(gpio_get(BUTTON)){ //If the button has been pressed
            i = 0; //Reset i
            core0 = 0; //Reset core 0 counter
            core1 = 0; //Reset core 1 counter
            startTime = get_absolute_time(); //Reset the start time
            printf("Start Time: %lld.\n", startTime); //Print out the new time
        }
        spin_unlock_unsafe(spinLock); //Release the spin lock without re-enabling interrupts
    }
}


Stepping through the code

Includes

The first lines of code in the C source file include some header files. One of these is standard C headers (stdio.h) and the others are headers which come from the C SDK for the Raspberry Pi Pico. The first of these, pico/stdlib.h is what the SDK calls a "High-Level API." These high-level API's "provide higher level functionality that isn’t hardware related or provides a richer set of functionality above the basic hardware interfaces." The architecture of this SDK is described at length in the SDK manual. All libraries within the SDK are INTERFACE libraries.

The next includes pull in hardware APIs which are not already brought in by pico/stdlib.h. These include hardware/gpio.h, hardware/timer.h, hardware/sync.h, pico/time.h and pico/multicore.h. As the names suggest, these interface libraries give us access to the API's associated with the hardware GPIO, hardware timer, hardware sync, pico time and pico multicore on the RP2040.

Don't forget to link these in the CMakeLists.txt file!

#include <stdio.h>
#include "pico/stdlib.h"
#include "pico/time.h"
#include "pico/multicore.h"
#include "hardware/gpio.h"
#include "hardware/timer.h"
#include "hardware/sync.h"


Global declarations and defines

The next section of the code is the #define's and the global variables which will be used throughout the code. The #define is the pushbutton pin declaration on GPIO 5. The variables declared include a spin lock number and a spin lock identifier. The next variables that are declared are the increment variable i and the tracker variables core0 and core1. The increment variable i is shared by both cores as it is declared in the global memory space.

#define BUTTON 5

int spinNum;
spin_lock_t *spinLock;

int i = 0;
int core0 = 0, core1 = 0;


Core 1 function

The core 1 function is the function which runs on the core 1 once it wakes up from its slumber. In other terms, this function is the main() function for core 1 and runs independent of the actual main() function running on core 0 (unless there is an intra-core communication). The core1_entry() function grabs the spin lock (if it is unlocked) and checks if i is less than 100000000. If it is, it increments both i and core1 and releases the spinlock. It then proceeds to repeat the process for eternity. As long as the spin lock is acquired by core 1, core 0 will not be able to acquire it.

Additional information

As per Wikipedia: A spinlock is a lock which causes a thread trying to acquire it to simply wait in a loop ("spin") while repeatedly checking if the lock is available. Since the thread remains active but is not performing a useful task, the use of such a lock is a kind of busy waiting. Once acquired, spinlocks will usually be held until they are explicitly released, although in some implementations they may be automatically released if the thread being waited on (the one which holds the lock) blocks, or "goes to sleep".

void core1_entry() {
    while(1){
        spin_lock_unsafe_blocking(spinLock);
        if(i < 100000000){
            i++;
            core1++;
        }
        spin_unlock_unsafe(spinLock);
    }
}


The main function

Initializing communication

The first line in main() is a call to stdio_init_all(). This function initializes stdio to communicate through either UART or USB, depending on the configurations in the CMakeLists.txt file.

stdio_init_all();


GPIO initialization and configuration

In the next 2 lines of the code, I initialized the button pin and configured it to be the input pin. The gpio_init() function is used to initialize the pin and the gpio_set_dir() function us used to set the pin direction which can be GPIO_OUT (output) or GPIO_IN (input).

gpio_init(BUTTON);
gpio_set_dir(BUTTON, GPIO_IN);


Spin lock initialization

The next line of code claims an unused spin lock using the spin_lock_claim_unused() function. This function takes a boolean value as an argument. If the argument is true, the function will panic if no spin locks are available. Then, this spin lock is initialized using the spin_lock_init() function.

spinNum = spin_lock_claim_unused(true);
spinLock = spin_lock_init(spinNum);


Waking up core 1 from its sleep

In order to wake up the core 1 from sleep, I used the multicore_launch_core1() function. This function resets core 1 and enters the given function on core 1 using the default core 1 stack (below core 0 stack).

multicore_launch_core1(core1_entry);


The infinite while loop

This part of the program is quite similar to the core1_entry() function but has some additional functionality. It runs on the core 0 and follows the following algorithm:

uint64_t startTime = get_absolute_time();
printf("Start Time: %lld.\n", startTime);
while(1){
    spin_lock_unsafe_blocking(spinLock);
    if(i < 100000000){
        i++;
        core0++;
    }
    else if(i == 100000000){
        uint64_t timeTaken = get_absolute_time() - startTime;
        printf("Time Taken: %lld.\n", timeTaken);       
        printf("Core0: %d, core1: %d\n", core0, core1);
        i++;
    }
    else if(gpio_get(BUTTON)){
        i = 0;
        core0 = 0;
        core1 = 0;
        startTime = get_absolute_time();
        printf("Start Time: %lld.\n", startTime);
    }
    spin_unlock_unsafe(spinLock);
}


The output

In order to view the output, I used the serial monitor provided by the Arduino IDE. As it is quite evident from the provided data, implementing the spin lock avoids the contention for shared memory between both cores and allows safe access of data.

Output of the Spin Lock Mechanism


CMakeLists.txt

cmake_minimum_required(VERSION 3.13)

include(pico_sdk_import.cmake)

project(MultiCoreCommonVarPerformaceCompDualCoreSpinLock)

pico_sdk_init()

add_executable(MultiCoreCommonVarPerformaceCompDualCoreSpinLock MultiCoreCommonVarPerformaceCompDualCoreSpinLock.c)

pico_enable_stdio_usb(MultiCoreCommonVarPerformaceCompDualCoreSpinLock 1)
pico_enable_stdio_uart(MultiCoreCommonVarPerformaceCompDualCoreSpinLock 1)

pico_add_extra_outputs(MultiCoreCommonVarPerformaceCompDualCoreSpinLock)

target_link_libraries(MultiCoreCommonVarPerformaceCompDualCoreSpinLock pico_stdlib pico_time pico_multicore hardware_gpio hardware_sync)