Захотелось вот поупражняться в программировании на SystemVerilog. Какую-то шибко интересную задачу выдумывать не стал — решил просто сделать часы на FPGA. Понятно, что электронные часы являются не слишком интересным устройством. Тем более, что их намного проще сделать на базе микроконтроллера . Однако реализация часов на SystemVerilog позволяет столкнуться с множеством тонкостей данного языка. Понимание этих тонкостей является необходимым для создания более сложных проектов.
На момент написания этих строк у меня было две платы с FPGA — Arty Artix-7 на базе FPGA Xilinx Artix-7 , а также iCEstick на базе Lattice ICE40HX1K . Для данного проекта я выбрал iCEstick, поскольку его возможностей должно вполне хватить, а с открытым стеком разработки под эту плату ( Yosys и Arachne-pnr ) мне лично работать намного приятней, чем с тяжелой, неуклюжей и закрытой Vivado.
Из прочих компонентов я использовал две кнопки, два резистора на 15 кОм, макетную плату, штырьки с шагом 2.54 мм, а также индикатор с четырьмя цифрами и немного одножильных проводов. Беглое гугление показало, что светодиоды в индикаторе соединены по матричной схеме . Вот наглядная иллюстрация ( источник ):
Подавая низкое напряжение на один из пинов, обозначенных красным цветом (6, 8, 9 или 12), мы выбираем соответствующую этому пину цифру. При помощи оставшихся пинов мы можем зажечь сегменты выбранной цифры, подав на пины высокое напряжение. Иллюзия одновременного горения всех четырех цифр возникает в результате быстрого переключения между ними. Поскольку iCEstick является 3.3-вольтовой платой, не способной, к тому же, выдавать большой ток, использовать в матрице светодиодов дополнительные резисторы не требуется.
Получившиеся у меня в итоге устройство выглядит так:
Секунды часы не показывают, но об их изменении свидетельствуют мигающая точка рядом со второй цифрой. Кнопка слева позволяет выбирать между часами и минутами. При выборе часов они начинают мигать, а при выборе минут мигают минуты. Вторая кнопка производит инкремент того, что выбрано. Изначально я хотел сделать еще и третью кнопку, для декремента. Но потом понял, что мне лень 🙂
Что же касается кода на SystemVerilog, у меня он получился следующим:
`default_nettype none
module encode_digit (
input logic [ 3 : 0 ] digit ,
input logic hide ,
output logic [ 0 : 6 ] seg ) ;
assign seg = ( hide == 1 ) ? 0 :
( digit == 0 ) ? 8’b1111110 :
( digit == 1 ) ? 8’b0110000 :
( digit == 2 ) ? 8’b1101101 :
( digit == 3 ) ? 8’b1111001 :
( digit == 4 ) ? 8’b0110011 :
( digit == 5 ) ? 8’b1011011 :
( digit == 6 ) ? 8’b1011111 :
( digit == 7 ) ? 8’b1110000 :
( digit == 8 ) ? 8’b1111111 :
( digit == 9 ) ? 8’b1111011 :
8’b1001111 ; // E for Error
endmodule
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 top (
input logic clk ,
input logic btn_set ,
input logic btn_inc ,
output logic [ 0 : 7 ] seg ,
output logic [ 0 : 3 ] d ) ;
logic [ 23 : 0 ] divider = 0 ;
logic [ 3 : 0 ] d_rot = 4’b1110 ;
logic [ 5 : 0 ] sec = 0 ;
// the time is displayed like this:
// hour2 hour1 : min2 min1
logic [ 3 : 0 ] min1 = 0 ;
logic [ 3 : 0 ] min2 = 0 ;
logic [ 3 : 0 ] hour1 = 0 ;
logic [ 3 : 0 ] hour2 = 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 ;
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 [ 9 : 0 ] == 0 )
d_rot <= { d_rot [ 2 : 0 ] , d_rot [ 3 ] } ;
if ( divider [ 14 : 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 @ 12 MHz oscillator
if ( divider == 12000000 )
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 …
assign d = d_rot ;
logic [ 3 : 0 ] disp =
( d_rot == 4’b1110 ) ? min1 :
( d_rot == 4’b1101 ) ? min2 :
( d_rot == 4’b1011 ) ? hour1 :
( d_rot == 4’b0111 ) ? hour2 :
13 ; // should never happen
logic hide_seg = ( ( ( current_set == 1 ) &&
( ( d_rot == 4’b0111 ) || ( d_rot == 4’b1011 ) ) ) ||
( ( current_set == 2 ) &&
( ( d_rot == 4’b1101 ) || ( d_rot == 4’b1110 ) ) ) ) &&
( sec [ 0 ] == 1 ) ;
encode_digit enc1 ( disp , hide_seg , seg [ 0 : 6 ] ) ;
assign seg [ 7 ] = ( d_rot == 4’b1011 ) && ( sec [ 0 ] == 1 ) ;
endmodule
Не стану утверждать, что получившееся у меня решение является вершиной элегантности. Но, по крайней мере, оно работает, и каких-либо дефектов мне выявить не удалось. Для меня, как новичка в SystemVerilog, это уже большое достижение.
Я не буду подробно разбирать приведенный код. Главным образом, потому что я не настолько хорошо знаю SystemVerilog, чтобы не наврать вам в три короба. Лучше обратитесь к великолепной книге Цифровая схемотехника и архитектура компьютера за авторством Дэвида и Сары Харрис. Она расскажет вам о SystemVerilog намного лучше меня.
Хотелось бы также сказать пару слов о тех самых тонкостях языка, о которых я упомянул в начале. Во-первых, кажется, я наконец-то смог нормально осознать семантику assign
и <=
. Первый как бы навсегда связывает сигналы функциональной зависимостью. При этом, если меняется один из сигналов, использованных справа от знака равенства, одновременно меняется и сигнал слева. Второй же говорит что-то вроде «присвоить сигналу такое-то значение при событии, указанном в always». При этом все присваивания происходят параллельно, благодаря чему можно успешно наплодить гонок. Для полноты картины стоит отметить, что также существует и блокирующее присваивание =
.
Во-вторых, если у вас есть два модуля, имеющих общий выходной сигнал, это наверняка означает ошибку. Однако компилятор не станет ругаться на эту ошибку или каким-то иным образом помогать ее искать. Это обстоятельство может нешуточно усложнить разработку.
В-третьих, как думаете, что произойдет, если вы сделаете в коде опечатку, например, такую?
logic hide_reg = ( ( ( current_set == 1 ) && // [… skipped …]
Программа успешно скомпилируется! Даже несмотря на то, что далее по коду модуль encode_digit получит ставший неинициализированным вход hide_seg. Чтобы не наступать на эти грабли, начинайте код с директивы:
Наконец, в-четвертых, в старом коде (например, выдаваемом Google) на Verilog’e, предшественнике SystemVerilog, можно увидеть использование типов reg и wire. Не всегда понятно, в чем их отличие друг от друга, а также от logic. На самом деле, все достаточно просто. Если сигнал встречается в always-блоке или в левой части оператора <=
, он должен быть объявлен как reg. В остальных случаях он должен быть объявлен, как wire. Поскольку это вносит некоторую путаницу и усложняет изучение языка, как и тот факт, что reg не имеет ничего общего с регистрами процессора, в SystemVerilog был введен новый тип logic, который можно смело использовать вместо reg и wire.
В общем и целом, это был весьма познавательный опыт. Я крайней рекомендую его к повторению, если вы тоже изучаете SystemVerilog. Если хотите, вы даже можете взять за основу полную версию моего кода, доступную на GitHub , и добавить поддержку кнопки декремента. Если же эта задача кажется вам слишком простой, могу предложить добавить в часы функцию будильника.
Дополнение: Генерация синусоидального сигнала, а следовательно и звука, на FPGA