; synth.asm ; 3/12/1999 ; ; SYNTHESIZER CODE GENERAL INFORMATION ; There are 5 main modes of operation for the synthesizer, corresponding to the five ; primary parameters that may be set by the user. The five modes are: ; ; MODE # FUNCTION ; 0 A frequency ; 1 B frequency ; 2 A attenuation ; 3 B attenuation ; 4 relative phase ; ; from these mode values, it can be seen that if bit 2 of the MODE byte is set, we ; are in phase mode. if bit 1 of MODE is set, we are in attenuation mode. otherwise, ; we are in frequency mode. likewise, for frequency and attenuation modes, if bit 0 ; of MODE is set, the B channel is active. Otherwise, the A channel is active. These ; bit distributions are used to mask the MODE byte throughout the code to pull out ; relevant mode information. ; ; in addition to the mode byte at MODE+0, there is an additional mode byte at MODE+1. ; this second byte is needed because MODE is cycled through all possible values during ; a display refresh in order to display the settings for each mode. however, we would ; like to remember the actual mode specified by the user. Therefore, MODE+1 is used ; to hold the "real" mode during a display refresh. The store_mode and restore_mode ; functions perform a similar function, but save the user setting pointer as well. ; ; The user setting pointer PTR references the user set parameters for a given mode ; of operation. PTR is assigned the following mode-dependent values: ; ; MODE PTR ; A freq #D ; B freq #D2 ; A atten #AD ; B atten #AD2 ; phase #PHASE ; ; ************ CONSTANTS ********************** .EQU ACTIVE_FREQ_DEFAULT, 3h ; default active frequency digit .EQU ACTIVE_ATT_DEFAULT, 1h ; default active attenuator digit ; ************ AT89S8252 SFR LOCATIONS *************** .EQU WMCON, 96h ; EEPROM access control, internal watchdog .EQU SPCR, 0d5h ; serial port interface (SPI) control ; ************ PC CMDS ********************* ; these are commands passed by a PC through the RS-232 serial interface .EQU ALIVE, 10h ; is the micro alive? .EQU TEST_CMD, 0eeh ; pass me a byte sequence to test serial port .EQU MOD_REF, 20h ; modify the PLL R / N counters (disabled) .EQU ALT_SETTING, 30h ; alter a user-set freq, atten, or phase .EQU RET_SETTING, 40h ; return a user setting .EQU SET_CAL_DATA, 50h ; change calibration data .EQU CAL_RESET, 51h ; eliminate calibration data .EQU CHAN_CONTROL, 60h ; turn a channel on or off ; ************ I/O DEFINITIONS ****************** .EQU LCD_E, P3.4 ; LCD enable .EQU PLL_DATA, P2.0 ; PLL chip serial data .EQU PLL_ENB, P1.0 ; PLL chip enable .EQU PLL_CLK, P1.1 ; PLL clock .EQU EEPOT_DATA, P2.0 ; EE pot data .EQU EEPOT_CLK, P2.1 ; EE pot clock .EQU EEPOT_CS, P0.2 ; EE pot chip select .EQU DOG, P1.4 ; MAX813L watchdog input .EQU DDSB_CLK, P0.3 ; B channel DDS W_CLK .EQU DDSB_UD, P0.4 ; B channel DDS FQ_UD .EQU DDS_RESET, P0.5 ; shared DDS reset .EQU DDSA_CLK, P0.6 ; A channel DDS W_CLK .EQU DDSA_UD, P0.7 ; A channel DDS FQ_UD .EQU ATTEN_DATA, P2.3 ; shared FET array data .EQU ADIS1, P0.1 ; FET array #1 (U20) enable .EQU ADIS2, P1.7 ; FET array #2 (U19) enable .EQU ADIS3, P0.0 ; FET array #3 (U18) enable .EQU PWR_SENSE, P1.6 ; power switch sense (off / onbar) ; ************ RAM LOCATIONS *********** .EQU CMD, 10h ; serial port buffer space (22) .EQU CURSOR, 26h ; cursor location (1) .EQU K, 27h ; math scratch (12) .EQU L, 33h ; math scratch (12) .EQU M, 3fh ; math scratch (9) .EQU ACTIVE, 48h ; which digit is active? (1) .EQU POT, 49h ; 4 EE pots (4) .EQU ATTEN, 4dh ; 0-63 dB atten setting (1) .EQU FREQ, 4eh ; frequency bytes (5) .EQU STORE, 53h ; storage bytes (2) .EQU PTR, 55h ; user setting pointer [D, D2, AD, AD2] (1) .EQU MODE, 56h ; see above for mode information (2) .EQU LOCAL, 58h ; local variable for parameter passing (2) .EQU ON_OFF, 5ah ; channel status x 2: bit 0 is on/offbar, bit 1 is ; extra attenuation / normalbar (2) .EQU AD, 5ch ; attenuator digits (6): 3 + 3 for calibration .EQU AD2, 62h ; atten digits for B channel (6) .EQU PWR, 68h ; is power on or off? (1) .EQU PHASE, 69h ; phase (1) .EQU CAL, 6ah ; calibration byte (1) .EQU CALDATA, 6bh ; temporary calibration data (3) .EQU CAL_MODE, 6eh ; bit 0: cal? bit 1: A good cal? bit 2: B (1) .EQU DEBUG, 6fh ; debug bytes (6) .EQU LAST_ENC, 75h ; encoder bits from the last loop (1) .EQU FR_EQ, 76h ; are the two frequencies equal? (1) .EQU AVG_DELAY, 77h ; average delay between encoder ticks (1) .EQU B_HOLD, 78h ; is a button being held down? (1) .EQU B_LAST, 79h ; which button is being held down? (1) .EQU P2_LAST, 7ah ; holder for P2 value (1) .EQU D, 7bh ; frequency digits D0..D8, D8 MSD (9) .EQU D2, 84h ; digits for b channel (9) .EQU STACKBOT, 90h ; stack pointer ; ************ EEPROM CONSTANTS ***************** .EQU EE_USERX, 0010h ; location of first byte for userx setting storage .EQU CAL_TABLE, 0100h ; location of first byte for calibration data .EQU MAGIC, 0abh ; magic code ; ************ CURSOR LOCATIONS ***************** ; LCD cursor locations. Each line holds 20 characters. ; line 1 starts at 0h ; line 2 starts at 40h ; line 3 starts at 14h ; line 4 starts at 54h .EQU A_FREQ_CUR, 2h ; location of first A freq digit .EQU A_ATTEN_CUR, 40h ; location of first A atten character .EQU B_FREQ_CUR, 16h ; location of first B freq digit .EQU B_ATTEN_CUR, 54h ; location of first B atten character .EQU PHASE_CUR, 5eh ; location of first phase digit .EQU LINE1_CUR, 0h ; location of first line .EQU LINE2_CUR, 40h ; location of second line .EQU LINE3_CUR, 14h ; location of third line .EQU LINE4_CUR, 54h ; location of fourth line .EQU A_CALSTAR, 13h ; location of A frequency out-of-cal star .EQU B_CALSTAR, 27h ; location of B frequency out-of-cal star ; ********************************* PROGRAM START ******************************* .org 0h ljmp zero ; program execution begins at zero mark ; ********************************* INTERRUPT VECTORS *************************** .org 0bh ; timer 0 interrupt vector lcall timer0_int ; timer 0 interrupt handler reti ; return from interrupt ; ********************************* MAIN CODE *********************************** .org 20h zero: ; start of program ; ********************************* SFR ASSIGNMENTS ***************************** mov sp, #STACKBOT-1 ; initialize stack pointer mov IE, #82h ; enable timer 0 interrupt mov IP, #2 ; give timer 0 interrupt priority mov PCON, #0 ; disable power saving, don't double baud rate mov TH1, #245d ; with 10 MHz clock, generates 2400 baud mov TMOD, #21h ; timer 0 --> mode 1, timer 1 --> mode 2 mov TCON, #50h ; timer 1 on, timer 0 on mov SCON, #52h ; serial port, rx enabled, mode 1, TI set mov WMCON, #08h ; enable EEPROM access, clear RDY/BSY* mov SPCR, #0 ; disable SPI (serial-programming interface) ; ******************************** I/O INITIALIZATION *************************** mov P0, #07h ; all outputs to active high lines, except P0.2 (EE pot CS*), ; P0.0 (U18 ENB*), P0.1 (U20 ENB*) mov P1, #0cdh ; input bits: 2,3,6 / P1.0 output to PLL_ENB*. P1.7 output ; to U19 ENB*. mov P2, #0ffh ; common bus, defaults to 0xff mov P3, #0efh ; bits 0,1 special function (serial port). bits 2,3,5,6,7 ; inputs. P3.4 is LCD enable ; ******************************** RAM INITIALIZATION *************************** clr RS0 ; point to register bank 0 clr RS1 mov R0, #0 ; reset registers R0-R7 mov R1, #0 mov R2, #0 mov R3, #0 mov R4, #0 mov R5, #0 mov R6, #0 mov R7, #0 mov ACTIVE, #ACTIVE_FREQ_DEFAULT ; A channel default active digit mov PTR, #D ; point to A freq digits mov MODE, #0 ; A freq mode mov ON_OFF+0, #1 ; A channel on (normal mode) mov ON_OFF+1, #1 ; B channel on (normal mode) mov LAST_ENC, #1 ; make encoder handler wait at least 1 loop ; ******************************* BOOT PHASE ************************************ lcall init_lcd ; initialize LCD lcall pll_default ; program PLL chip for default frequency lcall pot_default ; program EE pots to default values lcall dds_init ; initialize DDS chips lcall power_init ; initial power switch analysis mov R1, #ALIVE ; pass micro is alive command with no args mov R7, #0 lcall pass ; ******************************* MAIN LOOP ************************************** main_loop: lcall tog_dog ; reset watchdog lcall power_monitor ; monitor power switch mov A, PWR ; if power switch is off, just loop jz main_loop jb RI, gc ; has a serial command been received? lcall enc_check ; check rotary encoder lcall check_buttons ; check push buttons ljmp main_loop ; loop back gc: ljmp get_cmd ; **************************** USERX ************************ ; set frequency digits to initial value ; frequencies are set to 1 kHz ; attenuations are set to -0.0 dB ; phase is set to 0 degrees init_userx: mov A, R0 push ACC mov A, R1 push ACC mov R0, #D ; A freq stored at D+0..D+8 (D+8 MSD) mov R1, #9 ix2: mov @R0, #0 inc R0 djnz R1, ix2 mov R0, #D+4 ; 1 kHz default mov @R0, #1 mov R0, #D2 ; B freq stored at D2+0..D2+8 (D2+8 MSD) mov R1, #9 ix3: mov @R0, #0 inc R0 djnz R1, ix3 mov R0, #D2+4 mov @R0, #1 mov AD+0, #0 ; A atten stored at AD+0..AD+2 (AD+2 MSD) mov AD+1, #0 mov AD+2, #0 mov AD2+0, #0 ; B atten stored at AD2+0..AD2+2 (AD+2 MSD) mov AD2+1, #0 mov AD2+2, #0 mov PHASE, #0 pop ACC mov R1, A pop ACC mov R0, A ret ; ***** DISPLAY ROUTINES ; refresh the whole screen display_all: lcall store_mode ; save the current mode mov MODE+1, MODE ; also place it in MODE+1 lcall disp1 ; display the 4 lines lcall disp2 ; lcall disp3 ; lcall disp4 lcall restore_mode ; restore the mode lcall place_cursor ; position the blinking cursor ret ; refresh whichever line is indicated by the current mode. Store the mode ; in case we are in phase mode, because displaying line 4 switches to B atten ; mode. Therefore we need restore_mode to switch back to phase mode in this ; case. display: lcall store_mode ; store the current mode mov A, MODE mov MODE+1, A ; place it in MODE+1 as well cjne A, #0, dplay2 ; A frequency mode? lcall disp1 ; if so, display line 1 ljmp dplay_done dplay2: cjne A, #1, dplay3 ; B frequency mode? lcall disp3 ; if so, display line3 ljmp dplay_done dplay3: cjne A, #2, dplay4 ; A atten mode? lcall disp2 ; if so, display line2 ljmp dplay_done dplay4: lcall disp4 ; b atten mode or phase mode, so display line 4 dplay_done: lcall restore_mode ; restore the mode lcall place_cursor ; position the blinking cursor ret ; displays line 2 info (used for lines 2 and 4) disp_2nd: mov A, R0 push ACC mov dptr, #LINE2 ; display fixed string lcall displine mov R0, #ON_OFF+0 ; point to A chan ON/OFF flag mov A, MODE ; which channel is active? anl A, #1 jz disp2_2 ; if channel B, inc R0 ; point to ON_OFF+1 (B chan ON/OFF flag) disp2_2: mov A, @R0 anl A, #1 ; if bit 0 is set, channel is ON jz c_off mov dptr, #CHAN_ON ljmp show_it c_off: mov dptr, #CHAN_OFF show_it: lcall displine ; display ON or OFF as appropriate lcall disp_atten ; now show the attenuation value pop ACC mov R0, A ret ; display line 1 on the LCD disp1: lcall line1_clear ; clear the line, reset the cursor mov dptr, #LINE1 ; point to fixed text string lcall displine ; display it mov MODE, #0 ; set the mode and ptr to A frequency mov PTR, #D lcall disp_freq ; display the frequency ret ; display line 2 on the LCD disp2: lcall line2_clear ; clear the line, reset the cursor mov MODE, #2 ; set the mode and ptr to A atten mov PTR, #AD lcall disp_2nd ; display the line ret ; display line 3 on the LCD disp3: lcall line3_clear mov dptr, #LINE3 lcall displine mov MODE, #1 mov PTR, #D2 lcall disp_freq ret ; display line 4 on the LCD disp4: lcall line4_clear mov MODE, #3 mov PTR, #AD2 lcall disp_2nd lcall disp_phase ; display phase info (NOTE: not done in disp2) ret ; display the ASCII line pointed to by dptr, which should have been set prior to ; calling this function. The ASCII line should be terminated by a NULL character. displine: mov A, R0 push ACC dispnxt: mov A, #0 ; zero offset from dptr movc A, @A+dptr ; load the character jz null ; if it's zero we're done mov R0, A ; otherwise, display it lcall send_lcd_data inc dptr ; increment data pointer mov A, CURSOR inc A ; increment the cursor field mov CURSOR, A ljmp dispnxt ; display the next character null: pop ACC mov R0, A ret ; display integer digit in R0. Use lookup table to find ASCII value for ; this integer. Assume that R0 holds a digit (i.e. < 10). Otherwise this ; function will display garbage. disp_dig: mov dptr, #DIGIT ; point to start of table mov A, R0 ; offset = digit in R0 movc A, @A+dptr ; retrieve the character mov R0, A ; and display it lcall send_lcd_data ret ; displays byte in R0 as three digits. Hides leading zeros. when a non-zero ; digit is encountered, a flag is set which indicates that further zeros ; should not be suppressed. This actually only applies to the 10's digit, ; because the 1's digit is always displayed. disp_byte: mov A, R1 push ACC mov A, R2 push ACC mov R2, #0 mov A, R0 mov R1, A ; store byte in R1 mov B, #100d ; divide by 100 to get the 100's digit div AB jz dby2 ; if it's zero, skip it mov R0, A ; otherwise, show it lcall disp_dig mov R2, #1 ; set the don't-skip-zeros flag mov A, B ; store the remainder in R1 mov R1, A dby2: mov A, R1 ; load the remaining value mov B, #10d ; divide by 10 to get the 10's digit div AB cjne R2, #0, dby4 ; if the don't-skip-zeros-flag is set, jump jz dby3 ; otherwise, if digit is zero, skip it dby4: mov R0, A ; display the digit lcall disp_dig mov A, B mov R1, A ; store remainder in R1 dby3: mov A, R1 mov R0, A ; display the remaining value lcall disp_dig mov R0, #20h ; display a blank space lcall send_lcd_data pop ACC mov R2, A pop ACC mov R1, A ret ; clear the display and reset CURSOR clearscrn: mov A, R0 push ACC mov R0, #1 ; send the clear screen command lcall send_lcd_ctrl mov CURSOR, #0 ; reset the cursor pop ACC mov R0, A ret ; clear the first display line by displaying a blank line, and reset the cursor ; to the beginning of the line line1_clear: lcall line1_cursor ; position cursor at start of line mov dptr, #BLANK ; display a blank line lcall displine lcall line1_cursor ; position cursor at start of line ret ; clear the second display line line2_clear: lcall line2_cursor mov dptr, #BLANK lcall displine lcall line2_cursor ret ; clear the third display line line3_clear: lcall line3_cursor mov dptr, #BLANK lcall displine lcall line3_cursor ret ; clear the fourth display line line4_clear: lcall line4_cursor mov dptr, #BLANK lcall displine lcall line4_cursor ret ; ***** CURSOR POSITIONING ; set the cursor position to the location held in the CURSOR RAM byte move_cursor: mov A, R1 push ACC mov A, R2 push ACC mov A, CURSOR ; load the cursor position orl A, #80h ; set bit 7 (indicates address instruction) mov R0, A ; place in R0 mov R1, #0 ; this is a command, so clear RS lcall send_lcd_cmd ; send the command byte pop ACC mov R2, A pop ACC mov R1, A ret ; position the cursor at the start of the first line line1_cursor: mov CURSOR, #LINE1_CUR lcall move_cursor ret ; position the cursor at the start of the second line line2_cursor: mov CURSOR, #LINE2_CUR lcall move_cursor ret ; position the cursor at the start of the third line line3_cursor: mov CURSOR, #LINE3_CUR lcall move_cursor ret ; position the cursor at the start of the fourth line line4_cursor: mov CURSOR, #LINE4_CUR lcall move_cursor ret ; positions the blinking cursor place_cursor: mov A, MODE anl A, #4 jnz ph_cursor ; phase mode? (bit 2 of MODE) mov A, MODE anl A, #2 jnz att_cur ; attenuation mode? (bit 1 of MODE) lcall freq_cursor ; otherwise, frequency mode ljmp cur_done att_cur: lcall atten_cursor ljmp cur_done ph_cursor: lcall phase_cursor cur_done: ret ; define START to be the initial position of the cursor ; define ACTIVE to be the active digit (in freq word 87,654,321.0) ; define FIRST to be the most significant non-zero digit of the word ; define CURSOR to be the actual cursor position fed to the LCD ; then CURSOR = START + X + (int)((X+1)/3) - Y - (int)((Y+1)/3) ; where X = (8 - ACTIVE) and Y = (8 - FIRST) ; START will be set to #A_FREQ_CUR or #B_FREQ_CUR depending on the active channel ; when this function is called, FIRST should be located at LOCAL (A chan) or LOCAL+1 ; ; EXCEPTION: when FIRST = 0, then define CURSOR = START+1 (due to decimal point) freq_cursor: mov A, R0 push ACC mov A, R1 push ACC mov A, R2 push ACC mov A, R3 push ACC mov A, MODE anl A, #1 jnz fc_bchan mov A, #A_FREQ_CUR mov R2, A ; R2 = START mov A, LOCAL mov R3, A ; R3 = FIRST (LOCAL) ljmp fc2 fc_bchan: mov A, #B_FREQ_CUR mov R2, A ; R2 = START mov A, LOCAL+1 mov R3, A ; R3 = FIRST (LOCAL+1) fc2: jz last_digit ; if FIRST = 0, then see above mov A, #8 clr C subb A, ACTIVE mov R0, A ; R0 = X mov A, #8 clr C subb A, R3 mov R1, A ; R1 = Y mov A, R0 add A, R2 clr C subb A, R1 mov R2, A ; R2 = START + X - Y mov A, R0 inc A mov B, #3 div AB add A, R2 mov R2, A ; R2 = START + X - Y + (int)((X+1)/3) mov A, R1 inc A mov B, #3 div AB mov R1, A ; hold (int)((Y+1)/3) in R1 now that Y is no longer needed mov A, R2 clr C subb A, R1 mov R2, A ; R2 = START + X - Y + (int)((X+1)/3) - (int)((Y+1)/3) mov CURSOR, A ; store in cursor set_it: lcall move_cursor ; actually set the location of the cursor pop ACC mov R3, A pop ACC mov R2, A pop ACC mov R1, A pop ACC mov R0, A ret last_digit: ; FIRST = 0, so CURSOR=START+1 mov A, R2 inc A mov CURSOR, A ljmp set_it ; set the cursor if we're in attenuator mode atten_cursor: mov A, R0 push ACC mov A, ACTIVE cjne A, #2, atc2 ; digit 2 active? mov R0, #1 ; if so, offset = 1 (due to minus sign) ljmp atc4 atc2: cjne A, #1, atc3 ; digit 1 active? mov R0, #2 ; if so, offset = 2 ljmp atc4 atc3: mov R0, #4 ; otherwise, digit 0 is active, so offset = 4 (decimal pt) atc4: mov A, MODE anl A, #1 jnz atc_bchan ; which channel is active? mov A, #A_ATTEN_CUR ljmp atc5 atc_bchan: mov A, #B_ATTEN_CUR atc5: add A, R0 ; add offset to starting position mov CURSOR, A lcall move_cursor ; set the cursor position pop ACC mov R0, A ret ; set the cursor in phase mode phase_cursor: mov A, #PHASE_CUR ; first phase digit at PHASE_CUR add A, #2 ; always an offset of 2 for cursor mov CURSOR, A lcall move_cursor ; set the cursor position ret ; ***** PUSHBUTTONS ; hardware button-checking function. Asserts rows one at a time, and polls ; columns. See hardware documentation for button row/column assignments. check_buttons: mov B_HOLD, #0 mov P2, #0ffh clr P2.0 ; row 0 jnb P3.5, b1 ; column 0 ; jnb P3.6, b2 ; column 1 jnb P3.7, b3 ; column 2 setb P2.0 clr P2.1 ; row 1 ; jnb P3.5, b4 jnb P3.6, b5 ; jnb P3.7, b6 setb P2.1 clr P2.2 ; row 2 ; jnb P3.5, b7 jnb P3.6, b8 jnb P3.7, b9 setb P2.2 clr P2.3 ; row 3 jnb P3.5, b10 jnb P3.6, b11 setb P2.3 mov P2, #0ffh ; no button was pressed; reset P2 ret bdone: mov P2, #0ffh ; some button was pressed; reset P2 lcall display ; and refresh whichever line was affected mov A, B_HOLD ; is a button being held down? jnz cb2 ; jump if so ret ; a button is being held down. repeat the last action. cb2: lcall delay ; pause mov A, P2_LAST ; reload the last P2 value mov P2, A mov A, B_LAST ; and the last button value mov R0, A ljmp button_parse ; repeat the action b1: mov R0, #1 ; button 1 pressed ljmp button_parse b2: mov R0, #2 ; button 2 pressed ljmp button_parse b3: mov R0, #3 ; button 3 pressed ljmp button_parse b4: mov R0, #4 ; button 4 pressed ljmp button_parse b5: mov R0, #5 ; button 5 pressed ljmp button_parse b6: mov R0, #6 ; button 6 pressed ljmp button_parse b7: mov R0, #7 ; button 7 pressed ljmp button_parse b8: mov R0, #8 ; button 8 pressed ljmp button_parse b9: mov R0, #9 ; button 9 pressed ljmp button_parse b10: mov R0, #10d ; button 10 pressed ljmp button_parse b11: mov R0, #11d ; button 11 pressed ljmp button_parse ; Button parsing routine. Uses R0 to determine which button is pressed. If a button ; is still held down, and the button is either the up or down arrow, enter the repeat ; mode (indicated by B_HOLD being set). If another button is being held down, wait ; until it is released before acting. button_parse: lcall tog_dog ; toggle watchdog while waiting mov A, P3 ; read columns anl A, #0e0h ; mask all but P3.5-P3.7 cjne A, #0e0h, bparse2 ; if not all set, a button is still pressed mov B_HOLD, #0 ; otherwise, set to no-repeat mode bparse4: mov A, R0 clr C subb A, #1 jz a_f_m ; A frequency mode mov A, R0 clr C subb A, #2 jz b_f_m ; B frequency mode mov A, R0 clr C subb A, #3 jz a_a_m ; A attenuation mode mov A, R0 clr C subb A, #4 jz b_a_m ; B attenuation mode mov A, R0 clr C subb A, #5 jz a_o_o ; A on/off toggle mov A, R0 clr C subb A, #6 jz b_o_o ; B on/off toggle mov A, R0 clr C subb A, #7 jz ph_m ; phase mode mov A, R0 clr C subb A, #8 jz id ; up arrow mov A, R0 clr C subb A, #9 jz sl ; left arrow mov A, R0 clr C subb A, #10d jz sr ; right arrow mov A, R0 clr C subb A, #11d jz dd ; down arrow mov A, R0 clr C subb A, #12d jz uxrst ; reset user interface combo ljmp bdone ph_m: ljmp phase_mode sl: ljmp shift_left sr: ljmp shift_right dd: ljmp dec_digit id: ljmp inc_digit a_f_m: ljmp a_f_mode a_a_m: ljmp a_a_mode b_f_m: ljmp b_f_mode b_a_m: ljmp b_a_mode b_o_o: ljmp b_onoff a_o_o: ljmp a_onoff uxrst: ljmp userx_reset bp_c: ljmp button_parse ; a button is being held down. if already in repeat mode, just return. otherwise, ; verify that the button being held down is a repeatable button (up or down arrow). ; if so, delay for a while; if the button is still pressed, enter repeat mode. bparse2: mov A, B_HOLD ; already in repeat mode? jnz bparse4 ; return if so mov A, R0 ; otherwise, which button is pressed? cjne A, #8d, bp_b ; is it the up arrow or the down arrow button? ljmp bp_d ; if not, jump back bp_b: cjne A, #11d, bp_c bp_d: mov R7, #5 ; otherwise, delay for a while bp_e: lcall delay djnz R7, bp_e mov A, P3 ; see if the button is still pressed anl A, #0e0h cjne A, #0e0h, bparse3 ljmp bparse4 ; if not, return bparse3: mov B_HOLD, #1 ; otherwise, indicate repeat mode mov A, R0 ; save the button being pressed mov B_LAST, A mov A, P2 ; and the status of P2 mov P2_LAST, A ljmp bparse4 ; ***** PUSHBUTTON HANDLERS ; A frequency button a_f_mode: mov PTR, #D ; A frequency digits at D+0..D+8 mov MODE, #0 ; A freq mode = 0 mov ACTIVE, #ACTIVE_FREQ_DEFAULT ; set default active digit lcall display_all ; refresh display ljmp bdone ; B frequency button b_f_mode: mov PTR, #D2 ; B frequency digits at D2+0..D2+8 mov MODE, #1 ; B freq mode = 1 mov ACTIVE, #ACTIVE_FREQ_DEFAULT ; set default active digit lcall display_all ; refresh display ljmp bdone ; A amplitude button a_a_mode: mov PTR, #AD ; A atten digits at AD+0..AD+2 mov MODE, #2 ; A atten mode = 2 mov ACTIVE, #ACTIVE_ATT_DEFAULT ; set default active digit lcall display_all ; refresh display ljmp bdone ; B amplitude button b_a_mode: mov PTR, #AD2 ; B atten digits at AD2+0..AD2+2 mov MODE, #3 ; B atten mode = 3 mov ACTIVE, #ACTIVE_ATT_DEFAULT ; set default active digit lcall display_all ; refresh display ljmp bdone ; switch to phase mode only if frequencies are equal phase_mode: lcall freqs_eq cjne R7, #1, pm_done mov MODE, #4 ; phase mode = 4 lcall display_all ; refresh display pm_done: ljmp bdone ; toggle A channel on / off a_onoff: mov A, ON_OFF+0 ; A chan on/off info at ON_OFF+0 anl A, #1 jz ex_aon ; if bit 0 is clear channel is currently off lcall a_off ; otherwise turn it off lcall display_all ; refresh screen ljmp bdone ex_aon: lcall a_on ; turn it on lcall display_all ; refresh screen ljmp bdone ; toggle B channel on / off b_onoff: mov A, ON_OFF+1 ; B chan on/off info at ON_OFF+1 anl A, #1 jz ex_bon ; if bit 0 is clear channel is currently off lcall b_off ; otherwise turn it off lcall display_all ; refresh screen ljmp bdone ex_bon: lcall b_on ; turn it on lcall display_all ; refresh screen ljmp bdone ; add one to the blinking digit inc_digit: mov A, MODE anl A, #4 jnz inc_ph ; phase mode? mov A, MODE anl A, #2 ; atten mode? jnz dec_att mov A, R0 ; otherwise, we're in freq mode push ACC lcall init_klm ; reset M fields mov A, #M ; point to M+0 add A, ACTIVE ; add a number of digit places equal to the active mov R0, A ; cursor location mov @R0, #1 ; put a 1 at this M digit lcall bcd_add ; then update the frequency value lcall dds_set ; and set the dds pop ACC mov R0, A ljmp bdone ; add one to the attenuation. Different routines are called depending on ; whether the active digit is 0 or 1,2. If 0 is the active digit, then ; altering the attenuation involves changing the settings of the EE pots, ; whereas if the active digit is 1 or 2, then changing the attenuation involves ; changing the FETs which control the GaAs attenuators. inc_att: mov A, ACTIVE jz asi ; if digit 0 is active, then need att_small_inc lcall inc_atten ; otherwise use inc_atten ljmp set_a asi: lcall att_small_inc set_a: lcall set_att ; actually set the attenuator ljmp bdone ; add one to the phase inc_ph: lcall inc_phase ljmp bdone ; take one away from the blinking digit dec_digit: mov A, MODE anl A, #4 jnz dec_ph ; phase mode? mov A, MODE anl A, #2 jnz inc_att ; atten mode? mov A, R0 ; otherwise, it's freq mode push ACC lcall init_klm ; reset M fields mov A, #M ; point to M+0 add A, ACTIVE ; add a number of digit places equal to the active mov R0, A ; cursor location mov @R0, #1 ; put a 1 at this M digit lcall freq_bcd_subb ; then update the frequency value lcall dds_set ; and set the dds pop ACC mov R0, A ljmp bdone ; reduce the attenuation by 1 dB. As in inc_att, use att_small_dec if digit ; 0 is active, and use dec_atten if digit 1 or 2 is active. dec_att: mov A, ACTIVE jz asd ; if digit 0 is active, use att_small_dec lcall dec_atten ; otherwise, use dec_atten ljmp set_a asd: lcall att_small_dec ljmp set_a ; actually set the attenuator ; decrement the phase value dec_ph: lcall dec_phase ljmp bdone ; shift the blinking cursor left one digit shift_left: mov A, MODE anl A, #4 jnz sl_done ; phase mode? Exit if so. mov A, MODE anl A, #2 jnz atsl ; atten mode? mov A, ACTIVE ; freq mode, load active digit inc A ; add one cjne A, #9, sl_ok ; if it's equal to 9, can't shift anymore mov A, #8 ; so reload 8 sl_ok: mov ACTIVE, A ; put back into ACTIVE ljmp bdone atsl: mov A, ACTIVE ; attenuator same as freq, except max ACTIVE value inc A ; is 2 cjne A, #3, atsl_ok mov A, #2 atsl_ok: mov ACTIVE, A sl_done: ljmp bdone ; shift the blinking cursor right one digit shift_right: mov A, ACTIVE ; load the active digit dec A ; decrement cjne A, #0ffh, sr_ok ; if we've rolled over, reload mov A, #0 ; a zero sr_ok: mov ACTIVE, A ; and replace into ACTIVE ljmp bdone ; called when buttons 5 and 6 are pressed together. Resets all user ; interface settings to their defaults. Turns both channels on, sets ; all dds and attenuators. userx_reset: lcall init_userx ; reset userx settings lcall channels_on ; both channels on lcall set_all ; set everything (includes display refresh) ljmp a_f_mode ; now act like A freq mode button was pushed ; ****************************** MODE SUPPORT ******************************** ; set the two dds channels and the two attenuator chains ; need to set MODE and PTR before each operation. Therefore, ; need to preserve the initial mode using store_mode / restore_mode ; finally, call display_all to refresh the screen set_all: lcall store_mode mov MODE, #0 mov PTR, #D lcall dds_set ; mov MODE, #1 ; mov PTR, #D2 ; lcall dds_set mov MODE, #2 mov PTR, #AD lcall set_att ; mov MODE, #3 ; mov PTR, #AD2 ; lcall set_att lcall restore_mode lcall display_all ret ; hold MODE and PTR in RAM. Used by functions which will want ; temporarily to change the mode. store_mode: mov A, MODE mov STORE+0, A mov A, PTR mov STORE+1, A ret ; restore MODE and PTR from RAM. restore_mode: mov A, STORE+0 mov MODE, A mov A, STORE+1 mov PTR, A ret ; based on MODE, set PTR and ACTIVE to their appropriate values. ; for frequency adjustment, default ACTIVE is 3. For attenuator ; adjustment, default ACTIVE is 1. ptr_adjust: mov A, MODE cjne A, #0, pa2 mov PTR, #D mov ACTIVE, #ACTIVE_FREQ_DEFAULT ljmp pa_done pa2: cjne A, #1, pa3 mov PTR, #D2 mov ACTIVE, #ACTIVE_FREQ_DEFAULT ljmp pa_done pa3: cjne A, #2, pa4 mov PTR, #AD mov ACTIVE, #ACTIVE_ATT_DEFAULT ljmp pa_done pa4: cjne A, #3, pa5 mov PTR, #AD2 mov ACTIVE, #ACTIVE_ATT_DEFAULT ljmp pa_done pa5: mov PTR, #PHASE pa_done: ret ; **************************** DDS ************************** ; when power is first applied, the DDS comes up with indeterminate values ; located in its 5 input registers. We need to replace these with known ; values before ever strobing the FQ_UD line; otherwise, a factory test ; code may accidentally entered into the device. If this happens, the DDS ; may act unpredictably, even potentially driving one of its "input" lines. ; this is probably not what we'd like to happen. So, assert the DDS reset ; line to ensure that we're pointing at the first input register, and clock ; in 5 zeros. that way, both DDS chips will be put into a known state at ; the first strobe to FQ_UD. dds_init: mov A, R0 push ACC setb DDS_RESET ; resets pointers on both DDS chips clr DDS_RESET ; (the two share a common reset line) mov R0, #0 ; need to put 0's into all registers lcall dds_bytes ; clock in the 5 bytes lcall dds_bytes lcall dds_bytes lcall dds_bytes lcall dds_bytes pop ACC mov R0, A ret ; set the active DDS chip. This is the function that is called whenever ; we want to update an output frequency. dds_set: lcall convert_freq ; bcd-to-bin the user-set digits in D+0..D+8 (D2+0..D2+8) lcall dds_prog ; program the device lcall calibrate ; calibrate based on the new frequency lcall freqs_eq ; are freqs equal? cjne R7, #1, dd2 ; jump if they are not mov A, FR_EQ ; they are equal jnz dds_set_done ; do nothing if they were equal last time lcall display_all ; otherwise, refresh screen ljmp dds_set_done ; and exit dd2: mov A, FR_EQ ; freqs are different jz dds_set_done ; do nothing if they were different last time lcall display_all ; otherwise, refresh screen dds_set_done: mov A, R7 ; store equality info for next time mov FR_EQ, A ; in FR_EQ ret ; convert bcd user-set frequency referred to by PTR into the 4-byte binary ; value which is fed to the DDS chip. The result is stored in ; FREQ+0..FREQ+3 (FREQ+3 MSB). The algorithm is as follows: ; ; 1) convert bcd frequency into binary ; 2) multiply by a floating-point scaling factor determined ; by the relation: scale = (2^32) / F-reference. ; 3) store the result. ; ; the 2^32 term in the scale factor results from the fact that the DDS ; chip has a 32-bit frequency input. For our system, ; F-reference = 130 MHz ; ; therefore, scale = 33.0382099. ; round this so that scale = 33.03821. ; ; step 2 actually contains 2 parts: multiply the frequency by ; 3303821, and then divide by 10^5. convert_freq: lcall init_klm ; reset K, L, M fields lcall bcd_to_bin ; convert frequency to binary, place in L+0..L+3 lcall load_K_M ; transfer L+0..L+3 to K+0..K+3 mov K+4, #8dh ; place scale factor in K+4..K+7 mov K+5, #69h ; scale factor = 3303821d = 0x0032698D mov K+6, #32h mov K+7, #0 lcall mult4x4 ; multiply frequency by scale factor lcall load_K_M ; reload into K+0..K+7 mov K+8, #40h ; divide by 1E6 = 0x000F4240 mov K+9, #42h mov K+10, #0fh mov K+11, #0 lcall divAB ; 4 bytes are now in L+0..L+3 lcall store_freq ; store the bytes in FREQ+0..FREQ+3 ret ; store 4-byte frequency (4 bytes in L+0..L+3) in FREQ+0..FREQ+3. ; if the B channel is active, left shift the 5-bit PHASE value and place in FREQ+4. ; otherwise, zero FREQ+4. NOTE: the 3 lowest bits of FREQ+4 will be the power ; and control bits of the DDS, but these should all be zero. See the Analog Devices ; AD9850 data sheet for more information about these 3 bits. store_freq: mov A, R0 push ACC mov FREQ+0, L+0 ; store the 4 frequency bytes mov FREQ+1, L+1 mov FREQ+2, L+2 mov FREQ+3, L+3 mov FREQ+4, #0 ; place a zero in FREQ+4 mov A, MODE ; which channel is active? anl A, #1 jz sf_done ; only add phase information if B channel is active mov A, PHASE ; load phase to accumulator mov R0, #3 ; left shift by 3 bits rot_ph_next: rl A ; shift left djnz R0, rot_ph_next ; more bits to shift? anl A, #0f8h ; mask the 3 low bits (power, control bits) mov FREQ+4, A ; store in FREQ+4 sf_done: pop ACC mov R0, A ret ; FREQ0..FREQ3 holds 4-byte frequency value. FREQ4 holds first prog. byte ; (phase, control info). dds_prog: mov A, R0 push ACC lcall dds_ud ; clocks input register vals into DDS chips, resets ptrs mov A, FREQ+4 ; control and phase info mov R0, A lcall dds_byte ; clock it in mov A, FREQ+3 ; 4 freq bytes mov R0, A lcall dds_byte mov A, FREQ+2 mov R0, A lcall dds_byte mov A, FREQ+1 mov R0, A lcall dds_byte mov A, FREQ+0 mov R0, A lcall dds_byte setb DDS_RESET ; reset both DDS chips in order clr DDS_RESET ; to sync up the two channels lcall dds_ud ; now clock the input values into the device pop ACC mov R0, A ret ; this function simultaneously strobes the update lines for the two DDS chips. ; we want to update both at the same time in order to synchronize them. ; P0.7 is DDSA_UD, and P0.4 is DDSB_UD dds_ud: mov A, P0 ; get the existing port value orl A, #90h ; set P0.7, P0.4 mov P0, A anl A, #6fh ; clear P0.7, P0.4 mov P0, A ret ; clocks the data byte in R0 into both DDS chips. dds_bytes: lcall ddsa_byte lcall ddsb_byte ret ; clocks the byte in R0 into the active DDS chip. dds_byte: mov A, MODE ; which channel is active? anl A, #1 ; call the appropriate function jz a_byte lcall ddsb_byte ret a_byte: lcall ddsa_byte ret ; programs the A channel DDS chip with the byte in R0. Asserts R0 onto P2, and ; strobes the W_CLK input on the DDS. ddsa_byte: mov A, R0 mov P2, A setb DDSA_CLK clr DDSA_CLK ret ; programs the B channel DDS chip with the byte in R0. Asserts R0 onto P2, and ; strobes the W_CLK input on the DDS. ddsb_byte: mov A, R0 mov P2, A setb DDSB_CLK clr DDSB_CLK ret ; display the nine frequency digits (8.1). save the place of the ; most significant non-zero digit in LOCAL for use by freq_cursor function disp_freq: mov A, R0 push ACC mov A, R1 push ACC mov A, R2 push ACC mov A, R7 push ACC mov A, MODE anl A, #1 ; which channel is active? jnz df4 mov LOCAL, #0 ; reset local variable for A channel ljmp df5 df4: mov LOCAL+1, #0 ; reset local variable for B channel df5: mov R7, #8 ; R7 holds the number of the digit we're looking at now mov A, PTR add A, #8 ; R1 holds pointer to mov R1, A ; D+8 (D2+8) mov R2, #0 ; flag is set at 1st non-zero digit nxt_dig: mov A, @R1 ; load the digit jz chk_flg ; if it's zero, should we display it? flg_set: cjne R2, #0, skip_local ; once R2 has been set, do not change value of LOCAL mov A, MODE anl A, #1 ; which channel is active? jnz df2 mov A, R7 ; store R7 (the most sig non-zero digit) mov LOCAL, A ; in LOCAL for use by freq_cursor (A channel) ljmp df3 df2: mov A, R7 ; store R7 in LOCAL+1 for use by freq_cursor (B channel) mov LOCAL+1, A df3: mov R2, #1 ; now that flag is set, won't skip zeroed digits skip_local: mov A, @R1 ; load the digit mov R0, A ; display it lcall disp_dig mov A, R7 ; freq is 87,654,321.0 therefore, put comma after dec A ; digits 7 and 4 i.e. when (R7-1) / 3 leaves no remainder jz flg_clr ; but not after digit 1 mov B, #3 div AB mov A, B ; move remainder to ACC jz comma ; if it's zero, we need a comma flg_clr: dec R1 ; decrement digit pointer djnz R7, nxt_dig ; loop again until we've processed digit #1 mov R0, #2eh ; now display a decimal point lcall send_lcd_data mov A, @R1 ; and the last digit mov R0, A lcall disp_dig lcall disp_cal ; displays a star if we're out of cal range pop ACC mov R7, A pop ACC mov R2, A pop ACC mov R1, A pop ACC mov R0, A ret chk_flg: mov A, R2 ; load flag jz flg_chk2 ; if flag is not yet set, go to part 2 of the check ljmp flg_set ; flag has already been set, so display the digit flg_chk2: mov A, MODE+1 ; if real mode is not equal to the mode we are currently cjne A, MODE, flg_clr ; displaying (as happens during a full screen refresh), ; then skip the digit. Otherwise, mov A, ACTIVE ; if active cursor location is beyond first non-zero clr C ; digit, we do want to show the zero subb A, R7 ; this is equivalent to saying that if ACTIVE >= R7, then jnc flg_set ; ACTIVE - R7 won't set the carry, so show the digit ljmp flg_clr ; otherwise, skip it after all comma: mov R0, #2ch ; display a comma lcall send_lcd_data ljmp flg_clr ; and jump back into the loop ; **************************** ATTEN ************************ ; GENERAL ATTENUATION INFORMATION ; there are 3 different pathways which control channel attenuation. ; 1) for 0.1 dB steps, the EE pots in U5 control the programming current to the DDS chip ; 2) the GaAs attenuators (U4, U9) provide 0-31 dB of attenuation in 1 dB steps (5 bits) ; 3) a fixed 32 dB resistive attenuator provides a 6th bit of attenuation. This ; fixed attenuator is either inserted or removed from the pathway via a pair of ; complementary reed relays. ; ; In total, the user-set attenuation can vary from 0-63.9 dB in 0.1 dB steps. ; ; the EE pots are controlled via a 3-wire serial interface. The GaAs attenuators and ; relays are controlled by 3 FET arrays (U18,U19,U20). The FETs are used to supply ; either +5 or -7.5 V to the GaAs attenuator gates. Each bit of each attenuator ; requires a differential pair of +5/-7.5 V. Therefore, there are a total of ; (5 bits * 2 FETs/bit * 2 channels) = 20 FETs needed to control the attenuators. ; Each relay (2 relays/channel * 2 channels) = 4 relays requires 1 FET to switch ; it on or off. Therefore, a total of 24 FETs is used, so therefore 3 8-FET ; array chips are needed. By the way, the FETs generating the 4 relay control ; signals (1/2 of 1 chip) swing from +5 to ground. The -7.5 V is needed only ; for the GaAs devices. ; ; use this function to set the attenuator for the active channel set_att: lcall calibrate ; must reduce attenuation at high frequencies to compensate ; for amplifier rolloff mov A, MODE ; which channel is active? anl A, #1 ; set the appropriate attenuator jnz set_a2 lcall set_a_atten ret set_a2: lcall set_b_atten ret ; R3=0 controls the A channel relays set_a_atten: mov R3, #0 lcall set_atten ret ; R3=2 controls the B channel relays set_b_atten: mov R3, #2 lcall set_atten ret ; set an attenuator. R3 is passed to this function and contains a 0 for the A channel ; or a 2 for the B channel. This value is used when setting the relays: ; the A relays are controlled by FETs #4,5 in U19 while the B relays are controlled ; by FETs #6,7 in U19. So point to #4+R3 for the first relay, and #5+R3 for the 2nd. ; ; depending on the attenuation setting, extra attenuation may be needed (see above). ; this extra is added through control of the 2 relays/channel. Since these relays ; also control the shutdown of the channel's output, we need to store the proper ; mode (normal vs. extra atten) in memory. For the A channel, bit 1 of ON_OFF+0 ; holds the mode info, while for the B channel, bit 1 of ON_OFF+1 contains the mode. set_atten: mov A, R0 push ACC mov A, R1 push ACC mov A, R2 push ACC lcall set_pots ; first, set the fine attenuation controlled by EE pots lcall convert_atten ; convert bcd digits to binary value, set ATTEN field mov R0, #ON_OFF+0 ; R0 points to ON_OFF+0 (ON_OFF+1) mov A, MODE anl A, #1 jz sa2 ; if A channel, then point to ON_OFF+0 inc R0 ; for B channel, point to ON_OFF+1 sa2: mov A, ATTEN ; first task is to set relays mov R2, A ; store atten setting in R2 clr C subb A, #32d jnc extra_on ; if atten >= 32, then need extra attenuation mov A, @R0 ; load ON_OFF+0 (ON_OFF+1) anl A, #1 ; clear bit 1 to indicate normal mode mov @R0, A ; store back in ON_OFF+0 (ON_OFF+1) jz main_aset ; if bit 0 isn't set, channel is off, so don't touch relays mov A, #4 add A, R3 mov P2, A ; point to FET #4+R3 (4 for A, 6 for B) setb ATTEN_DATA ; for normal mode, relay 0 is off clr ADIS2 setb ADIS2 mov A, #5 add A, R3 mov P2, A ; point to FET #5+R3 (5 for A, 7 for B) clr ATTEN_DATA ; for normal mode, relay 1 is on clr ADIS2 setb ADIS2 ljmp main_aset ; now, set the 5 GaAs attenuator bits extra_on: ; extra attenuation mov R2, A ; store ATTEN-32 in R2 when ATTEN >= 32 mov A, @R0 ; load ON_OFF+0 (ON_OFF+1) orl A, #2 ; set bit 1 to indicate need for extra attenuation mov @R0, A ; store back in ON_OFF+0 (ON_OFF+1) anl A, #1 jz main_aset ; if bit 0 isn't set, channel is off, so don't touch relays mov A, #4 add A, R3 mov P2, A clr ATTEN_DATA ; for extra atten, relay 0 is on clr ADIS2 setb ADIS2 mov A, #5 add A, R3 mov P2, A setb ATTEN_DATA ; for extra atten, relay 1 is off clr ADIS2 setb ADIS2 ; now we need to set the GaAs attenuators. The low 4 bits of the 5 bit attenuation ; are controlled by FETs 0-7 (2 FETs per bit) in U18 (B channel) or U20 (A channel) ; so, we loop for the first 4 bits, masking each bit in turn, and using it to set ; each FET in succession. The active channel determines which of the two chips ; actually gets strobed (this happens in the att_clk function). main_aset: mov P2, #0 ; point to FET #0 mov R1, #7 mov dptr, #MASK ; MASK table: { 80h, 40h, 20h, 10h, 08h, 04h, 02h, 01h } nbit: mov A, R2 ; load the attenuation value mov R0, A ; store in R0 for now mov A, R1 ; load the MASK table offset movc A, @A+dptr ; retrieve the appropriate mask anl A, R0 ; mask the attenuation value jnz set_adata ; set the data bit accordingly clr ATTEN_DATA ljmp aclk set_adata: setb ATTEN_DATA aclk: lcall att_clk ; clock it into the FET inc P2 ; point to the next FET cpl ATTEN_DATA ; next slot gets complemented data lcall att_clk ; clock it into the FET clr ATTEN_DATA ; clr P2.3 (ATTEN_DATA) inc P2 ; point to the first of the next pair of FETs dec R1 ; decrement the MASK table offset mov A, P2 ; if P2 = 8, we're done, because only the first 4 cjne A, #8, nbit ; bits are controlled by the first FET array mov A, R3 ; the 5th (most-sig) bit is controlled by FETs #0-1 mov P2, A ; (A channel) or #2-3 (B channel) in U19 mov A, R2 mov R0, A ; store atten value in R0 mov A, R1 ; load the table offset movc A, @A+dptr ; load the mask anl A, R0 ; mask the attenuation value jnz s_ad_2 ; set the data bit accordingly clr ATTEN_DATA ljmp aclk2 s_ad_2: setb ATTEN_DATA aclk2: clr ADIS2 ; clock U19 setb ADIS2 inc P2 ; point to the next FET cpl ATTEN_DATA ; which gets complemented data clr ADIS2 ; clock it setb ADIS2 ; we're finally done! pop ACC mov R2, A pop ACC mov R1, A pop ACC mov R0, A ret ; take the two higher order digits of the attenuator setting, convert from bcd ; to binary, and store in ATTEN. The user interface digits in AD+0..AD+2 ; (AD2+0..AD2+2) were adjusted for calibration and stored in AD+3..AD+5 ; (AD2+3..AD2+5) by the calibrate function. convert_atten: mov A, R0 push ACC mov A, R1 push ACC mov A, PTR ; point to AD+5 (AD2+5) which is the 10's digit add A, #5 mov R0, A mov A, @R0 ; load the 10's digit mov B, #10d ; multiply by 10 mul AB mov R1, A ; store in R1 dec R0 ; point to 1's digit mov A, @R0 add A, R1 ; add to previous calc mov ATTEN, A ; store in ATTEN RAM location pop ACC mov R1, A pop ACC mov R0, A ret ; clock the appropriate FET array; use MODE bit 0 to determine which channel ; is active and therefore which strobe line should be toggled. att_clk: mov A, MODE anl A, #1 jnz attclk_bchan clr ADIS1 ; A channel -> U20 setb ADIS1 ret attclk_bchan: clr ADIS3 ; B channel -> U18 setb ADIS3 ret ; add to the bcd attenuator value (either 1's or 10's digit). ; If attenuation >= 64.0, set attenution to 63.9 inc_atten: mov A, R0 push ACC mov A, PTR mov R0, A inc R0 ; point to AD+1 (AD2+1) mov A, ACTIVE cjne A, #2, incr_att2 ; is 10's digit active? ljmp incr_att3 incr_att2: ; otherwise, 1's digit is active, mov A, @R0 ; so load it inc A ; add one cjne A, #10d, no_carry ; did it roll over? incr_att3: inc R0 ; point to AD+2 (AD2+2) mov A, @R0 ; load it inc A ; add one cjne A, #10d, no_carry2 ; did it roll over? ljmp ia_fix ; it did, so just load max atten value no_carry2: mov @R0, A ; place 10's digit back into AD+2 (AD2+2) mov A, ACTIVE cjne A, #2, incr_att4 ; if the 10's digit is active, we're done ljmp inc_done incr_att4: dec R0 ; if 10's digit is not active, we carried, so reset mov @R0, #0 ; 1's digit to zero ljmp inc_done no_carry: mov @R0, A ; place 1's digit back into AD+1 (AD2+1) inc_done: mov A, PTR ; need to verify that atten <= 63.9 add A, #2 mov R0, A ; AD+2 (AD2+2) mov A, @R0 clr C subb A, #7 jnc ia_fix ; if ATTEN >= 70, fix it mov A, @R0 clr C subb A, #6 jc ia_done ; if ATTEN < 60, don't fix dec R0 mov A, @R0 ; AD+1 (AD2+1) clr C subb A, #4 jc ia_done ; if ATTEN < 64, don't fix ia_fix: mov A, PTR ; we're over the limit, so reset to 63.9 mov R0, A mov @R0, #9 ; AD+0 (AD2+0) inc R0 mov @R0, #3 ; AD+1 (AD2+1) inc R0 mov @R0, #6 ; AD+2 (AD2+2) ia_done: pop ACC mov R0, A ret ; increment the tenths attenuation digit. att_small_inc: mov A, R0 push ACC mov A, PTR mov R0, A mov A, @R0 ; AD+0 (AD2+0) inc A ; increment cjne A, #10d, asi_noc ; if it's under 10, no carry is needed mov A, #0 ; a carry is needed mov @R0, A ; so load a 0 into AD+0 (AD2+0) lcall inc_atten ; and carry (includes a max check of the atten setting) ljmp asi_done asi_noc: mov @R0, A ; move accumulator contents to AD+0 (AD2+0) asi_done: pop ACC mov R0, A ret ; decrement bcd attenuator value. Resets value to zero if it attempts ; to go negative. dec_atten: mov A, R0 push ACC mov A, PTR mov R0, A inc R0 ; AD+1 (AD2+1) mov A, ACTIVE cjne A, #2, decr_att2 ; is 10's digit active? ljmp decr_att3 decr_att2: mov A, @R0 ; load 1's digit jnz no_borrow ; if it's not zero, there's no need to borrow decr_att3: inc R0 ; AD+2 (AD2+2) mov A, @R0 ; load 10's digit jz att_zero ; if 10's digit is zero, we're pinned dec A ; otherwise, borrow one mov @R0, A ; and replace the 10's digit mov A, ACTIVE cjne A, #2, decr_att4 ; if 10's digit is active, we're done ljmp dec_done decr_att4: ; otherwise, dec R0 ; set the 1's digit to 9 (AD+1) mov @R0, #9 ljmp dec_done no_borrow: dec A mov @R0, A ; decrement units digit ljmp dec_done att_zero: ; zero all the atten digits mov @R0, #0 ; AD+2 (AD2+2) dec R0 mov @R0, #0 ; AD+1 (AD2+1) dec R0 mov @R0, #0 ; AD+0 (AD2+0) dec_done: pop ACC mov R0, A ret ; decrement the tenths attenuation digit att_small_dec: mov A, R0 push ACC mov A, PTR mov R0, A mov A, @R0 ; AD+0 (AD2+0) jnz asd_nob ; if it's not zero, no borrow is needed mov A, #9 ; a borrow is needed, so mov @R0, A ; load a 9 into AD+0 (AD2+0) lcall dec_atten ; and borrow (includes a min check of the att setting) ljmp asd_done asd_nob: dec A mov @R0, A ; AD+0 (AD2+0) asd_done: pop ACC mov R0, A ret ; set maximum GaAs attenuation in both channels. useful when power switch is ; turned off to reduce relay feedthrough in off-state at high freqs. The GaAs ; attenuators are 5-bit with 1 dB/step, so 31 dB is their maximum attenuation. max_atten: mov AD+3, #0 ; load 31.0 as atten setting for both channels mov AD+4, #1 mov AD+5, #3 mov AD2+3, #0 mov AD2+4, #1 mov AD2+5, #3 mov MODE, #2 ; set the mode to A atten mov PTR, #AD lcall set_a_atten ; and set the attenuator mov MODE, #3 ; set the mode to B atten mov PTR, #AD2 lcall set_b_atten ; and set the attenuator ret ; convert the attenuation digits AD+0..AD+2 (AD2+0..AD2+2) into a signal level in dBm. ; calculate 16.0 - (atten digits). If the result is negative, calculate ; (atten digits) - 16.0 and set the carry. In either case, the level in dBm should ; be stored in M+0..M+2. dbm_convert: mov M+0, #0 ; place 16.0 in M+0..M+2 mov M+1, #6 mov M+2, #1 lcall atten_bcd_subb1 ; calc 16.0 - (atten digits) jnc dbm_done ; if answer is negative, lcall atten_bcd_subb2 ; calculate (atten_digits - 16.0) setb C ; and set the carry dbm_done: ret ; display the signal level. convert the attenuation digits AD+0..AD+2 (AD2+0..AD2+2) ; into the signal level in dBm using dbm_convert. At this point, the signal level ; digits are located in M+0..M+2. Display these as xx.x, with a minus sign if ; the level is below 0.0 dBm and a plus sign otherwise. disp_atten: mov A, R0 push ACC mov A, R1 push ACC mov A, MODE ; which channel is active? anl A, #1 ; use to choose appropriate starting cursor location jnz da_bchan mov A, #A_ATTEN_CUR ljmp da2 da_bchan: mov A, #B_ATTEN_CUR da2: mov CURSOR, A lcall move_cursor ; set the cursor position lcall dbm_convert ; convert the attenuation into signal level jnc disp_plus ; is the level below 0.0 dBm? mov R0, #0b0h ; if so, lcall send_lcd_data ; display a minus sign ljmp da3 disp_plus: mov R0, #2bh ; otherwise, display a plus sign lcall send_lcd_data da3: mov A, M+2 mov R0, A lcall disp_dig ; display the 10's digit mov A, M+1 mov R0, A lcall disp_dig ; display 1's digit mov R0, #2eh ; display a decimal point lcall send_lcd_data mov A, M+0 mov R0, A lcall disp_dig ; display tenths digit pop ACC mov R1, A pop ACC mov R0, A ret ; ******************************* RELAY CONTROL ******************************* ; turn both A and B channels on. This is called during power_on and userx_reset ; for both of these, ON_OFF+0 and ON_OFF+1 may not have been set correctly ; in terms of normal mode vs. extra attenuation. However, both functions ; which call channels_on follow it up with a call to set_all which will ; straighten out the attenuation mode. channels_on: lcall a_on lcall b_on ret ; turn both A and B channels off. in addition, set the GaAs attenuators to their ; maximum attenuation (31 dB). this decreases the effect of capacitive feedthrough ; in the off-state of the relays at high output frequencies. channels_off: lcall a_off lcall b_off lcall max_atten ; set maximum GaAs attenuation ret ; turn off the A channel a_off: mov A, ON_OFF+0 ; ON_OFF+0 for A channel anl A, #2 ; clear bit 0 (preserve bit 1) mov ON_OFF+0, A mov P2, #4 ; FETs #4,5 in U19 control A relays setb ATTEN_DATA ; turn #4 off clr ADIS2 ; clock it in setb ADIS2 mov P2, #5 ; repeat for #5 setb ATTEN_DATA clr ADIS2 setb ADIS2 ret ; turn off the B channel b_off: mov A, ON_OFF+1 ; ON_OFF+1 for B channel anl A, #2 ; clear bit 0 (preserve bit 1) mov ON_OFF+1, A mov P2, #6 ; FETs #6,7 in U19 control B relays setb ATTEN_DATA ; turn 'em both off clr ADIS2 setb ADIS2 mov P2, #7 setb ATTEN_DATA clr ADIS2 setb ADIS2 ret ; turn on the A channel a_on: mov A, ON_OFF+0 ; ON_OFF+0 for A channel orl A, #1 ; set bit 0 mov ON_OFF+0, A anl A, #2 ; if bit 1 is set we need extra attenuation jnz aon2 ; otherwise turn it on with normal attenuation mov P2, #4 ; FETs #4,5 in U19 control A relays setb ATTEN_DATA ; for normal mode, relay 0 is off clr ADIS2 ; clock it in setb ADIS2 mov P2, #5 clr ATTEN_DATA ; and relay 1 is on clr ADIS2 setb ADIS2 ret aon2: mov P2, #4 ; turn it on with extra attenuation clr ATTEN_DATA ; for extra atten, relay 0 is on clr ADIS2 setb ADIS2 mov P2, #5 setb ATTEN_DATA ; and relay 1 is off clr ADIS2 setb ADIS2 ret ; turn the B channel on b_on: mov A, ON_OFF+1 ; ON_OFF+1 for B channel orl A, #1 ; set bit 0 mov ON_OFF+1, A anl A, #2 ; if bit 1 is set we need extra attenuation jnz bon2 ; otherwise turn it on with normal attenuation mov P2, #6 ; FETs #6,7 in U19 control B relays setb ATTEN_DATA ; for normal mode, relay 0 is off clr ADIS2 setb ADIS2 mov P2, #7 clr ATTEN_DATA ; and relay 1 is on clr ADIS2 setb ADIS2 ret bon2: mov P2, #6 ; turn it on with extra attenuation clr ATTEN_DATA ; for extra atten, relay 0 is on clr ADIS2 setb ADIS2 mov P2, #7 setb ATTEN_DATA ; and relay 1 is off clr ADIS2 setb ADIS2 ret ; **************************** EE POT *********************** ; GENERAL EE POT INFO ; ; U5 contains 4 pots. We assign 4 memory bytes, POT+0..POT+3, that ; contain the setting for each of the 4 pots. POT+0 and POT+3 correspond ; to the current programming resistor and offset resistor for the A channel ; DDS, respectively. POT+1 and POT+2 correspond to the current programming ; resistor and offset resistor for the B channel DDS, respectively. ; sets the EE pots for both channels based on the calibrated attenuation ; (i.e. user-set attenuation modified to account for HF amplifier rolloff) set_pots: lcall convert_apot ; convert the A channel setting lcall convert_bpot ; convert the B channel setting lcall prog_pots ; program the EE pots ret ; convert the calibrated attenuation's tenths digit into the EE pot setting ; for the A channel, the tenths digit is stored at AD+3. The A channel uses ; POT+0 and POT+3. For each step of the digit to change the attenuation by ; 0.1 dB, each step must change the EE pot setting by 10 places. The EE pots ; have 8-bit control; we define the operating range to be from 28d (digit is 0) ; to 118d (digit is 9). As the resistance increases, the programming current ; (and therefore signal amplitude) decreases. convert_apot: mov A, AD+3 ; load the digit mov B, #10d ; multiply by 10 to form proper 0.1 dB step mul AB add A, #28d ; add 28 so that range is from 28-118 mov POT+0, A mov POT+3, A ret ; do the same thing as convert_apot, except for the B channel. convert_bpot: mov A, AD2+3 mov B, #10d mul AB add A, #28d mov POT+1, A mov POT+2, A ret ; programs each of the 4 pots to mid-range pot_default: mov POT+0, #128d mov POT+1, #128d mov POT+2, #128d mov POT+3, #128d lcall prog_pots ret ; programs the 4 pots with the values at POT0..POT3. use R0 to hold the value ; to be set, and R1 to hold the pot #. prog_pots: mov A, R0 push ACC mov A, R1 push ACC mov R1, #0 ; R1=0 for pot 0 mov A, POT+0 ; POT+0 contains setting for pot 0 mov R0, A lcall prog_pot mov R1, #1 ; R1=1 for pot 1 mov A, POT+1 ; POT+1 contains setting for pot 1 mov R0, A lcall prog_pot mov R1, #2 ; R1=2 for pot 2 mov A, POT+2 ; POT+2 contains setting for pot 2 mov R0, A lcall prog_pot mov R1, #3 ; R1=3 for pot 3 mov A, POT+3 ; POT+3 contains setting for pot 3 mov R0, A lcall prog_pot pop ACC mov R1, A pop ACC mov R0, A ret ; progs pot #R1 with R0. Uses the 3-wire serial interface to program the chip. ; programming consists of: ; 1) enabling chip select ; 2) passing 10 bits in sequence using data and clock lines ; bit 0: high-order address bit ; bit 1: low-order address bit ; bit 2-9: 8-bit data (msb first) ; 3) disabling chip select prog_pot: mov A, R2 push ACC mov A, R7 push ACC clr EEPOT_CLK ; rising clock edge moves data clr EEPOT_CS ; enable pot chip data transfer mov A, R1 ; load pot address (2 bits) anl A, #2 ; pass the high-order bit first mov R2, A lcall potbit ; send it mov A, R1 ; reload pot address anl A, #1 ; send the low-order bit mov R2, A lcall potbit ; send it mov dptr, #MASK ; point to MASK table mov R7, #0 ; start at 0 table offset for msb nxt_pbit: mov A, R7 ; load mask offset movc A, @A+dptr ; load mask anl A, R0 ; mask the pot setting (located in R0) mov R2, A lcall potbit ; send this bit inc R7 ; increment mask table offset cjne R7, #8, nxt_pbit ; repeat until we've sent 8 bits setb EEPOT_CS ; disable pot chip data transfer pop ACC mov R7, A pop ACC mov R2, A ret ; set EEPOT_DATA (R2=0 --> 0, R2!=0 --> 1), clock the bit into the EE pot chip potbit: clr EEPOT_DATA mov A, R2 jz pclk setb EEPOT_DATA pclk: setb EEPOT_CLK clr EEPOT_CLK ret ; **************************** PHASE ************************ ; dds offers 5 phase bits corresponding to 32 possible phase values. So, ; when incrementing the phase value, roll over to 0 when phase=32. inc_phase: mov A, PHASE inc A ; add 1 to the phase value cjne A, #32d, ph_ok ; roll over if phase=32 mov A, #0 ph_ok: mov PHASE, A lcall phase_update ; update the 2 dds channels ret ; decrement the phase value dec_phase: mov A, PHASE jnz dec_ph2 ; is phase=0? mov A, #31d ; if so, roll over to phase=31 ljmp dec_ph3 dec_ph2: dec A ; otherwise, simply decrement phase value dec_ph3: mov PHASE, A lcall phase_update ; update the 2 dds channels ret ; applies a change in relative phase. The first dds channel is always set with a phase ; of 0. The second dds channel is always set with a phase equal to the PHASE RAM byte. ; so, to update the relative phase, just need to set the second dds channel. phase_update: mov PTR, #D2 ; set MODE and PTR for B freq mode mov MODE, #1 lcall dds_set ; and set the dds mov MODE, #4 ; back to phase mode ret ; determine if the two user-set frequencies are equal. if they are, ; (D+0..D+8 = D2+0..D2+8), then set R7 = 1. Otherwise, set R7 = 0. freqs_eq: mov A, R0 push ACC mov A, R1 push ACC mov A, R2 push ACC mov A, R3 push ACC mov R2, #9 ; 9 digits to compare mov A, #D+0 mov R0, A ; R0 points to D+0 mov A, #D2+0 mov R1, A ; R1 points to D2+0 comp_nxt: mov A, @R0 mov R3, A ; hold digit in R3 mov A, @R1 ; load digit from other channel clr C subb A, R3 ; subtract the first digit jnz f_not_eq ; if digits are different, so are freqs inc R0 ; point to next two digits inc R1 djnz R2, comp_nxt ; more digits to compare? mov R7, #1 ; freqs ARE equal. set R7=1 feq_done: pop ACC mov R3, A pop ACC mov R2, A pop ACC mov R1, A pop ACC mov R0, A ret f_not_eq: mov R7, #0 ; freqs are different. set R7=0 ljmp feq_done ; displays the relative phase between the two channels in degrees. Since the first ; channel is always set with a phase of 0, the phase of the second channel is the ; value to be displayed. There are 32 phase steps (5 bits), so each corresponds to ; 360/32 = 11.25 degrees. Here, we use a lookup table for displaying the 32 possible ; values for no particular reason. Each table entry has five characters (xxx.x) plus ; a null character. So, to generate the proper table index, multiply the phase value ; by 6. only display phase if two frequencies are the same. disp_phase: mov A, R0 push ACC lcall freqs_eq ; are freqs equal? cjne R7, #1, disp_ph_done ; if freqs aren't equal, don't display phase mov A, #PHASE_CUR ; first phase character cursor location mov CURSOR, A lcall move_cursor ; set the cursor position mov A, PHASE ; table offset = PHASE * 6 mov B, #6d mul AB mov R0, A ; hold offset in R0 mov dptr, #PHASE_TABLE ; first table entry at PHASE_TABLE jz dph3 ; if offset is zero, don't increment at all dph2: inc dptr ; otherwise, increment by R0 bytes djnz R0, dph2 dph3: lcall displine ; display the phase characters mov R0, #0dfh ; degree symbol lcall send_lcd_data disp_ph_done: pop ACC mov R0, A ret ; **************************** LCD ****************************** ; GENERAL LCD INFO ; ; we are using the LCD in 4-bit transfer mode, which means that only 4 of the ; 8 LCD data lines are actually used. In 4-bit mode, lines DB7..DB4 are used, ; and here they are tied to P2.3..P2.0 of the micro. During normal operation, ; the LCD's busy flag is used to time data transfers to the display. However, ; this feature is disabled during LCD initialization, so a special procedure ; must be followed. See the Optrex DMC-series LCD manual or below for more ; information. ; ; The LCD has 3 control lines in addition to the 4 data lines: RS, R/Wbar, and ; Enable. RS and R/Wbar have been assigned to P2.4 and P2.5 respectively. ; initializes the LCD after power is applied, when micro boots. The initialization ; procedure detailed here is taken from the Optrex DMC-series LCD manual. The delay ; calls all result in approximately 40 ms of dead time. During normal operation, the ; LCD provides a busy flag indicating whether the display is ready to accept a new ; command. But before the LCD has been properly initialized, it does not support ; use of the busy flag. Therefore, we must have an open-loop delay in between each ; command. We delay about 40 ms to maintain a conservative factor of two safety ; margin from the longest required delay after powerup. init_lcd: mov A, R0 push ACC lcall tog_dog ; toggle the watchdog before we begin lcall delay mov R0, #3 ; pass init code three times, lcall blind_ctrl8 ; delaying between each lcall delay mov R0, #3 lcall blind_ctrl8 lcall delay mov R0, #3 lcall blind_ctrl8 lcall delay mov R0, #2 ; now send a command to switch to 4-bit interface lcall blind_ctrl8 lcall delay ; we are now in 4-bit mode. send a command specifying mov R0, #28h ; 4-bit transfer mode (redundant), 1/16 duty cycle, and lcall blind_ctrl4 ; 5x7 font size lcall tog_dog ; keep the watchdog at bay lcall delay mov R0, #0dh ; send command specifying display on, cursor off, lcall blind_ctrl4 ; and blink on lcall delay mov R0, #06h ; send command specifying cursor increment and no lcall blind_ctrl4 ; display shift lcall delay mov R0, #1 ; send command to clear the screen lcall blind_ctrl4 lcall tog_dog ; toggle the dog once more pop ACC mov R0, A ret ; called when the soft power switch is shut off shutdown_lcd: lcall clearscrn ; clear the screen mov R0, #08h ; turn display off lcall send_lcd_ctrl ret ; called when the soft power switch is turned on resume_lcd: mov R0, #0dh ; activate the display (with cursor off, blink on) lcall send_lcd_ctrl mov R0, #1 ; clear the screen lcall send_lcd_ctrl ret ; sends the byte in R0 to the LCD. The byte is sent in two 4-bit pieces; ; the high nybble is sent first. After sending the data, the function ; holds until the ready flag indicates that the LCD is ready to receive ; another command. R1 should already contain the desired value of RS; this ; information is processed by the send_nyb function. send_lcd_cmd: mov A, R0 ; load the data byte mov R2, A ; store the byte in R2 anl A, #0f0h ; mask the low nybble mov R0, #4 ; rotate high nybble into low nybble rot2: rr A ; to align with P2.0..P2.3 djnz R0, rot2 mov R0, A ; send the high nybble lcall send_nyb mov A, R2 ; reload the data byte anl A, #0fh ; mask the high nybble mov R0, A ; send the low nybble lcall send_nyb rd_test: ; now poll the ready flag until the LCD isn't busy mov R1, #0 ; set R1 in order to clear RS lcall read_nyb ; read the first nybble mov A, R0 mov R2, A ; store in R2 lcall read_nyb ; read the second to complete the instruction cycle mov A, R2 ; reload the first received nybble anl A, #8h ; mask all but d3 jnz rd_test ; jump back if set (indicates LCD is still busy) ret ; send the command byte in R0 to the LCD. Sending a command means that ; RS=0. So, clear R1 before calling send_lcd_cmd send_lcd_ctrl: mov A, R1 push ACC mov A, R2 push ACC mov R1, #0 ; flag indicating RS to be cleared lcall send_lcd_cmd ; send the byte pop ACC mov R2, A pop ACC mov R1, A ret ; send the data byte in R0 to the LCD. Sending data means that ; RS=1. So, set R1 before calling send_lcd_cmd send_lcd_data: mov A, R1 push ACC mov A, R2 push ACC mov R1, #1 ; flag indicating RS to be set lcall send_lcd_cmd ; send the byte pop ACC mov R2, A pop ACC mov R1, A ret ; sends the lower nybble of R0 to the LCD. RS is set to the value passed in R1 send_nyb: mov A, R0 ; load the data cjne R1, #1, clr_rs ; if R1 != 1, clear RS orl A, #10h ; set RS (P2.4) ljmp start clr_rs: anl A, #0efh ; clear RS (bit 4) start: anl A, #01fh ; clear R/WBAR (P2.5), bits 6-7 as well mov P2, A ; assert the data setb LCD_E ; need to hold for 450 ns clr LCD_E ; 1 cycle is about 1 us mov P2, #0ffh ; reset the common port ret ; reads 4 data bits from the LCD. Prior to the read, RS is set to the value ; passed in R1. The data nybble is returned in R0. read_nyb: mov A, #0fh ; P2.0..P2.3 will be inputs cjne R1, #1, clr_rs2 orl A, #10h ; set RS ljmp start2 clr_rs2: anl A, #0efh ; clear RS start2: orl A, #20h ; set R/WBAR (P2.5) mov P2, A ; assert on port setb LCD_E ; enable lcd mov A, P2 ; read the port clr LCD_E ; disable lcd anl A, #0fh ; mask high nybble (non-data lines) mov R0, A ; place in R0 mov P2, #0ffh ; reset the common port ret ; send the value in R0 to the LCD without checking busy flag. follow 8-bit interface ; format, i.e. only one enable needed. blind_ctrl8: mov A, R1 push ACC mov R1, #0 ; R1=0 means that RS=0 lcall send_nyb ; send the nybble pop ACC mov R1, A ret ; send the value in R0 to the LCD without checking busy flag. Follow 4-bit interface, ; i.e. pass the upper nybble of R0 first, then the lower nybble (2 enables). blind_ctrl4: mov A, R1 push ACC mov A, R2 push ACC mov R1, #0 ; R1=0 means that RS=0 mov A, R0 ; load the data byte mov R2, A ; store in R2 for now anl A, #0f0h ; mask the low nybble mov R0, #4 bc2: rr A ; shift high nybble into low nybble djnz R0, bc2 ; to align with P2.0..P2.3 mov R0, A lcall send_nyb ; send the nybble mov A, R2 ; reload the data byte anl A, #0fh ; mask the high nybble this time mov R0, A lcall send_nyb ; send it pop ACC mov R2, A pop ACC mov R1, A ret ; **************************** ROTARY ENCODER ************************** ; checks for rotary encoder action. The 2 encoder bits must both have been low ; on the last loop, otherwise no action is taken if 1 of the lines goes high. enc_check: mov A, LAST_ENC ; load previous encoder state jnz enc_done ; were both bits zero? Exit if not jb P3.2, enc_jmp1 ; cw rotation if this bit is high jb P3.3, enc_jmp2 ; ccw rotation if this bit is high enc_done: mov A, P3 ; load P3 anl A, #0ch ; mask all but 2 encoder bits (P3.2, P3.3) mov LAST_ENC, A ; store for next loop enc_ret: ret ; the encoder has been turned cw ; set LAST_ENC to non-zero to ensure that there ; will be no further encoder action until the encoder ; returns to its nominal 00 state. enc_jmp1: lcall enc1 mov LAST_ENC, #1 ljmp enc_ret ; the encoder has been turned one notch clockwise enc1: mov A, MODE anl A, #4 ; phase mode? jnz phs_up mov A, MODE anl A, #2 ; atten mode? jnz ia_down ; otherwise, we're in freq mode, so lcall avg_update ; update the avergage encoder delay time lcall inc_freq ; calc acceleration factor and increase the frequency lcall dds_set ; set the dds lcall display ; and update the display ii1: ret ; the encoder has been turned ccw enc_jmp2: lcall enc2 mov LAST_ENC, #1 ljmp enc_ret ; the encoder has been turned one notch counter-clockwise enc2: mov A, MODE anl A, #4 ; phase mode? jnz phs_down mov A, MODE anl A, #2 ; atten mode? jnz ia_up ; otherwise, we're in freq mode, so lcall avg_update ; update the average encoder delay time lcall dec_freq ; calc acceleration factor and decrease the frequency lcall dds_set ; set the dds lcall display ; and update the display ii2: ret ; increase the active attenuator digit by 1 ia_up: mov A, ACTIVE jz iau2 ; if digit 0 is active, then need att_small_inc lcall inc_atten ; otherwise use inc_atten ljmp iau3 iau2: lcall att_small_inc iau3: lcall set_att ; actually set the attenuator lcall display ; and update the display ljmp ii1 ; increase the relative phase by 1 (of 32 phase levels) phs_up: lcall inc_phase ; increment the phase lcall display ; and update the display ljmp ii1 ; decrease the active attenuator digit by 1 ia_down: mov A, ACTIVE jz iad2 ; if digit 0 is active, use att_small_dec lcall dec_atten ; otherwise, use dec_atten ljmp iad3 iad2: lcall att_small_dec iad3: lcall set_att ; actually set the attenuator lcall display ; and update the display ljmp ii2 ; decrease the relative phase by 1 (of 32 levels) phs_down: lcall dec_phase ; decrement the phase lcall display ; and update the display ljmp ii2 ; AVG_DELAY holds an averaged delay value which is updated every time the encoder ; is rotated to a new position. AVG_DELAY is reset when timer 0 overflows, which ; happens after approximately 80 ms without the encoder being rotated. The ; smaller AVG_DELAY is, the greater the change in frequency will be. ; ; this function takes the high byte of timer 0, and uses this value to ; update the average delay held in AVG_DELAY by the following relation: ; new delay value = (15/16 old avg) + (1/16 new value). ; it then resets the 80 ms timer. ; avg_update: mov A, R0 push ACC mov A, AVG_DELAY cjne A, #0ffh, avg ; if AVG_DELAY was at its max value, dec A ; just reduce the average mov AVG_DELAY, A ; by one and return. This ensures that acceleration will ljmp ovr ; only occur after the third consecutive click avg: clr TR0 ; otherwise, stop the time mov A, TH0 ; load the high timer byte mov R0, A ; and store in R0 mov TH0, #0 ; reset the high and low timer bytes mov TL0, #0 setb TR0 ; and restart the timer mov A, AVG_DELAY ; load the old delay value mov B, #16d div AB mov B, #15d mul AB ; compute 15/16 * old_value mov AVG_DELAY, A ; store back in AVG_DELAY mov A, R0 mov B, #16 div AB ; compute 1/16 * new_value clr C add A, AVG_DELAY ; add to AVG_DELAY mov AVG_DELAY, A ; store back as new delay value ovr: pop ACC mov R0, A ret ; this is the timer 0 overflow interrupt handler. It resets the average delay to ; its rest state, i.e. after the encoder has not been rotated in a while. timer0_int: clr TR0 ; stop the timer mov TH0, #0 ; reset high and low timer bytes mov TL0, #0 setb TR0 ; restart the timer mov AVG_DELAY, #0ffh ; reset the average delay value to maximum ret ; increases the active frequency by an amount determined by the cursor location ; and the acceleration algorithm. It also zeroes any digits to the right of the ; active cursor location. inc_freq: lcall delta ; compute the frequency delta lcall bcd_add ; alter the frequency lcall zero_post ; zero digits to the right of the active cursor position ret ; the same as inc_freq, except the frequency is decreased rather than increased dec_freq: lcall delta lcall freq_bcd_subb lcall zero_post ret ; calculates the amount by which to alter the frequency based on the active freq ; digit and the acceleration algorithm (which uses the average encoder delay time ; stored in AVG_DELAY). The resulting frequency delta is stored in M+0..M+8 for use ; by the bcd_add or bcd_subb function which follows the execution of this function. ; ; the active freq digit determines the storage location of the 4 bytes retrieved ; from the table. delta: mov A, R0 push ACC mov A, R1 push ACC mov A, R7 push ACC lcall init_klm ; reset M fields to zero lcall table_lookup ; stores table byte offset in R7 mov A, ACTIVE ; load the # of the active freq digit add A, #M ; use to point to the appropriate M digit mov R0, A ; place M+ACTIVE pointer into R0 mov dptr, #INC_TABLE ; data pointer set at start of acceleration table mov R1, #4 ; 4 bytes to retrieve from table nxt_inc_dig: mov A, R7 ; load the table offset movc A, @A+dptr ; retrieve byte from table mov @R0, A ; store in M field inc R0 ; point to next M digit inc R7 ; increment table offset cjne R0, #M+9, not_over ; we can only modify M+0..M+8 ljmp del_done not_over: djnz R1, nxt_inc_dig ; more bytes to retrieve? del_done: pop ACC mov R7, A pop ACC mov R1, A pop ACC mov R0, A ret ; generates acceleration lookup table offset based on the average encoder delay ; time stored in AVG_DELAY. The acceleration table has 16 entries while AVG_DELAY ; is a 1 byte quantity. Therefore, to generate the appropriate table entry, ; first complement AVG_DELAY and then divide by 16. Each table entry consists of ; 4 bytes, so multiply the table entry number by 4 to form the table offset ; in bytes. Store the offset in R7. table_lookup: mov A, AVG_DELAY ; load the average delay cpl A ; generate complement (255 - AVG_DELAY) mov B, #16d div AB ; divide by 16, give table index from 0-15 mov B, #4 mul AB ; multiply by 4 to give table byte offset mov R7, A ; hold in R7 ret ; called after the frequency is updated. This function zeros any frequency digits ; to the right of the active digit, i.e. less significant digits. zero_post: mov A, R0 push ACC mov A, R1 push ACC mov A, R2 push ACC mov A, ACTIVE ; load active digit # jz zover ; if the lsd is active, we're done mov R2, ACTIVE ; otherwise, store in R2 dec R2 ; reduce by 1 (we don't want to zero the active digit) mov A, PTR ; point to active frequency digits with R0 mov R0, A nxt_zero: mov A, R0 ; load pointer add A, R2 ; add R2 offset mov R1, A ; hold in R1 mov @R1, #0 ; zero this digit mov A, R2 ; load offset jz zover ; if ACTIVE was 1 initially, need to exit now djnz R2, nxt_zero ; more digits to zero? mov @R0, #0 ; zero the lsd zover: pop ACC mov R2, A pop ACC mov R1, A pop ACC mov R0, A ret ; ********************************** PLL *********************************** ; load the default N and R values into the PLL chip. The default values are ; N = 50 and R = 650. With a 10 MHz reference, these N and R values give ; a 200 kHz PLL update frequency and a 130 MHz VCO output frequency, and ; therefore a 130 MHz DDS frequency. pll_default: mov CMD+2, #0h ; N = 0x0032 = 50d mov CMD+3, #32h mov CMD+4, #2h ; R = 0x028A = 650d mov CMD+5, #8ah lcall pll_load ; load these values into the PLL chip ret ; load the R (CMD+2[MSB], CMD+3) and N (CMD+4[MSB], CMD+5) values into PLL chip. ; in addition, a command byte is loaded, whose default value is 0x7C. This command ; byte specifies: ; - no inversion of PDout ; - enables PDout (A phase detector), disables B detector ; - enables lock detect output ; - sets REFout frequency = 1/16 of clock reference (1/16 * 10 MHz = 625 kHz) ; - disables Fv and Fr outputs ; ; the PLL chip uses its "BitGrabber" technology to determine whether the data ; being clocked into it is a command byte, an N value, or an R value. The ; number of clocks identifys the byte. 8 clocks specify the command byte, ; 15 clocks specify the R value, and 16 clocks specify the N value. pll_load: mov A, R0 push ACC mov A, R1 push ACC mov A, R2 push ACC lcall pll_reset ; reset the PLL chip mov R0, #8d ; 8 bytes for the control byte mov R1, #7ch ; control = 0x7C (see above) lcall send_pllcmd ; send it mov R0, #CMD+2 ; point to R MSB mov A, @R0 ; load it mov R1, A ; store in R1 inc R0 ; point to R LSB mov A, @R0 ; load it mov R2, A ; store in R2 mov R0, #15d ; 15 clocks for R value lcall send_pllcmd ; send it mov R0, #CMD+4 ; point to N MSB mov A, @R0 ; load it mov R1, A ; store in R1 inc R0 ; point to N LSB mov A, @R0 ; load it mov R2, A ; store in R2 mov R0, #16d ; 16 clocks for N value lcall send_pllcmd ; send it pop ACC mov R2, A pop ACC mov R1, A pop ACC mov R0, A ret ; clocks R0 bits into the PLL chip. The data is stored in R1 and in R2 if ; R0 > 8. In this case, the first byte passed is the one stored in R1. ; data transfers to the PLL chip are little-endian, so the byte stored in R1 ; should be the MSB. send_pllcmd: mov A, R7 push ACC mov A, R0 ; load the number of clocks mov R7, A ; store in R7 clr PLL_ENB ; enable the PLL chip cjne R7, #15d, one ; if R7 != 15, not an R value mov R0, #7 ; otherwise, send 7 bits of the value in R1 lcall send_pll_byte mov A, R2 ; load the LSB from R2 mov R1, A ; store in R1 one: mov R0, #8 ; send all 8 bits of R1 lcall send_pll_byte cjne R7, #16d, plldone ; if R7 != 16, that was the command byte, and we're done mov A, R2 ; otherwise, load the LSB from R2 mov R1, A ; into R1 mov R0, #8 ; and send all 8 bits (16 clocks for the N value) lcall send_pll_byte plldone: setb PLL_ENB ; disable the PLL chip pop ACC mov R7, A ret ; send R0 bits to the PLL. The data is stored in R1. The data transfer is ; little-endian, so the MSb goes first. If R0 < 8, then the bits in R1 ; are assumed to be LEFT-JUSTIFIED!!! This is probably stupid, but the ; only situation where R0 isn't 8 bits is for the MSB of the R value, and ; the default R value is 0x0032, so the MSB is just zero. send_pll_byte: mov A, R7 push ACC mov R7, #0 ; masking table offset set to zero mov dptr, #MASK ; point to first mask table entry send_next: mov A, R7 ; load the table offset movc A, @A+dptr ; load the mask anl A, R1 ; mask the data byte jz data_low ; if the bit is zero, bring the data line low setb PLL_DATA ; otherwise, set the data line ljmp clk_it data_low: clr PLL_DATA ; clear the data line if data bit is zero clk_it: setb PLL_CLK ; clock in the data bit (rising edge clocks in data) clr PLL_CLK inc R7 ; increment mask table offset mov A, R0 ; load the number of bits clr C subb A, R7 ; if we haven't clocked in all the bits yet, jnz send_next ; send another pop ACC mov R7, A ret ; send reset sequence to PLL chip. The reset sequence is as follows: ; - 4 clocks with data low and enable high ; - 3 clocks with data low and enable low ; - 1 clock with data high and enable low ; - 1 clock with data low and enable low ; - set enable high ; ; for more information on the reset sequence, see the Motorola MC145170 data sheet. pll_reset: setb PLL_ENB ; disable the PLL chip clr PLL_DATA ; clear the data input mov A, #4 ; send 4 clocks lcall clk_pll_n clr PLL_ENB ; enable the PLL chip mov A, #3 ; send 3 clocks lcall clk_pll_n mov A, #1 setb PLL_DATA ; set the data input lcall clk_pll_n ; send 1 clock mov A, #1 clr PLL_DATA ; clear the data input lcall clk_pll_n ; 1 clock setb PLL_ENB ; disable the PLL chip ret ; clock PLL chip n times, where n is in the accumulator. Used by the PLL ; reset function. clk_pll_n: setb PLL_CLK clr PLL_CLK dec A jnz clk_pll_n ret ; ************************ POWER SWITCH ******************* ; called during micro initialization. Based on status of power sense ; line, call appropriate power on/off function, and set the power flag power_init: jnb PWR_SENSE, pi_on ; if power sense is low, switch is on mov PWR, #0 ; PWR flag (0 -> off, 1 -> on) lcall init_power_off ret pi_on: mov PWR, #1 lcall power_on ret ; this function is called every time the code goes around the main ; loop. If the status of the power switch has changed from the last ; time around the loop, take appropriate action power_monitor: mov A, PWR jz pwr_is_off jnb PWR_SENSE, power_done ; if power sense is low, power is still on mov PWR, #0 ; otherwise, switch has been turned OFF lcall power_off ljmp power_done pwr_is_off: jb PWR_SENSE, power_done ; if power sense is high, power is still off mov PWR, #1 ; otherwise, switch has been turned ON lcall power_on power_done: ret ; this function is called when the power switch is turned on, or if the ; micro boots with the power switch in the on position. power_on: lcall init_userx ; reset user interface settings lcall eeverify ; determine if settings are saved in EEPROM ; if so, load them into RAM lcall cal_verify ; determine if calibration settings are saved ; in EEPROM, and load if so lcall resume_lcd ; activate the LCD lcall channels_on ; turn both channel relays on mov MODE, #0 ; set for A freq mode lcall ptr_adjust lcall set_all ; set both dds and attenuator channels ret ; this function is called when the power switch is turned off. power_off: lcall eestore ; store the userx settings in EEPROM lcall shutdown_lcd ; deactivate the LCD lcall channels_off ; shut off all relays ret ; this function is called when the micro boots with the power switch off. init_power_off: lcall shutdown_lcd ; deactivate the LCD lcall channels_off ; shut off all relays ret ; **************************** SERIAL PORT ******************* ; put the byte in the accumulator out through the serial port put_byte: jnb TI, put_byte ; pause until serial port is free clr TI ; clear transmission flag mov SBUF, A ; move accumulator byte to serial buffer ret ; passes a sequence of bytes to the PC through the serial interface. The same ; structure is followed as for the passing of a command to the micro from the PC: ; byte 0) command byte ; byte 1) # of args to follow ; bytes 2 on) the args ; ; prior to calling... ; R0 -- set with address of first data point ; R1 -- set with return code ; R7 -- set with # of args pass: mov A, R1 ; send return code lcall put_byte mov A, R7 ; send # of args lcall put_byte mov A, R7 jz a4 ; if no args, we're done a5: mov A, @R0 ; send an arg lcall put_byte inc R0 ; increment data pointer djnz R7, a5 ; was that the last point? a4: ret ; a byte has been received through the serial port. Acquire any additional passed ; bytes. Assume the following command-passing byte sequence: ; byte 0) command byte ; byte 1) NUM_ARGS_PASSED ; bytes 2 on) the arguments ; ; this function stores the bytes in sequence starting at RAM location CMD. To avoid ; clobbering other memory if the PC passes too many arguments, a maximum of 20 arguments ; will be saved; any additional args will be ignored. The wait_for_byte function ; is used to delay until either a new byte is received or a timeout occurs. The ; wait_for_byte function places a 0 into R6 if a timeout occurred, or a 1 into R6 ; if a byte was received. ; ; after all bytes have been received, parse byte 0, the command byte, and call the ; appropriate command handler function. get_cmd: mov A, R0 push ACC mov A, R1 push ACC mov A, R6 push ACC mov A, R7 push ACC mov R0, #CMD mov A, SBUF mov @R0, A ; put the command byte at CMD clr RI ; clear the receive flag inc R0 ; increment data pointer lcall wait_for_byte ; wait for another byte cjne R6, #1, post1 ; exit if a timeout occurred mov A, SBUF mov @R0, A ; hold # of args in mem at CMD+1 clr RI ; clear the receive flag clr C subb A, #21d ; subtract 21 from the # of args being passed jnc over_limit ; if the # of args is >= 21, only save 20 ljmp ok_now ; otherwise, we're fine over_limit: mov A, #20d ; only save 20 mov @R0, A ; and place this value into CMD+1 ok_now: mov A, @R0 ; load # of args mov R7, A ; also hold temp copy here jnz a2 ; are there more args to get? ljmp cmd_parse ; if not, jump to parse a2: inc R0 ; point to CMD+2 a3: lcall wait_for_byte ; wait for another byte cjne R6, #1, post1 ; exit if a timeout occurred mov A, SBUF mov @R0, A ; transfer to memory inc R0 ; point to next RAM byte djnz R7, a3 ; was that the last arg? ; determine what action to take because of the passed command cmd_parse: mov R0, #CMD ; reload ptr mov A, @R0 ; load command byte clr C subb A, #MOD_REF ; command for modifying the PLL frequency ; jz modref ; disable this feature for now mov A, @R0 clr C subb A, #TEST_CMD ; command for passing a sequence of bytes back to the PC ; jz test ; disable this feature for now mov A, @R0 clr C subb A, #ALIVE ; PC wants to know if micro is alive jz send_alive mov A, @R0 clr C subb A, #ALT_SETTING ; a user interface setting (freq, att, phase) is jz change_setting ; to be modified mov A, @R0 clr C subb A, #RET_SETTING ; a user interface setting (freq, att, phase) is jz ret_set ; to be passed back to the PC mov A, @R0 clr C subb A, #SET_CAL_DATA ; the PC is passing new calibration data that is to jz ncd ; be saved in EEPROM mov A, @R0 clr C subb A, #CAL_RESET ; the PC commands the micro to ignore its calibration jz calr ; data until new data is sent mov A, @R0 clr C subb A, #CHAN_CONTROL ; a channel is being turned on or off jz ch_cont ljmp post_cmd post1: ljmp post_cmd ret_set: ljmp return_setting ncd: ljmp new_cal_data calr: ljmp calib_reset ch_cont: ljmp chan_cont ; wait for a byte to come in through the serial port. However, if a timeout condition ; occurs, set a flag and return. The timeout value has been chosen (empirically) to ; be 256 * 15. This leaves plenty of margin for byte passing at 2400 baud. wait_for_byte: mov A, R0 push ACC mov A, R1 push ACC mov R0, #0fh ; outer timeout loop wt2: mov R1, #0ffh ; inner timeout loop wt3: jb RI, wt_good ; has data arrived? djnz R1, wt3 djnz R0, wt2 mov R6, #0 ; set timeout flag to indicate timeout ljmp wt_done wt_good: mov R6, #1 ; set flag to indicate byte received wt_done: ; clear the receive flag so it will be clr RI ; set by the arrival of the next byte pop ACC mov R1, A pop ACC mov R0, A ret ; execution jumps here after a command passed through ; the PC interface has been processed. Earlier, 4 registers ; were pushed onto the stack, so they're pulled off first. ; then, call set_all to set the two dds and two attenuators. post_cmd: pop ACC mov R7, A pop ACC mov R6, A pop ACC mov R1, A pop ACC mov R0, A lcall set_all ljmp main_loop ; **************************** PC COMMAND HANDLERS ************************ ; return the code indicating proper receipt of the status poll from the PC send_alive: mov R1, #ALIVE ; send the return code with no args mov R7, #0 lcall pass ljmp post_cmd ; load the PLL with the R and N vals in CMD2..CMD5 ; these RAM locs already loaded during get_cmd modref: lcall pll_load ; program the PLL chip mov R1, #MOD_REF ; return the command with no args mov R7, #0 lcall pass ljmp post_cmd ; a user interface setting is to be altered. The PC passes a sequence of arguments: ; arg #0 contains the mode of the parameter which is to be modified. As always, the ; modes are: ; 0) A channel frequency ; 1) B channel frequency ; 2) A channel attenuation ; 3) B channel attenuation ; 4) relative phase between A and B ; ; arg #1 contains the first data byte. For frequency changes, 9 digits should be ; passed. For attenuation changes, 3 digits should be passed. And for phase, ; 1 byte should be passed with the 5-bit relative phase value (0-31). ; in total, the PC should send a total of 10 args (freq), 4 args (atten), ; or 2 args (phase). For frequency and attenuation modes, the digit transfer is ; big-endian, i.e. the MSD is passed last. ; change_setting: mov R0, #CMD+2 mov A, @R0 ; point to the first argument mov MODE, A ; the first argument contains the MODE byte lcall ptr_adjust ; adjust PTR to account for the new MODE mov A, PTR mov R1, A ; hold user interface setting pointer in R1 inc R0 ; point to the second passed arg, the first data value mov A, MODE anl A, #4 ; phase mode? jnz alt_phase mov A, MODE anl A, #2 ; atten mode? jnz alt_amp mov R7, #9 ; otherwise, freq mode, so 9 digits have been passed ljmp alt_nxt alt_amp: mov R7, #3 ; in atten mode, 3 digits have been passed ljmp alt_nxt alt_phase: mov R7, #1 ; in phase mode, 1 byte has been passed alt_nxt: mov A, @R0 ; load the data byte mov @R1, A ; place into the user interface RAM location inc R0 ; point to the next arg inc R1 ; point to the next userx location djnz R7, alt_nxt ; have we stored all the values? mov R1, #ALT_SETTING ; return the code with no args mov R7, #0 lcall pass ljmp post_cmd ; a user interface setting is to be passed back to the PC. The PC should have passed ; one argument, the mode indicating which setting is to be returned, i.e. A freq ; (mode=0), B freq (mode=1), etc. The first arg in the sequence passed back to the PC ; will be an echo of the mode information. The following args will be the frequency / ; attenuation digits or phase byte. The micro will return a total of 10 args (frequency ; mode), 4 args (attenuation mode), or 2 args (phase mode). as before, a returned phase ; value will be a 5-bit number (0-31). Digit transfers (freq, atten modes) are ; big-endian, i.e. the MSD is passed last. return_setting: lcall store_mode ; store the current mode mov R0, #CMD+2 ; point to the first arg mov A, @R0 mov MODE, A ; set MODE equal to this first arg lcall ptr_adjust ; adjust PTR to account for the new MODE mov A, MODE ; analyze mode to determine number of data bytes to pass anl A, #4 ; i.e. 9 for freq, 3 for amp, 1 for phase jnz rs2 ; store # of data bytes in R7 mov A, MODE anl A, #2 jnz rs3 mov R7, #9 ljmp rs4 rs3: mov R7, #3 ljmp rs4 rs2: mov R7, #1 rs4: mov A, R7 ; load the number of data bytes mov R6, A ; store temp copy in R6 for transferring to CMD inc R7 ; # of args = # of data bytes + 1 (mode byte) mov A, PTR mov R0, A ; point to appropriate user setting with R0 mov R1, #CMD ; point to CMD (passing location) with R1 mov A, MODE ; store MODE byte at CMD, data starting at CMD+1 mov @R1, A inc R1 ; advance to CMD+1 rs5: mov A, @R0 ; load a user-set byte mov @R1, A ; place into passing RAM location inc R0 ; increment user setting pointer inc R1 ; increment pass location pointer djnz R6, rs5 ; have we transferred all the data bytes yet? mov R1, #RET_SETTING ; pass the return setting code first mov R0, #CMD ; with R7 args, the first of which is located at CMD lcall pass lcall restore_mode ljmp post_cmd ; the PC has passed new calibration data that is to be stored in EEPROM. new_cal_data: lcall store_cal_data ; store the data mov R1, #SET_CAL_DATA ; pass the cal set code with no args mov R7, #0 lcall pass lcall cal_verify ; reconfigure operation to allow for cal data ljmp post_cmd ; the PC demands calibration be disabled. To do so, write a byte which is not MAGIC ; at the CAL_TABLE position in the EEPROM, and reconfigure calibration operation. calib_reset: mov dptr, #CAL_TABLE ; point to CAL_TABLE mov R0, #0 ; write a non-MAGIC code at this address lcall eewrite ; into the EEPROM mov R1, #CAL_RESET ; pass the cal reset code with no args mov R7, #0 lcall pass lcall cal_verify ; reconfigure operation to allow for cal reset ljmp post_cmd post2: ljmp post_cmd ; the PC is turning a channel on or off. 2 args should have been passed: ; the first contains the channel (0 for A, 1 for B) and the second the command ; (0 for off, 1 for on). For example, if 0,1 were the two args passed, the ; A channel would be turned on. chan_cont: mov R0, #CMD+1 ; point to the # of args location cjne @R0, #2, post2 ; if 2 args were not passed, crap out inc R0 ; point to the first arg mov A, @R0 mov R1, A ; store first arg in R1 inc R0 ; point to second arg mov A, @R0 ; get second arg anl A, #1 jnz cc_chan_on ; if nonzero, the channel specified is to be turned on mov A, R1 ; reload first arg anl A, #1 ; first arg contains channel info (0 for A, 1 for B) jnz cc_off_bchan lcall a_off ; turn off A channel ljmp cc_done cc_off_bchan: lcall b_off ; turn off B channel ljmp cc_done cc_chan_on: mov A, R1 ; reload first arg anl A, #1 jnz cc_on_bchan lcall a_on ; turn on A channel ljmp cc_done cc_on_bchan: lcall b_on ; turn on B channel cc_done: mov R1, #CHAN_CONTROL mov R7, #0 lcall pass ljmp post_cmd ; return a sequence of 255 bytes (0-254) for testing purposes. test: mov A, #TEST_CMD ; send the test command code lcall put_byte mov A, #0ffh ; send the # of args (255) lcall put_byte mov R0, #0 mov R1, #0ffh tt2: mov A, R0 ; loop through, sending a byte each time lcall put_byte inc R0 ; increment the value to be sent djnz R1, tt2 ; done yet? ljmp post_cmd ; ********************************** EEPROM *********************************** ; checks to see if the MAGIC code is stored at EE_USERX. If so, the data following ; this code is valid user setting data and eerestore should be used to retrieve ; these settings. Otherwise, do nothing, and the default user interface settings ; will be used instead. eeverify: lcall set_eeaddr ; point to EEADDR lcall eeread ; read the byte cjne R0, #MAGIC, eefalse ; if it's not MAGIC, exit lcall eerestore ; otherwise, restore the settings eefalse: ret ; loads EE_USERX into dptr set_eeaddr: mov dptr, #EE_USERX ret ; store user settings in EEPROM to save for next power up cycle. A magic ; byte is stored at the start of the block (at EE_USERX) to indicate that ; proper settings have been saved on the next powerup. The following ; bytes are saved in the specified order, starting at EE_USERX+1: ; 1) A freq digits (9 bytes) ; 2) B freq digits (9 bytes) ; 3) A atten digits (3 bytes) ; 4) B atten digits (3 bytes) ; 5) relative phase (1 byte) ; ; therefore, a total of 25 bytes are stored in addition to the magic code. eestore: mov A, R0 push ACC mov A, R1 push ACC mov A, R2 push ACC lcall set_eeaddr ; point to EE_USERX+1 because the magic code will inc dptr ; be saved at EE_USERX mov R1, #8 ; write the A freq digits (9) mov R2, #D lcall wr_nxt_dig mov R1, #8 ; write the B freq digits (9) mov R2, #D2 lcall wr_nxt_dig mov R1, #2 ; write the A atten digits (3) mov R2, #AD lcall wr_nxt_dig mov R1, #2 ; write the B atten digits (3) mov R2, #AD2 lcall wr_nxt_dig mov R0, PHASE ; write the phase info (1) lcall eewrite lcall set_eeaddr ; now write the special code, MAGIC, at EE_USERX mov R0, #MAGIC lcall eewrite pop ACC mov R2, A pop ACC mov R1, A pop ACC mov R0, A ret ; restore the user interface settings from EEPROM. this function is not called ; unless the presence of the magic code at EE_USERX has been previously verified. ; the following bytes are read from the EEPROM, starting at address EE_USERX+1: ; 1) A freq digits (9 bytes) ; 2) B freq digits (9 bytes) ; 3) A atten digits (3 bytes) ; 4) B atten digits (3 bytes) ; 5) relative phase (1 byte) ; ; these 25 bytes are placed in RAM at their assigned locations. Finally, a zero ; is written to EE_USERX. This is done so that in the event of a loss of power ; without proper execution of the eestore function, no data will be read from ; the EEPROM on the next powerup. Instead, default user interface settings ; will be used in this situation. eerestore: mov A, R0 push ACC mov A, R1 push ACC mov A, R2 push ACC lcall set_eeaddr ; the first data byte is stored at EE_USERX+1 inc dptr ; because the magic code is stored at EE_USERX mov R1, #8 ; read the A freq digits (9) mov R2, #D lcall rd_nxt_dig mov R1, #8 ; read the B freq digits (9) mov R2, #D2 lcall rd_nxt_dig mov R1, #2 ; read the A atten digits (3) mov R2, #AD lcall rd_nxt_dig mov R1, #2 ; read the B atten digits (3) mov R2, #AD2 lcall rd_nxt_dig lcall eeread ; read the phase info (1) mov PHASE, R0 lcall set_eeaddr mov R0, #0 ; now write a zero at the magic address lcall eewrite pop ACC mov R2, A pop ACC mov R1, A pop ACC mov R0, A ret ; loop which writes a byte to eeprom every time around. This function takes R1 ; and R2 as parameters. R2 is the base address of the settings to be stored in ; EEPROM, and R1 is the offset from the base address of the first byte to be stored. ; therefore, the first byte is stored from R2+R1 and last byte is stored from R2. ; dptr should have been set prior to call of this function with the EEPROM address ; of the first byte to be stored. wr_nxt_dig: mov A, R1 ; load the offset add A, R2 ; add the base address mov R0, A ; store in R0 as pointer mov A, @R0 ; retrieve the value mov R0, A ; and write to EEPROM lcall eewrite inc dptr ; point to next EEPROM address mov A, R1 ; reload offset jz wr_loop_done ; if it's zero, we're done dec R1 ; otherwise, decrement ljmp wr_nxt_dig ; and loop back wr_loop_done: ret ; loop which reads a byte from eeprom every time around. This function takes R1 ; and R2 as parameters. R2 holds the base address of the RAM location where the ; data retrieved from the EEPROM is to be stored. R2 holds the offset from the ; base address where the first retrieved byte is to be stored. Therefore, the ; first retrieved byte is stored at R2+R1, and the last retrieved byte is stored ; at R2. Dptr should have been set prior the call of this function with the ; EEPROM address of the first byte to be retrieved. rd_nxt_dig: lcall eeread ; retrieve byte mov B, R0 ; store for now in B register mov A, R1 ; load the offset add A, R2 ; add the base address mov R0, A ; store in R0 as pointer mov A, B ; store the retrieved byte mov @R0, A inc dptr ; point to next EEPROM address mov A, R1 ; reload offset jz rd_loop_done ; if it's zero, we're done dec R1 ; otherwise, decrement ljmp rd_nxt_dig ; and loop back rd_loop_done: ret ; low-level function to write EEPROM with byte in R0 at EEPROM location ; pointed to by dptr, which should have been set prior to call. eewrite: mov A, WMCON ; load the WMCON SFR setb ACC.4 ; set WMCON.4 (EEMWE) mov WMCON, A ; store back in SFR w1: mov A, WMCON ; ready flag located at WMCON.1 jnb ACC.1, w1 ; wait here until EEPROM is ready mov A, R0 ; load the byte and movx @dptr, A ; store in EEPROM (movx accesses EEPROM) w2: mov A, WMCON jnb ACC.1, w2 ; wait here until write cycle is finished mov A, WMCON clr ACC.4 ; clear WMCON.4 (EEMWE) mov WMCON, A ret ; low-level funtion to read EEPROM at location pointed to by dptr, which should ; have been set prior to call. The retrieved byte is stored in R0. eeread: mov A, WMCON ; load SFR jnb ACC.1, eeread ; wait until EEPROM is ready movx A, @dptr ; retrieve byte from EEPROM (movx accesses EEPROM) mov R0, A ; store in R0 w3: mov A, WMCON jnb ACC.1, w3 ; wait here EEPROM is ready ret ; **************************** CAL ************************** ; determine whether calibration data is stored in EEPROM. If so, ; calculate the amount by which the user-set attenuation should ; be reduced in order to compensate for amplifier rolloff. The ; following is a brief description of the process: ; ; for each channel, 8 cal points are saved for the following freqs ; 1, 5, 10, 15, 20, 25, 30, 35 MHz. if the frequency is above ; 1 MHz, interpolate between adjacent points to determine the value ; by which to reduce the user-set attenuation. calibrate: lcall cal_prepare ; reset calibration parameters mov A, CAL_MODE ; bit 0 of CAL_MODE is a flag which marks anl A, #1 ; whether cal data is saved in EEPROM jz cal_done ; if it's not set, exit lcall interp ; otherwise, interpolate to get the cal factor lcall cal_sub ; now subtract it from the user-set atten. mov A, MODE ; if the user is modifying an amplitude, anl A, #2 ; there's no need to set the attenuator jnz calib2 ; otherwise, the user is modifying a frequency, lcall do_cal ; so we need to set the attenuator calib2: lcall disp_cal ; display a star if we are out of the cal range cal_done: ret ; determine if calibration data is stored in EEPROM. set the CAL_MODE ; bit 0 flag accordingly. cal_verify: mov CAL_MODE, #0 ; default to no stored data mov dptr, #CAL_TABLE ; if there is in fact stored data, the byte lcall eeread ; at #CAL_TABLE will equal #MAGIC cjne R0, #MAGIC, cv_done ; cal data must be bad, so don't set flag mov A, CAL_MODE ; otherwise, set the flag orl A, #1 mov CAL_MODE, A cv_done: ret ; now that calibration has been calculated, actually set the appropriate ; attenuator. This function is called if the user has been modifying ; a frequency setting. do_cal: mov A, MODE ; determine which channel is active anl A, #1 jnz docal_bchan mov MODE, #2 ; set the MODE and PTR to reflect an attenuation mov PTR, #AD ; operation lcall set_a_atten ; set the A attenuator mov MODE, #0 ; reset MODE and PTR to A frequency mode mov PTR, #D ret docal_bchan: mov MODE, #3 mov PTR, #AD2 lcall set_b_atten ; set the B attenuator mov MODE, #1 ; reset MODE and PTR to B frequency mode mov PTR, #D2 ret ; reset calibration parameters. When the attenuator is being set, the ; three decimal digits in AD+3..AD+5 (AD2+3..AD2+5 for B channel) are ; used. However, the user interface stores attenuator settings in AD+0..AD+2 ; (AD2+0..AD2+2). This function stores the user setting in the digits that ; are actually used to set the attenuator. In addition, the function sets ; the flag indicating that whether or not we are in or out of cal range. cal_prepare: mov A, MODE ; determine which channel is active anl A, #1 jnz cp_bchan mov AD+3, AD+0 ; store the user-specified digits in the active mov AD+4, AD+1 ; attenuator locations mov AD+5, AD+2 mov A, CAL_MODE ; bit 1 of CAL_MODE contains the A channel in/out of- anl A, #0fdh ; cal-range flag. 0->in-cal 1->out-of-cal mov CAL_MODE, A ljmp cal_prep_done cp_bchan: mov AD2+3, AD2+0 ; same for B channel mov AD2+4, AD2+1 mov AD2+5, AD2+2 mov A, CAL_MODE ; bit 2 of CAL_MODE holds B in/out of-cal-range flag anl A, #0fbh mov CAL_MODE, A cal_prep_done: ret ; cal points are stored for 1,5,10,15,20,25,30,35 MHz. This function uses ; linear interpolation with a resolution of 1 MHz to determine the proper ; calibration value, and stores it at the CAL RAM location. For example, ; if the frequency is 22 MHz, the calibration value will be ; 1/5 * ((2 * 20_MHZ_CAL_PT) + (3 * 25_MHZ_CAL_PT)). Below 1 MHz, no cal ; is needed. interp: mov A, R0 push ACC mov A, R1 push ACC mov A, R2 push ACC mov A, R3 push ACC mov dptr, #CAL_TABLE+1 ; first byte at CAL_TABLE holds magic code mov A, MODE ; determine which channel is active anl A, #1 ; set the EEPROM data pointer accordingly jnz itp2 mov A, #D ; for A channel, add A, #8 mov R0, A ; store #D+8 in R0 ljmp itp3 itp2: mov A, #D2 ; for B channel, add A, #8 mov R0, A ; store #D2+8 in R0 itp3: mov R3, A ; hold frequency digit pointer in R3 as well as in R0 mov A, @R0 ; get 10 MHz digit jnz cal2 ; if nonzero, major cal required dec R0 ; point to 1 MHz digit mov A, @R0 jnz cal2 ; if nonzero, major cal required dec R0 ; point to 100 kHz digit mov A, @R0 jz no_cal_needed ; if zero, no calibration is required ljmp low_cal ; otherwise, then we're between 100 kHz and 1 MHz cal2: mov A, R3 ; reload the frequency pointer mov R0, A ; point to the 10 MHz digit mov A, @R0 mov B, #2 mul AB mov R1, A ; store 2*(10 M digit) in R1 jz cal4 ; if it's zero, data ptr is positioned correctly cal3: inc dptr ; otherwise, djnz R1, cal3 ; advance the data pointer by R1 cal4: lcall retrieve_cal_data ; retrieve 3 points: x0, x5, (x+1)0 dec R0 ; point to the 1 MHz digit mov A, @R0 mov R1, A ; hold in R1 mov A, #5 clr C subb A, R1 ; compute 5 - R1 jc top_half ; if 5 - R1 is negative, then the digit is in the range ; 6-9, so interpolate between the two upper points mov B, A ; otherwise, hold in B mov A, CALDATA+0 ; get low point mul AB ; compute CALDATA+0 * (5-R1) mov R2, A ; hold in R2 inc R0 ; point to 10 MHz digit mov A, @R0 ; if 10 MHz digit is zero, we're between 1 and 5 MHz jz special ; which is a special case dec R0 ; point back to 1 MHz digit mov A, @R0 ; otherwise, scaling factor is just the digit itself mov B, A mov A, CALDATA+1 mul AB ; compute CALDATA+1 * R1 add A, R2 ; add to previous calc mov B, #5 div AB ; divide by 5 to form weighted average mov R2, A ; store in R2 ljmp interp_done top_half: mov A, R1 ; load the 1 MHz digit clr C subb A, #5 ; calc R1 - 5 mov B, A ; hold in B mov A, CALDATA+2 ; load high point mul AB ; compute (R1-5) * CALDATA+2 mov R2, A ; store in R2 mov A, #10d clr C subb A, R1 ; calc 10 - R1 mov B, A ; hold in B mov A, CALDATA+1 ; load middle point mul AB ; compute (10-R1) * CALDATA+1 add A, R2 ; add to previous calc mov B, #5 div AB ; divide by 5 to form weighted average mov R2, A ; store in R2 interp_done: mov A, R2 mov CAL, A ; transfer R2 to CAL RAM location pop ACC mov R3, A pop ACC mov R2, A pop ACC mov R1, A pop ACC mov R0, A ret ; the frequency is below 100 kHz, so we don't need to cal no_cal_needed: mov R2, #0 ljmp interp_done ; the frequency is b/w 100 kHz and 1 MHz. For now, don't cal. low_cal: mov R2, #0 ; for now, low cal and no cal are the same ljmp interp_done ; all points are spaced at 5 MHz intervals except the first 2. If the ; frequency is between 1 and 5 MHz, the interpolated value is: ; 1/4 * (((5-f) * 1_MHZ_CAL_PT) + ((f-1) * 5_MHZ_CAL_PT)) special: dec R0 ; point to 1 MHz digit mov A, @R0 dec A ; in the special case, scaling factor is the digit - 1 mov B, A ; hold in B mov A, CALDATA+1 ; get middle point mul AB ; calc (f-1) * CALDATA+1 add A, R2 ; add to previous calculation mov B, #4 ; divide by 4, not 5 (interpolating b/w 1/5, not 0/5) div AB mov R2, A ; hold in R2 ljmp interp_done ; data pointer has been set prior to this function call. This function ; retrieves three bytes from successive locations in EEPROM and holds them ; in CALDATA+0, CALDATA+1, and CALDATA+2. The 10 MHz digit of the frequency ; determines the data pointer location, and therefore the three cal values ; which are retrieved. retrieve_cal_data: mov A, R0 push ACC lcall eeread ; read the EEPROM mov A, R0 mov CALDATA+0, A ; store at CALDATA+0 inc dptr ; increment the data pointer lcall eeread ; repeat twice mov A, R0 mov CALDATA+1, A inc dptr lcall eeread mov A, R0 mov CALDATA+2, A pop ACC mov R0, A ret ; the amount by which to decrement the attenuator because of calibration ; is stored in CAL. This function converts the user-set attenuation from ; bcd to binary, subtracts CAL, converts this value back to bcd, and stores ; the digits in AD+3..AD+5 (AD2+3..AD2+5) cal_sub: mov A, R0 push ACC mov A, R1 push ACC mov R0, #AD+1 ; point to the unit atten digit mov A, MODE anl A, #1 jz cs2 mov R0, #AD2+1 ; if B channel is active, switch the ptr cs2: mov A, @R0 ; load the 1's digit mov B, #10d ; multiply by 10 mul AB mov R1, A dec R0 ; point to the tenths digit mov A, @R0 add A, R1 ; add it to 10 * 1's digit mov R1, A ; acc now holds bin version of attenuator's 2 low digs clr C subb A, CAL ; compute ATT - CAL jc calsub_fix ; if the result is negative, we need to borrow mov CAL, A ; otherwise, store the result back in CAL mov A, R0 add A, #4 ; now point to AD+4 (AD2+4) mov R0, A cs3: mov A, CAL ; load the new atten value mov B, #10d ; divide by 10 to convert back to bcd div AB mov @R0, A ; store quotient at AD+4 (AD2+4), mov A, B ; remainder at AD+3 (AD2+3) dec R0 mov @R0, A csub_done: pop ACC mov R1, A pop ACC mov R0, A ret ; this function is called when (ATT - CAL) is negative, meaning we ; need to borrow from the 10's digit. If the 10's digit is zero, ; we are out of calibration range, and act accordingly. calsub_fix: inc R0 inc R0 ; point to highest atten digit mov A, @R0 jz bad_cal ; if it's zero, we can't borrow, so cal is bad dec A ; otherwise, decrement digit inc R0 inc R0 inc R0 ; and store at AD+5 (AD2+5) mov @R0, A mov A, R1 ; reload the ATT value add A, #100d ; add 100 because of borrow operation clr C subb A, CAL ; now subtract CAL mov CAL, A ; store back in CAL dec R0 ; point to AD+4 (AD2+4) ljmp cs3 ; jump back to main function ; this function is called when we are out of calibration range. Set the ; appropriate out-of-cal bit in CAL_MODE, and zero all the attenuator ; digits. bad_cal: mov A, MODE ; determine which channel is active anl A, #1 jnz b_bad mov A, CAL_MODE orl A, #2 ; bit 1 of CAL_MODE is set to specify A out-of-cal mov CAL_MODE, A ljmp bad_done b_bad: mov A, CAL_MODE orl A, #4 ; bit 2 of CAL_MODE is set to specify B out-of-cal mov CAL_MODE, A bad_done: lcall zeroed_out ; now zero the attenuator digits ljmp csub_done ; set AD+3..AD+5 (AD2+3..AD2+5) to zero because we're out of cal zeroed_out: mov A, R0 push ACC mov R0, #AD+3 mov A, MODE ; which channel is active? anl A, #1 jz zo2 mov R0, #AD2+3 zo2: mov @R0, #0 inc R0 mov @R0, #0 inc R0 mov @R0, #0 pop ACC mov R0, A ret ; this function is called due to a command passed by a PC through the ; serial interface. Retrieve the # of args passed by the PC. Store the ; first arg at CAL_TABLE+1 (EEPROM address), and loop through, storing each ; additional arg at the next EEPROM address. store_cal_data: mov A, R0 push ACC mov A, R1 push ACC mov A, R2 push ACC mov R1, #CMD+1 ; # of args stored here mov A, @R1 mov R2, A ; store # of passed args in R2 inc R1 ; now point to first passed arg mov dptr, #CAL_TABLE ; CAL_TABLE holds magic code, so put inc dptr ; first arg at CAL_TABLE+1 nxt_cal_pt: mov A, @R1 ; load the argument mov R0, A lcall eewrite ; write to EEPROM inc R1 ; point to next argument inc dptr ; point to next EEPROM address djnz R2, nxt_cal_pt ; more args to store? mov dptr, #CAL_TABLE ; now write the magic code at CAL_TABLE mov R0, #MAGIC lcall eewrite pop ACC mov R2, A pop ACC mov R1, A pop ACC mov R0, A ret ; displays a star if we are out of calibration range. Otherwise, displays ; a space to erase a star that may be left over. The cursor location for the ; star is a constant. disp_cal: mov A, R0 push ACC mov A, MODE ; which channel is active? anl A, #1 jnz dispc_bchan mov CURSOR, #A_CALSTAR ; A_CALSTAR is the A channel cursor location mov A, CAL_MODE anl A, #2 ; for A channel, bit 1 holds the flag jz dispc_bad ; if bit 1 is set, then cal was good ljmp dispc_good dispc_bchan: mov CURSOR, #B_CALSTAR ; B_CALSTAR is the B channel cursor location mov A, CAL_MODE anl A, #4 ; for B channel, bit 2 holds the flag jz dispc_bad ; if bit 2 is set, then cal was good dispc_good: lcall move_cursor ; set the cursor location mov R0, #2ah ; display a star lcall send_lcd_data ljmp dispc_done dispc_bad: lcall move_cursor ; set the cursor location mov R0, #20h ; display a space lcall send_lcd_data dispc_done: pop ACC mov R0, A ret ; ************************** MATH ****************************** ; ***** MEMORY OPERATIONS ; resets the 12 K bytes, 12 L bytes, and 9 M bytes to zero. init_klm: mov K+0, #0 mov K+1, #0 mov K+2, #0 mov K+3, #0 mov K+4, #0 mov K+5, #0 mov K+6, #0 mov K+7, #0 mov K+8, #0 mov K+9, #0 mov K+10, #0 mov K+11, #0 mov L+0, #0 mov L+1, #0 mov L+2, #0 mov L+3, #0 mov L+4, #0 mov L+5, #0 mov L+6, #0 mov L+7, #0 mov L+8, #0 mov L+9, #0 mov L+10, #0 mov L+11, #0 mov M+0, #0 mov M+1, #0 mov M+2, #0 mov M+3, #0 mov M+4, #0 mov M+5, #0 mov M+6, #0 mov M+7, #0 mov M+8, #0 ret ; reset M+0..M+8. Used by mult4x4. init_M: mov M+0, #0 mov M+1, #0 mov M+2, #0 mov M+3, #0 mov M+4, #0 mov M+5, #0 mov M+6, #0 mov M+7, #0 mov M+8, #0 ret ; load K+0..K+7 into L+0..L+7. Used by mult4x4. load_L_K: mov L+0, K+0 mov L+1, K+1 mov L+2, K+2 mov L+3, K+3 mov L+4, K+4 mov L+5, K+5 mov L+6, K+6 mov L+7, K+7 ret ; load M+0..M+7 into K+0..K+7. Used by convert_freq. load_K_M: mov K+0, M+0 mov K+1, M+1 mov K+2, M+2 mov K+3, M+3 mov K+4, M+4 mov K+5, M+5 mov K+6, M+6 mov K+7, M+7 ret ; ***** ADDITION ; adds the 8-byte value in L0..L7 with the 8-byte value in M0..M7 ; leaves result in M0..M7. add8: clr C ; clear the carry initially mov A, L+0 ; start with LSBs add A, M+0 ; L+0 += M+0 mov M+0, A ; store in M+0 mov A, L+1 ; repeat for the other 7 bytes addc A, M+1 ; from now on, use add with carry mov M+1, A mov A, L+2 addc A, M+2 mov M+2, A mov A, L+3 addc A, M+3 mov M+3, A mov A, L+4 addc A, M+4 mov M+4, A mov A, L+5 addc A, M+5 mov M+5, A mov A, L+6 addc A, M+6 mov M+6, A mov A, L+7 addc A, M+7 mov M+7, A ret ; adds 9-digit bcd frequency in D+0..D+8 (D2+0..D2+8) to 9 digit bcd ; frequency delta value in M+0..M+8. The result is left in ; D+0..D+8 (D2+0..D2+8). after completion, the function calls ; check_max to determine if the new frequency value exceeds the ; maximum allowed frequency (35 MHz). bcd_add: mov A, R0 push ACC mov A, R1 push ACC mov A, R2 push ACC mov B, #0 ; B holds carry, set to 0 initially mov R0, #0 ; digit counter, set to 0 initially add_nxt: mov A, #M ; M+0 add A, R0 ; add digit counter mov R1, A ; R1 ptr mov A, @R1 ; load digit mov R2, A ; store in R2 mov A, PTR ; D+0 (D2+0) add A, R0 ; add digit counter mov R1, A ; R1 ptr mov A, @R1 ; load digit add A, B ; add carry add A, R2 ; add M digit mov B, #10d ; divide by 10 to calculate the div AB ; carry (quotient)and digit (remainder) values mov @R1, B ; move remainder to freq digit mov B, A ; move quotient to B to serve as carry for next loop inc R0 ; increment digit counter cjne R0, #9, add_nxt ; if we haven't done 9 digits, loop back lcall check_max ; verify new frequency to see if over the limit pop ACC mov R2, A pop ACC mov R1, A pop ACC mov R0, A ret ; called at the conclusion of bcd_add. This function checks to see if the frequency ; referenced by PTR exceeds the maximum allowed frequency of 35 MHz. If it does, the ; frequency is set to 35 MHz. check_max: mov A, R0 push ACC mov A, R1 push ACC mov A, PTR ; D+0 (D2+0) add A, #8 ; D+8 (D2+8) mov R0, A mov A, @R0 ; load digit clr C subb A, #4 jnc fix ; if highest digit >= 4, need to fix mov A, @R0 ; load D+8 (D2+8) clr C subb A, #3 jc nfix ; if highest digit < 3, freq okay dec R0 ; D+7 (D2+7) mov A, @R0 clr C subb A, #5 ; if 10 MHz digit is 3 and 1 MHz digit is < 5, freq okay jc nfix inc R0 ; D+8 (D2+8) fix: mov @R0, #3 ; set 10 MHz digit to 3 dec R0 ; D+7 (D2+7) mov @R0, #5 ; set 1 MHz digit to 5 mov R1, #7 ; 7 digits to set to zero dec R0 ; starting with D+6 (D2+6) cm2: mov @R0, #0 ; zero this digit dec R0 ; point to next one djnz R1, cm2 ; more digits to zero? nfix: pop ACC mov R1, A pop ACC mov R0, A ret ; ***** SUBTRACTION ; subtract K4..K7 by K8..K11 (K7, K11 MSB). The result is stored in ; M+0..M+3. There is one strange feature in this function, included ; because this subtraction is used by the divAB function. DivAB ; divides using shift-and-subtract. In order to maintain a full ; 32-bit divide capability, the carry flag is used as a 33rd dividend bit ; for subtraction operations. Therefore, this function is designed ; to support that operation. The special condition occurs when the ; carry flag is set prior to this function being called (by a shift ; of the dividend). In this case, sub4 clears the carry before ; carrying out a normal subtraction operation. But, the carry flag ; is automatically cleared cleared prior to a return of the function. ; this occurs because there cannot be a negative result if the carry ; was set (i.e. the 33rd bit was set). sub4: mov A, R1 push ACC mov R1, #0 ; 33rd bit flag nominally set to zero jc nset ; but is the carry set? do_sub: mov A, K+4 ; subtract the LSBs subb A, K+8 mov M+0, A ; store in M+0 mov A, K+5 ; now do the other 3 bytes subb A, K+9 mov M+1, A mov A, K+6 subb A, K+10 mov M+2, A mov A, K+7 subb A, K+11 mov M+3, A mov A, R1 ; is the 33rd bit flag set? jnz clrc ; if so, clear the carry ljmp sdone ; otherwise, leave the carry as is nset: mov R1, #1 ; if the carry is set, set the 33rd bit flag clr C ; now clear the carry and perform ljmp do_sub ; a normal subtraction operation clrc: clr C ; if the 33rd bit was set, can't have a negative result sdone: pop ACC mov R1, A ret ; subtracts R5-digit value in @(R7+0)..@(R7+R5) from value in @(R6+0)..@(R6+R5) ; leaves R5 digit result in @(R6+0)..@(R6+R5). if result is less than zero, ; the carry flag is set. One confusing part of this function is the difference ; between the borrow flag (R3) and the borrow chain flag (R4). To clarify the ; difference, consider the following examples: ; 1) compute 23 - 6. In this case, we need to borrow from the 2; once this happens, ; the borrowing is complete. Here, R3 would be set when the "2" ; digit is active, and cleared afterwards. ; 2) compute 20003 - 6. In this case, we still need to borrow from the 2; however, to ; get there, we need to form a borrow chain over the 3 zeros since ; we can't borrow from them. Here, R4 would be set at the first ; zero, and R3 would remain set until we reach the "2" digit ; because R4 would prevent R3 from being reset. bcd_subb: mov A, R0 push ACC mov A, R1 push ACC mov A, R2 push ACC mov A, R3 push ACC mov A, R4 push ACC mov R0, #0 ; digit counter, set initially to 0 mov R3, #0 ; flag indicates that current digit was borrowed from mov R4, #0 ; if set, indicates active borrow chain sub_nxt: mov A, R7 ; R7 set prior to call of this function add A, R0 ; add digit counter mov R1, A ; R1 ptr mov A, @R1 ; load digit mov R2, A ; store in R2 mov A, R6 ; R6 set prior to call of this function add A, R0 ; add digit counter mov R1, A ; R1 ptr cjne R3, #0, fdig2 ; if flag is set, this digit was borrowed from pl2: mov A, @R1 ; load freq digit clr C subb A, R2 ; subtract M digit jc fdig1 ; if result is negative, need to borrow cjne R4, #0, pl1 ; if there's an active borrow chain, still in borrow mode mov R3, #0 ; otherwise, end borrow mode by clearing flag (see above) pl1: mov @R1, A ; store back at location referenced by R6 inc R0 ; increment digit counter mov A, R5 ; R5 holds # of digits to subtract clr C ; if R5 - R0 = 0, we are done subb A, R0 jnz sub_nxt ; otherwise, loop back for next digit outta: ; we're done, so exit pop ACC mov R4, A pop ACC mov R3, A pop ACC mov R2, A pop ACC mov R1, A pop ACC mov R0, A ret fdig1: mov A, R5 ; R5 is the number of digits, so R5-1 is dec A ; the last digit we can borrow from clr C subb A, R0 ; so, if (R5-1) - R0 = 0, we're out of digits jnz fd_1b ; otherwise, we may borrow ljmp bcd_s2 ; if last digit, then answer < 0, so set carry flag fd_1b: mov A, @R1 ; add 10 to digit (borrow operation) add A, #10d clr C subb A, R2 ; now subtract the M digit mov R3, #1 ; set borrow flag ljmp pl1 ; and jump back into loop fdig2: mov A, @R1 ; this digit was borrowed from jz ezero ; if zero, need to make it 9 dec A ; otherwise, decrement it mov @R1, A ; move digit back mov R4, #0 ; the borrow chain stops here ljmp pl2 ; back into the fray ezero: mov A, R5 ; R5 is the number of digits, so R5-1 is dec A ; the last digit we can borrow from clr C subb A, R0 ; so, if (R5-1) - R0 = 0, we're out of digits jnz ez2 ; otherwise, we may borrow ljmp bcd_s2 ; if last digit, then answer < 0, so set carry ez2: mov @R1, #9 ; otherwise, set equal to 9 mov R4, #1 ; and set flag indicating active borrow chain ljmp pl2 ; jump back bcd_s2: setb C ; answer < 0, so set the carry and exit ljmp outta ; called if the result of freq_bcd_subb is negative. This function resets the ; 9 frequency digits referenced by R6 to zeros. negval: mov A, R0 push ACC mov A, R1 push ACC mov R1, #9 ; 9 digits to zero mov A, R6 ; R6 contains #D+0 (#D2+0) mov R0, A ; R0 ptr nv2: mov @R0, #0 ; zero this digit inc R0 ; point to the next digit djnz R1, nv2 ; more digits to zero? pop ACC mov R1, A pop ACC mov R0, A ret ; uses bcd_subb to subtract the contents of M+0..M+8 from D+0..D+8 (D2+0..D2+8) ; resets frequency to zero if result of subtraction is negative. freq_bcd_subb: mov A, R0 push ACC mov A, R1 push ACC mov R0, PTR ; D+0 (D2+0) mov R1, #K+0 ; place freq digits in K+0..K+8 mov R5, #9 ; 9 digits to transfer fbs2: mov A, @R0 ; load a digit mov @R1, A ; store at K location inc R0 ; next RAM locs inc R1 djnz R5, fbs2 ; more digs to transfer? mov R5, #9 ; 9 digs to subtract mov R6, #K+0 ; freq digits in K+0..K+8 mov R7, #M+0 ; subtraction value in M+0..M+8 lcall bcd_subb ; subtract jnc fbs3 ; if carry isn't set, result was > 0 lcall negval ; otherwise, zero the answer fbs3: mov R0, PTR ; load answer back into freq digits mov R1, #K+0 ; answer stored in K+0..K+8 mov R5, #9 ; 9 digs to transfer fbs4: mov A, @R1 ; load the answer digit mov @R0, A ; store in freq RAM loc inc R0 ; next RAM locs inc R1 djnz R5, fbs4 ; more digs to transfer? pop ACC mov R1, A pop ACC mov R0, A ret ; calculates 16.0 - (attenuation digits). 16.0 was loaded into M+0..M+2 during ; dbm_convert, which calls this function. atten_bcd_subb1: mov R5, #3 ; 3 digits to subtract mov R6, #M+0 ; M+0..M+2 holds 16.0 mov R7, PTR ; point to attenuation digits lcall bcd_subb ; subtract ret ; called if signal level < 0.0 dBm. In this case, calculate (atten digits) - 16.0. ; moves the attenuation digits to M+0..M+2, and loads 16.0 into K+0..K+2 before ; subtracting. atten_bcd_subb2: mov A, R0 push ACC mov A, PTR mov R0, A ; AD+0 (AD2+0) mov A, @R0 ; load LSD mov M+0, A ; move to M+0 inc R0 ; move other digits to M+1, M+2 mov A, @R0 mov M+1, A inc R0 mov A, @R0 mov M+2, A mov K+0, #0 ; store 16.0 in K+0..K+2 mov K+1, #6 mov K+2, #1 mov R5, #3 ; 3 digits to subract mov R6, #M+0 ; R6 points to atten digits mov R7, #K+0 lcall bcd_subb ; subtract pop ACC mov R0, A ret ; ***** MULTIPLICATION ; multiply 4 byte value in L+0..L+3 with R0. leaves result in L+0..L+4, and ; clears L+5..L+7. mult4x1: mov A, R7 push ACC mov A, L+0 ; load the LSB mov B, R0 ; multiply by R0 mul AB mov L+0, A ; store the LSB of the 2 byte result as the LSB mov R7, B ; store the MSB of the 2 byte result in R7 mov A, L+1 ; load the next byte mov B, R0 ; multiply by R0 mul AB add A, R7 ; add the MSB of the result of the previous multiply mov L+1, A ; store the LSB of the multiply back in L+1 mov R7, B ; store the MSB of the multiply in R7 jnc n1 ; if the addition rolled over the LSB of the result, inc R7 ; then increment the MSB of the result n1: mov A, L+2 ; repeat this process for L+2, L+3 mov B, R0 mul AB add A, R7 mov L+2, A mov R7, B jnc n2 inc R7 n2: mov A, L+3 mov B, R0 mul AB add A, R7 mov L+3, A mov R7, B jnc n3 inc R7 n3: mov A, R7 mov L+4, A ; the MSB of the last multiply is the MSB of the 4x1 result mov L+5, #0 ; zero the higher bytes mov L+6, #0 mov L+7, #0 pop ACC mov R7, A ret ; multiplies 4-byte value in K+0..K+3 with 4-byte value in K+4..K+7. ; leaves result in M+0..M+7. mult4x4: lcall init_M ; clear M+0..M+8 lcall load_L_K ; transfer K+0..K+7 into L+0..L+7 mov A, L+4 ; hold L+4 mov R0, A ; in R0 lcall mult4x1 ; multiply L+0..L+3 by R0 (L+4), leave in L+0..L+4 lcall add8 ; M+0..M+8 += L+0..L+8 lcall load_L_K ; reload K+0..K+7 into L+0..L+7 mov A, L+5 mov R0, A lcall mult4x1 ; multiply L+0..L+3 by R0 (L+5), leave in L+0..L+4 mov R0, #1 ; shift L+0..L+4 1 byte to the right, so that lcall shift_n ; L+0 -> L+1 .. L+4 -> L+5 lcall add8 ; M+0..M+8 += L+0..L+8 lcall load_L_K mov A, L+6 mov R0, A lcall mult4x1 mov R0, #2 ; this time, shift the result by 2 bytes lcall shift_n ; i.e. L+0 -> L+2 .. L+4 -> L+6 lcall add8 lcall load_L_K mov A, L+7 mov R0, A lcall mult4x1 mov R0, #3 ; shift the result by 3 bytes this time lcall shift_n lcall add8 ; result is now in M+0..M+7 ret ; shifts the 5-byte value at L0..L4 by (R0) bytes to the right. For example, ; if R0=3, then L4-->L7, L3-->L6, etc. zeros any bytes that are evacuated ; and not filled in by a new value. shift_n: mov A, R1 push ACC mov A, R0 ; load offset add A, #L+4 ; add the address of the MSB mov R1, A ; ptr R1 = (L+4) + R0 mov A, L+4 ; load the MSB mov @R1, A ; transfer to its new address dec R1 ; decrement pointer mov A, L+3 ; transfer the other 4 bytes mov @R1, A dec R1 mov A, L+2 mov @R1, A dec R1 mov A, L+1 mov @R1, A dec R1 mov A, L+0 mov @R1, A ; LSB has been transferred mov R1, #L ; L+0 zro: mov @R1, #0 ; zero the byte inc R1 ; increment pointer djnz R0, zro ; more bytes to zero? pop ACC mov R1, A ret ; ***** DIVISION ; divides K0..K7 (K7 MSB) by K8..K11 (K11 MSB). leaves result in L0..L7 (L7 MSB), ; remainder in L8..L11 (L11 MSB). This is a full 32-bit divide function. ; the basic algorithm is as follows: ; ; SETUP PHASE ; 1) left justify the divisor by rotating it left. Store the number of bits ; that the divisor is rotated. Add 32 to this number to account for the 4 low ; order bytes of the 8-byte dividend. The result is the maximum number of bits ; that the dividend may be shifted. ; 2) subtract the divisor from the dividend. ; 3) if the result is negative, shift the dividend left by 1 bit. use the carry ; flag as an additional dividend bit to allow a full 32-bit divide capability. ; Repeat steps 2-3 until result is not negative. Subtract the number of bits ; shifted from the total obtained in step 1. ; ; DIVIDE PHASE ; 4) store subtraction remainder as the new high 4 bytes of dividend. ; 5) set the LSb (least-significant bit) of the quotient. ; 6) rotate the quotient and the dividend left by 1 bit. ; 7) subtra