Friday, June 2, 2017

FreeRTOS 9.0.0 on ArduinoMEGA - Full Demo - Task Notifications

After the Demo Blinky and Demo AVR323 I'm feeling more confident in this port, so let's get serious. Let's make the Full Demo work!

I'll try to follow the same order I used to describe the Full Demo here and get each module to work properly. I'll also use all the files used in AVR323 Demo project to start the Full Demo. To do that, I duplicated the AVR323 project with a different name (FreeRTOS_ArduinoMEGA-Demo_Full) and copied a few source and header files from the FreeRTOS folder.
  • The whole Standard Demo Header files can be copied to the project folder "FreeRTOS_ArduinoMEGA-Demo_Full\FreeRTOS\Demo\Common\include" folder (there was only the partest.h header). 
  • The source files should be copied from the folder "FreeRTOS 9.0.0\FreeRTOS\Demo\Common\Minimal" to the project folder "FreeRTOS_ArduinoMEGA-Demo_Full\FreeRTOS\Demo\Common\Minimal", but not all of them. The list follows:
    • AbortDelay.c
    • BlockQ.c
    • blocktim.c
    • countsem.c
    • death.c
    • dynamic.c
    • EventGroupsDemo.c
    • flop.c
    • GenQTest.c
    • integer.c
    • IntSemTest.c
    • PollQ.c
    • QPeek.c
    • QueueOverwrite.c
    • QueueSet.c
    • QueueSetPolling.c
    • recmutex.c
    • semtest.c
    • TaskNotify.c
    • TimerDemo.c
  • You'll also need to add a few source files that implement the FreeRTOS. Add the following from the folder "FreeRTOSv9.0.0\FreeRTOS\Source" to you project folder "FreeRTOS_ArduinoMEGA-Demo_Full\FreeRTOS\Source" (there was only list.c, queue.c and tasks.c, which are the bare minimum):
    • croutine.c
    • event_groups.c
    • timers.c
  • For the Full Demo, we'll need to use some dynamic allocation, thus change your heap file to the heap_5.c to have the full experience.
Finally, before we start, I suggest you compile the project as it is (essencially the AVR323 project) and correct any problems that may appear. For me, there were two problems:
  • References missing: go to Project > Properties > C/C++ General > Paths and Symbols > Includes > GNU C, add the following workpace paths:
    • /FreeRTOS_ArduinoMEGA-Demo_Full
    • /FreeRTOS_ArduinoMEGA-Demo_Full/FreeRTOS/Source/portable/WinAVR/ATmega2560
    • /FreeRTOS_ArduinoMEGA-Demo_Full/FreeRTOS/Source/include
    • /FreeRTOS_ArduinoMEGA-Demo_Full/FreeRTOS/Demo/Common/include
  • Definitions missing: on FreeRTOSConfig.h:
/* Software timer related configs - Full Demo */
#define configTIMER_TASK_PRIORITY ( configMAX_PRIORITIES - 1 )
#define configTIMER_QUEUE_LENGTH 20

/* The following includes are used in Full DEMO */
#define INCLUDE_eTaskGetState   1

With everything compiling correctly, even though it doesn't do anything different, we can start implementing (or actually just using the implemented algorithms) the Full Demo functions.

Tasks Notifications


Task notification is used to either send simple messages to another specific task or to just unblock the other task. This API can be considered a "lightweight" one position queue (called mailbox), since it is faster and consumes less RAM. To understand how it is used, let's assume we have two tasks, one I'll call Producer and one Consumer. The Producer will send a notification to the Consumer using  xTaskNotify(),  xTaskNotifyGive(),  xTaskNotifyAndQuery() or its interrupt safe equivalents, and will remain blocked until the Consumer receives the notification through  xTaskNotifyWait(),  ulTaskNotifyTake() or some task calls xTaskNotifyStateClear() with the Producer's handle as parameter. If the Consumer was already waiting for a notification, it will be immediately unblocked.

The notification value is a 32-bit value that every task has and that is cleared (set to zero) when the task is created. When sending a notification, the Producer can change the Consumer's notification value, either by overwriting, setting one or more bits or by an increment. The notification can also not be changed, if desired.

A fast and basic reference for the functions related to this API follows. Firstly, the functions used by the Producer (the one who sends the notification).

/* Notify a task
Returns: pdFAIL only if eAction is eSetValueWithoutOverwrite and there was another notification waiting, otherwise, pdPASS */
BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify,
                        uint32_t ulValue, /* Interpretation of this value depends on eAction */
                        eNotifyAction eAction );

/* Notify and increment notification value 
Returns: pdPASS, always */
BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );

/* Notify and retrieve the previous value
Returns: pdFAIL only if eAction is eSetValueWithoutOverwrite and there was another notification waiting, otherwise, pdPASS */
BaseType_t xTaskNotifyAndQuery( TaskHandle_t xTaskToNotify,
                                uint32_t ulValue, /* Interpretation of this value depends on eAction */
                                eNotifyAction eAction,
                                uint32_t *pulPreviousNotifyValue ); /* Returns the previous notification value from the Consumer */

The eAction is an enumerated type and can take one of the following values:

  • eNoAction - ulValue is not used, the task is just notified;
  • eSetBits - ulValue represents the bits that will be set in the Consumer's notification value (bits not set won't be altered);
  • eIncrement - ulValue is not used, the Consumer's value is incremented by one;
  • eSetValueWithOverwrite - ulValue is written in the Consumer's value;
  • eSetValueWithoutOverwrite - ulValue is only written if no other Producer is waiting for the Consumer to be notified (there is not a notification pending).
Functions used by the Consumer (the one who receives a notification) or other tasks.

/* Waits for a notification and changes the notification value according to xClearCountOnExit
Returns: the notification value before it's altered*/
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, /* if pdTRUE, the notification value is reset to 0; if pdFALSE, the value is decremented by 1 */
                           TickType_t xTicksToWait ); /* Can wait forever, if set to portMAX_DELAY */

/* Waits for a notification, allowing the notification value to be altered bit by bit
Returns: pdFALSE if timed out, pdTRUE otherwise */
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry, /* Bits to clear in the notification value before the enters the block state waiting for a notification */
                            uint32_t ulBitsToClearOnExit, /* Bits to clear in the notification value after the notification is received and the value is saved */
                            uint32_t *pulNotificationValue, /* Notification value resultant from the notification, before alteration according to ulBitsToClearOnExit */
                            TickType_t xTicksToWait ); /* Can wait forever, if set to portMAX_DELAY */

/* Clears a pending notification, not altering the notification value
Returns: pdPASS if there was a notification pendinge, otherwise pdFAIL */
BaseType_t xTaskNotifyStateClear( TaskHandle_t xTask ); /* xTask can be either the handle of another task that may have a notification pending or NULL, which reflects the task that called the function */


Full Demo's Task Notify


OK, API explained, now let's check what's happening in the DEMO. Basically what it does is create a common task, which will receive the notifications, thus represent the Consumer (check prvNotifiedTask()), and two Producers, one from a software timer (check prvNotifyingTimer()) and one from an interruption (check xNotifyTaskFromISR()). The interruption used is Tick interruption, through the use of the vApplicationTickHook().

The Producer from the interruption will notify the Consumer every 50 ms each time using a different API (vTaskNotifyGiveFromISR(), xTaskNotifyFromISR() and xTaskNotifyAndQueryFromISR()), always incrementing the notification value by one, but will only do that after the Producer from the sotware timer is already running. The Producer from the software timer always notifies the Consumer using the xTaskNotifyGive() and its period varies from 10 to 90 ms.

The Consumer will first raise some single tasks tests (check prvSingleTaskTests()), even before it creates the software timer:

  • Tries to take a notification (xTaskNotifyWait()), even though none has or will be given, in order to check if the timeout works;
  • Gives a notification to itself (xTaskNotifyAndQuery() setting the notification value without overwriting) and takes (xTaskNotifyWait()) it, in order to check if it will be done ASAP, thus no timeout-ing;
  • Checks the no-overwrite function by giving two notifications to itself  with different notification values and eSetValueWithoutOverwrite (xTaskNotify()) - the second should fail - and then taking only once (xTaskNotifyWait()) - the notification value should be equal to the first one given;
  • Same as before, but using overwrite: uses eSetValueWithOverwrite and different values, the value taken should be equal to last given.
  • Checks whether giving a notification with no changes to the value works by using xTaskNotify() with eNoAction and a value for the notification
  • Checks increments on the notification value by giving notifications for a known number of times (xTaskNotify() with eIncrement), then taking once and comparing the values; it tries to take again to ensure there are no more notifications pending;
  • Checks whether each bit of the notification value can be set by setting one bit at a time while giving (xTaskNotify() with eSetBits) and taking (the notification value is cleared before it starts through a xTaskNotifyWait() with all bits set on bits to clear on entry and no timeout) - it should run 32 times, since the notification value is 32 bit wide;
  • Checks if bits will be cleared on the entry and not on the exit by trying to take a notification, even though none was given, using xTaskNotifyWait() with bit0 (0x01) as bits to clear on entry and bit1 (0x02) as bit to clear on exit - as no notification was given, the bits are only cleared on entry.
  • Checks if bits will be cleared on exit by giving one notification, taking once with bit1 (0x02) as bits to clear on exit and trying to take again just to save the previous value.
  • Checks if the previous value is correctly received while notifying a task using xTaskNotifyAndQuery();
  • Finally, takes all notifications that might still exist (shouldn't be any), gives one generic notification and tries to clear the state twice (xTaskNotifyStateClear()), the first time should succeed and the second shouldn't.
Now back to the Consumer: after the tests described, it creates the software timer Producer, which will liberate the interruption Producer, and both will create notifications by incrementing the notification value, which means the value will be the amount of notifications given. So the consumer loop will:
  • Restart the software timer with a random period;
  • Try to take one notification with another random timeout and not clearing the value;
  • Try to take another notification, but this time not blocking but also not clearing the value;
  • Takes another notification, with the same timeout as before, but this time clearing the value;
  • Every 50 cycles, it raises the priority of the Consumer, receives all notifications by blocking until there is one available and reseting the counter (notification value), and restores the original priority (it tests the path where the Consumer is notified from an ISR and becomes the highest priority ready state task, but the pxHigherPriorityTaskWoken parameter is NULL - which it is in the tick hook that sends notifications to this task);
  • Finally, a counter of cycles is incremented.
There is one more function used, which is the Check function (xAreTaskNotificationTasksStillRunning()), responsible to check whether this Task Notify algorithm is still running. This function is called from the "Check Task" (prvCheckTask()), which is responsible to call, every 2.5 seconds, all check functions from all algorithms and warn in case something is going wrong. In the case of this algorithm, the function checks if the current number of cycles from the Consumer is higher than the last time it checked and if the number of times a notification was given is roughly the same of the taken ones. In case something is wrong, pdFAIL is returned.

Check Task


The check task (prvCheckTask()) is declared in main.c and is initialized with a low priority. Because the original code was designed to run in Windows, there was no problems with using printf(). With ArduinoMEGA things are obviously different, so a few changes were made. Instead of printing on the terminal, one LED is used to signalize that some problem occurred. A string (a static char *pcStatusMessage declared in main.c) is still used to signalize which error occurred, but it is treated as an array of chars with a defined size (mainERRORSIZE) and the messages always have this size, they are defined in a special header file (error_messages.h) and they're always transferred to the string using memcpy. Maybe in the future I could dispatch those messages through the serial port. Maybe.




Have fun!

No comments:

Post a Comment