# Xilinx Zynq Platform Port

Hardware abstraction for the Xilinx Zynq family. One port, three sub-targets,
auto-detected at compile time:

| Device | Core | Architecture | Default free-running counter |
|--------|------|--------------|------------------------------|
| Zynq-7000 | Cortex-A9 (×2) | ARMv7-A (32-bit) | MPCore **global timer** low 32 bits (`0xF8F00200`) |
| ZynqMP APU | Cortex-A53 (×2/×4) | ARMv8-A (AArch64) | **`CNTVCT_EL0`** generic timer |
| ZynqMP RPU | Cortex-R5F | ARMv7-R (32-bit) | **`PMCCNTR`** PMU cycle counter |

The sub-target is selected from the compiler's architecture macros
(`__aarch64__`, `__ARM_ARCH_7R__`, `__ARM_ARCH_7A__`); no configuration is
needed for the common case.

## Enable the port

In your `trax_config.h`:

```c
#define TRAX_CFG_HW_PORT         TRAX_HW_PORT_ZYNQ
#define TRAX_CFG_TIMESTAMP_MODE  TRAX_TIMESTAMP_FREERUN   /* recommended on Zynq */
#define TRAX_CFG_TIMER_FREQ_HZ   333333333U               /* see "Clock" below  */
```

No include paths to add — `trax_hw.h` routes to this port from
`TRAX_CFG_HW_PORT`. FREERUN mode is the natural choice because every Zynq core
has a free-running counter; TICK_TIMER is supported too if you prefer to drive
`trax_timestamp_tick()` from a TTC/SysTick-like source.

## Clock (`TRAX_CFG_TIMER_FREQ_HZ`)

Set it to the frequency of whichever counter feeds `TRAX_HW_PORT_FREERUN_COUNTER`:

| Target | Counter | Frequency to use |
|--------|---------|------------------|
| Zynq-7000 A9 | global timer | **CPU clock ÷ 2** (CPU_3x2x), e.g. `333333333U` for a 666.66 MHz CPU |
| ZynqMP A53 | `CNTVCT_EL0` | **`CNTFRQ_EL0`** (board system-counter freq, typically ~100 MHz) |
| ZynqMP R5 | `PMCCNTR` | **R5 CPU clock** (ensure `PMCR.D` = 0 so it is not divided by 64) |

## One-time counter setup

- **A9 global timer** — the Xilinx standalone BSP already enables it
  (`XTime_GetTime()` reads it). If you run without that BSP, set the enable bit
  in the global-timer control register (`0xF8F00208`, bit 0).
- **A53 `CNTVCT_EL0`** — the system counter is started by the boot firmware
  (FSBL/ATF); nothing to do in the application.
- **R5 `PMCCNTR`** — enable the PMU cycle counter once at startup:

  ```c
  /* PMCR.E (enable) + PMCR.C (reset cycle counter), then count-enable it */
  uint32_t pmcr; __asm volatile("mrc p15,0,%0,c9,c12,0":"=r"(pmcr));
  pmcr |= (1U<<0) | (1U<<2); pmcr &= ~(1U<<3);   /* E=1, C=1, D=0 (no /64)  */
  __asm volatile("mcr p15,0,%0,c9,c12,0"::"r"(pmcr));
  __asm volatile("mcr p15,0,%0,c9,c12,1"::"r"(1U<<31)); /* PMCNTENSET: CC    */
  ```

(For FREERUN with the DWT-style cycle counter, the host re-syncs on wrap; you do
not need anything else.)

## What the port implements

| Macro | A53 (AArch64) | A9 / R5 (ARMv7) |
|-------|---------------|------------------|
| Critical section | save/restore `DAIF`, `msr DAIFSet,#2` | save `CPSR`, `cpsid i`, restore I-bit |
| `TRAX_PORT_GET_CORE_ID()` | `MPIDR_EL1` Aff0 | `MPIDR` (CP15 c0,c0,5) Aff0 |
| `TRAX_HW_PORT_FREERUN_COUNTER` | `CNTVCT_EL0` | global timer (A9) / `PMCCNTR` (R5) |
| ISR priority order | GIC: lower number = higher priority | same |

Every default is overridable from `trax_config.h` (define the macro before the
port is included): `TRAX_PORT_ENTER/EXIT_CRITICAL_SECTION`,
`TRAX_PORT_GET_CORE_ID`, `TRAX_HW_PORT_FREERUN_COUNTER`, and for the A9 the
global-timer base `TRAX_PORT_ZYNQ_GLOBAL_TIMER_BASE`.

## Deployment: single-core, AMP, or SMP

The port covers all three Zynq usage models. Which one you are in is **not** a
hardware-port setting — it follows from how many firmware images you build and
whether they share one TraxProbe buffer. It maps onto one knob,
`TRAX_CFG_CORE_COUNT`:

| Model | What runs where | TraxProbe instances | `TRAX_CFG_CORE_COUNT` | Critical section | How the host separates cores |
|-------|-----------------|:-------------------:|:---------------------:|------------------|------------------------------|
| **Single-core** | one core traces (e.g. A9#0 only, or one R5) | 1 | `1` (default) | IRQ mask only | n/a (one lane) |
| **AMP** | each core runs its **own** image + buffer + transport (bare-metal and/or RTOS, freely mixed) | one per core | `1` *in each image* | IRQ mask only (nothing shared) | by **session / transport** (each image is its own probe link) |
| **SMP** | **one** image (SMP RTOS) schedules tasks across cores into **one** shared buffer | 1 (shared) | `N` (= core count) | IRQ mask **+ spinlock** (added automatically) | by the runtime **coreId** byte stamped on every frame |

RTOS notes: an RTOS layer is orthogonal to this. **AMP + RTOS** means one RTOS
instance *per image* — each is single-core to TraxProbe, works today. **SMP +
RTOS** additionally needs an SMP-capable RTOS (e.g. FreeRTOS-SMP) and an
SMP-aware RTOS port for task enumeration; the spinlock below is necessary but
not sufficient for that.

### SMP critical section + lock storage

When you set `TRAX_CFG_CORE_COUNT > 1`, the port automatically upgrades the
critical section to *IRQ mask first, then a portable `__atomic` spinlock*
(compiles to `LDREX/STREX` on A9/R5, `LDXR/STXR` on A53). You only have to
provide the one shared lock word — define `TRAX_ZYNQ_PORT_DEFINE_LOCK` in
**exactly one** translation unit:

```c
#define TRAX_ZYNQ_PORT_DEFINE_LOCK
#include "trax.h"
```

Every other TU sees `trax_smp_lock` as `extern`. (Forget it and you get a clear
"undefined reference to `trax_smp_lock`" link error.) You can still override the
whole macro pair in `trax_config.h` to drive a hardware mutex/spinlock peripheral
instead.

## Timestamp coherence across cores (AMP)

For events to line up on **one** host timeline, the cores must read a common,
same-rate time source:

- **A9 AMP (Zynq-7000)** — already coherent: both A9s read the same MPCore
  **global timer**, same frequency, same zero. Nothing to do.
- **SMP (any device)** — coherent by construction: one shared counter.
- **Heterogeneous AMP on ZynqMP (A53 ↔ R5)** — the defaults are **not**
  coherent: A53 reads `CNTVCT_EL0`, R5 reads `PMCCNTR` — different source, rate,
  and zero point. If you need cross-core correlation, point
  `TRAX_HW_PORT_FREERUN_COUNTER` on **both** images at one **memory-mapped**
  counter (e.g. a TTC, or the system timestamp generator), and give both the
  same `TRAX_CFG_TIMER_FREQ_HZ`. If you only view each core's lane
  independently, the per-core defaults are fine.

## Caches

Inter-core visibility is automatic once the SCU/coherency is on (the Xilinx BSP
enables it). But a consumer outside the cache domain — a PL/DMA transport, or a
core with caches/MMU off — must either read from a **non-cacheable** out-buffer
or flush the cache before draining. If your transport DMAs the buffer, place
`TRAX_CFG_OUT_BUFFER` in a non-cacheable section.

## Usage

1. Set `TRAX_CFG_HW_PORT = TRAX_HW_PORT_ZYNQ` and the clock in `trax_config.h`.
2. Pick FREERUN (recommended) and make sure the chosen counter is running.
3. Add your transport (RTT over the debug link, or a custom UART/TCP/PL
   transport — see `transports/templates/`).
4. `trax_init();` then `trax_process();` from your main loop / drain task.
