This is a C language tutorial for the 8-core Propeller microcontroller. It features the Propeller Activity Board (original or WX version), but other Propeller development boards will work.
This tutorial shows you how to use more than one of the Propeller microcontroller’s eight processors (called cores or cogs) at a time. You will learn how to write a function specifically so it can be launched into another core.
Library Alert! These lessons require Learn folder 2014-5-14 or later.
You will be able to write functions to run in other cores. You will also know how to pass data between processes running in different cores. You can use these techniques for application code, or for writing your own libraries.
The Propeller chip has eight processors, called cores or cogs, numbered 0 through 7. In all of our example programs, the main function automatically launches into cog 0. Many Propeller C libraries automatically launch other cogs to handle tasks for you, behind the scenes. You can also write your own function and launch it to run in another cog using cog_run.
The example below shows the simplest use of the cog_run function. Here, the main routine running in cog 0 executes the cog_run function call, causing the blink function to run in cog 1.
The cog_run function itself needs two parameters:
While learning or prototyping in multicore you should always set more stack space aside than you think you will be using. In this example we use a stackSize of 128, but our small program doesn’t actually use it. This way, we have room to grow our code without worrying about running out of memory.
A function launched with cog_run:
So, let’s try it!
We’ll use the Activity Board’s (original or WX version) built-in LEDs on P26 and P27. See the Blink a Light tutorial for a schematic if you are using a different board.
After including the simpletools.h library, there is a forward declaration for the blink function. Since the blink function definition is below the main function, this lets the compiler know to expect it.
#include "simpletools.h" // Library include void blink(); // Forward declaration
Inside the main routine is just one statement: cog_run(blink, 128). This launches the function defined at address blink. It also provides a stack space of 128 32-bit memory locations used for performing calculations while executing the instructions in the blink code block.
int main() // Main function { cog_run(blink, 128); // Run blink in other cog }
Below the main routine is the blink function itself, written to be launched into another cog without any fuss. Note that it has an empty parameter list and void return type. Its code block is entirely inside a while(1) loop, so it will keep on blinking that P26 LED until the power shuts off. If those four instructions were not in a loop, they would execute just once and the cog would shut down, but without releasing its resources. This would make the cog and its stack space unavailable for other uses afterward.
void blink() // Blink function for other cog { while(1) // Endless loop for other cog { high(26); // P26 LED on pause(100); // ...for 0.1 seconds low(26); // P26 LED off pause(100); // ...for 0.1 seconds } }
Stack Size – how much? 10 is the bare minimum value you would want to use for the stackSize parameter. If you were to add more instructions to the blink function’s code block, you would need to increase it. Add 1 for every local variable used, 2 for each function called, and 1 for each parameter and each return value used by the functions called.
Print from one cog at a time, please! Print, scan and other functions that interact with the SimpleIDE Terminal can only run in one cog at a time. Usually, it is most convenient to call print and other such functions from the main routine, even if they need to print values updated by other cogs, as we do in this example. But, if you want to, you can Print from a Different Cog as well, as that lesson will show you.
Let’s expand the example program so that it blinks the P27 LED at a different rate, using another cog. The process is fairly simple: just add a second function and pass it to cog_run. You will need a second forward declaration for that function, and also a second cog pointer for shutting the cog back down.
Okay, so blinking two LEDs doesn’t seem so amazing. Yet, notice that one LED blinks on/off every 100 ms, and the other blinks on/off every 223 ms. Now, can you imagine writing code to do that from a single processor? Imagine that the on times were different from the off times for each LED. And then imagine adding six more LEDs, each with unique on and off times. The math would get complicated very quickly. This example is simple, but it shows how multi-processing can make a group of timing-sensitive tasks easier to do.
Just to prove that three cogs are really running different processes at the same time, let’s have the original cog do something more than launch the other two cogs.
The last lesson showed you how to launch a function in another core with cog_run. Now, let’s use its counterpart cog_end to stop the launched function.
We’ll keep using the Activity Board’s (original or WX version) built-in LEDs on P26 and P27. See the Blink a Light tutorial for a schematic if you are using a different board.
First, the simpletools library is included to give us access to cog_run and cog_end, along with other functions. Next is the forward declaration for the blink function, which is defined below the main routine.
/* Cog End Example.c */ #include "simpletools.h" // Library include void blink(); // Forward declaration
The cog_run function provides a return value. In this example, a local variable is declared with int *cog = in front of cog_run(&blink, 128); to capture it. The asterisk * indicates cog is a pointer, meaning that the value it holds is actually the address of a memory location. This is the place where the cog_run function call set aside stack space and recorded the ID number of the core it launched.
int main() // Main function { int *cog = cog_run(blink, 128); // Run blink in other cog
Next, pause(3000) makes the main routine idle for 3 seconds. Meanwhile, the P26 LED is blinking since the blink function is being executed by another core. Next comes the function call cog_end(cog); The cog_end function’s parameter uses the cog_run’s return value cog. Remember that cog is the memory address that stores the ID number of the core that was launched. So, cog_end can use the address get the ID number and shut the cog down.
pause(3000); // Wait while other cog blinks LED cog_end(cog); // Stop other cog }
The blink function itself is below the main routine. Even though its code block is inside a while(1) loop, it is not executed infinitely. That is because the core running blink was shut down by the cog_end function call made by the core running the main routine.
void blink() // Blink function for other cog { while(1) // Endless loop for other cog { high(26); // P26 LED on pause(100); // ...for 0.1 seconds low(26); // P26 LED off pause(100); // ...for 0.1 seconds } }
Always Recycle your Cores! — Always use cog_end to shut down a function that is no longer needed, whether or not it is using a while(1) loop. This frees up the core and stack space for re-use by another cog_run function call later in the program, and prevents unexpected results.
Keep it local, save a few bytes — The technique above, where *cog is declared locally and used by cog_end within the same function (in this case the main function), uses fewer bytes than declaring *cog globally.
Go global and delegate — Delcaring *cog globally allows it to be used with cog_end by any function in the application, including those running in other cores. It costs a few bytes more of code size, but frees the main routine from the task (a great advantage of multiprocessing).
As mentioned above, the *cog pointer can be declared globally, allowing it to be used with cog_end by any function in the application. You can put cog_end right inside the blink function, so the core running that code can shut itself down.
So far, we have tried shutting down a core from the main routine, and having a core shut itself down. Now, let’s launch two cores, and have one shut down both itself and the other.
Did it work? Hint: if your P27 LED didn’t stop blinking, take a look at the two cog_end calls in your blink function. Which one is executed first?
The Functions lesson Memory Functions Can Share showed you how to make functions share information with global variables instead of with parameters. Code running in different cogs can use this technique to exchange information as well. There’s just one extra detail: when you declare global variables for cores to share, you have to precede each one with the keyword volatile. So instead of int globalVar, the declaration would be volatile int globalVar.
This example program declares a global volatile variable named t, that allows the main function to control the blink function’s LED on/off rate. The main function sets the value of t, and then the blink function uses t to set its pause times between high/low instructions.
We’ll keep using the Activity Board’s (original or WX version) built-in LEDs on P26 and P27. See the Blink a Light tutorial for a schematic if you are using a different board.
/* Cog Info Exchange.c Example of two cogs exchanging information with a volatile global variable. The main function in cog 0 changes the value; and it affects the blink function's rate running in cog 1. https://learn.parallax.com/propeller-c-multicore-approaches */ #include "simpletools.h" // Library include void blink(); // Forward declaration int *cog; // For storing process ID volatile int dt; // Declare dt for both cogs int main() // Main function { dt = 100; // Set value of dt to 100 cog = cog_run(blink, 128); // Run blink in other cog pause(2000); // Let run for 2 s dt = 50; // Update value of dt pause(2000); // New rate for 2 s cog_end(cog); // Stop the cog } void blink() // Function for other cog { while(1) // Endless loop { high(26); // LED on pause(dt); // ...for dt ms low(26); // LED off pause(dt); // ...for dt ms } }
The program begins with three declarations. The first gives us access to the simpletools library functions, including cog_run and cog_end. The second is the forward declaration void blink(); for the blink function defined below the main routine. The third declares a global volatile int variable dt. Since it is declared outside of any functions it is global, and therefore available to all of the functions in the program. Since it is volatile, it can be used by functions running in different cores with confidence.
#include "simpletools.h" // Library include void blink(); // Forward declaration volatile int dt; // Declare dt for both cogs
First thing inside the main routine, dt is initialized to 100. Next, a call to cog_run launches the blink function in another cog. The blink function uses dt in the parameter of its pause functions to set the blink rate of the P26 LED. The call pause(2000) in the main routine means nothing else happens in that core for 2 seconds, while another core blinks the P26 LED on/off every 100 milliseconds.
int main() // Main function { dt = 100; // Set value of dt to 100 int *cog = cog_run(blink, 128); // Run blink in other cog pause(2000); // Let run for 2 s
The next bit of code lets the main routine change what’s happening in the other cog. First, the variable dt is assigned a new value of 50. Since dt is a volatile global variable, the blink function running in that other core is affected immediately — its pause(100) calls become pause(50) calls and the P26 LED starts blinking twice as fast. After another 2-second pause, the main routine shuts down the core running the blink function with cog_end(cog).
t = 50; // Update value of t pause(2000); // New rate for 2 s cog_end(cog); // Stop the cog
Below the main routine is the code for the blink function that was launched into the other core. It is simply an infinite while(1) loop that sets P26 high and low alternately every dt milliseconds, to blink the LED.
void blink() // Function for other cog { while(1) // Endless loop { high(26); // LED on pause(dt); // ...for dt ms low(26); // LED off pause(dt); // ...for dt ms } }
volatile – this keyword tells the C compiler not to perform any optimizations on statements with that variable. Optimizations are actions the compiler can take to make code execute faster and/or take less memory by removing “unused” code. For example, the compiler might see a function that just prints a variable without performing other operations on it. Since the function doesn’t change the variable’s value, the compiler might remove code to re-check the value before printing. But, if another function in another core also updates that variable, the wrong value may be used if it is not re-checked before printing. The volatile keyword prevents such unintended consequences.
The last example let the main routine’s core control the behavior of a function in another core. A core can also monitor another core’s behavior with a variable. Here’s an example that monitors how many times the blink function turned the LED on/off.
With these changes, the main function monitors and displays the value of reps from one core, while the blink function updates the value of reps from another core.
Let’s blink P27 in a third core, but at a rate of dt*2. We’ll display its accumulated blink repetitions as well.
If all goes well, your SimpleIDE Terminal output should look something like the image below.
Up to now, all of our example programs that use print statements have done so from the main routine, which starts executing in cog 0. This quick lesson shows you how to print from different cores.
The trick is that only one core at a time can use the SimpleIDE Terminal connection, and must be closed before being opened in another. Sort of like passing the talking stick or speaker’s staff from one person in the circle to another at a meeting.
On the Activity Board (original or WX version), the Propeller chip’s P30 and P31 pins are pre-wired to the USB port for programming and communication with the SimpleIDE Terminal. Any setup with these connections will work.
The example program prints two messages from two different cores.
The program includes the simpletools.h library, which also makes the simpletext library’s functions available, including print, simpleterm_close, and simpleterm_open.
#include "simpletools.h" // Library include void other(); // Forward declaration int *otherCog; // Global pointer for cog_run return
The main routine, which is running in cog 0, begins with a print statement that displays the text string in the SimpleIDE Terminal. Next, simpleterm_close releases the terminal connection for this core, so it can be opened by a different core. After that comes the cog_run function call to launch the other function in a new core, and the return value is stored in otherCog.
int main() // Main function { print("Cog 0 has the Talking Stick first... \n"); simpleterm_close(); // Close SimpleIDE Terminal for this core otherCog = cog_run(other, 128); // Run other function in another core }
Last comes the other function definition and its code block; this is the code running in a different core. The first instruction opens the SimpleIDE Terminal for this core, so it can display the string in the print statement that follows. After printing, the next line closes the SimpleIDE Terminal. The last line uses cog_end and the otherCog pointer to shut down the core.
void other() // other function definition { simpleterm_open(); // Open SimpleIDE Terminal for this core print("...and now the other cog has it. \n"); simpleterm_close(); // Close SimpleIDE Terminal cog_end(otherCog); // Shut down this core }
Printing Values Takes Stack Space — Printing a simple text string from another core doesn’t require much stack space. You could reduce the 128 in the example above all the way down to 10, and it might still run . But, if you want your launched core to check the value of global variables, use them in operations with local variables, and print the results, you are going to need an adequate stack! We reccommend 128 as a minimum. If your custom print statements do not appear with the as desired, try increasing the stack space in that core’s cog_run function call.
Making the Serial Connection Takes Time — If you find that not all of your print statements are showing up as they should, try adding pause(100); right after each instance of simpleterm_open. Your particular computer might need a moment to make the serial terminal connection.
Not Just print! See simpletext.h — There are many other functions in this library that interact with the SimpleIDE Terminal. So, like print, they can be used from one core at a time, after closing and opening the terminal as shown in this lesson. These include printi, scan, scani, and anything starting with put or get. See the library’s documentation for details.
Let’s expand this example program to print from a third core. First, we’ll need to add another function (let’s just name it “another”) with its own print statement. Then, we can launch it from the other function; in effect passing the Talking Stick.
Here’s the SimpleIDE Terminal output:
Let’s have our three cores access and update a global variable, perform an operation on it, and print the updated value. You will need to:
Here’s an example with a global variable initialized to 0 and increased by one in each core.