Enter Your Project for a chance to win a Nano Grand Prize bundle for the most innovative use of Arduino plus a $400 shopping cart! Back to homepage | Project14 Home | |
Monthly Themes | ||
Monthly Theme Poll |
In my prior blog, I shared the code that I used to generate a simple LCD menuing system (DMX diagnostic tool - Getting started ). Today I am sharing by progress in sending and receiving DMX data streams. This process was a lot more complex than I had though, partially due to using a Nano Classic, with a single UART and partially due to having limited visibility into resource usage (several conflict issues and several forced reboots on the Nano).
I had started this process with the expectation that I could port a lot of my existing code from my ATMEGA328PB based products onto the Arduino Nano. This turned out to be difficult as the ATMEGA328PB part is a bit different than the ATMEGA328P part in the Nano. Here is a summary of these differences:
Resource | ATMEGA328P | ATMEGA328PB |
---|---|---|
Timers | 2-8 bit timers / 1-16 bit timer | 2-8 bit timers / 3-16 bit timer |
UART | 1-UART port | 2-UART ports |
Two Wire Serial | 1-TWI port | 2-TWI ports |
In addition to the limited timers on the Nano, the situation is made worst by some of the timers being reserved by the LCD library. To make matter worst, try as I might, I was unable to use any of the interrupt vectors for Timer1 (16-bit timer). The interrupts did not appear to be in use, as the compiler allowed me to compile and link in the vectors, but at runtime, the Nano would reset set itself if the Timer1 interrupts fired (appearing as if the vectors were not handled correctly). But where there is a will, there is a way. I ended up simplifying my code, using Timer1, but not with interrupts.
With the code ported (and heavily modified), I was able to send and receive data. Here are some scope captures of my DMX data streams.
The Red and Yellow traces are the differential outputs of the RS-485 driver. The Green trace is the generated DMX signal that is pasted to differential driver. At this level of detail, we can see the repeat rate of the DMX signal. It appears to be a little of specification, as it was intended to by repeating at a 50 mSec rate (currently 50.7 mSec). I will need to double check my timing, which is determined by using Timer2.
The Red and Yellow traces are the differential outputs of the RS-485 driver. The Green trace is the generated DMX signal that is sent to differential driver. Looking at the DMX decoding (below the Green trace), the Break pulse (also referred to as Mark) is 80.58 uSec and the Post Mark is 50.97 uSec (set via the menu system to 80 and 50 uSec respectively). The first frame following the Mark sequence is a zero, this is the start code. The second frame, seen here as 23 is being used as a frame counter, which is being incremented with each generated message. All other of the remaining frames (or slots) are set to zero. As I continue to code this product, I want to have the ability to select different frames and set the values either using the joystick, or from a script file stored on an external EEPROM memory.
The Red trace is one half of the differential outputs of the RS-485 driver. The Green trace is the received DMX signal that is received from differential receiver. At this level of detail, we can see the repeat rate of the DMX signal. In this case the DMX signal is being generated by a PC using a crude DMX decoder. The Mark and Post Mark sequences are generated directly from the PC, where uSec timing is not possible, therefore the Mark and Post Mark pulses are about 1 mSec in duration. The Yellow trace is a debug pulse being generated by code on the Aurduino, showing the Mark detection. The Blue trace is another debug pulse, showing the 512 bytes of data in the DMX data stream.
With the same channel assignments as above, I have zoomed into the Mark/Post Mark portion of the waveform, showing the abnormally wide Mark/Post Mark pulses. In this capture the Mark pulse is 1.232 mSec and the Post Mark is 1.055 mSec. These pulses are way outside the DMX specification, where a typical Mark is 80 uSec, but still being detected by the Arduino Nano.
Using the Zoom feature of the PicoScope software, we can see the beginning of the DMX serial stream. After receiving the first serial byte (i.e. the start code, which is equal to 0), the blue trace can be seen going high, signifying the serial data portion of the DMX data stream. The remaining frames are actual DMX data, being sent from my control panel software to command a decoder hosting 12 servos and 3 LED channels.
Reviewing the code changes necessary to transmit a DMX mesage, I will go through the various code snippets. First the high level function, sendDMXpacket() is called every time the millisecond counter exceeds the frame rate value. This code is included inside the loop() {} code.
// DMX processing if (runSendFlag) { if (mSecs > frameRate) { mSecs = 0; DMXdata++; sendDMXpacket(); } }
The sendDMXpacket() function, first disables the UART to generate the DMX preable (Mark and Post Mark pulses), then re-enables the UART and writes the start code value (0), thus starting the UART transmit complete interrupts to continue the transmit process. The Mark and Post Mark pulses are generated by writing directly to the PORTS (D1). The uSwait function uses Timer1 (running on a 2.0 MHz clock) to provide a delay between pulse edges.
void sendDMXpacket(void) { DMXindex = 0; // disable transmitter UCSR0B = 0x00; // generate DMX preable PORTD &= 0xfd; uSwait(breakWidth_us); PORTD |= 0x02; // re-enable transmitter UCSR0B = (0<<RXCIE0) | (1<<TXCIE0) | (0<<UDRIE0) | (0<<RXEN0) | (1<<TXEN0) | (0<<UCSZ02) | (0<<RXB80) | (0<<TXB80); uSwait(markAfterBreak_us); // write first character to the serial port UDR0 = 0; DMXBusy = 1; }
From here the process is handed off to the UART interrupt (Transmit complete) to continue the DMX serial transmission. A simple if statement is used to trigger data being sent to the selected DMX address, otherwise a zero is sent. As the program proceeds, this scheme will be expanded to allow data to sent to a multiple of DMX addresses. As opposed to using the attach method of interrupt processing I am using the ISR connection to the interrupt vector.
// USART0 Transmitter interrupt service routine ISR(USART_TX_vect) { DMXindex++; if (DMXindex < 513) { if (DMXindex == DMXAddress) // ToDo: extend logic to allow multiple active DMX addresses { UDR0 = DMXdata; } else { UDR0 = 0; } } else { // disable xmitter - not sending another character DMXBusy = 0; } }
Here are the code snippets for handling the receiving of a DMX message. I will go through the various code snippets. The highest level of the DMX receive function will live inside of the loop() {}, but as of this writing, I have not attached to data being received. This is will be covered in a later update. For now, we will start the receive process as the function is enabled (inside the menuing system). The DMX receive task is run as a simple state machine, with DMX_state indicating the active state. Here are the DMX states:
DMX_state | Actions |
---|---|
0 | Awaiting a falling edge on the RX line, processed by a pin change interrupt |
1 | Awaiting a rising edge on the RX line that is at least 80 uSec after the prior falling edge. If valid, the Pin change interrupt is disabled and the UART receiver is enabled. |
2 | Actively receiving DMX data via the UART. This continues until the receiver has processed 513 bytes (1 start code and 512 bytes of DMX channel data). |
Here is the initializing code for the DMX receive process:
PORTB &= 0xFC; // debug port bits DMX_state = 0; // clear timer and overflow flag TCNT1 = 0x0000; // clear Timer1 TOV flag (all flags) TIFR1 = 0x27; // enable the pin change interrupt - testing enablePCI(); digitalWrite(rx_en, LOW); // enable receiver
Now the Pin change interrupt is active, waiting changes on the RX line (UART disabled). First the Timer1 counter is read and limited to 255 uSecs, and the timer is reset for the next measurement. Then depending on the DMX_state, we are either looking for a falling or rising edge (falling for state 0, rising for state 1). Here is the code that process the pin changes:
ISR(PCINT2_vect) { unsigned int timerValI; unsigned char timerValC; // read timer value (limited to 0xFF) if (TIFR1 & 0x01) { timerValC = 0xFF; } else { timerValI = TCNT1; timerValI >>= 1; // convert to uSec if (timerValI & 0xFF00) // check if out of expected range (< 255uS) { timerValC = 0xFF; // limit value to max } else { timerValC = (unsigned char) timerValI; // set char version of timerVal } } // clear timer and overflow flag TCNT1 = 0x0000; // clear Timer1 TOV flag (all flags) TIFR1 = 0x27; if (DMX_state == 0) { if ((PIND & 0x01) == 0) // Recv Pin low? { DMX_state = 1; PORTB |= 0x01; // debug - possible start of MARK pulse } } else if(DMX_state == 1) { if (timerValC >= DMXMarkMinWidth) // minimum pulsewidth of Mark pulse { PORTB &= 0xFE; // debug - end of a valid MARK pulse DMX_state = 2; // Disable Interrupt on change PCICR = (0<<PCIE2) | (0<<PCIE1) | (0<<PCIE0); PCMSK2 = (0<<PCINT23) | (0<<PCINT22) | (0<<PCINT21) | (0<<PCINT20) | (0<<PCINT19) | (0<<PCINT18) | (0<<PCINT17) | (0<<PCINT16); PCIFR = (0<<PCIF2) | (0<<PCIF1) | (0<<PCIF0); // enable serial receiver UCSR0A = (0<<RXC0) | (0<<TXC0) | (0<<UDRE0) | (0<<FE0) | (0<<DOR0) | (0<<UPE0) | (0<<U2X0) | (0<<MPCM0); UCSR0B = (1<<RXCIE0) | (0<<TXCIE0) | (0<<UDRIE0) | (1<<RXEN0) | (0<<TXEN0) | (0<<UCSZ02) | (0<<RXB80) | (0<<TXB80); UCSR0C = (0<<UMSEL01) | (0<<UMSEL00) | (0<<UPM01) | (0<<UPM00) | (1<<USBS0) | (1<<UCSZ01) | (1<<UCSZ00) | (0<<UCPOL0); UBRR0H=0x00; UBRR0L=0x03; DMX_byte_count = 0; } else { DMX_state = 0; PORTB &= 0xFE; } } }
With a valid Mark pulse detected, the Pin change interrupt is disabled and the UART is enabled. From here, the process is handed over to the UART to receive the incoming DMX data using the Receive complete interrupt.
ISR(USART_RX_vect) { char data; char done = 0; status = UCSR0A; data = UDR0; if ((status & (FRAMING_ERROR | PARITY_ERROR | DATA_OVERRUN)) == 0) { if (DMX_byte_count == 0) { if (data == 0) { PORTB |= 0x02; // debug - start code received } } // monitor DMX byte count if (DMX_byte_count >= 512) { done = 1; DMXdataReady = 1; } else { DMX_byte_count++; } } else { // insert code to flag/log errors PORTB &= 0xFC; // debug clear all debug flags done = 1; } if (done) { // disableSerialRecv(); UCSR0B = (0<<RXCIE0) | (1<<TXCIE0) | (0<<UDRIE0) | (0<<RXEN0) | (0<<TXEN0) | (0<<UCSZ02) | (0<<RXB80) | (0<<TXB80); // clear Timer1 TOV flag (all flags) TIFR1 = 0x27; TCNT1 = 0; // enable Interrupt on any change on pins PCINT16 PCICR = (1<<PCIE2) | (0<<PCIE1) | (0<<PCIE0); PCMSK2 = (0<<PCINT23) | (0<<PCINT22) | (0<<PCINT21) | (0<<PCINT20) | (0<<PCINT19) | (0<<PCINT18) | (0<<PCINT17) | (1<<PCINT16); PCIFR = (1<<PCIF2) | (0<<PCIF1) | (0<<PCIF0); DMX_state = 0; PORTB &= 0xFC; } }
From here, I am working on expanding the menu system, to complete the project.
Thanks for following along on my project.