Creating a Calculator for the Commodore 64 Using the C Language

Alexey Medvecky
12 min readMay 9, 2024

Introduction

I hadn’t been documenting new discoveries and projects for a while, but today, I am eager to share my latest adventure back in the 1980s. During this period, I embarked on developing software for an intriguing device — the Commodore 64. My research revealed that while calculators were available for many platforms, including Atari, there was no reliable engineering calculator specifically for the Commodore 64.

This gap inspired me to create my own solution, which had the potential to evolve into a successful startup.

Task and Solution

Initial Concept and Ambitions

The original concept was to create a scientific calculator, starting with the ambitious task of developing my own floating-point number system. However, due to complexities and memory constraints, I quickly shifted to using standard BASIC ROM procedures.

Choice of Algorithm

I selected reverse Polish notation (RPN) as the foundational algorithm because it simplifies the processing of expressions by avoiding issues related to operation priority and parentheses. This approach was already employed in many engineering calculators of the era.

The core algorithm was as follows:

  • The user enters a string containing a chain of RPN operations.
  • The application parses the string from start to finish.
  • If the current token is a number, it is placed in the stack.
  • If the current token is an operation:
  • Take arguments from the top of the stack.
  • Perform the operation.
  • Place the result back in the stack.

After processing the string, the result of the sequence of operations appears at the top of the stack.

Implementation of the Stack

The stack, a central element of the algorithm, was implemented using a linked list to economically utilize the limited memory resources of the Commodore 64. The numbers were stored in a format similar to the Microsoft Binary Format.

Specifically, it used a 40-bit MBF.

I defined storage for this format as follows:

Accordingly, the linked list node for storing these numbers was designed as follows:

The basic stack implementation was structured as follows:

The function takes two parameters: a pointer to the top of the stack (topPtr) and the value to be added to the stack (value). The topPtr is a pointer to a pointer, allowing the function to modify the actual pointer to the top of the stack.

At the start of the function, a new node (newPtr) is allocated on the heap using the malloc function. The size of the memory allocated is equal to the size of a StackNode.

The function then checks if the memory allocation was successful by checking if newPtr is not NULL. If the memory allocation was successful, the function proceeds to copy the value into the data field of the new node using the memcpy function. The nextPtr field of the new node is then set to point to the current top of the stack. Finally, the topPtr is updated to point to the new node, effectively adding the new node to the top of the stack.

If the memory allocation was not successful (i.e., newPtr is NULL), the function prints the value that was not inserted using the FP_printRaw function and outputs an error message indicating that no memory is available.

The function takes one parameter: a pointer to the current node in the stack (currentPtr).

At the start of the function, it checks if the stack is empty by checking if currentPtr is NULL. If the stack is empty, it outputs a message indicating that the stack is empty using the puts function.

If the stack is not empty, it outputs a message indicating the top of the stack. It then enters a while loop that continues as long as currentPtr is not NULL. Inside the loop, it does the following:

  1. It copies the data from the current node to resultFp using the memcpy function. The size of the data copied is equal to the size of FP.
  2. It calls the enableBasicRom function.
  3. It calls the FP_printResult function to print the result. This function prints the contents of resultFp.
  4. It calls the disableBasicRom function.
  5. It updates currentPtr to point to the next node in the stack.

After the loop, it outputs a message indicating the bottom of the stack. This function, therefore, prints the contents of the stack from top to bottom.

The function takes two parameters: a pointer to the top of the stack (topPtr) and a pointer to a variable where the popped value will be stored (popValue). The topPtr is a pointer to a pointer, allowing the function to modify the actual pointer to the top of the stack.

At the start of the function, it checks if the stack is empty by calling the Stack_isEmpty function with *topPtr as the argument. If the stack is empty, it outputs a message indicating that the stack is empty using the puts function and returns EXIT_FAILURE.

If the stack is not empty, it proceeds to pop the top element from the stack. It first stores the pointer to the top node in a temporary variable (tempPtr). It then copies the data from the top node to *popValue using the memcpy function. The size of the data copied is equal to the size of FP.

Next, it updates topPtr to point to the next node in the stack, effectively removing the top node from the stack. It then frees the memory allocated to the removed node using the free function.

Finally, the function returns EXIT_SUCCESS to indicate that the operation was successful.

Calculator Functionality

The calculator began with basic functionality:

- Parsing the string: Inputting a string with RPN operations, splitting it into tokens, recognizing numbers and operations.

- Processing numbers: String data of numbers were processed and transformed into floating-point numbers using BASIC ROM functions.

- Executing operations: Basic functions implemented arithmetic operations and power calculations.

Once the stack was ready, I proceeded to implement the calculator’s primary function. The first step was to receive the string with calculation arguments and split it into tokens using standard C functions.

Tokens were then divided into commands and numbers and processed accordingly.

Processing Numbers

In this function, I first validated the string representation of the number. Then, I copied this string to a memory area, which I later passed as an argument to a BASIC ROM function to convert the string to a floating-point number.

The function, called by the address B7B5, converts a numerical PETSCII string into a floating-point number in FAC Expects string address in $22/$23 and the length of the string in the accumulator.

The function, called by the address BBD4, stores the number currently in FAC to a RAM location. It uses X and Y rather than A and Y to point to RAM. (X=Addr.LB, Y=Addr.HB)

Using the stack’s abstract data type, I saved the processed number in the stack.

Processing Commands

For the first version, I implemented four arithmetic operations and a power function, which could also compute roots.

A switch operator was used to select operation handlers.

The function handlers utilized assembly to call BASIC ROM functions for operations with floating-point numbers.

For example:

The function, called by the address BA28 multiplies a number from RAM and FAC (clobbers ARG, A=Addr.LB, Y=Addr.HB)

In subsequent versions, I began using lookup tables to select handlers as the number of operations increased.

The function takes one parameter: a pointer to a character string (token), which represents the name of the operation to be performed.

At the start of the function, a pointer (op) is initialized to point to the first element of the operations array.

The function then enters a for loop that continues as long as the name field of the current Operation is not NULL. Inside the loop, it does the following:

  1. It compares the token with the name field of the current Operation using the strcmp function. If they are equal (i.e., strcmp returns 0), it proceeds to the next steps. Otherwise, it moves to the next Operation in the array.
  2. It calls the handleOneOperandOperation function with the func field of the current Operation as the argument. This function gets the argument from the stack and calls the BASIC ROM function to handle the operation.
  3. It calls the Stack_push function to push the result of the operation (stored in resultFp) onto the stack. The stackPtr is passed as the pointer to the top of the stack.
  4. It returns EXIT_SUCCESS to indicate that the operation was successful.

If the function exits the loop without finding a matching operation, it outputs a message indicating that the function is invalid and returns EXIT_FAILURE.

This formed the foundation for the first version of the calculator, which looked like this:

Versions and Extensions

Version 2.0

In the next release, I added functions for engineering calculations, improved error handling and validation, and expanded the functionality of the stack (viewing and modifying elements).

Then, I implemented the swap and dupe functions from Forth.

These functions allowed for writing more complex sequences of calculations.

Version 3.0

A significant new feature was the operation history, which facilitated the reuse and editing of previous calculations. This enhancement significantly simplified the use of the calculator, making it more convenient for complex engineering tasks. The history function was tasked with:

1. Saving the last n operations.

2. Reusing a line from history.

3. Editing a line from history before use.

The implementation of history used a queue based on a linked list.

The function takes one parameter: the capacity of the queue (capacity).

At the start of the function, memory is allocated for a new Queue structure using the malloc function. The size of the memory allocated is equal to the size of a Queue. The returned pointer from malloc is cast to a Queue * and stored in q.

Next, the head and tail pointers of the new queue are initialized to NULL, indicating that the queue is initially empty. The size field of the queue is also initialized to 0.

The capacity field of the queue is then set to the capacity parameter passed to the function. This sets the maximum number of elements that the queue can hold.

Finally, the function returns the pointer to the newly created queue (q).

The function Queue_enqueue is designed to add a new element to a queue data structure. The queue is implemented as a linked list, with each node in the list containing a pointer to a string (data) and a pointer to the next node (next).

The function begins by allocating memory for a new node. It then checks if the queue is at its capacity. If it is, the function dequeues the oldest element to make room for the new one. This is done by moving the head pointer to the next node in the queue, effectively removing the current head node from the queue. If this operation leaves the queue empty (i.e., q->head is NULL), it also sets the tail pointer to NULL. The function then frees the memory allocated for the string data and the dequeued node, and decrements the queue's size.

Next, the function allocates memory for the new element’s data (a string), copies the input string into this memory, and sets the next pointer of the new node to NULL.

Finally, the function adds the new node to the queue. If the queue is currently empty (i.e., q->tail is NULL), it sets both the head and tail pointers to the new node. Otherwise, it adds the new node at the end of the queue by setting the next pointer of the current tail node to the new node, and then updating the tail pointer to the new node. The function then increments the queue's size.

The function Queue_getNthElement is designed to retrieve the nth element from a queue data structure. The queue is implemented as a linked list, with each node in the list containing a pointer to a string (data) and a pointer to the next node (next).

The function begins by initializing an index counter to 0, a pointer current to the head of the queue, and a copy pointer to NULL. The variable isNeedToClear is set to 1, but it's not clear from this code snippet what its purpose is.

The function then decrements n by 1. This is because in C, arrays and similar data structures are zero-indexed, meaning the first element is at index 0.

Next, the function checks if n is greater than or equal to the size of the queue. If it is, this means the requested index is out of range, so the function prints an error message and returns NULL.

If n is within range, the function enters a while loop that continues as long as current is not NULL and index is less than n. Inside the loop, current is updated to point to the next node in the queue, and index is incremented by 1. This loop effectively moves the current pointer n nodes along the queue.

Once the loop finishes, current points to the nth node in the queue. The function then allocates memory for copy, copies the data from the nth node into this memory, and returns copy. This means the function returns a copy of the nth element's data, not a pointer to the data itself.

The function historyGetLastElement is designed to retrieve the last element from a queue and handle it. The queue is presumably a queue of strings, given the char * type of lastElement.

The function begins by calling Queue_getLastElement with queuePtr as an argument. Queue_getLastElement is function that retrieves the last element from a queue, and queuePtr is a pointer to the queue from which to retrieve the element. The retrieved element is stored in lastElement.

Next, the function checks if lastElement is not NULL. If lastElement is NULL, this would indicate that the queue is empty, so there is no element to handle.

If lastElement is not NULL, the function calls handleArgumentString with lastElement as an argument.

Eventually, the calculator was enhanced to include these features:

Example of approximating Pi using Newton’s method! I employ the formula Xn+1 = Xn — F(Xn)/F’(Xn), where F(X) = cos(X) + 1 (root at Pi) and its derivative F’(X) = -sin(X), starting our first approximation with X0 = 3.

Example of numerical derivative calculation:

For those interested in delving deeper into the technical specifics or exploring the source code of my latest project, the complete repository is available here. This comprehensive resource provides access to all the developmental stages and updates of my project.

Additionally, to get a more personal glimpse into the creative process behind our projects or to connect with the project’s author, follow my Instagram here. This platform offers regular updates and behind-the-scenes content directly from the creator, enhancing your understanding of the project’s journey and the minds behind it.

Conclusion

Despite its technical limitations, the Commodore 64 calculator project proved successful and had the potential for commercial success during its era. The implementation in C was chosen for ease of support, despite being less optimal than assembly. This project not only underscores the ability to create useful and functional applications within the constraints of limited technology but also highlights the importance of understanding and recreating fundamental data structures like queues and stacks. These structures are crucial for efficient algorithm design and can significantly enhance the functionality of even basic computing systems. Stay tuned and continue with me on my time travel journey as we explore more innovative creations from the past, demonstrating the timeless nature of ingenuity and determination.

--

--