Захотелось вот поупражняться в программировании на SystemVerilog. Какую-то шибко интересную задачу выдумывать не стал — решил просто сделать часы на FPGA. Понятно, что электронные часы являются не слишком интересным устройством. Тем более, что их намного проще сделать на базе микроконтроллера. Однако реализация часов на SystemVerilog позволяет столкнуться с множеством тонкостей данного языка. Понимание этих тонкостей является необходимым для создания более сложных проектов.
В одной из прошлых статей мы создали проект, позволяющий отображать на экране монитора семисегментный индикатор. Тогда в качестве вывода данных для отображения на индикаторе использовался единый вход: input wire [NUM_SEGMENTS*4-1:0]segments. Для упрощения нашего дизайна мы переделаем входные сигналы. Единый вход данных, мы заменим на 6 отдельных входов для каждого отдельного знакоместа:
Verilog Code:
module display ( input wire reset, input wire clk_video, //74MHz input wire [NUM_RED_LEDS-1:0]red_leds, input wire [NUM_GREEN_LEDS-1:0]green_leds, input wire [4:0]dig1, input wire [4:0]dig2, input wire [4:0]dig3, input wire [4:0]dig4, input wire [4:0]dig5, input wire [4:0]dig6, //VGA output output wire hsync, output wire vsync, output wire [3:0]r, output wire [3:0]g, output wire [3:0]b );
В самом коде мы изменим формирование сигналов для каждого сегмента:
Verilog Code:
always @(posedge clk_video) begin seg7_0 <= seg7( dig1 ); seg7_1 <= seg7( dig2 ); seg7_2 <= seg7( dig3 ); seg7_3 <= seg7( dig4 ); seg7_4 <= seg7( dig5 ); seg7_5 <= seg7( dig6 ); end
А также поменяем знакогенератор, добавив кроме символов 0-F еще два символа: пустое место и символ подчеркивания.
Verilog Code:
function [7:0]seg7; input [4:0]a; begin case(a) 0: seg7 = 63; 1: seg7 = 6; 2: seg7 = 91; 3: seg7 = 79; 4: seg7 = 102; 5: seg7 = 109; 6: seg7 = 125; 7: seg7 = 7; 8: seg7 = 127; 9: seg7 = 111; 10: seg7 = 119; 11: seg7 = 124; 12: seg7 = 57; 13: seg7 = 94; 14: seg7 = 121; 15: seg7 = 113; 16: seg7 = 0; 17: seg7 = 8; endcase end endfunction
Как не трудно догадаться 16 это пустое место, а 17 символ подчеркивания.
Секунды часы показывают в двоичном коде с помощью виртуальных светодиодов, также об их изменении свидетельствуют мигающая линия между часами и минутами. Для управления используются две кнопки. Первая позволяет выбирать между часами и минутами. При выборе часов они начинают мигать, а при выборе минут мигают минуты. Вторая кнопка производит инкремент того, что выбрано.
Что же касается кода на SystemVerilog, у меня он получился следующим:
Verilog Code:
module inc_minute( input logic [3:0] min1, input logic [3:0] min2, output logic [3:0] out_min1, output logic [3:0] out_min2); assign out_min1 = (min1 == 9) ? 0 : min1 + 1; assign out_min2 = (min1 == 9) ? ( (min2 == 5) ? 0 : min2 + 1 ) : min2; endmodule module inc_hour( input logic [3:0] hour1, input logic [3:0] hour2, output logic [3:0] out_hour1, output logic [3:0] out_hour2); assign out_hour1 = (((hour2 != 2)&&(hour1 == 9)) || ((hour2 == 2)&&(hour1 == 3))) ? 0 : hour1 + 1; assign out_hour2 = ((hour2 == 2) && (hour1 == 3)) ? 0 : ( (hour1 == 9) ? hour2 + 1 : hour2 ); endmodule module clock( input logic clk, input logic btn_set, input logic btn_inc, // the time is displayed like this: // hour2 hour1 : min2 min1 output logic [3:0] min1 = 0, output logic [3:0] min2 = 0, output logic [3:0] hour1 = 0, output logic [3:0] hour2 = 0, output logic [5:0] sec=0, output logic dot); logic [25:0] divider = 0; logic [3:0] next_min1; logic [3:0] next_min2; logic [3:0] next_hour1; logic [3:0] next_hour2; logic [1:0] current_set = 0; logic btn_set_was_pressed = 0; logic btn_inc_was_pressed = 0; assign dot = sec[0]; inc_minute im(min1, min2, next_min1, next_min2); inc_hour ih(hour1, hour2, next_hour1, next_hour2); always_ff @(posedge clk) begin if(divider[16:0] == 0) begin if(btn_set == 1) btn_set_was_pressed <= 1; else begin if(btn_set_was_pressed == 1) current_set <= (current_set == 2) ? 0 : current_set + 1; btn_set_was_pressed <= 0; end if(btn_inc == 1) btn_inc_was_pressed <= 1; else begin if(btn_inc_was_pressed == 1) begin if(current_set == 1) begin hour1 <= next_hour1; hour2 <= next_hour2; sec <= 0; divider <= 0; end else if(current_set == 2) begin min1 <= next_min1; min2 <= next_min2; sec <= 0; divider <= 0; end end btn_inc_was_pressed <= 0; end end // once a second @ 50 MHz oscillator if(divider == 50000000) begin divider <= 0; sec <= (sec == 59) ? 0 : sec + 1; if(sec == 59) begin min1 <= next_min1; min2 <= next_min2; if((min1 == 9) && (min2 == 5)) begin hour1 <= next_hour1; hour2 <= next_hour2; end end end else divider <= divider + 1; end // always ... endmodule
Не стану утверждать, что получившееся у меня решение является вершиной элегантности. Но, по крайней мере, оно работает, и каких-либо дефектов мне выявить не удалось. Для меня, как новичка в SystemVerilog, это уже большое достижение.
Я не буду подробно разбирать приведенный код. Главным образом, потому что я не настолько хорошо знаю SystemVerilog, чтобы не наврать вам в три короба. Лучше обратитесь к великолепной книге Цифровая схемотехника и архитектура компьютера за авторством Дэвида и Сары Харрис. Она расскажет вам о SystemVerilog намного лучше меня.
Хотелось бы также сказать пару слов о тех самых тонкостях языка, о которых я упомянул в начале. Во-первых, кажется, я наконец-то смог нормально осознать семантику assign и <=. Первый как бы навсегда связывает сигналы функциональной зависимостью. При этом, если меняется один из сигналов, использованных справа от знака равенства, одновременно меняется и сигнал слева. Второй же говорит что-то вроде «присвоить сигналу такое-то значение при событии, указанном в always». При этом все присваивания происходят параллельно, благодаря чему можно успешно наплодить гонок. Для полноты картины стоит отметить, что также существует и блокирующее присваивание =.
Во-вторых, если у вас есть два модуля, имеющих общий выходной сигнал, это наверняка означает ошибку. Однако компилятор не станет ругаться на эту ошибку или каким-то иным образом помогать ее искать. Это обстоятельство может нешуточно усложнить разработку.
В-третьих, как думаете, что произойдет, если вы сделаете в коде опечатку, например, такую?
Verilog Code:
// typo: 'reg' instead of 'seg' !!! logic hide_reg = (((current_set == 1) && // [... skipped ...]
Программа успешно скомпилируется! Даже несмотря на то, что далее по коду модуль encode_digit получит ставший неинициализированным вход hide_seg. Чтобы не наступать на эти грабли, начинайте код с директивы:
Verilog Code:
`default_nettype none
Наконец, в-четвертых, в старом коде (например, выдаваемом Google) на Verilog’e, предшественнике SystemVerilog, можно увидеть использование типов reg и wire. Не всегда понятно, в чем их отличие друг от друга, а также от logic. На самом деле, все достаточно просто. Если сигнал встречается в always-блоке или в левой части оператора <=, он должен быть объявлен как reg. В остальных случаях он должен быть объявлен, как wire. Поскольку это вносит некоторую путаницу и усложняет изучение языка, как и тот факт, что reg не имеет ничего общего с регистрами процессора, в SystemVerilog был введен новый тип logic, который можно смело использовать вместо reg и wire.
В общем и целом, это был весьма познавательный опыт. Я крайней рекомендую его к повторению, если вы тоже изучаете SystemVerilog. Если хотите, вы даже можете взять за основу полную версию моего кода, доступную на GitHub, и добавить поддержку кнопки декремента. Если же эта задача кажется вам слишком простой, могу предложить добавить в часы функцию будильника.
По традиции видео и исходники:
Скачать: vga_clock.zip
А теперь настала ваша очередь рассказать, как вы учили SystemVerilog (или Verilog/VHDL). Кроме того, если вы знаете, как можно существенно улучшить приведенный мной код, я весь внимание.
Автор: Александр