Протокольный контроллер МКО для серьёзных ребят

Nov 23, 2023 03:03

Продолжение этого ( раз, два)

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

А для начала надо вспомнить, как у нас всё работало...




На этой "схеме" ничего особенно интересного нет. Видим, что всё завязано на модуль протокольного контроллера, который здесь именуется MilStdRTwithInts. MilStd - значит Military Standart, "военный стандарт", это просто сокращённо от MilStd 1553B, с которого и пошёл наш ГОСТ Р 52070-2003. RT значит Remote Terminal, т.е оконечное устройство (ОУ). То есть, этот протокольный контроллер умеет работать только в таком режиме, что делает его гораздо более компактным в реализации, чем "универсальный комбайн", который может быть и КШ, и ОУ, и МШ, и всё это со множеством наворотов... И наконец, with Ints - т.е "с прерываниями". Это пока не соответствует истине, но скоро и до этого дойдём. Как ни странно, именно этот протокольный контроллер будет в некотором смысле ГЛАВНЫМ на ПЛИС, это он будет управлять процессором, запуская фрагменты кода в ответ на получаемые команды по шине, а не наоборот!

Мы видим, что этот контроллер соединён с памятью данных: с неё на контроллер идут провода MemIn (16-битная шина данных) и MemReady (сигнал, что запрошенные данные пришли). С контроллера в память идут провода Addr (9 бит адреса), MemWrReq (запрос на запись по данному адресу), MemRdReq (запрос на чтение по данному адресу) и Q (16-битная шина данных на запись).

Также он в тесной связке с "физическим уровнем" МКО, рассмотренным в прошлый раз. И в дополнение у нас тут стоят часы реального времени RTC, они генерируют паузу от 2 до 12 мкс между командным и ответным словом, но главное, синхронизируются с "бортовым временем", посылаемым командами управления "Синхронизация (с СД)", и запоминают одну метку времени - середину экспозиции информативного кадра.

Что ж, теперь заглянем вовнутрь, по маленьким кусочкам.

module MilStdRTwithInts (input clk,
//интерфейс с приёмником МКО
input [15:0] D, input RXisData, input DataValid, input FrameError, input CRCerror,
//интерфейс с передатчиком МКО
output [15:0] Q, output TXisData, output start, input TxReady,
//управление CRC
output ComputeCRC, output TransmitCRC,
//интерфейс с оперативной памятью (малого объёма, около 1000 слов)
output [MemWidth-1:0] Addr, input [15:0] MemIn, input MemReady, output MemWrReq, output reg MemRdReq = 1'b0,
//интерфейс с часами реал времени
output sync, input [15:0] TimeStamp, input ce_2us,
//интерфейс с адресной заглушкой
input [4:0] OurAddr, input AddrParity
);

parameter MemWidth = 9;

Все подсоединения мы уже разобрали. И ещё ширину адресной шины задали, 9 бит. Адресация у нас по 16-битным словам, т.е при 9 битах мы получаем 1024 байта памяти (512 слов). В МКО вообще может быть 30 подадресов, в каждом по 32 слова, это 960 слов, но мы переиначили старший бит подадреса - он теперь играет роль бита чётности, и подадресов остаётся только 15, или 480 слов. Поэтому сейчас ограничиваемся 9 битами. В ПЛИС 5576ХС6Т памяти на 5 килобайт, так что если удастся обойтись оперативкой в 1 килобайт и памятью кода в 4 килобайт - это будет успех!

reg OurAddrOK = 1'b0;
always @(posedge clk)
OurAddrOK <= (^OurAddr^AddrParity);

Проверка адресной заглушки - тут пока по-старинке, в двоичных кодах. Троичные сюда не приплетаю, хотя штука и интересная вышла.

wire [4:0] curWordAddr = D[15:11]; //адрес ОУ
wire curWordDoTransmit = D[10]; //признак "приём-передача" (1 значит мы должны передавать данные)
wire [4:0] curWordSubAddr = D[9:5]; //подадрес
wire [4:0] curWordWordsCount = D[4:0]; //число слов данных (0 означает 32), при подадресах 00000 или 11111 это "команды управления"

Для удобства "распилили" входную шину на много отдельных полей.

localparam sIdle = 2'b00;
localparam sReceive = 2'b01;
localparam sReply = 2'b10;
localparam sTransmit = 2'b11;

reg [1:0] State = sIdle;
wire isIdle = (State == sIdle);
wire isReceive = (State == sReceive);
wire isReply = (State == sReply);
wire isTransmit = (State == sTransmit);

В кои-то веки у нас реализован конечный автомат на 4 состояния:
- sIdle (ожидание),
- sReceive (приём слов данных),
- sReply (отправка ответного слова),
- sTransmit (отправка слов данных).

reg MessageError = 1'b0; //становится одним из флагов в ответном слове

reg DoWeNeedToTransmit = 1'b0;
always @(posedge clk) if (isIdle)
DoWeNeedToTransmit <= curWordDoTransmit;

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

Введены 1-битные регистры DoWeNeedToTransmit (0: мы получаем данные и передадим лишь ответное слово, 1: мы получили запрос и должны после ответного слова передать данные) и rBroadcast (0: обычные команды, 1: групповая/широковещательная команда, что значит, ответное слово посылать нельзя, все его пошлют и выйдет коллизия). По моей традиции, они непрерывно обновляются в состоянии sIdle, поскольку в самом состоянии sIdle они не используются, а из sIdle в последующие нас может перевести лишь приём правильного командного слова, так что пускай именно конечный автомат будет проверять, переключиться ли ему, а этим регистрам усложнять себе жизнь ни к чему. И вишенкой на торте регистр MessageError - по умолчанию он нулевой, но на каждом этапе, если что-то не то пришло (неверные подадреса, неверный заголовок массива, неверный CRC) - он установится в единицу, покажет ошибку в ответном слове и запретит передавать слова данных.

wire isOurAddr = (OurAddr == curWordAddr)&OurAddrOK | isBroadcast;

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

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

У нас тут напихано комбинаторной логики, которая из входящего слова вычленяет все его свойства: что оно действительно пришло (DataValid=1), и это командное слово (RXisData=0), оно адресовано нам либо всем подряд. А также: является ли это слово командой управления, и сколько слов данных мы ожидаем (от 0 до 32, чуть-чуть не лезет в 5 бит, поэтому взято 6).

wire noWordsLeft;
wire MinusOneWord = State[0] & TxReady | DataValid & RXisData;
wire [5:0] WordsLeft;
lpm_counter WordCounter (
.clock (clk),
.cnt_en (MinusOneWord),
.data (WordCount),
.sload (isIdle),
.Q (WordsLeft),
.cout (noWordsLeft) );
defparam
WordCounter.lpm_direction = "DOWN",
WordCounter.lpm_port_updown = "PORT_UNUSED",
WordCounter.lpm_type = "LPM_COUNTER",
WordCounter.lpm_width = 6;

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

reg OneTickLeft = 1'b0;

always @(posedge clk) begin
OneTickLeft <= ~State[1]? 1'b0 : ce_2us | OneTickLeft;

Чтобы гарантированно получить паузу между командным словом и ответным словом не менее 2,44 мкс, введён однобитный регистр OneTickLeft. В состояниях sIdle (ожидание) и sReceive (приём слов данных) он поддерживается равным нулю, а выйдя в состояние sReply, он защёлкнется в единицу по поступившему из RTC сигналу ce_2us. А по следующему сигналу ce_2us, когда уже будет OneTickLeft=1, наконец-то сформируется сигнал start, который запускает передатчик.

State <= FrameError? sIdle :
isIdle? (BeginMessage? sReceive : sIdle):
isReceive? ((DoWeNeedToTransmit | noWordsLeft)? (rBroadcast? sIdle : sReply) : sReceive):
isReply? (TxReady? sTransmit: sReply):
(~DoWeNeedToTransmit | noWordsLeft | MessageError)? sIdle : sTransmit;
end

Логика перехода между состояниями относительно проста, хотя условий там прилично. Из sIdle переходим в sReceive, если пришло командное слово, адресованное нам. В sReceive держимся, если нам нужно получать слова данных, и они ещё не закончились. Как только эти условия не выполняются, прыгаем в sReply, но только если команда не была групповой (широковещательной), в этом случае возвращаемся в sIdle. В sReply сидим, пока не закончится передача ответного слова, и оттуда прыгаем в sTransmit. Наконец, там мы сидим, если надо передавать слова данных и они ещё не закончились, и отсюда с чистой совестью возвращаемся в sIdle. И ещё мы в любой момент можем возвратиться в sIdle, если получим FrameError, т.е получение какой-то тарабарщины вместо очередного слова. В данный момент, впрочем, этот FrameError "висит в воздухе", в смысле, всегда нулевой.

assign start = State[1] & (OneTickLeft&ce_2us | State[0]);
assign TXisData = State[0];

Непосредственное управление передатчиком: когда ему стартовать и какой полярности пускать синхроимпульс (ответное слово / слово данных). Как видно, start сработает в состоянии sReply=102, но только после двух импульсов ce_2us (первый взведёт OneTickLeft, второй непосредственно даст start=1). А в состоянии sTransmit=112, у нас непрерывно start=1, у передатчика хватает ума не сбрасываться каждый раз и начинать передачу сначала. Синхроимпульс, как видно, определяется младшим битом состояния, поэтому в sReply это будет 0 (ответное слово), а в sTransmit это будет 1 (слово данных). А что там будет в sIdle и sReceive, нам по барабану, поскольку передатчик тупо не включится.

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

always @(posedge clk) if (isIdle) begin
HiMem[3] <= curWordSubAddr[3];
HiMem[2:0] <= curWordSubAddr[2:0] & {3{curWordDoTransmit}};
SubAddr <= curWordSubAddr[2:0];
end

assign Addr[8:5] = HiMem;

Началась возня с памятью. Тут заморочки конкретно моего прибора. Я определил для него следующие корректные подадреса:

На приём (K=0):
1 0001 - FT (Flight Task) - полётное задание
1 0010 - LT (Long range Target) - параметры МДД
0 0011 - ST (Short range Target) - параметры МБД
1 0100 - AF (AFfine matrix) - матрица аффинного преобразования
0 0101 - RA (RAw data) - сырые данные с соседнего комплекта для стереорежима
на передачу (K=1):
0 0110 - DA (DAta) - целевая информация
1 1000 - TM (TeleMetry) - телеметрическая информация
0 1001 - IM (IMage)- передача изображения
0 1010 - DU (DUmp) - передача дампа памяти
0 1100 - RA (RAw data) - передать сырые данные на соседний комплект для стереорежима

Старший бит отделён - он играет роль бита чётности, и как адресный уже "не работает". Как видно, сумма всех бит подадреса должна быть чётной.

Поначалу я просто брал 4 бита подадреса - и делал их старшими битами адреса в памяти. Но теперь, если мы ПРИНИМАЕМ сообщение, оно должно сохраняться по НУЛЕВОМУ адресу (т.е начать с нулевого, и занять первые 32 позиции), именно там мы решили расположить входной буфер. Только если сообщение принялось целиком без единой ошибки, должно сформироваться прерывание - и процессор уже сам переместит данные в более подходящую позицию, попутно сделав и другие необходимые операции. Там слишком всё вариативно, чтобы отдавать "жёсткому" протокольному контроллеру...

А поскольку мы теперь теряем биты подадреса в случае приёма сообщения (они устанавливаются в ноль), мы младшие 3 из них сохраняем в регистр SubAddr. Почему только 3 - видно из списка выше. На приём этого достаточно, бит 4 заведомо нулевой.

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;

А младшие адреса памяти мы формируем с помощью счётчика. Традиционно, он непрерывно сбрасывается в sIdle, а затем начинает прибавлять по единичке по мере приёма очередного слова данных, либо по мере их отправки.

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

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

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

assign Q = State[0]? TxMux : {OurAddr, MessageError, 10'b00_0000_0000};

Опять специфика нашего прибора: мы отслеживаем передачу целевой информации, и там одно слово заменяем меткой времени из RTC. Так мне показалось проще, чем протаскивать метку времени через процессор: так пришлось бы его ОСНОВНОЙ МУЛЬТИПЛЕКСОР ещё расширить, т.е научиться на шину данных ещё и значение с RTC выдавать, а этот мультиплексор и так отожрался до невероятных размеров, каждый новый вход - это снижение допустимой тактовой частоты, НЕ ХОЧУ. А здесь, "на периферии", с таймингами вообще никаких проблем. Тем более, нам всё равно нужно было сохранить слово данных из памяти (не будет же память держать его 20 мкс подряд на своём выходе, пока мы не соизволим им воспользоваться!), ничего не теряем.
И наконец, на вход передатчика мы коммутируем либо эти данные, либо "на ходу собранное" ответное слово: сначала наш адрес на шине, затем бит ошибки, а остальное - нули. Там куча отдельных "флажков": неисправность ОУ, абонент занят, готов стать новым КШ, запрос на обслуживание и пр., но мы их пока что не используем (в этом нет особого смысла - если ОУ неисправен, что на это может сделать борт - выключить нас. Проще не отвечать - всё равно выключит. Абонент занят - это нам говорить запретили, шина очень нагруженная, ждать когда кто-то освободится очень накладно, все циклограммы коту под хвост! Стать нам новым КШ - с какой стати? Что за обслуживание нам можно провести?)

assign MemWrReq = DataValid & RXisData & State[0] & (~MessageError);

always @(posedge clk)
MemRdReq <= TxReady;

Запрос на запись в память происходит, когда нам пришло новое слово данных, мы в состоянии sReceive (считалось, что в состоянии sTransmit нам слова данных прийти не могут, т.к мы сами передаём) и ошибки в сообщении пока нет. Последнее очень важно, чтобы неверные подадреса не "затирали нам память".

Запрос на чтение из памяти происходит, когда мы только что передали какое-то слово. Так, первым всегда отправляется ответное слово (для него запрашивать что-то из памяти не надо, мы него формируем на лету), и когда оно почти что отправилось, мы запрашиваем первое слово данных.

//проверка корректности пары "приём/передача" и подадреса

wire isSubAddrError;
VIPSsubaddrError ErrDetection (
.subAddr (curWordSubAddr),
.isError (isSubAddrError));
//соответствие приёма/передачи подадресу
wire isTransmitReceiveError = ((curWordSubAddr[3] | curWordSubAddr[2] & curWordSubAddr[1]) ^ curWordDoTransmit) & (~isServiceCommand);

wire isCWerror = isSubAddrError | isTransmitReceiveError;

Ещё некоторое количество комбинаторной логики, как раз-таки проверить подадрес и бит "приём-передача", т.е есть ли вообще такой подадрес и рассчитан ли он на запись или на чтение. Результатом становится "провод" isCWerror (т.е Command Word error), который в нужном месте "защёлкнется" в регистр MessageError.

reg isSyncDWCommand = 1'b0;

always @(posedge clk) if (isIdle)
isSyncDWCommand <= isServiceCommand & (curWordWordsCount == 5'b10001);

assign sync = DataValid & RXisData & State[0] & isSyncDWCommand;

Расшифровываем команду управления "Синхронизация (с СД)". В момент прихода командного слова мы убеждаемся, что это она, а когда приходит единственное слово данных, выдаём сигнал sync для RTC, так что содержание этого слова туда защёлкивается.

wire [15:0]ExpectedHeader;

ReedMuller5_16 HeaderCheck (.D ({HiMem[3]^(^SubAddr), {HiMem[3], SubAddr}}),
.Q (ExpectedHeader));

И ышшо немного комбинаторной логики: для выбранного подадреса находим, какой должен быть заголовок массива (это наша специфика, захотелось сделать всё шикарно, чтобы разные заголовки отличались друг от друга минимум на 8 бит при их размере всего лишь в 16). Вот где пригодился регистр SubAddr. В первой реализации здесь стоял просто HiMem, но когда решено было все приходящие сообщения кидать в нулевой адрес, заголовки стали генериться неправильно... Как видно, старший бит достраивается заново через чётность, следующий за ним берётся из HiMem, а вот 3 младших из SubAddr.

reg isFirstDataWord = 1'b0;
always @(posedge clk) if (isIdle | DataValid & RXisData)
isFirstDataWord <= isIdle;

always @(posedge clk)
MessageError <= isIdle? isCWerror : (isFirstDataWord? DataValid & RXisData & (D != ExpectedHeader) : CRCerror) | MessageError;

Вот оно, детектирование ошибок. Для начала формируется признак isFirstDataWord, означающий: "мы получили первое слово данных". По идее, можно было по младшим адресам памяти это проверить, но вот так показалось проще. В состоянии sIdle этот регистр остаётся в единичке, но это не страшно, никто его не проверяет. По приходу командного слова он всё ещё остаётся в единичке, и остаётся таковым до прихода первого слова данных. Когда оно пришло, DataValid=1, к следующему такту isFirstDataWord установится в ноль, но на текущем такте будет подтверждено: "это первое слово".

Итак, на последнем такте sIdle (а это по определению такт, на котором пришло командное слово), мы проверяем командное слово. Увидев, что оно ошибочно - мы не сбрасываемся совсем, потому как если адрес всё-таки совпал, то мы должны на него грязно выругаться. Далее, если мы получили первое слово данных, "заголовок массива", мы проверяем его. И наконец, в противном случае, мы проверяем CRC. "Провод" CRCerror идёт из модуля CRC: пока полином "работает", по этому проводу идут нули, мы продолжаем строить контрольную сумму. Но когда CRCenable=0, полином "отключается", модуль превращается в простейший сдвиговый регистр, "выталкивающий" посчитанное значение бит за битом. Оно сравнивается с поступающей в это же самое время контрольной суммой в последнем слове данных, и если было несовпадение, у нас "зажигается" MessageError. Пожалуй, у нас немножко это переусложнено получилось - уже в самом модуле CRC если хоть один бит не совпал, Error продолжает "гореть", хотя он мог быть полностью комбинаторным - один раз выскочившая единичка всяко "зашёлкнула" бы MessageError здесь. Один ЛЭ мы на этом экономим, о чудо.

(чувствую, у меня заканчивается объём поста, разметка кода очень прожорлива сейчас на HTML, поэтому дальше Plaintext...)

assign ComputeCRC = State[0] & (WordsLeft[4:0] != 5'b0_0001);
assign TransmitCRC = State[0] & (WordsLeft[4:0] == 5'b0_0001);

Ну и раз уж речь зашла о CRC... Непосредственно в модуль CRC идёт "провод" ComputeCRC. Когда ComputeCRC=0, модуль работает как сдвиговый регистр с "заземлённым" входом (т.е вталкиваются сплошные нули, а выталкивается бит за битом успевшее посчитаться значение), а когда ComputeCRC=1 - тот же сдвиговый регистр с обратными связями через XOR, что и превращает его в CRC. Здесь сказано следующее.

В состоянии sIdle=002 он сдвиговый регистр, с единственной целью обнулиться.
В состоянии sReceive=012 он работает как CRC, прогоняя через себя все слова данных. Если вдруг нам пошлют 2 командных слова подряд (формат передачи ОУ-ОУ, т.е нам сказали принять, а кому-то сказали передать), мы по ошибке посчитаем CRC и от "чужого" командного слова, и от "чужого" ответного слова. Чтобы такое не случилось, можно будет чуть усложнить выражение, но пока этого не требуется, у нас таких форматов на шине не бывает! На последнем слове ComputeCRC опять становится нулевым, чтобы как раз сравнить посчитанное с пришедшим и выдать ошибку по необходимости.
В состоянии sReply=102 CRC снова становится сдвиговым регистром, чтобы случайно "не вобрать в себя" ответное слово, этого нам не надо, пущай остаётся нулевым.
И наконец, в состоянии sTransmit=112 CRC снова работает все слова, кроме последнего, когда его надо передать, и нельзя туда вщёлкивать новые биты, это его запорет.

Провод TransmitCRC поступает в приёмопередатчик, заставляя того передавать не то слово, которое ему подали по входу D, а бит за битом его вытащить из модуля CRC по ходу дела. Это осуществляется как раз на последнем слове данных, которое и есть CRC.

reg rInterrupt = 1'b0;
wire cInterrupt;

MilStdIsInterrupt IsInterrupt ( .SubAddr (curWordSubAddr[4:2]),
.CWerror (isCWerror),
.IsInt (cInterrupt));

always @(posedge clk) if (isIdle)
rInterrupt <= cInterrupt;

И это недописанный код для прерываний...

endmodule

Фух... Вроде я вспомнил, что это за зверь... Он был вполне рабочим, все сообщения я проверял сначала на симуляции, потом уже "в железе", работало как надо.

Осталось понять, как его модифицировать. Комбинаторную логику, которая разными способами независимо друг от друга декодирует текущее слово - НЕ ТРОГАЕМ, с ней всё хорошо, это акын, "что вижу - о том пою". Надо лишь немножко поменять управляющую логику.

Во-первых, нужно ввести 1-битный регистр, указывающий, по какой шине мы сейчас работаем:

reg ActiveBus;

Условно, 0 - основная шина, 1 - резервная шина.

Именно его выход отправится в наш резервированный приёмопередатчик, чтобы он знал, передачу по какому каналу запускать, и CRC с какого из них считать.

Сейчас мы воспринимаем командное слово как таковое только в состоянии sIdle. Как только начался обмен данными - мы командные/ответные слова игнорируем начисто, обращая внимание только на слова данных. Но теперь мы должны ожидать его в любой момент, и всё так же защёлкивать все необходимые данные для дальнейшей работы, и переходить в состояние sReceive.

Так что моя весёлая "пофигистская" логика для множества регистров накрывается медным тазом. Вместо:

always @(posedge clk) if (isIdle)

надо ПОВСЮДУ написать:

always @(posedge clk) if (BeginMessage)

И счётчики, которые непрерывно остаются в начальном состоянии (либо сброс, либо загрузка из шины данных) в состоянии sIdle, теперь должны это делать по BeginMessage.

Разумеется, надо подрихтовать сам конечный автомат, сейчас он реагировал на BeginMessage только в состоянии sIdle, а надо ВСЕГДА.
Было:

State <= FrameError? sIdle :
isIdle? (BeginMessage? sReceive : sIdle):
isReceive? ((DoWeNeedToTransmit | noWordsLeft)? (rBroadcast? sIdle : sReply) : sReceive):
isReply? (TxReady? sTransmit: sReply):
(~DoWeNeedToTransmit | noWordsLeft | MessageError)? sIdle : sTransmit;

Стало:

State <= FrameError? sIdle :
BeginMessage? sReceive:
isIdle? sIdle:
isReceive? ((DoWeNeedToTransmit | noWordsLeft)? (rBroadcast? sIdle : sReply) : sReceive):
isReply? (TxReady? sTransmit: sReply):
(~DoWeNeedToTransmit | noWordsLeft | MessageError)? sIdle : sTransmit;

И по тому же самому BeginMessage надо защёлкивать значение ActiveBus:

always @(posedge clk) if (BeginMessage)
ActiveBus <= BusIndex;

BusIndex - это бит, который присылает резервированный приёмопередатчик, приняв очередное слово. 0 - значит слово пришло по основной шине, 1 - значит по резервной.

И наконец, нам нужно сбрасывать работу передатчиков и CRC, тупо по BeginMessage (т.е по приходу командного слова, в любой момент, по любой шине, но адресованного НАМ):

assign sclr = BeginMessage;

Вот, собственно, и всё! Изменения оказались минимальными. Количество ЛЭ увеличилось ровно на единицу, на регистр ActiveBus, тут уж никуда не денешься, надо запомнить, откуда нам данные пришли, чтобы туда же и отвечать.

Пора всё это собрать воедино. Сначала модуль протокольного контроллера "в сборе", PHY+RT+RTC (физический уровень, логика оконечного устройства и часы реального времени):




Чтобы проверить его работу на симуляции, подключим к нему память "в единоличное пользование", безо всяких там процессоров. Для этого схему выше оформляем как схемотехнический символ redunMilStdFull (от слова redundant - резервированный). И рисуем такую схемку:




Кроме памяти и МКО "в сборе", тут сидит аж два D-триггера, чтобы задержать запрос на получение данных на 2 такта. Именно тогда нам на шину MemIn придёт запрошенное, и мы должны проинформировать контроллер об этом.

Видно, что количество выводов у нас стремительно сокращается, всё замыкается "внутри", а наружу торчит одна лишь тактовая частота, входы и выходы МКО. Эту схему превратим в схемотехнический символ MilStdRedunWithMem, и, наконец, нарисуем тестовую схему:




Скромный модулёк по центру - это как раз весь наш драндулет в полном сборе. А два модуля сверху и снизу - это отдельно приёмопередатчики (PHY), которые мы можем попросить передать одно командное слово. Без этого наш модулёк ничего делать не будет, он же Оконечное Устройство (ОУ), не заговорит, пока к нему по уставу не обратятся.

Что ж, запускаем шарманку:




В самом начале симуляции мы отправляем командное слово 0x34D8 (адрес 6, подадрес 6, приём данных из ОУ, 24 слов) по ОСНОВНОМУ каналу передачи данных. Как только оно пришло, выдержав положенную паузу, ОУ начинает отвечать, также по ОСНОВНОМУ КАНАЛУ. Передано ответное слово, за ним пошло слово данных, но мы не дремали и послали то же самое командное слово по РЕЗЕРВНОМУ КАНАЛУ. И как только было принято оно (в разгар ответа по основному каналу), мы бросили передачу на полуслове, и через положенную паузу начали отвечать всё то же самое, но по РЕЗЕРВНОМУ.

Что ж, выглядит многообещающе... Хочется теперь "с нахрапу" запихать это в железо и проверить там.

странные девайсы, ПЛИС, программки, работа, троичная система счисления

Previous post Next post
Up