Снова здравствуйте, дамы и господа. Наш Отдел Перспективных Разработок обещал вас порадовать очередной странной вещью, и потому продолжает свой цикл статей о старых, но иногда полезных устройствах.
Суть сегодняшнего предмета для изучения опять сугубо академическая. Хотя бы просто потому, что стандарт 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.
Весь список скан-кодов нет смысла засовывать сюда в статью, его всегда можно найти в интернетах.
В случае приема данных клавиатурой все несколько сложнее. На управляющие коды компьютера положено отвечать, поэтому, разберем, что вообще от него прилетает.
На каждый принятый байт клавиатура должна ответить. Ответом может быть FА в случае успешного приема, 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)
DanilinS
17.10.2022 15:55+1В аналогичной ситуации я просто взял TCA8418 на опрос клавиатуры и простенький STM32 на мозги. Плюс простенькая схема на 2 транзисторах для организации 2-х стороннего обмена с имитацией ОК.
mpa4b
17.10.2022 18:14+6Как минимум нужны диоды со стороны левых регистров (которые драйвят ряды). Иначе при нажатии 3 кнопок в 2 рядах например будет гостинг и без диодов не просто появится 4ая нажатая виртуально кнопка (как было бы с диодами), а вообще хзчто из-за деления напряжения на резисторах. Как максимум нужно по диоду на каждой кнопке (можно применять сдвоенные в корпусе sot-23), тогда вообще любая комбинация клавиш будет точно опознаваться.
У протокола PS/2 в отличие от USB нет ограничений на одновременно нажатое кол-во клавиш. (если точнее, у USB от этого ограничения можно избавиться только извратами с несколькими ендпоинтами и т.д.)
rheinhard Автор
18.10.2022 01:54Верно. Но, я по ходу дела уже эмпирически установил, что две-три кнопки одновременно оно вполне нормально опознает. А дальше уже можно списать на "нефиг по клаве ходить".
В общем, концепт я сделал, а более въедливые товарищи уже под свои задачи доработают, если кому нужно.
Toloymak
19.10.2022 01:31+1А как же комбинации Ctrl+Alt+Shift+Button в IDE'шках?)
rheinhard Автор
19.10.2022 01:33+1В большинстве случаев можно развешать клавиши по линиям так, чтобы известные комбинации близко не пересекались, либо совсем не пересекались, особенно, CTRL-ALT-SHIFT.
Важное замечание, отмечу себе такой пунктик на будущее.
KN_Dima
19.10.2022 21:40+1Поддерживаю. Раскладка выбрана неудачно - комбинации Shift+W+Q, Shift+W+E, A+S+Alt приводят к фантомным нажатиям - клава не игровая :)
ciuafm
17.10.2022 18:24Работа проделана большая. Но что мешало взять контроллер от убитой PS/2 клавы и развести кнопки?
rheinhard Автор
18.10.2022 01:49+10Как-то даже и не знаю, что ответить.
Вопрос, наверное, должен пониматься примерно как "Зачем разбираться, как оно устроено, если вокруг только и валяются убитые клавы с контроллерами, которые кто-то более умный уже сделал?"
А я ведь в самом начале пишу - цель сугубо академическая.
2n2222
17.10.2022 19:56Реализацию в железе я начал с того, что обычным образом нашел в столе – микросхемку контроллера Attiny2313A и жмень сдвиговых регистров 74HC595 и 74HC165.
А если бы там лежали радиолампы? Делали бы на лампах? 64-пиновые PIC18 того же производителя стоят где-то пару долларов. Двух штук достаточно, чтобы все ваши кнопки подключить напрямую. Развести по 40 линий на микроконтроллер вроде как не проблема, если микроконтроллер на другой стороне платы. Может я что-то непонимаю. Хоть все кнопки одновременно нажимай. Код намного проще.
rheinhard Автор
18.10.2022 01:44+6Подловил. Я ведь и усилители на лампах тоже собираю.
Вообще, PICи не вписываются в мою личную дендрофекальную философию. Пытаюсь делать, чтобы любой школьник мог разобраться и повторить, а у школьника, скорее всего, как максимум, ардуина найдется.
В одной из следующих статей вообще про декатроны напишу.
YDR
18.10.2022 10:13+1И дело не только в трудоемкости рассыпухи, а еще в том, что на нормальном контроллере доступен и другой алгоритм опроса:
выдаем активный уровень на все строки/столбцы одновременно. Ждем прерывания с ортогональных линий. Как только уровень изменился (на любой линии), локализуем нажатие за 8-3-1 шагов (тоже возможны вариации в зависимости от возможности перенастраивать входы на выход).
С множественными нажатиями тоже возможно.
Это принесет повышение быстродействия и экономичности без необходимости повышать частоту сканирования.
ну и просто алгоритм красивый.
rheinhard Автор
18.10.2022 10:18+1Воу. Интересная идея. Надо будет попробовать.
YDR
18.10.2022 10:42+1Еще на основе похожего алгоритма я делал регистратор замыканий. Делал я его для расшифровки командаппарата, но можно применить и для расшифровки произвольной клавиатуры.
Логика такая: есть N=35 линий, пронумерованных. Все подтяжками притянуты к 0 и настроены на вход. Пускаем бегущую 1 на выход, и смотрим, какие входы на нее реагируют. Строим список соединений. Если список отличается от предыдущего, то сохраняем текущее время и список соединений.
А дальше расшифровываем в табличку.
2n2222
18.10.2022 10:50-5Вообще, PICи не вписываются в мою личную дендрофекальную философию. Пытаюсь делать, чтобы любой школьник мог разобраться и повторить, а у школьника, скорее всего, как максимум, ардуина найдется.
Так и я о том же, "Циклично опрашивая сетку пересечений клавишных проводников, определить пересечение, возникающее при нажатой клавише" - зачем это школьнику, когда он может просто напрямую сосчитать состояние кнопки, дешево, энергоэффективно, меньше помех от "тренировок" длинных линий, минимум кода. Доисторические сдвиговые регистры и нетривиальный код школьника скорее демотивируют. Не нравятся вам PIC, ну да на вкус и цвет как говорится, возьмите ATMEL или что угодно, хоть STM32. Кто не боится корпусов с большим количеством пинов, тот может взять микроконтроллер в 100-пиновом корпусе, или даже больше. Я не вижу проблем разводки дорожек, если на плате с кнопками микроконтроллер будет стоять на обратной стороне.
При всем этом респект за то что сделали что-то своими руками. Напрягает несколько только символика вашего автара в совокупности с никнеймом. Но это может быть только мое, остальным нормально, но модеры пусть по возможности обратят внимание. "rheinhard" как бы ассоциируется с "Рейнхард Гейдрих", крылья аватара - дубовые листья на его воротнике, а символ по центру внизу - половинка от стилизованного SS. Бред конечно, но вот как-то ассоцирируется, не то чтобы сильно напрягает, но некий дискомфорт в общение вносит.
rheinhard Автор
18.10.2022 11:00+3Хм. Под таким углом на меня ещё не смотрели. Аватарка-то у меня из компьютерной игры, а позывной со страйкбола.
Видимо, нужно что-то менять.
rheinhard Автор
18.10.2022 11:31А почему сдвиговые регистры-то доисторические? Вроде, самый ходовой продукт сейчас. Их много в интернет-магазинах, и они дешевые. Так-то и PICи недалеко от них в плане устаревания ушли. Да и на ассемблере пойди заставь школьника писать. :) Си естественнее.
А так, не спорю, можно было бы и stm взять. Как раз, лежит набор дожидается интересного проекта. Есть идея, например, рассмотреть создание самодельного накопителя на базе SPI RAM, с приводом и микросхемой-съемной памятью, располагающейся отдельно на удобной для замены платке. STM в этом случае будет эмулировать, по сути, флешку. А в памяти будет полноценно форматированный операционкой раздел.
2n2222
19.10.2022 19:22-1Главная идея статьи задекларирована в названии - подключение к компьютеру в качестве клавиатуры по PS/2 – это просто. Хорошая идея, многим интересно разобраться с протоколом, за этим сюда на этот ресурс и приходят. Конкретная реализация, особенно такая специфическая как ваша интересна гораздо меньшему кругу людей. Я не против вашей, я немного против того, чтобы она звучала как разумный вариант для всех. У каждого свои тараканы, лучше, на мой взгляд кратко пробежаться по самым ходовым вариантам, а на свой вариант просто дать ссылку.
Из ходовых вариантов вариантов в моем понимании (которое может быть весьма однобоко)
не хочу ничего паять (почти), возьму голубую пилюлю STM32 и сделаю эмулятор работы клавиатуры для удаленной работы;
признаю только пики и ассемблер (таковых на удивление немало), сотня клавиш мне не надо, но десяток-другой антивандальных я бы подключил через простейший пик.
на халяву досталось плат разработчика с таким-то микроконтроллером, пусть ребенок на Visual Studio на C# что-нибудь сочинит для бабушки, нажала на эту кнопку - ей такая радиостанция на компьютере, другую кнопу - сериалы, третью - песни.
и т.д. и т.п.
rheinhard Автор
20.10.2022 02:02+1Вы собственные суждения выдаете за общие суждения, что ли? Это мания величия. Главная идея статьи мной задекларирована в названии, и мне ее раскрывать. Ваше мнение, как писать статьи, мне малоинтересно.
Я выкладываю конкретную рабочую реализацию, коих итак почти нет, вы предлагаете превратить ее в обзорную мусорную статью, коих итак полно. Зачем? Не осознаете этого?
Не нравится чья-то реализация, ну сделайте свою, хоть на PIC в связке c Raspberry Pi. Опишите, напишите статью, даже я зайду. Посмеяться.
Javian
При этом у самого Z80 столько же.
gbg
Все же не кварц, а RC-генератор, хоть и калиброванный (даташит, стр. 23).
Javian
От этого мегагерцы не становятся другими.