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 0xbRun the applet | Run the editor (via Webstart)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