|
NetBurner 3.5.7
PDF Version |
Supported Platforms:** MODM7AE70, SBE70LC
The Serial TX Empty Callback lets application code run an action the instant the last bit of a transmission has physically left the UART pin. The typical use case is disabling an RS-485 transceiver or a wireless TX amplifier so the shared bus is released as quickly as possible after the final byte.
The examples provide are:
write() calls - as happens with printf,fdiprintf, or any sequence of writestring() calls that form a single logical message. It shows the race condition that can occur and the synchronization pattern that prevents it.Alternative on MODM7AE70 USART ports: If you only need automatic RTS toggling for RS-485 half-duplex, enable
Serial485HalfDupModeand the hardware will assert/de-assert RTS around each transmission without any callback code. The callback approach described here gives you full control for cases where that built-in mode is not sufficient (e.g. custom timing, GPIO-based enable, multi-write transactions).
If you just need the basic pattern (single write, wait for completion, act), here it is. See SimpleGPIO/src/main.cpp for the full working example.
For multi-write transactions (e.g. printf), you need additional synchronization to handle the race condition where the callback fires between writes. See Section 6 and Advanced/src/main.cpp.
A UART/USART transmitter on the SAME70 has two internal stages between software and the wire:
Two status flags in the Channel Status Register (US_CSR) reflect the state of these stages:
| Flag | US_CSR bit | Set when | Defined in |
|---|---|---|---|
| TXRDY | bit 1 | THR is empty – ready for the next byte | US_CSR_TXRDY (usart.h:256) |
| TXEMPTY | bit 9 | THR and shift register are both empty – the last bit has left the pin | US_CSR_TXEMPTY (usart.h:262) |
TXRDY** means *"I can accept another byte"*; TXEMPTY means *"all data is truly gone from the hardware."* The callback mechanism relies on TXEMPTY.
The driver keeps a pool-buffer-backed FIFO (fifo_buffer_storage) per port in UartData[port].m_FifoWrite. When application code calls write(), data is copied into this FIFO; the actual transmission happens asynchronously.
WakeTx() (serial.cpp:1032)WakeTx() checks the UART_TX_EMPTY software flag. If the transmitter is idle, it clears that flag and kicks off transmission:
StartTx(), which programs the XDMAC to transfer an entire pool buffer from the FIFO to the THR automatically.TxEnable(), which enables the TXRDY interrupt so the ISR can pull bytes from the FIFO one at a time.When a callback is registered, WakeTx() additionally enables the TXEMPTY interrupt so the driver can detect when the shift register drains (serial.cpp:1058-1061, 1074-1077).
DoSerIsr_Tx() (serial.cpp:1327)Each time the TXRDY interrupt fires:
TxEmptyCBCtx[port].bytesSinceLast (line 1369).UART_TX_EMPTY software flag (line 1374).TxEmpty() is already true (shift register drained): disables TX interrupts, invokes the callback with the accumulated byte count, resets the counter (lines 1377-1381).TxEmpty() is false (shift register still busy): disables TXRDY only, keeps the TXEMPTY interrupt enabled. When TXEMPTY fires later, the same ISR path invokes the callback.TxComp() (serial.cpp:1467)When a DMA transfer finishes:
bytesSinceLast (lines 1471-1472).StartTx() is called to start the next buffer. If no more buffers are queued, TxEnable() enables the TXEMPTY interrupt (line 1478).fd – the file descriptor returned by SimpleOpenSerial() / OpenSerial().pFunc – your callback function, or NULL to unregister.NULL).NULL also resets the internal bytesSinceLast counter.Declared in serial.h:351 (guarded by #if defined SAME70 || ...). Implementation at serial.cpp:2708.
portnum – the internal serial port number (not the fd).bytesSinceLast – number of bytes transmitted since the counter was last reset (i.e. since the previous callback invocation or since registration). Defined at serial.cpp:104. One instance per port in the TxEmptyCBCtx[] array.
The callback runs inside the TX ISR when:
This means the callback fires at the earliest possible moment after the final byte has been transmitted.
write() Functions and Their Relationship to Callbacks| Function | Behavior |
|---|---|
write(fd, buf, nbytes) | Copies up to nbytes into the FIFO. Returns actual count. May return less if the FIFO fills. Blocks only if FIFO is completely full. |
writeall(fd, buf, nbytes) | Loops write() until all nbytes are written. Blocks until complete. |
writestring(fd, str) | Equivalent to writeall() for a null-terminated string. |
Key point:** All write functions return when data is in the software FIFO, not** when data has been transmitted. The callback fires later, asynchronously, when the hardware finishes.
Number of callbacks per write:** A single write() produces exactly one callback – when the FIFO drains and the hardware empties. Multiple rapid write() calls that keep the FIFO non-empty produce only one callback at the end. But if the FIFO drains between writes, an intermediate callback fires.
Note: If your application only performs a single
writestring()orwriteall()per transaction, the race condition described here does not apply – the simple callback + semaphore pattern in the Quick Start section (andSimpleGPIO/src/main.cpp) is sufficient. Read on if you useprintf,fdiprintf, or multiplewrite()calls per transaction.
If the application uses multiple write() calls (or printf, which internally does multiple writes), the driver may finish transmitting the first write's data before the second write occurs. The callback fires "too early."
In Scenario B, callback 1 fires before the second write has even been called. If the callback unconditionally disables the transceiver, the second write's data goes out with the transceiver off.
The example demonstrates a robust pattern using three variables:
Callback** – accumulates bytes but only posts the semaphore when the application has signalled that all data has been queued:
Application** – uses a critical section after all writes to atomically check whether the callback already fired:
Why the critical section is necessary:** Without it, the callback could fire between the bytesStarted > bytesSent comparison and the allDataQueued = true assignment, causing both the callback (which sees allDataQueued == false) and the application (which proceeds to Pend()) to miss the event – a deadlock.
Before starting a new transaction, reset all state:
The example's main loop (see src/main.cpp) follows this sequence on each iteration:
writestring() calls.allDataQueued, and either Pend() or skip if transmission already complete.The example also cycles through simulated preemption delays (PREEMPT_NONE, PREEMPT_MIDWRITE, PREEMPT_ALL_QUEUED) to demonstrate that the pattern handles all race-condition scenarios correctly.
P2[26] as the demo GPIO pin.J1[3] as the demo GPIO pin.Serial485HalfDupMode for automatic hardware RTS toggling, which may be sufficient for simple RS-485 half-duplex without needing a callback.serial.h:341).