Hello all, If you are interested, I am working on a new personal project - an ARM9 scheduler ("Sand Storm", but the name is still volatile. It will inherit a lot from sourceforge.net/.../, however it will of course run on STR7/9 variants instead). It is still a rudimentary implementation, which I made for demonstration purposes only.
we begin with this declaration:
typedef struct { int32u context[17] ; // buffer for all processor registers int8u id ; task_state status ; int8u priority ; // task priority } t_tcb ;
(tcb = task control block).
assuming 3 tasks are run, the following definitions are made:
t_tcb g_task1tcb, g_task2tcb, g_task3tcb, *g_tasks_tcb[] = {&g_task1tcb, &g_task2tcb, &g_task3tcb} ;
and
#define TASK_STACK_SIZE_NORMAL 0x100 int32u g_current_task_index = 0, g_current_task_index_tcb_index = 0, g_next_task_ptr = 0, g_current_task_ptr = 0, lp_task1_stack[TASK_STACK_SIZE_NORMAL], lp_task2_stack[TASK_STACK_SIZE_NORMAL], lp_task3_stack[TASK_STACK_SIZE_NORMAL] ;
each task needs to be configured prior to running, like this:
void tcb_setup(t_tcb *const ap_tcb, task_ptr ap_task, int32u *ap_stack, int16u a_stack_size) { int32u *l_tcb_word = &ap_tcb->context[16], *lp_stack = ap_stack, l_value ; // IRQ ISR return address (ending of tcb) // the IRQ ISR must (according to the ARM architecture) decrement 4 from each // return address. so the return address of each IRQ ISR must be incremented by 4. *(l_tcb_word--) = (int32u)ap_task + 4 ; // SPSR *(l_tcb_word--) = (int32u)0x50 ; // R14 - internal task return address *(l_tcb_word--) = (int32u)ap_task ; // R13 - stack top *(l_tcb_word--) = (int32u)ap_stack ; // R12 *(l_tcb_word--) = (int32u)0x12012012 ; // R11 *(l_tcb_word--) = (int32u)0x11011011 ; // R10 *(l_tcb_word--) = (int32u)0x10101010 ; // R9 *(l_tcb_word--) = (int32u)0x99999999 ; // R8 *(l_tcb_word--) = (int32u)0x88888888 ; // R7 *(l_tcb_word--) = (int32u)0x77777777 ; // R6 *(l_tcb_word--) = (int32u)0x66666666 ; // R5 *(l_tcb_word--) = (int32u)0x55555555 ; // R4 *(l_tcb_word--) = (int32u)0x44444444 ; // R3 *(l_tcb_word--) = (int32u)0x33333333 ; // R2 *(l_tcb_word--) = (int32u)0x22222222 ; // R1 *(l_tcb_word--) = (int32u)0x11111111 ; // R0 (beginning of tcb) *(l_tcb_word--) = (int32u)0x00000000 ; // fill user provided stack with predicted, handy from debugging l_value = 0x1111 ; while (a_stack_size-- > 0) { *(lp_stack++) = l_value ; l_value <<= 1 ; if (l_value >= 0x1111000) { l_value = 0x1111 ; } } }
tcb_setup(&g_task1tcb, &task1, lp_task1_stack, TASK_STACK_SIZE_NORMAL) ; tcb_setup(&g_task2tcb, &task2, lp_task2_stack, TASK_STACK_SIZE_NORMAL) ; tcb_setup(&g_task3tcb, &task3, lp_task3_stack, TASK_STACK_SIZE_NORMAL) ;
where the task functions are defined the following way: (only task1 is demonstrated):
void task1(void) { while(1) { } }
As promised, here is the fix (see next post for the rest of the code and details):
__asm __irq void TIM0_IRQHandler(void) { PRESERVE8 IMPORT g_tasks_tcb IMPORT g_current_task_index IMPORT g_startup_task_index IMPORT g_next_task_ptr IMPORT g_current_task_ptr IMPORT vic0 IMPORT tim0 IMPORT g_software_interrupt IMPORT VIC_SWITCmd IMPORT scheduler_select_next_task IMPORT g_irq_stack_base // before switching the processor context, all necessary computations will be // carried out so that before the actual switch is done the values of these // registers can be restored to their original values (R0 - R12 are shared among // all processor modes) // Note that for the actual switch R13 is used, because the IRQ mode has its // own copy of it and there is no danger of corrupting user mode data. // manipulating R0 - R12 for this purpose would corrupt vital task state data. // save the value of the registers needed for context switch preparation // this occurs onto the IRQ stack STMDB R13!, {R0 - R3} // *********************************************************** // acquire a pointer to the tcb block of the task that is left // *********************************************************** LDR R1, =g_current_task_index // address of index to tcb LDR R3, [R1] // R3 is the index // if the scheduler is just started, there is no task running hence saving the processor's // context is skipped - only the next stage of task selection and restoration is carried out CMP R3, #0xFF BEQ context_not_saved // but if the scheduler is not starting up, first save the context, then select the next // task to run // point to the tcb block representing the current task LDR R0, =g_tasks_tcb // base address of tcb container LDR R1, [R0, R3, LSL#2] // R1 = R0 + R3*4 (multiply by 4 becasue each address in the container is 32 bits long = 4 bytes) LDR R2, =g_current_task_ptr // load the address of the destination variable STR R1, [R2] // and store there the contents of R1 // prepare to save context. Restore affected registers. remember: these registers // are shared among IRQ mode and user mode LDMIA R13!, {R0 - R3} // ************************************************************ // save the processor context // ************************************************************ LDR R13, =g_current_task_ptr LDR R13, [R13] // R13 now points to the current task tcb block STMIA R13, {R0 - LR}^ // save R0 - R14 from the beginning of the block (note that // each tcb starts with the task's processor register values) NOP // the RealView compiler compels this ADD R13, #56 // move further in the tcb MRS R0, SPSR // copy SPSR into R0 STMIB R13, {R0, LR} // save SPSR and the return address. note: the return address // saved here is the return address of the IRQ - hence, where in the task the // processor should resume operation. the above save LR (user mode LR - LR^) represents // the returns address local to the task itself - for example, if a function inside // the task was preempted, the return address from it shall be stored there. // ************************************************************ // select the next task to be executed // ************************************************************ // restore R13 to point to the IRQ stack before the call is done, because R13 points // into a user stack. if the function's assembly pushes registers onto the // stack, user data will get corrupted. notice how 'g_irq_stack_base' is initialized // at the startup file, and the scatter file of the linker prevents it from being // set to 0 by the program loader. LDR R13, =g_irq_stack_base LDR R13, [R13]
// this call sets the value of 'g_current_task_index', hence selected the next task to run LDR R2, =scheduler_select_next_task BLX R2 // save the new value required to place the pointer at the right offset to the next tcb. LDR R2, =g_current_task_index // save the index of the next task to run in R3 (see the label 'normal_context_switch' // to see why and where it is used) LDR R3, [R2] B normal_context_switch context_not_saved // the context switch ISR was started with a backup of the registers R0-R3. // the program flow so far did not pop these registers out of the IRQ stack. // prevent a stack overflow. note that R3 is needed for the selection of the right tcb, // so restoring the registers must take place first LDMIA R13!, {R0 - R3} LDR R1, =g_current_task_index // address of index to tcb LDR R2, =g_startup_task_index LDR R3, [R2] // load the index of the first task to run in R3 // save the selected startup task to the current running task to the address // of 'g_current_task_index'. R1 points to 'g_current_task_index'. STR R3, [R1] normal_context_switch // ************************************************************ // point to the tcb block representing the next task // ************************************************************ // R3 contains the index of the next task to execute LDR R0, =g_tasks_tcb // base address of tcb container LDR R2, =g_next_task_ptr // point to the address of the next task index LDR R1, [R0, R3, LSL#2] // R1 = R0 + R3*4 (multiply by 4 becasue each address in the container is 32 bits long) STR R1, [R2] // store the address at the variable // acknowledge the interrupt. TIM0 is serviced by VIC0. LDR R0, =vic0 LDR R0, [R0] // the address of interrupt controller 0 STR R0, [R0, #48] // store dummy value in the vector address register // clear TIM0 interrupt flag LDR R0, =tim0 LDR R0, [R0] ADD R0, #28 // point to the SR field MOV R1, #0x4000 // this is the required mask BIC R1, R0, R1 // AND NOT STR R1, [R0] // store at the right location // the 'g_software_interrupt' flag indicates whether or not a context switch was forced by // software. if so, the flag must be cleared and 'VIC_SWITCmd' must be called again // (the previous call happened when the context switch was forced // in 'scheduler_reschedule') in order to disable the software interrupt. LDR R0, =g_software_interrupt LDR R1, [R0] CMP R1, #0 BEQ hardware_interrupt // software forced context switch // clear 'g_software_interrupt' flag MOV R1, #0 STR R1, [R0] // prepare parameters and call 'VIC_SWITCmd' to clear the software interrupt MOV R0, #4 MOV R1, #0 BL VIC_SWITCmd hardware_interrupt // ************************************************************ // restore the processor context // ************************************************************ LDR R13, =g_next_task_ptr LDR R13, [R13] // R13 now points to the next task tcb block // now, the SPSR must come first in order not to spoil the user mode register R0. // note that R0 (or any other register) is required in order to handle the // save/restore of SPSR. ADD R13, #60 // point to the point where R14 and SPSR meet (see tcb_setup) LDMIA R13, {R0, LR} // load SPSR and the return address (notice the IA variant of LDM) MSR SPSR_cxsf, R0 LDMDB R13, {R0 - LR}^ // load user mode registers (notice the DB variant of LDM) // notice also that the user mode's R13 is updated, too, which is the key to a fast // context switch: unlike in the C166 architecture, the contents of the stack do // not have to be copied to a specific region in the memory which is dedicated // to stack memory. the ARM allows to set R13 as a pointer, to different places // in memory. also note that unlike in the C166, a simple // "push into system stack/select next task/copy task stack to system/pop system stack" // scheme will not work here, because of the processor modes: once in an ISR, the // ARM9 is in IRQ mode. pushing into the stack will update the IRQ stack, not the // user mode stack. NOP // the RealView compiler compels this //*********************************************************************/ // restore R13 to point to the IRQ stack. // note: it is assumed that nested interrupts are not allowed, otherwise // this would certainly be incorrect (because the IRQ stack might contain // function call parameters or backuped registers etc.). //*********************************************************************/ LDR R13, =g_irq_stack_base LDR R13, [R13] // ARM architecture requires that 4 is subtracted from PC upon return from an // IRQ handler SUBS PC, LR, #4 }
#pragma arm section zidata = "non_init_2" __attribute__ ((zero_init)) int32u g_irq_stack_base ; #pragma arm section zidata
The variable 'g_irq_stack_base' is placed in a section that is not initialized to zero upon startup. That is required because after the startup file sets the variable (during stack setup),
;*********************************************************** ; required for restoration of IRQ stack after context switch IMPORT g_irq_stack_base LDR R1, =g_irq_stack_base STR R0, [R1] ;***********************************************************
that branch to '__main' will reset it. Notice that this code is developed to be build with a RealView compiler. I have not made a GNU variant yet.
Greetings,
Tamir
Please note that 'scheduler_select_next_task' is a function that selects the next task to be run. I will not post it here as it has no value for this thread. The simplified logic at the original post can be used instead.