На работе периодически приходится делать какие-то приборы, для которых заказываются печатные платы. Обычно минимальный заказ плат – это 5 или 10 шт. А приборы чаще всего делаются в единственном экземпляре. Поэтому со временем накапливаются лишние печатные платы. Большинство из них разведены для очень специфических схем и применить их с пользой очень сложно. Но иногда попадаются платы, которые можно куда-то приспособить. Один из таких случаев и решил описать.
От очередного прибора остался комплект из нескольких плат. Часть из них очень специальные и годятся разве что для раздувания костров. Но нашлось и несколько довольно универсальных плат. Одна из них предназначена для установки микросхемы FPGA Altera Cyclone II. Большое количество ножек FPGA разведено на разъемы, есть место для тактового генератора и стабилизатора LD1117S12TR на 1.2 В для питания ядра FPGA. Короче говоря, эта плата довольно сильно похожа на универсальную отладочную плату.
Еще одна плата предназначена для установки микроконтроллера (далее МК) типа STM32F103, многие ножки которого тоже разведены на разъемы. На ней нет почти ничего лишнего, зато есть всё необходимое: разъем SWD, разъем UART. И что самое главное, в эту плату вторым этажом может вставляться плата с FPGA.
Вместе две этих платы представляют отличную основу для разнообразных конструкций. Есть еще и третья плата с разъемами SMA и буферными логическими микросхемами. На ней разведены 4 логических входа и 4 логических выхода.
И тут как раз возникла необходимость в 4-канальном генераторе импульсов, в качестве которого до этого использовались два готовых 2-канальных генератора сигналов произвольной формы (AWG), плюс еще третий генератор для внешнего запуска. Вся эта связка была крайне неудобна в использовании.
Найденные платы вполне позволяют построить программируемый четырехканальный генератор импульсов. Кроме выходного разъема, кждый канал будет иметь еще и входной разъем для внешнего запуска. Остается добавить лишь блок питания и плату интерфейса USB. Прибор должен управляться от компьютера, а это сильно упрощает задачу по сравнению с созданием автономного прибора с собственным дисплеем и управлением.
Плата интерфейса USB тоже нашлась готовая от другого проекта, который делал еще раньше. Этих маленьких платок в тот раз было изготовлено с большим избытком, поэтому ставил их много куда. На плате размещен разъем USB-B, микросхема FT232RL и цифровой изолятор ADuM1201. Гальваническая развязка интерфейса очень желательна для измерительных приборов, всегда ее делаю. Если обмен медленный, то для этого подходят дешевые оптроны. Что касается микросхемы от FTDI, то она мне нравится беспроблемностью драйверов, о которых совершенно не нужно заботиться. Хотя, конечно, удовольствие не из дешевых. Было бы выгодней сделать плату на чем-то вроде CH340E + CA-IS3722 (или NSi8221), возможно, в будущем попробую.
Блок питания (БП) решил не делать полноценный сетевой, а обойтись внешним адаптером с выходным напряжением 9 или 12 В. Для питания плат требуется напряжение +3.3 В, еще как опция может быть использовано +5 В для выходных буферов. Подходящей печатной платы для БП не нашлось. Можно было сделать БП на куске макетки, но я заморочился и развел специальную плату. На ней установил стандартный разъем питания, самовосстанавливающийся предохранитель от пожара, диод от переполюсовки, разъем для выключателя питания, входной конденсатор, стабилизатор +5 В (LM2940CT-5.0) и стабилизатор +3.3 В (LF33CV). Оба стабилизатора имеют землю на корпусе, что позволило их прикрутить к шасси без прокладок.
С шасси оказалось сложнее всего – найти подходящий кусок достаточно толстого дюраля (порядка 3 мм) не удалось, пришлось взять какой-то страшно дырявый кусок, который когда-то был частью какого-то прибора. Потом его кто-то для чего-то использовал, и не раз, сверля и прогрызая все новые дырки. Нужные мне отверстия с резьбой М3 для стоек плат с большим трудом удалось разместить между имеющимися дырками. По-другому их не назвать. Чтобы хоть немного исправить эстетику, прошелся сверлом, разверткой и зенковкой, сделав из дырок вполне аккуратные круглые отверстия. Но в идеале их тут быть вообще не должно.
Платы спаял не полностью, а установил только то, что будет использоваться. Множество отверстий остались пустыми – лишние разъемы и другие детали устанавливать ни к чему. На плате входов-выходов в качестве входных буферов поставил 74LVC125, которые при питании 3.3 В могут работать с входными сигналами 3.3 или 5 В. На входе разведена защита на диодах BAS70. Входное сопротивление не делал 50 Ом, чтобы не просаживать уровень сигнала (источник сигнала может оказаться маломощным). Вместо этого сделал 10 кОм. Если будут подаваться быстрые сигналы, то наверняка на стороне источника будет согласование с кабелем 50 Ом. А согласования с одной стороны вполне достаточно, особенно когда кабели не очень длинные.
В качестве выходных буферов поставил 74ABT245, на каждый выходной разъем параллельно работают по 4 выхода, выходное сопротивление резисторами доведено до 50 Ом. Серия ABT является BiCMOS, микросхемы очень быстрые, но из-за биполярного выходного каскада уровень логической единицы заметно ниже питания. Зря выбрал эту серию, надо было ставить 74ACT. Может потом поменяю.
Согласно грубой прикидке, все эти платы влезут в корпус G765 тайваньского производителя
Gainta. Такой корпус (версия «A», с алюминиевыми панелями) был специально куплен. К слову, алюминиевый сплав, из которого сделаны панели, очень мягкий и вязкий, обрабатывается крайне плохо.
Не влезала лишь плата входов-выходов, но на ней можно без особых последствий обрезать выступ, на котором расположен светодиод.
Компоновку плат в корпусе по-быстрому прикинул в CorelDraw. Рисованием в 3D не владею, да и не надо здесь это, только лишняя трата времени.
Самое главное – дизайн панелей. С этого начинается любой прибор. Пока нет дизайна, который устраивает, нельзя дать добро на применение корпуса. Но на сей раз всё вполне нормально вписалось в панели, хоть платы разрабатывались не для этого корпуса. На передней панели нарисовал логотип института, из отходов деятельности которого и делается этот прибор.
Осталось решить проблему с платой входов-выходов, которая в этот корпус не влезает. Ее надо не только обрезать, но еще и перенести на ней в другое место светодиод. А также разместить выключатель питания, который раньше вообще не был предусмотрен. Естественно, при проработке дизайна панели было учтено, где именно можно разместить эти компоненты. Для выключателя выпилил в плате паз (на этом месте ничего не было, только полигон земли), а для светодиода просверлил отверстия на краю площадки крепежного отверстия, которое здесь не используется. Площадку разрезал на два части, к которым теперь можно припаять выводы светодиода.
Выключатель питания пришлось несколько доработать. Дело в том, что отверстие для другого крепежного уха не помещалось на плату. Пришлось ухо удалить, оставив лишь небольшой усик, который будет входить в маленькое отверстие в плате и защищать выключатель от поворота.
В смонтированном виде это выглядит так:
Теперь плата влезает в корпус. Выключатель со светодиодом тоже на месте.
Следующий шаг – установка задней панели. Решил ее прикрепить к шасси с помощью уголка, так удобней. Винты сзади, на мой взгляд, допустимы. Хотя для кого-то с более строгим подходом к внешнему виду они могут показаться неприемлемыми. Тогда от них можно отказаться, панель и без этого нормально держится в пазах корпуса.
Еще одну неприемлемую с точки зрения эстетов вещь я сделал с нижней половинкой корпуса. Шасси внутри корпуса крепится с помощью саморезов к пластиковым стойкам, которые отлиты в корпусе. Но эти стойки тонкие и низкие, особой прочностью не отличаются. Допускают применение лишь коротких саморезов. Я же обычно добавляю к высоте этих стоек еще и толщину днища, для чего сверлю в стойках сквозные отверстия. Тогда можно применить более длинные саморезы, но снизу появляются четыре маленьких лишних дырочки…
В общем, блок собран.
Но чего-то не хватает: наклеек панелей. В простейшем случае печатаю наклейки на лазерном принтере на самоклеющейся бумаге, затем ламинирую холодным способом шагреневым ламинатом. Но гораздо лучше будут выглядеть наклейки, напечатанные рекламщиками на пленке ПВХ (например, Oracal 641). Они также могут ламинировать наклейку матовым ламинатом, а потом вырезать плоттером все необходимые отверстия. Стоит это очень дешево, для них это смешные объемы по сравнению с какой-нибудь внешней рекламой на здании или троллейбусе. Печать может быть полноцветной (вообще, печать всегда идет в цвете, даже черный цвет – составной), но для электронных приборов я цветом не злоупотребляю. Не вижу пока в себе сил сделать цветной дизайн панели, чтобы он не выглядел кустарно. Может быть, когда-нибудь научусь, но пока – нет.
Вот так панели выглядят с наклейками:
На этом аппаратная часть готова. Но прибор пока мертв. Чтобы вдохнуть в него жизнь, надо добавить программную часть. К сожалению, прошли времена, когда достаточно было спаять плату, и ее можно сразу же включать. Теперь постоянно приходится заниматься долгим и нудным программированием, без которого не сделать ничего приличного. Крайне неприятное занятие, не зря программистам так много платят.
Прежде всего нужно реализовать всю необходимую логику внутри FPGA. Поскольку строим генератор, то надо каким-то образом получить периодическую последовательность импульсов. Для этого можно использовать два подхода. Самый простейший – это программируемый делитель частоты. Коэффициент деления представляет собой целое число, он задает, сколько периодов опорной частоты будет в одном периоде выходной частоты. Т.е. фактически задается период. Чем меньше коэффициент деления, тем более грубой получается перестройка.
Есть другой подход, когда целое число задает частоту повторения выходных импульсов. При этом шаг изменения частоты всегда постоянен. Более того, он может быть сделан сколь угодно малым. Такой метод использует вместо счетчика сумматор. Называется это NCO (Numerically Controlled Oscillator) и используется обычно внутри прямых цифровых синтезаторов (DDS, Direct Digital Synthesizer). Однако чудес не бывает, такой метод обеспечивает лишь заданную среднюю частоту импульсов на некотором интервале времени. В то же время два соседних периода могут отличаться на величину периода опорного сигнала. Такой джиттер практически незаметен при генерации низких частот, но при высокой выходной частоте может стать неприемлемым.
Вообще говоря, чудо возможно, но требует дополнительных затрат. Если взять полноценный DDS с синусоидальным выходом, тщательно отфильтровать выходной сигнал от побочных компонентов, затем подать синус на компаратор, то на выходе получим меандр с низким джиттером и малым шагом перестройки по частоте. Но тут это не надо. Основной режим работы будет – обычный делитель частоты (DIV), а NCO может понадобиться лишь для каких-то отдельных задач. Допустим, можно сгенерировать меандр в некоторой полосе частот возле 10.7 МГц и с его помощью настроить ПЧ-тракт ЧМ-приемника.
Для получения широкого диапазона частот генератора выбрал разрядность счетчика (и сумматора для NCO) равной 32 битам. Для схем с такими счетчиками и прочей логикой максимальная тактовая частота достигает 100 МГц даже для чипа FPGA с самым плохим speed grade. Поэтому выбрал частоту опорного генератора 100 МГц. В результате в режиме делителя частоты оказалось возможным получить период сигнала от 20 нс до почти 43 с. В режиме NCO шаг частоты составил 100 МГц / 2^32 = 0.023 Гц. Это и есть минимальная выходная частота, ну а максимальная – половина от тактовой, т.е. 50 МГц. Пределы вполне достаточные для большинства практических нужд.
Тип задающего генератора (NCO или DIV) можно переключать с помощью бита в регистре режима (в данном случае это ModeReg.5). Две приведенных выше схемы на AHDL выглядят так:
PerCnt[i][].(clk, clrn) = (GCLK, nGCLR);
IF ModeReg[i][3]
THEN -- NCO mode:
PerOvf[i] = PerCnt[i][RES - 1];
PerCnt[i][] = PerCnt[i][] + PerReg[i][];
ELSE -- Auto mode:
PerOvf[i] = PerCnt[i][] >= PerReg[i][];
IF PerOvf[i]
THEN PerCnt[i][] = 0;
ELSE PerCnt[i][] = PerCnt[i][] + 1;
END IF;
END IF;
PerEnd[i] = DFF(PerOvf[i], GCLK, nGCLR, );
ASync[i] = DFF(PerOvf[i] & !PerEnd[i], GCLK, nGCLR, );
Чтобы задавать рабочую частоту, надо как-то загружать коды в регистр AutoReg[]. В простейшем случае код можно задавать в параллельном виде, используя группу ножек FPGA. Но при такой большой разрядности это очень громоздко. Поскольку быстрой передачи данных тут не требуется, то можно использовать последовательный обмен с МК. Проще всего реализовать порт SPI. Для него потребуются 4 линии: SCK, MOSI, MISO и NSS (Slave Select с низким активным уровнем). Данные передаются от МК по линии MOSI, дальше они вдвигаются в сдвиговый регистр по тактовому сигналу SCK, а по окончанию приема будут параллельно перегружаться в нужный регистр генератора.
Поскольку регистров управления будет много, их надо как-то адресовать. Адрес можно передавать в той же последовательной посылке, что и данные. Допустим, сначала можно передать 8 бит адреса, а затем 32 бита данных.
Обмен данными по SPI может быть двунаправленным. В качестве признака чтения данных можно использовать один из битов адреса (допустим, старший). Если он равен единице, значит МК хочет прочитать данные. Тогда сразу после приема адреса в сдвиговый регистр надо загрузить значение из одного из регистров генератора. Следующими импульсами SCK данные будут сдвигаться и выводиться на линию MISO.
Полярность и фаза сигнала SPI могут быть разными, обычно встроенный порт SPI микроконтроллеров позволяет это настраивать. В данном случае порт SPI будет использоваться не только для обмена данными, но и для загрузки конфигурации в FPGA (в режиме PS – passive serial). Поэтому желательно использовать одинаковые настройки SPI во всех случаях. При конфигурировании FPGA в режиме PS данные защелкиваются по фронту SCK, а между посылками сигнал SCK находится в состоянии низкого уровня. Это соответствует настройкам SPI микроконтроллера CPOL = 0, CPHA = 0.
Принимаемые данные должны защелкиваться по фронту сигнала SCK, а передаваемые должны выдаваться на линию MISO по спаду SCK. Поэтому для приема и передачи я использую отдельные сдвиговые регистры, один из которых тактируется прямым сигналом SCK, а второй – инверсным.
Адрес и данные принимаются отдельными регистрами. Когда данные приняты, формируется сигнал записи WR, по которому данные можно перезаписывать в регистры генератора. Адрес дешифрируется, на его основе формируется столько сигналов WRn, сколько нужно. Чтобы понимать, когда заканчивается прием адреса и данных, тактовые импульсы на входе SCK подсчитываются с помощью счетчика. Для первых 8-ми импульсов разрешена работа сдвигового регистра адреса, для последующих 32-х – регистра данных. Если в принятом адресе старший бит равен 1, то вместо сигналов записи формируются сигналы чтения RDn. По этим сигналам данные из выбранного регистра генератора копируются сначала в буферный регистр, а потом по спаду сигнала SCK записываются в выходной сдвиговый регистр и начинают сдвигаться. Они выводятся на линию MISO через буфер с тремя состояниями выхода.
Логика порта SPI тактируется сигналом SCK, в то время как вся остальная схема генератора тактируется сигналом GCLK частотой 100 МГц. Это таит в себе опасность под именем «метастабильность».
При записи данных в любой регистр должны быть выдержаны некоторые соотношения для входных сигналов по времени. Во-первых, длительность нуля и единицы тактового сигнала не может быть меньше определенного значения. Это означает выбор тактовой частоты не выше некоторого предела. Во-вторых, данные должны поступать на вход заранее, до фронта тактового сигнала должно быть не менее времени установления (Tsu, setup time). Сниматься данные тоже должны с задержкой, которая называется временем удержания (Th, hold time). Но как выдержать эти требования по времени, если данные приходят синхронно с одним тактовым сигналом, а сам регистр тактируется другим? Это называется разные клок-домены (Clock Domains). Есть отдельная обширная тема под названием CDC (Clock Domain Crossing), где рассматриваются способы безопасной передачи данных из одного клок-домена в другой.
Что будет, если не принимать никаких мер? Тогда данные, приходящие в случайный момент времени относительно тактового сигнала регистра, иногда будут попадать в интервал времени Tsu + Th. В этом случае триггеры регистра могут оказаться в состоянии метастабильности. Конкретное проявление этого эффекта зависит от технологии чипа, один из вариантов проявления показан на осциллограмме (найдена в Интернете):
Как видно, выход триггера некоторое время находится в «сером» состоянии между единицей и нулем, а затем переходит в одно из устойчивых состояний. При этом часть триггеров регистра может защелкнуть правильные данные, а часть – ошибочные. Двоичный код числа может быть сильно искажен. Вероятность появления метастабильности не такая и маленькая. Если не принимать никаких мер, то при циклической загрузке кода частоты со стороны МК можно легко наблюдать сбои в работе генератора. Такой прибор, как говорится, только «ф топку», ну или в клок-доменную печь :)
Как бороться с метастабильностью? Для этого переход сигнала из одного клок-домена в другой делают с помощью синхронизатора (Synchronizer). Существует много вариантов реализации синхронизаторов, в простейшем случае это цепочка из двух D-триггеров:
Такой синхронизатор подходит для перехода с домена с более низкой тактовой частотой в домен с более высокой. Это как раз наш случай – порт SPI здесь работает на частоте 16 МГц, а вся остальная схема генератора – на частоте 100 МГц. На AHDL синхронизатор выглядит примерно так:
WrOut = DFF(DFF(WrIn, GCLK, nGCLR, ), GCLK, nGCLR, );
Подобные синхронизаторы используются внутри микроконтроллеров для входных сигналов GPIO, так как внешний мир для МК является другим клок-доменом по отношению к миру внутреннему. Для примера ниже приведен рисунок структуры порта микроконтроллера AVR, желтым выделен синхронизатор:
Забавно, что синхронизатор не устраняет метастабильность полностью, а лишь снижает ее вероятность. Существует расчет среднего времени между отказами (mean time between failures, MTBF), при наличии синхронизатора он дает солидное время, сбоя можем и не дождаться.
Окончательная схема порта SPI приведена ниже. Сигнал глобального сброса не показывал, он тут не принципиален. Для формирования сигналов WR и RD, которые идут в другой домен, использованы синхронизаторы. За одно они еще выполняют функции формирователей коротких импульсов длиной в один период GCLK.
При чтении данные тоже пересекают границу клок-доменов. Она находится между регистрами BuffReg и DoutReg. Но тут метастабильности не возникает, потому что сигнал RD берет свое начало в клок-домене SPI и его максимальная задержка относительно SCK легко считается и не превышает максимально допустимой.
Обычно я схем логики для FPGA не рисую, сейчас нарисовал специально для обзора (карандашом Pentel 120 A3 0.5, а поверх – ErichKrause METRIX liquid ink roller 0.5). Намного проще схемы сразу описывать текстом на языке описания аппаратуры. Весь порт SPI на AHDL выглядит так:
------------------------------- SPI Interface ---------------------------------
SPICnt[].(clk, clrn) = (GSCK, !nSS & nGCLR); -- SPI clocks counter
SPICnt[] = SPICnt[] + 1;
SPIRen = DFFE(vcc, GSCK, !nSS & nGCLR, , SPICnt[] == 7); -- Read enable
SPIWen = DFFE(vcc, GSCK, !nSS & nGCLR, , SPICnt[] == 39); -- Write enable
SPIAddrReg[].(clk, ena, clrn) = (GSCK, !nSS & !SPIRen, nGCLR); -- AddrReg
SPIAddrReg[6..0] = SPIAddrReg[7..1]; -- shift data
SPIAddrReg[7] = MOSI; -- input data
SPIRdWr = SPIAddrReg[7];
SPIDinReg[].(clk, ena, clrn) = (GSCK, !nSS & SPIRen, nGCLR); -- DinReg
SPIDinReg[30..0] = SPIDinReg[31..1]; -- shift data
SPIDinReg[31] = MOSI; -- input data
SPIBuffReg[].(clk, ena, clrn) = (GCLK, SPIRd, nGCLR); -- BuffReg
SPIDoutReg[].(clk, ena, clrn) = (!GSCK, !nSS, nGCLR); -- DoutRegr
IF SPICnt[] == 8
THEN SPIDoutReg[] = SPIBuffReg[]; -- load data
ELSE SPIDoutReg[30..0] = SPIDoutReg[31..1]; -- shift data
END IF;
MISOTri.(in, oe) = (SPIDoutReg[0], !nSS & SPIRdWr); -- output enable
MISO = MISOTri; -- output data
SPIWs = DFF(SPIWen, GCLK, nGCLR, );
SPIWe = DFF(SPIWs, GCLK, , nGCLR);
SPIWr = DFF(SPIWs & !SPIWe, GCLK, nGCLR, ) & !SPIRdWr;
SPIRs = DFF(SPIRen, GCLK, nGCLR, );
SPIRe = DFF(SPIRs, GCLK, , nGCLR);
SPIRd = DFF(SPIRs & !SPIRe, GCLK, nGCLR, ) & SPIRdWr;
FOR i IN 0 TO 13
GENERATE
Wr[i] = SPIWr & SPIAddrReg[6..0] == i;
END GENERATE;
FOR i IN 0 TO 1
GENERATE
Rd[i] = SPIRd & SPIAddrReg[6..0] == i;
END GENERATE;
Когда связь с МК налажена, можно загружать в регистр периода (или частоты) нужное значение, получая выходную частоту в широком диапазоне. Но пока генератор умеет формировать лишь короткие импульсы, следующие с заданной частотой. Чтобы сделать генератор более практичным, надо добавить возможность получения на выходе импульсов произвольной длительности. Для этого потребуется цифровой одновибратор, который будет запускаться короткими импульсами задающего генератора. Схема выглядит примерно так:
С задающего генератора приходит импульс PlsBeg. JK-триггер при этом устанавливается, начинает формироваться выходной импульс. Одновременно начинает инкрементироваться счетчик длительности WidthCnt. Как только код в счетчике становится равным коду регистра длительности импульса WidthReg, формируется импульс конца интервала PlsEnd, который поступает на вход K триггера и сбрасывает его. При этом импульс заканчивается. В реальности сравнение надо делать «больше или равно», чтобы импульс прерывался, если будет загружена меньшая длительность. Иначе есть шанс зависнуть на время максимальной длительности импульса, а это десятки секунд. На AHDL формирователь выглядит так:
WidthCnt[i][].(clk, clrn) = (GCLK, nGCLR);
WidthOvf[i] = WidthCnt[i][] >= WidthReg[i][];
IF PlsBeg[i] # WidthOvf[i] # !Pulse[i]
THEN WidthCnt[i][] = 0;
ELSE WidthCnt[i][] = WidthCnt[i][] + 1;
END IF;
PlsEnd[i] = !PlsBeg[i] & WidthOvf[i] & Pulse[i];
Pulse[i].(j, k, clk, clrn) = (PlsBeg[i], PlsEnd[i], GCLK, nGCLR);
На первый взгляд, схема слишком сложная для простого формирования импульса заданной длительности. Но тут надо учитывать всякие «краевые» эффекты, когда в некоторых условиях возможны глитчи длительностью в один такт. Вернее, это даже не глитчи, а просто некоторые особенности логики работы. Настоящие глитчи – это короткие (короче периода тактового сигнала) импульсы, которые возникают в результате «гонок» сигналов из-за задержек в логических элементах. Тут с таким сталкиваться не приходится, схема генератора полностью синхронная, все триггеры тактируются единым сигналом GCLK. Никакой логики в цепи тактового сигнала нет, для управления используются входы Enable. В результате никакие «гонки» сигналов или промежуточные состояния многоразрядных регистров не беспокоят. Главное, чтобы все сигналы устаканились к следующему тактовому импульсу.
А проблемы могут быть связаны с логикой работы. Например, для получения импульса нужной длительности из стартового и стопового импульса логично использовать JK-триггер, у него как раз есть для этих целей отдельные входы. Но если период и длительность установить такими, что стартовый импульс совпадет со стоповым, триггер переключится в противоположное состояние, хотя по логике он должен установиться (ведь пришел стартовый импульс).
Еще хуже, если стартовые импульсы будут поступать чаще стоповых. Тогда на выходе триггера появятся импульсы хаотичной длительности. Исправить ситуацию можно добавлением логики на вход K триггера, что сделает вход J приоритетным. Как вариант, можно вместо JK-триггера применить D-триггер, но он тоже потребует дополнительной логики.
Это больше вопрос «дуракоустойчивости», ведь в добром здравии никто не будет устанавливать длительность больше периода. Но доброе здравие – это лишь математическая абстракция. В реальности пользователь может вводить что угодно, даже заведомо неправильные комбинации значений. Запрещать ввод таких значений не всегда представляется возможным, может потребоваться слишком много проверок (например, синхронизация каждого из каналов может приходить откуда угодно). Да и пользователю неудобно, когда запрещен ввод каких-то значений (придется сначала увеличивать период, а только затем длительность, наоборот интерфейс сделать не даст). Поэтому лучше просто принять меры, чтобы при неправильных параметрах поведение генератора было как можно более адекватным. К счастью, сделать машину адекватнее человека не так уж сложно. Когда длительность импульса установлена больше периода, логично ожидать, что импульс станет бесконечным, и на выходе все время будет активный уровень. Все подобные ситуации учтены в приведенной выше схеме.
Для еще большей универсальности генератора полезно добавить программируемую задержку между импульсом запуска и началом формирования выходного импульса. Это пригодится в том случае, если будут использоваться сразу несколько каналов. Тогда выходные импульсы можно будет сдвигать относительно друг друга. Формирование задержки аналогично формированию длительности выходного импульса. Опять возникает вопрос насчет поведения при некорректных параметрах. Что должно происходить, если задержка установлена больше периода? Ответ на этот вопрос не совсем очевиден. Я посчитал, что логичней всего в такой ситуации будет отсутствие выходных импульсов. По крайней мере, это намного лучше хаотичных импульсов.
Схема размножается на 4 канала, получается вполне пригодный для использования 4-канальный генератор. Размножение кода в AHDL делается просто, для этого существует конструкция FOR — GENERATE. В приведенном выше коде i – это и есть номер канала.
На плате входов/выходов есть 4 входных разъема. Чтобы они не пропадали даром, их можно задействовать для внешнего запуска генератора. С входами внешней синхронизации такая же ситуация, как и с портом SPI – каждый такой вход представляет собой отдельный клок-домен и требует применения синхронизатора.
Схема синхронизатора очень напоминает схему фильтра импульсных помех. Там тоже цепочка триггеров, выходы которых объединены по логике «И». Такая схема не будет пропускать короткие импульсы, длительность которых меньше такого количества периодов тактовой частоты, сколько имеется триггеров в цепочке. Подобные фильтры помех есть, например, на входах захвата таймеров в микроконтроллерах AVR. Подключая разное число триггеров в цепочке, можно регулировать минимальную длительность импульса, который может пройти через фильтр. На всякий случай сделал здесь такой же фильтр. Мало ли какой сигнал будет поступать на вход внешней синхронизации, нежелательные «иголки» можно таким способом убрать. Включить или отключить фильтр можно с помощью специального бита в регистре режима. Длину цепочки я сделал 6, но можно сделать какой угодно в зависимости от того, что надо фильтровать.
При работе с внешней синхронизацией мне показалось полезным иметь индикацию частоты, которая поступает на синхровход. Полноценный частотомер реализовывать не стал, а сделал просто измеритель периода, который может делать это с точностью 10 нс. Немного подумав, добавил еще и измеритель частоты, который производит счет входных импульсов в заданном окне. Первый способ дает хорошую точность при измерении низких частот, второй – высоких. Их можно объединить, получив постоянную относительную погрешность для любой частоты, но лень тут этим заниматься – не частотомер ведь.
Обмен по SPI сразу предусматривал как запись, так и чтение. Но чтение до этого не использовалось. Какой смысл читать регистры, которые мы сами загружаем, и которые меняться не могут? Но с появлением измерителя частоты операция чтения стала востребованной. И тут случился великий облом – чтение не заработало. Вообще, я в первый раз в STM32 использую встроенный SPI для чтения. До этого приходилось только передавать по нему данные во всякие внешние микросхемы. Хотя на AVR чтение по SPI неоднократно делал. Там все просто: загрузили данные в регистр данных SPI – они начали передаваться. Как только передались – можно читать регистр, в нем будут принятые данные. По наивности думал, что и в STM32 так же, но не тут-то было. Из SPI читался всякий бред. В который раз получаю доказательство, что STM32 – это не радиолюбительский процессор, с ним всегда куча проблем. Потратил на вылавливание глюка два дня и две ночи (с перерывами на обед, разумеется). Сначала не было уверенности, что проблема именно в STM. Предполагал, что FPGA неправильно отдает данные, ведь SPI в ней самодельный и тоже может содержать ошибки. На моем осциллографе это увидеть крайне сложно, тот редкий случай, когда цифровой осциллограф был бы полезен. Но такое случается не чаще, чем раз в год, поэтому бежать и покупать его вряд ли имеет смысл. Начал писать всякие тесты, параллельно модифицируя код и в STM, и в FPGA. Вроде, на осциллографе смутно вижу, что данные идут правильные. Чтобы окончательно в этом убедиться, временно подключил макетную плату с ATmega88. Конечно же, она приняла данные абсолютно корректно. Получается, собака зарыта именно в STM. С помощью параметрического программирования (когда изменения кода там и тут делаются без всякого понимания, а наблюдается лишь конечный результат) был найден рабочий вариант исходника. Кривой, но рабочий. Так и оставил, выдохнув и разжав кулаки.
В итоге имеем 4 канала автогенераторов, способных генерировать заданную частоту и 4 канала внешней синхронизации. Для большей гибкости, можно подключать каждый выходной канал генератора к любому источнику синхронизации. Можно все каналы к одному, можно к разным. Для еще большей гибкости сигнал внешней синхронизации может иметь активный фронт или активный спад. Или фронт и спад сразу. Для выбора источника синхронизации сделал мультиплексор, который управляется битами регистра режима.
В принципе, на этом можно и остановиться, реализован довольно богатый набор функций для генератора прямоугольных импульсов. Но задача, с которой пришлось столкнуться на работе, требовала большего. Вместо генерации отдельных импульсов с некоторой частотой повторения, должны генерироваться последовательности импульсов (говоря по-другому, серии, или паттерны). Внутри серии может быть несколько импульсов, разделенных промежутками.
Если дать полную свободу, то для описания серии нужно будет слишком много данных, что усложнит программирование параметров генератора. На практике это не надо (по крайней мере, в моем случае), поэтому свободу лучше ограничить. Вариант, который обычно используется в AWG, где форма записывается в память и потом «проигрывается» из нее, мне подходил меньше всего. Требовалась не совершенно произвольная последовательность выходных импульсов, а последовательность в некой сетке.
Строить серию решил из одинаковых интервалов, которые я назвал «шаг» (Step). Для этого потребовался еще один счетчик и регистр длительности шага. Все импульсы начинаются в начале шага и имеют одинаковую длительность внутри серии. Длительность импульсов задается в регистре длительности, как обычно. Пауза между импульсами – это разность между величиной шага и длительностью импульса.
Сам паттерн хранится в специальном регистре, в начале каждой серии его значение загружается в сдвиговый регистр. Если бит равен единице, то на данном шаге генерируется импульс, если ноль – то импульс пропускается. Если длительность импульса установлена равной длительности шага, а импульсы в серии идут подряд, то они сливаются в один длинный импульс. Количество шагов в серии я ограничил до 32. Без проблем можно сделать и больше, но мне даже столько с избытком. Серии в разных каналах генератора можно сдвигать относительно сигнала синхронизации с дискретностью в 1 период тактовой частоты, тут величина шага ограничений не накладывает. Если задать только 1 импульс в серии, то генератор вырождается в обычный генератор импульсов, тогда величина шага не используется. Схему этой части генератора приводить не вижу смысла, она очень специальная и вряд ли вызовет интерес.
Чтобы дать генератору еще больше возможностей, на выходе реализовал объединение каналов по логике «И» и «ИЛИ». Это позволяет получить совсем странные последовательности на выходе. Эти возможности, скорее всего, не нужны, но в самом начале пути я думал, что просто обойдусь логическими выходными операциями без паттернов. Но паттерны пришлось все-таки добавить, а выходную логику при этом убирать не стал, раз уже написана.
Долго думал, как реализовать интерфейс пользователя для задания паттернов. Решил задавать паттерн в виде строки из 0 и 1, но для большей наглядности еще рисовать на экране, какой выходной сигнал в итоге получится. Цифровые значения (период, длительность, задержка и т.д.) задаются с помощью полей ввода.
Внутри FPGA проект занял 3513 логических элементов (LE), что составляет 76% от полной емкости кристалла. Набравшись наглости, я решил попробовать задействовать PLL внутри FPGA и поднять тактовую частоту. Временной анализатор Quartus-а показывает максимальную частоту для проекта примерно 120 МГц (она немного меняется при каждой перекомпиляции в зависимости от структуры проекта). Можно оптимизировать проект по скорости, но я решил попробовать без этого. Подняв с помощью PLL частоту до 200 МГц, я не обнаружил никаких глюков – все работало как следует, обеспечивая очень даже неплохое разрешение по времени в 5 нс. Нагрев микросхемы FPGA тоже незначительный – она еле теплая. Конечно, в серьезных проектах так делать нельзя (но в несерьезных – можно).
Мой осциллограф имеет слишком узкую полосу пропускания для полноценного наблюдения сигналов этого генератора. Однако по падению отображаемой амплитуды импульса можно определить его длительность, даже если она 10 нс (или даже 5 нс). Ниже показан скриншот экрана осциллографа с одним из сигналов, который был получен с помощью этого генератора:
Встроенный микроконтроллер генератора выполняет лишь примитивные функции: полученные по USB коды загружает в регистры FPGA, а также по запросу читает коды из регистров и передает их по USB. Это позволило почти избежать мучительного процесса его программирования. Еще МК по включению питания загружает конфигурацию в FPGA из своей FLASH. По умолчанию записал туда вариант на 200 МГц, но в случае чего с помощью программы с компьютера можно загрузить обычную, на 100 МГц. В программе предусмотрел загрузку конфигурации FPGA из файла в формате .rbf, который генерирует Quartus. Так что можно держать на диске сколько угодно прошивок FPGA под конкретные задачи.
В настройках программы сделал возможность указания тактовой частоты FPGA, мало ли какие будут еще прошивки.
Все это я делал под конкретную задачу, но получилось довольно универсально. Например, можно «нарисовать» пакет для передачи данных в какое-нибудь устройство по SPI или другому последовательному интерфейсу. При этом можно плавно двигать сигналы относительно друг друга с шагом 5 нс. Что еще можно было бы добавить, так это однократное формирование паттерна при нажатии кнопки. Пока этого нет, но допишу при первой востребованности. Тут и так сделано много лишнего.
Программа работает через DLL, в которой реализовал все функции управления. Сделал так специально, чтобы можно было использовать генератор из среды Matlab.
Мораль всей этой истории – когда разводите печатные платы для работы, разводите их так, чтобы потом можно было использовать для чего-нибудь полезного.
Китайцы умеют из песка делать аккумы.
Осталось это совместить)
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.