Stacktrace

Overview

This set of example/test programs serves to explore stack traces created when encountering Errors or Faults for different use cases and situations. The main purpose is to demonstrate that the stack traces correctly represent the state of the MCU as regards the stack, or stacks in the cases using kernel threads.

There are three pairs of programs, each pair comprised of a variant not using kernel threads, and one based on threads, for the following situations:

  1. Stacktr0.mod, StacktrK2C0.mod: a plain chain of procedure calls, resulting in an Error on core 0, and a Fault on core 1.
  2. Stacktr1.mod, StacktrK2C1.mod: a chain of procedure calls, with one setting a first interrupt pending, and then the corresponding interrupt handler setting a second interrupt pending, that is, creating a nested interrupt. The interrupt handlers call intermediate procedures.
  3. Stacktr2.mod, StacktrK2C2.mod: same as 2, but without intermediate procedures, ie. the first interrupt handler immediately sets the second interrupt pending.

Remarks:

  • Errors and Faults caught in a plain chain of procedure calls, including creating a stack trace, are the rather bland baseline. Not a lot to write home about here.
  • Errors and Faults caught in exceptions, including creating a stack trace, are more interesting and challenging cases.

For each case, both MCU cores are used, to check that Error and Fault handling is indeed independent for each core.

Main sections:

  • Test Case 0: run Stacktr0.mod and StacktrK2C0.mod on both RP2040 and RP2350, comparing and interpreting the results, reverting back to the corresponding stack dumps as needed and useful
  • Test Case 1: run Stacktr1.mod and StacktrK2C1.mod on both RP2040 and RP2350, same
  • Test Case 2: run Stacktr2.mod and StacktrK2C2.mod on both RP2040 and RP2350, same
  • Miscellaneous: stack switching, tail chaining, false positives
  • Bottom Line

Triggering Interrupts from Software

The corresponding test cases (2. and 3. as listed above) trigger interrupts by setting their pending bit in the NVIC. If we want the interrupts being triggered synchronously, ie. before any instructions are executed after pending the interrupt, the ARM architecture manuals stipulate to use memory barriers in order to make sure that the pending bits are recognised and acknowledged by the subsequent instructions (DSB, ISB). Here, we omit using memory barriers, as this results in more challenging stack trace situations for testing purposes. In particular, the pending bit is not immediately recognised, and the MCU continues to execute code while the instruction that sets the pending bit completes. This results in different behaviour and stack traces for the RP2040 and the RP2350.

The required memory barrier instructions are inserted into the test code, but commented out. If we uncomment these instructions, the stack traces for both RPs are the same, as the handlers for the triggered interrupts execute synchronously with the triggering code, ie. execute right when the pending bit is set in the code. To be clear, in a “non-test” program, we most likely want to insert the memory barriers.

Further reading:

  • RP2040: ARM v6-M Architecture Reference Manual, section B2.5, Barrier support for system correctness
  • RP2350: ARM v8-M Architecture Reference Manual, section B7.2.9, Memory barriers

Basis

Running the Test Programs

When running the test programs, you may get different values for stack and code addresses, due to revisions of the programs, including removing test code in RuntimeErrors – needed to get the stack dump values presented here.1

Notably, the code address values also depend on the code start address configured. All code below was compiled and linked with a code start address of 010000100H on both the RP2040 and RP2350.

Terminal Output

Note that the terminal outputs presented below are created by RuntimeErrorsOut. The stack tracing algorithm itself does not imply any order or presentation of output, including omitting parts of the data, if so desired. Just “plug in” your own output procedure, or modify RuntimeErrorsOut.PrintException to your liking.

Shorthands

  • To avoid lengthy wording, “exception h0” and similar are used to refer to the exception that is handled by handler h0.
  • PSP and ‘process stack’, or MSP and ‘main stack’, are used quite loosely, but hopefully sufficiently clear in the context.
  • “Pending behaviour” means the behaviour of the MCU when an exception is pending.

Test Case 0: Stacktr0.mod, StacktrK2C0.mod

The straightforward, “normal” cases without any application exception handlers.

RP2040

Running Stacktr0.mod we get for core 0:

run-time error in thread mode: 7 core: 0
integer divided by zero or negative divisor
Stacktr0.error  addr: 10004A38H  ln: 32
trace:
  Stacktr0.error             10004A38H    32   2002FB18H
  Stacktr0.p2                10004A70H    40   2002FB24H
  Stacktr0.p1                10004A8EH    54   2002FB2CH
  Stacktr0.p0                10004A9EH    62   2002FB30H
  Stacktr0.run               10004AD6H    70   2002FB34H
  Stacktr0..init             10004B06H    76   2002FB3CH
  start.sequence             10004B80H         2002FB40H
stacked registers:
 psr: 61000000H
  pc: 10004A3AH
  lr: 10004A75H
 r12: 0A0B0C0DH
  r3: 00000002H
  r2: 10003DC4H
  r1: 00000000H
  r0: 00000000H
  sp: 2002FB00H
current registers:
  sp: 2002FACCH
  lr: FFFFFFF9H
  pc: 10002D4AH
 psr: 6000000BH

Each stack trace line shows:

  • the procedure name;
  • the address where the procedure “above” in the trace was called;
  • the source code line number, if available;
  • the stack address where the reference to the call address was found.

For the procedure “on top” of the trace, here Stacktr0.error, the address is the Error or Fault address.

For core 1, omitting the recorded registers for brevity:

MCU fault in thread mode: 3 core: 1
HardFault
Stacktr0.fault  addr: 10004A1CH
trace:
  Stacktr0.fault             10004A1CH         2003FFD0H
  Stacktr0.p2                10004A7CH    42   2003FFDCH
  Stacktr0.p1                10004A8EH    54   2003FFE4H
  Stacktr0.p0                10004A9EH    62   2003FFE8H
  Stacktr0.run               10004AD6H    70   2003FFECH
  MultiCore.init             100049B2H   108   2003FFF4H

Note that on core 0, we can create a trace to the very beginnings of the program in the start-up sequence, which runs on core 0, while on core 1 the execution and trace starts with MultiCore.init, which is the procedure that initialises core 1, and then starts the program on core 1 – see MultiCore.StartCoreOne.

Running the kernel-threaded StacktrK2C0.mod, we get for core 1:

run-time error in thread mode: 7 core: 1
integer divided by zero or negative divisor
StacktrK2C0.error  addr: 1000543CH  ln: 38
trace:
  StacktrK2C0.error          1000543CH    38   2003F6D0H
  StacktrK2C0.p2             10005480H    48   2003F6DCH
  StacktrK2C0.p1             10005492H    54   2003F6E4H
  StacktrK2C0.p0             100054A2H    61   2003F6E8H
  StacktrK2C0.run            100054BEH    66   2003F6ECH
  StacktrK2C0.t0c            100054CEH    72   2003F6F0H

The bottom of the stack trace here is the base procedure of the thread: StacktrK2C0.t0c.

RP2350

On the RP2350, running Stacktr0.mod we get for core 0:

run-time error in thread mode: 7 core: 0
integer divided by zero or negative divisor
Stacktr0.error  addr: 10004AC4H  ln: 32
trace:
  Stacktr0.error             10004AC4H    32   2003FB18H
  Stacktr0.p2                10004AECH    40   2003FB24H
  Stacktr0.p1                10004B06H    54   2003FB2CH
  Stacktr0.p0                10004B16H    62   2003FB30H
  Stacktr0.run               10004B54H    70   2003FB34H
  Stacktr0..init             10004B88H    76   2003FB3CH
  start.sequence             10004C00H         2003FB40H

Unsurprisingly, the same source line numbers as with the RP2040, but different addresses both for the procedure calls as well as for the stack.

Test Case 1: Stacktr1.mod, StacktrK2C1.mod

These two programs implement two application exception handlers, with procedure calls in-between triggering the interrupts, on both cores, with and without kernel threads. Interrupts are triggered by setting their state to pending from software, omitting memory barriers.

RP2040

Running Stacktr1.mod we get for core 1:

MCU fault in handler mode: 3 core: 1
HardFault
Stacktr1.fault  addr: 100051D0H
trace:
  Stacktr1.fault             100051D0H         2003FF48H
  Stacktr1.i2                10005230H    47   2003FF54H
  Stacktr1.i1                10005242H    53   2003FF5CH
  Stacktr1.i0                1000524EH    58   2003FF60H
  --- exc ---
  Stacktr1.h2                10005262H         2003FF78H
  Stacktr1.h1                10005290H    78   2003FF9CH
  Stacktr1.h0                100052A2H    83   2003FFA8H
  --- exc ---
  Stacktr1.p1                100052C8H         2003FFC0H
  Stacktr1.p0                100052DEH   105   2003FFE8H
  Stacktr1.run               1000535EH   119   2003FFECH
  MultiCore.init             10004C46H   108   2003FFF4H

Running StacktrK2C1.mod we get for core 1:

MCU fault in handler mode: 3 core: 1
HardFault
StacktrK2C1.fault  addr: 10005BD4H
trace:
  StacktrK2C1.fault          10005BD4H         2003FF88H
  StacktrK2C1.i2             10005C34H    50   2003FF94H
  StacktrK2C1.i1             10005C46H    56   2003FF9CH
  StacktrK2C1.i0             10005C52H    61   2003FFA0H
  --- exc ---
  StacktrK2C1.h2             10005C66H         2003FFB8H
  StacktrK2C1.h1             10005C94H    81   2003FFD8H
  StacktrK2C1.h0             10005CA6H    86   2003FFE4H
  --- exc ---
  StacktrK2C1.p1             10005CC6H         2003F6C8H
  StacktrK2C1.p0             10005CDAH   106   2003F6E8H
  StacktrK2C1.run            10005CF6H   111   2003F6ECH
  StacktrK2C1.t0c            10005D06H   117   2003F6F0H
  • How to read: p1 was interrupted by h0, h2 was interrupted by i0.
  • --- exc --- is the annotation that RuntimeErrorsOut.PrintStacktrace writes at the corresponding marker as inserted by RuntimeErrors.stacktrace (described here), indicating the exception.

RP2350

Running Stacktr1.mod we get for core 1:

MCU fault in handler mode: 6 core: 1
UsageFault
Stacktr1.fault  addr: 10004CFCH
trace:
  Stacktr1.fault             10004CFCH         2007FF30H
  Stacktr1.i2                10004D44H    47   2007FF3CH
  Stacktr1.i1                10004D52H    53   2007FF44H
  Stacktr1.i0                10004D60H    58   2007FF48H
  --- exc ---
  Stacktr1.h1                10004DB4H         2007FF70H
  Stacktr1.h0                10004DC0H    83   2007FF98H
  --- exc ---
  Stacktr1.p1a               10004DCCH         2007FFC0H
  Stacktr1.p0                10004E02H   105   2007FFE8H
  Stacktr1.run               10004E8AH   119   2007FFECH
  MultiCore.init             10004C7EH   108   2007FFF4H
  • Compare to the RP2040 case – not the same trace:
    • h0 interrupted p1a, not p1;
    • i0 interrupted h1, not h2.
  • This means:
    • p1 had already called p1a when the interrupt was accepted;
    • h2 had already returned to h1 when the interrupt was accepted.
  • Based on the stack dumps (see below), the stack traces of the RP2040 and the RP2350 are not the same simply since the MCU stack state is not. This is due to the different pending behaviour of the MCUs if not using memory barriers, which causes the MCU to execute a different number of instructions until the pending status is acknowledged.

A question remains: why do we not see p1 in the trace anyway, since it’s the caller of p1a?

Let’s analyse the status of the stack at the point of the failure. The following is an annotated plain dump of the stack values above the stack frame created by the Error handler:

    stack addr   stack value
    ----------   -----------
    -- MultiCore.init push
    2007FFF8H    00000181H  lr
    -- run push & sub sp,#4
    2007FFF4H    10004C83H  lr
    2007FFF0H    2003FB44H  x: INTEGER
    -- p0 push
    2007FFECH    10004E8FH  lr
    -- p1 push & sub sp,#4
    2007FFE8H    10004E07H  lr
    2007FFE4H    0000000DH  VAR y: INTEGER
    -- h0 stacking
>     2007FFE0H    200002A8H    double-word padding
>     2007FFDCH    09000200H    psr
>     2007FFD8H    10004DCCH    pc= return address in p1a
>     2007FFD4H    10004DEFH    lr
>     2007FFD0H    0A0B0C0DH    r12 = marker
>     2007FFCCH    00008000H    r3
>     2007FFC8H    E000E104H    r2
>     2007FFC4H    00004000H    r1
>     2007FFC0H    E000E204H    r0
    -- h0 push
    2007FFBCH    FFFFFFF9H  lr
    2007FFB8H    FFFFFFFFH  r11
    2007FFB4H    FFFFFFFFH  r10
    2007FFB0H    FFFFFFFFH  r9
    2007FFACH    FFFFFFFFH  r8
    2007FFA8H    E000ED08H  r7
    2007FFA4H    40078000H  r6
    2007FFA0H    E000E400H  r5
    2007FF9CH    00000001H  r4
    -- h1 push & sub sp,#8
    2007FF98H    10004DC5H  lr
    2007FF94H    00000001H  VAR r: REAL
    2007FF90H    3F800000H  VAR cid: INTEGER
    -- i0 stacking
>     2007FF8CH    2900003EH    psr
>     2007FF88H    10004DB4H    pc
>     2007FF84H    10004DB5H    lr
>     2007FF80H    0A0B0C0DH    r12
>     2007FF7CH    00008000H    r3
>     2007FF78H    E000E104H    r2
>     2007FF74H    00008000H    r1
>     2007FF70H    E000E204H    r0
    -- i0 push
    2007FF6CH    FFFFFFF1H  lr
    2007FF68H    FFFFFFFFH  r11
    2007FF64H    FFFFFFFFH  r10
    2007FF60H    FFFFFFFFH  r9
    2007FF5CH    FFFFFFFFH  r8
    2007FF58H    E000ED08H  r7
    2007FF54H    40078000H  r6
    2007FF50H    E000E400H  r5
    2007FF4CH    00000001H  r4
    -- i1 push
    2007FF48H    10004D65H  lr
    -- i2 push & sub sp,#4
    2007FF44H    10004D57H  lr
    2007FF40H    00000001H  VAR cid: INTEGER
    -- fault push (leaf procedure)
    2007FF3CH    10004D49H  lr

> denotes the stack frames identified by the stack trace scanning logic.

There’s no trace of p1 indeed on the stack: its bl.w return address in the link register has never been pushed. Remember that a procedure call is only registered on the stack if, or when, the callee pushes the link register, containing the call’s return address, onto the stack.

If we check the exception return address (pc) inside the stack frame of h0, above, we read 10004DCCH.

Partial assembly listing:

  PROCEDURE p1a;
    VAR x: INTEGER;
  BEGIN
.   220  010004DCCH      0B500H  push      { lr }   <= return address
.   222  010004DCEH      0B081H  sub       sp,#4
    x := 42
  END p1a;
.   224  010004DD0H      0202AH  movs      r0,#42
.   226  010004DD2H      09000H  str       r0,[sp]
.   228  010004DD4H      0B001H  add       sp,#4
.   230  010004DD6H      0BD00H  pop       { pc }

  PROCEDURE p1;
    VAR y: INTEGER;
  BEGIN
.   232  010004DD8H      0B500H  push      { lr }
.   234  010004DDAH      0B081H  sub       sp,#4
    y := 13;
.   236  010004DDCH      0200DH  movs      r0,#13
.   238  010004DDEH      09000H  str       r0,[sp]
    (* set int for h0 pending *)
    SYSTEM.PUT(MCU.PPB_NVIC_ISPR0 + ((IntNo0 DIV 32) * 4), {IntNo0 MOD 32});
.   240  010004DE0H  0F8DF0014H  ldr.w     r0,[pc,#20] -> 264
.   244  010004DE4H  0F2440100H  movw      r1,#16384
.   248  010004DE8H      06001H  str       r1,[r0]
    (*SYSTEM.EMIT(MCU.DSB); SYSTEM.EMIT(MCU.ISB);*)
    p1a
  END p1;
.   250  010004DEAH  0F7FFFFEFH  bl.w      -34 -> 220
.   254  010004DEEH      0E000H  b         0 -> 258
.   256  010004DF0H      00063H  <LineNo: 99>
.   258  010004DF2H      0B001H  add       sp,#4
.   260  010004DF4H      0BD00H  pop       { pc }
.   262  010004DF6H      0BF00H  nop
.   264  010004DF8H  0E000E204H  <Const:  -536813052>

Address 10004DCCH contains push { lr }, which would be the instruction that “registers” p1 on the stack, but the exception interrupted p1a before the push was executed. Hence, no trace of p1 in this particular situation.

To compare, let’s look at the stack dump on the RP2040, running Stacktr1.mod.

Pro memoria, the stack trace:

MCU fault in handler mode: 3 core: 1
HardFault
Stacktr1.fault  addr: 100051D0H
trace:
  Stacktr1.fault             100051D0H         2003FF48H
  Stacktr1.i2                10005230H    47   2003FF54H
  Stacktr1.i1                10005242H    53   2003FF5CH
  Stacktr1.i0                1000524EH    58   2003FF60H
  --- exc ---
  Stacktr1.h2                10005262H         2003FF78H
  Stacktr1.h1                10005290H    78   2003FF9CH
  Stacktr1.h0                100052A2H    83   2003FFA8H
  --- exc ---
  Stacktr1.p1                100052C8H         2003FFC0H
  Stacktr1.p0                100052DEH   105   2003FFE8H
  Stacktr1.run               1000535EH   119   2003FFECH
  MultiCore.init             10004C46H   108   2003FFF4H

The stack dump:

    stack addr   stack value
    ----------   -----------
    -- MultiCore.init push
    2003FFF8H    0000017FH  lr
    -- run push & sub sp,#4
    2003FFF4H    10004C4BH  lr
    2003FFF0H    2002FB44H  VAR x: INTEGER
    -- p0 push
    2003FFECH    10005363H  lr
    -- p1 push & sub sp,#4
    2003FFE8H    100052E3H  lr
    2003FFE4H    0000000DH  VAR y: INTEGER
    -- h0 stacking
>     2003FFE0H    40800000H    double-word padding
>     2003FFDCH    01000200H    psr: bit 9 set => alignment/padding
>     2003FFD8H    100052C8H    pc = return address in p1
>     2003FFD4H    100052E3H    lr
>     2003FFD0H    0A0B0C0DH    r12
>     2003FFCCH    00000002H    r3
>     2003FFC8H    10003DC4H    r2
>     2003FFC4H    04000000H    r1
>     2003FFC0H    E000E200H    r0
    -- h0 push
    2003FFBCH    FFFFFFF9H  lr
    2003FFB8H    E000ED00H  r7  
    2003FFB4H    00000179H  r6
    2003FFB0H    00000009H  r5
    2003FFACH    1000452DH  r4
    -- h1 push & sub sp,#8
    2003FFA8H    100052A7H  lr
    2003FFA4H    00000001H  VAR r: REAL
    2003FFA0H    3F800000H  VAR cid: INTEGER
    -- h2 push
    2003FF9CH    10005295H  lr
    -- i0 stacking
>     2003FF98H    2003FFC0H    double-word padding
>     2003FF94H    0100022AH    psr
>     2003FF90H    10005262H    pc
>     2003FF8CH    10005295H    lr
>     2003FF88H    0A0B0C0DH    r12 = marker
>     2003FF84H    00000002H    r3
>     2003FF80H    10003DC4H    r2
>     2003FF7CH    08000000H    r1
>     2003FF78H    E000E200H    r0
    -- i0 push  
    2003FF74H    FFFFFFF1H  lr
    2003FF70H    E000ED00H  r7
    2003FF6CH    00000179H  r6
    2003FF68H    00000009H  r5
    2003FF64H    1000452DH  r4
    -- i1 push
    2003FF60H    10005253H  lr
    -- i2 push & sub sp,#4
    2003FF5CH    10005247H  lr
    2003FF58H    00000001H  VAR cid: INTEGER
    -- fault push & sub sp,#4
    2003FF54H    10005235H  lr
    2003FF50H    E000E101H  VAR x: INTEGER

The exception return address pc of h0 is 100052C8H, of i0 it’s 10005262H.

Partial assembly listing:

  PROCEDURE h2;
  BEGIN
.   152  010005258H      0B500H  push      { lr }
    (* set int for i0 pending *)
    SYSTEM.PUT(MCU.PPB_NVIC_ISPR0 + ((IntNo1 DIV 32) * 4), {IntNo1 MOD 32});
.   154  01000525AH      04802H  ldr       r0,[pc,#8] -> 164
.   156  01000525CH      02101H  movs      r1,#1
.   158  01000525EH      006C9H  lsls      r1,r1,#27
.   160  010005260H      06001H  str       r1,[r0]
    (*SYSTEM.EMIT(MCU.DSB); SYSTEM.EMIT(MCU.ISB)*)
  END h2;
.   162  010005262H      0BD00H  pop       { pc }   <= return address
.   164  010005264H  0E000E200H  <Const:  -536813056>

  PROCEDURE p1;
    VAR y: INTEGER;
  BEGIN
.   248  0100052B8H      0B500H  push      { lr }
.   250  0100052BAH      0B081H  sub       sp,#4
    y := 13;
.   252  0100052BCH      0200DH  movs      r0,#13
.   254  0100052BEH      09000H  str       r0,[sp]
    (* set int for h0 pending *)
    SYSTEM.PUT(MCU.PPB_NVIC_ISPR0 + ((IntNo0 DIV 32) * 4), {IntNo0 MOD 32});
.   256  0100052C0H      04804H  ldr       r0,[pc,#16] -> 276
.   258  0100052C2H      02101H  movs      r1,#1
.   260  0100052C4H      00689H  lsls      r1,r1,#26
.   262  0100052C6H      06001H  str       r1,[r0]
    (*SYSTEM.EMIT(MCU.DSB); SYSTEM.EMIT(MCU.ISB);*)
    p1a
  END p1;
.   264  0100052C8H  0F7FFFFF0H  bl.w      -32 -> 236   <= return address
.   268  0100052CCH      0E000H  b         0 -> 272
.   270  0100052CEH      00063H  <LineNo: 99>
.   272  0100052D0H      0B001H  add       sp,#4
.   274  0100052D2H      0BD00H  pop       { pc }
.   276  0100052D4H  0E000E200H  <Const:  -536813056>
  • 100052C8H is indeed the address of the call (bl.w) of p1a in p1, right after the store instruction that sets interrupt h0 pending.
  • 10005262H is the address of pop { pc } in h2, again right after the store instruction that sets interrupt i0 pending.

That is:

  • The RP2350 has executed more instructions after setting the interrupt pending, before the latter is accepted by the MCU, due to not using memory barriers.
  • However, the stack trace in both cases is reporting correctly what it finds on the stack, or stacks with kernel threads. The difference between traces for the RP2040 and RP2350 is a different MCU (stack) state.

Test Case 2: Stacktr2.mod, StacktrK2C2.mod

These two programs implement two application exception handlers, without procedure calls in-between triggering the interrupts (“back-to-back”), on both cores, with and without kernel threads. Interrupts are triggered by setting their state to pending from software.

RP2040

Running Stacktr2.mod gives for core 0:

run-time error in handler mode: 7 core: 0
integer divided by zero or negative divisor
Stacktr2.i0  addr: 10004CB4H  ln: 36
trace:
  Stacktr2.i0                10004CB4H    36   2002FAD0H
  --- exc ---
  Stacktr2.h0                10004CEAH         2002FAE8H
  --- exc ---
  Stacktr2.p1                10004CFAH         2002FB10H
  Stacktr2.p0                10004D06H    64   2002FB30H
  Stacktr2.run               10004D86H    78   2002FB34H
  Stacktr2..init             10004DBEH    95   2002FB3CH
  start.sequence             10004E3CH         2002FB40H
stacked registers:
 psr: 6100022BH
  pc: 10004CB6H
  lr: FFFFFFF1H
 r12: 0A0B0C0DH
  r3: 00000002H
  r2: 10003DC4H
  r1: 00000000H
  r0: 00000000H
  sp: 2002FAB8H
current registers:
  sp: 2002FA84H
  lr: FFFFFFF1H
  pc: 10002D4AH
 psr: 6000000BH

Running StacktrK2C2.mod gives for core 0 (omitting the registers):

run-time error in handler mode: 7 core: 0
integer divided by zero or negative divisor
StacktrK2C2.i0  addr: 100056B8H  ln: 33
trace:
  StacktrK2C2.i0             100056B8H    33   2002FAF8H
  --- exc ---
  StacktrK2C2.h0             100056EEH         2002FB10H
  --- exc ---
  StacktrK2C2.p1             100056FEH         2002F200H
  StacktrK2C2.p0             1000570AH    56   2002F220H
  StacktrK2C2.run            10005726H    61   2002F224H
  StacktrK2C2.t0c            10005736H    67   2002F228H

Since we know already :) that the RP2350 trace will be different, here’s the stack dump for Stacktr2.mod as baseline:

    stack addr   stack value
    ----------   -----------
    -- .init push
    2002FB40H    10004E41H  lr
    -- run push & sub sp, #4
    2002FB3CH    10004DC3H  lr
    2002FB38H    2002FB44H  VAR x: INTEGER
    -- p0 push
    2002FB34H    10004D8BH  lr
    -- p1 push
    2002FB30H    10004D0BH  lr
    -- h0 stacking
>     2002FB2CH    01000000H    psr
>     2002FB28H    10004CFAH    pc
>     2002FB24H    10004D0BH    lr
>     2002FB20H    0A0B0C0DH    r12
>     2002FB1CH    00000002H    r3
>     2002FB18H    10003DC4H    r2
>     2002FB14H    04000000H    r1
>     2002FB10H    E000E200H    r0
    -- h0 push
    2002FB0CH    FFFFFFF9H  lr = EXC_RETURN
    -- i0 stacking
>     2002FB08H    0000000DH    double-word padding
>     2002FB04H    0100022AH    psr
>     2002FB00H    10004CEAH    pc
>     2002FAFCH    FFFFFFF9H    lr = EXC_RETURN
>     2002FAF8H    0A0B0C0DH    r12 = marker
>     2002FAF4H    00000002H    r3
>     2002FAF0H    10003DC4H    r2
>     2002FAECH    08000000H    r1
>     2002FAE8H    E000E200H    r0
    -- i0 push & sub sp,#4
    2002FAE4H    FFFFFFF1H  lr
    2002FAE0H    1000452DH  r4
    2002FADCH    00000000H  VAR x: INTEGER
  • Note the EXC_RETURN value at the stacked lr position in the i0 stack frame – one of the possible values when creating the stack trace to identify a stack frame to be skipped, as described here.
  • Also note that even though i0 is a leaf procedure, it does use the stack for variable x, which is different from the RP2350, see below (conforming to Astrobe’s documentation).

RP2350

Running Stacktr2.mod gives for core 0 (omitting the registers):

run-time error in handler mode: 7 core: 0
integer divided by zero or negative divisor
Stacktr2.i0  addr: 10004D02H  ln: 36
trace:
  Stacktr2.i0                10004D02H    36   2003FB00H
  --- exc ---
  Stacktr2.p0                10004D52H         2003FB10H
  Stacktr2.run               10004DD6H    78   2003FB34H
  Stacktr2..init             10004E0CH    95   2003FB3CH
  start.sequence             10004E88H         2003FB40H

Again, a different stack trace compared to the RP2040: h0 and p1 are “missing”. The question, however, is if the stack trace as presented corresponds to the stack contents:

    stack addr   stack value
    ----------   -----------
    -- .init push
    2003FB40H    10004E8DH  lr
    -- run push & sub sp,#4
    2003FB3CH    10004E11H  lr
    2003FB38H    2003FB44H  VAR x: INTEGER
    -- p0 push
    2003FB34H    10004DDBH  lr
    -- i0 stacking
>     2003FB30H    10004D53H    double-word padding
>     2003FB2CH    09000200H    psr
>     2003FB28H    10004D52H    pc
>     2003FB24H    10004D53H    lr
>     2003FB20H    0A0B0C0DH    r12
>     2003FB1CH    00008000H    r3
>     2003FB18H    E000E104H    r2
>     2003FB14H    00004000H    r1
>     2003FB10H    E000E204H    r0
    -- i0 push
    2003FB0CH    FFFFFFF9H  lr = EXC_RETURN
  • EXC_RETURN = FFFFFFF9H (see i0 push) tells us that i0 interrupted code in thread mode, and will return using the main stack pointer, that is, i0 interrupted p0, which we can confirm via return address pc = 10004D52H:
  PROCEDURE p0;
  BEGIN
.    88  010004D48H      0B500H  push      { lr }
    SYSTEM.LDREG(12, 0A0B0C0DH); (* marker *)
.    90  010004D4AH  0F8DFC00CH  ldr.w     r12,[pc,#12] -> 104
    p1
  END p0;
.    94  010004D4EH  0F7FFFFF1H  bl.w      -30 -> 68
.    98  010004D52H      0E000H  b         0 -> 102     <= return address
.   100  010004D54H      00040H  <LineNo: 64>
.   102  010004D56H      0BD00H  pop       { pc }
.   104  010004D58H  00A0B0C0DH  <Const:  168496141>
  • Hence, the stack trace represents the state of the RP2350 MCU correctly.
  • Note that leaf procedure i0 does not use stack space for variable x, but handles all computations in registers (see the RP2040 case above; again, as specified by Astrobe’s docs).

Miscellaneous

Stack Switch

As described, with kernel threads, we need to switch from the main stack pointer to the process stack pointer when scanning to create the stack trace.

Stack trace from StacktrK2C2.mod on RP2040:

run-time error in handler mode: 7 core: 0
integer divided by zero or negative divisor
StacktrK2C2.i0  addr: 100056B8H  ln: 33
trace:
  StacktrK2C2.i0             100056B8H    33   2002FAF8H
  --- exc ---
  StacktrK2C2.h0             100056EEH         2002FB10H
  --- exc ---
  StacktrK2C2.p1             100056FEH         2002F200H
  StacktrK2C2.p0             1000570AH    56   2002F220H
  StacktrK2C2.run            10005726H    61   2002F224H
  StacktrK2C2.t0c            10005736H    67   2002F228H
  • t0c, run, p0, p1: the base thread program runs using the process stack pointer (PSP, 2002F2xx range)
  • h0 stacking still using PSP
  • h0 handler runs using main stack pointer( MSP, 2002FBxx range)
  • i0 stacking in MSP
  • i0 handler runs using MSP

Hence, we should see one stacking using the PSP (h0), one using the MSP (i0) – which we do in the stack trace above, the last column is the stack address.

We can also check in detail the stack addresses used in the stack dump to identify the stack switch:

    stack addr   stack value
    ----------   -----------
== PSP used
    -- t0c push
    2002F22CH    00000000H
    -- run push
    2002F228H    1000573BH
    -- p0 push
    2002F224H    1000572BH
    -- p1 push
    2002F220H    1000570FH
    -- h0 stacking
>     2002F21CH    01000000H
>     2002F218H    100056FEH
>     2002F214H    1000570FH
>     2002F210H    0A0B0C0DH
>     2002F20CH    00000000H
>     2002F208H    2000032CH
>     2002F204H    04000000H
>     2002F200H    E000E200H

== MSP used
    -- h0 push
    2002FB30H    FFFFFFFDH
    -- i0 stacking
>     2002FB2CH    0100002AH
>     2002FB28H    100056EEH  pc = return address in h0
>     2002FB24H    FFFFFFFDH
>     2002FB20H    0A0B0C0DH
>     2002FB1CH    00000000H
>     2002FB18H    2000032CH
>     2002FB14H    08000000H
>     2002FB10H    E000E200H
    -- i0 push
    2002FB0CH    FFFFFFF1H          ^ scan direction
    2002FB08H    2002FB24H          |
    2002FB04H    00000000H          |

Scanning is done in the main stack first, until we find an EXC_RETURN value at the top of the stack (FFFFFFFDH), then switch to the process stack. FFFFFFFDH means (RP2040): return to thread mode using the PSP.

As an aside, if you’re eagle-eyed you may have noticed that there’s no stack space for h0’s VAR x. The return address pc in the i0 stacking is 100056EEH:

  PROCEDURE* h0[0];
    VAR x: INTEGER;
  BEGIN
.    56  0100056DCH      0B500H  push      { lr }
.    58  0100056DEH      0B081H  sub       sp,#4
    x := 13;
.    60  0100056E0H      0200DH  movs      r0,#13
.    62  0100056E2H      09000H  str       r0,[sp]
    (* set int for i0 pending *)
    SYSTEM.PUT(MCU.PPB_NVIC_ISPR0 + ((IntNo1 DIV 32) * 4), {IntNo1 MOD 32});
.    64  0100056E4H      04802H  ldr       r0,[pc,#8] -> 76
.    66  0100056E6H      02101H  movs      r1,#1
.    68  0100056E8H      006C9H  lsls      r1,r1,#27
.    70  0100056EAH      06001H  str       r1,[r0]
    (*SYSTEM.EMIT(MCU.DSB); SYSTEM.EMIT(MCU.ISB)*)
    (*x := 17*)
  END h0;
.    72  0100056ECH      0B001H  add       sp,#4
.    74  0100056EEH      0BD00H  pop       { pc }           <= return address
.    76  0100056F0H  0E000E200H  <Const:  -536813056>

That is, h0 had already executed add sp,#4 when it was interrupted, which shows that also the RP2040 can execute further instructions until the pending bit for i0 is acknowledged. In fact, ARM recommends memory barriers also for the RP2040 when writing to the System Control Space.

Tail Chaining

If we set the two interrupt priorities for h0 and i0 to the same value, we see tail chaining, ie. i0 will use the same exception stacking as h0.

Running Stacktr2.mod on the RP2040, with equal interrupt priorities, gives this stack trace for core 0 on the RP2040:

run-time error in handler mode: 7 core: 0
integer divided by zero or negative divisor
Stacktr2.i0  addr: 10004CB4H  ln: 36
trace:
  Stacktr2.i0                10004CB4H    36   2002FAF8H
  --- exc ---
  Stacktr2.p1                10004CFAH         2002FB10H
  Stacktr2.p0                10004D06H    64   2002FB30H
  Stacktr2.run               10004D86H    79   2002FB34H
  Stacktr2..init             10004DBEH    96   2002FB3CH
  start.sequence             10004E3CH         2002FB40H

Based on this (not annotated, I am sure you recognise the pattern by now) stack dump, with only one block of stacked registers:

    2002FB40H    10004E41H
    2002FB3CH    10004DC3H
    2002FB38H    2002FB44H
    2002FB34H    10004D8BH
    2002FB30H    10004D0BH
>     2002FB2CH    01000000H
>     2002FB28H    10004CFAH
>     2002FB24H    10004D0BH
>     2002FB20H    0A0B0C0DH
>     2002FB1CH    00000002H
>     2002FB18H    10003DC4H
>     2002FB14H    04000000H
>     2002FB10H    E000E200H
    2002FB0CH    FFFFFFF9H
    2002FB08H    1000452DH
    2002FB04H    00000000H

That is, h0 sets i0 pending, which however does not pre-empt h0, but executes after h0 returns, using the same stacked registers. Therefore we only see i0 in the stack trace.

False Positives

As explained, there can be false positives. Uncomment (* VAR x, y: INTEGER; *) in Stacktr0.p0:

  PROCEDURE p0;
    CONST R12 = 12;
    VAR x, y: INTEGER;
  BEGIN
    SYSTEM.LDREG(R12, 0A0B0C0DH);
    p1
  END p0; 

And we get on core 0 on the RP2040:

run-time error in thread mode: 7 core: 0
integer divided by zero or negative divisor
Stacktr0.error  addr: 10004A38H  ln: 32
trace:
  Stacktr0.error             10004A38H    32   2002FB10H
  Stacktr0.p2                10004A70H    40   2002FB1CH
  Stacktr0.p1                10004A8EH    54   2002FB24H
  Stacktr0.p0                10004AA0H    62   2002FB28H
  Out.Ln                     1000263CH    51   2002FB2CH
  Stacktr0.run               10004ADAH    70   2002FB34H
  Stacktr0..init             10004B0AH    76   2002FB3CH
  start.sequence             10004B84H         2002FB40H

False positive: Out.Ln.

Bottom Line

Correct Stack Traces

The stack traces created by RuntimeErrors correctly represent the call chain of the failing program as registered on the stack. The stack trace features work with “plain” programs as well as with kernel threads, and also with exceptions in both cases. Stack traces are a useful programmer’s tool. The possibility of false positives does not diminish the overall value, since they are usually pretty easily spotted, and at times even usefully point out not initialised local variables.

Memory Barriers for Synchronous Interrupts

The test cases trigger exceptions by setting them pending. As demonstrated, the behaviour of the RP2040 and RP2350 in this case is not the same, as we’re not using memory barriers, which would ensure that the mutation of the corresponding NVIC register in the System Control Space has completed before continuing. If you run the test cases with the memory barrier instructions uncommented, you’ll get equal stack traces for both MCUs. For testing reasons the memory barriers are omitted to get more interesting test results to challenge the stack trace algorithm.

If we want strictly synchronous interrupts with our program code, we must use the memory barriers. For example, if there is interaction between the MCU thread mode code and handler mode code, eg. when sharing data, we want this synchronicity.

Notes

  • In RuntimeError.stacktrace, there are commented-out output instructions that produce the stack dumps used above. Look for debug in the comments.
  • The repo contains the RP2350 variants of the “extended” assembly list files for the test programs used above, even though they are generated and usually not kept in the repository. The assembly listings were produced with the stack dump code in RuntimeError still active, in order to be as close as possible to the listings used above as regards code addresses.

Output Terminal

See Set-up, two-terminal set-up.

Build and Run

Repository


  1. The code creating the stack dumps is still included in RuntimeErrors.stacktrace, commented out. This will be removed at some point (or handled using conditional compilation with the forthcoming next major release of Astrobe). ↩︎