Alexey Medvecky

Posted on

# A Retro Adventure: Implementing an Aspect Ratio Calculator for the Commodore 64 in Multiple Programming Languages

In this article, I explore the possibility of using C64 to solve mathematical problems. Using the example of solving the proportion, I will show how BASIC, C, and MOS 6510 (6502) Assembly can be used for these purposes using the BASIC functions for the floating point operation.

## What do I want to do?

And so I continue my journey through time in the 80s. I’m trying to settle down here and use C64 as a tool for study and work.

In past articles:

Time-Travelling to Commodore 64: Hello World Program on C.

Time-Travelling to Commodore 64: Hello World Program on Assembly.

I learned a little about the tools I can use: BASIC, Assembly, C. Contrary to my expectations, C could have been better here than BASIC and Assembly. It’s time to do something more exciting and valuable than just “Hello, world”.

Problems from the school mathematics course for solving proportions are best suited for this. The formula for equivalent ratios looks like this.

Given three knowns and one unknown, the unknown can continuously be computed. For example, in the formula n1 / d1 = n2 / d2, if d1 is unknown, it can be calculated as follows d1 = n1 * d2 / n2.

The proportions are simple enough to solve without mastering programming tools. At the same time, the proportion calculator is quite a helpful thing.

It can be used for various calculations:

1. The ratio in proportions in the preparation of recipes

2. Conversion from one unit of measurement to another

3. Percent-related problems.

4. etc

## First Approach

I made the prototype in BASIC.

Works well enough.

There is implemented following algorithm. nj

1. I read the parameters and used 0 to sign that the parameter is unknown.

2. Using branching, I select the desired formula for the calculation.

3. I bring out the result.

4. I offer the choice to continue the work or exit the program.

Now I want to make a more compact program version with my design.

Since I still feel unsure about the MOS 6510(6502) Assembly, I decide to try C again on C64.

Of course, I can also do the screen design in BASIC, but I need to be more active to remember all this again. Still, I want to use more advanced tools.

## Big fail with C.

And so the implementation in C.

``````void setUpScreen( void )
{
clrscr();
defaultBGColor = bgcolor( COLOR_BLACK );
defaultBorderColor = bordercolor( COLOR_BLACK );
defaultTextColor = textcolor( COLOR_GREEN );
}
``````

I started by setting screen parameters using library functions. Functions have an exemplary implementation. When selecting new parameters, they return the original ones. It is very convenient to save them to restore the settings when I exit the program.

Usage hint output function

``````void usage( void )
{
cputsxy( 12, 0, "RATIO CALCULATOR" );
cputsxy( 12, 2, "N1/D1 = N2/D2" );
gotoxy( 0, 4 );
puts( "Input ratio pramams by prompt" );
puts( "Press any key to continue" );
puts( "or q to exit" );
}
``````

Here I use both library functions for C64 and standard C functions for text output and positioning.

Argument input function.

``````void getArgs( int * n1, int * d1, int * n2, int * d2 )
{
cursor( 1 );
printf( "%s? ", "n1 (zero if unknown)" );
scanf( "%d", n1 );
printf( "%s? ", "d1 (zero if unknown)" );
scanf( "%d", d1 );
printf( "%s? ", "n2 (zero if unknown)" );
scanf( "%d", n2 );
printf( "%s? ", "d2 (zero if unknown)" );
scanf( "%d", d2 );
cursor( 0 );
}
``````

I use a library function for C64 to turn the cursor blinking on and off.

Response Calculation Function

``````char getAnswer( int n1, int d1, int n2, int d2 )
{

if ( n2 == 0 )
{
answer = d2 * n1 / d1;
printf( "n2 = %d\n", answer );
}
else if ( d2 == 0 )
{
answer = n2 * d1 / n1;
printf( "d2 = %d\n", answer );
}
else if ( n1 == 0 )
{
answer = d1 * n2 / d2;
printf( "n1 = %d\n", answer );
}
else if ( d1 == 0 )
{
answer = n1 * d2 / n2;
printf( "d1 = %d\n", answer );
}
else
{
puts( "Wrong params" );
}

puts( "Press any key to continue" );
puts( "or q to exit" );

return cgetc();
}
``````

An unpleasant surprise awaited me, the C compiler available does not support floating point numbers.
This is one more point in favour of using Assembly for development under C64.

In this function, I violated the principle of sole responsibility
Because in addition to calculating the answer, the operator’s choice to continue the calculations or exit is returned here and also displays a hint.
I’ll probably fix this in the future.

The last function I wrote is to restore the screen state before exiting the program.

``````void resetDefaultScreen( void )
{
clrscr();
bgcolor( defaultBGColor );
bordercolor( defaultBorderColor );
textcolor( defaultTextColor );
* ( char *  ) 0xD018 = 0x15;
}
``````

Here I use the same functions as in the screen settings.
I also use the system variable setting.
This command is equivalent to POKE 53272.21, which sets the output mode of letters to uppercase.

The main function looks like this.

``````int main ( void )
{
int n1 = 0;
int d1 = 0;
int n2 = 0;
int d2 = 0;
unsigned character = 0;

setUpScreen();

usage();
character = cgetc();

while ( character != 'q' )
{
getArgs( &n1, &d1, &n2, &d2 );
character = getAnswer( n1, d1, n2, d2 );

clrscr();
}

resetDefaultScreen();

return EXIT_SUCCESS;
}
``````

It uses the C64 library function to clear the screen.

Here is a working example

The complete source code of the program is available here.

The executable file turned out to be 7167 bytes.

Calculating the proportions of the possibility of a floating point operation could be more helpful. So I take up the implementation in Assembly.

## Assembly Saves the World.

I’ll start as before with the preparation of the screen.

``````prepare_screen:
// set gren text color
lda     #5
sta     text_color
jsr     clearscreen

// set border and background black
lda     #\$00
sta     border
sta     background
rts
``````

Here, the same thing happens as in the analogue in C. I write to the system variables at the desired addresses and values through the accumulator.

Here is the main hint.

``````main_usage:
ldx     #\$00
ldy     #\$0a
jsr     set_cursor

lda     #<usage_1
ldy     #>usage_1
jsr     print_str

lda     #new_line
jsr     print_char
jsr     print_char

lda     #<usage_1_1
ldy     #>usage_1_1
jsr     print_str

lda     #new_line
jsr     print_char
jsr     print_char

lda     #<usage_2
ldy     #>usage_2
jsr     print_str

lda     #new_line
jsr     print_char

lda     #<usage_3
ldy     #>usage_3
jsr     print_str
lda     #new_line
jsr     print_char
rts
``````

Here I use three kernal functions

1. set_cursor (0xE50C) Arguments are passed through X Y registers

2. print_char (0xFFD2) character code for output must be in the accumulator

3. print_str(0xAB1E) in register A low byte of row address in Y high byte

The string must end with ‘\0’ as in this example.

``````usage_1:
.text   "RATIOS CALCULATOR"
.byte   \$00
``````

Next comes a piece of code responsible for waiting and processing the user’s reaction.

``````wait_for_continue:
jsr     getin
beq     wait_for_continue
cmp     #q_sym
beq     restore_and_exit_jmp
jmp     get_args

restore_and_exit_jmp:
jmp     restore_and_exit
``````

To read a character into the accumulator, I use the getin(0xFFE4) function

Three checks follow.

1. If the accumulator is 0, the user did not enter anything. Repeat the entry procedure.

2. If a user selects “Q “ as an exit, I proceed to the appropriate procedure.

Calculations begin with the collection of arguments

The following function performs them.

``````get_arguments:
jsr     input_n1_proc
jsr     input_d1_proc
jsr     input_n2_proc
jsr     input_d2_proc
rts
``````

Functions run similar functions for receiving each argument and, in the end, call the function to turn off the cursor blinking.

``````cursor_blink_off:
lda     \$00cf
lda     #\$01
sta     \$cc
rts
``````

According to the state of the system variable 0x00CF, this function waits for the moment when the cursor will go out and then writing one to the system variable 0x00CC disables the cursor blinking.

The function for getting an argument is already more complicated.

``````input_n1_proc:
jsr     input_n1_prompt
jsr     input_string_proc
jsr     string_to_fp
ldx     #<n1
ldy     #>n1
jsr     fp_store_fac_to_ram
lda     #space_sym
jsr     print_char
lda     #new_line
jsr     print_char
rts
``````

The input_prompt function prints an input prompt to the screen as described.
Next, the operation of reading the string representation of the argument from the keyboard starts.

``````input_string_proc:
ldy #\$00
ldx     #\$00
lda     #\$00
sta     counter
input_get:
jsr     getin
beq     input_get
cmp     #\$0d
beq     input_string_end
cmp     #\$14
bne     increase_counter
jsr     print_char
ldx     counter
dex
stx     counter
lda     #\$00
sta     input_string,x
jmp     input_get
increase_counter:
ldx     counter
sta     input_string,x
jsr     print_char
inx
stx     counter
cpx     #string_length
bne input_get
input_string_end:
rts
``````

In this function, I first reset the accumulator, all index registers, and the counter.

I call the get_in function

Next, I check for the following cases

1. If the user presses return, the input is complete.

2. If the user has entered backspace, I display the backspace character. Showing this character causes the cursor to jump back one character visually. Next, I decrease the counter of entered characters and delete the previously entered character in the buffer by writing zero in its place. After that, I return to the beginning of the procedure.

When entering any other character, I perform the following actions.

1. I store the character in the buffer with an offset equal to the counter of the entered characters.

2. I display the character on the screen.

3. I increment the counter.

4. I check the counter. If the maximum is reached, then an entry is terminated.

5. Otherwise, I proceed to the processing of the next entered character.

Further, the function input_n1_proc continues to be executed.
After the input of the string is completed, I use the following function
to convert the string to float.

``````string_to_fp:
lda     #<input_string
sta     \$22
lda     #>input_string
sta     \$23
lda     #string_length
jsr     fp_string_to_fac
jsr     clear_input_string
rts
``````

To do this, I use the existing BASIC function that can be called through the address (0xB7B5). As arguments at addresses 0x22 0x23, I need to place the low and high byte of the address of the buffer with the string, in register A, the length of the entered string. Next, I call the conversion function. The function puts the received number in the FAC, which is located at the following addresses in the RAM:

• Address 97/\$61 is the exponent byte.

• Addresses 98–101/\$62–\$65 hold the four-byte (32 bit) mantissa.

• Address 102/\$66 stores the sign in its most significant bit; 0 for positive, \$FF (-1) for negative.

• Address 112/\$70 contains rounding bits for intermediate calculations.

Then I clear the buffer by writing 0 to it.

After that, I write the number from the FAC to a variable using the function (0xBBD4) . Funcion needs address of 5 bytes variable as argument where will store value from FAC.

One important point some of operations with FAC corupt data in FAC. Fro example function for print FAC to scree. That’s why need in first store data from FAC before any operation with FAC.

Next, I output space to overwrite the courses and translate the line.

Similarly, I get and save all the necessary variables.

Next block of code

``````//check n1
lda     #<n1
ldy     #>n1
lda     #<fp_zero
ldy     #>fp_zero
jsr     fp_cmp
cmp     #\$00
beq     solve_for_n1

..................................

lda     #<wrong_params
ldy     #>wrong_params
jsr     print_str
lda     #new_line
jsr     print_char

jmp     wait_to_exit

solve_for_n1:
jsr     solve_for_n1_proc
jmp     wait_to_exit
``````

The entered variables will be checked for 0 one by one using the floating point comparison function (0xBC55B), which compares the FAC with the number in memory at A Y.
After the unknown variable is found, control is transferred to the corresponding calculation function.
If the operator has not entered an unknown value, an error message and a suggestion to exit the program or continue calculations are displayed.

The calculation function looks like this.

``````solve_for_n1_proc:
lda     #<d1
ldy     #>d1

lda     #<n2
ldy     #>n2
jsr     fp_mult

ldx     #<n2
ldy     #>n2
jsr     fp_store_fac_to_ram

lda     #<d2
ldy     #>d2

lda     #<n2
ldy     #>n2
jsr     fp_div

ldx     #<n1
ldy     #>n1
jsr     fp_store_fac_to_ram

jsr     result_n1_prompt

lda     #<n1
ldy     #>n1

jsr     fp_fac_print
lda     #new_line
jsr     print_char
rts
``````

The function performs multiplication and division of floating point numbers using the functions provided by BASIC. Further, using the previously described function, the result is output.

The final block of the program.

``````wait_to_exit:
jsr     usage_at_exit
wait_for_input:
jsr     getin
beq     wait_for_input
cmp     #q_sym
bne     continue
jmp     restore_and_exit
continue:
jsr     clearscreen
jmp     get_args
restore_and_exit:
jsr     restore_screen

rts
``````

Displays a hint prompting a user to continue computing or exit.
Depending on the user’s choice, respectively returns to the parameter input stage or restores the initial state of the screen and exits to BASIC.

Screen recovery.

``````estore_screen:
// restore border and background colors
lda     #\$0f6
sta     background
lda     #\$fe
sta     border

// restore text color
lda     #\$e
sta     text_color
jsr     clearscreen

// restore text mode
lda     #\$015
sta     \$d018
rts
``````

The program works like this.

The full text of the program is available here
The executable file size is 3137 bytes.
Approximately two times less than the option on C.

## Final conclusion of today’s journey.

What conclusions can be drawn from the work done?

1. C in the realities of C64 in the 80s is not as good as in our time.

2. BASIC is quite good and has extensive features, but it needs to be faster, and programs come out large.

3. Assembly, on the one hand, looks like a leader. Learning and using it is easier than it seemed, mainly if you use the ready-made functions that kernal and BASIC provide. But the development speed still needs to be improved, especially if you write something more voluminous.

If you look closely at Assembly, learning BASIC seems more valid now. BASIC is just fine for writing program prototypes before they are implemented in Assembly.

It was a good day in the 80s. I am more and more immersed in the realities of programming at this time. When you try something with your hands, many mental distortions dissipate.

I like it here more and more, and I’m still in no hurry to return. Thank you for being a part of this journey.

I hope it was at least fun.

Warm regards.

Stay tuned for my next posts.