• Welcome to our site! Electro Tech is an online community (with over 170,000 members) who enjoy talking about and building electronic circuits, projects and gadgets. To participate you need to register. Registration is free. Click here to register now.

Building a MIDI keytar

MichaelaJoy

Active Member
Ok. Here's my PIC24F32kA304 Assembler skeleton. (And I do mean skeleton)

I have no idea how this works as the CPU isn't here yet.
But it does compile successfully in MPLAB-X.

If I did this correctly, I've got some of the config registers programmed and the first Interrupt vector set up.

If anybody wants to tinker with this in MPLAB-X, just set up a new project with XC16 C as the tool.

Then, create a file called "whatever".s (The extension has to be ".s" or it'll fail.

Then, transplant this code in the Assembler file and build it.

It's not going to run; I'm still learning how to create an ISR for the main timer, and I have to cascade Timer #1 with timer #2
(more config stuff)

Code:
include "p24F32KA304.inc"

            .section __FWDT.sec, code
            .global __FWDT
__FWDT:        .pword FWDTEN_OFF

            .section __FDS.sec, code
            .global __FDS
__FDS:        .pword DSWDTEN_OFF

            .section __FICD.sec, code
            .global __FICD
__FICD:        .pword ICS_PGx1

            .section __FOSCSEL.sec, code
            .global __FOSCSEL
__FOSCSEL:    .pword FNOSC_PRI & OSCIOFNC_ON & LPRCSEL_HP & IESO_OFF & POSCFREQ_HS

            .section __FOSC.sec, code
            .global __FOSC
__FOSC:        .pword FNOSC_PRI & OSCIOFNC_ON & LPRCSEL_HP & IESO_OFF & POSCFREQ_HS

            .section ivt, code
;            goto __reset
            .pword paddr(KeyScanISR)

    .global KeyScanISR
    .text
KeyScanISR:
            bra    KeyScanISR

    .global   __reset
    .text
__reset:
        bra    __reset
    .end
Please let me know what you guys think. I'm experimenting with removing the C runtime alltogether.
There's a line that are commented out. The 'goto __reset' might be a spillover from the C compiler. If I remove the semicolon,
it seems to change the order of the segments.
 
Last edited:

wkrug

Active Member
Now I've played around with an ATMEL ATMEGA 16A running at 20MHz.
I know, You wanna using a Microchip Controller, but I have no experience with that.
In simulation a complete keyboard scan need's about 250µs.
A scanning restart will be done every 300µs.
( The Timing I've taken from the DD-E512 Datasheet there was used 250µs ).
So one velocity step is 300µs. The longest possible velocity cycle wanna be 37,5ms
I don't think a microchip controller works much faster.

What are the results:
There is not much processing time for other tasks.
I guess it's better to use a dedicated controller for keybed scanning process, when using velocity.
Using of assembler will be a must, when want scanning speed up.

Only for example the used C - code:
/*
* Keybordscanner.c
*
* Created: 04.01.2019 20:00:53
* Author : USER
*/

#define F_CPU 20000000
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

//********* Testvariablen ***********
uint8_t senddata = 0;

//MiDi variables
#define noteon 0x90 //Note On Byte
#define noteoff 0x80 //Note Off Byte
#define bufferlengh 168 //Bufferlengh for MiDi messages
#define transpose 20 //Transpose of MiDi notes
uint8_t midibuffer [bufferlengh]; //Buffer for MiDi messages
uint8_t midiwrite = 0; //MiDi buffer writepointer
uint8_t midiread = 0; //MiDi buffer readpointer

//scan process variables
#define idle 0
#define notestart 1
#define midisent 2

volatile uint8_t nextscan = 0; //Next Keyboard scan started
//Velocity counter
uint8_t note_velocity [56] =
{
128,128,128,128,128,128,128,128,
128,128,128,128,128,128,128,128,
128,128,128,128,128,128,128,128,
128,128,128,128,128,128,128,128,
128,128,128,128,128,128,128,128,
128,128,128,128,128,128,128,128,
128,128,128,128,128,128,128,128
};
//State memory
uint8_t note_state [56] =
{
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0
};


//Scan process will start every 300µs
ISR (TIMER1_COMPA_vect)
{
nextscan++;
}


void scan_keyboard (void)
{
uint8_t buffer1 = 0, buffer2 = 0, zeiger = 0, scanloop = 0, i = 0;

/* Outer loop - Pulls down the lines after the diodes */
for(scanloop=0;scanloop<8;scanloop++)
{

buffer1 = PINA;
buffer2 = PINC;
PORTB = ~(1<<(scanloop+1));
//Inner loop readout start velocity and end velocity lines and generate MiDi messages
for(i=0;i<7;i++)
{
//Pointer to actual note
zeiger = i*8 + scanloop;

//Scan velocity start first sample
if((buffer1 & (1<<i)) == 0)
{
if(note_state[zeiger] == idle) //Velocity counting begins
{
note_state[zeiger] = notestart;
}
if(note_state[zeiger] == notestart)
{
if(note_velocity[zeiger] > 1) //Velocity counting go on
{
note_velocity[zeiger]--;
}

}
}
else
{
/*MiDi sent, back to idle or send note off
when key pressed a little and go back to off state
the mode go back to idle.
When a complete scan was processed a Note On velocity 0 was sent */
if(note_state[zeiger] == notestart)
{
note_state[zeiger] = idle;
note_velocity[zeiger] = 128;
}
if(note_state[zeiger] == midisent)
{
midibuffer[midiwrite] = noteon;
midiwrite++;
if(midiwrite>bufferlengh){ midiwrite=0; }
midibuffer[midiwrite]= zeiger+transpose;
midiwrite++;
if(midiwrite>bufferlengh){ midiwrite=0; }
midibuffer[midiwrite]= 0;
midiwrite++;
if(midiwrite>bufferlengh){ midiwrite=0; }
note_state[zeiger] = idle;
}
}

//Velocity Stop second sample generate MiDi
if((buffer2 & (1<<i)) == 0)
{
if(note_state[zeiger] != midisent)
{
midibuffer[midiwrite++] = noteon;
if(midiwrite>bufferlengh){ midiwrite=0; }
midibuffer[midiwrite++]= zeiger+transpose;
if(midiwrite>bufferlengh){ midiwrite=0; }
midibuffer[midiwrite++]= note_velocity[zeiger];
if(midiwrite>bufferlengh){ midiwrite=0; }
note_state[zeiger]=midisent;
note_velocity[zeiger] = 128;
}

}
}
}
}



int main(void)
{
// Declare your local variables here

// Input/Output Ports initialization
// Port A initialization
// Function: Bit7=In Bit6=In Bit5=In Bit4=In Bit3=In Bit2=In Bit1=In Bit0=In
DDRA=(0<<DDA7) | (0<<DDA6) | (0<<DDA5) | (0<<DDA4) | (0<<DDA3) | (0<<DDA2) | (0<<DDA1) | (0<<DDA0);
// State: Bit7=T Bit6=T Bit5=T Bit4=T Bit3=T Bit2=T Bit1=T Bit0=T
PORTA=(0<<PORTA7) | (0<<PORTA6) | (0<<PORTA5) | (0<<PORTA4) | (0<<PORTA3) | (0<<PORTA2) | (0<<PORTA1) | (0<<PORTA0);

// Port B initialization
// Function: Bit7=Out Bit6=Out Bit5=Out Bit4=Out Bit3=Out Bit2=Out Bit1=Out Bit0=Out
DDRB=(1<<DDB7) | (1<<DDB6) | (1<<DDB5) | (1<<DDB4) | (1<<DDB3) | (1<<DDB2) | (1<<DDB1) | (1<<DDB0);
// State: Bit7=0 Bit6=0 Bit5=0 Bit4=0 Bit3=0 Bit2=0 Bit1=0 Bit0=0
PORTB=(0<<PORTB7) | (0<<PORTB6) | (0<<PORTB5) | (0<<PORTB4) | (0<<PORTB3) | (0<<PORTB2) | (0<<PORTB1) | (0<<PORTB0);

// Port C initialization
// Function: Bit7=In Bit6=In Bit5=In Bit4=In Bit3=In Bit2=In Bit1=In Bit0=In
DDRC=(0<<DDC7) | (0<<DDC6) | (0<<DDC5) | (0<<DDC4) | (0<<DDC3) | (0<<DDC2) | (0<<DDC1) | (0<<DDC0);
// State: Bit7=T Bit6=T Bit5=T Bit4=T Bit3=T Bit2=T Bit1=T Bit0=T
PORTC=(0<<PORTC7) | (0<<PORTC6) | (0<<PORTC5) | (0<<PORTC4) | (0<<PORTC3) | (0<<PORTC2) | (0<<PORTC1) | (0<<PORTC0);

// Port D initialization
// Function: Bit7=In Bit6=In Bit5=In Bit4=In Bit3=In Bit2=In Bit1=In Bit0=In
DDRD=(0<<DDD7) | (0<<DDD6) | (0<<DDD5) | (0<<DDD4) | (0<<DDD3) | (0<<DDD2) | (0<<DDD1) | (0<<DDD0);
// State: Bit7=T Bit6=T Bit5=T Bit4=T Bit3=T Bit2=T Bit1=T Bit0=T
PORTD=(0<<PORTD7) | (0<<PORTD6) | (0<<PORTD5) | (0<<PORTD4) | (0<<PORTD3) | (0<<PORTD2) | (0<<PORTD1) | (0<<PORTD0);

// Timer/Counter 0 initialization
// Clock source: System Clock
// Clock value: Timer 0 Stopped
// Mode: Normal top=0xFF
// OC0 output: Disconnected
TCCR0=(0<<WGM00) | (0<<COM01) | (0<<COM00) | (0<<WGM01) | (0<<CS02) | (0<<CS01) | (0<<CS00);
TCNT0=0x00;
OCR0=0x00;

// Timer/Counter 1 initialization
// Clock source: System Clock
// Clock value: 2500,000 kHz
// Mode: Normal top=0xFFFF
// OC1A output: Clear on compare match
// OC1B output: Disconnected
// Noise Canceler: Off
// Input Capture on Falling Edge
// Timer Period: 26,214 ms
// Output Pulse(s):
// OC1A Period: 26,214 ms
// Timer1 Overflow Interrupt: On
// Input Capture Interrupt: Off
// Compare A Match Interrupt: Off
// Compare B Match Interrupt: Off
TCCR1A=(0<<COM1A1) | (0<<COM1A0) | (0<<COM1B1) | (0<<COM1B0) | (0<<WGM11) | (0<<WGM10);
TCCR1B=(0<<ICNC1) | (0<<ICES1) | (0<<WGM13) | (1<<WGM12) | (0<<CS12) | (1<<CS11) | (0<<CS10);
TCNT1H=0x00;
TCNT1L=0x00;
ICR1H=0x00;
ICR1L=0x00;
OCR1AH=0x02;
OCR1AL=0xEE; //was 0x7F
OCR1BH=0x00;
OCR1BL=0x00;

// Timer/Counter 2 initialization
// Clock source: System Clock
// Clock value: Timer2 Stopped
// Mode: Normal top=0xFF
// OC2 output: Disconnected
ASSR=0<<AS2;
TCCR2=(0<<FOC2) | (0<<COM21) | (0<<COM20) | (0<<WGM21) | (0<<CS22) | (0<<CS21) | (0<<CS20);
TCNT2=0x00;
OCR2=0x00;

// Timer(s)/Counter(s) Interrupt(s) initialization
TIMSK=(0<<OCIE2) | (0<<TOIE2) | (0<<TICIE1) | (1<<OCIE1A) | (0<<OCIE1B) | (0<<TOIE1) | (0<<OCIE0) | (0<<TOIE0);

// External Interrupt(s) initialization
// INT0: Off
// INT1: Off
// INT2: Off
MCUCR=(0<<ISC11) | (0<<ISC10) | (0<<ISC01) | (0<<ISC00);
MCUCSR=(0<<ISC2);

// USART initialization
// Communication Parameters: 8 Data, 1 Stop, No Parity
// USART Receiver: Off
// USART Transmitter: On
// USART Mode: Asynchronous
// USART Baud Rate: 31250
UCSRA=(0<<RXC) | (0<<TXC) | (0<<UDRE) | (0<<FE) | (0<<DOR) | (0<<UPE) | (0<<U2X) | (0<<MPCM);
UCSRB=(0<<RXCIE) | (0<<TXCIE) | (0<<UDRIE) | (0<<RXEN) | (1<<TXEN) | (0<<UCSZ2) | (0<<RXB8) | (0<<TXB8);
UCSRC=(1<<URSEL) | (0<<UMSEL) | (0<<UPM1) | (0<<UPM0) | (0<<USBS) | (1<<UCSZ1) | (1<<UCSZ0) | (0<<UCPOL);
UBRRH=0x00;
UBRRL=0x27;

// Analog Comparator initialization
// Analog Comparator: Off
// The Analog Comparator's positive input is
// connected to the AIN0 pin
// The Analog Comparator's negative input is
// connected to the AIN1 pin
ACSR=(1<<ACD) | (0<<ACBG) | (0<<ACO) | (0<<ACI) | (0<<ACIE) | (0<<ACIC) | (0<<ACIS1) | (0<<ACIS0);
SFIOR=(0<<ACME);

// ADC initialization
// ADC disabled
ADCSRA=(0<<ADEN) | (0<<ADSC) | (0<<ADATE) | (0<<ADIF) | (0<<ADIE) | (0<<ADPS2) | (0<<ADPS1) | (0<<ADPS0);

// SPI initialization
// SPI disabled
SPCR=(0<<SPIE) | (0<<SPE) | (0<<DORD) | (0<<MSTR) | (0<<CPOL) | (0<<CPHA) | (0<<SPR1) | (0<<SPR0);

// TWI initialization
// TWI disabled
TWCR=(0<<TWEA) | (0<<TWSTA) | (0<<TWSTO) | (0<<TWEN) | (0<<TWIE);

// Globally enable interrupts
sei();

PORTB=0b11111110; //Prepare for first scan cycle

while (1)
{
if(nextscan > 0)
{
scan_keyboard();
nextscan=0;
PORTB = 0b11111110; //Prepare for next round
}
//send MiDi data out of the NiDi buffer
while((midiread != midiwrite)&&((UCSRA & (1<<UDRE))>0))
{
senddata = midibuffer[midiread];
UDR = midibuffer[midiread];
midiread++;
if(midiread>bufferlengh){midiread = 0;}
}
}
}
! Be aware the code was only tested in simulator, not in real world.
So there is the possibility of bug's!
 
Last edited:

MichaelaJoy

Active Member
There is not much processing time for other tasks.
I guess it's better to use a dedicated controller for keybed scanning process, when using velocity.
Using of assembler will be a must, when want scanning speed up.
wkrug: True on all accounts.
 

granddad

Active Member
I been time travelling ..year 2005 I last looked at my MiDi interface.. as previous post it had no velocity count (single contact manuals ) , but as I remember mcu had 5ms to scan the 132 note keys .. bear in mind I was restricted to the organs 8085 mpu scan time .. (only some of the time code was actually used scanning the keys) . far as i can read from my code.. scan was interrupt based .. the key buffer returns were sequentially read into a key NEW_MAP after the last scan , code return from ISR .. now NEW_MAP is bit compared with OLD_MAP .. down keys =0 . up keys are 1 , some maths needed to decide if down key is a new down key likewise a up key is a new up key .. changed keys positions are then coded to a note value and placed in the correct channel MiDi byte frame ... and dropped into USART ...then NEW_MAP becomes OLD_MAP .. NEW_MAP is cleared ready for the next scan.. the PIC mcu was running at 20Mhz (5 MIPS) as it will do 125k baud exactly ! .. for the scan reader PIC there was no time for any thing else..
I don't think a microchip controller works much faster.
Some of the MC PIC24's will run at 140Mhz ( 70 MIPS ) up to 1024kb program, loads of peripherals, memory and I/O ports ...

https://www.electro-tech-online.com/threads/diy-pic24fj1024gb610-breakout-board.147203/
 
Last edited:

wkrug

Active Member
PIC mcu was running at 20Mhz (5 MIPS) as it will do 125k baud exactly !
The ATMEGA runs nearly with 20 MIPS at 20 MHz ( XMEGA is faster too ).
With velocity the complete keybed ist to read 127 times faster than with a simple switch.
Ok You can make some optimations e.g. Read Line 2 only when Line 1 is active, but would this be really faster?
So 70 MIPS is not oversized.
 

MichaelaJoy

Active Member
MIDI Keyboard actions have come a long way. Look at the 104 key computer keyboard.
There's not 104 scan lines, but they're arranged in rows/columns.

So, you write out a 3 (or 4) bit scan code (to energize one row) and you read the column data in as a set of bytes or words in a faster CPU.

Nobody uses switch contacts in MIDI keyboards anymore. They all use the rubber cup / conductive foam contact, which is designed to minimize key bounce.
It's even used in computer keyboards, because it simplifies the CPU code necessary for reading the row/column.

Originally, I chose the PIC18F452 to do this project. Then, I was ordering parts for something else I was building and I saw the PIC24F series CPU.
So I pulled the data sheet and went with a chip that was through-hole.

So I bought 2 of them, along with 8mhz crystals for the 24F and 10MHZ crystals for the 18452.
If I hadn't found the TQFN to DIP adapter, I wouldn't have been able to use the 304 at all.

Worse case, I'll go with a faster CPU. or a KA304 as the key scanner and a KA302 to handle MIDI communication. They would communicate using SPI.
That would complicate things,.

Also, I have to scan 3 of the A/D ports for pitch bend, modulation, and MID volume.

Anyhow, we'll know next week when the parts come in. :)

On a side note: There are -no- good examples of how to program the 24FxxKA30x using Assembler with MPLAB-X.
As I progress with this project, the software will become more robust and the source code will be available here. :)
 

rjenkinsgb

Well-Known Member
Most Helpful Member
For info, the pic24 and dspic33 use the same assembly language - there may be more examples available for them.

It does not look like Microchip really expect people to use assembly for these devices; it uses the x16 C compiler as an assembler, which makes things a bit odd possibly.


Re. other posts - I'd advise doing the key scan in a high speed timer "clock" interrupt, read the inputs then update the scan outputs each interrupt.
The time between interrupts allows for any changes to settle at the input side, ready for the next cycle.

That does away with any hold up due to looping through things. The CPU is easily fast enough to do everything you need!
 

MichaelaJoy

Active Member
rjenkinsgb: OMG! That's totally awesome! Thanks for posting that. :)

I'm going to look at it in a bit and finish off my Timer interrupt routine. :)

For info, the pic24 and dspic33 use the same assembly language - there may be more examples available for them.

It does not look like Microchip really expect people to use assembly for these devices; it uses the x16 C compiler as an assembler, which makes things a bit odd possibly.

Re. other posts - I'd advise doing the key scan in a high speed timer "clock" interrupt, read the inputs then update the scan outputs each interrupt.
The time between interrupts allows for any changes to settle at the input side, ready for the next cycle.

That does away with any hold up due to looping through things. The CPU is easily fast enough to do everything you need!
That's what I was thinking of doing. Using a fast timer, and splitting the time between the keyboard scan stuf, the A/D scans and any buttons
that are needed.
 

MichaelaJoy

Active Member
Okay. Here's the power supply schematic. It may change, but here it is for now. :)

KeytarPS.jpg
 

MichaelaJoy

Active Member
granddad: It's not really recommended in the MIDI spec.

Besides...I want to use one supply voltage if I can. When I build the final prototype, I'll strip the unneeded electronics off of the board.
I can't do that yet because the CPU used may change.

So, I guess I'm hedging my bets on the prototype. :)
 

wkrug

Active Member
mj
I think You should make some thoughts about the used Display.
When the 2.3" OLED is too big, You can also use a 0.93" one.
Like this:
https://www.ebay.de/itm/0-96-inch-I2C-IIC-SPI-Serial-128x64-OLED-LCD-Display-SSD1306-Arduino-51-STM32/401336304276?hash=item5d71820294:m:myVYDcG4a519hJqIZoBrHxw:rk:12:pf:0
When I would make such a thing like a keytar i would implement splitting points and a free routing of controllers to the pots.
And free set of a MiDi channel to the splitted keys and additional a transpose function.
Additional some presets to space different settings.
A normal 3*7 segment can only display numbers but no alphanumeric characters.
For normal use You can take a big font and let it look like a 7 Segment Display.
For make Settings it is possible to use up to 8 lines with 20 characters.

The disadvantage is that to transfer much more bytes to the display, but with hardware SPI this should be no problem.

But it's Your decision, of course.
 

MichaelaJoy

Active Member
mj
I think You should make some thoughts about the used Display.
When the 2.3" OLED is too big, You can also use a 0.93" one.
Like this:
https://www.ebay.de/itm/0-96-inch-I2C-IIC-SPI-Serial-128x64-OLED-LCD-Display-SSD1306-Arduino-51-STM32/401336304276?hash=item5d71820294:m:myVYDcG4a519hJqIZoBrHxw:rk:12:pf:0
When I would make such a thing like a keytar i would implement splitting points and a free routing of controllers to the pots.
And free set of a MiDi channel to the splitted keys and additional a transpose function.
Additional some presets to space different settings.
A normal 3*7 segment can only display numbers but no alphanumeric characters.
For normal use You can take a big font and let it look like a 7 Segment Display.
For make Settings it is possible to use up to 8 lines with 20 characters.

The disadvantage is that to transfer much more bytes to the display, but with hardware SPI this should be no problem.

But it's Your decision, of course.
wkrug: I like that display. And that's -exactly- what I have planned. :)

I will need 3 potentiometers. One for pitch bend, one for modulation, and one for volume.
I will need a button for sustain as well as buttons for transpose and for choosing zones.

If I wanted to save on hardware, I can allow the user to switch the keytar into a programming mode
and use the keyboard to choose a MIDI channel and the split from the keybed.

Just about all of the keyboard makers do the same thing to save on hardware costs.
Also, I could add a smaller PIC just for handling the display. I would have to map the settings to a stream of SPI data
and send it to the second PIC. That would offload the display stuff, freeing up the main CPU for handling the keybed / hardware scan.
 

rjenkinsgb

Well-Known Member
Most Helpful Member
Be wary of those small displays - despite saying SPI in the title, the main description says I2C two wire.
That is vastly slower and more CPU intensive than SPI.
 

Latest threads

EE World Online Articles

Loading

 
Top