У меня был цикл постов "МКО для бедных", где описывался как verilog-код для минималистичного приёмопередатчика и протокольного контроллера МКО (он же MilStd1553, он же ГОСТ Р 52070-2003), так и простейшее "железо", в лице ОДНОГО ТРАНЗИСТОРА для приёмника. В итоге у меня получался довольно оригинальный модуль приёмника, которому не нужно два входа, RXP/RXN (первый должен откликаться на положительные полуволны сигнала, второй на отрицательные, а во время паузы между сообщениями "молчать" одновременно, давая 3 разных состояния), а хватит одного лишь RXP. Да, при этом половину синхроимпульсов мы вообще не можем отличить от идущей перед ними паузы, но этого и не нужно, мы просто воспринимаем за синхроимпульс любой логический уровень, не меняющийся в течение 1,5 микросекунды!
В ЖЖ я описывал работу всего этого на отладочной плате с ПЛИС (CPLD) MAX7128, с генератором тактовой частоты на 4 МГц. Увы, такой частоты самую чуточку не хватало для нормальной работы, начинали сказываться длительности фронтов-спадов и изначальная асимметрия такого "однотранзисторного" приёмника. А вот в моём весёлом "изделии", на частоте 25 МГц всё заработало замечательно, ещё ни одного сообщения не упустил, ни одной разошедшейся контрольной суммы, хотя честную проверку BER (Bit Error Rate) ещё не проводил.
Но пора уже остепениться и сделать резервированный МКО, с двумя линиями передачи информации. И здесь надо "громко подумать": сколько вообще приёмников нам надо, один или два?
Сам по себе принцип резервирования МКО описан в ГОСТе (а до этого в Military Standart, то бишь MilStd) буквально в 2 пунктах, приведу целиком, там немного.
8.3 Интерфейс с дублированием информационной магистрали
В интерфейсе с дублированием магистрали одна из магистралей должна быть в ненагруженном состоянии (в резерве).
8.3.1 Функционирование интерфейса с дублированием информационной магистрали
В каждый момент времени должна функционировать только одна магистраль, кроме случая, указанного в 8.3.2.
8.3.2 Переход в исходное состояние в интерфейсе с дублированием информационной магистрали
Если во время функционирования устройства интерфейса в соответствии с принятым КС в него поступает другое достоверное КС по любой из магистралей, устройство должно осуществить переход в исходное состояние и приступить к работе в соответствии с последней командой. Устройство должно передать ОС, как указано в 4.5.3, на последнее достоверное КС, используя магистраль, по которой поступило новое КС.
Очень долго меня это загоняло в полнейший ступор. Я обратился к английской версии "с комментариями", чтобы увидеть следующее:
4.6.3 Dual standby redundant data bus. If a dual redundant data bus is used, then it should be a dual standby redundant data bus as specified in the following paragraphs.
4.6.3.1 Data bus activity. Only one data bus can be active at any given time except as specified in 4.6.3.2.
4.6.3.2 Reset data bus transmitter. If while operating on command, a terminal receives another valid command from either data bus, it shall reset and respond to the new command on the data bus on which the new command is received. The terminal shall respond to the new command as specified in 4.3.3.8.
Хрен редьки не слаще... Может, они сейчас прокомментируют это, и я сразу всё пойму?
Those new paragraphs in 1553B reflect the common practice in current aircraft of using dual bus as one active, one standby, and it was the intent to restrict the operation of a dual data bus connected to terminal to a "one-at-a-time" operation. However, provision had to be made for a bus controller to override one bus to respond on the redundant bus. The requirement for this is in paragraph 4.6.3.2 above, and the reference to paragraph 4.3.3.8 is the response time requirement of a remote terminal to a valid command word.
Нет, "в целом" я представляю функционирование резервированной шины, это я уже воочию наблюдал с помощью плат сопряжения Элкус. Если работаешь как контроллер шины, то нужно сразу выбрать, по какой из двух линий работать, А или Б. Именно по ней будет отправляться командное слово (и, возможно, слова данных), и ПО НЕЙ ЖЕ ожидаться ответ оконечного устройства. Если оконечное устройство решит ответить по резервной шине, "его не поймут", не услышат. И алгоритм работы, в целом, подразумевается такой:
- контроллер шины работает по основной линии со всеми устройствами.
- не получив ответа / получая сплошные ошибки от одного из оконечных устройств, попытаться в общении с ним перейти на резервную линию,
- если и по резервной линии ничего не получается, устройство считается "умершим" и общение с ним прекращается / отрубается ему питание.
В целом, вроде логично. Но меня в данном случае больше интересует реализация оконечного устройства. Оно ОБЯЗАНО откликнуться на запрос, пришедший по любой из линий передачи информации, и запомнить, откуда пришёл запрос, чтобы ПО ТОЙ ЖЕ ЛИНИИ отправить ответ.
Самым экономичным решением в такой ситуации кажется сделать что-то вроде мультиплексора на вход и на выход единственного "приёмопередатчика". Я в данном случае имею в виду модуль в ПЛИС, который преобразует 16-битное слово в Манчестерский код для последовательной передачи (не забыв синхроимпульс правильной полярности и бит чётности) и наоборот, принимает манчестерский код и выдаёт 16-битное слово. В конце концов, передатчик нам нужен РОВНО ОДИН. Нигде ничего не сказано насчёт одновременной передачи по двум линиям, точнее, сказано что "это запрещено". А вот приёмников, такое ощущение, здесь нужно побольше...
Я изучал описание авиационного микроконтроллера 1986ВЕ1, а также протокольного контроллера 1895ВА2Т, прямо проникся поэзией их описания, см. скриншот "для привлечения внимания" выше. Так вот, там повсюду рисовалось ДВА приёмника и ДВА передатчика, наверное, на это всё-таки была своя причина?
Собственно, для того я и полез в ГОСТы, чтобы понять насчёт этих "граничных случаев". В пункте 8.3.2 пишут: вот наше устройство приняло командное слово (КС) и начало на него реагировать, как вдруг по резервной линии поступило другое командное слово. В таком случае надо БРОСИТЬ ВСЁ - и начать отвечать именно на него!
Наконец, я догадался заглянуть ещё в один документ, ГОСТ Р 52075-2003, по которому надлежит тестировать оконечные устройства МКО. В конце концов "если не понимаешь, чего от тебя хотят, посмотри, как тебя собираются проверять, и хотя бы выучи все правильные ответы!"
И там я нашёл уже вполне конкретный сценарий. Нам прислали командное слово по одной линии, и дождались, когда мы начнём отвечать по той же линии - сначала ответное слово, затем слова данных. Тем временем, нам присылают командное слово по резервной линии - и мы ДОЛЖНЫ ЕГО ПОЛУЧИТЬ, прервать передачу по основной линии - и начать отвечать по резервной!
В итоге до меня, кажется, дошло, в чём здесь смысл. Они представили, что у контроллера шины сгорел приёмник по основной линии. Передавать сообщения он может, а принимать - уже нет, с его точки зрения на шине полный молчок. Допустим, он посылает запрос (командное слово) по основной линии передачи данных. Мы его получили и, как положено, послали ответное слово, а затем слова данных, не догадываясь, что "нас с той стороны не слышат". А контроллер шины тем временем подумал: "прошло 4-12 мкс, а мне не ответили. Что ж, попробую их по резервной линии передачи опросить". Вот поэтому мы и должны, вовсю отправляя слова данных по основной линии, "услышать запрос" по резервной - и тут же начать отвечать на него! Если же мы "оглохнем" от собственной передачи и очухаемся только к паузе, нас уже могут посчитать мёртвыми.
Впрочем, такой сценарий не вполне понятен: хороший контроллер шины должен был бы сам понять, что у него приёмник отказал! Ведь при передаче своих же сообщений, он уже должен их в точности повторить. Если же у него в это время сплошная пауза - это очень подозрительно. Но есть много схожих сценариев: возникло какое-то переотражение на линии, из-за чего задержанный сигнал накладывается на самого себя, резко повышая количество ошибок. Командное слово ещё худо-бедно передаётся, но вероятность корректно передать друг за другом 32 слова данных - уже существенно ниже. Например с вероятностью 90% получим одно слово, тогда с вероятностью 3,4% (!) - 32 слова подряд без ошибок. Но наше оконечное устройство об этом не догадывается, "на его стороне" слова уходят без проблем (и приёмник "чувствует" ровно то, что было передано, наложение происходит уже где-то на стороне контроллера шины), а вот контроллер шины словил несовпадение чётности и уже "рвётся" перейти на другую линию!
Так что, пожалуй, в этом есть сермяжная правда.
А теперь стряхнём пыль с нашего приёмопередатчика. Вот он в составе чуть более крупного модуля:
Название MilStdPHYforCRC означает: это приёмопередатчик Mil-Std1553 (словечко PHY это скорее из всякого Ethernet, что означает физический уровень, PHYsical), заточенный под специфику моего конкретного "изделия", где заказчик не удовлетворился встроенными в сам МКО битами чётности и попросил дополнительно вычислять CRC и передавать его последним словом данных в каждом сообщении. И наоборот: проверять последние слово получаемых сообщений, и "ругаться" в случае, когда CRC не совпадёт.
Сначала разберёмся с интерфейсом. clk - это тактовая частота ровно 25 МГц, так уж повелось. Не очень удобно: хотелось бы иметь частоту, кратную 2 МГц, но и так всё получилось. Если надо, на другую тактовую частоту настраивается без особых проблем.
Далее, RxP - это собственно вход с приёмника. Традиционно входа должно быть два, RxP и RxN (Receiver Positive / Negative), но я обошёлся одним.
Следующие 4 входа - для работы передатчика. По 16-битной шине D приходит очередное слово, которое мы должны передать. По входу TxIsDataWord выбирается полярность передаваемого синхроимпульса (одна полярность означает командное/ответное слово, другая - слово данных). Вход IsCRC - это специфика нашего "изделия". Подача IsCRC означает: мы добрались до последнего слова в сообщении, и вместо 16 бит, подаваемых через D, мы должны по 1 биту "вытащить" из сидящего по соседству модуля CRC. Эти биты поступают на вход CRC_bit. И наконец, по приходу start=1 передатчик запускается.
Теперь смотрим выходы этого модуля. Q - это 16-битная шина с принятым словом. DV означает Data Valid. Когда DV=1, значит, мы только что приняли слово, и в этот конкретный такт его можно снять с выхода Q. Позже там может образоваться "мусор". RxIsDataWord нужно опросить тогда же, при DV=1. Там будет указано, что мы получили: командное слово или слово данных. Они отличаются друг от друга синхроимпульсом, и мы его полярность специально запомнили.
TxEnable - выход разрешения работы передатчика. Так нужно для работы с микросхемами 5559ИН13, но так же, как видно по схеме, с этого выхода управляется 1-битный мультиплексор входа CRC.
RxP и RxN - входы передатчика, для генерации положительной и отрицательной полуволны сигнала. Тут "проигнорировать" один из входов ну никак нельзя :)
Выход isDataBit нужен для работы модуля CRC: он отмечает моменты, когда с входа RxP можно снять бит данных, чтобы сосчитать CRC от всего сообщения.
И наконец, TxReady сообщает о том, что передача очередного слова завершилась, и можно браться за следующее. Остальные выходы - чисто отладочные.
Разобравшись с интерфейсом, залезем "внутрь", вот его код:
//приёмопередатчик слов МКО (ГОСТ Р 52070-2003, Mil-Std 1553) "для бедных"
//использует только один входной бит, который равен "1", когда на линии пауза или отрицательная полуволна,
//либо "0", когда положительная полуволна. (подойдёт и "0" во время паузы, нам вообще пофиг!)
//так мы можем сделать приёмник буквально на 1 транзисторе! (автор так и делал, работает)
//выходы нужны оба, разумеется. Сигналы, подающиеся на них, инвертированы, т.е лог "0" соотв. положительной полуволне, лог "1" - отрицательной
//(т.е для 5559ИН13У и подобных)
//рекомендуется достаточно высокая тактовая частота, и ОБЯЗАТЕЛЬНО кратная 1 МГц. В противном случае, вероятно, будем "уползать" к концу приёма слова.
//4 МГц - маловато (хотя конкретно этот вариант пока не пробовали), а вот 25 МГц хорошо.
//в данный момент ТОЛЬКО 25 МГц и подходит, т.к мы сделали довольно специфичный делитель частоты...
//потом подправим.
//дополнительная функциональность: когда передаём сообщения, то на последнем сообщении вместо 16-битного слова данных начинаем подавать на вход CRC бит за битом.
//только вот чётность неправильно считали от CRC...
`include "math.v"
module MilStdPHYforCRC ( input clk,
input RxP, //вход МКО, положительный. Для 5559ИН13У. Для схемы на 1 транзисторе он получится отрицательным, тогда ставим инвертор.
input [15:0] D, input start, input TxIsDataWord, //сигнал на запуск передачи, требуемые данные и признак "команда/данные".
input isCRC, input CRC_bit, //защёлкивать данные с входа D (нормальные данные) или по 1 биту с входа CRC_bit
output [15:0] Q, output DV, output RxIsDataWord, //сигнал Data Valid (получили из линии корректное слово), сами данные и признак "команда/данные"
output reg TxEnable = 1'b0, output reg TxP = 1'b1, output reg TxN = 1'b1, //разрешение работы передатчика, противофазные выходы. "0" на TxP,TxN - открытие соотв. транзистора, "1" - закрытие!
//поскольку они покидают пределы ПЛИС, хочется, чтобы на них отсутствовал комбинаторный мусор, так что делаем их регистрами.
//так нужно для 5559ИН13У! Иначе всё нахрен спалим...
output isDataBit, //нужно для модуля CRC, чтобы он вовремя принимал/выдавал по 1 биту.
output TxReady, //чуть загодя говорит, что всё передал, осталась чётность.
//по TxReady запрашивается следующее слово из памяти, по нему же меняется состояние из sReply в sTransmit.
//почему это надо делать именно в этот момент?
//ждать отключения TxEnable нельзя, возникнет заминка.
//а вот пораньше нельзя разве? Нельзя: именно TxReady управляет CRC, нужно их держать до последнего бита!
output [4:0] DState, output DSync, output Dce); //отладочные выходы.
//никакого Idle - эту роль должен исполнить TxEnable! (при передаче. А на приёме нас синхронизатор будет непрерывно сбрасывать)
localparam sSync = 5'b0_0000; //передаём синхроимпульс.
localparam sB1 = 5'b0_0001; //1. Тут у нас защёлкнут синхроимпульс (он же isDataWord), и по ce=1 защёлкнем первый бит данных. Сюда и надо прыгнуть...
localparam sB2 = 5'b0_0010; //2
localparam sB3 = 5'b0_0011; //3
localparam sB4 = 5'b0_0100; //4
localparam sB5 = 5'b0_0101; //5
localparam sB6 = 5'b0_0110; //6
localparam sB7 = 5'b0_0111; //7
localparam sB8 = 5'b0_1000; //8
localparam sB9 = 5'b0_1001; //9
localparam sB10 = 5'b0_1010; //A
localparam sB11 = 5'b0_1011; //B
localparam sB12 = 5'b0_1100; //C
localparam sB13 = 5'b0_1101; //D
localparam sB14 = 5'b0_1110; //E
localparam sB15 = 5'b0_1111; //F
localparam sB16 = 5'b1_0000; //10 защёлкнут 15-й бит, по ce=1 защёлкнем 16-й
localparam sParity = 5'b1_0001; //11 уже защёлкнут последний бит данных, а по ce=1 на входе появится бит чётности.
//но во время приёма успеваем также перейти в 5'b1_0010 = 12 и даже в 5'b1_0011 = 13.
//надо, чтобы isDataBit и на них тоже не срабатывал!
wire [4:0] State; //текущее состояние. Идёт ли приём или передача - определяем по выходному регистру TxEnable. По окончанию передачи 1 слова снова переключаемся на приём.
wire isSync = (State == 5'b0_0000); //нужна при передаче, чтобы затянуть длительность посылки (в 3 раза)
wire isB16 = State[4]; //нужна при передаче, чтобы вместо данных защёлкнуть на выход бит чётности. (повторно защёлкнуть чётность в конце передачи - вообще похрен)
wire isStopState = State[4] & State[0]; //пора формировать DV, а также отключать передачу
//логика запуска и останова
always @(posedge clk)
TxEnable <= isStopState & ce & Pol? 1'b0 : start? 1'b1 : TxEnable;
//приоритет за остановкой. Так, если у нас "стоят над душой" (start=1 и новые данные по D, в ожидании, когда же закончим),
//то мы всё-таки "мигнём" TxEnable=0 на один такт, затем уже запустимся по-новой. Считаю, что это не страшно!
//и приоритет за передачей. Это уж особенность нашего подхода к приёму (всё время нам кажется, что что-то принимается, но сихнронизатор нас перезапускает постоянно)
//не страшно: мы будем запускать передачу только в ответ на вполне конкретные посылки, вряд ли ошибёмся (нужно чтоб совпал наш адрес, был корректный подадрес, совпал бит чётности).
wire DoStart = start & (~TxEnable); //единичный импульс запуска.
wire ce;
reg Pol; //полярность импульса.
always @(posedge clk)
Pol <= (DoStart | Sync & (~TxEnable))? 1'b0 : Pol^ce;
//нужен ли &(~TxEnable)? По идее, нам на вход пойдёт повторение нашего же передатчика,
//но со сдвигом на несколько тактов. Оно нам надо... Так что при передаче мы его игнорируем напрочь!
//сразу введём сдвиговый регистр
reg [15:0] SR;
reg TxSerialOut;
always @(posedge clk) if (DoStart | ce)
TxSerialOut <= DoStart? TxIsDataWord : ~Pol? ~TxSerialOut : isB16? parity : isCRC? ~CRC_bit : ~SR[15];
//всё равно мерзопакостное выражение вышло... Во сколько же LE это выйдет?
//зато вот эти простые:
always @(posedge clk) begin
TxP <= ~TxEnable | TxSerialOut; //то есть, при TxEnable=0 у нас тупо единицы, чтобы транзисторы все закрылись.
TxN <= ~TxEnable | ~TxSerialOut;
end
//наверное, мы всё-таки их выставим в единицу по окончании передачи.
//всё-таки, они наружу торчат, это и энергопотребление лишнее, и наводки на другие цепи, если оно будет по чём зря "выступать"...
assign Q = SR;
reg rIsData;
assign RxIsDataWord = rIsData;
//входные данные в первую очередь надо защёлкнуть, во избежание метастабильности.
//точнее, нас пугает, если несколько частей нашей схемы среагируют чуть по-разному. Но и вероятность метастабильности снизим...
reg rD;
always @(posedge clk)
rD <= RxP;
//теперь повсюду мы должны использовать именно rD.
wire Sync;
wire Dprev; //пока не используем, а там подумаем...
MilStdSyncV3 syncer (
.clk (clk),
.D (rD),
.Q (Sync),
.Dprev (Dprev));
defparam
syncer.clkPERIODns = 40; //мучительно возвращаемся к 25 МГц
MilStdDividerFor25MHz Divider ( //а может, и не столь мучительно...
.clk (clk),
.lsb(Pol),
.start(DoStart | ce&isSync&(~Pol)),
.startRX(Sync&(~TxEnable)),
.ce (ce) );
lpm_counter StateMachine (
.clock (clk),
.cnt_en (ce & Pol),
.sclr (DoStart),
.sset (Sync & (~TxEnable)),
.q (State));
defparam
StateMachine.lpm_direction = "UP",
StateMachine.lpm_port_updown = "PORT_UNUSED",
StateMachine.lpm_type = "LPM_COUNTER",
StateMachine.lpm_width = 5,
StateMachine.lpm_svalue = sB1;
always @(posedge clk) if (ce & Pol | DoStart) begin
SR[14:0] <= DoStart? D[14:0] : {SR[13:0], ~rD};
//SR[15] <= DoStart? D[15] : (isCRC & (~isB16))? CRC_bit : SR[14];
SR[15] <= DoStart? D[15] : SR[14]; //передумали вносить CRC сюда, давайте сразу в Serial out...
end
//инверсия - поскольку в этот раз делаем сэмпл в конце каждого такта.
//точнее, потому что мы собираемся пока использовать инвертированный входной сигнал, оно как-то "привычнее"!
always @(posedge clk) if (Sync)
rIsData <= rD;
assign DV = isStopState & ce & Pol & (~TxEnable); //и пока нет проверки на чётность во время приёма!
reg parity;
always @(posedge clk) if (ce & Pol | DoStart)
// parity <= DoStart? 1'b0 : parity ^ SR[15];
parity <= DoStart? 1'b0 : parity ^ (isCRC? CRC_bit : SR[15]);
assign TxReady = isStopState & TxEnable & ce & (~Pol); //т.е на 500 нс раньше окончания передачи. Вроде сойдёт...
//нам isDataBit лишний мусор гонит во время синхроимпульсов. Надо это дело заблокировать
reg enableCRC;
always @(posedge clk)
enableCRC <= Sync & (~State[4])? 1'b1 : isStopState? 1'b0 : enableCRC;
//под DataBit нужно хорошо подобрать момент, чтобы сработало и на приёме, и на передаче!
//у нас с передачей проблема была, CRC опаздывал, мы уже защёлкивали старое значение.
//попробуем на середине такта выдавать, а там посмотрим...
assign isDataBit = (~isSync) & enableCRC & ce & (~Pol);
//правда, теперь я не уверен, что оно при передаче правильно сработает... Ща глянем.
assign Dce = ce;
assign DState = State;
assign DSync = Sync;
endmodule
Довольно забавная "помесь ежа и ужа", здесь приёмник и передатчик как "сиамские близнецы", с общим сдвиговым регистром, счётчиком состояний и делителем частоты. В итоге вся эта хреновина занимает 66 ЛЭ, что я считаю ОЧЕНЬ хорошим результатом, учитывая все тонкости даже этого, физического уровня (синхроимпульс втрое большей длительности чем обычный бит, его переменная полярность, манчестерский код, бит чётности).
Но всё же надо понимать: это вполне очевидный ПОЛУДУПЛЕКС. Данный модуль может либо передавать данные, либо получать их, но не всё сразу!
Внезапно ЖЖ говорит, что запись большая, поэтому - продолжение следует.