Подавляем дребезг контактов в ПЛИС. Debouncer на Verilog

Шауэрман Александр А. shamrel@yandex.ru

В учебном стенде LESO2 для ввода данных используются кнопка и тумблеры. Если тумблеры, как правило, используются для задания статического уровня, то кнопка используется в качестве генератора импульсов синхронизации. В этом случае схема должна реагировать не на статический уровень, а на факт нажатия, то есть на переход сигнала из одного логического состояния на ножке ПЛИС в другое. При использовании механических коммутаторов (кнопки, тумблеры и прочие ключи) возникает такое неприятное явление, как дребезг контактов. В лабораторных работах по цифровой схемотехнике, при изучении триггеров, счетчиков, мы рекомендовали использовать простейшую схему подавления нежелательных импульсов (Подавление дребезга контактов). Схема выполнена в редакторе Block Diagram/Schematic и проста для повторения, однако обладает рядом недостатков, не позволяющих применять ее в проектах, серьезнее лабораторной работы.

Во-первых, нажатие кнопки происходит в произвольный момент времени, и потому, относительно остальной схемы внутри ПЛИС, является событием асинхронным.  Смена логического состояния на входной ножке ПЛИС может совпасть с моментом переключения принимающего триггера, и есть вероятность, что триггер окажется в неопределенном ("нецифровом") метастабильном состоянии. Это может привести к непредсказуемым результатам. Во-вторых, в зависимости от продолжительности удержания кнопки, генерируется несколько событий, что не всегда удобно. Желательно однозначно фиксировать факт нажатия – генерировать только один импульс.

Создадим универсальный модуль на Verilog для обработки сигнала с механических переключателей. В иностранной литературе аналогичное устройство или блок называется Debouncer. Определимся с портами. Очевидно, что для ввода сигнала непосредственно с ножки ПЛИС нужен входной порт, назовем его sw_i (в статье Пишем "демку" для LESO2 на Verilog я приводил некоторые рассуждения по именованию входных/выходных портов, в этой и последующих работах я буду придерживаться такого стиля). Нужен вход для системных тактовых импульсов, с которыми синхронно работает вся остальная схема – clk_i. Добавим вход для асинхронного сброса всей схемы – rst_i. Сделаем универсальный модуль: предусмотрим два раздельных выхода для события "нажал кнопку" и события "отпустил кнопку" – sw_down_o и sw_up_o. Для того, чтобы не потерять информацию о длительности нажатия (а вдруг пригодится), создадим выход sw_state_o, на котором будет отражаться состояние кнопки, естественно, после устранения эффекта дребезга. Ниже показан листинг "заготовки" для нашего модуля.

module button_debouncer
(
    input clk_i,
	input rst_i,
    input sw_i,  
 
    output reg sw_state_o,  
    output reg sw_down_o,  
    output reg sw_up_o 
);
 
endmodule

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

reg	 [1:0] sw_r;
always @ (posedge rst_i or posedge clk_i)
	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_i сменилось (вернее, теперь корректнее анализировать не sw_i, а выход регистра sw_r[1]) запускаем таймер, если в течении определенного времени состояние не изменилось, то такое состояние считаем устойчивым. Введем триггер sw_state_r , в нем будем хранить последнее стабильное состояние кнопки. Флаг sw_change_f устанавливается в единицу, когда текущее стабильное состояние отличается от того, что установлено на входе sw_i:

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

Флаг sw_change_f может равняться единице в двух случаях: либо это нажатие кнопки, либо ложное срабатывание. Для того, чтобы выяснить с чем мы имеем дело, запускаем счетчик, и если за время работы счетчика (успеет досчитать до своего максимального значения) состояние вновь не смениться, то текущее состояние признаем стабильным: запишем его в 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;  // то обнуляем счет. Было ложное срабатывание. 

В общем случае максимальное значение счета можно задать по разному, один из самых простых методов, прекратить счет, когда в регистре sw_count будут все единицы:

wire sw_cnt_max = (sw_count == 16'hFFFF);

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

wire sw_cnt_max = &sw_count;

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

А на этой диаграмме ложное срабатывание. Значение sw_state_o осталось первоначальным:

Осталось добавить в блоки 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

Временная диаграмма работы модуля:

Проверим работоспособность нового модуля. За основу возьмем демонстрационный проект для leso2.4 из статьи "Пишем "демку" для LESO2 на Verilog". Скачиваем архив проекта и распаковываем в любое удобное место на локальном диске. В корневом каталоге проекта создаем текстовый файл для модуля button_debouncer и копируем туда исходный код. Файл должен иметь расширение .v, я его назвал debouncer.v Запускаем Quartus II, открываем проект. Для того, чтобы можно использовать модуль button_debouncer , мы должны включить файл debouncer.v в проект. Для этого заходим Assigment -> Settings … На вкладке Files добавляем файл с модулем (указываем путь, жмем "add", затем "Apply", только потом "OK" ).

Из всей периферии стенда нам понадобится тактовая кнопка и сегментные индикаторы. Для проверки модуля будем подсчитывать количество импульсов, генерируемых при нажатии кнопки. Создадим простейший 8-ми битный счетчик, пусть он будет срабатывать по заднему фронту (negedge sw_i ) сигнала с кнопки (мы же помним, что кнопка работает с инверсией: в нажатом состоянии генерируется логический ноль), результат счета выведем на семисегментные индикаторы (используем модуль seven):

Компилируем, загружаем. При однократном нажатии кнопки, счетчик не всегда увеличивает свое значение на единицу, часто перескакивает через несколько значений сразу. Это говорит о том, что при нажатии кнопки генерируется более одного импульса. И чем дольше стенд в процессе эксплуатации и больший износ у кнопки, тем в большей степени будет проявляться этот эффект. Особо сильно эффект дребезга будет выражен, если вместо кнопки использовать один из тумблеров.

Отлично. Обработаем сигнал с кнопки с помощью модуля:

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

В текущем примере не важно, сработает счетчик при нажатии или отпускании кнопки, потому вместо вывода sw_down_o (событие – "кнопка нажата"), может быть использованы выхода sw_state_o (статическое состояние кнопки), sw_up_o (событие – "кнопка отпущена"). При объявлении экземпляра модуля, для сокращения записи, неиспользуемые выводы можно опустить:

 button_debouncer  debouncer 
(.clk_i(clk_50MHz_i), .sw_i(sw_i), .sw_down_o(sw_down));

Ниже на листинге исходный код модуля верхнего уровня:

12 января 2016
Орфографическая ошибка в тексте:
Чтобы сообщить об ошибке автору, нажмите кнопку "Отправить сообщение об ошибке". Вы также можете отправить свой комментарий.