Native Mode "ManyTasks"
Posted: 28 January 2008, 21:08 PM
I have modified Mike's ManyTasks.bas program to work on the native mode devices. On a ZX-24n, it can handle 49 tasks. It might be able to do 50 but I reserved 100 bytes of heap space for the strings that are created as part of the output process.
One of the differences with native mode is that the minimum task size is much larger due to two factors. Firstly, when a task switch is done, the context of the current task is saved to its task stack. This requires 35 bytes (32 GP registers, IP, and status register). Secondly, whenever an interrupt occurs, the ISR saves whatever registers it needs to save on the stack of the currently executing task. I'd have to confirm the actual maximum but I suspect that this may require at most 10-15 bytes. Interrupts are not nested so the maximum stack use due to interrupts is the largest stack use of any ISR.
Another difference is that since tasks use the AVR hardware stack mechanism, the stack pointer grows in the opposite direction as compared to a task stack in the VM mode devices. The AVR stack pointer move toward zero as data is pushed on the stack. This means that the task control block had to be moved to the end of the task stack instead of being at the beginning.
You might recall that on VM mode devices, the Main() task stack and the heap grow toward each other, using all of the RAM that isn't otherwise allocated. This allows maximum flexibility but it also makes it more difficult to prevent the heap from encroaching on the Main() task stack. In the current VM version there is no check for this at all. If stack overflow checking is turned on, it will detect when the Main() task stack has encroached on the heap.
In the native mode devices, the heap grows downward from the end of RAm and the Main() task stack grows downward from some position in the otherwise-unallocated RAM space. By default, 512 bytes are reserved for the heap so the TCB for the Main() task is positioned 512 bytes from the end of RAM. The heap limit is just above the Main() TCB and the heap allocator will not grow the heap beyond the heap limit. This prevents the heap from trashing the Main() TCB. Unfortunately, however, there is no protection against the Main() task stack from trashing other allocated data located lower in RAM.
Because of the hard limit on the heap growth, several directives were added to allow you to specify how the unallocated RAM is split up between the Main() task stack and the heap. The simplest way to do this is to specify the number of bytes that you want for the Main() task stack using Option MainTaskStackSize. Since the heap limit is always immediately after the TCB of the Main() task stack, this means that all the remaining RAM is dedicated to the heap. I used this method in ManyTasks.bas to obtain the maximum heap size.
The second way of specifying the split is to use Option HeapSize to directly specify the heap size. This places the heap limit, and therefore the end of the Main() task stack that number of bytes from the end of RAM. The third way is to specify the heap limit directly as an absolute address. This is the least likely to be used but may be useful in special situations.
In the modified ManyTasks code, I added some calls to determine the amount of unused space in the Main() task stack and the first of the allocated task stacks. I tuned the application by choosing an arbitrary size for the two and then running the program. This first guess turned out to be too low (the app didn't run correctly) so I bumped it up by quite a bit and tried again. This time the app ran but only 25 or so tasks could be allocated. The information about the unused space in the task stacks allowed me to trim down the task stacks to just a bit larger than the indicated requirement, thus arriving at the 49 task mark.
One of the differences with native mode is that the minimum task size is much larger due to two factors. Firstly, when a task switch is done, the context of the current task is saved to its task stack. This requires 35 bytes (32 GP registers, IP, and status register). Secondly, whenever an interrupt occurs, the ISR saves whatever registers it needs to save on the stack of the currently executing task. I'd have to confirm the actual maximum but I suspect that this may require at most 10-15 bytes. Interrupts are not nested so the maximum stack use due to interrupts is the largest stack use of any ISR.
Another difference is that since tasks use the AVR hardware stack mechanism, the stack pointer grows in the opposite direction as compared to a task stack in the VM mode devices. The AVR stack pointer move toward zero as data is pushed on the stack. This means that the task control block had to be moved to the end of the task stack instead of being at the beginning.
You might recall that on VM mode devices, the Main() task stack and the heap grow toward each other, using all of the RAM that isn't otherwise allocated. This allows maximum flexibility but it also makes it more difficult to prevent the heap from encroaching on the Main() task stack. In the current VM version there is no check for this at all. If stack overflow checking is turned on, it will detect when the Main() task stack has encroached on the heap.
In the native mode devices, the heap grows downward from the end of RAm and the Main() task stack grows downward from some position in the otherwise-unallocated RAM space. By default, 512 bytes are reserved for the heap so the TCB for the Main() task is positioned 512 bytes from the end of RAM. The heap limit is just above the Main() TCB and the heap allocator will not grow the heap beyond the heap limit. This prevents the heap from trashing the Main() TCB. Unfortunately, however, there is no protection against the Main() task stack from trashing other allocated data located lower in RAM.
Because of the hard limit on the heap growth, several directives were added to allow you to specify how the unallocated RAM is split up between the Main() task stack and the heap. The simplest way to do this is to specify the number of bytes that you want for the Main() task stack using Option MainTaskStackSize. Since the heap limit is always immediately after the TCB of the Main() task stack, this means that all the remaining RAM is dedicated to the heap. I used this method in ManyTasks.bas to obtain the maximum heap size.
The second way of specifying the split is to use Option HeapSize to directly specify the heap size. This places the heap limit, and therefore the end of the Main() task stack that number of bytes from the end of RAM. The third way is to specify the heap limit directly as an absolute address. This is the least likely to be used but may be useful in special situations.
In the modified ManyTasks code, I added some calls to determine the amount of unused space in the Main() task stack and the first of the allocated task stacks. I tuned the application by choosing an arbitrary size for the two and then running the program. This first guess turned out to be too low (the app didn't run correctly) so I bumped it up by quite a bit and tried again. This time the app ran but only 25 or so tasks could be allocated. The information about the unused space in the task stacks allowed me to trim down the task stacks to just a bit larger than the indicated requirement, thus arriving at the 49 task mark.