Preferred architecture

Hi,
I have been reading quite a bit on freeRTOS but being at the beginning things got more confusing before they get clearer…

I am using freeRTOS with MPLAB Hamrony3 on a PIC32. I mention using Harmony3 because Harmony creates a different file for each task created (they call it app_task_1.c app_task_2.c etc).

Now… I have various devices on a serial bus and as mentioned in a previous post it has some inputs and some outputs which I want to be handled by separate tasks. The IC also needs initialisation and I do that by keeping a copy of all its registers (only 8) into the program and at boot those variables are read from memory and written into the IC to configure it.

Questions:
1) Where do I place the set of variables that mirror the I2C chip registers? Because I have at least 2 task files (inputs_read.c and outputs_write.c) it would be confusing to place them in either of them…
2) Do I create a separate task and in that task’s file place a) the variables themselves [and call them with ‘extern’ from the other files], b) initialise them and c) also place the code that writes via the I2C to configure the I2C chip registers?
3) And do I then put that task on suspend for the rest of the time never to be run again?

Where can I find good resources on embedded firmware architecture using RTOS/freeRTOS?
(apart from the PDF of the freeRTOS itself)

Thank you

I’m afraid it is not clear to me what you are wanting to do. Why do you want to store variables that mirror the I2C chip registers? If there is a reason you want to save a copy of the registers then presumably the copy will be out of date as soon as the hardware changes the actual register values.

I’m afraid I’m not familiar with the way harmony 3 creates tasks. Do the inputs_read.c and outputs_write.c files read and write to all peripherals, or is there two tasks per peripheral? In either case the arrangement does not seem optimal.

The right thing to do is very dependent on the requirements and complexity of the application you are writing - but in the general case with an RTOS you want to make everything event driven so a task never consumes processing time unless there is something to do - that means other tasks can run, and if there are no other tasks that are able to run, you have the opportunity to put the MCU into a sleep mode to save power.

For example, to write to a peripheral I would call a “write” function that sets up the write in a single function call - no separate task needed, but you may need to check for thread safety if more than one task writes to the same peripheral. The implementation of the write function can set up the peripheral to send the data then either return immediately while the data is being sent (by DMA, by interrupts, or whatever), or opt to block on a task notification (or other RTOS primitive) to wait for an interrupt that tells the task the send is complete.

Likewise when receiving data you can do something asynchronous like have interrupt service routines automatically buffer any incoming data, then when you want to read that data, have a single function that reads the data from the buffer - again no special task needed for that but do be aware of any thread safety issues that may need managing. Alternatively you can do something much simpler and just have the read function read from the peripheral directly - entering the blocked state while it is waiting and being unblocked by the peripheral’s interrupt when data arrives. You need to ensure you don’t miss data that arrives in between reads.

You can use schemes such as those depicted on this page: https://www.freertos.org/RTOS_Task_Notification_As_Counting_Semaphore.html

1 Like

Thank you Richard,
the reason why I want to keep a mirror image of the I2C chip registers is so I can read them much faster (few CPU cycles) than having to access the I2C (several microseconds). And then know that their value is always mirroring the hardware as fast/soon as possible because of the ISR updating any changes in the input.
But then again you made me think and perhaps I can work around that and do without.

What about my other (somewhat related) point? Is it a good practice to have a task that:

  1. contains all global variables of my program
  2. all the code that initialise them
  3. most important all the code that does all the external devices initialisation (such as I2C devices) and then call that task only once at the beginning then put it on suspend?

I am struggling on where to put the global variables… so if not the above, is it good style to have a file just for the global variables?

Thank you :slight_smile:

First, reserve using ‘Suspend’ for only the most special of cases, it normally is the wrong thing. Tasks should Block on some sort of thing to signal when they have something to do.

My structure for this sort of thing. I would define a task for each device interrupt that comes into the processor to indicate that data is ready out on the I2C bus, and that tasks when it wakes up from the notification of the interrupt, goes out and reads the device to get the data. You could have one task gather all the interrupts and do all the I2C operations, but then you need that one task to know everything about all the I2C activity in the system. I find it much cleaner to separate that knowledge so each device in the system has a module that knows about it, and pretty much just it, and other modules that know how they need to interact.

When It gets that data, if we might need it later, you put it in a global variable, probably defined in the file with that code, and exported via the header file for that file. Every .c file that exports data or routines to others gets a .h file with that information, and that .h file is the first include in that .c file.

That task might then do some processing with that data, if that module knows what to do with it, or it might trigger some event that other tasks can be waiting on to signal that new data is ready (good use for an event group here).

For organization, I tend to put all the code relating to one ‘system level function’ (NOT a ‘c function’ but an operational function) into a single file, separate from other functions.So one file for the pressure sensor, one file for the temperature sensor, etc, and then there will be one file for the generic I2C driver that those functions might call. Each file will have an init function that is called before the system starts (which will create the task if needed, and set up other variables and resources.

Most the time, I am actually using C++, so defining global objects makes this ‘init’ call automatic, you just need to be careful about assumptions on the order that they are created.

Each of these modules will define a header file which defines the ‘API’ that the modules will use to talk to it, and it includes the header files of the modules that it needs to talk to.

1 Like

Thank you Richard for the detailed explanation.

At the moment for blocking I am using vTaskDelay(). But my understanding is that if I receive a notification from another task (say from the ISR()) in the middle of that delay then that task will still wait for the remaining delay before it carries on with the rest of the code.

  1. Is that correct?

  2. what is the alternative so the task sits on that line of code where it was blocked and when it receives the external notification it immediately resumes from there?

  3. This might eb the same answer as to (2)… What is the best/preferred API to block a task waiting for a notification from ISR and/or from another task?

With reference to your comment about the task suspend… just out of curiosity, wouldn’t be Suspend be appropriate for initialization (if such task was to be created)? It would be called only once and never again. Just so I start understand the thinking logic of RTOS applications.

Thank you :slight_smile:

First, for the suspending after initialization, I don’t do that because why suspend if you can just terminate that task (if you will never resume it. reclaim the resources) and why delete a task that you can just recycle, so any such one shot initialization code that needs to be done at the startup of the system become a preamble to the task that needs that initialization done.

As to what makes a good blocking method. For interrupts where you know what task you would signal, I use the Direct-To-Task notification. The task blocks waiting for it, and when it comes in, the ISR gives the notification and wakes up the task. Other things might use a semaphore or a queue or a stream/message buffer, or even an event group. All the basic primitives have use cases where they are the right choice.