FreeRTOS has many conventional tools like semaphores and queues available to it, but it also has an extremely powerful unconventional tool as well, notifications.
Notifications were designed as a low level way to synchronize tasks. because of that, they are very fast and allow you to synchronize tasks in a lot of different ways.
There are two interfaces that you can use with notifications, so below I have prepared two simple examples about how to use each.
Here is the first example
TaskHandle_t address = NULL;
void send(void* arg){
while(true){
xTaskNotifyGive(address);
delay(800);
}
}
void receive(void* arg){
while(true){
ulTaskNotifyTake(true,portMAX_DELAY);
Serial.println("GOT MESSAGE");
}
}
void setup() {
Serial.begin(115200);
xTaskCreate(receive,"RECEIVE",2048,NULL,1,&address);
xTaskCreate(send,"SEND",2048,NULL,1,NULL);
}
void loop() {
}
How It Works
This example uses the simpler interface which I prefer, so we will start with it. Why don’t we quickly go through it and see how it works.
The Address
One of the big differences between Notifications and Semaphores, Mutexes, etc. is that when you send a notification you have to tell it the address of the task you are sending it to.
This address is actually the handler of the task, so for now we need to create a variable to hold that address like this.
TaskHandle_t address = NULL;
Of course, we can’t actually set the address until we create the task, so we will have to wait for now.
The Sender Function
Next, we are going to create a function for the task that sends the notifications, and we will start it with an infinite loop and a delay.
void send(void* arg){
while(true){
delay(800);
}
}
The actual line of code that send the notification is also pretty simple.
void send(void* arg){
while(true){
xTaskNotifyGive(address);
delay(800);
}
}
Receiving Notifications
The receiver task also starts out pretty simple.
void receive(void* arg){
while(true){
}
}
Then, we use the xTaskNotifyTake() function to halt the task until it receives a notification. This function takes two arguments, ClearCount and TicksToWait.
TicksToWait is pretty standard, Its just the maximum amount of time the function should wait for a notification, but ClearCount is a little more interesting.
First of all, here is what this command looks like when we add it in. I have also added a Serial.println() function to tell the user when it receives a notification.
void receive(void* arg){
while(true){
ulTaskNotifyTake(true,portMAX_DELAY);
Serial.println("GOT MESSAGE");
}
}
Now back to ClearCount, above you can see that I set it to true. Here is a quick table that shows you what ClearCount does.
False | Makes the notification act like a counting semaphore, allows you to GIVE multiple times in a row and then TAKE the same number of GIVES. |
True | Makes the notification act like a binary semaphore, allows you to GIVE multiple times in a row and then TAKE only once |
Order Matters
The last thing we need to do is turn those functions into tasks, and we will also need to update the address variable.
The order in which you create the tasks is extremly important. When I was making this code I made the mistake of creating the sender first, but the program kept crashing.
The reason the order matters is that, when you create the sender task it will immediately run, and if the receiver does not exist yet, when it tries to send a notification the whole computer will crash, so you have to create the receiver first.
void setup() {
Serial.begin(115200);
xTaskCreate(receive,"RECEIVE",2048,NULL,1,&address);
xTaskCreate(send,"SEND",2048,NULL,1,NULL);
}
The Second Example
Besides the simple mechanisms above, notifications also have a slightly more complex way to send a message that also has a 32-bit number sent with it, so here is the next example.
TaskHandle_t receiver = NULL;
void send(void* arg){
while(true){
unsigned int val = random(0,10);
Serial.printf("Sending Number: %i \n",val);
xTaskNotify(receiver,val,eSetValueWithOverwrite);
delay(800);
}
}
void receive(void* arg){
while(true){
uint32_t msg = 0;
xTaskNotifyWait(0,0,&msg,portMAX_DELAY);
Serial.printf("Got Number: %i \n",msg);
}
}
void setup() {
Serial.begin(115200);
xTaskCreate(receive,"RECEIVE",2048,NULL,1,&receiver);
xTaskCreate(send,"SEND",2048,NULL,1,NULL);
}
void loop() {
}
Because this example is so similar to the last, I will just explain the two areas that have changed.
Sending
The sending code has changed as follows.
unsigned int val = random(0,10);
Serial.printf("Sending Number: %i \n",val);
xTaskNotify(receiver,val,eSetValueWithOverwrite);
This example uses the xTaskNotify() function. The main advantage of using this function is that not only can you send a notification, but you can also directly manipulate the 32-bit number that is sent with it.
Of course to send a number, we first need to create one. For this example, we will just randomly generate one.
unsigned int val = random(0,10);
And then,it sends it with this.
xTaskNotify(receiver,val,eSetValueWithOverwrite);
If you look at the code above you will see eSetValueWithOverwrite, this controls what the notification does with the value. Besides overwrite, there are four other options which you can learn about at the notification documentation.
Receiving
Here is where we changed the receiving code.
uint32_t msg = 0;
xTaskNotifyWait(0,0,&msg,portMAX_DELAY);
Serial.printf("Got Number: %i \n",msg);
The first thing we do is create a variable to hold the incoming value from the notification.
uint32_t msg = 0;
Then, we use the xTaskNotifyWait() function. This function takes four arguments the first two are a bit complicated, so I will let the documentation explain them to you, but you can set them to zero if you don’t care about them.
The last two arguments are really easy, you just pass it the variable we made, so it can set it to the correct value, and after that you pass it the maximum time it should wait for a notification. portMAX_DELAY = forever
Serial.printf("Got Number: %i \n",msg);
The Downside to Notifications
While Notifications are extremely powerful and are a great tool to have around when working with microcontrollers, they have one main downside, complexity.
Admittedly, it’s not that hard to create a single notification, but because you have to store the address of your tasks and then determine which address to use later, It can be very hard to manage a large number of notifications.
If you are interested in some of FreeRTOS’s other powerful protocols, you might like to see how a mutex works.