Предыстория
В недалеком прошлом я стал обладателем удобной (по моему мнению) клавиатуры Logitech K800. Данная клавиатура является мембранной, но механизм переключения кнопок – «ножницы», как на ноутбуках, правда с бо́льшим ходом. Дополнительно она имеет приятную белую подсветку.
С вышеуказанной клавиатурой есть три проблемы:
-
Она уже не выпускается, а на б/у рынке стоит достаточно дорого, в непонятных состояниях.
-
С русской раскладкой она поставляется только в формате ISO - а это короткий LeftShift. Непонятно почему, но у нее укорочена правая часть «основной области», в результате чего BackSpace и Enter получились «ужатыми».
-
Легко ломается. В результате проливания жидкости мембрана умирает. Это может произойти не сразу, но процесс разрушения дорожек на мембране уже начат. Восстанавливать их очень кропотливо, да и результат непредсказуем. (я делал это трижды, два раза успешно)
Настал решающий момент заменить клавиатуру. Прогулявшись по интернет-магазинам, я понял, что ничего из предложенного среди мембранок не нравится. Рекомендации ютуба начали выдавать обзоры различных механических клавиатур, кастомы и прочее. Мне никогда не нравился огромный ход механики, потому стандартные варианты не подошли, но как-то раз в рекомендациях Aliexpress показались механические переключатели kailh low profile. И я понял, что это – оно. (сейчас уже доступны для покупки низкопрофильные механики, например, Logitech). Решено - сделаю свой хардкорный кастом.
Процесс сборки клавиатуры с нуля занял у меня без малого, два года в неспешном ритме, но обо всем по порядку.
Выбор переключателей
Все механические переключатели имеют свои особенности: Сила нажатия, момент срабатывания, обратная связь. Когда я читал информацию и слушал записи «тейпинга», то не смог в слепую определиться с типом переключателей, потому заказал «набор - пробник» из 13 различных переключателей. Какие-то со щелчком, какие-то без. Напечатал на 3D принтере матрицу для их фиксации.
Распределил их по силе нажатия и начал выбирать. Здесь я сделал небольшую ошибку: необходимо все переключатели смазать, чтобы они работали правильно. Эта ошибка не повлияла на мой выбор и вариант с коричневыми переключателями оказался наиболее удобным.
Следующим этапом было решение вопроса с кейкапами. Крепеж у данных переключателей отличается от стандартных, высоких переключателей. К этому моменту начали появляться статьи с кастомами на данных переключателях, но все использовали одинаковый (1u) размер кейкапа для всех клавиш, что мне не подходило. Так как готовых наборов кейкапов не было, пришлось вновь изобретать велосипед. Нашел наиболее универсальную форму кейкапа среди 3D моделей, которые представлены в открытом доступе. Смоделировал крепление и попробовал распечатать на 3D принтере. Высота слоя 0.1, сопло 0.3. Печатал белым ABS и светопрозрачным PETg пластиками. Результат на фото:
Результат меня устроил, как по форме, так и по качеству кейкапа, значит с вариантом изготовления было решено – печатать. Хотелось организовать подсветку - поэтому все спроектированные кейкапы в дальнейшем решил печатать светопрозрачным PETg пластиком. Далее покраска, лазерная гравировка и лак, но обо всем по порядку.
Выбор форм-фактора и раскладки
Следующим этапом был выбор форм-фактора. Как писал выше, ISO меня не устроил, потому – ANSI. Никаких вычурных форм и т.п., обычная полноценная клавиатура с NumPad- блоком, но захотелось мультимедийных клавиш – их 4шт. над блоком NumPad. Не нашел в свободной продаже стабилизаторов на длинные кейкапы, стал делать без них. По этой причине пробела – два (таких решений встречал много).
Схема матричного опроса клавиатуры
Как организовывать опрос клавиш. В моем случае получилось 109 переключателей. Вешать каждый на отдельную ногу целевого микроконтроллера конечно можно, но это дорогой путь. Организовал матричный опрос клавиатуры (6 строк, 20 столбцов) с использованием диодов на каждой клавише (чтобы избежать фантомных нажатий). Про подобный способ опроса клавиатуры существует множество статей, попробую вкратце:
Столбцы – входы микроконтроллера с внутренней подтяжкой к лог. «1».
Строки – выходы микроконтроллера.
В неактивном режиме: на всех выходах (строках) установлена лог. «1», на всех входах (столбцах) активна встроенная подтяжка, она перетягивает вход в лог. «1».
Во время опроса матрицы на выходы (строки) последовательно подаются лог. «0» и на каждом таком шаге происходит опрос всех входов (столбцов). Если на выбранной строке нажата клавиша, то логический уровень соответствующего подтянутого входа столбца перейдет в лог. «0». Так как каждый шаг задается пользователем, то нажатая клавиша определяется однозначно. На фото представлен опрос по шагам:
Теоретически можно избежать инверсии, если изменить направление диода и на выходы (строки) подавать лог. «1».
В мембранных клавиатурах не используются диоды и алгоритм опроса немного отличается. Описывать все не буду, информации в интернете достаточно. (чаще используются входы с открытым коллектором в совокупности с оптимальной матрицей и дополнительными проверками).
STM32
Сигнальным сердцем клавиатуры выступил микроконтроллер STM32F103C8T6. У данного камня на борту есть необходимые интерфейсы: USB, таймеры с ШИМ (в дальнейшем пригодятся для подсветки), DMA, достаточное количество выводов для матричного опроса кнопок.
На фото выше представлена использованная распиновка микросхемы в CubeMX.
Изначально работу по отладке USB (до заказа печатной платы) я производил на плате BluePill. Расположение USB (DP/DN) остается на тех же выводах.
Далее я бы рекомендовал прочитать документ «HID Usage Tables for Universal Serial Bus (USB)», чтобы написанное ниже было понятней.
В CubeMX, в разделе «Middleware» настроил USB_DEVICE в режиме HID, интервал опроса сделал минимальным (1мс).
Далее пришлось разобраться с дебрями USB. Самым сложным оказался «Report Descriptor». У меня получилось 2 устройства (2 точки монтирования, если я правильно понимаю). Первое устройство – клавиатура с индикацией состояний (Num/Scroll/Caps locks), второе – мультимедийное устройство с клавишами управления (В данный момент использую 4: Play/Pause, Vol-, Vol+, Mute).
Дескрипторы
Дескриптор клавиатуры можно сделать под свои задачи, но чтобы клавиатура работала в BIOS, необходимо соответствовать стандартному дескриптору из стандарта. Вот что получилось:
__ALIGN_BEGIN static uint8_t HID_KEYBOARD_ReportDesc[HID_KEYBOARD_REPORT_DESC_SIZE] __ALIGN_END =
{
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x06, // USAGE ID (Keyboard)
0xA1, 0x01, // COLLECTION (Application)
0x85, 0x01, // Report ID (1)
0x05, 0x07, // USAGE_PAGE (Keyboard)
0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl)
0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x75, 0x01, // REPORT_SIZE (1)
0x95, 0x08, // REPORT_COUNT (8)
0x81, 0x02, // INPUT (Data,Var,Abs) ;Keys byte
0x05, 0x07, // USAGE_PAGE (Keyboard)
0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated))
0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x65, // LOGICAL_MAXIMUM (101)
0x75, 0x08, // REPORT_SIZE (8)
0x95, 0x06, // REPORT_COUNT (6)
0x81, 0x00, // INPUT (Data Array)
0x05, 0x08, // USAGE_PAGE (LEDs)
0x09, 0x01, // Usage (Num Lock)
0x09, 0x02, // Usage (Caps Lock)
0x09, 0x03, // Usage (Scroll Lock)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x03, // Report Count (3)
0x91, 0x02, // OUTPUT (Constant Array) ;LED report padding
0x75, 0x05, // REPORT_SIZE (5)
0x95, 0x01, // REPORT_COUNT (1)
0x91, 0x01, // OUTPUT (Constant Array) ;LED report padding
0xc0, // END_COLLECTION
0x05, 0x0C, // Usage Page (Consumer)
0x09, 0x01, // Usage (Consumer Control)
0xA1, 0x01, // Collection (Application)
0x85, 0x02, // Report ID (2)
0x05, 0x0C, // Usage Page (Consumer)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x09, 0xB5, // Usage (Scan Next Track)
0x09, 0xB6, // Usage (Scan Previous Track)
0x09, 0xB7, // Usage (Stop)
0x09, 0xB8, // Usage (Eject)
0x09, 0xCD, // Usage (Play/Pause)
0x09, 0xE2, // Usage (Mute)
0x09, 0xE9, // Usage (Volume Increment)
0x09, 0xEA, // Usage (Volume Decrement)
0x75, 0x01, // Report Size (1)
0x95, 0x08, // Report Count (8)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0, // End Collection
};
В конфигурационном дескрипторе (на всякий случай во всех) пришлось изменить количество NumEndpoints и выставить максимальный ток USB (Клавиатура с подсветкой потребляет 850 мА, а в дескрипторе максимум 500 мА. Не уверен, что так можно, но некоторые флагманские клавиатуры потребляют до 2А).
При наличии двух точек монтирования, при отправке репорта (USBD_HID_SendReport), первым байтом идет идентификационный номер точки монтирования. В моем случае для клавиатуры это «1», для мультимедийных кнопок «2».
Репорт клавиатуры выглядит следующим образом:
struct HID_Keys_t {
uint8_t ID;
uint8_t MODIFIER;
uint8_t RESERVED;
uint8_t KEYCODE[6];
};
Т.е. в вышеуказанном случае это 9 байт данных. Если точка монтирования одна, то передается 8 байт данных.
ID – идентификатор, он равен «1»,
MODIFIER – битовая маска системных клавиш (LCTRL, LSHIFT, LALT, LWIN, RCTRL, RSHIFT, RALT, RWIN). Их объявил в дескрипторе (0xE0-0xE7)
RESERVED – зарезервированный байт данных.
KEYCODE[6] – 6 байт данных нажатых клавиш.
Из вышеописанного следует, что по стандарту клавиатура не может передать информацию о более чем 6 одновременно нажатых клавишах (не считая системные). Чтобы передать 12, можно теоретически сделать 2 точки монтирования, если это действительно нужно.
Проблема, которую не смог решить: В результате появления байта ID, ReportDescriptor стал 9 байтным. Последний KEYCODE игнорируется ПК (5 нажатых клавиш одновременно). Мне это не критично, но почему – не знаю. При изменении HID_EPIN_SIZE на 9, USB устройство перестает определяться.
По умолчанию не реализован функционал получения данных от ПК, что потребуется для определения состояний светодиодов на клавиатуре. Пришлось реализовать две функции:
static uint8_t USBD_HID_DataOut(USBD_HandleTypeDef *pdev, uint8_t epnum)
{
HAL_PCD_EP_Receive(pdev->pData, HID_EPOUT_ADDR, rx_buf, HID_EPOUT_SIZE);
return USBD_OK;
}
uint8_t * USBD_HID_GetData(void){
return rx_buf;
}
С USB все получилось (конечно не с первого раза, пришлось просидеть не один вечер), далее необходимо разобраться с подсветкой клавиш.
Адресные светодиоды
Для подсветки клавиш решил использовать адресные светодиоды, размером 2х2 мм. В выбранных механических переключателях под подсветку предусмотрено место.
Купленные адресные светодиоды имеют следующий протокол обмена данными:
Все адресные светодиоды соединяются последовательно, каждый «откусывает» себе 24 бита данных (GRB-888), а остальную часть посылки пропускает дальше. Сброс дает команду переключения с текущего цвета на последние полученные данные. Итого 109 светодиодов по 24 бита – это 2616 бит данных. При длительности каждого бита 1.25 мкс + минимальный сброс 50 мкс, получается 3.32мс на одну полную посылку. (теоретически 30 кадров в секунду, если я ничего не напутал)
В STM32 нет аппаратного контроллера для адресных светодиодов, но есть гибкий ШИМ + DMA.
В контроллере TIM2 я настроил таймер так, что при частоте ядра 72МГц, частота ШИМ была 800кГц (разделив на 90), а период ШИМ как раз 1.25мкс.
Задав длительность ШИМ в 58 (из 90), получается длительность лог «1» в 0.806мкс, а задав 29 (из 90), получается длительность лог «1» 0.403мкс. В первом случае получаем цифровую «1», а во втором цифровой «0» протокола адресных светодиодов.
Каждый раз отвлекать ядро на запись данных в регистр ШИМ не рационально, но можно использовать DMA. Сформировав заранее весь кадр данных, можно одной командой запустить передачу данных. Вот протокол и готов. Расплата за удобство – излишнее использование памяти. На каждый бит переданных данных тратится 2 байта ОЗУ. Чтобы посылка была полностью автоматизированной, нужно добавить в начало сброс, а в конце «нулевой» ШИМ.
Для удобства создал структуры:
#define START_COUNT 48UL
#define LED_COUNT 109UL
#define STOP_COUNT 1UL
#define BUF_LEN START_COUNT+LED_COUNT*24+STOP_COUNT
#define LED_HIGH 58
#define LED_LOW 29
struct LEDS_COLOR_t{
uint8_t R;
uint8_t G;
uint8_t B;
};
struct WS2812_LEDS_t{
struct LEDS_COLOR_t num[LED_COUNT];
};
struct WS2812_RAWColor_t{
uint16_t G[8];
uint16_t R[8];
uint16_t B[8];
};
struct WS2812_BufDMA_t{
uint16_t startDelay[START_COUNT];
struct WS2812_RAWColor_t LEDs[LED_COUNT];
uint16_t stopDelay[STOP_COUNT];
};
Структура WS2812_LEDS_t описывает массив цветовой палитры, который доступен для пользователя. Это просто удобно (потратить дополнительно 327 байт уже не кажется проблемой).
Структура WS2812_BufDMA_t описывает расположение данных, которые будут использоваться для DMA. Передав указатель на структуру, фактически передается указатель на массив (здесь это работает, т.к. линковщик дополнительно не выравнивает структуру по 32 битному смещению, в некоторых ситуациях и железе это делать можно, но с костылями).
При инициализации структуры, массивы startDelay и stopDelay заполняются нулевыми значениями.
Процесс использования:
правим кадр (объект структуры WS2812_LEDS_t),
запускаем конвертацию (используя WS2812_LEDS_t генерируем WS2812_BufDMA_t),
запускаем DMA: HAL_TIM_PWM_Start_DMA(&htim2, TIM_CHANNEL_2,(uint32_t*)&WS2812_BufDMA,sizeof(struct WS2812_BufDMA_t)/sizeof(uint16_t));
Разработка печатной платы
Прежде чем разрабатывать печатную плату, начал изучать, а какие размеры платы и в какую стоимость выйдет такой заказ. Оказалось, что на популярном (ранее) китайском сайте для заказа печатных плат, максимальный размер без увеличения стоимости за габариты, составил 430мм по одной из сторон. В целом – это оптимальный размер для полноценной клавиатуры с NumPad-ом.
Посчитал, что запаивать переключатели на впервые разрабатываемом девайсе плохая идея – приобрел ответные части для хотсвапа механических переключателей.
Разметил размер 1u клавиши в 18мм, все успешно убралось.
Для согласования сигнального уровня 3.3в от микроконтроллера и 5в светодиодов, на плате поставил согласователь уровней (можно обойтись без него, проинвертировав сигнал через транзистор с подтяжкой к 5В, что тоже будет работать с правильной настройкой ШИМ).
В целом, по плате ничего сложного, пара кнопок на всякий случай, стабилизатор на 3.3в, USB-C. Ввиду достаточно длинных линий, повышается их емкость, пришлось в матричном опросе клавиатуры дополнительно вводить задержку в 20мкс после переключения выходов (чтобы входы, которые переключались на предыдущем шаге, успели «подтянуться» через встроенную подтяжку в лог «1»).
5 печатных плат 430мм х 130мм с пересылкой вышли в 2360р.
Поставил предохранитель на 1А, пришлось ограничить яркость белого света адресных светодиодов до 50%.
Печать кейкапов
Так как готовых кейкапов нет, пришлось их моделировать и печатать на 3D принтере самостоятельно. Получились типоразмеры 1u (обычные клавиши), 1.25u (ctrl, alt и т.п.), 1.5u (tab), 1.75u (caps), 2u (num_0), 2.25u (lshift), 2Ru (num_enter) и много нестандартных – под space, rshift, enter и т.п.
Печать происходила соплом 0.3, высота слоя 0.1. Один кейкап 1u печатался 36 минут. Лучше печатать смолой, но такого принтера у меня нет, зато есть 4 FDM
Печать заняла значительное время. Неспешно можно проверять:
Дальше пошел этап ошкуривания всех кейкапов. На полупрозрачном пластике казалось, что результат хороший, но окраска проявила небольшие недостатки.
Для окраски пришлось смоделировать и напечатать фиксаторы под кейкапы
Получилась такая конструкция, которая быстро печатается и позволяет равномерно окрасить кейкапы (если прямые руки и из аэрографа, а не как я, из балончика). Эти же фиксаторы использовались для последующего нанесения матового лака.
Пока ожидал печати фиксаторов, решил смазать все переключатели, т.к. в стоковом состоянии нажатие одной кнопки заставляет «шуршать» ближайшие. Приобрел достаточно дешевую смазку для пластика SW-92SA (возможно, специальные брендовые смазывают лучше, но этой я смазываю пластиковые напечатанные механизмы, и она у меня уже была). Потратил на 109 переключателей 4 часа.
Разбирал тонким пинцетом неспешно. Ни одна пружинка не улетела и не была потеряна.
Печать корпуса
Когда начал разрабатывать модель корпуса, понял что не зря приобрел ответные контакты для хотсвапа переключателей. Снимать/устанавливать переключатели пришлось не один раз, особенно при установке «Плейта»
Корпус разрабатывал так, чтобы его можно было напечатать на моем большом принтере (стол 400х250 мм), детали соединялись между собой без склейки.
В итоге весь корпус собирается на винтах m2.5 6мм и 12мм. Верхняя рамка имеет пазы для соединения. Не стал клеить или красить, фиксируется достаточно жестко, а ошкуривать нет желания. Корпус выполнен из черного PETg, смотрится необычно (для тех, кто не сталкивался с 3D печатью).
Покраска + гравировка
Покраску кейкапов производил акриловой краской по пластику, которая не требует предварительной грунтовки.
А вот с гравировкой пришлось повозиться. Когда-то давно, в 2016 году я собрал светодиодный лазерный ЧПУ (2W тогда для светодиодного лазера было много). Рама была ужасной, поигравшись с изготовлением печатных плат (выжигание черной краски на текстолите), станок был успешно разобран до лучших времен. Не так давно от одного из проектов осталась рама (CoreXY механика, обрезанная верхушка от проекта 3D-принтера FriBot) с рабочей областью 400х400 мм. Заказал для этой рамы еще одну рельсу на ось X. Перепроектировал крепления, загрузил прошивку GRBL и можно пользоваться.
Напечатанных кейкапов, с учетом брака, было с запасом. Их стал использовать для экспериментов и калибровки лазера.
Для разметки области включал лазер на 0.5% мощности и на скорости 5000мм/мин размечал границу гравируемой области в режиме вектора. Для гравировки было два прохода со 100% мощностью на скорости 3000мм/мин в режиме изображения.
После гравировки вся выгоревшая краска тщательно убиралась ватной палочкой.
После чистки гравировка выглядела достаточно резкой, с небольшими дефектами. После нанесения матового лака, символы приобрели более приятный вид.
Результат
Теперь можно поделиться результатом:
В выключенном состоянии (еще старый неудачный корпус):
Во включенном состоянии:
Индикация активных состояний Num/Caps/Scroll locks отображается на подсветке самих клавиш желтым цветом.
Никаких дополнительных эффектов не делал, пользуюсь данной клавиатурой уже 2 месяца. Был один глюк, но он решился увеличением таймаута в матричном опросе клавиатуры, о чем я писал выше.
Алгоритм работы
В последней версии прошивки переписал алгоритм работы, чтобы выложить исходники. В микроконтроллере запущены два таймера. Один с периодом 1мс, второй с периодом 10мс. Оба таймера генерируют прерывания.
С частотой 1мс происходит опрос состояний нажатия клавиш и передача Report-дескрипторов. Данное прерывание имеет приоритет выше. С частотой 10мс происходит чтение принятого дескриптора с состоянием Num/Caps/Scroll locks, подготовка буфера кадра для светодиодов и запуск ШИМ с DMA.
UPD:
В моей версии печатной платы я перепутал местами DP/DN у USB, когда создавал footprint у STM32, в выложенной версии на гите все исправил. 3D модели тут.
UPD2:
Прочитав комментарии, дополню: Статья не является руководством к действию, я просто делюсь результатом, который получился. Экономического смысла проект не имеет - это не разработка ради продукта, а продукт ради разработки. Наиболее ценный ресурс, который я получил - опыт и эмоциональное удовлетворение + небольшой бонус в виде работающего девайса. Отношусь к проекту так: "сделал, потому что смог".
Автор:
LitLageR