Как я делал электронные часы на базе FPGA
27 декабря 2017
Захотелось вот поупражняться в программировании на 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
Метки: FPGA, Электроника.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.