| MIDI organ pedal controller (with 74138)
Circuit Description
This applet shows yet another MIDI controller based on the PIC16C84.
Here, the processor is connected to a matrix of multiplexed switches,
reads out the switch status, and generates MIDI note-on or note-off
messages to control a synthesizer.
The design was built to connect to an organ-pedal, where no velocity
information is needed and simple switches are sufficient.
This is also reflected in the choice of the base note of C0
for the least significant switch.
Naturally, the controller supports many closed switches at the same time
to allow for polyphonic play.
The 74138 decoder allows interfacing with up to 64 switches,
but only 32 switches are used in this demonstration
(sufficient to read-out a standard 30/31-note organ pedal).
; TITLE "Organ-Pedal-Controller: 32note MIDI organ pedal"
; SUBTITLE "FNH 21.08.2000"
; Processor 16C84
; Radix DEC
; EXPAND
; include "p16c84.inc"
;
; A simple MIDI controller for a 32-note organ pedal.
; MIDI channel, default velocity, and base note are set a assemble-time.
;
; Also a demonstration for the Pic16C84/Pic16F84 microprocessor and
; MidiReceiver simulation models in the HADES simulation environment,
; see the HADES homepage at
; http://tech-www.informatik.uni-hamburg.de/applets/hades/
;
; 23.08.00 - use channel 0 for testing
; 22.08.00 - restricted version without 74138
; 03.09.99 - minor doc cleanups
; 27.08.99 - implemented SendNoteMessages
; 26.08.99 - first version with full code skeleton
;
; To connect the PIC16C84 to 32 keyboard (pedal) switches, the design is
; heavily multiplexed. The PIC ports A0-A3 drive the keyboard row lines in
; one-hot active-low style, while the keyboard column lines are connected via
; diodes to ports B7-B0. Port A4 is used for MIDI transmission.
; Note that the switch row lines driven by the PIC are active low with
; pullup resistors to high. Therefore, closed switches (depressed pedal keys)
; will generate low (0) values.
;
; The software assumes a linear arrangement of keys, with
; A:1110 -> B0 being the lowest note,
; A:1110 -> B1 a half note higher,
; A:1101 -> B0 the eight-lowest note, and
; A:0111 -> B7 the highest note.
; Set the MIDI_BASE_NOTE to specify the note corresponding to A:1110 -> B0.
; Change the SendNoteOnOff function to provide other key-to-note mappings,
; for example to model a short-octave pedal as found on some historical organs.
;
; To keep total design cost small, simple switches are assumed for the keys.
; Therefore (and also due to PIC memory and performance limitations), the design
; does not support keyboard velocity. The default velocity for MIDI note-on
; messages can be specified via the MIDI_DEFAULT_VELOCITY constant.
;
; Once a key (pedal) is released, the software will generate a corresponding
; MIDI note-off event. This incurs a little overhead vs. the Yamaha practice
; of sending note-on messages with velocity zero (0), but should be more
; portable.
;
; On power-on, the design will send one ALL_NOTES_OFF MIDI messages.
;
; (C) 1999-2000, F.N.Hendrich, hendrich@informatik.uni-hamburg.de
;
;
_ResetVector equ 0x00
_IntVector equ 0x04
;
; assemble time MIDI constants
;
MIDI_BASE_NOTE equ 40 ; C0
MIDI_DEFAULT_CHANNEL equ 0x00 ; channel 0 (2 manuals, 1 pedal)
MIDI_DEFAULT_VELOCITY equ 0x64 ; MIDI spec default, if no velocity
MIDI_NOTE_ON equ 0x90 ; add channel
MIDI_NOTE_OFF equ 0x80 ; add channel
MIDI_CONTROL_CHANGE equ 0xb0 ; add channel
MIDI_PROGRAM_CHANGE equ 0xc0 ; add channel
;
; for all-notes-off, send 0xb 0x78 0x00
;
;
; MIDI transmission stuff (predefined for 4MHz clock input)
;
;_ClkIn equ 4000000
;_ClkOut set (_ClkIn >> 2)
;
;_Delay31250 set ( ((_ClkOut / 31250) - 13) / 3)
;_DataBits set 8 ; 8 bit data
;_StopBits set 1 ; 1 Stop Bit
_ClkIn equ 4000000
_ClkOut equ 1000000
; _Delay31250 set ( ((_ClkOut / 31250) - 13) / 3)
_Delay31250 equ 5
_DataBits equ 8 ; 8 bit data
_StopBits equ 1 ; 1 Stop Bit
;
; keyboard switch sampling stuff
;
_RowAccessPreDelay equ 10 ; value*3 = cycles in delay loop
;
; registers to hold current and last keyboard switch states. The bit
; corresponding to key #i is set when that key switch is closed.
;
_Last0700 equ 0x20
_Last1508 equ 0x21
_Last2316 equ 0x22
_Last3124 equ 0x23
_Switches0700 equ 0x24
_Switches1508 equ 0x25
_Switches2316 equ 0x26
_Switches3124 equ 0x27
_TXPort equ PORTA
_TXPin equ 4 ; the RS232 output pin
_TXReg equ 0x10 ; the RS232 transmit register
_TXDelayCounter equ 0x11 ; delay counter reg
_TXBitCounter equ 0x12 ; counter data bits
_MidiChannelReg equ 0x13 ; 4bit MIDI channel 0 .. 15
_RowAccessCounter equ 0x14 ; delay counter for row access pre delay
_SwitchIndexReg equ 0x15 ; index to current row switch register
; (0x24 .. 0x27)
_RowCounter equ 0x16 ; row index in SendNoteMessages
_BitCounter equ 0x17 ; bit index in SendNoteMessages
_BitMask equ 0x18 ; bit mask in SendNoteMessage,
; 10000000, 01000000, .. 00000001
_Difference equ 0x19 ; difference between current and last
; key state
_MidiNote equ 0x1a ; current Midi note index
;
;****************************************************************************
;****************************************************************************
;****************************************************************************
;
; code starts here
;
ORG _ResetVector
goto Start
;
; we should NEVER arrive here, because interrupts (and WD-timer) should
; be disabled...
;
ORG _IntVector
Interrupt:
goto Interrupt
Start:
movlw 0x00
movf INTCON,1 ; disable all interrupts
call InitPorts
call InitKeyRegs
call SendAllNotesOff
SampleLoop:
call SaveLastKeys
call SampleKeys
call SendNoteMessages
goto SampleLoop
;
; InitPorts:
; initialize the _TXPin pin PA4 and pins PA2..PA0 as outputs and PB7 .. PB0
; as inputs. Currently, the unused pin PA3 is set as an output, too.
;
InitPorts:
bsf STATUS, RP0 ; access port direction regs
movlw 0xE0 ; all five bits output
movwf TRISA
movlw 0xFF ; all eight bits input
movwf TRISB
bcf STATUS, RP0 ; access data regs
movlw 0x1F ;
movwf PORTA ; set PA4 (TX) high, all row outputs high (inactive)
return
;
; InitKeyRegs:
; initialize the keyboard switch registers to all-zero (all switches open).
;
InitKeyRegs:
movlw 0xFF ; all bits 1 means all switches open (pullups)
movwf _Last0700 ;
movwf _Last1508
movwf _Last2316
movwf _Last3124
movwf _Switches0700
movwf _Switches1508
movwf _Switches2316
movwf _Switches3124
movlw MIDI_DEFAULT_CHANNEL
movwf _MidiChannelReg ; initialize default MIDI channel
return
;
; SaveLastKeys:
; save the switch states from the last sample iteration into the 'last'
; registers.
;
SaveLastKeys:
movf _Switches0700,0
movwf _Last0700
movf _Switches1508,0
movwf _Last1508
movf _Switches2316,0
movwf _Last2316
movf _Switches3124,0
movwf _Last3124
return
;
; SampleKeys:
; generate the decoder inputs, wait a little, then sample the addressed
; row of keyboard switches. Currently, we use decoder outputs 00, 01, 10, 11
; to access rows 0 .. 3 of the keyboard matrix.
;
;
SampleKeys:
call AddressFirstRow
call RowAccessWait
call SampleRow
call AddressSecondRow
call RowAccessWait
call SampleRow
call AddressThirdRow
call RowAccessWait
call SampleRow
call AddressFourthRow
call RowAccessWait
call SampleRow
call DisableAllRows
return
;
; AddressFirstRow:
; set the port A bits to decode and access the first row (0) of the
; keyboard switches.
;
AddressFirstRow:
movlw b'00011110'
movwf PORTA ; PA4 (_TX) high, lowest row driver low
movlw _Switches0700
movwf _SwitchIndexReg
return
AddressSecondRow:
movlw b'00011101'
movwf PORTA ; PA4 (_TX) high, lowest row driver low
incf _SwitchIndexReg,1
return
AddressThirdRow:
movlw b'00011011'
movwf PORTA ; PA4 (_TX) high, lowest row driver low
incf _SwitchIndexReg,1
return
AddressFourthRow:
movlw b'00010111'
movwf PORTA ; PA4 (_TX) high, lowest row driver low
incf _SwitchIndexReg,1
return
DisableAllRows:
movlw b'00011111'
movwf PORTA
return
;
; AddressNextRow:
; set the port A bits to decode and access the next row of keyboard switches.
; The following trivial implementation only works for the first 3 calls.
;
AddressNextRow:
incf PORTA,1
incf _SwitchIndexReg,1
return
;
; RowAccessWait:
; simple delay loop to allow the keyboard switch output lines to settle.
; Set the _RowAccessPreDelay constant to change the delay value.
;
RowAccessWait:
clrwdt
movlw _RowAccessPreDelay
movwf _RowAccessCounter
RowAccessLoop:
decfsz _RowAccessCounter,1
goto RowAccessLoop
return
;
; SampleRow:
; sample the state of the current row keyboard switches from PB7 .. 0
; and store into the current switch register
;
SampleRow:
movf _SwitchIndexReg,0
movwf FSR
movf PORTB,0
movwf 0x2f
movwf INDF ; put W contents into reg pointed to by FSR
return
;
; SendAllNotesOff:
; transmit a MIDI all-notes-off panic message. At the moment, this is only
; used at startup, but it might be useful to include a command to generate
; this message afterwards to avoid/clear hanging notes.
;
SendAllNotesOff:
movlw 0xb0
addwf _MidiChannelReg,0
call SendOneChar
movlw 0x78
call SendOneChar
movlw 0x00
call SendOneChar
return
;
; SendNoteMessage:
; check for each keyboard switch whether the current sampled state differs
; from the last state (sampled during the last iteration). If so, calculate
; the corresponding MIDI note number and generate a note-on (switch closed)
; or note-off (switch open) MIDI message.
;
; Note that we want our row loop to run from n_rows to 0 (inclusive), the
; bit loop from n_bits to 0 (inclusive). Due to the semantics of decfsz,
; we have to increment/decrement the counters a little. Crazy:-(
;
SendNoteMessages:
; initialize the RowLoop counter with n_rows
movlw 0x03 ; last (highest) row is currently 3
movwf _RowCounter ;
incf _RowCounter,1
movlw MIDI_BASE_NOTE ; initialize note index from base note
addlw 32 ; 4 rows by 8 keys per row (TOP NOTE)
movwf _MidiNote ; we start with the highest note!
RowLoop:
decf _RowCounter,1 ; _RowCounter juggling, see function comment
; calculate the difference (XOR) of current
; and last state regs
movlw _Last0700 ; lowest last reg
addwf _RowCounter,0 ; index to LastXXXX reg for current row
movwf FSR ; move to index reg
movf INDF,0 ; move LastXXX reg to W
movwf _Difference ; and then to _Difference
movlw _Switches0700
addwf _RowCounter,0
movwf FSR
movf INDF,0 ; move CurrentXXX reg to W
xorwf _Difference,1 ; get the bit difference:
; _Difference := _Difference (XOR) W
; ; debugging a la printf():
; movlw 0xFF
; call SendOneChar
; movf _Difference,0
; call SendOneChar
; movlw 0x00
; call SendOneChar
movlw 0x80 ; 1000 0000 (mask with MSB set)
movwf _BitMask ; mask to check MSB first
movlw 0x07 ; initialize bit counter
movwf _BitCounter ; we have 8 bits (7..0) per key row
incf _BitCounter,1 ; see function comment
BitLoop:
decf _BitCounter,1
; ; debugging
; movf _BitMask,0
; call SendOneChar
decf _MidiNote,1 ; decrement current note index
movf _Difference,0 ; move current/last differences to W
andwf _BitMask,0 ; and bit mask to W
btfss STATUS,Z ; W Zero? no key changes, current==last
call SendNoteOnOff ; key changed, construct note message
addlw 0x00 ; reset carry and stuff to get rrf working
rrf _BitMask ; rotate bit mask right one bit
; (this needs the C-flag cleared)
incf _BitCounter,1 ; see function comment
decfsz _BitCounter,1 ; another bit iteration?
goto BitLoop
; done with all bits of one row, next row now
incf _RowCounter,1 ; see function comment
decfsz _RowCounter,1 ; another row iteration?
goto RowLoop
return ; we're ready now
;
; SendNoteOnOff
; called, when the current state of a key differs from its last value;
; check whether we need a note on (key switch closed) or note off (opened)
; message and dispatch to SendNoteOn or SendNoteOff
;
; Note that FSR still points to the _CurrentXXX register for the current row
; when this function is called.
;
SendNoteOnOff
movf INDF,0 ; move current row key states to W
andwf _BitMask,0 ; and bit mask to W
btfss STATUS,Z ; W Zero? then key state is on (switch to GND)
goto SendNoteOff
goto SendNoteOn
; should never arrive here
return
;
; SendNoteOn
; construct and transmit a MIDI note on message for _MidiChannel, note
; number from MIDI_BASE_NOTE + 8*_RowCounter + _BitCounter, with velocity
; MIDI_DEFAULT_VELOCITY.
;
SendNoteOn
movlw 0x90 ; basic note on opcode
addwf _MidiChannelReg,0 ; add channel to W
call SendOneChar
movf _MidiNote,0
call SendOneChar
movlw MIDI_DEFAULT_VELOCITY
call SendOneChar
return
SendNoteOff
movlw 0x80 ; basic note on opcode
addwf _MidiChannelReg,0 ; add channel to W
call SendOneChar
movf _MidiNote,0
call SendOneChar
movlw MIDI_DEFAULT_VELOCITY
call SendOneChar
return
;
; SendOneChar:
; the actual RS232 transmission routine, half-duplex, no-flow-control.
; See AN510 for an explanation
;
SendOneChar:
movwf _TXReg ; move W (char to send) to _TXReg
;bsf PORTA,0 ; debug sync impulse on port A0
;bcf PORTA,0
movlw 0x08
movwf _TXBitCounter ; send 8 bits
bcf _TXPort,_TXPin ; set _TXPin for start bit
nop
nop
nop
nop
call BitDelay
SendNextBit:
bcf STATUS,C
rrf _TXReg,1 ; rotate TXReg
btfsc STATUS,C
goto _setTX
_clearTX:
nop ; to get equal set/clear times
bcf _TXPort,_TXPin
goto _readyTX
_setTX:
bsf _TXPort,_TXPin
goto _readyTX
_readyTX:
call BitDelay ; was: DelayX !!!
decfsz _TXBitCounter,1 ; decrement bit counter (8..0)
goto SendNextBit
nop
nop
nop
nop
nop
bsf _TXPort,_TXPin ; send stop bit
call BitDelay
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
call BitDelay ; second stop bit to make sure..
return
BitDelay:
movlw _Delay31250 ; move baud delay constant to W
movwf _TXDelayCounter ; initialize delay counter
BitDelayLoop:
decfsz _TXDelayCounter,1 ; decrement delay counter
goto BitDelayLoop
return
END
| | |