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 commands:
git clone http://git.digitalcave.ca/avr/drummaster.git drummaster
git clone http://git.digitalcave.ca/avr/lib.git lib
git clone http://git.digitalcave.ca/avr/build.git build
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