DEV Community

Alexey Medvecky
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 );
}
Enter fullscreen mode Exit fullscreen mode

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" );
}
Enter fullscreen mode Exit fullscreen mode

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 );
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)