Нет, не децибелы и не изречение Лаврова, а всего лишь Define Byte. Одна из самых простых директив ассемблера, когда адресация памяти байтовая. Но у нас же система такая хитрая, с адресацией по словам 16 бит, но теперь с возможностью взять только старший байт либо только младший.
Я хочу, чтобы с точки зрения программиста всё работало "как обычно", несмотря на серьёзные различия в реализации. Вот тестовый код:
.data
EthDisable db 2,3,0x22,0x54,0x00
hello db 'Hello, wo'
DistStr db 'Dst'
RollStr db 'Roll'
YawStr db 'Ya'
WordData dw 0x1234,0x5678,0x9ABC,0xDEF0
Я хочу, чтобы компилятор разместил эти строки следующим образом:
То есть, началась первая строка, EthDisable - и компилятор ОБЯЗАН расположить байты последовательно друг за другом. Вся память была свободна, поэтому он начал с адреса 0, и выбрал к примеру СТАРШИЕ БАЙТЫ. Тогда, если по результатам у нас окажется 8-битная адресация и 2 дополнительных бита для выбора "байтового режима", то метка EthDisable в коде превратится в 0x300. Младшие 8 бит указывают адрес, 0, старший бит укажет, что обращаемся мы к отдельному байту, а следующий за ним бит - что к СТАРШЕМУ байту. Прибавление единицы к этому адресу даст следующий байт на этой строке, и так далее. В этом весь смысл!
Но если дойти до последнего байта строки EthDisable, и снова прибавить единицу к адресу - мы вовсе не обязаны получить первый байт следующей строки, hello! В этом разница с "классическим" действием директивы db, где выделение памяти идёт строго монотонно...
Эта дополнительная свобода даёт возможность разместить hello с того же нулевого адреса, но теперь в МЛАДШИХ БАЙТАХ. Соответственно, метка hello в коде программы превратится в адрес 0x200: адрес ноль, байтовый режим, МЛАДШИЙ байт.
Затем пойдёт очередная строка, и я думаю, компилятор в данном случае должен делать "жадный выбор" - искать, где сейчас больше вакантного места, в старших или в младших байтах - и отправлять строку туда. Потому как задача выглядит подозрительно похожей на "упаковку рюкзака", которая вообще-то является NP-полной. (хотя здесь работаем в целых числах, вроде можно динамическим программированием распихать строки ОПТИМАЛЬНО довольно-таки шустро, но давайте как-нибудь в следующий раз, сейчас бы хоть как-нибудь заработало бы).
Осталось заставить компилятор это "принять" и превратить в работающий код...
Пока мы заполняли сегмент данных (в смысле, начальное содержание оперативной памяти, Memory Initialization File, в общем, .data) довольно простым способом: у нас была переменная DataRamIndex, которая инициализировалась нулём, к ней прибавлялась единичка при объявлении каждого нового слова (dw или Int16), и ещё с помощью директивы ORG (Origin, "начальная позиция") можно задать DataRamIndex непосредственно. Метка добавляется в список Labels, с дополнительным указанием что она принадлежит СЕГМЕНТУ ДАННЫХ. (у нас пока возможно 3 варианта: адрес сегмента данных, адрес сегмента кода, либо литерал, когда и не адрес вовсе, а "произвольное число").
Когда мы натыкаемся на db первый раз - в целом происходит то же самое, мы продолжаем инициализировать слово за словом, на каждом байте прибавляя единицу к DataRamIndex. Но теперь, по окончании строки с db, мы должны отметить вакантное место в СТАРШИХ БАЙТАХ, куда можно занести другую байтовую строку!. И также мы должны указать в метке, что это именно БАЙТОВАЯ метка, а также - где именно она лежит, в младших байтах или в старших?
И в следующий раз, встретив db, мы должны поместить эту строку в ближайшее свободное место, будь оно в старших байтах или младших.
Что-то мы перемудрили в своё время с этими ячейками памяти, у каждого слова компилятор запоминает следующие параметры:
- IsSigned: число со знаком или без знака? Влияет, как число будет отображаться в листинге и в эмуляторе, тогда как до файла инициализации эта информация "не доходит" - тут уже сам программист должен понимать, где что у него лежит,
- IsReadOnly: предполагается ли модификация этого слова при выполнении? Приводит к появлению соответствующей иконки на эмуляторе и "матерной ругани", если на эмуляторе всё-таки произойдёт запись по данному адресу. Опять же, никакого MMU (Memory Management Unit), который мог бы запретить процессору записывать по определённым адресам, у нас НЕТ И НЕ ПРЕДВИДИТСЯ, так что дальше эмулятора этот флажок пойти не может,
- IsInitialized: было ли это слово инициализировано. Если нет, в листинге и на эмуляторе будет "вопросик" вместо значения, и эмулятор выругается, если мы попытаемся использовать такое значение. Кроме того, если мы перемудрим с директивой ORG (Origin, задать, с какого адреса дальше инициализировать память) и попытаемся положить несколько разных значений в одну и ту же ячейку - нас обругает уже компилятор. Реально в ПЛИС в неинициализированных ячейках памяти будут сидеть нули! Она как бы инициализирует память "оптом" при конфигурации, и как-то "сэкономить" на этом всё равно нельзя.
- VarName: имя этой ячейки памяти, которое появится в листинге слева и в эмуляторе.
Теоретически, теперь все эти параметры стоило бы распространить на каждый байт, но, честно говоря, не хочется. Пусть IsSigned, IsReadOnly и VarName будет определяться старшим байтом. Продублируем только IsInitialized, чтобы убедиться, что при возне с db мы случайно не затираем одни строки другими.
При организации связанных списков нам очень пригодилось провести "начальное связывание" списка Heap прямо на этапе компиляции, для чего использовался синтаксис
Elem0 dw Elem1
...
...
Elem1 dw Elem2
...
Теперь возникает ещё множество интересных ситуаций: что, если Elem1 - это байт или массив байтов? Или Elem0?
На данный момент я предусмотрел создание указателя на байт: компилятор добавит необходимые старшие биты, чтобы QuatCore по такому указателю действительно загрузил бы только один байт.
Но для хранения указателя подразумевается использование всех 16 бит. Уместить в 8 бит такой указатель можно только при использовании оперативной памяти всего в 64 слова (6 бит идёт на адрес слова, и 2 бита на выбор режима доступа), чего в сколько-нибудь серьёзной программе у нас не встретится.
Кроме того, возникает вопрос: как реагировать на директиву ORG между несколькими объявлениями байтовых строк?
Тоже не будем делать проблемы: мы попросту "забываем" о неиспользуемых байтах - и начинаем с новой позиции. По-моему, полезность директивы ORG для меня уже исчерпана... Её пришлось ввести, когда мы могли напрямую обращаться только к адресам от 0 до 63 и от "-64" до "-1", в дополнительном коде. К примеру, если адресация 8-битная (0..255), то напрямую можно было обращаться к значениям 0..63 и 192..255. И нужно было очень аккуратно "размечать" память. Сейчас, с появлением таблицы непосредственных значений, об этом всём можно забыть как о страшном сне.
Кажется, что-то у нас получилось.
По указанному выше "тестовому коду" образуется такой листинг:
EthDisable: 0 0x0248
EthDisable[1]: 1 0x0365
EthDisable[2]: 2 0x226C
EthDisable[3]: 3 0x546C
EthDisable[4]: 4 0x006F
DistStr: 5 0x442C
DistStr[1]: 6 0x7320
DistStr[2]: 7 0x7477
RollStr: 8 0x526F
RollStr[1]: 9 0x6F59
RollStr[2]: A 0x6C61
RollStr[3]: B 0x00??
WordData: C 0x1234
WordData[1]: D 0x5678
WordData[2]: E 0x9ABC
WordData[3]: F 0xDEF0
Да, "визуализация" страдает: можно было бы попросить компилятор отображать набор из двух байт как символы, или даже ещё сделать разделение - "db" это будет байт, а "char" - уже символ, и разница только в отображении.
И как и говорилось: название слову памяти даёт старший байт, поэтому hello и YawStr у нас "провалились". Чтобы убедиться, что расположено всё верно, лучше открыть через Hex-editor файл DataSegment.hex, в нём лежат "сырые данные", без всего:
Тут вполне разглядывается строка hello ("Hello, wo"), которая перемежается со строками EthDisable (абракадабра), DistStr ("Dst") и RollStr ("Roll"), потом за неё угадывается строчка "Ya", и наконец "во весь рост" идут 16-битные данные 0x1234, 0x5678 и 0x9ABCD.
Всё хорошо!
Мы написали и код:
.code
main proc
SetClock proc
;конфигурирует Ethernet-контроллер на частоту 25 МГц и отключает собственно Ethernet
;SP=0 при включении питания, на ПЛИС с этим строго
SP EthDisable ;байтовая адресация нужна (хотя если EthDisable занимает младшие байты, можно и без неё обойтись)
SIO ETH
j [SP++] ;количество посылок (счёт с нуля)
@@EthWord: k [SP++] ;количество байт в посылке (счёт с нуля)
@@EthByte: OUT [SP++]
kLOOP @@EthByte
;нужно, чтобы nCS сбросилось в единицу
NOP IN
jLOOP @@EthWord
SetClock endp
SIO UART
SP hello
C CALL(print)
@@endless: JMP @@endless
main endp
;посылает строку куда-нибудь. Пока модуль ввода-вывода всего один, SIO не нужен (нужный модуль по умолчанию будет активен)
;X указывает на начало строки текста
;конец обозначается отрицательным числом или НУЛЁМ (необходимо для работы с байтами)
;меняет значение регистра Acc и SP
;адрес возврата надо заносить в регистр C.
print proc
@@start: ZAcc RoundZero
SUB [SP]
JGE C
OUT [SP++]
JMP @@start
print endp
Но увы, как явствует из логов компилятора, где он перечисляет все "непосредственные значения":
0 000F SP EthDisable/SIO UART/SP hello
2 000F SIO ETH
4 001F kLOOP SetClock::@EthByte
3 001F jLOOP SetClock::@EthWord/ZACC RoundZero
11 001F JMP main::@endless
12 001F JMP print::@start
Как видно, обе метки EthDisable и hello "превращаются" в адрес 0, что даёт обычный режим, "по словам".
Сейчас ещё подрихтуем это место - и должно наконец-то заработать, хочу сегодня-завтра вернуться к "реальному железу"...
UPD. Вот он, smart pointer! Указатель, который на самом низком уровне "знает", на какой тип данных он указывает :) И ТРУЪ generic programming, когда безо всяких мерзопакостных Template можно заставить одну и ту же процедуру обрабатывать разные типы данных, при том это будет реально одна процедура, а не куча однотипных копий, которых наплодил компилятор. Да ещё и с "динамической типизацией" :)
Мне почему-то кажется, что если эту идею немножко развить, обработка изображений всевозможных форматов (ч/б, 2 бита, 4 бита, 8 бит, 16 бит "градаций серого", 24 бита RGB, 32 бита RGBA) в том числе превращение одних в другие стало бы гораздо более простым делом! (Я в своё время пытался сделать свой собственный ScanKromsator, с цветовыми профилями и многопоточностью, но потом стало РЕЗКО не до того)
UPD2.
Всё, непосредственные значения тоже "отрихтовал":
Таблица непоср. значений
48 003F SP EthDisable/SIO UART
2 000F SIO ETH
4 001F kLOOP SetClock::@EthByte
3 001F jLOOP SetClock::@EthWord/ZACC RoundZero
32 003F SP hello
11 001F JMP main::@endless
12 001F JMP print::@start
Хотя и EthDisable, и hello лежат по нулевому адресу, первый "транслируется" в 48 = 0x30, а второй - в 32 = 0x20. Всё верно: компилятор насчитал всего 16 слов памяти, поэтому младшие 4 бита - это непосредственно адрес, а следующие за ними - как раз выбор, грузить всё слово (00), или младший байт (10) или старший (11).
Пора "в железе" запускать!