§ Адаптер на верилоге

В прошлой части я рассказал о том, как работает протокол PS/2 и написал его симуляцию на языке С. Сегодня я разберу адаптер для верилога, который будет просимулирован как с помощью верилятора, так и в реальной схеме (синтезирован для ПЛИС).
Начнем с описания портов (файл ps2.v):
1module ps2
2(
3    input   clock,           // Тактовая частота 25 Мгц
4    input   ps_clock,        // Пин, подключенный к проводу CLOCK с PS/2
5    input   ps_data,         // Пин DATA
6
7    output reg       done,   // Устанавливается =1, если данные доступны
8    output reg [7:0] data    // Принятый байт с PS/2
9);
10...
11endmodule
На выходе я установил регистры, а значит, их надо инициализировать в 0:
1initial begin data = 8'h00; done = 1'b0; end
Теперь же, рассмотрим, какие регистры будут использоваться в модуле
1reg         kbusy   = 1'b0;   // =1 Если идет прием данных с пина (шины) DATA
2reg         kdone   = 1'b0;   // =1 Прием сигнала завершен, "фантомный" регистр к `done`
3reg [1:0]   klatch  = 2'b00;  // Сдвиговый регистр для отслеживания позитивного и негативного фронта CLOCK
4reg [3:0]   kcount  = 1'b0;   // Номер такта CLOCK
5reg [9:0]   kin     = 1'b0;   // Сдвиговый регистр для приема данных с DATA
6reg [19:0]  kout    = 1'b0;   // Отсчет таймаута для "зависшего" приема данных в случае ошибки
Принцип работы модуля такой.
  • Если текущее состояние модуля kbusy = 0, то тогда ничего не делать, ждать того момента, когда CLOCK перейдет из состояния 1 в состояние 0. Если это произошло, то установить kbusy = 1
  • В состоянии приема сигналов ожидать позитивного фронта CLOCK. Как только он будет получен, в сдвиговый регистр kin засылать новый бит с DATA, а также увеличить счетчик kcount + 1
  • Если счетчик будет равен kcount=10, то это значит, все необходимые биты приняты (старт-бит + 8 бит информации + бит четности), и потому на этом такте ставится done=1, но на следующем такте done становится равным 0, и также записывается в data принятое значение
  • В случае, если произошла ошибка, то есть, бит четности не совпадает с принятыми данными, done не ставить 1
  • Также, если прошло очень много времени во время приема сигнала, и CLOCK стабильно 1, то тогда сбросить состояние в kbusy=0. Вероятно, на линии произошла ошибка.
Разработав этот алгоритм, приступим к реализации:
1always @(posedge clock) begin
2
3    kdone <= 1'b0;
4
5    // Процесс приема сигнала
6    if (kbusy) begin
7
8        ...
9
10    end else begin
11
12        // Обнаружен негативный фронт \__
13        if (klatch == 2'b10) begin
14
15            kbusy   <= 1'b1; // Активировать прием данных
16            kcount  <= 1'b0; // Сброс двух счетчиков в 0
17            kout    <= 1'b0;
18
19        end
20
21    end
22
23    klatch <= {klatch[0], ps_clock};
24
25end
При каждом такте (25 мгц), значение kdone сбрасывается в 0, кроме того случая, когда сигнал был только что принят. В коде реализован сдвиговый регистр klatch, который вдвигает биты CLOCK справа налево. Этот регистр необходим для отслеживания позитивного фронта (klatch=2'b01) или негативного фронта (klatch=2'b10).
Если kbusy=0, то при появлении негативного фронта обнуляются все счетчики и на следующем такте (25 мгц), уже переходит к ожиданию сигнала CLOCK:
1// Позитивный фронт
2if (klatch == 2'b01) begin
3
4    // Завершающий такт
5    if (kcount == 4'hA) begin
6
7        data    <= kin[8:1];
8        kbusy   <= 1'b0;
9        kdone   <= ^kin[9:1]; // =1 Если четность совпадает
10
11    end
12
13    kcount  <= kcount + 1'b1;
14    kin     <= {ps_data, kin[9:1]};
15
16end
17
18// Считать "зависший процесс"
19kout <= ps_clock ? kout + 1 : 1'b0;
20
21// И если прошло более 20 мс, то перевести в состояние ожидания
22if (kout > 25000*20) kbusy <= 1'b0;
Поясню, что при 10-м такте от PS/2, необходимые данные будут находится в сдвиговом регистре kin[8:1], потому что в бите kin[0] находится старт-бит (всегда 0), а в бите kin[9] будет находится бит четности.
В kdone записывается побитовое XOR над всеми битами, включая бит четности. В случае совпадения четности, в kdone окажется 1. Это происходит по причине того, что бит четности вычисляется как ~^data[7:0], то есть, происходит инверсия, которая и оказывает хорошую услугу при вычислении корректности входящих данных.
Сдвиговый регистр kin работает слева направо, то есть, вдвигается сначала старший бит (MSB), а не младший, как в klatch. Таким образом, первый пришедший бит окажется в младших битах kin. Последний бит (четности), окажется в старшем бите kin (в бите 9).
Далее, вне зависимости от клока, будет считаться kout. Он увеличивается на +1 если CLOCK=1. И если такое состояние ожидания очень долго находится (25000*20, что равно 20 мс), то kbusy сбрасывается в режим ожидания.
И последний штрих:
1always @(negedge clock) done <= kdone;
Этот код нужен для того, чтобы переписывать kdone в done на негативном фронте. Это нужно для того, чтобы data успел сформироваться, и чтобы done пришел вовремя без гонки состоянии. То есть, done будет готов на негативном фронте clock, через некоторое время, на следующем позитивном фронте процессор например, сможет принять данные и обработать их без ошибок и без конфликтов времени.

§ Тестбенч

Для того, чтобы проверить работоспособность модуля и выполнить его отладку, я написал тестбенч для icarus verilog.
1`timescale 10ns / 1ns
2module tb;
3
4reg           clock;
5always  #0.5  clock = ~clock;
6initial begin clock = 0; #2000 $finish; end
7initial begin $dumpfile("tb.vcd"); $dumpvars(0, tb); end
8
9// ----------------
10reg ps_clock = 1'b1;
11reg ps_data  = 1'b1;
12
13wire [7:0] keytest = 8'hF0;
14
15initial begin
16
17    // Старт-бит
18    #8 ps_clock = 1'b0; ps_data = 1'b0;
19    #3 ps_clock = 1'b1;
20    // Биты данных
21    #3 ps_clock = 1'b0; ps_data = keytest[0]; #3 ps_clock = 1'b1;
22    #3 ps_clock = 1'b0; ps_data = keytest[1]; #3 ps_clock = 1'b1;
23    #3 ps_clock = 1'b0; ps_data = keytest[2]; #3 ps_clock = 1'b1;
24    #3 ps_clock = 1'b0; ps_data = keytest[3]; #3 ps_clock = 1'b1;
25    #3 ps_clock = 1'b0; ps_data = keytest[4]; #3 ps_clock = 1'b1;
26    #3 ps_clock = 1'b0; ps_data = keytest[5]; #3 ps_clock = 1'b1;
27    #3 ps_clock = 1'b0; ps_data = keytest[6]; #3 ps_clock = 1'b1;
28    #3 ps_clock = 1'b0; ps_data = keytest[7]; #3 ps_clock = 1'b1;
29    // Бит четности
30    #3 ps_clock = 1'b0; ps_data = ~^keytest; #3 ps_clock = 1'b1;
31    // Стоп-бит
32    #3 ps_clock = 1'b0; ps_data = 1'b1; #3 ps_clock = 1'b1;
33
34end
35// ----------------
36
37ps2 Keyb
38(
39    .clock      (clock),
40    .ps_clock   (ps_clock),
41    .ps_data    (ps_data)
42);
43
44endmodule
Вначале, тестовые входящие регистры ps_clock и ps_data устанавливаются в 1, а потом, следуя логике в блоке initial, им ставятся разные значения.
#8 ps_clock = 1'b0; ps_data = 1'b0; Этот блок значит, что спустя 8x10 нс после начала, установить ps_clock и ps_data в 0
#3 ps_clock = 1'b1; спустя 30 нс, установить в ps_clock в 1
И так далее. В качестве входящих данных используется значение регистра keytest. В тестбенче также считается бит четности ps_data = ~^keytest; и выставляется стоп-бит.
Так этот процесс выглядит в программе просмотра временных диаграмм gtkwave:

Как видно, сначала идет старт-бит 0, потом четыре 0, далее четыре 1, бит четности (1 — количество единиц четно), и стоп-бит 1, всего 11 тактов.

§ Встраивание модуля верилятора

Теперь стоит проверить, как работает модуль, используя верилятор.
1WARN=-Wall -Wno-unused -Wno-width -Wno-caseincomplete
2tbc: verilate
3	g++ -o tb -I$(VINC) tb.cc $(VINC)/verilated.cpp obj_dir/Vvga__ALL.a obj_dir/Vps2__ALL.a -lSDL2
4	./tb
5verilate:
6	verilator $(WARN) -cc vga.v
7	verilator $(WARN) -cc ps2.v
8	cd obj_dir && make -f Vvga.mk
9	cd obj_dir && make -f Vps2.mk
Это часть команд из makefile. Вместо одного модуля, теперь компилируется два. Я вынес в $(WARN) опции по удалению warnings, которые генерирует верилятор из-за слегка неаккуратного кода на верилоге.
В опциях к g++ добавляется только строка obj_dir/Vps2__ALL.a
Теперь же встроим код в tb.cc:
1#include "obj_dir/Vps2.h"
2...
3
4Vvga* top = new Vvga;
5Vps2* ps2 = new Vps2;
6
7while (app->main()) {
8    ...
9    app->kbd_pop(ps_clock, ps_data);
10
11    ps2->ps_clock = ps_clock;
12    ps2->ps_data  = ps_data;
13    ...
14    ps2->clock = 0; ps2->eval();
15    ps2->clock = 1; ps2->eval();
16    ...
17    if (ps2->done) printf("%02x ", ps2->data);
18    ...
19}
По идее, и все, что надо сделать. Получается, что добавился новый include, был создан новый объект класс Vps2, и через eval() запущен в работу, обеспечивая ту же самую частоту 25 мгц, что и видеоадаптеру.
Сразу же после kbd_pop, значение ps_clock и ps_data присваиваются входам модуля ps2.
Также, добавлена строка, при которой при появлении done на выходе ps2, будет в консоль выведена отладочная информация о принятом скан-коде от виртуальной клавиатуры.
Пожалуй, на этом можно и завершить эту тему. Единственное, что могу добавить, это то, как принимать данные от модуля:
1reg [7:0] data;
2always @(posedge clock_25)
3    if (ps_done)
4        data <= ps_data;
Это пример, как принимаются данные от модуля.
Как и обычно, скачать файлы можно здесь.