docs: add programming reference module and associated tests

- Implemented  to handle publication data for routed resources and integer timing/buffer examples.
- Introduced functions for UART, I2C, timer, and I2S timing calculations, along with buffer budget calculations.
- Added validation for programming specifications and constants extraction from source files.
- Updated  to integrate programming dependencies into the system reference collection.
- Created comprehensive tests in  to ensure the correctness of programming specifications and timing calculations.
This commit is contained in:
maksyuki committed 2026-09-08 14:22:41 +08:00
1 parent 5ff310d15f
commit c8f377cbf6
18 files changed
+2127 -5

No files matched your search

+18
View File
@@ -46,6 +46,24 @@ to software copy/CRC; its final Linux-ready wait has no firmware-local deadline.
CRC is not authentication. Power isolation, qualified entropy, physical ratings
and production APU codec support are not inferred from controller capabilities.
The cross-IP reference additionally covers register access conventions,
transaction/credit/arbitration rules, DMA request and interrupt routing,
resource coexistence, clock/divider examples and external-memory compatibility.
Two usage appendices provide software/buffer budgets and terminology/document
navigation. `programming_reference.py` collects the corresponding
`system-reference.json.programming` records, verifies RTL/SDK selector parity
and connection evidence, extracts interrupt/credit data, and calculates bounded
timing and buffer examples. The pure timing calculations are cross-checked
against the existing C helpers by `tests/test_publication_programming.py` in the
Linux host environment. No MMIO or new firmware/RTL simulation is involved in
those calculation checks.
The current native JPEG path is connected to master slot 6 but receives zero
normal read/write credits in the reviewed crossbar. The publication now records
that static integration limitation; it does not change RTL or promote standalone
codec tests into an end-to-end SoC DMA result. Existing IP register/codec details
remain available with this system-level qualification.
### Spacing and document flow
The [layout standard](datasheets/style.md) records the implemented page,
@@ -71,6 +71,8 @@ number is used to imply completion.
#include "performance-reference.typ"
#include "usage-appendices.typ"
= Document Control
== Revision History
#ds-table("revisions", [Document revision history],
@@ -78,7 +80,7 @@ number is used to imply completion.
(([2025-09-28],[0.1],[Create document],[Yuchi Miao]),
([2025-12-02],[0.2],[Initial draft],[Yuchi Miao]),
([2026-02-15],[0.3],[Initial release (historical document entry)],[Yuchi Miao]),
([2026-09-06],[0.4 DRAFT],[Expand IP and system-use reference, configuration/support matrices, boot/recovery guidance and qualification boundaries; retain the gray/gold layout and reviewed snapshot.],[Yuchi Miao / ECOS Team])),
([2026-09-06],[0.4 DRAFT],[Expand IP/system-use and cross-IP programming references, route/support matrices, calculation examples, usage appendices and qualification boundaries; retain the gray/gold layout and reviewed snapshot.],[Yuchi Miao / ECOS Team])),
widths:(0.8fr,0.65fr,1.8fr,1fr),
)
The retained historical release entry does not establish silicon availability or qualify
@@ -70,6 +70,8 @@ width multiplied by clock rate as measured application bandwidth.
#source-note("rtl/mini/integration/soc_topology.json", title:"Generated matrix source: data_master_policies")
#pagebreak()
#include "bus-programming.typ"
=== Address Mapping
All ranges below are inclusive. The SRAM range is resolved using the reference profile's
32 KiB setting. Peripheral windows are address allocations; not every offset implements a
@@ -125,6 +127,7 @@ from re-entering a restarted domain.
qualification. No crystal-oscillator range or maximum core frequency is specified here.]
#include "operating-states.typ"
#include "clock-programming.typ"
== Interrupt System
The management interrupt vector contains 32 allocated positions. The following numbers are
@@ -143,3 +146,4 @@ Claim/complete and priority rules are defined in the HP platform contract.
#source-note("docs/ip/hp-platform.md", title:"HP PLIC, local interrupts and mailbox")
#include "system-management.typ"
#include "register-programming.typ"
@@ -0,0 +1,64 @@
#import "../style.typ": *
#let programming = data.system_reference.programming
=== Bus Transaction Rules and Arbitration <bus-programming>
The usable transaction set is the intersection of the issuing master, fabric, adapters and
destination. A 64-bit fabric does not make every target a 64-bit register interface. Keep
device/MMIO accesses within the target's register contract even when a bus path accepts a wider request.
#ds-table("bus-path-rules",[Transaction rules by path in the reviewed implementation],
([Path],[Admission / ordering],[Application constraint]),
(([LP control fabric],[Aligned 1/2/4-byte beats; FIXED/INCR and legal 2/4/8/16-beat WRAP; at most 16 beats. One active transaction per master.],[Stay in one 4 KiB page and decoded target; locked transfers are rejected.]),
([HP data crossbar],[Up to 8-byte beats; unlocked FIXED/INCR or legal WRAP lengths; checks the computed last address against the 4 KiB limit.],[Target and adapter limits still apply. The crossbar admission predicate alone is not a guarantee of arbitrary unaligned or 256-beat target transfers.]),
([HP MMIO],[Downsized to AXI32 and crossed into the LP control path. Root write policy remains enforced.],[Use register width and side effects; do not infer system-wide exclusive/atomic support.]),
([Memory adapters],[Preserve byte lanes, response identity and the supported target transaction contract across width/clock conversion.],[Observe each memory controller's burst, device/page and alignment restrictions.]),
([APB4 register targets],[One register access with PSTRB/PSLVERR semantics defined by that IP.],[No APB burst assumption; unsupported offsets, strobes or busy writes can fail.])),
widths:(0.85fr,1.8fr,1.5fr))
The LP fabric explicitly checks natural alignment and the start/end target. The current native
crossbar's #code("burst_legal") predicate is different: it rejects #code("AxLOCK"), limits
beat size and burst encoding, and checks its computed address span. It must not be treated as
a complete implementation of every AXI4 feature. Portable software uses naturally aligned
transfers and the narrower documented target limits. See @register-programming for MMIO exceptions.
==== Outstanding credits and identity
#ds-table("master-credits",[Normal master credits extracted from the crossbar functions],
([Slot / initiator],[Read credits],[Write credits],[Interpretation]),
data.policies.map(p=>([#p.index / #code(p.name)],str(programming.read_credits.at(p.index)),
str(programming.write_credits.at(p.index)),
if programming.read_credits.at(p.index)==0 and programming.write_credits.at(p.index)==0
{[No normal address admission through this slot.]} else {[Subject to target credits, ID and lifecycle gates.]})),
widths:(1.5fr,0.55fr,0.6fr,1.65fr))
SRAM and SDRAM have separate four-read/two-write target limits; serial and error targets use
one read/one write. Target credits and master credits both constrain admission. The source
identity receives a fixed master prefix, and the same source ID is blocked while its earlier
transaction remains active. Multiple IDs do not remove ordering requirements imposed by software.
The JPEG private path is connected to slot 6 in #code("soc_data_plane"), but both current
master-credit functions return zero for that slot. Consequently normal JPEG payload addresses
cannot enter that route in this snapshot. Error handling can use a separate credit allowance;
it does not make the normal path usable. This static integration finding does not change the
standalone codec/register implementation. See @known-limitations; no RTL fix is included here.
==== Arbitration and its assumptions
Read and write arbitration are separate per target. The normal base priorities are HP I/D 12,
I/O gateways 10, DMA/EXT-H 8 and LP gateway 2; the default case, including slot 6, is zero.
Incoming QoS can raise a normal request to 15. Continuously eligible requests age to priority
16 after the configured 256-cycle interval, while LP recovery receives priority 31.
These priorities choose among eligible requests. A full credit count, a busy source ID,
blocked resource, unavailable target or withheld handshake can prevent eligibility or progress.
The aging interval is not a worst-case application latency. Report target readiness,
contention, clock and outstanding state when interpreting wait counters.
==== Error and retry behavior
The LP path distinguishes unmapped decode errors from illegal/denied transaction errors.
The native data plane can route denied accesses to a finite error target and record source
attribution. A target timeout can trigger synthetic error completion and fail-closed isolation.
Do not blindly retry a write with side effects after a timeout: first determine whether the
target accepted it, preserve the fault and complete the documented recovery/reset sequence.
Admission policy and a completed error response do not guarantee recovery of partially modified data.
#source-note("rtl/mini/top/axi4_interconnect.sv",title:"Actual LP alignment, burst and target checks")
#source-note("rtl/mini/top/axi4_data_crossbar.sv",title:"Native admission, credit and arbitration functions")
#source-note("rtl/mini/top/soc_data_plane.sv",title:"Private masters, prefixes, gateways and CDC wiring")
@@ -0,0 +1,89 @@
#import "../style.typ": *
#let examples = data.system_reference.programming.timing
=== Clock, Divider and Timebase Programming <clock-programming>
Select a functional clock before calculating a divider. A profile's CPU clock, PCLK, stable
memory clock and audio clock are not interchangeable. The existing clock/reset inventory names
the domains; the table below records which clock must be supplied to the relevant calculation.
#ds-table("functional-clock-use",[Functional-clock inputs and configuration consequences],
([Consumer],[Calculation input],[Change handling]),
(([UART / I2C / general timer],[Actual clock driving the selected engine; check PCLK/root routing.],[Recalculate divisors/timing or counter period after that clock changes.]),
([I2S],[External audio-domain frequency, not the HP CPU clock.],[Select a compatible preset or exact programmable divisors while disabled.]),
([SDRAM / QPI / OPI / XPI],[Stable memory domain plus the controller's local divider/sampling convention.],[Convert device timing to controller cycles and reinitialize where required.]),
([LP CLINT / HP ACLINT],[Configured CLINT timebase. The reference configuration selects 1 MHz.],[Use the timebase for deadlines rather than a core instruction frequency.]),
([RTC / watchdog],[Their actual independent timing source and divider contract.],[Do not infer their tick from a CPU frequency change.]),
([DVP / ULPI / JTAG],[Externally supplied functional clock under the interface contract.],[Validate external clock availability and board timing separately.])),
widths:(0.95fr,1.55fr,1.7fr))
==== UART fractional divider
The published calculator rounds the period to units of 1/256 input-clock cycle:
#code("P = floor((Fclk * 256 + floor(Btarget / 2)) / Btarget)").
#code("BAUD_INT = P >> 8") and #code("BAUD_FRAC = P & 255"); the nominal resulting rate is
#code("Bactual = Fclk * 256 / P"). The accepted range is #code("4096 <= P <= 0xffffffff").
Both zero inputs and a period below the supported minimum must be rejected.
#ds-table("uart-divider-examples",[UART integer/fraction examples checked against the SDK],
([Input Hz],[Target baud],[INT / FRAC],[Nominal baud],[Error ppm]),
examples.uart.map(e=>(str(e.clock),str(e.target),[#e.integer / #e.fraction],str(e.actual_hz),str(e.error_ppm))),
widths:(0.85fr,0.8fr,0.8fr,1.1fr,0.65fr))
The 24 MHz row represents that input frequency; the 72 MHz row is a conditional calculation
example, not a statement that every UART receives 72 MHz in the reference profile. Confirm
the source used by the caller and the actual engine clock. Nominal quantization error excludes
oscillator tolerance, jitter, receiver margin and pad timing.
==== I2C timing cycles
The SDK selects Standard/Fast/Fast-mode Plus timing constraints from the requested rate.
Each minimum time is converted with #code("ceil(Fclk * time_ns / 1e9)"). It then lengthens
the low interval if required so the total cycle count is at least #code("ceil(Fclk / Ftarget)").
Start, stop, setup and bus-free counters have their own constraints; a single SCL divider is insufficient.
#ds-table("i2c-divider-examples",[I2C nominal SCL timing at a 24 MHz engine clock],
([Target Hz],[Low cycles],[High cycles],[Nominal Hz],[Error ppm]),
examples.i2c.map(e=>(str(e.target),str(e.cycles.at(0)),str(e.cycles.at(1)),str(e.actual_hz),str(e.error_ppm))),
widths:(0.85fr,0.7fr,0.7fr,1.1fr,0.75fr))
All programmed timing counters must fit their 16-bit fields. Clock stretching, bus busy time,
arbitration and physical rise/fall time can make observed transaction throughput lower. A
nominal 1 MHz setting does not qualify a board or target for Fast-mode Plus electrical operation.
==== General timer periods
For a millisecond request, the helper first computes #code("C = ceil(Fclk * ms / 1000)").
It chooses #code("D = max(1, ceil(C / 2^32))") and #code("N = ceil(C / D)").
The register values are #code("PRESCALE = D - 1") and #code("LOAD = N - 1"); the nominal
period for this helper's configuration is #code("D * N / Fclk"). This formula does not replace
the timer chapter's rules for other count directions or operating modes.
#ds-table("timer-divider-examples",[General-timer period examples at 24 MHz],
([Target ms],[PRESCALE],[LOAD],[Nominal ms]),
examples.timer.map(e=>(str(e.target_ms),str(e.prescale),str(e.load),str(e.actual_ms))),
widths:(1fr,1fr,1fr,1fr))
Check overflow before multiplying clock and duration; use the bounded helper instead of a
32-bit intermediate. The divisor cannot exceed 65536. A counter tick and an interrupt-handler
response time are different quantities.
==== I2S sample-rate and packing modes
For the toggle-divider implementation, #code("SCLK = Faudio / (2 * (SCLK_DIV + 1))") and
#code("Fs = SCLK / (2 * (LRCK_DIV + 1))"). The exact helper selects #code("LRCK_DIV = bits - 1")
and requires an integral #code("Faudio / (4 * Fs * bits)") between 1 and 256. It rejects
unsupported widths or an inexact ratio instead of silently rounding to a different sample rate.
#ds-table("i2s-divider-examples",[Exact I2S configurations for the 18.432 MHz reference audio input],
([Bits],[Target Hz],[SCLK_DIV],[LRCK_DIV],[Nominal Hz]),
examples.i2s.map(e=>(str(e.bits),str(e.target),str(e.sclk_div),str(e.lrck_div),str(e.actual_hz))),
widths:(0.5fr,1fr,0.8fr,0.8fr,1fr))
For example, 44.1 kHz at this input frequency is not accepted by the exact helper. Program
format and divider selection while disabled, then configure the matching stream packing.
16-bit mode stores two samples in one 32-bit word; 24-bit mode stores one sample per word.
The resulting buffer budget is worked through in @software-memory-budget.
==== Reconfiguration checklist
+ Read the current source and capability, and quiesce the affected peripheral and DMA client.
+ Calculate every related timing field, validate range and rounding, then configure while idle/disabled.
+ Clear stale status, restart the interface and verify a bounded transfer before normal traffic.
+ On a failed clock transition, keep the observed safe-source configuration and recalculate
against that actual source; do not continue with divisors for the rejected request.
#source-note("crt/src/hal/uart_math.c",title:"UART rounding and field bounds")
#source-note("crt/src/hal/i2c_math.c",title:"I2C minimum-time and rate calculations")
#source-note("crt/src/hal/timer_math.c",title:"Timer period calculation and overflow checks")
#source-note("crt/src/hal/i2s_math.c",title:"Exact I2S divider and sample-packing helpers")
#source-note("rtl/ip/serial/i2s_clkgen.sv",title:"I2S divider edge behavior")
@@ -0,0 +1,39 @@
#import "../style.typ": *
== Peripheral Coexistence and Resource Conflicts <resource-conflicts>
This matrix describes shared resources and integration constraints. Hardware exclusions,
software conventions and bandwidth competition require different decisions. Allowed combinations
still require workload-specific validation.
#ds-table("coexistence-matrix",[Shared-resource constraints across IP boundaries],
([Shared resource],[Constraint class],[Condition / integration action]),
data.system_reference.programming.conflicts.map(r=>(r.resource,r.state,r.condition)),
widths:(1fr,0.95fr,2.2fr))
=== Choosing a coexistence configuration
+ Start from the generated GPIO alternate-function table and memory-pad mode. A single pad
cannot be driven by two independent peripheral outputs; identify the complete pin group.
+ Allocate software DMA contexts and check endpoint exclusivity. Reserve the entire buffer
lifetime, not just the interval needed to program the channel.
+ Check the central Resource Controller's owner. Its DMA resource represents the shared engine,
not an independent LP/HP owner bit for each software channel convention.
+ Account for gateway sharing, memory target credits and conservative drain/block behavior.
A second controller can be electrically independent yet share the same data admission path.
+ Establish clocks, stream-switch selection and interrupt ownership before enabling producers.
+ Validate the intended combination with actual input/output traffic, completion and error handling.
For example, DVP capture and an I2S convenience DMA client cannot both assume exclusive use
of bulk channel 3. Explicitly allocating a different context can address a software convention,
but it does not change a shared endpoint, stream-switch mode or target bandwidth limit. QPI/OPI
pad exclusion cannot be solved by choosing another DMA channel.
=== Failure handling and evidence
Reject a conflicting request without overwriting active state. Capture the context, owner,
pad mode and fault. Complete recovery before reassignment. An admission error is not permission
to widen an ACL; simultaneous-workload qualification remains separate.
#block(above:rhythm.metadata-before,below:rhythm.metadata-after,breakable:false)[
#set text(size:9pt)
#source("rtl/mini/integration/soc_topology.json",title:"Pin and initiator policy") ·
#source("rtl/mini/top/soc_data_plane.sv",title:"Gateway/lifecycle wiring") ·
#source("docs/ip/resource-controller.md",title:"Ownership and drain contract")
]
@@ -23,6 +23,8 @@ and selected component. Do not assume arbitrary QSPI or HyperBus devices are int
XPI writes use indirect commands; a CPU store into the read-only data-plane flash mapping is
not a programming operation. Verify erase/program completion before making code executable.
#include "device-compatibility.typ"
==== GPIO handoff and signal integrity
+ Reserve a pin group and check alternate-function conflicts before assigning a peripheral.
+ Stop the previous function and wait for its documented idle condition. Establish the
@@ -0,0 +1,39 @@
#import "../style.typ": *
==== External Memory and Device Compatibility <device-compatibility>
The table distinguishes a controller protocol profile, a simulation model and a qualified
physical component. Geometry or a similar command set is insufficient to establish drop-in
compatibility. Device order codes, supply, speed grade and board timing must be reviewed together.
#ds-table("external-device-profiles",[External-memory profiles and their evidence boundary],
([Device / profile],[Protocol and geometry],[Initialization / compatibility condition]),
data.system_reference.programming.devices.map(r=>(r.name,[#r.protocol \ #r.geometry],r.setup)),
widths:(1fr,1.4fr,1.75fr))
#ds-table("external-device-evidence",[Model coverage versus physical-device qualification],
([Device / profile],[Model evidence indexed],[Board],[Silicon]),
data.system_reference.programming.devices.map(r=>(r.name,r.model,r.board,r.silicon)),
widths:(1.1fr,1.85fr,0.6fr,0.6fr))
Model source and test availability are not a reviewed passing run. The fast XPI backend is
specifically narrower than a pin-level flash model: it is useful for selected boot/regression
work and must not qualify erase/program behavior, all LUT modes or board sampling margins.
Before substituting a component, compare its reset state, command/address encoding, mode/dummy
phases, latency configuration, page and chip boundaries, supported sampling clocks and status
bits. Validate the complete initialization and recovery sequence, not only a successful ID read.
For SDRAM, match bank/row/column geometry and convert device timings to the actual memory clock.
The QPI controller is device-specific. The OPI and modified single-clock HyperBus-style paths
use different protocol encoders and are boot/profile-selected; they do not imply generic
compatibility with every commercial octal or HyperBus device. A supplier's maximum device clock
is not the characterized maximum frequency of the Mini-to-board interface.
If no qualified device is listed, retain the profile/model status and obtain the missing
component, board and timing evidence before releasing a BOM. Do not select an arbitrary
commercial part merely to fill this table. Firmware must also respect @resource-conflicts,
because QPI and OPI use mutually exclusive shared pads.
#source-note("docs/ip/xpi.md",title:"XPI slot/LUT and full versus fast flash model")
#source-note("docs/ip/psram.md",title:"ESP-PSRAM64H initialization and device boundaries")
#source-note("docs/ip/opipsram.md",title:"Prototype OPI and single-clock HyperBus-style profiles")
#source-note("docs/ip/sdram.md",title:"SDRAM geometry, clocking and timing configuration")
@@ -98,6 +98,10 @@ an independently configured memory job. The drawing does not assert a direct har
connection from DVP to the JPEG engine. I2S uses its independent audio clock and compatible
external codec wiring. Production APU codec/KWS availability must be checked separately.
The current native JPEG master has zero normal admission credits in the reviewed crossbar.
The diagram describes the intended memory-mediated composition; an end-to-end JPEG private-DMA
application requires the separately corrected and validated route described in @bus-programming.
+ Select external interface modes, pin routing and clocks before starting capture/playback.
+ Allocate aligned buffers, prepare ownership, and configure the consumer before enabling
a producer that cannot tolerate sustained backpressure.
@@ -0,0 +1,79 @@
#import "../style.typ": *
== Register Access and Programming Conventions <register-programming>
The following conventions explain how to read the IP register chapters. They do not override
an individual register's access width, strobe, reset or side-effect contract. Use the current
IP version and capability before interpreting a familiar offset from another device or revision.
=== Access attributes and side effects
#ds-table("register-access-conventions",[Register access attributes and safe software interpretation],
([Attribute / behavior],[Meaning],[Programming consequence]),
(([RO],[Hardware supplies a value; software writes are not part of the interface.],[Do not assume a write is harmless; an IP can reject it.]),
([RW],[Software can update documented writable fields.],[Read-modify-write is valid only when reads and writes have no conflicting side effects.]),
([WO / command],[A write submits an action or value; reads need not return the last write.],[Use the command encoding and required state; do not read it to construct another command.]),
([W1C / RW1C],[Writing one clears the selected documented status bit.],[Write an explicit acknowledgement mask; a read-modify-write can clear unrelated pending events.]),
([Set / clear / toggle alias],[A specific IP provides a dedicated operation.],[Use only that IP's documented address/mask; there is no global SoC alias scheme.]),
([Read/pop or clear-on-read],[Reading consumes data or changes state.],[Avoid speculative reads, repeated debugger watches and reads used only for display.]),
([Live / snapshot],[A live value can change between accesses; a snapshot is captured by a defined operation.],[Use the register's snapshot/latch sequence before combining multiple words.]),
([Reserved / unsupported],[The offset or field is not a supported software feature.],[Follow stated reserved-bit rules; reject unsupported modes and preserve defined state.])),
widths:(0.85fr,1.55fr,1.75fr))
For an event that arrives concurrently with a software acknowledgement, the IP's stated
hardware-set versus software-clear priority applies. No single priority rule is imposed on
all peripherals. Clear only events already captured by the handler and re-check pending state
using the documented sequence before reenabling interrupts.
=== Access width, byte lanes and alignment
Use naturally aligned 32-bit register operations unless the IP explicitly supports another
width. APB4 PSTRB support is per register and per instance. A data window can require all four
byte strobes while adjacent configuration registers accept partial updates. The bus fabric's
supported beat sizes do not imply that a byte store is meaningful for every peripheral.
Offsets in the register tables are byte offsets from the listed base. Bit 0 is the least
significant bit of the displayed register value. Separate numeric bit numbering, bus byte lanes
and protocol payload packing. The boot-bundle serialization has its own little-endian format;
camera byte swaps and audio sample order must follow their IP definitions rather than a
generic byte-copy assumption.
#ds-table("register-access-exceptions",[Examples requiring an IP-specific access sequence],
([IP / case],[Exception],[Required handling]),
(([I2S data windows],[TXDATA/RXDATA are word interfaces; RXDATA can pop data.],[Use full-word accesses and the selected PIO/stream ownership mode.]),
([DVP RXDATA],[PIO pop is permitted only when stream output is disabled.],[Do not consume a frame simultaneously through PIO and DMA.]),
([DMA active configuration],[Unsupported widths/strobes and writes forbidden while busy can fail.],[Configure idle context first; inspect command/error status rather than assuming a write took effect.]),
([GPIO atomic outputs],[Dedicated output operations avoid an ordinary output-register read-modify-write race.],[Use documented GPIO operations; they are not general atomic memory operations.]),
([MPW demo IPs],[Legacy examples ignore PSTRB and have a small aliased decode.],[Use aligned full-word operations and the MPW contract; do not generalize to PRODUCT.]),
([Dynamic reset values],[Some status/identification fields reflect configuration or live inputs.],[Do not assume every reset value is a fixed zero constant.])),
widths:(0.9fr,1.5fr,1.75fr))
=== Busy-state writes and read-modify-write
+ Check identity/capability and the current busy/owner state before touching configuration.
+ Build a value from documented fields. Do not preserve arbitrary readback bits from a command,
FIFO, W1C or mixed-access register.
+ Stop or quiesce the engine when required, then write the supported field/word width.
+ Read a documented status or ordinary configuration register to confirm acceptance. A posted
write at one layer is not proof that an external device completed the operation.
+ On a bus error or rejected command, retain the cause and recover the previous operation
before retrying. Blind command replay can duplicate a side effect.
The volatile qualifier controls compiler treatment of an MMIO access; it does not provide
cache coherency, buffer ownership, device completion or an arbitrary inter-core ordering
guarantee. Use the platform's required barriers and the sequences in @memory-coherency.
Neither the LP fabric nor the native crossbar admits locked transactions in the reviewed
predicate; do not infer a global exclusive-access service from CPU ISA names alone.
=== RV32 access to multiword registers
For CLINT, the SDK reads high, low and high again, accepting the result only if both high
words match; its retry count is bounded. It writes the compare register by temporarily
setting the low word to all ones, then writing high and the final low word. These are the
actual CLINT helper sequences, not a blanket rule for every 64-bit peripheral value.
Use an IP's latch or snapshot trigger when one exists. A latched counter, FIFO pair, write-only
command pair and live monotonic counter can require different orders. Avoid inventing a high/low
retry loop for a read that pops data. If the interface lacks a safe coherent observation,
describe the result as separate samples rather than claiming an atomic 64-bit read.
#source-note("crt/src/hal/clint.c",title:"Bounded CLINT reads and compare-write order")
#source-note("docs/ip/i2s.md",title:"I2S strobes, data windows and event priority")
#source-note("docs/ip/dvp.md",title:"DVP PIO/stream exclusion and frame semantics")
#source-note("docs/ip/gpio.md",title:"GPIO-specific atomic operations and pad control")
#source-note("rtl/ip/peripheral/dma_reg.sv",title:"DMA register legality and busy-state handling")
@@ -0,0 +1,66 @@
#import "../style.typ": *
#let programming = data.system_reference.programming
== DMA Request, Interrupt and Event Routing <dma-routing>
Request selectors describe pacing or stream endpoints. Channel numbers describe software
contexts, and interrupt numbers describe completion delivery. None can be inferred from another.
Central DMA channel conventions are not independent hardware ownership or privilege domains.
=== Central DMA request map
#ds-table("dma-request-map",[DMA request numbers checked against RTL, SDK and top-level connections],
([Request],[Endpoint],[Data transport],[Software channel convention],[Endpoint LP IRQ]),
programming.dma_routes.map(r=>(str(r.number),r.label,r.transport,r.channel,r.lp_irq)),
widths:(0.5fr,0.9fr,1.25fr,1.15fr,0.65fr))
The two SDK XPI selectors map to the RTL's historical QSPI names with identical values.
UART selectors connect to UART0, not UART1. I2S, DVP and crypto have dedicated stream ports;
the other paced clients use peripheral data windows through the MM-to-MM path. Software
selector zero has no dedicated peripheral request wire. WS2812 uses that software-paced path.
The endpoint IRQ column identifies the peripheral's own event line where one exists; it is
not the DMA transfer-completion interrupt. Central DMA channel events aggregate at the DMA
resource's LP IRQ or HP PLIC source according to the central resource owner. Crypto has no
separate LP vector entry in this integration; inspect its register status and DMA completion path.
=== Software channel allocation
#ds-table("dma-channel-conventions",[Current SDK and application channel conventions],
([Channel],[Client / convention],[Basis]),
programming.channels.map(r=>(str(r.number),r.label,source(r.sources.last(),title:"Driver / allocation source"))),
widths:(0.5fr,2.7fr,0.9fr))
The bulk convention is shared and must be serialized by the application. XPI accepts an
explicit channel argument, but selecting another number does not bypass an active stream
endpoint, resource ownership or buffer-lifetime restriction. Crypto uses separate input/output
contexts. The HP boot loader uses its declared boot context; channel 7 remains reserved.
The central DMA start validator currently accepts only its 32-bit width code, even though
the SDK type also names 8- and 16-bit widths. Streams require a full-word length and the
supported source/destination increment mode. Do not infer DRE, narrow stream writes, cyclic
descriptors or 2D operation merely from fields in the descriptor structure.
=== Private DMA masters and owner-directed completion
#ds-table("private-dma-routes",[Central and private-master routes with owner-directed interrupts],
([Engine],[Native master / gateway],[LP vector bit],[HP PLIC source]),
programming.engines.map(r=>(link(label(r.id),r.name),r.master,str(r.lp_irq),str(r.hp_irq))),
widths:(1.2fr,1.5fr,0.65fr,0.7fr))
These HP source numbers are read from the actual PLIC assignments. The Resource Controller
selects one owner's route and masks routes according to its lifecycle contract; it does not
deliver a completion to both owners. Private DMA engines do not consume a central DMA channel
simply because both move memory. They can still share a gateway or destination bandwidth.
JPEG's slot-6 admission limitation is described in @bus-programming.
=== Completion and event handling
+ Prepare the receiving handler and buffers before enabling the channel, peripheral event and owner route.
+ On interrupt, identify the aggregate source and inspect the channel/IP event fields; a DMA
completion does not by itself prove that a serial device finished its on-wire operation.
+ Check actual length, error and endpoint status, then acknowledge only the documented sticky causes.
+ Observe each stream's boundary definition. I2S RX has no TLAST, while DVP TLAST marks a line;
a generic “TLAST means complete frame” rule is incorrect.
+ On timeout, stop submissions and complete abort/drain before releasing the context or changing ownership.
The central DMA receiver requires full byte qualification. Odd-pixel DVP lines create a
partial final stream word and are not supported by its current DMA path. Use the documented
PIO handling or a separately implemented extension; do not discard the partial-word error.
#source-note("rtl/ip/peripheral/dma_pkg.sv",title:"RTL request numbers and transfer kinds")
#source-note("crt/include/retrosoc/hal/dma.h",title:"SDK selectors, contexts and descriptor type")
#source-note("rtl/mini/top/apb4_periph.sv",title:"Request, stream and PLIC wiring")
#source-note("rtl/ip/peripheral/dma_core.sv",title:"Start validation, endpoint exclusion and stream acceptance")
@@ -1,6 +1,8 @@
#import "../style.typ": *
#import "../system-figures.typ": sequence-diagram
#include "routing-reference.typ"
== Multicore Operation and Resource Ownership <multicore-operation>
Hazard3 is the root-management hart; VexiiRiscv is the application hart. Linux does not take
over the SoC's root clock, reset, admission and recovery controls merely by booting. Decide
@@ -58,6 +60,8 @@ by the lifecycle policy. See @memory-coherency and @operating-states.
#source-note("crt/src/hal/resource.c",title:"Actual HAL handoff and acknowledgement sequence")
#source-note("docs/ip/hp-platform.md",title:"HP interrupt and mailbox platform")
#include "coexistence-reference.typ"
== Security and Access-Control Boundaries <security-boundaries>
The current protection model limits bus admission and root-control access. It must be used
with explicit software ownership and does not constitute a certified secure-boot or key-storage
@@ -0,0 +1,123 @@
#import "../style.typ": *
= Appendix: Software Memory and Buffer Requirements <software-memory-budget>
Memory-map capacity, image allocation, linked image size and peak runtime use are different
quantities. This appendix supplies source-based layouts and illustrative budgets. No matching
ELF/MAP files or measured stack/runtime high-water report were available for this update;
actual firmware occupation remains unprovided.
== Linker Layout and Runtime Accounting
#ds-table("software-memory-accounting",[Memory quantities and the evidence needed to publish them],
([Quantity],[How to obtain it],[Current boundary]),
(([Physical / mapped capacity],[Selected profile and fitted memory/controller geometry.],[The reference SRAM and address map are configuration data, not an application's usage.]),
([Flash load image],[ELF/MAP LMA, initialized sections and the final packaged image.],[Actual built size not supplied. Do not count NOLOAD BSS as stored bytes.]),
([Runtime static memory],[VMA for code/rodata/data/BSS in the chosen linker layout.],[Actual section sizes not supplied. Placement differs between SRAM, SDRAM and PSRAM layouts.]),
([Stack / working space],[Linker constraints plus reviewed worst-case or measured runtime demand.],[A stack-top symbol does not reserve or measure a safe stack budget.]),
([Boot artifacts],[Loader header allocation bounds and actual package entries.],[Maximum allocations are already listed in the boot chapter; actual image lengths must come from the bundle.]),
([DMA and media buffers],[Packing, alignment, lifetime, descriptor count and buffer multiplicity.],[The examples below are calculations, not measured allocations.])),
widths:(1fr,1.7fr,1.5fr))
The all-SRAM script places the main code/rodata/data/BSS in SRAM with initialized content
loaded from flash. Its #code("_stack_point") is at the SRAM-region end, and #code("_heap_start")
is a location symbol after static sections. These symbols do not prove an independently bounded
heap or stack reservation. The freestanding SDK's lack of dynamic allocation does not eliminate
stack, library scratch or application-buffer requirements.
The SDRAM and PSRAM linker variants place application material in their named external memory
regions; inspect the actual script and generated memory regions rather than relying on a
stale comment or filename. Retain both VMA and LMA in a size report. Compare the sum of static
sections, reserved buffers and required stack/scratch margin with the available region before boot.
== Worked Buffer Budgets
The examples use 64-byte allocation alignment for shared HP cache-block ownership. This does
not replace the peripheral's minimum alignment. Each independent buffer receives its own aligned
stride so a maintenance operation does not include another owner's boundary bytes.
#ds-table("buffer-budget-examples",[Payload, stored representation and aligned allocation],
([Scenario],[Payload / buffer],[Stored / buffer],[Aligned stride],[Total allocation]),
data.system_reference.programming.budgets.map(r=>(r.name,str(r.payload_bytes),str(r.storage_bytes),str(r.stride_bytes),str(r.total_bytes))),
widths:(1.65fr,0.65fr,0.65fr,0.65fr,0.75fr),notes:[All numeric entries are bytes. Total includes the stated buffer count.])
=== DVP RGB565 frame storage
An even-width RGB565 row has two bytes of effective payload per pixel. The current DVP core
collects two 16-bit pixels into a 32-bit stream word. For storage planning, use
#code("row_bytes = ceil(width / 2) * 4") and #code("frame_bytes = row_bytes * height").
The higher FIFO-side framing metadata is not appended to memory by the central DMA.
The VGA example therefore uses 614400 bytes per buffer and 1228800 bytes for two buffers.
This cannot fit in the 32 KiB reference SRAM. Two buffers are an application budgeting example;
the current DVP convenience interface is a single-frame operation, not automatic hardware ping-pong.
The 641-pixel example illustrates partial final words and storage rounding. It is not a
supported central-DMA capture configuration: odd-pixel lines assert partial TKEEP, while the
DMA receiver requires full words. Use the documented PIO path or a separately implemented
extension, and allocate explicit software frame metadata where the application needs it.
=== I2S audio blocks
For stereo, #code("sample_count = Fs * duration_ms / 1000 * 2"). Require a whole sample-frame
count. In 16-bit mode two samples occupy one 32-bit word; in 24-bit mode one sample occupies
one 32-bit word although only 24 bits are payload. A 48 kHz, 10 ms block therefore stores
1920 bytes in 16-bit mode and 3840 bytes in 24-bit mode. Double buffering requires 3840 and
7680 bytes respectively with the stated alignment.
Include application metadata, queue state and scheduling margin separately. The buffer
duration is not an end-to-end latency guarantee and does not establish tolerance to arbitrary
memory stalls. I2S RX has no TLAST; the programmed transfer length and software lifetime remain binding.
=== Descriptor arrays and overflow checks
Each central DMA TCD occupies 64 bytes and must satisfy its alignment. Eight descriptors
occupy 512 bytes. This describes a finite descriptor array; it does not imply supported cyclic
or 2D operation. Account for payload buffers separately from descriptor storage.
Validate nonzero dimensions, sample count and buffer count before multiplication. Check the
range before alignment rounding and before multiplying the aligned stride by the number of
buffers. The publication calculator rejects allocations outside a 32-bit byte range, invalid
alignment and incomplete audio frames; the application must apply equivalent bounds for its ABI.
#block(above:rhythm.metadata-before,below:rhythm.metadata-after,breakable:false)[
#set text(size:9pt)
#source("crt/linker/ld2_all_sram.lds",title:"SRAM layout") ·
#source("crt/linker/ld2_sdram.lds",title:"SDRAM layout") ·
#source("crt/linker/ld2_psram.lds",title:"PSRAM layout") ·
#source("rtl/ip/multimedia/dvp_core.sv",title:"DVP packing") ·
#source("crt/src/hal/i2s_math.c",title:"I2S packing")
]
= Appendix: Terminology, Notation and Document Map <document-map>
== Terminology and Units
#ds-table("system-glossary",[System terminology used in this publication],
([Term],[Meaning and boundary]),
data.system_reference.programming.glossary.map(r=>(r.term,r.meaning)),widths:(0.85fr,3.3fr))
== Reading Numbers and Status
Register offsets are in bytes; bit ranges name inclusive bit indices. Address-map ranges use
the stated inclusive end address. Numeric suffixes in signal names denote signal indices,
not package pin numbers. `_i`, `_o` and `_n` retain their declared direction/polarity meaning.
An RTL `inout` pad wrapper may contain a functionally input-only route.
LP interrupt vector bits, HP PLIC source IDs, DMA request values, software channel contexts
and native master slots are separate namespaces. A matching integer does not connect them.
Clock configuration, nominal calculated rate, model timing and characterized pin timing are
also distinct. Interpret every Min/Typ/Max entry with its stated evidence and conditions.
"Integrated" and "Partial" describe implementation scope. "Source reviewed", "Tests available"
and a matching "Reported pass" describe evidence. None silently implies board or silicon
qualification. Reserved, unsupported and TBD are different states; use the specific meaning
rather than assuming that an absent value is zero or that an address window guarantees a device.
== Document and Evidence Navigation
#ds-table("document-navigation",[Where to find an authoritative answer],
([Question],[Primary reference],[How to use this datasheet]),
(([Addresses, routes and pins],[Canonical memory/topology/pin inputs and generated bindings.],[Use the generated tables and cross-IP route/conflict summaries.]),
([Register behavior],[Current RTL, handwritten C definitions and reviewed IP contract.],[Use Register Programming plus the individual register exceptions.]),
([Driver sequence],[SDK implementation, platform source and the IP's software contract.],[Follow the supported path and its error/ownership boundaries.]),
([Board connection],[Approved board constraints/schematic and selected device/pad specifications.],[Use connection/compatibility tables to identify prerequisites and missing evidence.]),
([Validation or performance],[Matching source/profile report, tool versions, device/model and conditions.],[Use the support and characterization sections without promoting source presence to a pass.]),
([Publication identity],[This PDF's manifest and matching publication source set.],[Use Sources and Reproducibility for publication-only metadata links.])),
widths:(0.85fr,1.75fr,1.55fr))
The datasheet presents the reviewed snapshot for integration. Detailed IP contracts explain
local behavior; SDK sources show the implemented calling sequence; board guides define a
particular physical system; result reports establish only the scope actually tested. Where
an old narrative conflicts with current executable integration, use the current implementation
and record the discrepancy rather than silently copying the older statement.
#source-note("publications/datasheets/system-reference.json",title:"Publication content and evidence index")
#source-note("docs/README.md",title:"Engineering document map")
+54
View File
@@ -68,6 +68,60 @@ are not characterized operating limits or measured system performance. Keep the
benchmark profile, workload, placement, measurement boundary and report with any
published result; quick CoreMark regression is not a qualified public score.
## Cross-IP programming and usage appendices
Bus Transaction Rules and Arbitration belongs inside Interconnect; Clock,
Divider and Timebase Programming belongs inside Clock and Reset. Place the
DMA Request/Interrupt/Event map after Interrupt System and the coexistence
matrix after Multicore Operation. Register Access and Programming Conventions
precedes Peripherals. Device Compatibility is a child of External Interfaces.
The Software Memory/Buffer and Terminology/Document Map appendices precede
Document Control. Keep existing per-IP register details and link to exceptions
instead of reproducing them in every global table.
`system-reference.json.programming` owns reviewed route bindings, channel
conventions, conflicts, device-profile evidence and terminology. The publication
collector checks RTL/SDK request-number parity (including explicit QSPI/XPI
aliases), coverage, actual connection snippets and referenced instances. Channel
conventions are supported by driver call sites, not inferred from selector IDs.
Private masters and central DMA contexts are different resources. Preserve
endpoint IRQ, aggregate DMA IRQ, LP vector bit and HP PLIC source as separate
fields/namespaces. Derive PLIC assignments and normal master credits from the
current implementation; mark a zero-credit connected route explicitly.
Cross-IP rules are explanatory defaults with documented exceptions. Do not
invent a global atomic-register alias, exclusive-access guarantee or universal
multiword read/write order. Describe PSTRB, W1C, FIFO pop, busy-write and dynamic
reset behavior at the correct register/instance scope. A value present in an SDK
enumeration does not establish that the current hardware start validator accepts it.
Timing examples state input clock, target, integer register values, achieved
nominal rate and rounding/error. Cross-check UART/I2C/timer/I2S calculations
against the existing pure C helpers, including illegal and boundary inputs.
For I2S also check the actual toggle-divider implementation. Nominal digital
rates do not qualify board timing or device speed grades. Never substitute STA
periods for a peripheral's configured functional input frequency.
Compatibility tables distinguish protocol/geometry, model source/test coverage,
board validation and silicon validation. A board/silicon pass needs matching
report context, source revision and profile; absent evidence remains explicit.
Do not pick an arbitrary commercial part or copy its rated speed into Mini ratings.
Buffer examples state payload packing, word padding, alignment, buffer count and
overflow bounds. Distinguish VMA/LMA, static section sizes, image allocation and
runtime demand. A linker stack-top or heap-start symbol is not an actual usage
measurement. DVP odd-line partial words and I2S 16/24-bit packing must be accounted
for; an illustrative double-buffer budget does not imply a hardware ping-pong,
cyclic-descriptor or frame-ring capability. Missing ELF/MAP or high-water evidence
must remain unprovided, not filled with a calculated maximum allocation.
Validate duplicate/missing routes, number mismatches, missing connections, unknown
instances and unsupported qualification claims. Check rounding, field limits,
sample packing, alignment and allocation overflow. Host-only C calculation tests
do not touch MMIO and are not firmware or hardware measurements. Render all new
wide tables, formulas and appendices, check every directory destination, compare
unchanged IP engineering data, and repeat the PDF build in an independent directory.
## Page, typography and color
| Element | Current rule |
File diff suppressed because it is too large. Load diff
+237
View File
@@ -0,0 +1,237 @@
"""Publication data for routed resources and checked integer timing/buffer examples."""
from __future__ import annotations
import ast
import copy
import json
import re
from pathlib import Path
UINT32_MAX = (1 << 32) - 1
COLLECTIONS = ("dma_routes", "channels", "engines", "conflicts", "devices", "glossary")
def dependencies(spec: dict) -> set[str]:
paths = set(spec.get("sources", []))
for name in COLLECTIONS:
for row in spec.get(name, []):
paths.update(row.get("sources", []))
paths.update(x["file"] for x in row.get("bindings", []))
for report in row.get("reports", []):
paths.update((report["path"], report["profile"]))
return paths
def positive(*values: int) -> None:
if any(type(v) is not int or v <= 0 or v > UINT32_MAX for v in values):
raise ValueError("positive 32-bit inputs required")
def uart_timing(clock: int, baud: int) -> dict | None:
positive(clock, baud)
scaled = (clock * 256 + baud // 2) // baud
if not 4096 <= scaled <= 0xFFFFFFFF:
return None
return {"integer": scaled >> 8, "fraction": scaled & 255,
"actual_hz": round(clock * 256 / scaled, 6),
"error_ppm": round((clock * 256 / scaled / baud - 1) * 1_000_000, 3)}
def i2c_timing(clock: int, rate: int) -> dict | None:
positive(clock, rate)
if rate > 1_000_000:
return None
ns = ((4700, 4000, 4000, 4700, 250, 4000, 4700) if rate <= 100_000 else
(1300, 600, 600, 600, 100, 600, 1300) if rate <= 400_000 else
(500, 260, 260, 260, 50, 260, 500))
cycles = [(clock * value + 999_999_999) // 1_000_000_000 for value in ns]
cycles[0] += max(0, (clock + rate - 1) // rate - cycles[0] - cycles[1])
if any(not 0 < value <= 65535 for value in cycles):
return None
return {"cycles": cycles, "actual_hz": round(clock / (cycles[0] + cycles[1]), 6),
"error_ppm": round((clock / (cycles[0] + cycles[1]) / rate - 1) * 1_000_000, 3)}
def timer_timing(clock: int, milliseconds: int) -> dict | None:
positive(clock, milliseconds)
cycles = (clock * milliseconds + 999) // 1000
divider = max(1, (cycles + (1 << 32) - 1) // (1 << 32))
if divider > 65536:
return None
ticks = (cycles + divider - 1) // divider
return {"prescale": divider - 1, "load": ticks - 1,
"actual_ms": round(divider * ticks * 1000 / clock, 9)}
def i2s_timing(clock: int, rate: int, bits: int) -> dict | None:
positive(clock, rate, bits)
divisor = rate * 4 * bits
if bits not in (16, 24) or clock % divisor or not 1 <= clock // divisor <= 256:
return None
return {"sclk_div": clock // divisor - 1, "lrck_div": bits - 1,
"actual_hz": rate, "error_ppm": 0}
def aligned_bytes(size: int, alignment: int = 64) -> int:
positive(size, alignment)
if alignment & (alignment - 1):
raise ValueError("alignment must be a power of two")
result = (size + alignment - 1) // alignment * alignment
if result > UINT32_MAX:
raise ValueError("buffer allocation overflows 32 bits")
return result
def buffer_budget(kind: str, *, width=0, height=0, rate=0, bits=0, milliseconds=0,
count=1, descriptors=0, alignment=64) -> dict:
positive(count)
if kind == "frame":
positive(width, height)
payload = width * height * 2
storage = ((width + 1) // 2) * 4 * height
supported = width % 2 == 0
elif kind == "audio":
positive(rate, bits, milliseconds)
if bits not in (16, 24) or rate * milliseconds % 1000:
raise ValueError("audio block requires whole 16/24-bit sample frames")
samples = rate * milliseconds // 1000 * 2
payload = samples * bits // 8
storage = samples * (2 if bits == 16 else 4)
supported = True
elif kind == "descriptors":
positive(descriptors)
payload = storage = descriptors * 64
supported = True
else:
raise ValueError("unknown buffer kind")
stride = aligned_bytes(storage, alignment)
if stride * count > UINT32_MAX:
raise ValueError("buffer allocation overflows 32 bits")
return {"payload_bytes": payload, "storage_bytes": storage, "stride_bytes": stride,
"total_bytes": stride * count, "dma_supported": supported}
def constants(text: str, prefix: str, rtl=False) -> dict[str, int]:
text = re.sub(r"/\*.*?\*/|//[^\n]*", "", text, flags=re.S)
pattern = (rf"\b({prefix}\w+)\s*=\s*\d+'d(\d+)\s*;" if rtl else
rf"\b({prefix}\w+)\s*(?:=\s*|\s+)(\d+)(?:U)?(?=\s*[,;\n])")
pairs = re.findall(pattern, text)
if len({name for name, _ in pairs}) != len(pairs):
raise ValueError(f"duplicate constant: {prefix}")
return {name: int(value) for name, value in pairs}
def clean_source(text: str) -> str:
return re.sub(r"\s+", "", re.sub(r"/\*.*?\*/|//[^\n]*", "", text, flags=re.S))
def validate_programming(spec: dict, ids: set[str], root: Path, revision: str) -> None:
for relative in dependencies(spec):
path = root / relative
if Path(relative).is_absolute() or ".." in Path(relative).parts or not path.resolve().is_relative_to(root.resolve()):
raise ValueError("programming path escapes repository")
if not path.is_file():
raise ValueError(f"missing programming source: {relative}")
for name in COLLECTIONS:
rows = spec[name]
keys = [r["id"] for r in rows]
if len(set(keys)) != len(keys):
raise ValueError(f"duplicate programming identifier: {name}")
for row in rows:
if not row.get("sources"):
raise ValueError(f"programming source missing: {row['id']}")
if not set(row.get("ips", [])) <= ids:
raise ValueError(f"unknown programming IP: {row['id']}")
for binding in row.get("bindings", []):
text = (root / binding["file"]).read_text(encoding="utf-8")
if clean_source(binding["text"]) not in clean_source(text):
raise ValueError(f"programming binding changed: {row['id']}")
if name == "devices":
if row["board"] not in ("Not established", "Reported pass") or row["silicon"] not in ("Not established", "Reported pass"):
raise ValueError("invalid device qualification state")
for stage in ("board", "silicon"):
if row[stage] == "Reported pass":
reports = [r for r in row.get("reports", []) if r.get("stage") == stage]
if not reports or not all(r.get("revision") == revision and r.get("result") == "pass" and r.get("path") and r.get("profile") for r in reports):
raise ValueError(f"device claim requires matching evidence: {row['id']}")
sdk = constants((root / "crt/include/retrosoc/hal/dma.h").read_text(), "RS_DMA_REQUEST_")
rtl = constants((root / "rtl/ip/peripheral/dma_pkg.sv").read_text(), "DMA_REQUEST_", rtl=True)
routes = spec["dma_routes"]
if {r["sdk"] for r in routes} != set(sdk) or {r["rtl"] for r in routes} != set(rtl):
raise ValueError("DMA route coverage mismatch")
if len(routes) != len(sdk) or len({r["sdk"] for r in routes}) != len(routes):
raise ValueError("duplicate DMA request mapping")
for row in routes:
if sdk[row["sdk"]] != rtl[row["rtl"]]:
raise ValueError(f"RTL/SDK DMA request mismatch: {row['id']}")
if sdk[row["sdk"]] and not row.get("bindings"):
raise ValueError(f"missing DMA connection evidence: {row['id']}")
def credit_values(text: str, function: str, masters: int) -> list[int]:
"""Read only the bounded if/return form of the two current credit functions."""
body = re.search(rf"function automatic[^;]*\b{function}\([^;]*;(.+?)endfunction", text, re.S)
if body is None:
raise ValueError("credit function missing")
branches = re.findall(r"if\s*\((.*?)\)\s*return CountWidth'\((\d+)\);", body[1], re.S)
remainder = re.sub(r"if\s*\((.*?)\)\s*return CountWidth'\((\d+)\);", "", body[1], flags=re.S)
if clean_source(remainder) != "return'0;":
raise ValueError("unsupported credit function shape")
allowed = (ast.Expression, ast.BoolOp, ast.And, ast.Or, ast.Compare, ast.Name, ast.Load,
ast.Constant, ast.Eq, ast.LtE, ast.GtE, ast.Lt, ast.Gt)
parsed = []
for condition, value in branches:
tree = ast.parse(condition.replace("&&", " and ").replace("||", " or "), mode="eval")
if any(not isinstance(node, allowed) or isinstance(node, ast.Name) and node.id != "master" for node in ast.walk(tree)):
raise ValueError("unsupported credit condition")
parsed.append((compile(tree, "<credit condition>", "eval"), int(value)))
return [next((value for expression, value in parsed if eval(expression, {"__builtins__": {}}, {"master": master})), 0) for master in range(masters)]
def collect_programming(root: Path, spec: dict, ids: set[str], revision: str) -> dict:
validate_programming(spec, ids, root, revision)
result = copy.deepcopy(spec)
sdk_text = (root / "crt/include/retrosoc/hal/dma.h").read_text()
sdk = constants(sdk_text, "RS_DMA_REQUEST_")
channels = constants(sdk_text, "RS_DMA_CHANNEL_")
channels.update(constants((root / "crt/include/retrosoc/hal/dma_regs.h").read_text(), "RS_DMA_CHANNEL_"))
channels.pop("RS_DMA_CHANNEL_COUNT")
if {r["symbol"] for r in spec["channels"]} != set(channels):
raise ValueError("DMA channel allocation coverage mismatch")
for row in result["dma_routes"]:
row["number"] = sdk[row["sdk"]]
for row in result["channels"]:
row["number"] = channels[row["symbol"]]
result["dma_routes"].sort(key=lambda r: r["number"])
result["channels"].sort(key=lambda r: r["number"])
topology = json.loads((root / "rtl/mini/integration/soc_topology.json").read_text())
interrupts = {row["name"]: row["core_bit"] for row in topology["interrupts"]}
wiring = clean_source((root / "rtl/mini/top/apb4_periph.sv").read_text())
for row in result["dma_routes"]:
if row["irq"] is not None and row["irq"] not in interrupts:
raise ValueError(f"unknown endpoint interrupt: {row['id']}")
row["lp_irq"] = ("Client-dependent" if row["number"] == 0 else
str(interrupts[row["irq"]]) if row["irq"] else "No separate line")
for row in result["engines"]:
if row["irq"] not in interrupts:
raise ValueError(f"unknown DMA engine interrupt: {row['id']}")
routed = re.findall(r"s_hp_plic_source\[(\d+)\]=" + re.escape(row["hp_signal"]) + ";", wiring)
if len(routed) != 1:
raise ValueError(f"HP interrupt routing changed: {row['id']}")
row["lp_irq"], row["hp_irq"] = interrupts[row["irq"]], int(routed[0])
crossbar = (root / "rtl/mini/top/axi4_data_crossbar.sv").read_text()
result["read_credits"] = credit_values(crossbar, "master_read_limit", 8)
result["write_credits"] = credit_values(crossbar, "master_write_limit", 8)
result["timing"] = {
"uart": [{"clock": c, "target": b, **uart_timing(c, b)} for c, b in ((24_000_000, 115200), (72_000_000, 115200))],
"i2c": [{"clock": 24_000_000, "target": rate, **i2c_timing(24_000_000, rate)} for rate in (100_000, 400_000, 1_000_000)],
"timer": [{"clock": 24_000_000, "target_ms": t, **timer_timing(24_000_000, t)} for t in (1, 1000)],
"i2s": [{"clock": 18_432_000, "target": rate, "bits": bits, **i2s_timing(18_432_000, rate, bits)} for bits in (16, 24) for rate in (48000, 96000)],
}
result["budgets"] = [
{"name": "VGA RGB565, 640 x 480, two buffers", **buffer_budget("frame", width=640, height=480, count=2)},
{"name": "Odd-width example, 641 x 480, one buffer", **buffer_budget("frame", width=641, height=480)},
*[{"name": f"Stereo {bits}-bit, 48 kHz, 10 ms, two buffers", **buffer_budget("audio", rate=48000, bits=bits, milliseconds=10, count=2)} for bits in (16, 24)],
{"name": "Eight 64-byte DMA TCDs", **buffer_budget("descriptors", descriptors=8)},
]
return result
+4
View File
@@ -5,11 +5,14 @@ import json
import re
from pathlib import Path
from publications.programming_reference import collect_programming, dependencies
REFERENCE = "publications/datasheets/system-reference.json"
def source_paths(reference: dict) -> set[str]:
paths = set(reference["sources"])
paths.update(dependencies(reference.get("programming", {})))
for row in [*reference["support"], *reference["limitations"]]:
paths.update(row["sources"])
paths.update(row.get("tests", []))
@@ -77,4 +80,5 @@ def collect_system_reference(root: Path, revision: str) -> dict:
layout.append({"name": title, "address": f"0x{values['ADDRESS']:08X}",
"max_size_kib": values["MAX_SIZE"] // 1024})
reference["boot_layout"] = layout
reference["programming"] = collect_programming(root, reference["programming"], {row["id"] for row in index}, revision)
return reference
+184
View File
@@ -0,0 +1,184 @@
"""Cross-IP publication tables must match routed hardware and executable SDK math."""
from __future__ import annotations
import copy
import json
import os
import shutil
import subprocess
from pathlib import Path
import pytest
from publications import programming_reference as pr
ROOT = Path(__file__).resolve().parents[1]
REFERENCE = json.loads((ROOT / "publications/datasheets/system-reference.json").read_text(encoding="utf-8"))
IDS = {row["id"] for row in REFERENCE["support"]}
@pytest.fixture
def spec():
return copy.deepcopy(REFERENCE["programming"])
def check(spec):
pr.validate_programming(spec, IDS, ROOT, REFERENCE["source_revision"])
def test_complete_dma_routes_and_native_credit_boundary(spec):
result = pr.collect_programming(ROOT, spec, IDS, REFERENCE["source_revision"])
assert [r["number"] for r in result["dma_routes"]] == list(range(14))
assert [r["number"] for r in result["channels"]] == list(range(8))
assert result["read_credits"] == [4, 4, 4, 2, 2, 1, 0, 4]
assert result["write_credits"] == [0, 2, 2, 1, 1, 1, 0, 2]
@pytest.mark.parametrize("mutation", ["missing", "duplicate", "unknown_ip", "connection"])
def test_route_inventory_rejects_ambiguous_or_unwired_entries(spec, mutation):
if mutation == "missing":
spec["dma_routes"].pop()
elif mutation == "duplicate":
spec["dma_routes"].append(copy.deepcopy(spec["dma_routes"][0]))
elif mutation == "unknown_ip":
spec["dma_routes"][0]["ips"] = ["nonexistent"]
else:
spec["dma_routes"][1]["bindings"][0]["text"] = ".missing_port(missing_signal)"
with pytest.raises(ValueError, match="coverage|duplicate|unknown programming IP|binding changed"):
check(spec)
def test_sdk_request_number_drift_is_rejected(spec, monkeypatch):
original = pr.constants
def drift(text, prefix, rtl=False):
result = original(text, prefix, rtl)
if prefix == "RS_DMA_REQUEST_":
result["RS_DMA_REQUEST_DVP_RX"] = 14
return result
monkeypatch.setattr(pr, "constants", drift)
with pytest.raises(ValueError, match="RTL/SDK DMA request mismatch"):
check(spec)
def test_device_board_claim_needs_a_matching_report(spec):
spec["devices"][0]["board"] = "Reported pass"
with pytest.raises(ValueError, match="requires matching evidence"):
check(spec)
def test_channel_coverage_cannot_drop_the_boot_channel(spec):
spec["channels"] = [r for r in spec["channels"] if r["id"] != "hp"]
with pytest.raises(ValueError, match="channel allocation coverage"):
pr.collect_programming(ROOT, spec, IDS, REFERENCE["source_revision"])
def test_credit_parser_fails_closed_on_unreviewed_shape():
with pytest.raises(ValueError, match="unsupported credit function"):
pr.credit_values("function automatic int limits(input int master); return compute(master); endfunction", "limits", 8)
def test_memory_payload_packing_and_alignment_are_distinct():
frame = pr.buffer_budget("frame", width=640, height=480, count=2)
assert frame["storage_bytes"] == 614400 and frame["total_bytes"] == 1228800
odd = pr.buffer_budget("frame", width=641, height=480)
assert odd["payload_bytes"] == 615360 and odd["storage_bytes"] == 616320
assert odd["dma_supported"] is False
audio16 = pr.buffer_budget("audio", rate=48000, bits=16, milliseconds=10, count=2)
audio24 = pr.buffer_budget("audio", rate=48000, bits=24, milliseconds=10, count=2)
assert audio16["total_bytes"] == 3840
assert audio24["payload_bytes"] == 2880 and audio24["total_bytes"] == 7680
assert pr.buffer_budget("descriptors", descriptors=8)["total_bytes"] == 512
@pytest.mark.parametrize("args", [
{"kind": "frame", "width": 0, "height": 480},
{"kind": "frame", "width": 65536, "height": 65536},
{"kind": "audio", "rate": 44100, "bits": 16, "milliseconds": 1},
{"kind": "audio", "rate": 48000, "bits": 32, "milliseconds": 10},
{"kind": "descriptors", "descriptors": 1, "alignment": 3},
{"kind": "descriptors", "descriptors": 1, "count": 1 << 31},
])
def test_budget_rejects_invalid_sizes_alignment_and_overflow(args):
with pytest.raises(ValueError):
pr.buffer_budget(**args)
@pytest.fixture(scope="module")
def sdk_math(tmp_path_factory):
if os.name == "nt":
pytest.skip("SDK C math comparison runs in the Linux host test environment")
cc = shutil.which("cc") or shutil.which("gcc")
if cc is None:
pytest.skip("host C compiler unavailable")
folder = tmp_path_factory.mktemp("publication-math")
include = folder / "retrosoc/generated"
include.mkdir(parents=True)
# The pure calculations do not use MMIO. This only permits unrelated address
# accessor functions in i2s_math.c to compile; none are called by this driver.
(include / "memory_map.h").write_text("#define RS_SOC_APB4_I2S_BASE 0U\n")
driver = folder / "compare.c"
driver.write_text(r'''
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <retrosoc/hal/uart.h>
#include <retrosoc/hal/i2c.h>
#include <retrosoc/hal/timer.h>
#include <retrosoc/hal/i2s.h>
int main(int argc, char **argv) {
if (argc < 4) return 2;
uint32_t clock = (uint32_t)strtoul(argv[2], NULL, 10);
uint32_t value = (uint32_t)strtoul(argv[3], NULL, 10);
if (!strcmp(argv[1], "uart")) {
rs_uart_timing_t t = {0};
int s = rs_uart_timing_calculate(clock, value, &t);
printf("%d %u %u\n", s, t.baud_integer, t.baud_fraction);
} else if (!strcmp(argv[1], "i2c")) {
rs_i2c_timing_t t = {0};
int s = rs_i2c_timing_calculate(clock, value, &t);
printf("%d %u %u %u %u %u %u %u\n", s, t.scl_low_cycles, t.scl_high_cycles,
t.start_hold_cycles, t.start_setup_cycles, t.data_setup_cycles, t.stop_setup_cycles, t.bus_free_cycles);
} else if (!strcmp(argv[1], "timer")) {
rs_timer_period_t t = {0};
int s = rs_timer_period_from_ms(clock, value, &t);
printf("%d %u %u\n", s, t.prescale, t.load);
} else if (!strcmp(argv[1], "i2s") && argc == 5) {
uint8_t a = 0, b = 0;
int s = rs_i2s_div_from_hz(clock, value, (uint32_t)strtoul(argv[4], NULL, 10), &a, &b);
printf("%d %u %u\n", s, (unsigned)a, (unsigned)b);
} else return 2;
return 0;
}
''', encoding="utf-8")
exe = folder / "compare"
files = [ROOT / f"crt/src/hal/{name}_math.c" for name in ("uart", "i2c", "timer", "i2s")]
subprocess.run([cc, "-std=c11", "-Wall", "-Wextra", "-Werror", "-I", str(folder),
"-I", str(ROOT / "crt/include"), str(driver), *map(str, files), "-o", str(exe)], check=True)
return exe
@pytest.mark.parametrize("kind,clock,value,bits", [
("uart", 24000000, 115200, 0), ("uart", 72000000, 115200, 0),
("uart", 16000000, 1000000, 0), ("uart", 16000000, 1000001, 0), ("uart", 0, 115200, 0),
("i2c", 24000000, 100000, 0), ("i2c", 24000000, 400000, 0),
("i2c", 24000000, 1000000, 0), ("i2c", 24000000, 1000001, 0), ("i2c", 24000000, 1, 0),
("timer", 24000000, 1, 0), ("timer", 24000000, 1000, 0),
("timer", 4294967295, 1001, 0), ("timer", 1, 1, 0), ("timer", 24000000, 0, 0),
("i2s", 18432000, 48000, 16), ("i2s", 18432000, 96000, 16),
("i2s", 18432000, 48000, 24), ("i2s", 18432000, 96000, 24),
("i2s", 18432000, 44100, 16), ("i2s", 18432000, 48000, 32),
])
def test_published_integer_math_matches_sdk_c(sdk_math, kind, clock, value, bits):
output = subprocess.check_output([str(sdk_math), kind, str(clock), str(value), *([str(bits)] if kind == "i2s" else [])], text=True)
status, *actual = map(int, output.split())
try:
expected = getattr(pr, kind + "_timing")(clock, value, *([bits] if kind == "i2s" else []))
except ValueError:
expected = None
assert (status == 0) == (expected is not None)
if expected is not None:
fields = {"uart": ("integer", "fraction"), "timer": ("prescale", "load"), "i2s": ("sclk_div", "lrck_div")}
wanted = expected["cycles"] if kind == "i2c" else [expected[key] for key in fields[kind]]
assert actual == wanted