Your prototype can listen or play sound. Why not both?

A microphone example works. A speaker example works. Combine the two into an interactive audio prototype, and the program stops receiving data. For a music or education team, it is tempting to blame the processor or the audio hardware. Sometimes the missing piece is much smaller: a shared queue has been reassigned to the other direction.

That was the issue identified by the author of a March 17, 2023 Raspberry Pi forum report. Their PIO-based I2S program stopped working when transmit setup was added. Later that day, they reported finding the mistake: they had read FIFO joining as connecting a queue, but selecting the transmit join had instead removed the receive queue. This is one developer's resolved configuration problem, not a diagnosis of every audio dropout.

This note explains the RP2040 resource tradeoff behind it. It is based on the datasheet and documentation, not on an audio build or listening test of ours, and it is meant to help you ask whether two working examples can coexist in one configuration.

A deeper queue costs the other direction

PIO runs small programs in hardware units called state machines. A FIFO is a first-in, first-out queue between a state machine and the main system. On RP2040, the ordinary arrangement gives each state machine four 32-bit words for outgoing data and four for incoming data. Joining allocates all eight words to one direction; it does not create two larger queues. The RP2040 datasheet, section 3.5.3, describes this choice.

Three configurations for one state machine: default with four TX and four RX words, join TX with eight TX words and no RX queue, join RX with eight RX words and no TX queue
Joining the FIFOs trades one direction for depth: eight words one way, nothing the other way.

This can suit a state machine that only transmits or only receives. It is a different decision for one that needs both paths. A useful design note is simply: "This state machine needs data from the application and must return data to it." That requirement should be visible before someone copies a one-direction example's initialization settings.

In MicroPython, the documented fifo_join options are PIO.JOIN_NONE, PIO.JOIN_TX and PIO.JOIN_RX. The option belongs to the PIO program configuration. TX and RX describe direction relative to the system and state-machine interface; they are not commands to enable a microphone or a speaker.

Read the configuration before explaining the symptom

The forum author's symptom looked contradictory: the receiving side appeared blocked while the application was waiting for data. A disabled FIFO can produce exactly the kind of status that is confusing if it is treated as an ordinary queue.

FSTAT register table for RP2040 with TXEMPTY, TXFULL, RXEMPTY and RXFULL fields, and a note that a disabled direction reports both FULL and EMPTY as 1
With TX joined, the RX direction reports FULL and EMPTY at once; read these flags for the same state machine.

For the selected RP2040 state machine, a direction disabled by joining reports both its FULL and EMPTY status bits set. Read those bits together with the join configuration; they do not mean the queue somehow holds data and no data at once. The Pico SDK register definitions give four-bit status fields because each field covers four state machines, one bit each. Compare the bits for the same machine. Combining a status bit from one machine with a setting from another can send the investigation in the wrong direction.

When reviewing a driver, check the final configuration passed to initialization, not just the first setup call. Also check what the PIO program expects to push or pull. A program waiting on a queue that has been removed will not be repaired by increasing the application loop's polling rate. The exact waiting behavior still depends on the instruction and whether it blocks.

Choose the arrangement around the interaction

For a device that records while playing an instruction, decide whether one bidirectional state machine or separate input and output state machines fits the protocol and timing requirements. Separate machines allow different queue choices, but they also need a deliberate clocking and coordination design. They are not an automatic improvement.

For a device that only plays sound, using the otherwise idle receive storage for transmission may be useful. Keep that assumption with the driver. If a later product revision adds input, revisit the resource choice rather than assuming the existing setup already supports the new behavior.

Changing the join configuration discards queued data, as the RP2040 documentation warns. Treat a runtime change as a planned transition with clear data ownership. Do not toggle the setting during an active stream and expect everything already queued to survive.

Test the combined behavior that the user needs

A focused test starts with both directions required at the same time:

A successful one-direction demonstration cannot answer that integration question.

Keep the result specific. Removing a configuration deadlock establishes that this path can run; it does not establish audio quality, synchronization accuracy or freedom from data loss under every load. Those need their own observations against the intended interaction. A deeper queue may absorb a longer pause, but it does not guarantee a particular performance improvement.

Before the next review, keep the two working examples and the combined version together, with a short description of what should happen simultaneously. The aim is a configuration the whole interaction can use, with its resource assumptions written down for the next change.