Skip to content

Conversation

@ziuziakowska
Copy link

@ziuziakowska ziuziakowska commented Oct 23, 2025

This PR implements the Passthrough mode for OT SPI Device and any required changes in OT SPI Host. Passthrough mode allows for external SPI transfers to be passed through to a downstream device, and optionally intercepted or modified by OT SPI Device.

In HW, OT SPI Device and OT SPI Host are linked by their SPI bus and a wire that represents whether the Device is in Passthrough mode. If Passthrough mode is enabled, the SPI Host is entirely bypassed and SPI Device has full control over the bus, which it uses to talk to the same downstream flash as SPI Host. See Documentation reference, RTL.
This passthrough mechanism is only implemented by OT SPI Host if it has one CS line: RTL

The structure of the PR is as follows:

  • The initial commits consist of refactoring work in ot_spi_device for the Passthrough implementation.
  • The subsequent commits implement the requirements for Passthrough mode in ot_spi_host - two GPIOs are used to communicate Passthrough enabled and the Chip Select as driven by OT SPI Device, OT SPI Device and OT SPI Host are then linked together.
  • The final commits contain the implementation of Passthrough mode in ot_spi_device.

I have tried to handle most corner-cases I can think of, but some conflicting configuration options might not behave as expected.

Unimplemented features:

  • 2-cycle read pipeline is not implemented, as QEMU SPI transactions are modelled with byte granularity.
  • Dummy cycle counts between 1-7 are not supported, for the same reason as above. Any non-zero dummy cycle count will fallback to 8 cycles.

Testing:

This implementation has been tested against spi_passthru_test. See lowRISC/opentitan#28649 for the changes required in OpenTitan. To test manually the steps are:

  • Create a 32MiB backing storage for the flash: dd if=/dev/zero of=<flash> bs=1M count=32
  • Add QEMU to the execution environment of spi_passthru_test, with these additional flags: -global ot-earlgrey-board.spiflash0=w25q256 -drive if=mtd,file=<flash>,format=raw,bus=0

@ziuziakowska ziuziakowska force-pushed the spi_device_passthrough branch 2 times, most recently from 2264c77 to f1a68a2 Compare October 29, 2025 14:57
bool loop; /* Keep reading the buffer if end is reached */
bool watermark; /* Read watermark hit, used as flip-flop */
bool new_cmd; /* New command has been pushed in current SPI transaction */
bool cmd_addr_swap_en; /* Passthrough mode - address swap enabled */

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe group all the passthrough related value into a structure. (cmd....?)
This would also to clear the whole thing at once in ot_spi_device_passthrough_unmatched_command_params

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If not grouped in a structure no big deal but it seems these values are not reset yet.

@ziuziakowska ziuziakowska force-pushed the spi_device_passthrough branch from f1a68a2 to 6e8f034 Compare October 30, 2025 11:53
@ziuziakowska ziuziakowska marked this pull request as ready for review October 30, 2025 11:54
@ziuziakowska ziuziakowska force-pushed the spi_device_passthrough branch 2 times, most recently from e8ef6d7 to ad5c39c Compare October 30, 2025 14:20
@ziuziakowska ziuziakowska marked this pull request as draft October 30, 2025 16:51
@ziuziakowska ziuziakowska changed the title [WIP] ot_spi_device: Refactoring + Passthrough mode implementation [WIP] ot_spi_device,ot_spi_host: Refactoring + Passthrough mode implementation Oct 31, 2025
@ziuziakowska ziuziakowska force-pushed the spi_device_passthrough branch from ca61a08 to 28ffc3c Compare October 31, 2025 16:09
@ziuziakowska
Copy link
Author

I have discovered that OT SPI Device is not the only device controlling this SPI bus, the bus is actually shared by OT SPI Host 0 in Earlgrey, and OT SPI Host is bypassed entirely when Passthrough mode is enabled. This seems to only be documented here. As such this PR has been reworked a bit.

Copy link

@rivos-eblot rivos-eblot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

quick review.

@ziuziakowska ziuziakowska force-pushed the spi_device_passthrough branch from 28ffc3c to a49e9bf Compare November 3, 2025 14:12
@ziuziakowska ziuziakowska force-pushed the spi_device_passthrough branch from a49e9bf to 3a643ee Compare November 5, 2025 12:01
@ziuziakowska ziuziakowska changed the title [WIP] ot_spi_device,ot_spi_host: Refactoring + Passthrough mode implementation ot_spi_device,ot_spi_host: Bugfix + Refactoring + Passthrough mode implementation Nov 5, 2025
@ziuziakowska ziuziakowska force-pushed the spi_device_passthrough branch from 3a643ee to 05aa2a7 Compare November 5, 2025 12:14
@ziuziakowska ziuziakowska marked this pull request as ready for review November 5, 2025 12:15
@ziuziakowska ziuziakowska force-pushed the spi_device_passthrough branch from 05aa2a7 to 6fc1a21 Compare November 5, 2025 12:16
@ziuziakowska
Copy link
Author

Just missing some checks in OT SPI Host to inhibit its activity when Passthrough on SPI Device is enabled, which should be a very pathological situation. Otherwise ready for review and nits up to now.

ziuziakowska added a commit to ziuziakowska/opentitan that referenced this pull request Nov 5, 2025
ziuziakowska added a commit to ziuziakowska/opentitan that referenced this pull request Nov 5, 2025
Depends on lowRISC/qemu#255

Signed-off-by: Alice Ziuziakowska <[email protected]>
Copy link

@rivos-eblot rivos-eblot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two small features missing

  • complete cleanup of passthrough variables from reset_enter
  • add LOG_GUEST_ERROR message when the SPI host is attempting to drive its /CS line or data lines when passthrough_en is enabled. I guess it could be implemented by tracking passthrough_en whenever it attempts to change its first /CS line.

{
/* Passthrough is not implemented if SPI Host has more than one CS line */
if (s->num_cs != 1u) {
trace_ot_spi_host_passthrough_unimplemented(s->ot_id);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit pick: I wonder if the name of the trace is not a bit confusing, as unimplemented is usually ... used to report something that is not -yet- implemented, whereas here it is the HW that does not support this feature.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a LOG_GUEST_ERROR instead maybe? The software should know if there is more than one CS on the hardware it is running on.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it could be understood this way yes.
In this case, I think it would great to also add a boolean flag so that this error message is only emitted once (as we do for OT_UNIMP) so that the error log is not flooded with the same message for every attempt to transmit a byte or drive the /CS line.


/*
* Real HW muxes the CS line based on Passthrough Enable, but it doesn't
* make sense for a transfer to be handled by both SPI Host and SPI Device,
Copy link

@rivos-eblot rivos-eblot Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some LOG_GUEST_ERROR is definitely needed here:

It does not make sense, but it can happen (so it will). It's quite trivial to have two unrelated tasks that try to access the same HW resource. Especially with one driver managing the SPI device while another managing the SPI host.

If the HW does not provide a mean to manage exclusive access to the SPI bus from the SW, QEMU should neither do it, but it should inform the user that something unexpected is going on.

The VM not only aims at emulating the HW but also to give better insight of how the HW is driven to help fixing SW errors.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The means of exclusive access to the SPI bus is the Passthrough Enable signal, which controls the mux. I thought it would be really pathological for something to start on the SPI Host, have SPI device assert chip select too, then enable Passthrough so that it is controlling the bus for the remainder of the same transfer, so that is what I aimed to try and prevent. Do you mean that I should relax this and add a GUEST_ERROR warning if changing the Passthrough Enable when both have a Chip Select active?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how to define pathological here, but a simple SW bug would be enough.

It is a somewhat weird design to have one HW device silently using another HW device with no way to control this from SW (i.e. through a register in SPI host that manages the mux). That's what pinmuxes are for (among other things)...

What I meant is that if SPI device try to set passthrough_en while SPI host is using the same bus (i.e. it has asserted the /CS line), a LOG_GUEST_ERROR should be emitted, and it should take priority over SPI host, potentially trashing the flash.

Conversely, if the SPI host is trying to assert /CS while passthough_en is enabled, a LOG_GUEST_ERROR should be emitted. Moreover, in this case, its /CS action or ssi_transfer requests should be discarded.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I now track the passthrough chip select, passthrough enable, and chip select from the SPI Host, and mux those faithfully onto the CS[0] IRQ. I've added guest errors around any suspicious transitions.

@ziuziakowska ziuziakowska force-pushed the spi_device_passthrough branch 2 times, most recently from a847e12 to 1ef99aa Compare November 6, 2025 14:00
Copy link

@AlexJones0 AlexJones0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for all the work on this @ziuziakowska, it looks great to me and matches the HW implementation more closely now.

I agree with @rivos-eblot's remaining comments, but I'm happy to approve when those (and my new comments) are addressed.

As one final suggestion, it would also be great if you could rebase on #277 and then update the SPI device documentation to briefly note that passthrough is supported, and any known limitations (e.g. dummy cycles rounded up)?

default:
qemu_log_mask(LOG_UNIMP, "%s: %s: unsupported mode\n", __func__,
s->ot_id);
ibex_irq_raise(&s->passthrough_en);
Copy link

@AlexJones0 AlexJones0 Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should transition the bus state to discard?
In reality, disabling (or changing mode) mid transmission could be a huge edge case that is not supported - it is understandable to not handle that in this PR, but it would be good to put a TODO or an UNIMP on this case.

Both EG and DJ use E/I SRAM now

Signed-off-by: Alice Ziuziakowska <[email protected]>
The bit-fields previously used here are just advisory/convention, the
whole 22-bit field is RW and software maintained.

This is also expected behaviour for spi_passthru_test.

Signed-off-by: Alice Ziuziakowska <[email protected]>
The bytes are received over the wire and stored in the buffer
most-significant byte first (big-endian), so the dummy byte is the last
byte not the first.

Signed-off-by: Alice Ziuziakowska <[email protected]>
…ALUE

Name is more verbose but matches the usage better, as in `ot_spi_host`

Signed-off-by: Alice Ziuziakowska <[email protected]>
This commit simplifies command slot definitions and the associated state
and control flow.

The HW CFG and the HW STA commands have been combined into just HW
commands, as in flash mode they are handled in hardware by some way and
should therefore share the same data path. For passthrough mode, a
subset of these HW commands can be intercepted (all except for WREN and
WRDI).

As the matched command slot number determines whether the command is a
SW or HW command, the `OtSpiFlashCommand` enum has been removed in
favour of using `is_sw_command` to return the boolean.

Command slot matching has been brought out into its own function,
`match_command_slot`, which returns whether an opcode was matched in the
command info registers. The decoded slot number is only valid if this
returns true.

Also a bug is fixed in `ot_spi_device_exec_command` where hardcoded
opcodes for read commands were used instead of the opcodes in the
associated command slot.

Signed-off-by: Alice Ziuziakowska <[email protected]>
... and removes `OtSpiDeviceAddrMode`.

`ot_spi_device_get_command_address_size` now returns the size of the
address field in the current command, using the value in `cmd_info` and
the current 4B enable state.

Signed-off-by: Alice Ziuziakowska <[email protected]>
This implements a passthrough enable and chip select IRQ and a method to
interact with the downstream SPI bus, for use by upstream SPI Device.

Signed-off-by: Alice Ziuziakowska <[email protected]>
@ziuziakowska ziuziakowska force-pushed the spi_device_passthrough branch from 1ef99aa to 07a9072 Compare November 6, 2025 16:02
static uint8_t ot_spi_host_downstream_transfer(OtSPIHostState *s, uint8_t tx)
{
if (!ot_spi_host_supports_passthrough(s)) {
qemu_log_mask(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be useful to add a boolean value to only emit this error (passthrough triggerred) once per reset session, to avoid flooding the error stream (one per byte...)
The same boolean could be re-used with all siblings errors (/CS, EN)

It could be a bitmask of errors, as there are several ones to track

if (!ot_spi_host_supports_passthrough(s) || !s->passthrough_en) {
rx = (uint8_t)ssi_transfer(s->ssi, tx);
} else {
qemu_log_mask(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another boolean for this error (host-triggered) would be nice.

return s->num_cs == 1u;
}

static void ot_spi_host_update_muxed_chip_select(OtSPIHostState *s)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe renamed as ot_spi_host_update_muxed_cs0 to match host_cs0 and since it is specific to CS0

uint8_t rx = SPI_DEFAULT_TX_RX_VALUE;

if (s->fsm.output_en) {
if (!ot_spi_host_supports_passthrough(s) || !s->passthrough_en) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: might be worth adding some inline func

static inline bool ot_spi_host_passthough_active(const OtSpiHostState *s)
{
     return ot_spi_host_supports_passthrough(s) && s->passthrough_en;
}

as this test is used several times and it would make the syntax a bit lighter


if (!s->fsm.transaction) {
s->fsm.transaction = true;
if (ot_spi_host_supports_passthrough(s) && s->active.cmd.cs == 0u &&

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is s->active.cmd.cs == 0u required since ot_spi_host_supports_passthrough(s) should be false in case SPI host supports multiple CS, i.e. CS != 0?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not too familiar with SPI Host, so I am not sure if you couldn't accidentally get a non-zero CS number into this part of the FSM. If the value is bounded by num_cs at all times then I will remove this check as it would be redundant then.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is handled here:

        if (!(csid < s->num_cs)) {
            /* CSID exceeds max num_cs */
            qemu_log_mask(LOG_GUEST_ERROR, "%s: %s: invalid csid: %u\n",
                          __func__, s->ot_id, csid);
            REG_UPDATE(s, ERROR_STATUS, CSIDINVAL, 1u);
            csid = 0;
        }

        CmdFifoSlot slot = {
            .opts = s->regs[R_CONFIGOPTS],
            .command = val32,
            .cs = csid,
            .id = s->last_command_id++,
        };

If the guest SW uses an value not supported by the HW, the execution resumes with CS == 0
It is very weird, but this follow the HW (at least the last time I dug into the RTL :-))

If I'm not mistaken, when cs_num is 1, cs is always 0; when cs_num is greater, passthrough is not supported.

qemu_log_mask(LOG_GUEST_ERROR,
"%s: R/O register 0x%02" HWADDR_PRIx " (%s)\n", __func__,
addr, TPM_REG_NAME(reg));
"%s: %s: R/O register 0x%02" HWADDR_PRIx " (%s)\n",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I got tired on this syntax in other files, so I've started to use something more compact (less lines) and fully valid with OT:

"%s: %s: ..  0x%02x (%s)\n", 
__func__, s->ot_id, (uint32_t)addr, TPM_REG_NAME(reg));

@rivos-eblot
Copy link

Note: you will need to update ot_spi_device.md + ot_earlgrey.md and ot_darjeeling.md files. I think the list of supported feature can be removed from the machines, and the TPM vs. regular mode limitation documented in the ot_spi_device.md file with a dedicated section.

This adds the corresponding out IRQs to OT SPI Device, as well as a
property that contains the downstream OT SPI Host.

Signed-off-by: Alice Ziuziakowska <[email protected]>
This commit implements the Passthrough mode on SPI Device. Passthrough
mode allows the device to act as a proxy for a downstream flash device,
optionally intercepting commands to send back its own values, filtering
commands sent downstream, or transparently translating address and
payload bytes.

This also "implements" the Disabled mode, which discards all bytes sent
to the device.

Signed-off-by: Alice Ziuziakowska <[email protected]>
@ziuziakowska ziuziakowska force-pushed the spi_device_passthrough branch from 07a9072 to 74110d6 Compare November 6, 2025 16:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants