In this second part of the post we will cover threads and some important concepts about Parallelism and Concurrency.
Right to the code, remember to write this code on a separated sketch and to flash it to the sub-core 1, it will replace the code on that core, the main core will be unaffected.
#if (SUBCORE != 1)
#error "Select sub core 1 to upload the code (tools->SubCore1)"
#endif
/* --- The Spresense SDK for Multiprocessing and Mutex ---*/
#include <MP.h>
#include <MPMutex.h>
/* --- The NuttX level POSIX threads systems calls --- */
#include <pthread.h>
/* --- The Lock or Semaphore to avoid LED conflicts.
Note: This is done for educational purposes only, you
won't break the board if you access the LEDs at the same
time --- */
MPMutex ledLock(MP_MUTEX_ID0);
/* --- A function per thread, sorry for the cut and paste --- */
static void L0_thread(void *arg)
{
while(1)
{
int e= 0;
/* --- First acquire the Lock on the leds, the return code
will be 0 on success and -1 on failure --- */
e= ledLock.Trylock();
MPLog( "Thread 0 Lock Return Code: %d\n", e);
if (!e )
{
digitalWrite(LED0, HIGH);
sleep(1);
digitalWrite(LED0, LOW);
sleep(1);
/* -- Once the job is done release the lock -- */
ledLock.Unlock();
}
sleep(1);
}
}
static void L1_thread(void *arg)
{
while(1)
{
int e= 0;
e= ledLock.Trylock();
MPLog( "Thread 1 Lock Return Code: %d\n", e );
if ( !e )
{
digitalWrite(LED1, HIGH);
sleep(1);
digitalWrite(LED1, LOW);
sleep(1);
ledLock.Unlock();
}
sleep(1);
}
}
static void L2_thread(void *arg)
{
while(1)
{
int e= 0;
e= ledLock.Trylock();
MPLog ("Thread 2 Lock Return: %d\n", e);
if ( !e )
{
digitalWrite(LED2, HIGH);
sleep(1);
digitalWrite(LED2, LOW);
sleep(1);
ledLock.Unlock();
}
sleep(1);
}
}
static void L3_thread(void *arg)
{
int e= 0;
while( 1 )
{
e= ledLock.Trylock();
MPLog ("Thread 3 Lock Return: %d\n", e);
if (!e )
{
digitalWrite(LED3, HIGH);
sleep(1);
digitalWrite(LED3, LOW);
sleep(1);
ledLock.Unlock();
}
sleep( 1 );
}
}
void setup()
{
int e= 0;
/* --- Sub-core 1 starts executing --- */
MP.begin();
/* --- LED pins set as output --- */
pinMode(LED0, OUTPUT);
pinMode(LED1, OUTPUT);
pinMode(LED2, OUTPUT);
pinMode(LED3, OUTPUT);
/* --- A precautionary wait --- */
sleep(2);
/* --- Just in case the main core stopped the sub-core while
a lock is taken --- */
MPLog( "Releasing Locks...\n" );
ledLock.Unlock();
/* --- The thread handlers --- */
pthread_t thr0, thr1, thr2, thr3;
/* --- Threads will start running as they are created --- */
e= pthread_create(&thr0, NULL, L0_thread, NULL);
if ( !e ) MPLog ("Led 1 Thread Ok\n" );
e= pthread_create(&thr1, NULL, L1_thread, NULL);
if ( !e ) MPLog ("Led 2 Thread Ok\n" );
e= pthread_create(&thr2, NULL, L2_thread, NULL);
if ( !e ) MPLog ("Led 3 Thread Ok\n" );
e= pthread_create(&thr3, NULL, L3_thread, NULL);
if ( !e ) MPLog ("Led 4 Thread Ok\n" );
}
void loop()
{
MPLog( "Sub-core 1 Running...\n");
/* --- While the sub-core is idle the threads will be
turning LEDs on and off --- */
sleep(5);
}
Well, this code is a fancy version of the classic led blink, a kind of hello world! C code for embedded code. While the outcome of this code is pretty useless you will learn some really important things.
What is parallelism.
What is concurrency.
When to use each.
How to create threads and how they work.
How to prevent clashes.
You will see how predictable the NuttX RTOS is.
The moment you flash this code to the sub-core 1 (remember to create a new file, select sub-core 1 from the tools menu option and flash it) the Spresense will run the code on the main core and the sub-core 1 in parallel, that means, each core will do its job independently (for now). If you go to the loop function you will see it does almost nothing, it just prints a message and sleeps for a while.
Now if you look at the Spresense board you will notice that its LEDs will be turned ON and OFF in sequence, that happens because we have created 4 threads that run in the background. This last thing in Concurrency, code that will start to run when they have the chance to do it, that means, when the Micro-controller is doing nothing, threads will compete for MCU time and the thread function won't run from beginning to end when it has the focus, the Operating System can take away the focus on a thread to move to another one (you will see that in action the moment you analyze the log in the terminal console). Now, I am describing an almost chaotic scenario but the behavior of the LEDs is not chaotic at all, on the contrary they behave in a predictable way, that is because we have added some code to synchronize threads. Another key concept you can prove with this example is that the underlying operating system is deterministic, that means, things happen when they are supposed to happen. On a non-real time operating systems you have a lot processes (such as memory managers) that could cause unexpected delays, well, this is not the case with the NuttX.
Parallelism and Concurrency are good for different things. Suppose that you and a friend want to paint a house, you could be painting one wall and your friend could be painting another wall at the same time, that is parallelism. Now, if you and your friend want to fix a car engine, then while your friend opens the hood you could jump into the engine to remove some screws, then, your friend will remove a part while you keep working on other parts of the engine, that is concurrency, to do some work when you have the chance to do it. Sometimes you have to wait for your friend to finish something before you can continue, what your friend is doing then is called a critical session, nothing can interrupt a critical session.
The Code
Putting humans aside, and getting back to computers; the first thing to notice is that three header files are included now, the MP.h for the multi-core handling and two new, MPMutex.h and pthread.h (The POSIX way of handling threads, you could use the Spresense SDK MPTask() instead but I wanted to make a point here about code portability). The next line of code will define the lock or semaphore we are going to use. Semaphores are implemented as pseudo file, so, you cannot abuse in the use of semaphores because they are a limited resource, in this case, we are using the MP_MUTEX_ID0 (if you open the MPMutex.h file you will see the available locks you can use).
Then we will define a function for each thread, this is just an example, you will have to excuse me for the cut and paste abuse, all the functions will do the same thing on a different LED. The function will execute a loop and it will try to get access to the lock, if it can acquire the lock, it will turn the LED ON and OFF, if it can't get the lock, it will wait and try again next time the threat gains the attention of the scheduler (the part on the OS that selects which code should be executed next)
In this case, all the important things happen in the setup() function, the first lines enable the sub-core and set the LED pins as output. The next lines will give you an idea about the mentality you need in order to write concurrent and parallel code. In this context you have to write code that could be gracefully stopped and re-started any second because you don't have the control over when the code is being executed or interrupted. Taking into account that we are using semaphores (remember that you take them and release them) it could happen that the main core could stop the sub-core while one of its threads has a lock taken, when the sub-core is stopped, the lock will remain taken, so, the first thing you will do when the sub-core is started is to release the lock as a precaution. If you remove this line you will notice that at some point the LEDs will stop blinking, that is because every attempt to get the lock will fail.
The following lines will declare the thread handlers and create the 4 threads, the moment you call the pthread_create function the thread will start running. As you can see, we created 4 threads, each one using a different function. Finally, the main loop will do, again, almost nothing because all the action is being handled by the threads. Once you upload this code to the sub-core 1 you will have two cores running in parallel (both printing messages on the console) and 4 threads will be running concurrently in the sub-core 1.
When you have this code in place, you will notice that the LEDs will blink in a predictable sequence, that happens because:
We didn't set any thread priority.
Each thread will lock the LED usage, so, only one at a time will have access to them.
Every single LED will be activated, that means that the round robin mechanism is working fine and that the NuttX is quite predictable and no threat will lose its chance to run, that happens because you don't have delays at the moment the core passes the control to a thread, this is what an RTOS does.
In the following posts I will start building from here, adding inter-core communication, more advanced thread features and of course, we will be doing some useful things such as using the camera and analyzing images using this scheme.
But before we move on, play a little bit with this code and try to understand why things happen the way they happen, for instance, open the terminal console to see which threat is able to acquire the lock and why, change the sleep times on the thread function (some threads could lose their chance to execute while sleeping) remove the unlock on the setup function and even remove the lines that acquire the locks, in this later scenario, you should see all the LEDs being turned ON and OFF almost at the same time. With the provided code, your terminal window should look like this:
As you can see, you can achieve really powerful and reactive devices using these APIs and Boards. In addition, as you will see later, you can add interrupt handlers (to react to an external stimulus), running the MCU at different clock speeds to achieve power efficiency and many more things provided by other boards such as communications (we will stick to LoRa Mesh), so, the sky is not the limit and you never gonna guess what is, a nice The Strokes phrase.
I would also like to say that I would like to upload this kind of posts more often but it is really complicated to me to find the time to run my project and share this information with you, but believe me when I say I am doing my best.
Next topics to be covered, more on power management, AI at the edge, sensors and boards to achieve devices capable of gathering complex information (such as pictures, sounds, 3D Mappings, understanding information and communicating conclusions to other devices, all running on batteries being fed by solar power. We have a long way ahead of us. Stay tuned and sorry again for the delay(Y);
Comentarios