Пришла моя долгожданная плата EC для клавиатуры Agar Mini. Собрал, начал пользоваться, но вот что-то слишком много опечаток. Думал, повожусь с настройками, да не тут-то было — приключение растянулось на 7 часов.В статье о том, что такое EC, как отдебажить проблему и внести фикс в прошивку. Не обошлось и без реверс-инжиниринга.Все опечатки неслучайны. Читать далее
Пришла моя долгожданная плата EC для клавиатуры Agar Mini. Собрал, начал пользоваться, но вот что-то слишком много опечаток. Думал, повожусь с настройками, да не тут-то было — приключение растянулось на 7 часов.
В статье о том, что такое EC, как отдебажить проблему и внести фикс в прошивку. Не обошлось и без реверс-инжиниринга.
Все опечатки неслучайны.

8 вечера, казус: при быстрой печати клавиши нажимаются не в том порядке. Пример — название статьи.
Сперва понадеялся, что получится решить проблему сбросом настроек по умолчанию. По инструкции клавиатура настраивается в Vial. Я побегал по вкладкам и понажимал везде reset.

Вроде печатается нормально — как будто починилось. Калвиатура огноь! Ояпть?
Решил не церемониться и почистить persistent memory, включая всю кастомизацию, до которой не добрался в первый раз. Назначил QK_CLEAR_EEPROM на левый Alt и тут же нажал на левый Alt.

Это было ыб слишком легок, и статьи бы не блыо.
Что, если что-то собрано неправильно? Вдруг болты затянуты слишком туго или резинки-подкладки съехали — и это как-то повлияло на значения при нажатии кнопок? Если интересно, на внутренности Agar Mini EC можно посмотреть на видео.
Пересобрал — не помогло.
Но я наконец-то нашел простой способ воспроизведения. Очень быстро пишешь «лак», но иногда печатается «алк» или даже «кал». Имея репродьюсер, я открыл Karabiner-EventViewer и проследил, как происходят ошибки во время быстрого написания «лак»:
Нажатие на «л» ничего не регистрирует,
нажатие на «а» ничего не регистрирует,
нажатие на «к» регистрирует down к (тут печатается «к»),
отпускание «а» регистрирует down а, up а (тут печатается «а»),
отпускание «л» регистрирует down л, up л (тут печатается «л»),
отпускание «к» регистрирует up к.
Еще немного поэкспериментировав, заметил, что абсолютно все клавиши, кроме одной — «w», — ничего не регистрируют при быстром зажатии до упора. И только когда отпускаешь, получается и нажатие, и отпускание.
EC Edition is power by QMK… with VIAL real-time configuration tools. — сайт производителя.
Если бы все клавиши не работали на быстром нажатии, можно было бы поменять «EC Actuation Point» внутри Vial. Но одна клавиша работала, и это было странно.
Для начала сделаем шаг назад. Что такое EC?
EC-клавиатуры (electrostatic capacitive, электроёмкостные) работают, замеряя изменение электрического поля при нажатии на клавишу. Механизм старый, популяризированный компанией Topre на рынке клавиатур.
Что нам надо понимать, EC-переключатель — это аналоговый механизм, продуцирующий некий диапазон значений. На значения могут влиять:
жесткость пружины, ее положение, деформации;
материал и состояние EC-плунжера;
смазка на плунжере, пружине и PCB;
положение PCB в корпусе;
пыль и прочие загрязнения на PCB;
профили и форма кейкапов, посадка на стем;
влажность воздуха, температура.
Скорее всего, большая часть из перечисленного если и влияет, то очень слабо, поэтому пересборка и очистка клавиатуры не помогли. Давайте возвращаться к диапазону значений (ADC). Видимо, на одной клавише, которая работает при быстрых нажатиях, генерируется другой диапазон, который считывается прошивкой как нажатие.
Я начал гуглить, как этот диапазон замерить или откалибровать, и раз за разом натыкался на эту инструкцию:
Видео показывает, как откалибровать EC: открываем Via, добавляем свою прошивку (под видео ссылка на файлы), переходим в режим калибровки и жмем на каждую клавишу. Файла для моей прошивки в его архивах не нашел — скачал с официального сайта. Загрузил в Via, но кнопка для калибровки отсутствовала.
Via — open-source, а значит, можем собрать локально и подебажить. Покопавшись в исходниках Via, нашел, что EC-калибровка поддерживается только в V3 в виде кастома, который описывается в JSON-файле. Вот пример EC-клавиатуры с поддержкой кнопок калибровки. Подобного не было в файле с официального сайта Agar Mini. И в документации YDKB видно, что Agar Mini поддерживает V2 (а не V3).
Чтобы реализовать калибровку (или хотя бы визуализацию диапазона ADC), нам придется разобраться с прошивкой.
В репозитории YDKB есть ссылка на Agar Mini EC и там же любезное приглашение в исходники:
If you need source code, you can visit: https://github.com/yangdigi/vial-qmk-v5/tree/ava/keyboards/ydkb/
Я посетил местечко и нашел keymaps/agar_mini_ec_vial.
В родительской папке нашлись и правила: значения в диапазоне от ec_ap_value до 160 считаются за key down. Теперь бы понять, какие значения ADC производит проблемная клавиатура.
По слову «adc» нашелся нужный код в файле rawhid_user.c — функция raw_hid_receive_kb. Raw HID — канал коммуникации с клавиатурой, _kb — keyboard. Из функции видно, что
`command_id == 0xFD`
`data[1] == 0xA3` // ADCИспользуя значения выше и hidapitester, можно пообщаться с клавиатурой. Скрипт:
#!/usr/bin/env bash
for _ in {1..6}; do
line=$(
./hidapitester \
--vidpid 9D5B:2509 \
--usagePage 0xFF60 \
--usage 0x61 \
--open \
--send-output 0xFD,0xA3 \
--length 32 \
--read-input \
--close 2>&1 |
awk '/read 32 bytes:/{getline; print; exit}'
)
read -ra bytes <<< "$line"
row_byte=$((16#${bytes[2]#0x}))
printf "Row%d" "$((row_byte >> 4))"
for col in {0..7}; do
byte="${bytes[$((col + 3))]#0x}"
printf " %d" "$((16#$byte))"
done
printf "\n"
done
Значения взяты из:
--vidpid 9D5B:2509 \ # в ydkb_kbdfans_agar_mini_ec_via.json с официального сайта
--usagePage 0xFF60 \ # https://docs.qmk.fm/features/rawhid#basic-configuration
--usage 0x61 \ # https://docs.qmk.fm/features/rawhid#basic-configuration
--length 32 \ # https://docs.qmk.fm/features/rawhid
--send-output 0xFD,0xA3 \ # взяли из raw_hid_receive_kb, raw-данные и переводим ADC из 16-ричной в 10-тичную
--close 2>&1 Пример ответа:
Row1 94 88 98 108 86 71 92 62
Row2 74 88 98 220 88 98 92 107
Row3 91 178 90 57 94 90 99 94
Row4 89 93 90 65 85 84 91 61
Row5 87 96 98 86 108 86 89 67
Row0 92 84 87 59 92 97 82 101Я добавил Via-интерфейс для визуализации, можно собрать локально из ветки local и загрузить json-файл AgarMini с официального сайта. Нажатия на клавиши показывают продуцируемые значения в интерейсе. Выглядеть это будет так:

PR ребятам тоже сделал, но его предсказуемо закрыли.
Теперь, имея визуализацию ADC, я понажимал на все клавиши и вот что обнаружил. Работающая клавиша при полном нажатии давала значение 155, а все остальные клавиши — от 180 до 220. Помните уже ранее найденные нами правила? Всё, что выше 160, — не нажатие.
if (ec_key_value[row][col] > 40 && ec_key_value[row][col] < ec_rp_value) return 0;
else if (ec_key_value[row][col] < 160 && ec_key_value[row][col] >= ec_ap_value) return 0x80;
else return 0b10;Было уже 1:30 ночи, на более детальный разбор меня не хватило. Решил действовать в лоб — поправить в прошивке один-единственный байт, который изменит 160 на 220. Тогда рабочий диапозон будет не ec_ap_value..160, а ec_ap_value..220, и тогда все клавиши в него попадут.
Прошивку — ydkb_kbdfans_agar_mini_ec_vial.uf2 — скачиваем с официального сайта.
file ydkb_kbdfans_agar_mini_ec_vial.uf2
# UF2 firmware image, family 0x9d5becf1, address 0x8004000, 194 total blocksСпецификации UF2 можно посмотреть здесь.
Наша цель — получить дизассемблер, чтобы проще было найти нужный участок кода со сравнением 40 и 160. Переведем uf2 в bin, затем bin — в elf, и наконец elf — в disasm.
Ниже небольшой скрипт на JS (он был под рукой после работы над Via), чтобы перевести .uf2 в .bin. Скрипт — рутина, можно попросить любую LLM переписать на что угодно, справится даже локальная Gemma 4B. Магические байты взял из спецификаций, там же описано, что данные упакованы в блоки по 512 байтов.
const fs = require("fs");
const input = process.argv[2];
const output = process.argv[3];
if (!input || !output) {
console.error("usage: node uf2_extract.js input.uf2 output.bin");
process.exit(2);
}
const uf2 = fs.readFileSync(input);
if (uf2.length % 512 !== 0) {
throw new Error(`UF2 size is not a multiple of 512: ${uf2.length}`);
}
const MAGIC0 = 0x0a324655;
const MAGIC1 = 0x9e5d5157;
const MAGIC_END = 0x0ab16f30;
let minAddr = Number.POSITIVE_INFINITY;
let maxAddr = 0;
const blocks = [];
for (let off = 0; off < uf2.length; off += 512) {
const magic0 = uf2.readUInt32LE(off + 0);
const magic1 = uf2.readUInt32LE(off + 4);
const target = uf2.readUInt32LE(off + 12);
const payloadSize = uf2.readUInt32LE(off + 16);
const blockNo = uf2.readUInt32LE(off + 20);
const numBlocks = uf2.readUInt32LE(off + 24);
const magicEnd = uf2.readUInt32LE(off + 512 - 4);
if (magic0 !== MAGIC0 || magic1 !== MAGIC1 || magicEnd !== MAGIC_END) {
throw new Error(`bad UF2 magic at file offset ${off}`);
}
if (payloadSize > 476) {
throw new Error(`payload too large at block ${blockNo}: ${payloadSize}`);
}
minAddr = Math.min(minAddr, target);
maxAddr = Math.max(maxAddr, target + payloadSize);
blocks.push({ off, target, payloadSize, blockNo, numBlocks });
}
const bin = Buffer.alloc(maxAddr - minAddr, 0xff);
for (const block of blocks) {
uf2.copy(
bin,
block.target - minAddr,
block.off + 32,
block.off + 32 + block.payloadSize,
);
}
fs.writeFileSync(output, bin);
console.log(`blocks: ${blocks.length}`);
console.log(`address range: 0x${minAddr.toString(16)}..0x${maxAddr.toString(16)}`);
console.log(`output bytes: ${bin.length}`);
console.log(`base address: 0x${minAddr.toString(16)}`);Запускаем скрипт и получаем bin.
node uf2_extract.js ydkb_kbdfans_agar_mini_ec_vial.uf2 agar.binТеперь надо добыть человекочитаемый ассемблер, чтобы найти позицию нужного байта. В исходниках прошивки есть ссылка на STM32F103 микроконтроллер, где сказано, что это ARM Cortex-M3.
## Если нет LLVM, установите.
# brew install llvm
/opt/homebrew/opt/llvm/bin/llvm-objcopy \
-I binary \
-O elf32-littlearm \
-B arm \
agar.bin \
agar.elf
/opt/homebrew/opt/llvm/bin/llvm-objdump \
-D \
--triple=thumbv7m-none-eabi \
--mcpu=cortex-m3 \
--print-imm-hex \
agar.elf \
> agar.disasmВ файле agar.disasm нужно найти соседствующие строчки с инструкциями > 40 и < 160 (помните правила?). При этом компилятор мог заменить > 40 на >= 41 и < 160 на <= 159.
Инструкция сравнения — это cmp, 40 и 41 в шестнадцатеричной системе — 28 и 29, а 159 и 160 — 9f и a0. Напишем скрипт, чтобы найти такие участки.
awk '
/cmp.*#0x(28|29)/ { low=NR }
/cmp.*#0x(9f|a0)/ && low && NR-low < 30 {
print "candidate: lines " low "..." NR
}
' agar.disasm
Напечаталась одна строка:
candidate: lines 3787...3793И вот что там было:
239c: 2b28 cmp r3, #0x28
239e: d905 bls 0x23ac <_binary_agar_bin_start+0x23ac> @ imm = #0xa
23a0: 4a0b ldr r2, [pc, #0x2c] @ 0x23d0 <_binary_agar_bin_start+0x23d0>
23a2: 7812 ldrb r2, [r2]
23a4: 429a cmp r2, r3
23a6: d809 bhi 0x23bc <_binary_agar_bin_start+0x23bc> @ imm = #0x12
23a8: 2b9f cmp r3, #0x9f0x9f — это тот самый 159. И нужная позиция — 2b9f. ARM Cortex-M использует little-endian, поэтому позиция разворачивается и в исходной прошивке выглдяит так: 9f 2b. Достанем её:
xxd -g1 ydkb_kbdfans_agar_mini_ec_vial.uf2 | grep '9f 2b'Вывелось:
000046c0: 0b 4a 12 78 9a 42 09 d8 9f 2b 09 d8 09 4a 10 78 .J.x.B...+...J.xДо 9f — 8 байтов (0b 4a 12 78 9a 42 09 d8), поэтому финальная позиция — 000046c0 + 8 = 000046c8.
И мы хотим заменить 159 на 220, то есть 9f на dc. И вот команда, которая пофиксит прошивку:
printf '\xDC' | dd of=agar_cmp220.uf2 bs=1 seek=$((0x46c8)) count=1 conv=notruncИ проверка:
cmp -l ydkb_kbdfans_agar_mini_ec_vial.uf2 agar_cmp220.uf2
# 18121 237 334 237 и 334 — это наши 159 и 220, только в восьмеричной системе счисления.
Подключаем клавиатуру через провод и ставим на какую-то клавишу Bootloader:

Когда нажмем клавишу, появится новый диск. На него надо закинуть прошивку. Диск исчезнет, и клавиатура обновится сама.
Если у вас подобная проблема на той же самой Agar Mini, то сперва соберите локально из моей ветки проекта Via — появится возможность на панели EC Tools посмотреть диапазон значений ADC. Если проблема — как у меня, и многие клавиши генерируют значения до X, то читайте последнюю часть статьи.
Всем остальным — спасибо, что дочитали. Если не хотите так быстро прощаться, заходите ко мне в ТГ. Пишу про клавиатуры, гаджеты, языки программирования, VR, карьеру в IT и прочее.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Обзор и схемотехническое ревью платы TZT RK-ZYNQ7020-F с FMC LPC разъемом | 5 | 7 | 08-07-2026 |
| 2 | Оптимизация без AI: как я автоматизировал API-ручки и типы | -2 | 5 | 29-06-2026 |
| 3 | Баг-трекинг: почему баги возвращаются на прод и какая система это лечит | 0 | 7 | 24-06-2026 |
| 4 | Путь к миллиону точек: как я переписывал плоттер три раза, прежде чем он перестал лагать | 2 | 6 | 22-06-2026 |
| 5 | Загружаемся с Raspberry Pi Pico | 0 | 5 | 09-06-2026 |
| 6 | Попросили Claude создать WCAG-доступный DataPicker на React и потратили 3 дня на доработки | 0 | 7 | 28-06-2026 |
| 7 | Сложно о простом. Все, что бы вы хотели знать о SFP модулях. Часть 6. Топ-10 частых ошибок при выборе и эксплуатации SFP | 0 | 5 | 09-07-2026 |
| 8 | Организовал весь пентест-арсенал в одном месте: всё под рукой, офлайн и на русском | 5 | 7 | 28-06-2026 |
| 9 | Хакер в магазине: изучаем поверхность атаки типичного супермаркета | 0 | 8 | 09-07-2026 |
| 10 | sizeof(Mutex<()>) упал с 40 байт до 5: что внутри std::sync::Mutex после Rust 1.62 | 5 | 8 | 09-07-2026 |