DMX Input / Output

Posted on
  • Hi

    When I receive my PICO one of the projects I want to look at is a DMX to Ethernet gateway, initially I will be receiving DMX data, but may want to transmit in the future. Does anyone have any experience of DMX on the Espruino?

    DMX is a serial protocol that runs at 250kbaud see http://en.wikipedia.org/wiki/DMX512

    Its very simple in structure the message stream is started by receiving a Break the a control byte which is 0 followed by up to 512 bytes of data.

    Regards

    Martin

  • Hi Martin,

    I can't actually remember if anyone has used DMX before, but I'm quite interested in getting something working (I have some kit here already)... You should be able to send a 513 character string in one chunk pretty easily, so DMX output should be trivial (although the 128 byte output buffer will mean that you code will block until most of the DMX data is sent unless you send it in chunks).

    On the input side, Espruino has a 512 byte input buffer so getting reliable data in shouldn't be too hard either as long as your code doesn't block for too long. The only potential issue would be in detecting the gap between frames - if it's the minimum of 88us then you might have to resort to some external hardware (a capacitor + resistor) to detect the gap so that a function like setWatch will work.

    So yes, it should be fine - getting RX and TX at the same time might take a bit more fiddling, but should still be possible.

    The only potential problem area is if you were to try and control WS2811 LEDs - because in order to get the output speed high enough, Espruino has to disable interrupts when writing to them - which means it'd lose characters of serial data.

  • I guess you could connect two Espruino's together via I2C, SPI, UART and use one as a DMX processor the other as an Ethernet / Wifi / WS2811 processor. Although I'm not using WS2811 I'm trying to think of something that would be of most use to everyone.

    Raw DMX running at full bore is just continuous data with a break every 513 bytes. Note you do not need to send 513 bytes it could be less. This would put quite a load on the receiving device so using one Espruino just to handle DMX receiving might be a good idea.

    When I've implemented DMX before on other processors, I've cheated by detecting a framing error and assuming that this is a received break.

    Transmitting 4 x 128byte chunks for the output would be fine I think as in my experience DMX recievers only wait to see a break to restart comms, so do not care about a slight gap in the data stream.

    Does the STM chip warn when the TX buffer is getting empty, to give the software time to re-fill it? Or can it be fed from DMA?

  • WS2811

    Yes - WS2811 is one of the obvious things to do. Personally I'd really like to improve the SPI output code at some point soon (using DMA - it's already partially implemented for the TV out). When I do that then the WS2811 code wouldn't have to disable interrupts and everything would 'just work'.

    I've cheated by detecting a framing error

    You could probably check that with peek32 - however making sure that your JS got called at exactly the right point to check it could be problematic.

    Using the (hacky) resistor/capacitor thing fires a watch event, which shoves data into the same queue as the received serial data - so even if the JS doesn't get run immediately it still gets called at the same time relative to the Serial data that's been processed to date :)

    Transmitting 4 x 128byte chunks for the output would be fine

    Yes - actually you should be able to do 8x64 byte chunks with enough of a delay between that the data just gets dumped into the TX buffer and there's no blocking, but without running the risk of the TX buffer emptying and there being a pause.

    Does the STM chip warn when the TX buffer is getting empty

    There's only a single byte TX buffer afaik (it's all done via IRQ in Espruino). You could probably set up DMA for transmits (and receives) though.

    The issue with Espruino is that I don't allocate individual buffers for peripherals (because if you did that you'd use all your memory right at the start) so the buffers are shared and it makes it almost impossible to DMA.

    I'd have to add the ability to dynamically allocate a buffer, DMA, and then deallocate it after - quite a big change :)

  • @mhoneywill have you made any progress on DMX in/out?

    I am likely to be looking into this soon after I get the hang of my espruino.

  • @Gordon, could Wave be used to 'sub' it into the firmware?

  • I am also trying to do this for a project quite urgently, I was trying to use Arduino but the example code out there just doesn't work... I really don't know where to start... I have hardly used Espruino before so I am a bit lost...

    I managed to get it reading from an SD card dispite the error on the tutorial page, and I am about to try hooking up an Arduino DMX shield to the Esprino...

    I bought the SKPang one: http://skpang.co.uk/catalog/arduino-dmx-­shield-p-663.html

    I am not really sure what to do next, I guess I will try receiving DMX data, but I am not sure what to do about the so called framing error... I don't really understand how you could possibly deal with that issue...

    I could really use some help here...

    Basically I am trying to receive DMX data from a industrial DMX board so it could be transmitting up to the full 512 channels, I am hoping I can do some kind of DMX.on('1', function( val ) { //do something } ); type of thing as I am aiming to listen to some channels of data, look up something I have read into memory from an SD card, and then send a RS-485 response back out another USART to another device.

    All help greatly appreciated.

  • despite the error on the tutorial page,

    You mean the one about listing files?

    I'm actually out today, but I'll try and look at this tomorrow - I might be able to detect the framing error in software and pass it to JS. Assuming you're happy playing around with resistors and capacitor values, you could try and detect the start of frame electrically though. You'd be trying to set it up so that when the DMX signal went low for a long period of time, the input on that other pin went low, but it didn't go low for normal serial data.

    So something like:

    • Connect a diode with the non-marked end on the DMX RX pin
    • Connect a capacitor (1uF?) between GND and the other end of the diode
    • Put a resistor (1k?) across the capacitor
    • Connect the capacitor to a pin on Espruino

    You'd really need an oscilloscope, and then a play around with the capacitor/resistor values until you saw the voltage on the pin drop below 1v only at the start of frame.

    Once that happens you can do something like:

    function dmxFrame(data) {
    }
    
    var dmx = new Uint8Array(512);
    var dmxIdx = 0;
    Serial2.setup(250000);
    Serial2.on('data', function(d) { dmx.set(d, dmxIdx); dmxIdx += d.length; });
    setWatch(function() {
      dmxFrame(dmx);
      dmxIdx = 0;
    , DMX_SOF_PIN, { repeat: true, edge:"falling"});
    
  • I've had a go at this today, I have been experimenting with your explanation of the circuit...

    So this is the result...

    The white crocodile clip is the decoded DMX data from the shield. Through a 1n4001 diode into a 1uF capacitor, and a 1KΩ resistor to ground.

    This is the result on the scope...

    D0: Data– going into DMX Shield.
    D1: Data+ going into the DMX Shield.
    D2: Is the Logic analysers reading of the interrupt output.
    1: Is the same as D2 but on the scope. You can see the signal drops to close to 1V but only after the entire packet.

    The particular DMX board I am using is a Chauvet Obey 6 which appears to be controlling 6 faders on 6 pages/fixtures... i.e. 36 channels only at the moment.

    I am going to play about with the values now but I am not sure what I am really doing. I think probably a smaller capacitor as the drop off is long... maybe 0.1uF.

    If you see this and have a bright idea I am going to be at my desk for an hour or two with the setup to see if I can get below 1V at the start of the data. However I am not sure what the start is...

    The signal goes low for a long period, then high, and low again for a short period (is there a channel 0?) then the bits can clearly be seen with a short break between each...

  • I just tried a 0.1uF and that is much better, also got a 10KΩ Potentiometer there to get the right value... turns out that 1KΩ is right... I wonder if that was a type in your original post?

    Anyways, I swapped back in the 1KΩ resistor and it's got a nice little pulse just at the end of that long initial break but no where else...

    You can see on channel 1 there is lots of toing and froing but the only time it's enough to drop below 1V is the break at the start. The dotted line horizontal line represents 0V and the parallel one just above in solid is the 1V threshold.

    Next is to try looking at the code in Espruino to get the interrupt working.

    FYI: For those curious the scope is a Rigol MSO1074Z... It's the same as the DSO1054Z series but it has a built in logic analyser and a signal generator.

    I've used an analog scope before and that was revolutionary from not having one. Then getting a digital scope was another wow moment... Having access to a scope with build in Logic Analyser is mind bogglingly useful, in a way I never appreciated until I needed to get that red blip.

    IT's been a great project for learning!

  • With this code it kind of works...

    var dmx = new Uint8Array( 512 );
    var dmxIdx = 0;
    
    Serial2.setup( 250000 );
    
    Serial2.on( 'data', function( d ) {
      dmx.set( d, dmxIdx );
      dmxIdx += d.length;
    } );
    
    setWatch( function() {
      dmxFrame( dmx );
      dmxIdx = 0;
    }, B8, { repeat: true, edge: 'falling' } );
    
    function dmxFrame( data ) {
      console.log( "1: " + data[1] + " 2: " + data[2] + " 3: " + data[3] + " 4: " + data[4] + " 5: " + data[5] + " 6: " +  data[6] );
    }
    

    However the issue is that bytes later on seem to become corrupt if the values before it are low... I looked at the signal out of the DMX shield and it looks like the signal was jumping around a bit only when the circuit with the diode, capacitor and resistor were connected.

    My theory was that perhaps there was some electricity flowing back through the resistor or because it's a general purpose diode it is some how not quick enough. I remember that zener diode is faster or something.. anyways my electrical knowledge is limited here so I found a little zener and tried it and it's working great.

    It appears to be a 1n2268.

    More experimenting to happen...

  • After a bit further experimentation I was able to so far verify the data works fine all the way up to channel 36 which is as far as I can go...

    This is an amazing result... I didn't think it would work, let alone be this easy, next I need to wrap it up in a more user friendly way, however I couldn't resist tying it into the three LEDs on the board for a test...

    https://www.youtube.com/watch?v=29v4xd3U­CLA

    Code:

    var dmx = new Uint8Array( 512 );
    var dmxIdx = 0;
    
    Serial2.setup( 250000 );
    
    Serial2.on( 'data', function( d ) {
      dmx.set( d, dmxIdx );
      dmxIdx += d.length;
    } );
    
    setWatch( function() {
      dmxFrame( dmx );
      dmxIdx = 0;
    }, B8, { repeat: true, edge: 'falling' } );
    
    function dmxFrame( data ) {
      analogWrite( LED1, data[1]/256, { forceSoft: true } );
      analogWrite( LED2, data[2]/256, { forceSoft: true } );
      analogWrite( LED3, data[3]/256, { forceSoft: true } );
    //  console.log( "1: " + data[1] + " 2: " + data[2] + " 3: " + data[3] + " 4: " + data[4] + " 5: " + data[5] + " 6: " +  data[6] );
    //  console.log( "7: " +  data[7] + " 8: " +  data[8] + " 9: " +  data[9] + " 10: " +  data[10] + " 11: " +  data[11] + " 12: " +  data[12] );
    //  console.log( "13: " +  data[13] + " 14: " +  data[14] + " 15: " +  data[15] + " 16: " +  data[16] + " 17: " +  data[17] + " 18: " +  data[18] );
    //  console.log( "19: " +  data[19] + " 20: " +  data[20] + " 21: " +  data[21] + " 22: " +  data[22] + " 23: " +  data[23] + " 24: " +  data[24] );
    //  console.log( "25: " +  data[25] + " 26: " +  data[26] + " 27: " +  data[27] + " 28: " +  data[28] + " 29: " +  data[29] + " 30: " +  data[30] );
    //  console.log( "31: " +  data[31] + " 32: " +  data[32] + " 33: " +  data[33] + " 34: " +  data[34] + " 35: " +  data[35] + " 36: " +  data[36] );
    }
    
  • That's awesome! Thanks for posting all the steps - sorry for the lack of replies but I was out most of last week.

    ... and I definitely need to get one of those Rigol scopes. That looks a mile better than what I'm using at the moment!

    You might find that a 0.3v schottky diode makes it a little more reliable (it'll pull the voltage back up a bit higher than a normal (0.7v) diode would). I was going to say the capacitor value might be a bit high, but actually on the video the waveform you get looks really nice :)

  • Very cool! Are you by chance implementing Art-Net?

  • Art-Net is a very different thing. I don't imagine it. You'd be better looking for a library to run from a Raspberry Pi or a small computer running Node.js.

  • It was directed at this "DMX to Ethernet gateway" from 7 months ago... My bad.

    EDIT: Also, it's not really that different is it? Seems like a UDP wrapper around a standard DMX bit stream (the start bit removed).

  • I've been working on creating a module for DMX... Not done this before, how does this stack up?

    var exports = {};
    
    function DMX( serial, interrupt ) {
      this.data = new Uint8Array( 512 );
      this.id = 0;
      this.serial = serial;
      this.interrupt = interrupt;
      
      // Setup serial port
      this.serial.setup( 250000 );
      this.serial.on( 'data', this.newData );
      
      // Listeners
      this.dataListeners = [];
      this.channelListeners = {};
      
      // Listen for the interrupt pin to drop
      setWatch( this.newFrame, this.interrupt, { repeat: true, edge: 'falling' } );
    }
    
    // Handle new bytes of data as they come in
    DMX.prototype.newData = function( d ) {
      // Store the bytes received at the buffer index
      DMX.data.set( d, DMX.id );
      
      // Move the buffer index along.
      DMX.id += d.length;
    };
    
    // Send out notifications to all event listeners when frame is complete.
    DMX.prototype.newFrame = function() {
      // Reset the byte buffer index to 0 for the next frame of data.
      DMX.id = 0;
      
      // Itterate through all the registered data listeners and send them the data
      for ( var i in DMX.dataListeners )
        DMX.dataListeners[i]( DMX.data );
    
      // Itterate through all the registered channel listeners and send them their channel values
      for ( var ch in DMX.channelListeners )
        for ( var j in DMX.channelListeners[ch] )
          DMX.channelListeners[ch][j]( DMX.data[ ch ] );
    };
    
    // Handle registration to event notifications
    DMX.prototype.on = function( type, handler ) {
      // If data listener: push function onto array
      if ( type == 'data' ) DMX.dataListeners.push( handler ); 
      
      // If channel listener
      if ( type >= 1 && type <= 512 ) {
        
        // Check channel for existing listeners
        if ( DMX.channelListeners[type] !== undefined ) {
          // Push additional listener onto array
          DMX.channelListeners[type].push( handler );
        } else {
          // Otherwise: create new array with listener in it
          DMX.channelListeners[type] = [ handler ];
        }
      }
    };
    
    // Export the module
    exports.connect = function( serial, interrupt ) {
      return new DMX( serial, interrupt );
    };
    
    ///////////////
    
    var DMX = exports.connect( Serial2, B9 );
    
    // Listen for changes on channel 1
    DMX.on( 1, function( val ) {
      analogWrite( LED1, val / 255, { forceSoft: true } );
    } );
    
    // Listen for changes on channel 2
    DMX.on( 2, function( val ) {
      analogWrite( LED2, val / 255, { forceSoft: true } );
    } );
    
    // Listen for changes on channel 3
    DMX.on( 3, function( val ) {
      analogWrite( LED3, val / 255, { forceSoft: true } );
    } );
    
    // Listen for full frames of data
    DMX.on( 'data', function( data ) {
    //  console.log( data );
    } );
    
  • Looks good - the only thing I can see is in some functions you access the object directly with DMX but you should really be using this (or it'll fail if you call the object anything else).

    As you might have found out, it's slightly more painful than that though - you need to use bind:

    this.serial.on( 'data', this.newData.bind(this) ); // <----
    
    // Handle new bytes of data as they come in
    DMX.prototype.newData = function( d ) {
      // Store the bytes received at the buffer index
      this.data.set( d, this.id );
      
      // Move the buffer index along.
      this.id += d.length;
    };
    

    and the same for the end of frame marker too...

    Other thing I'd say is maybe calling all the handlers could end up being quite slow - I'm not sure. You'd have to be careful that the new frame handler didn't take so long that you missed data.

  • I was using this but it doesn't work for some reason... it says:

    Uncaught Error: Field or method "set" does not already exist, and can't create it on undefined at line 3 col 12...

    Specifically this doesn't exist inside that DMX.prototype.newData function, or indeed inside any other than the first one DMX()...

    I also don't know what you mean about bind, but I tried adding this and it also didn't work.

  • That's strange... bind should really fix the can't create on undefined error.

    What happens is this.serial.on calls your function, but it calls it as a function, rather than as a method call... so this never gets set to the correct thing.

    this.serial.on( 'data', this.newData.bind(this) ); is a bit like saying:

    x = this;
    this.serial.on( 'data', function(a) {
      x.newData(a);
    });
    

    So it makes sure that newData gets called on the correct object. Hope that makes sense!

  • Just to add, I've now committed a DMX module that will work with 1v83 of Espruino when it is released (or the latest builds).

    When 1v83 is released, the module will be available at this link.

    It uses the 'framing error' hardware of the USART, so you don't need the resistor/capacitor.

    The method above isn't bad or unreliable, it's just easier if you don't need any external components.

  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview
About

DMX Input / Output

Posted by Avatar for mhoneywill @mhoneywill

Actions