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

Parents
  • Additional details:

    Logic Analyzer serial interface format

    At 9600 baud, each byte correctly takes ~1.04 ms. However, the inter‑byte spacing changes depending on the context where uart_tx() is called:

    • From inside the UART RX callback: ~53 µs gap between bytes
    • From a k_work handler / thread context: ~313 µs gap between bytes

    Same UART config, same bytes, same baud rate. The only difference is the calling context.

    Our downstream RS‑232 device only accepts frames when the inter‑byte gap is ~300 µs (workqueue case). When TX is triggered inside the RX callback, the ~53 µs spacing causes the device to miss or reject bytes.

Reply
  • Additional details:

    Logic Analyzer serial interface format

    At 9600 baud, each byte correctly takes ~1.04 ms. However, the inter‑byte spacing changes depending on the context where uart_tx() is called:

    • From inside the UART RX callback: ~53 µs gap between bytes
    • From a k_work handler / thread context: ~313 µs gap between bytes

    Same UART config, same bytes, same baud rate. The only difference is the calling context.

    Our downstream RS‑232 device only accepts frames when the inter‑byte gap is ~300 µs (workqueue case). When TX is triggered inside the RX callback, the ~53 µs spacing causes the device to miss or reject bytes.

Children
No Data
Related