Week 19 - May 23rd 2012 - Project Development

Final Project Update

Over the past few weeks, I have completed several design cycles of my final project, reaching a stage where it meets all of the intial design criteria. Briefly, these were to:

  • - Implement a device that connects via USB to a host computer and is recognised as a MIDI device
  • - Receive MIDI messages and play them as audio
  • - Alternatively, send MIDI messages reporting button presses back to the host computer
  • - Display incoming and outgoing MIDI communications on an LCD screen
  • - House the device in a suitable case

Following is documentation of each component, hopefully sufficient to allow recreation of any/all of the project. Where Eagle schematics are provided without layouts, these are still being finalised and validated.

USB module

The first component handles USB connectivity. It comprises two circuit boards; one simply holds the USB socket, the second holds the first ATTiny44 microcontroller.

The USB master microcontroller runs the V-USB software as discussed previously, allowing direct USB connectivity. MIDI data packets are received and sent across the bus (pins PA3 and PA4, serial in/out) to the synth and LCD boards.

#include <avr/io.h> #include <avr/interrupt.h> #include <avr/sleep.h> #include <avr/pgmspace.h> #include <avr/wdt.h> #include <util/delay.h> #include "usbdrv/usbdrv.h" //lifted from NeilG #define output(directions,pin) (directions |= pin) // set port direction for output #define input(directions,pin) (directions &= (~pin)) // set port direction for input #define set(port,pin) (port |= pin) // set port pin #define clear(port,pin) (port &= (~pin)) // clear port pin #define pin_test(pins,pin) (pins & pin) // test for port pin #define bit_test(byte,bit) (byte & (1 << bit)) // test for bit set #define bit_delay_time 20 // bit delay for 9600 with overhead #define bit_delay() _delay_us(bit_delay_time) // RS232 bit delay #define half_bit_delay() _delay_us(bit_delay_time/2) // RS232 half bit delay #define led_port PORTA #define led_direction DDRA #define led_pin (1 << PA2) #define serial_port PORTA #define serial_direction DDRA #define serial_pins PINA #define serial_pin_in (1 << PA3) #define serial_pin_out (1 << PA4) #define mode_port PORTA #define mode_direction DDRA #define mode_pins PINA #define mode_pin (1 << PA1) //board IDs #define synthID 0x01 #define lcdID 0x02 #define triggerID 0x03 // modes #define synthMode 0x01 #define triggerMode 0x02 char mode; uchar midiMsg[8]; char note1 = 0x3c; char note2 = 0x3d; char note3 = 0x3e; char note4 = 0x3f; char iomsg_current = 0x00; char iomsg_transmit = 0x00; // This descriptor is based on http://www.usb.org/developers/devclass_docs/midi10.pdf // // Appendix B. Example: Simple MIDI Adapter (Informative) // B.1 Device Descriptor // static PROGMEM char deviceDescrMIDI[] = { /* USB device descriptor */ 18, /* sizeof(usbDescriptorDevice): length of descriptor in bytes */ USBDESCR_DEVICE, /* descriptor type */ 0x10, 0x01, /* USB version supported */ 0, /* device class: defined at interface level */ 0, /* subclass */ 0, /* protocol */ 8, /* max packet size */ USB_CFG_VENDOR_ID, /* 2 bytes */ USB_CFG_DEVICE_ID, /* 2 bytes */ USB_CFG_DEVICE_VERSION, /* 2 bytes */ 1, /* manufacturer string index */ 2, /* product string index */ 0, /* serial number string index */ 1, /* number of configurations */ }; // B.2 Configuration Descriptor static PROGMEM char configDescrMIDI[] = { /* USB configuration descriptor */ 9, /* sizeof(usbDescrConfig): length of descriptor in bytes */ USBDESCR_CONFIG, /* descriptor type */ 101, 0, /* total length of data returned (including inlined descriptors) */ 2, /* number of interfaces in this configuration */ 1, /* index of this configuration */ 0, /* configuration name string index */ 0, //USBATTR_BUSPOWER, USB_CFG_MAX_BUS_POWER / 2, /* max USB current in 2mA units */ // B.3 AudioControl Interface Descriptors // The AudioControl interface describes the device structure (audio function topology) // and is used to manipulate the Audio Controls. This device has no audio function // incorporated. However, the AudioControl interface is mandatory and therefore both // the standard AC interface descriptor and the classspecific AC interface descriptor // must be present. The class-specific AC interface descriptor only contains the header // descriptor. // B.3.1 Standard AC Interface Descriptor // The AudioControl interface has no dedicated endpoints associated with it. It uses the // default pipe (endpoint 0) for all communication purposes. Class-specific AudioControl // Requests are sent using the default pipe. There is no Status Interrupt endpoint provided. /* AC interface descriptor follows inline: */ 9, /* sizeof(usbDescrInterface): length of descriptor in bytes */ USBDESCR_INTERFACE, /* descriptor type */ 0, /* index of this interface */ 0, /* alternate setting for this interface */ 0, /* endpoints excl 0: number of endpoint descriptors to follow */ 1, /* */ 1, /* */ 0, /* */ 0, /* string index for interface */ // B.3.2 Class-specific AC Interface Descriptor // The Class-specific AC interface descriptor is always headed by a Header descriptor // that contains general information about the AudioControl interface. It contains all // the pointers needed to describe the Audio Interface Collection, associated with the // described audio function. Only the Header descriptor is present in this device // because it does not contain any audio functionality as such. /* AC Class-Specific descriptor */ 9, /* sizeof(usbDescrCDC_HeaderFn): length of descriptor in bytes */ 36, /* descriptor type */ 1, /* header functional descriptor */ 0x0, 0x01, /* bcdADC */ 9, 0, /* wTotalLength */ 1, /* */ 1, /* */ // B.4 MIDIStreaming Interface Descriptors // B.4.1 Standard MS Interface Descriptor /* interface descriptor follows inline: */ 9, /* length of descriptor in bytes */ USBDESCR_INTERFACE, /* descriptor type */ 1, /* index of this interface */ 0, /* alternate setting for this interface */ 2, /* endpoints excl 0: number of endpoint descriptors to follow */ 1, /* AUDIO */ 3, /* MS */ 0, /* unused */ 0, /* string index for interface */ // B.4.2 Class-specific MS Interface Descriptor /* MS Class-Specific descriptor */ 7, /* length of descriptor in bytes */ 36, /* descriptor type */ 1, /* header functional descriptor */ 0x0, 0x01, /* bcdADC */ 65, 0, /* wTotalLength */ // B.4.3 MIDI IN Jack Descriptor 6, /* bLength */ 36, /* descriptor type */ 2, /* MIDI_IN_JACK desc subtype */ 1, /* EMBEDDED bJackType */ 1, /* bJackID */ 0, /* iJack */ 6, /* bLength */ 36, /* descriptor type */ 2, /* MIDI_IN_JACK desc subtype */ 2, /* EXTERNAL bJackType */ 2, /* bJackID */ 0, /* iJack */ //B.4.4 MIDI OUT Jack Descriptor 9, /* length of descriptor in bytes */ 36, /* descriptor type */ 3, /* MIDI_OUT_JACK descriptor */ 1, /* EMBEDDED bJackType */ 3, /* bJackID */ 1, /* No of input pins */ 2, /* BaSourceID */ 1, /* BaSourcePin */ 0, /* iJack */ 9, /* bLength of descriptor in bytes */ 36, /* bDescriptorType */ 3, /* MIDI_OUT_JACK bDescriptorSubtype */ 2, /* EXTERNAL bJackType */ 4, /* bJackID */ 1, /* bNrInputPins */ 1, /* baSourceID (0) */ 1, /* baSourcePin (0) */ 0, /* iJack */ // B.5 Bulk OUT Endpoint Descriptors //B.5.1 Standard Bulk OUT Endpoint Descriptor 9, /* bLenght */ USBDESCR_ENDPOINT, /* bDescriptorType = endpoint */ 0x1, /* bEndpointAddress OUT endpoint number 1 */ 3, /* bmAttributes: 2:Bulk, 3:Interrupt endpoint */ 8, 0, /* wMaxPacketSize */ 10, /* bIntervall in ms */ 0, /* bRefresh */ 0, /* bSyncAddress */ // B.5.2 Class-specific MS Bulk OUT Endpoint Descriptor 5, /* bLength of descriptor in bytes */ 37, /* bDescriptorType */ 1, /* bDescriptorSubtype */ 1, /* bNumEmbMIDIJack */ 1, /* baAssocJackID (0) */ //B.6 Bulk IN Endpoint Descriptors //B.6.1 Standard Bulk IN Endpoint Descriptor 9, /* bLenght */ USBDESCR_ENDPOINT, /* bDescriptorType = endpoint */ 0x81, /* bEndpointAddress IN endpoint number 1 */ 3, /* bmAttributes: 2: Bulk, 3: Interrupt endpoint */ 8, 0, /* wMaxPacketSize */ 10, /* bIntervall in ms */ 0, /* bRefresh */ 0, /* bSyncAddress */ // B.6.2 Class-specific MS Bulk IN Endpoint Descriptor 5, /* bLength of descriptor in bytes */ 37, /* bDescriptorType */ 1, /* bDescriptorSubtype */ 1, /* bNumEmbMIDIJack (0) */ 3, /* baAssocJackID (0) */ }; uchar usbFunctionDescriptor(usbRequest_t * rq) { if (rq->wValue.bytes[1] == USBDESCR_DEVICE) { usbMsgPtr = (uchar *) deviceDescrMIDI; return sizeof(deviceDescrMIDI); } else { /* must be config descriptor */ usbMsgPtr = (uchar *) configDescrMIDI; return sizeof(configDescrMIDI); } } //network protocols lifted from NeilG void get_char(volatile unsigned char *pins, unsigned char pin, char *rxbyte) { // read character into rxbyte on pins pin - assumes line driver (inverts bits) *rxbyte = 0; while (pin_test(*pins,pin)) ;// wait for start bit half_bit_delay(); // delay to middle of first data bit bit_delay(); // unrolled loop to read data bits if pin_test(*pins,pin) *rxbyte |= (1 << 0); else *rxbyte |= (0 << 0); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 1); else *rxbyte |= (0 << 1); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 2); else *rxbyte |= (0 << 2); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 3); else *rxbyte |= (0 << 3); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 4); else *rxbyte |= (0 << 4); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 5); else *rxbyte |= (0 << 5); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 6); else *rxbyte |= (0 << 6); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 7); else *rxbyte |= (0 << 7); // wait for stop bit bit_delay(); half_bit_delay(); } void put_char(volatile unsigned char *port, unsigned char pin, char txchar) { // send character in txchar on port pin - assumes line driver (inverts bits) clear(*port,pin); // start bit bit_delay(); // unrolled loop to write data bits if bit_test(txchar,0) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,1) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,2) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,3) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,4) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,5) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,6) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,7) set(*port,pin); else clear(*port,pin); bit_delay(); // stop bit set(*port,pin); bit_delay(); // char delay bit_delay(); } /* ------------------------------------------------------------------------- */ /* ----------------------------- USB interface ----------------------------- */ /* ------------------------------------------------------------------------- */ uchar usbFunctionSetup(uchar data[8]) { return 0xff; } /*---------------------------------------------------------------------------*/ /* usbFunctionRead */ /*---------------------------------------------------------------------------*/ uchar usbFunctionRead(uchar * data, uchar len) { data[0] = 0; data[1] = 0; data[2] = 0; data[3] = 0; data[4] = 0; data[5] = 0; data[6] = 0; return 7; } /*---------------------------------------------------------------------------*/ /* usbFunctionWrite */ /*---------------------------------------------------------------------------*/ uchar usbFunctionWrite(uchar * data, uchar len) { return 1; } /*---------------------------------------------------------------------------*/ /* usbFunctionWriteOut */ /* */ /* this Function is called if a MIDI Out message (from PC) arrives. */ /* See http://www.usb.org/developers/devclass_docs/midi10.pdf for a */ /* description of the data structure */ /* */ /*---------------------------------------------------------------------------*/ void usbFunctionWriteOut(uchar * data, uchar len) { uchar recmsg[len]; // prepare to read in received message int i; for (i=0; i 250 ms wdt_reset(); _delay_ms(1); } usbDeviceConnect(); } void buttonsPoll() { output(serial_direction, serial_pin_out); put_char(&serial_port, serial_pin_out, triggerID); put_char(&serial_port, serial_pin_out, 0x00); put_char(&serial_port, serial_pin_out, 0x00); put_char(&serial_port, serial_pin_out, 0x00); input(serial_direction, serial_pin_out); get_char(&serial_pins, serial_pin_in, &iomsg_transmit); if(iomsg_transmit != iomsg_current) { if(bit_test(iomsg_current, 7) != bit_test(iomsg_transmit, 7) { // button 1 change if(bit_test(iomsg_current, 7) { // button 1 released midiMsg[0] = 0x08; midiMsg[1] = 0x80; midiMsg[2] = note1; midiMsg[3] = 0x00; } else { // button 1 pressed midiMsg[0] = 0x09; midiMsg[1] = 0x90; midiMsg[2] = note1; midiMsg[3] = 0xff; } } if(bit_test(iomsg_current, 6) != bit_test(iomsg_transmit, 6) { // button 2 change if(bit_test(iomsg_current, 6) { // button 2 released midiMsg[0] = 0x08; midiMsg[1] = 0x80; midiMsg[2] = note2; midiMsg[3] = 0x00; } else { // button 2 pressed midiMsg[0] = 0x09; midiMsg[1] = 0x90; midiMsg[2] = note2; midiMsg[3] = 0xff; } } if(bit_test(iomsg_current, 5) != bit_test(iomsg_transmit, 5) { // button 3 change if(bit_test(iomsg_current, 5) { // button 3 released midiMsg[0] = 0x08; midiMsg[1] = 0x80; midiMsg[2] = note3; midiMsg[3] = 0x00; } else { // button 3 pressed midiMsg[0] = 0x09; midiMsg[1] = 0x90; midiMsg[2] = note3; midiMsg[3] = 0xff; } } if(bit_test(iomsg_current, 4) != bit_test(iomsg_transmit, 4) { // button 4 change if(bit_test(iomsg_current, 4) { // button 1 released midiMsg[0] = 0x08; midiMsg[1] = 0x80; midiMsg[2] = note4; midiMsg[3] = 0x00; } else { // button 1 pressed midiMsg[0] = 0x09; midiMsg[1] = 0x90; midiMsg[2] = note4; midiMsg[3] = 0xff; } } usbSetInterrupt(midiMsg, 4); iomsg_current = iomsg_transmit; output(serial_direction, serial_pin_out); put_char(&serial_port, serial_pin_out, lcdID); put_char(&serial_port, serial_pin_out, recmsg[1]); put_char(&serial_port, serial_pin_out, recmsg[2]); put_char(&serial_port, serial_pin_out, recmsg[3]); input(serial_direction, serial_pin_out); } } int main() { initStatusLED(); initModeSwitch(); initTXRX(); initUSB(); // Globally enable interrupts sei(); if(pin_test(mode_pins, mode_pin)) {mode = synthMode;} else {mode = triggerMode;} // Endless loop for (;;) { wdt_reset(); if (mode = synthMode) {usbPoll();} if (mode = triggerMode) {buttonsPoll();} } return 0; }

Synth module

The second ATTiny44 microcontroller runs a modified version of the 4bitsynth software, which generates audio from MIDI messages. Output is through an R-2R resistor ladder acting as a DAC. The board includes a LM386 audio amplifier IC to amplify the audio signal sufficiently to be heard through the internal speaker.

//Modified by Joel R from 4bitsynth source /* * This file is part of 4bitsynth. * * 4bitsynth is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * 4bitsynth is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 4bitsynth. If not, see http://www.gnu.org/licenses/. */ #include <avr/io.h> #include <util/delay.h> #include <avr/interrupt.h> #include "main.h" char recmsg0; char recmsg1; char recmsg2; char recmsg3; #define synthID 1 //lifted from NeilG #define output(directions,pin) (directions |= pin) // set port direction for output #define input(directions,pin) (directions &= (~pin)) // set port direction for input #define set(port,pin) (port |= pin) // set port pin #define clear(port,pin) (port &= (~pin)) // clear port pin #define pin_test(pins,pin) (pins & pin) // test for port pin #define bit_test(byte,bit) (byte & (1 << bit)) // test for bit set #define bit_delay_time 20 // bit delay for 9600 with overhead #define bit_delay() _delay_us(bit_delay_time) // RS232 bit delay #define half_bit_delay() _delay_us(bit_delay_time/2) // RS232 half bit delay #define led_port PORTA #define led_direction DDRA #define led_pin (1 << PA6) #define serial_port PORTA #define serial_direction DDRA #define serial_pins PINA #define serial_pin_in (1 << PA4) #define serial_pin_out (1 << PA5) //network protocols lifted from NeilG void get_char(volatile unsigned char *pins, unsigned char pin, char *rxbyte) { // read character into rxbyte on pins pin - assumes line driver (inverts bits) *rxbyte = 0; while (pin_test(*pins,pin)) ;// wait for start bit half_bit_delay(); // delay to middle of first data bit bit_delay(); // unrolled loop to read data bits if pin_test(*pins,pin) *rxbyte |= (1 << 0); else *rxbyte |= (0 << 0); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 1); else *rxbyte |= (0 << 1); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 2); else *rxbyte |= (0 << 2); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 3); else *rxbyte |= (0 << 3); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 4); else *rxbyte |= (0 << 4); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 5); else *rxbyte |= (0 << 5); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 6); else *rxbyte |= (0 << 6); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 7); else *rxbyte |= (0 << 7); // wait for stop bit bit_delay(); half_bit_delay(); } //void init_interrupts() { //Turn on USART reception and | RX Interrupt // UCSR0B = (1 << RXEN0) | (1 << RXCIE0); //8-bit, 1 stop, Asynch. // UCSR0C = (0 << UMSEL00) | (0 << UPM00) | (0 << USBS0) | (3 << UCSZ00); /* Set the baud rate to 31250 for MIDI */ // UBRR0L = 0x27; // For 20MHz Clock //UBRR0L = 0x13; // FOr 10MHz Clock /* Enable USART Receive interrupt */ // enable_USART_interrupts(); //} void init_io() { //b0 - b3 of PORT A is output // DDRA = 0x0F; //b4 0 v7 of PORT D is input (MIDI Channel selection) // DDRD &= 0b00001111; //enable internal pull-up resistors for MIDI Channel selection bits // PORTD |= 0b11110000; output(led_direction, led_pin); set(serial_port, serial_pin_out); input(serial_direction, serial_pin_out); } void init_timers() { //8-bit timer 0 for decay, sweep, vibrato effects? //Enable Overflow interrupts for Timer 0 TIMSK0 = 0b00000001; //Normal counter operation TCCR0A = 0b00000000; //Divide by 1024 prescalar TCCR0B = 0b00000101; //Start terminal count at zero TCNT0 = 0x00; //16-bit timer 1 for main frequency generation TIMSK1 = 0b00000010; // Enable A and B compare interrupts TCCR1A = 0b00000001; // Prescaler 1, Fast PWM TCCR1B = 0b00010001; //Start count at zero now TCNT1H = 0; TCNT1L = 0; // CLKPR = (1 << CLKPCE); // CLKPR = (0 << CLKPS3) | (0 << CLKPS2) | (0 << CLKPS1) | (0 << CLKPS0); } int main(void) { /* Disable interrupts at first */ cli(); /* Setup I/O Pins */ init_io(); /* Setup Timers */ init_timers(); /* Enable USART and Pin Interrupts */ // init_interrupts(); // PORTA |= 0x0F; OCR1A = 500; /*Pitch bend needs to start at 64 (middle value) or pitch will start out two half steps too low (0 = max bend downward) */ current_midi_pb_l = current_midi_pb_h = 64; num_bytes = 0; num_ccs = 0; num_pbs = 0; byte_ready = 0; midi_channel = 0; master_volume = 127; amplitude = 0; fine_pitch_bend = 0; note_on_gate = 0; sweep_enabled = 1; sweep_direction = SWEEP_UP; sweep_amount = 0; sweep_loop_enabled = 1; fake_16_timer = 0; /* Finally, enable global interrupts */ sei(); /*Main Loop*/ while (1) { // check_channel_set(); // check_byte_received(); // if(pin_test(led_port, led_pin)) clear(led_port, led_pin); // else set(led_port, led_pin); // _delay_ms(200); get_char(&serial_pins, serial_pin_in, &recmsg0); get_char(&serial_pins, serial_pin_in, &recmsg1); get_char(&serial_pins, serial_pin_in, &recmsg2); get_char(&serial_pins, serial_pin_in, &recmsg3); /* int i; int j; for (i = 8; i > 0; i--) { for (j = 0; j < i; j++) { set(led_port, led_pin); _delay_ms(80); clear(led_port, led_pin); _delay_ms(20); } if (bit_test(recmsg[0], (i-1))) set(led_port, led_pin); _delay_ms(400); clear(led_port, led_pin); _delay_ms(100); }*/ if (recmsg0 == synthID) { byte_received = recmsg1; // byte_ready = 1; check_byte_received(); byte_received = recmsg2; // byte_ready = 1; check_byte_received(); byte_received = recmsg3; // byte_ready = 1; check_byte_received(); } } return 0; } /* USART Received byte interrupt (get MIDI byte)*/ //ISR(USART_RX_vect) { // byte_received = UDR0; // byte_ready = 1; //} ISR(TIM1_COMPA_vect) { if(note_on_gate == 1) { fivebit_counter ++; //Reset counter if 'overflowed' if(fivebit_counter> 31) fivebit_counter = 0; //Each amplitude b0-3 is XNORed with b4 to create output unsigned char strobe = (fivebit_counter & 0b00010000) >> 4; if(strobe == 1) { amplitude = ~(fivebit_counter ^ 0b00001111); } else { amplitude = ~(fivebit_counter ^ 0b00000000); } amplitude &= 0b11101111; amplitude |= (strobe << 4); PORTA = amplitude; } } ISR(TIM0_OVF_vect) { /* Sweep */ if((sweep_enabled == 1) && (sweep_amount> 0) && (note_on_gate == 1)) { fake_16_timer ++; if(fake_16_timer >= 2) { unsigned int sweep_mod = sweep_amount * note_table[playing_midi_note] / (8 * playing_midi_note); //Sweep down mode if(sweep_direction == SWEEP_DOWN) { if(frequency < MAXIMUM_FREQ) { frequency += sweep_mod; } else { if(sweep_loop_enabled == 1) { frequency = note_table[playing_midi_note]; } else { amplitude = 0; } } } //Sweep up mode else { if(frequency> MINIMUM_FREQ+sweep_mod) { frequency -= sweep_mod; } else { if(sweep_loop_enabled == 1) { frequency = note_table[playing_midi_note]; } else { amplitude = 0; } } } update_frequency(frequency); fake_16_timer = 0; } } } void check_byte_received() { //Is there a byte waiting in the buffer? // if (byte_ready == 1) { //Is this a stupid byte like Clock or Active sensing? if (byte_received < 0xF0) { //Is this a status byte? ... if (byte_received >= 0x80) { unsigned char temp_midi_channel = byte_received & 0x0F; //Is this for one of our channels? if (temp_midi_channel == midi_channel) { current_midi_channel = temp_midi_channel; //What kind of status byte is this? unsigned char status_type = (byte_received & 0xF0); switch (status_type) { case (MIDI_STATUS_TYPE_NOTEON): current_midi_status = MIDI_STATUS_NOTEON; break; case (MIDI_STATUS_TYPE_NOTEOFF): current_midi_status = MIDI_STATUS_NOTEOFF; break; case (MIDI_STATUS_TYPE_CC): current_midi_status = MIDI_STATUS_CC; break; case (MIDI_STATUS_TYPE_PB): current_midi_status = MIDI_STATUS_PB; break; default: current_midi_status = MIDI_STATUS_NONE; } } else { current_midi_status = 0; } } // ... or is it a data byte? else { switch (current_midi_status) { case (MIDI_STATUS_NOTEON): //Is this a velocity byte? if (num_bytes > 0) { //If the velocity sent was 0, then this is really a NOTE-OFF if (byte_received > 0) { //current_midi_velocity = byte_received; playing_midi_note = current_midi_note; note_on(); } else { if (current_midi_note == playing_midi_note) { //current_midi_velocity = 0; note_off(); } } num_bytes = 0; } //Or is this a note data byte? else { if((byte_received > 17) & (byte_received < 66)) { current_midi_note = byte_received; num_bytes++; } } //Clear the byte so we don't process it twice clear_byte_received(); break; case (MIDI_STATUS_NOTEOFF): //Is this a velocity byte? if (num_bytes > 0) { //Who cares? We aren't implementing aftertouch num_bytes = 0; } else { /* This is a note byte. Let's see if it's the same as the currently * playing note. Only then will we note_off() */ if (byte_received == playing_midi_note) { note_off(); } num_bytes++; } //Clear the byte so we don't process it twice clear_byte_received(); break; case (MIDI_STATUS_CC): //Did we already get a CC Status byte? if (num_ccs > 0) { current_midi_ccdata = byte_received; process_cc(); } //Or is this a new CC status byte? else { current_midi_cc = byte_received; num_ccs++; } break; case (MIDI_STATUS_PB): //How many PB related bytes have we gotten? switch (num_pbs) { case (0): //First byte is 7 LSB //Don't care about it for now //current_midi_pb_l = byte_received; num_pbs++; break; case (1): //Second byte has 7 MSB current_midi_pb_h = byte_received; //Combine to get 14 bytes 0 - 13 //current_midi_pb = ((current_midi_pb_h << 7)|(current_midi_pb_l << 0)); bend_pitch(); break; } break; } } } // byte_ready = 0; // } } /* void enable_USART_interrupts() { UCSR0A = (1 << RXCIE0); } void disable_USART_interrupts() { UCSR0A = (0 << RXCIE0); } */ void note_on() { num_bytes = 0; //Reset main timer1 TCNT1 = 0; //Set timer count corresponding to midi note and thus musical note frequency = note_table[current_midi_note]; update_frequency(frequency); note_on_gate = 1; set(led_port, led_pin); } void note_off() { num_bytes = 0; note_on_gate = 0; clear(led_port, led_pin); } void process_cc() { num_ccs = 0; switch (current_midi_cc) { case MASTER_VOLUME_CC: master_volume = current_midi_ccdata; break; case FINE_PITCH_CC: fine_pitch_bend = ((note_table[playing_midi_note - 1] - note_table[playing_midi_note]) * current_midi_ccdata) / 192; update_frequency(note_table[playing_midi_note]); break; case SWEEP_ENABLED_CC: if (current_midi_ccdata > 64) sweep_enabled = 1; else sweep_enabled = 0; break; case SWEEP_DIRECTION_CC: if (current_midi_ccdata > 64) sweep_direction = SWEEP_UP; else sweep_direction = SWEEP_DOWN; break; case SWEEP_AMOUNT_CC: sweep_amount = current_midi_ccdata; OCR1B = sweep_amount * 1024; break; case SWEEP_LOOP_ENABLED_CC: if (current_midi_ccdata > 64) sweep_loop_enabled = 1; else sweep_loop_enabled = 0; break; } } void update_frequency(unsigned int new_frequency) { OCR1A = new_frequency + fine_pitch_bend; } void bend_pitch() { num_pbs = 0; if (current_midi_pb_h > 63) { distance = ((note_table[playing_midi_note] - note_table[playing_midi_note + 2]) * (current_midi_pb_h - 63)) / 64; update_frequency(note_table[playing_midi_note] - distance); } else if ((current_midi_pb_h < 63) && (playing_midi_note > 1)) { distance = ((note_table[playing_midi_note - 2] - note_table[playing_midi_note]) * (64 - current_midi_pb_h)) / 64; update_frequency(note_table[playing_midi_note] + distance); } } void clear_byte_received() { byte_received = 0; } /* void check_channel_set() { midi_channel = 0; //Get 4-bit (0-16) MIDI CHannel from PORTD b4-b7) midi_channel |= (~PIND & 0xF0) >> 4; } */

LCD module

The third ATTiny44 microcontroller drives the LCD screen as per the examples linked previously. Within the code there is a function to decode the MIDI instruction and generate a suitable message string.

#include <avr/io.h> #include <util/delay.h> #include <avr/pgmspace.h> #include "main.h" //lifted from NeilG #define output(directions,pin) (directions |= pin) // set port direction for output #define input(directions,pin) (directions &= (~pin)) // set port direction for input #define set(port,pin) (port |= pin) // set port pin #define clear(port,pin) (port &= (~pin)) // clear port pin #define pin_test(pins,pin) (pins & pin) // test for port pin #define bit_test(byte,bit) (byte & (1 << bit)) // test for bit set #define bit_delay_time 20 // bit delay for 9600 with overhead #define bit_delay() _delay_us(bit_delay_time) // RS232 bit delay #define half_bit_delay() _delay_us(bit_delay_time/2) // RS232 half bit delay #define lcd_port PORTA #define lcd_direction DDRA #define lcd_pins PINA #define lcd_SCE (1 << PA0) #define lcd_RST (1 << PA1) #define lcd_DC (1 << PA2) #define lcd_SDIN (1 << PA3) #define lcd_SCLK (1 << PA4) #define led_port PORTA #define led_direction DDRA #define led_pin (1 << PA5) #define serial_port PORTA #define serial_direction DDRA #define serial_pins PINA #define serial_pin_in (1 << PA6) #define serial_pin_out (1 << PA7) #define LCD_C 0 #define LCD_D 1 #define LCD_X 84 #define LCD_Y 48 #define lcdID 2 char recmsg0; char recmsg1; char recmsg2; char recmsg3; const uint8_t PROGMEM ascii_table[] = { 0x00, 0x00, 0x00, 0x00, 0x00, // 20 0x00, 0x00, 0x5f, 0x00, 0x00, // 21 ! 0x00, 0x07, 0x00, 0x07, 0x00, // 22 " 0x14, 0x7f, 0x14, 0x7f, 0x14, // 23 # 0x24, 0x2a, 0x7f, 0x2a, 0x12, // 24 $ 0x23, 0x13, 0x08, 0x64, 0x62, // 25 % 0x36, 0x49, 0x55, 0x22, 0x50, // 26 & 0x00, 0x05, 0x03, 0x00, 0x00, // 27 ' 0x00, 0x1c, 0x22, 0x41, 0x00, // 28 ( 0x00, 0x41, 0x22, 0x1c, 0x00, // 29 ) 0x14, 0x08, 0x3e, 0x08, 0x14, // 2a * 0x08, 0x08, 0x3e, 0x08, 0x08, // 2b + 0x00, 0x50, 0x30, 0x00, 0x00, // 2c , 0x08, 0x08, 0x08, 0x08, 0x08, // 2d - 0x00, 0x60, 0x60, 0x00, 0x00, // 2e . 0x20, 0x10, 0x08, 0x04, 0x02, // 2f / 0x3e, 0x51, 0x49, 0x45, 0x3e, // 30 0 0x00, 0x42, 0x7f, 0x40, 0x00, // 31 1 0x42, 0x61, 0x51, 0x49, 0x46, // 32 2 0x21, 0x41, 0x45, 0x4b, 0x31, // 33 3 0x18, 0x14, 0x12, 0x7f, 0x10, // 34 4 0x27, 0x45, 0x45, 0x45, 0x39, // 35 5 0x3c, 0x4a, 0x49, 0x49, 0x30, // 36 6 0x01, 0x71, 0x09, 0x05, 0x03, // 37 7 0x36, 0x49, 0x49, 0x49, 0x36, // 38 8 0x06, 0x49, 0x49, 0x29, 0x1e, // 39 9 0x00, 0x36, 0x36, 0x00, 0x00, // 3a : 0x00, 0x56, 0x36, 0x00, 0x00, // 3b ; 0x08, 0x14, 0x22, 0x41, 0x00, // 3c < 0x14, 0x14, 0x14, 0x14, 0x14, // 3d = 0x00, 0x41, 0x22, 0x14, 0x08, // 3e > 0x02, 0x01, 0x51, 0x09, 0x06, // 3f ? 0x32, 0x49, 0x79, 0x41, 0x3e, // 40 @ 0x7e, 0x11, 0x11, 0x11, 0x7e, // 41 A 0x7f, 0x49, 0x49, 0x49, 0x36, // 42 B 0x3e, 0x41, 0x41, 0x41, 0x22, // 43 C 0x7f, 0x41, 0x41, 0x22, 0x1c, // 44 D 0x7f, 0x49, 0x49, 0x49, 0x41, // 45 E 0x7f, 0x09, 0x09, 0x09, 0x01, // 46 F 0x3e, 0x41, 0x49, 0x49, 0x7a, // 47 G 0x7f, 0x08, 0x08, 0x08, 0x7f, // 48 H 0x00, 0x41, 0x7f, 0x41, 0x00, // 49 I 0x20, 0x40, 0x41, 0x3f, 0x01, // 4a J 0x7f, 0x08, 0x14, 0x22, 0x41, // 4b K 0x7f, 0x40, 0x40, 0x40, 0x40, // 4c L 0x7f, 0x02, 0x0c, 0x02, 0x7f, // 4d M 0x7f, 0x04, 0x08, 0x10, 0x7f, // 4e N 0x3e, 0x41, 0x41, 0x41, 0x3e, // 4f O 0x7f, 0x09, 0x09, 0x09, 0x06, // 50 P 0x3e, 0x41, 0x51, 0x21, 0x5e, // 51 Q 0x7f, 0x09, 0x19, 0x29, 0x46, // 52 R 0x46, 0x49, 0x49, 0x49, 0x31, // 53 S 0x01, 0x01, 0x7f, 0x01, 0x01, // 54 T 0x3f, 0x40, 0x40, 0x40, 0x3f, // 55 U 0x1f, 0x20, 0x40, 0x20, 0x1f, // 56 V 0x3f, 0x40, 0x38, 0x40, 0x3f, // 57 W 0x63, 0x14, 0x08, 0x14, 0x63, // 58 X 0x07, 0x08, 0x70, 0x08, 0x07, // 59 Y 0x61, 0x51, 0x49, 0x45, 0x43, // 5a Z 0x00, 0x7f, 0x41, 0x41, 0x00, // 5b [ 0x02, 0x04, 0x08, 0x10, 0x20, // 5c ¥ 0x00, 0x41, 0x41, 0x7f, 0x00, // 5d ] 0x04, 0x02, 0x01, 0x02, 0x04, // 5e ^ 0x40, 0x40, 0x40, 0x40, 0x40, // 5f _ 0x00, 0x01, 0x02, 0x04, 0x00, // 60 ` 0x20, 0x54, 0x54, 0x54, 0x78, // 61 a 0x7f, 0x48, 0x44, 0x44, 0x38, // 62 b 0x38, 0x44, 0x44, 0x44, 0x20, // 63 c 0x38, 0x44, 0x44, 0x48, 0x7f, // 64 d 0x38, 0x54, 0x54, 0x54, 0x18, // 65 e 0x08, 0x7e, 0x09, 0x01, 0x02, // 66 f 0x0c, 0x52, 0x52, 0x52, 0x3e, // 67 g 0x7f, 0x08, 0x04, 0x04, 0x78, // 68 h 0x00, 0x44, 0x7d, 0x40, 0x00, // 69 i 0x20, 0x40, 0x44, 0x3d, 0x00, // 6a j 0x7f, 0x10, 0x28, 0x44, 0x00, // 6b k 0x00, 0x41, 0x7f, 0x40, 0x00, // 6c l 0x7c, 0x04, 0x18, 0x04, 0x78, // 6d m 0x7c, 0x08, 0x04, 0x04, 0x78, // 6e n 0x38, 0x44, 0x44, 0x44, 0x38, // 6f o 0x7c, 0x14, 0x14, 0x14, 0x08, // 70 p 0x08, 0x14, 0x14, 0x18, 0x7c, // 71 q 0x7c, 0x08, 0x04, 0x04, 0x08, // 72 r 0x48, 0x54, 0x54, 0x54, 0x20, // 73 s 0x04, 0x3f, 0x44, 0x40, 0x20, // 74 t 0x3c, 0x40, 0x40, 0x20, 0x7c, // 75 u 0x1c, 0x20, 0x40, 0x20, 0x1c, // 76 v 0x3c, 0x40, 0x30, 0x40, 0x3c, // 77 w 0x44, 0x28, 0x10, 0x28, 0x44, // 78 x 0x0c, 0x50, 0x50, 0x50, 0x3c, // 79 y 0x44, 0x64, 0x54, 0x4c, 0x44, // 7a z 0x00, 0x08, 0x36, 0x41, 0x00, // 7b { 0x00, 0x00, 0x7f, 0x00, 0x00, // 7c | 0x00, 0x41, 0x36, 0x08, 0x00, // 7d } }; //network protocols lifted from NeilG void get_char(volatile unsigned char *pins, unsigned char pin, char *rxbyte) { // read character into rxbyte on pins pin - assumes line driver (inverts bits) *rxbyte = 0; while (pin_test(*pins,pin)) ;// wait for start bit half_bit_delay(); // delay to middle of first data bit bit_delay(); // unrolled loop to read data bits if pin_test(*pins,pin) *rxbyte |= (1 << 0); else *rxbyte |= (0 << 0); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 1); else *rxbyte |= (0 << 1); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 2); else *rxbyte |= (0 << 2); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 3); else *rxbyte |= (0 << 3); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 4); else *rxbyte |= (0 << 4); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 5); else *rxbyte |= (0 << 5); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 6); else *rxbyte |= (0 << 6); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 7); else *rxbyte |= (0 << 7); // wait for stop bit bit_delay(); half_bit_delay(); } void lcd_write(uint8_t dc, uint8_t data) { if (dc) set(lcd_port, lcd_DC); else clear(lcd_port, lcd_DC); clear(lcd_port, lcd_SCE); int i; for (i = 0; i < 8; i++) { if (data&(1<<7)) set(lcd_port, lcd_SDIN); else clear(lcd_port, lcd_SDIN); clear(lcd_port, lcd_SCLK); set(lcd_port, lcd_SCLK); data <<= 1; } set(lcd_port, lcd_SCE); clear(lcd_port, lcd_SDIN); } void lcd_char(char c) { lcd_write(LCD_D, 0x00); int i; for (i = 0; i < 5; i++) lcd_write(LCD_D, pgm_read_byte(ascii_table+(c-0x20)*5+i)); lcd_write(LCD_D, 0x00); } void lcd_str(const char* str) { while (pgm_read_byte(str)) { lcd_char(pgm_read_byte(str)); str++; } } void lcd_clear(void) { int i; for (i = 0; i < LCD_X*LCD_Y/8; i++) lcd_write(LCD_D, 0x00); } void lcd_gotoXY(int x, int y) // x 0 - 84, y 0 - 5 { lcd_write(0, 0x80 | x); // column lcd_write(0, 0x40 | y); // row } void setup_lcd() { clear(lcd_port, lcd_RST); set(lcd_port, lcd_RST); lcd_write(LCD_C,0x21); //switch to extended commands lcd_write(LCD_C,0xBB); //set value of Vop (controls contrast) = 0x80 | 0x60 (arbitrary) lcd_write(LCD_C,0x04); //set temperature coefficient lcd_write(LCD_C,0x14); //set bias mode to 1:48. lcd_write(LCD_C,0x20); //switch back to regular commands lcd_write(LCD_C,0x0C); //enable normal display (dark on light), horizontal addressing } void setup_io() { output(lcd_direction, lcd_SCE); output(lcd_direction, lcd_RST); output(lcd_direction, lcd_DC); output(lcd_direction, lcd_SDIN); output(lcd_direction, lcd_SCLK); output(led_direction, led_pin); set(serial_port, serial_pin_out); input(serial_direction, serial_pin_out); } int main() { setup_io(); setup_lcd(); lcd_clear(); lcd_str(PSTR("FabSynth 0.1")); _delay_ms(2000); // this is to allow for v-usb board to fully boot while (1) { get_char(&serial_pins, serial_pin_in, &recmsg0); get_char(&serial_pins, serial_pin_in, &recmsg1); get_char(&serial_pins, serial_pin_in, &recmsg2); get_char(&serial_pins, serial_pin_in, &recmsg3); if (recmsg0 == lcdID) { lcd_clear(); lcd_gotoXY(0,0); lcd_str(PSTR("note played")); switch (recmsg2 % 12) { case (0): lcd_str(PSTR("C")); break; case (1): lcd_str(PSTR("C#")); break; case (2): lcd_str(PSTR("D")); break; case (3): lcd_str(PSTR("D#")); break; case (4): lcd_str(PSTR("E")); break; case (5): lcd_str(PSTR("F")); break; case (6): lcd_str(PSTR("F#")); break; case (7): lcd_str(PSTR("G")); break; case (8): lcd_str(PSTR("G#")); break; case (9): lcd_str(PSTR("A")); break; case (10): lcd_str(PSTR("A#")); break; case (11): lcd_str(PSTR("B")); break; } switch (recmsg2 / 12) { case (0): lcd_str(PSTR("-2")); break; case (1): lcd_str(PSTR("-1")); break; case (2): lcd_str(PSTR("0")); break; case (3): lcd_str(PSTR("1")); break; case (4): lcd_str(PSTR("2")); break; case (5): lcd_str(PSTR("3")); break; case (6): lcd_str(PSTR("4")); break; case (7): lcd_str(PSTR("5")); break; case (8): lcd_str(PSTR("6")); break; case (9): lcd_str(PSTR("7")); break; case (10): lcd_str(PSTR("8")); break; } switch (recmsg3 > 0) { case (0): lcd_str(PSTR(" off")); clear(led_port, led_pin); break; case (1): lcd_str(PSTR(" on")); set(led_port, led_pin); break; } } } return 0; }

Trigger module

The fourth microcontroller, when it receives the ready instruction from the USB master microontroller, reads and transmits the states of the four push buttons. The master module then determines whether there have been any state changes, and generates a MIDI message if necessary. The buttons are wired with a common ground, and internal pullups activated on the relevant microcontroller input pins.

#include <avr/io.h> #include <util/delay.h> #include <avr/pgmspace.h> #include "main.h" //lifted from NeilG #define output(directions,pin) (directions |= pin) // set port direction for output #define input(directions,pin) (directions &= (~pin)) // set port direction for input #define set(port,pin) (port |= pin) // set port pin #define clear(port,pin) (port &= (~pin)) // clear port pin #define pin_test(pins,pin) (pins & pin) // test for port pin #define bit_test(byte,bit) (byte & (1 << bit)) // test for bit set #define bit_delay_time 20 // bit delay for 9600 with overhead #define bit_delay() _delay_us(bit_delay_time) // RS232 bit delay #define half_bit_delay() _delay_us(bit_delay_time/2) // RS232 half bit delay #define button_port PORTA #define button_direction DDRA #define button_pins PINA #define button1_pin (1 << PA0) #define button2_pin (1 << PA1) #define button3_pin (1 << PA2) #define button4_pin (1 << PA3) #define led_port PORTA #define led_direction DDRA #define led_pin (1 << PA5) #define serial_port PORTA #define serial_direction DDRA #define serial_pins PINA #define serial_pin_in (1 << PA6) #define serial_pin_out (1 << PA7) #define triggerID 2 char recmsg0; char recmsg1; char recmsg2; char recmsg3; char sendchr; //network protocols lifted from NeilG void get_char(volatile unsigned char *pins, unsigned char pin, char *rxbyte) { // read character into rxbyte on pins pin - assumes line driver (inverts bits) *rxbyte = 0; while (pin_test(*pins,pin)) ;// wait for start bit half_bit_delay(); // delay to middle of first data bit bit_delay(); // unrolled loop to read data bits if pin_test(*pins,pin) *rxbyte |= (1 << 0); else *rxbyte |= (0 << 0); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 1); else *rxbyte |= (0 << 1); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 2); else *rxbyte |= (0 << 2); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 3); else *rxbyte |= (0 << 3); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 4); else *rxbyte |= (0 << 4); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 5); else *rxbyte |= (0 << 5); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 6); else *rxbyte |= (0 << 6); bit_delay(); if pin_test(*pins,pin) *rxbyte |= (1 << 7); else *rxbyte |= (0 << 7); // wait for stop bit bit_delay(); half_bit_delay(); } void put_char(volatile unsigned char *port, unsigned char pin, char txchar) { // send character in txchar on port pin - assumes line driver (inverts bits) clear(*port,pin); // start bit bit_delay(); // unrolled loop to write data bits if bit_test(txchar,0) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,1) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,2) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,3) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,4) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,5) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,6) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,7) set(*port,pin); else clear(*port,pin); bit_delay(); // stop bit set(*port,pin); bit_delay(); // char delay bit_delay(); } void setup_io() { set(button_port, button1_pin); set(button_port, button2_pin); set(button_port, button3_pin); set(button_port, button4_pin); input(button_direction, button1_pin); input(button_direction, button2_pin); input(button_direction, button3_pin); input(button_direction, button4_pin); output(led_direction, led_pin); set(serial_port, serial_pin_out); input(serial_direction, serial_pin_out); } int main() { setup_io(); while (1) { get_char(&serial_pins, serial_pin_in, &recmsg0); get_char(&serial_pins, serial_pin_in, &recmsg1); get_char(&serial_pins, serial_pin_in, &recmsg2); get_char(&serial_pins, serial_pin_in, &recmsg3); if (recmsg0 == triggerID) { sendchr = button_pins && 0xF0; output(serial_direction, serial_pin_out); putchar(&serial_pins, serial_pin_out, &sendchar); input(serial_direction, serial_pin_out); } return 0; } }

Casing

The case was modelled in Sketchup then lasercut from 3mm plywood. The clip joints hold the case together tightly and have withstood several attachment/detachment cycles.

Ongoing work

Shortcomings of the project (set aside for future developments) focus on two areas:

  • - Commercial MIDI hardware is designed such that lag is kept to an absolute minimum, in order to allow for expressive, responsive instruments. This has not been a focus of my project, as it would greatly increase the complexity both of the components required and of the implementation.
  • - Commercial MIDI synthesisers also tend to have the ability to generate more than a square wave; I have not initially concentrated on making a fully featured synth, rather I have tried to create a proof of concept that will be of help to people undertaking similar projects in the future.

In addition to these, I aim to complete the layouts for the remaining circuit boards to take the final modules out of the prototyping environment.

Conclusions

Overall I am very happy with how this project turned out. I found the spiral development approach very helpful, working towards gradual increments which forced me to think through the individual components of the project before starting. At times I also reached the limits of the capabilities of the microcontroller and my ability to efficiently program it, giving useful insight into the scope of what can be achieved with these components.

<<< Week 18

Module Index >>>