Receive DMX-512 with an Arduino
Part II: InstructionsPart III: Known Limitations, Notes, and the Code
Known Limitations:
- Atmega168 and Atmega328 based processors only (you will have to rename the registers and interrupt vectors if you want to use it for another processor).
- Currently only tested with a USB-DMX Pro controlled via Lightfactory and MagicQ software, since that’s what I could get my hands on. But it handles a variety of frame rates, break lengths, etc. just fine. If you’re in the Portland area, and you’d like to let me test it with other controllers, drop me a line. Also, if you use it with other controllers, let me know how it turned out.
- Because I needed access to exact timing, I had to use the Timer2 functionality, so pins 3 and 11 cannot be used for PWM. This is also the most frequent cause of incompatibility problems when using community-developed libraries. See the comments for remedies if you’re affected by this.
- Will not detect bad addressing. For example, with it set to receive the default 8 channels, “dmxaddress=510;” would give you two good channels and six channels of junk. Or ”dmxaddress=50;” when only 55 addresses are sent by the controller.
- When addressing, sometimes when you hit the 0 or 1 switch it doesn’t take. I’ve programmed the LED to turn off briefly if the bit was successfully entered, so if you don’t see it go off, you’ll have to hit the switch again until it takes. I don’t know why it’s doing this, if you have some time to wade through the logic let me know why and I’ll update the code.
Future Development:
- Add a dip switch and code to allow in-the-field addressing.
- Reduce the number of Atmega168-specific functions to improve code portability.
- Design a shield for better durability, signal pass-through, termination, etc. Check here!
- Add frame error and data overrun error handling routines.
- Add bad start address value detection, based on the number of frames received (so if you set the start address to 50, and the number of addresses to 10, but only 55 DMX slots are being sent by the controller, the error will be indicated by the pin 13 LED).
- Add RDM functionality.
- Develop a separate DMX monitor application.
Release History and Notes:
- 9 July 2009: Rev13
- Tested and working with IDE version 0016.
- The number of channels to receive is now easily user-configurable.
- Replaced static variables with #define statements for memory optimization (+48 bytes, woot!).
- 12 May 2009: Rev12
- In-the-field addressing via two tact switches (works with the previously released I/O Shield, here).
- Address is stored in non-volatile EEPROM, so it is retained when power is lost to the Arduino.
- Addressing hardware allows full use of the pins (which is why I didn’t use the more conventional dip switch setup).
- Some of the variables were localized, since the sketch is now getting pretty complex.
- 27 April 2009: Rev11
- Adjusted pin layout for better routing on the DMX I/O Shield. Pins 3 and 4 in Rev10 are now pins 2 and 3, respectively.
- 1 April 2009: Rev10
- Cleaned up and improved code commenting.
- Adjusted HardwareSerial.cpp (included) so the code will compile on Arduino software release 0015. If you’re still using 0014 or 0013, you’ll replace wiring_serial.c instead (also included).
- Replaced manual register configuration of the USART with the Arduino function serial.Begin(250000), which apparently works just as well and reduces the number of Atmega168-specific register calls considerably.
- Moved the action() loop (what you want the Arduino to do with the received values) to its own tab, to make the code easier to use.
- 20 March 2009: Rev09
- First release
- Rev00-Rev08: Pre-release betas.
The Code: Here is the .pde sketch file. It may be of general interest as well if you’re trying to write interrupt-based programs for the Arduino. In the download, there is also a tab for the user code and another for in-the-field addressing module.
/***********************************************************
* DMX-512 Reception *
* Developed by Max Pierson *
* Version Rev13 9 July 2009 *
* Released under the WTFPL license, although I would *
* appreciate Attribution and Share-Alike *
* See blog.wingedvictorydesign.com for the latest version. *
************************************************************/
/******************************* Addressing variable declarations *****************************/
#include
#define NUMBER_OF_CHANNELS 8
//the number of channels we want to receive (8 by default).
#define SWITCH_PIN_0 11 //the pin number of our "0" switch
#define SWITCH_PIN_1 12 //the pin number of our "1" switch
unsigned int dmxaddress = 1;
/* The dmx address we will be listening to. The value of this will be set in the Addressing()
* function, if triggered, and read from EEPROM addresses 510 and 511.
/******************************* MAX485 variable declarations *****************************/
#define RECEIVER_OUTPUT_ENABLE 2
/* receiver output enable (pin2) on the max485.
* will be left low to set the max485 to receive data. */
#define DRIVER_OUTPUT_ENABLE 3
/* driver output enable (pin3) on the max485.
* will left low to disable driver output. */
#define RX_PIN 0 // serial receive pin, which takes the incoming data from the MAX485.
#define TX_PIN 1 // serial transmission pin
/******************************* DMX variable declarations ********************************/
volatile byte i = 0; //dummy variable for dmxvalue[]
volatile byte dmxreceived = 0; //the latest received value
volatile unsigned int dmxcurrent = 0; //counter variable that is incremented every time we receive a value.
volatile byte dmxvalue[NUMBER_OF_CHANNELS]; //stores the DMX values we're interested in using.
volatile boolean dmxnewvalue = 0; //set to 1 when new dmx values are received.
/******************************* Timer2 variable declarations *****************************/
volatile byte zerocounter = 0;
/* a counter to hold the number of zeros received in sequence on the serial receive pin.
* When we've received a minimum of 11 zeros in a row, we must be in a break. As written,
* the timer2 ISR actually checks for 22 zeros in a row, for the full 88uS break. */
void setup() {
/******************************* Max485 configuration ***********************************/
pinMode(RECEIVER_OUTPUT_ENABLE, OUTPUT);
pinMode(DRIVER_OUTPUT_ENABLE, OUTPUT);
digitalWrite(RECEIVER_OUTPUT_ENABLE, LOW);
digitalWrite(DRIVER_OUTPUT_ENABLE, LOW); //sets pins 3 and 4 to low to enable reciever mode on the MAX485.
pinMode(RX_PIN, INPUT); //sets serial pin to receive data
/******************************* Addressing subroutine *********************************/
pinMode(SWITCH_PIN_0, INPUT); //sets pin for '0' switch to input
digitalWrite(SWITCH_PIN_0, HIGH); //turns on the internal pull-up resistor for '0' switch pin
pinMode(SWITCH_PIN_1, INPUT); //sets pin for '1' switch to input
digitalWrite(SWITCH_PIN_1, HIGH); //turns on the internal pull-up resistor for '1' switch pin
byte switch1 = 0;
byte switch0 = 0; //the switch states for the 1 and 0 pin
switch0 = digitalRead(SWITCH_PIN_0);
switch1 = digitalRead(SWITCH_PIN_1);
if (switch0 == 0 || switch1 == 0)
{
Addressing(switch0, switch1);
//call the addressing subroutine if either switch is pressed.
}
//read the previously stored value from EEPROM addresses 510 and 511.
dmxaddress = EEPROM.read(511); //read the high byte into dmxaddress
dmxaddress = dmxaddress << 8; //bitshift the high byte left 8 bits to make room for the low byte dmxaddress = dmxaddress | EEPROM.read(510); //read the low byte into dmxaddress dmxaddress = dmxaddress + 3; /* this will allow the USART receive interrupt to fire an additional 3 times for every dmx frame. * Here's why: * Once to account for the fact that DMX addresses run from 0-511, whereas channel numbers * start numbering at 1. * Once for the Mark After Break (MAB), which will be detected by the USART as a valid character * (a zero, eight more zeros, followed by a one) * Once for the START code that precedes the 512 DMX values (used for RDM). */ /******************************* USART configuration ************************************/ Serial.begin(250000); /* Each bit is 4uS long, hence 250Kbps baud rate */ cli(); //disable interrupts while we're setting bits in registers bitClear(UCSR0B, RXCIE0); //disable USART reception interrupt /******************************* Timer2 configuration ***********************************/ //NOTE: this will disable PWM on pins 3 and 11. bitClear(TCCR2A, COM2A1); bitClear(TCCR2A, COM2A0); //disable compare match output A mode bitClear(TCCR2A, COM2B1); bitClear(TCCR2A, COM2B0); //disable compare match output B mode bitSet(TCCR2A, WGM21); bitClear(TCCR2A, WGM20); //set mode 2, CTC. TOP will be set by OCRA. bitClear(TCCR2B, FOC2A); bitClear(TCCR2B, FOC2B); //disable Force Output Compare A and B. bitClear(TCCR2B, WGM22); //set mode 2, CTC. TOP will be set by OCRA. bitClear(TCCR2B, CS22); bitClear(TCCR2B, CS21); bitSet(TCCR2B, CS20); // no prescaler means the clock will increment every 62.5ns (assuming 16Mhz clock speed). OCR2A = 64; /* Set output compare register to 64, so that the Output Compare Interrupt will fire * every 4uS. */ bitClear(TIMSK2, OCIE2B); //Disable Timer/Counter2 Output Compare Match B Interrupt bitSet(TIMSK2, OCIE2A); //Enable Timer/Counter2 Output Compare Match A Interrupt bitClear(TIMSK2, TOIE2); //Disable Timer/Counter2 Overflow Interrupt Enable sei(); //reenable interrupts now that timer2 has been configured. } //end setup() void loop() { // the processor gets parked here while the ISRs are doing their thing. if (dmxnewvalue == 1) { //when a new set of values are received, jump to action loop... action(); dmxnewvalue = 0; dmxcurrent = 0; zerocounter = 0; //and then when finished reset variables and enable timer2 interrupt i = 0; bitSet(TIMSK2, OCIE2A); //Enable Timer/Counter2 Output Compare Match A Interrupt } } //end loop() //Timer2 compare match interrupt vector handler ISR(TIMER2_COMPA_vect) { if (bitRead(PIND, PIND0)) { // if a one is detected, we're not in a break, reset zerocounter. zerocounter = 0; } else { zerocounter++; // increment zerocounter if a zero is received. if (zerocounter == 22) // if 22 0's are received in a row (88uS break) { bitClear(TIMSK2, OCIE2A); //disable this interrupt and enable reception interrupt now that we're in a break. bitSet(UCSR0B, RXCIE0); } } } //end Timer2 ISR ISR(USART_RX_vect){ dmxreceived = UDR0; /* The receive buffer (UDR0) must be read during the reception ISR, or the ISR will just * execute again immediately upon exiting. */ dmxcurrent++; //increment address counter if(dmxcurrent > dmxaddress) { //check if the current address is the one we want.
dmxvalue[i] = dmxreceived;
i++;
if(i == NUMBER_OF_CHANNELS) {
bitClear(UCSR0B, RXCIE0);
dmxnewvalue = 1; //set newvalue, so that the main code can be executed.
}
}
} // end ISR
Go to Section:
- Part I: Prologue
- Part II: Instructions
- Part III: Known Limitations, Notes, and the Code
- View All


December 20th, 2009 at 1:21 pm
Hi. Very interesting project thanks! can be used in various applications. I would like to add encoder and managed using several DMX channel setting motor speed and position
December 21st, 2009 at 4:52 pm
Firstly, thanks and well done for cracking this, I’ve wanted to be able to add DMX to my lighting projects for a long time now but sussing it out myself is currently beyond my ability. I’m totally building one of these! :D
Something I’m unsure about though, Rapid Electronics don’t have a “MAX485″, but they do have “MAX485CPA+” and “MAX485CSA+”. Are either of those suitable for this? I don’t know what the difference is.
Thanks again for publishing your work,
Dan
December 22nd, 2009 at 2:08 pm
Hi Daniel,
The difference is the package. The CPA is a through hole package, whereas the CSA is the surface mount. You’ll want the CPA. Best of luck with your project!
January 5th, 2010 at 3:45 pm
Hi there,
Any update on the code? Will there be a future version that does not need to modify the Arduino lib?
January 9th, 2010 at 3:40 pm
Incredible work!
I am having trouble getting things to work for me.
I am using Arduino 0017.
Do you have any more recent builds of your project?
Thanks so much!
January 11th, 2010 at 9:23 am
Hi All,
I will put together an update for IDE 0017 or verify that the current version works. If you don’t want to wait, you could just take a look at what I’ve done to the hardware.cpp file and do the same to the IDE 0017 version.
Note that the hack to the Arduino library files is necessary due to code on the Arduino side, not my software, so I have no control over when/if this will be fixed.
January 29th, 2010 at 6:38 am
For everyone else using 0017:
The mod you need to make to the hardwareserial.cpp is this:
Change line 115 from:
uint8_t rxen, uint8_t txen, uint8_t rxcie, uint8_t udre)
to:
uint8_t rxen, uint8_t txen, uint8_t rxcie, uint8_t udre, uint8_t u2x)
and it should compile.
February 25th, 2010 at 8:57 am
The fix given by BlueFusion does work. One note for a little more clarity: you should do this as an additional modification in the already modified hardwareserial.cpp file (the one included in the download from this site).
Thanks anyway for the work already done on this, I will be constructing a centerpiece for a disco with this which will have 8 pinspots, mirrors and servo’s! (and maybe we will abuse it every now and then with our band :))
February 27th, 2010 at 3:39 pm
Can your modifications send and receive or not?
March 1st, 2010 at 3:58 pm
Doing research.
Looking for a solution.
I Need to manipulate 2000+ 3w-5w LED’s for my art project.
http://www.TheBallofLight.com
Your project seems like it would allow for it. Each of the 2000 lights would need to be able to be driven by a central DMX console. As I read each of your units can manipulate 512 individual lights. That sounds astounding. That would mean I would need to make only 4 of these units. That would also mean 4 Universes I believe. I think I would also need 4 optical splitters to lead to these units. Then back to the Console.
I have many questions on this topic and am just getting started on a two and half year project.
If you are interested at all interested I would love to tell all and pick your brain some you seem extremely knowledgeable on this subject.
March 2nd, 2010 at 12:25 am
I actually would not use DMX for this application– I would try to get your LEDs to take a video input. That way, you’ll be able to program it from a VJ software (such as Arkaos, there are dozens) rather than a theatrical lighting console, which will give you a more intuitive and flexible programming paradigm. Also, I would try to source your LED drivers from commercially available hardware such as the driver boards for LED billboards. By the time you buy the components for 2,000 DIY drivers, you won’t be saving any money, and the soldering involved gives me the creeping horrors. I mean, why spend all your time reinventing the wheel, when you could be focusing on the art? My $.02