nabbla (nabbla1) wrote,
nabbla
nabbla1

Category:

"МКО (Mil-Std1553) через UART", часть 2

Автомат Калашникова - устройство, превращающее стек в очередь (автора не знаю)

Мы торжественно написали заголовок, пора наполнять модуль содержанием!

В первую очередь, разберёмся с состояниями нашего конечного автомата.

Понятное дело, sIdle - "ожидание". В этом состоянии мы сидим, пока не получим командное слово, адресованное НАМ, не важно, корректно ли сочетание "приём/передача - подадрес - количество слов". Как только это случилось, переходим в состояние sReceive - получение слов данных. При этом информации из командного слова достаточно, чтобы знать, сколько слов ДАННЫХ должно прийти (могут вклиниться командные и ответные слова, если это "формат 3", но их мы отличим). Мы должны оставаться в состоянии sReceive, пока счётчик оставшихся слов не упадёт до нуля. Может оказаться, что изначально принять надо НОЛЬ слов (либо данные должны отправить МЫ, либо это одна из команд управления, не содержащая слова данных) - не страшно, мы можем с чистой совестью перейти в sReceive аж на ОДИН ТАКТ (40 наносекунд) и тут же пойти дальше.


Из sReceive мы переходим либо назад в sIdle, если это было групповое сообщение (на него отвечать нельзя, а то все ответят и друг друга заглушат), либо в состояние sReply ("передача ответного слова"). Полагаю, что положенную по ГОСТу паузу от 2 до 10 мкс (там написано от 4 до 12, но это если считать от последнего перепада уровня прошлого сообщения до первого перепада уровня следующего) должен будет сделать сам передатчик. Мы же передаём ему "провод" TXisData, вот пущай на словах данных он никакой паузы не делает, а на ответных словах делает.

Из sReply мы переходим в sTransmit ("передача слов данных"), предварительно определив, сколько слов мы хотим передать и с какого адреса. Это может быть от нуля до 32. Придётся вводить 6-битный счётчик, так как 5-битного самую малость не хватает. И снова, можно не бояться перейти в sTransmit на один такт (40 нс), даже если никаких слов данных передавать не надо - оттуда мы вернёмся прямиком в sIdle.

Итак, получается sIdle, sReceive, sReply и sTransmit - четыре состояния, выразим 2 битами, хотя квартус, если углядит в этой конструкции конечный автомат, может решить представить его как One-Hot, т.е 4 регистра, из которых "активен" только один - это иногда упрощает комбинаторную логику автомата, не нужно городить дешифраторы. Но это оставим на его совести, а сами запишем:

localparam sIdle 	= 2'b00;
localparam sReceive 	= 2'b01;
localparam sReply 	= 2'b10;
localparam sTransmit	= 2'b11;
	
reg [1:0] State = sIdle;


Вообще, автомат с такими переходами между состояниями можно было и в виде счётчика выразить (основное движение - прибавление единички, но возможен преждевременный сброс в ноль, т.е sIdle, если было принято групповое сообщение), но не уверен, что 2-битный счётчик получится так уж лучше, чем "логика общего назначения". Может, поэкспериментирую потом, если будет охота.

Далее, всё-таки нужен "флаг" doWeNeedToTransmit, в который "защёлкивается" признак "приём/передача" во время приёма командного слова. Он очень понадобится в дальнейшем.

Можем сразу записать логику его работы:
wire isIdle = (State == sIdle);
	
reg DoWeNeedToTransmit = 1'b0;
always @(posedge clk) if (isIdle)
	DoWeNeedToTransmit <= curWordDoTransmit; 


Тут есть выбор: то ли мы "безусловно" защёлкиваем этот признак, как указано здесь, то ли ещё проверяем корректность подадреса - поддерживаем ли мы его?

Одно из применений этого флага - избежать необходимости в двух отдельных счётчиках слов, одного для приёма, другого для передачи. Так мы можем всегда защёлкнуть указанное количество слов в момент прихода командного слова, а по флагу DoWeNeedToTransmit понять, к какому моменту это значение относится. Если мы выставим DoWeNeedToTransmit = 0 там, где нас просили передать данные, то нужно будет ещё и число слов задать нулевым, т.е дополнительно усложнить логику счётчика, и так сложную из-за "команд управления". Так что наверное не будем этого делать, а в момент передачи проверим ещё и "флаг" MessageError, если там единичка - от передачи слов данных мы откажемся.

Поясним очень "слабые" условия защёлкивания этого флага: мы лишь требуем состояния isIdle, И ВСЁ.

Допустим, сообщение вообще не пришло ещё - ну и пущай он защёлкивается, в состоянии sIdle он ни на что не влияет (мы ничего не делаем). Пришло сообщение, да не нам - и снова пофиг, мы всё равно останемся в sIdle. В общем, последний раз он защёлкнется ровно на том моменте, когда придёт КОМАНДНОЕ слово, адресованное НАМ (либо широковещательное), в ответ на что мы перейдём в другие состояния, и далее флаг будет держаться до окончания текущего сообщения. К чему лишнюю логику городить :)

Давайте всё-таки сформируем комбинаторный сигнал, по которому мы будем переходить из sIdle в sReceive. Как-то так:

wire isBroadcast = (curWordAddr == 5'b11111);	
reg rBroadcast = 1'b0;
always @(posedge clk) if (isIdle)
	rBroadcast <= isBroadcast;
	
wire isOurAddr = (OurAddr == curWordAddr)&OurAddrOK | isBroadcast;

wire BeginMessage = DataValid & (~RXisData) & isOurAddr;


Тут мы проверяем, является ли сообщение групповым (широковещательным) и запоминаем этот факт. Запоминаем также "без фанатичной проверки" - знаем, что выйдя из sIdle мы получим там ровно то, что надо.

Сигнал isOurAddr, определяющий, адресовано ли сообщение нам (включая вариант "вообще всем") сделан чуть более мягко, чем изначально задумывалось. Мы формировали OurAddrOK говорящий, что на адресной заглушке сходится контрольная сумма и адрес в ней корректный, от 0 до 30, и заявляли: если это не так, можно вообще ничего не делать. Но в нынешней реализации, не имея собственного адреса, мы тем не менее будем принимать групповые сообщения! Отвечать на них мы, конечно, не сможем, но какие-то действия вполне способны предпринять, в конце концов в макете есть экранчик, можно на него что-нибудь вывести. Не думаю, что воспользуюсь такой возможностью, но логичным кажется сделать именно так :)

Ну и провод BeginMessage, по которому мы и будем стартовать. Как видно, должно прийти некоторое слово, оно должно быть командным/ответным (не словом данных) и оно должно иметь либо наш адрес, либо групповой адрес.

Поле адреса в командном слове мы задействовали "по-максимуму": если уж началась дальнейшая работа, значит там могло быть лишь два значения - "наш" адрес либо групповой адрес, и регистра rBroadcast вполне хватает, чтобы одно отличить от другого, больше ничего хранить не нужно.

Поле "приём-передача" мы сохранили "как есть", оно тоже не пропадёт.

Остаётся только разобраться с полями "подадрес" и "число СД". Всё было бы относительно просто, если бы не "команды управления". Это "служебные" команды самой шины, для которых зарезервированы подадреса 00000 и 11111. По этим подадресам поле "число СД" означает номер команды. Старший бит можно воспринимать как признак наличия слова данных, ровно ОДНОГО. Т.е команды от 00000 до 01111 идут без слова данных (ни на приём, ни на передачу), а от 10000 до 11111 - с ОДНИМ словом данных.

Так что здесь комбинаторных цепей тоже прилично:
wire isServiceCommand = (curWordSubAddr == 5'b1_1111) | (curWordSubAddr == 5'b0_0000);

wire [5:0] WordCount;

assign WordCount[4:0] = isServiceCommand? {4'b0, curWordWordsCount[4]} : curWordWordsCount;
//если команда управления, то либо 0 слов (для команд от 00000 до 01111), либо 1 слово (для команд от 10000 до 11111)
//в противном случае, берём число слов "как есть"
assign WordCount[5] = isServiceCommand? 1'b0 : (curWordWordsCount == 5'b00000);
//число слов "0" мы должны интерпретировать как 32


Тут ещё не забываем один "кульбит" в этом протоколе, что количество слов "ноль" надо интепретировать как 32. Именно поэтому пришлось добавить лишний бит - и соответствующим образом сформировать.

Пора добавить счётчик слов: именно в него будет заносится их число по приходу командного слова, а затем, по мере приёма или передачи - уменьшаться вплоть до нуля. lpm_counter, на выход:

wire noWordsLeft;
wire MinusOneWord = DoWeNeedToTransmit? (start & (~TxBusy)) :
					DataValid & RXisData;
lpm_counter WordCounter (
			.clock (clk),
			.cnt_en (MinusOneWord),
			.data (WordCount),
			.sload (isIdle),
			.cout (noWordsLeft) );
defparam
	WordCounter.lpm_direction = "DOWN",
	WordCounter.lpm_port_updown = "PORT_UNUSED",
	WordCounter.lpm_type = "LPM_COUNTER",
	WordCounter.lpm_width = 6;


Как ни странно, мы уже описали работу этого счётчика на ВСЕХ этапах. Опять он защёлкивает всё подряд в состоянии sIdle, но последнее защёлкнутое значение и будет истинным. Само по себе значение на выход нам не нужно, только индикация "слова закончились". Вычитает единичку он либо во время прихода слов данных, если признак "приём/передача" нулевой, либо когда мы посылаем очередное слово на передатчик, и он его "получает" (он может это сделать только если не занят передачей предыдущего слова). Возможно, здесь получилось бы сделать "финт ушами" и всегда вычитать единицу именно по приходу очередного слова данных на приёмник, считая, что он примет и "наши собственные" слова, только что отправленные на шину. Это же полудуплексный канал, на котором нет отдельной линии на приём и на передачу, но разница по мощности не так велика, как в радиосистемах, где приёмник надо бы напрочь изолировать, когда работает передатчик :) Но чего-то страшно, пусть уж "лоб" пока. С коммутацией приёмников и передатчиков ещё придётся попотеть, когда введём резервированную шину.

Самое время описать переключение состояний нашего автомата!

wire isReceive 	= (State == sReceive);
wire isReply 	= (State == sReply);
wire isTransmit = (State == sTransmit);

always @(posedge clk)
	State <= 	isIdle? 	(BeginMessage? sReceive : sIdle):
			isReceive?	((DoWeNeedToTransmit | noWordsLeft)? (rBroadcast? sIdle : sReply) : sReceive):
			isReply?	sTransmit:
					(~DoWeNeedToTransmit | noWordsLeft | MessageError)? sIdle : sTransmit;


Уж напишем в классическом стиле "конечного автомата". Можно было бы его "разложить" на вход разрешения и сброса, потом попробую, сравним реализацию Квартуса и мою :)

А пока код громоздкий, но предельно понятный. В состоянии sIdle сидим до тех пор, пока не получим командное слово по "нашему адресу", тогда переходим в sReceive.

В sReceive мы сразу же покидаем, если нам задали передачу данных (DoWeNeedToTransmit=1), либо если уже получили все слова данных, до тех пор сидим, ждём. Только в одном случае не нужно будет послать ответное слово - это при групповом сообщении. Передачи данных, очевидно, тоже не будет, так что с чистой совестью вернёмся к ничегонеделанию в sIdle. В противном случае идём в sReply.

sReply мы безусловно покидаем СРАЗУ ЖЕ, поскольку до этого передатчик очевидно не работал (работал приёмник), сейчас мы его озадачили, а дальше он о таймингах позаботится, в смысле что не примет нового слова на передачу, пока не передаст это.

И наконец, из sTransmit мы либо уходим сразу же, если DoWeNeedToTransmit=0, т.е данные шли К НАМ, а не от нас, либо если все слова для передачи уже закончились, либо если мы сочли команду некорректной (не можем мы обработать такой подадрес с таким режимом "приём/передача").

До сих пор модуль синтезировался в 0 ЛЭ, т.к у нас менялись внутренние состояния, но мы не подключили НИ ОДИН ИЗ ВЫХОДОВ! Надо это потихоньку исправлять.

Начнём с управляющих сигналов, а там доберёмся и до адреса оперативной памяти, и до Data Path.

Самое первое, управление передатчиком. Безусловно посылаем сигнал start и TXisData=0 в состоянии sReply. В состоянии sTransmit тоже имеем полное право непрерывно слать start=1, просто передатчик не будет реагировать на него, пока "занят". И счётчик переданных слов будет вычитать единички лишь те редкие моменты, когда busy=0.

И сразу оказывается, что мы чуть ошиблись с управлением счётчиком, он единичку вычтет и во время передачи ответного слова, хотя оно считаться не должно! Решение простое, нужно заменить start на isTransmit:

wire MinusOneWord = DoWeNeedToTransmit? (isTransmit & (~TxBusy)) :
					DataValid & RXisData;


Да, это уже лучше. Первый раз выйдя на состояние sTransmit, у нас будет busy=1 - только-только началась передача ответного слова (точнее, пауза в 2 мкс перед его передачей), так что счётчик довольно долго (22 мкс) будет оставаться на исходном значении, и только тогда "мигнёт" busy=0, и единичка вычтется - начнётся передача уже слова данных. Именно эта безусловная занятость передатчика при "заходе" в sTransmit гарантирует что "быстро поднятое упавшим не считается" - если слов указано 0, то мы ровно на один такт отправим start=1, но знаем заведомо - передатчик это проигнорирует, так что лишнего слова мы не отправим.

А управление передатчиком получается такое:
assign start 	= State[1];
assign TXisData = State[0];


Ну шикарно же! Просим передатчик работать "непрерывно" в состоянии sReply = 10 и sTransmit = 11, при этом младший бит состояния покажет, передаём ли мы данные или ответное слово. А что туда передаётся в состояниях sIdle и sReceive - вообще пофиг, в эти моменты передатчик отключён!

Вот сейчас синтез даёт 38 ЛЭ при всего 11 регистрах. То ли ещё будет...

Львиную долю времени на передатчик будут идти данные из оперативной памяти, но ровно в одном слове данных на конкретном подадресе нужно будет подсоединить часы реального времени. Это подадрес 0 0110 ("целевая информация") и слово под номером 2 (нумерация с нуля).

Впрочем, можно сделать по-другому: заместить совершенно конкретный адрес оперативной памяти! Так что самое время поговорить и о ней.

Чтобы не городить очередные таблицы, мы просто разобъём всю память на 16 участков по 32 слова в каждом, это 512 слов. В реальности из 16 будет в обмене участвовать только 10, и далеко не в каждом будет строго 32 слова, но я так думаю, мы сможем "набить" в эти прорехи всевозможные временные переменные, которые в информационном обмене не участвуют - и задействуем память практически по-максимуму.

Напомню: это специфическое исполнение для моего приборчика, в котором старший бит подадреса используется в качестве чётности, поскольку пока хватило лишь 10 подадресов из 30.

В итоге, нам нужно 9 бит адреса для памяти. Ширину адресной шины мы сделали параметризуемой, чтобы проще подключаться к памяти с бОльшим адресным пространством (вдруг в 1 килобайт памяти не влезем, хотя я очень надеюсь, влезем :))

Из этих 9 бит, старшие 4 - это просто регистр, в который защёлкивается 4 младших бита подадреса из командного слова. Тут снова можем не заморачиваться, защёлкивать непрерывно в состоянии sIdle, а потом держать:

reg [3:0] HiMem = 4'b0000;

always @(posedge clk) if (isIdle)
	HiMem <= curWordSubAddr[3:0];
	
assign Addr[8:5] = HiMem;


Квартус просто счастлив от таких защёлок, они роутятся вообще без проблем :)

А младшие 5 бит адреса оперативной памяти - это должен быть счётчик, который сбрасывается в ноль в sIdle, а потом прибавляет единичку каждый раз, как единичку отнимает счётчик слов:

wire [4:0] LoMem;
lpm_counter LoMemCounter (
			.clock (clk),
			.cnt_en (MinusOneWord),
			.sclr (isIdle),
			.Q (LoMem );
defparam
	LoMemCounter.lpm_direction = "UP",
	LoMemCounter.lpm_port_updown = "PORT_UNUSED",
	LoMemCounter.lpm_type = "LPM_COUNTER",
	LoMemCounter.lpm_width = 5;

assign Addr[4:0] = LoMem;	


Как ни странно, два счётчика, считающие синхронно - это куда проще и эффективнее, чем один счётчик плюс регистр, где хранится "до скольки считать" и компаратор, ну по крайней мере на этой ПЛИС.

Вот теперь можно ещё написать условие, где вместо обращения к памяти мы должны вытащить значение из "часов реального времени". Это будет адрес 9'b0110_00010. Но вот как интересно, на передачу у нас идут "сегменты" (старшие 4 бита) 0110, 1000, 1001, 11010 и 1100, а все "сегменты", идущие на чтение, начинаются с нуля: 0001, 0010, 0011, 0100, 0101. Что в это время коммутируется на передатчик - нам пофиг, он всё равно не включится, поэтому вместо проверки 9 бит можно проверять только 6:

wire ConnectRTC = (~HiMem[3]) & (LoMem == 5'b00010);


И теперь можем "изобразить" мультиплексор на вход передатчика:

reg [15:0] TxMux = 1'b0;

always @(posedge clk) if (MemReady)
	TxMux <= ConnectRTC? TimeStamp : MemIn;
	
assign Q = TxMux;


Как видно, он сделан "с защёлкой", поскольку никто не обещает, что на входе MemIn значение будет держаться хотя бы такт после поступления MemReady - процессор снова может "вернуть память себе" и заказать любой другой адрес. Но если мы обратились к одному специфическому адресу, где должна лежать метка времени - мы её взамен берём из "часов". А в памяти эту ячейку можно применить ещё для чего-нибудь, процессору-то эта "замена" неведома!

Как ни странно, мы "отоварили" почти все ножки, как на вход, так и на выход. Пока оно синтезируется в 65 ЛЭ и может работать на максимальной частоте в 89,29 МГц на 5576ХС4Т - неплохо.

Осталось разобраться с выходами MemWrReq, MemRdReq и sync (синхронизация "часов реального времени").

Запрос на запись в память должен идти в состоянии sReceive, по мере получения очередного слова данных. Впрочем, поскольку из состояния sTransmit никакие сообщения приходить не должны, то можно проверить лишь младший бит State на "единичку":
assign MemWrReq = DataValid & RXisData & State[0];


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

assign MemRdReq = start & (~busy);


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


На сегодня хватит... Ещё не всё реализовали, остаётся формирование "ошибки сообщения", и вообще ответного слова (про его содержание самую малость забыли!), корректную обработку команд управления, а также автоматическая проверка заголовков сообщений и CRC. Но не всё сразу, думаю сначала эту штуковину отладить, а потом уже потихоньку нарастить.
Tags: ПЛИС, работа, странные девайсы
Subscribe

Recent Posts from This Journal

  • Так ли страшно 0,99969 вместо 1?

    В размышлениях о DMA, о возможных последствиях "смешивания" старых и новых значений при выдаче целевой информации, опять выполз вопрос: насколько…

  • Как продлить агонию велотрансмиссии на 1500+ км

    Последний раз о велосипедных делах отчитывался в середине мая, когда прошёл год "велопробегом по коронавирусу". Уже тогда я "жаловался", что…

  • DMA для QuatCore

    Вот фрагмент схемы нашего "процессорного ядра" QuatCore: Справа сверху "притаилась" оперативная память. На той ПЛИС, что у меня есть сейчас…

  • "МКО через UART" в железе - 2

    Продолжим проверять этот модулёк. "Для закрепления пройденного", снова запрашиваем телеметрию, сначала 1 слово данных (командное слово 0x3701, и…

  • "МКО через UART" в железе

    Долго мучали эту штуковину на симуляции, пора уже в макет это прошить и посмотреть, как оно себя поведёт. Оставлю "основную схему" (процессор, SPI,…

  • "Ошибка на единицу" при передаче CRC

    В прошлый раз обнаружили нехорошую вещь: при передаче данных от нашего устройства, CRC начинает работать одним словом раньше, чем надо, и результат…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 0 comments