UART TX timing differs between ISR callback and workqueue

Hello Nordic team,

Environment:

  • SDK 3.1.0
  • nRF54L15
  • UART / RS-232

I am seeing different UART TX timing depending on whether I transmit from inside the UART RX callback vs using a k_work handler.

On our nRF‑based device at 9600 baud, each byte correctly takes ~1.04 ms.
However, the gap between bytes (idle time between stop bit of one byte and start bit of the next) changes drastically depending on context:

1. UART RX callback → UART TX

If we call our transmit function directly inside the UART RX callback (or a parser function invoked from it), the inter‑byte gap is only ~53 µs.

2. k_work_delayable handler → UART TX

If we schedule the exact same transmit function using k_work_schedule(), the inter‑byte gap increases to ~313 µs, and the target device (RS‑232 peer) accepts the frame reliably.

Nothing else changes; same bytes, same baud rate, same framing.

This difference in inter‑byte spacing is causing a downstream RS‑232 device (non‑Nordic) to fail to parse frames correctly unless TX happens from a workqueue instead of inside the RX callback.

I will attach logic analyzer screenshots showing both cases.

Questions:

  1. Is it expected that UART TX behaves differently when called inside the RX callback vs from a workqueue?
  2. Is there a known restriction that UART TX should not be triggered directly inside the RX callback?

Any guidance or clarification would be appreciated. I will provide LA captures and source snippets in the attachments. Thank you.

// This psuedo code is placed in the same location within an UART RX callback.
// My device recieves a UART message then transmits a UART acknoweldgement. 

// Non-working code
void ProcessUartRx()
{
    SendAck(message->header.message_id);
}

// Working code
void delayed_ack_handler(struct k_work *work);
static uint8_t g_pending_ack_msg_id;
K_WORK_DELAYABLE_DEFINE(g_delayed_ack_work, delayed_ack_handler);

void delayed_ack_handler(struct k_work *work)
{
    SendAck(g_pending_ack_msg_id);
}

void ProcessUartRx()
{
    _pending_ack_msg_id = message->header.message_id;
    k_work_schedule(&g_delayed_ack_work, K_MSEC(0));
}


Saleae Logic Analyzer captures

failed-uart.salworking-uart.sal

Screenshots

Related