Создание Ultimate Hacking Keyboard

в 22:43, , рубрики: diy или сделай сам, Автомобильные гаджеты, клавиатура, краудфандинг

Перевод истории задумки и создания Ultimate Hacking Keyboard – необычной клавиатуры, проект которой вскоре планируют запустить на Kickstarter.

Как я решил построить идеальную клавиатуру для разработчика с нуля

В августе 2007 на работе я заметил, насколько далека от идеала стандартная клавиатура для PC. Мне приходится сотни раз в день перемещать руки в разные её места, а ладони нужно держать близко друг к другу, что не очень удобно. Я подумал, что должен быть способ лучше.

Я начал думать, как можно улучшить клавиатуру и пришёл к этому:

image

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

Как сделать клавиатуру?

Попробуем начать с чего-нибудь простого, например с Arduino, и постепенно повышать градус сложность, чтобы в конце прийти к Ultimate Hacking Keyboard.

Шаг 1: Клавиатура без клавиш

Сделаем USB-клавиатуру, которая выдаёт символ х раз в секунду. Для этого идеально подойдёт Arduino Micro. На ней стоит ATmega32U4 – это такой же AVR-микроконтроллер, который служит мозгом UHK.

image

С USB AVR микроконтроллером проще всего работать через библиотеку Lightweight USB Framework for AVRs (LUFA). При подключении к USB устройство должно передать особые структуры данных – USB-дескрипторы. Они сообщают компьютеры тип и свойства подключённого устройства и представлены в виде дерева. При этом устройство может реализовывать несколько функций.

Вот структура дескриптора для UHK:

Device descriptor
    Configuration descriptor
        Interface descriptor 0: GenericHID
            Endpoint descriptor
        Interface descriptor 1: Keyboard
            Endpoint descriptor
        Interface descriptor 2: Mouse
            Endpoint descriptor

Стандартные клавиатуры выдают только один дескриптор. Но на нашей клавиатуре можно запрограммировать произвольные клавиши так, чтобы они выполняли роль мыши. А интерфейс GenericHID работает как канал для коммуникаций и передачи настроек. Полную реализацию для устройства на основе LUFA можно взять здесь.

Теперь можно ежесекундно отправлять символ х:

uint8_t isSecondElapsed = 0;
 
int main(void)
{
    while (1) {
        _delay_us(1000);
        isSecondElapsed = 1;
    }
}
 
bool CALLBACK_HID_Device_CreateHIDReport(USB_ClassInfo_HID_Device_t* const HIDInterfaceInfo,
                                         uint8_t* const ReportID,
                                         const uint8_t ReportType,
                                         void* ReportData,
                                         uint16_t* const ReportSize)
{
    USB_KeyboardReport_Data_t* KeyboardReport = (USB_KeyboardReport_Data_t*)ReportData;
    if (isSecondElapsed) {
        KeyboardReport->KeyCode[0] = HID_KEYBOARD_SC_X;
        isSecondElapsed = 0;
    }
    *ReportSize = sizeof(USB_KeyboardReport_Data_t);
    return false;
}

USB протокол работает по принципу поллинга, то есть компьютер опрашивает устройство через определённые промежутки времени (обычно 125 раз в секунду). Функция обратного вызова CALLBACK_HID_Device_CreateHIDReport() отправляет сканкод символа на компьютер, если в переменной isSecondElapsed содержится 1. Переменной присваивается 1 в цикле каждую секунду, и 0 в функции обратного вызова.

Шаг 2: Клавиатура с четырьмя клавишами

Пока наша клавиатура не особенно полезна. Чтобы иметь возможность что-либо печатать, нужны клавиши. Их надо расположить по какой-то схеме (матрице). У 104-клавишной клавиатуры 6 рядов и 18 столбцов, но мы пока ограничимся матрицей 2х2. Схемка:

image

Как это выглядит на доске для прототипирования:

image

Если ROW1 соединён с PINA0, ROW2 с PINA1, COL1 с PORTB0 и COL2 с PORTB1, то код для опроса нажатий выглядит так:

/* Пин микроконтроллера, к которому подсоединён ряд или столбец */
typedef struct {
    volatile uint8_t *Direction;
    volatile uint8_t *Name;
    uint8_t Number;
} Pin_t;
 
/* Эта часть матрицы хранится в Flash для экономии места в SRAM. */
typedef struct {
    const uint8_t ColNum;
    const uint8_t RowNum;
    const Pin_t *ColPorts;
    const Pin_t *RowPins;
} KeyMatrixInfo_t;
 
/* Эта часть матрицы хранится в SRAM */
typedef struct {
    const __flash KeyMatrixInfo_t *Info;
    uint8_t *Matrix;
} KeyMatrix_t;
 
const __flash KeyMatrixInfo_t KeyMatrix = {
    .ColNum = 2,
    .RowNum = 2,
    .RowPins = (Pin_t[]) {
        { .Direction=&DDRA, .Name=&PINA, .Number=PINA0 },
        { .Direction=&DDRA, .Name=&PINA, .Number=PINA1 }
    },
    .ColPorts = (Pin_t[]) {
        { .Direction=&DDRB, .Name=&PORTB, .Number=PORTB0 },
        { .Direction=&DDRB, .Name=&PORTB, .Number=PORTB1 },
    }
};
 
void KeyMatrix_Scan(KeyMatrix_t *KeyMatrix)
{
    for (uint8_t Col=0; Col<KeyMatrix->Info->ColNum; Col++) {
        const Pin_t *ColPort = KeyMatrix->Info->ColPorts + Col;
        for (uint8_t Row=0; Row<KeyMatrix->Info->RowNum; Row++) {
            const Pin_t *RowPin = KeyMatrix->Info->RowPins + Row;
            uint8_t IsKeyPressed = *RowPin->Name & 1<<RowPin->Number;
            KeyMatrix_SetElement(KeyMatrix, Row, Col, IsKeyPressed);
        }
    }
}

Код сканирует клавиатуру по одному столбцу за раз, и в столбце читает состояние отдельных переключателей. Затем состояние кнопок сохраняется в массиве. Внутри CALLBACK_HID_Device_CreateHIDReport() на основании этого массива будут назначены соответствующие сканкоды.

Шаг 3: клавиатура из двух половинок

Продолжая путь к UHK, необходимо добавить вторую половину клавиатуры. Она будет содержать ещё одну матрицу клавиш, которая будет работать тем же методом. Интересным моментом будет общение двух половинок между собой. Три самых популярных протокола для соединения устройств — SPI, I2C и UART. В статье будет использован UART.

image

Двустороннее общение идёт через RX вправо и через TX влево. VCC и GND нужны для передачи питания. Для UART нужно, чтобы у всех устройств была одинаковая скорость передачи, количество бит данных и стоповых битов.

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

Отправка сообщений с левой части:

USART_SendByte(IsKeyPressed<<7 | Row*COLS_NUM + Col);

Правая часть принимает сообщения:

void KeyboardRxCallback(void)
{
    uint8_t Event = USART_ReceiveByte();
    if (!MessageBuffer_IsFull(&KeyStateBuffer)) {
        MessageBuffer_Insert(&KeyStateBuffer, Event);
    }
}

Хэндлер прерывания KeyboardRxCallback() срабатывает при получении байта. Для скорейшего срабатывания хэндлеров полученные сообщения складываются в кольцевой буфер. Он обрабатывается из основного цикла, и матрица клавиатуры обновляется на основании этого сообщения.

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

Прототип уже выглядит внушительно:

image

Шаг 4: LED дисплей

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

image

Дисплей представляет из себя матрицу 8х6

image

Каждые два ряда красных символов – это один из 14-сегментных дисплеев. Белые символы – три дополнительных статусных индикатора.

Для того, чтобы LED загорелся, на соответствующий столбец подаётся высокое напряжение, а на ряд – низкое. Из этого следует, что работать может только один столбец. Можно решить, что из-за этого нельзя работать со всеми дисплеями одновременно – но суть в том, что они просто мигают настолько быстро, что этого не замечает глаз.

Матрица LED управляется двумя интегральными микросхемами (ИС), одна из которых управляет рядами, а другая – столбцами. Столбцами управляет PCA9634 I2C:

image

Рядами управляет TPIC6C595:

image

Код:

uint8_t LedStates[LED_MATRIX_ROWS_NUM];

void LedMatrix_UpdateNextRow(bool IsKeyboardColEnabled)
{
    TPIC6C595_Transmit(LedStates[ActiveLedMatrixRow]);
    PCA9634_Transmit(1 << ActiveLedMatrixRow);

    if (++ActiveLedMatrixRow == LED_MATRIX_ROWS_NUM) {
          ActiveLedMatrixRow = 0;
    }
}

LedMatrix_UpdateNextRow() вызывается каждую миллисекунду и обновляет ряд матрицы. Массив LedStates сохраняет состояния отдельных LED, и обновляется через UART на основании сообщений с правой стороны клавиатуры примерно так же, как обрабатываются нажатия клавиш.

Общий план

Вот мы и построили компоненты для нашей клавиатуры. Внутренности клавиатуры представляют из себя компьютерную сеть в миниатюре: множество узлов, соединённых между собою.

image

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

UHK Agent – приложение для настроек клавиатуры под нужды пользователя. Хотя это и толстый (rich) клиент, Agent использует веб-технологии и работает на платформе webkit. С клавиатурой он общается через библиотеку node-usb. Он Express.js для доступа к нему сторонних приложений через REST API. Также он Angular.js для рисования красивого интерфейса пользователя.

var enumerationModes = {
    'keyboard'         : 0,
    'bootloader-right' : 1,
    'bootloader-left'  : 2
};

function sendReenumerateCommand(enumerationMode, callback)
{
    var AGENT_COMMAND_REENUMERATE = 0;
    sendAgentCommand(AGENT_COMMAND_REENUMERATE, enumerationMode, callback);
}

function sendAgentCommand(command, arg, callback)
{
    setReport(new Buffer([command, arg]), callback);
}

function setReport(message, callback)
{
    device.controlTransfer(
        0x21,             // bmRequestType (константа этого запроса)
        0x09,             // bmRequest (константа этого запроса)
        0,                // wValue (MSB тип отчёта, LSB номер отчёта)
        interfaceNumber,  // wIndex (номер интерфейса)
        message,          // сообщение
        callback
    );
}

У каждой команды есть 8-битный идентификатор и набор аргументов, зависящих от команды. Сейчас реализована только команда re-enumerate.

Что же умеет этот софт? Agent может отображать износ кнопок и предупреждать о необходимости их замены. Он может предоставлять интерфейс для настройки различных раскладок. Задавать скорость и ускорение курсора мыши. И много всего другого.

Создание прототипа

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

image

Корпус клавиатуры, распечатанный на 3D-принтере:

image

На основании механического дизайна и схемы нужно изготовить печатную плату. Правая часть выглядит в KiCad так:

image

Делаем плату и вручную припаиваем элементы:

image

Наконец, изготовив все части, напечатав и отшлифовав пластиковые детали, и собрав всё это вместе, мы получаем работающий прототип клавиатуры:

image

Заключение

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

Поэтому для меня разработка UHK – это привилегия, и несмотря на все трудности, это волнующее приключение и прекрасный опыт в обучении новому.

За подробной информацией я приглашаю вас посетить сайт ultimatehackingkeyboard.com, и там же вы сможете подписаться на уведомление о начале нашей краудфандинговой компании.

Автор: SLY_G

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js