Безумный MAX3032: зазеркалье отрицательных фронтов

Mar 30, 2022 01:54

[Описание и оглавление серии постов ]
В этой серии будем "пробовать на зуб" азбучные истины плисоводства, раз за разом понимая: они не просто так появились, позволяют избежать множества ошибок и не закапываться чересчур глубоко, но если быть аккуратным, можно их и нарушить :)

Все части на этот момент:
Генератор зла - об использовании "логики общего назначения" для генерации тактовой частоты;
Великая перезагрузка - нужен ли аппаратный Reset и как без него обойтись;
Зазеркалье отрицательных фронтов


Как выстрелить себе в ногу на VHDL:
Вы аккуратно описываете компоненты «нога», «рука», «пистолет» и «пуля». Любовно их отлаживаете и моделируете по отдельности. После синтеза всей системы обнаруживается, что нога и пистолет активируются на чётных тактах сигнала синхронизации, а рука и пуля на нечётных.
(автор неизвестен, гуляет с незапамятных времён)

Очередная прописная истина: все переключения в схеме должны происходить по ФРОНТУ тактового импульса. То, что в verilog называется posedge clk (т.е positive edge, положительный фронт), в новом VHDL: rising_edge(clk), а в более старом VHDL (единственный, который поддерживается в Quartus II 9.0) это называется (clk'event AND clk = '1').

В коде можно прописать и СПАД тактового импульса, negedge clk в верилоге, falling_edge(clk) в новом VHDL, (clk'event AND clk = '0') в более старом, но куда ни глянь, просят ТАК НЕ ДЕЛАТЬ. Дескать, да, оно даже отсинтезируется (в большинстве ПЛИС) всеми правдами и неправдами, но ВАМ ЭТОГО НЕ НАДО, А ЕСЛИ ВЫ СЧИТАЕТЕ, ЧТО ЭТО ВАМ НАДО, ЗНАЧИТ ВЫ ЧТО-ТО НЕ ПОНИМАЕТЕ. Или вы решили подключить память DDR, что означает Double Data Rate, то есть как раз-таки переключение данных как по фронту, так и по спаду (это когда-то позволило по тем же проводам и на той же тактовой частоте передавать вдвое больше данных, что для памяти компьютера очень пользительно), но тогда "не мне вас учить, сами всё знаете". Хотя сейчас и в этой ситуации скорее посоветуют приобрести IP-блок, заточенный под общение с требуемым устройством, а дальше общение с этим IP-блоком уже будет происходить по фронтам.

Я сам до недавнего времени пребывал в заблуждении относительно того, как работают триггеры внутри ПЛИС, и на основе этого заблуждения приходил к выводу, что "смешивать" фронты и спады - вещь нетривиальная, и лучше этого не делать.

Но на этой неделе наконец-то разобрался, и обнаружил, что не так уж это и страшно, и иногда позволяет решить задачу проще и элегантнее. Обеспечить все тайминги OLED-экранчика, инициализировать его по инструкции и впридачу вывести текст (хранящийся здесь же), и всё это на 16 макроблоках? Легко:




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


Разумеется, начинают всегда с этого друга, RS-триггера на двух элементах И-НЕ:



Пока на оба входа подаётся лог. "1", схема вообще "превращается" в два инвертора, закольцованные друг на друга, а два инвертора - это просто буфер. Когда его вход соединяешь с выходом, у такой штуки два устойчивых состояния: лог "1" и лог "0", и он способен сохранять 1 бит информации. А заменив инверторы на элементы И-НЕ, мы можем установить его (в лог "1") и сбросить (в лог "0").

Дальше к входу этого триггера пририсовывают ещё немножко логики, превращая его в D-триггер-защёлку:



В общем, да, в ТТЛ логике именно так всё и делалось. Базовый элемент ТТЛ-логики (Транзисторно-Транзисторной Логики на биполярных транзисторах NPN) - это именно И-НЕ, он наиболее просто реализуется, и всё остальное стараются построить на нём. Ну и в целом, известный факт, что на И-НЕ можно реализовать вообще любую логику! Как и на ИЛИ-НЕ, а вот на других - проблематично...

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

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



Это не так уж и важно, работают они в итоге одинаково, но в такой схеме разобраться проще! Тут вся сущность этого триггера понятна почти без слов. Когда переключатель стоит в позиции по умолчанию (как нарисовано), выход буфера соединён с его входом, поэтому, если там хранилась единица - она так и останется единицей, хранился нолик - останется ноликом. А вот когда подаёшь clk=1, схема превращается в самый простой буфер: любой входной сигнал дублируется на выход. Важно, что в момент переключения clk назад в ноль схема сохранит самое последнее значение, которое "через него проходило".

Такой триггер имеет много имён у нас и "у них": триггер-защёлка (latch), прозрачный триггер (поскольку при clk=1 пропускает сигнал с входа на выход), триггер, срабатывающий по УРОВНЮ.

Применение у них, безусловно, есть. Скажем, к нам идёт шина адреса и шина данных. Нам назначен адрес. Как только увидели, что данные адресуют НАМ - мы их ЗАЩЁЛКИВАЕМ ровно таким триггером, и где-то их используем. Хотя бы светодиодики включаем...

Но как только мы пытаемся реализовать более сложную логику, у нас возникает желание "соединить выход со входом". Что такое счётчик - он берёт старое своё значение, прибавляет к нему единичку - и сохраняет новое значение. Хотя бы даже простейший однобитный счётчик (который на одном такте переключается из нуля в единицу, на следующем - из единицы в ноль, и так далее) на триггере-защёлке исполнить невозможно! Это выглядело бы примерно так, к выходу триггера подключили инвертор, и его выход подали на вход:



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

Самым универсальным и надёжным способом избавиться от такого нехорошего поведения оказалось соединение двух защёлок друг за другом:



Как-то так вышло, что я даже не пытался вникать в подробности, как оно работает, ОБЩИЙ СМЫСЛ был ясен. Один продолжает выдавать старое значение, в другой мы заносим преобразованное значение, а затем второй защёлкиваем в первый, и вот тогда, наконец, всё заработает!

Можно сравнить со шлюзовой камерой: нельзя открывать СКВОЗНОЙ ПРОХОД, он соединит входы и выходы комбинаторной логики! Поэтому такой триггер уже НЕПРОЗРАЧНЫЙ.

А раз уж тактовая частота называется clock, он же clk, т.е "часы", то ещё можно вспомнить анкер:



Ведь его назначение примерно то же самое: обеспечить КОНТРОЛИРУЕМОЕ приращение на одну позицию на каждый "период тактовой частоты", в данном случае её обеспечивает маятник. Правда, анкер участвует ещё и в генерации самих колебаний, сообщая маятнику энергию от спускающегося грузика. Но смысл в том, что с одной собачкой мы бы не справились, стоило бы ей отойти, как храповик начал бы неконтролируемо разгоняться!

У такого триггера тоже множество названий. "У них" его называют Flip-Flop или просто FF, а ещё это триггер СРАБАТЫВАЮЩИЙ ПО ФРОНТУ (а не по уровню), и этот факт подчёркивают на схемах, рисуя треугольничек у вывода clk. Ещё его могут назвать двухступенчатым триггером, или триггером "мастер-помощник", а "у них" это было master-slave, но сейчас, наверное, уже нет. ВСЕ ТРИГГЕРЫ В ПЛИС ИМЕННО ТАКИЕ! Хотя половинку триггера можно отключить, и использовать как защёлку, но довольно редко это нужно.

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

Но если мы наконец-то посмотрим внимательно, как такой триггер работает, то обнаружим, всё проще:




Итак, только начинается clk=0. Ключи в двухступенчатом триггере, как изображено сверху рисунка. Первая половинка выступает буфером, пропуская всё "насквозь". Идут переходные процессы (распространение сигналов по комбинаторной логике, которая бывает весьма и весьма протяжённое), и пускай идут. Главное, чтобы к фронту они все уже завершились. Именно когда происходит переключение в clk=1, последнее значение на входе "защёлкивается" в первой половинке триггера.

Дальше смотрим вниз рисунка. Значение хранится в первой половинке триггера, а вторая половинка уже "забыла" старое значение и сейчас ПРОСТО ТРАНСЛИРУЕТ НОВОЕ ЗНАЧЕНИЕ! Вот этот факт я в своё время не уяснил, что правильное значение на выходе появляется буквально через несколько наносекунд после фронта тактовой частоты! Спад тактового импульса выполняет очень важную роль - в этот момент хранение данных передаётся с первой на вторую половинку триггера, но НА ВЫХОДЕ ЭТОГО ВООБЩЕ НЕ ЗАМЕТНО!

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

Обещанный пример использования: подключение OLED-экранчика winstar weh001602, хотя интерфейс практически один-в-один и с ЖК-экранчиками, см. QuatCore: подключаем ЖК-экранчик. Идёт 8 бит данных DB0..DB7 (data bit), плюс 1 бит RS выбора между данными и командами, плюс выбор RW "чтение/запись" (который мы опять заземлили, чтобы всегда была запись) и ещё один провод E - "строб". Вот тайминги, которые мы должны соблюдать:




"Алгоритм" примерно такой:
1. устанавливаем RS,
2. через заданный интервал (или дольше) ставим E=1,
3. устанавливаем данные DB0..DB7,
4. через заданный интервал (или дольше) ставим E=0,
5. выдерживаем ещё некоторую паузу, и только после этого можем устанавливать новые значения RS и DB0..DB7.

Паузы там совсем небольшие, по 10..20 нс, но НАМ ОТ ЭТОГО НЕ ЛЕГЧЕ. Это означает, что КАКУЮ-ТО ЗАДЕРЖКУ МЫ ВНЕСТИ ОБЯЗАНЫ!

классический подход: работать строго по тактам. Первый такт: загрузили RS и DB0..DB7, ждём. Второй такт: переключили E=1. Третий такт: переключили E=0. И далее по накатанной. Тут уже впору целый конечный автомат припахать, с тремя состояниями, я такие реализации видел :)

А минимальный отрезок, который мы можем отсчитать - это половина такта. Т.е, мы можем:
1. по фронту clk установить RS и тут же DB0..DB7, "чтобы два раза не ходить",
2. спустя полтакта (по спаду clk) установить E=1,
3. ещё спустя полтакта снять E=0. Это уже следующий фронт clk, и по нему мы устанавливать RS и DB0..DB7 НЕ ИМЕЕМ ПРАВА. Значит, в один такт мы не вписались, НУЖНО МИНИМУМ 2 ТАКТА!
4. выжидаем ещё целый такт - и начинаем всё сначала!

Этого можно было бы достичь, объединяя по AND значение регистра и тактовую частоту, и тогда мы всё сможем сделать исключительно по фронту:

module stupidLCDinterface (input clk, output reg ena = 1'b0, output E, output reg [addrWidth-1:0] adr = {addrWidth{1'b0}});

parameter addrWidth = 4;
parameter TerminalCount = 15;

wire cout = (adr == TerminalCount);
always @(posedge clk) if (~ena & ~cout)
adr <= adr + 1'b1;

always @(posedge clk) if (~cout)
ena <= ~ena;

assign E = ena & (~clk);

endmodule

На симуляции:



Как будто бы нас всё устраивает, но:
- мы истратили 1 ЛЭ на регистр ena и ещё один ЛЭ, чтобы объединить его выход с clk,
- рискованно подавать на выход комбинаторный сигнал, в нём может быть дребезг. Здесь нам на симуляции его не показали:




Здесь первым переключился clk, а только потом ena. Если бы сигнал clk "запоздал" к логическому элементу AND, было бы хреново, возник бы очень короткий пик, и кто знает, вдруг экранчик среагировал бы на него. Хотя, именно цепи clk должны быть самыми шустрыми в ПЛИС, так что как будто бы всё нормально идёт. Но переживать за эти считанные наносекунды как-то совсем не хочется.

Благо, есть решение, лучшее по всем показателям! Вот оно:

module LCDinterface (input clk, output reg E = 1'b0, output reg [addrWidth-1:0] adr = {addrWidth{1'b0}});

parameter addrWidth = 4;
parameter TerminalCount = 15;

wire cout = (adr == TerminalCount);
always @(posedge clk) if (~E & ~cout)
adr <= adr + 1'b1;

always @(negedge clk) if (~cout)
E <= ~E;

endmodule

И работа на симуляции:



Во-первых, это красиво :)

Выход строба E теперь регистровый, так что точно никакого дребезга. И весь этот модуль занимает 5 ЛЭ. 4 из них - счётчик от 0 до 15, это чтобы отправить последовательность байт из "ПЗУ". И пятый - этот самый E, причём от него также уйти было невозможно. Если есть ножка, значит, и логический элемент должен быть, который ей управляет! То есть это абсолютный минимум, на который вообще можно было рассчитывать!

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

Наконец, представлю своё "ПЗУ":

module HelloBigWolf (input [addrWidth-1:0] adr, output [7:0] Q, output RS);

parameter addrWidth = 5;

assign Q = (adr == 5'h00)? 8'b0011_1_0_10 : //function set, 0011 - N - F - FT[1:0]. N=1: 2 строки, F=1: 5x10, F=0: 5x8, FT=10: English_Russian
(adr == 5'h01)? 8'b0011_1_0_10 : //повторим, не будем жадничать (что, хуже что ль станет??)
(adr == 5'h02)? 8'b0000_1_1_10 : //display on/of, 00001-D-C-B. D=1 (включить), C=1 (показать курсор), B=0 (отключить мигание символа над курсором)
(adr == 5'h03)? 8'b000_00001 : //dipslay clear, может подложить свинью из-за 6,2 мс выполнения.
(adr == 5'h04)? 8'b000001_1_0 : //entry mode set, 000001-I/D-SH. I/D=1 (прибавлять единицу к адресу),SH=0 (не сдвигать весь текст при вводе символа)
//с инициализацией вроде и всё! Можно какой-нибудь текст сварганить...
(adr == 5'h05)? 8'hA8 : //"П"
(adr == 5'h06)? "p" :
(adr == 5'h07)? 8'hB0 : //"Ю"
(adr == 5'h08)? 8'hB3 :
(adr == 5'h09)? "e" :
(adr == 5'h0A)? 8'hBF : //"т"
(adr == 5'h0B)? " " :
(adr == 5'h0C)? "B" :
(adr == 5'h0D)? "o" :
(adr == 5'h0E)? 8'hBB : //"л"
(adr == 5'h0F)? 8'hBA : //"к"
(adr == 5'h10)? "y" :
(adr == 5'h11)? "!" :
(adr == 5'h12)? 8'b1100_0000 : //установить курсор на следующую строку
(adr == 5'h13)? "(" :
(adr == 5'h14)? "o" :
(adr == 5'h15)? 8'hBF : //"т"
(adr == 5'h16)? " " :
(adr == 5'h17)? "E" :
(adr == 5'h18)? "P" :
(adr == 5'h19)? "M" :
(adr == 5'h1A)? "3" :
(adr == 5'h1B)? "0" :
(adr == 5'h1C)? "3" :
(adr == 5'h1D)? "2" :
")"; //1E. А 1F уже никак.
assign RS = (adr > 5'h04)&(adr != 5'h12);

endmodule

И всё вместе: генератор зла + светодиодик, и пока, для наглядности, тактовую частоту берём прямо с него, поразительные 0,6 герца:



Инвертор поставил ровно для того, чтобы "ноль" не пропустить, передать тоже. Смотрим на симуляции (только не на 0,6 Гц всё же-таки, это было бы издевательством):



Синтезируется это дело ровно в 32 макроблока из 32 возможных, заполнение 100% :) 1 макроблок мы используем для генерации тактовой частоты. Ещё 14 - это делитель частоты для светодиода. Ещё 1 мы пока что впустую истратили на ножку RW. Она тупо "заземлена", но CPLD так устроены, макроблок на это всё равно тратится! Так что на хранение байтов в нашей самопальной "ПЗУ" и на всю логику инициализации и формирования сигналов ушло ровно 16 макроблоков, как я и обещал!

Наконец, покажу, как оно работает на 0,6 Гц, это интересно:

image Click to view



(тут ещё не весь текст я ввёл. Как все 2 строчки набираются, тоже заснял, но уже стемнело к тому времени, уж больно насыщаться начало, не так красиво)

Каждая вспышка светодиода - это СПАД тактовой частоты. Можно прямо проследить, как экранчик реагирует на каждую команду. Сначала "молчит", потом появляется "мусор", потом (на очистке экрана) он пропадает. И наконец, ввод каждого символа проходит В ДВА ЭТАПА. Когда мы только определили, что будем выдавать символ, а не команду (т.е RS = 1) и выставили E=1, экран уже на это реагирует, сдвигая курсор! А когда мы выставляем E=0, уже защёлкивается очередной символ!

Мне кажется, формирование правильных таймингов "малой кровью" - это не единственное применение отрицательного фронта, но сходу назвать другие применения пока не могу, не успел об этом громко подумать, разработать "философию".

Poll Зазеркалье отрицательных фронтов

странные девайсы, безумный MAX3032, ПЛИС, работа

Previous post Next post
Up