Hades logoHades applet banner
MIDI organ pedal controller (with 74138)

applet icon

The image above shows a thumbnail of the interactive Java applet embedded into this page. Unfortunately, your browser is not Java-aware or Java is disabled in the browser preferences. To start the applet, please enable Java and reload this page. (You might have to restart the browser.)

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

Print version | Run this demo in the Hades editor (via Java WebStart)
Usage | FAQ | About | License | Feedback | Tutorial (PDF) | Referenzkarte (PDF, in German)
Impressum http://tams.informatik.uni-hamburg.de/applets/hades/webdemos/72-pic/72-midipedal/midi-organ-pedal.html