Снова здравствуйте, дамы и господа. Наш Отдел Перспективных Разработок обещал вас порадовать очередной странной вещью, и потому продолжает свой цикл статей о старых, но иногда полезных устройствах.

Суть сегодняшнего предмета для изучения опять сугубо академическая. Хотя бы просто потому, что стандарт PS/2 уже фактически не нужен, и разработчики железа оставляют его порой разве что для возможности 1.5 старых ценителей подключить свои древние замызганные, но такие привычные клавиатуры и мышки.

Однако, сама концепция клавиатуры как устройства ввода не меняется в зависимости от протокола связи с основным компьютерным устройством – будь то PS/2 или USB. Это вся та же матрица клавиш, которые опрашиваются по пересечению контактной сетки, и контроллер клавиатуры, в зависимости от совпавшего пересечения, отправляет в порт компьютера кодированный скан-код клавиши, как при нажатии так и при отпускании.

Лично я взялся за подобную разработку по одной простой причине: Потребовалась клавиатура для самодельного компьютера Z80-MBC2. Схемы видеокарты вывода для него собираются на достаточно мощных чипах (особенно, по сравнению с самим компьютером, что довольно иронично) stm32 или esp32, и которые вполне тянут параллельно с выводом информации и обработку ввода, например, с порта PS/2.

Сам компьютер довольно небольших размеров, плюс, мне захотелось сделать клавиатуру именно компактных карманных размеров (для реализации своего плана создания впоследствии кибердеки на базе упомянутого Z80-MBC2), а все стандартные полноразмерные клавиатуры из магазина слишком огромные.

Законченных же схем клавиатур выложенных энтузиастами в интернетах не найти, особенно, PS/2.

Ну что же, сделаем что-то сами.

Теория

Схема работы клавиатурного процессора складывается по следующей схеме:

  • Циклично опрашивая сетку пересечений клавишных проводников, определить пересечение, возникающее при нажатой клавише, отправить ее код через порт и запомнить клавишу;

  • При определении факта ранее запомненной и отпущенной клавиши, отправить ее код отпускания;

  • Ожидать данные с порта на прием, и при их поступлении, интерпретировать полученные коды управления для принудительного задания параметров клавиатуры – тайм-аута повторения клавиш, включения/выключения светодиодов и т.п.

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

Протокол PS/2 предполагает передачу данных по двухпроводной шине (Как обычно, DATA и CLOCK) стробами из последовательностей битов.

Частота линии CLOCK, по разным источникам, может приниматься от 10 до 16.7кГц. Точнее и подробнее можно почитать про это в описаниях к драйверу и спецификациях к микросхеме контроллера 8048/8042, который изначально и предоставлял реализацию порта PS/2 для устройств. Сейчас же он эмулируется другими средствами, и варианты частот CLOCK могут варьироваться в еще более широких пределах. Лично я, в процессе написания статьи, игрался уже с готовым устройством и повышал частоту CLOCK до 20кгц, и всё вполне работало.

Для передачи по DATA от клавиатуры в компьютер потребуется соблюсти следующие требования:

  • Ожидать, пока по линии CLOCK не появится логическая «1» в районе 50 мкс, далее, последовательно;

  • Передать по DATA старт-бит (всегда 1);

  • Передать последовательно 8 бит данных;

  • Передать бит четности;

  • Передать стоп-бит (всегда 1).

Передача данных так же осуществляется и в обратную сторону. Компьютер может задать клавиатуре состояние ее параметров принудительно. Для этого передача выглядит чуть сложнее:

  • CLOCK ставится компьютером в «0» на 100 мкс;

  • Компьютером устанавливается по DATA логический 0;

  • Компьютер устанавливает по CLOCK логическую 1, которая фиксируется клавиатурой как старт-бит;

  • Далее клавиатура сама формирует сигнал CLOCK, а компьютер передает согласно нему последовательность из 8 бит, затем бит четности, и стоп-бит;

  • Клавиатура посылает по DATA бит 0, обозначая подтверждение конца приема.

Теперь поднимемся на уровень выше и разберем, что же вообще передается и принимается клавиатурой.

Касаясь отправки данных самой клавиатурой, здесь все просто:

У каждой клавиши есть свой скан-код нажатия и скан-код отпускания. В классическом случае скан-коды однобайтные при нажатии, но, при отпускании используется двухбайтный код, состоящий из флага отпускания F0 и скан-кода нажатия той же клавиши. Некоторые специальные клавиши имеют многобайтные коды, в основном, двухбайтные. В отдельных случаях длина последовательности может составлять даже 8 байт (например, как у клавиши Pause/Break). Коды специальных клавиш, как правило, начинаются с первого байта E0.

Весь список скан-кодов нет смысла засовывать сюда в статью, его всегда можно найти в интернетах.

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

На каждый принятый байт клавиатура должна ответить. Ответом может быть в случае успешного приема, FE в случае неуспешного (например, несовпадающего CRC), дабы компьютер повторил отправку, и FC в случае иной (какой???) ошибки.

Первый из принятых байтов интерпретируется как команда:

ED – сообщает о том, что следующий байт содержит байт состояния индикаторов (светодиодов).

F3 – Сообщает о том, что следующий байт кодирует длительность повтора нажатой клавиши.

F2 – Запрашивает двухбайтный код идентификатора оборудования. Данный код прилетает в момент идентификации оборудования компьютером и его не лишне было бы обрабатывать чтобы сообщить, что клавиатура подключена. В ответ на команду, собственно, клавиатура должна отправить двухбайтный код оборудования, например, AB83. Гугл говорит, что это answer of a standard Cherry keyboard, ну, пусть так и будет.

Кроме того, клавиатура может сообщить в любой момент компьютеру команду AA, означающую нормальную готовность клавиатуры к работе после включения питания.

Подробно все коды команд нет смысла рассматривать в рамках статьи, их и их обработку можно найти, например, в интернетах, в библиотеке для работы с PS/2 к какому-нибудь контроллеру. В моем случае, я так же не писал всю обработку с нуля, а за основу взял первую попавшуюся библиотеку PS2Dev. Но, об этом далее.

Аппаратная часть

Реализацию в железе я начал с того, что обычным образом нашел в столе – микросхемку контроллера Attiny2313A и жмень сдвиговых регистров 74HC595 и 74HC165.

Attiny2313A поначалу показался мне не очень подходящим выбором в плане размера доступной памяти, но, впоследствии его впритык таки хватило. Кроме того, на борту был встроенный кварц на 8мгц. А большего оказалось и не нужно.

Линии пересечений кнопок я реализовал на выходных и входных сдвиговых регистрах. Схема эта была давно уже отработана для самых разных случаев, потому, я просто накидал уже готовую схему в KiCad, не заморачиваясь с тестовыми реализациями. Схема рабочая, мамой клянус.

Поскольку клавиш для адресации предполагалось больше чем 64 (т.е. 8 ног выходного регистра с пересечением 8 ног входного регистра), я применил по два составных 16-битных регистра, что позволило адресовать уже 256 клавиш (т.е. 16 ног на 16 ног). Впоследствии при проектировании клавиатуры выяснилось, что столько клавиш просто не нужно, и я добавил возможность варьировать длину входного регистра – т.е. применять 8-битный регистр из одной микросхемы либо 16-битный из двух. В прошивке же полная поддержка всех 256 клавиш вполне поддерживается, потому, плату клавиатуры всегда можно перепроектировать для поддержки большего количества клавиш.

Сетка образованная пересечением выходных линий представлена на следующей схеме:

Схема под катом

На схеме в левой части два последовательно соединенных регистра 74HC595 образуют 16-битный регистр линий выходного сигнала.

В нижней части схемы представлен входной 16-битный регистр, собранный из 74HC165. У данного регистра имеется переключатель: Соединяя переключатель по варианту 1 или по варианту 2 можно выбирать либо соединение двух микросхем, полноценный 16-битный регистр и поддержку до 256 клавиш, либо 1 микросхему, 8-битный регистр и поддержку до 128 клавиш (обычно, больше и не нужно). Во втором случае второй чип 74HC165 и вторую резисторную сборку 10К на плату можно не ставить. Переключение данного переключателя подразумевает изменение параметра в прошивке (#define CHIP_J_COUNT).

Посередине находится собственно сетка пересекающихся линий проводников, на пересечения которой сажаются отдельные клавиши.

Логика работы контроллера для опроса клавиатуры подразумевается в следующем виде:

  • В цикле последовательно устанавливать один бит на выходном регистре, включая только одну строчку сетки пересечений клавиш, остальные опуская в «0».

  • За каждую установленную строчку считать сразу весь входной регистр (столбцов сетки) и попытаться определить на нем установленный бит. Если такой бит найден, то, по двумерному массиву кодов клавиш вычислить код нужной нажатой клавиши (первая координата – номер строки по номеру бита в выходном регистре, вторая координата – номер столбца по номеру бита, считанного со входного регистра), отправить ее код в порт PS/2 и запомнить нажатое состояние.

  • Если же при проверке входного регистра в нем по определенному адресу значится «0», а ранее известно, что клавиша с данным адресом была нажата, то, это означает, что клавиша была отпущена. Следовательно, нужно отправить коды отпущенной клавиши и освободить сохраненное состояние нажатой клавиши.

  • Далее контроллер слушает, не прилетело ли входных команд от компьютера (и реагирует), и следует снова по бесконечному циклу.

Полная схема готовой клавиатуры в PDF открывается по ссылке, если нужно.

В гитхабе найдете и схемы подключения регистров к Attiny, и плату, и прошивку.

Ах да. Провод PS/2 можно выдрать из обычной клавиатуры. Ну, или, кроме шуток, заказать на АлиЭкспрессе. Я вообще от дохлой шариковой мышки отодрал.

Программная часть

Как ранее упоминал, я не стал писать прошивку с абсолютного нуля, поскольку, вариантов полно в интернете, и использовал первую попавшуюся PS2Dev для Arduino. Но, для маленькой Attiny ее пришлось переработать. Я взял лишь отдельные функции, максимально ускорил их и минимизировал использование памяти как смог. Возможно, кто-то выжал бы из этого больше, тут смотрите сами.

Для своего варианта клавиатуры я использовал лишь два индикаторных светодиода (реально из них нужен только CAPS LOCK, второй просто для чего-нибудь), не использовал дополнительную цифровую панель, не использовал поддержку задержки клавиш (клавиатура предполагалась для самодельного компьютера, и все это просто не нужно). Возможно, попозже чуть доработаю, когда буду проектировать USB-вариант.

Вы вольны собрать для себя любую конфигурацию по желанию.

Прилагаю исходник прошивки, он откомментирован и доступен там же в ГитХабе, если нужно.

Код прошивки Attiny2313A
// https://github.com/Harvie/ps2dev/
// https://codeandlife.com/2013/06/28/minimal-ps2-keyboard-on-attiny2313/

#include <avr/io.h> //подключаем библиотеку аппаратных описаний
#include <util/delay.h> //поключаем библиотеку задержек

//#define KEYMAPSIZE 256

//Битовый массив нажатых клавиш. Если клавиша нажата, то в слове по номеру строки бит по номеру столбца будет установлен
word pr[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};

//Для использования 1 чипа 165 оставить значение 8
//Для двух чипов - 16
#define CHIP_J_COUNT 8

//Массив скан-кодов клавиш
const unsigned char keymap[] PROGMEM =
{
  0x76, 0x26, 0x4E, 0x2D, 0x5B, 0x33, 0x22, 0x4A,   0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
  0x05, 0x25, 0x55, 0x2C, 0x5A, 0x3B, 0x21, 0x59,   0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
  0x06, 0x2E, 0x5D, 0x35, 0x58, 0x42, 0x2A, 0x14,   0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
  0x04, 0x36, 0x66, 0x3C, 0x1C, 0x4B, 0x32, 0x11,   0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
  0x0C, 0x3D, 0x0D, 0x43, 0x1B, 0x4C, 0x31, 0x29,   0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
  0x0E, 0x3E, 0x15, 0x44, 0x23, 0x41, 0x3A, 0xE0,   0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
  0x16, 0x46, 0x1D, 0x4D, 0x2B, 0x12, 0x41, 0xE1,   0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
  0x1E, 0x45, 0x24, 0x54, 0x34, 0x1A, 0x49, 0xE2,   0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,

  0x03, 0xE3, 0xE4, 0x29, 0x29, 0x29, 0x29, 0x29,   0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
  0x0B, 0xE5, 0xE6, 0x29, 0x29, 0x29, 0x29, 0x29,   0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
  0x83, 0xE7, 0xE8, 0x29, 0x29, 0x29, 0x29, 0x29,   0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
  0x0A, 0xE9, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,   0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
  0x01, 0xEA, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,   0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
  0x09, 0xEB, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,   0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
  0x78, 0xEC, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,   0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
  0x07, 0xED, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,   0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29
};

//Длина максимальной последовательности расширенных кодов для клавиши
#define EXT_K_SIZE 8

//Массив клавиш с расширенными кодами.
//Если в массиве основных кодов выше указан код >= E0, то будет выбрана последовательность
//из данного массива с номером (Номер - E0), т.е., код E0 - первая, E1 - вторая, и т.д.
//Коды 0x00 будут игнорироваться
const unsigned char extcodes[]/*14][EXT_K_SIZE]*/ PROGMEM = {
  0xE0, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,   //R ALT  (E0)
  0xE0, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,   //R CTRL (E1)
  0xE1, 0x14, 0x77, 0xE1, 0xF0, 0x14, 0xE0, 0x77,   //PAUSE  (E2)
  0xE0, 0x75, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,   //UP     (E3)
  0xE0, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,   //END    (E4)
  0xE0, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,   //DOWN   (E5)
  0xE0, 0x7D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,   //PG UP  (E6)
  0xE0, 0x6B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,   //LEFT   (E7)
  0xE0, 0x12, 0xE0, 0x7C, 0x00, 0x00, 0x00, 0x00,   //PT SCR (E8)
  0xE0, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,   //RIGHT  (E9)
  0xE0, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,   //INS    (EA)
  0xE0, 0x6C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,   //HOME   (EB)
  0xE0, 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,   //PG DN  (EC)
  0xE0, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00    //DEL    (ED)
};

//То же самое для кодов отпускания клавиш
const unsigned char extcanscodes[]/*14][EXT_K_SIZE]*/ PROGMEM = {
  0xE0, 0xF0, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00,   //R ALT  (E0)
  0xE0, 0xF0, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00,   //R CTRL (E1)
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,   //PAUSE  (E2)
  0xE0, 0xF0, 0x75, 0x00, 0x00, 0x00, 0x00, 0x00,   //UP     (E3)
  0xE0, 0xF0, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00,   //END    (E4)
  0xE0, 0xF0, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00,   //DOWN   (E5)
  0xE0, 0xF0, 0x7D, 0x00, 0x00, 0x00, 0x00, 0x00,   //PG UP  (E6)
  0xE0, 0xF0, 0x6B, 0x00, 0x00, 0x00, 0x00, 0x00,   //LEFT   (E7)
  0xE0, 0xF0, 0x7C, 0xE0, 0xF0, 0x12, 0x00, 0x00,   //PT SCR (E8)
  0xE0, 0xF0, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00,   //RIGHT  (E9)
  0xE0, 0xF0, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00,   //INS    (EA)
  0xE0, 0xF0, 0x6C, 0x00, 0x00, 0x00, 0x00, 0x00,   //HOME   (EB)
  0xE0, 0xF0, 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00,   //PG DN  (EC)
  0xE0, 0xF0, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00    //DEL    (ED)
};






//Зададим пины светодиода Св1
#define LED1 7
#define LED1_PORT PORTD
#define LED1_PIN 5

//Зададим пины светодиода Св2
#define LED2 8
#define LED2_PORT PORTD
#define LED2_PIN 6

//Задаем пины для управления PS/2
#define PS2_CLOCK_DDR DDRB
#define PS2_CLOCK_PORT PORTB
#define PS2_CLOCK_PIN 0
#define PS2_CLOCK_INPUT PINB
#define ps2clock 9

#define PS2_DATA_DDR DDRB
#define PS2_DATA_PORT PORTB
#define PS2_DATA_PIN 1
#define PS2_DATA_INPUT PINB
#define ps2data 10

//Задаем пины 74HC595
#define RPORT PORTD
#define RDATA_PORT PORTD
#define RDATA_PIN 0
#define RCLOCK_PORT PORTD
#define RCLOCK_PIN 1
#define RLATCH_PORT PORTD
#define RLATCH_PIN 4
#define RDATA 0
#define RCLOCK 1
#define RLATCH 6

//Задаем пины 74HC165
#define IPORT PORTB
#define IDATA_DDR DDRB
#define IDATA_PORT PORTB
#define IDATA_PIN 4
#define ICLOCK_PORT PORTB
#define ICLOCK_PIN 2
#define ILATCH_PORT PORTB
#define ILATCH_PIN 3
#define IDATA 13
#define ICLOCK 11
#define ILATCH 12

//байт состояния светодиодов
unsigned char leds;
unsigned char km;

//Установить вывод PS/2 CLOCK в HIGH
static void clockHigh(void) {
  PS2_CLOCK_DDR &= ~_BV(PS2_CLOCK_PIN); // set as input
  PS2_CLOCK_PORT |= _BV(PS2_CLOCK_PIN); // set pullup
}

//Установить вывод PS/2 CLOCK в LOW
static void clockLow(void) {
  PS2_CLOCK_PORT &= ~_BV(PS2_CLOCK_PIN); // zero output value
  PS2_CLOCK_DDR |= _BV(PS2_CLOCK_PIN); // set as output
}

//Установить вывод PS/2 DATA в HIGH
static void dataHigh(void) {
  PS2_DATA_DDR &= ~_BV(PS2_DATA_PIN); // set as input
  PS2_DATA_PORT |= _BV(PS2_DATA_PIN); // set pullup
}

//Установить вывод PS/2 DATA в LOW
static void dataLow(void) {
  PS2_DATA_PORT &= ~_BV(PS2_DATA_PIN); // zero output value
  PS2_DATA_DDR |= _BV(PS2_DATA_PIN); // set as output
}

//Ускоренные процедуры чтения пинов PS/2
#define readClockPin() (PS2_CLOCK_INPUT & (1 << PS2_CLOCK_PIN))
#define readDataPin() (PS2_DATA_INPUT & (1 << PS2_DATA_PIN))
#define readReg165Pin() (PINB & (1 << IDATA_PIN))

//Тайминги ожидания при установке выводов PS/2
#define CLK_FULL 40 // 40+40 us for 12.5 kHz clock
#define CLK_HALF 20
//Таймаут проверки состояния пинов PS/2 при чтении
#define TIMEOUT 30

//Инициализация порта PS/2
void init_ps2() {
  //Установим выводы PS/2 CLOCK и DATA в HIGH
  clockHigh();
  dataHigh();

  //Отправим по PS/2 код 0xAA, означающий готовность клавиатуры к работе
  keyb_write(0xAA);
  _delay_us(10);
}


//Отдельная процедура - Сделать строб на выводе PS/2 CLOCK
void do_clock_lo_hi() {
  _delay_us(CLK_HALF);
  clockLow(); // start bit
  _delay_us(CLK_FULL);
  clockHigh();
  _delay_us(CLK_HALF);
}

//Проверка состояния выводов PS/2 CLOCK и DATA
int keyb_check() {
  return (!readClockPin() || !readDataPin());
}

//Универсальный ответ клавиатуры - подтверждение об успешном приеме
void ack() {
  keyb_write(0xFA);
  //_delay_us(CLK_HALF);
  }



//Считывание с регистра 74HC165
uint16_t shiftIn165()
{
  uint16_t value = 0;

  //Включим-выключим защелку
  ILATCH_PORT &= ~(1 << ILATCH_PIN); //LOW
  ILATCH_PORT |= (1 << ILATCH_PIN);  //HIGH

  //Считаем побитно содержимое регистра
  for (uint8_t i = 0; i < CHIP_J_COUNT; ++i)
  {
    ICLOCK_PORT &= ~(1 << ICLOCK_PIN); //LOW
    value |= bitRead(PINB, IDATA_PIN) << ((CHIP_J_COUNT - 1) - i); //digitalRead(IDATA) << (15 - i);
    ICLOCK_PORT |= (1 << ICLOCK_PIN);  //HIGH
  }

  return value;
}

//Функция чтения данных с порта PS/2
int keyboard_read(unsigned char * value) {
  unsigned int data = 0x00;
  unsigned int bit = 0x01;

  unsigned char calculated_parity = 1;
  unsigned char received_parity = 0;

  unsigned long waiting_since = millis();
  while ((readDataPin()) || (!readClockPin()) /*(digitalRead(ps2data) != LOW) || (digitalRead(ps2clock) != HIGH)*/ ) {
    if ((millis() - waiting_since) > TIMEOUT) return -1;
  }

  do_clock_lo_hi();

  while (bit < 0x0100) {
    if (readDataPin()/*digitalRead(ps2data) == HIGH*/) {
      data = data | bit;
      calculated_parity = calculated_parity ^ 1;
    } else {
      calculated_parity = calculated_parity ^ 0;
    }
    bit = bit << 1;
    do_clock_lo_hi();
  }

  if (readDataPin()/*digitalRead(ps2data) == HIGH*/) {
    received_parity = 1;
  }

  do_clock_lo_hi();

  _delay_us(CLK_HALF);
  dataLow();
  clockLow();
  _delay_us(CLK_FULL);
  clockHigh();
  _delay_us(CLK_HALF);
  dataHigh();

  *value = data & 0x00FF;

  if (received_parity == calculated_parity) {
    return 0;
  } else {
    return -2;
  }
}



//Запись байта в PS/2
int keyb_write(unsigned char data)
{
  _delay_us(1000);

  unsigned char i;
  unsigned char parity = 1;

  if (/*digitalRead(ps2clock) == LOW*/ !readClockPin()) {
    return -1;
  }

  if (/*digitalRead(ps2data) == LOW*/ !readDataPin()) {
    return -2;
  }

  dataLow();

  do_clock_lo_hi();

  for (i = 0; i < 8; i++)
  {
    if (data & 0x01)
    {
      dataHigh();
    } else {
      dataLow();
    }

    do_clock_lo_hi();

    parity = parity ^ (data & 0x01);
    data = data >> 1;
  }
  // parity bit
  if (parity)
  {
    dataHigh();
  } else {
    dataLow();
  }
  do_clock_lo_hi();

  // stop bit
  dataHigh();
  do_clock_lo_hi();

  _delay_us(1000);

  return 0;
}



int keyboard_reply(unsigned char cmd, unsigned char *leds) {

  unsigned char val;
  //unsigned char enabled;

  switch (cmd) {
    case 0xFF: //reset
      ack();
      //the while loop lets us wait for the host to be ready
      keyb_write(0xAA);
      break;
    case 0xFE: //resend
      ack();
      break;
    case 0xF6: //set defaults
      //enter stream mode
      ack();
      break;
    case 0xF5: //disable data reporting
      //FM
      //enabled = 0;
      ack();
      break;
    case 0xF4: //enable data reporting
      //FM
      //enabled = 1;
      ack();
      break;
    case 0xF2: //Сообщить device id
      ack();
      //0xAB83 - идентификатор оборудования стандартной клавиатуры ps/2
      keyb_write(0xAB);
      //_delay_us(CLK_HALF);
      keyb_write(0x83);
      /*digitalWrite(LED1, HIGH);
        _delay_us(80);
        digitalWrite(LED1, LOW);*/
      break;
    case 0xF0: //set scan code set
      ack();
      if (!keyboard_read(&val)) ack(); //do nothing with the rate
      break;
    case 0xEE: //echo
      //ack();
      keyb_write(0xEE);
      break;
    case 0xED: //set/reset LEDs
      ack();
      if (!keyboard_read(leds)) ack(); //do nothing with the rate
      return 1;
      break;
  }
  return 0;
}

//Проверка чтения прилетевшей от компьютера информации
int keyboard_read_check(unsigned char *leds) {
  unsigned char c;
  if ( keyb_check() ) {
    if (!keyboard_read(&c)) return keyboard_reply(c, leds);
  }
  return 0;
}


int main() {
  //Установим выходы регистра 27HC595 и светодиодов
  DDRD = (1 << RDATA_PIN) | (1 << RCLOCK_PIN) | (1 << RLATCH_PIN) | (1 << LED1_PIN) | (1 << LED2_PIN);

  //Установим оба светодиода в LOW
  LED1_PORT &= ~(1 << LED1_PIN);
  LED2_PORT &= ~(1 << LED2_PIN);

  //Инициализируем пины PS/2
  init_ps2();

  //установим выходы регистра 27HC165
  DDRB |= (1 << ICLOCK_PIN) | (1 << ILATCH_PIN);
  DDRB &= ~(1 << IDATA_PIN);

  //Главный бесконечный цикл
  while (1) {
    //Цикл опроса регистра 595
    for (byte i = 0; i < 16; i++) {
      //сформируем адрес строки опроса клавиш в 16битном сдвиговом 
      //регистре
      word ww = (word)1 << i;
      //И запишем его в порт 595
      RLATCH_PORT &= ~(1 << RLATCH_PIN);  //LOW
      shiftOut(RDATA, RCLOCK, MSBFIRST, highByte(ww));
      shiftOut(RDATA, RCLOCK, MSBFIRST, lowByte(ww));
      RLATCH_PORT |= (1 << RLATCH_PIN);  //HIGH

      //Считаем значение с регистра 165
      word b = shiftIn165();

      //Проверяем по строкам от 0 до CHIP_J_COUNT (8 или 16)
      for (byte j = 0; j < CHIP_J_COUNT; j++) {
        if (bitRead(b, j) == 1) {  // Проверяем на нажатие
          if (bitRead(pr[i], j) == 0) { //Если клавиша еще не нажата, то отправим ее код
            //LED1_PORT |= (1 << LED1_PIN);  //HIGH
            km = pgm_read_byte(&keymap[j + /*KEYMAPSIZE*/16 * i]);
            //Проверим, не должна ли клавиша сообщать расширенный код
            if (km >= 0xE0) {
              //Если код из таблицы кодов >= E0, то читаем 
              //последовательность из массива
              //с номером (Код - E0)
              for (int k = 0; k < EXT_K_SIZE; k++) {
                unsigned char ekm = pgm_read_byte(&extcodes[k + EXT_K_SIZE * (km - 0xE0)]);
                if (ekm > 0x00) {
                  keyb_write(ekm);                  
                }
              }
            } else {
              //Если не расширенный код, отправляем просто код клавиши
              keyb_write(km);
            }
            bitWrite(pr[i], j, 1);
            LED2_PORT |= (1 << LED2_PIN);
          }
        } else {  //Проверяем на отпускание
          if (bitRead(pr[i], j) == 1) { //Если клавиша была нажата, то, 
                                        //отправим код отпускания
            km = pgm_read_byte(&keymap[j + 16 * i]);
            if (km >= 0xE0) { // По вышеописанному принципу - если код 
                              //клавиши должен быть расширенным,
                              //то выдаем расширенную последовательность из массива extcanscodes
              for (int k = 0; k < EXT_K_SIZE; k++) {
                unsigned char ekm = pgm_read_byte(&extcanscodes[k + EXT_K_SIZE * (km - 0xE0)]);
                if (ekm > 0x00) {
                  keyb_write(ekm);
                  //LED2_PORT &= ~(1 << LED2_PIN);
                }
              }
            } else { // Или выдаем просто один код из массива keymap
              keyb_write(0xF0);
              keyb_write(km);
            }
            bitWrite(pr[i], j, 0);
            LED2_PORT &= ~(1 << LED2_PIN);
          }

        }


      }  //j

    }

    //Опрос входящих данных с порта PS/2 и проверка байта состояния светодиодов
    if (keyboard_read_check(&leds)) {
      //Если СAPS включен, зажжем светодиод Св1
      //digitalWrite(LED1, leds);
      if (bitRead(leds, 2) == 1) {
        //digitalWrite(LED1, HIGH);
        LED1_PORT |= (1 << LED1_PIN);
      } else {
        //digitalWrite(LED1, LOW);
        LED1_PORT &= ~(1 << LED1_PIN);
      }
    }


  }  //конец главного цикла

  return 1;
}

Комментарии (26)


  1. Javian
    17.10.2022 14:52
    +1

    на борту был встроенный кварц на 8мгц. А большего оказалось и не нужно

    При этом у самого Z80 столько же.


    1. gbg
      17.10.2022 15:47
      +3

      Все же не кварц, а RC-генератор, хоть и калиброванный (даташит, стр. 23).


      1. Javian
        18.10.2022 19:03
        +1

        От этого мегагерцы не становятся другими.


  1. DanilinS
    17.10.2022 15:55
    +1

    В аналогичной ситуации я просто взял TCA8418 на опрос клавиатуры и простенький STM32 на мозги. Плюс простенькая схема на 2 транзисторах для организации 2-х стороннего обмена с имитацией ОК.


  1. mpa4b
    17.10.2022 18:14
    +6

    Как минимум нужны диоды со стороны левых регистров (которые драйвят ряды). Иначе при нажатии 3 кнопок в 2 рядах например будет гостинг и без диодов не просто появится 4ая нажатая виртуально кнопка (как было бы с диодами), а вообще хзчто из-за деления напряжения на резисторах. Как максимум нужно по диоду на каждой кнопке (можно применять сдвоенные в корпусе sot-23), тогда вообще любая комбинация клавиш будет точно опознаваться.

    У протокола PS/2 в отличие от USB нет ограничений на одновременно нажатое кол-во клавиш. (если точнее, у USB от этого ограничения можно избавиться только извратами с несколькими ендпоинтами и т.д.)


    1. rheinhard Автор
      18.10.2022 01:54

      Верно. Но, я по ходу дела уже эмпирически установил, что две-три кнопки одновременно оно вполне нормально опознает. А дальше уже можно списать на "нефиг по клаве ходить".

      В общем, концепт я сделал, а более въедливые товарищи уже под свои задачи доработают, если кому нужно.


      1. Toloymak
        19.10.2022 01:31
        +1

        А как же комбинации Ctrl+Alt+Shift+Button в IDE'шках?)


        1. rheinhard Автор
          19.10.2022 01:33
          +1

          В большинстве случаев можно развешать клавиши по линиям так, чтобы известные комбинации близко не пересекались, либо совсем не пересекались, особенно, CTRL-ALT-SHIFT.

          Важное замечание, отмечу себе такой пунктик на будущее.


    1. KN_Dima
      19.10.2022 21:40
      +1

      Поддерживаю. Раскладка выбрана неудачно - комбинации Shift+W+Q, Shift+W+E, A+S+Alt приводят к фантомным нажатиям - клава не игровая :)


  1. ciuafm
    17.10.2022 18:24

    Работа проделана большая. Но что мешало взять контроллер от убитой PS/2 клавы и развести кнопки?


    1. rheinhard Автор
      18.10.2022 01:49
      +10

      Как-то даже и не знаю, что ответить.

      Вопрос, наверное, должен пониматься примерно как "Зачем разбираться, как оно устроено, если вокруг только и валяются убитые клавы с контроллерами, которые кто-то более умный уже сделал?"

      А я ведь в самом начале пишу - цель сугубо академическая.


  1. 2n2222
    17.10.2022 19:56

    Реализацию в железе я начал с того, что обычным образом нашел в столе – микросхемку контроллера Attiny2313A и жмень сдвиговых регистров 74HC595 и 74HC165.

    А если бы там лежали радиолампы? Делали бы на лампах? 64-пиновые PIC18 того же производителя стоят где-то пару долларов. Двух штук достаточно, чтобы все ваши кнопки подключить напрямую. Развести по 40 линий на микроконтроллер вроде как не проблема, если микроконтроллер на другой стороне платы. Может я что-то непонимаю. Хоть все кнопки одновременно нажимай. Код намного проще.


    1. rheinhard Автор
      18.10.2022 01:44
      +6

      Подловил. Я ведь и усилители на лампах тоже собираю.

      Вообще, PICи не вписываются в мою личную дендрофекальную философию. Пытаюсь делать, чтобы любой школьник мог разобраться и повторить, а у школьника, скорее всего, как максимум, ардуина найдется.

      В одной из следующих статей вообще про декатроны напишу.


    1. YDR
      18.10.2022 10:13
      +1

      И дело не только в трудоемкости рассыпухи, а еще в том, что на нормальном контроллере доступен и другой алгоритм опроса:

      выдаем активный уровень на все строки/столбцы одновременно. Ждем прерывания с ортогональных линий. Как только уровень изменился (на любой линии), локализуем нажатие за 8-3-1 шагов (тоже возможны вариации в зависимости от возможности перенастраивать входы на выход).

      С множественными нажатиями тоже возможно.

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

      ну и просто алгоритм красивый.


      1. rheinhard Автор
        18.10.2022 10:18
        +1

        Воу. Интересная идея. Надо будет попробовать.


        1. YDR
          18.10.2022 10:42
          +1

          Еще на основе похожего алгоритма я делал регистратор замыканий. Делал я его для расшифровки командаппарата, но можно применить и для расшифровки произвольной клавиатуры.

          Логика такая: есть N=35 линий, пронумерованных. Все подтяжками притянуты к 0 и настроены на вход. Пускаем бегущую 1 на выход, и смотрим, какие входы на нее реагируют. Строим список соединений. Если список отличается от предыдущего, то сохраняем текущее время и список соединений.

          А дальше расшифровываем в табличку.


  1. axe_chita
    17.10.2022 20:41
    +4

    Астрологи объявили на Хабре неделю годных статей? Однозначно плюсую!


  1. 2n2222
    18.10.2022 10:50
    -5

    Вообще, PICи не вписываются в мою личную дендрофекальную философию. Пытаюсь делать, чтобы любой школьник мог разобраться и повторить, а у школьника, скорее всего, как максимум, ардуина найдется.

    Так и я о том же, "Циклично опрашивая сетку пересечений клавишных проводников, определить пересечение, возникающее при нажатой клавише" - зачем это школьнику, когда он может просто напрямую сосчитать состояние кнопки, дешево, энергоэффективно, меньше помех от "тренировок" длинных линий, минимум кода. Доисторические сдвиговые регистры и нетривиальный код школьника скорее демотивируют. Не нравятся вам PIC, ну да на вкус и цвет как говорится, возьмите ATMEL или что угодно, хоть STM32. Кто не боится корпусов с большим количеством пинов, тот может взять микроконтроллер в 100-пиновом корпусе, или даже больше. Я не вижу проблем разводки дорожек, если на плате с кнопками микроконтроллер будет стоять на обратной стороне.

    При всем этом респект за то что сделали что-то своими руками. Напрягает несколько только символика вашего автара в совокупности с никнеймом. Но это может быть только мое, остальным нормально, но модеры пусть по возможности обратят внимание. "rheinhard" как бы ассоциируется с "Рейнхард Гейдрих", крылья аватара - дубовые листья на его воротнике, а символ по центру внизу - половинка от стилизованного SS. Бред конечно, но вот как-то ассоцирируется, не то чтобы сильно напрягает, но некий дискомфорт в общение вносит.


    1. rheinhard Автор
      18.10.2022 11:00
      +3

      Хм. Под таким углом на меня ещё не смотрели. Аватарка-то у меня из компьютерной игры, а позывной со страйкбола.
      Видимо, нужно что-то менять.


      1. KN_Dima
        19.10.2022 20:55
        +1

        Не надо ничего менять. У товарища излишне буйная фантазия.


        1. rheinhard Автор
          20.10.2022 01:32

          Да это местный блаженный, почти в каждый пост на хабре приходит гадить. Я его не провоцирую, иначе он тут простыни бессвязного бреда начнет писать.


    1. rheinhard Автор
      18.10.2022 11:31

      А почему сдвиговые регистры-то доисторические? Вроде, самый ходовой продукт сейчас. Их много в интернет-магазинах, и они дешевые. Так-то и PICи недалеко от них в плане устаревания ушли. Да и на ассемблере пойди заставь школьника писать. :) Си естественнее.

      А так, не спорю, можно было бы и stm взять. Как раз, лежит набор дожидается интересного проекта. Есть идея, например, рассмотреть создание самодельного накопителя на базе SPI RAM, с приводом и микросхемой-съемной памятью, располагающейся отдельно на удобной для замены платке. STM в этом случае будет эмулировать, по сути, флешку. А в памяти будет полноценно форматированный операционкой раздел.


      1. 2n2222
        19.10.2022 19:22
        -1

        Главная идея статьи задекларирована в названии - подключение к компьютеру в качестве клавиатуры по PS/2 – это просто. Хорошая идея, многим интересно разобраться с протоколом, за этим сюда на этот ресурс и приходят. Конкретная реализация, особенно такая специфическая как ваша интересна гораздо меньшему кругу людей. Я не против вашей, я немного против того, чтобы она звучала как разумный вариант для всех. У каждого свои тараканы, лучше, на мой взгляд кратко пробежаться по самым ходовым вариантам, а на свой вариант просто дать ссылку.

        Из ходовых вариантов вариантов в моем понимании (которое может быть весьма однобоко)

        • не хочу ничего паять (почти), возьму голубую пилюлю STM32 и сделаю эмулятор работы клавиатуры для удаленной работы;

        • признаю только пики и ассемблер (таковых на удивление немало), сотня клавиш мне не надо, но десяток-другой антивандальных я бы подключил через простейший пик.

        • на халяву досталось плат разработчика с таким-то микроконтроллером, пусть ребенок на Visual Studio на C# что-нибудь сочинит для бабушки, нажала на эту кнопку - ей такая радиостанция на компьютере, другую кнопу - сериалы, третью - песни.

        • и т.д. и т.п.


        1. rheinhard Автор
          20.10.2022 02:02
          +1

          Вы собственные суждения выдаете за общие суждения, что ли? Это мания величия. Главная идея статьи мной задекларирована в названии, и мне ее раскрывать. Ваше мнение, как писать статьи, мне малоинтересно.

          Я выкладываю конкретную рабочую реализацию, коих итак почти нет, вы предлагаете превратить ее в обзорную мусорную статью, коих итак полно. Зачем? Не осознаете этого?

          Не нравится чья-то реализация, ну сделайте свою, хоть на PIC в связке c Raspberry Pi. Опишите, напишите статью, даже я зайду. Посмеяться.


  1. KN_Dima
    19.10.2022 13:48

    Чем обусловлен выбор кнопок?
    Такой клавиатурой пользоваться же будет неудобно, и это мягко сказано.


    1. rheinhard Автор
      19.10.2022 13:50

      Прототип. На самом деле, терпимо, и можно работать. Клавиатура нужна была к компьютеру, который сам по себе, скорее, экспонат, чем рабочая машина.