.area FUSE (ABS) .org 0x3ff*2 .word ( 0x0260 | 1<<0 | 5<<2 | 1<<7 | 3<<10 ) ; reserved_bits | security_off | lvr_1v8 | io_drv_norm | boot_fast .area OSEG (OVR,DATA) notes: .ds 16 ; 0x00 .. 0x0f i0: .ds 1 ; 0x10 i1: .ds 1 ; 0x11 i2: .ds 1 ; 0x12 n: .ds 1 ; 0x13 pwm: .ds 1 ; 0x14 tmp_1: .ds 1 ; 0x15 .even ; make next two bytes word-aligned tmp_lo: .ds 1 ; 0x16 tmp_hi: .ds 1 ; 0x17 .even ; SP must be aligned stack_start: .ds 1 .area SSEG stack: .ds 1 ; aliases for memory locations: notes_ix = tmp_lo t = tmp_1 mul2 = tmp_hi mul1 = tmp_lo mod3hi = tmp_hi mod3lo = tmp_lo ; io addresses clkmd = 0x03 inten = 0x04 intrq = 0x05 tm2c = 0x1C tm2b = 0x09 tm2s = 0x17 t16m = 0x06 eoscr = 0x0A padier = 0x0D pa = 0x10 pac = 0x11 paph = 0x12 misc = 0x1B gpcc = 0x1A ihrcr = 0x0B ; Calibration Parameters: ; Bitshift Variations calls for an 8kHz sample rate; with an interrupt every ; 512 cycles (the next power of two above the 495 cycles the program needs for ; execution), this gives us a clock speed of 512 * 8khz = 4.096MHz. The MCU ; will be powered by a 3V lithium coin cell. do_calib = 1 calib_freq = 4096000 ; Hz calib_vdd = 3000 ; mV ; Clock Parameters: ; during playback: IHRC/4, WDT off, keep ILRC on active_clock = (( 0<<5 | 1<<4 | 0<<3 | 1<<2 | 0<<1 | 0<<0 )) ; during deep-sleep: ILRC/1, WDT off sleep_clock = (( 7<<5 | 1<<4 | 0<<3 | 1<<2 | 0<<1 | 0<<0 )) ; for extra power saving, consider: 6<<5|0<<3 for ilrc/4, 2<<5|1<<3 for ilrc/16 ; cycle count (worst-case) ; mod3: 28 ; g: 81 ; sample: 115 + 4*g + 2*mod3 = 495 ; isr overhead: 12 ; TOTAL: sample + overhead = 507 ; portA.4: audio out ; portA.6: debug pin .area CSEG (CODE,ABS) .org 0x0000 GOTO init .org 0x0020 GOTO interrupt .org 0x0040 ; leave some space after IVT to allow 'overprogramming' mod3: MOV a, mod3hi ADD mod3lo, a ; mod3lo = hi+lo MOV a, #0 ADDC a ; mod3hi, 1bit SWAP a MOV mod3hi, a MOV a, mod3lo SWAP a AND a, #0xf ; (mod3lo>>4) XCH mod3lo ; a=mod3lo, mod3lo=mod3lo>>4 AND a, #0xF ; a=mod3lo&0xf, mod3lo=mod3lo>>4 ADD a, mod3lo ; (mod3lo & 0xF) ADD a, mod3hi MOV mod3lo, a AND a, #0x3 ; a = (mod3lo & 0x3) SR mod3lo SR mod3lo ; (mod3lo >> 2) ADD a, mod3lo MOV mod3lo, a AND a, #0x3 ; a = (mod3lo & 0x3) SR mod3lo SR mod3lo ; (mod3lo >> 2) ADD a, mod3lo SUB a, #3 T0SN f, c ADD a, #3 RET g: CLEAR mul2 ; this is notes_ix_hi (and should be 0) AND a, #0x7 MOV notes_ix, a ; assumes notes are the first thing in SRAM ; test i2 & 3: MOV a, i2 AND a, #3 T1SN f, z SET1 notes_ix, #3 IDXM a, notes_ix MOV t, a CLEAR mul1 ; alias of notes_ix, so clear after idxm ; note: LSB of result (mul0) is not needed for our purposes ;;1/8: SR t T1SN f, c GOTO skip1 MOV a, i0 ADD mul1, a MOV a, i1 ADDC mul2, a skip1: SR mul2 SRC mul1 ;;2/8: SR t skip2: SR mul2 SRC mul1 ;;3/8: SR t T1SN f, c GOTO skip3 MOV a, i0 ADD mul1, a MOV a, i1 ADDC mul2, a skip3: SR mul2 SRC mul1 ;;4/8: SR t T1SN f, c GOTO skip4 MOV a, i0 ADD mul1, a MOV a, i1 ADDC mul2, a skip4: SR mul2 SRC mul1 ;;5/8: SR t T1SN f, c GOTO skip5 MOV a, i0 ADD mul1, a MOV a, i1 ADDC mul2, a skip5: SR mul2 SRC mul1 ;;6/8: SR t T1SN f, c GOTO skip6 MOV a, i0 ADD mul1, a skip6: SR mul1 ;;7/8: SR t T1SN f, c GOTO skip7 MOV a, i0 ADD mul1, a skip7: SR mul1 ;;8/8: SR t T1SN f, c GOTO skip8 MOV a, i0 ADD mul1, a skip8: SR mul1 MOV a, mul1 RET init: ; clock setup: SET1 clkmd, #4 ; enable IHRC MOV a, #active_clock MOV clkmd, a ; switch to IHRC ; calibration placeholder: .if do_calib AND a, #'R' AND a, #'C' AND a, #1 ; IHRC AND a, #( calib_freq ) AND a, #( calib_freq>>8 ) AND a, #( calib_freq>>16 ) AND a, #( calib_freq>>24 ) AND a, #( calib_vdd ) AND a, #( calib_vdd>>8 ) AND a, #ihrcr .else .word 0xffff, 0xffff, 0xffff, 0xffff, 0xffff .word 0xffff, 0xffff, 0xffff, 0xffff, 0xffff .endif ;stack setup: MOV a, #stack_start MOV sp, a ; portA setup: MOV a, #0x50 ; data direction: PWM & debug output, rest input MOV pac, a ; (conserves power, apparently) MOV a, #(( 1<<4 )) MOV padier, a ; disable pin wakeup, except on audio pin MOV paph, a ; enable pull-up on audio pin ; Line input typically has an impedance of 10k-100kOhm, so we need our ; pull-ups to be even higher. Since the ones of the PMS150C are ; typically 100k-220k, we can use it and don't need an external one. MOV a, #0 MOV pa, a ; PortA data = 0 ; timer2/pwm setup: ; Since (unlike in the ATTiny4 version) the interrupt timer is not tied ; to the PWM frequency, we can use a much faster clock for PWM. The ; highest "carrier frequency" for the PCM samples we can generate is by ; setting Timer2 to 6 bit, (IHRC/1)/1 mode, giving a frequency of ; (4*4.096MHz)/2^6 = 256kHz. We really use (4*4.096MHz)/2^8 = 64kHz. MOV pwm, a ; clear MOV tm2b, a ; clear MOV a, #(( 2<<4 | 3<<2 | 1<<1 | 0<<0 )) MOV tm2c, a ; timer2: IHRC, PA4, PWM, not inverted MOV a, #(( 0<<7 | 0<<5 | 0<<0 )) MOV tm2s, a ; 8bit, /4 prescaler, divide by (0+1) ;timer16/ivr setup MOV a, #(( 0<<0 | 1<<3 | 4<<5 )) ; ovf@bit8 (512cy; ยง9.2.5), clk/4, ihrc ;^xxx: could use 0b000000 syntax for compact binary values MOV t16m, a MOV a, #(1<<2) ; enable timer16 int, disable all others MOV inten, a ; misc setup: SET1 eoscr, #0 ; disable bandgap and lvr SET0 gpcc, #7 ; disable comparator ; memory setup: CLEAR i0 CLEAR i1 CLEAR i2 ;rom is not mmapped; must load notes into ram first MOV a, #0x84 MOV notes+0x0, a MOV notes+0x5, a MOV a, #0x9d MOV notes+0x1, a MOV notes+0x4, a MOV a, #0xb0 MOV notes+0x2, a MOV notes+0xA, a MOV a, #0x69 MOV notes+0x3, a MOV notes+0x6, a MOV notes+0xB, a MOV notes+0xE, a MOV a, #0x58 MOV notes+0x7, a MOV notes+0xF, a MOV a, #0x75 MOV notes+0x8, a MOV notes+0xD, a MOV a, #0x8c MOV notes+0x9, a MOV notes+0xC, a ENGINT loop: MOV a, i2 CEQSN a, #0x78 ; compare, skip next if equal ; Note: usually, this is the place where the MCU is put into some ; sort of low power/sleep mode. But the Padauk's stopexe instruction ; causes the ISR to a) run at greatly reduced frequency (100hz vs ; 1khz for timer16@bit11; probably due to slow wakeup), b) ; double-fire some (20-30%) of the time, c) jitter -50% to +10%. so ; we don't sleep at all between samples (which is only a short time ; anyways). GOTO loop ; at this point, i2==0x78, i.e. the music is finished. ; => goto halt (fallthrough) halt: DISGINT CLEAR i2 ; clear halting signal ; Note: disabling the timers isn't strictly necessary (as stopsys halts ; all timers anyways), but I'm hoping it may reduce power consumption. ; We're lucky that we only need to toggle a single bit to switch ; between the required clock source and 'off' (0010xxxx->0000xxxx for ; timer2, 100xxxxx->000xxxxx for timer16), so we can hack our way out ; of loading an immediate each time. SET0 tm2c, #5 SET0 t16m, #7 SET1 pa, #4 ; assert a high level on the audio pin for good measure SET0 pac, #4 ; ... before setting it to input mode (optional) ;switch to ilrc clock MOV a, #sleep_clock MOV clkmd, a SET0 clkmd, #4 ; disable ihrc STOPSYS ; (at this point, we wait for an i/o-toggle wake up event to resume execution) MOV a, #active_clock MOV clkmd, a ; switch to IHRC again SET1 pac, #4 ; restore output mode for audio pin ;reenable timer16, timer2 SET1 tm2c, #5 SET1 t16m, #7 ENGINT GOTO loop interrupt: PUSH af T1SN intrq, #2 ; if intrq.t16 is triggered, skip next GOTO ivr_end ;clear t16int: SET0 intrq, #2 SET1 pa, #6 ; debug ; send pwm data to timer2: MOV a, pwm MOV tm2b, a ; generate new sample: voice1: MOV a, i2; "mov mem,mem" MOV n, a; does not exist SL n SL n MOV a, i1 SWAP a AND a, #0xf SR a SR a OR n, a MOV a, n CALL g SWAP a AND a, #0x1 MOV pwm, a voice2: MOV a, i2 SL a SL a SL a MOV tmp_1, a ; fresh tmp_1: MOV a, i1 SWAP a AND a, #0xf SR a OR a, tmp_1 ; tmp_1 done. XOR a, n CALL g SR a AND a, i2 SR a AND a, #3 ADD pwm, a voice3: MOV a, i2 MOV mod3hi, a SR mod3hi SR mod3hi SR mod3hi SWAP a AND a, #0xf0 SL a MOV mod3lo, a MOV a, i1 SR a SR a SR a OR mod3lo, a CALL mod3 ADD a, n CALL g SR a SR a MOV tmp_1, a ; a saved in tmp_1; fresh a MOV a, i2 ; shift-divide by six ; note: i2 is max 0x78; so a will <= 20. (breaks for values >=128) SR a ADD a, i2 SR a SR a ADD a, i2 SR a SR a ADD a, i2 SR a SR a SR a ; end divide by six AND a, tmp_1 ; a restored from tmp_1 AND a, #3 ADD pwm, a voice4: MOV a, i2 MOV mod3hi, a SR mod3hi SR mod3hi SWAP a AND a, #0xf0 SL a SL a MOV mod3lo, a MOV a, i1 SR a SR a OR mod3lo, a CALL mod3 SUB a, n SUB a, #8 NEG a CALL g SR a MOV tmp_1, a ; a saved in tmp_1; fresh a MOV a, i2 ; shift-divide by ten ; note: i2 is max 0x78; so a will <= 12. INC i2 SR a ADD a, i2 SR a SR a SR a ADD a, i2 SR a ADD a, i2 SWAP a DEC i2 ; end divide by ten AND a, tmp_1 ; a restored from tmp_1 AND a, #3 ADD a, pwm SWAP a MOV pwm, a ; next sample is now ready. INC i0 ADDC i1 ADDC i2 SET0 pa, #6 ; debug ivr_end: POP af RETI