В этой статье я исследую возможность использования C64 для решения математических задач. На примере решения пропорции я покажу, как для этих целей можно использовать BASIC, C и MOS 6510 (6502) Assembly, используя функции BASIC для работы с плавающей запятой.

Что я хочу делать?

И так я продолжаю свое путешествие во времени в 80-х. Я пытаюсь обосноваться здесь и использовать C64 как инструмент для учебы и работы.

В прошлых статьях:

Путешествие во времени к Commodore 64: программа Hello World на C.

Путешествие во времени к Commodore 64: программа Hello World на ассемблере.

Я немного узнал об инструментах, которые могу использовать: BASIC, Assembly, C. Вопреки моим ожиданиям, C мог быть здесь лучше, чем BASIC и Assembly. Пришло время сделать что-то более захватывающее и ценное, чем просто «Привет, мир».

Лучше всего для этого подходят задачи из школьного курса математики на решение пропорций. Формула эквивалентных отношений выглядит следующим образом.

Учитывая три известных и одно неизвестное, неизвестное можно вычислять непрерывно. Например, в формуле n1/d1 = n2/d2, если d1 неизвестно, его можно рассчитать следующим образом: d1 = n1 * d2/n2.

Пропорции достаточно просты, чтобы решить их без освоения инструментов программирования. В то же время калькулятор пропорций — вещь весьма полезная.

Его можно использовать для различных расчетов:

  1. Соотношение в пропорциях при составлении рецептов
  2. Перевод из одной единицы измерения в другую
  3. Проблемы с процентами.
  4. и т. д.

Первый подход

Я сделал прототип на Бейсике.

Достаточно хорошо работает.

Реализуется следующий алгоритм. штат Нью-Джерси

  1. Я прочитал параметры и использовал 0, чтобы подписать, что параметр неизвестен.
  2. С помощью ветвления выбираю нужную формулу для расчета.
  3. Вывожу результат.
  4. Предлагаю на выбор продолжить работу или выйти из программы.

Теперь хочу сделать более компактную версию программы с моим дизайном.

Поскольку я все еще не уверен в сборке MOS 6510(6502), я решил снова попробовать C на C64.

Дизайн экрана я, конечно, могу сделать и на БЕЙСИКЕ, но мне нужно быть более активным, чтобы вспомнить все это еще раз. Тем не менее, я хочу использовать более продвинутые инструменты.

Большой провал с C.

И так реализация на C.

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

Я начал с настройки параметров экрана с помощью библиотечных функций. Функции имеют примерную реализацию. При выборе новых параметров возвращают исходные. Очень удобно их сохранять для восстановления настроек при выходе из программы.

Функция вывода подсказки по использованию

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" );
}

Здесь я использую как библиотечные функции для C64, так и стандартные функции C для вывода текста и позиционирования.

Функция ввода аргумента.

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 );
}

Я использую библиотечную функцию для C64, чтобы включать и выключать мигание курсора.

Функция расчета отклика

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();
}

Меня ждал неприятный сюрприз, имеющийся компилятор C не поддерживает числа с плавающей запятой.
Это еще один довод в пользу использования ассемблера для разработки под C64.

В этой функции я нарушил принцип единоличной ответственности
Потому что помимо расчета ответа сюда возвращается выбор оператора продолжить расчеты или выйти и еще выводит подсказку.
Наверное исправлю это в будущем.

Последняя функция, которую я написал, это восстановление состояния экрана перед выходом из программы.

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

Здесь я использую те же функции, что и в настройках экрана.
Я также использую настройку системной переменной.
Эта команда эквивалентна POKE 53272.21, которая устанавливает режим вывода букв в верхний регистр.

Основная функция выглядит так.

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;
}

Он использует функцию библиотеки C64 для очистки экрана.

Вот рабочий пример

Полный исходный код программы доступен здесь.

Исполняемый файл получился 7167 байт.

Расчет пропорций возможности операции с плавающей запятой мог бы быть более полезным. Так что берусь за реализацию на ассемблере.

Сборка спасает мир.

Начну как и раньше с подготовки экрана.

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

Здесь происходит то же самое, что и в аналоге на C. Записываю в системные переменные по нужным адресам и значениям через аккумулятор.

Вот главная подсказка.

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

Здесь я использую три функции ядра

  1. set_cursor (0xE50C) Аргументы передаются через регистры X Y
  2. print_char (0xFFD2) код символа для вывода должен быть в аккумуляторе
  3. print_str(0xAB1E) в регистре Младший байт адреса строки в старшем байте Y

Строка должна заканчиваться на «\0», как в этом примере.

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

Далее идет кусок кода, отвечающий за ожидание и обработку реакции пользователя.

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

Чтобы считать символ в аккумулятор, я использую функцию getin(0xFFE4)

Далее следуют три проверки.

  1. Если аккумулятор равен 0, пользователь ничего не вводил. Повторите процедуру входа.
  2. Если пользователь выбирает «Q» в качестве выхода, я перехожу к соответствующей процедуре.
  3. Если любой другой символ, я перехожу к расчетам.

Вычисления начинаются со сбора аргументов

Их выполняет следующая функция.

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

Функции запускают аналогичные функции для получения каждого аргумента и, в конце концов, вызывают функцию, чтобы отключить мигание курсора.

cursor_blink_off:
        lda     $00cf
        beq     cursor_blink_off
        lda     #$01
        sta     $cc
        rts

По состоянию системной переменной 0x00CF эта функция дожидается момента, когда курсор погаснет и затем запись единицы в системную переменную 0x00CC отключает мигание курсора.

Функция получения аргумента уже сложнее.

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

Функция input_prompt выводит на экран приглашение ввода, как описано.
Далее начинается операция чтения строкового представления аргумента с клавиатуры.

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

В этой функции я сначала сбрасываю аккумулятор, все индексные регистры и счетчик.

Я вызываю функцию get_in

Далее я проверяю следующие случаи

  1. Если пользователь нажимает клавишу возврата, ввод завершен.
  2. Если пользователь ввел backspace, я отображаю символ возврата. При отображении этого символа курсор визуально перемещается на один символ назад. Далее я уменьшаю счетчик введенных символов и удаляю ранее введенный символ в буфере, записывая на его место ноль. После этого возвращаюсь к началу процедуры.

При вводе любого другого персонажа я выполняю следующие действия.

  1. Я сохраняю символ в буфере со смещением, равным счетчику введенных символов.
  2. Я отображаю персонажа на экране.
  3. Я увеличиваю счетчик.
  4. Я проверяю счетчик. При достижении максимума запись прекращается.
  5. В противном случае я перехожу к обработке следующего введенного символа.

Далее продолжает выполняться функция input_n1_proc.
После завершения ввода строки я использую следующую функцию
для преобразования строки в число с плавающей запятой.

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

Для этого я использую существующую функцию BASIC, которую можно вызвать через адрес (0xB7B5). В качестве аргументов по адресам 0x22 0x23 мне нужно поместить младший и старший байт адреса буфера со строкой, в регистр А - длину введенной строки. Далее я вызываю функцию преобразования. Полученный номер функция помещает в КВС, который находится по следующим адресам в ОЗУ:

  • Адрес 97/$61 является байтом экспоненты.
  • Адреса 98–101/$62–$65 содержат четырехбайтную (32-битную) мантиссу.
  • Адрес 102/$66 хранит знак в его старшем бите; 0 для положительного, $FF (-1) для отрицательного.
  • Адрес 112/$70 содержит биты округления для промежуточных вычислений.

Затем я очищаю буфер, записывая в него 0.

После этого я записываю число из КВС в переменную с помощью функции (0xBBD4). Функции нужен адрес 5-байтовой переменной в качестве аргумента, где будет храниться значение из FAC.

Один важный момент: некоторые операции с FAC повреждают данные в FAC. Вот пример функции для печати FAC на осыпи. Вот почему необходимо сначала сохранить данные из FAC перед любой операцией с FAC.

Далее вывожу пробел для перезаписи курсов и перевода строки.

Аналогично получаю и сохраняю все необходимые переменные.

Следующий блок кода

//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

Введенные переменные будут проверяться на 0 одна за другой с помощью функции сравнения с плавающей запятой (0xBC55B), которая сравнивает FAC с числом в памяти в A Y.
После того, как неизвестная переменная найдена, управление передается соответствующую функцию расчета.
Если оператор не ввел неизвестное значение, отображается сообщение об ошибке и предложение выйти из программы или продолжить расчеты.

Функция расчета выглядит следующим образом.

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

Функция выполняет умножение и деление чисел с плавающей запятой, используя функции, предоставляемые BASIC. Далее с помощью ранее описанной функции выводится результат.

Заключительный блок программы.

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

Отображает подсказку, предлагающую пользователю продолжить вычисления или выйти.
В зависимости от выбора пользователя, соответственно, возвращается к этапу ввода параметров или восстанавливает исходное состояние экрана и выходит в BASIC.

Восстановление экрана.

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

Программа работает так.

Полный текст программы доступен здесь
Размер исполняемого файла 3137 байт.
Примерно в два раза меньше, чем вариант на С.

Окончательный итог сегодняшнего путешествия.

Какие выводы можно сделать из проделанной работы?

  1. C в реалиях C64 в 80-х не так хорош, как в наше время.
  2. BASIC довольно хорош и имеет обширные возможности, но он должен быть быстрее, а программы получаются большими.
  3. Сборка, с одной стороны, выглядит лидером. Изучить и использовать его проще, чем казалось, в основном, если использовать готовые функции, которые предоставляют ядро ​​и Бейсик. Но скорость разработки все равно нужно улучшать, особенно если писать что-то более объемное.

Если внимательно присмотреться к ассемблеру, изучение BASIC кажется более актуальным. BASIC отлично подходит для написания прототипов программ до того, как они будут реализованы на ассемблере.

Это был хороший день в 80-х. В настоящее время я все больше и больше погружаюсь в реалии программирования. Когда вы пробуете что-то своими руками, многие психические искажения рассеиваются.

Мне здесь нравится все больше и больше, и я пока не спешу возвращаться. Спасибо за участие в этом путешествии.

Надеюсь, это было хотя бы весело.

Теплые пожелания.

Следите за моими следующими сообщениями.