Zynq 7000. Порты GPIO, PS, PL

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

c51e196397912a83b0e1cbd9a6784bf6.png

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

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

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

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

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

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

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

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

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

5e3315b42e394384e675c604e8bb5772.png

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

bc8e7e9795ed87789d8264d08d36db41.png

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

6262089765e36a7786ec5cd666f85d39.png

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

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

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

b3987e550c1b65b6558bb03d136e7edb.png

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

7647d8075237e2e3b561459499ed594f.png

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

91dde6fa568d94067e85ce3490bc3689.png

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

4c45df95c4e2ef9b71d4d7d42d12ea4f.png

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

fadda278b5947766876eda34b891d1af.png

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

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

c063d82d1eda75440ad0048ba290c207.png

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

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

306391a2afd4d9a019bd3addd7875aab.png

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

c3dedf46183d5d45429ef841d1fc6fee.png

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

845a67641fe200d24988e6db5f2a05dd.png

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

ba837d77331ae2ac6eac54de77ed2fcc.png

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

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

7d8737fd77753007925e9b75e24893ff.png

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

2bcf4de15323ea8b818e31cfa1723f9e.png

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

fa115b48a609c604549cecc4290e7af9.png

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

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

47bdde372916261fc0ba2f48572d35f6.png

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

4fa35c97eb70e733104a0a1b9b037b63.png
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 } ];

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

b4615414c66ed9fac99d611567767123.png

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

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

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

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

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

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

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

bdafd186aec5ef1d2892603c6d35c6f4.png

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

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

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

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

d6c7782a206ad82327039ab14e3d2649.png

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

95481f27f0e71794ca6ea1d8766398cf.png

Первым шагом определимся с портами ввода/вывода из данного 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. Ниже на рисунке показана временная диаграмма работы цепочки данных триггеров.

f1f55470c9185c1cb9f203998628f6d9.png

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

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

  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;

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

79fe0f11c4f1e5d124c794a947cdd298.png

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

fad7e897d3769935e1b07e732dbedf6b.png

Следующим шагом необходимо добавить в блоки 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

a12215a8dba4a03ead7dbf27d151c401.png

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

211b2ce29027c730784a36165918e935.png

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

c306f3e346ae405650b409971b95b59c.png

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

73fd21cd81d489d561107cbb04dd96a0.png

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

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

6afd97415e21a5bdd87f6fee6d083a92.png

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

af6d7274f909b78b4b2ffbace03337e6.png

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

5406873c5bc9528f8a8ed862ceb91f9f.png

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

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

41fdda20fa353309b3af9edc244c5364.png

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

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

1ac747665b673f2ddf6229fcb09d950f.png

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

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

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

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

© Habrahabr.ru