Продолжаю описывать свою “беготню по граблям” по мере освоения SoC Xilinx Zynq XC7Z020 с использованием отладочной платы QMTech Bajie Board. В этой статье хотелось бы рассказать, как я решил задачу по настройке тактирования из PS, получению и работе с входными сигналами с кнопок, реализацию примитивного фильтра антидребезга и логического элемента "И" в PL.

Всем интересующимся - добро пожаловать под кат.

Важно! Перед началом повествования, хотелось бы заранее оговориться, что основная цель которую я преследую при написании этой статьи - показать любителям, с чего можно начать, при изучении отладочных плат на базе Zynq. Я не являюсь профессиональным разработчиком под ПЛИС и SoC Zynq и могу допускать какие-либо ошибки в использовании терминологии, использовать не самые оптимальные пути решения задач, etc. Но конструктивная и аргументированная критика только приветствуется. Что ж, поехали…

Постановка задачи

В прошлом уроке я познакомил своих читатателей с отладкой, которую я купил для того, чтобы начать знакомство с Xilinx Zynq, и описал самый примитивный способ моргания светодиодом, который подключен к PL. В этот раз мне захотелось немного усложнить задачу.

Теперь, по задумке, пользователь должен сам решать когда гореть светодиоду, а когда нет.

Итак. Эту возможность мы реализуем за счет кнопок которые мы подключим к ножкам, которые выведены на гребенку JP2. Но простое включение и выключение светодиода по кнопке мне показалось очень скучным и я подумал, что неплохо было бы задействовать что-нибудь из цифровой логики и добавил в постановку задачи наличие логического элемента "И", на вход которого будут подаваться сигналы с двух кнопок, а выход этого логического элемента будет сигнализировать о том, что надо включить или выключить светодиод, с которым мы имели дело в прошлом уроке (D4).

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

Создаём новый проект

Открываем Vivado и создаем новый проект File - Projects - New.

Создаём RTL Project, отметим галку “Do not specify sources at this time”.

Находим интересующий нас SoC, выбираем его и идём дальше.

Видим финальное окно и нажимаем Finish.

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

Перед нами открывается главное окно программы Vivado и мы можем приступать к решению задачи. Первым делом мы создаем Block design и добавляем блок процессорной системы. 

На открывшемся поле нажимаем кнопку “+”  и пишем в поле поиска Zynq…

Добавляем его и видим ZYNQ7 Processing System блок. Кликаем на него два раза и переходим к настройкам процессорной системы. Накидаем нужные нам настройки и пойдем дальше.

Переходим во вкладку PS-PL Configuration и видим настройки интерфейсов взаимодействия программируемой логики и процессорной системы. Отключаем, пока что, не нужный нам GP Master AXI GP0 включенный по умолчанию.

Переходим на вкладку MIO Configuration и смотрим настройки периферии. Отключаем всё ненужное кроме UART1 и выставляем питание Bank 1 в LVCMOS 1.8V.

Переходим в Clock Configuration и настраиваем частоты тактирования и смотрим, что всё считается корректно и нет предупреждений от мастера настройки.

В этом блоке необходимо убедиться, что входная частота равна 33.333333 МГц и включено тактирование для PL Fabric Clocks FCLK_CLK0:

Внимание! В этом месте я словил интересный глюк связанный с настройками локалей в Linux. Если используются русские локали - могут быть проблемы с интерпретацией знака разделяющего дробную и целую части и клоки могут считаться криво. В этом случае нужно переключить все локали на английские и проблема будет пофикшена.

Переходим во вкладку DDR Configuration и настраиваем контроллер оперативной памяти в соответствии с рекомендациями производителя отладки:

В документации к плате можно найти более подробное пояснение по этим настройкам. На этом мы не будем акцентировать внимание. Нажимаем Ok и на зеленой полоске нажимаем Run Block Automation для выполнения автоматизированной настройки рутинных параметров и опций. Оставляем всё без изменений и нажимаем Ok.

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

Подключаем линию тактирования из процессорной системы и сигнал сброса к добавленному блоку Processor System Reset

Сигналы с Processor System Reset мы будем использовать в нашем коде. Покажу этот момент позже.

Следующим шагом добавим нужные нам пины в physical constraints и определим, куда подключить наши кнопки. Переходим в меню Source и добавляем новый constraints-файл.

Нажимаем Create file, пишем ему имя physical_constr и нажимаем Finish.

Обратимся к принципиальной схеме любезно предоставленной производителем и определим какие пины SoC подключены к гребенке JP2 и внесем информацию о том, куда мы подключили наши кнопки.

Я подключу имеющуюся у меня платку изготовленную ЛУТ-ом в незапамятные времена к этой гребенке. Пины питания тоже возьму в этой гребенки. 

Возьму в качестве GND 1-й пин гребенки, в качестве 3.3V 3-й пин, для подключения кнопок буду использовать 5-й (P20) и 6-й (N20) пины. В дополение к этому совет - лучше использовать инверсный сигнал с кнопки, где логический 0 - это высокое напряжение на ножке, а логическая 1 - низкое напряжение. Такой способ подачи сигнала с кнопки выглядит как более помехозащищенный и однозначный.  

Открываем файл physical_constr его и прописываем в него указания для именования пинов и их режим работы:

set_property -dict { PACKAGE_PIN H17 IOSTANDARD LVCMOS33 } [get_ports { led_h17_d4 } ];
set_property -dict { PACKAGE_PIN P20 IOSTANDARD LVCMOS33 } [get_ports { sw1 } ];
set_property -dict { PACKAGE_PIN N20 IOSTANDARD LVCMOS33 } [get_ports { sw2 } ];

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

Можете самостоятельно поисследовать этот пункт меню, для себя вы найдете там много интересного и возможно в последующем откажетесь от набивания constraints-файлов вручную ;)

Перейдем к реализации модулей необходимых для работы нашей схемы.

Модуль “Debouncer” для подавления дребезга контактов

Очень часто для организации взаимодействия с пользователем используются кнопки. При использовании механических кнопок, тумблеров и прочих ключей возникает такое неприятное явление, как дребезг контактов. При дребезге входное значение напряжения идущее от клавиши в момент нажатия или после него может “дребезжать” и быть в нестабильном состоянии, причем вне зависимости от того, насколько “идеально” и как была нажата клавиша. 

Существует несколько способов устранить подобного рода вредное явление, путем использования определенных схемотехнических приемов и непосредственно в ПЛИС. Т.к. кнопки которые я буду использовать, уже имеют соответствующую “обвязку” для подавления дребезга, я усилю эту мощь подавления дребезга используя реализацию такой функциональности внутри ПЛИС. 

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

Общий принцип я изобразил на временной диаграмме:

При знакомстве с типовыми решениями подобных задач, я обратил внимание на два важных момента.

  1. Нажатие кнопки может происходить в случайный момент времени и по сути, относительно того, что происходит во всей остальной ПЛИС - является асинхронным событием. Смена логического состояния на ножке Zynq, к которой подключена кнопка, может совпасть с моментом переключения принимающего триггера и есть вероятность, что триггер остается в неопределенном “нецифровом” метастабильном состоянии. Это может привести к самым интересным и непредсказуемым результатам.

  2. Необходимо однозначно фиксировать факт нажатия генерацией единичного импульса. 

Итак. Приступим к реализации данного модуля и создадим RTL-блок через меню Sources - Add or Create Design Sources. Назовём его debouncer и перейдем к написанию Verilog-кода. Нажимаем Finish и Ok

Двойным кликом открываем в редакторе файл debouncer.v:

Первым шагом определимся с портами ввода/вывода из данного RTL-элемента:

  • Clock Input. В схеме обозначим его как clk_i. Это будет входной порт тактового сигнала для обеспечения синхронной работы со всей остальной схемой.

  • Reset. Его назовём rst_n. Будет использоваться для асинхронного сброса. Активен при low. 

  • Switch button in. Назовём его sw_i. Этот порт задействуем в качестве входа для сигнала от кнопки. 

  • Switch button negative edge. Название у него будет sw_down_o. Этот порт будет генерировать единичный импульс, который будет указывать нам на то, что кнопка перешла из разомкнутого состояния в нажатое - “нажал кнопку”.

  • Switch button positive edge. Этот порт назовём sw_up_o. Этот порт генерирует одиночный импульс на переход от нажатого состояния в разомкнутое - “отпустил кнопку”.

  • Switch button state. Этот порт назовём sw_state_o. Из данного порта будет формироваться фильтрованный сигнал который будет сообщать о том, нажата клавиша кнопки или нет. Сигнал может быть использован в т.ч. для отсчёта времени нажатия кнопки.

Запишем эти определения в листинг модуля:

module debouncer

// Порты
(
    input clk_i,               // Clock input
    input rst_i,               // Reset input
    input sw_i,                // Switch input

    output reg sw_state_o,     // Switch button state
    output reg sw_down_o,      // Switch button negative edge pulse
    output reg sw_up_o         // Switch button positive edge pulse
);

endmodule

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

reg     [1:0] sw_r;
always @ (posedge rst_i or posedge clk_i)
if (~rst_i)
		sw_r   	<= 2'b00;
else
		sw_r    <= {sw_r[0], ~sw_i};

Оба триггера входят в состав регистра sw_r. С каждым тактом на линии clk_i уровень с линии sw_i защелкивается с инверсией в триггер sw_r[0], а предыдущее содержимое sw_r[0] попадает в sw_r[1]. После этого выход триггера sw_r[1] можно считать синхронным относительно clk_i. И в случае если приходит сигнал сброса - мы обнуляем sw_r. Ниже на рисунке показана временная диаграмма работы цепочки данных триггеров.

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

Построим алгоритм данного поведения:

  1. Состояние линии sw_i изменилось, следовательно изменилось состояние sw_r[1];

  2. Запускаем таймер, с отслеживанием состояния входного состояния;

  3. Если в течение определенного времени состояние не изменялось - состояние считаем устойчивым;

Введем триггер sw_state_r, в котором будем хранить последнее стабильное состояние кнопки. Также добавим флаг sw_change_f, который устанавливается в единицу, когда текущее стабильное состояние отличается от того, что установлено на входе sw_i (а точнее sw_r[1]).

wire sw_change_f = (sw_state_o != sw_r[1]);

Флаг sw_change_f будет равняться единице в двух случаях:

  1. Случилось нажатие кнопки;

  2. Случилось ложное срабатывание; 

Для того, чтобы однозначно определить какое событие наступило - запускаем счетчик, и если счётчик успевает досчитать до своего максимального значения и состояние входного сигнала не изменилось - то текущее состояние признаем стабильным и записываем его в sw_state_r. В альтернативном случае - сбрасываем счётчик и оставляем sw_state_r без изменений.

always @(posedge clk_i)    	// Каждый положительный фрон сигнала clk_i
if(sw_change_f)        			// проверяем, состояние на входе sw_i
begin                				// и если оно по прежнему отличается от предыдущего стабильного,
    sw_count <= sw_count + 'd1;  // то счетчик инкрементируется.
    if(sw_cnt_max)               // Счетчик достиг максимального значения.
        sw_state_o <= ~sw_state_o;    // Фиксируем смену состояний.
end
else                  // А вот если, состояние опять равно зафиксированному стабильному, 
	sw_count <= 0;  		// то обнуляем счет. Было ложное срабатывание. 

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

Добавим параметр разрядности нашего счетчика:

module debouncer

//! Параметры
#(
    parameter		CNT_WIDTH = 16
)

И определим в коде параметры счетчика:

reg [CNT_WIDTH-1:0] sw_count;
wire sw_cnt_max = &sw_count;

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

И так же диаграмма для ложного срабатывания:

Следующим шагом необходимо добавить в блоки always асинхронный сброс для всех элементов включающих в себя память и защелкнуть сигналы на выходные триггеры:

always @(posedge clk_i)
begin
	sw_down_o <= sw_change_f & sw_cnt_max & ~sw_state_o;
	sw_up_o   <= sw_change_f & sw_cnt_max &  sw_state_o;
end

Итоговый код debouncer.v получился следующим:

`timescale 1ns / 1ps

module debouncer

// Параметры
#(
    parameter CNT_WIDTH = 16 	// Разрядность счётчика
)

// Порты
(
input clk_i,                // Clock input
input rst_i,                // Reset input
input sw_i,                 // Switch input
 
output reg sw_state_o,  	    // Состояние нажатия клавиши
output reg sw_down_o,        // Импульс “кнопка нажата”
output reg sw_up_o           // Импульс “кнопка отпущена”
);

    reg [1:0] sw_r;                    // Триггер для исключения метастабильных состояний
    always @ (negedge rst_i or posedge clk_i)           
        if (~rst_i)
            sw_r   	<= 2'b00;
        else
            sw_r    <= {sw_r[0], ~sw_i};
        
        
    reg [CNT_WIDTH-1:0] sw_count;       // Счетчик для фиксации состояния
        
    wire sw_change_f = (sw_state_o != sw_r[1]);
    wire sw_cnt_max = &sw_count;
    
    
    always @(negedge rst_i or posedge clk_i)            // Каждый положительный фронт сигнала clk_i проверяем, состояние на входе sw_i
        if (~rst_i)
        begin
            sw_count <= 0;
            sw_state_o <= 0;
        end 
        else if(sw_change_f)	                        // И если оно по прежнему отличается от предыдущего стабильного, то счетчик инкрементируется.  
        begin                                                           
            sw_count <= sw_count + 'd1;
                                                                   
            if(sw_cnt_max)                              // Счетчик достиг максимального значения. 
                sw_state_o <= ~sw_state_o;              // Фиксируем смену состояний.    
        end                                                             
        else                                            // А вот если, состояние опять равно зафиксированному стабильному,
            sw_count <= 0;                              // то обнуляем счет. Было ложное срабатывание               

    always @(posedge clk_i)
    begin
        sw_down_o <= sw_change_f & sw_cnt_max & ~sw_state_o;
        sw_up_o <= sw_change_f & sw_cnt_max &  sw_state_o;
    end
                                   
endmodule

AND GATE и LED driver

Таким же образом добавим еще два примитивных модуля: первый будет включать/выключать светодиод и второй реализует функциональность логического элемента “И”. Я думаю, что данный код очень прост и не требует дополнительных пояснений.

and_gate.v:

`timescale 1ns / 1ps

module ang_gate(
    output y,
    input a,b
    
    );
    
    assign y = a & b;
    
endmodule

led_driver.v:

`timescale 1ns / 1ps

module led_driver(
    input clk_i,
    input rst_i,
    input state_i,
    output led_o
    );    
                
    reg r_led; 

    always @ (negedge rst_i or posedge clk_i)
    begin
        if (~rst_i)
        begin
            r_led <= 0;
        end 
        else if(state_i)
        begin
            r_led <= 1'b1;
        end
        else
        begin
            r_led <= 1'b0;
        end
    end
    
    assign led_o = r_led;            
           
endmodule

После этого добавляем модули на Block Design и соединяем их в соответствии с назначением. Добавляем два debouncer-а на схему через нажатие правой кнопки мыши Add module. 

Входы sw_i обоих debouncer-ов делаем внешними через правую кнопку мыши и команду Make external. Прописываем им имена в соответствии с тем, что мы прописали в файле physal_constr

Добавляем на схему модуль led_driver и по тому же принципу делаем ножку led_o внешней:

Добавляем на схему логический элемент “И”, соединяем всю схему т.к. это задумано изначально и подключаем клоки + сигналы ресета ко всем блокам, в которых они задействованы:

После этого обращаемся в боковое меню Sources и нажимаем правой кнопкой на zynq.bd, выбираем Create HDL Wrapper и нажимаем Ok:

 

Дожидаемся процедуры обновления и сохраняем полученный результат. После нажимаем Generate bitstream, чтобы получить результат и убедиться, что мы всё сделали правильно. Ожидаем пока не закончится процесс синтеза, имплементации и генерации битстрима.

Дождавшись окончания выполняем команды File - Export - Export Hardware и ставим галку Include bitstream. После этого выполняем команду File - Launch SDK и кнопку Ok. 

Добавим проект Hello World для процессорной системы через меню  File - New - Application Project. Укажем ему имя HelloWorld:

Нажимаем Next и выбираем Hello World и нажимаем Finish:

Будет создан простейший baremetal-проект который выведем в UART фразу Hello World. Для нас этот момент не принципиален, самое главное что нужно - это чтобы процессорная система была инициализирована и была запущена.

Далее выбираем пункт меню Xilinx - Program FPGA и нажимаем Program.

На плату зальется битстрим и нужно теперь нужно запустить программу HelloWorld на процессорной системе. Для этого нажмем правой кнопкой в структуре проекта на HelloWorld, выберем пункт Run As - Launch on Hardware (System debugger).

Для запуска проекта к нашей плате должен быть подключен JTAG-отладчик.

Произойдет заливка barematal-приложения, будет проинициализирована PS-система на Zynq плате и теперь можно проверить как реагирует на нажатия кнопки светодиод. При одновременном нажатии двух кнопок светодиод должен изменить свое состояние на обратное. 

Схема выглядит рабочей, можем считать, что задача поставленная перед нами в самом начале выполнена. Неплохо было бы добавить счетчик нажатий и вывести информацию на дисплей, чтобы точно увидеть, что debouncer отрабатывает и реализовать симуляцию и тестирование логических элементов, но пока уровень скиллов не позволяет =)

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

Спасибо за прочтение! Пробуйте, пишите комментарии.