Jack Brzozowski (jtb237) and Kyle Infantino (ki88)
December 6, 2022
The goal of this project is to use a Raspberry Pi microcontroller to control stage lights using the DMX512 protocol. This would allow a small and inexpensive Raspberry Pi to replace the large and costly light controller boards that are typically used for this application.
Short video containing an overview of the design followed by a show illustrating some of the features of the light and a sample light show for "Nutshell" by Alice in Chains.
This project was an effort to simplify and streamline the control of a stage light using the DMX512 protocol. The DMX512 protocol was invented by the Entertainment Services and Technology Association (ESTA) in 1988. The typical setup for producers setting up a light show involves getting a DMX controller, which is a board full of sliders and buttons larger than a typical laptop. These bulky controllers are cumbersome (and in our opinion, unnecessary) so our goal was to eliminate the need for them entirely by implementing the DMX controller by bit-banging GPIO signals. Ultimately, this means that we can replace the typical system with a Raspberry Pi connected to an XLR cable using two GPIO pins. While coming up with a solution that met the timing constraints of the protocol proved challenging, ultimately we successfully created an API capable of running light shows using a Raspberry Pi, and created a robust system that can reliably send packets to fixtures using DMX.
For this project, we used two Betopper LS10 stage light fixtures, pictured below. These fixtures are 9 channel devices, where each channel allows us to control a different aspect of the light, such as its x position, y position, brightness, or color. While it is possible to control the lights independently, for the purposes of this project, we simply daisy chain the two together such that they both perform the same actions.
Betopper LS10 Stage Light
Given the functionality described above, we can achieve this in hardware using a modified XLR cable that connects to our new Raspberry Pi controller, which at the other end connects to the light fixture input. The light fixture also has an output port where we can use a standard XLR cable to connect the two lights together.
Example DMX Universe with Multiple Slave Devices
The DMX512 (or just DMX) protocol is the driving force behind this entire project. The stage light mentioned above, but also many others used in theatrical or musical applications, adhere to this protocol. In theory, this protocol is easy to understand. Whenever the line is idle, a logical 1 should be written to the data bus. To initiate a DMX frame, a “break” condition is applied in which the line is pulled low for a minimum of 92us. To be safe, we implement the break condition to be 100us. The break condition must be followed by a Mark After Break (MAB) which pulls the line high for at least 12us, but we implement this as 14us. After this, the DMX frame begins. A DMX frame consists of one NULL START code, which is 9 zeros, followed by two stop bits. From there, a maximum of 512 data bytes can be sent to all of the fixtures connected to this controller. This grouping of fixtures in an enclosed system with one controller is known as a “DMX universe.” Each byte is prefixed with one low start bit, and post-fixed with two high stop bits. The baud rate of the protocol is 250 kbits per second, meaning that each bit lasts 4us. This portion of the protocol looks a lot like serial UART communication with a baud rate of 250 kbits per second. Unfortunately, the break and MAB make it impossible to use a standard UART bus to control the fixture.
Each byte of data in a DMX Frame is referred to as a “channel.” Each fixture in the DMX universe can configure itself to have a starting address between 0 and 512, which indicates which channel it begins listening to. In the case of our target device, if the address is set to 1, the light will listen to the first 9 bytes of data after the null start code, but if the address is 9, it will ignore bytes 1-8 and listen to bytes 9-17. This allows us to independently control fixtures with the very same packet. However, this also means that we need to send redundant light commands to fixtures that should not move to avoid telling that light to do something undesirable. For example, if one light is supposed to be stationary, but another is supposed to begin some animation, we need to reconfirm to the first light that it should maintain its current position, its current brightness, etc.
Waveform of DMX Protocol
The physical layer of the DMX protocol adheres to the RS-485 physical layer parameters. This ultimately means that the logical values are represented by a differential voltage between a data pair called Data+ and Data-. A logical zero is represented by Data+ being pulled low, and Data- being pulled high, resulting in -3.3V across D+/D-. A logical 1 is just the opposite. For this reason, we can use two GPIO signals without any ground connection to the Raspberry Pi to toggle the data lines. This makes interfacing with devices extremely easy and simple, which is really great when playing different venues that require transporting and setting up equipment over and over again.
Timing Diagram of DMX Protocol
In order to properly send packets to the light, we needed to connect to the light’s XLR port. Obviously, the RasPi does not possess an XLR port of its own, so in order to connect properly to the RasPi, we modified an XLR cable at one end, cutting off the connector to expose the data lines. We soldered solid core wire to the frayed copper to make it easier to plug into a jumper cable, and used heat shrink around each connection to ensure they wouldn’t short. A larger piece of heat shrink holds everything firmly in place and makes the hardware setup relatively clean. Each GPIO is connected to the light through a 220 ohm resistor to match the parameters of the RS-485 standard.
Modified XLR to USB Cable
To ensure we could successfully send any packet over DMX, we first attempted to create a simple script to move the light. Our original plan was to use a USB to XLR wire to communicate with the light from the Raspberry Pi. However, after investigation into multiple Python libraries, we decided implementing the DMX protocol over USB was not feasible. We instead pivoted to using the GPIO pins to bit-bang the protocol. We used the PiGPIO C library to configure the desired GPIO pins and write the output. Since DMX requires a differential voltage between two wires, each wire was connected to a different GPIO pin.
The main issue with the GPIO approach is that DMX is a time-based protocol and requires that each data bit lasts exactly 4 microseconds. The timing of a typical program running on the Debian kernel is much too imprecise for this application. In order to make the timing more consistent, we needed to go through a series of improvements to our design. We used the PreemptRT kernel patch and gave our process a higher priority of 51 to start off. This ensured our process would not be preempted by lower priority processes. To verify the functionality of this solution, we generated a square wave on the Raspberry Pi and viewed it on an oscilloscope. The square wave generated using this setup would achieve 4 microsecond pulses most of the time, but it did not reliably transition at a perfect 4us every time. As it turns out, this was close enough to begin testing with the actual stage light, and send some successful commands to the light.
In order to adhere to the DMX protocol, we had to accurately model the behavior of the idle state, break, mark after break, and null start code, as well as the start bit, data, and stop bits for each data byte. In order to manage the timing of the protocol, we created a “wait” function that simply stalls for some number of microseconds based on the clock frequency of the RaspPi. To implement each step of the protocol, we write the correct value to the GPIO pins and wait for the amount of time defined above. For the initial testing of the design, we used hard coded data packets to minimize potential sources of error.
Once we got simple, hard coded commands working on the stage light, we moved on to creating an easy-to-use library to control the light. Since the light has 9 channels to control different features of the light, we created an array of size 9 with each entry corresponding to a different channel. We then created human readable functions such as “set_xpos” to set the x position of the light or “set_color” to change the color of the light. These functions take in an integer 0-255 and write this value to the array index corresponding to that feature. The mapping of integer values to different light behavior for each feature is specified in the LS10 user manual. For example, a value of 10 in the color channel turns the light red, while a value of 30 turns the light blue. To eliminate the need for constantly consulting the user manual while programming the lights, we included mappings in the form of #define’s of each feature option to their corresponding integer. Once the user has modified the desired features, they call the “send_frame” function. This function sends each value of the array over the GPIO lines in accordance with the DMX protocol.
After creating a basic library for controlling the light, we moved to adding more advanced multi-command functions such as drawing a line or a circle. The draw_line function takes in arguments such as starting and ending x and y angles as well as the speed the light should move. The function then handles orienting the light to the correct starting position and moving it to the ending coordinates. The draw_circle function takes in a circle radius (in degrees) and a movement speed. Since the light only has a 540 degree range of horizontal motion, the function uses the current x position of the light to calculate which direction it should turn in order to achieve a full 360 degree rotation. Since each of these functions do not overwrite other indices in the send array, other desired features for the shape such as light color, gobo, and strobe can be set before calling the function.
Some of the API functions used to control the light
Daisychained Lights Showing Pink Spiral Gobo
The creation of the light control library increased the programmability of the lights, however, we found the error rate of sending commands over GPIO was a significant barrier to using the lights. In terms of performance, this significantly diminished the impact of the performance the lights were attempting to display. For example, a smooth orange spin might suddenly turn into a strobing green shaking back and forth if a packet was misinterpreted. This would make the library effectively unusable for any real application. While the PreemptRT kernel patch and higher priority provided enough precision to send packets, the occasionally inconsistent timing still resulted in a frustratingly large number of these errors. In another effort to minimize disruption of the process, we decided to give the process its own exclusive core. To do this, we first isolated core 2 by adding the “isolcpus=2” command to /boot/cmdline.txt. We then ran the process on core 2 by prefacing the executable with “taskset -c 2”. While a designated core did somewhat reduce the error rate, it was still too high for real-world use.
Waveform of 4us square wave with PreemptRT kernel patch. Each transition is not exactly 4us apart, indicating possible transmission errors.
One idea that we came up with to reduce the visibility of misinterpreted packets was to send packets over and over again every 2ms. The idea behind this is that the error rate we were seeing was roughly 10% by inspection, so 90% of the time the light would be told to do the right thing. For only 2ms, the light may flicker or jitter a bit if some bits are misinterpreted. In practice, this idea did indeed make the light show look a bit better, but there was still clear visible shaking and small flickers that made it look like the light was burning out. We knew this idea still wasn’t where we wanted to be.
The final optimization we implemented to reduce the error rate was the use of hardware PWM. This feature is included as part of the PiGPIO library and allows for data to be directly sent over GPIO lines using DMA. This is done through the use of a coprocessor, meaning the timing of the transmission cannot be disrupted by the linux operating system. What this boils down to is that the waveforms become far more precise in time. Given that DMX has no error checking or clock, this is crucial. All the interfacing with the PiGPIO library was done in the send_frame function, allowing the implementation details of the library to be abstracted away from the rest of our movement library. To implement DMX using the PiGPIO library, we created a “generic” waveform with 94 pulses. Each pulse included an attribute for which GPIO lines should be turned on during that pulse, which GPIO lines should be turned off during the pulse, and how long the pulse should last. Since the DMX protocol uses a differential voltage between two GPIO pins, each pulse would turn on one of the two pins and turn off the other pin. For example, to write a 0, the D+ pin would be turned on and the D- pin would be turned off. The first four of these pulses were used for the break, mark after break, null start, and stop bits. The other 90 pulses were for the 9 frames, with 10 pulses per frame. In each frame, the first pulse was used for the start bit, the next 8 pulses were used for data, and the last pulse was used for the stop bits. Since the length of each pulse could be set independently, a new pulse would only need to be added whenever the data lines changed. For example, instead of adding a new pulse for every 4 microseconds of the break, we could add one pulse with a length of 100 microseconds. We add a generic waveform with the correct number of pulses using the gpioWaveAddGeneric() function. Once the waveform is created, we send the waveform once over the GPIO pins.
The move to hardware PWM within the PiGPIO library allowed us to get extremely reliable packets. Another consideration that needs to be taken into account in music is staying in time with the song. Since this program will be run unsupervised (i.e. it will be executed and run from start to finish) it is really important to be able to accurately measure delays such that the program stays in time. We found that our busy-wait functions were not precise enough for these musical applications. Additionally, they simply busy-waited for some time, but did not consider any other computation time, and therefore would slowly drift further and further out of time. In order to remedy this, we decided to use linux’s “CLOCK_MONOTONIC” to determine when to send packets. Let’s suppose we wanted to be able to send one packet per second. We implement this using two different timestamps represented using the struct timespec data type. One of these timestamps is set by the send_frame() function right before it sends the break condition of the DMX frame. This timestamp is called last_frame, and indicates when the previous frame was sent. We create another timestamp called elig_time, which refers to the time at which packets will be eligible to be sent out over the line. If the current time is greater than the elig_time, then send_frame can immediately send its next packet. In order to make this scheme work, now when we call wait we no longer busy wait in a loop. Instead, we simply set elig_time to be the time of the last frame (set by the send_frame() function) + the wait time. This ensures that as long as the computation takes less than the wait time, we will send frames out at the exact right time with respect to the indicated wait amount specified between frames. When testing this scheme using the actual light we found that it was very accurate and stayed closely in time with the songs we tested.
We wanted to separate the library code from the ability to make light shows associated with different songs. In order to do this, we create a show_skeleton.c file which contains all of the skeleton code for setting up a show. This code contains declarations for global arrays and timers needed for our DMX library and sets up the program’s priority, prefaults the stack, etc. In addition to all of this code, to try to make the runtime as consistent as possible we also initialize the two timestamps mentioned above to get the current time, so the first frame can be sent off at any time. We then initialize the light fixture by setting all of the channels to 0. From there, we call a function called play_show() which is defined in the header file show.h.
In our architecture, different (showname).c files can include show.h, and implement the play_show() function. By compiling with the correct (showname).c file, different binaries can be made without needed to modify any code from previous versions, making creating a show extremely simple and self contained. The play_show() function contains a series of commands from our DMX library applicable to that specific song.
Code Snippet for Example Show
The final result of this project is a reliable working light controller that can be programmed by users. To increase the programmability of the lights, we created a library that allows users to design their own shows without knowledge of the underlying protocols or hardware. We had initially planned on using the touchscreen on the piTFT as a way to control the lights. However, after becoming more familiar with the abundant features of the stage lights, we decided a small touchscreen interface would provide the user insufficient control over the device. As an alternative to this, we created a skeleton program that allows users to create shows by simply adding the commands they want to send to the light, minimizing the amount of programming required. Creating a program has the added benefit of portability, allowing users to create multiple shows in advance and play them on demand.
Initially Proposed TFT Display
During this project, we were able to achieve our initial goal of controlling a stage light using a Raspberry Pi microcontroller. The main issue we encountered during the design process was the reliability of communication to the light, resulting from slight inconsistencies in the timing of when pin values were changed. After many attempts to resolve this issue, including using the PreemptRT kernel patch, giving the process a higher priority and running the process on an isolated core, our final solution of using hardware PWM has resulted in a highly robust system we have yet to see fail. We plan on using this design to control light shows at concerts, replacing the need for an expensive and cumbersome light control board.
We added some functionality for waiting musical intervals, like measures or beats. However, these functions have not been working with the accuracy necessary to be used in the shows. It is very easy to imagine fixing these functions to enable a more logical animation scheduling mechanism for someone who is musically inclined. Another potential idea is to create a function that would save scenes or full animations. This would allow the user to set up a scene one time and call it at the correct moment. This might make the program even easier to write.
Diagrams and stock stage light photos taken from the above Wikipedia pages and Betopper website.
Kyle Infantino (email@example.com) and Jack Brzozowski (firstname.lastname@example.org)
The team worked closely together at the beginning to define the goals and scope of the project and understand the DMX512 protocol. To implement the protocol, we also worked together brainstorming and attempting different solutions to communicate with the light. Once communication with the light was working, we divided the work of creating API functions to set each feature of the light. We also each worked on a sample show, with Kyle working on the first display show seen in the demo video and Jack working on the second show that was in time with the music. On the report, Jack worked on the Introduction, Hardware Setup and Creating Light Shows sections of the Design and Testing section, and Future Work section. Kyle wrote the Sending a DMX Packet in C and Creating a Light Control Library sections of the Design and Testing section, as well as the Results and Conclusion sections. We each worked on parts of the Improving Stage Light Consistency subsection as well.