CAPI Selftest
See projects/capi_selftest (doxygen) for the Doxygen documentation.
Overview
capi_selftest exercises the CAPI (Common API) platform abstraction layer
against real hardware. Each peripheral is driven through its CAPI contract only
-- never the vendor BSP directly - so the same test sources run unchanged on
every platform that provides a CAPI backend (Xilinx, STM32, ...). The tests are
integration tests, not unit tests: a GPIO edge really fires an interrupt, a SPI
transfer really loops MOSI back to MISO, an I2C initiator really addresses a
second controller wired up as a target, and so on.
The project ships two examples, selected by PROJECT_DEFCONFIG:
basic.conf- the basic example (src/examples/basic). UART only: it brings up the console UART and runs the test framework over it. Use it to confirm the most fundamental wiring - that the board boots, the UART is mapped correctly and characters reach your terminal - before trusting any richer result. Ifbasicdoes not print, nothing else will.loopback.conf- the capi_loopback example (src/examples/capi_loopback). The full self-test suite: GPIO, IRQ, SPI, timer, I2C, UART and DMA, each a table of subtests run back-to-back with a summary at the end.
Both examples share src/common (the test framework and common_data) and
a per-platform src/platform/<plat>/ directory that supplies main.c and
parameters.h. parameters.h is the only file that knows the board: it
maps each peripheral to a CAPI backend and publishes the capability macros the
tests gate on.
What gets tested
The loopback suite runs seven groups, in this order, each a table of subtests
driven by test_framework_run_cases(). A subtest is either run, or reported
as a SKIP with a category - it never silently disappears.
Each group below lists what it proves, then which subtests always run and which are conditional. A conditional subtest runs only when its gate macro is set; otherwise it reports as a SKIP.
GPIO- output port drives an input port over the wire, read backAlways:
LOOPBACK(value),PORT_ACTIVE_LOW,PIN_TOGGLE.PORT_TOGGLE- gated onGPIO_HAS_TOGGLE.PIN_LOOPBACK- gated onGPIO_HAS_PIN_LOOPBACK.
IRQ- a real GPIO edge reaches a connected callbackWhole group gated on
IRQ_CTRL_IDENTIFIER.FIRE_AND_LISTEN(edge delivered),ENABLE_GATES(enable/disable masks it),RECONNECT(re-point the callback),REPEATED_EDGES(N pulses deliver N events, scaled byGPIO_IRQ_EVENTS_PER_EDGE).
SPI- external MOSI->MISO loopbackAlways:
BASIC,MODES(all four),LSB_FIRST,DATA.ASYNC_IRQ,MANUAL_ISR,ABORT_IRQ- gated onSPI_HAS_IRQ.ASYNC_DMA,ABORT_DMA- gated onSPI_HAS_DMA.
TIMER- counter advances; rate cross-checks against the uptime clockAlways:
BASIC(rate within tolerance).COMPARE- gated onTIMER_HAS_COMPARE.ASYNC_IRQ,IRQ_RATE(overflow interrupt fires at expected rate) - gated onTIMER_HAS_IRQ.
I2C- initiator addresses a second controller wired as a targetWithout a target mapped the whole group collapses to one skipping
BASIC.Always (target mapped):
BASIC,DATA,READDRESS,SUBADDR(register read),BUS_SPEED(on-wire slow/fast),TARGET_TX.MASTER_ASYNC- gated onI2C_MASTER_ASYNC(async initiator).
UART- a second UART strapped TX->RX, never the consoleAlways:
BASIC(sync echo).ASYNC_BASIC- gated onUART_ASYNC_HAS_IRQ.ASYNC_SPEED(baud-change timing) - also needsUART_ASYNC_HAS_LINE_CONFIG.ASYNC_IRQ(per-source IRQ masking) - also needsUART_ASYNC_HAS_IRQ_CTL.
DMA- memory-to-memoryWhole group gated on
DMA_OPS.BASIC(copy),PATTERN(fill/verify).
Build
Configure with a board preset and, on Xilinx, point HARDWARE at the matching
.xsa. The preset sets PLATFORM, BOARD and BOARD_CONFIG_FILE; the
defconfig picks the example; the .xsa decides which peripherals and
interrupts the BSP exposes.
The suite is designed to compile and link against any BSP. A peripheral the hardware design does not expose simply drops out of the build (see How a test is skipped), so the same sources cover all three Cora Z7-07S builds with no source edits - a GIC build, a cascaded AXI-INTC build and a polled ("noirq") build.
Every build is the same three commands - configure, build, flash - changing
only the .xsa and the build directory. Pick a mode and run:
MODE=gic # gic | intc | noirq
cmake -B build-coraz7s-$MODE --preset coraz7s \
-DPROJECT_DEFCONFIG=capi_selftest/loopback.conf \
-DHARDWARE=$(pwd)/projects/capi_selftest/coraz7s_$MODE.xsa
cmake --build build-coraz7s-$MODE --target capi_selftest -j$(nproc)
cmake --build build-coraz7s-$MODE --target flash # program the board
The three modes differ only in the .xsa (see Which IP per build):
gic- interrupts through the GIC.intc- fabric interrupts cascaded through an AXI INTC into the GIC.noirq- no fabric interrupt wired; async paths skip, sync still runs.
For a first-boot smoke test, build the UART-only basic example instead of
the full suite (any .xsa works):
cmake -B build-basic --preset coraz7s \
-DPROJECT_DEFCONFIG=capi_selftest/basic.conf \
-DHARDWARE=$(pwd)/projects/capi_selftest/coraz7s_gic.xsa
cmake --build build-basic --target capi_selftest -j$(nproc)
Notes:
One build directory per
.xsaso they do not clobber each other.--target flashprograms the attached board; only one board attaches at a time, so flash the build that matches it.Console is PS UART0 on the USB-UART - set jumper JP3 to USB.
Other presets exist (
zed,zc702,zc706,zcu102,kcu105...);zed_{gic,intc,noirq}.xsamirror this three-build split on a ZedBoard.
Wiring - Cora Z7-07S
All three builds use the same PMOD straps - only the IP behind each slot changes (see Which IP per build). Wire the board once, reflash freely. Console is PS UART0 on the USB-UART; set jumper JP3 to USB.
Loopback |
Strap |
Notes |
|---|---|---|
GPIO |
JA7 (out) to JA8 (in) |
One pin drives, the adjacent one reads back. |
SPI |
JB7 (MOSI) to JB8 (MISO) |
SCLK (JB3) and CS (JB4) are driven, not strapped - only the data pair loops back. |
UART async |
JA1 (TX) to JA2 (RX) |
The second UART, never the console; its own TX feeds its own RX. |
I2C |
JA9-JB1 (SDA), JA10-JB2 (SCL) |
Two controllers on one bus: one is the initiator, the other the target. |
Which IP per build
The build is keyed to its interrupt mode, auto-detected from the BSP (override
with -DIRQ_SEL_GIC / -DIRQ_SEL_CASCADE / -DIRQ_SEL_NOIRQ). The
straps above never move; the grid only says which silicon answers at each slot.
Read down a column for one build, across a row to see how a peripheral moves.
Peripheral |
|
|
|
|---|---|---|---|
GPIO |
PS |
PL |
PS |
SPI |
PL |
PS |
PL |
I2C initiator |
PL |
PS |
PL |
I2C target |
PS |
PL |
PS |
Timer |
TTC |
AXI |
SCU |
UART async |
NS550 |
Lite |
PS UART |
Reading the columns:
gic - every async path is live through the GIC.
intc - the fabric peripherals' interrupts arrive via an AXI INTC cascaded into the GIC. UART async is UART Lite, whose line format is fixed in the IP, so
ASYNC_SPEEDandASYNC_IRQskip.noirq - no fabric interrupt is wired, so the fabric peripherals' async subtests SKIP (sync still runs). The SCU timer and PS UART keep their own GIC lines, so timer and UART async still fire.
The two-way peripherals (GPIO, SPI, I2C) swap PS<->PL between gic and
intc; noirq mirrors gic. The three-way ones (timer, UART) take a
third option under noirq - the SCU timer and the PS UART, both of which keep
a GIC line even with no fabric interrupt.
Not physically strapped
Three groups need no jumper - their loopback is internal to the SoC or the BSP:
IRQ - reuses the GPIO strap. The GPIO output pin is driven, the wired input pin is armed as an interrupt source; no separate wiring. The whole group gates on
IRQ_CTRL_IDENTIFIER(the GIC, always present on Zynq), and each case skips at runtime (SKIP_IRQ_ABSENT) when the build wired no fabric GPIO interrupt.Timer - a free-running counter and its overflow interrupt, entirely on chip. Nothing external to connect; the rate check times it against the independent uptime clock.
DMA - memory-to-memory, no peripheral and no pins. (Not mapped on Cora today; present on STM32.)
Wiring - STM32 (NUCLEO-F767ZI)
Same groups, on-chip peripherals. brings up clocks/pins/NVIC by hand, so the platform hooks are real, not no-ops.
Group |
Peripheral |
Strap / notes |
|---|---|---|
GPIO |
PE0 -> PC0 |
Jumper PE0 (CN10/D34) to PC0 (CN9, Arduino A1). No toggle op. Input line 0 routes to EXTI0 for IRQ. |
SPI |
SPI1 |
Jumper PA7 (MOSI) to PA6 (MISO); PA5 is SCK. IRQ, no DMA. |
I2C |
I2C1 <-> I2C2 |
Jumper PB6/PB9 (I2C1) to PB10/PB11 (I2C2). Polled ( |
IRQ / Timer / DMA |
NVIC / TIM2 / DMA2 |
No strap. IRQ reuses the GPIO edge; TIM2 is a 32-bit on-chip counter (1 us resolution); DMA2 Stream0 is memory-to-memory, polled. |
USART3 is the console only
How a test is skipped
There are three independent skip mechanisms, from coarsest to finest. Prefer the coarsest one that fits: a peripheral that is absent from the hardware should disappear at compile time, not fail at runtime.
- Compile out an entire group - do not map its ops
Each test group is gated on the mapping macros that
common_datapublishes fromparameters.h. If the backend is not mapped, the group's data and body are#ifdef-ed out and the group's entry point collapses to a stub that returns0.Peripheral groups gate on
<MODULE>_OPS-GPIO_OUTPUT_OPS,SPI_OPS,TIMER_OPS,I2C_OPS,UART_ASYNC_OPS,DMA_OPS. Leave the*_OPS(and the identifier/extra) undefined inparameters.hand the group is gone.The IRQ group is special: it gates on
IRQ_CTRL_IDENTIFIER. If the BSP maps no interrupt controller, leaveIRQ_CTRL_IDENTIFIERundefined andtest_irq.ccompiles to a skipping stub.
The recommended pattern in
parameters.his to auto-detect the backend from the BSP'sXPAR_..._NUM_INSTANCESmacros and only define the mapping when an instance exists, so an absent peripheral leaves its*_OPSnaturally undefined.
- Skip a single subtest at compile time - the
skipflag Every entry in a group's
struct test_casetable carries askipbool. Set it from a capability macro to drop just that subtest while the rest of the group still runs. For example the GPIO table gates its pin-level cases onGPIO_HAS_PIN_LOOPBACK:static const struct test_case gpio_subtests[] = { { "LOOPBACK", gpio_loopback, !GPIO_HAS_PORT_LOOPBACK }, #if GPIO_HAS_PIN_LOOPBACK { "PIN_LOOPBACK", gpio_pin_loopback, !GPIO_HAS_PIN_LOOPBACK }, #endif };A
skip == truecase is reported asSKIP_FEATURE_DISABLED; a case whoserunpointer isNULLis reported asSKIP_NOT_IMPLEMENTED.This is the mechanism that makes the polled ("noirq") build behave: the fabric peripherals'
*_HAS_IRQmacros come out 0, so their async subtests SKIP instead of running against a controller that would reject them with-ENOTSUP. The capability macro must be derived from the sameXPAR_*_INTERRUPTSmacro that decidesuse_irq, or the two disagree and a SKIP turns into a spurious FAIL.
- Skip a single subtest at compile time - the
- Skip at runtime -
TEST_SKIP_CAT When absence can only be discovered at runtime (a hook reports the board has no path for this test, an optional feature is off), call
TEST_SKIP_CAT(category, message)and return. The categories live intest_framework.h:SKIP_HW_ABSENT,SKIP_IRQ_ABSENT,SKIP_BSP_ABSENT,SKIP_UNSAFE,SKIP_NOT_IMPLEMENTED,SKIP_BOARD_STATE,SKIP_FEATURE_DISABLED. The IRQ suite uses this: on a GIC-only build with no fabric GPIO interrupt the arm hook returns-ENOTSUPand each case skips withSKIP_IRQ_ABSENTrather than failing.
- Skip at runtime -
Note
The skip_all field on the top-level registry in capi_loopback.c
(struct test_entry) is not a hardware-capability switch. It exists
only for personal, ad-hoc testing - flip it to true to temporarily
omit a whole group from the default all-run while you focus on another.
Leave every entry false in committed code; real "this hardware does not
have it" skipping belongs in mechanisms 1-3 above, which are driven by the
BSP, not by a hand-edited flag.
Extending the suite
To add a new peripheral test group:
Write the test in
src/examples/capi_loopback/tests/test_<mod>.cwith a matching.hdeclaringint test_<mod>(void);. Drive only the CAPIcapi_<mod>_*API - no vendor calls in the test body. Put board- or vendor-specific setup behind platform hooks (seemain.cand the IRQ arm/ack/disarm hooks incommon_data.h) so the test stays platform-agnostic.Gate the whole file on the mapping macro so it compiles everywhere:
#ifdef <MODULE>_OPS ... tests, table, test_<mod>() ... #else int test_<mod>(void) { return 0; } #endif
Table-drive the subtests with
struct test_caseand run them throughtest_framework_run_cases(). Use theskipflag for optional-feature subtests andTEST_SKIP_CATfor runtime-discovered absence.Publish the mapping in every
src/platform/<plat>/parameters.h:<MODULE>_IDENTIFIER,<MODULE>_OPS, and any<MODULE>_EXTRA_TYPE/<MODULE>_EXTRA_INITthe backend needs. Auto-detect the backend from the BSP so an absent peripheral leaves*_OPSundefined and the group drops out. Derive every*_HAS_*capability macro from the same interrupt macro that setsuse_irq. Consume it insrc/common/common_data.hunder#ifdef <MODULE>_OPS, giving each tunable an#ifndefdefault there so a platform overrides only what it must in itsparameters.h.Register the group in
capi_loopback.c: add#include "tests/test_<mod>.h"and a{ 0, "<MOD>", test_<mod>, false }row in thetests[]table. Keepskip_allfalse.
Rules
CAPI only in test bodies. Anything vendor-specific goes behind a platform hook.
Absent hardware must never fail. Compile it out (undefined
*_OPS/IRQ_CTRL_IDENTIFIER), gate the subtest, or skip at runtime with a category. A FAIL on a board that lacks the peripheral means the gate is wrong - fix the gate, not the test.Every subtest is visited even if an earlier one fails; the first non-zero return is propagated so the end-of-run summary stays complete.
skip_allin the registry is a developer convenience, not a capability gate - committed code keeps itfalse.