; ----------------------------------- ; TRANZ 330 LIBRARIES AND MOZART DEMO ; May 9, 2011 - Steve Chamberlin ; steve@bigmessowires.com ; ----------------------------------- ; --- library storage locations card_buffer = 0x8000 ; must start on a 256 byte boundary, size is 0x400 keypad_last_keycode = 0x8400 keypad_shift = 0x8401 keypad_buffer = 0x8402 ; size is 0x10 random_seed = 0x8413 ; --- mozart demo storage locations mozart_tonic = 0x8500 mozart_scale_ptr = 0x8501 ; size 2 mozart_length = 0x8503 mozart_tempo = 0x8504 mozart_proximity_ptr = 0x8505 ; size 2 mozart_rhythm_offset = 0x8507 mozart_repetition = 0x851B mozart_beat = 0x8508 mozart_octave = 0x8509 mozart_degree = 0x850A mozart_new_phrase = 0x850B mozart_is_repeat = 0x850C mozart_note_beats = 0x850D mozart_rhythm_index = 0x850E mozart_scale_index = 0x850F mozart_interval_chances = 0x8510 ; size 11 mozart_last_octave = 0x851C mozart_last_degree = 0x851D mozart_last_random_seed = 0x851E ; size 2 mozart_demo_mode = 0x8520 ; --------------------------------- ; reset ; --------------------------------- DI JP main ; --------------------------------- ; interrupt vectors ; --------------------------------- code @ 0x0018 DW card_interrupt_handler ; interrupt 18 ; --------------------------------- ; main demo program ; --------------------------------- code @ 0x0100 main: LD SP,0xFFFF ; init stack pointer CALL system_init CALL display_init CALL speaker_init LD BC,0x4C0E ; arbitrary random seed CALL random_init CALL keypad_init CALL card_init EI ; play startup beep LD C,0xFE CALL speaker_beep_period CALL speaker_beep ; print a greeting message CALL display_clear LD HL,greeting_msg CALL display_print_string LD BC,0x0BB8 CALL wait_ms mozart_prompt: ; print a prompt message CALL display_clear LD HL,prompt_msg CALL display_scroll_string LD A,0 LD (mozart_demo_mode),A LD BC,0x07D0 CALL wait_ms ; wait_ms will jump to got_card when needed JR mozart_prompt mozart_init: LD A,0 LD (mozart_beat),A LD (mozart_is_repeat),A LD A,4 LD (mozart_new_phrase),A LD A,2 LD (mozart_octave),A mozart_init_length: ; count the number of characters in the card data LD DE,card_buffer .L1 LD A,(DE) INC DE OR A JR NZ,.L1 EX DE,HL LD DE,card_buffer CCF SBC HL,DE ; HL now contains character count. In practice it will always be less than 256 LD A,L SRL L SUB A,L SRL L SRL L SUB A,L ; A = measure_count = 0.375 * count SRL A SLA A ; rounded down to the closest even number SLA A SLA A ; beat length = measure_count * 4. 39 chars->16 measures,16 chars->6 measures CP 16 JR NC,.L2 LD A,16 ; minimum length 16 beats / 4 measures .L2 LD (mozart_length),A mozart_init_key_and_mode: ; find the field separator, which marks the expiration date LD DE,card_buffer .L1 LD A,(DE) INC DE CP '=' JR Z,.L3 OR A JR Z,.L2 JR .L1 .L2 LD DE,card_buffer+7 ; field separator wasn't found- just use the account number .L3 INC DE LD A,(DE) ; A = ones digit of expiration year SUB '0' AND 0x01 JR NZ,.L4 LD HL,scale_data JR .L5 .L4 LD HL,scale_data+12 .L5 LD (mozart_scale_ptr),HL ; odd-number expiration year plays minor mode LD B,0 INC DE LD A,(DE) ; A = first digit of expiration month CP '0' JR Z,.L6 LD B,10 .L6 INC DE LD A,(DE) ; A = second digit of expiration month SUB '0' ADD A,B DEC A ; A = expiration month in 0-11 format AND 0x0F ; force A into 0-11 range (in case of unexpected data) CP 12 JR C,.L7 SUB A,12 .L7 LD (mozart_tonic),A LD (mozart_degree),A mozart_setup_random_seed: LD HL,random_seed LD DE,card_buffer+7 ; start of account number, for most cards LD A,(DE) SUB A,'.' RLD INC DE LD A,(DE) SUB A,'.' RLD INC HL INC DE LD A,(DE) SUB A,'.' RLD INC DE LD A,(DE) SUB A,'.' RLD mozart_randomize_parameters: CALL random_get_next LD A,H AND 0x07 LD (mozart_rhythm_offset),A CALL random_get_next LD A,H OR 0x80 LD (mozart_tempo),A CALL random_get_next LD A,H AND 0x03 LD (mozart_repetition),A CALL random_get_next LD A,H AND 0x03 CP 3 JR NZ,.L4 LD A,1 .L4 LD HL,proximity_data LD DE,0x000B OR A JR Z,.L5 ADD HL,DE DEC A JR .L4 .L5 LD (mozart_proximity_ptr),HL mozart_next_note: ; if CLEAR is held, cancel playback CALL keypad_read CP 0x04 JP Z,mozart_prompt LD A,(mozart_beat) AND 0x07 JR NZ,mozart_get_note_beats mozart_next_phrase: ; should we repeat the previous phrase? LD A,(mozart_beat) OR A JR Z,.L3 LD A,(mozart_new_phrase) LD HL,mozart_repetition CP (HL) ; newPhrase < repetition? JR NC,.L3 JR .L4 .L3 ; this is not a repeat phrase LD A,(mozart_octave) LD (mozart_last_octave),A LD A,(mozart_degree) LD (mozart_last_degree),A LD HL,(random_seed) LD (mozart_last_random_seed),HL LD A,0 LD (mozart_is_repeat),A JR .L5 .L4 ; this is a repeat phrase LD A,(mozart_last_octave) LD (mozart_octave),A LD A,(mozart_last_degree) LD (mozart_degree),A LD HL,(mozart_last_random_seed) LD (random_seed),HL LD A,1 LD (mozart_is_repeat),A ; choose a new rhythm .L5 CALL random_get_next RLC H RLC H LD A,H AND 0x03 LD H,A ; random 0-3 LD A,(mozart_rhythm_offset) ADD A,H AND 0x07 LD (mozart_rhythm_index),A ; choose whether this phrase should be repeated next time CALL random_get_next LD A,H AND 0x03 ; random value 0-3 LD B,A LD A,(mozart_new_phrase) LD HL,mozart_repetition CP (HL) ; newPhrase < repetition? JR NC,.L1 LD A,4 ; don't allow phrases to repeat more than once JR .L2 .L1 LD A,B .L2 LD (mozart_new_phrase),A mozart_get_note_beats: LD A,(mozart_rhythm_index) ; get the note beat count SLA A SLA A SLA A ; rhythm_index * 8 LD E,A LD A,(mozart_beat) AND 0x07 ADD A,E LD E,A ; rhythm_index * 8 + beat % 8 LD D,0x00 LD HL,rhythm_data ADD HL,DE LD A,(HL) ; rhythm data for current note LD (mozart_note_beats),A mozart_build_interval_chance_table: ; if this is the last note, resolve to the tonic LD A,(mozart_beat) LD HL,mozart_note_beats ADD A,(HL) LD HL,mozart_length CP (HL) JP Z,mozart_resolve_to_tonic ; other special cases where the pitch is predetermined LD A,(mozart_beat) OR A JP Z,mozart_play_note ; don't choose a pitch for the first note CP 8 JR NZ,.L1 ; do choose a pitch if not the 8th note LD A,(mozart_is_repeat) OR A JP NZ,mozart_play_note ; don't choose a pitch if 8th note and repeated phrase ; get the current note's index in the scale table .L1 LD D,0 LD A,(mozart_degree) LD HL,mozart_tonic SUB A,(HL) ADD A,5 ; scale index = degree - tonic - 5 + 10 JP P,.L7 ADD A,12 ; scale index %= 12 JR .L8 .L7 CP 12 JR C,.L8 SUB 12 .L8 LD (mozart_scale_index),A LD B,10 ; boundary check .L2 LD A,(mozart_octave) CP 0 ; octave 0? JR NZ,.L3 LD A,(mozart_degree) ADD A,B SUB A,5 ; A = degree + interval - 5 JR NC,.L3 ; would this interval go below the last octave? LD A,0 JR .L5 .L3 LD A,(mozart_octave) CP 3 ; octave 3? JR NZ,.L4 LD A,(mozart_degree) ADD A,B SUB A,5 ; A = degree + interval - 5 CP 12 JR C,.L4 ; would this interval go above the last octave? LD A,0 JR .L5 ; get chance for this interval .L4 LD A,(mozart_scale_index) LD E,A LD HL,(mozart_scale_ptr) ADD HL,DE LD A,(HL) ; A = scale chance for this interval OR A JR Z,.L5 LD C,A LD HL,(mozart_proximity_ptr) LD E,B ADD HL,DE LD E,(HL) ; E = proximity chance for this interval LD H,C LD L,0 CALL multiply8 ; 16 bit product is returned in HL LD A,L .L5 LD HL,mozart_interval_chances LD E,B ADD HL,DE LD (HL),A ; store the chance for this interval LD A,(mozart_scale_index) ; decrement the scale index DEC A CP 0xFF JR NZ,.L6 LD A,0x0B .L6 LD (mozart_scale_index),A DEC B JP P,.L2 mozart_choose_interval: CALL random_get_next LD C,H LD HL,mozart_interval_chances+10 LD B,10 .L2 LD A,(HL) ; get next interval chance DEC HL CP C JR NC,.L1 ; is the threshold higher than the random number? LD D,A LD A,C SUB A,D LD C,A ; reduce the random number by the threshold DJNZ .L2 JR mozart_choose_interval ; no interval was selected. pick a new random number and try again ; interval B was chosen. degree += interval - 5 .L1 LD A,(mozart_degree) ADD A,B SUB A,5 JR NC,.L3 ADD A,12 LD HL,mozart_octave DEC (HL) JR .L4 .L3 CP 12 JR C,.L4 SUB A,12 LD HL,mozart_octave INC (HL) .L4 LD (mozart_degree),A JR mozart_play_note mozart_resolve_to_tonic: ; force resolution to the tonic for the last note LD A,(mozart_tonic) LD HL,mozart_degree SUB A,(HL) ; distance to tonic. possible range (-11,11) JR NC,.L1 CP 0xFA ; negative distance: further than -6? JR NC,.L2 LD HL,mozart_octave ; further than -6, so resolve up to the next octave instead INC (HL) JR .L2 .L1 CP 6 ; positive distance: less than 6? JR C,.L2 LD HL,mozart_octave ; further than +5, so resolve down to the previous octave instead DEC (HL) .L2 LD A,(mozart_tonic) LD (mozart_degree),A mozart_play_note: LD A,(mozart_note_beats) LD E,A LD D,0 LD A,(mozart_tempo) LD H,A LD L,0 CALL multiply8 ; duration = note_beats * tempo. could use a shift instead PUSH HL CALL mozart_print_note ; setup the note parameters LD A,(mozart_degree) LD E,A LD A,(mozart_octave) LD D,A POP BC ; move HL multiply result to BC CALL play_note LD A,(mozart_beat) LD HL,mozart_note_beats ADD A,(HL) LD (mozart_beat),A LD HL,mozart_length CP A,(HL) JP C,mozart_next_note ; song is done, but we still may need to append a longer resolution note LD A,(mozart_note_beats) CP 4 JR NC,.L1 LD H,0 LD A,(mozart_tempo) LD L,A ADD HL,HL ADD HL,HL ; HL = tempo * 4 PUSH HL POP BC ; move HL to BC CALL play_note .L1 CALL display_clear LD BC,0x03E8 CALL wait_ms LD A,(mozart_demo_mode) OR A JP NZ,mozart_init JP mozart_prompt mozart_print_note: ; print the note name to the display CALL display_clear LD HL,note_names LD A,(mozart_degree) SLA A LD E,A LD D,0 ADD HL,DE LD C,(HL) CALL display_send_byte INC HL LD A,(HL) CP ' ' JR Z,.L2 LD C,A CALL display_send_byte .L2 LD A,(mozart_octave) ADD A,'0' LD C,A CALL display_send_byte RET play_note: ; octave is in D ; half-step is in E ; duration is in BC ; preserves DE PUSH DE LD A,(mozart_demo_mode) OR A JR NZ,.L3 LD HL,pitch_data LD A,D ; HL = pitch_data + octave * 24 LD DE,0x0018 ; 24 bytes per octave .L2 OR A JR Z,.L1 ADD HL,DE DEC A JR .L2 .L1 POP DE PUSH DE ; HL += half-step * 2 SLA E LD D,0x00 ADD HL,DE ; play the note LD A,(HL) ; get the prescale type INC HL OR A,0x07 ; auto-trigger, time constant follows OUT (0x12),A LD A,(HL) ; get the time constant OUT (0x12),A ; delay for note duration .L3 CALL wait_ms ; speaker off LD A,0x03 OUT (0x12),A POP DE RET wait_for_card: JR wait_for_card ; interrupt routine will jump to got_card got_card: DI ; speaker off LD A,0x03 OUT (0x12),A ; process the data CALL card_parse_bits CALL card_parse_characters ; check for parity error LD HL,card_buffer LD A,(HL) EI OR A JR Z,parity_error ; if ALPHA is held down, enter debug mode CALL keypad_read CP 0x0C JP Z,.L1 ; if BACKSPACE is held down, set the demo mode flag (loop forever, silently) CP 0x08 JR NZ,.L2 LD A,1 LD (mozart_demo_mode),A ; print a play message .L2 CALL display_clear LD HL,play_msg CALL display_print_string ; wait 1 second before starting the song LD BC,0x03E8 CALL wait_ms JP mozart_init ; display the result string, 16 characters at a time ; scroll through the result with the left/right scroll keys .L1 LD HL,card_buffer JR scroll_next scroll: CALL keypad_read OR A JR NZ,.L2 ; was a key pressed? JR scroll .L2 CP 0x0D ; scroll left? JR Z,scroll_left CP 0x0F ; scroll right? JR Z,scroll_right JR scroll scroll_left: LD DE,0xFFF0 ; go back 16 bytes ADD HL,DE JR scroll_next scroll_right: LD DE,0x0010 ; advance 16 bytes ADD HL,DE scroll_next: CALL print_current_data LD BC,0x0096 CALL wait_ms ; wait 150 ms to avoid detecting multiple keypress JR scroll print_current_data: ; print the next 16 characters beginning at (HL) EX DE,HL ; move string base pointer into DE LD HL,0x0010 ADD HL,DE ; get end pointer for next 16 characters EX DE,HL LD A,(DE) ; save the byte at this location PUSH AF LD A,0x00 LD (DE),A ; overwrite the byte with null PUSH HL CALL display_clear CALL display_print_string POP HL POP AF EX DE,HL ; move string base pointer into DE LD HL,0x0010 ADD HL,DE ; get end pointer for next 16 characters EX DE,HL LD (DE),A ; restore the original value of nulled byte RET parity_error: ; play a low beep to show the card data wasn't read successfully CALL display_clear LD HL,parity_error_msg CALL display_print_string LD C,0xFF CALL speaker_beep_period JP wait_for_card breakpoint: PUSH BC CALL display_clear LD HL,breakpoint_msg CALL display_print_string POP BC CALL display_print_hex_byte .L1 CALL keypad_read CP 4 JR NZ,.L1 ; was a key pressed? JP 0 greeting_msg: DB "MOZART'S CC",0 prompt_msg: DB "SWIPE CARD TO PLAY MUSIC",0 parity_error_msg: DB "TRY AGAIN",0 breakpoint_msg: DB "BREAKPOINT ",0 play_msg: DB "PLAYING CARD ",0 note_names: DB "C C*D E&E F F*G A&A B&B " pitch_data: ; each note is a (prescale type, time constant) pair. Prescale type 32 = prescale 256, 0 = 16. ; 12 pairs per octave ; 1 octave per line: C, D, Eb, E, F, F#, ..., B DB 32, 60, 32, 56, 32, 53, 32, 50, 32, 47, 32, 45, 32, 42, 32, 40, 32, 38, 32, 36, 32, 34, 32, 32 DB 32, 30, 32, 28, 32, 27, 32, 25, 32, 24, 32, 22, 32, 21, 32, 20, 32, 19, 32, 18, 32, 17, 0, 253 DB 0, 239, 0, 225, 0, 213, 0, 201, 0, 190, 0, 179, 0, 169, 0, 159, 0, 150, 0, 142, 0, 134, 0, 127 DB 0, 119, 0, 113, 0, 106, 0, 100, 0, 95, 0, 89, 0, 84, 32, 5, 0, 75, 0, 71, 0, 67, 0, 63 DB 0, 60, 0, 56, 0, 53, 0, 50, 0, 47, 0, 45, 0, 42, 0, 40, 0, 38, 0, 36, 0, 34, 32, 2 scale_data: ; two scale variants, each with 12 entries ; scale[x] is the relative probability of a note x half-steps above the tonic DB 10, 0, 5, 0, 8, 5, 0, 8, 0, 5, 0, 5 ; major DB 10, 0, 5, 8, 0, 5, 0, 8, 0, 5, 0, 5 ; minor proximity_data: ; three proxmity scale variants, each with 11 entries ; proximity[x] is the relative probability of the next note being x-5 half-steps from the previous one DB 3, 4, 5, 7, 10, 0, 10, 7, 5, 4, 3 ; normal DB 0, 0, 5, 7, 10, 3, 10, 7, 5, 0, 0 ; constrained DB 0, 0, 0, 7, 10, 5, 10, 7, 0, 0, 0 ; constrained, repeating rhythm_data: ; 2 measures (8 beats) per rhythm ; rhythm[x] is the length of the note beginning on beat x DB 2, 0, 2, 0, 1, 1, 2, 0 DB 1, 1, 1, 1, 2, 0, 2, 0 DB 1, 1, 1, 1, 4, 0, 0, 0 DB 2, 0, 1, 1, 4, 0, 0, 0 DB 4, 0, 0, 0, 2, 0, 2, 0 DB 1, 1, 2, 0, 1, 1, 2, 0 DB 2, 0, 2, 0, 2, 0, 2, 0 DB 2, 0, 1, 1, 2, 0, 1, 1 ; --------------------------------- ; TRANZ 330 LIBRARY ROUTINES ; --------------------------------- ; --------------------------------- ; display interface routines ; --------------------------------- display_init: IN A,(0x00) ; get current port state AND 0xEF ; clear bit 4 (display reset) OUT (0x00),A LD B,0x1C ; wait 0x1C cycles .L1 DJNZ .L1 OR 0x10 ; set bit 4 (display reset) OUT (0x00),A LD C,0xFF ; set the display duty cycle to 31 (maximum brightness) CALL display_send_byte RET display_scroll_string: ; pointer to the string is in HL ; the string must be terminated with a 0 byte LD D,H LD E,L INC DE ; DE = HL+1 LD B,0 .L1 LD A,(HL) OR A RET Z ; return if the next character is NULL LD A,B CP 16 ; 16 characters already printed? JR C,.L2 PUSH BC JR NZ,.L3 LD BC,0x03E8 ; wait 1000ms JR .L4 .L3 LD BC,0x00C8 ; wait 200ms .L4 CALL wait_ms PUSH DE CALL display_clear POP DE ; print 15 characters LD B,15 PUSH DE .L5 LD A,(DE) INC DE LD C,A PUSH BC CALL display_send_byte POP BC DJNZ .L5 POP DE INC DE POP BC .L2 LD A,(HL) ; load next character to print INC HL LD C,A PUSH BC CALL display_send_byte POP BC INC B JR .L1 display_print_string: ; pointer to the string is in HL ; the string must be terminated with a 0 byte LD A,(HL) INC HL OR A RET Z LD C,A CALL display_send_byte JR display_print_string display_print_hex_word: ; word to be printed is in DE PUSH DE LD C,D CALL display_print_hex_byte POP DE LD C,E JR display_print_hex_byte display_print_hex_byte: ; byte to be printed is in C PUSH BC LD A,C SRL A SRL A SRL A SRL A CALL display_print_hex_nibble POP BC LD A,C AND 0x0F JR display_print_hex_nibble display_print_hex_nibble: ; nibble to be printed is in A3-A0 ADD A,'0' ; add '0' CP 0x3A JR C,.L1 ; is result less than ':'? ADD A,0x07 ; make relative to 'A' .L1 LD C,A JR display_send_byte display_clear: LD C,0xAF ; set the cursor to the beginning of the line CALL display_send_byte LD D,0x10 ; 16 spaces to print .L1 LD C,' ' CALL display_send_byte DEC D JR NZ,.L1 RET display_send_byte: ; byte to be sent is in C ; note the display controller does not support lower-case letters! LD B,0x08 ; 8 bits to send .L1 IN A,(0x00) ; get current port state RLA ; rotate the port word until the data bit is in the carry flag RLA RLA RL C ; shift the next output data bit into the carry flag RRA ; rotate the port word until the data bit is in bit 5 RRA RRA OUT (0x00),A ; setup the output bit OR 0x40 ; set clock high (bit 6) OUT (0x00),A AND 0xBF ; set clock low (bit 6) OUT (0x00),A DJNZ .L1 ; continue with the next bit RET ; --------------------------------- ; speaker interface routines ; --------------------------------- speaker_init: LD A,0x03 ; disable interrupts, timer mode, prescale=16, auto-trigger, no time constant OUT (0x12),A RET speaker_beep: LD C,0x7F ; default time const = 128*16 clocks = 512us @ 4MHz clock = 1953Hz speaker_beep_period: ; beep tone period time constant should be passed in C LD A,0x07 ; disable interrupts, timer mode, prescale=16, auto-trigger, time constant follows OUT (0x12),A LD A,C ; time const OUT (0x12),A ; speaker on LD BC,0x0096 ; wait 150 ms CALL wait_ms LD A,0x03 OUT (0x12),A ; speaker off RET ; --------------------------------- ; keypad interface routines ; --------------------------------- keypad_init: LD HL,keypad_last_keycode LD (HL),0x00 LD HL,keypad_shift LD (HL),0x00 RET keypad_get_string: ; waits for the user to enter a string and press ENTER ; secondary shifted characters for each key can be obtained by pressing ALPHA ; the string is stored in keypad_buffer LD HL,keypad_shift LD (HL),0x00 LD HL,keypad_buffer LD (HL),0x00 ; initialize input buffer to empty string ; --- KEYPRESS loop .L1 CALL keypad_get_next CP 0x04 ; CLEAR key? JR Z,.L4 ; ignore it CP 0x08 ; BACKSPACE key? JR Z,.L2 CP 0x0C ; ALPHA key? JR Z,.L3 CP 0x10 ; ENTER key? JR Z,.L8 ; --- NORMAL KEYPRESS handler PUSH HL LD HL,keypad_last_keycode LD (HL),A ; remember the last regular keycode LD HL,keypad_shift LD (HL),0x00 ; reset shift .L7 CALL keycode_to_ascii POP HL LD (HL),A ; append to string INC HL LD (HL),0x00 ; add zero terminator JR .L4 ; --- BACKSPACE handler .L2 LD A,H CP high keypad_buffer JR NZ,.L5 LD A,L CP low keypad_buffer JR Z,.L4 ; ignore backspace if at start of string .L5 DEC HL ; perform backspace LD (HL),0x00 ; add zero terminator PUSH HL LD HL,keypad_shift LD (HL),0x00 ; reset shift POP HL JR .L4 ; --- ALPHA handler .L3 LD A,H CP high keypad_buffer JR NZ,.L6 LD A,L CP low keypad_buffer JR Z,.L4 ; ignore alpha if at start of string .L6 DEC HL ; perform alpha PUSH HL LD HL,keypad_shift LD A,(HL) INC A ; get new shift value AND 0x03 LD (HL),A LD HL,keypad_last_keycode LD A,(HL) JR .L7 ; replay the last keycode with the new shift value ; --- ENTER handler .L8 LD HL,keypad_shift LD (HL),0x00 ; reset shift LD BC,0x00FF ; wait 255 ms CALL wait_ms RET ; --- POST KEYPRESS handler .L4 PUSH HL LD HL,keypad_buffer CALL display_clear CALL display_print_string POP HL LD BC,0x00FF ; wait 255 ms CALL wait_ms JR .L1 keypad_get_next: ; waits for a keypress, then returns the keycode in A CALL keypad_read OR A JR Z, keypad_get_next RET keypad_read: ; read the current state of the keyboard ; returns the 1-16 keycode in A, or 0 if no key is pressed ; columns are activated by setting port 0 bits 3-0 low ; active rows are detected by sensing port 2 bits 3-0 low LD C,0xFE ; initial column mask .L1 IN A,(0x00) ; get current port state OR 0x0F ; disable all columns AND C ; activate next column OUT (0x00),A IN A,(0x02) ; read result AND 0x0F ; mask the rows CP 0x0F ; any rows active? JR NZ,keypad_read_key RLC C ; adjust column mask LD A,0xEF ; was that the last column? CP C JR NZ,.L1 ; next column LD A,0x00 ; no keys are pressed RET keypad_read_key: LD B,A ; put row data in B LD A,0x00 ; initialize key code .L4 SRA B ; shift right row data JR NC,.L3 ; was that the active row? ADD A,0x04 ; no, add 4 to key code JR .L4 .L3 SRA C ; shift right column data JR NC,.L5 ; was that the active column? INC A ; no, add 1 to key code JR .L3 .L5 INC A ; convert to 1-based key code RET keycode_to_ascii: ; keycode is in A ; result is returned in A SLA A ; multiply keycode by 4 SLA A LD HL,keypad_shift ADD A,(HL) ; add the shift value LD E,A LD D,0x00 LD HL,keycode_table-4 ADD HL,DE LD A,(HL) RET keycode_table: DB "1QZ.2ABC3DEFA 4GHI5JKL6MNOB 7PRS8TUV9WXYC *,'\"0- +#:;@D " ; --------------------------------- ; card reader routines ; --------------------------------- card_init: RET card_interrupt_handler: ; handle a card swipe EXX EX AF,AF' CALL card_read CALL speaker_beep LD SP,0xFFFF ; init stack pointer LD HL,got_card PUSH HL ; substitute got_card for the interrupt return address EX AF,AF' EXX EI RETI card_read: ; store the duration of each low-high cycle LD HL,card_buffer LD D,(high card_buffer)+0x4 LD E,0x01 LD BC,0100H CALL card_wait_for_high RET card_low: INC HL LD A,H CP D RET Z LD B,E card_wait_for_high: IN A,(C) JP M,card_high INC B LD (HL),B JR NZ,card_wait_for_high RET card_wait_for_low: IN A,(C) JP P,card_low card_high: INC B LD (HL),B JR NZ,card_wait_for_low RET card_parse_bits: ; parses the raw high-low durations in card_buffer, and converts them to bits ; all the durations should be approximately length N or 2N ; a cycle of duration 2N is a zero bit ; two successive cycles of duration N are a one bit ; the data begins with a long series of zeroes that can be used to determine the value of N ; -------------------------- LD DE,card_buffer+8 ; get the duration of the 8th cycle, and use it as the initial time duration reference LD A,(DE) LD B,A ; store the duration threshold for zero bits in B LD HL,card_buffer card_find_first_1_bit: LD A,(DE) CALL card_scale_bit_duration JR C,.L1 ; if it overflowed, it's a zero bit CP B JR C,card_get_next_bit ; is it a 1 bit? .L1 INC DE LD A,C ADD A,B RRA ; = average of old threshold and this bit's duration LD B,A ; save new threshold JR card_find_first_1_bit card_get_next_bit: LD A,(DE) INC DE ; scale the time duration before comparing it to the zero bit duration threshold CALL card_scale_bit_duration JR C,.L1 ; if it overflowed, it's a zero bit CP B ; otherwise, compare the scaled value to the zero threshold JR NC,.L1 ; is the next cycle duration less than the threshold? LD A,(DE) ; yes - it's a 1 bit INC DE ADD A,C ; sum of the two 1 bit durations ADD A,B ; plus the old duration threshold RRA ; = average of old threshold and this bit LD B,A ; save new threshold LD A,0x01 JR .L2 .L1 LD A,C ; it's a 0 bit ADD A,B RRA ; = average of old threshold and this bit's duration LD B,A ; save new threshold LD A,0x00 .L2 LD (HL),A ; store the new bit INC HL LD A,D CP (high card_buffer)+0x4 ; end of buffer? RET Z JR card_get_next_bit card_scale_bit_duration: ; a helper function used by card_parse_bits ; multiplies A by 11/8 ; original duration is passed in A, scaled duration is returned in A, and original duration is copied to C LD C,A ; save the unscaled duration SRL A ; a/2 ADD A,C ; 3a/2 RRA ; 3a/4 SRL A ; 3a/8 ADD A,C ; 11a/8 RET card_parse_characters: ; parses the raw bits in card_buffer, and converts them to a null-terminated character string ; if a parity error is detected, an empty string will be returned LD HL,card_buffer LD DE,card_buffer .L2 LD B,0x04 ; 4 data bits to per character LD C,0x00 ; store character data in C .L1 LD A,(DE) INC DE RR A ; put the bit into the carry flag RR C ; shift the carry flag into C DJNZ .L1 ; read the parity bit LD A,(DE) INC DE ; combine the data and parity bits SLA A SLA A SLA A ; parity is now in bit 3 OR C ; data is now in bits 7-4 SRL A SRL A SRL A ; data and parity are now in bits 4-0 ; count the number of 1 bits LD B,0x00 ; 1 bit count SRL A JR NC,.L3 INC B .L3 SRL A JR NC,.L4 INC B .L4 SRL A JR NC,.L5 INC B .L5 SRL A JR NC,.L6 INC B .L6 SRL A JR NC,.L7 INC B ; check the number of 1 bits: should be odd .L7 LD A,B AND 0x01 JP NZ,.L8 ; parity error: return null string LD HL,card_buffer LD A,0x00 LD (HL),A RET .L8: ; convert the data bits into a character SRL C SRL C SRL C SRL C ; data is now in bits 3-0 LD A,C ADD A,'0' ; offset from ASCII '0' LD (HL),A ; store the new character INC HL CP '?' ; was it the end sentinel? JR Z,card_parse_characters_done LD A,D CP (high card_buffer)+0x4 JR Z,card_parse_characters_done JR .L2 ; do the next character card_parse_characters_done: LD A,0x00 LD (HL),A ; null terminate RET ; --------------------------------- ; utility routines ; --------------------------------- system_init: IM 2 ; use mode 2 interrupts LD A,00H ; interrupt vectors in page 0 LD I,A ; init port 0 - controls the display (bits 6,5,4) and keypad columns (bits 3,2,1,0) ; port 1 is the control register for port 0 LD A,0xCF ; we want to control each port bit individually OUT (0x01),A LD A,0x80 ; bit 7 is input, others are outputs OUT (0x01),A LD A,0x18 ; use interrupt vector 18 OUT (0x01),A LD A,0x97 ; generate interrupt if any masked bit is low OUT (0x01),A LD A,0x7F ; mask = bit 7 OUT (0x01),A LD A,0x3F ; set the initial output values for port 0 OUT (0x00),A RET wait_ms: ; time to wait in milliseconds is in BC PUSH BC LD BC,0086H .L1 DEC BC LD A,B OR C JR NZ,.L1 POP BC DEC BC LD A,B OR C JR NZ,wait_ms RET random_init: ; random seed is in BC LD (random_seed),BC RET random_get_next: ; returns a 16-bit random number in HL LD DE,(random_seed) LD A,D LD H,E LD L,253 OR A SBC HL,DE SBC A,0 SBC HL,DE LD D,0 SBC A,D LD E,A SBC HL,DE JR NC,.L1 INC HL .L1 LD (random_seed),HL RET multiply8: ; 8-bit * 8-bit unsigned ; Input: H = Multiplier, E = Multiplicand, L = 0, D = 0 ; Output: HL = Product SLA H JR NC,.L1 LD L,E .L1 ADD HL,HL JR NC,.L2 ADD HL,DE .L2 ADD HL,HL JR NC,.L3 ADD HL,DE .L3 ADD HL,HL JR NC,.L4 ADD HL,DE .L4 ADD HL,HL JR NC,.L5 ADD HL,DE .L5 ADD HL,HL JR NC,.L6 ADD HL,DE .L6 ADD HL,HL JR NC,.L7 ADD HL,DE .L7 ADD HL,HL JR NC,.L8 ADD HL,DE .L8 RET