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

В статье о том, что такое EC, как отдебажить проблему и внести фикс в прошивку. Не обошлось и без реверс-инжиниринга.

Все опечатки неслучайны.

Идентификация проблемы

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

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

https://get.vial.today/

Вроде печатается нормально — как будто починилось. Калвиатура огноь! Ояпть?

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

https://get.vial.today/

Это было ыб слишком легок, и статьи бы не блыо.

Что, если что-то собрано неправильно? Вдруг болты затянуты слишком туго или резинки-подкладки съехали — и это как-то повлияло на значения при нажатии кнопок? Если интересно, на внутренности Agar Mini EC можно посмотреть на видео.

Пересобрал — не помогло.

Но я наконец-то нашел простой способ воспроизведения. Очень быстро пишешь «лак», но иногда печатается «алк» или даже «кал». Имея репродьюсер, я открыл Karabiner-EventViewer и проследил, как происходят ошибки во время быстрого написания «лак»:

  1. Нажатие на «л» ничего не регистрирует,

  2. нажатие на «а» ничего не регистрирует,

  3. нажатие на «к» регистрирует down к (тут печатается «к»),

  4. отпускание «а» регистрирует down а, up а (тут печатается «а»),

  5. отпускание «л» регистрирует down л, up л (тут печатается «л»),

  6. отпускание «к» регистрирует up к.

Еще немного поэкспериментировав, заметил, что абсолютно все клавиши, кроме одной — «w», — ничего не регистрируют при быстром зажатии до упора. И только когда отпускаешь, получается и нажатие, и отпускание.

Разбор проблемы через Via

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, #0x9f

0x9f — это наш 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:

https://get.vial.today/

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

Итоги

Если у вас подобная проблема на той же самой Agar Mini, то сперва соберите локально из моей ветки проекта Via — появится возможность на панели EC Tools посмотреть диапазон значений ADC. Если проблема — как у меня, и многие клавиши генерируют значения до X, то читайте последнюю часть статьи.

Всем остальным — спасибо, что дочитали. Если не хотите так быстро прощаться, заходите ко мне в ТГ. Пишу про клавиатуры, гаджеты, языки программирования, VR, карьеру в IT и прочее.