Skip to content

Commit 9e0777f

Browse files
committed
feat(freertos-s32z2): boot FreeRTOS to a UART heartbeat on Cortex-R52
Bring the kernel up on real S32Z2 hardware: NXP RTD startup/linker (referenced in place from the FreeRTOS example), board_init (Clock_Ip + Siul2 pin mux + UART9 console), newlib stubs, the FreeRTOS generic-timer tick, and the pthread shim that maps the common/ node threads onto FreeRTOS tasks. Several Cortex-R52-specific walls are fixed here: - cp15_arm.S provides ARM-mode wrappers (bx lr) for NXP's Thumb CP15/MPU/GIC/VFP helpers, whose `mov pc, lr` epilogue does not interwork on this core and returns to ARM callers still in Thumb state; it also wires the undefined- instruction vector to the port's lazy-FPU handler and the IRQ vector to the dispatcher. - vendor_patched/port.c fixes the ARM solicited-resume path that restored SPSR_irq with a stray Thumb T-bit (inherited from interrupted Thumb libm), which ran resumed ARM tasks as Thumb — the wall that previously blocked reaching the controller. - FreeRTOSConfig.h sets configCPU_CLOCK_HZ to the 40 MHz generic-timer base (a 1 kHz tick), configSTACK_DEPTH_TYPE to uint32_t (the default 16-bit type truncated large stack depths to 0 and tripped the overflow hook), and the FPU context options. - operator_new.cpp routes C++ new/delete onto pvPortMalloc (libstdc++ new otherwise hits the 16 KiB newlib heap and throws in the controller ctor). - heap_in_sram.ld / node_stack_in_sram.ld relocate the FreeRTOS heap and the controller node stack into the 7 MiB int_sram region (int_sram_dram is full); freertos_threading.h places the node stack via .node_stack. vApplicationStackOverflowHook reports the offending task over UART so a too-small stack names itself instead of hanging silently. Stacked on the build-system PR. Signed-off-by: Yutaka Kondo <yutaka.kondo@youtalk.jp>
1 parent e776d3b commit 9e0777f

13 files changed

Lines changed: 2828 additions & 0 deletions

File tree

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// Copyright (c) 2026, Arm Limited and contributors.
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
// S32Z2 board bring-up: RTD init, UART9 console, newlib _write retarget.
5+
6+
#include <stdint.h>
7+
#include <unistd.h>
8+
#include <errno.h>
9+
10+
#include "platform/freertos/s32z2/board_init.h"
11+
12+
#include "Clock_Ip.h"
13+
#include "Siul2_Port_Ip.h"
14+
#include "Linflexd_Uart_Ip.h"
15+
#include "Linflexd_Uart_Ip_VS_0_PBcfg.h"
16+
17+
// S32CT-generated pin-mux table. NUM_OF_CONFIGURED_PINS comes from the board
18+
// Siul2_Port_Ip_Cfg.h; g_pin_mux_InitConfigArr_VS_0 (the actual pad settings)
19+
// from the generated PB cfg. This table muxes the NETC ETH0 RGMII pins
20+
// (SIUL2_1[58..65] -> ALT1 = ETH_0_RGMII_TXD/RXD/TXC/TXCTL/RXCTL/RX_CLK) plus
21+
// the PHY GPIO control pins. The lwip example applies it via Port_Init(); we
22+
// apply it directly with Siul2_Port_Ip_Init() (see board_init()).
23+
#include "Siul2_Port_Ip_Cfg.h"
24+
#include "Siul2_Port_Ip_VS_0_PBcfg.h"
25+
26+
// AUTOSAR Mcu_Init / Port_Init / Platform_Init would require Mcu_Cfg.c /
27+
// Port_Cfg.c / Platform_Cfg.c — those PB configs are absent from the lwip
28+
// S32CT example (Components view does not instantiate them), so the build
29+
// filters them out. The NXP startup.s + SystemInit handle MCU bring-up
30+
// before main(); we still need the IP-layer Clock_Ip_Init below to enable
31+
// the LINFLEXD9 module clock that SystemInit does not touch.
32+
33+
// Mcu_aClockConfigPB[] is defined in Clock_Ip_VS_0_PBcfg.c (S32CT
34+
// generated). The lwip example uses the AUTOSAR-Mcu naming
35+
// (Mcu_aClockConfigPB) rather than the bare Clock_Ip_aClockConfig from
36+
// non-AUTOSAR examples — both are the same Clock_Ip_ClockConfigType
37+
// array. Element 0 is the default ClockConfig0 that turns the LINFLEXD9
38+
// module clock on.
39+
extern const Clock_Ip_ClockConfigType Mcu_aClockConfigPB[];
40+
41+
// LINFLEXD instance 9 is wired to the X-S32Z27X-DC FT232RQ console
42+
// (matches the Zephyr `_D` overlay's `zephyr,console = &uart9`).
43+
// PB cfg symbol is generated by S32CT into Linflexd_Uart_Ip_VS_0_PBcfg.c.
44+
extern const Linflexd_Uart_Ip_UserConfigType Linflexd_Uart_Ip_xHwConfigPB_0_VS_0;
45+
46+
// SIUL2_5 PORT150 → ALT1 → LIN_9_TX. The S32CT Pins tool added the routing
47+
// to the .mex but the Update Code action did not regenerate the Siul2_Port
48+
// PB cfg, so we configure the pad inline via the RTD IP-layer API.
49+
static const Siul2_Port_Ip_PinSettingsConfig kLin9TxPin = {
50+
.base = IP_SIUL2_5,
51+
.pinPortIdx = 150U,
52+
.mux = PORT_MUX_ALT1,
53+
.safeMode = PORT_SAFE_MODE_DISABLED,
54+
.pullConfig = PORT_INTERNAL_PULL_NOT_ENABLED,
55+
.slewRateCtrlSel = PORT_SLEW_RATE_CONTROL0,
56+
.receiverSel = PORT_RECEIVER_NOT_AVAILABLE,
57+
.terminationResistor = PORT_TERMINATION_RESISTOR_NOT_AVAILABLE,
58+
.currentReferenceControl = PORT_CURRENT_REFERENCE_CONTROL_NOT_AVAILABLE,
59+
.rxCurrentBoost = PORT_RX_CURRENT_BOOST_NOT_AVAILABLE,
60+
.openDrain = PORT_OPEN_DRAIN_DISABLED,
61+
.inputBuffer = PORT_INPUT_BUFFER_DISABLED,
62+
.outputBuffer = PORT_OUTPUT_BUFFER_ENABLED,
63+
.initValue = 0U,
64+
.inputMuxReg = { 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U },
65+
.inputMux = { 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U },
66+
};
67+
68+
void uart9_init_115200_8N1(void) {
69+
Siul2_Port_Ip_PinInit(&kLin9TxPin);
70+
(void)Linflexd_Uart_Ip_Init(9U, &Linflexd_Uart_Ip_xHwConfigPB_0_VS_0);
71+
}
72+
73+
int uart9_tx_byte(uint8_t b) {
74+
return (Linflexd_Uart_Ip_SyncSend(9U, &b, 1U, 100U)
75+
== LINFLEXD_UART_IP_STATUS_SUCCESS) ? 0 : -1;
76+
}
77+
78+
int board_init(void) {
79+
(void)Clock_Ip_Init(&Mcu_aClockConfigPB[0U]);
80+
uart9_init_115200_8N1();
81+
// Mux the NETC ETH0 RGMII + PHY-control pins. Without this the MAC<->PHY
82+
// RGMII pins stay at their reset function and the Ethernet wire interface is
83+
// dead in BOTH directions (no TX on the wire, no RX) -- the lwip example
84+
// does this inside Port_Init(), which the board otherwise skips.
85+
(void)Siul2_Port_Ip_Init(NUM_OF_CONFIGURED_PINS, g_pin_mux_InitConfigArr_VS_0);
86+
return 0;
87+
}
88+
89+
// generic_timer.c registers SysTick_Handler as the physical-timer ISR but
90+
// leaves it up to the application. We alias it to the CR52 port's
91+
// xPortSysTickHandler via a linker --defsym in CMakeLists.txt.
92+
93+
// Newlib retarget: every printf/puts/fwrite to stdout/stderr ends up here.
94+
int _write(int fd, const char *buf, int len) {
95+
if (fd != 1 && fd != 2) {
96+
errno = EBADF;
97+
return -1;
98+
}
99+
for (int i = 0; i < len; ++i) {
100+
if (uart9_tx_byte((uint8_t)buf[i]) != 0) {
101+
return i;
102+
}
103+
}
104+
return len;
105+
}
106+
107+
// Newlib stubs to keep the link clean for bare metal.
108+
int _close(int fd) { (void)fd; errno = EBADF; return -1; }
109+
int _lseek(int fd, int off, int whence) { (void)fd; (void)off; (void)whence; errno = EBADF; return -1; }
110+
int _read(int fd, char *buf, int len) { (void)fd; (void)buf; (void)len; errno = EBADF; return -1; }
111+
int _fstat(int fd, void *st) { (void)fd; (void)st; errno = EBADF; return -1; }
112+
int _isatty(int fd) { return (fd == 1 || fd == 2) ? 1 : 0; }
113+
int _getpid(void) { return 1; }
114+
int _kill(int pid, int sig) { (void)pid; (void)sig; errno = EINVAL; return -1; }
115+
116+
// ---- PIT-driven FreeRTOS tick ----
117+
// Engineer substitutes the actual PIT register layout from the S32Z2
118+
// reference manual; the RTD typically exposes Pit_Ip_Init / Pit_Ip_StartChannel.
119+
// PIT channel 0, period = configCPU_CLOCK_HZ / configTICK_RATE_HZ.
120+
121+
#include "FreeRTOSConfig.h"
122+
123+
extern void Pit_Ip_Init(void);
124+
extern void Pit_Ip_StartChannel(uint8_t instance, uint8_t channel, uint32_t ticks);
125+
extern void Pit_Ip_ClearInterruptFlag(uint8_t instance, uint8_t channel);
126+
127+
void s32z2_pit_setup_tick_interrupt(void) {
128+
Pit_Ip_Init();
129+
Pit_Ip_StartChannel(0u, 0u,
130+
(uint32_t)(configCPU_CLOCK_HZ / configTICK_RATE_HZ));
131+
}
132+
133+
void s32z2_pit_clear_tick_interrupt(void) {
134+
Pit_Ip_ClearInterruptFlag(0u, 0u);
135+
}
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
// Copyright (c) 2026, Arm Limited and contributors.
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
// ARM-mode replacements for NXP's RTD Thumb-encoded CP15 / VFP accessors,
5+
// hooked in via `-Wl,--wrap=read_CPACR,...` so callers reach these
6+
// instead of NXP's. NXP ships read_CPACR / write_CPACR / write_FPEXC
7+
// inside core_utility.s with `.thumb2`. Core_FPU_Init in core.c is
8+
// ARM-mode and reaches them via `blx`; the `blx` correctly switches to
9+
// Thumb on entry, but the Thumb `mov pc, lr` epilogue does *not*
10+
// interwork back to ARM on this Cortex-R52 — the T bit stays set, the
11+
// caller's ARM-encoded body is decoded as Thumb-2, and the CPU branches
12+
// off into the .intc_vector data area. Replacing the helpers with
13+
// ARM-mode versions that finish with `bx lr` keeps the LSB-clear LR set
14+
// by the caller's `blx`, so the return is interworked correctly and
15+
// control comes back to the caller in ARM state.
16+
17+
.syntax unified
18+
.arch armv8-r
19+
.arm
20+
21+
.section .sram_code, "ax"
22+
23+
.global __wrap_read_CPACR
24+
.type __wrap_read_CPACR, %function
25+
__wrap_read_CPACR:
26+
mrc p15, 0, r0, c1, c0, 2
27+
bx lr
28+
.size __wrap_read_CPACR, . - __wrap_read_CPACR
29+
30+
.global __wrap_write_CPACR
31+
.type __wrap_write_CPACR, %function
32+
__wrap_write_CPACR:
33+
mcr p15, 0, r0, c1, c0, 2
34+
isb sy
35+
bx lr
36+
.size __wrap_write_CPACR, . - __wrap_write_CPACR
37+
38+
.global __wrap_write_FPEXC
39+
.type __wrap_write_FPEXC, %function
40+
__wrap_write_FPEXC:
41+
vmsr fpexc, r0
42+
isb sy
43+
bx lr
44+
.size __wrap_write_FPEXC, . - __wrap_write_FPEXC
45+
46+
.global __wrap_read_FPEXC
47+
.type __wrap_read_FPEXC, %function
48+
__wrap_read_FPEXC:
49+
vmrs r0, fpexc
50+
bx lr
51+
.size __wrap_read_FPEXC, . - __wrap_read_FPEXC
52+
53+
// write_64bit(lo=r0, hi=r1, dest=r2) — 64-bit atomic store for ECC SRAM.
54+
// NXP's Thumb-encoded original has the same `mov pc, lr` non-interwork
55+
// problem as the CPACR helpers; init_data_bss → zero_copy64 → write_64bit
56+
// is the chain that derailed before this wrap was added.
57+
.global __wrap_write_64bit
58+
.type __wrap_write_64bit, %function
59+
__wrap_write_64bit:
60+
strd r0, r1, [r2]
61+
bx lr
62+
.size __wrap_write_64bit, . - __wrap_write_64bit
63+
64+
// ===== CP15 / MPU / GIC helpers =====
65+
// The same `mov pc, lr` interworking failure also affects every other
66+
// CP15 accessor NXP ships in core_utility.s. SystemInit → Core_IC_Init
67+
// and the GIC500 init path call these via `blx` from ARM C code, and
68+
// any non-interworked return derails into Thumb decode of ARM bytes.
69+
// Wrap every helper that is actually called via BLX in the current
70+
// build (15 total, identified by `objdump -d | grep 'blx 321012...'`).
71+
//
72+
// Encodings are byte-identical to NXP's originals at 0x32101276..
73+
// 0x32101324, just re-emitted as ARM-mode with a `bx lr` epilogue.
74+
75+
.global __wrap_read_SCTLR
76+
.type __wrap_read_SCTLR, %function
77+
__wrap_read_SCTLR:
78+
mrc p15, 0, r0, c1, c0, 0
79+
bx lr
80+
.size __wrap_read_SCTLR, . - __wrap_read_SCTLR
81+
82+
.global __wrap_write_SCTLR
83+
.type __wrap_write_SCTLR, %function
84+
__wrap_write_SCTLR:
85+
mcr p15, 0, r0, c1, c0, 0
86+
isb sy
87+
bx lr
88+
.size __wrap_write_SCTLR, . - __wrap_write_SCTLR
89+
90+
.global __wrap_write_PRSELR
91+
.type __wrap_write_PRSELR, %function
92+
__wrap_write_PRSELR:
93+
mcr p15, 0, r0, c6, c2, 1
94+
isb sy
95+
bx lr
96+
.size __wrap_write_PRSELR, . - __wrap_write_PRSELR
97+
98+
.global __wrap_write_PRBAR
99+
.type __wrap_write_PRBAR, %function
100+
__wrap_write_PRBAR:
101+
mcr p15, 0, r0, c6, c3, 0
102+
bx lr
103+
.size __wrap_write_PRBAR, . - __wrap_write_PRBAR
104+
105+
.global __wrap_write_PRLAR
106+
.type __wrap_write_PRLAR, %function
107+
__wrap_write_PRLAR:
108+
mcr p15, 0, r0, c6, c3, 1
109+
bx lr
110+
.size __wrap_write_PRLAR, . - __wrap_write_PRLAR
111+
112+
.global __wrap_write_MAIR0
113+
.type __wrap_write_MAIR0, %function
114+
__wrap_write_MAIR0:
115+
mcr p15, 0, r0, c10, c2, 0
116+
bx lr
117+
.size __wrap_write_MAIR0, . - __wrap_write_MAIR0
118+
119+
.global __wrap_write_MAIR1
120+
.type __wrap_write_MAIR1, %function
121+
__wrap_write_MAIR1:
122+
mcr p15, 0, r0, c10, c2, 1
123+
bx lr
124+
.size __wrap_write_MAIR1, . - __wrap_write_MAIR1
125+
126+
.global __wrap_read_MPIDR
127+
.type __wrap_read_MPIDR, %function
128+
__wrap_read_MPIDR:
129+
mrc p15, 0, r0, c0, c0, 5
130+
bx lr
131+
.size __wrap_read_MPIDR, . - __wrap_read_MPIDR
132+
133+
.global __wrap_write_ICC_SRE
134+
.type __wrap_write_ICC_SRE, %function
135+
__wrap_write_ICC_SRE:
136+
mcr p15, 0, r0, c12, c12, 5
137+
isb sy
138+
bx lr
139+
.size __wrap_write_ICC_SRE, . - __wrap_write_ICC_SRE
140+
141+
// gic500_enableCpuInterface reaches write_ICC_PMR via *plain* ARM `bl`,
142+
// not `blx` — the compiler/linker did not insert an interworking veneer.
143+
// Without a wrap the ARM caller jumps to a Thumb-encoded body and decodes
144+
// its first halfword as ARM (UndefInstr). All other ICC_* helpers in the
145+
// same call sequence happen to be reached via `blx` and are caught by
146+
// the BLX-only audit; PMR is the lone exception, so it needs a wrap too.
147+
.global __wrap_write_ICC_PMR
148+
.type __wrap_write_ICC_PMR, %function
149+
__wrap_write_ICC_PMR:
150+
mcr p15, 0, r0, c4, c6, 0
151+
isb sy
152+
bx lr
153+
.size __wrap_write_ICC_PMR, . - __wrap_write_ICC_PMR
154+
155+
.global __wrap_write_ICC_BPR0
156+
.type __wrap_write_ICC_BPR0, %function
157+
__wrap_write_ICC_BPR0:
158+
mcr p15, 0, r0, c12, c8, 3
159+
bx lr
160+
.size __wrap_write_ICC_BPR0, . - __wrap_write_ICC_BPR0
161+
162+
.global __wrap_write_ICC_BPR1
163+
.type __wrap_write_ICC_BPR1, %function
164+
__wrap_write_ICC_BPR1:
165+
mcr p15, 0, r0, c12, c12, 3
166+
bx lr
167+
.size __wrap_write_ICC_BPR1, . - __wrap_write_ICC_BPR1
168+
169+
.global __wrap_write_ICC_IGRPEN0
170+
.type __wrap_write_ICC_IGRPEN0, %function
171+
__wrap_write_ICC_IGRPEN0:
172+
mcr p15, 0, r0, c12, c12, 6
173+
isb sy
174+
bx lr
175+
.size __wrap_write_ICC_IGRPEN0, . - __wrap_write_ICC_IGRPEN0
176+
177+
.global __wrap_write_ICC_IGRPEN1
178+
.type __wrap_write_ICC_IGRPEN1, %function
179+
__wrap_write_ICC_IGRPEN1:
180+
mcr p15, 0, r0, c12, c12, 7
181+
isb sy
182+
bx lr
183+
.size __wrap_write_ICC_IGRPEN1, . - __wrap_write_ICC_IGRPEN1
184+
185+
.global __wrap_read_ICC_CTLR
186+
.type __wrap_read_ICC_CTLR, %function
187+
__wrap_read_ICC_CTLR:
188+
mrc p15, 0, r0, c12, c12, 4
189+
bx lr
190+
.size __wrap_read_ICC_CTLR, . - __wrap_read_ICC_CTLR
191+
192+
.global __wrap_write_ICC_CTLR
193+
.type __wrap_write_ICC_CTLR, %function
194+
__wrap_write_ICC_CTLR:
195+
mcr p15, 0, r0, c12, c12, 4
196+
isb sy
197+
bx lr
198+
.size __wrap_write_ICC_CTLR, . - __wrap_write_ICC_CTLR
199+
200+
// ===== Undefined-Instruction vector -> FreeRTOS lazy-FPU handler =====
201+
// NXP's Vector_Table.s routes the undef vector to a .weak UndefInstr_Handler
202+
// (exceptions.c) that just spins. The ARM_CR52_GIC port instead ships its
203+
// own vPortUndefinedInstruction (port.c, built when configENABLE_FPU==1)
204+
// that lazily re-enables the FPU on a task's first VFP instruction and
205+
// manages per-task FP context -- but it is never wired up. The port clears
206+
// FPEXC.EN on every task switch and *relies* on this handler to re-enable
207+
// it, so without the wiring the first VFP op in any task (e.g. _vfprintf_r's
208+
// `vpush {d8-d9}`) traps into the spinning default and hangs.
209+
//
210+
// Provide a strong UndefInstr_Handler (overriding the weak one) that jumps
211+
// to vPortUndefinedInstruction. The vector reaches this exactly as it
212+
// reached the spinning default (ARM-mode, UND mode, LR_und set); the
213+
// `ldr pc, =` interworks to the port handler (Thumb) without disturbing LR,
214+
// which vPortUndefinedInstruction adjusts itself. FP-using tasks must be
215+
// created with xTaskCreateFpu so the handler finds their FP context in TLS.
216+
.global UndefInstr_Handler
217+
.type UndefInstr_Handler, %function
218+
UndefInstr_Handler:
219+
ldr pc, 1f
220+
1: .word vPortUndefinedInstruction
221+
.size UndefInstr_Handler, . - UndefInstr_Handler
222+
223+
// ===== IRQ vector -> FreeRTOS interrupt dispatcher =====
224+
// NXP's Vector_Table.s routes the IRQ vector to a weak IRQ_Handler
225+
// (exceptions_stack.s) that saves the interrupted context onto the SYS
226+
// (task) stack, calls the registered ISR, and returns. It cannot perform a
227+
// FreeRTOS context switch: xPortSysTickHandler calls vTaskSwitchContext()
228+
// (updating pxCurrentTCB) but the generic handler never swaps stacks, and
229+
// the periodic-timer IRQ data-aborts in its `ldmia sp!, {r0-r12}` epilogue.
230+
//
231+
// The port ships vPortInterruptDispatcher (port.c) -- the real FreeRTOS IRQ
232+
// entry that reads ICC_IAR1, dispatches low-priority ISRs, and routes OS
233+
// interrupts (>= configMAX_SYSCALL_INTERRUPT_PRIORITY) through its osInt
234+
// path which does the full save / vTaskSwitchContext / restore. Override
235+
// the weak IRQ_Handler to jump to it. It uses the IRQ/SVC mode stacks that
236+
// startup.s set up, and looks up ISRs via __VECTOR_RAM (defsym'd to the
237+
// core-0 RAM vector table that NXP's startup populates).
238+
.global IRQ_Handler
239+
.type IRQ_Handler, %function
240+
IRQ_Handler:
241+
ldr pc, 2f
242+
2: .word vPortInterruptDispatcher
243+
.size IRQ_Handler, . - IRQ_Handler

0 commit comments

Comments
 (0)