In the ongoing quest to scavenge yet another byte of useful memory one option is to recover the stack used by main() once the scheduler has been started. Effectively the main() routine is treated as a task that runs once and is then deleted, allowing its stack to be allocated to another task.
While trying to think of a way to do this on the MSP430 architecture have thought of an alternative that might make sense:
Reuse the main() stack as a stack exclusively for the use of ISR’s.
The logic goes like this: Some ISRs are light-weight in their stack usage, but if the ISR calls other routine(s) and/or performs a lot of calculations then their use of the stack can blow out quite a lot. Threads need to have sufficient stack space to accommodate each ISR’s requirements; which can occur at any time. So if an ISR requires 40 bytes of stack, then all threads need to have an extra 40 bytes added to their stack space. This gets worse if you have multiple ISRs in the application.
As a suggested alternative, ISRs that require a lot of stack could point the SP into an ISR only stack space (perhaps reuseing main()'s stack). If this works it would mean that:
- the stack used by main() gets to be reused
- the stack space allocated to each thread can be reduced.
Am not suggesting that this need to be done for all ISRs, just the memory hungry ones.
Implementation details would require handling of the case when one ISR interrupts another ISR, but that should be fairly easy?
Can anyone else spot anything else that can go wrong with this before aLUNZ launches into trying it? (For instance, will these SP shenanigans be compatable with cQueueSendFromISR() and cQueueReceiveFromISR()? aLUNZ thinks so, but has yet to walk through the code to make sure).
The idea is to just change the SP. The fact that we are servicing an interrupt effectively means that we have achieved a pre-emptive context switch.
For the MSP430 / GCC (other s / compilers have the same basic approach, but the specifics may vary) this will require:
- declaring the ISR functions as “naked” which means that the compiler won’t automatically save the registers. (The will save the PC and SR on the current threads stack - we cannot do anything about that).
- when called the ISR will test the current SP to determine if we are interrupting an ISR. If interrupting an ISR then we do not need to change the SP, if interrupting the application then the SP is saved (where?) then changed to use the ISR only stack space.
- all registers are saved to the stack
…
- the ISR does its thing
…
- on exit the ISR loads the registers, reloads the saved SP then calls IRET. (The will then pop the SR and PC from the interrupted threads stack and continue executing the interrupted thread).
There are dangers with this approach and FreeRTOS. For instance an ISR might call TaskYield() to force a context switch (if some action has been taken that might wake a suspended thread). Am pretty sure that calling TaskYield() inside this scheme will causes all sorts of mayhem.
There could be other problems, just haven’t thought of any yet.
A bit of background before: I am currently working on an H8 port, when this is finished and released I would like to create another release that addresses a couple of issues. The first is this very point - how to use the main stack, the second is reorganizing the sTaskCreate() function call so that it does not require so much stack. Currently sTaskCreate() can be recursive (to one level only) when the idle task is automatically created. This uses more stack than is ideal. Reducing the necessary stack size of main() lessens the problem of not being able to recover it.
So far there are two suggestions for using the main() stack:
+ The first is giving it over to the idle task - but there is a big problem here as the idle task needs to configure itself which means creating its default stack used when it first runs. This means the idle task could only be created immediately before the scheduler was started otherwise main() would crash because it’s stack was clobbered, or alternatively main() would clobber the idle stack before the task ran.
+ The second alternative (thus far) is to have a function that allows main() to be turned into a task. main() would then be two stage, the first would be sequential and create the tasks and start the scheduler as normal - the second would be an infinite loop that acted as a task. The problem with this is the extra complexity to the end user. They would have to understand the process - but it could be made optional.
aLUNZ has a third suggestion that is very attractive from the point of view of memory saved for the reasons he points out. There may be portability issues however as each processor on which FreeRTOS runs has different ways of handling interrupts.
For interrupts that do not cause a context switch I cannot see any problems.
For interrupts that do cause a context switch (for reasons already mentioned by aLUNZ) there are some difficulties. Each port uses the stack slightly differently. What has to be considered is the processor state when the task starts executing again. If the switch out is only performed once the stack is back at the task level (rather than the shared stack) then this should be ok. The idea with the queue and semaphore functions that are ISR safe (the ones with FromISR in their name) is that they return a value that indicates whether a yield is required or not rather than just performing the switch. This allows the application to then perform any tidy up necessary before making the switch and the switch is only ever performed as the very last action in the ISR. Take a look at the ARM ports, where the processor forces the use of another stack anyway.