Martin and Erich,
I wanted to enter this conversation on a few points and ask a few questions about how your are doing things. I'm running several FreeRTOS applications with "#define configUSE_TIME_SLICING 0" and letting my application select the appropriate executing task without any issues. I believe that I've archtected my applications in such a way that I won't experience the issues that you do.
First off, let me say that I think the FreeRTOS examples are terrible; they do not provide anything close to a good example of how the demo interface should be used in a FreeRTOS application. Most often, they seem to be the bare metal code with just "xTaskCreate" for the functions and a "vTaskStartScheduler" with minimal thought to things like task priorities, stack sizes or how a user would use their code in their applications. I seriously recommend not trying to use the FreeRTOS demo code in applications - start with the bare metal examples and port them into FreeRTOS tasks that perform the work that you require. If an author of these examples is out there and offended; please contact me for my opinion on how they should be written to provide meaningful examples to users.
While I'm dissing NXP FreeRTOS code, I'm also going to say that one of the things I've learned is to NOT use the FreeRTOS drivers in the SDKs. The reason is somewhat subtle and philosophical - the drivers, from what I can see are well written but they're written for the general case; the code is architected in a way that it would work well in a multi-user system in which multiple tasks are going to access the resource but when we're talking about single purpose MCU, they're overly complex and not optimized for virtually all applications that users are going to be developing for.
We're blessed with inexpensive chips with a plethora of IO options - that means that the goal of an application should be that each device provides a single function and each peripheral on the device has it's own specific IO. This probably seems wasteful and inefficient, but I would argue that it greatly simplifies hardware system design as well as application software development and debugging and will result in a cheaper overall product. The only interfaces that I would consider sharing are I2C and CAN (as "intraSystem" tasks below) as communication data packets include the destination address and there's no need to complicate things with semaphores and mutexes.
My first questions to you are:
- Why do you have any data processing tasks that are of the same priority?
- Why do you have multiple data processing tasks?
- How are you breaking up your task functions and how do you expect them to operate?
In my (Free)RTOS applications, I have four types of tasks:
- Execution initiated by a hardware interrupt (Typically Intersystem Communications and User Inputs)
- Execution initiated by a timer delay (Typically repeating Sensor functions)
- Execution initiated by another task (User Interface Processing)
- Always executing (What I call a "Data Processing" Task/The Basic Application)
These tasks are prioritized using the include file:
/*
* Task_priorities.h
*
* Created on: Apr. 29, 2020
* Author: myke
*/
#ifndef TASK_PRIORITIES_H_
#define TASK_PRIORITIES_H_
#if ((defined configMAX_PRIORITIES) && (10 > configMAX_PRIORITIES))
#error TOO FEW configMAX_PRIORITIES
#elif (!(defined configMAX_PRIORITIES))
#error configMAX_PRIORITIES NOT Defined
#endif
enum { interSystemTask_Priority = (configMAX_PRIORITIES - 2)
, intraSystemTask_Priority = (configMAX_PRIORITIES - 3)
, sensorTask_Priority = (configMAX_PRIORITIES - 4)
, actuatorTask_Priority = (configMAX_PRIORITIES - 5)
, fileSystemTask_Priority = (configMAX_PRIORITIES - 6)
, userInterfaceTask_Priority = (configMAX_PRIORITIES - 7)
, consoleTask_Priority = (configMAX_PRIORITIES - 8)
, dataProcessing_Priority = (configMAX_PRIORITIES - 9)
};
#endif /* TASK_PRIORITIES_H_ */
I prioritize tasks using the philosophy that for a task to execute as fast as possible, all the called tasks are a higher priority. By doing this, I don't have any task starvation issues.
Just to explain the task functions:
- interSystem - Communications with other (typically host) systems. Examples are USB, Ethernet, BT & UART. If the application is an I2C or CAN multi-master or slave then these interfaces could be considered at this level
- intraSystem - Communications with devices within the application. This typcally is I2C, CAN, SPI, UART
- sensor - ADC, Digital Inputs, Temperature sensors, etc. If they are not on the chip, then the task communicates with the interface task at the "intraSystem" level (higher priority). I should point out that the run time task for run time stats (Erich's Code) is run at this priority level and I use it for measuring dataProcessing delays.
- actuator - Digital Outputs, PWM, etc. The assumption here is that some actuators may require sensor data for correct operation
- fileSystem - At the lowest level simple non-volatile storage of data. At a higher level, a FAT file system
- userInterface - LEDs, LCDs, etc. Buttons, knobs are all "sensors" and their values are requested from the userInteface
- consoleTask - USB device/UART/Telnet to access system and provide monitoring/debug interface
- dataProcessing - Doing the work using data from higher priority tasks and sending commands to other tasks
How are you architecting your applications? I should point out that in the structure above, I only expect that the userInterface and dataProcessing tasks operate autonomously. Side tasks that operate autonomously very quickly complicate things and makes debugging/validating the application much more difficult. The userInterface provides information to the user and may save user inputs until required by the dataProcessing task or send them directly to the dataProcessing task - in rereading what I've written, I'm giving the impression that userInterface operates continuously and that's not the case; userInterface waits on a timer or message from another task to execute. For all intents and purposes dataProcessing replaces the FreeRTOS IDLE task and is continuously operating (although the console task may halt dataProcessing if a user is connected and needs to directly access different system function tasks).
I feel like I'm missing something the way that you are architecting and specifying your data processing tasks - could you please describe them? I'm curious to know if my model would work with them.
Now, FreeRTOS is a bit different from other RTOS's I've worked with as sending a message doesn't automatically call the scheduler - after putting the message on the receiving task's queue, execution returns to the caller which I think is part of your issues. To work with this, I use a "send/receive" process in which the sender is blocked (which initiates the scheduler) until it receives a reply from the receiver (after which the receiver waits for another message, becoming blocked and causing the scheduler to resume caller execution). For example to send a request message and wait for its completion I use the method:
void static inline flashMsgTXRX(struct flashQueueMsg* txMsg
, struct flashQueueMsg* rxMsg) {
xQueueSend(flashQUEUE_Handle
, (void*)txMsg
, portMAX_DELAY);
xQueueReceive(flashRXQUEUE_Handle
, (void*)rxMsg
, portMAX_DELAY);
}
This actually works really well with the priority scheme as the higher priority tasks are blocked waiting for a message to come in, at which point they service the request and then reply that it is complete.
I must confess there is one case that I do cheat on getting data from tasks (that my old university RTOS professor would probably roast me for) and that is getting simple data that a task is storing - rather than sending a message requesting the data and then waiting to receive it I access the variable in the task using the inline method:
uint32_t static inline rtpSensorValueRead(uint32_t dimension) {
extern volatile uint32_t rtpSensorValue[2];
return rtpSensorValue[dimension];
}
In deference to my professor; he was adament that a task could not directly access any data in another task - this code prevents a task from modifying the other task's variable, which is in the spirit of the professor's dictum.
It should be noted that FreeRTOS provides a copy of the message to the receiver, not the actual data or a pointer to the data so the rule of not accessing another task's data is followed and by always waiting for a response, I don't get into a situation where the sender continues updating its data which may result in a race condition.
I *think* I have shown how I have architeched my FreeRTOS applications in such a way that I am not going to experience the issues that you are having, but I'd be interested in your comments back.
myke