Skip to content

busio UART hardware flow control blocks control-C on raspberrypi and espressif #10621

@anecdata

Description

@anecdata

CircuitPython version and board name

Adafruit CircuitPython 10.0.0-beta.3 on 2025-08-29; Pimoroni Pico Plus 2 W with rp2350b
vs.
Adafruit CircuitPython 10.0.0-beta.3 on 2025-08-29; Adafruit Feather M4 Express with samd51j19

Code/REPL

# in all cases, u1 is wired to u2: tx to rx, and cts to rts

import time ; time.sleep(3)  # wait for serial after reset
import sys
import board
import busio

def write(size, ch):
    print(f'writing {size} bytes...', end=" ")
    written = u1.write(f'{size * ch}')
    time.sleep(1)
    print(f'{written=} {} {u2.in_waiting=}')

if "Pico" in sys.implementation._machine:
    u1 = busio.UART(tx=board.GP0, rx=board.GP1, cts=board.GP2, rts=board.GP3)
    u2 = busio.UART(tx=board.GP4, rx=board.GP5, cts=board.GP6, rts=board.GP7)
elif "M4" in sys.implementation._machine:
    u1 = busio.UART(tx=board.A4, rx=board.A5, cts=board.D9, rts=board.D6)
    u2 = busio.UART(tx=board.A2, rx=board.A3, cts=board.D11, rts=board.D10)

u2.reset_input_buffer()
print(f'\nbusio initial {u2.in_waiting=}')

write(64, "A")  # presumably fills busio default receiver_buffer_size=64 bytes

write(32, "B")  # raspberry pi only: presumably fills raspberrypi hw uart rx buffer 32 bytes

if "Pico" in sys.implementation._machine:
    size = 6  # <=5 somehow works; 6 will hang the serial console (CIRCUITPY is OK)
elif "M4" in sys.implementation._machine:
    size = 16  # can write as much as memory allows (e.g., 32768)
write(size, "C")

r = u2.read(256)
print(f'read {len(r)} bytes: {r}')

Behavior

Pico output (writing 102 bytes):

busio initial u2.in_waiting=0
writing 64 bytes... written=64 () u2.in_waiting=64
writing 32 bytes... written=32 () u2.in_waiting=64
writing 6 bytes... 

• The write blocks, but also the serial console hangs (no control-c). CIRCUITPY is still accessible. Reset is required to regain use of the serial console.

or

Pico output (writing 101 bytes):

busio initial u2.in_waiting=0
writing 64 bytes... written=64 () u2.in_waiting=64
writing 32 bytes... written=32 () u2.in_waiting=64
writing 5 bytes... written=5 () u2.in_waiting=64
read  101 bytes: b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCC'

• Flow control limit not reached: bytes written == bytes read.
• Note that in_waiting only counts the receiver_buffer_size, not the pico hardware buffer or the magic extra 5 bytes.

...compare to...

M4 output:

busio initial u2.in_waiting=0
writing 64 bytes... written=64 () u2.in_waiting=64
writing 32 bytes... written=32 () u2.in_waiting=64
writing 16 bytes... written=16 () u2.in_waiting=64
read 64 bytes: b'AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCC'

• Hardware flow control doesn't seem to work. Any number of bytes can be written and the buffer will contain the last 64 bytes sent.

Description

No response

Additional information

I also tested an Adafruit ESP32-S2 Feather, same pinout as the M4, so I plugged it into the same wiring that the M4 was in. Hardware flow control seemed to work here, lending support to the wiring being correct. The ESP32-S2 will write the 64 bytes, then the 32 bytes, then up to 242 more bytes (implying some buffering behind-the-scenes of default receiver_buffer_size of 242+32=274 bytes). If 243 are attempted, the write blocks and the serial console hangs, like raspberrypi. I looked for esp-idf definitions of receive size, but didn't find it.

Note that on the ESP32-S2, in_waiting shows a maximum of 96, which presumably includes the 64 receiver_buffer_size bytes, but only 32 of the behind-the-scenes bytes.

Perhaps it's intended behavior for hardware flow control to block writes indefinitely, ignoring the UART init timeout value. This creates a hazard for any code with hardware flow control in the case that the receiver has trouble.

It's even more hazardous for PIO UART with hardware flow control since only 8 received bytes can be buffered at a time before blocking.

It also rules out the use of asyncio for UART writer + reader configurations communicating with each other, where the reader may not keep up with the receive buffer for a wide variety of reasons.

My goal is to understand the (intended) behaviors of UART across ports, including raspberrypi PIO UART, to be able to write more universal CircuitPython UART code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugbusioespressifapplies to multiple Espressif chipsrp2Both RP2 microcontrollers

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions