В этой статье я исследую возможность использования 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.
Пропорции достаточно просты, чтобы решить их без освоения инструментов программирования. В то же время калькулятор пропорций — вещь весьма полезная.
Его можно использовать для различных расчетов:
- Соотношение в пропорциях при составлении рецептов
- Перевод из одной единицы измерения в другую
- Проблемы с процентами.
- и т. д.
Первый подход
Я сделал прототип на Бейсике.
Достаточно хорошо работает.
Реализуется следующий алгоритм. штат Нью-Джерси
- Я прочитал параметры и использовал 0, чтобы подписать, что параметр неизвестен.
- С помощью ветвления выбираю нужную формулу для расчета.
- Вывожу результат.
- Предлагаю на выбор продолжить работу или выйти из программы.
Теперь хочу сделать более компактную версию программы с моим дизайном.
Поскольку я все еще не уверен в сборке 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
Здесь я использую три функции ядра
- set_cursor (0xE50C) Аргументы передаются через регистры X Y
- print_char (0xFFD2) код символа для вывода должен быть в аккумуляторе
- 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)
Далее следуют три проверки.
- Если аккумулятор равен 0, пользователь ничего не вводил. Повторите процедуру входа.
- Если пользователь выбирает «Q» в качестве выхода, я перехожу к соответствующей процедуре.
- Если любой другой символ, я перехожу к расчетам.
Вычисления начинаются со сбора аргументов
Их выполняет следующая функция.
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
Далее я проверяю следующие случаи
- Если пользователь нажимает клавишу возврата, ввод завершен.
- Если пользователь ввел backspace, я отображаю символ возврата. При отображении этого символа курсор визуально перемещается на один символ назад. Далее я уменьшаю счетчик введенных символов и удаляю ранее введенный символ в буфере, записывая на его место ноль. После этого возвращаюсь к началу процедуры.
При вводе любого другого персонажа я выполняю следующие действия.
- Я сохраняю символ в буфере со смещением, равным счетчику введенных символов.
- Я отображаю персонажа на экране.
- Я увеличиваю счетчик.
- Я проверяю счетчик. При достижении максимума запись прекращается.
- В противном случае я перехожу к обработке следующего введенного символа.
Далее продолжает выполняться функция 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 байт.
Примерно в два раза меньше, чем вариант на С.
Окончательный итог сегодняшнего путешествия.
Какие выводы можно сделать из проделанной работы?
- C в реалиях C64 в 80-х не так хорош, как в наше время.
- BASIC довольно хорош и имеет обширные возможности, но он должен быть быстрее, а программы получаются большими.
- Сборка, с одной стороны, выглядит лидером. Изучить и использовать его проще, чем казалось, в основном, если использовать готовые функции, которые предоставляют ядро и Бейсик. Но скорость разработки все равно нужно улучшать, особенно если писать что-то более объемное.
Если внимательно присмотреться к ассемблеру, изучение BASIC кажется более актуальным. BASIC отлично подходит для написания прототипов программ до того, как они будут реализованы на ассемблере.
Это был хороший день в 80-х. В настоящее время я все больше и больше погружаюсь в реалии программирования. Когда вы пробуете что-то своими руками, многие психические искажения рассеиваются.
Мне здесь нравится все больше и больше, и я пока не спешу возвращаться. Спасибо за участие в этом путешествии.
Надеюсь, это было хотя бы весело.
Теплые пожелания.
Следите за моими следующими сообщениями.