3

For a university project, our project teams wants to write a bare-metal operating system for the Lego Mindstorm EV3 plattform. We encountered a problem regarding the interrupt handling which we were unable to solve, although we did a lot of research and testing.

This are details of the plattform:
CPU: ARM926EJ-S (ARMv5 architecture)
SoC: TexasInstruments Sitara AM1808

SoC-Short Documentation: http://www.ti.com/lit/ds/symlink/am1808.pdf
SoC-Technical Reference Manual: http://www.ti.com/lit/ug/spruh82a/spruh82a.pdf

Here is what we try to do and how we try to do it:
We want to initialize a timer interrupt which should be triggered every millisecond in order to get a system tick. This tick is required by various other components of our operating system (e.g. Software-I2C-Management, etc.). Since our team is quite new to the bare-metal programming, we followed an example by TexasInstruments to intialize the timer interrupt and interrupt controller of the SoC. This example is part of StarterWare and designed to run on an AM1808 evaluation board. Since the Lego Mindstorm EV3 uses the same SoC, it should work for us as well. See the following source code:

startup.S: Entry-Point of our program (based on TI provided source code)

#include "hw_aintc.h"
#include "soc_AM1808.h"

    .global Entry
    .global _stack                  
    .global _bss_start
    .global _bss_end
    .global start_boot
    .global _Reset
    .global IRQHandler
    .global FIQHandler
    .global AbortHandler
    .global SWIHandler
    .global UndefInstHandler
    .global CPUAbortHandler

@************************ Internal Definitions ********************************
@
@ Define the stack sizes for different modes. The user/system mode will use
@ the rest of the total stack size
@
    .set  UND_STACK_SIZE, 0x8
    .set  ABT_STACK_SIZE, 0x8
    .set  FIQ_STACK_SIZE, 0x8
    .set  IRQ_STACK_SIZE, 0x500
    .set  SVC_STACK_SIZE, 0x8

@
@ to set the mode bits in CPSR for different modes
@        
    .set  MODE_USR, 0x10            
    .set  MODE_FIQ, 0x11
    .set  MODE_IRQ, 0x12
    .set  MODE_SVC, 0x13
    .set  MODE_ABT, 0x17
    .set  MODE_UND, 0x1B
    .set  MODE_SYS, 0x1F  

    .equ  I_F_BIT, 0xC0 
    .equ ADDR_HIPVR1, SOC_AINTC_0_REGS + AINTC_HIPVR(0)
    .equ ADDR_HIPVR2, SOC_AINTC_0_REGS + AINTC_HIPVR(1)
    .equ MASK_SWI_NUM, 0xFF000000

@**************************** Code Seection ***********************************
    .text

@
@ This code is assembled for ARM instructions
@
    .code 32
@******************************************************************************
@
@******************************************************************************
@
@ The reset handler sets up the stack pointers for all the modes. The FIQ and
@ IRQ shall be disabled during this. Then, clearthe BSS sections, switch to the
@ main() function. 
@
Entry:
@
@ Set up the Stack for Undefined mode
@
     LDR   r0, =_stack                     @ Read the stack address
     MSR   cpsr_c, #MODE_UND|I_F_BIT       @ switch to undef  mode
     MOV   sp,r0                           @ write the stack pointer
     SUB   r0, r0, #UND_STACK_SIZE         @ give stack space
@
@ Set up the Stack for abort mode
@        
     MSR   cpsr_c, #MODE_ABT|I_F_BIT       @ Change to abort mode
     MOV   sp, r0                          @ write the stack pointer
     SUB   r0,r0, #ABT_STACK_SIZE          @ give stack space
@
@ Set up the Stack for FIQ mode
@       
     MSR   cpsr_c, #MODE_FIQ|I_F_BIT       @ change to FIQ mode
     MOV   sp,r0                           @ write the stack pointer
     SUB   r0,r0, #FIQ_STACK_SIZE          @ give stack space
@
@ Set up the Stack for IRQ mode
@       
     MSR   cpsr_c, #MODE_IRQ|I_F_BIT       @ change to IRQ mode
     MOV   sp,r0                           @ write the stack pointer
     SUB   r0,r0, #IRQ_STACK_SIZE          @ give stack space
@
@ Set up the Stack for SVC mode
@        
     MSR   cpsr_c, #MODE_SVC|I_F_BIT       @ change to SVC mode
     MOV   sp,r0                           @ write the stack pointer
     SUB   r0,r0, #SVC_STACK_SIZE          @ give stack space
@
@ Set up the Stack for USer/System mode
@      
     MSR   cpsr_c, #MODE_SYS|I_F_BIT       @ change to system mode
     MOV   sp,r0                           @ write the stack pointer

@
@ Clear the BSS section here
@
Clear_Bss_Section:

     LDR   r0, =_bss_start                 @ Start address of BSS
     LDR   r1, =(_bss_end - 0x04)          @ End address of BSS
     MOV   r2, #0  
Loop: 
     STR   r2, [r0], #4                    @ Clear one word in BSS
     CMP   r0, r1
     BLE   Loop                            @ Clear till BSS end


@
@ Enter the start_boot function. The execution still happens in system mode
@
Enter_main:
     LDR   r10,=start_boot                 @ Get the address of start_boot
     MOV   lr,pc                           @ Dummy return 
     BX    r10                             @ Branch to start_boot

@ Interrupt Handler from exceptionhandler.S (provided by TI) - Note: the following code 
@ will not be reached since start_boot contains an endless loop at it's end
@******************************************************************************
@*                  Function Definition of SWI Handler
@******************************************************************************    
@
@ The SWI Handler switches to system mode if the SWI number is 458752. If the
@ SWI number is different, no mode switching will be done. No other SWI are 
@ handled here
@
SWIHandler:
    STMFD    r13!, {r0-r1, r14}       @ Save context in SVC stack
    LDR      r0, [r14, #-4]           @ R0 points to SWI instruction
    BIC      r0, r0, #MASK_SWI_NUM    @ Get the SWI number
    CMP      r0, #458752
    MRSEQ    r1, spsr                 @ Copy SPSR  
    ORREQ    r1, r1, #0x1F            @ Change the mode to System
    MSREQ    spsr_cf, r1              @ Restore SPSR
    LDMFD    r13!, {r0-r1, pc}^       @ Restore registers from IRQ stack

@******************************************************************************
@*                  Function Definition of IRQ Handler
@******************************************************************************    
@
@ The IRQ handler jumps to the ISR of highest priority pending IRQ. The address
@ is taken from the HIPVR2 register, which contains the ISR address of highest
@ pending IRQ. This handler doesnot support nesting.
@
IRQHandler:
    STMFD    r13!, {r0-r3, r12, r14}  @ Save context in IRQ stack
    LDR      r0, =ADDR_HIPVR2         @ R0 points to address of HIPVR2
    LDR      r1, [r0]                 @ R1 contains address of ISR
    ADD      r14, pc, #0              @ Save return address in LR 
    LDR      pc, [r1]                 @ Go to ISR (still in IRQ mode)
    LDMFD    r13!, {r0-r3, r12, r14}  @ Restore registers from IRQ stack
    SUBS     pc, r14, #0x4            @ Return to program before IRQ

@******************************************************************************
@*                  Function Definition of FIQ Handler
@******************************************************************************    
@
@ The FIQ Handler jumps to the ISR of the highest priority pending FIQ. The
@ address is taken from HIPVR1, which contains the ISR address of the highest
@ pending FIQ. This handler doesnot support nesting
@
FIQHandler:
@
@ Save the required context in FIQ stack. 
@
    STMFD    r13!, {r0-r3, r12, r14}  @ Save context in FIQ stack
    LDR      r0, =ADDR_HIPVR1         @ R0 points to address of HIPVR1
    LDR      r1, [r0]                 @ R1 contains address of ISR
    ADD      r14, pc, #0              @ Save return address in LR 
    LDR      pc, [r1]                 @ Go to ISR (still in FIQ mode)
    LDMFD    r13!, {r0-r3, r12, r14}  @ Restore registers from FIQ stack
    SUBS     pc, r14, #0x4            @ Return to program state before FIQ 

@******************************************************************************
@*             Function Definition of Abort/Undef Handler
@******************************************************************************    
@
@ The Abort handler goes to the C handler of abort mode. Note that the undefined
@ instruction is not handled separately.
@ if nothing is done in the abort mode, the execution enters infinite loop.
@
AbortHandler:
UndefInstHandler:
@
@ Disable all the interrupts
@
    MRS     r0, cpsr                  @ Read from CPSR
    ORR     r0, r0, #0xC0             @ Clear the IRQ and FIQ bits    
    MSR     cpsr, r0                  @ Write to CPSR
    ADD     r14, pc, #0               @ Store the return address
    LDR     pc, =CPUAbortHandler      @ Go to C handler

test.c: our application and initialization code provided by TI

#include <stdio.h>
#include <stdlib.h>
#include <systick.h>
#include <mytypes.h>
#include "cpu.h"

extern void Entry(void);
extern void UndefInstHandler(void);
extern void SWIHandler(void);
extern void AbortHandler(void);
extern void IRQHandler(void);
extern void FIQHandler(void);

// This is our own IRQ-Handler - it is not called either, we just wanted to check if the assembler code was the source of the problem
void irqHandler(void) {
    unsigned int* isr_pointer = *(unsigned int *)*((unsigned int*)0xFFFEF604);
    printf("ISR Handler called: %#10x\n", isr_pointer);
    typedef void func(void);
    func* f = (func*) isr_pointer;
    f();
}

void c_entry() {
    unsigned int counter = 1;
    systick_init();  
    U32 localSystick = systick_get_ms();
    // printf will write debug output to a UART port connected to a PC
    printf("Current tick: %u (%u)\n", localSystick, counter);
    do {
        ++counter;
        localSystick = systick_get_ms();
        printf("Current tick: %u (%u)\n", localSystick, counter);
    } while (1);
}


// Code from Startup.c (TI example)
#include "hw_syscfg0_AM1808.h"
#include "hw_syscfg1_AM1808.h"
#include "hw_pllc_AM1808.h"
#include "hw_ddr2_mddr.h"
#include "soc_AM1808.h"
#include "evmAM1808.h"
#include "hw_types.h"
#include "psc.h"

#define E_PASS    0
#define E_FAIL    -1

static void CopyVectorTable(void);
void BootAbort(void);

static unsigned int const vecTbl[16]=
{
    // Primary vector Table entries
    (unsigned int)Entry,
    (unsigned int)UndefInstHandler,
    (unsigned int)SWIHandler,
    (unsigned int)AbortHandler,
    (unsigned int)AbortHandler,
    0xE59FF010,
    (unsigned int)IRQHandler,
    (unsigned int)FIQHandler,
    // Secondary Vector Table entires
    (unsigned int)Entry,
    (unsigned int)UndefInstHandler,
    (unsigned int)SWIHandler,
    (unsigned int)AbortHandler,
    (unsigned int)AbortHandler,
    0xE59FF010,
    (unsigned int)IRQHandler,
    (unsigned int)FIQHandler
};


/**
 * \brief   Boot strap function which enables the PLL(s) and PSC(s) for basic
 *          module(s)
 *
 * \param   none
 *
 * \return  None.
 * 
 * This function is the first function that needs to be called in a system.
 * This should be set as the entry point in the linker script if loading the
 * elf binary via a debugger, on the target. This function never returns, but
 * gives control to the application entry point
 **/
unsigned int start_boot(void) 
{
    printf("start_boot called\n");
    SysCfgRegistersLock();

    /* Disable write-protection for registers of SYSCFG module. */
    SysCfgRegistersUnlock();

    /* Initialize the vector table with opcodes */
    CopyVectorTable();

    c_entry();

    while(1);
}


static void CopyVectorTable(void)
{
    printf("CopyVectorTable called\n");
    // According to the AM1808 Technical Reference Manual (Page 88), the vector table has to be located at 0xFFFF0000
    unsigned int *dest = (unsigned int *)0xFFFF0000;
    unsigned int *src =  (unsigned int *)vecTbl;
    unsigned int count;

    for(count = 0; count < sizeof(vecTbl)/sizeof(vecTbl[0]); count++)
    {  
        dest[count] = src[count];
    }
}


void BootAbort(void)
{
    printf("BootAbort called");
    while (1);
}

systick.c: here we initialize the timer interrupt and the interrupt controller

/*
 *  This provides a 1000Hz tick for the system.
 *
 *  We're using the AMT1808 Hardware Timer 2
 * 
 *  See also: timerCounter.c in TI/examples/evmAM1808/timer
 */

#include "soc_AM1808.h"
#include "hw_syscfg0_AM1808.h"
#include "interrupt.h"
#include "timer.h"
#include "evmAM1808.h"
#include "cpu.h"

#include "systick.h"

#include <stdio.h>

#define TMR_PERIOD_LSB32               (0x07FFFFFF)
#define TMR_PERIOD_MSB32               (0x0)

static volatile U32 systick_ms;

/* ISR, called 1000 times per second */
void
systick_isr_C(void)
{
    /* Disable the timer interrupt */
    TimerIntDisable(SOC_TMR_2_REGS, TMR_INT_TMR12_NON_CAPT_MODE);

    printf("ISR called");

    /* Clear the interrupt statusIntChannelSet(SYS_INT_TIMR2_ALL, 0); in AINTC */
    IntSystemStatusClear(SYS_INT_TIMR2_ALL);
    TimerIntStatusClear(SOC_TMR_2_REGS, TMR_INT_TMR12_NON_CAPT_MODE);

    ++systick_ms;

    /* Enable the timer interrupt */
    TimerIntEnable(SOC_TMR_2_REGS, TMR_INT_TMR12_NON_CAPT_MODE);
}


U32
systick_get_ms(void)
{
    return systick_ms;
}


void
systick_init(void)
{
    /* Setup timer for 64 bit mode */
    /* Configuration of Timer */
    TimerConfigure(SOC_TMR_2_REGS, TMR_CFG_64BIT_CLK_INT);

    /* Set the 64 bit timer period */
    TimerPeriodSet(SOC_TMR_2_REGS, TMR_TIMER12, TMR_PERIOD_LSB32);
    TimerPeriodSet(SOC_TMR_2_REGS, TMR_TIMER34, TMR_PERIOD_MSB32);

    /* Set up the ARM Interrupt Controller for generating timer interrupt */
    /* Initialize AINTC and register timer interrupt */
    IntAINTCInit();

    /* Register the Timer ISR */
    IntRegister(SYS_INT_TIMR2_ALL, systick_isr_C);

    /* Set the channel number for Timer interrupt, it will map to IRQ */
    IntChannelSet(SYS_INT_TIMR2_ALL, 2);

    /* Enable IRQ for ARM (in CPSR)*/
    IntMasterIRQEnable();

    /* Enable AINTC interrupts in GER */
    IntGlobalEnable();

    /* Enable IRQ in AINTC */
    IntIRQEnable();

    /* Enable timer interrupts in AINTC */
    IntSystemEnable(SYS_INT_TIMR2_ALL);

    /* Enable the timer interrupt */
    TimerIntEnable(SOC_TMR_2_REGS, TMR_INT_TMR12_NON_CAPT_MODE);

    /* Start the timer */
    TimerEnable(SOC_TMR_2_REGS, TMR_TIMER12, TMR_ENABLE_CONT);

    // Deactivate interrupts on CPU - for test purposes
    //IntMasterIRQDisable();
}

All other source code files required (e.g. for controlling the interrupt controller) are unmodified versions of TI's StarterWare code (it provides drivers to control the various hardware components of the SoC like the interrupt controller, timers, etc.). I could add them here, but I am not sure if they are of any use. I looked them through and for me it looks like there is nothing strange happening in this code.

We use the Uboot bootloader of the EV3 the run our program. This bootloader is already part of the default firmware. It loads the programm which is located on an SD card to 0xC1000000 (an adress which maps to the RAM of the EV3) and jumps the the program's entry point ("Entry:" in assembler code). No problem so far, the code compiles and is executed as expected.

And here is the problem:
The interrupt is triggered as expected as well. This happens excatly after 455 cycles of the loop in c_entry() every time the programm is executed. But as soon as the interrupt is triggered, the program stops without any error or something. Neither the interupt service routine (systick_isr_C in systick.c) nor the interrupt handler (IRQHandler in assembler code or if we set our own handler irqHandler in test.c) is called. Our guess is that this problems is caused by the fact that the interrupt vector table might be at the wrong location, although 0xFFFF0000 is the address described in the manual (this address maps to the ARM CPU local RAM).

If we deactivate the interrupt handling on the CPU (with the last line of code in systick.c --> systick_init()) and check the register indicating that an interrupt was triggered manually, everything works perfectly fine. In this case we trigger the irqHandler-function directly. We do this using the following code:

unsigned int* isr_pointer = *(unsigned int *)*((unsigned int*)0xFFFEF604);
if (isr_pointer != 0) {
    irqHandler();
}

We also tried setting the irqHandler as default handler for all possible interrupts (just to make sure no other interrupt was triggered). This did not lead to any other result.

Unfortunatly we have no JTAG adapter at the moment, which would allow us to check the registers when the interrupt is triggered (e.g. program counter). We expect to get on soon, but for now we have to work without one.

Has anyone any idea what might be the problem or why our interrupt handler is not called at all?

I tried to describe the problem as detailed as possible, but if I can provide any further information that might be useful I am happy to do so.

Thanks for any hint in advance.

  • Is the code for this project available somewhere? I'm looking into making my own EV3 OS as well. – Pepijn Dec 28 '16 at 13:41

1 Answers1

1

After more research and experimenting, we were able to solve the problem. In case anyone is interested in the solution, here is what caused the problem:

Problem 1:
The interrupt vector was not properly initialized. In contrast to the TexasInstruments example, the vector should not contain the addresses of the interrupt handlers. Instead, it should contain machine code operations. Anything that changes the Program Counter to the correct address works, e.g. B ... (Branch) or LDR pc, ... (Load program counter).

Here is the new code to solve this problem:

In startup.S:

ExceptionHandler:
    B Entry
    B ExceptionHandler
    B ExceptionHandler
    B ExceptionHandler
    B ExceptionHandler
    B ExceptionHandler
    B IRQHandler
    B ExceptionHandler

In test.c:

/* Copy the vector table to it's destination at 0xFFFF0000 - this address is specified in the board's manual. */
static void CopyVectorTable(void)
{
    unsigned int *dest = (unsigned int *)0xFFFF0000;
    /* The address of the assembler exception vector */
    unsigned int *addrExceptionHandler = (unsigned int *)ExceptionHandler;
    int i;

    /* We only set vector 1 to 7 and leave the Reset vector in peace */
    /* Important: don't set the address of the vectors - we need to write the operation (i.e. the machine code) to that location */
    for (i = 1; i < 8; ++i) {
        dest[i] = addrExceptionHandler[i];
    }

    /* This code is required in order for the branch instructions (B ...) to work */
    for (; i < 2048; ++i) {
        dest[i] = addrExceptionHandler[i];
    }
}

Problem 2:
The IRQ-Handler written in assembler was unable to determine the correct address of the corresponding ISR. But it was possible to call any C-function we wanted. So we decided to delegate the task of calling the correct ISR to our own IRQ-Handler written in C (see test.c --> irqHandler).

In startup.S:

IRQHandler:
    STMFD    r13!, {r0-r3, r12, r14}  @ Save context in IRQ stack
    ADD      r14, pc, #0              @ Save return address in LR 
    @ For whatever reason, we are unable to get the correct address of the ISR in this assembler code
    @ So we just call our C handler which has a static address in RAM and let it handle the interrupt
    @LDR      pc, =systick_isr_C       @ Works as well, but is not dynamic - we could only handle 1 interrupt
    LDR      pc, =irqHandler
    LDMFD    r13!, {r0-r3, r12, r14}  @ Restore registers from IRQ stack
    SUBS     pc, r14, #0x4            @ Return to program before IRQ

This solved out problems. Maybe this can help someone in the future so I figured I would post our solution.