Помимо полноценных клавиатур, есть клавиатуры с несколькими кнопками, клавиши которых можно запрограммировать. Их называют еще макропадами. Макропад не сложно купить, хоть часто он стоит дороже обычной клавиатуры, но можно и сделать. Статей как его смастерить много, самый простой способ — использовать каждый вывод микроконтроллера, пока они не закончатся. Но у меня не было никакого желания делать все как у всех. Хотелось чего-то по-настоящему DIY.
Поэтому в статье пойдет подробный рассказ о разработке USB-клавиатуры на микроконтроллере CH32V003, в котором по умолчанию нет поддержки USB. Но чтобы проект был более интересным, клавиатура состоит из 9 кнопок, 3-х энкодеров и 2-х светодиодов. При этом корпус и кейкапы тоже сделаны самостоятельно (почти).
Но стоит еще добавить, что эта статья могла бы и не появится на свет, потому что когда сделал клавиатуру, то она работала с ошибками. Какое-то время я пытался их поправить, но сдался и забросил проект на месяц. Но вернулся с новыми знаниями, исправил ошибку в коде и оно заработало! Поэтому надеюсь, этот проект теперь может заслуживать вашего внимания.
❯ История идеи
Все началось где-то в начале 2025-го, когда я приобрел себе 3D-принтер на Новый год. Специально под праздники, чтобы можно было спокойно его изучить. У меня FDM принтер, поэтому пробовал то, что этот принтер мог делать. Сначала, конечно, скачивал модели из сети и печатал их, но потом захотел большего и начал делать их сам, поначалу в Blender, потому что его уже знал, но потом освоил FreeCAD.
Можно сказать, в то время я очень сильно воодушевлен был этой технологией, мне и сейчас она нравится, но уже мало что попадается нового, много попробовал. Так вот, у меня закралась мысль, что ведь я же хотел делать электронные устройства, поэтому мне нужен был 3д-принтер. И перед тем как придумывать какое электронное устройство делать следующим, я решил изучить Printables и составить список какие кто уже готовые устройства сделал, чтобы иметь хоть какое-то представление о моих возможностях. Тогда я и написал статью Идеи для печати на 3д принтере любителям электроники на 3DToday. Меня, конечно, опустили на землю и сказали, что этот список не больше, чем фантазия, но я его составил как примерный ориентир, какие мне бы интересны делать устройства.
И одним из пунктов в этом списке была макро клавиатура. Одной из популярной на Thingiverse является Raspberry Pi Pico Macro Keys. Если посмотрите на её страничку, то увидите, что там используется обычная плата разработчика Raspberry Pi Pico и кнопки припаиваются напрямую к её выводам. Т.к. кнопок 6 штук, то выводов хватает и в такой схеме ничего сложного нет. Все очень просто. Поэтому я и решил, что этот проект — делать макро клавиатуру — будет просто реализовать.


Я уже было заказал себе Raspeberry Pi Pico, но… передумал.

А передумал потому, что не понимал, зачем мне это нужно. К слову, и сейчас тяжело это понимаю, но глаз точно радуется. Но я эту идею включил же в список для любителей электроники, почему? А причина была в том, что мне было интересно изучить, как сделать её самому, с нуля, а не брать готовый проект. Когда так рассуждал, то у меня пробуждался запал и возникало желание заниматься этой идеей.
Дальше, что было в деталях не помню, знаю, что позже отошел от увлечения 3д печатью и начал изучать электронику. Т.к. меня интересовала мысль, чтобы максимально удешевить разработку электроники, то вскоре обнаружил микроконтроллер CH32V003. Он оказался очень дешевым и вполне сносным.
Потом я написал статью на Хабр про светодиодный брелок и понял, что пользователям понравилось мое хобби и тогда решил, что может быть написать еще одну статью и тут же подумал, что это шанс для макро клавиатуры.

rv003usb Вскоре я обнаружил библиотеку rv003usb, которая добавляла поддержку USB к CH32V003 программно. И тогда начал «прощупывать почву» и делать простые прототипы, чтобы понять на что способна библиотека. Про это можете почитать в Знакомство с программным USB на CH32V003 — первой части этой статьи. В ней я показал, как можно сделать клавиатуру с одной кнопкой и одним светодиодом используя rv003usb.
Вот такой был путь от идеи к тому, что я начал делать полноценную макро клавиатуру. Но в этот раз не с 6-ю кнопками, а с 9-ю, 3-мя энкодерами и 2-мя лампочками и всё с нуля, как и хотелось.
❯ Разработка
Печатные платы
Не помню точно разработка началась с рисования принципиальной схемы или моделирования корпуса. Возможно и то и другое: сначала одно, потом другое и так по кругу.
В конечном итоге, как всегда, мне понравилась идея делать модульно и все устройство разбил на части: плата контроллера (MacroPad Controller), плата с 3-мя энкодерами (Encoders Module), две платы со светодиодом (Led Module) и одна плата с разъемом usb (куплена на Aliexpress).
Модульная архитектура интересна еще тем, что модули заменяемые, если кто-то решит делать такое же устройство, например, на другом микроконтроллере, то может заменить только одну плату. Еще мне нравится, что отлаживать их проще и места занимают меньше. Минусы тоже есть, в основном, что надо эти модули между собой состыковывать, в данном случае припаивать проводами, да с одной монолитной платой работать проще.
Плата контроллера

Плата контроллера задумывалась как головной центр устройства, т.е. все модули подключались бы к ней.
За основу взят микроконтроллер CH32V003F4P6, т.е. в TSSOP-20 корпусе. Для работы USB необходим регулятор и резисторы на линии D-, D+, DPU. Более подробно можете почитать в первой статье, там описываются разные схемы подключения. Эта схема подключения лучше всего работает.
Дальше вы можете заметить 3 секции: кнопки, светодиоды, энкодеры.
С энкодерами и светодиодами все понятно — это просто выводы для подключения модулей.

А вот кнопки, это матрица из кнопок. Работает очень просто: подаем на один столбец высокий уровень сигнала и считываем поочередно каждую строку. Если в строке высокий уровень, то кнопка зажата, если низкий, то разжата. Схема действия очень похожа на работу со светодиодной матрицей.
Но есть один нюанс и вы его уже могли заметили. Это диоды. Они нужны, потому что если зажать несколько кнопок одновременно (от трех и больше), которые не находятся на одной линии, то откроются лишние линии, которые не нужны. Из-за этих фантомных сигналов происходят ложные срабатывания. Решение, это добавить диоды, тогда ток не будет течь туда, куда не должен. Я попытался коротко и просто объяснить проблему матричных клавиатур и не стал углубляться, потому что материала очень много и думаю есть какая-нибудь статья даже на Хабре, но я пользовался этой.

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


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


Один из вариантов работы с энкодером может быть такой: снимаем сигналы A и B, если A не равен B, то значит у нас произошел поворот, следующим шагом надо понять в какую сторону произошел поворот (делается через xor A и B) затем ждем когда снова изменятся A и B.
Думаю схему подключения тоже можно было бы упростить, убрать резисторы и конденсаторы, но с ними однозначно должно быть лучше, меньше дребезга, т.к. это простая RC цепочка.
Плата модуля светодиода

Здесь все просто. Светодиод и токоограничивающий резистор. Таких модулей будет два.
Плата USB разъема

Изначально я хотел, чтобы разъем USB был на плате контроллера, но быстро понял, что лучше его вынести. Этот модуль у меня уже был и изготавливать ничего не пришлось, но в теории это можно и самому сделать.
Производство печатных плат


Платы я делаю на фрезерном станке и постепенно улучшаю свои процессы. На фотографиях можно заметить как я уже научился накладывать простенькую шелкографию. Стал добавлять дату. Как видно разводка была сделана еще 25 сентября.
А вот на обратной стороне можно увидеть много меди. После этого проекта я стал её полностью счищать в своих новых проекта, потому что замучился с ложными срабатываниями кнопок, когда острова меди касаются друг друга.
С другой стороны эти острова меди научили меня хорошо работать мультиметром, т.к. если вижу две рядом дорожки, то чаще просматриваю их на свет или прозваниваю мультиметром.
По крайней мере у меня не получается аккуратно и без изъянов изготавливать плату на фрезерном станке, легко может быть что-то где-то не счиститься и замкнется. Поэтому где могу там стараюсь обезопасить себя счищением меди, это на самом деле делается просто и быстро фрезой кукуруза, а потом ручной постобработкой чертилкой.
Но не смотря на все это платы получились хорошими и работающими, переделывать ничего не пришлось.
Еще заметил, что в этом проекте использую обычные перемычки, тогда как сейчас перешел на 0R резисторы, после комментариев на Хабре к другой статье.
❯ 3д печать
Корпус

Проект во FreeCAD у меня состоит из 4-х частей: верх, низ, колпачки под светодиоды и референсы.

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

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

Колпачок для светодиода нужно было сделать очень тонкой пластинкой, чтобы через неё проходил свет.

Референсы меня часто выручают. Это 3д модели, экспортируемые из EasyEDA, т.е. из разводки печатной платы. На основе их очень точно получается проставить размеры и разместить отверстия под винты.
Кейкапы
Кейкап — это другими словами колпачок для клавиш.
Один день я пролистывал ленту новостей и наткнулся на кейкап Hornet из Hollow Knight и звезды сошлись, теперь я понимал зачем мне макропад — коллекционировать кейкапы.


Интересная штука, один кейкап я купил примерно за 800 рублей, а другой за 500, оба по Hollow Knight. Мне кажется себестоимость моей клавиатуры будет в разы меньше, чем один такой кейкап. В свое оправдание скажу, что я не предполагал, что на FDM принтере у меня получатся сделать хорошие кейкапы, плюс мне очень хотелось их приобрести.

Например, кейкап Hollow Knight купил на маркетплейсе и когда его получил, то увидел на нем слои. Т.е. он был распечатан на FDM принтере и затем покрашен! Быстрым поиском в сети я нашел эту модель, получается можно было все эти кейкапы и самому сделать. Но не страшно, конечно, дальше я уже оторвался и напечатал все сам что нашел и понравилось в сети.

Самым действенным способ оказалось взять базовую модель кейкапа и добавить к ней иконки.

На фото со всеми кейкапами может заметить два пакетика, вот эти оказались не нужными, т.к. я думал, что на моем принтере получаться плохие кейкапи, поэтому взял готовые и планировал приклеить на них наклейки. Но у меня получились очень даже ничего кейкапы. Поэтому эти пакетики с клавиши не пригодились.
Ссылки на бесплатные кейкапы с Printables, используемые в клавиатуре, я оставил в репозитории. Там же можете найти все модели, которые я распечатал.
❯ Сборка

На фотографии выше вы можете видеть интересную картину, как вместо того, чтобы припаять провода напрямую я добавил разъемы JST. Мне казалась эта хорошей идеей, чтобы можно было подсоединять и отсоединять разные модули. Но на практике оказалось, что места под провода не хватает, разъемы слишком высоко торчат и корпус нужно было бы переделывать, делать в два раза выше. Потом я подумал и решил все выпаять и припаять напрямую как делают все. Не выпендриваться. А жалко, т.к. я уже готовые провода с разъемами купил.

В финальной сборке, все уместилось, но есть терпимая проблема — крышка легко не отсоединяется. Поэтому очень тяжело подлезть за плату и посмотреть, где контакты замкнулись из-за моих островов меди. Поэтому я и говорил, что замучился с этим.
❯ Программирование
Многое в этом проекте используется из первой моей статьи. Поэтому я не буду полностью объяснять настройку и конфигурацию USB, а сразу перейду к алгоритмам. Возможно для всех читателей так будет даже лучше.
Я опущу обычные вещи, такие как настройка проекта или мигание светодиодов, а опишу основные классы для работы клавиатуры и энкодеров.
Важная деталь: -DCPLUSPLUS
Мне нравится использовать С++, но почему-то код работал очень странно или вообще не работал. Вроде все делаю правильно. Оказалось, надо добавить флаг-DCPLUSPLUS и все заработало. Без него я потратил кучу времени впустую, когда класс, который должен работать, почему-то отказывался выполнять простые операции.
Обработка клавиатуры
Keyboard.h
#ifndef _KEYBOARD_H
#define _KEYBOARD_H
#include "config.h"
#include "BitArray.h"
#define ROW_SIZE 3
#define COL_SIZE 3
class Keyboard {
uint8_t _array1Buffer[CEILING(ROW_SIZE * COL_SIZE, 8)];
uint8_t _array2Buffer[CEILING(ROW_SIZE * COL_SIZE, 8)];
BitArray _bitArray1;
BitArray _bitArray2;
const uint8_t Rows[ROW_SIZE] = { ROW1, ROW2, ROW3 };
const uint8_t Cols[COL_SIZE] = { COL1, COL2, COL3 };
volatile bool _inTick;
public:
Keyboard();
void Init();
void Tick();
bool IsSet(uint32_t position);
};
#endifKeyboard.cpp
#include "Keyboard.h"
Keyboard::Keyboard() :
_bitArray1(_array1Buffer, ROW_SIZE * COL_SIZE),
_bitArray2(_array2Buffer, ROW_SIZE * COL_SIZE)
{
_bitArray1.Clear();
_bitArray2.Clear();
}
void Keyboard::Init() {
for (uint8_t c = 0; c < COL_SIZE; c++) {
funPinMode(Cols[c], GPIO_CFGLR_OUT_10Mhz_PP);
funDigitalWrite(Cols[c], FUN_LOW);
}
for (uint8_t r = 0; r < ROW_SIZE; r++) {
funPinMode(Rows[r], GPIO_CFGLR_IN_PUPD);
}
}
bool Keyboard::IsSet(uint32_t position) {
return _bitArray1.IsSet(position);
}
void Keyboard::Tick() {
_inTick = true;
uint8_t position = 0;
_bitArray2.Clear();
for (uint8_t c = 0; c < COL_SIZE; c++) {
funDigitalWrite(Cols[c], FUN_HIGH);
for (uint8_t r = 0; r < ROW_SIZE; r++) {
_bitArray2.Set(c + r * COL_SIZE, funDigitalRead(Rows[r]) == FUN_HIGH);
position++;
}
funDigitalWrite(Cols[c], FUN_LOW);
}
// Swap buffers.
BitArray tmp = _bitArray2;
_bitArray2 = _bitArray1;
_bitArray1 = tmp;
_inTick = false;
}
#endifКлавиатура знает об именах пинов в Rows и Cols. Использует вспомогающий класс BitArray, его можно было не создавать, но мне хотелось. И в самом классе три функции: Init, Tick, IsSet.
В Init происходит конфигурация пинов, Cols на вывод, а Rows на вход.
Tick вызывается в бесконечном цикле и устанавливает в битовых массивах флаги о нажатых клавишах. Алгоритм описывал выше, но повторю: подаем высокий уровень на столбец и снимаем показания со строки, если там замечен высокий уровень, то кнопка зажата, а если низкий, то нет. Затем у меня происходит двойная буферизация. Наверно её можно убрать, но мне кажется правильным делать одну операцию до конца и уже заявлять, что данные готовы к обработке, чем оставлять данные в промежуточном состоянии. Это должно быть лучше.
IsSet используется, когда хотим узнать нажата ли клавиша.
Больше функций не потребовалось.
Обработка энкодеров
Encoder.h
#ifndef _ENCODER_H
#define _ENCODER_H
#include "config.h"
class Encoder {
uint8_t _aPin;
uint8_t _bPin;
public:
volatile int8_t position;
Encoder(uint8_t aPin, uint8_t bPin) : _aPin(aPin), _bPin(bPin) {
}
void Init() {
funPinMode(_aPin, GPIO_CFGLR_IN_PUPD);
GpioOf(_aPin)->OUTDR |= 1 << ((_aPin)&0xf);
funPinMode(_bPin, GPIO_CFGLR_IN_PUPD);
GpioOf(_bPin)->OUTDR |= 1 << ((_bPin)&0xf);
}
bool _clkState;
void Tick() {
bool e0 = funDigitalRead(_aPin) == FUN_LOW;
if (_clkState != e0) {
bool e1 = funDigitalRead(_bPin) == FUN_LOW;
if (e0 ^ e1) {
position--;
} else {
position++;
}
_clkState = e0;
}
}
};
#endifЭнкодер представляет собой один класс, который знает о состоянии своих пинов. В Init происходит конфигурация пинов на вход, с подтяжкой вверх. В Tick происходит сам алгоритм увеличения или уменьшение position при вращении. Алгоритм простой: один пин считаем и запоминаем, затем считаем следующий, если они не равны, то значит энкодер повернули, дальше определяем в какую сторону произошло вращение и исходя из этого либо увеличиваем, либо уменьшаем position.
Когда надо отправить сообщение о нажатой клавиши, просто считываем position, отправляем исходя из значения и обнуляем.
❯ Отправка событий о нажатиях и поворотах
После того, как теперь понятно как узнается состояние о нажатых клавишах или поворотах, можно посмотреть как я организовал отправку сообщений по USB.
main.cpp
#include "ch32fun.h"
#include <stdio.h>
#include <string.h>
#include "rv003usb.h"
#include "Keyboard.h"
#include "Encoder.h"
#define LED1 PC0
#define LED2 PC1
Keyboard keyboard;
Encoder encoder1 = Encoder(PD5, PD6);
Encoder encoder2 = Encoder(PD7, PA1);
Encoder encoder3 = Encoder(PA2, PD0);
volatile int8_t position1;
volatile int8_t position2;
volatile int8_t position3;
hid_keyboard_report_t buttonDataToSend = { 0 };
volatile bool dataHasBeenSent = false;
int main()
{
SystemInit();
funGpioInitAll();
funPinMode(LED1, GPIO_CFGLR_OUT_10Mhz_PP);
funPinMode(LED2, GPIO_CFGLR_OUT_10Mhz_PP);
keyboard.Init();
encoder1.Init();
encoder2.Init();
encoder3.Init();
Delay_Ms(1); // Ensures USB re-enumeration after bootloader or reset; Spec demand >2.5µs ( TDDIS )
usb_setup();
while(1) {
#if RV003USB_EVENT_DEBUGGING
uint32_t * ue = GetUEvent();
if( ue )
{
printf( "%lu %lx %lx %lx\n", ue[0], ue[1], ue[2], ue[3] );
}
#endif
if (keyboard.IsSet(8)) {
FLASH->BOOT_MODEKEYR = 0x45670123; // KEY1
FLASH->BOOT_MODEKEYR = 0xCDEF89AB; // KEY2
FLASH->STATR = 0x4000;
RCC->RSTSCKR |= 0x1000000;
PFIC->CFGR = 0xBEEF0080;
}
keyboard.Tick();
encoder1.Tick();
encoder2.Tick();
encoder3.Tick();
position1 += encoder1.position;
encoder1.position = 0;
position2 += encoder2.position;
encoder2.position = 0;
position3 += encoder3.position;
encoder3.position = 0;
hid_keyboard_report_t buttonData = {0};
uint8_t pressed = 1;
if (position1 < -1) {
buttonData.keycode[pressed++] = HID_KEY_BRACKET_RIGHT;
position1 = 0;
} else if (position1 > 1) {
buttonData.keycode[pressed++] = HID_KEY_BRACKET_LEFT;
position1 = 0;
}
if (position2 < -1) {
buttonData.keycode[pressed++] = HID_KEY_EQUAL;
position2 = 0;
} else if (position2 > 1) {
buttonData.keycode[pressed++] = HID_KEY_MINUS;
position2 = 0;
}
for (uint8_t i = 0; i < 8; i++) {
if (keyboard.IsSet(i)) {
uint8_t key = HID_KEY_NONE;
switch (i) {
case 0:
key = HID_KEY_0;
break;
case 1:
key = HID_KEY_1;
break;
case 2:
key = HID_KEY_2;
break;
case 3:
key = HID_KEY_3;
break;
case 4:
key = HID_KEY_4;
break;
case 5:
key = HID_KEY_5;
break;
case 6:
key = HID_KEY_6;
break;
case 7:
key = HID_KEY_7;
break;
case 8:
key = HID_KEY_8;
break;
}
buttonData.keycode[pressed++] = key;
}
if (pressed > 5) {
break;
}
}
if (pressed >= 2 && pressed <= 5) {
buttonData.keycode[0] = HID_KEY_M;
buttonData.modifier |= KEYBOARD_MODIFIER_LEFTALT | KEYBOARD_MODIFIER_LEFTSHIFT;
} else {
memset(&buttonData, 0, sizeof(buttonData));
}
if ((memcmp(&buttonData, &buttonDataToSend, sizeof(buttonData)) != 0 || dataHasBeenSent) && pressed >= 2) {
memcpy(&buttonDataToSend, &buttonData, sizeof(buttonData));
dataHasBeenSent = false;
}
}
}
void usb_handle_user_data( struct usb_endpoint * e, int current_endpoint, uint8_t * data, int len, struct rv003usb_internal * ist )
{
if (current_endpoint == 0 && len > 0) {
if (data[0] & KEYBOARD_LED_CAPSLOCK) {
funDigitalWrite(LED1, FUN_HIGH);
} else {
funDigitalWrite(LED1, FUN_LOW);
}
}
}
uint8_t encoderData;
void usb_handle_user_in_request( struct usb_endpoint *e, uint8_t *scratchpad, int endp, uint32_t sendtok, struct rv003usb_internal * ist )
{
if (endp == 3) {
encoderData = 0;
if (position3 > 1) {
encoderData |= 2;
position3 = 0;
} else if (position3 < -1) {
encoderData |= 1;
position3 = 0;
}
usb_send_data(&encoderData, sizeof(encoderData), 0, sendtok);
} else if( endp == 2 )
{
if (!dataHasBeenSent) {
usb_send_data(&buttonDataToSend, sizeof(buttonDataToSend), 0, sendtok);
dataHasBeenSent = true;
} else {
hid_keyboard_report_t empty = { 0 };
usb_send_data(&empty, sizeof(empty), 0, sendtok);
}
}
else
{
usb_send_empty( sendtok );
}
}Привожу весь файл целиком, чтобы было проще и удобнее посмотреть, не стал обрезать. Кому интересно будет посмотрят, а я объясню ключевой алгоритм:
В бесконечном цикле происходит вызов всех функций Tick. У клавиатуры и у энкодеров. В зависимости от нажатых клавиш на клавиатуре подготавливаются данные для отправки по USB. Я так сделал, потому что функция отправки по USB походу вызывается в прерывании и это для того, чтобы его лишний раз не нагружать. В принципе, можно все в функции отправки сообщений делать.
Пока я решил, что нажатие клавиши на макро клавиатуре фиксируется как нажатие нескольких клавиш M + Left Alt + Left Shift + Клавиша. Т.е. четыре. Это сделано для того, чтобы потом повесит на это сочетание клавиш какое-то событие.
Функция отправки сообщений, usb_handle_user_in_request, опрашивается системой с какой-то переодичностью, если есть какие-то данные, чтобы отправить, они туда вставляются, если нет, то нет. И все.
❯ Отчаяние
И до этого момента, у меня все получалось. Устройство готово и выполняет нужные команды. Только возникла одна проблема — когда кручу энкодер, то он может залипнуть и громкость уйти в потолок или, наоборот, упасть вниз. В самом начале, я написал, что из-за этой проблемы хотел забросить проект и не писать статью, но потом я случайно увидел, что в rv003usb есть отладка, т.е. он без проблем может логировать сообщения через printf. Надо только все настроить и оно заработает.
А знаете что самое интересное? Я все это время отлаживал программу только через два светодиода. Можно, конечно, было разобрать корпус, подключить как-то отладку и увидеть стандартные printf, но оно сбоило. Когда я подключал отладку напрямую и плата была подключена по USB, т.е. в нее приходило 2 питания: от компьютера и от программатора. И тогда программатор, любой, WCH-LinkE и minichlink, отваливался. И то, что я добавил диод на линию SWIO тоже не помогло.
В итоге, я подключил отладку средствами rv003usb и оно заработало. И я достаточно быстро нашел ошибку.
Она была в этом коде, который теперь уже удален
uint8_t encoderData;
void usb_handle_user_in_request( struct usb_endpoint *e, uint8_t *scratchpad, int endp, uint32_t sendtok, struct rv003usb_internal * ist )
{
if (endp == 2) {
if (position3 > 0) {
encoderData = 2;
position3 = 0;
} else if (position3 < 0) {
encoderData = 1;
position3 = 0;
} else {
encoderData = 0;
position3 = 0;
usb_send_empty( sendtok );
return;
}
usb_send_data(&encoderData, sizeof(encoderData), 0, sendtok);
}
// Остальной код
}
Как вы можете заметить, когда никакая кнопка не была нажата я отправлял пустое сообщение (usb_send_empty), после него у клавиатуры сносило крышу и она начинала отправлять повторно предыдущее событие. Кто знал, по моей логике я делал все правильно, но оказалось, что правильно слать не пустое сообщение, а сообщение о нажатии пустой клавиши.
❯ Тестирование
Клавиатура собрана, затея удалась, но надо немного хотя бы ею попользоваться. Понять, что делать дальше. И что можно вообще с помощью неё сделать.
Поиск в интернете выдает достаточно интересные рекомендации, использовать QMK/VIA. Но как оказалось, это прошивка для AVR микроконтроллеров. Поэтому кто захочет повторить что-то похожее, подумайте, может быть имеет смысл взять другой микроконтроллер и использовать готовую прошивку для макро клавиатуры. Или добавить в QMK свою. В любом случае, это более правильное решение, но конкретно не наш случай.
Пока я придумал такой способ взаимодействия: одно нажатие на макро клавиатуре равно нажатию четырех клавиш M + Left Alt + Left Shift + Клавиша и на это сочетание клавиш повесит обработчик в Windows.
Я нашел два решения для Windows: использовать PowerToys и AutoHotkey.
AutoHotkey выглядит очень интересным и я наверно позже им займусь, но для начала хотел попробовать, что-то попроще, поэтому воспользовался PowerToys.
Для того, чтобы задать обработчик сочетаний клавиш в PowerToys. Нужно зайти в раздел Keyboard Manager, включить его, открыть окно Remap a shortcut. Добавить переопределение клавиши. Надо будет нажать галочку Allow Chords, потому что у нас используется больше чем одна клавиша в сочетании, не считая клавиш-модификаторов.

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

И все, теперь по нажатию на 3-ю клавишу, будет открываться страница с Хабром.
Пока я заметил несколько ограничений, что можно на сочетание клавиш задавать 4-е действия: переопределить на другое сочетание клавиш, отправлять текст, запускать программу и открывать страницу в браузере. На самом деле не много, что можно сделать, например, нельзя сделать, что какое-то сочетание клавиш отправляло событие прокрутки колеса мыши или еще чего-то. Но и этого списка должно хватить для повседневного использования.
Еще одна проблема, что когда переопределяешь одну клавишу на другую, то нельзя выбрать что-то из списка, надо сочетание клавиш нажать на клавиатуре, из-за этого могут возникнуть трудности на переопределение на не стандартные клавиши (например, изменение громкости).
Теперь у вас должно быть понимание, как можно использовать макро клавиатуру.
А вот AutoHotkey позволяет вместо этих 4-х действий вешать исполнение своих скриптов, что достаточно интересно и надо будет потом попробовать.
❯ Заключение

В заключение скажу, что после этого проекта мне очень понравилось работать с библиотекой rv003usb, когда не нужно доставать программатор и очень удобно, когда работает и программирование и отладка сразу по USB. Поэтому хочу добавить её в свои остальные проекты по мере возможностей. Возможно, вы не заметите разницы с отладочными платами Arduino, в которых это есть по-умолчанию, просто Arduino нас знатно избаловало и принимаем поддержку USB как данность.
Проект же макро клавиатуры оказался очень поучительным для меня, который начал тянуться почти год назад, но в итоге я его сделал и у меня теперь есть работающая клавиатура. Пока не знаю что с ней делать, кроме регулирования громкости, но значит буду собирать кейкапы.
Единственный недостаток, который я сообразил позже, это надо было вместо светодиодов добавить OLED дисплей по I2C, как раз 2 лишний провода оставалось. Но сразу не догадался, а было бы гораздо лучше, в особенности из-за того, что светодиодами почти не пользуюсь.
Надеюсь вам было интересно следить за этим проектом тоже. Но серию с rv003usb я еще не закончил, у меня есть одна идея для следующей статьи, но боюсь тоже проблемная и придется долго её вынашивать. И которая может опять не получится. Так что посмотрим.
Кому нравится то, чем я занимаюсь, то можете следить за мной в моей группе в ВК.
Все файлы проекта находятся в репозитории MacroPad.
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩