Project: simple MIDI controller

Started by danngreen, August 05, 2006, 01:03:25 AM

Previous topic - Next topic

danngreen

OK, I've been away for a while but I'm back and I dug up and old project that uses the AVR and MIDI!
Hopefully someone will find this a good intro project.

Anyways,
It's a simple MIDI controller using the AVR '4433 (28-pin), and I programmed the code in C (using WinAVR/avr-gcc). It has up to 6 pots and 6 switches (I only used 3 pots and 5 switches on the one I built). Each pot goes to an ADC input pin, and the voltage gets converted to a value between 0 and 127. This value is sent to the MIDI output port as a continuous controller command.
Similarly, each switch goes to a digital input pin and gets converted to a 0 (if the switch is open) or a 127 (if the switch is closed). The value is also sent as a CC command. Each pot and switch has it's own CC#.  Also, there's an LED to indicate each switch's state.

I built this thing for an art-installation at a fundraiser. (You can read about it at http://www.4mspedals.com/influx9/index.html if you wish...) The MIDI controller ran into to a linux box that was running real-time effects, whose parameters were controlled by MIDI commands. The linux serial port driver was simplier to get working so I used a serial-port output from the AVR chip. It would be easy to change that to a standard MIDI output, in fact, the code has a switch in it to select serial port baud rate, or MIDI baud rate. But I'm getting ahead of myself..

First, here's the schematic:

danngreen

#1
OK, and here's the code:

/*
* CV or pot/switch
* to
* MIDI
* converter
*
* The controls go to the AVR chip's ADC pins,
   first control to AD1, second control to AD2, third to AD3
* A switch goes to PIND2 which selects
   hi=38400 baud (serial port)
   lo=31500 baud (standard MIDI)
*
* Use with a 3.6864MHz crystal
*/

#include <stdarg.h>
#include <ctype.h>
#include <io4433.h>

#define xADCSR   (*(volatile unsigned char*)0x26)
#define xUCSRA  (*(volatile unsigned char*)0x2B)

#define debug 0
#define NUM_KNOBS 3 //number of controls/pots
#define NUM_BUTTONS 5 //number of controls/pots
#define POT_CHANNEL 0x00 //midi channel to use for controllers (pots)
#define BUT_CHANNEL 0x01 //midi channel to use for effect selection (buttons)
#define IDLE_LOOPS 400 //number of loops with no midi signal before we send status of all buttons/knobs
void adc_init(void);

void adc_init(void)
{
   outp(0x00, DDRC);
   outp(0x00, PORTC);
    outp(0x02 | (1 << ADEN)  | (1 << ADSC) | (0 << ADFR) ,   ADCSR);        //set up ADC Prescaler Divide 4 mHz by 128 ADC enable
                        //ADC Free Run   SB 20 samples per hz
}

int main(void)
{
short ain_lo, ain_hi; //analog input high and low byte
short ain; //analog in (full word)
short val[NUM_KNOBS]; //array of current controller values
short fx_on[NUM_BUTTONS]; //stores which fx are on/off
short cur_pot; //current control number (0..NUM_KNOBS-1) that we're reading in the ADC
short cur_but; //current button (0..NUM_BUTTONS-1) that we're reading from PINB
short count,count2,idle_count;
short last_baud;


if (debug){
}else{
   outp(0x00, DDRB); //portb as inputs for effect selection
   outp(0xFF, PORTB); //pull portB high
   outp(0<<PD2, DDRD);//portD pin 2 as input for baud select
   outp(1<<PD2, PORTD);//pullup portD pin 2 high
   
   adc_init();                   /* init the ADC and MUX  buffer */
   
   outp(0x00, UCSRA);       //uart init
   outp((1<<TXEN) | (1<<RXEN), UCSRB);
   
   outp(5 , UBRR);last_baud=5;
   
   outp(0x00,  ADMUX);           /*set up mux to channel 0*/
   
   cur_pot=0;
   cur_but=0;
        idle_count=IDLE_LOOPS;

   for (count=0;count<NUM_KNOBS;count++){
      val[count]=0x40;
   }
   for (count=0;count<NUM_BUTTONS;count++){
      fx_on[count]=0x00;
   }

    for (;;)                      /* loop forever */
    {
   
       /*********************
       ** BAUD RATE SELECT **
       **********************/
       
        ain_lo=inp(PIND);
        if (ain_lo & (1<<PD2)){
                ain_hi=5;          //pinD2 high means serial MIDI (38400 baud)
        }else{  //we're just using ain_hi to conserve variables :)
                ain_hi=6;           //pinD2 low means MIDI (31500 baud)
        }
        if (ain_hi!=last_baud){
                last_baud=ain_hi;
                outp(last_baud,UBRR);  //only output to UART baud reg if it changed!
        }
       
   
       /*************************
       **  BUTTONS: FX SELECT  **
       **************************/
       
      ain_lo=inp(PINB); //6 bits
      count2=((ain_lo & (1 << cur_but)) ? 0x7F : 0x00); //if the bit is set, MIDI ctrlr value = 127
      if (fx_on[cur_but]!=count2){
         fx_on[cur_but]=count2;
         
                        idle_count=0;
         while ( !(xUCSRA & (1<<UDRE)) ) ;
         outp(0xB0 | BUT_CHANNEL, UDR);
               //send status byte: continuous controllers
         while ( !(xUCSRA & (1<<UDRE)) ) ;
         outp(cur_but, UDR);
               //send data byte1: controller number (starts at controller 1 [0x00])
         while ( !(xUCSRA & (1<<UDRE)) ) ;
         outp(fx_on[cur_but], UDR);
               //send data byte2: value of controller (0..127)

      }
      cur_but++;
      if(cur_but>=NUM_BUTTONS) cur_but=0;

         
          /*********************
       **  READ CTRL VALUE **
       **********************/

        xADCSR |= 1<<ADSC;
       do{ } while(!(xADCSR & (1<<ADIF)));     // Wait for A/D conversion to finish
        xADCSR |= 1<<ADIF;                    // Clear the flag
   
      ain_lo = inp(ADCL);                  //eight bits
      ain_hi = inp(ADCH);                  //two bits
      
      
   /*********************
       **  CTRL MIDI OUT   **
       **********************/

      ain=(ain_hi << 5) |  (ain_lo>>3);
         //scale the 10 bit value to a 7 bit value (0..127)
   
      if (((val[cur_pot]-ain)>1) || ((val[cur_pot]-ain)<-1)){   //if this control's value has changed...
         val[cur_pot]=ain;    //...update the array of control values

                        idle_count=0;
         while ( !(xUCSRA & (1<<UDRE)) ) ;
         outp(0xB0 | POT_CHANNEL, UDR);
               //send status byte: continuous controllers
         while ( !(xUCSRA & (1<<UDRE)) ) ;
         outp(cur_pot, UDR);
               //send data byte1: controller number (starts at controller 1 [0x00])
         while ( !(xUCSRA & (1<<UDRE)) ) ;
         outp(val[cur_pot], UDR);
               //send data byte2: value of controller (0..127)
      }

        /**********************
        ** IDLE MIDI UPDATES **
        ***********************/

                idle_count++;
                if (idle_count>=IDLE_LOOPS){
                    for (count=0;count<NUM_KNOBS;count++){
                    while ( !(xUCSRA & (1<<UDRE)) ) ;
              outp(0xB0 | POT_CHANNEL, UDR);
               //send status byte: continuous controllers
              while ( !(xUCSRA & (1<<UDRE)) ) ;
              outp(count, UDR);
               //send data byte1: controller number (starts at controller 1 [0x00])
              while ( !(xUCSRA & (1<<UDRE)) ) ;
              outp(val[count], UDR);
               //send data byte2: value of controller (0..127)
                   }
                   for (count2=0;count2<NUM_BUTTONS;count2++){
              while ( !(xUCSRA & (1<<UDRE)) ) ;
              outp(0xB0 | BUT_CHANNEL, UDR);
               //send status byte: continuous controllers
              while ( !(xUCSRA & (1<<UDRE)) ) ;
              outp(count2, UDR);
               //send data byte1: controller number (starts at controller 1 [0x00])
              while ( !(xUCSRA & (1<<UDRE)) ) ;
              outp(fx_on[count2], UDR);
               //send data byte2: value of controller (0..127)
                   }

                        idle_count=0;

                }

      count=1000;do{
         count2=10;while (--count2);
      } while(--count) ;

      cur_pot++;
      if(cur_pot>=NUM_KNOBS) cur_pot=0;

      outp(cur_pot ,ADMUX);   //set up mux to read next control


        count = 66;                         
        do{ } while(--count);                   // wait some cycles
       
   } //endless loop
} //if debug
} //main()


danngreen

Some notes:
-To change the serial port MIDI to a regular MIDI, you'd need to toggle pin 2 of port D (PD2, pin 4 of the AVR) to low. The schematic shows a switch there, but you can hard-wire it the way you want it. Then you can omit the MAX232 chip and instead run PD1 (pin 3 of the AVR) through a 220 ohm resistor to pin 5 of the MIDI jack (signal). Pin 2 of the MIDI jack gets grounded as usual on MIDI output jacks, and pin 4 of the jack goes through a 220 ohm to V+ on the board (+5V). Simple!

-Notice there are a lot of #defines at the start of the code:
NUM_KNOBS is the number of pots. I have it set to 3 but you can go up to 6 (or more, if you use an AVR chip with more ADC pins!)

NUM_BUTTONS is the number of switches. I have it set to 5, but again it will handle 6 (or more if you use a different chip...)

POT_CHANNEL is the MIDI channel for outputting the pot's CC commands. Note that the number here is one less than the standard MIDI value, so channel 1 is 0x00, channel 2 is 0x01, channel 3 is 0x02.. up to channel 16 is 0x0F (look out, we're in hexadecimal here, pay attention!)

BUT_CHANNEL is the MIDI channel for the switches. Same format as POT_CHANNEL

IDLE_LOOPS is probably at a good value, but you may want to tweak it to suit your needs. What the code does is only send a CC command when a pot/switch changes value. But I found that sometimes (especially at startup) things would get a-drift. So every so often, the code sends out all the values for all the switches/pots. The higher in value IDLE_LOOPS is, the longer between automatic updates. A value of 400 is a few hundred milliseconds (I think...), which is often enough to keep things tight, but not often enough to overload whatever MIDI device you're controlling.

-More than 6 switches/pots could be added in a variety of ways: the first that comes to mind would be a multiplexor. With 8 data pins controlling a mulitplexor, you could have 256 pots! Another way is using a serial connection such as is done in the midibox DIN module. Up to 8 switches can be connected to a 74HC165 chip, which can be cascaded to a number of more 74HC165 chips... the chain of 74HC165's can communicate with an AVR over I2C. I'm not sure which AVRs can use I2C (I've only used it on PICs), but this is a neat way to stack hundreds of switches into just four pins on the AVR/PIC.

Enjoy!
Dann

Peter Snowberg

Eschew paradigm obfuscation

bioroids

Eramos tan pobres!

kayaman



The Tone God

Dan, could you specify which compiler you are using ? The "io4433.h" header might throw off others trying to use your code in different compilers. I don't know about other compilers but in GNU-GCC you don't include the specific part header directly.

Do any other compilers still use those outdated functions ?

Andrew

danngreen

Quote from: The Tone God on August 16, 2006, 01:42:00 PM
Dan, could you specify which compiler you are using ? The "io4433.h" header might throw off others trying to use your code in different compilers. I don't know about other compilers but in GNU-GCC you don't include the specific part header directly.

OK, you probably could/should replace #include <io4433.h> with
#include <io.h>

I used avr-gcc to compile, and I believe I used WinAVR as my IDE. I may have done this on a linux box, but I still would have used avr-gcc. Anyways I know normally you can just include <io.h>, which then finds the io file for the proper chip, but in this case I included the file directly for some reason (despite the "never include this file directly" warning, oh well!)

Quote from: kayaman on August 16, 2006, 09:30:39 AM
I found that in :http://www.propox.com/download/edunet_doc/all/html/io4433_8h-source.html

Yep this looks like it! Geez, I have to get to my other workspace to dig up the exact file I used to compile, but the above link sure looks like the file I used.

This brings up an interesting point that I've found to be a useful shortcut with the avr-gcc compiler. Some other compilers (Imagecraft???) define the PORT's/PIN's in such a manner that you can assign and read them directly.
For instance:

PORTD=0xFF;
$mypinb3=PINB & (1<<PB3);

instead of:

outp(0xFF, PORTD);
$mypinb3=(inp(PINB)) & (1<<PB3);

You can make gcc do this by changing the defines (or defining new PORTs):
For example, in io4433.h (the standard avr-gcc io file), we have
#define PORTD   _SFR_IO8(0x12)

which is intended to be used with the outp function.

But if we define this:
#define PORTD        (*(volatile unsigned char *)0x32)

Then we can say
PORTD=0xF5;

We just add 0x20 to the address of the standard avr-gcc register, and make it a pointer to a volatile unsigned char *. I read once why we have to add 0x20, but I don't grasp that concept anymore. Something about how memory is counted. In any case, it works for both input and outputs! This allows you to port over code from other compilers, as well as write code in a slightly more readable fashion.

Of course, I did the most confusing thing possible and *mixed* usage of the standard avr-gcc convention and the shortcut convention (all my PORTs/PINS are standard, as defined in io4433.h/io.h, EXCEPT for
#define xADCSR   (*(volatile unsigned char*)0x26)
#define xUCSRA  (*(volatile unsigned char*)0x2B)
Note that I added an "x" in front of the name so I could keep them straight, which ones are directly assignable/readable (the "x"s) and which ones use inp/outp.

And this brings us back to the original question, why did I include io4433 directly? Probably because I was playing around with using both methods, and I may have tried including a io4433_direct.h file at some point.

The Tone God

I look at that code and I can tell right off the bat it will not compile even with the header changes as it uses outdated/obsoleted functions. The code needs to be update if anyone hopes to use it atleast with GCC. Not to mention the writer deserves a beating for their "formatting style". ;)

Andrew

danngreen

Quote from: The Tone God on August 31, 2006, 06:54:12 PM
Not to mention the writer deserves a beating for their "formatting style". ;)

What do you mean, I can read my chicken-scratches just fine  ;)

And OK, I did my homework and inp() and outp() are indeed deprecated, since before 2002. Oops I've been living in the stone ages  :icon_redface: (and yes, i just recently upgraded from OS 9 to OS X, and I like Windoze2000 better than XP). No wonder no one knew what I was talking about. Oh well, even old software will compile on new chips, so that's why my project worked.....for me (and the rest of us dinosaurs).

Well, I'm going to pull out the old AVR project board sometime soon, so I'll update the code. I swear I'll download the latest version of WinAVR, and even use some auto-formatting. In the mean time, if anyone wants to get this going, it shouldn't be too hard by changing the inp() and outp() functions to simple assigns (as I explained in the last post).
E.g:
outp(0x00, DDRC);
becomes
DDRC=0x00;

and
myvar=inp(PIND);
becomes
myvar=PIND;

danngreen

OK! I took some time and cleaned up the code, downloaded the latest version of AVRStudio (and WinAVR), compiled on both platforms, dusted off my STK500 and burnt it in with AVR Studio. I also got rid of the "baud select" feature, as this isn't something most people will need to flip on the fly. For standard MIDI baud, use a 4MHz crystal and comment out the appropriate line for SERIAL_MIDI. For serial port baud, use a 3.86MHz crystal.

I don't know how well people can read C code.... meaning I don't how detailed I should comment. I'd like this to be a good intro project for someone who's already gotten the hardware set-up and wants something useful beyond a blinky light. This code could also be used in a variety of applications where you want to output MIDI from your project. I'd be happy to explain what's going on line-by-line or answer specific questions if that would be helpful.


/*
* Voltage to MIDI controller
*
* Uses a 4433 AVR chip
*
* Dann Green, 2006-09-27
* version 2.0
*/

#include <stdarg.h>
#include <ctype.h>
#include <avr/io.h>

#define SERIAL_MIDI 0 //31500 baud (standard MIDI), use a 4 MHz crystal
//#define SERIAL_MIDI 1 //38400 baud (serial port), use a 3.6864MHz crystal

#define NUM_KNOBS 3 //number of controls/pots
#define NUM_BUTTONS 5 //number of buttons
#define POT_CHANNEL 0x00 //midi channel to use for controls/pots
#define BUT_CHANNEL 0x01 //midi channel to use for buttons
#define IDLE_LOOPS 400 //number of loops with no midi signal before we send status of all buttons/pots

//Here's two functions to make code more readable. Both of them just watch the registers until
//the some part of the AVR is ready for use

void wait_for_UART(void);
void wait_for_UART(void){
while ( !(UCSRA & (1<<UDRE)) ) ;  //wait until the Ready Flag is set in the UART Status Register
}

void wait_for_ADC(void);
void wait_for_ADC(void){
while( !(ADCSR & (1<<ADIF)) ); //wait until the Flag indicates ADC is set in the UART Status Register
}

void send_MIDI_command(short status, short data1, short data2);
void send_MIDI_command(short status, short data1, short data2){

wait_for_UART();
UDR=status; //send status byte
wait_for_UART();
UDR=data1; //send data byte1
wait_for_UART();
UDR=data2; //send data byte2
}

int main(void)
{
short ain_lo, ain_hi;         //analog input high and low byte
short ain;            //analog in (full word)
short pot_state[NUM_KNOBS];       //array of current pot values, so we know when they change
short button_state[NUM_BUTTONS]; //array of current button states
short cur_pot;        //current control number (0..NUM_KNOBS-1) that we're reading in the ADC
short cur_but;      //current button (0..NUM_BUTTONS-1) that we're reading from PINB
short idle_count;
short dummyvar;    //useful dummy variable;

/***************
** INITIALIZE **
****************/

//init the PORT's
DDRB=0x00; //portb as inputs for buttons
PORTB=0xFF; //pull portB high

if (SERIAL_MIDI) UBRR=5; //set baud rate to 38400 with a 3.86MHz crystal
else UBRR=7; //,..or set baud rate to 31500 with a 4MHz crystal

//init the ADC:
DDRC=0x00; //ADC pins to inputs
PORTC=0x00; //pull-down the input pins
ADCSR=0x02 | (1 << ADEN)  | (1 << ADSC) | (0 << ADFR);
//set flags: Prescaler=div by 4, Enable On, StartConversion On, Free-running Off
ADMUX=0x00; //Start with reading ADC channel 0

//init the UART:
UCSRA=0x00;
UCSRB=(1<<TXEN) | (1<<RXEN); //enable TX and RX



cur_pot=0;
cur_but=0;
    idle_count=IDLE_LOOPS;

//initialize our arrays of pots (default value is 0x40, half of MIDI's max of 0x7F = 127)
for (dummyvar=0;dummyvar<NUM_KNOBS;dummyvar++){
pot_state[dummyvar]=0x40;
}

//initialize our arrays of buttons (default value is off)
for (dummyvar=0;dummyvar<NUM_BUTTONS;dummyvar++){
button_state[dummyvar]=0x00;
}

    for (;;)   // loop forever
    {
   
   
    /**************
    **  BUTTONS  **
    ***************/
   
if (PINB & (1<<cur_but)) //check to see if the current button is set...
dummyvar=0x7F; //and we'll output a 0x7F (127 in decimal) if it's set
else
dummyvar=0x00; //or a 0x00 (0 in decimal) if it's not set

if (button_state[cur_but]!=dummyvar){ //if the new value is different than the current state...
button_state[cur_but]=dummyvar; //... then update the current state

            idle_count=0; //... and reset the idle_count timer
//... and output the MIDI commands:
send_MIDI_command(0xB0 | BUT_CHANNEL, cur_but, button_state[cur_but]);
//CC, controller number, controller value
}
cur_but++; //now get ready to check the next button the next time around
if(cur_but>=NUM_BUTTONS) cur_but=0;

         
        /*********************
    **  READ CTRL VALUE **
    **********************/
//Begin our ADC Conversion:

        ADCSR |= 1<<ADSC; //set the Start Conversion Flag in the ADC Status Register
wait_for_ADC();
        ADCSR |= 1<<ADIF;    // Clear the flag
   
ain_lo = ADCL; //grab the lower 8 bits into ain_lo
ain_hi = ADCH; //...and the upper 2 bits into ain_hi


/*********************
    **  CTRL MIDI OUT   **
    **********************/

ain=(ain_hi << 5) |  (ain_lo>>3);
//scale the 10 bit value (0..1024) to a 7 bit value (0..127)

if (((pot_state[cur_pot]-ain)>1) || ((pot_state[cur_pot]-ain)<-1)){
//if this control's value has changed by more than +/-1...
pot_state[cur_pot]=ain; //...then update the array of control values

            idle_count=0; //...and reset the idle_count timer

//... and output the MIDI commands:
send_MIDI_command(0xB0 | POT_CHANNEL, cur_pot, pot_state[cur_pot]);
//CC, controller number, controller value
}
cur_pot++; //now get ready to check the next pot the next time around
if(cur_pot>=NUM_KNOBS) cur_pot=0;


        /**********************
        ** IDLE MIDI UPDATES **
        ***********************/

        if (++idle_count>=IDLE_LOOPS){
            for (dummyvar=0;dummyvar<NUM_KNOBS;dummyvar++){
send_MIDI_command(0xB0 | POT_CHANNEL, dummyvar, pot_state[dummyvar]);
}
            for (dummyvar=0;dummyvar<NUM_BUTTONS;dummyvar++){
send_MIDI_command(0xB0 | BUT_CHANNEL, cur_but, button_state[cur_but]);
            }
            idle_count=0;
}

dummyvar=10000;
do{} while(--dummyvar) ;                   // wait some cycles

ADMUX=cur_pot; //set up mux to read next control

        dummyvar = 66;                         
        do{ } while(--dummyvar);                   // wait some cycles
       
   } //endless loop
} //main()



danngreen

#12
And the schematic....


The Tone God

Great work Dann! Thanks for all the effort. It is appreciated. :)

Andrew

Rafa

Im sorry for being so ignorant
But whats that midi controller for?? Is it something like  a lake buttler rfc1, to use wid rack effects??
Thanks
Rafa

Peter Snowberg

You can use it to control anything with a MIDI input. Dann's controller has three pots and five switches on it. How those control an external device is up to how you program it. You could also make the various controls affect different pieces of, or multiple pieces of MIDI enabled gear.

The beauty of DIY is that you can easily add things that would be outside the reach of off-the-shelf gear life being able to cross-fade multiple parameters on multiple boxes from a single pot.
Eschew paradigm obfuscation