Electronic – the reason the PIC16 multitasking RTOS kernel doesn’t work

assemblycmicrocontrollerpicrtos

I am trying to create a semi-pre-emptive (co-operative) RTOS for PIC x16 microcontrollers. In my previous question, I've learnt that accessing hardware stack pointer is not possible in these cores. I have looked at this page in PIClist, and this is what I am trying to implement using C.

My compiler is Microchip XC8 and currently I am working on a PIC16F616 with 4MHz internal RC oscillator selected in the configuration bits.

I have learnt that I can access PCLATH and PCL registers with C, looking at the header file of my compiler. So, I tried to implement a simple task switcher.

It works as wanted in the debugger if I pause the debugger after restart, reset, and set PC at cursor when the cursor is not on the first line (TRISA=0;) but on an another line (for example ANSEL=0;). In the first start of the debugger I get these messages in the Debugger Console:

Launching
Programming target
User program running
No source code lines were found at current PC 0x204

Edit: I don't know what made it work, but debugger now works perfectly. So, omit the above output and paragraph.

Edit: Changing the main definition like this makes the code below work. This starts the main function at program address 0x0099. I don't know what causes this. This is not a real solution. I am now guessing that there is a compiler specific error.

void main(void) @ 0x0099
{

Here is my C code:

/* 
 * File:   main.c
 * Author: abdullah
 *
 * Created on 10 Haziran 2012 Pazar, 14:43
 */
#include <xc.h> // Include the header file needed by the compiler
__CONFIG(FOSC_INTOSCIO & WDTE_OFF & PWRTE_ON & MCLRE_OFF & CP_OFF & IOSCFS_4MHZ & BOREN_ON);
/*
 * INTOSCIO oscillator: I/O function on RA4/OSC2/CLKOUT pin, I/O function on RA5/OSC1/CLKIN
 * WDT disabled and can be enabled by SWDTEN bit of the WDTCON register
 * PWRT enabled
 * MCLR pin function is digital input, MCLR internally tied to VDD
 * Program memory code protection is disabled
 * Internal Oscillator Frequency Select bit : 4MHz
 * Brown-out Reset Selection bits : BOR enabled
 */

/*
 * OS_initializeTask(); definition will copy the PCLATH register to the task's PCLATH holder, which is held in taskx.pch
 * This will help us hold the PCLATH at the point we yield.
 * After that, it will copy the (PCL register + 8) to current task's PCL holder which is held in taskx.pcl.
 * 8 is added to PCL because this line plus the "return" takes 8 instructions.
 * We will set the PCL after these instructions, because
 * we want to be in the point after OS_initializeTask when we come back to this task.
 * After all, the function returns without doing anything more. This will initialize the task's PCLATH and PCL.
 */
#define OS_initializeTask(); currentTask->pch = PCLATH;\
                             currentTask->pcl = PCL + 8;\
                             asm("return");

/*
 * OS_yield(); definition will do the same stuff that OS_initializeTask(); definition do, however
 * it will return to "taskswitcher" label, which is the start of OS_runTasks(); definition.
 */

#define OS_yield();          currentTask->pch = PCLATH;\
                             currentTask->pcl = PCL + 8;\
                             asm("goto _taskswitcher");

/*
 * OS_runTasks(); definition will set the "taskswitcher" label. After that it will change the
 * current task to the next task, by pointing the next item in the linked list of "TCB"s.
 * After that, it will change the PCLATH and PCL registers with the current task's. That will
 * make the program continue the next task from the place it left last time.
 */

#define OS_runTasks();       asm("_taskswitcher");\
                             currentTask = currentTask -> next;\
                             PCLATH = currentTask->pch;\
                             PCL = currentTask->pcl;

typedef struct _TCB // Create task control block and type define it as "TCB"
{
    unsigned char pch; // pch register will hold the PCLATH value of the task after the last yield.
    unsigned char pcl; // pcl register will hold the PCL value of the task after the last yield.
    struct _TCB* next; // This pointer points to the next task. We are creating a linked list.
} TCB;

TCB* currentTask; // This TCB pointer will point to the current task's TCB.

TCB task1; // Define the TCB for task1.
TCB task2; // Define the TCB for task2.

void fTask1(void); // Prototype the function for task1.
void fTask2(void); // Prototype the function for task2.

void main(void)
{
    TRISA = 0; // Set all of the PORTA pins as outputs.
    ANSEL = 0; // Set all of the analog input pins as digital i/o.
    PORTA = 0; // Clear PORTA bits.

    currentTask = &task1; // We will point the currentTask pointer to point the first task.

    task1.next = &task2; // We will create a ringed linked list as follows:
    task2.next = &task1; // task1 -> task2 -> task1 -> task2 ....

    /*
     * Before running the tasks, we should initialize the PCL and PCLATH registers for the tasks.
     * In order to do this, we could have looked up the absolute address with a function pointer.
     * However, it seems like this is not possible with this compiler (or all the x16 PICs?)
     * What this compiler creates is a table of the addresses of the functions and a bunch of GOTOs.
     * This will not let us get the absolute address of the function by doing something like:
     * "currentTask->pcl=low(functionpointer);"
     */
    fTask1(); // Run task1 so that we get the address of it and initialize pch and pcl registers.
    currentTask = currentTask -> next; // Point the currentTask pointer to the next pointer which
    fTask2(); // is task2. And run task2 so that we get the correct pch and pcl.

    OS_runTasks(); // Task switcher. See the comments in the definitions above.
}

void fTask1(void)
{
    OS_initializeTask(); // Initialize the task
    while (1)
    {
        RA0 = ~RA0; // Toggle PORTA.0
        OS_yield(); // Yield
        RA0 = ~RA0; // Toggle PORTA.0
    }
}

void fTask2(void)
{
    OS_initializeTask(); // Initialize the task
    while (1)
    {
        RA1 = ~RA1; // Toggle PORTA.1
        OS_yield(); // Yield
        RA1 = ~RA1; // Toggle PORTA.1
    }
}

And here is the disassembly listing file that my compiler created. Starts at line 74.

I have programmed the actual chip, and no change on PORTA at all; it doesn't work.

What is the reason my program doesn't work?

Best Answer

What you are trying to do is tricky, but very educational (if you are prepared to spend a lot of effort).

First, you must realise that this kind of PC-only (as opposed to PC+SP) task switching (which is the only thing you can do on a plain 12 or 14-bit PIC core) will only work when all the yield() statements in a task are in the same funtion: they can't be in a called function, and the compiler must not have messed with the function structure (as optimization might do).

Next:

currentTask->pch = PCLATH;\
currentTask->pcl = PCL + 8;\
asm("goto _taskswitcher");
  • You seem to assume that PCLATH is the upper bits of the program counter, as PCL is the lower bits. This is NOT the case. When you write to PCL the PCLATH bits are written to the PC, but the upper PC bits are never (implicitly) written to PCLATH. Re-read the relevant section of the datasheet.
  • Even if PCLATH was the upper bits of the PC, this would get you into trouble when the instruction after the goto is on not on the same 256-instruction 'page' as the first instruction.
  • the plain goto will not work when _taskswitcher is not in the current PCLATH page, you will need an LGOTO or equivalent.

A solution to your PCLATH problem is to declare a label after the goto, and write the lower and upper bits of that label to your pch and pcl locations. But I am not sure you can declare a 'local' label in inline assembly. You sure can in plain MPASM (Olin will smile).

Lastly, to this kind of context switching you must save and restore ALL context that the compiler might depend on, which might include

  • indirection register(s)
  • status flags
  • scratch memory locations
  • local variables that might overlap in memory because the compiler does not realise that your tasks must be independent
  • other things I can't imagine right now but the compiler author might use in the next version of the compiler (they tend to be very imaginative)

The PIC architecture is more problematic in this respect because a lot of resources are loacted all over the memory map, where more traditional architectures have them in registers or on the stack. As a consequence, PIC compilers often do not generate reentrant code, which is what you definitely need to do the things you want (again, Olin will probaly smile and assemble along.)

If you are into this for the joy of writng an task switcher I suggest that you swicth to a CPU that has a more traditional organization, like an ARM or Cortex. If you are stuck with your feet in a concrete plate of PICs, study existing PIC switchers (for instance salvo/pumkin?).