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

Alexey Medvecky
11 min readJul 11, 2023

--

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 )
{
int answer = 0;

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;
int answer = 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.
  3. If any other character, I jump to calculations.

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
jsr cursor_blink_off
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
beq cursor_blink_off
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
jsr fp_load_ram_to_fac
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
jsr fp_load_ram_to_fac

lda #<n2
ldy #>n2
jsr fp_mult

ldx #<n2
ldy #>n2
jsr fp_store_fac_to_ram

lda #<d2
ldy #>d2
jsr fp_load_ram_to_fac

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_load_ram_to_fac

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.

--

--