Sanity check: USB->DMX adapter with processing, on a Teensy 4.1

AaronD

Active member
There are several libraries for asynchronous DMX, where the driver basically freewheels in the background to produce a constant spew of the latest values, and the user code simply pokes new values in at random and moves on. That's fine for a general-purpose project that's easy to code, and has some pretty lax timing requirements (even at worst-case 44Hz, a human is not going to notice), but I have a more complex project that does raise some concerns about overall latency, and so each individual component gets squeezed tighter than it would by itself.

Essentially:
Code:
+----------------- PC -----------------+              +----- Teensy 4.1 -----+        +---------- AVR ----------+
|                                      |              |                      |        |                         |
|            network_loopback          |              |                      |        |                         |
|                    |                 |  USB_Serial  |                      |  UART  |                         |  DMX
|  [DMX Controller] --> [Custom App] -------------------> [more processing] ------------> [capture and replay] ---------> [Fixtures]
|                                      |              |                      |        |                         |
+--------------------------------------+              +----------------------+        +-------------------------+

I want that entire chain to be as fast as possible, so if the user decides to use a soft button on a touchscreen, all the way at the start of the chain, instead of a physical button attached to the Teensy, it still feels at least somewhat responsive. Or an audio lowpass in the same off-the-shelf PC DMX Controller to connect a strobe to the kick drum, etc.

Async effectively has random latency per frame, or possibly per channel, depending on how each library actually works, anywhere between zero and one full frame. I want as close to zero as possible, and constant. Thus, no transmission from the Teensy until a frame is received, so it's all ready to go immediately when it comes, then modify that one frame and send it out. Then no transmission until the next frame, etc. So the "easy to use" async libraries are probably not going to work for this.

The AVR at the end, automatically switches to its own stored data when its input stops, so that the fixtures' no-signal behavior is not triggered. When the AVR does have an incoming stream, it's transparent with 1 byte of latency. (just enough to read from the UART and stuff it back in again)

I also had thoughts of the Teensy being both an RDM master and slave on different ports, to talk to some fixtures and to respond to an upstream controller, but as I continue to lay things out, it looks more and more "not worth it", at least as a master, towards the fixtures. I'd have to get RDM past the AVR in both directions, for one thing...

Anyway, my first thought was to have a fully standards-compliant DMX stream from the Teensy, except that it's synchronous to the incoming data from the PC and therefore may stop at any time and that's okay. But I suspect the better solution would be to *require* the AVR downstream from it, to convert from my own standard into DMX in addition to providing that safety net.

Either way, I think I need a physical Serial driver on a Teensy 4.1 that can take a buffer of 513 bytes all at once (the Teensy's processing could modify anything, so I can't send any of it until that's done), send it immediately, break at the end, and stop there until the next buffer is available. Or it could break first and then send 513 bytes, and just run out of data and stop.
The break could be done by messing with the baud rate and transmitting a zero like the libraries do, or by a GPIO and an external AND with the TX pin.

Is there a library that already does that? Or if not, how hard is it to make one? No RX, just TX. Just break, and dump a buffer.

---

For a possible upstream DMX controller that connects to the Teensy and gets fed upstream from there to the PC Controller, it pretty much has to be async, and so the existing libraries are probably fine. Since that connection is pretty much standard, I wonder about allowing RDM *there*? And since RDM is *half-duplex*, I also wonder if I can break the rule about not using the same UART for both TX and RX?

As I understand, the reason that rule exists is because this hardware doesn't actually have a break character like some do. So to avoid using another pin, it changes the baud rate, transmits a zero, and changes it back. That completely wrecks reception, hence the rule, but again this is *half-duplex*. I'm not going to try to receive during a break.

Do the more mature DMX libraries support *that*? Half-duplex DMX/RDM on the same UART, plus a GPIO to control the direction of a transceiver chip? The README's and manuals aren't exactly clear about that, except to say "don't combine directions" and why, which leaves this question.
 
Well, this changes things: I just noticed that the RS485 transceivers run at 5V per the '485 spec, while the Teensy's I/O is 3.3V and *not* 5V tolerant.

So I need some level conversion too, which tips the balance in favor of:
  • An external AND to create the TX break: powered from 5V with TTL inputs (not CMOS)
  • An external OR to continue driving RX high through a voltage divider, when the transceiver is transmitting and its RX output is Hi-Z with a pull-up resistor (direction=high to transmit, which also feeds the OR, just to be sure)
  • A single NPN/N-channel transistor, base/gate to 3.3V (with resistor for BJT), emitter/source to Teensy's output, collector/drain with pull-up to 5V, to create a slow level shifter for the direction signal
Which puts me back to 4 pins per serial port - TX, RX, Break, Direction - and no baud shuffling.

Or I could save an output, which I don't really need to do, by using a 5V-powered TTL-input buffer for each of those three signals (half of a hex chip) and bringing back the baud shuffling.

I actually need several of these for some other '485 connections, not just DMX, which can justify a quad chip for each type, or two hex buffers.

Anyone else have thoughts or experience either way?
 
...a 5V-powered TTL-input buffer...
It appears that those don't exist. The part number would be 74*HCT07* or thereabouts, but it seems that nobody makes that with a push-pull output. Open-drain exists, but that doesn't help. Lots of inverters though: 74*HCT14*

Not the end of the world...if the Teensy's UART can itself be inverted like some can. The AVR, for example, can invert any pin independently of whatever peripheral it might be attached to, and I've seen other microcontrollers that can invert the UART itself. Either way would work. Can the Teensy do that?

Edit:
Having finally found the datasheet, it appears that the answer is yes, it can invert the UART itself. So a 5V-powered TTL-input push-pull-output inverter is a viable option.
AND it can send a break character directly, with no baud changes! So what are the DMX libraries doing with the baud rate?!
AND it can drive a transceiver's direction pin automatically in hardware! Seriously! What are the DMX libraries doing?!
I might have to go vent over there now...
https://www.pjrc.com/teensy/IMXRT1060RM_rev3_annotations.pdf pages 2898 - 2902
 
Last edited:
Not the end of the world...if the Teensy's UART can itself be inverted like some can. The AVR, for example, can invert any pin independently of whatever peripheral it might be attached to, and I've seen other microcontrollers that can invert the UART itself. Either way would work. Can the Teensy do that?
This is handled by specifying the appropriate mode when calling the uart's begin() function, e.g. Serial1.begin(9600, SERIAL_8N1_RXINV_TXINV) specifies 9600 baud using 8 data bits, 1 stop bit, both RX and TX inverted.
Although I think it would be simpler to use an open drain buffer with pull-ups...
 
...yes, it can invert the UART itself. So a 5V-powered TTL-input push-pull-output inverter is a viable option.
This is handled by specifying the appropriate mode when calling the uart's begin() function, e.g. Serial1.begin(9600, SERIAL_8N1_RXINV_TXINV) specifies 9600 baud using 8 data bits, 1 stop bit, both RX and TX inverted.
Nice! I hadn't seen that in the documentation, but it's possible that I just missed it. Thanks!

Although I think it would be simpler to use an open drain buffer with pull-ups...
I'm concerned about parasitic capacitance, but maybe 250kbaud is still slow enough to not worry about it?

---

And I still have the question of a synchronous serial driver, instead of the DMX libraries' freewheeling async design. Considering some other RS485/RS422 links in addition to DMX, how hard is it to:
  • Directly send a break on TX
  • Dump a big buffer to TX immediately before or after the break
  • Detect a break on RX
  • Capture an unknown-size buffer on RX
Yes, some of those are basic fundamentals that are covered on the front page for the UART library (the last one boils down to polling Serial1.read() and handling one byte at a time), but not all are.
(And yes, I did miss the inversion options. They're on that page, towards the bottom.)
 
Last edited:
Have you ruled out using 3.3V RS485 chips?
As an example MAX3485 and others to choose from.
 
Have you ruled out using 3.3V RS485 chips?
As an example MAX3485 and others to choose from.
No, I just never considered them. I had always understood '485 and '422 to be 5V things, and that was just that. You made me look up some more actual specs and, sure enough, it works down to 1.5V differential (3V pk-pk) in addition to 6V max (12V pk-pk)

That greatly simplifies things too! Thank you for the reminder that "rules of thumb" aren't always universal and are sometimes dubious to start with.
 
...how hard is it to:
  • Directly send a break on TX
  • Dump a big buffer to TX immediately before or after the break
  • Detect a break on RX
  • Capture an unknown-size buffer on RX
According to Arduino IDE's autocomplete, SerialX.write() has more overloads than the online documentation says. Can the online doc be updated?
Specifically, there are (const char *buffer, size_t size) and (const uint8_t *buffer, size_t size), of which the latter appears to be exactly what I need...but it's not mentioned on that page.

So, between that and polling SerialX.read(), I think that has the buffers covered...except for knowing the default size, or whether I need to
addMemoryForWrite or not. I guess I could just do that anyway, with a full-size buffer of my own, just to be sure?

But the breaks still aren't answered. The datasheet says at the top of page 2899, "The CTRL[SBK] bit sends break characters...", and continues to describe some other ways to do it too. Is that implemented? Both transmission as on that page, and reception?
 
Notes (I've also answered elsewhere in relevant places):
1. The hardware send-break function doesn't send a break that's long enough for DMX. I also believe that every time you send a hardware break, it also sends a start and stop bit.
2. The TeensyDMX library can be operated synchronously.
 
I responded to the GitHub issue, and closed it as "no action required", but I'll re-type it here for those that read this and not that.

Yes you're right. Re-reading the official DMX spec, I see a minimum break time of effectively 23 bits. Typical 44 bits. If the hardware insists on sending stop bits every 13 or less, then that's not going to work. Sorry for the trouble.

That then, begs the question of, "Why is the break so long?" It's an old standard, and the original CPU clocks might not have been much faster than the bitrate (1MHz, perhaps?), so maybe a bit-banged hardware driver with that clock just needed some time to figure out what's going on? And that concession got baked in so we're now stuck with it?

For operating the library synchronously: Yes, I saw that in the manual, and I haven't *completely* gone away from it, but:
  1. I see unnecessary overhead, in the form of the mechanics of making it asynchronous free-running, and then restricting *that* to a series of one-shots. The fast responses that I want the Teensy to do, are user-defined, and I'm a little bit concerned with a 600MHz ARM7 keeping up with a ton of interpreted user instructions. A few are fine, but I do want to support the user as much as possible.
  2. The AVR downstream will intercept what comes out of the Teensy, so the physical stream stops there and doesn't necessarily *have* to be standards-compliant DMX yet. What comes out of the AVR does, but that's my own code on baremetal.
 
The BREAK is longer than one character because if it wasn't, it would be hard for a receiver to tell the difference between noise and a zero character on one hand, and a valid frame start on the other.

Now, there was some discussion a couple years ago in one of the DMX meetings on what a receiver should allow for a minimum BREAK time. The spec says what minimum BREAK time a receiver must accept, but some manufacturers read that as being able to accept shorter-than-the-minimum BREAK time because accepting shorter BREAKs doesn't mean that a device doesn't _also_ accept the minimum required BREAK. Those notes got into the latest version of the DMX512 spec.

See ANSI E1.11 - 2024, "Note 2" after "Table 7 - Timing Diagram Values for Receivers" on page 16 (exact page 30 in the PDF):
Note 2: Receivers may accept BREAK timings less than the minimum value defined in Table 7 unless specifically disallowed elsewhere in the standard.

However, accepting BREAKs that are too short makes finding the start of a frame more difficult and error-prone.
 
The hardware send-break function doesn't send a break that's long enough for DMX. I also believe that every time you send a hardware break, it also sends a start and stop bit.
In playing with it myself, including some tweaks to the official HardwareSerial library, I discovered that a Teensy 4.1 at least, does *not* put stop bits between consecutive breaks! So you can buffer up four BREAKs in a row, followed by one IDLE for a lazy MAB, then data.

See the other threads for more details, including code and logic analyzer:
 
Back
Top