I was asked by my theatre group (Genesian Theatre) if it was possible to build a controllable wall of LED pixels that could perform certain functions for their upcoming production of Strangers on a train.
If you know the story, there is a moment in the play where the character takes '16 steps' at a key moment in the play (no spoilers). Effectively, they wanted '16 steps' to illuminate as the character walked.
I said: "sure, why not. I'll give it a go".
I then spent the best part of a week exploring different challenges and problems. This is that story.
I already had most of the parts needed to get this up and running from previous projects.
- 4 x 144 LED Strips (WS2812B 5V)
- 2 x 5V @ 14 Amp Power Supplies (Meanwell)
- DMX Shield for Arduino
- Option 1: Arduino Uno
- Option 2: Robo HAT MM1
- Option 3: Raspberry Pi Pico RP2040 (need to purchase)
There were two key challenges with this…
- Challenge 1: What MCU or microcontroller could I use to control 576 WS2812B pixels?
- Challenge 2: How can I control them over the DMX512 protocol?
Since this was for the theatre group, the lighting desk needed to be in-charge of the lights. That was an added layer of complexity I did know about in advance, but was I wrong about how hard it would be. More on this shortly.
I thought that I could solve both challenges with the RP2040 microcontroller. It has enough RAM/memory for the job. It has a nice CircuitPython library for the pixels and supposedly libraries that can read DMX512.
So, I embarked on an adventure that would take me on a journey that will teach me things I will soon not forget.
Plan A: RP2040 + DMX Shield + CircuitPython + Serial.I am a huge fan of Adafruit’s CircuitPython and one of the earliest supporters of it. I’ve written libraries, I’ve written guides (remember the CircuitPython: Creating Custom Boards guide on Hackster.io ). and even added boards to the ecosystem (Robo HAT MM1 was around board number 50 something supported, now over 400).
That aside, I knew the neopixel library was up for the job. It has good optimised code and plenty of guides for using the RP2040.
This was the first time I have ever used the RP2040 (or even bought one). Guides and open-source are key to success.
I hooked everything up like so (using a logic level converter for safety).
I initially thought I could just read the DMX512 packets off the UART bus over Serial and then everything would be hunky-dory, but that was not the case. This was the first problem: How do I know which is the first packet?
Let's chat about what is DMX512...
You can read this detailed guide here about DMX512-A from Element14. It gives you the detailed breakdown. The TDLR from this guide is:
The DMX protocol will send a specific start-of-packet procedure on the serial bus, and then send a sequence of frames. Usually it will perform a start-of-packet procedure, send 513 frames, and then pause (idle) for a while and then repeat. It is explained in more detail next. By the way not all DMX controllers may send 513 frames, some may send less.
So - just for clarification - DMX512 sends 512 'values' that correspond to '512' channels BUT only sends 'values'.
This is where the first problem comes from just reading the serial bus. How do I know which is the first packet?
Unfortunately, as I discovered the hard way, DMX512 doesn't send a specific character or sequence of characters that the normal UART port can read. You MUST use the specific timing of 80 microseconds to detect the start of the stream.
With that new knowledge - Plan A was toast.
Plan B: RP2040 + DMX Shield + CircuitPython + Library.I then attempted to find a CircuitPython library with Pico support for reading DMX signals. I found many libraries for transmitting or sending DMX, but not many for reading it.
So Plan B was dead before it even got off the ground!
Plan C: RP2040 + DMX Shield + Arduino + Library.From experience, I know that Arduino has plenty of community support. When CircuitPython can't do something, you can be pretty sure there is an Arduino alternative.
Again, plenty of control libraries out there (master mode) for DMX, but very few for listening. However, I found one: Pico-DMX by Jostein Løwer.
I really do love the open-source community. It is so great to be able to track down things like this.
Anyway, using the same circuit, I was able to decode and read DMX channels with mixed success. The problem though seemed to be the RP2040 kept crashing or dying after 1-2 seconds. I was not able to debug this, so I had to abandon this as well.
Plan D: Something different from left-fieldI won't explain this one. It was way to complex, didn't involve reading DMX and -12V signals were involved. Basically, it wasn't going to work.
Plan E: Arduino Uno + DMX Shield + Serial + RP2040 + CircuitPythonRunning out of ideas, I decided to strap the DMX Shield design for Arduino onto an Arduino. I knew I wouldn't be able to use the Arduino to control the pixels because of the RAM limitations, however, the challenge to read DMX was defeating me.
Since the DMX Shield had it's own DMX Library for Arduino Uno, I decided to give that a shot. Surely this was going to be reliable...and it was!
Wow, who would have known using things for how they intended was actually going to work?
Alright, so I had the DMX signal nicely decoded on the Arduino. Now to get it into the RP2040 so we could tie this all up in a bow and move on.
Back to reading serial packets on the RP2040 in CircuitPython. Easy peasy.
Not the most elegant solution, however, it was working.
There was a few issues though with the code:
- I was only sending two bytes every 20-50ms (channel then value)
- This means that it would take a minimum of 960ms to 1000ms per update cycle. This is just too slow for a live production.
- Sometimes the packets got out of order (value then channel) which confused the code and then index out of bounded.
One issue at a time...
Plan F: Fix the slow packetsI decided, why don't I just send all the channels and values at the same time. This way I can read them all in with only a single delay.
This took a bit of tinkering, but worked! Now I could get a minimum update cycle of 100ms (or 10 updates per second) for all channels.
while True:
# Read in 96 bytes at a time
data = uart.read(96)
if data is not None:
led.value = True
data_string = ''.join([chr(b) for b in data])
# MORE LOGIC GOES HERE
# . . .
# update all the pixels once a full stream has been processed
update_all_pixels()
# fill in the pixels relevant for that channel (RGB)
led.value = False
# wait 100 ms before next read
time.sleep(0.1)
Plan G: Fix the ordering of arrived packetsThe final implementation challenge. For some reason the serial packets were getting out of order. Probably because the Arduino is sending them faster than the RP2040 can process them (and show pixels).
In the end, this was an unsolved problem. The solution is very painful. You can all post your ideas to make this better in the comments.
I check if the packet is a 0x00 (off) or a 0xFF (fully on). If the packet is neither of these, it must be a channel.
if ord(c) == 255 or ord(c) == 0:
value = ord(c)
# other logic below here...
else:
channel = ord(c)+dmx_offset
# other logic below here...
Now, since this is a closed, single problem and I control all the parameters, this will continue to work. It does mean the code is not generalised. It also means the LEDs cannot dim, however, that is ok and an accepted limitation.
Other Thoughts and ImprovementsAfter I finished this, I had some thoughts on how I could have done this differently...
- Instead of a broadcast style protocol from the Arduino, I could have had it as a request from the Pico. This would guarantee (almost) that the packets arrived in order.
- I'm still unsure why the Arduino Pico-DMX Library wasn't working. Maybe it was an electrical fault in the circuit. Comments welcome.
- If I had more time, I would have tried using things without the logic level converter. Online advice seemed to suggest it made pixel control more reliable. It made everything 2x more complicated power wise.
- Similarly, I am pretty sure Arduino Uno serial is at a lower 3v3 volts with 5v tolerances. I couldn't find anything solid to support this hypothesis. I know I have connected Raspberry Pis and Arduinos together in the past with straight jumper wires. Removing the logic level converter would have been nice.
- Still tossing up soldering the connections together. It's all in a breadboard right now and hopefully it lasts long enough.
- I'm powering everything via the USB ports. Any other method either caused magic smoke (happened 1 time) or instability.
This project was an interesting one that took some interesting turns along the way. It shows some gaps in the specialist field of stage lighting and working in some environments.
The moral of the story is to keep trying, no matter what, and there will always be a solution to your problem (even if it is not your ideal one).
Comments
Please log in or sign up to comment.