;*************************************************************************** ; ** ; C0001-01.ASM MIDI Instrument Controller firmware * * ; Copyright (C)2007 Jonathan Clift * * ; ***** ; This program is free software; you can redistribute it and/or modify * ; it under the terms of the GNU General Public License as published by * ; the Free Software Foundation; either version 2 of the License, or * ; (at your option) any later version. * ; * ; This program is distributed in the hope that it will be useful, * ; but WITHOUT ANY WARRANTY; without even the implied warranty of * ; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * ; GNU General Public License for more details. * ; * ; You should have received a copy of the GNU General Public License * ; along with this program; if not, write to the Free Software * ; Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * ; * ; Source and other supporting documentation can be found here: * ; http://jonathan.curiositycollective.org * ; * ;******************************************************************************* ;* C0001-01.ASM PIC16F873A PCB CC01-01 16MHz clock * ;* * ;* MIDI Instrument Controller firmware * ;* For Curiosity Collective 'Robot Orchestra' project * ;* * ;* code to: * ;* - receive MIDI commands * ;* - activate port B outputs (darlington driver) * ;* either directly using note-on and note-off messages * ;* or for a time dependent on velocity value after note-on * ;* or for a fixed time (link-selectable) * ;* - activate power FETS * ;* either as above * ;* or by controlling PWM with note value or velocity value * ;* * ;* 29th July 2007 Jonathan Clift * ;******************************************************************* ;* Ver Build Date Notes * ;* 01 01 29-07-07 First go - no MOSFET outputs for moment, * ;* just the 8 darlington outputs * ;* 02 13-09-07 Problem with running status fixed * ;* 03 23-09-07 Use of link (PA4) to determine output mode * ;* Link off: straight note-on/note-off * ;* Link on: output on-time defined by velocity * ;******************************************************************* ;* * ;* Configuration settings (for programming): * ;* Oscillator: HS * ;* Watchdog Timer: On * ;* Power Up Timer: On * ;* Code Protect: All * ;* Brown Out Detect: On * ;* Low Voltage Program: Disabled * ;* Data EE Protect: Off * ;* Flash Program Write: Enabled * ;* Background Debug: Disabled * ;* * ;******************************************************************* ;* Notes: * ;******************************************************************* ; ; My general conventions: ; ; 1) RP0 and RP1 both normally = 0 (ie bank 0 selected) ; After accessing other banks I immediately reset these page ; select bits. Slightly less efficient code but more likely to ; work first off. Can always optimise later if time or space is an issue. ; 2) IRP normally = 0. So indirect addressing ; operates with banks 0 & 1 by default. (Same reasons as above.) ; 3) default radix is decimal (for hex prefix with '0x') ; I just find this most natural. ; radix dec ; ; specify processor type and include the standard Microchip ; header file for the part ; LIST P=PIC16F873A INCLUDE "P16F873A.INC" ; ; FLAGS bits: ; T2FLAG SET 0 ;Timer 2 flag (signals interrupt has occurred) MIDIRX SET 1 ;MIDI receive flag (indicates note and velocity waiting) TICK SET 2 ;tick flag (prescaler done) ; ; Constants: ; MBASE SET 60 ;MIDI note for bottom of the group of eight outputs (60=middle C) BRATE SET 31 ;31,250 baud (with 16 MHz clock - see datasheet for how this value is derived) PSCALE SET 40 ;this is number of timer 2 periods for each output timer tick (40 = approx 10mS) OUTTIME SET 10 ;output time in multiples of PSCALE period (for when links set to fixed output time) ; ;**************************************************** ;* Variables (register usage) * ;* * ;**************************************************** ; ; --- Bank 0 ; ; general ; LOOPCNT EQU 0x20 FLAGS EQU 0x21 ;various flags OUTBYTE EQU 0x22 BITSW EQU 0x23 PRETICK EQU 0x24 ;prescale counter for output timers ; ; serial ; RSTATE EQU 0x30 ;receive state RTEMP EQU 0x31 RCCHAR EQU 0x32 ;received character ; ; MIDI ; MIDCOM EQU 0x40 ;command MIDNOTE EQU 0x41 ;note value MIDVELO EQU 0x42 ;velocity value ; ; output timers - one for each output ; TIMER1 EQU 0x50 TIMER2 EQU 0x51 TIMER3 EQU 0x52 TIMER4 EQU 0x53 TIMER5 EQU 0x54 TIMER6 EQU 0x55 TIMER7 EQU 0x56 TIMER8 EQU 0x57 ; ; context save during interrupts ; (makes use of 0x70-0x7F being common to all banks ; so don't move outside this area) ; ITEMP EQU 0x7B WTEMP EQU 0x7C STEMP EQU 0x7D PTEMP EQU 0x7E FTEMP EQU 0x7F ; ;************************************************* ;* Power up or watchdog entry point * ;* (this is fixed address - don't change origin) * ;************************************************* ; ORG 0x0000 RESET GOTO INIT ;jump to initialisation code ; ;****************************************************** ;* Interrupt routine * ;* (this is fixed address - don't change origin) * ;* * ;* Saves and restores state. Some of this might not * ;* be necessary, but I find it safer to start like * ;* this and then reduce to what's essential if * ;* pushed for processor time. Note this is NOT being * ;* saved to a stack, so don't re-enable * ;* interrupts part way through - leave that to * ;* the RETFIE instruction at the end. The swap * ;* instructions are necessary to avoid the problem of * ;* the status register getting corrupted - there's a * ;* Microchip application note that explains this in * ;* more detail. * ;****************************************************** ; ORG 0x0004 ; ; --- save state ; INT MOVWF WTEMP SWAPF STATUS,W CLRF STATUS MOVWF STEMP MOVF PCLATH,W MOVWF PTEMP CLRF PCLATH MOVF FSR,W MOVWF FTEMP ; ; --- determine source of interrupt ; BTFSC PIR1,TMR2IF GOTO T2INT ;timer 2 interrupt ; BTFSC PIR1,RCIF GOTO SRXINT ;serial rx interrupt ; ; BTFSC PIR1,TXIF ;serial tx interrupt ; GOTO STXINT ;(MUST be last in list) ; ; --- restore state ; --- and return from interrupt ; IRETURN MOVF FTEMP,W MOVWF FSR MOVF PTEMP,W MOVWF PCLATH SWAPF STEMP,W MOVWF STATUS SWAPF WTEMP,F SWAPF WTEMP,W ; RETFIE ;return from interrupt ; ; ****************************************** ; --- handle timer 2 interrupt ; --- (interrupts every 256uS) ; T2INT ; BSF FLAGS,T2FLAG ;set timer 2 flag ; ; deal with prescaler for output counters ; DECF PRETICK,F ;decrement counter BTFSS STATUS,Z ;down to zero? GOTO T2INT1 ;no, don't need to do anything else ; MOVLW PSCALE ;yes, get prescaler value MOVWF PRETICK ;and use to preset counter ; BSF FLAGS,TICK ;set tick flag (signal to main routine) ; T2INT1 BCF PIR1,TMR2IF ;reset timer 2 interrupt bit GOTO IRETURN ; ; ****************************************** ; ; --- handle serial rx interrupt ; ; Start by dealing with any comms errors and getting the received character. ; The receive character is saved so that we can do repeated tests ; on it: we can't go back to RCREG for this because this register is ; really the end of a FIFO and we won't get the same character the ; second time. ; SRXINT BTFSC RCSTA,OERR ;over-run error? GOTO SRX110 ;yes, jump BTFSC RCSTA,FERR ;framing error (possible break)? GOTO SRX100 ;yes, jump MOVF RCREG,W ;get receive character MOVWF RCCHAR ;and save temporarily ; ; --- test receive state ; --- This is chain of tests on RSTATE, by putting it in a temporary register and then ; --- repeatedly decrementing and testing for zero. ; --- (If there are a lot of states then it's much more efficient to make this a ; --- computed jump into a table of GOTO instructions, but such tables need care if they ; --- cross a page boundary) ; MOVF RSTATE,W ;get the receive state MOVWF RTEMP ;put in temp register for testing BTFSS STATUS,Z ;receive state = 0? GOTO SRX20 ;no, jump ; ; --- state 0 waiting for command ; ; this will either be ; a) a channel command (for instance, note-on or note-off) ; b) a system message (which we'll just cheerfully ignore for now) ; SRX10 BTFSS RCCHAR,7 ;is bit 7 high (status byte)? GOTO IRETURN ;no - not command so ignore ; MOVF RCCHAR,W ;get receieved char ANDLW 0xF0 ;isolate top 4 bits XORLW 0xF0 ;test if top nibble = 0xF BTFSC STATUS,Z ;if not skip (channel message) GOTO IRETURN ;if is, system message (ignore for now) ; SRX11 MOVF PORTA,W ;read bitswitch ANDLW 0x0F ;select appropriate bits MOVWF BITSW ;save ; MOVF RCCHAR,W ANDLW 0x0F ;isolate bottom nibble (channel) SUBWF BITSW,W ;same as bitswitch BTFSS STATUS,Z ;yes, skip GOTO IRETURN ;no, finish ; MOVF RCCHAR,W MOVWF MIDCOM MOVLW 1 ;set state to 1 MOVWF RSTATE GOTO IRETURN ; ; --- test for state 1 ; SRX20 DECF RTEMP,F ; BTFSS STATUS,Z ;state = 1? GOTO SRX30 ;no, jump ; ; --- state 1 waiting for note value ; ; first, check in case it's a command ; (It's permissable for single-byte system commands to come part ; way through a channel command. Also, there might have been a problem ; on the comms with a dropped character and this will help get us back in step.) ; BTFSS RCCHAR,7 ;is bit 7 high (status byte)? GOTO SRX21 ;no, skip next bit ; MOVF RCCHAR,W ;get received char ANDLW 0xF0 ;isolate top 4 bits XORLW 0xF0 ;test if top nibble = 0xF BTFSC STATUS,Z ;if not skip (channel message) GOTO IRETURN ;if is, system message (ignore for now) MOVLW 0 ;set state to 0 MOVWF RSTATE GOTO SRX11 ;and treat as if were state 0 all along ; ; not command byte, so must be note value ; (we just temporarily save it until the velocity comes in) ; SRX21 MOVF RCCHAR,W ;save note MOVWF MIDNOTE ; MOVLW 2 ;set next state to 2 MOVWF RSTATE ; GOTO IRETURN ; ; --- test for state 2 ; SRX30 DECF RTEMP,F ;get the receive state BTFSS STATUS,Z ;state = 0? GOTO SRX40 ; ; --- state 2 waiting for velocity ; ; again, we'll do the command check, as we did for the note value ; BTFSS RCCHAR,7 ;is bit 7 high (status byte)? GOTO SRX31 ;no, skip next bit ; MOVF RCCHAR,W ;get received char ANDLW 0xF0 ;isolate top 4 bits XORLW 0xF0 ;test if top nibble = 0xF BTFSC STATUS,Z ;if not skip (channel message) GOTO IRETURN ;if is, system message (ignore for now) MOVLW 0 ;set state to 0 MOVWF RSTATE GOTO SRX11 ;and treat as if were state 0 all along ; ; not command byte, so must be the velocity value ; SRX31 MOVF RCCHAR,W MOVWF MIDVELO ;save the velocity ; MOVLW 3 ;set next state to 0 MOVWF RSTATE ; BSF FLAGS,MIDIRX ;set MIDI receive flag ; GOTO IRETURN ; ; --- test for state 3 ; SRX40 DECF RTEMP,F ;get the receive state BTFSS STATUS,Z ;state = 0? GOTO SRX50 ; ; --- state 3 waiting for command, or another note value (running status) ; ; this will either be ; a) a channel command (for instance, note-on or note-off) ; b) a system message (which we'll ignore for now) ; c) a new note value (so-called 'running status') ; BTFSS RCCHAR,7 ;is bit 7 high (status byte)? GOTO SRX21 ;no - treat as though status=1 (wait for note) ; MOVF RCCHAR,W ;get receieved char ANDLW 0xF0 ;isolate top 4 bits XORLW 0xF0 ;test if top nibble = 0xF BTFSC STATUS,Z ;if not skip (channel message) GOTO IRETURN ;if is, system message (ignore for now) ; MOVLW 0 ;set state to 0 MOVWF RSTATE GOTO SRX11 ;and do rest as though state 0 ; ; --- illegal state value ; (just in case this ideal virtual world has any flaws ; in its perfection) ; SRX50 MOVLW 0 ;set next state to 0 MOVWF RSTATE ; GOTO IRETURN ; ; --- error processing ; ; framing error (might be break) ; (This code is just a skeleton. If you were doing this properly, you ; might want to turn all the outputs off while the break condition ; persisted.) ; SRX100 MOVF RCREG,W ;get receive character (junk) BTFSC STATUS,Z ;zero character? GOTO SRX101 ;yes (likely break condition), jump GOTO SRX40 ;ignore for moment ; SRX101 GOTO SRX40 ;ignore for moment ; ; overflow error ; SRX110 BCF RCSTA,CREN ;reset serial receive BSF RCSTA,CREN GOTO SRX40 ; ;************************************************* ;* Initialisation code for start up * ;* (falls into main routine at end) * ;************************************************* ; INIT ; ; --- set initial i/o direction ; CLRWDT BCF INTCON,GIE ;disable interrupts BCF STATUS,RP0 ;select page 0 BCF STATUS,RP1 BCF STATUS,IRP MOVLW 0x00 ;initialise port B MOVWF PORTB MOVLW 0xFF ;initialise port C MOVWF PORTC BSF STATUS,RP0 ;select page 1 MOVLW 0x06 ;set port A digital i/o MOVWF ADCON1 MOVLW 0xFF MOVWF TRISA ;port A - all inputs MOVLW 0x00 MOVWF TRISB ;port B - all outputs MOVLW 0xFF MOVWF TRISC ;port C - all inputs ; ; --- allocate prescaler to WDT rather than timer ; --- (consult datasheet before changing this) ; MOVLW 0xD7 MOVWF OPTION_REG BCF STATUS,RP0 ;select page 0 CLRF TMR0 ;clear timer BSF STATUS,RP0 ;select page 1 MOVLW 0xDF ;prescaler set in MOVWF OPTION_REG ; option register CLRWDT ;clear watchdog MOVLW 0xCC ;prescaler set in MOVWF OPTION_REG ; option register ; BCF STATUS,RP0 ;select page 0 ; CLRWDT ; ; --- set up the two PWM channels (note that the two ; --- channels run from one timer, so they always ; --- have the same frequency and are synchronised ; --- together). The timer (TMR2) is set to count 0-255. Prescaler ; --- is set to 4, so with 16Mhz clock giving fosc/4 of 4MHz ; --- the period is 256uS. This also defines the rate at which ; --- the timer 2 interrupt goes off. ; MOVLW 0xFF MOVWF PR2 ;timer counts 0 to 255 BCF STATUS,RP0 ;select page 0 MOVLW 0xC0 MOVWF CCPR1L MOVLW 0x04 MOVWF CCPR2L BSF STATUS,RP0 ;select page 1 BCF TRISC,2 ;set both PWM pins to outputs BCF TRISC,1 BCF STATUS,RP0 ;select page 0 MOVLW 0x05 MOVWF T2CON ;timer on, prescale 4, post 1 MOVLW 0x0F MOVWF CCP1CON ;PWM mode MOVWF CCP2CON ;PWM mode CLRF TMR2 ;clear timer 2 BCF PIR1,TMR2IF ;reset timer 2 interrupt flag BSF STATUS,RP0 ;select page 1 BSF PIE1,TMR2IE ;enable timer 2 interrupt BCF STATUS,RP0 ;select page 0 ; ; --- initialise variables ; CLRF RSTATE CLRF FLAGS MOVLW PSCALE MOVWF PRETICK ; MOVLW 8 ;do 8 times, once for each timer MOVWF LOOPCNT MOVLW TIMER1 ;set pointer to start of timer block MOVWF FSR INIT1 CLRF INDF ;and clear each in turn INCF FSR,F DECF LOOPCNT,F BTFSS STATUS,Z GOTO INIT1 ; ; --- initialise serial stuff ; BSF STATUS,RP0 ;select page 1 MOVLW BRATE ;set baud rate MOVWF SPBRG MOVLW 0x65 ;set BRGH=1, async, tx on MOVWF TXSTA BCF STATUS,RP0 ;select page 0 MOVLW 0x80 ;enable serial port MOVWF RCSTA ;(don't enable CREN with this command) BSF RCSTA,CREN ;enable continuous receive ; BSF STATUS,RP0 ;select page 1 BSF PIE1,RCIE ;enable rx interrupt BCF STATUS,RP0 ;select page 0 ; ; --- enable interrupts ; BSF INTCON,PEIE ;enable peripheral interrupts BSF INTCON,GIE ;global interrupt enable ; ; --- and then fall into the main routine... ; ;*********************************************************** ;* Main routine. * ;* * ;*********************************************************** ; ; --- start of main loop ; ; basically just a despatch loop for the messages from ; the various interrupts ; MAIN CLRWDT ;clear watchdog timer BTFSC FLAGS,MIDIRX ;is MIDI receive flag set? GOTO MAIN10 ;yes, jump ; BTFSC FLAGS,TICK ;is tick flag set? GOTO MAIN20 ;yes, jump ; GOTO MAIN ;otherwise, just keep looping ; ; --- MIDI receive processsing ; ; this isn't very logical and needs reworking ; - split between interrupt and here is awkward ; - note test could be subroutine ; MAIN10 MOVF MIDNOTE,W ;get note SUBLW MBASE-1 ;less than bottom note? BTFSC STATUS,C GOTO MAIN11 ;below bottom - ignore ; MOVF MIDNOTE,W ;get note again SUBLW MBASE+7 ;test against top note BTFSS STATUS,C GOTO MAIN11 ;above top - ignore ; ADDLW TIMER1 ;add note remainder (0-7) to timer address MOVWF FSR ;this then is pointer to that note's timer ; MOVF MIDCOM,W ;get command ANDLW 0xF0 SUBLW 0x90 ;is it note on? BTFSC STATUS,Z GOTO MAIN12 ;yes, jump ; MOVF MIDCOM,W ;get command ANDLW 0xF0 SUBLW 0x80 ;is it note off? BTFSS STATUS,Z GOTO MAIN11 ;no, finish ; MAIN15 MOVLW 0 ;set timer to 0 for note off GOTO MAIN13 ; MAIN12 MOVF MIDVELO,F ;test velocity value BTFSC STATUS,Z ;zero? GOTO MAIN15 ;yes, treat as if note off ; BTFSS PORTA,4 ;test link (how to treat note on) GOTO MAIN14 ; MOVLW 128 ;no link: note stays on until set off GOTO MAIN13 ; MAIN14 MOVF MIDVELO,W ;link: use velocity for note on time MAIN13 MOVWF INDF ;and write to timer ; MAIN11 BCF FLAGS,MIDIRX ;clear MIDI receive flag GOTO MAIN ; ; --- output timer stuff ; ; There are 8 timers, running from the postscaler in the timer 2 interrupt routine, ; one for each output. Each is tested. ; If zero, output will be set off, timer not changed. ; If >127, output on, timer not changed. ; Between 0 and 128, timer decremented and output set according to whether result is ; now zero or not. ; Outputs get assembled into OUTBYTE first so that they ; all change simultaneously when it is written to the port. ; MAIN20 MOVLW 8 ;do 8 times, once for each timer MOVWF LOOPCNT MOVLW TIMER1 - 1 ;set pointer to one before timer block MOVWF FSR ; MAIN21 INCF FSR,F ;move pointer on one MOVF INDF,F ;test timer (copy onto itself modifies zero status flag) BTFSC STATUS,Z ;zero? GOTO MAIN24 ;yes, timer idle, go and set output low BTFSC INDF,7 ;msb set (ie >127)? GOTO MAIN22 ;yes, don't do timer stuff, just set output high ; DECF INDF,F ;neither of above, decrement timer BTFSS STATUS,Z ;now zero? GOTO MAIN22 ;no, timing still in progress, so set output bit ; MAIN24 BCF STATUS,C ;clear carry bit GOTO MAIN23 ; MAIN22 BSF STATUS,C ;set carry bit MAIN23 RRF OUTBYTE,F ;rotate into output byte ; DECF LOOPCNT,F ;decrement loop count BTFSS STATUS,Z ;all done (count is down to zero)? GOTO MAIN21 ;no, go round loop again for next timer ; MOVF OUTBYTE,W ;finally get the output byte we've assembled MOVWF PORTB ;and write to output port ; BCF FLAGS,TICK ;clear tick flag GOTO MAIN ; END