Как я делал электронные часы на базе 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-вольтовой платой, не способной, к тому же, выдавать большой ток, использовать в матрице светодиодов дополнительные резисторы не требуется.

Получившиеся у меня в итоге устройство выглядит так:

Электронные часы на FPGA

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

Что же касается кода на SystemVerilog, у меня он получился следующим:

/* vim: set ai et ts=4 sw=4: */
`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». При этом все присваивания происходят параллельно, благодаря чему можно успешно наплодить гонок. Для полноты картины стоит отметить, что также существует и блокирующее присваивание =.

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

В-третьих, как думаете, что произойдет, если вы сделаете в коде опечатку, например, такую?

    // typo: 'reg' instead of 'seg' !!!
    logic hide_reg = (((current_set == 1) && // [... skipped ...]

Программа успешно скомпилируется! Даже несмотря на то, что далее по коду модуль encode_digit получит ставший неинициализированным вход hide_seg. Чтобы не наступать на эти грабли, начинайте код с директивы:

`default_nettype none

Наконец, в-четвертых, в старом коде (например, выдаваемом Google) на Verilog’e, предшественнике SystemVerilog, можно увидеть использование типов reg и wire. Не всегда понятно, в чем их отличие друг от друга, а также от logic. На самом деле, все достаточно просто. Если сигнал встречается в always-блоке или в левой части оператора <=, он должен быть объявлен как reg. В остальных случаях он должен быть объявлен, как wire. Поскольку это вносит некоторую путаницу и усложняет изучение языка, как и тот факт, что reg не имеет ничего общего с регистрами процессора, в SystemVerilog был введен новый тип logic, который можно смело использовать вместо reg и wire.

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

Дополнение: Генерация синусоидального сигнала, а следовательно и звука, на FPGA

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.