вівторок, 28 червня 2011 р.

Есе про контролер клавіатури Intel 8042

Фрагмент 5-го уроку захищеного режиму, вартий уваги сам по собі. Інформація місцями не до кінця перевірена, написано несистематично -- тема широка, з захищеним режимом пов'язана слабо :-), але може комусь згодиться.


Робота з клавіатурою, сканкоди клавіш


Раніше контролером клавіатури служив мікроконтролер, сумісний з Intel 8042. Крім того, на IBM-PC сумісних машинах, в рамках традиції тих часів, "в нього" засунуто ще кілька функцій, які до клавіатури жодного відношення не мають, але були потрібні і "було місце". Це:

  • контроль перезавантаження CPU (введено в IBM AT, так як 286 не вмів виходити з захищеного режим і для повернення в реальний його доводилося перезавантажувати),
  • контроль лінії A20 (див. попередні уроки),
  • керування вбудованим динаміком разом з програмованим таймером (Programmable Interval Timer),
  • керування PS/2-мишкою на відповідних системах.
З того часу, як і для PIC, багато що змінилося в апаратній реалізації9, але програмно все виглядає так само. Більше того, USB-клавіатура та USB-мишка емулюються BIOS як звичайні типу PS/2 аж до спроби ініціалізувати контролер USB10.

Детально роботу контролера клавіатури не розглядатимемо, обмежившись необхідним для цілісного розуміння мінімумом. Додаткову, дуже детальну інформацію можна знайти тут: http://www.brokenthorn.com/Resources/OSDev19.html та трішки тут: http://wiki.osdev.org/PS2_Keyboard. Коли клавіша натискається чи відпускається генерується так-званий скан-код, що описує яка саме клавіша. Процесор повідомляється про це за допомогою IRQ1. Таблиць відповідності між скан-кодами та клавішами існує декілька, див. http://www.brokenthorn.com/Resources/OSDevScanCodes.html та http://www.win.tue.nl/~aeb/linux/kbd/scancodes.html. Надалі вважатимемо що на наших системах використано типову для сучасних машин таблицю (друга за посиланням вище). Контролер містить невеликий внутрішній буфер для натискань, що ще не були оброблені процесором.

Насправді контролер клавіатури це два різних пристрої - контролер кодування (Keyboard Encoder) та вбудований контролер клавіатури (Onboard Keyboard Controller). Всі команди спочатку отримує вбудований контролер, а потім ті що призначені не йому а контролеру
кодування передає далі.

Порти для роботи з ними такі:

Порт R/W Опис
Контролер кодування
0x60 читання Прочитати вміст буфера вводу
0x60 запис Послати команду
Вбудований контролер клавіатури
0x64 читання Регістр статусу
0x64 запис Послати команду

Контролери клавіатури - сутності повільні, тому перш ніж робити з ними щось слід перевіряти їхню готовність. Більше того, перш ніж посилати команду чи читати відповідь контролера кодувань слід спершу перевірити готовність вбудованого контролера, адже команду
передаватиме він.

В регістрі статусу важливими для нас можуть бути наступні біти:

  • біт 0 - статус буфера виводу. 0 - буфер виводу порожній, читати ще не можна; 1 - буфер повний, можна читати11.
  • біт 1 - статус буфера вводу. 0 - буфер порожній, можна записувати; 1 - буфер повний, записувати не можна.
  • Решта бітів, коротко: біт 2 - системний, стає 1 після самотестування; біт 3 - визначає що було останнім, команда чи дані; біт 4 - чи заблокована (locked) клавіатура; 5 - статус заповнення допоміжного буфера виводу, на PS/2 системах одиниця може свідчити про дані від PS/2-мишки, на AT - про відсутність клавіатури; біт 6 - таймаут; біт 7 - контроль цілісності (Parity error).
Перш ніж робити щось із клавіатурою слід перевіряти біти 0 чи 1, щоб знати чи можна. Наприклад12:

KYBRD_ENC_INPUT_BUF =   0x60;
KYBRD_ENC_CMD_REG   =   0x60;

KYBRD_CTRL_STATS_REG    =   0x64;
KYBRD_CTRL_CMD_REG  =   0x64;

KYBRD_CTRL_STATS_MASK_OUT_BUF   =   1;
KYBRD_CTRL_STATS_MASK_IN_BUF    =   2;

unsigned char status;
/*Чекаємо поки можна буде передати команду вбудованому контролеру*/
while(1)
{
   status = inportb (KYBRD_CTRL_STATS_REG);
   if(status & KYBRD_CTRL_STATS_MASK_IN_BUF == 0)
      break;
}
/*Посилаємо команду */
outportb(KYBRD_CTRL_CMD_REG, cmd_code);

/*Передаємо команду контролеру кодувань*/
/*Спершу перевіряємо чи ВБУДОВАНИЙ КОНТРОЛЕР здатен її прийняти*/
while(1)
{
   status = inportb (KYBRD_CTRL_STATS_REG);
   if(status & KYBRD_CTRL_STATS_MASK_IN_BUF == 0)
      break;
}
/*Посилаємо команду */
outportb(KYBRD_ENC_CMD_REG, enc_cmd_code);

Команд контролера кодувань теж багато, розглянемо лише декілька:

  • команда 0xED - керування LED. Після неї контролер очікуватиме виводу в порт 0x60 байта стану LED: біт 0 - Scroll lock (1 - ввімкнути, 0 - вимкнути); біт 1 - Num lock; біт 2 - Caps lock.
  • команда 0xEE - ехо, виводить 0xEE в порт 0x60 як діагностичний тест.
  • 0xF0 - на PS/2 вибір альтернативних таблиць скан-кодів. Біт 0 - прочитати поточний код (виведеться в порт 0x60); біти 1,2,3 - встановити таблицю номер 1,2,3 відповідно. В конкретній команді відмінним від нуля може бути тільки один біт.
  • 0xF3 - встановити затримку перед початком автоповтору клавіші та частоту повторів. Біти 0-4 - частота повторів, 0 відповідає приблизно 30 клавіш у секунду, 0x1F - 2 клавіші в секунду; біти 5-6 - затримка, 00: 1/4 с, 01: 1/2 c, 10: 3/4 c, 11: 1 секунда.
В порт 0x60 він виводить скан-коди клавіш та коди помилок, якщо такі
відбулися.
  • 0 - переповнення внутрішнього буфера;
  • 0x1-0x58, 0x81-0xD8 - скан-коди клавіш;
  • Решта - коди помилок13.
Коротко про команди вбудованого контролера:
  • 0x20 - прочитати командний байт14,
  • 0x60 - записати командний байт,
  • 0xAA, 0xAB - самотестування15, тестування інтерфейсу16,
  • 0xAD, 0xAE - заборонити та дозволити клавіатуру відповідно,
  • 0xC0 - прочитати порт вводу17, результат поміщає в порт 0x64 18,
  • 0xD0 - прочитати порт виводу, результат поміщає в порт 0x64. Нічого цікавого там на сучасних комп'ютерах немає, за винятком біту 1 - стан лінії A20, якщо виникає потреба його перевірити чи змінити командою 0xD1,
  • 0xD1 - записати порт виводу,
  • 0xE0 - прочитати тестовий ввід,
  • 0xFE - перезавантажити систему (згадуємо 286 і AT),
  • 0xA7 - заборонити порт мишки (PS/2),
  • 0xA8 - дозволити порт мишки (PS/2),
  • 0xA9 - тест порту мишки (PS/2),
  • 0xD4 - запис в порт мишки,
  • 0xDD - заборонити A20,
  • 0xDF - дозволити A20,
  • багато-багато інших, менш стандартизованих
Формат командного байта:
  • біт 0: 0 - заборонити переривання, 1 - дозволити;
  • біт 1: на PS/2, 0 - заборонити переривання від мишки, 1 - посилає IRQ12 при заповненні буфера виводу мишки;
  • біт 2: системний прапорець, 0 - холодне перезавантаження, 1 - "тепле", але таке що вже відбулося.
  • біт 3: - пов'язаний з блокуванням клавіатури;
  • біт 4: 0 - клавіатура дозволена, 1 - заборонена;
  • біт 5: на PS/2, 0 - мишка дозволена, 1 - заборонена19;
  • біт 6: трансляція сканкодів;
  • біт 7: не використовується, має бути нуль.
Приклад читання скан-коду натиснутої клавіші:

KYBRD_ENC_INPUT_BUF =   0x60;
KYBRD_ENC_CMD_REG   =   0x60;

KYBRD_CTRL_STATS_REG    =   0x64;
KYBRD_CTRL_CMD_REG  =   0x64;

KYBRD_CTRL_STATS_MASK_OUT_BUF   =   1;
KYBRD_CTRL_STATS_MASK_IN_BUF    =   2;

unsigned char status;
unsigned char scancode;
/*Чекаємо поки можна буде здійснити читання*/
while(1)
{
   status = inportb (KYBRD_CTRL_STATS_REG);
   if(status & KYBRD_CTRL_STATS_MASK_OUT_BUF )
      break;
}

/*читаємо */
scancode = inportb(KYBRD_ENC_INPUT_BUF);

або на чистому асемблері (може кому так буде зрозуміліше):

;чекаємо
 .spin:
   in  al, 0x64   ; KYBRD_CTRL_STATS_REG
   and al, 0x01   ; KYBRD_CTRL_STATS_MASK_OUT_BUF
   jz  .spin

   ;read scancode
   in  al, 0x60   ; KYBRD_ENC_INPUT_BUF

Сканкоди можуть складатися з кількох байт. Зокрема якщо скан-код F0 чи E0, значить сканкод розширений, багатобайтовий і слід читати наступні. Наприклад, для найпоширенішої таблиці сканкодів натискання клавіші 'A' має код 1C, а її відпускання - код F0,1C. Правий Ctrl при натисканні генерує E0,14 а при відпусканні - E0,F0,14. За певних умов можливі і складніші комбінації. Проста комбінація Shift-A викличе наступну послідовність скан-кодів: 0x12 (натиснуто
shift), 0x1C (натиснуто A), 0xF0 0x1C (відпущено A), 0xF0 0x12 (відпущено Shift).

Перезавантаження CPU за допомогою контролера клавіатури20:
 ;чекаємо на готовність контролера прийняти команду
 .spin:
   in  al, 0x64   ; KYBRD_CTRL_STATS_REG
   and al, 0x01   ; KYBRD_CTRL_STATS_MASK_OUT_BUF
   jz  .spin

  ;Посилаємо команду перезавантаження
  mov  al, 0xFE
  out  0x64, al

TODO: Додати трохи прикладів, Зокрема роботу з LED.

Роль BIOS в роботі з клавіатурою

Безпосередньо з апаратурою клавіатури працювати не завжди зручно, хоча іноді й необхідно. BIOS значну частину нюансів роботи з нею може брати на себе, передаючи програмі вже напівфабрикат а не просто послідовність скан-кодів. IRQ1 в реальному режимі передається int 09h, яке обробляється BIOS. Здійснюється читання скан-кода натиснутої чи відпущеної клавіші (з порту 0x60), та у спеціальний кільцевий буфер в нижній пам'яті21 поміщається два байти
- ASCII-код натиснутої клавіші та її скан-код. Клавіші для яких ASCII-код не передбачено, такі як функціональні, поміщають в нього нуль. Скан-коди клавіші, такі як Shift, Ctrl i Alt, та Caps Lock, Scroll lock, Insert в цей буфер не поміщаються, натомість виставляються прапорці натиснуто/не натиснуто в двох байтах за адресою 0x417 та 0x418.

Для зчитування натиснутих клавіш та стану клавіш типу Shift, Ctrl і т.д. BIOS надає відповідні функції, доступні за допомогою int 16h.

Детальніше про це див. лінки тут http://wiki.osdev.org/BIOS_(PC) та "Ralf Brown's Interrupt
List" (наприклад на офіційній сторінці автора http://www.cs.cmu.edu/~ralf/files.html).

Керування лінією A20

З приводу контролера клавіатури напишемо зразу і про славнозвісну A20.

Найпростіше можна використати команди 0xDD/0xDF, якщо вони підтримуються:

mov al, 0xDD    ; дозволити a20, 0xDF --- заборонити
out 0x64, al    ; KYBRD_CTRL_CMD_REG

Гарантовано можна скористатися портом виводу22 за допомогою команди вбудованого контролера клавіатури 0xD1 і 0xD0:

; На початку можна додатково
; виконати команду 0xAD --- заборонити клавіатуру.

mov     al,0xD0 ; прочитати порт виводу
out     0x64,al ;
call    wait_for_kbd_controller_in ;

; читаємо значення буфера вводу
; це і я значення, прочитане з порта виводу
in      al,0x60
; зберігаємо його в стек для подальшого використання
push    eax
call    wait_for_kbd_controller_in ;

; посилаємо команду запису в порт виводу
mov     al,0xD1
out     0x64,al
call    wait_for_kbd_controller_in ;

; дістаємо прочитане зі стеку та встановлюємо біт 1 --- дозволити A20,
; не змінюючи інших бітів:
pop     eax
or      al,10b     ; двійкове 10 == 2
out     0x60,al

Де, щоб не повторювати кожен раз, wait_for_kbd_controller_in це щось типу:

;чекаємо
 .spin:
   in  al, 0x64   ; KYBRD_CTRL_STATS_REG
   and al, 0x02   ; KYBRD_CTRL_STATS_MASK_IN_BUF
   jz  .spin
   ret

На сучасних архітектурах23 цього можна досягнути простіше та швидше за допомогою Fast A20 Gate:

in al, 0x92
or al, 2
out 0x92, al

хоча працездатність методу не гарантується, він може не підтримуватися, або викликати конфлікти з іншими пристроями.

Ще варіант - через BIOS:

mov ax, 0x2401
int 0x15

Якщо функція виконалася успішно, CF=0 i AH=0, якщо ж CF=1 то AH містить код помилки, зокрема AH=0x86 - функція не підтримується. Функція 0x2400 навпаки забороняє A20 а 0x2402 повертає її статус.

Затримки при вводі-виводі

Більшість периферійних пристроїв значно повільніші ніж процесор. Тому між виводом в порт команди та зчитуванням з іншого порту її результатів слід зробити паузу. Як показав багаторічний досвід, робити затримки, виконуючи певну послідовність команд процесора, не
варто - час їх виконання дуже сильно залежить від моделі. Те, що було хорошою затримкою для одного процесора, може виявитися практично нульовою для іншого, більш просунутого.

Одним з найпростіших способів зробити таку затримку - вивести щось в порт 80h, який на IBM PC зарезервовано під початкове завантаження комп'ютера. Шкоди це принести не повинно, принаймні Лінукс цим трюком користується:

void io_wait(void)
{
   outportb(0x80, 0);
} 

Footnotes:

9Зокрема, багато таких контролерів об'єднано в Advanced Integrated Peripheral, наприклад Intel 82091AA.
10Припускається, що після цього OS, яка його ініціалізує вже і з клавіатурою та мишкою розбереться самостійно.
11Так, станів тільки два - повний та порожній, ніяких там напівпорожніх та напівповних буферів.
12Взято з модифікаціями http://www.brokenthorn.com/Resources/OSDev19.html, public
domain.
13Зокрема, щодо пари, 0x83 0xAB, 0xAA, 0xEE, 0xF0, 0xFA, 0xFC, 0xFD, 0xFE, 0xFF див. http://www.brokenthorn.com/Resources/OSDev19.html
14Взагалі, це один з байтів внутрішньої пам'яті контролера. Шість молодших бітів - номер байта
для читання. Командний байт просто є нульовим. Доступність для читання інших залежить від конкретної моделі контролера.
15Результат можна побачити через порт 0x60: 0x55 - все добре, 0xFC - збій.
160 - все добре, інші - поломка.
17Зауважимо, що це порт виводу клавіатури, а не якийсь з портів процесора.
18Нічого цікавого там на сучасних комп'ютерах немає.
19На інших архітектурах використання інше
20Метод застарів.
21Формат його такий, вказано лінійні адреси:
  • 0x41A, два байти - вказівник на голову кільцевого буфера
  • 0x41С, два байти - вказівник на хвіст кільцевого буфера
  • 0x41E - 0x43D, 32 байти, записами по два, що відповідають ASCII та скан-кодам.
22Нагадаємо що це порт виводу клавіатури, а не якийсь з портів процесора.
23Див. також http://www.brokenthorn.com/Resources/OSDev9.html, "Method 1: System Control Port A".

1 коментар:

  1. Трішки про сам контролер: http://devster.monkeeh.com/z80/upi42/ (Опис вище стосується, в значній мірі, конкретної його прошивки).

    ВідповістиВидалити