Проблема высшего программистского образования в том что студенты весьма подробно изучают отдельные аспекты вырванные из контекста не понимая как это всё увязывается вместе. Несколько семестров высшей математики, чтобы понимать физику, чтобы понимать электротехнику, электроприборы, ассемблер, ОС, алгоритмы, системное программирование и куча других предметов утрамбованных в стандартную пятилетнюю программу. Обилие деталей и никто не объясняет как это вписывается в общую картину, предполагается что через 5 лет студент сам увяжет это в голове, а потом пойдёт работать с .Net и никогда не притронется к электронике и режиму ядра. Я считаю, что не нужно так подробно знать о работе компьютера, достаточно общего понимания что происходит ниже по технологическому стеку. Если бы люди составляющие программы обучения для ВУЗов открывали автошколы, вы бы учили русский язык, каллиграфию и гидродинамику, потому как надо общаться с инспекторами, менять жидкости и писать объяснительные. В статье будут некоторые неточности, так что для сдачи экзамена по профильным предметам она не подойдёт, но после неё будет легче понять устройство ПК.
Под катом трафик.
Клавиша клавиатуры представляет из себя кнопку, которая замыкает контакты и через них проходит электрический ток. Механизм замыкания/размыкания сделан таким образом чтобы кнопку не приходилось вдавливать до конца, потому как иначе пальцы будут быстро уставать и клавиатура будет неэргономичной. В разрезе клавиша выглядит так. В правой части находится контакт на который подаётся напряжение.
“Мозгами” клавиатуры которые могут определять нажатие клавиши является микроконтроллер, который по сути является мини-компьютером с программой проверяющей каждую клавишу подавая на неё напряжение и если оно проходит то клавиша нажата. Ножки микроконтроллера имеют свои названия и к ним можно обращаться в коде, они используются для общения с внешним миром. Ножки могут управлять работой микроконтроллера, сообщать состояние чипа или служить для чтения/передачи данных.
Программу (прошивку) можно писать на языке С или ассемблере, она заливается на микроконтроллер через программатор. Он представляет из себя устройство с разъёмом для установки микроконтроллера и может подключаться к компьютеру через USB, на котором запущен специальный софт. Вот как выглядит программатор в реальной жизни:
Через программу на каждую ножку можно установить либо замерить напряжение. Алгоритм нахождения нажатой клавиши заключается в том чтобы подавать напряжение на одну ножку и замерять его на другой и если клавиша была нажата, то на второй ножке будет примерно столько же вольт сколько на входной. Так в бесконечном цикле проверяются все клавиши. Как правило клавиатура имеет 80-110 кнопок, а у контроллера контактов (пинов) гораздо меньше. Поэтому используют такой подход как “клавиатурная матрица” — все клавиши распределяют по столбцам и строкам и алгоритм сводится к нахождению столбца который пересекается со строкой на которую программа подала напряжение. Здесь отмечено цветом соответствие столбцов/строк контактам.
В реальности матрица может выглядеть так. Слева промышленная и справа самодельная.
На псевдокоде часть программы прошивки определяющая нажатые клавиши может выглядеть следующим образом.
for (int i = 0; i < COLSC; i++) {
SetPower(columns[i], 3.0 f);
for (int j = 0; j < ROWSC; j++) {
float power = GetPower(rows[j]);
if (power >= 3.0f - THRESHOLD) {
BYTE key = keys[i, j];
SendKey(key);
}
}
}
Каждой клавише соответствует скан код, он стандартизирован и представляет из себя 8ми битное число, т.е. один байт. Поэтому когда нажата клавиша Y контроллер клавиатуры должен будет отправить число 21 (0x15), а когда отпущена — 149 (0x95). Каким образом происходит отправка? Наверняка вы работали с JSON, веб-сервисами или отправляли данные между процессами и знаете что для отправки данные надо сериализовать, т.е. превратить в массив байт или отформатированный текст, которые получатель может десериализовать, т.е. воссоздать объект в своём адресном пространстве. А во что можно сериализовать данные на таком низком уровне? Нам нужно передать всего лишь 1 байт (8 бит). Забегая вперёд скажу, что данные мы будем передавать побитно.
В математике есть формула которая может преобразовать любое привычное нам десятичное число в последовательность нулей и единиц и обратно. Этой формуле нашлось применение в вычислительной технике. В первой части я вскользь упомянул, что аналоговая техника эксплуатирует законы физики, в то время как цифровая работает на уровне нулей и единиц. Это означает, что аналоговый телефон кодирует весь спектр человеческого голоса в последовательность электромагнитных волн, а цифровой телефон использует микросхему которая преобразовывает человеческий голос в цифровые данные, к примеру в файлы в формате WAV, а потом передаёт их последовательностью нулей и единиц в виде электромагнитных волн. Только в данном случае вместо всего спектра голоса надо представлять только два значения — 0 и 1. Представлять их можно волнами разной длины, разным напряжением, световыми импульсами через оптоволокно, черными и белыми полосками на бумаге, дырками на перфокарте.
Скан код нажатой клавиши Y в двоичном виде выглядит как 0001 0101. Передавать мы их будем по ножке микроконтроллера которая отвечает за данные (DATA). Логическая единица — это напряжение 3.3В и выше, логический ноль — напряжение около 0В. Здесь возникает загвоздка — как передать три нуля подряд? Для этого нам нужна вторая ножка которую назовём CLOCK, когда на ней единичка это значит что сеанс передачи одного бита начался, а ноль — закончился. Такая перемена значений (напряжений) будет происходить с определённым интервалом времени, скажем 50 наносекунд, потому что на другом конце находится второй микроконтроллер который работает со своей скоростью и в бесконечном цикле слушает ножки к которым подключены CLOCK и DATA. В данном примере я буду исходить из того что клавиатура подключается через разъём PS2, который показан ниже. Через USB порт алгоритм передачи будет другим. Как видите у порта PS2 есть пины которые называются Data, Clock. Помимо них есть ещё контакт по которому контроллер порта PS2 раздаёт клавиатуре напряжение 5В необходимое для работы и контакт заземления, который просто выводится на корпус клавиатуры. Остальные контакты не используются.
В чём разница между портом, шиной (bus) и протоколом? Шина как и порт это набор контактов (проводков) и соглашение как их использовать, только порт имеет соединение для подключения внешних устройств, а шина используется для общения компонентов на материнской плате. Порт это по сути шина с разъёмом по середине. Протокол — это порядок взаимодействия через контакты. В примере с PS/2 это был порядок передачи данных через контакты Clock и Data.
Раньше микроконтроллер Intel 8042 был очень распространённым и использовался как в клавиатуре, так и в качестве контроллера порта PS2, т.е. данными обменивались два одинаковых чипа. Драйвер порта PS2 в Windows называется 8042prt.sys.
На самом деле мы передаём не 8 бит, а 11, потому что данные передаются в виде пакета данных или же сообщения. Дополнительные 3 бита обозначают начало и конец данных — один нолик в начале и 0 1 в конце, такой протокол передачи данных от устройства хосту в PS2. Так может выглядеть функция SendKey в псевдокоде, если вам удобнее понимать код. Она отправляет данные о нажатой клавише через шину PS2.
void SendBit(BYTE bit) {
float power = (bit != 0) ? 3.3f : 0.0f;
SetPower(DATA, power);
SetPower(CLOCK, 3.3f);
Sleep(50);
SetPower(CLOCK, 0.0f);
Sleep(50);
}
void SendData(BYTE data) {
SendBit(0);
for (BYTE i = 0; i < sizeof(BYTE); i++) {
BYTE bit = (data >> i) & 1;
SendBit(bit);
}
SendBit(0);
SendBit(1);
}
Не всегда для передачи данных нужно вручную устанавливать напряжение на каждой ножке индивидуально. В некоторых случаях значение сохранённое в регистре автоматически отображается на контакты.
На графике такая передача данных будет визуализирована следующим образом. По оси X время, по Y — напряжение.
В аналоговой технике сигнал может искажаться, т.е. лежит провод ни к чему не подключённый, но вольтметр показывает на нём 0.5В из-за того что рядом есть электромагнитное поле. Поэтому используется понятие порогового напряжения. Если напряжение меньше порогового, то считаем что получили логический ноль, иначе единичка. С учётом возможных искажений скан-код нажатой клавиши Y может прийти вот таким:
Прежде чем мы подробно рассмотрим как данные от клавиатуры добираются до CPU давайте поговорим о микросхемах, шинах и материнских платах.
Микроконтроллеры и микросхемы
Микроконтроллер это цифровое устройство, он может выполнять вшитую в него программу, имеет некоторый объём RAM памяти и место для хранения данных и кода программы. Микросхема — это аналоговое устройство, внешне похожее на микроконтроллер и её нельзя перепрограммировать, потому что программа задаётся ещё на этапе проектирования. Вручную создавать электрическую схему реализующую алгоритм очень трудоёмко и поэтому для проектирования микросхем используется специальный язык программирования который называется VHDL (Hardware Description Language). Это высокоуровневый язык программирования который транслируется в план электрической схемы, она прогоняется через программу находящую оптимальное расположение радиоэлементов на плате и в конечном счёте производится в физическом виде. Изображения носят иллюстративный характер.
Каким же образом данные и команды представлены в микросхемах и микроконтроллерах? Основой вычислительной техники является транзистор, который человечество научилось делать в микроскопических размерах. В классическом виде транзистор это такой радиоэлемент у которого есть три ножки: вход, выход и между ними управляющая, своего рода затвор. Выглядят транзисторы как на картинке ниже:
На электрических схемах транзистор представлен кружком с тремя линиями и стрелкой. Рисунок ниже иллюстрирует работу транзистора, вода иллюстрирует ток. Basis, Kollektor, Emitter — названия ножек.
На коллектор подаётся напряжение, если вентиль открыт (база имеет напряжение), то ток проходит на эмиттер, иначе там будет 0В. Имея 8 транзисторов у каждого из которых на выходной ножке (Эмитер) подключен светодиод который либо горит либо нет, мы можем представить 256 уникальных комбинаций (2 в степени 8). Лапочки интерпретируются справа налево, так же как и десятичные числа. Младшие разряды находятся справа.
Т.е. одна лампочка представляет один бит информации (0 или 1), а восемь таких лампочек соответствуют одному байту. На транзисторах можно строить и логические операторы И, ИЛИ, НЕ, XOR.
К примеру в схеме оператора AND (слева на картинке выше) на выходе будет напряжение только если оба входных напряжения ненулевые. Есть уже кем-то придуманные алгоритмы сложения, умножения, деления, вычитания основанные на побитовых логических операциях и битовых сдвигах. Производителям микросхем надо их просто реализовать. Ниже проиллюстрирована работа алгоритма побитового сложения, разбирать мы его не будем:
Нанотранзисторы микроскопические и их можно размещать на плате миллионами. Ниже изображён процессор Intel и как примерно может выглядеть одно из его ядер. Картинка носит иллюстративный характер.
Микросхемы могут содержать на той же плате и в том же корпусе и микроконтроллер.
Шины
Обычно в учебниках шины показывают в виде жирных стрелок, как на картинке ниже. Это делается чтобы не рисовать все соединения контактов, которых может быть много. Работа шины PS2 очень простая, там всего нужно три контакта. Но есть шины у которых к примеру 124 контакта для передачи данных.
Различают шины следующих типов:
- Данных — биты на этих контактах интерпретируются как данные: число, символ, часть картинки или других бинарных данных. Ширина шины влияет на пропускную способность, количество переданных бит за секунду
- Адреса — биты на этих пинах интерпретируются как физический адрес в памяти. Ширина этой шины определяет максимальный поддерживаемый объём RAM.
- Управления — контакты которые используются для передачи управляющих сигналов на подключенные устройства. CPU использует их для отправки команд чтения записи памяти RAM и устройств I/O.
- Питания. Хоть она обычно и не упоминается в списке шин, но привести в пример её стоит. Вот как выглядит ATX 24 Pin 12V PSU Connector, она подаёт разные напряжения для разных подсистем.
Шина может состоять из подшин, т.е. одни контакты используются для данных, другие для адресов, третьи для управления и контакты по которым передаётся питание. Подход когда одни и те же контакты используются попеременно для передачи и данных и адресов называется мультиплексированием. К примеру процессор Intel 8086 имеет шину данных и адресов 20 бит, на диаграмме пинов её контакты обозначены AD0-AD19 (ножки 16-2 и 39-35).
В более сложном случае у нас могут быть несколько микросхем подключенных к тем же контактам. Для нормального общения им нужен дополнительный чип, который будет определять кто в какой момент времени может их использовать, он называется контроллер шины. На рисунке ниже сферическая шина в вакууме: четыре одинаковых микроконтроллера передают данные микроконтроллеру-потребителю через контроллер шины. Красный провод — напряжение, которое контроллер шины раздаёт всем подключенным к нему чипам. По зелёным проводам передаются данные и производится “договаривание” с контроллером шины и синий провод это Clock, по которому контроллер шины синхронизирует общение контроллеров, потому как они могут работать с разной скоростью. Если на синем проводе логическая единица, то чип имеющий право на пользование шиной может выполнить один акт взаимодействия со внешним миром — прочитать бит например.
Контроллер шины можно рассматривать как цельный компонент, потому общение со внешним миром будет проходить через него и сколько реально на шине чипов или реальных внешних устройств не важно. Для передачи данных в материнской плате имеется разветвленная сеть шин. Несмотря на то что чип-сетов великое множество, в большинстве своём они следуют типовой компоновке, потому что все устройства подключённые к материнской плате делятся на:
- Медленные — клавиатура, жёсткий диск, сетевая карта, аудио и пр.
- Быстрые — CPU, RAM, GPU.
Чипсет — это набор микросхем, которые все были созданы для работы друг с другом. Они обеспечивают коммуникацию компонентов на материнской плате и предоставляют функциональность, например таймеры. Чип-сет работает только с одной маркой процессоров, AMD нельзя вставить в материнку с чипсетом Intel, у них даже контакты разные. Схема материнской платы представлена ниже:
Современный порт PS2 напрямую подключается к чипу Super I/O своими контактами. Зелёное — клавиатура, фиолетовый — мышка. Раньше он подключался к микроконтроллеру Intel 8042.
Материнская плата выполнена из диэлектрика, т.е. материала который не проводит ток. Ток может проходить только про пропечатанным на плате магистралям. Материнская плата имеет множество слоёв, на каждом из которых пропечатаны свои контакты и поэтому если просверлить материнку там где магистралей не видно её можно испортить повредив невидимые контакты внутри платы. Теперь можно детально рассмотреть процесс распространения данных от PS2 к CPU.
Дорога от PS2 к процессору
Как правило архитектуру компьютера рассматривают на процессоре 8086. С одной стороны это правильно, потому как он достаточно простой по сравнению с современными CPU, с другой стороны неправильно, потому что он старый и не отражает архитектуру современной машины. Intel 8086 не нужны были никакие мосты, потому что он был настолько медленный что мог работать с периферией на одной шине, т.е. на одной частоте. Я плохо знаю современные CPU и чип сеты, поэтому буду объяснять на выдуманных, которые напоминают реальные. В моём примере будет вымышленный CPU сильно похожий на Intel 8086. У Super IO чипа больше ста котактов и по ним есть документация в Интернете, но я не вижу смысла разбирать какие пины используются клавиатурой и LPC-шиной для общения с South Bridge на самом деле. Главное это принцип, который может быть реализован по-разному.
Давайте быстренько посмотрим на картинку чтобы вспомнить что мы уже прошли. Зелёные стрелки показывают путь который мы рассмотрим.
Итак данные от клавиатуры уже пришли в контроллер порта PS2, который когда-то был чипом Intel 8042, а теперь эмулируется чипом Super IO. А теперь давайте разбирать дальнейший ход действий на моей выдуманной материнке с выдуманным CPU. Контроллер PS2 получил скан код нажатой клавишы Y и теперь подаёт напряжение на контакт сигнал (фиолетовый, см картинку ниже) на котором должен уведомить программируемый контроллер прерываний о данных с клавиатуры. Этот сигнал передаётся от одного чипа к другому пока Северный мост не передаст его чипу управляющему прерываниями.
Programmable Interrupt Controller представляет из себя чип Intel 8259 у которого 8 ножек (их имена IRQ0-IRQ7) зарезервированы для получения уведомлений от определённых портов (Interrupt ReQuest). На пин IRQ1 подвязана клавиатура, IRQ7 — принтер, на какой-то пин Floppy диск, звуковая карта, параллельные порты и другие. Конечно устройств может гораздо больше восьми, поэтому применялся такой приём как каскадирование, когда к ножке с именем IRQ2 подключался другой такой же PIC, у которого отсчёт начинался не с 0, а 7. Мышка привязана к IRQ12, т.е. ножка IRQ5 на втором PIC.
Сейчас контроллер прерываний должен уведомить CPU о событии на клавиатуре. Это происходит следующим образом:
- Контроллер прерываний подаёт на свою ножку INT (Interrupt) напряжение, которое идёт на ножку INTR (Interrupt Request) процессора. CPU может быть в данный момент занят и потому будет игнорировать этот сигнал, потому что уведомление от клавиатуры считается маскируемым прерыванием, т.е. его можно игнорировать до поры до времени. Немаскируемое прерывание требует немедленного внимания, к примеру ошибки внутри логики работы чипсета. Ошибку следует понимать так же как и exception внутри приложения.
- Процессор наконец обращает внимание на сигнал и несколько раз снижает и повышает сигнал на ножке INTR, давая понять что к приёму номера прерывания готов.
- Контроллер прерывания выводит на свою шину данных (ножки D0-D7) номер прерывания, который называется вектором прерывания. Во время загружки ОС настраивает PIC возвращать определённый номер когда происходит уведомление от определённого устройства (сигнал на ножку IRQ).
Почему вектор, а не номер? У меня есть два объяснения. Вектором называется одномерный массив и данные здесь передаются по сути как массив нулей и единиц. Второе объяснение — вектор в математике обозначает направление (x, y, z, w). В процессоре вектор прерывания меняет направление исполнения программы.
- CPU считывает со своих ножек AD0-AD7 этот номер, вектор прерывания. Операционная система сохранила в регистре IDT (Interrupt Descriptior Table) указатель на массив функций, которые называются таблица векторов прерываний, где вектор прерывания используется как индекс в этом массиве. По нему CPU вызывает обработчик прерывания.
На самом деле таблица векторов прерываний содержит больше данных чем просто указатель на функцию. В этом массиве хранятся данные такого типа. Для простоты будем думать, что в IDT хранятся указатели на функцию или в терминах C# делегаты.
struct IDT_entry{ unsigned short int offset_lowerbits; unsigned short int selector; unsigned char zero; unsigned char type_attr; unsigned short int offset_higherbits; }; struct IDT_entry IDT[256];
- Обработчик клавиатуры вызывает команду процессора которая завершает обработку прерывания, о чем сообщает контроллеру прерываний подав напряжение на ножку INTA (Interrupt Acknowledged).
Обработчик прерывания от клавиатуры в самом простом коде будет выглядеть так. Он вызывает команду которая сигнализирует о завершении обработки прерывания, т.е. подаёт сигнал на ножку INTA.
void irq1_handler(void) {
outb(0x20, 0x20); //EOI
}
Более подробно ознакомится с тем как настраивается таблица векторов прерываний можно на osdev.
Теперь мы знаем о том как произошло прерывание, но не знаем как обработчик прерываний считывает информацию о нажатой клавише. С программной точки зрения порт PS2 представляет собой два регистра, только обращение к ним происходит не по именам или адресам в памяти о по номеру порта ввода/вывода. Эти два однобайтовых регистра закреплены за портами 0x60 и 0x64, в первом (0x60) будет лежать скан-код клавиши. Второй порт используется для передачи статуса и комманд порту PS2 (не клавиатуре!). В наборе инструкций архитектуры x86 есть команда IN storeTo, fromPortNum, которая считывает значение из указанного I/O port в указанный регистр. Например IN AL, 0x60 сохранит данные с клавиатуры в регистр AL. Она может работать примерно так:
- В процессоре который мы рассматриваем есть ножки AD0-AD20, они могут использоваться для указания адреса так и данных. Это и шина данных и шина адреса. Помимо них есть ряд управляющих ножек, к примеру пин №28 (S2) значение которого укажет северному мосту откуда будет происходить чтение — из памяти или устройства ввода вывода. Команда IN выставляет сюда значение говорящее об I/O устройстве.
- На шину адреса CPU (какие-то из ножек AD0-AD20) выставляется последовательность 0110 0000, что и есть 0x60. Совместно с другими управляющими ножками CPU отправляет сообщение Северному мосту, оно направляется на Южный мост. Между чипами сообщение может передаваться по разным протоколам, где-то это последовательная шина, где-то параллельная. Южный мост приняв сообщение видит порт 0x60 и понимает что его надо направить в SuperIO чип, у того будет какое-то количество времени его обработать.
- Как мы уже говорили Super IO чип нужен для того чтобы скрыть в себе архаичный функционал старых устройств. По номеру 0x60 он понимает что это сообщение предназначено контроллеру PS2, который уже выставляет скан код на свою шину данных.
- Теперь скан код идёт обратно. От эмулятора Intel 8042, через внутреннюю схему чипа SuperIO, по шине LPC состоящей из трёх контактов в Южный мост, оттуда в Северный в виде расширенного сообщения. Т.е. к скан коду было добавлено чуть больше информации, к примеру откуда она.
- Северный мост уведомляет комбинацией сигналов CPU о прибытии данных с запрошенного порта и выставляет скан код на шину данных (какие-то 8 пинов из AD0-AD20).
- Микроархитектура CPU считывает эту последовательность высоких и низких сигналов с шины данных и и помещает её на транзисторы представляющие регистр AL. С этого момента программа может обрабатывать данные от клавиатуры.
Весь этот алгоритм работает в масштабе наносекунд и потому выполняется почти моментально, хоть процессор и провёл некоторое время в ожидании операции ввод-вывода.
Как вы теперь понимаете чтение с внешних устройств, даже таких как память RAM с т.зр. CPU достаточно медленное. Эту медлительность можно заметить написав программу которая печатает 10 000 строчек в файл построчно, вместо того чтобы скопить их в буфере и сохранить сразу. Жёсткий диск подключен к Южному мосту и внутри него так же есть контроллер управляющим непосредственным размещением данных.
Оперативная память подключается к CPU через шину и чтение с неё занимает некоторое время. Для ускорения работы CPU у него имеется кэш, т.е. область в которой расположены транзисторы представляющие данные которые скоро понадобятся или часто используются, их чтение происходит гораздо быстрее чем из платы RAM, которая общается с CPU через Северный мост. Оперативная память называется Dynamic Random Access Memory, потому как для представления данных в ней используются конденсаторы. Конденсатор это радиоэлемент который как аккумулятор держит некоторое время заряд пока полностью не разрядится. Только здесь разрядка происходит очень быстро. Поэтому конденсаторы надо перезаряжать, это происходит моментально, достаточно подать напряжение. Заряженный конденсатор — логическая 1, иначе 0. Для памяти кэша используется Static RAM, т.е. её не надо перезаряжать и поэтому она работает быстрее, но стоит дороже. Кэш делится на 3 уровня, которые последовательно проверяются в процессе поиска запрошенных данных, прежде чем процессор обратиться к RAM. На старых процессорах кэш первого уровня (L1) был частью CPU и работал с ним на одной частоте, когда как L2 и L3 кэши были внешними чипами. Сейчас они все находятся на одной микросхеме с процессором. Кэш L1 самый быстрый и самый маленький по объёму памяти, L2 имеет больше памяти но медленнее. L3 самый большой кэш и самый медленный, часто его называют shared cache, потому что он хранит данные для всех ядер CPU, в то время как L1 и L2 созданы для каждого отдельного ядра.
В следующей части поговорим как Windows принимает и обрабатывает полученные данные.