§ Как работает протокол PS/2

Этот материал будет очень обширным, потому что сегодня я буду делать сразу по 3-м направлениям — эмуляцию для верилятора, сам код верилога и немного по физической реализации на отладочной плате. Начну с теории, о том, как передаются данные от клавиатуры к контроллеру.

Схема работы протокола. Есть 2 провода, по которым передаются данные, как от клавиатуры, так и к ней. Передачу данных к клавиатуре я делать пока не буду, а вот от клавиатуры — да.
Большую часть времени оба провода содержат сигнал 1 (притянуты к Vcc). Когда клавиатура начинает передавать данные, она опускает в 0 оба провода, что сигнализирует о начале передачи.
  • Первый такт на позитивном фронте принимает старт-бит, который всегда равен 0 и всегда должен быть равен 0, иначе это ошибка
  • Второй такт принимает бит 0 и записывает его в результирующий регистр
  • Далее, принимаются все оставшиеся биты 1-7
  • После приема битов идет бит четности, который нужен для проверки того, что данные пришли успешно. Бит четности равен 0, если количество принятых единиц было нечетно и 1, если четно
  • Самый последний такт называется "стоп"-бит, на проводе Data всегда должно быть значение 1
После приема одного байта данных, с клавиатуры ничего не должно отсылаться в течении нескольких миллисекунд.

§ Событие нажатия кнопки в эмуляторе

Теперь, давайте добавим обработчик события при нажатии на кнопку. Когда на клавиатуре нажимается кнопка, то по протоколу PS2 отсылается ее сканкод. Я постарался обработать как можно больше клавиш и записал все, что смог найти.
1// Сканирование нажатой клавиши
2// https://ru.wikipedia.org/wiki/Скан-код
3void kbd_scancode(int scancode, int release) {
4
5    switch (scancode) {
6
7        // Коды клавиш A-Z
8        case SDL_SCANCODE_A: if (release) kbd_push(0xF0); kbd_push(0x1C); break;
9        case SDL_SCANCODE_B: if (release) kbd_push(0xF0); kbd_push(0x32); break;
10        case SDL_SCANCODE_C: if (release) kbd_push(0xF0); kbd_push(0x21); break;
11        case SDL_SCANCODE_D: if (release) kbd_push(0xF0); kbd_push(0x23); break;
12        case SDL_SCANCODE_E: if (release) kbd_push(0xF0); kbd_push(0x24); break;
13        case SDL_SCANCODE_F: if (release) kbd_push(0xF0); kbd_push(0x2B); break;
14        case SDL_SCANCODE_G: if (release) kbd_push(0xF0); kbd_push(0x34); break;
15        case SDL_SCANCODE_H: if (release) kbd_push(0xF0); kbd_push(0x33); break;
16        case SDL_SCANCODE_I: if (release) kbd_push(0xF0); kbd_push(0x43); break;
17        case SDL_SCANCODE_J: if (release) kbd_push(0xF0); kbd_push(0x3B); break;
18        case SDL_SCANCODE_K: if (release) kbd_push(0xF0); kbd_push(0x42); break;
19        case SDL_SCANCODE_L: if (release) kbd_push(0xF0); kbd_push(0x4B); break;
20        case SDL_SCANCODE_M: if (release) kbd_push(0xF0); kbd_push(0x3A); break;
21        case SDL_SCANCODE_N: if (release) kbd_push(0xF0); kbd_push(0x31); break;
22        case SDL_SCANCODE_O: if (release) kbd_push(0xF0); kbd_push(0x44); break;
23        case SDL_SCANCODE_P: if (release) kbd_push(0xF0); kbd_push(0x4D); break;
24        case SDL_SCANCODE_Q: if (release) kbd_push(0xF0); kbd_push(0x15); break;
25        case SDL_SCANCODE_R: if (release) kbd_push(0xF0); kbd_push(0x2D); break;
26        case SDL_SCANCODE_S: if (release) kbd_push(0xF0); kbd_push(0x1B); break;
27        case SDL_SCANCODE_T: if (release) kbd_push(0xF0); kbd_push(0x2C); break;
28        case SDL_SCANCODE_U: if (release) kbd_push(0xF0); kbd_push(0x3C); break;
29        case SDL_SCANCODE_V: if (release) kbd_push(0xF0); kbd_push(0x2A); break;
30        case SDL_SCANCODE_W: if (release) kbd_push(0xF0); kbd_push(0x1D); break;
31        case SDL_SCANCODE_X: if (release) kbd_push(0xF0); kbd_push(0x22); break;
32        case SDL_SCANCODE_Y: if (release) kbd_push(0xF0); kbd_push(0x35); break;
33        case SDL_SCANCODE_Z: if (release) kbd_push(0xF0); kbd_push(0x1A); break;
34
35        // Цифры
36        case SDL_SCANCODE_0: if (release) kbd_push(0xF0); kbd_push(0x45); break;
37        case SDL_SCANCODE_1: if (release) kbd_push(0xF0); kbd_push(0x16); break;
38        case SDL_SCANCODE_2: if (release) kbd_push(0xF0); kbd_push(0x1E); break;
39        case SDL_SCANCODE_3: if (release) kbd_push(0xF0); kbd_push(0x26); break;
40        case SDL_SCANCODE_4: if (release) kbd_push(0xF0); kbd_push(0x25); break;
41        case SDL_SCANCODE_5: if (release) kbd_push(0xF0); kbd_push(0x2E); break;
42        case SDL_SCANCODE_6: if (release) kbd_push(0xF0); kbd_push(0x36); break;
43        case SDL_SCANCODE_7: if (release) kbd_push(0xF0); kbd_push(0x3D); break;
44        case SDL_SCANCODE_8: if (release) kbd_push(0xF0); kbd_push(0x3E); break;
45        case SDL_SCANCODE_9: if (release) kbd_push(0xF0); kbd_push(0x46); break;
46
47        // Keypad
48        case SDL_SCANCODE_KP_0: if (release) kbd_push(0xF0); kbd_push(0x70); break;
49        case SDL_SCANCODE_KP_1: if (release) kbd_push(0xF0); kbd_push(0x69); break;
50        case SDL_SCANCODE_KP_2: if (release) kbd_push(0xF0); kbd_push(0x72); break;
51        case SDL_SCANCODE_KP_3: if (release) kbd_push(0xF0); kbd_push(0x7A); break;
52        case SDL_SCANCODE_KP_4: if (release) kbd_push(0xF0); kbd_push(0x6B); break;
53        case SDL_SCANCODE_KP_5: if (release) kbd_push(0xF0); kbd_push(0x73); break;
54        case SDL_SCANCODE_KP_6: if (release) kbd_push(0xF0); kbd_push(0x74); break;
55        case SDL_SCANCODE_KP_7: if (release) kbd_push(0xF0); kbd_push(0x6C); break;
56        case SDL_SCANCODE_KP_8: if (release) kbd_push(0xF0); kbd_push(0x75); break;
57        case SDL_SCANCODE_KP_9: if (release) kbd_push(0xF0); kbd_push(0x7D); break;
58
59        // Специальные символы
60        case SDL_SCANCODE_GRAVE:        if (release) kbd_push(0xF0); kbd_push(0x0E); break;
61        case SDL_SCANCODE_MINUS:        if (release) kbd_push(0xF0); kbd_push(0x4E); break;
62        case SDL_SCANCODE_EQUALS:       if (release) kbd_push(0xF0); kbd_push(0x55); break;
63        case SDL_SCANCODE_BACKSLASH:    if (release) kbd_push(0xF0); kbd_push(0x5D); break;
64        case SDL_SCANCODE_LEFTBRACKET:  if (release) kbd_push(0xF0); kbd_push(0x54); break;
65        case SDL_SCANCODE_RIGHTBRACKET: if (release) kbd_push(0xF0); kbd_push(0x5B); break;
66        case SDL_SCANCODE_SEMICOLON:    if (release) kbd_push(0xF0); kbd_push(0x4C); break;
67        case SDL_SCANCODE_APOSTROPHE:   if (release) kbd_push(0xF0); kbd_push(0x52); break;
68        case SDL_SCANCODE_COMMA:        if (release) kbd_push(0xF0); kbd_push(0x41); break;
69        case SDL_SCANCODE_PERIOD:       if (release) kbd_push(0xF0); kbd_push(0x49); break;
70        case SDL_SCANCODE_SLASH:        if (release) kbd_push(0xF0); kbd_push(0x4A); break;
71        case SDL_SCANCODE_BACKSPACE:    if (release) kbd_push(0xF0); kbd_push(0x66); break;
72        case SDL_SCANCODE_SPACE:        if (release) kbd_push(0xF0); kbd_push(0x29); break;
73        case SDL_SCANCODE_TAB:          if (release) kbd_push(0xF0); kbd_push(0x0D); break;
74        case SDL_SCANCODE_CAPSLOCK:     if (release) kbd_push(0xF0); kbd_push(0x58); break;
75        case SDL_SCANCODE_LSHIFT:       if (release) kbd_push(0xF0); kbd_push(0x12); break;
76        case SDL_SCANCODE_LCTRL:        if (release) kbd_push(0xF0); kbd_push(0x14); break;
77        case SDL_SCANCODE_LALT:         if (release) kbd_push(0xF0); kbd_push(0x11); break;
78        case SDL_SCANCODE_RSHIFT:       if (release) kbd_push(0xF0); kbd_push(0x59); break;
79        case SDL_SCANCODE_RETURN:       if (release) kbd_push(0xF0); kbd_push(0x5A); break;
80        case SDL_SCANCODE_ESCAPE:       if (release) kbd_push(0xF0); kbd_push(0x76); break;
81        case SDL_SCANCODE_NUMLOCKCLEAR: if (release) kbd_push(0xF0); kbd_push(0x77); break;
82        case SDL_SCANCODE_KP_MULTIPLY:  if (release) kbd_push(0xF0); kbd_push(0x7C); break;
83        case SDL_SCANCODE_KP_MINUS:     if (release) kbd_push(0xF0); kbd_push(0x7B); break;
84        case SDL_SCANCODE_KP_PLUS:      if (release) kbd_push(0xF0); kbd_push(0x79); break;
85        case SDL_SCANCODE_KP_PERIOD:    if (release) kbd_push(0xF0); kbd_push(0x71); break;
86        case SDL_SCANCODE_SCROLLLOCK:   if (release) kbd_push(0xF0); kbd_push(0x7E); break;
87
88        // F1-F12 Клавиши
89        case SDL_SCANCODE_F1:   if (release) kbd_push(0xF0); kbd_push(0x05); break;
90        case SDL_SCANCODE_F2:   if (release) kbd_push(0xF0); kbd_push(0x06); break;
91        case SDL_SCANCODE_F3:   if (release) kbd_push(0xF0); kbd_push(0x04); break;
92        case SDL_SCANCODE_F4:   if (release) kbd_push(0xF0); kbd_push(0x0C); break;
93        case SDL_SCANCODE_F5:   if (release) kbd_push(0xF0); kbd_push(0x03); break;
94        case SDL_SCANCODE_F6:   if (release) kbd_push(0xF0); kbd_push(0x0B); break;
95        case SDL_SCANCODE_F7:   if (release) kbd_push(0xF0); kbd_push(0x83); break;
96        case SDL_SCANCODE_F8:   if (release) kbd_push(0xF0); kbd_push(0x0A); break;
97        case SDL_SCANCODE_F9:   if (release) kbd_push(0xF0); kbd_push(0x01); break;
98        case SDL_SCANCODE_F10:  if (release) kbd_push(0xF0); kbd_push(0x09); break;
99        case SDL_SCANCODE_F11:  if (release) kbd_push(0xF0); kbd_push(0x78); break;
100        case SDL_SCANCODE_F12:  if (release) kbd_push(0xF0); kbd_push(0x07); break;
101
102        // Расширенные клавиши
103        case SDL_SCANCODE_LGUI:         kbd_push(0xE0); if (release) kbd_push(0xF0); kbd_push(0x1F); break;
104        case SDL_SCANCODE_RGUI:         kbd_push(0xE0); if (release) kbd_push(0xF0); kbd_push(0x27); break;
105        case SDL_SCANCODE_APPLICATION:  kbd_push(0xE0); if (release) kbd_push(0xF0); kbd_push(0x2F); break;
106        case SDL_SCANCODE_RCTRL:        kbd_push(0xE0); if (release) kbd_push(0xF0); kbd_push(0x14); break;
107        case SDL_SCANCODE_RALT:         kbd_push(0xE0); if (release) kbd_push(0xF0); kbd_push(0x11); break;
108        case SDL_SCANCODE_KP_DIVIDE:    kbd_push(0xE0); if (release) kbd_push(0xF0); kbd_push(0x4A); break;
109        case SDL_SCANCODE_KP_ENTER:     kbd_push(0xE0); if (release) kbd_push(0xF0); kbd_push(0x5A); break;
110
111        case SDL_SCANCODE_INSERT:       kbd_push(0xE0); if (release) kbd_push(0xF0); kbd_push(0x70); break;
112        case SDL_SCANCODE_HOME:         kbd_push(0xE0); if (release) kbd_push(0xF0); kbd_push(0x6C); break;
113        case SDL_SCANCODE_END:          kbd_push(0xE0); if (release) kbd_push(0xF0); kbd_push(0x69); break;
114        case SDL_SCANCODE_PAGEUP:       kbd_push(0xE0); if (release) kbd_push(0xF0); kbd_push(0x7D); break;
115        case SDL_SCANCODE_PAGEDOWN:     kbd_push(0xE0); if (release) kbd_push(0xF0); kbd_push(0x7A); break;
116        case SDL_SCANCODE_DELETE:       kbd_push(0xE0); if (release) kbd_push(0xF0); kbd_push(0x71); break;
117
118        case SDL_SCANCODE_UP:           kbd_push(0xE0); if (release) kbd_push(0xF0); kbd_push(0x75); break;
119        case SDL_SCANCODE_DOWN:         kbd_push(0xE0); if (release) kbd_push(0xF0); kbd_push(0x72); break;
120        case SDL_SCANCODE_LEFT:         kbd_push(0xE0); if (release) kbd_push(0xF0); kbd_push(0x6B); break;
121        case SDL_SCANCODE_RIGHT:        kbd_push(0xE0); if (release) kbd_push(0xF0); kbd_push(0x74); break;
122
123        // Клавиша PrnScr
124        case SDL_SCANCODE_PRINTSCREEN: {
125
126            if (release == 0) {
127
128                kbd_push(0xE0); kbd_push(0x12);
129                kbd_push(0xE0); kbd_push(0x7C);
130
131            } else {
132
133                kbd_push(0xE0); kbd_push(0xF0); kbd_push(0x7C);
134                kbd_push(0xE0); kbd_push(0xF0); kbd_push(0x12);
135            }
136
137            break;
138        }
139
140        // Клавиша Pause
141        case SDL_SCANCODE_PAUSE: {
142
143            kbd_push(0xE1);
144            kbd_push(0x14); if (release) kbd_push(0xF0); kbd_push(0x77);
145            kbd_push(0x14); if (release) kbd_push(0xF0); kbd_push(0x77);
146            break;
147        }
148    }
149}
Этот метод встраивается в класс App.
Первым аргументом идет int scancode — он приходит из события SDL2. Второй аргумент int release равен либо 0 — что означает, что клавиша нажата, либо 1 — клавиша отпущена.
Когда нажимается клавиша, то, в зависимости от того, какой номер был у этой клавиши, отсылается некоторый код клавиши. Он может не один, есть клавиши, которые отсылают целую последовательность кодов.
К примеру, если нажать пробел, то будет отослан код 0x29. Если пробел отпустить — то будет отослал код 0xF0, а потом 0x29. В случае нажатия клавиши "Направо", будет отосланы 0xE0, 0x74, а если отпустить, то 0xE0, 0xF0, 0x74. При разработке программного обеспечения, которое это будет обрабатывать, надо это учитывать.
Можно заметить, что используется метод kbd_push, который занимается тем, что добавляет новый символ в очередь на отправку:
1void kbd_push(int data) {
2
3    if (kbd_top >= 255) return;
4    kbd[kbd_top] = data;
5    kbd_top++;
6}
В очередь kbd будет добавлен новый код, и увеличена высота очереди. Максимальное количество элементов в очереди 256.
И да, надо объявить новые переменные и буферы в зоне protected класса App:
1unsigned char kbd[256];
2int kbd_top, kbd_phase, kbd_ticker;
  • kbd[256] — очередь
  • kbd_top — верх очереди
  • kbd_phase и kbd_ticker — необходимы для симуляции протокола ps2
В конструкторе класса надо обязательно инициализировать переменные:
1kbd_top     = 0;
2kbd_phase   = 0;
3kbd_ticker  = 0;
Необходимо добавить код, который будет отслеживать новые события, которые приходят от SDL2, код добавляется в метод main():
1// Прием событий
2while (SDL_PollEvent(& evt)) {
3
4    switch (evt.type) {
5
6        case SDL_QUIT:
7            return 0;
8
9        case SDL_KEYDOWN:
10
11            kbd_scancode(evt.key.keysym.scancode, 0);
12            break;
13
14        case SDL_KEYUP:
15
16            kbd_scancode(evt.key.keysym.scancode, 1);
17            break;
18    }
19}
При появлении события SDL_KEYDOWN (нажата клавиша), будет вызван kbd_scancode с параметром 0, который добавил в очередь сканкод, основанный на сканкоде SDL. При отпускании клавиши, будет вызвал kbd_scancode с параметром 1.
Итак, приготовления готовы, при нажатии и отпускании клавиш добавляется в очередь, но теперь нужно выполнить обработку этих данных, симулировать отсылку их по протоколу PS2.
Задача такая. На каждый такт (25 мгц) вызывается метод, который устанавливает ps2_clock и ps2_data в нужные значения, симулируя протокол. Вот и всё.
1int ps_clock = 1, ps_data = 1;
2
3while (app->main()) {
4...
5  app->kbd_pop(ps_clock, ps_data);
6...
7}
Первоначально, ps_clock и ps_data должны быть равны 1.
Рассмотрим то, как будет работать метод kbd_pop, который работает на каждом такте (25 Мгц):
1void kbd_pop(int& ps_clock, int& ps_data) {
2
3    // В очереди нет клавиш для нажатия
4    if (kbd_top == 0) return;
5
6    // 25000000/2000 = 12.5 kHz Очередной полутакт для PS/2
7    if (++kbd_ticker >= 2000) {
8
9        ps_clock = kbd_phase & 1;
10        ...
11        kbd_ticker = 0;
12        kbd_phase++;
13    }
14}
Как видно из кода, если в очереди нет больше данных, то есть kbd_top == 0, то ничего не делается. Если данные есть, то спустя 2000 тактов (работает клавиатура не быстро), будет установлен ps_clock = 0 или 1. Сначала ставится 0, потом после kbd_phase++ становится 1, и так далее. Фаза kbd_phase необходима, чтобы точно знать, в какой момент и что выдавать в ps_data. Таймер kbd_ticker сбрасывается в 0, чтобы через 2000 тактов снова сделать полутакт.
1switch (kbd_phase) {
2
3    // Старт-бит [=0]
4    case 0: case 1: ps_data = 0; break;
5
6    // Бит четности
7    case 18: case 19:
8
9        ps_data = 1;
10        for (int i = 0; i < 8; i++)
11            ps_data ^= !!(kbd[0] & (1 << i));
12
13        break;
14
15    // Стоп-бит [=1]
16    case 20: case 21: ps_data = 1; break;
17
18    // Небольшая задержка между нажатиями клавиш
19    case 22: case 23:
20    case 24: case 25:
21
22        ps_clock = 1;
23        ps_data  = 1;
24        break;
25
26    // Завершение
27    case 26:
28
29        // Удалить символ из буфера
30        for (int i = 0; i < kbd_top - 1; i++)
31            kbd[i] = kbd[i+1];
32
33        kbd_top--;
34        kbd_phase = -1;
35        ps_clock  = 1;
36        break;
37
38    // Отсчет битов от 0 до 7
39    // 0=2,3   | 1=4,5   | 2=6,7   | 3=8,9
40    // 4=10,11 | 5=12,13 | 6=14,15 | 7=16,17
41    default:
42
43        ps_data = !!(kbd[0] & (1 << ((kbd_phase >> 1) - 1)));
44        break;
45}
Вот здесь представлен код основной части симулятора протокола.
  • Для полутактов 0 и 1 на шину ps_data выставляется 0 — старт-бит
  • Далее идут пары полутактов от 2 до 17 включительно (8 бит). На шину данных выставляется последовательно биты от 0 до 7 (начиная с 0) из первого символа в буфере kbd, очередь работает в режиме FIFO
  • Полутакты 18,19 выставляют бит четности, который равен 1, если количество единиц в отправленном символе было четно, и 0, если нет
  • В 20,21 записывается стоп-бит, ps_data=1, это последний такт, после него ps_data=1, ps_clock=1
  • В течении 22-26 полутакта это состояние продлевается
  • На 26 полутакте из буфера kbd удаляется первый символ и очередь уменьшается на -1. В данном случае ставится kbd_phase=-1, потому что в конце метода идет инкремент kbd_phase, и он снова становится равным 0.
Собственно, на этом вся логика работы данного кода и заканчивается. Симулятор протокола PS/2 готов. Теперь остается сделать обработчик данных на верилоге, встроить в верилятор и начать работу.
Но этот материал уже в следующей статье.
Скачать код можно здесь.