;This piece of software is called Archy's AT89C2051 melody playing routine ;8031/32/51/52/AT89C2051/4051 assembly routine for playing a melody. ;Author: Arto Niskanen (archy@staff.oamk.fi) ;compiles with as31IntHex, ;ftp://se.aminet.net/pub/aminet/dev/cross/as31IntHex.lha ;The .lha contains binaries for Amiga, Linux and MS-DOS. ;This piece of software is Bibleware. That means, that if you use this ;software, you have to read the Gospel according to Luke chapter 9, and ;read it good. Plus, Christmas is not the birthday of Santa Claus. ;Source V33: Public release! ;Planned to work in AT89C2051. Testing done using Temic 80C52. ;Calculations made for crystal of 11.0592 MHz. ;Outputs music to P3.1/TxD as square wave and to a 4-bit DAC connected to ;P1.0-P1.3. To use different instruments, DAC must be used. ;Song format: ; .db OCT|0,AA|L8,BB|L8,CC|L8,D|L8,E|L8,F|L8,G|L8,REST|L1 ;each byte consisting of ccccpppp, in which "cccc" are command of four bits ;e.g. D -> play note D and "pppp" parameter, like in D|L4 -> play a quarter ;note D. ;OCT|2 -> next notes will be from octave 2. ;REST|L4 -> rest of a quarter ;EFF|5 -> launch effect #5 ;EOS -> End Of Pattern ;EOM -> End Of Melody ;S|4 -> speed. Lower value makes music faster ;Note: Because of assembler's syntax, note ;"C" is presented as "CC", because "C" is Carry flag, ;"B" is "BB", "B" is B register and ;"A" is "AA", "A" is A register. .equ CODEBASE,0x0000 ;Compile to this address. ;Should be 0x0000 for AT89C2051 ;0x8000 for testing purposes .equ VECT_Reset,0x00 .equ VECT_IntE0,0x03 .equ VECT_IntT0,0x0B .equ VECT_IntE1,0x13 .equ VECT_IntT1,0x1B .equ VECT_IntRS232,0x23 .equ B0R0,0x00 .equ B0R1,0x01 .equ B0R2,0x02 .equ B0R3,0x03 .equ B0R4,0x04 .equ B0R5,0x05 .equ B0R6,0x06 .equ B0R7,0x07 .equ b_Melody,0x94 ;P1.4, bit to output melody to. ;4-bit DAC is at P1.0-P1.3 .equ b_POWEROFF,0xB7 ;Pulling this low should ;turn off the power .equ DB_BASE,0x08 ;"Database", i.e. variables ;begin from this address .equ db_Octave,DB_BASE+0 .equ db_PerH,DB_BASE+1 .equ db_PerL,DB_BASE+2 .equ db_NoteLenH,DB_BASE+3 .equ db_NoteLenL,DB_BASE+4 .equ db_NoteDone,DB_BASE+5 .equ db_Div4,DB_BASE+6 .equ db_DivSpeed,DB_BASE+7 .equ db_PatternH,DB_BASE+8 .equ db_PatternL,DB_BASE+9 .equ db_PlaySpeed,DB_BASE+10 .equ db_InstrPos,DB_BASE+11 .equ db_Timer1ms,DB_BASE+12 .equ db_InstrRAM,DB_BASE+13 .equ db_InstrRAMEND,DB_BASE+21 .equ db_IT0Toggle,DB_BASE+22 .equ STACKBASE,DB_BASE+24 ;definition of notes, lengths etc. .equ EOP,0x00 ;End Of Pattern ;C C#/Db D D#/Eb E F F#/Gb G G#/Ab A A#/Bb B -> 0...11 respectively ;Command encoding = command nibble .equ EFF,0x00 ;Effect or Instrument Select .equ AA,0x10 .equ Au,0x20 .equ BBb,Au .equ BB,0x30 .equ CC,0x40 .equ Cu,0x50 .equ Dbb,Cu .equ D,0x60 .equ Du,0x70 .equ Eb,Du .equ E,0x80 .equ F,0x90 .equ Fu,0xA0 .equ Gb,Fu .equ G,0xB0 .equ Gu,0xC0 .equ Abb,Gu .equ REST,0xD0 .equ OCT,0xE0 ;octave .equ SPD,0xF0 ;speed ;Note length encoding for parameter nibble .equ L_2,0x00 ;two whole notes (is there such thing?) .equ L1d,0x01 ;whole note with dot .equ L1,0x02 ;whole note .equ L2d,0x03 ;half note with dot .equ L2,0x04 ;half note .equ L4d,0x05 ;quarter note with dot .equ L4,0x06 ;quarter note .equ L8d,0x07 ;1/8 note with dot .equ L8,0x08 ;1/8 note .equ L16d,0x09 ;1/16 with dot .equ L16,0x0A ;1/16 .equ L32d,0x0B ;1/32 with dot .equ L32,0x0C ;1/32 .equ L64d,0x0D ;1/64 with dot .equ L64,0x0E ;1/64 .equ EOM,EFF|0 ;End Of Melody .equ EFF_INSTR1,EFF|1 ;Command for selecting Instrument 1 .equ EFF_INSTR2,EFF|2 ;---------------------- .org CODEBASE+VECT_Reset ljmp INIT .org CODEBASE+VECT_IntT0 ljmp IntT0 .org CODEBASE+VECT_IntT1 ljmp IntT1 ;---------------------- INIT: mov SP,#STACKBASE ;enable stack space mov R0,#DB_BASE ;clear all variables INIT_clrloop: mov A,#0 mov @R0,A inc R0 mov A,R0 cjne A,#STACKBASE,INIT_clrloop mov db_NoteDone,#1 ;note is not playing! mov TMOD,#0x21 ;T0=16-bit, T1=reloading setb PT0 ;T0 priority up mov TL1,#256-230 ;4 kHz for T1 mov TH1,#256-230 setb TR1 ;start Timer1 setb ET1 ;enable interrupt for Timer1 setb EA ;enable interrupts in general ;;; main: mainloop: ;System is up & running. Let's do the trick! ;To play several melodies after another, just copy the ;following two lines, add melodies at the end of this ;source file and change melodies' names accordingly. mov DPTR,#DeckTheHalls lcall PlayMelodY ;Melody played, let's power off. clr b_POWEROFF ;Power is about to go out... let's loop for a while ;to let capacitors discharge. ;If for some reason power doesn't go off, let's play ;again to inform user, that we're still here. mov db_Timer1ms,#250 PowerOffLoop: mov A,db_Timer1ms jnz PowerOffLoop sjmp mainloop ;---------------------- PlayMelodY: ;Plays melody cuts called patterns from a pattern ;table given in DPTR. push B0R2 mov db_PatternH,DPH mov db_PatternL,DPL mov R2,#0 PMY_loop: mov DPH,db_PatternH mov DPL,db_PatternL mov A,R2 movc A,@DPTR+A mov R0,A mov A,R2 inc A movc A,@DPTR+A mov DPL,A mov DPH,R0 jnz PMY_notEOM ;If both DPL mov A,R0 ;and DPH jz PMY_done ;are 0x00, we're done. PMY_notEOM: lcall PlayPatterN ;Otherwise, play pattern inc R2 inc R2 sjmp PMY_loop PMY_done: pop B0R2 ret ;---------------------- PlayPatterN: ;Play Pattern of music. Pattern consists of notes and ;rests that form the music and commands like SPD ;for controlling speed of music etc. ;Pattern ends with a EOP char. push B0R2 PPN_loop: PPN_getchar: clr A movc A,@DPTR+A ;Get a char from current pattern inc DPTR ;point to the next char PPN_parsecmd: jnz PPN_noEOP ljmp PPN_EndOfPattern ;0x00=EOP -> End Of Pattern PPN_noEOP: mov R2,A ;store char anl A,#0xF0 ;mask out the parameter nibble ;;; ;Let's compare and find out, what this char means PPNp_Octaveko: cjne A,#OCT,PPNp_noOctave ;rule out OCTave mov A,R2 ;get parameter anl A,#0x0F ;mask out command nibble mov db_Octave,A ;store value ljmp PPN_parsedone ;and we're done. PPNp_noOctave: ;it wasn't OCTave. ;;; PPNp_Effectko: cjne A,#EFF,PPNp_noEffect ;rule out EFFect push DPH push DPL mov A,R2 anl A,#0x0F ;get effect #. ;Select Instrument cjne A,#EFF_INSTR1,PPNp_noI1 mov DPTR,#Instrument1 sjmp PPNp_SetInstr PPNp_noI1: cjne A,#EFF_INSTR2,PPNp_noI2 mov DPTR,#Instrument2 sjmp PPNp_SetInstr PPNp_noI2: sjmp PPNp_noINSTR PPNp_SetInstr: mov R0,#db_InstrRAM mov R1,#8 ;8 bytes to transfer PPNp_SIcloop: clr A movc A,@DPTR+A mov @R0,A inc R0 inc DPTR djnz R1,PPNp_SIcloop sjmp PPNp_EffDone PPNp_noINSTR: ;;; ;wasn't an instrument selection. ;If you create some effects, launch them here. sjmp PPNp_noEffect ;;; PPNp_EffDone: pop DPL pop DPH sjmp PPN_parsedone PPNp_noEffect: ;;; PPNp_Speedko: cjne A,#SPD,PPNp_noSpeed ;rule out SPeeD mov A,R2 anl A,#0x0F mov db_PlaySpeed,A ;store speed value sjmp PPN_parsedone PPNp_noSpeed: ;;; PPNp_restko: cjne A,#REST,PPNp_norest ;rule out REST ;Rest: silence for a note's length. mov A,R2 acall PPN_NoteLen ;Set "silence" playing for needed time PPNp_restwait: mov A,PCON ;get current state of Power Control orl A,#0x01 ;Idle flag on mov PCON,A ;store. This puts the CPU into ;IDLE mode to save some power. mov A,db_NoteDone jz PPNp_restwait sjmp PPN_parsedone PPNp_norest: ;;; PPNp_note: mov A,R2 ;all others ruled out, it's a note. ;Note includes pitch and length. ;First, set length. acall PPN_NoteLen ;set note's len for IntT1 ;Then, set pitch. mov A,R2 ;get raw pitch clr C subb A,#AA ;"shift" AA to be #0, Cu to be #1 etc. swap A anl A,#0x0F ;mask out command nibble mov R0,A mov A,db_Octave ;get current octave mov B,#12 ;There are 12 notes in an octave mul AB add A,R0 ;add note to octave ;now, set T0 to generate sampling frequency of the note. push DPH push DPL clr C rlc A ;pointer *2 for word access mov R0,A mov DPTR,#PerTable ;periods are stored in PerTable clr TR0 ;stop current note setb b_Melody ;digital output off movc A,@DPTR+A ;get High byte of period mov TH0,A ;store mov db_PerH,A ;store also into database for IntT0 mov A,R0 inc A movc A,@DPTR+A ;Low byte mov TL0,A mov db_PerL,A clr TF0 mov db_Timer1ms,#3 PPNp_1ms: mov A,db_Timer1ms jnz PPNp_1ms ;Wait for a few ms to create a "hit" ;into the beginning of the note setb ET0 setb TR0 ;enable output pop DPL pop DPH ;note is playing, let's wait it to end. PPNp_waitnoteend: mov A,PCON ;get current state of Power Control orl A,#0x01 ;Idle flag on mov PCON,A ;store. This puts the CPU into ;IDLE mode to save some power. mov A,db_NoteDone ;is the note played completely? jz PPNp_waitnoteend ;no, wait more clr ET0 ;Stop note from playing setb b_Melody sjmp PPN_parsedone PPNp_nonote: mov A,R2 ;;; PPN_parsedone: ;char parsed, note played, effect launched or whatever. ;Next char, maestro! ljmp PPN_loop PPN_EndOfPattern: PPN_done: pop B0R2 PPN_ret: ret ;---------------------- PPN_NoteLen: ;note length in ACC0-ACC3. Convert it to noteticks ;and set db_NoteLenH/L accordingly. anl A,#0x0F ;mask out command nibble clr C rlc A ;*2 for word addressing mov R0,A push DPH push DPL mov DPTR,#NoteLenTable movc A,@DPTR+A ;get note length's High byte clr ET1 ;We'll set a 16-bit value, have to! mov db_NoteLenH,A ;store mov A,R0 inc A movc A,@DPTR+A ;get Low byte mov db_NoteLenL,A ;store setb ET1 ;OK, go on, IntT1. mov db_NoteDone,#0 ;new note playing pop DPL pop DPH ret ;Length of notes at SPD=10, value*10ms ;2/1 384 ;1/1d 288 ;1/1 192 ;1/2d 144 ;1/2 96 ;1/4d 64 ;1/4 48 ;1/8d 32 ;1/8 24 ;1/16d 18 ;1/16 12 ;1/32d 9 ;1/32 6 ;1/64d 5, should be 4.5. ;1/64 3 NoteLenTable: .dw 384,288,192,144,96,64,48,32,24,18,12,9,6,5,3 ;---------------------- PerTable: ;Note pitches converted to frequencies converted to ;T0 periods using the formula ;1/(freq*2)*11.0592/12*1000000 .equ FINETUNE,11 ;Compensate delay caused by ;Timer0 reloading. See IntT0 ;Octave 0 ; Note Freq *2->T0 .dw 65535-523+FINETUNE ; A 440.00 523 .dw 65535-494+FINETUNE ; A#,Bb 466.16 494 .dw 65535-467+FINETUNE ; B 493.88 467 .dw 65535-440+FINETUNE ; C 523.25 440 .dw 65535-416+FINETUNE ; C#/Db 554.37 416 .dw 65535-392+FINETUNE ; D 587.33 392 .dw 65535-370+FINETUNE ; D#/Eb 622.25 370 .dw 65535-349+FINETUNE ; E 659.26 349 .dw 65535-330+FINETUNE ; F 698.46 330 .dw 65535-311+FINETUNE ; F#/Gb 739.99 311 .dw 65535-294+FINETUNE ; G 783.99 294 .dw 65535-277+FINETUNE ; G#/Ab 830.61 277 ;Octave 1 ; Note Freq *4->T0 .dw 65535-262+FINETUNE ; A 440.00 262 .dw 65535-247+FINETUNE ; A#,Bb 466.16 247 .dw 65535-233+FINETUNE ; B 493.88 233 .dw 65535-220+FINETUNE ; C 523.25 220 .dw 65535-208+FINETUNE ; C#/Db 554.37 208 .dw 65535-197+FINETUNE ; D 587.33 197 .dw 65535-185+FINETUNE ; D#/Eb 622.25 185 .dw 65535-175+FINETUNE ; E 659.26 175 .dw 65535-165+FINETUNE ; F 698.46 165 .dw 65535-156+FINETUNE ; F#/Gb 739.99 156 .dw 65535-147+FINETUNE ; G 783.99 147 .dw 65535-139+FINETUNE ; G#/Ab 830.61 139 ;Octave 2 ; Note Freq *8->T0 .dw 65535-131+FINETUNE ; A 440.00 131 .dw 65535-124+FINETUNE ; A#,Bb 466.16 124 .dw 65535-117+FINETUNE ; B 493.88 117 .dw 65535-110+FINETUNE ; C 523.25 110 .dw 65535-104+FINETUNE ; C#/Db 554.37 104 .dw 65535-98+FINETUNE ; D 587.33 98 .dw 65535-93+FINETUNE ; D#/Eb 622.25 93 .dw 65535-87+FINETUNE ; E 659.26 87 .dw 65535-83+FINETUNE ; F 698.46 83 .dw 65535-78+FINETUNE ; F#/Gb 739.99 78 .dw 65535-74+FINETUNE ; G 783.99 74 .dw 65535-69+FINETUNE ; G#/Ab 830.61 69 ;===================================================================== IntT1: push PSW ;important! push ACC ;We'll use ACC in this interrupt routine ;;; ;4 kHz is the interrupt's frequency at this point. ;divide frequency by 4 to get 1 ms mov A,db_Div4 jz IT1_do1ms dec db_Div4 ljmp IT1_done IT1_do1ms: mov db_Div4,#4 ;;; ;1 kHz ;Timer of n*1ms for general use IT1_1ms: mov A,db_Timer1ms jz IT1_1ms_1msOK dec db_Timer1ms IT1_1ms_1msOK: ;;; ;Playing speed (~BPM), set by command SPD. mov A,db_DivSpeed jz IT1_doSpeed dec db_DivSpeed sjmp IT1_done IT1_doSpeed: mov db_DivSpeed,db_PlaySpeed ;;; ;Is note's playing time about to end? ;16-bit subtraction with zero detect IT1_Note: mov A,db_NoteLenL jz IT1_decko ;L=0, how about H? dec db_NoteLenL ;L!=0, dec it sjmp IT1_decdone ;and we're done for now. IT1_decko: mov A,db_NoteLenH ;Do we have something in H? jnz IT1_dec ;yes, we'll dec. mov db_NoteDone,#1 ;no, note is done. sjmp IT1_decdone IT1_dec: dec db_NoteLenH ;dec H dec db_NoteLenL ;dec L, this turns it from 0 to FF IT1_decdone: ;;; IT1_done: pop ACC pop PSW reti ;===================================================================== IntT0: ;Plays instrument from internal RAM at given pitch. ;All instruments are 16 nibbles long, thus 8 bytes. Format: ;.db 0x12,0x34,0x56,0x78,0x9A,0xBC,0xDE,F0 for a sawtooth wave of 16 samples ;Calculations for FINETUNE in PerTable push PSW ;2 cycles mov TH0,db_PerH ;2 cycles mov TL0,db_PerL ;2 cycles clr TF0 ;1 cycle setb TR0 ;1 cycle ;plus interrupt jump 3 cycles -> FINETUNE is 11 cycles push ACC push B0R0 inc db_IT0Toggle mov A,db_IT0Toggle jnb ACC.0,IT0_nocpl cpl b_Melody ;play "digitally" IT0_nocpl: ;Get a nibble, that is, one sample mov R0,#db_InstrRAM ;Instrument's position in RAM mov A,db_InstrPos ;current pointer clr C rrc A ;4 bit samples, C is U/L nibble flag mov F0,C ;store for a while add A,R0 mov R0,A mov A,@R0 ;get byte, that is two nibbles mov C,F0 ;which nibble to choose? jc IT0_lower IT0_upper: swap A ;Take upper nibble IT0_lower: ;take lower nibble anl A,#0x0F ;mask out upper nibble ;Nibble-sized sample now in A. mov R0,A ;store for a while mov A,P1 ;Take P1's present value anl A,#0xF0 ;mask out lower bits orl A,R0 ;OR in new bits mov P1,A ;output ;Increment pointer mov A,db_InstrPos inc A anl A,#0x0F ;pointer runs 0-15, so this will do mov db_InstrPos,A pop B0R0 pop ACC pop PSW reti ;====================================================================== Instrument1: .db 0x7C,0xFC,0x72,0x02,0x79,0xA9,0x75,0x45 ;sine wave Instrument2: .db 0x78,0x9A,0xBC,0xDE,0xF0,0x12,0x34,0x56 ;sawtooth wave ;====================================================================== ;---------------------- ;Melody for Archy's AT89C2051 melody playing routine. ;Typed in: Archy Nov-2000 ;Deck The Halls, trad. DeckTheHalls: .dw DeckTH_INIT,DeckTH1,DeckTH1,DeckTH2,DeckTH_END,EOM DeckTH_INIT: .db EFF_INSTR1,SPD|5,EOP DeckTH1: .db OCT|2,CC|L4d,BBb|L8,AA|L4,OCT|1,G|L4 .db F|L4,G|L4,OCT|2,AA|L4,OCT|1,F|L4 .db G|L8,OCT|2,AA|L8,BBb|L8,OCT|1,G|L8 .db OCT|2,AA|L4d,OCT|1,G|L8 .db OCT|1,F|L4,E|L4,F|L2,EOP DeckTH2: .db OCT|1,G|L4d,OCT|2,AA|L8,BBb|L4,OCT|1,G|L4 .db OCT|2,AA|L4d,BBb|L8,CC|L4,OCT|1,G|L4 .db OCT|2,AA|L8,BBb|L8,CC|L4,D|L8,E|L8,F|L4 .db E|L4,D|L4,CC|L2 .db CC|L4d,BBb|L8,AA|L4,OCT|1,G|L4 .db F|L4,G|L4,OCT|2,AA|L4,OCT|1,F|L4 .db OCT|2,D|L8,D|L8,D|L8,D|L8,CC|L4d,BBb|L8 .db AA|L4,OCT|1,G|L4,F|L2,EOP DeckTH_END: .db REST|L1,EOP