Yellow Rabbit

FPGA и простые внешние устройства

FPGA и датчик угла поворота

При изучении FPGA1 а первых этапах мне очень хотелось иметь какой-то видимый отклик на мою программу. Встроенного RGB светодиода хватило ненадолго в частности из-за плохого цветоощущения :wink:, так что первым внешним устройством стал 17-ти сегментный

Индикатор

У меня нашёлся индикатор с общим катодом, 17 резисторов по 100 Ом из расчёта того, что на сегменте зелёного цвета падает 2.4 Вольта, напряжения 3.3 Вольта на выходе FPGA получем ток: Так что даже все включенные сегменты не приведут к перегрузке по току.

Вот простой модуль для индикатора. A1, A2,… — символические имена сегментов индикатора согласно документации.

/*****************/
/* 0-15 on 17led */
/*****************/
module Display16(
	input wire [3:0] i_x, 
	output reg [16:0] o_led);

	localparam A1 =  1 << 4;
	localparam A2 =  1 << 5;
	localparam B  =  1 << 6;
	localparam C  =  1 << 7;
	localparam D1 =  1 << 1;
	localparam D2 =  1 << 0;
	localparam E  =  1 << 2;
	localparam F  =  1 << 3;
	localparam G1 =  1 << 11;
	localparam G2 =  1 << 15;
	localparam H  =  1 << 12;
	localparam J  =  1 << 13;
	localparam K  =  1 << 14;
	localparam L  =  1 <<  8;
	localparam M  =  1 <<  9;
	localparam N  =  1 << 10;
	localparam DP =  1 << 16;


	always @(i_x) begin
	  case (i_x)
	    4'h0 : o_led = A1 + A2 + B + C + D2 + D1 + E + F; // 0
	    4'h1 : o_led = K + B + C; // 1
	    4'h2 : o_led = A1 + A2 + B + G2 + N + D1 + D2; // 2
	    4'h3 : o_led = A1 + A2 + B + G2 + C + D2 + D1; // 3
	    4'h4 : o_led = F + G1 + G2 + B + C; // 4
	    4'h5 : o_led = A1 + A2 + F + G1 + G2 + C + D1 + D2; // 5
	    4'h6 : o_led = A1 + A2 + F + G1 + G2 + C + D2 + D1 + E; // 6
	    4'h7 : o_led = A1 + A2 + K + M; // 7
	    4'h8 : o_led = A1 + A2 + B + C + D2 + D1 + E + F + G1 + G2; // 8
	    4'h9 : o_led = A1 + A2 + B + C + D2 + D1 + F + G1 + G2; // 9
	    4'ha : o_led = A1 + A2 + B + C + E + F + G1 + G2; // A
	    4'hb : o_led = A1 + J + G2 + C + D2 + D1 + E + F + G1; // B
	    4'hc : o_led = D2 + D1 + E + F + A1 + A2; // C
            4'hd : o_led = A1 + A2 + B + C + D2 + D1 + M + J; // D
            4'he : o_led = A1 + A2 + G1 + D2 + D1 + E + F; // E
            4'hf : o_led = E + F + A1 + A2 + G1; // F
	  endcase
	end

endmodule
// vim: expandtab:sw=4:ts=4:

Комбинаторная схема и нетактуемый регистр на выходе. Это был самый первый мой модуль на Verilog:smile: Вот во что он превращается после компиляции: Реализация индикатора

Было потрачено 13 LUT4 из 1152.

Дребезг контактов

Мой датчик угла поворота имеет весьма шумную в электрическом плане конструкцию: Разобранный датчик угла поворота

Сначала устраним метастабильность на входах, для этого хватит двух флипфлопов

`default_nettype none
module metastab(
    input wire i_clk,
    input wire i_in,
    output wire o_out);

    reg r_stage0;
    reg r_stage1;

    always @(posedge i_clk) begin
        {r_stage1, r_stage0} = {r_stage0, i_in};
    end

    assign o_out = r_stage1;
endmodule
// vim: expandtab:sw=4 ts=4

И код действительно превращается в два флипфлопа:smile:

Анти-метастабильность в железе

Мы избавились от метастабильности, однако это всего лишь гарантирует нам что сигнал будет стабильным в течение тактового импульса и никак не избавляет от паразитных срабатываний контактов.

Дребезг контактор на осциллографе

Одним из вариантов подавления дребезга контактов является использование RC-фильтра в комбинации с триггером Шмитта:

RC фильтр как подавитель дребезга контактов

Фильтр нижних частот сглаживает высокочастотный шум, а триггер Шмитта нужен для того, чтобы корректно отрабатывать входной сигнал в диапазоне от 0.8 до 2 Вольт. Однако, как заметил Rue (https://twitter.com/RueNahcMohr):

Ответ Rue

Он также предложил использовать программную реализацию RC фильтра и триггера Шмитта:

Метод Rue

Практически всё это перекочевало в код, который я в конце концов и использовал. Некоторые пояснения:

`default_nettype none
module Debounce5ms(
        input wire i_clk,
        input wire i_btn,
        output wire o_btn);
    /* 24MHz ~ 42e-9s, for 5ms we need *1e5 ~ 2^16
     * for Schmitt part we can ignore lower ~2^9 */
    localparam TOTAL_BITS = 16;
    localparam HI_BIT = TOTAL_BITS - 1;
    localparam IGNORE_BITS = TOTAL_BITS - 7;
     
    // test bounds
    reg [HI_BIT:0]r_counter;
    wire w_isLow;
    wire w_isHigh;
    assign w_isLow = !(|r_counter[HI_BIT:IGNORE_BITS]);
    assign w_isHigh = &r_counter[HI_BIT:IGNORE_BITS];

    // Capacitor
    always @(posedge i_clk) begin
        if (i_btn) begin 
            if (!w_isHigh) begin
                r_counter <= r_counter + 1'b1;
            end
        end else begin
            if (!w_isLow) begin
                r_counter <= r_counter - 1'b1;
            end    
        end
    end

    // Schmitt
    reg r_debouncedBtn;
    always @(posedge i_clk) begin
        if (w_isLow) begin
            r_debouncedBtn <= 0;
        end
        if (w_isHigh) begin
            r_debouncedBtn <= 1;
        end
    end
    assign o_btn = r_debouncedBtn;
endmodule
// vim: expandtab:ts=4 sw=4

Можно убедиться что генерируемые элементы именно так и соединяются: Два LUT4

К этому моменту мы дошли до стадии когда сложность генерируемой схемы возросла настолько, что далее не имеет смысла приводить её здесь.

Десятичный счётчик

Это очень простой модуль, я даже зачем-то предусмотрел перенос, хотя для данной системы всего с одним индикатором это не нужно. Кроме того граничные условия и проверяются нестрого, это помогает избавиться от невозможных состояний десятичного счётчика, что актуально при отсутствии сигнала RESET. Я не стал заморачиваться с RESET потому что:

`default_nettype none
module DecadeCounter(
		input wire i_clk,
		input wire i_inc,
		input wire i_dec,
		output wire [3:0]o_cnt,
        output wire o_carry);
    reg [3:0]r_cnt;
    reg r_carry;

    always @(posedge i_clk) begin
        if (i_inc) begin
            if (r_cnt >= 'd9) begin
                r_cnt <= 0;
                r_carry <= ~r_carry;
            end else begin
                r_cnt <= r_cnt + 1'b1;
            end
        end
        if (i_dec) begin
            if (r_cnt == 'd0 || r_cnt > 'd9) begin
                r_cnt <= 'd9;
                r_carry <= ~r_carry;
            end else begin
                r_cnt <= r_cnt - 1'b1;
            end
        end
    end
    assign o_cnt = r_cnt;
    assign o_carry = r_carry;

endmodule
// vim: expandtab:sw=4 ts=4

Одновибратор

Вспомогательный модуль, который только и делает, что выдает импульс фиксированной продолжительности по положительному фронту входного сигнала. Возможно и не нужен, но все мои попытки использовать posedge input_signal приводили к истерике компилятора, который почему-то решал, что я хочу создать ещё одни часы:smile:

`default_nettype none
module SingleTick(
    input wire i_clk,
    input wire i_btn,
    output wire o_btn);
    reg r_oldstate;
    reg r_btn;

    always @(posedge i_clk) begin
        r_oldstate <= i_btn;
        r_btn <= i_btn && (i_btn != r_oldstate);
    end

    assign o_btn = r_btn;
endmodule
// vim: expandtab:sw=4 ts=4

Датчик угла

Как работает датчик угла описано в статье. А тут я приведу две картинки оттуда, по которым становится ясно что я тут пытаюсь сделать в программе: Фазы датчика Последовательность состояний датчика

Если коротко, то знаковым для меня является состояние {1, 1}, и я определяю направление вращение в зависимости от того из какого состояния происходит переход в {1, 1}.

`default_nettype none
module Rotary2018(input wire i_clk,
            input wire i_btn_a_,
            input wire i_btn_b_,
            output wire o_cw,
            output wire o_ccw);

        /* (meta)stablize buttons */
        wire w_btn_a;
        wire w_btn_b;
        metastab mbtn0(i_clk, ~i_btn_a_, w_btn_a);
        metastab mbtn1(i_clk, ~i_btn_b_, w_btn_b);

        /* debounce buttons */
        wire w_dbtn_a;
        wire w_dbtn_b;
        Debounce5ms dbtn0(i_clk, w_btn_a, w_dbtn_a);
        Debounce5ms dbtn1(i_clk, w_btn_b, w_dbtn_b);

        reg r_cw;
        reg r_ccw;
        reg r_a;
        reg r_b;

        // is {1, 1}
        wire w_is_signal_state;
        assign w_is_signal_state = w_dbtn_a & w_dbtn_b;

        always @(posedge i_clk) begin
            r_a <= w_dbtn_a;
            r_b <= w_dbtn_b;
            // signal only when change to {1, 1}
            if (w_is_signal_state) begin
                if (!(r_a & r_b)) begin
                    r_cw <= r_a;
                    r_ccw <= r_b;
                end
            end else begin
                r_cw <= 0;
                r_ccw <= 0;
            end
        end

        SingleTick st0(i_clk, r_cw, o_cw);
        SingleTick st1(i_clk, r_ccw, o_ccw);
endmodule
// vim: expandtab:sw=4 ts=4

Небольшой график обнаружения вращения по часовой стрелке: График сигналов

Всё вместе

`default_nettype none
module main(input wire i_clk,
            input wire i_btn_a_,
            input wire i_btn_b_,
            output wire [16:0] o_led);

        wire [3:0] w_counter;
        /* verilator lint_off UNUSED */
        wire w_carry;
        /* verilator lint_on UNUSED */

        /* rotary */
        wire w_cw;
        wire w_ccw;
        Rotary2018 rot0(i_clk, ~i_btn_a_, ~i_btn_b_, w_cw, w_ccw);

        DecadeCounter dcnt0(i_clk, w_cw, w_ccw, w_counter, w_carry);
        
        // display
        Display16 disp0(w_counter, o_led);

endmodule
// vim: expandtab:sw=4 ts=4

Как эта система работает:

Заключение

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

  1. Я использую TangNano производства Sipeed. Можно купить на Али Экспресс