Drum Master

DIY Electronic Drum Brain

 

Master Software

The Master software is what runs on the Arduino controller. In its very simplest form, it polls all 32 analog inputs and 8 digital inputs, reports back to the computer when a hit occurs. To decrease the latency, we actually poll the digital triggers, and only read an analog sensor if a logic high is detected.

The AVR GCC code as of the time of this writing is included below. This is a GCC program which must be compiled with AVR GCC, and uploaded with AVRDude. I have a distribution tar file which includes my libraries (serial I/O, analog reading, timers) which you can download (or better yet, get the absolute most up-to-date code via git; see below for instructions). To make it and upload, use the command 'make program' from the src directory.

/*
 * Drum Master - controller for up to 32 + 8 sensors.
 * Copyright 2008 - 2009 Wyatt Olson <wyatt.olson@gmail.com>
 * 
 * At a very high level, Drum Master will listen to a series of sensors (both analog, via piezo
 * transducers, and digital, via grounding pullup resistors), and report the values back to the
 * computer via the serial port.  Each signal is sent in a packet, using a binary protocol 
 * consisting of 3 bytes / packet:
 *
 *    |sssscccc|ccvvvvvv|vvvvkkkk|
 *      <start:4><channel:6><value:10><checksum:4>
 *
 * Each portion of the packet is described below:
 *    <start> is the 4 bit sequence 0xF.  It is used to signal the start of a packet to synch 
 *         with the slave software, in case packet loss occurs.
 *    <channel> is the 6 bit representation of a channel number between 0x0 (0) and 0x27 (39).
 *    <value> is the 10 bit representation of the actual analog value between 0x0 and 0x3FF.
 *    <checksum> is the 4 bit checksum on the rest of the packet, calculated using the 
 *        4 bit parity word algorithm.  Each 4-bit chunk of the packet is XOR'd together. 
 *        The slave software will XOR all 6 4 bit words together; if the result is not 0x0
 *        then we know there was an error in transmission.
 *
 * Slave software, running on the computer, must take these data packets and map them to digital
 * audio samples, based on the channel, velocity, and current state of the program.
 *
 * For more information, please visit http://drummaster.digitalcave.ca.
 * 
 * Changelog:
 * 1.2.0.1 - October 15 2010:  -Further bugfixes; now things are working (somewhat decently) with Slave software
 *                version 2.0.1.1.  Main problem was a driver issue for the MUX selector where the 
 *                endian-ness of the selector was backwards.
 * 1.2.0.0 - Aug 25 2010:    -Converting to plain AVR code from Arduino, to hopefully see a speedup
 *                in analog reading and serial communication.
 * 1.1.2.0 - Sept 12 2009:    -Fixed a bug with active channels which did not send data if the value
 *                was below the trigger threshold.
 *                -Adjusted active channel values to be more verbose in sending data, so
 *                that the slave program has better data to work with.  This has resulted
 *                in substantially more realistic hi hat behaviour.
 * 1.1.1.0 - July 2 2009:    -Adjusted #define values to better suit hardware.
 *                -Added more comments for all #define values to indicate more clearly
 *                what each does.
 *
 * 1.1.0.0 - June ? 2009:    -Added the concept of 'active channels'; channels that report
 *                a value X number of times in a row are assumed to be active;
 *                this is used for things like analog hi-hat controllers, where
 *                we want to have a continuous report of changes, but not
 *                constantly waste time on the serial port if there are no changes.
 *                -Combine multiple simultaneous strikes into a single data packet
 *                to reduce the number of expensive (~20ms each) serial writes.   
 *                This has successfully decreased latency to unnoticable levels
 *                when there are multiple simultaneous strikes.
 *
 * 1.0.0.0 - May ? 2008:    -Initial version.   Works fine for basic drumming requirements.
 */

#include <avr/io.h>
#include <stdlib.h>
#include "../lib/analog/analog.h"
#include "../lib/serial/serial.h"
#include "../lib/timer/timer.h"

//Send debug signals as well as data.  This will likely make slave software 
// stop working; only define it when you are debugging via serial console.
// Debug values can be from 1 (little debug data, essentially only ASCII 
// representation of protocol) to 5 (everything).  Set to 0 or comment out to
// disable debug, for production use.
//#define DEBUG 5


//How many times to read the input for each hit; this can help to ensure a good reading 
// is obtained.   A good default to this is 3; only adjust this if you find you need more
// accuracy, or you are noticing a great deal of lag.   Note that each analog read will
// take approximately 1ms to complete, which is quite expensive.
#define ANALOG_SAMPLES 3
//After a hit, don't report the same sensor for this long (ms).   This is the absolute 
// shortest 'double hit' which the hardware will report.  Since the slave software will
// (or at least should) also allow you to pick the double trigger threshold for each zone,
// this setting is more to ensure that the serial link is not overloaded with constant 
// data, rather than to actually add to the usability by limiting double triggers.
// A good default is 25; this is 1/40 second, and even very fast snare rolls should be 
// longer than this.
#define ANALOG_BOUNCE_PERIOD 25   
//After a digital change of state, don't report changes for this long (ms).   A good default
// is 75; any less than this tends to (at least on my hardware) make hi hat chics sound 
// multiple times.
#define DIGITAL_BOUNCE_PERIOD 75  


//After this many consecutive polling iterations which contain data, we will assume that the channel 
// is an 'active channel', and will poll less frequently.   Furthermore, we will only send a new event 
// when the state changes by more than a given percentage, to reduce useless chatter on the
// serial port.   (Active channels are used, for instance, on a hi hat, or other device where 
// there will always be some voltage present).  We only use this for analog sensors, as digital
// sensors will report all the time anyway, for any state changes (and there is almost no overhead
// incurred in reading a digital sensor, while there is about 1ms delay when reading analog).
#define MIN_ACTIVE_CHANNEL_POLL_COUNT 10
//How much of a change (in absolute values, based on a full data range of 0-1023) must happen 
// before we report a change over the serial port
#define MIN_ACTIVE_CHANNEL_REQUIRED_CHANGE 50
//We must wait at least this long between polling
#define MIN_ACTIVE_CHANNEL_POLL_INTERVAL 10
//We must not wait any longer than this between polling, even if a large change has not happened.
// This helps to avoid the situation where the change was missed (either from the slave software
// or the master), but we still want to know what the current state is.   Larger values here will
// keep the rest of the system responsive; a good place to start is 1000 or so (1 second).
#define MAX_ACTIVE_CHANNEL_POLL_INTERVAL 500


//This is used for a number of data buffers.  It should be set to the number of channels (this is
// 40 in default hardware).   You will only want to change this if you have modified / custom hardware.
#define BUFFER_SIZE 40



//Temp variables; i and j are iterators for port and bank respectively; s and v are the selector 
// address (channel) and velocity respectively;   x and y are truly temp variables, and can be used for
// anything you wish.   There are no guarantees that they will keep its value for any length of time,
// so make sure nothing else stores to them before you read your value.
uint8_t i, j, s;
uint16_t v;
uint64_t x, y;

//Is it time to read active channels?   Set to true at the beginning of a loop if this is the case, and
// reset at the end.
uint8_t read_active_channels;

//Active counter.   Increments each cell by one each time a value for the corresponding channel is
// read; if there is no value read, it will reset the cell to 0.  Once a given cell gets above
// MIN_ACTIVE_CHANNEL_POLL_COUNT, we will poll on a less frequent basis, and will only report large
// changes over the serial port.
uint8_t consecutive_reads[BUFFER_SIZE];

//Keep track of active channels, to reduce computations.  Once a channel is set to be active,
// only a reset will change it back.
uint8_t active_channel[BUFFER_SIZE];

//Set to the current time at the beginning of each loop, to reduce the number of expensive 
// calls to timer_millis().
uint64_t time;

//When was the last time that each channel was read?  Used for debounce.
uint64_t last_read_time[BUFFER_SIZE];
//What was the last value read from each channel?
uint16_t last_value[BUFFER_SIZE];
//When did we last read the active channels?
uint64_t last_read_active_channels;


#if DEBUG > 0
char temp[32];
#endif


/*
 * Sets pins S2 - S0 to select the multiplexer output.
 */
void set_mux_selectors(uint8_t s){
  PORTB &= 0xF8; //Clear bottom three bits
  //Set bits based on s, making sure we don't set more than 3 bits.
  //For PCB layout simplicity, we designed the board such that the MSB is 
  // PINB0 and LSB is PINB2.  Thus, we need to flip the 3-bit word.
  s = s & 0x7;
  PORTB |= ((s & 0x1) << 2) | (s & 0x2) | ((s & 0x4) >> 2);
}

/*
 * Convert from a bank / port tuple to single channel number (value from 0 - 39 inclusive)
 * for sending to Drum Slave software.
 */
uint8_t get_channel(uint8_t bank, uint8_t port){
  return (bank << 3) | port;
}

/*
 * Reads the input a number of times, and returns the maximum.  The number 
 * of times to read the input is defined by ANALOG_SAMPLES
 */
uint16_t get_velocity(uint8_t pin){
#if DEBUG > 4
  x = timer_millis(); 
#endif

  uint16_t max_val = 0;
  for (uint8_t t = 0; t < ANALOG_SAMPLES; t++){
    uint16_t val = analog_read_p(pin);
    if (val > max_val) max_val = val;
  }
#if DEBUG > 4
  y = timer_millis();
  serial_write_s("Read ");
  serial_write_s(itoa(ANALOG_SAMPLES, temp, 10));
  serial_write_s(" samples from pin ");
  serial_write_s(itoa(pin, temp, 10));
  serial_write_s(" in ");
  serial_write_s(itoa(y - x, temp, 10));
  serial_write_s("ms\n\r");
#endif
  return max_val;
}

/*
 * Sends data.  Channel is a 6 bit number, between 0 and 39 inclusive.  Data is a 10 bit number.
 */
void send_data(uint8_t channel, uint16_t data){
#if DEBUG == 0
  uint8_t packet;
  uint8_t checksum = 0x0;
  
  //First packet consists of start bits and the 4 MSB of channel.
  packet = 0xF0 | ((channel >> 2) & 0xF);
  checksum ^= packet >> 4;
  checksum ^= packet & 0xF;
  serial_write_c(packet);

  //Second packet consists of 2 LSB of channel and 6 MSB of data.
  packet = ((channel & 0x3) << 6) | ((data >> 4) & 0x3F);
  checksum ^= packet >> 4;
  checksum ^= packet & 0xF;
  serial_write_c(packet);
  
  //Third packet consists of 4 LSB of data and checksum.
  packet = ((data & 0xF) << 4);
  checksum ^= packet >> 4;
  packet |= checksum & 0xF;
  serial_write_c(packet);
#else
  serial_write_s("Data: channel ");
  serial_write_s(itoa(channel, temp, 10));
  serial_write_s(", value ");
  serial_write_s(itoa(data, temp, 10));
  serial_write_s(".  Packets: ");  
  serial_write_s(itoa(0xF0 | ((channel >> 2) & 0xF), temp, 16));
  serial_write_s(" ");  
  serial_write_s(itoa(((channel & 0x3) << 6) | ((data >> 4) & 0x3F), temp, 16));  
  serial_write_s(" ");  
  serial_write_s(itoa(((data & 0xF) << 4), temp, 16));  
  serial_write_s("\n\r");
#endif

}


//Setup method is called once at the beginning of execution.
void setup() {
  //Init libraries  
  uint8_t apins[4];
  apins[0] = 0;
  apins[1] = 1;
  apins[2] = 2;
  apins[3] = 3;
  analog_init(apins, 4);
  
#if DEBUG >= 1
  serial_init(9600, 8, 0, 1);
#else 
  serial_init(57600, 8, 0, 1);
#endif
  
  timer_init();

  //Set the analog triggers (2::5) and digital input (6) to input mode.
  DDRD &= ~(_BV(DDD2) | _BV(DDD3) | _BV(DDD4) | _BV(DDD5) | _BV(DDD6));

  //The three MUX selector switches need to be set to output mode
  DDRB |= _BV(DDB0) | _BV(DDB1) | _BV(DDB2);

  //Initialize array counters
  for (i = 0; i < BUFFER_SIZE; i++){
    //Initialize the active consecutive reads counter
    consecutive_reads[i] = 0;

    //Reset the active channels to all 0
    active_channel[i] = 0;
  }
}

//Loop is called repeatedly after setup has completed.
void loop() {
  //We cache the current time to avoid needing to call timer_millis() each time throughout the loop.
  time = timer_millis();

  //If ACTIVE_CHANNEL_MIN_POLL_INTERVAL ms have passed since the last time we read active channels,
  // go ahead and read them now.
  if (time - last_read_active_channels > MIN_ACTIVE_CHANNEL_POLL_INTERVAL){
    read_active_channels = 1;
    last_read_active_channels = time;
  }

  //We read pin 0 on each multiplexer in turn, then select pin 1 and read from each in turn,
  // etc.   This reduces the number of switches we need to make on the multiplexers.   While 
  // in theory these switches are not very expensive (the spec sheets indicate that they are
  // almost instantaneous), doing it this way can possibly allow people to use slower MUXs
  // if desired; you could add a couple Microsecond delay after selecting the next port, and
  // give the MUX time to settle out.
  for (i = 0; i < 0x8; i++){  //i == port (one channel on a multiplexer)
    set_mux_selectors(i);

    //Read the analog pins
    for (j = 0; j < 4; j++){ // j == bank
      s = get_channel(j, i);

      //If the channel is defined to be 'active' (i.e., connected to a device which always
      // reports its state, such as a hi hat pedal, rather than a device which only reports
      // state when struck, like a piezo), then we poll less frequently.
      if (consecutive_reads[s] > MIN_ACTIVE_CHANNEL_POLL_COUNT && !active_channel[s]){
        active_channel[s] = 1;
#if DEBUG >= 2
        serial_write_s("Switching channel ");
        serial_write_s(itoa(s, temp, 10));
        serial_write_s(" to be 'active'\n\r");
#endif
      }

      //Check the last read time, and whether the trigger is active.  Triggers are used to determine
      // whether an analog sensor has any data waiting to be read; reading a digital signal 
      // (Trigger) is much less expensive than reading an analog sensor, (an analog sensor 
      // seems to take upwards of 1ms to read, while a digital sensor is a fraction of that).
      // Triggers are active if there is a voltage greater than about 0.3v on the corresponding 
      // analog pin.
//      if (!active_channel[s] || read_active_channels){
        if (time - last_read_time[s] > ANALOG_BOUNCE_PERIOD){
          if ((PIND & _BV(PIND2 + j)) || active_channel[s]){
            v = get_velocity(j);
            if (!active_channel[s] 
                  || abs(v - last_value[s]) > MIN_ACTIVE_CHANNEL_REQUIRED_CHANGE
                  || (active_channel[s] && time - last_read_time[s] > MAX_ACTIVE_CHANNEL_POLL_INTERVAL)){
#if DEBUG >= 2
              if (active_channel[s]){
                if (abs(v - last_value[s]) > MIN_ACTIVE_CHANNEL_REQUIRED_CHANGE){
                  serial_write_s("Large change of value for channel ");
                  serial_write_s(itoa(s, temp, 10));
                  serial_write_s("; old value is ");
                  serial_write_s(itoa(last_value[s], temp, 10));
                  serial_write_s("; new value is ");
                  serial_write_s(itoa(v, temp, 10));
                  serial_write_s("\n\r");
                }
                if (time - last_read_time[s] > MAX_ACTIVE_CHANNEL_POLL_INTERVAL){
                  serial_write_s("Timeout for channel ");
                  serial_write_s(itoa(s, temp, 10));
                  serial_write_s("; resending current value of ");
                  serial_write_s(itoa(v, temp, 10));
                  serial_write_s("\n\r");
                }
              } 
#endif
              //Write data
              send_data(s, v);

              last_read_time[s] = time;
              last_value[s] = v;

              //Only bother incrementing consecutive reads for channels not already defined as active.
              if (!active_channel[s]){
                consecutive_reads[s] = consecutive_reads[s] + 1;
              }
            }
          }
          else {
#if DEBUG >= 3
            //serial_write_s("Resetting consecutive reads for channel ");
            //serial_write_s(itoa(s, temp, 10));
            //serial_write_s("\n\r");
#endif
            consecutive_reads[s] = 0;
          }   
        }    
//      }
    } //end for j ...

    //Read the digital pins
    j = 4; //Digital pin.   Change this to a loop if we add another.
    s = get_channel(j, i);
    if (time - last_read_time[s] > DIGITAL_BOUNCE_PERIOD){
      //Remember that digital switches in drum master are reversed, since they 
      // use pull up resisitors.  Logic 1 is open, logic 0 is closed.   We invert
      // all digital readings to make this easy to keep straight.
      v = (PIND & _BV(PIND6)) >> PIND6;
      if (v != last_value[s]){
        send_data(s, v);
        
        last_read_time[s] = time;
        last_value[s] = v;
      }
    }
  }

  //We will read active channels again in ACTIVE_CHANNEL_POLL_INTERVAL ms.
  read_active_channels = 0;
}

int main (void){
  //Do setup here
  setup();

  //Main program loop
  while (1){
    loop();
  }
}

Pulling Git Code

To pull the latest code via git, use the following command: git clone http://git.digitalcave.ca/avr.git Drum Master is located in the projects/drummaster/src folder. You can build it using 'make', and install the program using 'make program'.

All of these must be downloaded into the same folder. Once you have the git repository already cloned, you can update it using the command: git pull origin master