;   ***************************************************************
;   * Copyright (C) 2010, Embed Inc (http://www.embedinc.com)     *
;   *                                                             *
;   * Permission to copy this file is granted as long as this     *
;   * copyright notice is included in its entirety at the         *
;   * beginning of the file, whether the file is copied in whole  *
;   * or in part and regardless of whether other information is   *
;   * added to the copy.                                          *
;   *                                                             *
;   * The contents of this file may be used in any way,           *
;   * commercial or otherwise.  This file is provided "as is",    *
;   * and Embed Inc makes no claims of suitability for a          *
;   * particular purpose nor assumes any liability resulting from *
;   * its use.                                                    *
;   ***************************************************************
;
;   Task management.
;
;   The routines in this module implement a cooperative multi-tasking system.
;   For context switching speed, each task has its own stack.
;
;   The task scheduler is of the "round robin" type.  Each call to TASK_YIELD
;   switches the context to the next task in the list in a circular fashion.
;
;   The constant TSKSAVE defined in the include file indicates which registers
;   will be preserved accross a task switch.
;
/include "qq2.ins.dspic"

;*******************************************************************************
;
;   Configuration constants.
;
/const   maxtasks integer = 4 ;maximum number of concurrent tasks supported (0-31)
/const   glbtasknum bool = true ;maintain global curr task number in CURRTASK
.equiv   endlim, 6           ;stack error trap when push with this many bytes left on stack
;
;   Data structure for the static state kept per task.
;
         struct_start
         field   tsk_stkpnt  ;stack pointer to resume task
         field   tsk_splim   ;appropriate SPLIM value for task's stack

.equiv   tsksize, struct_offset ;size of the per-task data structure
;
;   Derived constants.
;
.equiv   maxtasks, [v maxtasks]
.equiv   glbtasknum, [if glbtasknum 1 0]

/show "  Configured for up to " maxtasks " concurrent tasks"
/show "  CURRTASK " [if glbtasknum "enabled" "disabled"]

;*******************************************************************************
;
;   Variables.
;
;*******************
;
;   Global state.
;
.section .task,  bss

.if      glbtasknum
alloc    currtask, 2, 2      ;0-N number of currently running task
         .global currtask
         .endif

;*******************
;
;   Local state.
;
alloc    tasks,  tsksize * maxtasks, 2 ;tasks table, per-task state for each task
alloc    after_p, 0, 2       ;first address after tasks table

alloc    task_p, 2, 2        ;adr of task table entry for current task
alloc    last_p, 2, 2        ;pointer to last used task table entry

.if      debug
         .global tasks
         .global task_p
         .global last_p
         .endif

/if debug
  /then
         .text
  /else
         .section .task_code, code
  /endif
;*******************************************************************************
;
;   Subroutine TASK_INIT
;
;   Initialize the hardware and software state managed by this module.
;   This must be the first call made to this module.  The first task slot
;   will be reserved for the task making this call.  The remaining task
;   slots will be initialized to empty.  Calls to TASK_YIELD are allowed
;   after TASK_INIT, although no task switching will take place until
;   at least one other task is created by calling TASK_NEW.
;
         glbsub  task_init, regf0

         mov     #tasks, w0
         mov     w0, task_p  ;init pointer to table entry for current task
         mov     w0, last_p  ;init pointer to last valid table entry
         mov     Splim, w0   ;set saved stack limit value for this task
         mov     w0, tasks + tsk_splim

.if      glbtasknum
         mov     #0, w0
         mov     w0, currtask ;indicate task 0 is now running
         .endif

         leaverest

;*******************************************************************************
;
;   Subroutine TASK_NEW
;
;   Create a new task.  The current values of the registers indicated by TSKSAVE
;   will be the intial values of those registers in the task.  These registers
;   can be used to pass parameters to the task.
;
;   Software error ERR_NOTASK is signalled if no empty task slot is available.
;
;   The call parameters are:
;
;     W13  -  Size of new task stack, must be even.
;     W14  -  Stack start address for the new task.
;
;   The new task start address will be immediately after the call to TASK_NEW.
;   TASK_NEW will return 2 instruction words (4 addresses) after the call.  This
;   provides an opportunity to put a GOTO immediately after the TASK_NEW call.
;   The first instruction executed by the new task will be the GOTO, whereas
;   TASK_NEW will return to immediately after the GOTO.
;
;   The existing task will continue to run until TASK_YIELD is called.
;
         glbsub  task_new, regf0 | regf14
;
;   Update LAST_P to the next unused task table entry.  Abort if LAST_P is
;   already at the last allocated task table entry.
;
         mov     last_p, w0  ;get pointer to current last table entry
         add     #tsksize, w0 ;make pointer to next table entry
         mov     #after_p, w14 ;get first address past end of table
         cp      w0, w14     ;compare new table entry address to past table adr
         bra     geu, tkn_abort ;task table is already full ?
         mov     w0, last_p  ;update pointer to last used task table entry
;
;   Initialize the stack of the new task.  The stack will be set up so that to
;   restart the task, the registers indicated by TSKSAVE are restored, then a
;   RETURN is performed.  This means the task start address is pushed first,
;   then the TSKSAVE registers.
;
;   Our stack currently looks like this:
;
;     [W15 - 8] --> return address low word
;     [W15 - 6] --> return address high word
;     [W15 - 4] --> saved W0
;     [W15 - 2] --> saved W14 (start of new task stack)
;     [W15]     --> empty
;
         ;
         ;   Push the task start address onto the task stack.
         ;
         mov     [w15 - 2], w14 ;set W14 as stack pointer for the new task
         mov     [w15 - 8], w0 ;get task start address low word
         mov     w0, [w14++] ;push it onto new task stack
         mov     [w15 - 6], w0 ;get task start address high word
         mov     w0, [w14++] ;push it onto new task stack
         ;
         ;   Push the registers saved accross task switches onto the stack
         ;   of the new task.
         ;
         mov     [w15 - 4], w0 ;restore original W0 value

.irp     ii,     0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 ;once for each reg W0-W13
         .if     tsksave & (1 << \ii) ;this register will be saved ?
         mov     w\ii, [w14++] ;push this register onto task stack
         .endif
         .endr

         .if     tsksave & (1 << 14) ;W14 needs to be saved on new stack ?
         mov     [w15 - 2], w0 ;get original W14 value
         mov     w0, [w14++] ;push it onto the new task stack
         .endif
         ;
         ;   Push any application specific data that is to be saved accross
         ;   task swaps onto the new task stack.  W14 is the stack pointer
         ;   and W0 is available for scratch.
         ;

;
;   Fill in the task descriptor for the new task.  LAST_P is pointing to the
;   task descriptor.
;
         mov     last_p, w0  ;point W0 to the task descriptor
         mov     w14, [w0 + tsk_stkpnt] ;save the task stack pointer
         mov     [w15 - 2], w14 ;get start address of new task stack
         add     w14, w13, w14 ;make first address past end of stack
         sub     #endlim, w14 ;make SPLIM value for this stack
         mov     w14, [w0 + tsk_splim] ;save it in the task descriptor
;
;   Return to 4 addresses after the call.
;
         mov     [w15 - 8], w0 ;get return address low word
         add     #4, w0      ;make new return address low word
         mov     w0, [w15 - 8] ;replace it on the stack

         mov     [w15 - 6], w0 ;get return address high word
         addc    #0, w0      ;make new return address high word
         mov     w0, [w15 - 6] ;replace it on the stack

         leaverest
;
;   The task table was already full.  Signal software error ERR_NOTASK.
;
tkn_abort:
         swerr   err_notask  ;signal the error

;*******************************************************************************
;
;   Subroutine TASK_YIELD
;
;   End the time slice of the current task, run all other tasks for a time
;   slice, then resume the caller's task and return.  Only those registers
;   indicated by TSKSAVE will be preserved.
;
         glbsub  _task_yield ;C callable interface
         glbsub  task_yield
;
;   Save all old task state that is saved accross task swaps onto the stack.
;
         ;
         ;   Save all the registers listed in TSKSAVE.
         ;
.irp     ii,     0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 ;once for each reg
         .if     tsksave & (1 << \ii) ;this register will be saved ?
         push    w\ii        ;push this register onto the stack
         .endif
         .endr
         ;
         ;   Push any application specific data that is to be saved accross
         ;   task swaps onto the stack.
         ;

;
;   Save the stack pointer and SPLIM value in the task table entry for this task.
;
         mov     task_p, w0  ;point W0 to table entry for the current task
         mov     w15, [w0 + tsk_stkpnt] ;write the stack pointer into the table entry
         mov     Splim, w1
         mov     w1, [w0 + tsk_splim] ;write the SPLIM value into the table entry
;
;   Advance to the task pointer to the next task.  W0 is the task pointer for
;   the current task.
;
.if      glbtasknum
         mov     currtask, w2 ;get 0-N number of the current task
         .endif

         mov     last_p, w1  ;get pointer to last entry in the table
         cp      w0, w1      ;compare current pointer to last pointer
         bra     geu, ylast  ;currently at the last task ?
         ;
         ;   Not at last task in table, advance to next.
         ;
         add     #tsksize, w0 ;make pointer to next table entry
.if      glbtasknum
         add     #1, w2      ;update number for this task table entry
         .endif
         bra     ynew        ;now at next task
         ;
         ;   At last task in table, wrap back to first.
         ;
ylast:
         mov     #tasks, w0  ;point to first task in the table
.if      glbtasknum
         mov     #0, w2      ;first task table entry is task number 0
         .endif

ynew:                        ;W0 points to task table entry of the new task
.if      glbtasknum
         mov     w2, currtask ;update global current task number
         .endif
         mov     w0, task_p  ;update pointer to task table entry for current task
         disi    #3
         mov     [w0 + tsk_splim], w1 ;get the SPLIM value for this stack
         mov     w1, Splim   ;set the hardware limit for this stack
         mov     [w0 + tsk_stkpnt], w15 ;switch to the stack of the new task
;
;   Restore the registers indicated by TSKSAVE and other per-task saved state
;   from the stack, and return from the TASK_YIELD call made by this task.
;
         ;
         ;   Pop any application specific data that is saved accross task
         ;   swaps off the stack.
         ;

         ;
         ;   Restore all the registers listed in TSKSAVE.
         ;
.irp     ii,     14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 ;once for each reg
         .if     tsksave & (1 << \ii) ;this register will be saved ?
         pop     w\ii        ;pop this register from the stack
         .endif
         .endr
         return              ;return from TASK_YIELD of the resumed task

;*******************************************************************************
;
;   Subroutine TASK_YIELD_SAVE
;
;   Like TASK_YIELD except that all the W registers are preserved, not just the
;   ones listed in TSKSAVE.
;
         glbsub  task_yield_save, tsknsave

         mcall   task_yield

         leaverest

.end