Пора проверить работу приёмника UART "в составе QuatCore". Для этого написал такую вот немудрёную программку, HelloUserName.asm:
;проверяем приёмник UART, а также директиву DUP и процедуру ReadLn
%include "QuatCoreConsts.inc"
%include "Win1251.inc"
.code
main proc
%include "SetClock.asm"
SP Stack
.rodata
NamePromptStr db 'Как вас зовут?',13,10,0
HelloStr db 'Добрый день, ',0
UserName db 32 DUP(0)
.code
SIO UART
X NamePromptStr
CALL print
X UserName
CALL ReadLn
X HelloStr
CALL print
X UserName
CALL print
OUT 13
OUT 10
@@endless: JMP @@endless
main endp
%include "Print.asm"
%include "ReadLn.asm"
.data
Stack dw 2 DUP(?) ;1 место под адрес возврата, ещё одно под сохранение Acc в процедуре print
Но прежде чем она сможет заработать, нужно "научить" компилятор понимать директиву DUP (DUPlicate), а также написать процедуру ReadLn...
Честно говоря, синтаксис DUP, принятый в большинстве ассемблеров, мне не очень нравится: компилятору приходится "заглядывать вперёд" по чём зря. То есть, в строке
Username db 32 DUP(0)
прочитав число 32, компилятор не должен тут же упихивать его в очередной байт, он должен прочитать дальше. И увидев DUP, он должен понять, 32 - это не значение, которое помещается в память, а указание, что число 0 (которое в аргументе для DUP) нужно повторить 32 раза!
Как-то криво оно парсится: видимо сначала мы должны прочитать строку до очередной запятой, разделяющей значения. Потом попытаться прочитать число в начале, а за ним - отыскать этот самый DUP. Понятно, ничего особенно навороченного, но мне кажется, можно было и более "прямолинейно", поставить DUP в начало, а потом, к примеру, число повторений и значение передать ему как аргументы.
Но уж ладно, пусть будет... Заодно, подправили наш код, и сообразили, почему он так нехорошо работал и не допускал пробелов между запятыми.
Далее, нужна процедура ReadLn. Вот как она описана в одноимённом файле ReadLn.asm:
;прочитать строку, т.е до символа 13 (CR)
;в регистре X передаётся адрес, куда класть строку
;будет прочитываться очередное значение с ввода-вывода (не обязательно даже 8-битное), и если оно меньше или равно 13 (всевозможные "специальные символы"), мы возвращаемся
;в противном случае записываем символ и ждём следующий
;также мы выйдем по прочтении 32 символов (делать полноценное "динамическое выделение памяти" не хочется, дать возможность пользователю "всё сломать" тоже,
;а 32 нам должно хватить :)
;полагаем, что k=0. По выходу из процедуры k может измениться и будет выражать количество принятых символов
;также может измениться значение Acc, других регистров мы не трогаем
ReadLn proc
Acc IN
SUB 14
JL [--SP] ;возврат из процедуры
[X+k] Acc ;раз дошли до этого места, значит надо положить принятый символ
kLoopUp ReadLn
;а если дошли до этого места, значит уже приняли 32 символа, хватит!
JMP [--SP]
ReadLn endp
Вроде всё объяснено в комментариях: читаем, пока не наткнёмся на "специальный символ" от 0x00 до 0x0D. При этом "таб" тоже окажется недоступен, он в терминале таб не очень-то и введёшь - фокус с поля ввода уйдёт! Ну не хочется мне вводить "флаг нуля", это уже упрямство :) Хотя чтобы проверить на ноль можно двумя дополнительными строками: после вычитания сделать ABS Acc, и SUB 1. Только в случае нуля результат получится отрицательным
Также по-моему ни разу ещё мы не использовали имя процедуры как метку внутри её самой, чтобы прыгать в самое её начало. Это вполне нормальная практика, ведь имя процедуры - это и есть метка по большому счёту. (ну ещё "область видимости" для локальных меток, и те немногие строки без кода, которые всё-таки попадают в листинг)
Компиляция проходит успешно. Сначала посмотрим листинг оперативной памяти (т.е "данных"):
NamePromptStr: 00 0xCA02
NamePromptStr[1]: 01 0xE003
NamePromptStr[2]: 02 0xEA22
NamePromptStr[3]: 03 0x2054
NamePromptStr[4]: 04 0xE200
NamePromptStr[5]: 05 0xE001
NamePromptStr[6]: 06 0xF103
NamePromptStr[7]: 07 0x2022
NamePromptStr[8]: 08 0xE766
NamePromptStr[9]: 09 0xEE00
NamePromptStr[10]: 0A 0xE218
NamePromptStr[11]: 0B 0xF302
NamePromptStr[12]: 0C 0xF222
NamePromptStr[13]: 0D 0x3F6F
NamePromptStr[14]: 0E 0x0D02
NamePromptStr[15]: 0F 0x0AC4
NamePromptStr[16]: 10 0x00EE
UserName: 11 0x00E1
UserName[1]: 12 0x00F0
UserName[2]: 13 0x00FB
UserName[3]: 14 0x00E9
UserName[4]: 15 0x0020
UserName[5]: 16 0x00E4
UserName[6]: 17 0x00E5
UserName[7]: 18 0x00ED
UserName[8]: 19 0x00FC
UserName[9]: 1A 0x002C
UserName[10]: 1B 0x0020
UserName[11]: 1C 0x0000
UserName[12]: 1D 0x00??
UserName[13]: 1E 0x00??
UserName[14]: 1F 0x00??
UserName[15]: 20 0x00??
UserName[16]: 21 0x00??
UserName[17]: 22 0x00??
UserName[18]: 23 0x00??
UserName[19]: 24 0x00??
UserName[20]: 25 0x00??
UserName[21]: 26 0x00??
UserName[22]: 27 0x00??
UserName[23]: 28 0x00??
UserName[24]: 29 0x00??
UserName[25]: 2A 0x00??
UserName[26]: 2B 0x00??
UserName[27]: 2C 0x00??
UserName[28]: 2D 0x00??
UserName[29]: 2E 0x00??
UserName[30]: 2F 0x00??
UserName[31]: 30 0x00??
Stack: 31 ????
Stack[1]: 32 ????
33 ????
34 ????
35 ????
36 ????
37 ????
38 ????
39 ????
3A ????
3B ????
3C ????
3D ????
3E ????
3F ????
Да, DUP срабатывает как надо - действительно выделяется 32 байта под UserName и 2 слова под Stack. "Упаковка" байтовых строк не самая оптимальная, на всё про всё ушло 96 байт, хотя хватило бы 92 байта. Ну да ладно, пока нас это устраивает.
Теперь листинг кода:
main proc
SetClock proc
00 1021 SIO ETH
01 A1F3 j [SP++] ;количество посылок (счёт с нуля)
02 A2F3 @@EthWord: k [SP++] ;количество байт в посылке (счёт с нуля)
03 00F3 @@EthByte: OUT [SP++]
04 AA61 kLOOP @@EthByte
05 8990 NOP IN
06 A921 jLOOP @@EthWord
SetClock endp
07 FD46 SP Stack
08 1001 SIO UART
09 CD01 X NamePromptStr
0A F3B4 CALL print
0B CD45 X UserName
0C F3B8 CALL ReadLn
0D CD78 X HelloStr
0E F3B4 CALL print
0F CD45 X UserName
10 F3B4 CALL print
11 0058 OUT 13
12 0028 OUT 10
13 B065 @@endless: JMP @@endless
main endp
print proc
14 F380 [SP++] Acc
15 FDCD SP X
16 CDFD X SP
17 8861 @@start: ZAcc RoundZero
18 83FC SUB [SP]
19 BC1D JGE @@finish ;увы, теперь из процедуры так просто не выпрыгнешь
1A 00F3 OUT [SP++]
1B B075 JMP @@start
1C FDCD SP X
1D CDFD X SP
1E 80FF Acc [--SP]
1F B0FF JMP [--SP]
print endp
ReadLn proc
20 8090 Acc IN
21 8338 SUB 14
22 B8FF JL [--SP] ;возврат из процедуры
23 C880 [X+k] Acc ;раз дошли до этого места, значит надо положить принятый символ
24 AE03 kLoopUp ReadLn
25 B0FF JMP [--SP]
ReadLn endp
Выглядит неплохо.
Попробуем прошить!
Поскольку эта программа куда компактнее нашего VideoProcessing.asm, то и таблица непосредственных значений здесь совсем "игрушечная", в 1 ЛЭ:
//таблица непосредственных значений, сгенерированная под конкретный исполняемый код
module QuatCoreImmTable (input [7:0] SrcAddr, output [15:0] Q);
wire[6:0] adr = SrcAddr[6:0];
assign Q[0]=adr[6];
assign Q[1]=adr[5];
assign Q[2]=adr[4];
assign Q[3]=adr[3];
assign Q[4]=adr[2];
assign Q[5]=adr[1];
assign Q[6]=adr[0];
assign Q[8]=1'b0;
assign Q[9]=1'b0;
assign Q[10]=1'b0;
assign Q[11]=1'b0;
assign Q[12]=1'b0;
assign Q[13]=1'b0;
assign Q[14]=1'b0;
assign Q[15]=1'b0;
wire [2:0] adr7={adr[6],adr[5],adr[0]};
assign Q[7]=
(adr7==3'b100)? 1'b0:
(adr7==3'b010)? 1'b0:
(adr7==3'b110)? 1'b1:
(adr7==3'b001)? 1'b1:
(adr7==3'b101)? 1'b1:
1'bx;
//Непосредственные значения и их адреса:
// Значение (dec) Значение (hex) Маска Адрес Где используется
// 2 0002 003F 0021 SIO ETH/jLOOP SetClock::@EthWord
// 3 0003 003F 0061 kLOOP SetClock::@EthByte/ZACC RoundZero
// 49 0031 00FF 0046 SP Stack
// 192 00C0 00FF 0001 SIO UART/X NamePromptStr
// 209 00D1 00FF 0045 X UserName/X UserName
// 143 008F 00FF 0078 X HelloStr
// 13 000D 03FF 0058 OUT 13
// 10 000A 03FF 0028 OUT 10
// 19 0013 003F 0065 JMP main::@endless
// 28 001C 003F 001D JGE print::@finish
// 23 0017 003F 0075 JMP print::@start
// 14 000E FFFF 0038 SUB 14
// 32 0020 003F 0003 kLoopUp ReadLn
endmodule
Так что должно всё "летать". Но увы:
Опять не укладывается в свои 25 МГц.
Попробуем костыль:
Мы по сути убрали мультиплексор между выходом UART и SPI, зная, что пока что команда IN на SPI используется лишь чтобы дождаться окончания передачи, чтобы между посылками nCS успевал бы "мигнуть" единицей. А вот сигнал busy важен как от UART, так и от SPI, его трогать мы не могли.
Ещё раз синтезируем:
Ээх, 24,88 МГц, не хватает самой капельки. Всего 11 "заваленных путей". В принципе, этого можно было ожидать: наша "конвейерная логика", увы, преимущественно комбинаторная, и по мере добавления к ней новых модулей она всё сильнее и сильнее "проседает".
Нужно упростить логику формирования DestStallReq и SrcStallReq от устройств ввода-вывода!
Сейчас происходит довольно странная вещь: модуль QuatCoreIOselector мучительно декодирует, к какому из устройств ввода-вывода сейчас обратиться, и выдаёт соответствующие сигналы: UARTtxEN, UARTrxEN, SPItxEN, SPIrxEN, LCD_EN.
Затем каждое из устройств формирует свои индивидуальные сигналы busy, обязательным условием выполнения которых становится наличие соответсвующего "сигнала разрешения работы" на входе.
И наконец, все эти сигналы объединяются по "ИЛИ". Выходит как-то вот так:
Только вход INcommand, изображённый здесь, комбинаторно выражается через 7 сигналов (5 битов SrcAddr, а также SrcStall и SrcDiscard). Также относительно сложные выражения для UARTworking (проверяется, что мы в правильном состоянии, при ce=1 и даже значение на входе задействовано) и SPIworking.
Не знаю, хватает ли у Квартуса прозорливости увидеть всю эту часть целиком и сообразить, что сигнал INcommand можно перенести в конец:
Давайте проверим... Выходы busy в приёмниках UART и SPI больше не будут зависеть от "входа разрешения", и поступят на QuatCoreIOselector. Дальнейшая логика будет реализована там.
Как ни странно, помогло:
ПРОШИВАЕМСЯ!
Очень интересно :) Начальное сообщение он успешно выдаёт. Столь же успешно он ждёт, когда же мы что-нибудь напишем, но потом почему-то зацикливается.
На аппаратную ошибку не похоже. Как будто из процедуры ReadLn он возвращается на нулевой адрес, что сродни reset.
И действительно, дурацкая ошибка, как всегда решил выпендриться!:
ReadLn proc
Acc IN
SUB 14
JL [--SP] ;возврат из процедуры
[X+k] Acc ;раз дошли до этого места, значит надо положить принятый символ
kLoopUp ReadLn
;а если дошли до этого места, значит уже приняли 32 символа, хватит!
JMP [--SP]
ReadLn endp
Это кажется красиво возвращаться из процедуры "условным переходом". Только вот незадача - ПРАВАЯ ЧАСТЬ ВЫЧИСЛЯЕТСЯ ЕЩЁ ДО ТОГО, КАК БУДЕТ ПРИНЯТО РЕШЕНИЕ О ПЕРЕХОДЕ! Поэтому декремент выполняется в любом случае, такая уж логика QuatCore! Вот и выходит, что мы начисто убиваем стек и прыгаем вообще непонятно куда. Но благодаря младшим адресам, инициализированным нулями перед стеком (см. листинг памяти), очень даже понятно - в начало работы мы прыгаем.
Ладно, переправим как надо:
ReadLn proc
Acc IN
SUB 14
JL @@ret ;возврат из процедуры
[X+k] Acc ;раз дошли до этого места, значит надо положить принятый символ
kLoopUp ReadLn
;а если дошли до этого места, значит уже приняли 32 символа, хватит!
@@ret: JMP [--SP]
ReadLn endp
Да, попрыгает немножко - ну и ладно!
Хотели бы скорости - можно было в самом начале поставить NOP [--SP], а потом в середине цикла JL [SP], и в конце JMP [SP]. Но ReadLn вообще не обязана быть быстрой, когда на приём одного байта по UART заведомо уходит 270 тактов!
Ладно, попробуем ещё раз:
Интересное кино...
По некоторым размышлениям, нашёл здесь аж две ошибки:
- программная, всё в том же злосчастном ReadLn - мы заносим не принятое значение, из него вычитается 14.
- аппаратная: "байтовый режим" работает только на чтение, а запись пока производится во всё слово целиком! А если посмотреть листинг в очередной раз, обнаруживаем, что как раз с третьей буквы HelloStr ("Добрый день, ") делит одни и те же слова с UserName, вот и выходит, что "До" отправляется нормально, а потом идёт введённая нами строка, но повёрнутая на 14 :)
Для начала, просто заставим эту хрень работать... Переписываем ReadLn:
ReadLn proc
C IN
Acc 13
SUB C
JGE @@ret ;возврат из процедуры
[X+k] C ;раз дошли до этого места, значит надо положить принятый символ
kLoopUp ReadLn
;а если дошли до этого места, значит уже приняли 32 символа, хватит!
@@ret: JMP [--SP]
ReadLn endp
Ну нет у нас CMP (Compare), чтобы два числа сравнить между собой, а значение в аккумуляторе сохранить старое! Приходится извращаться. И операнды местами поменяли (из 13 вычитаем символ), чтобы не допустить Hazard по регистру C, хотя по АЛУ у нас блокировка стоит, автоматически на такт задержит. А так и задерживать не будет. хотя нет, всё равно задержит. Он не понимает, что пока мы делаем вычитание, регистр C не меняется, его можно "старый" взять. И ладно, при умножении C циклически крутится, и тогда его посреди работы действительно брать не надо! Ох, жесть какая. А такой маленький был QuatCore, такой няшный :)
А с байтовым режимом пока что костыль, для хранения UserName вместо db ставим dw, и пробуем ещё разок:
И снова ошибка закралась: пока отлаживал, день закончился, надо было "добрый вечер".
А в целом, жить можно... И мне кажется, программирование вполне себе в досовском стиле, даже покомпактнее. Всяко приятнее, чем в микроконтроллерах, где надо уйму конфигурационных регистров настроить, отправлять байтики, уходить в "бесконечный цикл", ожидая, когда значение в одном из этих регистров переключится, и т.д. Может, это уже стокгольмский синдром...