MIDI
MIDI protocol
Kind: Serial
Frequency: 31.25 kHz
Logic levels: "1" = 5V ; "0" = 0V
High on idle, positive logic
MIDI Byte structure:
1 start bit (low)
8 message bits (least significant bit is
transmitted first)
1 stop bit (high)
- - - - - - - - -
Bits total: 10
Time used per byte: 320 us
Information on MIDI messages can be found
here...
PIC microcontrollers algorithms for MIDI receive
Since low end PIC micros don't have onboard UARTs, handling the asynchronous serial ports/MIDI have to be done with software implemented shift register. The shift register has to shift its data on each bit; for MIDI that means every 32 us. Getting the timing right can be done with three different methods, where each has its own advantages and disadvantages:
1.) Continuous looping on 32us
This method is the most time
efficient of all three, but it is quite demanding to implement since you
have to trace every program branch and calculate the time that is used
in it. Further on, on MIDI receive there has to be synchronization procedure
at start bit to prevent the program of faulty bit reads due to MIDI input's
optocoupler rise/fall times. Still, it is the most useful in less demanding
systems (check out my DINer project's source code;
that project could not run from 4MHz if I'd use some other method).
2.) Interrupt on TMR0 overflow
This method is similar to
the one above; the difftakes some more time due to various register reinitialization
and the fact that in this case you always start from the same point and
you have to figure out what ierence is that PIC in this case handles the
timing by itself, so you don't have to worry about it. On the other hand
this method s the current status of the MIDI lines (idle, start bit, one
of message bits, stop bit). It also suffers from start bit synchronization
problem, which in this case can't be corrected. It is useful for high demanding
systems, with simultaneous receving and transmiting.
3.) Interrupt on RB0 pin change
This method is suitable for
receiving only. MIDI input has to be connected to RB0 (pin 0 of PortB i.e.
pin 6 on DIL chip). It has best synchronization, while it is least time
effective (microcontroller can return from interrupt only when it has red
the whole byte). Like the first method, this is useful in less demanding
systems.
Code examples
Note: Below are just procedures for MIDI byte
receiving/transmiting without any variable declarations and register initialization.
To make procedures work you'll have to add that yourself. Also, instead
of setting the correct timing in the code I have just put CALL DELAY
commands in the code. This routines are to be considered not to be the
same hence the same name. Important lines (commands) have "[number]" in
their comments; I'll refer to those numbers in additional explanations.
If you are not familiar with
commands, please download a datasheet for PIC16F84 from
Microchip's
homepage and check the Instruction set chapter.
This chunks of code are free for use in non-commercial
projects. If you'll use them, please mail
me so I'll know writing this wasn't just a waste of time. Thanks!
MIDI receiving examples
Continuous looping example (1. variation)
;----------------
;START OF CODE
;----------------
MIDI_BYTE_RECEIVE
IDLE
; Program loops here when MIDI is at rest
BTFSS MIDI_INPUT ; [1] Check for low state of MIDI
line, which means start bit
GOTO START_BIT ; Start bit in progress, let's move
to label START_BIT
CALL DELAY ; Basically
you can do some tasks instead of delay here,
; just make sure you don't exceed the 32us with them.
; If you don't have any tasks to perform, you can also
; delete the delay call and use the code as is,
; which will give better start bit synchronization
GOTO IDLE ; Return
to IDLE 32us after the previous reading
;
START_BIT
MOVLW 8
; Initialize MIDI_BIT variable which represents
MOVWF MIDI_BIT ; the number of bits
that you have to receive
CALL DELAY ; [2] Delay
to get the timing correct
READ_MIDI
; This is the fun part. Shift register kicks in here
BSF CARRY ; Assume
the MIDI line will be high for next command
BTFSS MIDI_INPUT ; [3] Is it really high?
BCF CARRY ; If the
MIDI line was low we correct our assumption
RRF MIDI_BYTE,F ; This line represents shift
register. It moves the
; carry bit to current MSB of MIDI_BYTE and shifts
; all bits to lower place; LSB shifts to carry and
; gets lost with next READ_MIDI cycle
; The first bit which gets to MIDI_BYTE becomes LSB
; after 8 READ_MIDI cycles, which is correct, since
; MIDI sends LSB first.
DECF MIDI_BIT,F ; Decrement MIDI_BIT variable
BTFSC ZERO ; if MIDI_BIT
is zero then the stop bit will follow
GOTO BYTE_END ; So the MIDI_BIT is zero;
jump from READ_MIDI follows
CALL DELAY ; [4] Another
delay or task for timing adjustment
GOTO READ_MIDI ; Go back again
BYTE_END
; Stop bit is in progress and this is ideal time to
CALL TASK ; do something
with MIDI byte which we red
GOTO IDLE ; Return
to IDLE
; It isn't necessary to have correct timing here, since
; MIDI line should be high anyway, just make sure that
; task doesn't exceed 32us
;------------
;END OF CODE
;------------
Notes:
Make sure that there is exactly
32us between [1] and [3] (adjust timing with lenght of [2])and also that
READ_MIDI section is 32us long (adjust with [4]).
Timing calculation equation (universal for any
asynchronous serial protocol):
Number_of_machine_cycles =
XTAL_frequency [Hz] / (4*Baud_rate [bit/s])
where Baud_rate=31250
Common quartz frequencies vs. number of machine
cycles:
4 MHz -> 32
8 MHz -> 64
10 MHz -> 80 etc.
Continuous looping example (2.
variation)
This
variation is used in my DINer project. The code
is similar to the one above, there is just a bit of alteration to gain
some machine cycles for the BYTE_END section, which allows more
time consuming tasks there. I will only write a READ_MIDI section
and MIDI_BIT variable initialization in START_BIT section
here, other things are the same:
;----------------
;START OF CODE
;----------------
START_BIT
MOVLW 9
; The value is increased since MIDI_BIT is decremented
MOVWF MIDI_BIT ; on start of READ_MIDI
section
;... see 1. variation
READ_MIDI
DECF MIDI_BIT,F ; Decrement MIDI_BIT
GOTO BYTE_END ; Go to BYTE_END section
BSF CARRY ; Assume
the MIDI line will be high for next command
BTFSS MIDI_INPUT ; Is it really high?
BCF CARRY ; If the
MIDI line was low we correct our assumption
RRF MIDI_BYTE,F ; This line represents shift
register. It moves the
; carry bit to current MSB of MIDI_BYTE and shifts
; all bits to lower place; LSB shifts to carry and
; gets lost with next READ_MIDI cycle
; The first bit which gets to MIDI_BYTE becomes LSB
; after 8 READ_MIDI cycles, which is correct, since
; MIDI sends LSB first.
CALL DELAY ; Adjust timing
GOTO READ_MIDI ; Return
;------------
;END OF CODE
;------------
TMR0 interrupt example
I'll assume this is the only
enabled interrupt (if not, check the datasheets for all adjustments).
;----------------
;START OF CODE
;----------------
ORG H'0004'
INTERRUPT_VECTOR
; Program jumps here when the interrupt occurs
BCF INTCON,T0IF ; Clear TMR0 interrupt flag
MOVLW TMR0_OFFSET ; [1] Add TMR0_OFFSET (constant) to
ADDWF TMR0,F ;
TMR0 value (see note below)
MIDI_BIT_RECEIVE
; Here we read one MIDI bit on each interrupt
MOVF MIDI_BIT,F ; Check if MIDI_BIT is zero; if
it is, that
BTFSC ZERO ;
means that MIDI line is idle (high)
GOTO MIDI_IN_START? ; Check if that is true...
;
; We get here if there is a byte reading in progress
DECF MIDI_BIT,F ; Decrement bit counter
BTFSC ZERO ; Check for
byte end
GOTO BYTE_END ; MIDI_BIT was zero, so jump
to BYTE_END
;
BCF CARRY ; This
part is the same as the one described at
; continuous loop method
BTFSC MIDI_INPUT ; [2]...
BSF CARRY ;
...
RLF MIDI_BYTE,F ;
RETFIE
; Return from interrupt
MIDI_IN_START?
; Check for start bit
CLRW
; Assume MIDI line is high - try to leave MIDI_BIT on zero
BTFSS MIDI_INPUT ; [3] Check if MIDI is low
MOVLW 9
; There is start bit, now set MIDI_BIT to 9
MOVWF MIDI_BIT ;
RETFIE
; Return from interrupt
BYTE_END
CALL TASK ; Do something
with MIDI byte, or just make indication
; that MIDI byte was received (e.g. set some specific bit)
; The task must not exceed 32us
RETFIE
; Return from interrupt
;------------
;END OF CODE
;------------
Notes:
Make sure that MIDI line is
red at the exactly the same time in both cases - in the MIDI_BIT_RECEIVE
section (line [2]) or in MIDI_IN_START? section (line [3]) - depeneding
on the start of the interrupt.
TMR0_OFFSET constant calculation:
TMR_OFFSET = 258
- Number_of_machine_clocks
(see continuous looping example,1. variation)
You may wonder why 258 is there, it should be 256. But if you check the timer write chart in datasheets, notice that the PIC takes two machine clocks to actually write the new value into timer.
You can also use command 'MOVLW -Number_of_machine_clocks+2' (note the minus in front!) instead of 'MOVLW TMR0_OFFSET'.
RB0 change interrupt example
I'll assume this is the only
enabled interrupt (if not, check the datasheets for all adjustments).
;----------------
;START OF CODE
;----------------
ORG H'0004'
INTERRUPT_VECTOR
; Program jumps here when the interrupt occurs
BCF INTCON,INTF ; Clear RB0 interrupt flag
MOVLW 8
; Initialize MIDI_BIT variable which represents
MOVWF MIDI_BIT ; the number of bits
that you have to receive
CALL DELAY ; Delay for
a bit more than 32us, to make sure
; you won't get misreading because of MIDI line
; rise/fall times
; Further code is the same as at continuous
looping (with all the notes that go
; with it), just instead of "GOTO IDLE" at
the BYTE_END section is 'RETFIE'.
;Also both variation from there are possible
to implement. Here is only first one shown...
READ_MIDI
; This is the fun part. Shift register kicks in here
BSF CARRY ; Assume
the MIDI line will be high for next command
BTFSS MIDI_INPUT ; Is it really high?
BCF CARRY ; If the
MIDI line was low we correct our assumption
RRF MIDI_BYTE,F ; This line represents shift
register. It moves the
; carry bit to current MSB of MIDI_BYTE and shifts
; all bits to lower place; LSB shifts to carry and
; gets lost with next READ_MIDI cycle
; The first bit which gets to MIDI_BYTE becomes LSB
; after 8 READ_MIDI cycles, which is correct, since
; MIDI sends LSB first.
DECF MIDI_BIT,F ; Decrement MIDI_BIT variable
BTFSC ZERO ; if MIDI_BIT
is zero then the stop bit will follow
GOTO BYTE_END ; So the MIDI_BIT is zero;
jump from READ_MIDI follows
CALL DELAY ; Another delay
or task for timing adjustment
GOTO READ_MIDI ; Go back again
BYTE_END
; Stop bit is in progress and this is ideal time to
CALL TASK ; do something
with MIDI byte which we red
RETFIE
; Return from interrupt (NOTE THIS!!!)
; It isn't necessary to have correct timing here, since
; MIDI line should be high anyway, just make sure that
; task doesn't exceed 32us
MIDI transmiting examples
Coming soon...