Часы на ПЛИС

Сегодня я покажу как можно реализовать простейшие часы на ПЛИС. Как и обычно, я буду использовать язык Verilog и отладочную плату Cyclone V GX Starter Kit для демонстрации.

IMAG1090

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

Кварцевые резонаторы и генераторы позволяют получать колебания довольно высокой частоты (от десятков килогерц до десятков мегагерц), в то время как для часов удобно отмерять интервалы времени, равные 1 секунде, что соответствует частоте 1 Гц. Частоту можно поделить на целое число, используя обыкновенный счетчик. Выпускаются даже, так называемые, часовые кварцы, имеющие частоту 32768 Гц. Такое странное число есть не что иное, как 2 в 15 степени. Такой кварц очень удобен для реализации часов, поскольку тактируя 15-разрядный счетчик сигналом с частотой 32768 Гц, его старший разряд будет переключаться с частотой в 32768 раз меньше тактовой, т. е. 1 Гц.

Однако, на моей отладочной плате нет источника тактового сигнала с «часовой» частотой, а имеются только источники 25 и 50 МГц. Поэтому придется сбрасывать счетчик как только он досчитает до 12,5 или 25 миллионов соответственно. Я буду использовать частоту от тактового генератора 50 МГц, поэтому необходим счетчик, который бы смог считать (не переполняясь) по крайней мере до 25.000.000. Число 33.554.432 является двойкой в 25 степени, значит 25-разрядного счетчика нам хватит.

Теперь, давайте подумаем, что еще необходимо для часов. Конечно же, установка текущего времени. Для этого выделим 3 кнопки («режим», «+» и «-»). Первое нажатие кнопки «режим» позволит нам установить текущий час (кнопками «+» и «-»), вторым нажатием кнопки «режим» переходим к вводу минут, а третье нажатие запускает отсчет времени. Поскольку у меня на плате только четыре 7-сегментных индикатора, то я их выделю для часов и минут, а секунды будут показываться в двоичном коде на светодиодах :-).

Ядро наших часов — это 25-разрядный счетчик играющий роль делителя частоты. Как только значение счетчика достигнет половины этого значения, логический уровень сигнала clk_1hz инвертируется (меняется на противоположный), а счетчик сбрасывается и начинает считать сначала. Таким образом, за 50 млн. тактов пройдет один период сигнала clk_1hz (сигнал инвертируется два раза — переключится в противоположное состояние и вернется обратно к изначальному). Получается, что частота сигнала clk делится на 50.000.000 и снимается с clk_1hz. Тактируя счетчик сигналом clk с частотой 50 МГц, получим на clk_1hz частоту 1 Гц:

1
2
3
4
5
6
7
8
9
10
11
reg         clk_1hz     = 1'b0;	       
reg  [24:0] div_cnt     = 26'd0;       
 
always @(posedge clk)
     if (div_cnt == 25'd25_000_000) begin
	      clk_1hz <= ~clk_1hz;
	      div_cnt <= 25'd0;
	  end
	  else begin
	      div_cnt <= div_cnt + 25'd1;
	  end

Помимо основного 25-разрядного счетчика-делителя, необходимо еще 3 счетчика для отсчета часов, минут и секунд, тактируемые сигналом clk_1hz. Для минут и секунд достаточно 6-разрядного счетчика (позволяет считать до 63), а для часов хватит и 5-разрядного.

Поскольку при включении питания наш таймер начинает отсчет времени с нуля часов, нуля минут и нуля секунд, то необходимо предусмотреть загрузку в счетчики часов и минут установленных пользователем значений (счетчик секунд сбрасывается в ноль после установки времени). Для этого введена переменная load, которая определяет загрузить ли новое значение в счетчики или продолжить отсчет времени. Заметьте, что сигнал load асинхронный, об этом говорит список событий, по которому срабатывает блок always. Срабатывание происходит не только по тактовому сигналу clk_1hz, но и при переключении сигнала load в единицу. Переменная en разрешает или запрещает отсчет времени (запрет нужен, например, во время установки нового значения времени).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
reg       load = 1'b0;
reg       en = 1'b1;
reg [4:0] hour = 5'b00000;
reg [5:0] min = 6'b000000;
reg [5:0] sec = 6'b000000;
reg [4:0] hour_reg = 5'b00000;
reg [5:0] min_reg = 6'b000000;
 
always @(posedge clk_1hz, posedge load)
    if (load == 1'b1) begin
	 sec <= 6'd0;        //сбрасываем счетчик секунд
	 min <= min_reg;     //загружаем счетчик минут новым значением из min_reg
	 hour <= hour_reg;   //загружаем счетчик часов новым значением из hour_reg
    end 
    else begin
        if (en == 1'b1) begin         
            /*если счетчик секунд достиг максимального значения 
              нужно его обнулить и инкрементировать счетчик минут*/        
            if (sec == 6'd59) begin
		sec <= 6'd0;
                /*если счетчик минут достиг максимального значения
                  нужно его обнулить и инкременировать счетчик часов*/
		if (min == 6'd59) begin
		    min <= 6'd0; 
	            if (hour == 5'd23) //если счетчик часов достиг максимума - обнуляем его
			hour <= 5'd0;
		    else
			hour <= hour + 5'd1; //инкрементируем часы
		end 
		else begin
		    min <= min + 6'd1;       //инкрементируем минуты
		end //if (min == 6'd59)
            end
            else begin
	        sec <= sec + 6'd1;           //инкрементируем секунды
	    end //if (sec == 6'd59)
	end //if (en == 1'b1)
    end

Теперь давайте продумаем механизм установки времени и его логику работы. Можно выделить 3 основных режима работы, которые переключаются нажатием кнопки «режим»: нормальный режим (работа часов), установка часа и установка минут. Для определения текущего режима работы введем переменную mode, которая инкрементируется при нажатии кнопки «режим» (нажатие этой кнопки формирует импульс сигнала nf_mode_out длительностью один такт, как он формируется рассмотрим позже). При задании часов и минут, их значения хранятся в соответствующих регистрах (hour_reg, min_reg), а кнопки «+» (сигнал nf_plus_out) и «-» (сигнал nf_minus_out) прибавляют или вычитают единицу из них. При возврате к нормальному режиму работы, сохраненные значения записываются в регистры счетчика hour и min путем установки сигнала load в 1. В режиме установки часов я отключаю индикаторы минут, а при установке минут отключаю индикаторы часов (сигналы hour_ind_en и min_ind_en управляют отключением соответствующих 7-сегментных индикаторов).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
reg [4:0] hour_reg = 5'b00000;     //устанавливаемое значение часа
reg [5:0] min_reg = 6'b000000;     //устанавливаемое значение минут
reg [1:0] mode = 2'b00;            //режим
reg       hour_ind_en = 1'b1;      //сигнал включения индикатора часов
reg       min_ind_en = 1'b1;       //сигнал включения индикатора минут
reg       src_sel = 1'b0;          //выбор источника для индикатора
 
always @(posedge clk) begin
    //если нажали кнопку "режим", то переходим к следующему режиму
    if (nf_mode_out == 1'b1)       
        mode <= mode + 2'b01;
	case (mode)
            //нормальный режим работы (отсчет времени)
	    2'd0:  begin                  
		       load <= 1'b0;
		   end
 
            //режим установки часа		
	    2'd1:  begin
		       min_ind_en <= 1'b0;     //отключаем индикатор минут
		       hour_ind_en <= 1'b1;   
		       en <= 1'b0;             //останавливаем отсчет времени
		       src_sel <= 1'b1; //настраиваем индикаторы на регистры hour_reg и min_reg
 
                       //если нажали кнопку "+", то увеличиваем hour_reg на 1	
		       if (nf_plus_out == 1'b1) begin
		           if (hour_reg == 5'd23)
			       hour_reg <= 5'd0;
			   else
			       hour_reg <= hour_reg + 5'd1;
		       end
 
                       //если нажали кнопку "-", то уменьшаем hour_reg на 1
		       if (nf_minus_out == 1'b1) begin
		           if (hour_reg == 5'd0)
			       hour_reg <= 5'd23;
		           else
			       hour_reg <= hour_reg - 5'd1;
		       end
		   end
 
            //режим установки минут
	    2'd2:  begin
		       min_ind_en <= 1'b1; 
		       hour_ind_en <= 1'b0;    //отключаем индикатор часов
		       en <= 1'b0;             //останавливаем отсчет времени
		       src_sel <= 1'b1; //настраиваем индикаторы на регистры hour_reg и min_reg
 
                       //если нажали кнопку "+", то увеличиваем min_reg на 1	
		       if (nf_plus_out == 1'b1) begin
		           if (min_reg == 6'd59)
			       min_reg <= 6'd0;
			   else
			       min_reg <= min_reg + 6'd1;
		       end
 
                       //если нажали кнопку "-", то уменьшаем min_reg на 1	
		       if (nf_minus_out == 1'b1) begin
		           if (min_reg == 6'd0)
			       min_reg <= 6'd59;
			   else
			       min_reg <= min_reg - 6'd1;
		       end
		   end
 
            /*формируем сигнал load=1 для загрузки нового значения в счетчики,
              включаем все индикаторы, разрешаем отсчет времени и 
              перенастраиваем индикаторы на выходы счетчиков hour и min*/	
	    2'd3:  begin
	               mode <= 1'b0;
		       load <= 1'b1;
		       en <= 1'b1;
		       min_ind_en <= 1'b1;
		       hour_ind_en <= 1'b1;
		       src_sel <= 1'b0;
		   end
	endcase
end

Еще пару слов стоит сказать про сигнал src_sel. Он управляет мультиплексором, который подключает к 7-сегментным индикаторам часов и минут либо выходы регистров hour и min (в обычном режиме работы), либо регистры hour_reg и min_reg (в режиме установки времени). Ведь при установке времени мы должны видеть не значения счетчиков hour, min, а значения которые мы устанавливаем кнопками «+» и «-» (hour_reg, min_reg). Мультиплексор это комбинационное устройство (при изменении любого сигнала на его входах на выходе сразу устанавливается соответствующее значение спустя время задержки), поэтому срабатывание происходит по уровню, а не по фронту, и в списке событий блока не нужны ключевые слова posedge или negedge:

1
2
3
4
5
6
7
8
9
10
11
12
reg [4:0] hour_mux_out; 
reg [5:0] min_mux_out;
 
always @(src_sel)
    if (src_sel == 1'b0) begin
	hour_mux_out = hour;
	min_mux_out = min;
    end
    else begin 
        hour_mux_out = hour_reg; 
	min_mux_out = min_reg; 
    end

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

clock2

Для формирования двузначных чисел часов и минут на индикаторе будут использоваться 14-разрядные переменные hour_ind_out и min_ind_out (по два семисегментных индикатора на часы и минуты), которые являются выходами дешифратора и подключаются ко входам соответствующих индикаторов (старший разряд — вывод g индикатора старшей цифры, младший разряд — вывод a индикатора младшей цифры). Дешифратор преобразует десятичное число, поступающее на его вход в соответствующие сигналы индикаторов для отображения этого числа.

Я не стал сильно заморачиваться и просто прописал выходное состояние дешифратора для каждого возможного входного десятичного числа (отдельно для часов и минут). Я понял где ошибся только когда я прошил эту конфигурацию в ПЛИС, оказалось, чтобы сегмент индикатора зажегся, необходимо подать на соответствующий ему вход НИЗКИЙ уровень (индикатор с общим анодом). Исправлять все выходные комбинации было лень, поэтому я просто добавил символ инверсии (~) к каждой константе:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
always @(hour_mux_out, hour_ind_en)
    if (hour_ind_en == 1'b0) begin
	hour_ind_out = ~14'b0000000_0000000;    //выключаем индикатор
    end
    else begin
	case (hour_mux_out)
	    5'd0:  hour_ind_out = ~14'b0111111_0111111;
	    5'd1:  hour_ind_out = ~14'b0111111_0000110;
	    5'd2:  hour_ind_out = ~14'b0111111_1011011;
	    5'd3:  hour_ind_out = ~14'b0111111_1001111;
	    5'd4:  hour_ind_out = ~14'b0111111_1100110;
	    5'd5:  hour_ind_out = ~14'b0111111_1101101;
	    5'd6:  hour_ind_out = ~14'b0111111_1111101;
	    5'd7:  hour_ind_out = ~14'b0111111_0000111;
	    5'd8:  hour_ind_out = ~14'b0111111_1111111;
	    5'd9:  hour_ind_out = ~14'b0111111_1101111;
	    5'd10: hour_ind_out = ~14'b0000110_0111111;
	    5'd11: hour_ind_out = ~14'b0000110_0000110;
	    5'd12: hour_ind_out = ~14'b0000110_1011011;
	    5'd13: hour_ind_out = ~14'b0000110_1001111;
	    5'd14: hour_ind_out = ~14'b0000110_1100110;
	    5'd15: hour_ind_out = ~14'b0000110_1101101;
	    5'd16: hour_ind_out = ~14'b0000110_1111101;
	    5'd17: hour_ind_out = ~14'b0000110_0000111;
	    5'd18: hour_ind_out = ~14'b0000110_1111111;
	    5'd19: hour_ind_out = ~14'b0000110_1101111;
	    5'd20: hour_ind_out = ~14'b1011011_0111111;
	    5'd21: hour_ind_out = ~14'b1011011_0000110;
	    5'd22: hour_ind_out = ~14'b1011011_1011011;
	    5'd23: hour_ind_out = ~14'b1011011_1001111;
	endcase
    end
 
always @(min_mux_out, min_ind_en)
    if (min_ind_en == 1'b0) begin
	min_ind_out = ~14'b0000000_0000000;    //выключаем индикатор
    end
    else begin
	case (min_mux_out)
	    6'd0:  min_ind_out = ~14'b0111111_0111111;
	    6'd1:  min_ind_out = ~14'b0111111_0000110;
	    6'd2:  min_ind_out = ~14'b0111111_1011011;
			      ...
	    6'd57: min_ind_out = ~14'b1101101_0000111;
	    6'd58: min_ind_out = ~14'b1101101_1111111;
	    6'd59: min_ind_out = ~14'b1101101_1101111;
        endcase
    end

Очередным непредвиденным препятствием стали множественные срабатывания при нажатии кнопки в первых версиях проекта. Например, если я нажимал кнопку  «+» единожды, то в некоторых случаях происходило несколько срабатываний и значение увеличивалось не на 1 как я хотел, а на 2 или более. Это связано с тем, что кнопка это механическое устройство, и в момент коммутации сигнал может измениться несколько раз туда и обратно до тех пор, пока контакт надежно не зафиксируется в новом положении. А поскольку тактовый сигнал имеет довольно высокую частоту, то он поймает эти множественные переключения (помехи) с кнопки во время нажатия. Чтобы исключить этот негативный эффект необходимо каким-то образом отфильтровать шумовой сигнал, появляющийся при коммутации механического контакта. Сделать это можно следующим образом. При изменении уровня сигнала с кнопки необходимо выждать некоторое время (применим для этого еще один счетчик), в течение которого гарантированно устанавливается новое состояние, после чего необходимо опять считать уровень сигнала с кнопки и фиксировать нажатие только в том случае, если оно подтвердилось оба раза (уровни сигналов совпадают после первого переключения и спустя время ожидания). Я использовал 20-разрядный счетчик, что соответствует задержке 2^20/50000000 = 21 мс.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
reg [19:0] cnt;
reg        state = 1'b0;
reg        prev = 1'b1;                //предыдущее положение кнопки
reg        out = 1'b1;
 
always @(posedge clk) begin
    if (state == 1'b0)                 //первая фаза
        if (nf_in != prev) begin       //если обнаружили изменение уровня
	    state <= 1'b1;             //переходим ко второй фазе
	    cnt <= 20'h00000;          //сброс счетчика
	end 
        else begin
            state <= 1'b0;
        end
    else                               //вторая фаза
	if (cnt == 20'hfffff) begin    //при переполнении счетчика
            state <= 1'b0;             
            if (nf_in != prev) begin   //снова проверяем состояние кнопки
	        out <= nf_in;
	        prev <= nf_in;
	    end
        end
	else begin
	    cnt <= cnt + 20'h00001;   
	end
end

Если помните, режим работы (переменная mode) сменяется с каждым фронтом clk если сигнал nf_mode_out=1. Этот сигнал нельзя брать непосредственно с кнопки, поскольку пока кнопка удерживается, то mode успеет смениться множество раз и очень сложно будет попасть в нужный нам режим. Та же проблема справедлива и для кнопок «+» и «-». Для того, чтобы то или иное действие происходило единожды, при нажатии кнопки необходимо сформировать импульс длительностью равной 1 такту сигнала clk. Для этого применяют следующую синхронную схему:

clock3_2

Тогда весь код фильтра помех для кнопки, с генератором единичного импульса при нажатии будет следующим:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
module noise_filter(
    input  wire clk,
    input  wire nf_in,
    output wire nf_out,
    output wire nf_edge_out
);
 
reg [19:0] cnt;
reg        state = 1'b0;
reg        prev = 1'b1;
reg        out = 1'b1;
reg        w1 = 1'b1;
reg        w2 = 1'b1;
 
assign nf_out = ~out;
assign nf_edge_out = w1 & ~w2;
 
always @(posedge clk) begin
    if (state == 1'b0)
	if (nf_in != prev) begin
	    state <= 1'b1;
	    cnt <= 20'h00000;
	end 
	else begin
	    state <= 1'b0;
	end
    else
	if (cnt == 20'hfffff) begin
	    state <= 1'b0;
	    if (nf_in != prev) begin
		out <= nf_in;
		prev <= nf_in;
	    end
	end
	else begin
	    cnt <= cnt + 20'h00001;
	end
end
 
//edge detector
always @(negedge clk) begin
    w1 <= out;
    w2 <= w1;
end
 
endmodule

Единичный импульс формируется по спадающему фронту clk (negedge) чтобы по нарастающему фронту сигнал nf_edge_out гарантированно находился в единице (поскольку все остальные действия у нас выполняются по posedge). Ключевое слово assign физически соединяет проводник с другими проводниками или какой либо их логической функцией (в операторе assign слева от знака равенства может стоять только переменная с типом wire). Этот оператор удобен для формирования простой комбинационной логики.

Интерфейс модуля верхнего уровня выглядит следующим образом:

1
2
3
4
5
6
7
8
9
module clock (
    input  wire        clk,	        //тактовый сигнал
    input  wire        btn_mode,        //кнопка "режим"
    input  wire        btn_plus,        //кнопка "+"
    input  wire        btn_minus,	//кнопка "-"
    output reg  [13:0] hour_ind_out,    //индикаторы часов
    output reg  [13:0] min_ind_out,     //индикаторы минут
    output wire [5:0]  sec_ind_out      //светодиоды секунд
);

Не забудем каждую кнопку подключить к фильтру помех:

1
2
3
noise_filter nf_mode(.clk(clk), .nf_in(btn_mode), .nf_edge_out(nf_mode_out));
noise_filter nf_plus(.clk(clk), .nf_in(btn_plus), .nf_edge_out(nf_plus_out));
noise_filter nf_minus(.clk(clk), .nf_in(btn_minus), .nf_edge_out(nf_minus_out));

Теперь нужно назначить порты модуля реальным пинам ПЛИС, откомпилировать проект и прошить в конфигурационную память. Вот что у меня в итоге получилось:

Упрощенная блок-схема проекта:

clock_bs

Код всего проекта здесь.

6 комментариев

  1. В целях обучающей концепции Вашего блога есть совет по развитию проекта с часами. Например, можно сделать передатчик/приемник по com-порту с компьютером, и все это дело интегрировать с Matlab (сделать GUI с отображением и установкой часов/минут) + например, добавить будильник.
    Успехов!

    P.S.: слишком заморочились с «фильтром шума». для простого объяснения есть ссылка http://marsohod.org/index.php/verilog/157-verilogedges

  2. Привет. Отправил еще раз и продублировал здесь.
    Моя почта poi_83@mail.ru
    Прочитал статью об часах на ПЛИС.
    Наверно надо добавить блок схему часов для лучшего восприятия

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

    Есть еще пару задумок по часам, можно поделиться?
    Может удобно будет переписываться в skype?
    Мой логин medina_ns

    1. В таком довольно простом проекте создание отдельных модулей для каждого функционального блока, я считаю, не оправдано, потратите больше времени на описание интерфейса каждого модуля. Для каждого функционального блока у меня отдельное описание процесса (always).
      А так, конечно, если проект большой, то для каждого функционального блока делается свой модуль.
      Отображение в виде схемы наглядно, если самих блоков и выводов в них немного, в противном случае мешанина проводов и блоков только запутает.
      Тут, конечно, кому как нравится, но я не использую представление в виде схемы, а в топовом модуле просто делаю соединение блоков между собой на Verilog.

      З.Ы.: Ваша почта что-то не доходит, странно. Можете писать в скайп pav2051.

    1. Пожалуйста. Только мне на почту ничего не приходило, попробуйте отправить еще раз (pav2051@gmail.com).

Добавить комментарий для Nick Отменить ответ

Ваш e-mail не будет опубликован. Обязательные поля помечены *