Упрощённая архитектура компьютера

Все современные вычислительные устройства от персональных компьютеров до микроконтролеров имеют похожее структурное устройство - центральный процессор, память и периферийные устройства, связанные через шину данных. Процессор, исполняя программу шаг за шагом может обращаться к другим устройствам с помощью шины данных. Можно говорить о двух типах операций работы с шиной - операции чтения и операции записи. Во втором случае процессор выдаёт на шину адрес (число) и, собственно, слово данных (слово данных может быть 8-битным, 16-битным, 32-битным, 64-битным и т. д. - различные процессоры реализуют различную разрядность шины данных, она иногда совпадает с разрядностью самого процессора, а иногда нет, важно понимать, что любой обмен данными с внешними устройствам происходит блоками этого размера). Все периферийные устройства получают запрос и сравнивают указанный адрес со своим диапазоном адресов (жёстко заданным производителем устройства или конфигурируемым программно). То устройство, которое считает, что адрес принадлежит ему, считывает переданное слово данных и распоряжается им по своему усмотрению. Чтение данных происходит чуть сложнее - процессор сначала передаёт адрес для чтения через шину данных, а затем ждёт небольшой промежуток времени в течении которого одно из устройств на шине точно также распознаёт, что адрес принадлежит ему, и выдаёт на шину данных какое-то слово данных ассоциированное с этим адресом (это может быть содержимое внутренней памяти устройства, фиксированное или генерированное значение), которое после паузы забирается процессором.

На физическом уровне шина может быть организована весьма разнообразным образом. Часто шина состоит из двух частей - шины адреса и шины данных, которые в добавок могут иметь разную разрядность (разрядность шины адреса ограничивает количество устройств, которые могут быть одновременно подключены к шине, разрядность шины данных задаёт размер блоков данных, которыми возможен обмен - чем она больше, тем выше скорость передачи информации при прочих равных). Также возможно существование нескольких шин. Например, оперативная память подключается по выделенной высокоскоростной шине, а остальные устройства по более медленной низкоскоростной. Ещё возможны мосты между различными шинами - например, одно из периферийных устройств на шине процессора на самом деле имеет ещё одну шину, к которой подключены какие-то другие устройства, соответственно устройство-мост ретранслирует все запросы процессора (но только те, которые входят в диапазон адресов этого устройства) в эту вторую шину, делая возможной работу с ним. В классической схеме ведущую роль на шине играет процессор (все коммуникации происходят исключительно по его инициативе), однако существуют системы, где процессоров несколько, либо же периферийные устройства могут взаимодействовать друг с другом без его участия (например, сетевая карта сохранять принятый пакет данных напрямую в оперативную память, а процессор лишь уведомлять о том, что приём пакета завершён), в этом случае физический уровень шины предусматривает какие-либо механизмы очередности обращения или разрешения конфликтных ситуаций.

Помимо корректной работы шины существует два некорректный сценария:

Что такое адрес устройства на шине? Адрес представляет собой порядковый номер байта (или машинного слова) в некоем “адресном пространстве”. Из того что было описано выше следует, что каждое машинное слово может принадлежать одному и тому же или разным периферийным устройствам, а также не принадлежать никому.

Особняком стоит такое устройство как оперативная память. Она обычно занимает весьма крупный и при этом непрерывный диапазон адресов (больше, чем все остальные периферийные устройства), при этом реализуя максимально прозрачную логику работы - она помнит всё, что и по каким адресам в неё было записано, а операции чтения возвращают один в один те же данные, которые были последними записаны по этому адресу.

Оперативная память компьютера теряет все записанные в неё данные при отключении питания, однако к шине данных помимо оперативной памяти может быть также подключена энергонезависимая память. Скорость чтения из неё может быть ниже, а операции записи могут игнорироваться, вызывать аппаратную ошибку или требовать перевода в специальный режим изменения содержимого.

Другие периферийные устройства обычно реализуют более непрозрачную логику работы - например, чтение одного и того же адреса может возвращать совсем не то, что туда было записано ранее (например, потому что записи используются для изменения режима работы устройства, а чтения для проверки его состояния) и даже не то, которое было оттуда прочитано в прошлый же раз. Нередки случаи, когда при запросе чтения устройство выдаёт машинное слово, которое буквально собрано из состояний различных его логических линий и каждый бит имеет особое значение, а при запросе записи записанные биты непосредственно приводят к выставлению логических уровней на различных проводниках внутри устройства. То есть как таковая память у устройства может отсутствовать, несмотря на то, что с точки зрения процессора устройство выглядит так же как оперативная память.

Исполнение программы процессором

Упрощённый алгоритм исполнения процессором программы

Откуда процессор получает инструкции, которые ему нужно исполнять? Из того или иного вида памяти (оперативной или постоянной). Любой процессор содержит внутри себя так называемый “счётчик команд”, который указывает на адрес команды, которая будет исполняться следующей. Процессор инициирует операцию чтения на шине, используя этой счётчик, и получает очередную команду, которая подвеграется декодированию и исполнению. В процессе исполнения команд могут быть выполнены дополнительные запросы к шине (как на чтение, так и на запись), зависящие от результата декодирования команды и состояния процессора. По окончании исполнения команды процессор переходит к следующей увеличивая свой счётчик команд на размер обработанной команды в байтах, однако некоторые команды могут приводить к иным изменениям счётчика команд. Такие команды называются командами перехода. Они бывают условные и безусловные.

Из описанного выше следует, что программа и её данные находятся в одном и том же адресном пространстве и доступ к ним осуществляется через одну и ту же шину данных. Это архитектура фон Неймана. Она используется большинством современных процессоров таких как x86, x86-64, ARM и других. Существует альтернативный подход - Гарвардская архитектура. Он подразумевает, что процессор имеет две независимых шины - для инструкций программы и для данных, которые она обрабатывает. Такой подход упрощает аппаратную реализацию процессора, однако затрудняет программирование. Он применяется в основном в некоторых микроконтроллерах. Например, микроконтроллерах архитектуры AVR.

Загрузка программы в память

Что происходит при включении компьютера (или микроконтроллера)? Его счётчик команд аппаратно инициализируется неким значением жёстко заданным изготовителем с помощью электрических схем. И процессор начинает исполнять команды начиная с этого адреса. Система собрана таким образом, чтобы на шине обязательно оказалось какое-либо запоминающее устройство, которое будет расположено по этим адресам.

Если по этим адресам находится оперативная память, то программа для исполнения должна быть каким-то образом туда загружена другим устройством отличного от процессора. Поэтому обычно по этим адресам находится какой-нибудь вид энергонезависимой памяти. Эта память может быть однократнопрограммируемой интегрированной в сам процессор (неоторые устройства на шине могут быть физически расположены внутри чипа процессора), либо внешней перезаписываемой (внутренняя память процессора тоже может быть перезаписываемая).

В простейшем случае эта память и содержит всю программу для исполнения, но если мы говорим про персональный компьютер, то это не так, поскольку недостаточно гибко. В таком случае программа записанная в эту память не выполняет основную функцию сама по себе, а обеспечивает считывание другой программы из другого вида памяти с более сложным протоколом взаимодействия (например, жёсткий диск) и модифицирует счётчик команд специальной инструкцией, что приводит к запуску новой программы. Загруженная программа может уже выполнять основную функцию устройства, либо также быть промежуточным звеном для загрузки в оперативную память и запуска ещё одной программы. Это позволяет менять функционал устройства меняя лишь конечную цель загрузчиков, которая может быть расположена на наиболее удобном для пользователя носителе. Все эти загрузчики могут быть названы словом bootloader. Также существуют такие термины как “первичный (начальный) загрузчик” и “вторичный загрузчик” (secondary loader), где первый выполняет загрузку и запуск второго, а второй не может быть запущен напрямую (потому что находится в труднодоступном для процессора виде памяти или ожидает предварительную настройку некоторых переферийных устройств). Однако это относительные термины, то есть первичным загрузчиком считается тот, который мы рассматриваем первым в контексте обсуждения, даже если на самом деле он тоже загружается кем-то.

В современном персональном компьютере цепочка загрузчиков может быть весьма длинной. Например, первичный загрузчик может быть расположен в специальной однократно программируемой памяти внутри процессора. Он выполняет инициализацию внешнего SPI чипа EEPROM, который проецируется в адресное пространство, и передаёт управление BIOS (Basic Input Output System). Тот в свою очередь инициалиризует большую часть остальных периферийных устройств (например, видеокарты). При этом некоторые устройства могут содержать дополнительный программный код, который загружается и исполняется BIOS в ходе инициализации (однако, управление от этого кода в конечном счёте возвращается BIOS). Дальше BIOS исходя из своих настроек пытается загрузить код с различных запоминающих устройств (жёсткие диски, дискеты и т. д.). Этот код может быть желаемой операционной системой сам по себе, но чаще выполняет ещё более комплексную загрузку других файлов, которые являются настоящей операционной системой. Или, например, загрузчик на жёстком диске на самом деле может быть “мультизагрузчиком” и предоставлять пользователю выбор из нескольких установленных систем. Также вторичные загрузчики обеспечивают поддержку нескольких загрузочных устройств, нескольких файловых систем и т. д. В случае если компьютер поддерживает UEFI ничего принципиально не меняется, просто UEFI выполняет более комплексную инициализацию устройств, чем BIOS, и способен без дополнительных загрузчиков считывать вторичный загрузчик операционной системы с файловой системы, а не из начального сектора диска.

Начальный загрузчик может быть полностью перезаписан в оперативной памяти (если он был загружен туда) вторичным загрузчиком, а может остаться нетронутым и предоставлять ему какие-то функции (так работает BIOS и UEFI, которые предоставляют ОС ряд функций работы с аппаратурой, которыми она может пользоваться, а может не пользоваться).

С микроконтроллерами дела обстоят проще. Во-первых, загрузчика может не быть вовсе - основная программа непосредственно запускается из энергонезависимой памяти, куда помещается с помощью программатора. Если же загрузчик имеется, как правило он просто проверяет наличие специального признака (например, замкнутой перемычки или ждёт секунду прихода символа по последовательному интерфейсу). При наличии этого признака, вместо запуска основной программы он переходит в “режим программирования”, который позволяет с использованием каких-либо удобных пользователю интерфейсов (UART, USB и т. д.) осуществить перезапись памяти программ (обычно загрузчик не даёт при этом перезаписать самого себя, но возможны исключения) без программатора. Если признак не обнаружен, то управление передаётся на основной участок кода. Так работает, например, загрузчик Arduino.

Некоторые микроконтроллеры (например, STM32) могут иметь два загрузчика - один из них хранится в однократнопрограммируемой памяти и записан на заводе. Он реализует один или несколько простых интерфейсов программирования. Затем он передаёт управление на основной код. Однако присутствует техническая возможность в части области основного кода поместить вторичный загрузчик, реализующий более комплексный функционал программирования, и уже он будет передавать управление главной программе. Например, микроконтроллер STM32F103 с завода поддерживает только загрузчик для программирования через UART, однако на него можно записать вторичный загрузчик, обеспечивающий обновление прошивки с помощью USB подключения.

Разумеется, надо понимать, что все загрузчики микроконтроллеров кроме заводских занимают часть памяти программ уменьшая доступное место для пользовательского кода. Также основа технической возможности создания загрузчика лежит в наличии функций для программной перезаписи памяти программ без программатора. При её отсутствии написание загрузчика может быть невозможно (хотя в архитектуре фон Неймана при наличии достаточно большого ОЗУ можно загрузить программу с внешнего носителя туда), а ограничения наложенные на эти функции накладывает ограничения на загрузчик (например, на AVR загрузчик может занимать лишь фиксированную область памяти, заданную fuse-битами, поскольку операции записи и стирания работают пакетно над всей памятью за пределами этой).

Подытожив можно сказать, что на персональных компьютерах загрузчики в основном осуществляют возможность запускать операционную систему с различных периферийных устройств хранения данных, а на микроконтроллерах обычно программа хранится в одном и том же месте, но загрузчики обеспечивают более удобные способы её перезаписи пользователем без применения программатора.