|
NetBurner 3.5.7
PDF Version |
Supported Platforms:** MODM7AE70, SBE70LC
This example demonstrates robust RS-485 transceiver control when a serial transmission spans multiple 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.
When you make a single writestring() call, the TX empty callback fires exactly once after all data has been transmitted. No race condition is possible (see the Simple example for this case).
But when a transmission is split across multiple writes, the UART may finish sending the first write's data before the second write occurs - especially if a higher-priority task preempts yours between the writes. The callback fires "too early," and if it unconditionally disables the transceiver, the second write's data goes out with the transceiver off.
The example uses three shared variables between the application task and the ISR callback:
| Variable | Type | Purpose |
|---|---|---|
TxDoneSem | OS_SEM | Callback posts this when truly done |
allDataQueued | volatile bool | Application sets this after all writes complete |
bytesSent | volatile uint32_t | Callback accumulates transmitted byte count |
Both allDataQueued and bytesSent are marked volatile because they are accessed from both the application task and the ISR callback.
The callback always accumulates bytes, but only posts the semaphore when the application has signalled that all data has been queued. If the callback fires "early" (between writes), it records the byte count but does not post - the application hasn't set the flag yet.
After all writes complete, the application enters a critical section to atomically check whether the callback has already fired:
USER_ENTER_CRITICAL()) - prevents the callback from running between the check and the flag set.bytesStarted (total bytes queued) against bytesSent (total bytes the callback has reported).allDataQueued = true so the next callback invocation will post the semaphore. Re-enable interrupts, then TxDoneSem.Pend() to wait.The critical section prevents a deadlock: without it, the callback could fire between the bytesStarted > bytesSent check and the allDataQueued = true assignment. The callback would see allDataQueued == false (so it wouldn't post), and the application would proceed to Pend() on a semaphore that will never be posted.
The example cycles through three modes to prove the pattern works in all timing scenarios:
| Mode | What It Simulates |
|---|---|
| PREEMPT_NONE | No delay. Both writes complete before the FIFO drains. The callback fires once after all data is sent - the normal fast path. |
| PREEMPT_MIDWRITE | Delay between the two writes. The FIFO drains and the callback fires before the second write. Tests that allDataQueued == false correctly suppresses the early post. |
| PREEMPT_ALL_QUEUED | Delay between writes and after writes but before the critical section. The callback has already fired and recorded all bytes by the time we check. Tests the bytesStarted <= bytesSent fast path that skips the pend. |
All three modes complete successfully, printing "Action Complete" each iteration. The console also shows the current preemption mode and the bytesStarted/bytesSent counters so you can see the synchronization in action.
bytesStarted: 16 - bytesSent: 0 (or similar), meaning the application reached the pend before all bytes were sent.bytesSent count will equal bytesStarted, and the pend is skipped entirely.DEMO_PIN (P2[26] on MODM7AE70, J1[3] on SBE70LC) goes high before the first byte and low after the last byte, regardless of preemption mode.For multi-write transactions, you need the allDataQueued flag plus a critical section to avoid a race between the application setting the flag and the callback checking it. The bytesSent counter lets you detect when the callback has already reported all bytes, avoiding an unnecessary (and potentially deadlocking) pend.