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:
Stacktr0.mod
,StacktrK2C0.mod
: a plain chain of procedure calls, resulting in an Error on core 0, and a Fault on core 1.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.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
andStacktrK2C0.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
andStacktrK2C1.mod
on both RP2040 and RP2350, same - Test Case 2: run
Stacktr2.mod
andStacktrK2C2.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 handlerh0
. PSP
and ‘process stack’, orMSP
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 byh0
,h2
was interrupted byi0
. --- exc ---
is the annotation thatRuntimeErrorsOut.PrintStacktrace
writes at the corresponding marker as inserted byRuntimeErrors.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
interruptedp1a
, notp1
;i0
interruptedh1
, noth2
.
- This means:
p1
had already calledp1a
when the interrupt was accepted;h2
had already returned toh1
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
) ofp1a
inp1
, right after the store instruction that sets interrupth0
pending.10005262H
is the address ofpop { pc }
in h2, again right after the store instruction that sets interrupti0
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 stackedlr
position in thei0
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 variablex
, 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
(seei0
push) tells us thati0
interrupted code in thread mode, and will return using the main stack pointer, that is,i0
interruptedp0
, which we can confirm via return addresspc = 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 variablex
, 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 PSPh0
handler runs using main stack pointer( MSP,2002FBxx
range)i0
stacking in MSPi0
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 fordebug
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
- for RP2040/Pico and RP2350/Pico2: Stacktrace
-
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). ↩︎