Кожен знає, що таке клавіатура і для чого вона призначена, але далеко не всі знають, що і як відбувається при натисканні тієї чи іншої клавіші.
У цій статті я розповім про низкоуровневой роботі з клавіатурою і приведу приклад реалізації простого обробника клавіатурного переривання для реального режиму (драйвер).
При натисканні на яку або клавішу електроніка клавіатури генерує скан-код клавіші довжиною від 1 до 6 байт, який можна отримати читанням порту введення-виведення 0x60. Скан-код - унікальне число, однозначно визначає натиснуту клавішу, але не ASCII-код. Скан-коди бувають двох видів: при натисканні клавіші генерується так званий Make-код, а при її відпусканні Break-код. Відрізняються вони лише тим, що в Break-коді старший біт кожного байта встановлений в одиницю. Скан-коди поділяються на групи: звичайні, розширені, додаткові і код клавіші Pause. Звичайний скан-код складається з одного байта, розширений - з 2-х байт, перший з яких - 0xE0. Довжина додаткового коду - від 2 до 4 байт. Він так само починається з 0xE0. Дуже сильно з колії вибивається Pause - це єдина клавіша, код якої складається з 6 байт, причому Break-код у неї відсутня.
Клавіатура може працювати в двох режимах: за опитуванням і по перериванню. Я розглядаю тільки другий режим, так як перший менш ефективний і його реалізація простіше. Коли у вхідному буфері клавіатури є будь-які дані, відбувається запит переривання по лінії IRQ1. Крім самих скан-кодів, клавіатура може передавати комп'ютера спеціальні коди при виконанні команд (наприклад, зміна стану світлодіодів).
По ходу статті передбачається, що клавіатура працює в режимі «за замовчуванням»: встановлено набір скан-кодів Set2, більшість клавіш працюють в режимі автоповтора і т. Д.
Таблиця скан-кодів клавіатури:
Якщо уважніше придивитися до скан-коду клавіші Pause (E1 1D 45 E1 9D C5), можна помітити, що перші 3 байти є Make-кодом, а другі Break-кодом. Тобто клавіатура при її натисканні генерує відразу ж і код натискання і віджимання. Надалі це трохи спростить нам життя.
Якщо натиснути клавішу і утримувати її, то через заданий час, зване часом автоповтора, скан-код утримуваної клавіші буде повторюватися з частотою автоповтора. При автоповтор додаткові скан-коди містять два байта замість чотирьох. Наприклад, сукупність електронних даних при утриманні та відпуску клавіші Page Up (E0 2A E0 49) виглядає так: E0 2A E0 49 E0 49 E0 49 E0 49 E0 49 E0 49 E0 49 E0 C9 E0 AA. Зверніть увагу: при відпуску клавіші з додатковим кодом послідовність перевернута - ознака додаткового коду (E0 AA) йде в кінці, а не на початку.
Наступні позиції в таблиці скан-кодів зайняті і однозначно не можуть бути використані іншими клавішами (і, відповідно, нами):
- звичайні скан-коди: [0x01 - 0x53], [0x57 - 0x58]
- розширені: [0x1C - 0x1D], 0x35, 0x38, [0x5B - 0x5F], 0x63
- додаткові: 0x37, [0x47 - 0x49], 0x4B, 0x4D, [0x4F - 0x53]
Всім розширеним скан-кодами передує байт 0x0E, а додатковим - сукупність електронних даних 0xE0, 0x2A, 0xE0.
Якщо вирішувати нашу задачу в лоб, то доведеться перевіряти кожен байт, отриманий від клавіатури. А так як байти з набору звичайних кодів перетинаються з останніми байтами інших наборів (маються однакові значення), додаткові коди при відпуску йдуть перевернутими парами, переривання виникає стільки раз, скільки байт в скан-коді, є ще і Pause, то визначити, що саме за клавіша була натиснута, стає не дуже просто. В остаточному підсумку всі ці аспекти приведуть до реалізації з купою нелегкотравних розгалужень.
Чесно кажучи, навіть не уявляю, чим керувалися розробники, створюючи такий безлад. У цій статті я зосереджуся саме на тому, як все це неподобство привести в більш простий і красивий вид.
Для інформування про те, що скан-код є розширеним або додатковим, використовуються значення 0xE0, 0x2A / 0xAA і 0xE1 для Pause. Цей факт однозначно говорить про те, що ці значення також не використовуються ні однією з клавіш - інакше визначити натиснуту / отжатую клавішу було б просто неможливо.
Введемо поняття віртуальної клавіші - це номер, який визначає функцію клавіші. Наприклад: Ctrl виконаний у вигляді двох фізичних клавіш, але їх віртуальний код - один і той же.
Мої визначення віртуальних клавіш дещо відрізняються від використовуваних в Windows, хоча і дуже схожі на них.
Створимо таблицю трансляції скан-кодів в віртуальну клавіатуру, що складається з 256 елементів по 2 байта кожен. У першому байті ми будемо зберігати сам віртуальний код клавіші, а в другому - деякі атрибути.
Якби всі коди складалися з одного байта, то по молодшим семи розрядів значення цього байта можна було б однозначно ідентифікувати клавішу. Втім, для більшої частини клавіш ця умова виконується, і їх перетворення можна звести до такого правила: обнулити старший розряд, і отримане значення використовувати в якості індексу в таблиці трансляції, за якою знаходиться віртуальний код клавіші незалежно від того, натискається вона або віджимається.
Оскільки ми Обнуляємо старший розряд, все значення в проміжку [128 - 255] таблиці трансляції вільні для наших потреб. Віртуальні коди розширених і додаткових клавіш ми будемо зберігати саме в цьому діапазоні. Поступимо таким чином: якщо скан-код є розширеним або додатковим (Pause поки відкинемо), то встановимо в одиницю старший розряд в його останньому байті.
Все ускладнюється тим, що впродовж одного переривання ми отримуємо лише один байт скан-коду, і так просто встановити старший розряд останнього байта не вдасться.
Почнемо з розгляду додаткових клавіш. Всі вони починаються з байта 0xE0. Модифікуємо раніше наведене правило: якщо віртуальний код, отриманий з таблиці трансляції, дорівнює нулю, то віртуальний код не готовий і скан-код ще не прийнятий повністю. Відкинувши старший розряд у 0xE0, ми отримаємо значення 0x60 (яке також не використовується жодної клавішею), яке можна використовувати в якості індексу, за яким буде зберігатися значення 0. При обробці наступного переривання ми отримаємо останній байт скан-коду, у якого і повинні встановити старший розряд. Найпростіше це зробити, модифікувавши правило перетворення. Згадаймо про те, що у нас є ще й байт атрибутів в таблиці, який до цих пір ніяк не задіяне. Нове правило буде виглядати так: при кожному отриманні значення з таблиці будемо зберігати його в змінної, а при формуванні індексу будемо використовувати будь-якої розряд байта атрибутів, що відображає, що необхідно змінити старший розряд отриманого байта.
Тепер визначимося, який розряд ми будемо використовувати, і як з його допомогою отримувати індекс. Як я вже сказав, нам необхідно встановити найстарший біт. Виходить все дуже просто і красиво: старший біт атрибутів, взятий із змінної, переноситься в старший розряд отриманого байта.
Давайте розберемо все вищевикладене на прикладі клавіші Ctrl Right (E0 1D):
Назвемо змінну, яка повинна зберігати байт віртуального коду і байт атрибутів, KeyInfo. Ініціалізіруем її значенням 0. Перший одержуваний байт має значення 0xE0. Обнуляємо старший розряд і отримуємо 0x60. Беремо старший розряд байта атрибутів з змінної KeyInfo і переносимо його в старший розряд отриманого індексу: (0xE0 0x7F) | ((KeyInfo >> 8) 0x80). У підсумку отримуємо індекс 0x60, за яким в таблиці буде зберігатися віртуальний код з нульовим значенням і байт атрибутів з встановленим старшим розрядом: KeyInfo = 0x8000. Наступний байт в послідовності 0x1D: KeyInfo = Table [(0x1D 0x7F) | ((KeyInfo >> 8) 0x80)] == 0x9D - ось ми і отримали кінцевий індекс, за яким в таблиці буде знаходитися віртуальний код.
Їдемо далі - на черзі додаткові коди. Відрізняються вони лише тим, що перші 2 байта мають значення 0xE0, 0x2A. Найпростіший спосіб їх прийому навіть не зажадає змін правила отримання індексу. Після отримання 0xE0 байт 0x2A перетвориться в індекс 0xAA, за яким будемо зберігати значення 0, що вказує на те, що віртуальний код не готовий, і модифікувати старший розряд наступного байта не потрібно (як нібито цієї послідовності і зовсім не було). Наступні 2 байта послідовності нічим не відрізняються від розширеного скан-коду, і для їх прийому вже все готово.
Все вищевикладене чудово працює при отриманні 2 байт замість 4 при автоповтор і зворотному порядку проходження пар при відпуску. Причому при відпуску перші 2 байта дадуть віртуальний код віджатої клавіші, а останні 2 (E0 AA) будуть перетворені в 0x0000 (проігноровані).
Але як ви вже, напевно, забули, ми відкинули клавішу Pause (E1 1D 45) - давайте тепер розберемося і з нею. Якщо піти за попереднім шляху, і за індексом 0x61 (0xE1 0x7F) зберігати значення 0x8000, то ми отримаємо колізію, пов'язану з правим Ctrl'ом (E0 1D). Що ж нам в цьому випадку робити? Ну що ж, будемо в черговий раз модифікувати наше правило отримання індексу: подивимося на двійкове подання числа 0x1D - 0001 1101. Щоб не виникло колізій, можна, наприклад, модифікувати 5-й, 6-й, або обидва розряду разом, або 7 й і 1-й, але якщо вже ми почали модифікувати старші розряди, то давайте продовжимо. Нове правило отримання індексу буде таким: (Value 0x7F) | ((KeyInfo >> 8) 0xC0). Але на цьому наші муки з клавішею Pause не закінчилися. За індексом 0x61 будемо зберігати значення KeyInfo = 0x4000 (НЕ 0xC000, щоб не виникло колізії з Applications (E0 5D)), і, слідуючи новим правилом, отримаємо: (1D 0x7F) | ((KeyInfo >> 8) 0xC0) == 0x5D. Але, оскільки код Pause складається з трьох байт, то за цим індексом теж повинен бути якийсь модифікатор - і про благо: якщо взяти 0x8000, то ніяких колізій не виникне, і за індексом 0xC5 буде знаходитися віртуальний код Pause.
Незважаючи на те, що KeyInfo змінюється після кожного прийнятого байта, ніяких проблем при прийомі наступного скан-коду не виникне, так як для модифікації прийнятого байта використовується тільки байт атрибутів. Якщо кінцевий індекс отриманий, то по ньому в таблиці модифікують розряди відсутні (скинуті в нуль) і вже неважливо, що знаходиться в першому байті KeyInfo.
Таблиця трансляції:
За індексом 0x7A зберігається нульове значення для ігнорування відповіді від клавіатури при прийомі команди, але це не єдина можлива відповідь, і все їх за допомогою даної таблиці ігнорувати не вдасться. Але це завдання вирішується вкрай просто, наприклад, при посилці команди можна відключити переривання взагалі і працювати за опитуванням, або завести змінну, яка вказує на те, що була послана команда, і трансляцію скан-кодів використовувати не потрібно.
З трансляцією ми повністю розібралися, тепер давайте задіємо ще деякі розряди байта атрибутів. Наприклад, нехай маємо в 0-м розряді ознака відпускання клавіші - при віджиманні він буде встановлюватися в одиницю. Як я вже говорив, віртуальний код визначає саме функцію клавіші, а не її фізичне розташування. Але уявіть, що ця інформація може знадобитися, 1-й розряд буде ознакою того, що клавіша є правою.
У тестовому прикладі реалізований обробник клавіатурного переривання (роботи з командами немає), який при натисканні або відпуску клавіші виводить інформацію про те, що і з якою клавішею сталося.
P. S. Конструктивна критика, виправлення і доповнення вкрай вітаються.