; ************************************************************************** ; ** ; ** MSync101.asm - MIDI Sync Box, release version ; ** ; ** See http://www.doctort.org/adam/projects/MIDISync/ ; ** ; ** Assembly code for PIC16F84A microcontroller ; ** by Adam Pierce ; ** ; ** version 1.02 15-Jan-2002 ; ** ; ************************************************************************** ; ************************************************************************** ; ** PROGRAM DESCRIPTION ; ** ; ; This program is for a PIC16F84A running at 10MHz. For shematics and other ; info, see http://www.doctort.org/adam/projects/MIDISync/ ; ; Port assignments are as follows: ; ; A0 (pin 17) [OUT] MIDI Output ; A1 (pin 18) [OUT] LED Digit 0 Enable (0 = enabled) ; A2 (pin 1) [OUT] LED Digit 1 Enable (0 = enabled) ; A3 (pin 2) [OUT] LED Digit 2 Enable (0 = enabled) ; A4 (pin 3) [OUT] Buttons and rotary encoder enable (0 = enabled) ; ; B0 (pin 6) [IN/OUT] LED Segment 0 / Stop button _2_ ; B1 (pin 7) [IN/OUT] LED Segment 1 / rotary encoder 1| |3 ; B2 (pin 8) [IN/OUT] LED Segment 2 / rotary encoder | 0 | ; B3 (pin 9) [IN/OUT] LED Segment 3 / Start button --- ; B4 (pin 10) [OUT] LED Segment 4 | | ; B5 (pin 11) [OUT] LED Segment 5 6|___|4 ; B6 (pin 12) [OUT] LED Segment 6 5 ; B7 (pin 13) [OUT] Flashing Light ; MIDI Protocol Description ; ; MIDI is transmitted serially at 31250 bits per second. Each byte begins ; with a start bit (always 0), 8 data bits then a stop bit (always 1) ; The data bits are transmitted back-to-front, eg. bit 0 first. MIDI Port ; is set to 1 when idle. ; ; We want a clock pulse every 32µs. With a 10MHz crystal, this should be ; the equivalent of 80 instructions. ; ; We need to send the following MIDI messages (note they are one byte each) ; ; 0xFA Start ; 0xFC Stop ; 0xF8 MIDI Clock ; ; MIDI Clocks need to be sent 24 times per beat (eg. for 120bpm, they need ; to be sent 48 times per second) ; Implementation notes ; ; * All timing-critical stuff goes in ISR, ISR should be no more than ; about 60 instructions. ; ; * Memory bank 0 is selected by default. Any function which selects ; bank 1 should restore bank 0 before it finishes. ; ; * Args[] is a shared memory area. Be careful that it will not get ; wiped out by an interrupt. ; Functions to be performed (timing-critial ones are handled by the ISR) ; ; * Read inputs (buttons & rotary encoder) (timing-critical) ; * Display BPM on LEDs (timing-critical) ; * Calculate timing delays based on the BPM ; * Work out when to send MIDI messages ; * Send MIDI messages (timing-critical) ; * Flash the light ; ; Functions implemented: ; ; * Timer interrupt at 32µs intervals ; * LED display multiplexing ; * Display of decimal number on LED ; * Input multiplexing, debounce and change detection ; * Rotary encoder decoding ; * Serial output sequencing ; * Timing calculations ; * Serial output queue ; * Control Logic for start and stop buttons ; * Storage of settings in Flash memory ; Functions still to implement: ; ; * Acceleration for the knob maybe ? ; ************************************************************************** ; ** CPU configuration ; ** processor PIC16F84A include __config _XT_OSC & _WDT_OFF & _PWRTE_ON radix dec ; ************************************************************************** ; ** Variable declarations ; ** Digit equ H'0C' ; 3 byte - LED display values DispBpm equ H'0F' ; 1 byte - buffer for the display PushW equ H'10' ; 1 byte - Somewhere to store W during interrupts PushF equ H'11' ; 1 byte - Somewhere to store STATUS during interrupts MyFlags equ H'12' ; 1 byte - Some flags for various things DebValue equ H'13' ; 1 byte - Input debounce buffer DebCount equ H'14' ; 1 byte - Input debounce timer InpChange equ H'15' ; 1 byte - Input change detection MidiSeq equ H'16' ; 1 byte - Current MIDI transmit state MidiData equ H'17' ; 1 byte - MIDI byte being transmitted BPM equ H'18' ; 1 byte - Current music speed Count equ H'19' ; 2 byte - Beat counter ClockTime equ H'1B' ; 2 byte - Current speed as a 16 bit timer value Clock equ H'1D' ; 1 byte - Current clock (fraction of a beat) Beat equ H'1E' ; 1 byte - Current beat WrTimer equ H'1F' ; 1 byte - Countdown until EE write Args equ H'20' ; 10 byte - args for various functions ; Bit definitions for MyFlags MF_STARTREQ equ 7 ; Start button has been pressed - Request restart MF_STOPREQ equ 6 ; Stop button has been pressed - Request stop MF_MIDICLOCK equ 5 ; It's time to send a MIDI clock message MF_RUNNING equ 4 ; Are we currently in Play mode ? ; ************************************************************************** ; ** Constants ; ** BpmMax equ 250 ; You can't go any faster than this BpmMin equ 10 ; You can't go any slower than this DebTimeout equ 10 ; Debounce time (in units of 32µs, eg 10 is 320µs) MidiMsgClock equ H'F8' ; MIDI Clock message MidiMsgStart equ H'FA' ; MIDI Start message MidiMsgStop equ H'FC' ; MIDI Stop message EeAddrBpm equ H'00' ; Location in EEPROM where the BPM is stored ; ************************************************************************** ; ** Vector Table ; ** ; Vector table org 0 ; Reset vector goto Reset org 4 ; Interrupt Vector goto Isr ; ************************************************************************** ; ** LEDBitmap - Changes a number (from 0x0 to 0xF) into a bitmap suitable ; ** for outputting to the LED display. ; ** ; ** (for memory-segmentation reasons, this should be as close as possible ; ** the the start of memory) ; ** LEDBitmap: andlw B'1111' ; To be sure we don't get any stupid values ; crashing our code, restrict it to 0..F addwf PCL, f retlw B'01111110' retlw B'00011000' retlw B'01101101' retlw B'00111101' retlw B'00011011' retlw B'00110111' retlw B'01110111' retlw B'00011100' retlw B'01111111' retlw B'00111111' retlw B'01011111' retlw B'01110011' retlw B'01100110' retlw B'01111001' retlw B'01100111' retlw B'01000111' ; ************************************************************************** ; ** InitHardware - Set up all the chip features we are going to be using ; ** InitHardware: movlw B'00000000' ; Set up port data directions tris PORTA ; 0 = output 1 = input movlw B'00000000' tris PORTB movlw B'00011110' ; Set up various enable lines movwf PORTA movlw B'01111111' movwf PORTB movlw 0x0C ; Clear all RAM movwf FSR ClearRAM: clrf INDF incf FSR movfw FSR xorlw 0x50 btfss STATUS, Z goto ClearRAM bsf STATUS, RP0 ; Set up the timer movlw B'00001111' movwf OPTION_REG bcf STATUS, RP0 clrf INTCON ; Enable the timer interrupt clrf TMR0 ; This should really be done after InitData bsf INTCON, GIE bsf INTCON, T0IE bcf INTCON, T0IF return ; ************************************************************************** ; ** InitData - Set default values for variables ; ** InitData: movlw B'01111111' ; Switch PORTB to input tris PORTB bcf PORTA, 4 ; Activate the buttons nop movfw PORTB ; Store the current input state so no spurious movwf InpChange ; inputs will be generated on startup movlw 24 ; Reset to first beat of the bar movwf Clock movlw 4 movwf Beat movlw 6 ; Set Count to something reasonable. This will movwf Count + 0 ; be overwritten soon but it should not be very ; high or the device will take ages to start up. movlw EeAddrBpm ; Read the initial BPM setting from the EEPROM call ReadEE movwf BPM return ; ************************************************************************** ; ** WriteEE - Stores a byte in the EEPROM memory. Put the address in ; ** EEWrAddr and the data value in W. ; ** EEWrAddr equ Args + 0 EETemp equ Args + 1 ; Somewhere to store the value for a moment WriteEE: movwf EETemp ; Shove the value somewhere for a moment movfw EEWrAddr ; Set the address to write to movwf EEADR movfw EETemp ; and the value to write movwf EEDATA bsf STATUS, RP0 ; Select bank 1 WaitEEW: btfsc EECON1, WR ; Wait for any previous writes to finish goto WaitEEW clrf EECON1 bcf INTCON, GIE ; Disable interrupts bsf EECON1, WREN ; EEPROM Write enable movlw H'55' ; Do stupid EE trigger sequence movwf EECON2 movlw H'AA' movwf EECON2 bsf EECON1, WR bsf INTCON, GIE ; Enable interrupts bcf EECON1, WREN ; EEPROM Write disable bcf STATUS, RP0 ; Select bank 0 return ; ************************************************************************** ; ** ReadEE - Retrieve a byte from EEPROM memory. Put the address you want ; ** read in W. ; ** ReadEE: bsf STATUS, RP0 ; Select bank 1 WaitEER: btfsc EECON1, WR ; Wait for any previous writes to finish goto WaitEER bcf STATUS, RP0 ; Select bank 0 movwf EEADR ; Set the address to read from bsf STATUS, RP0 ; Select bank 1 bsf EECON1, RD ; Read EEPROM bcf STATUS, RP0 ; Select bank 0 movfw EEDATA ; Put result in W return ; ************************************************************************** ; ** UpdateLED - Refresh the LED display ; ** ; ** LED Bitmap is stored in Digit[3] ; ** UpdateLED: movlw B'10000000' ; Disable all LED digits andwf PORTB btfss PORTA, 1 ; We only update one digit per call. goto Digit2 ; These lines jump to the appropriate btfss PORTA, 2 ; digit handler goto Digit1 Digit3: bsf PORTA, 3 ; Handles digit 3, the rightmost one movfw Digit + 2 iorwf PORTB, f bcf PORTA, 1 return Digit2: bsf PORTA, 1 ; Handles digit 2, the middle one movfw Digit + 1 iorwf PORTB, f bcf PORTA, 2 return Digit1: bsf PORTA, 2 ; Handles digit 1, the leftmost one movfw Digit + 0 iorwf PORTB, f bcf PORTA, 3 return ; ************************************************************************** ; ** NextBit - Send the next data bit out the MIDI port ; ** ; ** MidiSeq bits 7, 6 and 5 correspond to these states: ; ** 000 - Not transmitting ; ** 100 - Sending start bit ; ** 010 - Sending data bits ; ** 001 - Sending stop bit ; ** ; ** When sending data, MidiSeq bits 2, 1 and 0 count the bits ; ** NextBit: btfsc MidiSeq, 6 ; Determine which state we are in goto DataBit btfsc MidiSeq, 7 goto StartBit btfsc MidiSeq, 5 goto StopBit return StartBit: bcf PORTA, 0 ; Transmit start bit (always 0) bcf MidiSeq, 7 bsf MidiSeq, 6 return StopBit: bsf PORTA, 0 ; Transmit stop bit (always 1) bcf MidiSeq, 5 return DataBit: movfw PORTA ; Load the next data bit onto the output pin xorwf MidiData, w andlw B'00000001' xorwf PORTA, f movfw MidiSeq ; Count bits andlw B'111' btfsc STATUS, Z goto DoneData decf MidiSeq ; Advance to next bit rrf MidiData return DoneData: bcf MidiSeq, 6 ; All data bits have been sent bsf MidiSeq, 5 return ; ************************************************************************** ; ** IsMIDIFree - Sets the Z flag if the MIDI port is free ; ** IsMIDIFree: movfw MidiSeq ; Is the MIDI port free ? xorlw 0 return ; ************************************************************************** ; ** Transmit - Takes the byte in W and sends it to the MIDI output ; ** You need to check that the MIDI port is free before calling ; ** this. ; ** Transmit: movwf MidiData movlw B'10000111' movwf MidiSeq return ; ************************************************************************** ; ** ReadInputs - check all buttons and the rotary encoder ; ** ReadInputs: movlw B'01111111' ; Switch PORTB to input tris PORTB bcf PORTA, 4 ; Activate the buttons nop movfw DebValue ; Debounce - Does our register match the xorwf PORTB, w ; actual port state ? It has to stay steady btfss STATUS, Z ; for 320 microseconds before we'll accept it clrf DebCount movfw DebCount sublw DebTimeout btfss STATUS, Z goto EndInput movfw InpChange ; Change detection - has the port changed xorwf DebValue,w btfsc STATUS, Z goto EndInput movfw InpChange ; Check rotary encoder andlw B'0110' xorlw B'0000' btfss STATUS, Z goto EndEncoder btfsc DebValue, 2 goto LeftTurn RightTurn: incf BPM goto EndEncoder LeftTurn: decf BPM EndEncoder: btfss DebValue, 3 ; Check the start button bsf MyFlags, MF_STARTREQ btfss DebValue, 0 ; Check the stop button bsf MyFlags, MF_STOPREQ movfw DebValue ; Remember the current state movwf InpChange EndInput: movfw PORTB ; Remember the current port state (for debounce) movwf DebValue incf DebCount movlw B'00000000' ; Switch PORTB to output tris PORTB bsf PORTA, 4 ; deactivate the buttons so they ; don't interfere with the display return ; ************************************************************************** ; ** CountTime - Keeps track of clocks, beats and bars. This gets called ; ** once every 32µs ; ** CountTime: decfsz Count + 1 ; Count down 16 bit counter. When it reaches return ; zero, it's time to send another MIDI clock. decfsz Count + 0 return movfw ClockTime + 0 ; Reset the 16 bit counter. movwf Count + 0 movfw ClockTime + 1 movwf Count + 1 btfss MyFlags, MF_RUNNING ; Are we in PLAY mode ? goto StopMode bsf MyFlags, MF_MIDICLOCK ; Request a MIDI clock be sent. decfsz Clock ; Count clocks. There are 24 Clocks per Beat. return movlw 24 movwf Clock decfsz Beat ; Count beats. There are 4 Beats per Bar. return movlw 4 movwf Beat return StopMode: movlw 24 ; STOP mode, reset everything movwf Clock movlw 4 movwf Beat return ; ************************************************************************** ; ** Display - Converts the number in W to a decimal number and loads that ; ** into the LED digits. ; ** DBuf equ Args + 0 ; 3 byte - LED Double-buffer DispAcc equ Args + 4 ; 1 byte - Display accumulator - a working variable for DisplayBPM DispBuf equ Args + 5 ; 1 byte - Display value buffer Display: movwf DispBuf ; Move the input value to a temporary register clrf DispAcc Do100: movlw 100 ; Subtract 100 until we can't anymore subwf DispBuf, w bnc Got100 movwf DispBuf incf DispAcc goto Do100 Got100: movfw DispAcc ; Convert value into bitmap call LEDBitmap ; and load into display register movwf DBuf + 0 clrf DispAcc Do10: movlw 10 ; Subtract 10 until we can't anymore subwf DispBuf, w bnc Got10 movwf DispBuf incf DispAcc goto Do10 Got10: movfw DispAcc ; Convert value into bitmap call LEDBitmap movwf DBuf + 1 movfw DispBuf ; Get remainder and convert into bitmap call LEDBitmap movwf DBuf + 2 movfw DBuf + 0 ; Blank leading zeroes xorlw b'01111110' btfss STATUS, Z goto Blast clrf DBuf + 0 movfw DBuf + 1 xorlw b'01111110' btfsc STATUS, Z clrf DBuf + 1 Blast: movfw DBuf + 0 ; Copy buffer contents to display movwf Digit + 0 movfw DBuf + 1 movwf Digit + 1 movfw DBuf + 2 movwf Digit + 2 return ; ************************************************************************** ; ** Div24 - Divide 24 bit int by 16 bit int ; ** ; ** DivArg1Hi:DivArg1Mid:DivArg1Lo is divided by DivArg2Hi:DivArg2Lo ; ** ; ** result is in DivRsltHi:DivRsltLo ; ** ; ** remainder is in DivRemHi:DivRemLo ; ** ; ** DivArg1Hi:DivArg1Mid:DivArg1Lo will not be preserved ; ** ; ** How it works (it's basically long division in binary) ; ** ; ** For each bit, ; ** left shift the MSB of arg1 into the remainder ; ** compare the remainder with arg2 ; ** if remainder > arg2 ; ** remainder -= arg2 ; ** shift 1 onto the result ; ** else ; ** shift 0 onto the result ; ** DivArg1Hi equ Args + 0 DivArg1Mid equ Args + 1 DivArg1Lo equ Args + 2 DivArg2Hi equ Args + 3 DivArg2Lo equ Args + 4 DivRsltHi equ Args + 5 DivRsltLo equ Args + 6 DivRemHi equ Args + 7 DivRemLo equ Args + 8 DivCounter equ Args + 9 Div24: movlw 24 ; Initialise all registers movwf DivCounter clrf DivRsltHi clrf DivRsltLo clrf DivRemHi clrf DivRemLo DivLoop: rlf DivArg1Lo ; Shift MSB of arg1 into the remainder rlf DivArg1Mid rlf DivArg1Hi rlf DivRemLo rlf DivRemHi rlf DivRsltLo ; Left shift the result rlf DivRsltHi movfw DivRemLo ; do subtraction: arg2 - remainder subwf DivArg2Lo, w movfw DivRemHi btfss STATUS, C sublw 1 subwf DivArg2Hi, w btfss STATUS, C ; was there any carry (ie, was remainer > arg2 ?) goto DivCarry bcf DivRsltLo, 0 goto DivEndLoop DivCarry: movfw DivArg2Lo ; do subtraction rem = rem - arg2 subwf DivRemLo, f movfw DivArg2Hi btfss STATUS, C sublw 1 subwf DivRemHi, f bsf DivRsltLo, 0 DivEndLoop: decfsz DivCounter goto DivLoop return ; ************************************************************************** ; ** Flashes the big red light - hopefully in time with the music ; ** FlashLight: btfss MyFlags, MF_RUNNING ; Are we in PLAY mode ? goto FlashOff ; We are in STOP mode. Turn off the light movlw 18 ; Should the light be on or off ? subwf Clock, w btfss STATUS, C goto FlashOff movlw 4 ; We want a brighter flash for the first beat xorwf Beat, w ; of the bar, so test for this condition btfsc STATUS, Z goto FlashOn ; Full brightness for 1st beat of the bar movfw Count + 1 ; 6% brightness for all other beats andlw B'1111' xorlw B'0000' btfsc STATUS, Z goto FlashOn FlashOff: bcf PORTB, 7 return FlashOn: bsf PORTB, 7 return ; ************************************************************************** ; ** SendMIDI - Sends any MIDI messages that need to be sent now ; ** SendMIDI: btfss MyFlags, MF_MIDICLOCK ; Is it time to send a MIDI Clock ? goto NoMidiClock movlw MidiMsgClock ; Send MIDI Clock message call Transmit bcf MyFlags, MF_MIDICLOCK return NoMidiClock: btfss MyFlags, MF_STARTREQ ; Has the START button been pushed ? goto NoStartMsg movlw 24 ; Is it the first beat of the bar ? xorwf Clock, w btfss STATUS, Z goto NoStartMsg movlw 4 xorwf Beat, w btfss STATUS, Z goto NoStartMsg movlw MidiMsgStart ; Send MIDI Start message call Transmit bcf MyFlags, MF_STARTREQ bsf MyFlags, MF_RUNNING ; We are now in PLAY mode return NoStartMsg: btfss MyFlags, MF_STOPREQ ; Has the STOP button been pushed ? goto NoStopMsg movlw MidiMsgStop ; Send MIDI Stop message call Transmit bcf MyFlags, MF_STOPREQ bcf MyFlags, MF_RUNNING ; We are now in STOP mode NoStopMsg: return ; ************************************************************************** ; ** CheckBpmRange - Does not allow the speed to get too fast or too slow ; ** CheckBpmRange: movfw BPM ; Make sure the BPM falls within the sublw BpmMax ; allowed range btfss STATUS, C goto BpmTooHigh movfw BPM sublw BpmMin btfss STATUS, C return BpmTooLow: movlw BpmMin movwf BPM return BpmTooHigh: movlw BpmMax movwf BPM return ; ************************************************************************** ; ** DisplayBpm - Display the current BPM value on the LED display ; ** DisplayBpm: movfw BPM ; Pretty self-explanatory movwf DispBpm goto Display ; ************************************************************************** ; ** CalculateTiming - Works out the delay between MIDI clocks based on the ; ** current BPM value. Result is placed in ClockTime. ; ** ; ** Formula to calculate MIDI timing: ; ** Number of ticks per MIDI clock = 78125 / BPM ; ** ; ** NB. 78125 = 0x1312D, one tick is 32µs ; ** CalculateTiming: movlw H'01' ; Divide 78125 by the BPM value movwf DivArg1Hi movlw H'31' movwf DivArg1Mid movlw H'2D' movwf DivArg1Lo clrf DivArg2Hi movfw BPM movwf DivArg2Lo call Div24 movfw DivRsltHi ; Put the result in the beat timer addlw 1 ; (We have to add 1 because of movwf ClockTime + 0 ; the way the timer counts down) movfw DivRsltLo addlw 1 movwf ClockTime + 1 return ; ************************************************************************** ; ** SaveBPM - Store the current BPM in EEPROM ; ** SaveBPM: movlw EeAddrBpm ; Read the currently stored value call ReadEE xorwf BPM, w ; Is it up to date ? btfsc STATUS, Z return movlw EeAddrBpm ; EEPROM needs to be updated movwf EEWrAddr movfw BPM call WriteEE return ; ************************************************************************** ; ** Start Here ; ** Reset: call InitHardware ; Set stuff up call InitData ; ************************************************************************** ; ** Main program loop. Non timing-critical code goes here ; ** Loop: call IsMIDIFree ; Is the MIDI port free ? btfsc STATUS, Z call SendMIDI ; Send MIDI messages movfw BPM ; Has the BPM value changed ? xorwf DispBpm, w btfsc STATUS, Z goto NoBPMChange call CheckBpmRange ; Check the speed is within reason call CalculateTiming ; Start clocking at the new speed call DisplayBpm ; Update the screen movfw BPM movwf DispBpm NoBPMChange: decfsz WrTimer ; Every now and then, check that the goto NoEEWrite ; BPM has been stored in the EEPROM call SaveBPM NoEEWrite call FlashLight ; Update the state of the flashing light goto Loop ; ************************************************************************** ; ** Interrupt handler - Called once every 32µs. Timing-critical code goes ; ** here. ; ** Isr: movwf PushW ; Save context swapf STATUS, w movwf PushF bcf STATUS, RP0 ; Select bank 0 (in case the code we ; interrupted was using bank 1) ; Set the timer to interrupt us again in 32µs. When running with a 10MHz ; crystal, this is equivalent to 80 cycles. It takes 2 cycles to load a ; new value into the counter, so that brings it down to 78 cycles. movlw 78 ; Set the delay until the next interrupt subwf TMR0, f bcf INTCON, T0IF ; Enable the timer interrupt call ReadInputs ; Do various timing-critical stuff call CountTime call UpdateLED call NextBit swapf PushF, w ; Restore context movwf STATUS swapf PushW, f swapf PushW, w retfie ; End of interrupt service function end