Casio PD‑700 — Часть 6: хранение программ, данных и переменных

    После того, как стало ясно, по каким адресам располагаются пользовательские данные, имеет смысл разобраться, в каком именно виде они хранятся. Для начала я постарался понять, как хранится в RAM программа, с помощью которой я считывал этот самый RAM. Для этого я взял часть данных, начинающихся с адреса &H000FA0, и приступил к анализу их содержимого.

    Программа была записана в ячейку "P2", и, как следствие, первые 2 байта, являющиеся содержимым "P0" и "P1", хранят значение &H00, а программа "P2" начинается с адреса &H000FA2. После сопоставления 16-ричных данных с программой на BASIC картина начала проясняться.
    10     DEFSEG  =  0  :  CLS
0B 0A00 20  0478  3D 30 01 0471 00
    20     OPEN        "COM0:"        FOR     OUT  PUT     AS      #1
1C 1400 20 0497 20 22434F4D303A22 20 0481 20 0499 04A5 20 07BC 20 2331 00
    30      FOR     I  =  0     TO        65535
13 1E00 20 0481 20 49 3D 30 20 07C1 20 3635353335 00
    40     PRINT     #1   ,    RIGHT$  ( HEX$  ( PEEK     I  )  ,  2  )  ;
1A 2800 20  04A3 20 2331 2C 20  069D  28 06A3 28 0586 20 49 29 2C 32 29 3B 00
    50     NEXT     I
08 3200 20 0482 20 49 00
    60     CLOSE      #1
09 3C00 20  0472  20 2331 00
    70     BEEP  : BEEP1   : BEEP  : BEEP1   : BEEP  : BEEP1
18 4600 20 0470 01 047031 01 0470 01 047031 01 0470 01 047031 00
00
    Уже можно заметить, что первый байт хранит количество байт в строке, конец строки и конец программы помечаются как &H00. Пробелы хранятся символом ASCII (&H20), а двоеточия, служащие разделителем, хранятся как &H01, и это не соответствует символу ASCII, а скорее играет роль управляющего байта. Команды хранятся в 2-х байтовом виде и выглядят так же, как и во многих подобных устройствах.

    Далее я приступил к опытам: вводил разное и проверял как оно токенизируется. Коротко опишу основные моменты:
  • 00 - конец строки или программы
  • 01 - ":", разделитель
  • 02 - "'", начало комментария
  • 03 - начало номера строки для перехода
  • 04..07 – первый байт 2-х байтовой команды
  • 08..1F - как при отсутствующей команде: 2 байта заменяет на "??"
  • Калькулятор проверяет, чтобы в начале строки был номер, удаляет пробелы, если они стоят между двумя цифрами, проверяет, чтобы результат был в диапазоне 1-65535, переводит в 2-х байтовое значение в little-endian.
  • Добавляет пробел после номера строки, если его не было.
  • Заменяет команды на их внутренний 2-х байтовый код.
  • Двоеточия для разделения команд заменяет на код &H01.
  • Апостроф для начала комментария заменяет на код &H02.
  • Остальные знаки пунктуации, всё, что не сошлось с именами команд, всё между кавычками, всё после DATA, REM и апострофа (комментарии) сохраняет как есть в соответствии с таблицей символов.
  • Перед ELSE добавляет код &H01, даже если там уже были двоеточия, и скрывает добавленное при просмотре кода.
  • Номера строк для перехода (после GOTO, GOSUB, THEN, ELSE, RESUME, RESTORE) сохраняет как 2-х байтовые числа в little-endian, а перед ними добавляет код &H03, означающий, что дальнейшее следует интерпретировать как номер. В отличие от FX‑880P, тут интерпретатор ожидает только такой формат, и не умеет читать число в виде ASCII-кодов цифр.
  • Добавляет &H00 в конец строки.
  • Добавляет в начало строки количество байт в получившейся токенизированной строке.
  • В конце программы ещё раз добавляет &H00.
Некоторые интересные наблюдения:
    Во время выполнения программы первый байт каждой строки (означающий количество байт в строке) и адрес конца программы не имеют значения: конец строки и конец программы определяются только по &H00 в самом коде.
    Хотя длина строки ограничена 255 байтами, если принудительно вставить в RAM более длинную строку, она выполняется правильно, но криво отображается при просмотре.
    Несмотря на то, что калькулятор не даёт сохранить строку с номером 0, если принудительно заменить байты в номере на &H0000, программа работает, но эта строка не удаляется обычным способом.


Хранение файлов DATA ("D0"-"D9"):
  • После каждого добавленного элемента добавляет разделитель &H0D0A (стандартный CR LF).
  • Текст сохраняет как есть и без кавычек.
    Пример: PRINT #1,"TEXT"  =>  "54455854 0D0A"
  • Числа сохраняет с добавлением пробела в начало и в конец (передний пробел резервирует место для знака "-").
    Пример: PRINT #1,123  =>  "20 313233 20 0D0A"
  • Длинные числа переводит в экспоненциальную форму.
    Пример: PRINT #1,123456789012345  =>  "20 31 2E 3233343536373839 45 2B 3134 20 0D0A" ( 1.23456789E+14 )
  • При добавлении ";" в конце, следующее пишется подряд как одно целое.
    Пример: PRINT #1,"ABCDEFG";12345  =>  "41424344454647 20 3132333435 20 0D0A"
  • При добавлении элементов через запятую, для элемента перед ней выделяется область в размере 20 байт, оставшееся заполняется пробелами (&H20). Если данных ровно 20 байт, выделяется новая область.
    Пример: PRINT #1,"ABCDEFG",12345  =>  "41424344454647 20202020202020202020202020 20 3132333435 20 0D0A"
  • Конец файла помечается как "&H1A".
    Числа, записанные через запятую или через точку с запятой, считываются как отдельные данные. Но текст считывается как одно целое до разделителя &H0D0A.
    Пример 1: PRINT #1,123;456    INPUT #1,A:PRINT A  =>  " 123 "
    Пример 2: PRINT #1,"ABC";123  INPUT #1,A$:PRINT A$  =>  "ABC 123 "


Хранение переменных и массивов:

    Имена переменных и массивов пишутся от конца области переменных в начало в порядке их объявления. Значения числовых переменных пишутся перед именами (ближе к концу), а значения текстовых переменных пишутся от начала области переменных.

    Имя переменной или массива:
  • Первая тетрада - длина имени.
  • Вторая тетрада - тип переменной:
  • "0" (0000) - текст
  • "4" (0100) - число
  • "8" (1000) - текстовый массив
  • "C" (1100) - числовой массив
  • Далее - само имя.
  • Последние 2 байта - адрес значения переменной.

    Значение числовой переменной:
8 байт, little-endian. После перестановки в big-endian для удобства анализа:
  • Первая тетрада - всегда "0".
  • Далее 12 тетрад - число в формате BDC в экспоненциальной форме.
  • Следующая тетрада - знак числа и экспоненты:
  • "0" (0000) - число положительно, экспонента отрицательна.
  • "1" (0001) - число положительно, экспонента положительна.
  • "5" (0101) - число отрицательно, экспонента отрицательна.
  • "6" (0110) - число отрицательно, экспонента положительна.
  • И последний байт - экспонента (отрицательная экспонента хранится как 100 минус значение).
Пример:  "02 01 00 00 00 00 23 01"  =>  "0 123000000000 1 02"  =>  1.23, число положительно, экспонента положительна, экспонента = 2.

    Значение текстовой переменной:
  • Первый байт – длина.
  • Далее сама переменная.
Для наглядного примера очищу переменные с помощью CLEAR и создам одну текстовую и одну числовую переменную:
TEXT$="ABCDEFG"
START=123
В памяти это будет храниться так:
[00F958]
[F958]
07 41424344454647 => Длина = 7, Значение = ABCDEFG
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
[FF41]
0201000000002301 => Экспонента = 2, Знак = 1, Значение = 1.23

54 5354415254 3DFF => Длина имени = 5, Число, Адрес = FF41
40 54455854 58F9 => Длина имени = 4, Текст, Адрес = F958
[00FF58]

    Данные массива:
Как и переменные, числовые массивы хранятся перед именами (ближе к концу), а текстовые от начала области переменных.
  • Первый байт – мерность массива.
  • Далее по 2 байта - длины каждой мерности.
  • После этого перечисляются все элементы массива.
Для примера можно написать программу, очищающую область переменных, а затем создающую и заполняющую двумерный массив:
10 CLEAR: DIM ARR(2,2)
20 ARR(0,0)=1:ARR(0,1)=11:ARR(0,2)=111
30 ARR(1,0)=2:ARR(1,1)=22:ARR(1,2)=222
40 ARR(2,0)=3:ARR(2,1)=33:ARR(2,2)=333
После её выполнения содержимое памяти будет следующим:
[00F958]
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
[FF05]
02 0200 0200 => Мерность = 2, Длина1 = 2, Длина2 = 2
0001000000000001 => Экспонента = 0, Знак = 1, Значение = 1.0
0101000000001001 => Экспонента = 1, Знак = 1, Значение = 1.1
0201000000001101 => Экспонента = 2, Знак = 1, Значение = 1.11
0001000000000002 => Экспонента = 0, Знак = 1, Значение = 2.0
0101000000002002 => Экспонента = 1, Знак = 1, Значение = 2.2
0201000000002202 => Экспонента = 2, Знак = 1, Значение = 2.22
0001000000000003 => Экспонента = 0, Знак = 1, Значение = 3.0
0101000000003003 => Экспонента = 1, Знак = 1, Значение = 3.3
0201000000003303 => Экспонента = 2, Знак = 1, Значение = 3.33
3C 415252 05FF => Длина имени = 3, Числовой, Адрес = FF05
[00FF58]


Организация некоторых данных в области стека:

    Для выполнения цикла FOR выделяется по 26 байт на каждый уровень вложенности:
  • 3 B - адрес имени переменной в NEXT (или байта после NEXT, если имя опущено) в коде программы.
  • 3 B - адрес начала выполняемого в цикле (следующего байта после объявления FOR) в коде программы.
  • 2 B - номер строки, в которой объявлен FOR.
  • 2 B - адрес имени переменной в области переменных (2-х байтовый, так как стек всегда в области 0).
  • 8 B - Шаг цикла. Организация хранения такая же, как у числовых переменных.
  • 8 B - Финальное число цикла. Организация хранения такая же, как у числовых переменных.
    Для возврата после GOSUB и продолжения выполнения программы выделяется по 8 байт на каждый уровень вложенности:
  • 3 B - адрес адреса программы.
  • 2 B - номер строки.
  • 3 B - адрес в коде программы для продолжения выполнения (следующий байт после GOSUB X).

    Теперь мы готовы собирать образ RAM со своими программами для отправки на Casio PD‑700B, но ещё только предстоит узнать все возможные команды и операторы BASIC, а также изучить все тонкости синтаксиса!

Комментарии