Using DMA with UARTs?

AaronD

Well-known member
Yes I know, another one. I see from other threads that it's not magic, and may actually slow things down, but I'm still kinda fuzzy on the details of why.

For an example of the slowdown, does it tie up a single shared memory bus so the CPU is effectively stalled (except for cache) while the DMA is running? Which would mean that an ISR is likely faster. I've seen some datasheets that say that their DMA's have dedicated access so that doesn't happen, but I don't see anything one way or the other for the i.MX RT1060 on the Teensy 4.0 and 4.1. Maybe I missed it?

If the DMA does *not* block the CPU, what's the problem with:
C:
//16 bits to support BREAK and IDLE characters in the buffer too
uint16_t final_data[BUFFER_TX_SIZE]{0};
uint16_t buffer_tx[BUFFER_TX_SIZE];
bool want_response = false;

void setup()
{
    SerialX.begin(...);
    SetialX.transmitterEnable(...);     //RS485 half-duplex

    Setup_DMA_A(buffer_tx, final_data, BUFFER_TX_SIZE);             //async memcpy()
    Setup_DMA_B(SerialX.port->DATA, buffer_tx, BUFFER_TX_SIZE);     //async spew
    Auto_trigger(DMA_B, DMA_A);                                     //when DMA_A completes, run DMA_B without CPU intervention
    Auto_trigger(DMA_A, DMA_B);                                     //complete a free-running loop
    DMA_A_start();
}

void loop()
{
    if (DMA_A_completed_flag)
    {
        DMA_A_completed_flag = false;

        if (want_response)
        {
            DMA_A_disable();
        }

        //do synchronous things, like calculating the next final_data and possibly want_response = true;
    }

    while(true)
    {
        uint16_t rx_char = SerialX.read();
        if (rx_char < 0)
        {
            break;
        }

        //process one character at a time, including possible BREAK

        if (got_response)
        {
            want_response = false;
            DMA_A_enable();
            DMA_A_start();
        }
    }
}
with no interrupts at all?
 
For an example of the slowdown, does it tie up a single shared memory bus so the CPU is effectively stalled (except for cache) while the DMA is running?

No, the potential issues aren't hardware based. Well, except that the hardware provides features with limited scope.

CPU efficieny-wise, the hardware side is excellent. It has a bus matrix which even allows multiple memories to be used simultaneously. For example, in the common case where the CPU is mainly accessing code from ITCM and data from DTCM, the DMA controller can access RAM2 (aka DMAMEM) simultaneously. Of course, if both CPU and DMA want to access the same memory region at the same exact moment, one must wait. But that is only a matter of a few clock cycles, not tying up the CPU.

The possible slowness issues are all about software design you might need if the hardware's features aren't a good fit to the requirements of the serial communication protocol you will implement. Even with the best possible hardware, when that hardware capability doesn't nicely fit your needs, it's still very easy for software that fills in the gaps to become complex and end up slower than a simpler approach.

but I'm still kinda fuzzy on the details of why.

It all depends on the nature of the serial communication protocol you wish to implement. The DMA controller fundamentally works by configuring fixed size transfers. If your protocol uses large fixed size blocks or "packets", DMA might be very efficient.

But most serial communication protocols aren't built around large fixed size messages. If your communication protocol will use small or variable size messages, or messages requiring parsing incoming bytes, then it just isn't a good fit to how the DMA hardware works. The overall performance depends on deep low-level details, much lower level than shown in this high-level pseudo code, of how you will deal with that mismatch between the DMA controller's fixed size transfers and whatever your specific data sizes and parsing the serial protocol requires.

So to explain, I'll try make some bad-case assumptions. This isn't to say you will write such slow code. I'm only trying to answer your question to explain what could potentially go inefficiently...

DMA transfers use a TCD (Transfer Control Descriptor) structure which eight 32 bit words. TCD is documented in the reference manual at page 115. But some of those 8 words are sub-divided into smaller fields and even bits for various status. The DMA controller has some awesome hardware features which potentially let you reuse most or even all of the previously set TCD when your next transfer will repeat the same way as the last. But that hardware becomes less useful if you will need the TCD to be different for each transfer. Since we're talking about a bad case scenario that ends up inefficient, let's assume your program ends up reconfiguring all the TCD fields for every transfer, even if you don't need to add complex code features you can't get from the TCD (many examples of those serial protocols below).

DMA can still be a huge win, even if you spend a lot of cycles configuring the TCD. The more bytes each transfer moves, the more you amortize the cost of configuring TCD and other overhead to manage everything. If you move thousands or maybe even dozens of bytes per transfer, you can probably achieve a huge improvement over regular interrupt-based serial where a small ISR function runs for every few bytes (assuming the small FIFO helps a bit). DMA is really good for uses like audio where you send or receive the same buffer every time, so the TCD just gets reused.

But if use is a very small transfer, or in the worst case each transfer moving only 1 byte, all that overhead to set up the TCD and manage the process can become a substantial cost. Why would that be a concern? Well, let's look at these lines from your pseudo code:

C++:
    while(true)
    {
        uint16_t rx_char = SerialX.read();
        if (rx_char < 0)
        {
            break;
        }

This suggests your application would want to receive the incoming data 1 byte at a time. Maybe you can find an effient way to do this, but the concept of digesting the incoming data in 1 byte increments is probably a poor fit to the way DMA works efficiently in relatively large transfers. If this pseudo code showed checking the total number of bytes and handling them all as a block, DMA would likely be a better fit to the nature of the DMA controller which transfers whole buffers which are generally intended to be much larger than a single byte.

The DMA controller really shines when trasfers are large and uniform in size. Part of your pseudo code has BUFFER_TX_SIZE which seems to suggest you're planning to use big fixed size buffers for transmitting. So DMA can be a good fit to some serial protocols like DMX512 where you repeatedly transmit a large message. But DMA can be a poor fit to many other serial protocols where small different size messages are transmitted.

When we talk of performance, often there can be many conflicting goals. So far your question seems focused on minimizing CPU overhead. Indeed that can be a worth goal if you need the CPU available for other tasks. But often with serial protocols, other goals are much more important.

Communication latency is often of overriding importance. Many serial protocols involve one side sending commands or questions, where the other side (often using a microcontroller like Teensy) replies with status or answers. In those situations, you want to reply as quicly as possible, because the other side is waiting. Even if you expended CPU cycles to receive each byte as the arrived, and you will spend more CPU cycles as the response transmits, that "wasted" CPU time occurs mostly during the data transfers without impacting your response latency. With DMA, you (probably) suffer all the overhead at the moment that impacts response latency. But as before, this involves a lot of speculation. Maybe the overhead is minimal, especially if the TCD is fully or mostly reused.

Latency can really suffer if your receiving side uses a large buffer which must fill up (complete the DMA transfer you set up in the TCD before receiving even the first byte). It might not be able to work at all if your receiving code doesn't respond until the buffer fills but the other side which send a short command expects a response before it will send another command. Maybe you can find some way to work around this and still use a large transfer size, but then you'll be adding code to inspect the DMA registers before the transfer completes, and maybe even look in the buffer or play other tricks. Meddling with hardware while it's active risks horricly tough bugs like race conditions, so this would need to be done cautiously. Hopefully it's easy to imagine how that could become a very complex problem that gets solved by more and more code which consumes more CPU cycles. Then again, Teensy 4.x CPU is fast, so even quite a lot of software overhead might be acceptable, especially with medium or slow baud rates.

But as a general rule of thumb, the DMA controller might be a huge win for protocols where you regularly transmit the same large message, and where receiving where the incoming messages are large and/or fixed size, especially where the nature of transmitting and receiving looks more like independent messages or "packets" where neither side is waiting on the other. Sadly, that's not how most serial communication protocols work, or at least not the ones I've seen and implemented.

DMA might is probably not a good fit for serial protocols which use small or variable size messages. If the protocol is built around command-response messages, DMA might be more CPU efficient overall but might hurt latency.

If the protocol uses different size messages and you need to parse the just-arrived message to determine how much to transmit, you will end up reconfiguing the TCD. How much overhead that adds if difficult to predict. But hopefully this gives you a way to imagine how DMA might not necessarily be the best solution.

Even worse are serial protocols where you must parse the first bytes of an incoming message to know how long the entire message will be. The TCD doesn't give you any way to parse data as it arrives. It requires a fixed size to be configured before any data transfer starts. Likewise for serial protocol with 1 or more reserved bytes, which use an "escape character" where certain 8 bit data becomes 2 bytes. For transmitting you end up sending different size messages, but the huge problem with DMA is you just don't know how large the incoming message will be. Maybe you'll configure the TCD for the minimum size if no bytes are "escaped", then once you've parsed the bytes you receive configure another TCD if you discover any escape chars, and keep repeating if the next TCD receives more 2 byte sequences until you have the entire message. Or maybe you go with some sort of scheme to inspect the incoming data before the TCD completes, possibly with a large TCD reused over and over? Whatever you do, the point is extra complexity.

Many protocols which use a reserved byte typically also are implemented with parsing on every incoming byte, where you immediately discard any previous partial message and begin receiving an new message upon unexpectedly seeing the reserved byte which indicate the beginning of the message. This just doesn't fit the DMA controller's fixed size transfer approach. Again, you might find a way to make it work, but at what cost of complexity and extra CPU usage on the software side.

Some serial protocols, like Modbus RTU, are often implemented with a inter-byte receive timeout which is similar or even shorter than the full imcoming message time.

On top of all that, many serial protocols are implemented with ways to recover from errors. Serial isn't like USB or Ethernet where the hardware detects corrupted data. Again, your concern should be how well any sort of error recovery scheme fits to the TCD transfers. If you will need to possibly take some recovery action immediately upon parsing every incoming byte, the DMA controller is probably a poor fit.

Hopefully this admittedly vague answer helps explain why DMA might not end up working efficiently, despite the amazing hardware features and bus switch matrix that allows it to run very effiently with little or no impact on the CPU during the DMA transfer. It really all boils down to the DMA controller working in fixed size transfers which can't parse data as it arrives. If you try to use DMA for those sorts of protocols with requirements that don't easily fit the way TCDs work, a lot of potentially quite complex code will be needed. Complex code isn't necessarily slow or ineffecient, but I hope this long-winded explanation helps you to imagine how it could go wrong.

I should also quickly mention this is only about the general purpose DMA controller. Not all DMA is with the general purpose DMA controller. Some peripherals have built in DMA, especially USB, SDIO, Ethernet. Those don't use TCDs and everything I've said here doesn't apply. But with hardware serial and most of the other peripherals which don't have it built in, DMA support is always done with the general purpose DMA controller with TCD configuration.
 
Last edited:
Hopefully this admittedly vague answer helps explain why DMA might not end up working efficiently, despite the amazing hardware features and bus switch matrix that allows it to run very effiently with little or no impact on the CPU during the DMA transfer. It really all boils down to the DMA controller working in fixed size transfers which can't parse data as it arrives. If you try to use DMA for those sorts of protocols with requirements that don't easily fit the way TCDs work, a lot of potentially quite complex code will be needed. Complex code isn't necessarily slow or ineffecient, but I hope this long-winded explanation helps you to imagine how it could go wrong.
That's...actually pretty good! And good to hear. Sounds like I do understand more than I thought. Fixed size only, and that size must be known before the transfer starts. Great for repeated transfers that are all exactly the same, less so but still possibly worth it if some details change each time. Thank you!

This suggests your application would want to receive the incoming data 1 byte at a time.
...
Part of your pseudo code has BUFFER_TX_SIZE which seems to suggest you're planning to use big fixed size buffers for transmitting. So DMA can be a good fit to some serial protocols like DMX512 where you repeatedly transmit a large message.
Yes, I normally transmit the same size message continuously, with the boundary between messages denoted by a BREAK. Free running, ideally hands-off, with just a notification that it's ready for me to give it the next message of the same size. Normally no response, but occasionally I might send a different-sized message that does want a response.

The receiving end, of course, needs to detect that BREAK, which means that it needs to see each character as it comes in. In a perfect world, the DMA would match the data being transferred and ping-pong between two destination buffers when the match occurs, with a notification that that happened, but your explanation sounds like this one at least doesn't do that.

So, what pitfalls would there be in a sort-of hybrid approach?:
  • Transmission is normally free-running hands-off, entirely DMA-driven with two channels triggering each other automatically as in my original pseudocode, but with an occasional oddball message stuffed in, and an optional pause after that for a half-duplex response.
    • Almost like my original pseudocode for TX, except when the commit-to-send DMA says it's done (and so the UART-sending one has just started), first reconfigure it for the different-sized oddball message (only change the addresses in both TCD's) and let it fire one more time to send that, then disable it and wait for the response.
  • Reception is one byte at a time, possibly interrupt-driven. Unknown size, looking for a BREAK to separate individual messages.
  • When ready to resume the normal spew, set the TCD addresses back to that, enable, and start.
  • Possibly instead of explicitly [en|dis]abling the DMA itself, [en|dis]able the auto-trigger connection, so it just doesn't fire again until we're ready.
And for a different, full-duplex connection:
  • Manual discovery, which determines a constant size for each direction at runtime. The only reason to change size is for physical reconfiguration, which won't normally happen. But I do want to make that easy too, hence the discovery instead of hard-coding it.
  • Then set up the DMA's for that size in each direction, sync'ed to the half-duplex one above, either manually or automatically. No interrupts at all for this channel, TX or RX, in this mode: the volatile buffers "just automagically work".
    • I'm requesting and receiving data from here, processing it, and sending it to the other one with hopefully only 1 frame of latency, and sending some feedback data along with the next request. Again, all constant size.
  • If the received buffer does not have a BREAK in the expected place, or has a BREAK anywhere else, then something went wrong. Likely reconfigured. Back to manual discovery.
    • With the receiving DMA looking for an entire packet before it completes, I may not know anything until the reconfiguration is finished and data starts flowing again. Then I'll have a partial old packet followed immediately in the same buffer by a misaligned new packet, and so the BREAK won't be in the expected place. I could use a timer to detect the connection lost, but in this case, I think I want to continue using old data until it's all fixed up again. So no detection needed until then. And then it all gets reset anyway with another discovery.
I have a few other connections that are completely ad-hoc, random size in both directions. So those will just be manual...though as I'm thinking about it, is it really all that much for TX, to adjust the TCD's ending address every time and then start a one-shot? Compared to software-unloading a circular buffer like HardwareSerial does? Either way, a buffer still has to be loaded, so it's only the unloading that's different.

So far your question seems focused on minimizing CPU overhead. Indeed that can be a worth goal if you need the CPU available for other tasks.
Yes, the processing that I've mentioned a few times, is user-defined, variable-length with no hard maximum. It's managed by a PC app with its own saved files, and the compiled bytecode dumped into RAM here over USB Serial. So I want to leave as much CPU time for that interpreter as I can. The reason to do that processing here instead of on the PC, is all about latency: no round-trip over USB Serial, or waiting for a multitasking OS to get around to it.
 
Might be worthwhile to give links to these other conversations you've mentioned. Not everyone reads every forum message, and especially for people who later find this conversation by search, those other things you've mentioned provide context.

I recall at least 2 people have previously shared DMA-based serial code they wrote. But I don't have links. Maybe someone who reads this will remember where that code is? As I vaguely recall, both were meant for pretty specific usage cases where DMA really helped.
 
I don't remember all of them, but I found two in my browser history:
Same user, different dates, similar code, but not identical.

I've looked through the datasheet enough by now, looking for the boundaries of possibility, that I feel like I can try to make my own from scratch now. Good for understanding at least, if nothing else.
The UART is on pages 2893-2942, with register descriptions on pages 2915-2942.
The DMA is on pages 43-172, spanning three chapters interleaved with interrupts and other related things.

I think it would still be useful to have an official DMA library at some point, with the appropriate warnings of course, of how it's not magic. Not sure how it would tie into or replace the existing peripheral libraries without causing problems though. The last thing we want to do is break a beginner's code when they did everything per the docs.
 
My experience is with the classical Kinetis parts, so this comment is subject to verification with repsect to the RT1060.

Sections 49.3.5.2.1 and 49.3.6.2 (Rev3, 7/2021), "Idle length" implies that the RT1060 has idle-line detection which allows competent management of variable-sized packet protocols whose boundaries are recognizable by interrupt service. The RT doc needs a lawyer-like reading to be sure though. It seems likely to work, the RT is a generation newer than the K series.

My project (started in 2016) drives the K6x UARTS at maximum bit rate (9.375 Mbits/sec given a system clock rate of 150 MHz). DMA is necessary to support that crazy speed (our application fills nearly the entire bandwidth due to a fair amount of daisy chaining in live installations). Admittedly, this is full custom development, all built in house...

Congrats to the Kinetis designers, we are pushing these controllers to their limits.
 
Sections 49.3.5.2.1 and 49.3.6.2 (Rev3, 7/2021), "Idle length" implies that the RT1060 has idle-line detection which allows competent management of variable-sized packet protocols whose boundaries are recognizable by interrupt service.
It does - section 49.3.5.2.1 says so explicitly - but unfortunately, I think I really do need BREAK detection instead of IDLE or address, which appears to be the one thing the RT still can't do directly. It can detect a frame error (incorrect stop bit(s)), but that's as far as the UART goes.

As long as I don't saturate my full-duplex connection above, I might be able to use IDLE detection anyway, or just a frame error, but a different half-duplex mostly-RX could easily be saturated except for the BREAK time, and thus need the BREAK detection specifically.

Byte-oriented RX can simply detect a frame error, or for a longer BREAK, maybe a timer can do that? I haven't looked at the RT's timers yet, but some 8-bit micros can have a timer held in reset by a specified level of a specified input pin (like "space" on RX) and then interrupt at some value (like the minimum BREAK time). If one of the RT's timers can do that, and if a DMA channel can be configured for effectively infinite size (longer than the longest expected), and then cancelled and restarted on that timer event, then maybe that could work? I suppose a short ISR for that would be okay. It would only go off once per message, instead of once per byte, and the processing on that data is going to need the CPU anyway. Flush the UART's FIFO, reset the DMA, set a flag for the main loop, and leave.

My full-duplex above is talking to a loop of much simpler micros that barely have any special functionality. My TX goes to the first one, which modifies the stream and passes it on, and likewise for all the rest until it comes back to my RX. They can't detect IDLE except with an explicit timer, and their BREAK boils down to explicitly bit-banging a "mark" for too long, or receiving a frame error or using a timer as above. No DMA for them either, so they're *definitely* one byte at a time, but the scope of work for each one is limited enough that that's fine.

So in the interest of timely assured detection, I'm using a single BREAK character (null with frame error) to denote the end of a packet, regardless of where it might be in the physical loop. And I think I can send that on the micros by disconnecting their TX pin from the transmitter so that the bit-bang register takes over, sending a character, receiving the normal byte-completed signal, and reconnecting it again. Or by using an on-chip general-purpose LUT to make an AND or OR function (depending on polarity at that point) between TX and a bit-banged input. (intended for modulated TX, where the second input is from a PWM module or something like that, but it also has that bit-banged option and lots of others for general purpose use...)

Much nicer on the RT to simply queue up a BREAK in the normal buffer! It requires 16-bit characters to do that, which the HardwareSerial library needs some modification to support (keep that width throughout, and remove the 10-bit mask), but then it just works! BREAK's can be made as long as desired with no stop bits in between, with character-level resolution, just by putting that many consecutive in the buffer. Likewise for IDLE, possibly to give the receiving end some time to get ready before dumping on it. See page 2932 to build their respective 16-bit character codes.
Still doesn't receive an arbitrary-length BREAK though, as described above, but if the RX side of HardwareSerial is modified the same way, it does receive a frame error, which the application code can check for in place of a normal 16-bit character. Same character code as to send a BREAK, though in some situations it might be useful to use it as a bitmask instead of a strict equality. A corrupted attempt at normal data will have a guess at that data, along with the FE flag, which will likely not match a strict equality. A true BREAK though, *will* be strictly equal to the code to send one, and is detected after one character time when the first stop bit is wrong.

The RT doc needs a lawyer-like reading to be sure though.
No kidding! It's not the worst I've seen, but it's definitely down a ways. Might just boil down to some testing...
 
I haven't looked at the RT's timers yet, but some 8-bit micros can have a timer held in reset by a specified level of a specified input pin (like "space" on RX) and then interrupt at some value (like the minimum BREAK time). If one of the RT's timers can do that, and if a DMA channel can be configured for effectively infinite size (longer than the longest expected), and then cancelled and restarted on that timer event, then maybe that could work? I suppose a short ISR for that would be okay. It would only go off once per message, instead of once per byte, and the processing on that data is going to need the CPU anyway. Flush the UART's FIFO, reset the DMA, set a flag for the main loop, and leave.
Well, section 54.4.5.4 on pages 3058-3059 seems to have compatible wording, but it's unclear whether the timer is reset to 0 for every pulse (reporting only the last individual pulse when finally read) or simply paused and resumed from there (concatenating all pulses until explicitly reset). I can see it working either way, with the way it's worded, and it doesn't have a diagram to confirm.

If it's meant to be read every time and explicitly reset while the CPU is there anyway, then the rest of the wording might be okay as-is, but it doesn't actually say that either. But Instead of looking at every pulse (one or more consecutive "mark" bits), I'm going to ignore it completely until I get a compare match, and assume that that's a BREAK. I am NOT going to explicitly reset it every so many bits!

---

Section 54.4.5.9 on page 3062 has a diagram that is *almost* what I need - start counting from 0 when the input becomes active - but then it continues running while the input is inactive. So any activity followed by IDLE, will still set it off, when I only want BREAK.

---

By the way, thank you @PaulStoffregen for adding notes to this copy! The errata about the timer overflow not working would definitely be a head scratcher to figure out!
 
By the way, thank you @PaulStoffregen for adding notes to this copy! The errata about the timer overflow not working would definitely be a head scratcher to figure out!
Oh it was... it really was.

You might want to consider checking out the FlexIO examples for implementing UARTs, since FlexIO is flexible enough to implement the basic functionality and any extra timers for break detection and such. They're a bit barebones, i.e. they don't implement parity checking but I think that would be fairly easy to add using one shift buffer in state mode. FlexIO can also do pattern matching on received data...
 
Last edited:
You might want to consider checking out the FlexIO examples for implementing UARTs, since FlexIO is flexible enough to implement the basic functionality and any extra timers for break detection and such. They're a bit barebones, i.e. they don't implement parity checking but I think that would be fairly easy to add using one shift buffer in state mode. FlexIO can also do pattern matching on received data...
Hmm... My first thought on seeing a FlexIO-based UART was, "Why bother when I have enough hardware ones already?" But now that you mention those details, it looks a lot more attractive. More reading the datasheet...
 
More reading the datasheet...
From what I understand so far, it's both an inferior transmitter and receiver to the dedicated hardware UART, but it does work, and the two data match functions both prevent actually reading the data itself because the data access register is reused to tell it what to match. I guess I could use two channels with the same RX pin...

Anyway, if I also route the RX pin through a FlexIO Timer's Trigger, set all three events to that trigger (Enable, Disable, Reset), and use the Timer Status interrupt, I can have an arbitrary-length BREAK detector that only tells me that and nothing else. I think that's all that was missing from what I was thinking above.

So, for a sanity-check:
  1. Half-duplex, 99.99...% constant-size TX, free-running saturated:
    • TX:
      • Two DMA transfers, triggering each other on completion:
        1. TX_buffer_1 to hardware UART.
          • Triggers transfer #2 on completion.
        2. TX_buffer_2 to hardware UART.
          • Triggers transfer #1 on completion.
      • Buffers are 16 bits wide, to support BREAKs as normal data.
      • Poll both completion flags:
        • Which one determines which TX_buffer to write to.
        • Otherwise, same processing.
      • To send a different-sized message:
        • Build it in TX_buffer_special.
        • Update a special TCD with:
          • Message size.
          • One of:
            • Pointer to the normal TCD that it's going to replace, for a simple insertion.
            • Termination, if we want a response.
        • Set the running TCD's "next" pointer to the special one.
    • RX:
      • One DMA transfer, not ping-pong like TX is:
        1. Hardware UART to single RX_buffer
          • Configured for "infinite size". (the entire buffer, which is larger than max expected)
          • Re-triggers itself on completion.
      • BREAK detector, as a FlexIO Timer:
        • Input Pin set to the same RX and routed through its Trigger logic.
        • All three functions set to that Trigger alone: Enable, Disable, Reset.
      • Interrupt on BREAK:
        • Flush hardware RX.
        • Cancel the transfer, which automatically restarts it from the beginning.
        • Cache the TCD's byte counter, as a change detector or "new data" flag for the main loop.
      • Poll the running TCD's byte counter for a change from previous:
        • Update change detector.
        • Read and process the RX data so far.
  2. Half-duplex, 99.99...% RX, unknown size but always denoted by a BREAK, free-running saturated:
    • RX:
      • Two DMA transfers, triggering each other on completion:
        1. Hardware UART to RX_buffer_1.
          • Configured for "infinite size". (the entire buffer, which is larger than max expected)
          • Triggers transfer #2 on completion.
        2. Hardware UART to RX_buffer_2.
          • Configured for "infinite size". (the entire buffer, which is larger than max expected)
          • Triggers transfer #1 on completion.
      • Another BREAK detector, same as for connection #1.
      • Interrupt on BREAK:
        • Cache the size indicated in the running TCD.
        • Flush hardware RX.
        • Cancel the running transfer.
          • Automatically triggers the other transfer.
          • Also sets the completion flag, for the poll to catch.
      • Poll both completion flags:
        • Which one determines which RX_buffer to read from.
        • Otherwise, same processing, and same cached size.
    • TX:
      • One DMA transfer, as a simple one-shot, not ping-pong like RX is:
        1. TX_buffer to hardware UART
          • Set the buffer and its size, and explicitly start it.
          • Terminates with just the one transfer.
      • Buffer is 16 bits wide, to support BREAKs as normal data.
  3. Full-duplex, normally synchronized to connection #1:
    • TX:
      • Two DMA transfers, as a double-buffered one-shot:
        1. TX_buffer_N to intermediate buffer.
          • Which TX_buffer is determined by other processing, all the same size.
          • Triggers transfer #2 on completion.
        2. Intermediate buffer to hardware UART.
          • Terminates here.
      • Buffers are 16 bits wide, to support BREAKs as normal data.
    • RX:
      • Three DMA transfers:
        1. Hardware UART to intermediate buffer.
          • Configured for "infinite size". (the entire buffer, which is larger than max expected)
          • Terminates here.
        2. Intermediate buffer to RX_buffer_N.
          • Which RX_buffer is determined by leading data, all the same full size, even if received less.
          • Triggers transfer #3 on completion.
        3. Zero-out intermediate buffer.
          • Terminates here.
      • Another BREAK detector, same as for connection #1.
      • Interrupt on BREAK:
        • Flush hardware RX.
        • Start transfer #1.
    • Polling of connection #1 TX transfer completion flags, also drives this connection:
      • Cancel running RX transfer. (probably #1, still waiting for "infinite" data)
      • Read the routing byte from the RX intermediate buffer, set the destination in RX TCD #2, and start it.
      • Set the source in TX TCD #1, and start it.
    • For Discovery instead (start in this mode):
      • Don't sync to connection #1.
      • Configure special TX TCD for discovery message direct to hardware. (bypass the intermediate buffer)
      • Periodic Trigger by CPU.
      • Receive as normal.
        • Same routing and size fields either way.
Totals:
  • Hardware UARTs:
    • 3
  • DMA channels (possible simultaneous transfers):
    • 6
  • TCD's (individual transfers):
    • 12
  • Interrupts:
    • 3 on BREAK detection, all short
  • FlexIO Timers:
    • 1 for each BREAK detection
    • 3 total
I actually have 3 more UARTs to figure out, but these are the fast ones, with two of them synchronized. The others are usually idle, with no particular timing requirements except for the baud rate itself, and one is abysmally slow.
 
It sounds like FlexIO could be a way to enable your application, I hope it works out for you. (It looks like defining a UART but customized exactly to very specific needs...)

That said, I wonder whether you've seen the LPUART Baud Rate Register (BAUD), which describes LBKDIE at bit 15? In sec 49.6.1.6 which describes the bits in the BAUD register? It look like it enables break detection, and the bit in the STAT is LBKDIF, at bit 31 in the STAT register. So perhaps enable it with LBKDIE, and during interrupt service, recognize it with STAT[LBKDIF]. It is not in the paragraph-formatted docs, it's only in the register bits documentation! Lawyerly indeed...

I found it by doing a search of the PDF for the word "break" which found lots of non-relevant references, many of which are TX related.
 
I wonder whether you've seen the LPUART Baud Rate Register (BAUD), which describes LBKDIE at bit 15? In sec 49.6.1.6 which describes the bits in the BAUD register? It look like it enables break detection, and the bit in the STAT is LBKDIF, at bit 31 in the STAT register. So perhaps enable it with LBKDIE, and during interrupt service, recognize it with STAT[LBKDIF]. It is not in the paragraph-formatted docs, it's only in the register bits documentation! Lawyerly indeed...

I found it by doing a search of the PDF for the word "break" which found lots of non-relevant references, many of which are TX related.
Good find! A bit of googling on the LIN protocol, makes it look like a copy of the frame error flag, but with a hardwired comparison to null added to it. A LIN message starts with a break of nominally 13 bits, followed by an auto-baud byte of 0x55, then a message ID byte, some amount of data, and finally a checksum byte.

Given the potential mismatch between baud rates, and the need to auto-baud every message, I suspect that an intended 13-bit break could be received as 10 bits before the auto-baud arrives, and so that's what the UART detects. No different from a null with frame error.

Some of my connections could use that, but others need a longer break. So do I want some to go one way and some another? I just finished splitting a single "MySerial" into a separate module for each connection, so I easily could...
 
I think it would still be useful to have an official DMA library at some point, with the appropriate warnings of course, of how it's not magic. Not sure how it would tie into or replace the existing peripheral libraries without causing problems though. The last thing we want to do is break a beginner's code when they did everything per the docs.
During some serious poking through the Teensyduino library, following the structure and lining it up with the datasheet, I found that there already is one! And it makes a lot of sense. So nevermind that request.

I'm on the fence about adding it to the online documentation. On one side, "it exists, so it should be documented and easy to find like everything else," but on the other, lots of people still think it's magic and will hang themselves with it...
 
Back
Top